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/__init__.py +83 -0
- diffct/analytical.py +569 -0
- diffct/constants.py +72 -0
- diffct/differentiable.py +74 -0
- diffct/geometry.py +991 -0
- diffct/kernels/__init__.py +43 -0
- diffct/kernels/cone_beam.py +617 -0
- diffct/kernels/fan_beam.py +451 -0
- diffct/kernels/geometry_vjp.py +562 -0
- diffct/kernels/parallel_beam.py +400 -0
- diffct/operators.py +546 -0
- diffct/projectors.py +910 -0
- diffct/utils.py +357 -0
- diffct-2.0.0.dist-info/METADATA +240 -0
- diffct-2.0.0.dist-info/RECORD +18 -0
- diffct-2.0.0.dist-info/WHEEL +4 -0
- diffct-2.0.0.dist-info/licenses/LICENSE +201 -0
- diffct-2.0.0.dist-info/licenses/NOTICE +18 -0
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,,
|