- ``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 witheps = 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=Trueevaluates the unmodified log-likelihood (bins withy == 0, e.g. virtual bins, are handled exactly) and requires a positive expectation in all bins with counts. An optional absoluteepsallows sharing one epsilon across subset objectives, andenable_extra_checks=Truewarns on inputs producingnan/infNegPoissonLogLListmode— listmode Poisson log-likelihood with built-in forward model; optionalepskwarg (default0.0) smooths the per-event log / division terms (expectation-only shift — the symmetric shifted-Poisson surrogate ofNegPoissonLogLwould 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 deviationSumC1Function/SumC2Function— also created viaf1 + f2operator overloadingC1AffineObjective/C2AffineObjective— compose a loss with an affine forward modelNonNegativeIndicator— non-negativity constraint with proximal operatorMixedL21Norm— 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: newMichelogramLayoutenum andlayout=argument, plus aMichelogram.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).spanis ignored for this layout and :attr:`Michelogram.span` returnsNone. Combine it with a matchingRegularPolygonPETLORDescriptorfor the GE scanner of interest. - Selectable segment ordering for
Michelogram: newSegmentOrderenum andsegment_order=argument (also onMichelogram.geand, astarget_segment_order=, onSinogramAxialCompressionOperator).SegmentOrder.POSITIVE_FIRST(default) keeps the existing0, +1, -1, +2, -2, …layout;SegmentOrder.NEGATIVE_FIRSTgives0, -1, +1, -2, +2, …(negative segment before the positive one of each±kpair). This is a pure permutation of the sinogram planes -- segment numbering, plane counts and multiplicities are unchanged -- and works for both theSTANDARDandGElayouts. - ``SinogramAxialCompressionOperator`` (
parallelproj.pet_lors):LinearOperatorthat axially compresses a span-1 sinogram to a higher odd span (mode="sum"ormode="average"). Atarget_layout=MichelogramLayout.GEoption compresses a span-1 sinogram to the GE layout; itsmode="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 restrictedMichelogramkeeping only the requested signed segments (the order-preserving subsequence, with plane indices renumbered but segment labels unchanged), exposingparent_plane_indicesfor gather/scatter against the full sinogram. The companionSinogramSegmentSelectionOperator(parallelproj.pet_lors) is aLinearOperatorbuilt from a full LOR descriptor and a list ofsegments: it provides arestricted_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. Groupstransaxial_factorwithin-side crystals andaxial_factorrings 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 ascoarse_scanner/coarse_lor_descriptor(a regular-polygon descriptor), so a standardRegularPolygonPETProjectorprojects 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); passcoarse_radial_trimto override. The operator is span-1 only; GE-layout sinograms are mashed by composition -- convert GE -> span-1 with themode="average"adjoint of a span-1 <-> GESinogramAxialCompressionOperator, then mash, giving a pure span-1 coarse sinogram (see the01_pet_geometry/07_run_detector_mashing.pyexample). - ``TOFBinMashingOperator`` (
parallelproj.pet_lors): mashes (groups) everymashing_factorneighbouring 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 (takestof_parametersand the leadingnon_tof_data_shape), it exposes the matchingcoarse_tof_parametersso a projector can target the mashed TOF grid, and composes withSinogramMashingOperatorviaCompositeLinearOperator. Formode="sum"the mashed forward projection equals a direct coarse-TOF projection (erf additivity over adjacent bins). See the01_pet_geometry/08_run_tof_bin_mashing.pyexample. - ``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 axesreduce_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) andto_subset_mmap(write a sinogram to disk as subset-ordered memory maps).count_event_multiplicitynow 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 torchdetection_times_to_tof_bin— convert raw detection-time differences (nanoseconds) to projector-convention unsigned TOF bin indices ready for histogramming
- New sinogram / scanner ordering options:
SinogramZigZagOrder(azig_zag_orderargument onRegularPolygonPETLORDescriptor) andRingEndpointOrdering(with newphis,phi0,ring_endpoint_orderingandlor_endpoint_positionsarguments onRegularPolygonPETScannerGeometry) make the crystal / LOR endpoint ordering explicit and configurable. - Selectable LOR start/end (TOF-bin) convention: new
LOREndpointOrderenum andlor_endpoint_orderargument onRegularPolygonPETLORDescriptor.START_END(default) keeps the current behaviour;END_STARTswapsxstart/xendfor 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(inparallelproj.pet_lors) return fully configuredRegularPolygonPETLORDescriptorobjects for two built-in cylindrical TOF scanner geometries, and the matchingget_tof_parameters_G1/get_tof_parameters_G2(inparallelproj.tof) return theirTOFParameters. 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 :class:`~parallelproj.pet_lors.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 requirexpexplicitly. - ``EqualBlockPETProjector`` ``num_chunks`` parameter: split block-pair projections into chunks to reduce peak GPU memory usage.
- ``RegularPolygonPETProjector.convert_sinogram_to_listmode`` gained a
shuffleparameter to randomly permute the returned event list. - ``VstackOperator`` now raises
ValueErroron inconsistentin_shapeacross stacked operators (previously silent). - ``TOFParameters`` validates its arguments:
num_tofbinsmust be a positive integer andtofbin_width/sigma_tof/num_sigmasstrictly positive and finite (andtofcenter_offsetfinite), raising a clearValueErrorotherwise (a common cause is passing timing quantities in seconds/ps instead of the expected spatial mm). - Clearer fail-fast errors:
SumC1Function/SumC2FunctionraiseValueErroron an empty function sequence, and thestart_plane_index/end_plane_indexaccessors raiseValueError(instead ofAttributeError) 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 parallelprojis silent, but the reference to cite is available on demand asparallelproj.__citation__(plain text) andparallelproj.__bibtex__(BibTeX); aCITATION.cfffile is also provided (GitHub "Cite this repository").
- 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
Michelogrammaps ring pairs to sinogram planes/segments, usingSinogramAxialCompressionOperatorto compress a span-1 sinogram to a higher odd span (and to/from the GE layout), aSegmentOrdercomparison (positive-first vs negative-first) for both the STANDARD and GE layouts, and aSinogramSegmentSelectionOperatorsection that projects/back-projects only a chosen subset of segments. - New example: zig-zag LOR sampling in a sinogram view — visualises the
SinogramZigZagOrdercrystal/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 mashing —
SinogramMashingOperatorgroups 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 mashing —
TOFBinMashingOperatorgroups neighbouring TOF bins, the matchingcoarse_tof_parameters, and composition with detector mashing. - New example: histogramming listmode data into sinograms — using
parallelproj.unlistto 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_utilsmodule (interactive slice viewershow_vol_cuts,suggest_array_backend_and_device, analytic phantoms and demo priors). Examples and downloaded scripts/notebooks now import them straight fromparallelproj— noPYTHONPATHsetup and no separateexample_utils.pydownload are required. The module is private and examples-only (not part of the public API) and may change without notice.
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 theparallelprojnamespace. Import them fromparallelproj_coreinstead. Note that the TOF function names were also reordered (e.g.joseph3d_fwd_tof_sino→joseph3d_tof_sino_fwd).Top-level namespace reduced: only
Array,empty_cuda_cache, andto_numpy_array(plus the__version__,__citation__and__bibtex__dunders) are exported fromparallelprojdirectly. 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.datasubmodule and is no longer exported at the top level. Replaceparallelproj.count_event_multiplicity/from parallelproj import count_event_multiplicitywithfrom parallelproj.data import count_event_multiplicity.Runtime-detection variables removed from the
parallelprojnamespace:cuda_present,cupy_enabled,torch_enabled,num_visible_cuda_devices,lib_parallelproj_c_fname,lib_parallelproj_cuda_fname,cuda_kernel_file,is_cuda_array. Useparallelproj_core.cuda_enabledfor CUDA detection.``RegularPolygonPETScannerGeometry`` signature changed:
ring_positionsandsymmetry_axisare now required (and no longer acceptNone); 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 clearTypeErrorinstead 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_differenceparameter replaced by amichelogramparameter that accepts aMichelogramobject. See newMichelogramclass below. It now also raises aValueErrorifradial_trimis 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(:class:`~parallelproj.pet_lors.ViewDirection`) andradial_direction(:class:`~parallelproj.pet_lors.RadialDirection`), which flip the view / radial index directions. Together with the scanner'sring_endpoint_ordering(crystal numbering),phi0(module-0 azimuth;phi0=0puts module 0 at the top ---yforsymmetry_axis=2-- see the coordinate-convention change below) andzig_zag_orderthey reproduce any vendor's(view, radial) <-> detector-pairconvention. See the01_pet_geometry/02_run_regular_polygon_pet_sino.pyexample.World coordinate / detector-orientation convention changed (breaking): for
symmetry_axis=2the transaxial detector layout is now defined for the standard viewpoint -- standing in front of the scanner, looking along the bore from-ztoward+z, withx0running left->right andx1top->bottom (+x1points 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; phi0is now a right-hand rotation about the symmetry axis -- a positivephi0moves module 0 toward+x0(previously-x0);RingEndpointOrdering.CLOCKWISE/COUNTERCLOCKWISEare 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
ax0coordinate of everyRegularPolygonPETScannerGeometryendpoint (forsymmetry_axis=2this is thex1/yaxis), 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 viaax.view_init(elev=..., azim=..., roll=180, vertical_axis="y").- module/side 0 (
``TOFNonTOFElementwiseMultiplicationOperator`` removed.
``ParallelViewProjector3D`` signature changed: the
spanandmax_ring_diffkeyword arguments have been replaced by a singlemichelogramparameter (a :class:`~parallelproj.pet_lors.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, andsigma_tofare now required arguments (no defaults).num_sigmasdefaults to3.0andtofcenter_offsetdefaults to0.``MatrixOperator.iscomplex`` and ``ElementwiseMultiplicationOperator.iscomplex`` changed from method to property: replace
op.iscomplex()calls withop.iscomplex.``GradientFieldProjectionOperator`` numeric change: the
etanormalisation formula was corrected (sqrt(sum(g²) + η²)instead ofsqrt(sum(g² + η²))). Results will differ from v1.x.License changed from MIT to Apache-2.0.
scipy >= 1.15now required (was~=1.0).array-api-compat >= 1.7now required.Import-time banner and
PARALLELPROJ_SILENT_IMPORTenvironment variable removed;import parallelprojis now silent.
- add compatibility for latest cupy version (>= 13.5) which require
from_dlpackto convert from torch tensors - fix minor issues to be compatible with
array-api-strict~=2.0
- 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
- add support for numpy>=2.0
- add tests with numpy 2.0 on python 3.9 and 3.12
- remove tox.ini
- BUGFIX: add missing device in BlockPET LOR descriptor (needed for pytorch + cuda backend)
- 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
- 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
- print banner
- test also on Windows
- require python>=3.9
- replace
distuils.spawnbyshutil.which
- BUGFIX: correct bug in the "chunking" of TOF sinogram projections in the python interface
- update of documentation
- addition of more examples
- addition of high-level classes for RegularPolygonPETScanner and LOR descriptors
- 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
- BUGFIX: add sigma as explicit argument in
GaussianFilterOperatorand convert correctly to numpy/cupy arrays
- rewrite
LinearOperatorbase class to support python array api including devices - add missing type hints
- add finite difference operator
- remove obsolete functions
- 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
- add Linear Operators
- update documentation
- enable readthedocs
- add py.typed for mypy type checker
- rename python binding back to parallelproj
- import annotations from
__future__to be compatible with older versions
- rename test folder
- lower absolute tolerance for forward TOF tests (otherwise windows builds might fail)
- add
num_visible_devicesdefinition when cuda is not present
- 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
- improve way to detect whether visible GPUs are present in the python API
- remove AS approximation of
erffin openMP lib (too large inaccuracies) - add TOF LM tests
- add listmode wrappers
- add TOF sino fwd test
- 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
- make target link libraries (m and OpenMP) private
- fix variable expansion in Config.cmake.in
- update README
- add link to arxiv preprint
- set CUDA_HOST_COMPILER only when using clang
- skip build of cuda lib if cuda is not present
- set default
CMAKE_CUDA_HOST_COMPILERtoCMAKE_CXX_COMPILER
- 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
- BUGFIX: correct calculation of
x_pr2when principal direction is 0
- do not install test binaries
- require CXX compiler only for CUDA
- clean up CMake logic
- add conditions to nested if-else when adding cuda subdir
- add fatal error if cuda lib is to be built but no cuda compiler is found
- add skip option for cmake
- read version from package.json
- add conda build