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 > 0 only 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. 0 disables edge correction (default).

Returns:

All valid block-shifted copies of (r1, r2). When n_edge > 0 only 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 i in [0, num_bins), bin_to_class[i] is the index of the equivalence class that contains i.

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:
Returns:

Inverse map: element i is the class index that contains bin i.

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 a ValueError is raised, because the symmetry reduction is only well-defined for span-1 sinograms.

Returns a list of num_classes numpy integer arrays. Element c contains the sinogram plane indices for equivalence class c. Ring pairs whose plane_for_ring_pair_table entry is -1 (outside max_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 -1 if 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

Sinogram symmetries

Sinogram symmetries
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 r and num_rad - 1 - r subtend the same perpendicular distance from the FOV centre and are therefore equivalent for a centred, cylindrically-symmetric object.

For regular-polygon scanners num_rad is always odd (num_rad = N - 1 - 2 * radial_trim with N even), so the centre bin (num_rad - 1) // 2 maps to itself and forms a singleton class.

Parameters:

num_rad (int) – Number of radial bins (RegularPolygonPETLORDescriptor.num_rad). Must be odd.

Returns:

Element c contains the one or two radial-bin indices in class c. Classes are ordered from the outermost pair inward; the last class is the centre singleton when num_rad is odd.

Return type:

list of np.ndarray, length (num_rad + 1) // 2

Raises:

ValueError – If num_rad is even.

Examples

Sinogram symmetries

Sinogram symmetries
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_sides sides maps view v to view v + n (where n = num_lor_endpoints_per_side = view_period), because one scanner rotation step equals exactly n view steps. Views v, 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 c is a 1-D int64 array of the view indices in class c. There are view_period distinct classes, each containing num_views // view_period views.

Return type:

list of np.ndarray, length view_period

Examples

Sinogram symmetries

Sinogram symmetries
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. None includes all planes (default).

  • n_edge (int, optional) – Number of edge rings at each scanner end with different sensitivity due to missing neighbouring blocks. 0 disables 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

Sinogram symmetries

Sinogram symmetries
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, using build_bin_to_class() to build the mapping and xp.take to broadcast.

The function is array-API compliant and works with any backend that implements xp.take and xp.asarray (numpy, PyTorch, CuPy, …).

Parameters:
  • reduced (array) – Compact array as returned by reduce_sinogram_by_symmetry_class(). Its size along axis must equal len(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

Sinogram symmetries

Sinogram symmetries
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=0 treats 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. 0 disables the distinction so every ring is treated as interior.

Returns:

True if ring is an interior ring.

Return type:

bool

Examples

Sinogram symmetries

Sinogram symmetries
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. 0 disables edge correction (default).

Returns:

Sorted list of all planes in the orbit.

Return type:

list of tuple[int, int]

Examples

Sinogram symmetries

Sinogram symmetries
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.take and reduced along axis, so that axis shrinks from its original size to len(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, and xp.stack (numpy, PyTorch, CuPy, …).

Notes

Typical workflow for geometric sensitivity:

  1. Reduce – call this function on each sinogram axis (planes, views, radial bins) to obtain a compact sinogram with one entry per equivalence class.

  2. Compute – run the forward projection or sensitivity calculation on the compact sinogram.

  3. 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(), and build_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_num as appropriate.

  • reduction (callable or None, optional) – Reduction function with signature f(x, axis=k) -> array. Defaults to xp.sum. Pass xp.mean to normalise by the number of bins per class.

Returns:

reduced – Shape identical to sinogram except sinogram.shape[axis] is replaced by len(class_indices).

Return type:

array, same backend as sinogram

Examples

Sinogram symmetries

Sinogram symmetries
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