diffct 2.0.2__tar.gz → 2.1.0__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 (63) hide show
  1. {diffct-2.0.2 → diffct-2.1.0}/CHANGELOG.md +29 -0
  2. {diffct-2.0.2 → diffct-2.1.0}/CITATION.cff +2 -2
  3. {diffct-2.0.2 → diffct-2.1.0}/PKG-INFO +28 -7
  4. {diffct-2.0.2 → diffct-2.1.0}/README.md +27 -6
  5. {diffct-2.0.2 → diffct-2.1.0}/diffct/__init__.py +1 -1
  6. diffct-2.1.0/diffct/chunking.py +73 -0
  7. {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/cone_beam.py +46 -36
  8. {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/fan_beam.py +30 -22
  9. {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/geometry_vjp.py +77 -48
  10. {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/parallel_beam.py +32 -22
  11. diffct-2.1.0/diffct/operators.py +1033 -0
  12. {diffct-2.0.2 → diffct-2.1.0}/diffct/projectors.py +143 -18
  13. {diffct-2.0.2 → diffct-2.1.0}/diffct/utils.py +5 -0
  14. diffct-2.1.0/docs/video/README.md +119 -0
  15. {diffct-2.0.2 → diffct-2.1.0}/examples/README.md +17 -2
  16. {diffct-2.0.2 → diffct-2.1.0}/formal/README.md +41 -9
  17. {diffct-2.0.2 → diffct-2.1.0}/pyproject.toml +1 -1
  18. diffct-2.1.0/tests/test_chunked_projector.py +1202 -0
  19. diffct-2.1.0/tests/test_detector_surface_dtypes.py +80 -0
  20. diffct-2.1.0/tests/test_detector_surface_mapping.py +79 -0
  21. diffct-2.1.0/tests/test_detector_surface_units.py +62 -0
  22. diffct-2.1.0/tests/test_detector_surfaces.py +705 -0
  23. {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_api.py +9 -3
  24. diffct-2.0.2/diffct/operators.py +0 -555
  25. diffct-2.0.2/docs/video/README.md +0 -47
  26. {diffct-2.0.2 → diffct-2.1.0}/.gitignore +0 -0
  27. {diffct-2.0.2 → diffct-2.1.0}/LICENSE +0 -0
  28. {diffct-2.0.2 → diffct-2.1.0}/NOTICE +0 -0
  29. {diffct-2.0.2 → diffct-2.1.0}/diffct/analytical.py +0 -0
  30. {diffct-2.0.2 → diffct-2.1.0}/diffct/constants.py +0 -0
  31. {diffct-2.0.2 → diffct-2.1.0}/diffct/differentiable.py +0 -0
  32. {diffct-2.0.2 → diffct-2.1.0}/diffct/geometry.py +0 -0
  33. {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/__init__.py +0 -0
  34. {diffct-2.0.2 → diffct-2.1.0}/examples/data/NOTICE +0 -0
  35. {diffct-2.0.2 → diffct-2.1.0}/pytest.ini +0 -0
  36. {diffct-2.0.2 → diffct-2.1.0}/tests/__init__.py +0 -0
  37. {diffct-2.0.2 → diffct-2.1.0}/tests/_phantoms.py +0 -0
  38. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/README.md +0 -0
  39. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/__init__.py +0 -0
  40. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/_common.py +0 -0
  41. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_cone.py +0 -0
  42. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_fan.py +0 -0
  43. {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_parallel.py +0 -0
  44. {diffct-2.0.2 → diffct-2.1.0}/tests/distributed_projector_check.py +0 -0
  45. {diffct-2.0.2 → diffct-2.1.0}/tests/test_adjoint_inner_product.py +0 -0
  46. {diffct-2.0.2 → diffct-2.1.0}/tests/test_analytical_regressions.py +0 -0
  47. {diffct-2.0.2 → diffct-2.1.0}/tests/test_cone_projector_autograd.py +0 -0
  48. {diffct-2.0.2 → diffct-2.1.0}/tests/test_cuda_exit_deallocations.py +0 -0
  49. {diffct-2.0.2 → diffct-2.1.0}/tests/test_cuda_smoke.py +0 -0
  50. {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_fan_accuracy.py +0 -0
  51. {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_fan_offsets.py +0 -0
  52. {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_parallel_accuracy.py +0 -0
  53. {diffct-2.0.2 → diffct-2.1.0}/tests/test_fdk_cone_accuracy.py +0 -0
  54. {diffct-2.0.2 → diffct-2.1.0}/tests/test_fdk_cone_offsets.py +0 -0
  55. {diffct-2.0.2 → diffct-2.1.0}/tests/test_geometry_autograd.py +0 -0
  56. {diffct-2.0.2 → diffct-2.1.0}/tests/test_geometry_gradients.py +0 -0
  57. {diffct-2.0.2 → diffct-2.1.0}/tests/test_gradcheck.py +0 -0
  58. {diffct-2.0.2 → diffct-2.1.0}/tests/test_multi_gpu_exit.py +0 -0
  59. {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_degenerate_geometry.py +0 -0
  60. {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_geometry_devices.py +0 -0
  61. {diffct-2.0.2 → diffct-2.1.0}/tests/test_ramp_filter_windows.py +0 -0
  62. {diffct-2.0.2 → diffct-2.1.0}/tests/test_release_2_0_1.py +0 -0
  63. {diffct-2.0.2 → diffct-2.1.0}/tests/test_weights.py +0 -0
@@ -5,6 +5,35 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [2.1.0] - 2026-10-10
11
+
12
+ ### Added
13
+
14
+ - `Projector(..., detector_surface=surface)` samples local parameterized surfaces
15
+ into per-pixel world positions for native parallel, fan and cone projection
16
+ and matched backprojection. Shared or per-view surfaces can contain trainable
17
+ PyTorch parameters. Flat detectors remain the default; analytical helpers
18
+ retain their flat-detector assumptions.
19
+ - CUDA regression tests and CPU formal verification for detector
20
+ surfaces, including bounded production-kernel checks against cell intersections.
21
+ - Automatic CPU-backed spatial tiles and view batches for native `Projector`
22
+ projection, matched backprojection, data autograd/Hessians and first-order
23
+ geometry gradients. CUDA inputs retain the full-volume path when its estimated
24
+ working set fits. Optional `volume_chunk_shape` and `view_chunk_size` override
25
+ automatic limits; full CPU arrays are not staged on CUDA. Several local
26
+ devices compute their view batches concurrently.
27
+ - A CPU-backed CGLS example and chunk-boundary, CUDA memory and
28
+ formal partition/translation/adjoint checks.
29
+
30
+ ### Fixed
31
+
32
+ - The exit-time cleanup hook returns early under the CUDA simulator
33
+ (``NUMBA_ENABLE_CUDASIM=1``). Before, each simulator process printed an
34
+ ``ImportError`` for ``_PendingDeallocs`` at exit. The simulator loads no
35
+ CUDA modules, so it needs no cleanup.
36
+
8
37
  ## [2.0.2] - 2026-10-09
9
38
 
10
39
  ### Fixed
@@ -13,8 +13,8 @@ authors:
13
13
  given-names: Yipeng
14
14
  email: yipeng.sun@fau.de
15
15
  alias: sypsyp97
16
- version: 2.0.2
17
- date-released: "2026-10-09"
16
+ version: 2.1.0
17
+ date-released: "2026-10-10"
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.0.2
3
+ Version: 2.1.0
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/
@@ -47,11 +47,11 @@ Description-Content-Type: text/markdown
47
47
  </p>
48
48
 
49
49
  <p align="center">
50
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
50
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/edf7c52227ec2cc14c988918faa7313f8ee0fe03/docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
51
51
  </p>
52
52
 
53
53
  <p align="center">
54
- <a href="https://github.com/sypsyp97/diffct/blob/main/docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
54
+ <a href="https://github.com/sypsyp97/diffct/blob/edf7c52227ec2cc14c988918faa7313f8ee0fe03/docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
55
55
  <a href="https://www.preprints.org/manuscript/202605.1446/v1">Technical report</a> ·
56
56
  <a href="https://doi.org/10.20944/preprints202605.1446.v1">DOI</a>
57
57
  </p>
@@ -75,8 +75,10 @@ Description-Content-Type: text/markdown
75
75
  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
76
 
77
77
  - **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).
78
79
  - **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.
79
- - **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 and the volume is replicated. Speedup depends on workload and communication costs.
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.
80
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.
81
83
 
82
84
  Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
@@ -144,6 +146,23 @@ loss.backward() # x.grad = A^T (A x - y)
144
146
  A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
145
147
  ```
146
148
 
149
+ **Volumes larger than GPU memory.** Keep the arrays on CPU. The same calls use
150
+ CUDA tiles automatically; CUDA inputs retain the full-volume path when its
151
+ estimated working set fits. Explicit limits are optional:
152
+
153
+ ```python
154
+ A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8)
155
+ volume = torch.ones(A.volume_shape, device="cpu", requires_grad=True)
156
+ sinogram = A.project(volume) # CPU output, CUDA computation
157
+ sinogram.square().mean().backward() # CPU volume.grad
158
+ # Optional: volume_chunk_shape=(32, 64, 64), view_chunk_size=16
159
+ ```
160
+
161
+ Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
162
+ CUDA allocation. Chunking preserves the Siddon model, with float32 summation
163
+ differences; smaller blocks can increase transfer and launch overhead. Full
164
+ arrays and iterative state still need enough host RAM.
165
+
147
166
  ## Performance
148
167
 
149
168
  <p align="center">
@@ -169,10 +188,12 @@ python examples/benchmark_projector.py --devices 0 1
169
188
 
170
189
  ## Gallery
171
190
 
191
+ TV denotes TV-regularized iterative reconstruction; Adam is the optimizer. Figure labels distinguish the method from its optimizer.
192
+
172
193
  **Measured walnut.** 240 measured views, circular cone beam, 256³: FDK (Hann window), SIRT (200 iterations), CGLS (20), TV (300, weight 0.3); axial and coronal centre slices.
173
194
 
174
195
  <p align="center">
175
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_measured.png" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
196
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_measured.png?v=e0092c4" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
176
197
  </p>
177
198
 
178
199
  ```bash
@@ -182,7 +203,7 @@ python examples/walnut_reconstruction.py --figure out.png
182
203
  **Simulated helical scan of the walnut.** The FDK walnut volume as ground truth, 720 views, 1% noise, 256³. PSNR: FDK 26.67 dB, CGLS (30 iterations) 34.13 dB, SIRT (200) 34.52 dB, TV (200, weight 1.0) 37.77 dB.
183
204
 
184
205
  <p align="center">
185
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_helical.png" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
206
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_helical.png?v=e0092c4" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
186
207
  </p>
187
208
 
188
209
  ```bash
@@ -194,7 +215,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
194
215
 
195
216
  ## Examples
196
217
 
197
- `quickstart.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).
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).
198
219
 
199
220
  ## Documentation
200
221
 
@@ -16,11 +16,11 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
19
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/edf7c52227ec2cc14c988918faa7313f8ee0fe03/docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
20
20
  </p>
21
21
 
22
22
  <p align="center">
23
- <a href="https://github.com/sypsyp97/diffct/blob/main/docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
23
+ <a href="https://github.com/sypsyp97/diffct/blob/edf7c52227ec2cc14c988918faa7313f8ee0fe03/docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
24
24
  <a href="https://www.preprints.org/manuscript/202605.1446/v1">Technical report</a> ·
25
25
  <a href="https://doi.org/10.20944/preprints202605.1446.v1">DOI</a>
26
26
  </p>
@@ -44,8 +44,10 @@
44
44
  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
45
 
46
46
  - **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).
47
48
  - **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.
48
- - **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 and the volume is replicated. Speedup depends on workload and communication costs.
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.
49
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.
50
52
 
51
53
  Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
@@ -113,6 +115,23 @@ loss.backward() # x.grad = A^T (A x - y)
113
115
  A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
114
116
  ```
115
117
 
118
+ **Volumes larger than GPU memory.** Keep the arrays on CPU. The same calls use
119
+ CUDA tiles automatically; CUDA inputs retain the full-volume path when its
120
+ estimated working set fits. Explicit limits are optional:
121
+
122
+ ```python
123
+ A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8)
124
+ volume = torch.ones(A.volume_shape, device="cpu", requires_grad=True)
125
+ sinogram = A.project(volume) # CPU output, CUDA computation
126
+ sinogram.square().mean().backward() # CPU volume.grad
127
+ # Optional: volume_chunk_shape=(32, 64, 64), view_chunk_size=16
128
+ ```
129
+
130
+ Run `python examples/chunked_reconstruction.py` for CPU-backed CGLS and measured
131
+ CUDA allocation. Chunking preserves the Siddon model, with float32 summation
132
+ differences; smaller blocks can increase transfer and launch overhead. Full
133
+ arrays and iterative state still need enough host RAM.
134
+
116
135
  ## Performance
117
136
 
118
137
  <p align="center">
@@ -138,10 +157,12 @@ python examples/benchmark_projector.py --devices 0 1
138
157
 
139
158
  ## Gallery
140
159
 
160
+ TV denotes TV-regularized iterative reconstruction; Adam is the optimizer. Figure labels distinguish the method from its optimizer.
161
+
141
162
  **Measured walnut.** 240 measured views, circular cone beam, 256³: FDK (Hann window), SIRT (200 iterations), CGLS (20), TV (300, weight 0.3); axial and coronal centre slices.
142
163
 
143
164
  <p align="center">
144
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_measured.png" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
165
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_measured.png?v=e0092c4" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
145
166
  </p>
146
167
 
147
168
  ```bash
@@ -151,7 +172,7 @@ python examples/walnut_reconstruction.py --figure out.png
151
172
  **Simulated helical scan of the walnut.** The FDK walnut volume as ground truth, 720 views, 1% noise, 256³. PSNR: FDK 26.67 dB, CGLS (30 iterations) 34.13 dB, SIRT (200) 34.52 dB, TV (200, weight 1.0) 37.77 dB.
152
173
 
153
174
  <p align="center">
154
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_helical.png" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
175
+ <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_helical.png?v=e0092c4" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
155
176
  </p>
156
177
 
157
178
  ```bash
@@ -163,7 +184,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
163
184
 
164
185
  ## Examples
165
186
 
166
- `quickstart.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).
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).
167
188
 
168
189
  ## Documentation
169
190
 
@@ -49,7 +49,7 @@ try:
49
49
  except ImportError:
50
50
  pass
51
51
 
52
- __version__ = '2.0.2'
52
+ __version__ = '2.1.0'
53
53
 
54
54
  __all__ = [
55
55
  'Projector',
@@ -0,0 +1,73 @@
1
+ """Spatial slices and immutable execution limits for streamed projectors."""
2
+
3
+ from dataclasses import dataclass
4
+ from itertools import product
5
+ import math
6
+
7
+ import torch
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class _ChunkPlan:
12
+ chunk_shape: tuple
13
+ view_chunk_size: int
14
+ devices: tuple
15
+
16
+
17
+ def _memory_budget(device):
18
+ """Usable allocator bytes, retaining 25% headroom for allocation overhead.
19
+
20
+ Reusable reserved blocks count as available, but never bypass the process
21
+ fraction ceiling. Older PyTorch without its public fraction getter can
22
+ only observe driver/allocator memory; explicit limits remain available.
23
+ """
24
+ free, total = torch.cuda.mem_get_info(device)
25
+ allocated = torch.cuda.memory_allocated(device)
26
+ reusable = max(0, torch.cuda.memory_reserved(device) - allocated)
27
+ getter = getattr(torch.cuda, "get_per_process_memory_fraction", None)
28
+ fraction = getter(device) if getter is not None else 1.0
29
+ allocator_room = max(0, int(total * fraction) - allocated)
30
+ available = max(0, min(free + reusable, allocator_room))
31
+ budget = available * 3 // 4
32
+ allocation_limit = (1 << 63) - 1 # Unbounded limit in scalar int64 agreement.
33
+ backend = getattr(torch.cuda, "get_allocator_backend", lambda: "native")()
34
+ # Native requests >1 MiB and <10 MiB reserve a 20 MiB segment. A smaller
35
+ # current available room must use the <=1 MiB small pool, whose segments are 2 MiB.
36
+ # Source: pytorch/c10/cuda/CUDACachingAllocator.cpp (Native allocator notes).
37
+ if backend == "native" and available < 20 * 1024 ** 2:
38
+ allocation_limit = 1024 ** 2
39
+ return budget, allocation_limit
40
+
41
+
42
+ def _working_set_bytes(beam, shape, views, detector_shape, surface):
43
+ """Conservative native staging/layout, cotangent, geometry and VJP scratch."""
44
+ rank = len(shape)
45
+ voxels = math.prod(shape)
46
+ rays = views * math.prod(detector_shape)
47
+ geometry = (rank * (views + rays) if surface
48
+ else rank * (4 if beam == "cone" else 3) * views)
49
+ # Cone permuted layout and its returned tensor can coexist with staging.
50
+ # Two ray buffers cover native output/cotangent and transfer scratch; two
51
+ # geometry buffers cover staged values and their first-order VJP/reduction.
52
+ tile_buffers = 3 if beam == "cone" else 2
53
+ return 4 * (tile_buffers * voxels + 2 * rays + 2 * geometry)
54
+
55
+
56
+ def _largest_buffer_bytes(beam, shape, views, detector_shape, surface):
57
+ rank = len(shape)
58
+ rays = views * math.prod(detector_shape)
59
+ geometry = (rank * (views + rays) if surface
60
+ else rank * (4 if beam == "cone" else 3) * views)
61
+ return 4 * max(math.prod(shape), rays, geometry)
62
+
63
+
64
+ def _tiles(shape, chunk_shape, spacing):
65
+ """Yield tensor slices, clipped shapes and physical centres in xyz order."""
66
+ starts = (range(0, size, chunk) for size, chunk in zip(shape, chunk_shape))
67
+ for start in product(*starts):
68
+ tile_shape = tuple(min(chunk, size - first)
69
+ for size, chunk, first in zip(shape, chunk_shape, start))
70
+ slices = tuple(slice(first, first + size) for first, size in zip(start, tile_shape))
71
+ centre = tuple((first + size / 2 - full / 2) * spacing
72
+ for first, size, full in zip(start, tile_shape, shape))
73
+ yield slices, tile_shape, centre[::-1]
@@ -30,7 +30,7 @@ def _cone_3d_forward_kernel(
30
30
  d_vol, Nx, Ny, Nz,
31
31
  d_sino, n_views, n_u, n_v,
32
32
  du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
33
- cx, cy, cz, voxel_spacing
33
+ cx, cy, cz, voxel_spacing, d_detector_positions=None,
34
34
  ):
35
35
  """Compute the 3D cone-beam forward projection with arbitrary source-detector trajectories.
36
36
 
@@ -93,27 +93,32 @@ def _cone_3d_forward_kernel(
93
93
  src_y = d_src_pos[iview, 1] / voxel_spacing
94
94
  src_z = d_src_pos[iview, 2] / voxel_spacing
95
95
 
96
- # Read detector center and orientation vectors
97
- det_cx = d_det_center[iview, 0] / voxel_spacing
98
- det_cy = d_det_center[iview, 1] / voxel_spacing
99
- det_cz = d_det_center[iview, 2] / voxel_spacing
96
+ if d_detector_positions is None or d_detector_positions.size == 0:
97
+ # Read detector center and orientation vectors
98
+ det_cx = d_det_center[iview, 0] / voxel_spacing
99
+ det_cy = d_det_center[iview, 1] / voxel_spacing
100
+ det_cz = d_det_center[iview, 2] / voxel_spacing
100
101
 
101
- u_vec_x = d_det_u_vec[iview, 0]
102
- u_vec_y = d_det_u_vec[iview, 1]
103
- u_vec_z = d_det_u_vec[iview, 2]
102
+ u_vec_x = d_det_u_vec[iview, 0]
103
+ u_vec_y = d_det_u_vec[iview, 1]
104
+ u_vec_z = d_det_u_vec[iview, 2]
104
105
 
105
- v_vec_x = d_det_v_vec[iview, 0]
106
- v_vec_y = d_det_v_vec[iview, 1]
107
- v_vec_z = d_det_v_vec[iview, 2]
106
+ v_vec_x = d_det_v_vec[iview, 0]
107
+ v_vec_y = d_det_v_vec[iview, 1]
108
+ v_vec_z = d_det_v_vec[iview, 2]
108
109
 
109
- # Calculate detector element offset from center
110
- u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
111
- v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
110
+ # Calculate detector element offset from center
111
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
112
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
112
113
 
113
- # Calculate 3D detector element position using center + u*u_vec + v*v_vec
114
- det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
115
- det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
116
- det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
114
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
115
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
116
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
117
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
118
+ else:
119
+ det_x = d_detector_positions[iview, iu, iv, 0] / voxel_spacing
120
+ det_y = d_detector_positions[iview, iu, iv, 1] / voxel_spacing
121
+ det_z = d_detector_positions[iview, iu, iv, 2] / voxel_spacing
117
122
 
118
123
  # === 3D RAY DIRECTION CALCULATION ===
119
124
  # Ray direction vector from source to detector element in 3D space
@@ -238,7 +243,7 @@ def _cone_3d_backward_kernel(
238
243
  d_sino, n_views, n_u, n_v,
239
244
  d_vol, Nx, Ny, Nz,
240
245
  du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
241
- cx, cy, cz, voxel_spacing
246
+ cx, cy, cz, voxel_spacing, d_detector_positions=None,
242
247
  ):
243
248
  """Compute the 3D cone-beam backprojection with arbitrary source-detector trajectories.
244
249
 
@@ -305,27 +310,32 @@ def _cone_3d_backward_kernel(
305
310
  src_y = d_src_pos[iview, 1] / voxel_spacing
306
311
  src_z = d_src_pos[iview, 2] / voxel_spacing
307
312
 
308
- # Read detector center and orientation vectors
309
- det_cx = d_det_center[iview, 0] / voxel_spacing
310
- det_cy = d_det_center[iview, 1] / voxel_spacing
311
- det_cz = d_det_center[iview, 2] / voxel_spacing
313
+ if d_detector_positions is None or d_detector_positions.size == 0:
314
+ # Read detector center and orientation vectors
315
+ det_cx = d_det_center[iview, 0] / voxel_spacing
316
+ det_cy = d_det_center[iview, 1] / voxel_spacing
317
+ det_cz = d_det_center[iview, 2] / voxel_spacing
312
318
 
313
- u_vec_x = d_det_u_vec[iview, 0]
314
- u_vec_y = d_det_u_vec[iview, 1]
315
- u_vec_z = d_det_u_vec[iview, 2]
319
+ u_vec_x = d_det_u_vec[iview, 0]
320
+ u_vec_y = d_det_u_vec[iview, 1]
321
+ u_vec_z = d_det_u_vec[iview, 2]
316
322
 
317
- v_vec_x = d_det_v_vec[iview, 0]
318
- v_vec_y = d_det_v_vec[iview, 1]
319
- v_vec_z = d_det_v_vec[iview, 2]
323
+ v_vec_x = d_det_v_vec[iview, 0]
324
+ v_vec_y = d_det_v_vec[iview, 1]
325
+ v_vec_z = d_det_v_vec[iview, 2]
320
326
 
321
- # Calculate detector element offset from center
322
- u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
323
- v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
327
+ # Calculate detector element offset from center
328
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
329
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
324
330
 
325
- # Calculate 3D detector element position using center + u*u_vec + v*v_vec
326
- det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
327
- det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
328
- det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
331
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
332
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
333
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
334
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
335
+ else:
336
+ det_x = d_detector_positions[iview, iu, iv, 0] / voxel_spacing
337
+ det_y = d_detector_positions[iview, iu, iv, 1] / voxel_spacing
338
+ det_z = d_detector_positions[iview, iu, iv, 2] / voxel_spacing
329
339
 
330
340
  # === 3D RAY DIRECTION CALCULATION ===
331
341
  # Ray direction vector from source to detector element in 3D space
@@ -30,7 +30,7 @@ def _fan_2d_forward_kernel(
30
30
  d_image, Nx, Ny,
31
31
  d_sino, n_ang, n_det,
32
32
  det_spacing, d_src_pos, d_det_center, d_det_u_vec,
33
- cx, cy, voxel_spacing
33
+ cx, cy, voxel_spacing, d_detector_positions=None,
34
34
  ):
35
35
  """Compute the 2D fan beam forward projection with arbitrary source-detector trajectories.
36
36
 
@@ -82,19 +82,23 @@ def _fan_2d_forward_kernel(
82
82
  src_x = d_src_pos[iang, 0] / voxel_spacing
83
83
  src_y = d_src_pos[iang, 1] / voxel_spacing
84
84
 
85
- # Read detector center and orientation vector
86
- det_cx = d_det_center[iang, 0] / voxel_spacing
87
- det_cy = d_det_center[iang, 1] / voxel_spacing
85
+ if d_detector_positions is None or d_detector_positions.size == 0:
86
+ # Read detector center and orientation vector
87
+ det_cx = d_det_center[iang, 0] / voxel_spacing
88
+ det_cy = d_det_center[iang, 1] / voxel_spacing
88
89
 
89
- u_vec_x = d_det_u_vec[iang, 0]
90
- u_vec_y = d_det_u_vec[iang, 1]
90
+ u_vec_x = d_det_u_vec[iang, 0]
91
+ u_vec_y = d_det_u_vec[iang, 1]
91
92
 
92
- # Calculate detector element offset from center
93
- u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
93
+ # Calculate detector element offset from center
94
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
94
95
 
95
- # Calculate 2D detector element position using center + u*u_vec
96
- det_x = det_cx + u_offset * u_vec_x
97
- det_y = det_cy + u_offset * u_vec_y
96
+ # Calculate 2D detector element position using center + u*u_vec
97
+ det_x = det_cx + u_offset * u_vec_x
98
+ det_y = det_cy + u_offset * u_vec_y
99
+ else:
100
+ det_x = d_detector_positions[iang, idet, 0] / voxel_spacing
101
+ det_y = d_detector_positions[iang, idet, 1] / voxel_spacing
98
102
 
99
103
  # === RAY DIRECTION CALCULATION ===
100
104
  # Ray direction vector from source to detector element
@@ -192,7 +196,7 @@ def _fan_2d_backward_kernel(
192
196
  d_sino, n_ang, n_det,
193
197
  d_image, Nx, Ny,
194
198
  det_spacing, d_src_pos, d_det_center, d_det_u_vec,
195
- cx, cy, voxel_spacing
199
+ cx, cy, voxel_spacing, d_detector_positions=None,
196
200
  ):
197
201
  """Compute the 2D fan beam backprojection with arbitrary source-detector trajectories.
198
202
 
@@ -248,19 +252,23 @@ def _fan_2d_backward_kernel(
248
252
  src_x = d_src_pos[iang, 0] / voxel_spacing
249
253
  src_y = d_src_pos[iang, 1] / voxel_spacing
250
254
 
251
- # Read detector center and orientation vector
252
- det_cx = d_det_center[iang, 0] / voxel_spacing
253
- det_cy = d_det_center[iang, 1] / voxel_spacing
255
+ if d_detector_positions is None or d_detector_positions.size == 0:
256
+ # Read detector center and orientation vector
257
+ det_cx = d_det_center[iang, 0] / voxel_spacing
258
+ det_cy = d_det_center[iang, 1] / voxel_spacing
254
259
 
255
- u_vec_x = d_det_u_vec[iang, 0]
256
- u_vec_y = d_det_u_vec[iang, 1]
260
+ u_vec_x = d_det_u_vec[iang, 0]
261
+ u_vec_y = d_det_u_vec[iang, 1]
257
262
 
258
- # Calculate detector element offset from center
259
- u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
263
+ # Calculate detector element offset from center
264
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
260
265
 
261
- # Calculate 2D detector element position using center + u*u_vec
262
- det_x = det_cx + u_offset * u_vec_x
263
- det_y = det_cy + u_offset * u_vec_y
266
+ # Calculate 2D detector element position using center + u*u_vec
267
+ det_x = det_cx + u_offset * u_vec_x
268
+ det_y = det_cy + u_offset * u_vec_y
269
+ else:
270
+ det_x = d_detector_positions[iang, idet, 0] / voxel_spacing
271
+ det_y = d_detector_positions[iang, idet, 1] / voxel_spacing
264
272
 
265
273
  # === RAY DIRECTION CALCULATION ===
266
274
  # Ray direction vector from source to detector element