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.
- {diffct-2.0.2 → diffct-2.1.0}/CHANGELOG.md +29 -0
- {diffct-2.0.2 → diffct-2.1.0}/CITATION.cff +2 -2
- {diffct-2.0.2 → diffct-2.1.0}/PKG-INFO +28 -7
- {diffct-2.0.2 → diffct-2.1.0}/README.md +27 -6
- {diffct-2.0.2 → diffct-2.1.0}/diffct/__init__.py +1 -1
- diffct-2.1.0/diffct/chunking.py +73 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/cone_beam.py +46 -36
- {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/fan_beam.py +30 -22
- {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/geometry_vjp.py +77 -48
- {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/parallel_beam.py +32 -22
- diffct-2.1.0/diffct/operators.py +1033 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/projectors.py +143 -18
- {diffct-2.0.2 → diffct-2.1.0}/diffct/utils.py +5 -0
- diffct-2.1.0/docs/video/README.md +119 -0
- {diffct-2.0.2 → diffct-2.1.0}/examples/README.md +17 -2
- {diffct-2.0.2 → diffct-2.1.0}/formal/README.md +41 -9
- {diffct-2.0.2 → diffct-2.1.0}/pyproject.toml +1 -1
- diffct-2.1.0/tests/test_chunked_projector.py +1202 -0
- diffct-2.1.0/tests/test_detector_surface_dtypes.py +80 -0
- diffct-2.1.0/tests/test_detector_surface_mapping.py +79 -0
- diffct-2.1.0/tests/test_detector_surface_units.py +62 -0
- diffct-2.1.0/tests/test_detector_surfaces.py +705 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_api.py +9 -3
- diffct-2.0.2/diffct/operators.py +0 -555
- diffct-2.0.2/docs/video/README.md +0 -47
- {diffct-2.0.2 → diffct-2.1.0}/.gitignore +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/LICENSE +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/NOTICE +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/analytical.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/constants.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/differentiable.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/geometry.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/diffct/kernels/__init__.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/examples/data/NOTICE +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/pytest.ini +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/__init__.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/_phantoms.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/README.md +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/_common.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/distributed_projector_check.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_gradcheck.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.0.2 → diffct-2.1.0}/tests/test_release_2_0_1.py +0 -0
- {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
|
|
17
|
-
date-released: "2026-10-
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
- **
|
|
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/
|
|
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/
|
|
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
|
-
- **
|
|
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
|
|
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
90
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
91
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
260
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
261
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|