diffct ====== .. raw:: html diffct provides differentiable projectors and matched adjoints for computed tomography (CT) in PyTorch. It supports 2D parallel and fan beams and 3D cone and parallel beams. Each view can follow any trajectory, and autograd gives gradients for the volume and for trajectory tensors. Detector pixels can follow parameterized surfaces, with first-order gradients for their parameters. Views can be split across several GPUs. Native kernels run on NVIDIA CUDA GPUs. The portable PyTorch backend runs on CPU, PyTorch ROCm and Apple Silicon (MPS) devices. Code written for diffct 1.x must be updated; see :doc:`migration`. Install ------- diffct needs Python 3.10 or newer, and a Python version for which PyTorch publishes wheels. Install PyTorch for your device first. Then print the CUDA version of your PyTorch build: .. code-block:: bash python -c "import torch; print(torch.version.cuda)" Install diffct with the extra that matches this version. The extra installs the CUDA compiler libraries that Numba CUDA needs. .. tab-set:: .. tab-item:: 13.x .. code-block:: bash pip install "diffct[cu13]" .. tab-item:: 12.x .. code-block:: bash pip install "diffct[cu12]" .. tab-item:: None (CPU, ROCm or MPS) .. code-block:: bash pip install diffct Use ``backend="torch"``. Importing diffct and using this backend require no CUDA runtime. .. tab-item:: From source .. code-block:: bash git clone https://github.com/sypsyp97/diffct.git cd diffct pip install -e ".[cu13]" # or ".[cu12]", or "." without CUDA python examples/quickstart.py Most scripts in ``examples/`` need an NVIDIA GPU; ``plot_trajectory.py`` and ``benchmark_trajectory_admission.py --backend torch`` run on CPU. Check the installation: .. code-block:: bash python -c "import torch, diffct; print(diffct.__version__); print(torch.cuda.is_available())" Backend, precision and solver contracts are in :doc:`execution`. Quickstart ---------- Create the acquisition once. Then call ``project()`` and ``backproject()``. .. tab-set:: .. tab-item:: CUDA .. code-block:: python import torch from diffct import Projector, spiral_trajectory_3d trajectory = spiral_trajectory_3d(360, sid=320.0, sdd=512.0, z_range=40.0, n_turns=1.0, device="cpu") A = Projector(trajectory, volume_shape=(128, 128, 128), detector_shape=(384, 256), detector_spacing=0.8) volume = torch.rand(A.volume_shape, device="cuda") sinogram = A.project(volume) # (360, 384, 256) line integrals adjoint = A.backproject(sinogram) # matched adjoint A^T x = torch.zeros_like(volume, requires_grad=True) loss = 0.5 * (A.project(x) - sinogram).square().sum() loss.backward() # x.grad = A^T (A x - y) .. tab-item:: CPU (PyTorch backend) .. code-block:: python import torch from diffct import Projector, spiral_trajectory_3d trajectory = spiral_trajectory_3d(60, sid=120.0, sdd=192.0, z_range=15.0, n_turns=1.0, device="cpu") A = Projector(trajectory, volume_shape=(48, 48, 48), detector_shape=(144, 96), detector_spacing=0.8, backend="torch") volume = torch.rand(A.volume_shape) sinogram = A.project(volume) # (60, 144, 96) line integrals adjoint = A.backproject(sinogram) # matched adjoint A^T x = torch.zeros_like(volume, requires_grad=True) loss = 0.5 * (A.project(x) - sinogram).square().sum() loss.backward() # x.grad = A^T (A x - y) .. tab-item:: Apple Silicon (MPS) .. code-block:: python import torch from diffct import Projector, spiral_trajectory_3d trajectory = spiral_trajectory_3d(60, sid=120.0, sdd=192.0, z_range=15.0, n_turns=1.0, device="cpu") A = Projector(trajectory, volume_shape=(48, 48, 48), detector_shape=(144, 96), detector_spacing=0.8, backend="torch") volume = torch.rand(A.volume_shape, device="mps") sinogram = A.project(volume) # (60, 144, 96) line integrals adjoint = A.backproject(sinogram) # matched adjoint A^T x = torch.zeros_like(volume, requires_grad=True) loss = 0.5 * (A.project(x) - sinogram).square().sum() loss.backward() # x.grad = A^T (A x - y) See :doc:`quickstart` for shapes, devices and troubleshooting, :doc:`examples` for complete reconstructions, and :doc:`performance` for multi-GPU timings. Citation and license -------------------- For software and technical-report citations, see the `README `_. The project uses the `Apache 2.0 license `_. This site documents the source checkout, including `unreleased APIs `_. .. toctree:: :hidden: getting_started user_guide examples api changelog Getting Started =============== .. grid:: 1 2 2 2 :gutter: 3 :class-container: feature-grid .. grid-item-card:: Installation :link: installation :link-type: doc Choose the extra that matches the CUDA version of your PyTorch build, or use the PyTorch backend on CPU. .. grid-item-card:: Quickstart :link: quickstart :link-type: doc Project an image, apply the matched adjoint and compute a gradient. .. toctree:: :hidden: installation quickstart Installation ============ Native execution uses an NVIDIA CUDA GPU. The portable PyTorch backend also supports CPU, PyTorch ROCm and Apple Silicon (MPS) devices; see :doc:`execution` for precision and hardware-validation limits. diffct needs Python 3.10 or newer, and a Python version for which PyTorch publishes wheels. Install PyTorch for your device first. Then print the CUDA version of your PyTorch build: .. code-block:: bash python -c "import torch; print(torch.version.cuda)" Install diffct with the extra that matches this version. The extra installs the CUDA compiler libraries that Numba CUDA needs. .. tab-set:: .. tab-item:: 13.x .. code-block:: bash pip install "diffct[cu13]" .. tab-item:: 12.x .. code-block:: bash pip install "diffct[cu12]" .. tab-item:: None (CPU, ROCm or MPS) .. code-block:: bash pip install diffct Use ``backend="torch"``. Importing diffct and using this backend require no CUDA runtime. .. tab-item:: From source .. code-block:: bash git clone https://github.com/sypsyp97/diffct.git cd diffct pip install -e ".[cu13]" # or ".[cu12]", or "." without CUDA python examples/quickstart.py Most scripts in ``examples/`` need an NVIDIA GPU; ``plot_trajectory.py`` and ``benchmark_trajectory_admission.py --backend torch`` run on CPU. Check the installation: .. code-block:: bash python -c "import torch, diffct; print(diffct.__version__); print(torch.cuda.is_available())" Numba CUDA initializes on native use. The NVVM and NVJitLink packages must match the CUDA libraries that PyTorch loads, so the extra must match ``torch.version.cuda``. A plain ``pip install diffct`` also works for native execution if a matching CUDA Toolkit is already installed. Troubleshooting --------------- - **CUDA is unavailable:** check that the active Python environment has a CUDA build of PyTorch and that the GPU is visible to the process. - **Kernel compilation fails:** use Numba CUDA, NVVM and NVJitLink versions that match each other. Do not upgrade only one of them. - **nvJitLink error:** ``nvJitLinkError`` with "may need newer version of nvJitLink library" means that the diffct extra does not match the CUDA version of PyTorch. Print ``torch.version.cuda`` and reinstall with ``diffct[cu12]`` for 12.x or ``diffct[cu13]`` for 13.x. Quickstart ========== This page projects an image, applies the matched adjoint and computes a gradient with autograd. Install diffct first; see :doc:`installation`. .. tab-set:: .. tab-item:: CUDA .. code-block:: python import torch from diffct import Projector, circular_trajectory_2d_parallel trajectory = circular_trajectory_2d_parallel(180, device="cpu") A = Projector(trajectory, (64, 64), 96, beam="parallel", detector_spacing=1.0, voxel_spacing=1.0) image = torch.zeros(A.volume_shape, device="cuda", dtype=torch.float32) image[20:44, 20:44] = 1.0 measurements = A.project(image) # (180, 96) adjoint = A.backproject(measurements) # (64, 64), not an inverse estimate = torch.zeros_like(image, requires_grad=True) loss = 0.5 * (A.project(estimate) - measurements).square().sum() loss.backward() # A^T (A estimate - measurements) print(measurements.shape, estimate.grad.shape) .. tab-item:: CPU (PyTorch backend) .. code-block:: python import torch from diffct import Projector, circular_trajectory_2d_parallel trajectory = circular_trajectory_2d_parallel(180, device="cpu") A = Projector(trajectory, (64, 64), 96, beam="parallel", detector_spacing=1.0, voxel_spacing=1.0, backend="torch") image = torch.zeros(A.volume_shape, dtype=torch.float32) image[20:44, 20:44] = 1.0 measurements = A.project(image) # (180, 96) adjoint = A.backproject(measurements) # (64, 64), not an inverse estimate = torch.zeros_like(image, requires_grad=True) loss = 0.5 * (A.project(estimate) - measurements).square().sum() loss.backward() # A^T (A estimate - measurements) print(measurements.shape, estimate.grad.shape) ``Projector`` defaults to ``beam="cone"``. Set ``beam="parallel"`` or ``beam="fan"`` for 2D data. Inputs must be floating-point CPU or CUDA tensors with the configured volume or sinogram shape in their trailing dimensions. Leading batch/channel dimensions are preserved. Outputs use the selected precision on the input device. Native CPU data streams through CUDA tiles; the PyTorch backend computes on its input device. See :doc:`execution` and :doc:`chunking`. Trajectory tensors can stay on CPU. Next steps ---------- - A custom or non-circular scan: see :doc:`trajectories`. - A curved detector: see :ref:`detector-surfaces` and run ``python examples/curved_detector.py`` from a source checkout. - An iterative reconstruction: run ``python examples/iterative_reconstruction.py --trajectory helical``. See :doc:`examples`. - Several GPUs or nodes: see :doc:`multi_gpu`. - Code written for diffct 1.x: see :doc:`migration`. - Gradient-based reconstruction with autograd and an optimizer: see :doc:`gradient_reconstruction`. Troubleshooting --------------- - **Shape error:** use ``(D, H, W)`` for cone volumes and ``(views, U, V)`` for cone sinograms. In distributed mode, use ``A.projection_shape`` and ``A.view_slice`` rather than the global view count. User Guide ========== These pages describe the contracts behind ``Projector`` and the reconstruction APIs. Start with :doc:`getting_started` if you have not installed diffct yet. .. grid:: 1 2 2 3 :gutter: 3 :class-container: feature-grid .. grid-item-card:: Trajectories and geometry :link: trajectories :link-type: doc Per-view trajectory tensors, built-in generators, custom scans, detector surfaces and geometry gradients. .. grid-item-card:: Execution and backends :link: execution :link-type: doc Backends, precision modes and solver contracts. .. grid-item-card:: Memory and chunking :link: chunking :link-type: doc Memory limits, spatial, view and pixel tiles, block stores and disk-backed volumes. .. grid-item-card:: Multi-GPU and multi-node :link: multi_gpu :link-type: doc Several GPUs in one process, or ``torchrun`` across nodes, with view, space or hybrid partitions. .. grid-item-card:: Performance :link: performance :link-type: doc Forward, adjoint and CGLS timings for a 512³ volume on 1, 4 and 8 GPUs. .. grid-item-card:: Differentiable FBP and FDK :link: analytical_learning :link-type: doc Analytical reconstruction with autograd, and learnable filters and weights. .. grid-item-card:: Migrating from 1.x :link: migration :link-type: doc Move code from the circular-orbit API of diffct 1.x to per-view trajectories. .. toctree:: :hidden: :maxdepth: 2 trajectories execution chunking multi_gpu performance analytical_learning migration Trajectories and Geometry ========================= Trajectory format ----------------- ``Projector`` takes one row per view in each trajectory tensor. The beam type selects the tuple layout. The helpers in ``diffct`` create these tensors. You can also pass measured or calibrated poses in the same layout. .. list-table:: :header-rows: 1 * - Beam - Tuple (each component's shape) - Volume / sinogram * - ``parallel`` - ``(ray_dir, det_origin, det_u)``, each ``(N, 2)`` - ``(H, W)`` / ``(N, U)`` * - ``fan`` - ``(src_pos, det_center, det_u)``, each ``(N, 2)`` - ``(H, W)`` / ``(N, U)`` * - ``cone`` (default) - ``(src_pos, det_center, det_u, det_v)``, each ``(N, 3)`` - ``(D, H, W)`` / ``(N, U, V)`` * - ``parallel3d`` - ``(ray_dir, det_center, det_u, det_v)``, each ``(N, 3)`` - ``(D, H, W)`` / ``(N, U, V)`` Coordinates are Cartesian ``(x, y[, z])``. Volume indexing is ``[y, x]`` for 2D and ``[z, y, x]`` for 3D. The origin is the volume centre. Use one length unit for positions, voxel spacing and detector spacing. Voxel spacing is a single positive scalar or a positive per-axis tuple in tensor order: ``(dy,dx)`` or ``(dz,dy,dx)``. Both backends support per-axis spacing; see :doc:`execution`. Detector axes are unit direction vectors, not pixel-step vectors. The detector spacing sets the pixel pitch. A scalar spacing applies to both 3D detector axes. With the default flat detector, pixel ``(u, v)`` is at: .. code-block:: text det_center + (u - (U - 1) / 2) * du * det_u + (v - (V - 1) / 2) * dv * det_v A measured sinogram in ``(views, V, U)`` order must be transposed to ``(views, U, V)`` before use. Check the physical direction of each detector axis. Reshaping does not correct axis orientation. Custom acquisition example -------------------------- This example modifies a generated helix with a per-view detector shift. For a measured acquisition, replace the four tensors with your calibrated arrays in the coordinate system above. .. code-block:: python import torch from diffct import Projector, spiral_trajectory_3d trajectory = spiral_trajectory_3d( 90, sid=80.0, sdd=128.0, z_range=10.0, n_turns=1.0, device="cpu") source, center, u_axis, v_axis = trajectory phase = torch.arange(90, dtype=torch.float32) * (2 * torch.pi / 90) center = center + (0.25 * phase.sin())[:, None] * u_axis A = Projector((source, center, u_axis, v_axis), (32, 32, 32), (96, 64), detector_spacing=(0.8, 0.8)) volume = torch.ones(A.volume_shape, device="cuda") sinogram = A.project(volume) # (90, 96, 64) All components must be finite floating-point tensors with the same nonzero view count. ``ray_dir`` and ``det_u`` must be orthogonal for 2D parallel beams. ``det_u`` and ``det_v`` must be orthogonal for 3D beams. ``parallel3d`` permits oblique detectors: the unit ray direction must not be tangent to the detector plane. Degeneracy is checked by the frame's numerical rank in float64. Its rays are infinite lines through each pixel. It uses the PyTorch backend on CPU or CUDA; ``backend="auto"`` selects it automatically and explicit ``backend="numba"`` is unsupported. With flat fan/cone detectors, the source must not coincide with the detector centre or view the detector edge-on. The native FP32 backend requires the nearer of source and detector centre to lie within 1e6 voxel spacings of the origin. Native surface detectors validate each actual source/pixel pair: the endpoints must remain distinct in float32, and the nearer endpoint of every ray must lie within 1e6 voxel spacings of the origin. Both native endpoints must lie within 1e15 voxel spacings. Portable precision modes validate finite, nondegenerate rays in their selected dtype; see :doc:`execution`. Arbitrary trajectories do not guarantee enough angular coverage for an inverse problem. Use iterative methods with regularization for sparse or incomplete acquisitions. The analytical helpers assume a circular orbit (see :doc:`api`). .. _detector-surfaces: Parameterized detector surfaces ------------------------------- ``Projector(..., detector_surface=surface)`` places each pixel on a native curve. The legacy two-argument callback places pixels on an arc, cylinder or another parameterized surface. For 3D beams it receives physical float64 ``u, v`` grids on CPU with shape ``(U, V)`` and ``ij`` indexing. It returns local ``(u, v, n)`` offsets shaped ``(U, V, 3)`` or ``(views, U, V, 3)``. Here ``views`` is the total trajectory view count, also in distributed mode. The world point is: .. code-block:: text center + offset_u * det_u + offset_v * det_v + offset_n * cross(det_u, det_v) For 2D beams, ``u, v`` have shape ``(U,)`` and ``v`` is zero. Return ``(U, 3)`` or ``(views, U, 3)`` with zero middle offsets; the normal is ``(det_u_y, -det_u_x)``. A cylindrical surface can be written as: .. code-block:: python radius = torch.tensor(128.0, requires_grad=True) def cylinder(u, v): u, v = u.to(radius.device), v.to(radius.device) angle = u / radius return torch.stack((radius * angle.sin(), v, radius * (angle.cos() - 1)), dim=-1) C = Projector(trajectory, (32, 32, 32), (96, 64), detector_spacing=(0.8, 1.0), detector_surface=cylinder) For this generated frame the normal points away from the source; negative normal offsets and a radius equal to ``sdd`` centre the cylinder on the source. The callback is sampled at construction and afresh on each operation, and its captured parameters receive first-order gradients. Every sample must be a finite floating-point tensor of the documented shape; fan/cone pixels cannot coincide with the source. ``project`` and ``backproject`` retain their matched Siddon model, with one ray per pixel and no detector-area integration. They operate directly on the native curved grid without resampling. Run ``python examples/curved_detector.py`` for a complete circular cone example, also shown in :doc:`examples`. Full-volume and streamed operations accept CPU or CUDA offsets. The operator transfers selected offsets to the compute device before expanding world points for that device's view shard or streamed batch. Native execution preserves dtype promotion during composition before staging complete coordinates as float32. The PyTorch backend uses the selected precision. See :doc:`chunking` for memory requirements. For bounded view/pixel sampling, pass explicit tensor parameters: .. code-block:: python from diffct import ParameterizedSurface def sample(u, v, view_indices, r): return torch.stack((r * torch.sin(u / r), v, r * (torch.cos(u / r) - 1)), dim=-1) surface = ParameterizedSurface(sample, parameters=(radius,)) C = Projector(trajectory, (32, 32, 32), (96, 64), detector_spacing=(0.8, 1.0), detector_surface=surface, view_chunk_size=8, detector_chunk_shape=(32, 32)) ``u,v`` are a rectangle of global physical pixel centres; ``view_indices`` contains global view IDs. Return ``(*pixel_shape,3)`` shared offsets or ``(len(view_indices),*pixel_shape,3)`` batched offsets. In 2D the middle component remains zero. Streamed grids, IDs and explicit parameter snapshots are CPU tensors; parameter dtypes and original gradient devices are preserved. Native sampled-surface arithmetic promotes frames and offsets to one common dtype, at least float32; float64 inputs retain float64 arithmetic. The PyTorch backend uses the selected precision. Gradients return in each input's original dtype. Offsets and world points are validated on every executing batch. The sampler is pure. Mutable geometry tensors must be explicit parameters; other captured configuration must be immutable. Coupling across views uses global IDs or explicit parameters, including values for omitted views. Each forward captures the sampler and one set of frame/parameter values. Backward recomputes local geometry from those snapshots and reduces pixel cotangents to parameter gradients before communication. It retains first-order geometry gradients and data Hessians. Explicit parameter snapshots scale with parameter count; allocations inside user samplers remain caller-controlled. Do not cache mutable callback values across calls. Analytical FBP/FDK and weighting helpers continue to assume flat detectors and do not accept the callback. Reusing a flat-detector FDK requires matching-ray resampling of the line integrals onto a covered virtual flat grid; this adds interpolation error and is outside the core operator. Geometry gradients ------------------ Gradients with respect to trajectory tensors are first order only. Set ``requires_grad=True`` on each tensor before you construct ``Projector``. The projector keeps references to all trajectory components when any component requires gradients, and reads and validates their current values on each call. If none require gradients, it clones the entire trajectory. For example: .. code-block:: python # Continue from the custom acquisition above. target = sinogram.detach() learnable_center = center.clone().requires_grad_() B = Projector((source, learnable_center, u_axis, v_axis), (32, 32, 32), (96, 64), detector_spacing=0.8) loss = (B.project(volume) - target).square().sum() loss.backward() print(learnable_center.grad.shape) # (90, 3) This loss uses the same target geometry, so the gradient is zero. For a real calibration loop, see ``examples/geometry_calibration.py``. If poses come from learnable angles or offsets, rebuild the derived tensors inside each optimization step and pass them to the same projector: .. code-block:: python from diffct import circular_trajectory_3d angles = torch.linspace(0, 2 * torch.pi, 91, device=volume.device)[:-1] angles = angles.detach().requires_grad_() optimizer = torch.optim.Adam([angles], lr=1e-3) for _ in range(10): optimizer.zero_grad() current = circular_trajectory_3d(angles, sid=80.0, sdd=128.0) loss = (A.project(volume, trajectory=current) - target).square().sum() loss.backward() optimizer.step() ``project``, ``backproject``, ``__call__``, ``project_into`` and ``backproject_into`` accept the keyword-only ``trajectory`` argument. It replaces the complete tuple for that call and must keep the configured beam and view count. Omission or ``None`` uses the constructor trajectory. Each calculation keeps its own geometry snapshot for backward; overrides leave the default trajectory unchanged. The ``*_into`` methods retain their numerical, no-autograd contract. Distributed ranks must agree on whether an override is present and supply one coherent global trajectory. Call-time admission checks flat-detector corners and coordinate bounds, with exact pixel checks for ambiguous views. Frames stage once per validation device. Native curved-detector admission uses bounded batches on each compute device after view sharding. Distributed admission errors reach all ranks before numerical execution. Parameterize rotations so that axes stay unit length and orthogonal. Frame checks run at construction and on each call with live default frames or an override; sampled surface positions are checked on every operation. Differentiable geometry helpers ------------------------------- ``circular_trajectory_2d_parallel``, ``circular_trajectory_2d_fan``, ``circular_trajectory_3d`` and ``circular_trajectory_3d_parallel`` accept either a view count or a nonempty floating-point angle tensor as the first argument. Angles are in radians and may be nonuniform or unsorted. A tensor preserves its device, dtype and autograd history. ``start_angle``, ``end_angle``, ``device`` and ``dtype`` only configure count-based generation. Parallel helpers cover ``[0, pi)`` by default; fan and cone helpers cover ``[0, 2*pi)``. The 3D parallel helper lifts the 2D orbit into the XY plane with ``det_v=(0,0,1)``. In both parallel helpers, ``detector_distance`` is a transverse shift along ``det_u``. ``transform_trajectory(trajectory, rotation=None, translation=None, *, beam="cone")`` applies a rigid transform about the world origin. Points become ``R @ p + t``; directions become ``R @ d``. ``rotation`` is a proper orthogonal matrix shaped ``(d,d)`` or ``(views,d,d)``, and ``translation`` is shaped ``(d,)`` or ``(views,d)``. Use the same dtype and device as the trajectory. Both shared and per-view transforms preserve gradients to the frames and transform parameters. The helper validates frames and transforms; ``Projector`` checks actual detector pixels. Surface offsets may define valid rays even when the reference detector center coincides with the source. .. code-block:: python from diffct import transform_trajectory shift = torch.zeros(3, device=volume.device, requires_grad=True) current = transform_trajectory(tuple(t.detach() for t in current), translation=shift) shifted = A.project(volume, trajectory=current) Limits: - Second derivatives with respect to geometry raise an error. - Forward-mode derivatives (``torch.func.jvp``, ``jacfwd``) with respect to trajectory tensors raise an error. Use reverse mode for geometry gradients. - Second derivatives with respect to the volume or sinogram are available, including when trajectory or surface parameters require gradients. - Detector and voxel spacing are fixed physical settings, not differentiable parameters. - Geometry derivatives are piecewise: at exact voxel edges or corners they are one-sided. - Distributed geometry gradients are summed by the operator. See :doc:`multi_gpu` for loss scaling. ```{include} ../EXECUTION.md ``` Large Volumes and Chunked Execution =================================== This page describes the native CUDA path. For portable execution and analytical streaming, see :doc:`execution`. Pass CPU volumes and sinograms to ``Projector`` when complete arrays exceed GPU memory. Projection and the matched adjoint execute on CUDA; outputs and data gradients return to the input device. Tensor methods still hold complete arrays on that device. For data exceeding host RAM, use the caller-owned block-store methods below and the disk-backed CGLS example. .. code-block:: python import torch from diffct import Projector, circular_trajectory_3d trajectory = circular_trajectory_3d(60, sid=160, sdd=256, device="cpu") A = Projector(trajectory, (64, 64, 64), (192, 128), detector_spacing=0.8) x = torch.ones(A.volume_shape, device="cpu", requires_grad=True) y = A.project(x) # CPU tensor, real CUDA computation y.square().mean().backward() # x.grad stays on CPU adjoint = A.backproject(y.detach()) Automatic selection and manual limits ------------------------------------- ``volume_chunk_shape=None`` is the automatic default. CPU data always streams. For CUDA data, the original full-volume path is retained when its estimated additional working set fits each selected card and the output device. Otherwise the operation streams spatial tiles and view batches. The arithmetic estimate accounts for float32 staging, cone layout/output copies, view data, frame or sampled pixel geometry, VJP buffers and collective scratch. It reserves 25% of available memory for runtime overhead, respects PyTorch's per-process allocator ceiling and sizes against the smallest participating budget. Concurrent allocations can still cause allocation errors. Native PyTorch admission checks allocation-segment capacity separately from the live tensor payload estimate. A free split inside a partially occupied cached segment is not a promise that another buffer can use it. Small-pool and large-pool segment rounding also contribute to admission; a fitting sum of tensor sizes alone is insufficient. This check is conservative and may select smaller automatic tiles. Other allocator backends do not inherit native segment sizes. The segment estimate assumes standard native allocator settings and fresh fixed-buffer inventories; arbitrary fragmentation or allocator overrides can still cause allocation errors. On older PyTorch versions without the public memory-fraction getter, the estimate can observe driver/allocator memory only; use explicit limits when a process ceiling is set there. To override the automatic limits: .. code-block:: python A = Projector(trajectory, (64, 64, 64), (192, 128), detector_spacing=0.8, volume_chunk_shape=(16, 32, 32), view_chunk_size=8, detector_chunk_shape=(64, 64)) Spatial limits follow tensor order: ``(H, W)`` for parallel/fan and ``(D, H, W)`` for cone beams. Limits are positive integral dimensions, clipped at the volume edge. ``view_chunk_size`` may also be supplied alone to force view streaming. Its automatic value is at most 32 and can shrink when geometry/data buffers require it. ``detector_chunk_shape`` follows ``(U,)`` or ``(U,V)`` order. Automatic execution can split a single detector image when it does not fit; reducing the volume alone cannot solve that case. Pixel rectangles keep the global detector origin. Explicit limits must fit the planned working set. Tiles retain the global origin-centred voxel lattice. Projection adds disjoint tile integrals; the adjoint accumulates each tile over view batches. Odd sizes, partial tails and internal boundaries retain the same cell ownership. Chunking preserves the cell-constant Siddon model and matched adjoint, with float32 rounding differences from a different summation order. Smaller tiles may cost more transfers and kernel launches. Resident layouts and accumulation --------------------------------- For backprojection, each active GPU keeps one contiguous native-layout accumulator for a spatial tile. Every view batch adds to that accumulator; layout conversion and return to the caller happen after all views contribute. The accumulator is cleared once per tile. Projection prepares the native volume layout once per tile/device and reuses its ray output buffer across view batches. Geometry VJPs also prepare each tile once across their view batches. Cone kernels retain contiguous ``(W, H, D)`` access; tensor volumes remain ``(D, H, W)``. Forward kernels overwrite each active ray, including missed rays and short final batches, so they do not need unconditional output clearing. ``native_volume_layout=True`` optionally retains native full-CUDA cone output strides ``(1, D, D * H)`` for an unbatched flat-detector backprojection. The shape stays ``(D, H, W)``. Projection accepts that layout without conversion, so one compute device can avoid both volume layout copies in ``A^T A``. The default is ``False``. Several local devices, surfaces, batches, streaming and the PyTorch backend still involve copies. Distributed ranks must agree on this setting. For example, a ``1024**3`` float32 volume is 4 GiB. With 720 views in batches of 32, returning a complete partial volume after each of the 23 batches would transfer 92 GiB. Returning the completed accumulated volume transfers 4 GiB on a single GPU, excluding projections, geometry and other solver data. This is a copy-payload estimate; it does not imply a 23-fold reconstruction speedup. Float32 summation order can change slightly, while the underlying Siddon cell model and matched adjoint stay the same. Scheduling and transfers ------------------------ ``schedule="auto"`` compares a few spatial-first, view-first and bounded-window orders, including contiguous z slabs and three-dimensional blocks. Selection uses bounded real CUDA pilot times scaled by the full native launch count, with estimated full-workload copy payloads as a tie-breaker. The pilots use small residency/order prefixes, so selection is a heuristic; reduced copy payloads alone do not establish a runtime improvement. The benchmark example preserves the initial selection report separately from its warmed timings. ``schedule="spatial"``, ``"views"`` or ``"window"`` enables a reproducible comparison. Backprojection always completes each retained volume accumulator before returning it. View-first backprojection is eligible only when all its accumulators fit together. The transfer pipeline uses at most two reusable pinned slots per device, separate upload, compute and download streams, and CUDA events. Events protect input consumption, host output reads and reuse of GPU buffers. Each composite host slot is capped at 8 MiB, with a separate 8 MiB pageable read/cast scratch ceiling. These are memory policy ceilings. Larger resident GPU tiles use bounded host fragments; fragmentation changes copy call counts, while the completed volume payload is transferred once. Planned local surface geometry also has an independent host working-set ceiling. Arbitrary allocations inside a user sampler and OS file-cache residency remain outside these ceilings. All additional GPU slots, layouts, geometry and collective staging contribute to the GPU budget. Very small budgets can use a bounded serial fallback. Single-slot plans retire prior native computation before allocating the next tile. Compute and internal allocations use the caller stream. Repeated calls on that stream can reuse its cache, so temporary transfer streams do not divide the native allocator's reusable cache. A new buffer is fenced against earlier caller work before upload. This can delay prefetch of a newly allocated volume buffer; existing ray/geometry slots retain their bounded overlap. Transfers hold source and destination references in the bounded completion-event queue until they finish. Completed internal copies release these references directly, avoiding stale transfer-stream reservations across calls. ``A.last_execution_stats`` reports the last instrumented operation's schedule, limits, layout/kernel counts and actual upload/download/peer-copy payloads. ``estimated_gpu_bytes`` describes the tensor-buffer inventory; ``estimated_reserved_bytes`` and ``reservation_budget_bytes`` expose the separate allocation-segment estimate and available room. Copy counts describe transferred numerical tensor bytes, independently of segment sizes. Distributed layout metadata and planning control messages are excluded. Pilot timing and payloads are reported separately; candidate full-workload costs are estimates. These counts are not physical bus measurements. The value is ``None`` before execution or for an uninstrumented full-CUDA path. Run ``python examples/benchmark_execution.py`` to compare schedules and slab/block shapes on your hardware. Caller-owned blocks and disk files ------------------------------------------------------------ ``TensorStore`` provides detached numerical block access to an existing tensor. ``NpyStore`` maps a float32/float64 ``.npy`` file. Creating a store reserves a new path exclusively; existing files are not overwritten. Block reads/casts and writes do not materialize a complete mapped array. .. code-block:: python from diffct import NpyStore # The file has shape A.volume_shape, in tensor axis order. source = NpyStore("volume.npy", mode="r") rays = NpyStore.create("projections.npy", A.projection_shape) A.project_into(source, rays) rays.flush() result = NpyStore.create("adjoint.npy", A.volume_shape) A.backproject_into(rays, result) result.flush() ``project_into`` and ``backproject_into`` return the supplied output. They accept tensors, these stores, or structural backends with ``shape``, ``dtype``, ``read(slices)``, ``write(slices,value,accumulate=False)`` and ``flush()``. Outputs must match ``A.dtype`` and be writable. Native fast output is float32; explicit ``precision="float64"`` output is float64. Each completed output region overwrites its prior contents; no full result is allocated internally. These numerical methods do not build an autograd graph. Use tensor ``project``/``backproject`` for differentiation. Custom CUDA stores must produce returned blocks on the source device's current stream, or make that stream wait for their producer. The upload stream waits for this work before reading a block, including on the same GPU. Input and output must have distinct backing data. Identical objects, detectable overlapping tensors and the same canonical mapped file path are rejected before execution. Custom backends must guarantee that hidden backing storage is distinct; an opaque object's path attribute alone is not an alias guarantee. Calling a store's ``read`` with a complete array slice can still request a complete array; keep application-side reads and vector updates blockwise too. Autograd, curved detectors and several GPUs ------------------------------------------- Data gradients and data Hessians, plus first-order frame/surface gradients, use the streamed path too. Saved CPU geometry keeps backward consistent with its forward call if parameters change. Geometry second derivatives remain unsupported. For bounded geometry, use ``ParameterizedSurface(sampler, parameters=...)``. Its sampler receives global physical pixel centres, global view IDs and explicit parameter snapshots, and generates only the executing view/pixel batch. Pixel cotangents are reduced through the local sampler/frame graph to parameter gradients before communication. The captured sampler and one set of frame/parameter snapshots preserve backward behavior after parameter mutation. See :ref:`detector-surfaces`. Legacy two-argument callbacks receive complete CPU detector grids and can return CPU or CUDA offsets. The operator transfers selected offsets to each compute device before expanding world points for its owned views or streamed batch. Memory planning uses these local view counts and the composition dtype. Gradient-connected offsets are snapshotted on CPU and transferred per batch. A full single-process CUDA call instead keeps admitted CUDA snapshots on the input GPU. Streamed composition stages a complete admitted view/pixel batch and composes it with one launch into reusable float32 geometry buffers. If that workspace does not fit, a smaller fallback workspace composes one view and a bounded detector-row segment at a time. Geometry backward replays one view and a bounded detector-row segment at a time, without retaining complete CUDA composition graphs. Composition and replay buffers never coexist. Admission charges the larger phase total and the largest allocation of either phase; the segment estimate counts the buffers of both phases, because one phase can leave segments pinned. Native admission starts with the owned view shard and detector rectangle, limited by explicit hints. It reduces validation batches to fit device memory, allocation and host-staging limits. These batches are independent of execution chunks and their automatic 32-view cap. Callback allocations remain caller-controlled. Callback values are not cached across operations. ``devices=[0, 1]`` still partitions views in list order. Each card streams tiles; the full output returns to the input device. Distributed ``partition="views"`` ranks retain local view shards and replicated volumes. Local GPU backprojection sums and NCCL reductions occur while the tile is resident, before its final host transfer. Gloo uses bounded CPU staging. All ranks need matching settings and must participate in the same calls, including empty ranks. The ownership-specific loss rules in :doc:`multi_gpu` apply. Scalar NCCL reductions also use CUDA tensors. With CPU streaming, the cards compute their view batches concurrently. Each card receives every volume tile, so host transfers limit the multi-GPU speedup. With ``partition="views"``, every rank holds the complete tensor volume and iterative state. One process per node controlling ``devices=[...]`` avoids extra host replicas. ``partition="space"`` instead assigns balanced contiguous first-axis slabs to ranks. ``global_volume_shape`` retains the constructor shape; ``volume_slice`` gives global ownership and ``volume_shape``/ ``local_volume_shape`` give the required local tensor/store shape. Projection SUMs the slab contributions into replicated ray batches; backprojection returns only the owned slab. Physical coordinates remain on the global voxel lattice. Empty slabs are valid. The PyTorch backend also supports ``partition="hybrid", partition_grid=(view_ranks, space_ranks)``; see :doc:`execution` for ownership and loss divisors. Caller-owned CUDA inputs and complete CUDA outputs still occupy their full size. Keep arrays, trajectory tensors and learnable surface parameters on CPU for bounded GPU residency. CUDA geometry snapshots or CUDA intermediates made inside a callback also consume GPU memory outside the streamed tile buffers. ``fbp``/``fdk`` also support view/spatial chunks and numerical stores; see :doc:`execution`. The helical approximation and low-level CUDA helpers retain their original execution path. Runnable iterative reconstruction --------------------------------- The example reuses the existing CGLS solver with CPU arrays. It supports one process with one or several GPUs and prints measured residuals and peak PyTorch CUDA tensor allocation. The peak excludes driver/context memory. .. code-block:: bash python examples/chunked_reconstruction.py python examples/chunked_reconstruction.py --chunk-shape 16 32 32 --view-chunk-size 8 python examples/chunked_reconstruction.py --devices 0 1 The tensor CGLS helper computes FP64 norms in bounded pieces and reuses vector update buffers. Its complete tensor states still need memory. If any rank's CUDA state exceeds its budget, all ranks agree to keep state on CPU and replan together, preserving collective order for unequal slabs. Disk-backed CGLS keeps ``x,s,p`` plus ``r,q`` in stores, performs blockwise vector updates and writes completed-iteration checkpoints without gathering the volume. ``q`` remains available until the global norm determines ``alpha`` and the ``x``/residual updates finish. Account for all three volume states together; fitting one volume does not establish that the complete solver fits. CUDA admission also checks each state and FP64 norm block against the feasible individual allocation limit, together with the forward and adjoint pipeline inventories. A small total state size cannot bypass an allocation-segment limit. State allocations can pin complete native allocator segments. Admission reserves these segments separately from the later pipeline or FP64 norm work, matching the fresh-segment budget used when execution replans after state allocation. It does not assume later calls can use free splits inside the state segments. The state bound also includes compatible idle segments on the caller's stream: reusing an oversized cached segment can pin more memory than a fresh allocation. The example helpers set ``operator.last_solver_stats`` with state and scratch capacity metadata. This report belongs to the example helpers. The library also provides numerical CGLS/SIRT/TV solvers; see :doc:`execution`. The disk entrypoint saves it alongside the actual residual, elapsed time and CUDA tensor peak. .. code-block:: bash python examples/disk_reconstruction.py --output disk-run python examples/disk_reconstruction.py --surface saddle --output saddle-run torchrun --standalone --nproc-per-node=2 examples/disk_reconstruction.py \ --partition space --output spatial-run The default disk example generates explicitly synthetic measurements blockwise. Each rank owns its output, workspace and checkpoint files. Resume requires matching partition, ownership, shapes, scan and measurements. Files back the arrays; the OS may cache their pages in RAM. Local tests and bounded models provide evidence separately from physical multi-GPU or cross-node runs. The iteration count is the target total: resuming iteration 2 with ``--iterations 5`` performs three further iterations in a new run directory. .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/chunked_reconstruction.py :language: python :linenos: Multiple GPUs and Nodes ======================= By default, ``Projector`` splits views across selected GPUs and automatically streams native spatial tiles when arrays are CPU-backed or the estimated CUDA working set does not fit. For volumes larger than one GPU, use CPU arrays or numerical block stores; see :doc:`chunking`. There are two modes: - **One process, several GPUs:** pass ``devices`` to ``Projector``. - **One process per GPU:** launch with ``torchrun`` and pass ``distributed=True``. Default ``partition="views"`` holds a local view shard and a replicated volume. ``partition="space"`` holds an owned volume slab and replicated ray batches. ``partition="hybrid"`` combines view and space ownership on the PyTorch backend. GPU working buffers hold bounded tiles, pixels and view batches. The CUDA examples below use NCCL. Portable library calls can use CPU with Gloo or supported PyTorch accelerators; see :doc:`execution`. Spatial ownership ----------------- ``partition="space"`` splits the first tensor axis into balanced half-open slabs. ``global_volume_shape`` is the constructor shape; ``volume_slice`` gives the rank's global ownership. ``volume_shape`` and ``local_volume_shape`` give the local input/output shape. Global physical coordinates are preserved, and empty slabs are valid. Projection SUMs slab contributions; backprojection generates only the owned volume. Collective order does not depend on local tile counts. Shared parameter gradients are reduced after the local pixel VJP. In space mode, divide identical replicated projection losses by ``world_size`` before backward; sum losses on local backprojection slabs. CGLS SUMs owned volume norms but counts replicated ray norms once. The library's ``total_variation`` exchanges neighboring faces; custom boundary-coupled regularizers need their own halo exchange. The existing TV/SIRT examples continue to use views mode. The complete ownership and loss table is in ``docs/DISTRIBUTED.md`` in the repository. For replicated CPU state use one process per node controlling multiple GPUs to avoid extra host copies. For true spatial ownership use one process per GPU, with streaming inside an owned slab when needed. Disk-backed CGLS and rank-local checkpoints are available in ``examples/disk_reconstruction.py``; see :doc:`chunking`. The solver capacity includes ``x,s,p`` together plus ray, geometry and pipeline state. Hybrid mode uses ``partition_grid=(view_ranks, space_ranks)`` and parent-group rank ``view_rank * space_ranks + space_rank``. Divide projection losses by ``space_world_size`` and backprojection losses by ``view_world_size``. The disk example accepts only views and space partitions. One process, several GPUs ------------------------- No process group is needed. Views are split into contiguous shards in the order of the device list. Outputs return to the input tensor's device. .. code-block:: python import torch from diffct import Projector, spiral_trajectory_3d trajectory = spiral_trajectory_3d( 31, sid=80.0, sdd=128.0, z_range=8.0, n_turns=1.0, device="cpu") operator = Projector(trajectory, (32, 32, 32), (48, 40), devices=["cuda:0", "cuda:1"]) volume = torch.ones((32, 32, 32), device="cuda:0", requires_grad=True) sinogram = operator.project(volume) # (31, 48, 40), cuda:0 adjoint = operator.backproject(sinogram.detach()) # (32, 32, 32), cuda:0 sinogram.square().sum().backward() # volume.grad on cuda:0 Use the ordinary full-sinogram loss in this mode. No world-size scaling applies. One process per GPU ------------------- Save the following as ``distributed_demo.py``. Each rank creates the same volume, computes its local measurements, and differentiates one global data term. .. code-block:: python import os import torch import torch.distributed as dist from diffct import Projector, spiral_trajectory_3d # Select this process's GPU before creating the NCCL process group. device = torch.device("cuda", int(os.environ["LOCAL_RANK"])) torch.cuda.set_device(device) dist.init_process_group("nccl") try: n_views = 31 detector_shape = (48, 40) # (U, V) trajectory = spiral_trajectory_3d( n_views, sid=80.0, sdd=128.0, z_range=8.0, n_turns=1.0, device="cpu") operator = Projector(trajectory, (32, 32, 32), detector_shape, distributed=True) truth = torch.ones(operator.volume_shape, device=device) local_measurements = operator.project(truth) # this rank's views only volume = torch.zeros_like(truth, requires_grad=True) local_prediction = operator.project(volume) # Divide the local sum by the global ray count, not by the shard size. total_rays = n_views * detector_shape[0] * detector_shape[1] local_loss = (0.5 * (local_prediction - local_measurements).square().sum() / total_rays) local_loss.backward() # every rank receives the full volume gradient # Reduce a detached copy for reporting only. report = local_loss.detach().clone() dist.all_reduce(report, op=dist.ReduceOp.SUM) if operator.rank == 0: print(f"global half-MSE: {report.item():.6g}") finally: dist.destroy_process_group() Launch on one node with two GPUs: .. code-block:: bash torchrun --standalone --nproc-per-node=2 distributed_demo.py Launch on two nodes with two GPUs each. Run the same command on both nodes, changing only ``--node-rank``. Use the hostname of node 0 as ``--master-addr``: .. code-block:: bash # On node 0: torchrun --nnodes=2 --node-rank=0 --nproc-per-node=2 \ --master-addr= --master-port=29500 distributed_demo.py # On node 1: torchrun --nnodes=2 --node-rank=1 --nproc-per-node=2 \ --master-addr= --master-port=29500 distributed_demo.py Both nodes need the same checkout and Python environment. Every rank must join the same collective calls and backward passes, including ranks with zero views. Views mode: data on each rank ----------------------------- - ``operator.view_slice``: the contiguous range of global views on this rank. To use full measurements, select ``full_measurements[operator.view_slice]``. - ``operator.projection_shape``: the shape of ``project()`` output on this rank, and the required input shape of ``backproject()``. - The full volume. All ranks must start with the same volume and trajectory. The operator does not broadcast them. - ``backproject()`` sums the contributions of all ranks and returns the full volume on each rank. It is the matched adjoint, not an inverse. Views mode: loss scaling ------------------------ The projector sums the volume and learnable-geometry gradients across ranks. Scale the loss as follows: - **Projection data terms:** form the loss from local views and call ``backward()`` on every rank. Use a local ``.sum()`` for global least squares. Divide that sum by the global number of rays for a mean. A local ``.mean()`` weights unequal shards incorrectly and is undefined for an empty shard. - **Losses on replicated backprojection output:** divide each rank's copy of the same output loss by ``operator.world_size`` before ``backward()``. - **DDP:** do not add another DDP reduction for image or geometry gradients that ``Projector`` already reduces. For regularizers, logging and the full rule set, see `docs/DISTRIBUTED.md `_. Iterative reconstruction with several GPUs ------------------------------------------ The iterative example runs on one process with several GPUs or with torchrun: .. code-block:: bash python examples/iterative_reconstruction.py --devices 0 1 2 3 torchrun --nproc-per-node=4 examples/iterative_reconstruction.py For multi-node Slurm jobs, see ``examples/slurm/multi_node.sbatch`` in the repository. Set the account, partition and GPU request for your cluster. For the full multi-GPU and multi-node reference, see `docs/DISTRIBUTED.md `_. Performance =========== Time per forward projection, adjoint and CGLS iteration for a 512³ volume on 1, 4 and 8 GPUs. Run the same workload with examples/benchmark_scaling.py. .. image:: https://sypsyp97.github.io/diffct/_images/scaling_light.png :class: only-light performance-chart :alt: Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume .. image:: https://sypsyp97.github.io/diffct/_images/scaling_dark.png :class: only-dark performance-chart :alt: Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume .. list-table:: :header-rows: 1 :class: performance-table * - 512³ volume - 1 GPU - 4 GPUs, 1 node - 8 GPUs, 2 nodes * - Forward projection - 566.79 ms - 150.76 ms (3.8×) - 81.58 ms (6.9×) * - Adjoint (backprojection) - 1060.77 ms - 279.37 ms (3.8×) - 224.04 ms (4.7×) * - CGLS iteration - 1753.73 ms - 468.09 ms (3.7×) - 337.39 ms (5.2×) diffct 2.4.3, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap. Projection times are medians of seven samples after three warmups. CGLS uses three ten-iteration solves after a two-iteration warmup; the median includes initialization and is divided by ten. Raw samples and configuration: `scaling.json `_. Differentiable analytical reconstruction =========================================== ``Projector.project`` and ``Projector.backproject`` implement the cell-constant Siddon forward operator and its matched adjoint. Analytical FBP and FDK use a different, voxel-driven interpolation operator. Its backward pass scatters the same interpolation coefficients, including distance weights and normalization. It does not substitute the Siddon projector for the analytical transpose. .. figure:: https://sypsyp97.github.io/diffct/_images/learning.png :alt: FBP with the Ram-Lak filter and with a learned filter :class: example-figure Abdominal slice of TCIA case L213 at 256² (see :ref:`data-sources`), parallel beam, 360 views, 2% noise, 200 Adam steps on the filter only. These settings differ from ``examples/learnable_reconstruction.py`` below. The reconstruction APIs return signed images in the selected precision on the input device. Clamping is a caller decision. In the default native fast mode, low-precision measurements are promoted to float32 before geometry, weighting and FFT operations; gradients retain the original input/parameter dtype. Sinogram, filter and explicit weight gradients propagate through the pipeline, including higher derivatives with respect to those tensors. Acquisition geometry and isocenter are fixed; trainable geometry is rejected explicitly. Use the Siddon ``Projector`` for geometry calibration. Short-scan distance and offset checks account for physical coordinate scale and the original trajectory precision, including when float32 geometry is promoted for float64 measurements. Each check uses only the trajectory components involved in its calculation; detector-v precision cannot relax the detector-u offset check. Each source/detector coordinate and detector-axis component counts with its own rounding along the checked direction, and each allows twice the largest two-view change that this rounding can cause. Axis components have half the smallest subnormal as a rounding floor. Eight eps of the in-plane ray length covers arithmetic and orbit rounding. Both checks also allow a relative tolerance of 1e-4, and the absolute tolerance is at least 1e-5. A change smaller than these bounds cannot be told apart from rounding: low-precision or far-translated coordinates that have a component along the checked direction widen the check. Helical slice centers use float64 independently of the angle dtype; low-precision angles do not round physical voxel coordinates. ``fbp`` and ``fdk`` support flat detectors, portable FP32/FP64 arithmetic, view/spatial chunks, several local devices, distributed views/space/hybrid ownership and numerical ``fbp_into``/``fdk_into`` stores; see :doc:`execution`. The helical approximation and low-level CUDA helpers retain their original execution contracts. FDK is an approximate inverse even though its implemented discrete backward pass transposes the interpolation coefficients. Use the complete APIs with the same trajectory tensors as ``Projector``: .. code-block:: python from diffct import fbp, fdk, helical_fdk image = fbp(sino2d, trajectory2d, (H, W), angles, beam="fan", detector_spacing=du, isocenter=(0, 0)) volume = fdk(sino3d, trajectory3d, (D, H, W), angles, detector_spacing=(du, dv), isocenter=(0, 0, 0)) helical = helical_fdk(sino3d, helix, (D, H, W), unwrapped_angles, detector_spacing=(du, dv), isocenter=(0, 0, 0)) Sinograms use ``(views,U)`` or ``(views,U,V)``; images use ``(H,W)`` or ``(D,H,W)``. Distances, pitches and isocenter share one physical unit. ``projection_weights`` replaces the complete pre-filter cosine weight; ``angular_weights`` replaces angular quadrature in ``fbp``/``fdk``. ``filter_response`` replaces the full real rFFT multiplier, already scaled in physical units, with length ``pad_factor*U//2+1``. It overrides ``window``. For circular fan/cone short scans, ``short_scan=True`` adds Parker weighting before filtering and checks the circular orbit and required angular span. Native curved data needs covered flat-detector resampling before these APIs. Building and learning a pipeline ------------------------------------- Run the examples from a source checkout: .. code-block:: bash python examples/analytical_reconstruction.py --size 64 python examples/learnable_reconstruction.py --beam parallel --size 32 python examples/learnable_reconstruction.py --beam fan --size 32 python examples/learnable_reconstruction.py --beam cone --size 32 python examples/helical_fdk.py --size 32 ``learnable_reconstruction.py`` shows the full pipeline in ordinary PyTorch: detector cosine weights, a trainable frequency response, FFT filtering, angular quadrature and the analytical gather. It optimizes the filter and projection weights for FBP or FDK on a synthetic noisy phantom. This demonstrates autograd and composition; the fitted parameters are specific to the example. Helical approximation --------------------- ``helical_fdk`` filters detector rows once, then reconstructs each axial slice from a full turn centered at the angle where the source crosses that slice. ``helical_view_weights`` exposes those per-slice quadrature weights. ``helical_weighted_backproject`` accepts replacement weights after filtering, so users can learn or replace this stage independently. This is a native flat-detector, slice-windowed FDK approximation for an axial helix with radial detector normals. It is not an implementation of exact Katsevich inversion, cone-parallel rebinning or conjugate-ray 3D weighting. The source-z samples and explicit unwrapped angles determine the window; nominal pitch parameters are not substituted for the actual acquisition. Incomplete angular windows are rejected. Detector truncation remains possible and the example uses a detector tall enough to cover its selected windows. The full-turn window and half redundancy factor are motivated by equation 14 of `Kong et al. (2018) `_. That paper derives the formula after cone-parallel rebinning; applying the window directly to the native cone gather here is an approximation. Increasing pitch or cone angle can increase reconstruction error. The implementation processes one slice at a time, keeping the window stage readable and avoiding a views-by-volume weight tensor. This trades kernel-launch overhead for bounded temporary storage; inputs, outputs and autograd state still reside on the CUDA device. Example results --------------- The 32-voxel learning examples use 180 views and 20 Adam steps on a synthetic phantom with Gaussian projection noise. These training results are specific to that example: .. list-table:: :header-rows: 1 * - Beam - Initial PSNR - Learned filter/weight PSNR * - Parallel FBP - 31.96 dB - 33.81 dB * - Fan FBP - 29.90 dB - 32.86 dB * - Cone FDK - 30.24 dB - 32.09 dB The 32-cubed, 720-view, four-turn helical example gives 32.99 dB. The integration weights of each slice sum to pi. Manual composition and the complete API agree within 4e-7. Float32 atomic accumulation order depends on the GPU, so the last digit of these values can change. ```{include} ../MIGRATION.md ``` Examples ======== Each page has the commands, an explanation and one result figure. Figure captions state the data and settings. The scripts are in the ``examples/`` directory of a source checkout; see the "From source" tab in :doc:`installation`. Analytical reconstruction ------------------------- - :doc:`Parallel-beam FBP `: FBP with a choice of ramp-filter window. - :doc:`Fan-beam FBP `: fan-beam FBP with cosine weights over a full turn. - :doc:`Cone-beam FDK `: FDK on a circular orbit, with its cone-beam artifacts. - :doc:`Learnable FBP `: learn the FBP filter through autograd. Iterative reconstruction ------------------------ - :doc:`Gradient-based reconstruction `: the volume is a parameter; project it, compare with the measurement, optimize. - :doc:`CGLS, SIRT and TV `: solvers on circular, helical, saddle or sinusoidal 3D scans. - :doc:`Measured walnut `: FDK, SIRT, CGLS and TV on 240 measured cone-beam views. Clinical data and detectors --------------------------- - :doc:`Clinical helical CT `: measured patient projections, flying focal spot, curved detector, 8 GPUs. - :doc:`Curved detector `: projection, matched adjoint and gradients on a native cylindrical detector. All scripts ----------- The scripts in the repository ``examples/`` directory show how to use ``diffct``. Run them from the repository root. - ``quickstart.py``: ``Projector`` basics for parallel, fan and cone beams, with the adjoint check and gradients. - ``curved_detector.py``: native cylindrical cone detector projection, matched backprojection, adjoint check and volume/surface gradients on a circular scan. - ``chunked_reconstruction.py``: CPU-backed CGLS with automatic CUDA tiles, optional manual limits and measured peak allocation; see :doc:`chunking`. - ``disk_reconstruction.py``: blockwise disk CGLS, optional native curves, rank-owned spatial slabs and completed-iteration checkpoints. - ``benchmark_execution.py``: schedule and slab/block comparisons with actual copy payloads, pilot metadata, timing and CUDA tensor peaks. - ``benchmark_trajectory_admission.py``: per-call trajectory validation without numerical projection. Use ``--backend torch --frame-device cpu`` on CPU. - ``benchmark_surface_geometry.py``: forward, adjoint and geometry-gradient timings of both curved-detector interfaces on a synthetic helical scan. - ``benchmark_gpu_overheads.py``: synthetic launch and transfer overheads of trajectories, detector surfaces, solvers and tiled FDK. - ``analytical_reconstruction.py``: parallel-beam FBP, fan-beam FBP and cone-beam FDK, with a choice of ramp-filter window. - ``learnable_reconstruction.py``: build and train a filter and detector weights for parallel/fan FBP or cone FDK using public helpers. - ``helical_fdk.py``: approximate axial helical FDK with explicit slice windows and weight gradients. - ``iterative_reconstruction.py``: 3D cone-beam CGLS, SIRT and TV reconstruction on circular, helical, saddle or sinusoidal trajectories. - ``walnut_reconstruction.py``: FDK, SIRT, CGLS and TV reconstruction of a measured walnut scan. - ``geometry_calibration.py``: recovers per-view angle errors and a detector shift from projections, using geometry gradients. - ``benchmark_projector.py``: compares one GPU with several GPUs. - ``benchmark_scaling.py``: measures the 512³ README workload on 1, 4 and 8 GPUs, with raw timings and numerical checks. - ``plot_trajectory.py``: plots the trajectory generators (CPU only). See ``examples/README.md`` in the repository for all launch modes. .. _data-sources: Data sources ------------ - **Patient data:** cases L213 (abdomen) and C002 (chest) of McCollough et al., *Low Dose CT Image and Projection Data (LDCT-and-Projection-data)*, Version 7, The Cancer Imaging Archive, 2020, https://doi.org/10.7937/9npb-2637; dataset paper: Moen et al., Medical Physics, 2020, https://doi.org/10.1002/mp.14594. Chest and Liver subsets: `CC BY 4.0 `_. The data were acquired under grants EB017095 and EB017185 (Cynthia McCollough, PI) from the National Institute of Biomedical Imaging and Bioengineering. "Ground truth" panels show the vendor images, converted from HU to water units; all other panels are reconstructions or simulations computed with diffct. - **Walnut:** Meaney 2022, Zenodo 6986012, CC BY 4.0; see ``examples/data/NOTICE``. - **Shepp-Logan phantom:** computed by ``examples/_common.py``. The figures are made by ``docs/assets/gallery/compute.py`` and ``docs/assets/gallery/render.py``; see ``docs/assets/gallery/NOTICE``. .. toctree:: :hidden: :caption: Analytical reconstruction fbp_parallel_example fbp_fan_example fdk_cone_example .. toctree:: :hidden: :caption: Iterative reconstruction gradient_reconstruction iterative_reco_cone_example walnut_example .. toctree:: :hidden: :caption: Clinical data and detectors clinical_helical_example curved_detector_example Parallel Beam Filtered Backprojection (FBP) =========================================== This page describes the ``examples/analytical_reconstruction.py`` pipeline. For filter options and helper signatures, see :doc:`api`. .. figure:: https://sypsyp97.github.io/diffct/_images/fbp_parallel.png :alt: Parallel-beam FBP of the Shepp-Logan phantom and of a patient slice :class: example-figure Ground truth: Shepp-Logan, and an abdominal slice of TCIA case L213 (see :ref:`data-sources`). 512², 720 views over 180°, 1536 detector bins at half the pixel size, Shepp-Logan window. PSNR in the lower corner. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. ``--figure`` needs Matplotlib: ``pip install matplotlib``. .. code-block:: bash python examples/analytical_reconstruction.py --size 64 --window shepp-logan python examples/analytical_reconstruction.py --size 128 --figure analytical.png The script runs parallel, fan and cone reconstruction on one CUDA GPU and prints PSNR against the synthetic phantom. ``--figure`` requires Matplotlib. ``--window`` accepts ``ram-lak``, ``shepp-logan``, ``cosine``, ``hamming`` or ``hann``. Pipeline -------- The example uses a 180-degree parallel-beam scan with 360 views. The detector has ``3 * size`` cells and a pitch of 0.5 voxels. 1. Project a phantom slice with ``Projector(beam="parallel")``. 2. Filter along the detector dimension (``dim=1``) with ``ramp_filter_1d(..., sample_spacing=pitch, pad_factor=2)``. 3. Multiply by ``angular_integration_weights(half_turn, redundant_full_scan=False)``. 4. Call ``parallel_weighted_backproject`` on the sinogram's CUDA device. The analytical helpers are not the ``Projector.backproject()`` adjoint. Do not use the adjoint in this pipeline, and do not add extra scale factors. The helpers apply their own normalization. Source ------ .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/analytical_reconstruction.py :language: python :linenos: :caption: Parallel FBP, fan FBP and cone FDK Fan Beam Filtered Backprojection (FBP) ====================================== This page describes the fan-beam part of ``examples/analytical_reconstruction.py``. For filter options and helper signatures, see :doc:`api`. .. figure:: https://sypsyp97.github.io/diffct/_images/fbp_fan.png :alt: Fan-beam FBP of the Shepp-Logan phantom and of a patient slice :class: example-figure Ground truth: Shepp-Logan, and a chest slice of TCIA case C002 (see :ref:`data-sources`), lung window. 512², 720 views over 360°, magnification 1.6, Shepp-Logan window. PSNR in the lower corner. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. ``--figure`` needs Matplotlib: ``pip install matplotlib``. .. code-block:: bash python examples/analytical_reconstruction.py --size 64 --window shepp-logan python examples/analytical_reconstruction.py --size 128 --figure analytical.png The script runs parallel, fan and cone reconstruction on one CUDA GPU and prints PSNR against the synthetic phantom. ``--figure`` requires Matplotlib. Pipeline -------- The example uses a full-turn circular fan-beam scan with 360 views, ``sid = 2.5 * size``, ``sdd = 4 * size``, ``3 * size`` detector cells and a pitch of 0.8. Distances use the same units as the unit-spaced voxels. 1. Project a phantom slice with ``Projector(beam="fan")``. 2. Apply ``fan_cosine_weights`` for the detector pitch and source-detector distance. 3. Apply ``ramp_filter_1d`` along ``dim=1``, with ``sample_spacing=pitch`` and ``pad_factor=2``. 4. Multiply by ``angular_integration_weights`` with ``redundant_full_scan=True``. 5. Call ``fan_weighted_backproject`` for the distance-weighted gather. This example uses a full scan. Short-scan reconstruction needs Parker weights and suitable angular coverage. Changing the view count does not enable it. The analytical helpers are not the ``Projector.backproject()`` adjoint. Do not use the adjoint in this pipeline, and do not add extra scale factors. Source ------ .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/analytical_reconstruction.py :language: python :linenos: :caption: Parallel FBP, fan FBP and cone FDK Cone Beam Feldkamp-Davis-Kress (FDK) ==================================== This page describes the cone-beam part of ``examples/analytical_reconstruction.py``. For filter options and helper signatures, see :doc:`api`. .. figure:: https://sypsyp97.github.io/diffct/_images/fdk_cone.png :alt: Cone-beam projection and FDK slices of a 3D phantom :class: example-figure 3D Shepp-Logan phantom, 256³, 720 views on a circular orbit. The off-centre slice and the coronal section show the approximation error of FDK away from the orbit plane. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. ``--figure`` needs Matplotlib: ``pip install matplotlib``. .. code-block:: bash python examples/analytical_reconstruction.py --size 64 --window shepp-logan python examples/analytical_reconstruction.py --size 128 --figure analytical.png The script runs parallel, fan and cone reconstruction on one CUDA GPU and prints PSNR against the synthetic phantom. ``--figure`` requires Matplotlib. Pipeline -------- The example uses a flat detector and a circular cone-beam scan with 360 views. It does not reconstruct native curved-detector data directly. To reuse this pipeline for a curved detector, first resample line integrals along matching rays onto a covered virtual flat grid. This adds interpolation error; FDK still retains its circular cone-beam approximation. For native curved projection and matched backprojection, see :ref:`detector-surfaces` and :doc:`examples`. The example projects a 3D phantom with ``Projector`` into a ``(views, U, V)`` sinogram. The helper ``examples/_common.py::fdk`` then calls :func:`diffct.reconstruction.fdk`, which performs these steps: 1. Apply detector cosine weights computed from the actual per-view rays. 2. Filter along the detector u axis (``dim=1``) with ``sample_spacing=du`` and ``pad_factor=2``. The last axis is v, not u. 3. Apply ``angular_integration_weights``. A full turn includes the ``1/2`` redundancy factor. 4. Call ``cone_weighted_backproject``. The example clamps negative values to zero. The same stages are public helpers. See :doc:`analytical_learning` to replace or learn the weights and filter. FDK is approximate for a circular cone-beam scan away from the central plane. Large cone angles increase artifacts. On non-circular trajectories, FDK is a heuristic baseline, not an exact reconstruction. The analytical helpers are not the ``Projector.backproject()`` adjoint. Do not use the adjoint in this pipeline, and do not add extra scale factors. Source ------ .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/analytical_reconstruction.py :language: python :linenos: :caption: Parallel FBP, fan FBP and cone FDK Gradient-Based Iterative Reconstruction ======================================= The volume is a tensor of parameters. The differentiable forward projector maps it to projections, a loss compares them with the measurement, and a PyTorch optimizer updates the volume. Autograd computes the gradient with the matched adjoint ``A^T``, so no backprojection code is written by hand. The same loop takes other losses, regularizers, constraints and network parameters. For least squares without extra terms, the :func:`~diffct.solvers.cgls` and :func:`~diffct.solvers.sirt` solvers converge in fewer iterations. Objective --------- For a projector ``A`` and measured projections ``y``, the examples minimize the mean squared error over a nonnegative volume ``x``: .. math:: \hat{x} = \arg\min_{x \ge 0} \tfrac{1}{M} \lVert A x - y \rVert_2^2 ``M`` is the number of measured values. After each Adam step, ``x`` is clamped to zero or more. 2D parallel and fan beam ------------------------ .. figure:: https://sypsyp97.github.io/diffct/_images/gradient_2d.png :alt: Ground truth, Adam steps and data loss for parallel and fan beams :class: example-figure The loop below at 512², with 720 views, Adam learning rate 0.1, 1000 steps and the abdominal slice of TCIA case L213 as ground truth (see :ref:`data-sources`). PSNR uses the peak of the ground truth. The code below uses a 128² square so that it runs in seconds. .. tab-set:: .. tab-item:: Parallel beam .. code-block:: python import torch from diffct import Projector, circular_trajectory_2d_parallel n, n_views, n_det = 128, 360, 384 trajectory = circular_trajectory_2d_parallel(n_views, device="cuda") A = Projector(trajectory, (n, n), n_det, beam="parallel", detector_spacing=0.5) truth = torch.zeros((n, n), device="cuda") truth[40:88, 40:88] = 1.0 measured = A.project(truth).detach() image = torch.zeros((n, n), device="cuda", requires_grad=True) optimizer = torch.optim.Adam([image], lr=0.1) for step in range(500): optimizer.zero_grad() loss = 0.5 * (A.project(image) - measured).square().sum() loss.backward() optimizer.step() with torch.no_grad(): image.clamp_(min=0) .. tab-item:: Fan beam .. code-block:: python import torch from diffct import Projector, circular_trajectory_2d_fan n, n_views = 128, 360 sid, sdd, n_det, pitch = 2.5 * n, 4.0 * n, 3 * n, 0.8 trajectory = circular_trajectory_2d_fan(n_views, sid, sdd, device="cuda") A = Projector(trajectory, (n, n), n_det, beam="fan", detector_spacing=pitch) truth = torch.zeros((n, n), device="cuda") truth[40:88, 40:88] = 1.0 measured = A.project(truth).detach() image = torch.zeros((n, n), device="cuda", requires_grad=True) optimizer = torch.optim.Adam([image], lr=0.1) for step in range(500): optimizer.zero_grad() loss = 0.5 * (A.project(image) - measured).square().sum() loss.backward() optimizer.step() with torch.no_grad(): image.clamp_(min=0) The 2D loops use the summed squared error; its gradient differs from the mean only by a constant factor, and Adam steps are nearly invariant to such a factor. 3D measured walnut ------------------ .. figure:: https://sypsyp97.github.io/diffct/_images/gradient_walnut.png :alt: FDK reference, Adam steps and MSE for the measured walnut :class: example-figure 240 measured cone-beam views of a walnut, 256³ volume, Adam with learning rate 0.01. The FDK column is a reference only; the loop starts from zero. The measured projections are scaled so that the walnut shell is about 1, as in ``examples/walnut_reconstruction.py``: .. code-block:: python import torch from diffct import Projector # trajectory, detector shape and spacing from examples/walnut_reconstruction.py A = Projector(trajectory, (256, 256, 256), detector, detector_spacing=(du, dv), voxel_spacing=voxel) volume = torch.zeros(A.volume_shape, device="cuda", requires_grad=True) optimizer = torch.optim.Adam([volume], lr=0.01) for step in range(500): optimizer.zero_grad() loss = torch.nn.functional.mse_loss(A.project(volume), measured) loss.backward() optimizer.step() with torch.no_grad(): volume.clamp_(min=0) The full script is the ``gradient`` command of ``docs/assets/gallery/compute.py``. Variations ---------- - **Regularization:** add a penalty to the loss, for example ``loss = loss + weight * diffct.total_variation(volume, voxel_spacing=voxel)``. :func:`~diffct.solvers.tv_reconstruction` packages this loop with residual history and checkpoints. - **Learned components:** replace the volume tensor with the output of a network, or make trajectory tensors trainable to calibrate the geometry; see :doc:`trajectories`. - **Large volumes and several GPUs:** the same loop works with the ``devices`` and ``distributed`` options of ``Projector``; see :doc:`multi_gpu`. - **Stopping:** the learning rate and step count are starting values. On real data, stop with a rule that does not use a ground truth. Cone Beam Iterative Reconstruction ================================== ``examples/iterative_reconstruction.py`` reconstructs a 3D cone-beam phantom with ``diffct.Projector``. It runs on any supported trajectory, on one GPU, on several GPUs in one process, or on several processes and nodes. .. figure:: https://sypsyp97.github.io/diffct/_images/iterative_cone.png :alt: Axial and coronal slices of a simulated helical scan :class: example-figure Simulated helical walnut scan from :doc:`walnut_example`: 720 views, 256³, 1% noise. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. ``--figure`` needs Matplotlib: ``pip install matplotlib``. .. code-block:: bash python examples/iterative_reconstruction.py --trajectory helical python examples/iterative_reconstruction.py --trajectory circular --devices 0 1 2 3 torchrun --nproc-per-node=4 examples/iterative_reconstruction.py ``--devices`` and ``torchrun`` cannot be used together. For multiple nodes, see :doc:`multi_gpu`. Options ------- .. list-table:: :header-rows: 1 * - Option - Meaning * - ``--size`` - Volume edge length in voxels (default 128). * - ``--views`` - Number of views (default 360). * - ``--trajectory`` - ``circular``, ``helical``, ``saddle`` or ``sinusoidal`` (default ``helical``). * - ``--algorithms`` - One or more of ``cgls``, ``sirt`` and ``tv`` (default: all). * - ``--iterations`` - Override the per-algorithm defaults (CGLS 30, SIRT 200, TV 200). * - ``--tv-weight`` - TV penalty weight for ``tv`` (default 1.0). * - ``--noise`` - Gaussian noise, relative to the largest projection (default 0.0). * - ``--phantom`` - ``(size, size, size)`` ``.npy`` volume with values in [0, 1], used instead of the Shepp-Logan phantom. * - ``--figure`` - Save central slices to this PNG. Requires Matplotlib. Pipeline -------- 1. Project the phantom with ``Projector`` (cone beam, on the selected trajectory). 2. Reconstruct with CGLS and SIRT. Both use the projector and its adjoint. 3. Reconstruct with TV-regularized nonnegative least squares. The script uses Adam and autograd for this solver. 4. When the full sinogram is on one GPU, compute an FDK baseline. On non-circular trajectories, FDK is a heuristic baseline, not an exact reconstruction. Notes ----- - The iteration counts are demonstration settings, not a general convergence schedule. Choose a stopping rule for your data. - The TV weight is an example setting. Choose it for your geometry, resolution, voxel spacing and data scale. The solver uses half the mean squared residual per ray and a mean TV term. Its voxel differences are not divided by voxel spacing. - The script keeps complete input/output tensors on the process's CUDA device. Native working buffers may stream; CGLS state can fall back to CPU. - To adapt the loss for several ranks, see :doc:`multi_gpu`. Source ------ .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/iterative_reconstruction.py :language: python :linenos: :caption: Iterative Reconstruction Example (3D cone beam, any trajectory) Measured Walnut =============== ``examples/walnut_reconstruction.py`` reconstructs a measured walnut from 240 cone-beam projections on a full circular orbit. The data are from Meaney 2022 (Zenodo 6986012, CC BY 4.0; see ``examples/data/NOTICE``). The scan geometry is passed to ``Projector`` as explicit per-view tensors, the same way as a calibrated trajectory. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. ``--figure`` needs Matplotlib: ``pip install matplotlib``. .. code-block:: bash python examples/walnut_reconstruction.py --figure walnut.png python examples/walnut_reconstruction.py --size 512 --devices 0 1 2 3 The figure shows axial and coronal centre slices. The reconstructions are: - FDK with a Hann window. - SIRT (200 iterations). - CGLS (20 iterations). - TV-regularized least squares (300 Adam steps, weight 0.3). Use ``--algorithms`` with no values to run FDK only. Use ``--window`` to change the FDK filter. .. image:: https://sypsyp97.github.io/diffct/_images/walnut.png :alt: Measured walnut: FDK, SIRT, CGLS and TV reconstructions Simulated helical scan ---------------------- ``--save-volume`` stores the FDK volume scaled to [0, 1]. Use it as the phantom for a simulated helical scan: .. code-block:: bash python examples/walnut_reconstruction.py --algorithms --save-volume walnut256.npy python examples/iterative_reconstruction.py --size 256 --views 720 --trajectory helical \ --noise 0.01 --phantom walnut256.npy --figure helical.png .. image:: _static/gallery/iterative_cone.png :alt: Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions Clinical Helical CT =================== This example reconstructs measured projections of patients from a clinical helical CT scanner. The same script also reconstructs case C002 (chest, Siemens, flying focal spot, 11072 views). The data are the full-dose projections of LDCT-and-Projection-data (see :ref:`data-sources`). Each view is one detector readout in the DICOM-CT-PD format. .. figure:: https://sypsyp97.github.io/diffct/_images/helical_L213.png :alt: Axial, coronal and sagittal slices of a measured abdominal helical scan :class: example-figure Case L213 (abdomen, GE): 7140 views, 888 × 64 cylindrical detector, 1 mm voxels, 30 CGLS iterations without regularization. Soft-tissue window. Geometry -------- The scanner geometry goes to ``Projector`` as per-view tensors, without rebinning to a circular orbit or a flat detector: - **Source:** the focal spot of each view, including the flying focal spot offsets in angle, radius and z. - **Detector:** a cylindrical surface centred on the nominal focal spot, given as a ``detector_surface`` callback. Projection and backprojection use these pixel positions directly. - **Views:** the views are split over the GPUs with ``distributed=True``; :func:`~diffct.solvers.cgls` runs on the distributed operator. The detector column of the central ray is 368.56 for C002 and 442.2 for L213, as 0-based stored columns. We estimated both values beforehand with gradient-based calibration of the column offset; that calibration is not part of this script. .. literalinclude:: https://sypsyp97.github.io/diffct/../assets/gallery/compute.py :language: python :pyobject: helical :caption: docs/assets/gallery/compute.py Run it ------ 1. Download the full-dose projection and image series of a Chest or Liver case from The Cancer Imaging Archive. 2. Convert one case to arrays: .. code-block:: bash pip install pydicom python docs/assets/gallery/ldct_prep.py ldct/L213 3. Reconstruct on 8 GPUs: .. code-block:: bash torchrun --nproc-per-node=8 docs/assets/gallery/compute.py helical \ --case ldct/L213 --center 442.2 --axial-z -418 -364 -307 --out arrays Native Curved Detector ====================== ``examples/curved_detector.py`` projects a phantom onto a native cylindrical cone detector, checks the matched backprojection and computes volume and surface gradients on a circular scan. .. figure:: https://sypsyp97.github.io/diffct/_images/curved_detector.png :alt: Projections of a patient volume on a cylindrical detector :class: example-figure Vendor images of TCIA case L213 (see :ref:`data-sources`) as a volume, projected onto a cylindrical detector centred on the source: 888 × 625 pixels, radius 949 mm, source 541 mm from the isocentre. Made by the ``volumes`` command of ``docs/assets/gallery/compute.py``; the script below uses a smaller geometry. Run it ------ Run the commands from the root of a source checkout; ``pip install diffct`` does not install the ``examples/`` directory. See the "From source" tab in :doc:`installation`. .. code-block:: bash python examples/curved_detector.py This small CUDA example uses the native curved operators directly, without resampling or FDK. See :ref:`detector-surfaces` for the callback contract. Source ------ .. literalinclude:: https://sypsyp97.github.io/diffct/../../examples/curved_detector.py :language: python :linenos: :caption: Native cylindrical detector operators API Reference ============= .. raw:: html High-level Projector -------------------- .. currentmodule:: diffct .. autosummary:: :toctree: generated :template: api_class.rst :nosignatures: Projector ``Projector`` takes trajectory tensors as described in :doc:`trajectories`. For multiple GPUs and nodes, see :doc:`multi_gpu`. Each operation also accepts ``trajectory=...`` to replace the full trajectory for that call, preserving its view count and the constructor default. ``detector_surface`` defines native pixel positions for both projection and matched backprojection. See :ref:`detector-surfaces` for its coordinate and shape contract, and :doc:`examples` for a runnable cylindrical detector. Native CPU data uses streamed CUDA execution; ``backend="torch"`` computes on its PyTorch device. ``precision`` selects fast FP32 or strict FP32/FP64. Leading batch/channel axes and per-axis voxel spacing are supported; see :doc:`execution`. ``volume_chunk_shape``, ``view_chunk_size`` and ``detector_chunk_shape`` specify optional limits; ``schedule`` allows operation-specific comparisons. ``partition="space"`` assigns rank-local volume slabs. See :doc:`chunking` for memory, storage, geometry and autograd requirements, and :doc:`multi_gpu` for ownership. Complete Reconstruction Pipelines ------------------------------------ .. currentmodule:: diffct.reconstruction .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: fbp fdk fbp_into fdk_into helical_fdk helical_view_weights helical_weighted_backproject Numerical Solvers ----------------- .. currentmodule:: diffct.solvers .. autosummary:: :toctree: generated :template: api_class.rst :nosignatures: SolverResult .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: cgls sirt tv_reconstruction cgls_into total_variation Geometry Helpers ---------------- .. currentmodule:: diffct.geometry **3D trajectories** .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: circular_trajectory_3d circular_trajectory_3d_parallel random_trajectory_3d spiral_trajectory_3d sinusoidal_trajectory_3d saddle_trajectory_3d custom_trajectory_3d **2D trajectories** .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: circular_trajectory_2d_parallel sinusoidal_trajectory_2d_parallel custom_trajectory_2d_parallel circular_trajectory_2d_fan sinusoidal_trajectory_2d_fan custom_trajectory_2d_fan **Rigid transforms** .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: transform_trajectory Block stores and batched surfaces --------------------------------- .. currentmodule:: diffct .. autosummary:: :toctree: generated :template: api_class.rst :nosignatures: TensorStore NpyStore ParameterizedSurface ``project_into`` and ``backproject_into`` use caller-owned numerical outputs. ``ParameterizedSurface`` exposes mutable geometry as explicit tensor parameters and samples bounded global view/pixel rectangles. Legacy two-argument surface callbacks remain available. See :ref:`detector-surfaces`. Analytical Reconstruction Helpers --------------------------------- .. currentmodule:: diffct.analytical These helpers build the pre-weights, angular weights, filter and weighted backprojection of an analytical FBP or FDK pipeline. The weighted backprojection helpers support sinogram autograd with dedicated transpose kernels, including higher derivatives. They use fixed geometry and reject geometry/isocenter gradients. The returned images are already scaled. See :doc:`analytical_learning` for complete APIs and learnable filter/weight examples. Analytical FBP and FDK assume a flat detector and an acquisition model. These helpers do not accept ``detector_surface``. Native curved data must be resampled along matching rays onto a covered virtual flat detector before using these flat-detector helpers; resampling adds interpolation error. The native ``Projector`` forward/adjoint pair needs no such conversion. A non-circular trajectory does not make the result exact. The helpers accept the same per-view ``(src_pos, det_center, det_u[, det_v])`` arrays as ``Projector``. Fan and cone backprojection accept a keyword-only ``isocenter`` vector in physical units. With ``None`` (default), it is estimated from the detector normals. Give it explicitly for non-circular or ambiguous geometry, including a single view. The three weighted backprojection helpers need at least two detector bins along each interpolated axis, including the cone detector's v axis. .. autosummary:: :toctree: generated :template: api_function.rst :nosignatures: detector_coordinates_1d angular_integration_weights fan_cosine_weights cone_cosine_weights parker_weights ramp_filter_1d parallel_weighted_backproject fan_weighted_backproject cone_weighted_backproject Ramp Filter Options ------------------- ``ramp_filter_1d`` is the filter used by the analytical examples. Its signature is:: ramp_filter_1d(sinogram_tensor, dim=-1, sample_spacing=1.0, pad_factor=1, window=None, use_rfft=True) ``sample_spacing`` Physical detector-cell spacing along ``dim``, for example ``du`` for cone beam. The output is in physical units. Pass ``1.0`` to work in sample units. ``pad_factor`` Zero-pads the signal to ``pad_factor * N`` samples along ``dim``. Use ``2`` for cone-beam FDK. Use ``4`` when objects lie close to the detector edge. ``use_rfft`` Uses the real FFT for real inputs. Set ``False`` only to force the complex FFT path. The ``window`` argument selects a window applied to the Ram-Lak ramp: .. list-table:: :header-rows: 1 * - ``window`` - Effect * - ``None`` or ``"ram-lak"`` - No window. Sharpest, with the most noise. * - ``"cosine"`` - Half-cosine roll-off. * - ``"shepp-logan"`` - Sinc roll-off. Classical choice. * - ``"hamming"`` - Milder than Hann. * - ``"hann"`` or ``"hanning"`` - Strongest high-frequency suppression. Low-level Functions ------------------- These ``autograd.Function`` wrappers take explicit trajectory tensors. Use the ``forward`` signature as the argument order for ``.apply(...)``. For new code, prefer ``Projector``, which checks inputs and manages devices. .. currentmodule:: diffct .. autosummary:: :toctree: generated :template: api_autograd_function.rst :nosignatures: ParallelProjectorFunction ParallelBackprojectorFunction FanProjectorFunction FanBackprojectorFunction ConeProjectorFunction ConeBackprojectorFunction Projector ========= .. currentmodule:: diffct .. autoclass:: Projector :members: :special-members: __call__ fbp === .. currentmodule:: diffct.reconstruction .. autofunction:: fbp fdk === .. currentmodule:: diffct.reconstruction .. autofunction:: fdk fbp\_into ========= .. currentmodule:: diffct.reconstruction .. autofunction:: fbp_into fdk\_into ========= .. currentmodule:: diffct.reconstruction .. autofunction:: fdk_into helical\_fdk ============ .. currentmodule:: diffct.reconstruction .. autofunction:: helical_fdk helical\_view\_weights ====================== .. currentmodule:: diffct.reconstruction .. autofunction:: helical_view_weights helical\_weighted\_backproject ============================== .. currentmodule:: diffct.reconstruction .. autofunction:: helical_weighted_backproject SolverResult ============ .. currentmodule:: diffct.solvers .. autoclass:: SolverResult :members: :special-members: __call__ cgls ==== .. currentmodule:: diffct.solvers .. autofunction:: cgls sirt ==== .. currentmodule:: diffct.solvers .. autofunction:: sirt tv\_reconstruction ================== .. currentmodule:: diffct.solvers .. autofunction:: tv_reconstruction cgls\_into ========== .. currentmodule:: diffct.solvers .. autofunction:: cgls_into total\_variation ================ .. currentmodule:: diffct.solvers .. autofunction:: total_variation circular\_trajectory\_3d ======================== .. currentmodule:: diffct.geometry .. autofunction:: circular_trajectory_3d circular\_trajectory\_3d\_parallel ================================== .. currentmodule:: diffct.geometry .. autofunction:: circular_trajectory_3d_parallel random\_trajectory\_3d ====================== .. currentmodule:: diffct.geometry .. autofunction:: random_trajectory_3d spiral\_trajectory\_3d ====================== .. currentmodule:: diffct.geometry .. autofunction:: spiral_trajectory_3d sinusoidal\_trajectory\_3d ========================== .. currentmodule:: diffct.geometry .. autofunction:: sinusoidal_trajectory_3d saddle\_trajectory\_3d ====================== .. currentmodule:: diffct.geometry .. autofunction:: saddle_trajectory_3d custom\_trajectory\_3d ====================== .. currentmodule:: diffct.geometry .. autofunction:: custom_trajectory_3d circular\_trajectory\_2d\_parallel ================================== .. currentmodule:: diffct.geometry .. autofunction:: circular_trajectory_2d_parallel sinusoidal\_trajectory\_2d\_parallel ==================================== .. currentmodule:: diffct.geometry .. autofunction:: sinusoidal_trajectory_2d_parallel custom\_trajectory\_2d\_parallel ================================ .. currentmodule:: diffct.geometry .. autofunction:: custom_trajectory_2d_parallel circular\_trajectory\_2d\_fan ============================= .. currentmodule:: diffct.geometry .. autofunction:: circular_trajectory_2d_fan sinusoidal\_trajectory\_2d\_fan =============================== .. currentmodule:: diffct.geometry .. autofunction:: sinusoidal_trajectory_2d_fan custom\_trajectory\_2d\_fan =========================== .. currentmodule:: diffct.geometry .. autofunction:: custom_trajectory_2d_fan transform\_trajectory ===================== .. currentmodule:: diffct.geometry .. autofunction:: transform_trajectory TensorStore =========== .. currentmodule:: diffct .. autoclass:: TensorStore :members: :special-members: __call__ NpyStore ======== .. currentmodule:: diffct .. autoclass:: NpyStore :members: :special-members: __call__ ParameterizedSurface ==================== .. currentmodule:: diffct .. autoclass:: ParameterizedSurface :members: :special-members: __call__ detector\_coordinates\_1d ========================= .. currentmodule:: diffct.analytical .. autofunction:: detector_coordinates_1d angular\_integration\_weights ============================= .. currentmodule:: diffct.analytical .. autofunction:: angular_integration_weights fan\_cosine\_weights ==================== .. currentmodule:: diffct.analytical .. autofunction:: fan_cosine_weights cone\_cosine\_weights ===================== .. currentmodule:: diffct.analytical .. autofunction:: cone_cosine_weights parker\_weights =============== .. currentmodule:: diffct.analytical .. autofunction:: parker_weights ramp\_filter\_1d ================ .. currentmodule:: diffct.analytical .. autofunction:: ramp_filter_1d parallel\_weighted\_backproject =============================== .. currentmodule:: diffct.analytical .. autofunction:: parallel_weighted_backproject fan\_weighted\_backproject ========================== .. currentmodule:: diffct.analytical .. autofunction:: fan_weighted_backproject cone\_weighted\_backproject =========================== .. currentmodule:: diffct.analytical .. autofunction:: cone_weighted_backproject ParallelProjectorFunction ========================= .. currentmodule:: diffct .. autoclass:: ParallelProjectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: ParallelBackprojectorFunction ============================= .. currentmodule:: diffct .. autoclass:: ParallelBackprojectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: FanProjectorFunction ==================== .. currentmodule:: diffct .. autoclass:: FanProjectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: FanBackprojectorFunction ======================== .. currentmodule:: diffct .. autoclass:: FanBackprojectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: ConeProjectorFunction ===================== .. currentmodule:: diffct .. autoclass:: ConeProjectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: ConeBackprojectorFunction ========================= .. currentmodule:: diffct .. autoclass:: ConeBackprojectorFunction :class-doc-from: init :members: :undoc-members: :show-inheritance: ```{include} ../../CHANGELOG.md ``` :orphan: Moved ===== .. raw:: html This page moved to :doc:`gradient_reconstruction`. :orphan: Moved ===== .. raw:: html This page moved to :doc:`gradient_reconstruction`.