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.
- {diffct-2.1.0 → diffct-2.2.1}/CHANGELOG.md +67 -0
- {diffct-2.1.0 → diffct-2.2.1}/CITATION.cff +2 -2
- {diffct-2.1.0 → diffct-2.2.1}/PKG-INFO +24 -19
- {diffct-2.1.0 → diffct-2.2.1}/README.md +23 -18
- {diffct-2.1.0 → diffct-2.2.1}/diffct/__init__.py +18 -1
- {diffct-2.1.0 → diffct-2.2.1}/diffct/analytical.py +151 -6
- diffct-2.2.1/diffct/chunking.py +201 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/__init__.py +8 -0
- diffct-2.2.1/diffct/kernels/analytical_transpose.py +295 -0
- diffct-2.2.1/diffct/operators.py +2047 -0
- diffct-2.2.1/diffct/pipeline.py +258 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/projectors.py +109 -204
- diffct-2.2.1/diffct/reconstruction.py +399 -0
- diffct-2.2.1/diffct/storage.py +125 -0
- diffct-2.2.1/diffct/streaming.py +48 -0
- diffct-2.2.1/diffct/surfaces.py +103 -0
- {diffct-2.1.0 → diffct-2.2.1}/examples/README.md +60 -0
- {diffct-2.1.0 → diffct-2.2.1}/formal/README.md +37 -0
- {diffct-2.1.0 → diffct-2.2.1}/pyproject.toml +1 -1
- diffct-2.2.1/tests/test_allocator_capacity.py +349 -0
- diffct-2.2.1/tests/test_analytical_autograd.py +313 -0
- diffct-2.2.1/tests/test_axis_aligned_chords.py +118 -0
- diffct-2.2.1/tests/test_batched_surfaces.py +705 -0
- diffct-2.2.1/tests/test_block_cgls.py +996 -0
- diffct-2.2.1/tests/test_block_storage.py +545 -0
- diffct-2.2.1/tests/test_cgls_state_admission.py +173 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_chunked_projector.py +135 -39
- diffct-2.2.1/tests/test_completed_transfer_reuse.py +92 -0
- diffct-2.2.1/tests/test_cuda_sink_lifetimes.py +125 -0
- diffct-2.2.1/tests/test_example_psnr.py +75 -0
- diffct-2.2.1/tests/test_execution_buffers.py +613 -0
- diffct-2.2.1/tests/test_execution_schedules.py +582 -0
- diffct-2.2.1/tests/test_output_pipeline_overlap.py +130 -0
- diffct-2.2.1/tests/test_reconstruction_api.py +872 -0
- diffct-2.2.1/tests/test_spatial_partition.py +433 -0
- diffct-2.2.1/tests/test_transfer_pipeline.py +646 -0
- diffct-2.2.1/tests/test_transfer_retirement_lifetimes.py +166 -0
- diffct-2.1.0/diffct/chunking.py +0 -73
- diffct-2.1.0/diffct/operators.py +0 -1033
- {diffct-2.1.0 → diffct-2.2.1}/.gitignore +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/LICENSE +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/NOTICE +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/constants.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/differentiable.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/geometry.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/cone_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/fan_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/geometry_vjp.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/kernels/parallel_beam.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/diffct/utils.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/docs/video/README.md +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/examples/data/NOTICE +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/pytest.ini +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/__init__.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/_phantoms.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/README.md +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/_common.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/distributed_projector_check.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_dtypes.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_mapping.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surface_units.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_detector_surfaces.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_gradcheck.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_api.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.1.0 → diffct-2.2.1}/tests/test_release_2_0_1.py +0 -0
- {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
|
|
17
|
-
date-released: "2026-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
|
|
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.**
|
|
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.**
|
|
81
|
-
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or
|
|
82
|
-
- **
|
|
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.**
|
|
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,
|
|
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
|
-
|
|
|
177
|
+
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
176
178
|
|---|---:|---:|---:|
|
|
177
|
-
| Forward projection |
|
|
178
|
-
| Adjoint (backprojection) |
|
|
179
|
-
| CGLS iteration |
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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.**
|
|
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.**
|
|
50
|
-
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or
|
|
51
|
-
- **
|
|
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.**
|
|
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,
|
|
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
|
-
|
|
|
146
|
+
| 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
|
|
145
147
|
|---|---:|---:|---:|
|
|
146
|
-
| Forward projection |
|
|
147
|
-
| Adjoint (backprojection) |
|
|
148
|
-
| CGLS iteration |
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
371
|
-
|
|
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
|
|
455
|
-
|
|
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
|
|
511
|
-
|
|
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__
|