PET sinogram symmetries parallelproj.sinogram_symmetries¶
Utilities for exploiting the geometric symmetries of regular-polygon sinograms to save memory and computation (for example when precomputing sensitivity images). The typical workflow is to compute the sinogram-plane symmetry classes, build the per-axis class indices, reduce a sinogram to its unique classes, operate on the reduced data, and finally expand back to the full sinogram. These are advanced helpers; most users do not need them directly.
Sinogram symmetry utilities for cylindrically-symmetric regular-polygon PET scanners.
Provides tools to partition sinogram bins into geometric equivalence classes and to reduce a full sinogram to one representative value per class or expand it back – the basis for efficient geometric sensitivity calculation.
- parallelproj.sinogram_symmetries.axial_block_shifted_planes(r1: int, r2: int, block_size: int, num_rings: int, n_edge: int = 0) list[tuple[int, int]][source]¶
All planes obtained by shifting both ring indices by a multiple of block_size.
When
n_edge > 0only shifts are returned in which both endpoints remain in the same interior / edge category as the originals – preventing edge-of-scanner rings from being treated as equivalent to interior rings.- Parameters:
r1 (int) – Start ring index of the seed plane.
r2 (int) – End ring index of the seed plane.
block_size (int) – Number of axial crystals per detector block.
num_rings (int) – Total number of detector rings (
block_size * num_blocks).n_edge (int, optional) – Number of edge rings at each scanner end.
0disables edge correction (default).
- Returns:
All valid block-shifted copies of
(r1, r2). Whenn_edge > 0only copies preserving the interior / edge category are included.- Return type:
list of tuple[int, int]
Examples
- parallelproj.sinogram_symmetries.axially_mirrored_plane(r1: int, r2: int, num_rings: int) tuple[int, int][source]¶
Return the plane obtained by reflecting the scanner about its axial midplane.
Reflection maps ring r -> N-1-r, so plane (r1, r2) becomes (N-1-r2, N-1-r1). The endpoint order is reversed so that the mirrored plane has the same sign of ring difference as the original.
- Parameters:
r1 (int) – Start ring index.
r2 (int) – End ring index.
num_rings (int) – Total number of detector rings.
- Returns:
The axially mirrored plane
(N-1-r2, N-1-r1).- Return type:
tuple[int, int]
Examples
- parallelproj.sinogram_symmetries.build_bin_to_class(class_indices: list[ndarray], num_bins: int) ndarray[source]¶
Build an inverse map from bin index to equivalence-class index.
For each bin
iin[0, num_bins),bin_to_class[i]is the index of the equivalence class that containsi.This is used internally by
expand_sinogram_by_symmetry_class()to build the index array needed to broadcast class values back to all bins.- Parameters:
class_indices (list of np.ndarray) – Per-class bin index arrays as returned by
build_plane_class_indices(),build_view_class_indices(), orbuild_radial_class_indices().num_bins (int) – Total number of bins along the axis being described (e.g.
RegularPolygonPETLORDescriptor.num_planes,.num_views, or.num_rad).
- Returns:
Inverse map: element
iis the class index that contains bini.- Return type:
np.ndarray, shape (num_bins,), dtype int64
Examples
- parallelproj.sinogram_symmetries.build_plane_class_indices(plane_for_ring_pair_table: ndarray, class_to_planes: dict, num_classes: int) list[ndarray][source]¶
Build per-class sinogram plane index arrays from a Michelogram lookup table.
Requires a span-1 LOR descriptor: each ring pair
(r1, r2)must map to a unique sinogram plane. If any valid plane index appears more than once in the table aValueErroris raised, because the symmetry reduction is only well-defined for span-1 sinograms.Returns a list of
num_classesnumpy integer arrays. Elementccontains the sinogram plane indices for equivalence classc. Ring pairs whoseplane_for_ring_pair_tableentry is-1(outsidemax_ring_diff) are silently omitted.- Parameters:
plane_for_ring_pair_table (np.ndarray, shape (R, R)) –
Michelogram.plane_for_ring_pair_table– entry[r1, r2]is the sinogram plane index for ring pair(r1, r2), or-1if absent. Only span-1 descriptors (one plane per ring pair) are supported.class_to_planes (dict[int, list[(int, int)]]) – Reverse lookup from
compute_sinogram_plane_symmetries().num_classes (int) – Third return value of
compute_sinogram_plane_symmetries().
- Returns:
Each element is a 1-D int64 array of sinogram plane indices.
- Return type:
list of np.ndarray, length num_classes
- Raises:
ValueError – If any sinogram plane index appears more than once in plane_for_ring_pair_table, indicating a span > 1 descriptor.
Examples
- parallelproj.sinogram_symmetries.build_radial_class_indices(num_rad: int) list[ndarray][source]¶
Per-class radial-bin index arrays under the FOV mirror symmetry.
Radial bins
randnum_rad - 1 - rsubtend the same perpendicular distance from the FOV centre and are therefore equivalent for a centred, cylindrically-symmetric object.For regular-polygon scanners
num_radis always odd (num_rad = N - 1 - 2 * radial_trimwith N even), so the centre bin(num_rad - 1) // 2maps to itself and forms a singleton class.- Parameters:
num_rad (int) – Number of radial bins (
RegularPolygonPETLORDescriptor.num_rad). Must be odd.- Returns:
Element
ccontains the one or two radial-bin indices in classc. Classes are ordered from the outermost pair inward; the last class is the centre singleton whennum_radis odd.- Return type:
list of np.ndarray, length
(num_rad + 1) // 2- Raises:
ValueError – If num_rad is even.
Examples
- parallelproj.sinogram_symmetries.build_view_class_indices(num_views: int, view_period: int) list[ndarray][source]¶
Per-class view index arrays under the scanner’s rotational symmetry.
A regular-polygon scanner with
num_sidessides maps viewvto viewv + n(wheren = num_lor_endpoints_per_side = view_period), because one scanner rotation step equals exactlynview steps. Viewsv, v + n, v + 2n, ...therefore form one equivalence class.- Parameters:
num_views (int) – Total number of views (
RegularPolygonPETLORDescriptor.num_views).view_period (int) – Number of views per scanner rotation period, equal to
RegularPolygonPETScannerGeometry.num_lor_endpoints_per_side.
- Returns:
Element
cis a 1-D int64 array of the view indices in classc. There areview_perioddistinct classes, each containingnum_views // view_periodviews.- Return type:
list of np.ndarray, length view_period
Examples
- parallelproj.sinogram_symmetries.compute_sinogram_plane_symmetries(block_size: int, num_blocks: int, max_ring_diff: int | None = None, n_edge: int = 0) tuple[dict, dict, int][source]¶
Partition all span-1 sinogram planes into axial equivalence classes.
Iterates over all valid ring pairs
(r1, r2)with|r1 - r2| <= max_ring_diff, groups them by orbit under the three axial symmetries, and assigns a unique integer class index to each group.- Parameters:
block_size (int) – Number of axial crystals per detector block.
num_blocks (int) – Number of axial detector blocks (total rings = block_size x num_blocks).
max_ring_diff (int or None, optional) – Maximum
|r1 - r2|included in the sinogram.Noneincludes all planes (default).n_edge (int, optional) – Number of edge rings at each scanner end with different sensitivity due to missing neighbouring blocks.
0disables edge correction (default).
- Returns:
plane_to_class (dict[(int, int), int]) – Maps every valid sinogram plane to its equivalence-class index.
class_to_planes (dict[int, list[(int, int)]]) – Reverse lookup: class index -> sorted list of member planes.
num_classes (int) – Total number of distinct equivalence classes.
- Return type:
tuple[dict, dict, int]
Examples
- parallelproj.sinogram_symmetries.expand_sinogram_by_symmetry_class(reduced, class_indices: list[ndarray], num_original_bins: int, axis: int)[source]¶
Expand a reduced sinogram back to the full bin count along one axis.
This is the inverse of
reduce_sinogram_by_symmetry_class(). Each bin in the full sinogram is assigned the value of the equivalence class it belongs to, usingbuild_bin_to_class()to build the mapping andxp.taketo broadcast.The function is array-API compliant and works with any backend that implements
xp.takeandxp.asarray(numpy, PyTorch, CuPy, …).- Parameters:
reduced (array) – Compact array as returned by
reduce_sinogram_by_symmetry_class(). Its size along axis must equallen(class_indices).class_indices (list of 1-D np.ndarray) – Per-class bin index arrays (same list that was passed to
reduce_sinogram_by_symmetry_class()).num_original_bins (int) – Number of bins in the original (unreduced) sinogram along axis (e.g.
RegularPolygonPETLORDescriptor.num_planes,.num_views, or.num_rad).axis (int) – The sinogram axis to expand.
- Returns:
expanded – Shape identical to reduced except
reduced.shape[axis]is replaced by num_original_bins.- Return type:
array, same backend as reduced
Examples
- parallelproj.sinogram_symmetries.is_interior_ring(ring: int, num_rings: int, n_edge: int) bool[source]¶
Return True iff ring is not affected by edge effects.
Rings 0 … n_edge-1 and N-n_edge … N-1 are edge rings – they sit at the outer face of the first / last detector block and have no neighbouring block on one side. All others are interior rings.
n_edge=0treats every ring as interior (no edge correction).- Parameters:
ring (int) – Zero-based ring index.
num_rings (int) – Total number of detector rings (
block_size * num_blocks).n_edge (int) – Number of edge rings at each scanner end.
0disables the distinction so every ring is treated as interior.
- Returns:
Trueif ring is an interior ring.- Return type:
bool
Examples
- parallelproj.sinogram_symmetries.plane_orbit(r1: int, r2: int, block_size: int, num_rings: int, n_edge: int = 0) list[tuple[int, int]][source]¶
Return all sinogram planes equivalent to (r1, r2) under the three axial symmetries.
The orbit is generated by applying – and composing – the axial block shift, the scanner midplane reflection, and the endpoint swap. Four seeds are constructed from
(r1, r2)by applying each combination of the reflection and swap symmetries; the full set of block shifts is then taken for each seed and merged.- Parameters:
r1 (int) – Start ring index.
r2 (int) – End ring index.
block_size (int) – Number of axial crystals per detector block.
num_rings (int) – Total number of detector rings (
block_size * num_blocks).n_edge (int, optional) – Number of edge rings at each scanner end.
0disables edge correction (default).
- Returns:
Sorted list of all planes in the orbit.
- Return type:
list of tuple[int, int]
Examples
- parallelproj.sinogram_symmetries.reduce_sinogram_by_symmetry_class(sinogram, class_indices: list, axis: int, reduction=None)[source]¶
Contract one sinogram axis by symmetry-class aggregation.
For each equivalence class the bins belonging to that class are gathered with
xp.takeand reduced along axis, so that axis shrinks from its original size tolen(class_indices). The same function handles view, radial, and plane reductions – just pass the appropriate index list and axis number.The function is array-API compliant and works with any backend that implements
xp.take,xp.sum, andxp.stack(numpy, PyTorch, CuPy, …).Notes
Typical workflow for geometric sensitivity:
Reduce – call this function on each sinogram axis (planes, views, radial bins) to obtain a compact sinogram with one entry per equivalence class.
Compute – run the forward projection or sensitivity calculation on the compact sinogram.
Expand – call
expand_sinogram_by_symmetry_class()to broadcast the computed values back to the full sinogram shape.
- Parameters:
sinogram (array) – Any array-API-compatible array.
class_indices (list of 1-D integer arrays) – One array per equivalence class containing the bin indices along axis that belong to that class. Outputs of
build_plane_class_indices(),build_view_class_indices(), andbuild_radial_class_indices()all conform to this contract.axis (int) – The sinogram axis to reduce. Use
RegularPolygonPETLORDescriptor.plane_axis_num,.view_axis_num, or.radial_axis_numas appropriate.reduction (callable or None, optional) – Reduction function with signature
f(x, axis=k) -> array. Defaults toxp.sum. Passxp.meanto normalise by the number of bins per class.
- Returns:
reduced – Shape identical to sinogram except
sinogram.shape[axis]is replaced bylen(class_indices).- Return type:
array, same backend as sinogram
Examples
- parallelproj.sinogram_symmetries.swapped_plane(r1: int, r2: int) tuple[int, int][source]¶
Return (r2, r1) – the same LOR traversed in the opposite axial direction.
- Parameters:
r1 (int) – Start ring index.
r2 (int) – End ring index.
- Returns:
The swapped plane
(r2, r1).- Return type:
tuple[int, int]
Examples