diffct 2.0.0__py3-none-any.whl

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/utils.py ADDED
@@ -0,0 +1,357 @@
1
+ """Utility classes and helper functions for diffct package.
2
+
3
+ This module provides utility classes and functions for device management,
4
+ PyTorch-CUDA bridging, stream caching, trigonometric table generation,
5
+ memory layout validation, and CUDA grid computation.
6
+ """
7
+
8
+ import math
9
+ import numpy as np
10
+ import torch
11
+ from numba import cuda
12
+
13
+ from .constants import _DTYPE, _TPB_2D, _TPB_3D, _INF, _EPSILON
14
+
15
+
16
+ # ============================================================================
17
+ # Device Management Utilities
18
+ # ============================================================================
19
+
20
+ class DeviceManager:
21
+ """Utilities for managing PyTorch tensor devices."""
22
+
23
+ @staticmethod
24
+ def get_device(tensor):
25
+ """Get the device of a PyTorch tensor.
26
+
27
+ Parameters
28
+ ----------
29
+ tensor : torch.Tensor
30
+ Tensor whose device to determine.
31
+
32
+ Returns
33
+ -------
34
+ torch.device
35
+ Device of the tensor or CPU if unavailable.
36
+
37
+ Examples
38
+ --------
39
+ >>> DeviceManager.get_device(torch.tensor([1, 2, 3]))
40
+ device(type='cpu')
41
+ """
42
+ return tensor.device if hasattr(tensor, "device") else torch.device("cpu")
43
+
44
+ @staticmethod
45
+ def ensure_device(tensor, device):
46
+ """Ensure a tensor resides on a given device.
47
+
48
+ Parameters
49
+ ----------
50
+ tensor : torch.Tensor
51
+ Tensor to move.
52
+ device : torch.device
53
+ Desired device.
54
+
55
+ Returns
56
+ -------
57
+ torch.Tensor
58
+ Tensor on the specified device. Unchanged if already on it.
59
+
60
+ Examples
61
+ --------
62
+ >>> DeviceManager.ensure_device(
63
+ ... torch.tensor([1, 2, 3]),
64
+ ... torch.device('cuda')
65
+ ... )
66
+ tensor([1, 2, 3], device='cuda:0')
67
+ """
68
+ if hasattr(tensor, "to") and tensor.device != device:
69
+ return tensor.to(device)
70
+ return tensor
71
+
72
+
73
+ # ============================================================================
74
+ # PyTorch-CUDA Bridge
75
+ # ============================================================================
76
+
77
+ class TorchCUDABridge:
78
+ """Bridge between PyTorch tensors and Numba CUDA arrays."""
79
+
80
+ @staticmethod
81
+ def tensor_to_cuda_array(tensor):
82
+ """Convert a PyTorch CUDA tensor to a Numba CUDA DeviceNDArray.
83
+
84
+ Provides a zero-copy view of a detached PyTorch tensor as a Numba CUDA array,
85
+ avoiding CPU data transfers. The returned array shares memory with the
86
+ original tensor.
87
+
88
+ Parameters
89
+ ----------
90
+ tensor : torch.Tensor
91
+ PyTorch tensor on a CUDA device.
92
+
93
+ Returns
94
+ -------
95
+ numba.cuda.cudadrv.devicearray.DeviceNDArray
96
+ Numba CUDA array view sharing memory with `tensor`.
97
+
98
+ Raises
99
+ ------
100
+ ValueError
101
+ If `tensor` is not on a CUDA device.
102
+
103
+ Examples
104
+ --------
105
+ >>> t = torch.randn(10, device='cuda')
106
+ >>> arr = TorchCUDABridge.tensor_to_cuda_array(t)
107
+ """
108
+ if not tensor.is_cuda:
109
+ raise ValueError("Tensor must be on CUDA device")
110
+ return cuda.as_cuda_array(tensor.detach())
111
+
112
+
113
+ # ============================================================================
114
+ # Stream Management (cached external Numba stream)
115
+ # ============================================================================
116
+
117
+ _cached_stream_ptr = None
118
+ _cached_numba_stream = None
119
+
120
+
121
+ def _get_numba_external_stream_for(pt_stream=None):
122
+ """Return a cached numba.cuda.external_stream for the current PyTorch CUDA stream.
123
+
124
+ Caches by the CUDA device and stream pointer to avoid repeated construction.
125
+
126
+ Parameters
127
+ ----------
128
+ pt_stream : torch.cuda.Stream, optional
129
+ PyTorch CUDA stream. If None, uses current stream.
130
+
131
+ Returns
132
+ -------
133
+ numba.cuda.cudadrv.driver.Stream
134
+ Numba external stream wrapper around PyTorch CUDA stream.
135
+ """
136
+ global _cached_stream_ptr, _cached_numba_stream
137
+ if pt_stream is None:
138
+ pt_stream = torch.cuda.current_stream()
139
+ # A stream pointer is only unique within one CUDA device.
140
+ ptr = int(pt_stream.cuda_stream)
141
+ device_index = pt_stream.device.index
142
+ if device_index is None:
143
+ device_index = torch.cuda.current_device()
144
+ key = (device_index, ptr)
145
+ if _cached_stream_ptr == key and _cached_numba_stream is not None:
146
+ return _cached_numba_stream
147
+ numba_stream = cuda.external_stream(pt_stream.cuda_stream)
148
+ _cached_stream_ptr = key
149
+ _cached_numba_stream = numba_stream
150
+ return numba_stream
151
+
152
+
153
+ # ============================================================================
154
+ # GPU-aware Trigonometric Table Generation
155
+ # ============================================================================
156
+
157
+ def _trig_tables(angles, dtype=_DTYPE, device=None):
158
+ """Compute cosine and sine tables for input angles.
159
+
160
+ Precompute cosine and sine values and return as torch tensors on the
161
+ same device as `angles`.
162
+
163
+ Parameters
164
+ ----------
165
+ angles : array-like or torch.Tensor
166
+ Projection angles in radians. Can be a NumPy array or a PyTorch tensor on CPU or CUDA.
167
+ dtype : numpy.dtype or torch.dtype, optional
168
+ Desired data type for output tables. Default is `_DTYPE`.
169
+ device : torch.device, optional
170
+ Target device for output tensors. If None, uses the device of `angles`.
171
+
172
+ Returns
173
+ -------
174
+ cos : torch.Tensor
175
+ Cosine values of `angles` on the same device.
176
+ sin : torch.Tensor
177
+ Sine values of `angles` on the same device.
178
+
179
+ Examples
180
+ --------
181
+ >>> angles = torch.linspace(0, torch.pi, 180, device='cuda')
182
+ >>> cos, sin = _trig_tables(angles)
183
+ >>> cos.device
184
+ device(type='cuda', index=0)
185
+ """
186
+ if isinstance(angles, torch.Tensor):
187
+ device = angles.device if device is None else device
188
+ # Compute both cos and sin in one call to avoid redundant kernel launches
189
+ angles_device = angles.to(dtype=dtype, device=device)
190
+ cos = torch.cos(angles_device)
191
+ sin = torch.sin(angles_device)
192
+ return cos, sin
193
+ else:
194
+ # fallback for non-tensor inputs: compute via PyTorch on CPU for consistency
195
+ # Determine desired torch dtype
196
+ if isinstance(dtype, torch.dtype):
197
+ torch_dtype = dtype
198
+ else:
199
+ _NP_TO_TORCH = {
200
+ np.float32: torch.float32,
201
+ np.float64: torch.float64,
202
+ }
203
+ torch_dtype = _NP_TO_TORCH.get(dtype, torch.float32)
204
+ # Convert input angles to a CPU torch tensor and compute both simultaneously
205
+ angles_cpu = torch.tensor(angles, dtype=torch_dtype)
206
+ cos_cpu = torch.cos(angles_cpu)
207
+ sin_cpu = torch.sin(angles_cpu)
208
+ if device is not None:
209
+ return cos_cpu.to(device), sin_cpu.to(device)
210
+ else:
211
+ return cos_cpu, sin_cpu
212
+
213
+
214
+ # ============================================================================
215
+ # Memory Layout Validation
216
+ # ============================================================================
217
+
218
+ def _validate_3d_memory_layout(tensor, expected_order='DHW'):
219
+ """Validate 3D tensor memory layout to prevent coordinate system inconsistencies.
220
+
221
+ Parameters
222
+ ----------
223
+ tensor : torch.Tensor
224
+ 3D tensor to validate
225
+ expected_order : str, optional
226
+ Expected memory order ('DHW', 'VHW', etc.). Default is 'DHW'.
227
+
228
+ Raises
229
+ ------
230
+ ValueError
231
+ If tensor has unexpected memory layout or is non-contiguous
232
+ """
233
+ shape = tensor.shape
234
+ if len(shape) != 3:
235
+ raise ValueError(f"Expected 3D tensor, got {len(shape)}D")
236
+
237
+ # Early return for common case - contiguous tensor with expected ordering
238
+ if tensor.is_contiguous() and expected_order in ('DHW', 'VHW'):
239
+ # For DHW and VHW, the expected order matches memory layout when contiguous
240
+ return
241
+
242
+ # Only check memory order for DHW and VHW, not for internal WHD layout
243
+ if expected_order in ('DHW', 'VHW'):
244
+ if not tensor.is_contiguous():
245
+ raise ValueError(
246
+ "Input tensor must be contiguous. Call .contiguous() before passing to "
247
+ "cone beam functions to avoid memory duplication and ensure correct results."
248
+ )
249
+
250
+ strides = tensor.stride()
251
+ order_mapping = {
252
+ 'DHW': (0, 1, 2), # Depth, Height, Width
253
+ 'VHW': (0, 1, 2), # Views, Height, Width (for sinograms)
254
+ }
255
+ if expected_order not in order_mapping:
256
+ raise ValueError(f"Unsupported expected_order: {expected_order}")
257
+
258
+ expected_stride_order = order_mapping[expected_order]
259
+ # Check if actual strides match expected order
260
+ sorted_strides = sorted(enumerate(strides), key=lambda x: x[1], reverse=True)
261
+ actual_order = tuple(idx for idx, _ in sorted_strides)
262
+
263
+ if actual_order != expected_stride_order:
264
+ # Create appropriate error message based on context
265
+ if expected_order == 'VHW':
266
+ actual_str = f"({shape[0]}, {shape[1]}, {shape[2]})"
267
+ expected_str = "(Views, Height, Width)"
268
+ fix_str = "ensure your sinogram has shape (num_views, det_v, det_u)"
269
+ elif expected_order == 'DHW':
270
+ actual_str = f"({shape[0]}, {shape[1]}, {shape[2]})"
271
+ expected_str = "(Depth, Height, Width)"
272
+ fix_str = "ensure your volume has shape (D, H, W)"
273
+ else:
274
+ actual_str = str(tuple(shape))
275
+ expected_str = expected_order
276
+ fix_str = "check tensor dimensions"
277
+
278
+ raise ValueError(
279
+ f"Memory layout mismatch: expected {expected_str} order, "
280
+ f"but tensor has shape {actual_str}. Please {fix_str} and ensure "
281
+ f"the tensor is contiguous (.contiguous()) before passing to the function."
282
+ )
283
+ # For 'WHD' (internal layout), skip stride check entirely
284
+
285
+
286
+ # ============================================================================
287
+ # CUDA Grid Computation
288
+ # ============================================================================
289
+
290
+ def _grid_2d(n1, n2, tpb=_TPB_2D):
291
+ """Compute 2D CUDA grid and block dimensions.
292
+
293
+ Determine optimal grid and block sizes for 2D CUDA ray-tracing kernels.
294
+
295
+ Parameters
296
+ ----------
297
+ n1 : int
298
+ Number of elements along the first dimension (e.g., projection angles).
299
+ n2 : int
300
+ Number of elements along the second dimension (e.g., detector elements).
301
+ tpb : tuple of int, optional
302
+ Threads per block (default is `_TPB_2D`) to balance occupancy and memory.
303
+
304
+ Returns
305
+ -------
306
+ grid : tuple of int
307
+ Blocks count per axis.
308
+ tpb : tuple of int
309
+ Threads per block per axis.
310
+
311
+ Examples
312
+ --------
313
+ >>> grid, tpb = _grid_2d(180, 256)
314
+ >>> grid
315
+ (12, 16)
316
+ >>> tpb
317
+ (16, 16)
318
+ """
319
+ return (math.ceil(n1 / tpb[0]), math.ceil(n2 / tpb[1])), tpb
320
+
321
+
322
+ def _grid_3d(n1, n2, n3, tpb=_TPB_3D):
323
+ """Compute 3D CUDA grid and block dimensions.
324
+
325
+ Determine optimal grid and block sizes for 3D CUDA cone-beam kernels.
326
+
327
+ Parameters
328
+ ----------
329
+ n1 : int
330
+ Number of elements along the first dimension (e.g., projection views).
331
+ n2 : int
332
+ Number of elements along the second dimension (e.g., detector u-axis).
333
+ n3 : int
334
+ Number of elements along the third dimension (e.g., detector v-axis).
335
+ tpb : tuple of int, optional
336
+ Threads per block (default is `_TPB_3D`) to balance occupancy and registers.
337
+
338
+ Returns
339
+ -------
340
+ grid : tuple of int
341
+ Blocks count per axis.
342
+ tpb : tuple of int
343
+ Threads per block per axis.
344
+
345
+ Examples
346
+ --------
347
+ >>> grid, tpb = _grid_3d(360, 256, 256)
348
+ >>> grid
349
+ (45, 32, 32)
350
+ >>> tpb
351
+ (8, 8, 8)
352
+ """
353
+ return (
354
+ math.ceil(n1 / tpb[0]),
355
+ math.ceil(n2 / tpb[1]),
356
+ math.ceil(n3 / tpb[2]),
357
+ ), tpb
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.5
2
+ Name: diffct
3
+ Version: 2.0.0
4
+ Summary: Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU and multi-node execution, geometry gradients
5
+ Project-URL: Homepage, https://github.com/sypsyp97/diffct
6
+ Project-URL: Documentation, https://sypsyp97.github.io/diffct/
7
+ Project-URL: Changelog, https://github.com/sypsyp97/diffct/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/sypsyp97/diffct/issues
9
+ Author-email: Yipeng Sun <yipeng.sun@fau.de>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Keywords: CT reconstruction,CUDA,PyTorch,computed tomography,differentiable programming
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: GPU :: NVIDIA CUDA
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
20
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: numba
23
+ Requires-Dist: numba-cuda
24
+ Requires-Dist: numpy<2.5
25
+ Requires-Dist: torch
26
+ Provides-Extra: cu12
27
+ Requires-Dist: numba-cuda[cu12]; extra == 'cu12'
28
+ Provides-Extra: cu13
29
+ Requires-Dist: numba-cuda[cu13]; extra == 'cu13'
30
+ Description-Content-Type: text/markdown
31
+
32
+ <h1 align="center">diffct</h1>
33
+
34
+ <p align="center">English · <a href="README.zh.md">简体中文</a></p>
35
+
36
+ <p align="center">
37
+ Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.
38
+ </p>
39
+
40
+ <p align="center">
41
+ <a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=flat-square" alt="License"></a>
42
+ <a href="https://doi.org/10.5281/zenodo.14999333"><img src="https://img.shields.io/badge/DOI-10.5281%2Fzenodo.14999333-blue.svg?style=flat-square" alt="DOI"></a>
43
+ <a href="https://pypi.org/project/diffct/"><img src="https://img.shields.io/pypi/v/diffct.svg?style=flat-square&logo=pypi&logoColor=white" alt="PyPI version"></a>
44
+ <a href="https://sypsyp97.github.io/diffct/"><img src="https://img.shields.io/github/actions/workflow/status/sypsyp97/diffct/docs.yml?branch=main&label=docs&style=flat-square" alt="Documentation"></a>
45
+ <a href="https://github.com/sypsyp97/diffct/actions"><img src="https://img.shields.io/github/actions/workflow/status/sypsyp97/diffct/ci.yml?branch=main&label=CI&style=flat-square" alt="CI/CD"></a>
46
+ <a href="https://deepwiki.com/sypsyp97/diffct"><img src="docs/assets/deepwiki-badge.svg" alt="Ask DeepWiki"></a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <img src="docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
51
+ </p>
52
+
53
+ <p align="center">
54
+ <a href="docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
55
+ <a href="https://www.preprints.org/manuscript/202605.1446/v1">Technical report</a> ·
56
+ <a href="https://doi.org/10.20944/preprints202605.1446.v1">DOI</a>
57
+ </p>
58
+
59
+ <p align="center">
60
+ <a href="#install">Install</a> ·
61
+ <a href="#quickstart">Quickstart</a> ·
62
+ <a href="#why-diffct">Features</a> ·
63
+ <a href="#performance">Performance</a> ·
64
+ <a href="#examples">Examples</a> ·
65
+ <a href="#citation">Citation</a>
66
+ </p>
67
+
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](docs/MIGRATION.md).
69
+ > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
70
+
71
+ ## Why diffct
72
+
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.
74
+
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.
76
+ - **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.
77
+ - **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.
78
+ - **Analytical helpers.** `diffct.analytical` provides ramp filters (ram-lak, shepp-logan, cosine, hamming, hann), fan, cone and Parker weights, and FBP and FDK backprojection.
79
+ - **Validated.** FDK matches ASTRA 2.5.0 `FDK_CUDA`: PSNR within 0.3 dB. Geometry gradients match an independent float64 reference to about 1e-6. The [recorded A100 validation](docs/VALIDATION.md) reports 229 passing pytest tests. Full notes: [REFERENCE](docs/REFERENCE.md#validation-summary).
80
+
81
+ Capabilities, limits and isocenter rules: [docs/REFERENCE.md](docs/REFERENCE.md#capabilities-and-limits).
82
+
83
+ ## Install
84
+
85
+ You need a CUDA GPU and PyTorch. Install PyTorch for your CUDA version first.
86
+
87
+ ```bash
88
+ pip install "diffct[cu12]" # [cu13] for CUDA 13; install PyTorch for your CUDA first
89
+ ```
90
+
91
+ From source, with the examples:
92
+
93
+ ```bash
94
+ git clone https://github.com/sypsyp97/diffct.git
95
+ cd diffct && pip install -e ".[cu12]"
96
+ python examples/quickstart.py # smoke test: prints adjoint mismatch ~1e-8 for each beam
97
+ ```
98
+
99
+ <details>
100
+ <summary>Notes on CUDA versions</summary>
101
+
102
+ - Numba CUDA imports `numpy.row_stack`, which NumPy 2.5 removes. Keep
103
+ `numpy<2.5`.
104
+ - Keep NVVM and NVJitLink compatible with the CUDA libraries that PyTorch
105
+ loads. A newer NVVM with an older NVJitLink fails when the kernels compile.
106
+ - A tested CUDA 13 set:
107
+ `pip install "numpy<2.5" "numba-cuda[cu13]" "cuda-toolkit[cccl,cudart,nvrtc,nvvm]==13.0.2" "nvidia-nvjitlink<13.1"`.
108
+ - A tested CUDA 12 set: PyTorch 2.10 (cu126) with `numba-cuda[cu12]` 0.30.4 on
109
+ NVIDIA driver 535.
110
+
111
+ </details>
112
+
113
+ ## Quickstart
114
+
115
+ Create the acquisition once. Then call `project()` and `backproject()`.
116
+
117
+ ```python
118
+ import torch
119
+ from diffct import Projector, spiral_trajectory_3d
120
+
121
+ trajectory = spiral_trajectory_3d(360, sid=320.0, sdd=512.0, z_range=40.0, n_turns=1.0, device="cpu")
122
+ A = Projector(trajectory, volume_shape=(128, 128, 128), detector_shape=(384, 256), detector_spacing=0.8)
123
+
124
+ volume = torch.rand(A.volume_shape, device="cuda")
125
+ sinogram = A.project(volume) # (360, 384, 256) line integrals
126
+ adjoint = A.backproject(sinogram) # matched adjoint A^T
127
+
128
+ x = torch.zeros_like(volume, requires_grad=True)
129
+ loss = 0.5 * (A.project(x) - sinogram).square().sum()
130
+ loss.backward() # x.grad = A^T (A x - y)
131
+ ```
132
+
133
+ **Custom or calibrated trajectories.** Pass calibrated tensors directly instead of a generator; no circular fit is needed. See [REFERENCE](docs/REFERENCE.md#custom-or-calibrated-trajectories).
134
+
135
+ **Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](docs/REFERENCE.md#geometry-gradients).
136
+
137
+ **Several GPUs and nodes.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun for one or more nodes. See [REFERENCE](docs/REFERENCE.md#several-gpus-and-nodes) and [DISTRIBUTED.md](docs/DISTRIBUTED.md).
138
+
139
+ ```python
140
+ A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
141
+ ```
142
+
143
+ ## Performance
144
+
145
+ <p align="center">
146
+ <picture>
147
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/scaling_dark.png">
148
+ <img src="docs/assets/scaling_light.png" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 64^3 and 128^3">
149
+ </picture>
150
+ </p>
151
+
152
+ | 128³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
153
+ |---|---:|---:|---:|
154
+ | Forward projection | 8.45 ms | 2.66 ms (3.2×) | 1.68 ms (5.0×) |
155
+ | Adjoint (backprojection) | 20.66 ms | 5.89 ms (3.5×) | 4.62 ms (4.5×) |
156
+ | CGLS iteration | 30.08 ms | 8.08 ms (3.7×) | 5.59 ms (5.4×) |
157
+
158
+ Circular trajectory, 360 views, detector (2n, 1.5n) cells at pitch 1.25, A100 64 GB on Leonardo Booster. Small volumes scale less: at 64³, one CGLS iteration takes 4.63 ms on 1 GPU, 1.57 ms on 4 and 1.56 ms on 8. Raw data: [docs/assets/scaling.json](docs/assets/scaling.json).
159
+
160
+ To measure one against several GPUs on your machine (default scan of the script, not the table setup):
161
+
162
+ ```bash
163
+ python examples/benchmark_projector.py --devices 0 1
164
+ ```
165
+
166
+ ## Gallery
167
+
168
+ **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.
169
+
170
+ <p align="center">
171
+ <img src="docs/assets/walnut_measured.png" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
172
+ </p>
173
+
174
+ ```bash
175
+ python examples/walnut_reconstruction.py --figure out.png
176
+ ```
177
+
178
+ **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.
179
+
180
+ <p align="center">
181
+ <img src="docs/assets/walnut_helical.png" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
182
+ </p>
183
+
184
+ ```bash
185
+ python examples/walnut_reconstruction.py --algorithms --save-volume walnut256.npy
186
+ python examples/iterative_reconstruction.py --size 256 --views 720 --trajectory helical --noise 0.01 --phantom walnut256.npy --figure out.png
187
+ ```
188
+
189
+ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](examples/data/NOTICE).
190
+
191
+ ## Examples
192
+
193
+ `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, distributed-loss rules and measured results are in [examples/README.md](examples/README.md).
194
+
195
+ ## Documentation
196
+
197
+ | Document | Contents |
198
+ |---|---|
199
+ | [Trajectory guide](docs/source/trajectories.rst) | Trajectory tuples and `Projector` usage |
200
+ | [docs/REFERENCE.md](docs/REFERENCE.md) | Capabilities and limits, custom trajectories, geometry gradients, multi-GPU details, geometry and units |
201
+ | [docs/DISTRIBUTED.md](docs/DISTRIBUTED.md) | Execution/memory choices, distributed loss rules, Slurm and cross-node checks |
202
+ | [docs/VALIDATION.md](docs/VALIDATION.md) | Test commands, validation details and the limits of each check |
203
+ | [docs/MIGRATION.md](docs/MIGRATION.md) | Moving from the circular-only API |
204
+ | [docs/video/README.md](docs/video/README.md) | How the intro video is rendered |
205
+ | [CHANGELOG.md](CHANGELOG.md) | Release history |
206
+
207
+ Run the test suite on a CUDA host with `python -m pytest tests/ -q`.
208
+
209
+ ## Citation
210
+
211
+ If you use this library in your research, please cite the software and the technical report:
212
+
213
+ ```bibtex
214
+ @software{diffct2025,
215
+ author = {Yipeng Sun},
216
+ title = {diffct: Differentiable Computed Tomography Reconstruction with CUDA},
217
+ year = 2025,
218
+ publisher = {Zenodo},
219
+ doi = {10.5281/zenodo.14999333},
220
+ url = {https://doi.org/10.5281/zenodo.14999333}
221
+ }
222
+
223
+ @article{202605.1446,
224
+ doi = {10.20944/preprints202605.1446.v1},
225
+ url = {https://doi.org/10.20944/preprints202605.1446.v1},
226
+ year = 2026,
227
+ month = {May},
228
+ publisher = {Preprints},
229
+ author = {Yipeng Sun and Linda-Sophie Schneider and Chengze ye and Andreas Maier},
230
+ title = {diffct: Differentiable CT Operators from Circular Orbits to Arbitrary Trajectories},
231
+ journal = {Preprints}
232
+ }
233
+ ```
234
+
235
+ ## License and acknowledgements
236
+
237
+ Apache 2.0, see [LICENSE](LICENSE). The project draws on
238
+ [PYRO-NN](https://github.com/csyben/PYRO-NN) and
239
+ [geometry_gradients_CT](https://github.com/mareikethies/geometry_gradients_CT).
240
+ Issues and pull requests are welcome. The walnut data are CC BY 4.0 (Meaney 2022); see [NOTICE](NOTICE).
@@ -0,0 +1,18 @@
1
+ diffct/__init__.py,sha256=dvV3ttkCyMsaINHz1oBB-I_e9OLM6LYOR0RpG1tVlDo,2191
2
+ diffct/analytical.py,sha256=lNY3lzzauxo6XbCYdwZ5mY7zqFXKlVAt1vxoHPpCdEE,25106
3
+ diffct/constants.py,sha256=IKC_0u9aoNCG4M13KcRrl2plO0dTiokac-2aCYJJMb4,3060
4
+ diffct/differentiable.py,sha256=cFirhqXRpUemL7y7Sl_0uqZKGJNj__ptQEcTwYNKzys,1879
5
+ diffct/geometry.py,sha256=uN6J5fO4I4_dF3ecGP5IUaQIB7_odPocbr2_AKd3r5U,37576
6
+ diffct/operators.py,sha256=BfBkZpAXhWoCxOPgPF0VfNp1atenNYDTphM5Q6Xy2ns,24602
7
+ diffct/projectors.py,sha256=9-kLHYJFeohz-fhKZiYFyRNY7vOKnRplazpnNfIhckc,41108
8
+ diffct/utils.py,sha256=bfrmO_HlHmDkUQQQ0yf_fk-OSidSKRBpYUMNAnYxW_Q,11988
9
+ diffct/kernels/__init__.py,sha256=PhdgiZEyPzdnJ6tVmX6zzUvQH4EESwW8rfC_jCTlzhU,1106
10
+ diffct/kernels/cone_beam.py,sha256=TBtjX51f-WkaFqm6umQfr6zm2JQGjFIDsgs-skMj090,24620
11
+ diffct/kernels/fan_beam.py,sha256=5rRdIiqYL7ghJkk-MGlW5kfcznUlZhoOA-NKZtFhPJ4,16498
12
+ diffct/kernels/geometry_vjp.py,sha256=K8pKSqqw86ejvDkv-VLnv_qvxfWzs9IentSr6SROYe4,19102
13
+ diffct/kernels/parallel_beam.py,sha256=k6lOCAPvY1F9P9E2d-dYPx55328-66OKQ-MhPuP7hmw,16517
14
+ diffct-2.0.0.dist-info/METADATA,sha256=wj5VNajRfnkDnnJ2LvXyiMyBLblY2VvwZW2SPLvLBMk,12096
15
+ diffct-2.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
+ diffct-2.0.0.dist-info/licenses/LICENSE,sha256=a1fjzGs44LpDnV_LmA2Tzp8z-eJKoNVVdLOnh-F6J6E,11344
17
+ diffct-2.0.0.dist-info/licenses/NOTICE,sha256=du9I2sDF5IDfiY9vF64WWzMJh-MarpVXyLfWQmhbvmI,714
18
+ diffct-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any