Changelog

2.x

2.0.0 (Jul 17, 2026)

New Features

Major new capabilities
  • ``parallelproj.functions`` submodule: new module providing abstract base classes (C1Function, C2Function, FunctionWithProx, FunctionWithConjProx) and concrete loss/regularisation implementations for optimisation:

    • NegPoissonLogL — Poisson log-likelihood with two evaluation modes. The default (“safe epsilon”) mode evaluates a shifted-Poisson surrogate with eps = rel_eps * mean(y) added to data and expectation: finite for any non-negative expectation, per-bin minimiser unchanged, gradient bias proportional to the residual; the dual prox is shifted consistently. exact=True evaluates the unmodified log-likelihood (bins with y == 0, e.g. virtual bins, are handled exactly) and requires a positive expectation in all bins with counts. An optional absolute eps allows sharing one epsilon across subset objectives, and enable_extra_checks=True warns on inputs producing nan / inf

    • NegPoissonLogLListmode — listmode Poisson log-likelihood with built-in forward model; optional eps kwarg (default 0.0) smooths the per-event log / division terms (expectation-only shift — the symmetric shifted-Poisson surrogate of NegPoissonLogL would require full-sinogram projections and is not listmode-compatible)

    • LogCosh — edge-preserving log-cosh prior (smooth, with bounded curvature); used by the penalised reconstruction examples (MAPTR, MLAA)

    • HalfSquaredL2Deviation — weighted least-squares deviation

    • SumC1Function / SumC2Function — also created via f1 + f2 operator overloading

    • C1AffineObjective / C2AffineObjective — compose a loss with an affine forward model

    • NonNegativeIndicator — non-negativity constraint with proximal operator

    • MixedL21Norm — mixed L2,1 norm for group sparsity / TV-type regularisation

  • ``Michelogram`` class (parallelproj.pet_lors): encapsulates the full axial plane layout for cylindrical PET scanners under odd-span compression, including ring-pair-to-plane tables and visualisation methods.

  • GE-style axial sinogram plane layout for Michelogram: new MichelogramLayout enum and layout= argument, plus a Michelogram.ge(num_rings, max_ring_difference) convenience constructor. The GE-style layout uses segment 0 = ring differences {−1, 0, +1} (cross planes merged into virtual direct planes) and oblique segments = ring- difference pairs {±2k, ±(2k+1)}, ordered 0, +1, −1, +2, −2, … (the segment / ring-difference plane ordering used by GE-style sinograms; also known as “span 2” in STIR). span is ignored for this layout and Michelogram.span returns None. Combine it with a matching RegularPolygonPETLORDescriptor for the GE scanner of interest.

  • Selectable segment ordering for Michelogram: new SegmentOrder enum and segment_order= argument (also on Michelogram.ge and, as target_segment_order=, on SinogramAxialCompressionOperator). SegmentOrder.POSITIVE_FIRST (default) keeps the existing 0, +1, -1, +2, -2, layout; SegmentOrder.NEGATIVE_FIRST gives 0, -1, +1, -2, +2, (negative segment before the positive one of each ±k pair). This is a pure permutation of the sinogram planes – segment numbering, plane counts and multiplicities are unchanged – and works for both the STANDARD and GE layouts.

  • ``SinogramAxialCompressionOperator`` (parallelproj.pet_lors): LinearOperator that axially compresses a span-1 sinogram to a higher odd span (mode="sum" or mode="average"). A target_layout=MichelogramLayout.GE option compresses a span-1 sinogram to the GE layout; its mode="average" adjoint distributes a GE sinogram back onto the span-1 grid while preserving counts (e.g. to convert GE data to span-1 before detector mashing).

  • Segment selection: new Michelogram.select_segments([...]) returns a restricted Michelogram keeping only the requested signed segments (the order-preserving subsequence, with plane indices renumbered but segment labels unchanged), exposing parent_plane_indices for gather/scatter against the full sinogram. The companion SinogramSegmentSelectionOperator (parallelproj.pet_lors) is a LinearOperator built from a full LOR descriptor and a list of segments: it provides a restricted_lor_descriptor (for building the matching projector), gathers the selected planes out of a full sinogram (forward) and scatters them back into a zero-filled full sinogram (adjoint). It is a pure plane selection, so it supports non-TOF and TOF sinograms and has operator 2-norm 1.

  • ``SinogramMashingOperator`` (parallelproj.pet_lors): detector mashing for a span-1 regular-polygon sinogram. Groups transaxial_factor within-side crystals and axial_factor rings into larger virtual detectors at the averaged endpoint position, mapping the fine sinogram to a much smaller mashed one (mode="sum" for counts, mode="average" for multiplicative factors) with a genuine transpose and closed-form norm. The mashed geometry is exposed as coarse_scanner / coarse_lor_descriptor (a regular-polygon descriptor), so a standard RegularPolygonPETProjector projects directly along the mashed LORs. By default the coarse radial trim is derived automatically from the fine->coarse mapping so that no fine LOR is lost to trimming and no empty peripheral coarse radial bins remain (only the geometrically unavoidable degenerate self-pairs are dropped); pass coarse_radial_trim to override. The operator is span-1 only; GE-layout sinograms are mashed by composition – convert GE -> span-1 with the mode="average" adjoint of a span-1 <-> GE SinogramAxialCompressionOperator, then mash, giving a pure span-1 coarse sinogram (see the 01_pet_geometry/07_run_detector_mashing.py example).

  • ``TOFBinMashingOperator`` (parallelproj.pet_lors): mashes (groups) every mashing_factor neighbouring TOF bins along the trailing TOF axis into fewer, wider bins (mode="sum" for counts, mode="average" for multiplicative factors), with a genuine transpose and closed-form norm (sqrt(G) / 1/sqrt(G)). Geometry-agnostic (takes tof_parameters and the leading non_tof_data_shape), it exposes the matching coarse_tof_parameters so a projector can target the mashed TOF grid, and composes with SinogramMashingOperator via CompositeLinearOperator. For mode="sum" the mashed forward projection equals a direct coarse-TOF projection (erf additivity over adjacent bins). See the 01_pet_geometry/08_run_tof_bin_mashing.py example.

  • ``parallelproj.sinogram_symmetries`` submodule: new module for exploiting the cylindrical symmetry of regular-polygon PET scanners to speed up geometric sensitivity calculations. Provides:

    • compute_sinogram_plane_symmetries — partition all axial ring pairs into equivalence classes under axial block-shift, midplane reflection, and endpoint-swap symmetries (with optional edge-ring correction)

    • build_plane_class_indices, build_view_class_indices, build_radial_class_indices — per-class index arrays for the three sinogram axes

    • reduce_sinogram_by_symmetry_class / expand_sinogram_by_symmetry_class — array-API-compatible reduce/expand operations for the typical reduce -> compute -> expand sensitivity workflow

  • ``parallelproj.data`` submodule: new module for memory-mapped, ordered-subset access to sinogram data, enabling out-of-core OSEM on datasets larger than RAM. Provides SubsetArrayMmap (a lazily-loaded per-subset view of an on-disk array) and to_subset_mmap (write a sinogram to disk as subset-ordered memory maps). count_event_multiplicity now also lives here (see the breaking change below — it is no longer exported at the top level).

  • ``parallelproj.unlist`` submodule: new module for histogramming listmode PET data into sinograms for RegularPolygonPETScannerGeometry-based scanners. Provides:

    • regular_polygon_events_to_sinogram — histogram per-event crystal and ring indices into a non-TOF or TOF sinogram array; supports numpy, cupy, and torch

    • detection_times_to_tof_bin — convert raw detection-time differences (nanoseconds) to projector-convention unsigned TOF bin indices ready for histogramming

Smaller additions and improvements
  • New sinogram / scanner ordering options: SinogramZigZagOrder (a zig_zag_order argument on RegularPolygonPETLORDescriptor) and RingEndpointOrdering (with new phis, phi0, ring_endpoint_ordering and lor_endpoint_positions arguments on RegularPolygonPETScannerGeometry) make the crystal / LOR endpoint ordering explicit and configurable.

  • Selectable LOR start/end (TOF-bin) convention: new LOREndpointOrder enum and lor_endpoint_order argument on RegularPolygonPETLORDescriptor. START_END (default) keeps the current behaviour; END_START swaps xstart/xend for every LOR. Non-TOF projections are unchanged; for TOF this reverses the TOF-bin axis, letting users match a given vendor’s start/end (and hence first-TOF-bin) convention. See the “PET TOF sinogram projector” example.

  • Ready-made demo scanners: get_lor_descriptor_G1 / get_lor_descriptor_G2 (in parallelproj.pet_lors) return fully configured RegularPolygonPETLORDescriptor objects for two built-in cylindrical TOF scanner geometries, and the matching get_tof_parameters_G1 / get_tof_parameters_G2 (in parallelproj.tof) return their TOFParameters. Users can start projecting without assembling the scanner, Michelogram, sinogram conventions and TOF model themselves. All geometry / convention / timing parameters are exposed as keyword overrides (e.g. num_units, radial_trim, max_ring_difference, num_tofbins, sigma_tof).

  • ``ParallelViewProjector3D`` now supports any odd span: the projector accepts a Michelogram and uses the averaged-LOR z-position per plane (exact for span=1, standard approximation for span>1), with no loop over ring-pair multiplicities.

  • ``LinearOperator.H`` property and ``AdjointLinearOperator`` class: obtain the adjoint of any operator via A.H.

  • ``LinearOperator.adjointness_test`` and ``LinearOperator.norm`` infer ``xp`` / ``dev``: when omitted, the array namespace and device are taken from the operator (self.xp / self.dev) so both can be called without arguments; backend-agnostic operators (e.g. FiniteForwardDifference, CompositeLinearOperator) still require xp explicitly.

  • ``EqualBlockPETProjector`` ``num_chunks`` parameter: split block-pair projections into chunks to reduce peak GPU memory usage.

  • ``RegularPolygonPETProjector.convert_sinogram_to_listmode`` gained a shuffle parameter to randomly permute the returned event list.

  • ``VstackOperator`` now raises ValueError on inconsistent in_shape across stacked operators (previously silent).

  • ``TOFParameters`` validates its arguments: num_tofbins must be a positive integer and tofbin_width / sigma_tof / num_sigmas strictly positive and finite (and tofcenter_offset finite), raising a clear ValueError otherwise (a common cause is passing timing quantities in seconds/ps instead of the expected spatial mm).

  • Clearer fail-fast errors: SumC1Function / SumC2Function raise ValueError on an empty function sequence, and the start_plane_index / end_plane_index accessors raise ValueError (instead of AttributeError) for descriptors with more than one ring pair per plane (span>1 or GE), where a single ring pair per plane is undefined.

  • ``parallelproj.__version__`` is now exposed at the top level.

  • Citation metadata: import parallelproj is silent, but the reference to cite is available on demand as parallelproj.__citation__ (plain text) and parallelproj.__bibtex__ (BibTeX); a CITATION.cff file is also provided (GitHub “Cite this repository”).

New examples and documentation
  • Example gallery substantially reorganised and expanded, now grouped into PET scanner / sinogram geometry, projectors, iterative algorithms, listmode algorithms, transmission / joint estimation, and PyTorch integration. Highlights below.

  • New example: Michelograms and axial sinogram compression — how the Michelogram maps ring pairs to sinogram planes/segments, using SinogramAxialCompressionOperator to compress a span-1 sinogram to a higher odd span (and to/from the GE layout), a SegmentOrder comparison (positive-first vs negative-first) for both the STANDARD and GE layouts, and a SinogramSegmentSelectionOperator section that projects/back-projects only a chosen subset of segments.

  • New example: zig-zag LOR sampling in a sinogram view — visualises the SinogramZigZagOrder crystal/LOR endpoint pairing within a view.

  • New example: sinogram symmetries — partitioning ring pairs into symmetry classes and the reduce -> compute -> expand workflow for geometric sensitivity (parallelproj.sinogram_symmetries).

  • New example: detector mashingSinogramMashingOperator groups within-side crystals and rings into larger virtual detectors (exact vs fast coarse projector, multiplicity, count-preserving up/downsampling), including mashing GE sinograms by composition.

  • New example: TOF-bin mashingTOFBinMashingOperator groups neighbouring TOF bins, the matching coarse_tof_parameters, and composition with detector mashing.

  • New example: histogramming listmode data into sinograms — using parallelproj.unlist to bin per-event crystal/ring (and TOF) indices into a sinogram.

  • New example: transmission reconstruction (MLTR / SPS / L-BFGS-B) — exact Poisson transmission model with strictly positive scatter background, presenting MLTR (Nuyts et al.) and monotone SPS with optimal curvature (Erdoğan & Fessler) as one preconditioned gradient ascent differing only in the diagonal preconditioner, and L-BFGS-B on the same smooth objective with a non-negativity box constraint.

  • New example: accelerating MLTR with ordered subsets and SVRG — OS-MLTR and a preconditioned SVRG variant compared against full MLTR and a converged L-BFGS-B reference, showing the per-epoch speed-up of subset-based transmission reconstruction.

  • New example: penalised transmission reconstruction (MAPTR) — MLTR / OS-MLTR / SVRG on the penalised objective with an edge-preserving log-cosh prior, using the transmission “harmonic-mean” preconditioner (inverse of data plus prior curvature).

  • New example: joint activity/attenuation reconstruction (MLAA) for TOF PET — interleaved penalised OS-MLEM (activity) and OS-MLTR (attenuation, with the activity forward projection as the transmission blank scan), NAC warm-start, support-constrained attenuation update, and a known-water region to fix the TOF scale ambiguity.

  • Example helpers now ship inside the package as the private parallelproj._examples_utils module (interactive slice viewer show_vol_cuts, suggest_array_backend_and_device, analytic phantoms and demo priors). Examples and downloaded scripts/notebooks now import them straight from parallelproj — no PYTHONPATH setup and no separate example_utils.py download are required. The module is private and examples-only (not part of the public API) and may change without notice.

Breaking Changes

  • Python ≥ 3.12 required (dropped support for 3.9, 3.10, 3.11)

  • New required dependency: parallelproj-core >= 2.0.5 — the compiled C/CUDA projection kernels have been extracted into a separate conda-forge package (parallelproj-core). The old shared-library loading via ctypes is gone.

  • Low-level projection functions moved to parallelproj_core: joseph3d_fwd, joseph3d_back, and all TOF variants (e.g. joseph3d_fwd_tof_sino) are no longer in the parallelproj namespace. Import them from parallelproj_core instead. Note that the TOF function names were also reordered (e.g. joseph3d_fwd_tof_sinojoseph3d_tof_sino_fwd).

  • Top-level namespace reduced: only Array, empty_cuda_cache, and to_numpy_array (plus the __version__, __citation__ and __bibtex__ dunders) are exported from parallelproj directly. Everything else must be imported from the relevant submodule:

    from parallelproj.pet_scanners import RegularPolygonPETScannerGeometry
    from parallelproj.pet_lors import RegularPolygonPETLORDescriptor
    from parallelproj.projectors import RegularPolygonPETProjector
    
  • ``count_event_multiplicity`` moved to the new parallelproj.data submodule and is no longer exported at the top level. Replace parallelproj.count_event_multiplicity / from parallelproj import count_event_multiplicity with from parallelproj.data import count_event_multiplicity.

  • Runtime-detection variables removed from the parallelproj namespace: cuda_present, cupy_enabled, torch_enabled, num_visible_cuda_devices, lib_parallelproj_c_fname, lib_parallelproj_cuda_fname, cuda_kernel_file, is_cuda_array. Use parallelproj_core.cuda_enabled for CUDA detection.

  • ``RegularPolygonPETScannerGeometry`` signature changed: ring_positions and symmetry_axis are now required (and no longer accept None); all remaining arguments after them (num_lor_endpoints_per_side, lor_spacing, phis, ring_endpoint_ordering, phi0, lor_endpoint_positions) are keyword-only. Omitting a required argument now raises a clear TypeError instead of failing later. Calls that already pass these by keyword (the documented style) are unaffected; calls that passed them positionally must switch to keywords.

  • ``RegularPolygonPETLORDescriptor`` signature changed: max_ring_difference parameter replaced by a michelogram parameter that accepts a Michelogram object. See new Michelogram class below. It now also raises a ValueError if radial_trim is so large that no radial bins remain (num_rad < 1).

  • Sinogram transaxial convention changed (breaking): view 0’s central radial bin now connects detector 0 and detector N/2 (diametrically opposing), matching STIR and the PET vendors. Previously view 0 was anchored a quarter-ring away, so the bin <-> detector-pair mapping of existing regular-polygon sinograms differs from v1.x / earlier v2.0 pre-releases. Two new descriptor kwargs make the convention fully configurable: view_direction (ViewDirection) and radial_direction (RadialDirection), which flip the view / radial index directions. Together with the scanner’s ring_endpoint_ordering (crystal numbering), phi0 (module-0 azimuth; phi0=0 puts module 0 at the top – -y for symmetry_axis=2 – see the coordinate-convention change below) and zig_zag_order they reproduce any vendor’s (view, radial) <-> detector-pair convention. See the 01_pet_geometry/02_run_regular_polygon_pet_sino.py example.

  • World coordinate / detector-orientation convention changed (breaking): for symmetry_axis=2 the transaxial detector layout is now defined for the standard viewpoint – standing in front of the scanner, looking along the bore from -z toward +z, with x0 running left->right and x1 top->bottom (+x1 points down), aligned with the DICOM/LPS axes of a head-first-supine patient. Concretely, relative to earlier v2.0 pre-releases:

    • module/side 0 (phi0=0) now sits at the top (-x1) instead of +x1;

    • phi0 is now a right-hand rotation about the symmetry axis – a positive phi0 moves module 0 toward +x0 (previously -x0);

    • RingEndpointOrdering.CLOCKWISE / COUNTERCLOCKWISE are defined as clockwise / counterclockwise as seen in that ``-z`` view (the physical numbering for each enum value therefore swapped).

    These follow from reflecting the transaxial ax0 coordinate of every RegularPolygonPETScannerGeometry endpoint (for symmetry_axis=2 this is the x1/y axis), so absolute endpoint coordinates and the module-0 / crystal-0 anchor of existing scanners differ. Segment numbering, plane counts and sinogram shapes are unaffected. The 3-D example plots now render this viewpoint via ax.view_init(elev=..., azim=..., roll=180, vertical_axis="y").

  • ``TOFNonTOFElementwiseMultiplicationOperator`` removed.

  • ``ParallelViewProjector3D`` signature changed: the span and max_ring_diff keyword arguments have been replaced by a single michelogram parameter (a Michelogram object). This enables support for any odd span and makes the axial plane layout explicit. Replace:

    ParallelViewProjector3D(..., span=1, max_ring_diff=d)
    

    with:

    from parallelproj.pet_lors import Michelogram
    ParallelViewProjector3D(..., michelogram=Michelogram(num_rings, d, span=1))
    
  • ``TOFParameters`` defaults removed: num_tofbins, tofbin_width, and sigma_tof are now required arguments (no defaults). num_sigmas defaults to 3.0 and tofcenter_offset defaults to 0.

  • ``MatrixOperator.iscomplex`` and ``ElementwiseMultiplicationOperator.iscomplex`` changed from method to property: replace op.iscomplex() calls with op.iscomplex.

  • ``GradientFieldProjectionOperator`` numeric change: the eta normalisation formula was corrected (sqrt(sum(g²) + η²) instead of sqrt(sum(g² + η²))). Results will differ from v1.x.

  • License changed from MIT to Apache-2.0.

  • scipy >= 1.15 now required (was ~=1.0).

  • array-api-compat >= 1.7 now required.

  • Import-time banner and PARALLELPROJ_SILENT_IMPORT environment variable removed; import parallelproj is now silent.

1.x

1.10.2 (Aug 20, 2025)

  • add compatibility for latest cupy version (>= 13.5) which require from_dlpack to convert from torch tensors

  • fix minor issues to be compatible with array-api-strict~=2.0

1.10.1 (Jan 15, 2025)

  • add a check whether sum of tof bins along LOR is non-zero before running TOF sinogram back projector

  • update installation instructions after conda-forge recipe was updated

  • clean up RTD docs build

1.10.0 (July 29, 2024)

  • add support for numpy>=2.0

  • add tests with numpy 2.0 on python 3.9 and 3.12

  • remove tox.ini

1.9.1 (June 19, 2024)

  • BUGFIX: add missing device in BlockPET LOR descriptor (needed for pytorch + cuda backend)

1.9 (June 18, 2024)

  • add functionality to create scanners, LOR descriptors and projectors for scanners consisting of equal “block” modules

  • BUGFIX: correct behavior of TOF kernel truncation which was wrong in the case that the tof bin width was >> tof resolution

1.8 (March 20, 2024)

  • add function to count event multiplicity

  • add more examples (e.g. DePierro and LM SPDHG)

  • re-organize folder structure and pyproject.toml

  • force array-api-compat<1.5 (bug in 1.5.0)

  • use array-api-strict instead of numpy.array_api

1.7.3 (January 26, 2024)

  • print banner

  • test also on Windows

1.7.2 (January 26, 2024)

  • require python>=3.9

  • replace distuils.spawn by shutil.which

1.7.1 (January 19, 2024)

  • BUGFIX: correct bug in the “chunking” of TOF sinogram projections in the python interface

1.7.0 (January 15, 2024)

  • update of documentation

  • addition of more examples

  • addition of high-level classes for RegularPolygonPETScanner and LOR descriptors

1.6.2 (December 01, 2023)

  • BUGFIX: correct use of conj() of scalar value to be array api compatible

  • BUGFIX: divided by float() to be array api compatible

  • add scipy dependency

1.6.1 (October 18, 2023)

  • BUGFIX: add sigma as explicit argument in GaussianFilterOperator and convert correctly to numpy/cupy arrays

1.6.0 (October 16, 2023)

  • rewrite LinearOperator base class to support python array api including devices

  • add missing type hints

  • add finite difference operator

  • remove obsolete functions

1.5.0 (July 29, 2023)

  • add compatibility of python wrapper to python array api (via array-api-compat) such that numpy, cupy, pytorch arrays can be directly projected

  • no changes to the C/CUDA libs

1.4.0 (June 11, 2023)

  • add Linear Operators

1.3.7 (April 27, 2023)

  • update documentation

1.3.6 (April 25, 2023)

  • enable readthedocs

1.3.5 (April 23, 2023)

  • add py.typed for mypy type checker

1.3.4 (April 21, 2023)

  • rename python binding back to parallelproj

1.3.3 (April 20, 2023)

  • import annotations from __future__ to be compatible with older versions

1.3.2 (April 18, 2023)

  • rename test folder

  • lower absolute tolerance for forward TOF tests (otherwise windows builds might fail)

1.3.1 (April 17, 2023)

  • add num_visible_devices definition when cuda is not present

1.3.0 (April 17, 2023)

  • clean up pyproject.toml

  • move tests and rename imports in tests

  • rename python package to parallelprojpy and adapt setup.cfg

  • add first version of pyproject.toml

1.2.16 (April 16, 2023)

  • improve way to detect whether visible GPUs are present in the python API

  • remove AS approximation of erff in openMP lib (too large inaccuracies)

  • add TOF LM tests

  • add listmode wrappers

  • add TOF sino fwd test

1.2.15 (April 15, 2023)

  • add TOF sino projector wrappers and first test

  • BUGFIX: correct start and stop of loop over planes in cuda TOF sino projector when direction=2

  • add adjointness test (indirect test for back projection)

  • add first python unit test for non-tof fwd projection

  • add first python wrappers for non-tof Joseph projectors

1.2.14 (February 15, 2023)

  • make target link libraries (m and OpenMP) private

1.2.13 (January 13, 2023)

  • fix variable expansion in Config.cmake.in

  • update README

  • add link to arxiv preprint

1.2.12 (January 08, 2023)

  • set CUDA_HOST_COMPILER only when using clang

  • skip build of cuda lib if cuda is not present

1.2.11 (January 05, 2023)

  • set default CMAKE_CUDA_HOST_COMPILER to CMAKE_CXX_COMPILER

1.2.10 (December 30, 2022)

  • link parallelproj_c against libm (using PUBLIC link interface)

  • use better way to test whether we have to link against libm

  • add adjoint back projection test

  • add more generic non-tof test that tests rays in all 3 directions

1.2.9 (December 09, 2022)

  • BUGFIX: correct calculation of x_pr2 when principal direction is 0

1.2.8 (December 02, 2022)

  • do not install test binaries

  • require CXX compiler only for CUDA

1.2.6 (November 18, 2022)

  • clean up CMake logic

1.2.5 (November 11, 2022)

  • add conditions to nested if-else when adding cuda subdir

1.2.4 (November 10, 2022)

  • add fatal error if cuda lib is to be built but no cuda compiler is found

1.2.3 (November 04, 2022)

  • add skip option for cmake

1.2.2 (November 03, 2022)

  • read version from package.json

  • add conda build