diffct 2.1.0__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {diffct-2.1.0 → diffct-2.2.1}/CHANGELOG.md +67 -0
  2. {diffct-2.1.0 → diffct-2.2.1}/CITATION.cff +2 -2
  3. {diffct-2.1.0 → diffct-2.2.1}/PKG-INFO +24 -19
  4. {diffct-2.1.0 → diffct-2.2.1}/README.md +23 -18
  5. {diffct-2.1.0 → diffct-2.2.1}/diffct/__init__.py +18 -1
  6. {diffct-2.1.0 → diffct-2.2.1}/diffct/analytical.py +151 -6
  7. diffct-2.2.1/diffct/chunking.py +201 -0
  8. {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/__init__.py +8 -0
  9. diffct-2.2.1/diffct/kernels/analytical_transpose.py +295 -0
  10. diffct-2.2.1/diffct/operators.py +2047 -0
  11. diffct-2.2.1/diffct/pipeline.py +258 -0
  12. {diffct-2.1.0 → diffct-2.2.1}/diffct/projectors.py +109 -204
  13. diffct-2.2.1/diffct/reconstruction.py +399 -0
  14. diffct-2.2.1/diffct/storage.py +125 -0
  15. diffct-2.2.1/diffct/streaming.py +48 -0
  16. diffct-2.2.1/diffct/surfaces.py +103 -0
  17. {diffct-2.1.0 → diffct-2.2.1}/examples/README.md +60 -0
  18. {diffct-2.1.0 → diffct-2.2.1}/formal/README.md +37 -0
  19. {diffct-2.1.0 → diffct-2.2.1}/pyproject.toml +1 -1
  20. diffct-2.2.1/tests/test_allocator_capacity.py +349 -0
  21. diffct-2.2.1/tests/test_analytical_autograd.py +313 -0
  22. diffct-2.2.1/tests/test_axis_aligned_chords.py +118 -0
  23. diffct-2.2.1/tests/test_batched_surfaces.py +705 -0
  24. diffct-2.2.1/tests/test_block_cgls.py +996 -0
  25. diffct-2.2.1/tests/test_block_storage.py +545 -0
  26. diffct-2.2.1/tests/test_cgls_state_admission.py +173 -0
  27. {diffct-2.1.0 → diffct-2.2.1}/tests/test_chunked_projector.py +135 -39
  28. diffct-2.2.1/tests/test_completed_transfer_reuse.py +92 -0
  29. diffct-2.2.1/tests/test_cuda_sink_lifetimes.py +125 -0
  30. diffct-2.2.1/tests/test_example_psnr.py +75 -0
  31. diffct-2.2.1/tests/test_execution_buffers.py +613 -0
  32. diffct-2.2.1/tests/test_execution_schedules.py +582 -0
  33. diffct-2.2.1/tests/test_output_pipeline_overlap.py +130 -0
  34. diffct-2.2.1/tests/test_reconstruction_api.py +872 -0
  35. diffct-2.2.1/tests/test_spatial_partition.py +433 -0
  36. diffct-2.2.1/tests/test_transfer_pipeline.py +646 -0
  37. diffct-2.2.1/tests/test_transfer_retirement_lifetimes.py +166 -0
  38. diffct-2.1.0/diffct/chunking.py +0 -73
  39. diffct-2.1.0/diffct/operators.py +0 -1033
  40. {diffct-2.1.0 → diffct-2.2.1}/.gitignore +0 -0
  41. {diffct-2.1.0 → diffct-2.2.1}/LICENSE +0 -0
  42. {diffct-2.1.0 → diffct-2.2.1}/NOTICE +0 -0
  43. {diffct-2.1.0 → diffct-2.2.1}/diffct/constants.py +0 -0
  44. {diffct-2.1.0 → diffct-2.2.1}/diffct/differentiable.py +0 -0
  45. {diffct-2.1.0 → diffct-2.2.1}/diffct/geometry.py +0 -0
  46. {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/cone_beam.py +0 -0
  47. {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/fan_beam.py +0 -0
  48. {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/geometry_vjp.py +0 -0
  49. {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/parallel_beam.py +0 -0
  50. {diffct-2.1.0 → diffct-2.2.1}/diffct/utils.py +0 -0
  51. {diffct-2.1.0 → diffct-2.2.1}/docs/video/README.md +0 -0
  52. {diffct-2.1.0 → diffct-2.2.1}/examples/data/NOTICE +0 -0
  53. {diffct-2.1.0 → diffct-2.2.1}/pytest.ini +0 -0
  54. {diffct-2.1.0 → diffct-2.2.1}/tests/__init__.py +0 -0
  55. {diffct-2.1.0 → diffct-2.2.1}/tests/_phantoms.py +0 -0
  56. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/README.md +0 -0
  57. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/__init__.py +0 -0
  58. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/_common.py +0 -0
  59. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_cone.py +0 -0
  60. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_fan.py +0 -0
  61. {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_parallel.py +0 -0
  62. {diffct-2.1.0 → diffct-2.2.1}/tests/distributed_projector_check.py +0 -0
  63. {diffct-2.1.0 → diffct-2.2.1}/tests/test_adjoint_inner_product.py +0 -0
  64. {diffct-2.1.0 → diffct-2.2.1}/tests/test_analytical_regressions.py +0 -0
  65. {diffct-2.1.0 → diffct-2.2.1}/tests/test_cone_projector_autograd.py +0 -0
  66. {diffct-2.1.0 → diffct-2.2.1}/tests/test_cuda_exit_deallocations.py +0 -0
  67. {diffct-2.1.0 → diffct-2.2.1}/tests/test_cuda_smoke.py +0 -0
  68. {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_dtypes.py +0 -0
  69. {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_mapping.py +0 -0
  70. {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_units.py +0 -0
  71. {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surfaces.py +0 -0
  72. {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_fan_accuracy.py +0 -0
  73. {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_fan_offsets.py +0 -0
  74. {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_parallel_accuracy.py +0 -0
  75. {diffct-2.1.0 → diffct-2.2.1}/tests/test_fdk_cone_accuracy.py +0 -0
  76. {diffct-2.1.0 → diffct-2.2.1}/tests/test_fdk_cone_offsets.py +0 -0
  77. {diffct-2.1.0 → diffct-2.2.1}/tests/test_geometry_autograd.py +0 -0
  78. {diffct-2.1.0 → diffct-2.2.1}/tests/test_geometry_gradients.py +0 -0
  79. {diffct-2.1.0 → diffct-2.2.1}/tests/test_gradcheck.py +0 -0
  80. {diffct-2.1.0 → diffct-2.2.1}/tests/test_multi_gpu_exit.py +0 -0
  81. {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_api.py +0 -0
  82. {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_degenerate_geometry.py +0 -0
  83. {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_geometry_devices.py +0 -0
  84. {diffct-2.1.0 → diffct-2.2.1}/tests/test_ramp_filter_windows.py +0 -0
  85. {diffct-2.1.0 → diffct-2.2.1}/tests/test_release_2_0_1.py +0 -0
  86. {diffct-2.1.0 → diffct-2.2.1}/tests/test_weights.py +0 -0
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.1] - 2026-10-13
11
+
12
+ ### Added
13
+
14
+ - Public differentiable `fbp`, `fdk` and approximate slice-windowed `helical_fdk`
15
+ pipelines, with replaceable projection, angular and frequency-response tensors.
16
+ Low-precision measurements run in float32 and keep gradients in their own dtype.
17
+ - Dedicated CUDA transposes of the analytical parallel/fan/cone gathers,
18
+ including sinogram gradients and higher derivatives with fixed geometry.
19
+ - Circular short-scan checks for `short_scan=True`: orbit, detector-u handedness,
20
+ angular span, and constant detector distance and offset. Tolerances follow the
21
+ physical geometry scale and the original trajectory precision.
22
+ - Public helical view weights and weighted gather. Slice centers and angular
23
+ quadrature use float64, so large angle or height offsets keep full-turn coverage.
24
+ - Examples `learnable_reconstruction.py` and `helical_fdk.py`, and the
25
+ analytical learning guide.
26
+
27
+ ### Changed
28
+
29
+ - `parallel_weighted_backproject`, `fan_weighted_backproject` and
30
+ `cone_weighted_backproject` support sinogram autograd. Geometry and isocenter
31
+ gradients raise an error.
32
+ - The FDK baselines in `examples/_common.py` and `walnut_reconstruction.py` call `fdk`.
33
+
34
+ ## [2.2.0] - 2026-10-11
35
+
36
+ ### Added
37
+
38
+ - Operation-specific automatic schedules and slab/block comparisons for
39
+ native projection and matched backprojection, with bounded CUDA pilots
40
+ and per-call execution/copy metadata.
41
+ - Detector-pixel tiling and explicit `ParameterizedSurface` batches using
42
+ global pixel/view coordinates, saved parameter/frame values and local VJP
43
+ reduction. Legacy two-argument surface callbacks remain compatible.
44
+ - Caller-owned numerical `project_into`/`backproject_into` outputs, tensor
45
+ block access and mapped `.npy` stores for data exceeding host RAM.
46
+ - Distributed `partition="space"` with fixed volume slabs, ownership-aware
47
+ autograd/norms and disk CGLS state/checkpoints without full-volume gathering.
48
+ - Independent CUDA tests and bounded formal protocol/ownership verification.
49
+
50
+ ### Changed
51
+
52
+ - GPU backprojection accumulators now span all contributing view batches,
53
+ with final transfer after tile completion. Contiguous native volume layouts
54
+ and ray buffers are reused across batches.
55
+ - Transfer execution uses bounded pinned slots, separate streams and events;
56
+ all extra staging/cached GPU buffers are included in capacity estimates.
57
+ Local GPU and NCCL reductions occur before completed-result downloads.
58
+ - Example CGLS norms accumulate FP64 blocks and vector updates reuse buffers.
59
+ The disk example keeps all three volume states plus residual/projection state
60
+ blockwise and retains `q` until the global `alpha` decision.
61
+
62
+ ### Fixed
63
+
64
+ - Tensor CGLS ranks agree on CPU fallback before jointly replanning unequal
65
+ spatial shards, preserving collective order.
66
+ - Parameterized detector surfaces use a common arithmetic dtype of at least
67
+ float32, supporting mixed frame dtypes and finite half-precision inputs.
68
+ - CUDA store uploads wait for newly generated blocks on their source stream,
69
+ including transfers within one GPU.
70
+ - Single-slot execution retires prior native computation before allocating
71
+ the next tile, keeping deferred CUDA allocations within its admission bound.
72
+ - Mapped input and output stores reject hard links to the same file before writes.
73
+ - Example PSNR supports CPU reconstruction results with CUDA reference volumes.
74
+ - Deferred CPU output writes retain their submission order across spatial tiles.
75
+ - Coronal slice displays preserve the measured detector row orientation.
76
+
10
77
  ## [2.1.0] - 2026-10-10
11
78
 
12
79
  ### Added
@@ -13,8 +13,8 @@ authors:
13
13
  given-names: Yipeng
14
14
  email: yipeng.sun@fau.de
15
15
  alias: sypsyp97
16
- version: 2.1.0
17
- date-released: "2026-10-10"
16
+ version: 2.2.1
17
+ date-released: "2026-10-13"
18
18
  license: Apache-2.0
19
19
  repository-code: "https://github.com/sypsyp97/diffct"
20
20
  url: "https://sypsyp97.github.io/diffct/"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: diffct
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU and multi-node execution, geometry gradients
5
5
  Project-URL: Homepage, https://github.com/sypsyp97/diffct
6
6
  Project-URL: Documentation, https://sypsyp97.github.io/diffct/
@@ -65,8 +65,6 @@ Description-Content-Type: text/markdown
65
65
  <a href="#citation">Citation</a>
66
66
  </p>
67
67
 
68
- > **Install** with the CUDA extra, `pip install "diffct[cu12]"` or `pip install "diffct[cu13]"`, after PyTorch. A plain `pip install diffct` needs a system CUDA Toolkit.
69
- >
70
68
  > **Version 2.0** replaces the circular-orbit API of 1.x with per-view trajectories. Code written for 1.x needs changes; see the [migration notes](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
71
69
  > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
72
70
 
@@ -75,11 +73,11 @@ Description-Content-Type: text/markdown
75
73
  To our knowledge, diffct is the only open-source GPU CT library that combines arbitrary per-view trajectories, autograd through the volume, first-order gradients through the acquisition geometry, and built-in multi-GPU and multi-node execution. We compared it with LEAP, TIGRE, ASTRA/tomosipo, DiffDRR and others in October 2026.
76
74
 
77
75
  - **Arbitrary trajectories.** Each view has its own source, detector centre and detector axes. Circular, helical, saddle, sinusoidal, random and calibrated scans use the same code. Trajectory tensors with `requires_grad=True` receive geometry gradients for calibration.
78
- - **Curved detectors.** A `detector_surface(u, v)` callback defines each pixel's position for arc, cylindrical or other parameterized surfaces. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
76
+ - **Curved detectors.** `ParameterizedSurface` defines native arc, cylindrical or arbitrary pixels from global coordinates and explicit parameters, sampled in bounded view/pixel batches. Legacy `detector_surface(u, v)` callbacks remain available. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
79
77
  - **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
80
- - **Large volumes.** CPU volumes and sinograms automatically stream spatial tiles and view batches through CUDA. Full arrays and data gradients stay on CPU; tile sizes follow the available device memory and can be overridden. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
81
- - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun and NCCL. Views are partitioned. CPU-backed volumes stay in host RAM while GPUs compute tiles; distributed ranks retain replicated-volume semantics. Speedup depends on workload and communication costs.
82
- - **Analytical helpers.** `diffct.analytical` provides ramp filters (ram-lak, shepp-logan, cosine, hamming, hann), fan, cone and Parker weights, and FBP and FDK backprojection.
78
+ - **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
79
+ - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
80
+ - **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
83
81
 
84
82
  Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
85
83
 
@@ -140,7 +138,7 @@ loss.backward() # x.grad = A^T (A x - y)
140
138
 
141
139
  **Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
142
140
 
143
- **Several GPUs and nodes.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun for one or more nodes. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
141
+ **Several GPUs and nodes.** See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
144
142
 
145
143
  ```python
146
144
  A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
@@ -161,29 +159,36 @@ sinogram.square().mean().backward() # CPU volume.grad
161
159
  Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
162
160
  CUDA allocation. Chunking preserves the Siddon model, with float32 summation
163
161
  differences; smaller blocks can increase transfer and launch overhead. Full
164
- arrays and iterative state still need enough host RAM.
162
+ tensor arrays and iterative state still need enough host RAM. For disk-backed
163
+ state and sharded checkpoints, run
164
+ `python examples/disk_reconstruction.py --output disk-run`; for fixed rank-local
165
+ volume slabs use `--partition space` under torchrun. Run
166
+ `python examples/benchmark_execution.py` to compare schedules and copy payloads.
165
167
 
166
168
  ## Performance
167
169
 
168
170
  <p align="center">
169
171
  <picture>
170
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
171
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 64^3 and 128^3">
172
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.2.0">
173
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.2.0" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
172
174
  </picture>
173
175
  </p>
174
176
 
175
- | 128³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
177
+ | 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
176
178
  |---|---:|---:|---:|
177
- | Forward projection | 8.45 ms | 2.66 ms (3.2×) | 1.68 ms (5.0×) |
178
- | Adjoint (backprojection) | 20.66 ms | 5.89 ms (3.5×) | 4.62 ms (4.5×) |
179
- | CGLS iteration | 30.08 ms | 8.08 ms (3.7×) | 5.59 ms (5.4×) |
179
+ | Forward projection | 493.05 ms | 132.22 ms (3.7×) | 73.18 ms (6.7×) |
180
+ | Adjoint (backprojection) | 1400.80 ms | 369.29 ms (3.8×) | 265.87 ms (5.3×) |
181
+ | CGLS iteration | 2054.36 ms | 547.06 ms (3.8×) | 373.38 ms (5.5×) |
180
182
 
181
- Circular trajectory, 360 views, detector (2n, 1.5n) cells at pitch 1.25, A100 64 GB GPUs. Small volumes scale less: at 64³, one CGLS iteration takes 4.63 ms on 1 GPU, 1.57 ms on 4 and 1.56 ms on 8. Raw data: [docs/assets/scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
183
+ diffct 2.2.0, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
182
184
 
183
- To measure one against several GPUs on your machine (default scan of the script, not the table setup):
185
+ 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. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
186
+
187
+ Run the same workload:
184
188
 
185
189
  ```bash
186
- python examples/benchmark_projector.py --devices 0 1
190
+ python examples/benchmark_scaling.py --output one.json
191
+ torchrun --standalone --nproc-per-node=4 examples/benchmark_scaling.py --expected-nodes 1 --output four.json
187
192
  ```
188
193
 
189
194
  ## Gallery
@@ -215,7 +220,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
215
220
 
216
221
  ## Examples
217
222
 
218
- `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `analytical_reconstruction.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
223
+ `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
219
224
 
220
225
  ## Documentation
221
226
 
@@ -34,8 +34,6 @@
34
34
  <a href="#citation">Citation</a>
35
35
  </p>
36
36
 
37
- > **Install** with the CUDA extra, `pip install "diffct[cu12]"` or `pip install "diffct[cu13]"`, after PyTorch. A plain `pip install diffct` needs a system CUDA Toolkit.
38
- >
39
37
  > **Version 2.0** replaces the circular-orbit API of 1.x with per-view trajectories. Code written for 1.x needs changes; see the [migration notes](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
40
38
  > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
41
39
 
@@ -44,11 +42,11 @@
44
42
  To our knowledge, diffct is the only open-source GPU CT library that combines arbitrary per-view trajectories, autograd through the volume, first-order gradients through the acquisition geometry, and built-in multi-GPU and multi-node execution. We compared it with LEAP, TIGRE, ASTRA/tomosipo, DiffDRR and others in October 2026.
45
43
 
46
44
  - **Arbitrary trajectories.** Each view has its own source, detector centre and detector axes. Circular, helical, saddle, sinusoidal, random and calibrated scans use the same code. Trajectory tensors with `requires_grad=True` receive geometry gradients for calibration.
47
- - **Curved detectors.** A `detector_surface(u, v)` callback defines each pixel's position for arc, cylindrical or other parameterized surfaces. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
45
+ - **Curved detectors.** `ParameterizedSurface` defines native arc, cylindrical or arbitrary pixels from global coordinates and explicit parameters, sampled in bounded view/pixel batches. Legacy `detector_surface(u, v)` callbacks remain available. Projection and matched backprojection support first-order surface gradients. See [the surface API](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#parameterized-detector-surfaces).
48
46
  - **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
49
- - **Large volumes.** CPU volumes and sinograms automatically stream spatial tiles and view batches through CUDA. Full arrays and data gradients stay on CPU; tile sizes follow the available device memory and can be overridden. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
50
- - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun and NCCL. Views are partitioned. CPU-backed volumes stay in host RAM while GPUs compute tiles; distributed ranks retain replicated-volume semantics. Speedup depends on workload and communication costs.
51
- - **Analytical helpers.** `diffct.analytical` provides ramp filters (ram-lak, shepp-logan, cosine, hamming, hann), fan, cone and Parker weights, and FBP and FDK backprojection.
47
+ - **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
48
+ - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
49
+ - **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
52
50
 
53
51
  Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
54
52
 
@@ -109,7 +107,7 @@ loss.backward() # x.grad = A^T (A x - y)
109
107
 
110
108
  **Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
111
109
 
112
- **Several GPUs and nodes.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun for one or more nodes. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
110
+ **Several GPUs and nodes.** See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md).
113
111
 
114
112
  ```python
115
113
  A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
@@ -130,29 +128,36 @@ sinogram.square().mean().backward() # CPU volume.grad
130
128
  Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
131
129
  CUDA allocation. Chunking preserves the Siddon model, with float32 summation
132
130
  differences; smaller blocks can increase transfer and launch overhead. Full
133
- arrays and iterative state still need enough host RAM.
131
+ tensor arrays and iterative state still need enough host RAM. For disk-backed
132
+ state and sharded checkpoints, run
133
+ `python examples/disk_reconstruction.py --output disk-run`; for fixed rank-local
134
+ volume slabs use `--partition space` under torchrun. Run
135
+ `python examples/benchmark_execution.py` to compare schedules and copy payloads.
134
136
 
135
137
  ## Performance
136
138
 
137
139
  <p align="center">
138
140
  <picture>
139
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
140
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 64^3 and 128^3">
141
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.2.0">
142
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.2.0" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
141
143
  </picture>
142
144
  </p>
143
145
 
144
- | 128³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
146
+ | 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
145
147
  |---|---:|---:|---:|
146
- | Forward projection | 8.45 ms | 2.66 ms (3.2×) | 1.68 ms (5.0×) |
147
- | Adjoint (backprojection) | 20.66 ms | 5.89 ms (3.5×) | 4.62 ms (4.5×) |
148
- | CGLS iteration | 30.08 ms | 8.08 ms (3.7×) | 5.59 ms (5.4×) |
148
+ | Forward projection | 493.05 ms | 132.22 ms (3.7×) | 73.18 ms (6.7×) |
149
+ | Adjoint (backprojection) | 1400.80 ms | 369.29 ms (3.8×) | 265.87 ms (5.3×) |
150
+ | CGLS iteration | 2054.36 ms | 547.06 ms (3.8×) | 373.38 ms (5.5×) |
149
151
 
150
- Circular trajectory, 360 views, detector (2n, 1.5n) cells at pitch 1.25, A100 64 GB GPUs. Small volumes scale less: at 64³, one CGLS iteration takes 4.63 ms on 1 GPU, 1.57 ms on 4 and 1.56 ms on 8. Raw data: [docs/assets/scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
152
+ diffct 2.2.0, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
151
153
 
152
- To measure one against several GPUs on your machine (default scan of the script, not the table setup):
154
+ 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. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
155
+
156
+ Run the same workload:
153
157
 
154
158
  ```bash
155
- python examples/benchmark_projector.py --devices 0 1
159
+ python examples/benchmark_scaling.py --output one.json
160
+ torchrun --standalone --nproc-per-node=4 examples/benchmark_scaling.py --expected-nodes 1 --output four.json
156
161
  ```
157
162
 
158
163
  ## Gallery
@@ -184,7 +189,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
184
189
 
185
190
  ## Examples
186
191
 
187
- `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `analytical_reconstruction.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
192
+ `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
188
193
 
189
194
  ## Documentation
190
195
 
@@ -15,6 +15,15 @@ from .projectors import (
15
15
  )
16
16
 
17
17
  from .operators import Projector
18
+ from .storage import TensorStore, NpyStore
19
+ from .surfaces import ParameterizedSurface
20
+ from .reconstruction import (
21
+ fbp,
22
+ fdk,
23
+ helical_fdk,
24
+ helical_view_weights,
25
+ helical_weighted_backproject,
26
+ )
18
27
 
19
28
  from .geometry import (
20
29
  circular_trajectory_3d,
@@ -49,10 +58,18 @@ try:
49
58
  except ImportError:
50
59
  pass
51
60
 
52
- __version__ = '2.1.0'
61
+ __version__ = '2.2.1'
53
62
 
54
63
  __all__ = [
55
64
  'Projector',
65
+ 'TensorStore',
66
+ 'NpyStore',
67
+ 'ParameterizedSurface',
68
+ 'fbp',
69
+ 'fdk',
70
+ 'helical_fdk',
71
+ 'helical_view_weights',
72
+ 'helical_weighted_backproject',
56
73
  'ParallelProjectorFunction',
57
74
  'ParallelBackprojectorFunction',
58
75
  'FanProjectorFunction',
@@ -40,6 +40,9 @@ from .kernels import (
40
40
  _parallel_2d_fbp_backproject_kernel,
41
41
  _fan_2d_fbp_backproject_kernel,
42
42
  _cone_3d_fdk_backproject_kernel,
43
+ _parallel_2d_fbp_transpose_kernel,
44
+ _fan_2d_fbp_transpose_kernel,
45
+ _cone_3d_fdk_transpose_kernel,
43
46
  )
44
47
 
45
48
 
@@ -367,8 +370,8 @@ def _as_contig_f32(t, device):
367
370
 
368
371
 
369
372
  @_on_device_of("sinogram")
370
- def parallel_weighted_backproject(sinogram, ray_dir, det_origin, det_u_vec,
371
- detector_spacing, H, W, voxel_spacing=1.0):
373
+ def _parallel_weighted_backproject_raw(sinogram, ray_dir, det_origin, det_u_vec,
374
+ detector_spacing, H, W, voxel_spacing=1.0):
372
375
  """Voxel-driven parallel-beam FBP backprojection with analytical constant.
373
376
 
374
377
  Runs the dedicated parallel-beam FBP gather kernel (one thread per
@@ -451,8 +454,8 @@ def _analytical_geometry(src_pos, det_center, n, isocenter):
451
454
 
452
455
 
453
456
  @_on_device_of("sinogram")
454
- def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
455
- detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
457
+ def _fan_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec,
458
+ detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
456
459
  """Voxel-driven fan-beam FBP backprojection with analytical constant.
457
460
 
458
461
  Runs the dedicated fan-beam FBP gather kernel (one thread per output
@@ -507,8 +510,8 @@ def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
507
510
 
508
511
 
509
512
  @_on_device_of("sinogram")
510
- def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
511
- D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
513
+ def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
514
+ D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
512
515
  """Voxel-driven cone-beam FDK backprojection with analytical constant.
513
516
 
514
517
  Runs the dedicated cone-beam FDK gather kernel (one thread per output
@@ -571,3 +574,145 @@ def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_ve
571
574
  # Convert back to (D, H, W) layout for the user.
572
575
  reco = reco_perm.permute(2, 1, 0).contiguous() * scale
573
576
  return reco
577
+
578
+
579
+ def _validate_analytical_inputs(sinogram, geometry, ndim, isocenter=None):
580
+ if not isinstance(sinogram, torch.Tensor) or sinogram.ndim != ndim:
581
+ raise ValueError(f"sinogram must have rank {ndim}")
582
+ if min(sinogram.shape[1:]) < 2:
583
+ raise ValueError("Analytical backprojection requires at least two detector bins on each axis")
584
+ for component in geometry:
585
+ if not isinstance(component, torch.Tensor) or component.shape != (sinogram.shape[0], ndim):
586
+ raise ValueError(f"Every geometry tensor must have shape (n_views, {ndim})")
587
+ if component.requires_grad:
588
+ raise ValueError("Analytical backprojection does not support geometry gradients")
589
+ if isinstance(isocenter, torch.Tensor) and isocenter.requires_grad:
590
+ raise ValueError("Analytical backprojection does not support isocenter gradients")
591
+ if not sinogram.is_cuda:
592
+ raise ValueError("sinogram must be on a CUDA device")
593
+
594
+
595
+ def _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter):
596
+ if beam == "parallel":
597
+ return _parallel_weighted_backproject_raw(sinogram, *geometry, *parameters)
598
+ if beam == "fan":
599
+ return _fan_weighted_backproject_raw(sinogram, *geometry, *parameters, isocenter=isocenter)
600
+ return _cone_weighted_backproject_raw(sinogram, *geometry, *parameters, isocenter=isocenter)
601
+
602
+
603
+ @_on_device_of("image")
604
+ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sinogram_shape):
605
+ device = image.device
606
+ geometry = tuple(_as_contig_f32(component, device) for component in geometry)
607
+ image = _as_contig_f32(image, device)
608
+ sinogram = torch.zeros(sinogram_shape, device=device, dtype=torch.float32)
609
+ d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
610
+ d_geometry = tuple(TorchCUDABridge.tensor_to_cuda_array(component) for component in geometry)
611
+ stream = _get_numba_external_stream_for(torch.cuda.current_stream())
612
+
613
+ scale = 1.0 / (2.0 * math.pi)
614
+ iso = ()
615
+ if beam != "parallel":
616
+ source, detector, u = geometry[:3]
617
+ normal = (torch.stack((-u[:, 1], u[:, 0]), dim=1) if beam == "fan"
618
+ else torch.cross(u, geometry[3], dim=1))
619
+ isocenter, sid_mean, sdd_mean = _analytical_geometry(source, detector, normal, isocenter)
620
+ scale = sdd_mean / (2.0 * math.pi * sid_mean)
621
+ iso = tuple(_DTYPE(value.item() / parameters[-1]) for value in isocenter)
622
+ image = image * scale
623
+
624
+ if beam == "cone":
625
+ D, H, W, du, dv, spacing = parameters
626
+ image = image.permute(2, 1, 0).contiguous()
627
+ d_image = TorchCUDABridge.tensor_to_cuda_array(image)
628
+ grid, tpb = _grid_3d(D, H, W)
629
+ _cone_3d_fdk_transpose_kernel[grid, tpb, stream](
630
+ d_sino, *sinogram_shape, d_image, W, H, D, _DTYPE(du), _DTYPE(dv),
631
+ *d_geometry, _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5),
632
+ _DTYPE(spacing), *iso,
633
+ )
634
+ else:
635
+ pitch, H, W, spacing = parameters
636
+ d_image = TorchCUDABridge.tensor_to_cuda_array(image)
637
+ grid, tpb = _grid_2d(W, H)
638
+ kernel = (_parallel_2d_fbp_transpose_kernel if beam == "parallel"
639
+ else _fan_2d_fbp_transpose_kernel)
640
+ kernel[grid, tpb, stream](
641
+ d_sino, *sinogram_shape, d_image, W, H, _DTYPE(pitch), *d_geometry,
642
+ _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(spacing), *iso,
643
+ )
644
+ return sinogram
645
+
646
+
647
+ class _AnalyticalGather(torch.autograd.Function):
648
+ @staticmethod
649
+ def forward(ctx, sinogram, beam, geometry, parameters, isocenter):
650
+ # Backward uses the exact geometry observed by forward, even if the
651
+ # caller edits its fixed trajectory or isocenter afterwards.
652
+ geometry = tuple(component.detach().clone() for component in geometry)
653
+ if isocenter is not None:
654
+ isocenter = torch.as_tensor(isocenter, device=sinogram.device,
655
+ dtype=torch.float32).detach().clone()
656
+ ctx.save_for_backward(*geometry, isocenter)
657
+ ctx.beam, ctx.parameters = beam, parameters
658
+ ctx.input_shape, ctx.input_dtype, ctx.input_device = sinogram.shape, sinogram.dtype, sinogram.device
659
+ return _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter)
660
+
661
+ @staticmethod
662
+ def backward(ctx, grad_output):
663
+ *geometry, isocenter = ctx.saved_tensors
664
+ grad_input = _AnalyticalTranspose.apply(
665
+ grad_output, ctx.beam, tuple(geometry), ctx.parameters, isocenter, ctx.input_shape,
666
+ ).to(device=ctx.input_device, dtype=ctx.input_dtype)
667
+ return grad_input, None, None, None, None
668
+
669
+
670
+ class _AnalyticalTranspose(torch.autograd.Function):
671
+ @staticmethod
672
+ def forward(ctx, image, beam, geometry, parameters, isocenter, sinogram_shape):
673
+ ctx.beam, ctx.geometry, ctx.parameters, ctx.isocenter = beam, geometry, parameters, isocenter
674
+ ctx.input_dtype, ctx.input_device = image.dtype, image.device
675
+ return _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sinogram_shape)
676
+
677
+ @staticmethod
678
+ def backward(ctx, grad_output):
679
+ grad_input = _AnalyticalGather.apply(
680
+ grad_output, ctx.beam, ctx.geometry, ctx.parameters, ctx.isocenter,
681
+ ).to(device=ctx.input_device, dtype=ctx.input_dtype)
682
+ return grad_input, None, None, None, None, None
683
+
684
+
685
+ def _analytical_backproject(sinogram, beam, geometry, parameters, isocenter=None):
686
+ _validate_analytical_inputs(sinogram, geometry, 3 if beam == "cone" else 2, isocenter)
687
+ if torch.is_grad_enabled() and sinogram.requires_grad:
688
+ return _AnalyticalGather.apply(sinogram, beam, geometry, parameters, isocenter)
689
+ return _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter)
690
+
691
+
692
+ def parallel_weighted_backproject(sinogram, ray_dir, det_origin, det_u_vec,
693
+ detector_spacing, H, W, voxel_spacing=1.0):
694
+ return _analytical_backproject(
695
+ sinogram, "parallel", (ray_dir, det_origin, det_u_vec),
696
+ (detector_spacing, H, W, voxel_spacing),
697
+ )
698
+
699
+
700
+ def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
701
+ detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
702
+ return _analytical_backproject(
703
+ sinogram, "fan", (src_pos, det_center, det_u_vec),
704
+ (detector_spacing, H, W, voxel_spacing), isocenter,
705
+ )
706
+
707
+
708
+ def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
709
+ D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
710
+ return _analytical_backproject(
711
+ sinogram, "cone", (src_pos, det_center, det_u_vec, det_v_vec),
712
+ (D, H, W, du, dv, voxel_spacing), isocenter,
713
+ )
714
+
715
+
716
+ parallel_weighted_backproject.__doc__ = _parallel_weighted_backproject_raw.__doc__
717
+ fan_weighted_backproject.__doc__ = _fan_weighted_backproject_raw.__doc__
718
+ cone_weighted_backproject.__doc__ = _cone_weighted_backproject_raw.__doc__