diffct 2.0.0__py3-none-any.whl → 2.0.2__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 +1 -1
- diffct/analytical.py +4 -0
- diffct/geometry.py +15 -2
- diffct/operators.py +9 -0
- diffct/projectors.py +49 -48
- diffct/utils.py +69 -0
- {diffct-2.0.0.dist-info → diffct-2.0.2.dist-info}/METADATA +32 -29
- diffct-2.0.2.dist-info/RECORD +18 -0
- diffct-2.0.0.dist-info/RECORD +0 -18
- {diffct-2.0.0.dist-info → diffct-2.0.2.dist-info}/WHEEL +0 -0
- {diffct-2.0.0.dist-info → diffct-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {diffct-2.0.0.dist-info → diffct-2.0.2.dist-info}/licenses/NOTICE +0 -0
diffct/__init__.py
CHANGED
diffct/analytical.py
CHANGED
|
@@ -34,6 +34,7 @@ from .utils import (
|
|
|
34
34
|
_get_numba_external_stream_for,
|
|
35
35
|
_grid_2d,
|
|
36
36
|
_grid_3d,
|
|
37
|
+
_on_device_of,
|
|
37
38
|
)
|
|
38
39
|
from .kernels import (
|
|
39
40
|
_parallel_2d_fbp_backproject_kernel,
|
|
@@ -365,6 +366,7 @@ def _as_contig_f32(t, device):
|
|
|
365
366
|
return DeviceManager.ensure_device(t, device).to(dtype=torch.float32).contiguous()
|
|
366
367
|
|
|
367
368
|
|
|
369
|
+
@_on_device_of("sinogram")
|
|
368
370
|
def parallel_weighted_backproject(sinogram, ray_dir, det_origin, det_u_vec,
|
|
369
371
|
detector_spacing, H, W, voxel_spacing=1.0):
|
|
370
372
|
"""Voxel-driven parallel-beam FBP backprojection with analytical constant.
|
|
@@ -448,6 +450,7 @@ def _analytical_geometry(src_pos, det_center, n, isocenter):
|
|
|
448
450
|
return isocenter, float(sid_n.mean().item()), float(sdd_n.mean().item())
|
|
449
451
|
|
|
450
452
|
|
|
453
|
+
@_on_device_of("sinogram")
|
|
451
454
|
def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
|
|
452
455
|
detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
|
|
453
456
|
"""Voxel-driven fan-beam FBP backprojection with analytical constant.
|
|
@@ -503,6 +506,7 @@ def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
|
|
|
503
506
|
return reco * scale
|
|
504
507
|
|
|
505
508
|
|
|
509
|
+
@_on_device_of("sinogram")
|
|
506
510
|
def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
|
|
507
511
|
D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
|
|
508
512
|
"""Voxel-driven cone-beam FDK backprojection with analytical constant.
|
diffct/geometry.py
CHANGED
|
@@ -13,6 +13,17 @@ from typing import Callable, Tuple
|
|
|
13
13
|
# Trajectory Generation Functions
|
|
14
14
|
# ============================================================================
|
|
15
15
|
|
|
16
|
+
def _check_source_positions(src_pos):
|
|
17
|
+
"""Reject non-finite sources and sources at the isocenter, which have no viewing direction."""
|
|
18
|
+
if not torch.isfinite(src_pos).all():
|
|
19
|
+
raise ValueError("source_path_fn returned non-finite source positions")
|
|
20
|
+
if (torch.linalg.vector_norm(src_pos.detach(), dim=1) == 0).any():
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"source_path_fn returned a source at the isocenter; every source needs a "
|
|
23
|
+
"nonzero distance from the origin"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
16
27
|
def circular_trajectory_3d(n_views, sid, sdd, start_angle=0.0, end_angle=None, device='cuda', dtype=torch.float32):
|
|
17
28
|
"""Generate circular trajectory geometry for cone-beam CT.
|
|
18
29
|
|
|
@@ -500,11 +511,11 @@ def custom_trajectory_3d(n_views, sid, sdd,
|
|
|
500
511
|
|
|
501
512
|
Examples
|
|
502
513
|
--------
|
|
503
|
-
>>> #
|
|
514
|
+
>>> # Circular orbit with a two-lobe (figure-8) vertical motion
|
|
504
515
|
>>> def figure8_path(angles, sid):
|
|
505
516
|
... src_pos = torch.zeros((len(angles), 3), device=angles.device, dtype=angles.dtype)
|
|
506
517
|
... src_pos[:, 0] = -sid * torch.sin(angles)
|
|
507
|
-
... src_pos[:, 1] = sid * torch.cos(angles)
|
|
518
|
+
... src_pos[:, 1] = sid * torch.cos(angles)
|
|
508
519
|
... src_pos[:, 2] = 50 * torch.sin(2 * angles)
|
|
509
520
|
... return src_pos
|
|
510
521
|
>>>
|
|
@@ -523,6 +534,7 @@ def custom_trajectory_3d(n_views, sid, sdd,
|
|
|
523
534
|
if src_pos.shape != (n_views, 3):
|
|
524
535
|
raise ValueError(f"source_path_fn must return tensor of shape ({n_views}, 3), "
|
|
525
536
|
f"got {src_pos.shape}")
|
|
537
|
+
_check_source_positions(src_pos)
|
|
526
538
|
|
|
527
539
|
# Keep the entire frame in the source-path autograd graph. Constructing
|
|
528
540
|
# tensors from scalar tensors detaches them; rewriting view slices in
|
|
@@ -750,6 +762,7 @@ def custom_trajectory_2d_fan(n_views, sid, sdd,
|
|
|
750
762
|
if src_pos.shape != (n_views, 2):
|
|
751
763
|
raise ValueError(f"source_path_fn must return tensor of shape ({n_views}, 2), "
|
|
752
764
|
f"got {src_pos.shape}")
|
|
765
|
+
_check_source_positions(src_pos)
|
|
753
766
|
|
|
754
767
|
# Preallocate remaining position matrices
|
|
755
768
|
det_center = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
diffct/operators.py
CHANGED
|
@@ -523,6 +523,15 @@ class Projector:
|
|
|
523
523
|
*self.detector_spacing, self.voxel_spacing,
|
|
524
524
|
)
|
|
525
525
|
|
|
526
|
+
@property
|
|
527
|
+
def process_group(self):
|
|
528
|
+
"""Process group of a distributed operator, or None for the default group.
|
|
529
|
+
|
|
530
|
+
Pass it as ``group=`` to collectives that combine per-rank results of this
|
|
531
|
+
operator, so they use the same ranks as the operator.
|
|
532
|
+
"""
|
|
533
|
+
return self._process_group
|
|
534
|
+
|
|
526
535
|
def project(self, volume):
|
|
527
536
|
"""Project a ``(H, W)`` or ``(D, H, W)`` CUDA tensor to float32.
|
|
528
537
|
|
diffct/projectors.py
CHANGED
|
@@ -12,6 +12,7 @@ from .utils import (
|
|
|
12
12
|
DeviceManager,
|
|
13
13
|
TorchCUDABridge,
|
|
14
14
|
_get_numba_external_stream_for,
|
|
15
|
+
_on_device_of,
|
|
15
16
|
_trig_tables,
|
|
16
17
|
_validate_3d_memory_layout,
|
|
17
18
|
_grid_2d,
|
|
@@ -77,6 +78,7 @@ class _GeometryVJPFunction(torch.autograd.Function):
|
|
|
77
78
|
"""First-order geometry gradient; differentiating it again raises."""
|
|
78
79
|
|
|
79
80
|
@staticmethod
|
|
81
|
+
@_on_device_of("volume")
|
|
80
82
|
def forward(ctx, beam, volume, grad_sino, detector_spacing, voxel_spacing, *geometry):
|
|
81
83
|
return _geometry_vjp(beam, volume, grad_sino, geometry, detector_spacing, voxel_spacing)
|
|
82
84
|
|
|
@@ -119,23 +121,17 @@ class ParallelProjectorFunction(torch.autograd.Function):
|
|
|
119
121
|
Examples
|
|
120
122
|
--------
|
|
121
123
|
>>> import torch
|
|
122
|
-
>>> from diffct
|
|
124
|
+
>>> from diffct import ParallelProjectorFunction, circular_trajectory_2d_parallel
|
|
123
125
|
>>>
|
|
124
|
-
>>> # Create a 2D image with gradient tracking
|
|
125
126
|
>>> image = torch.randn(128, 128, device='cuda', requires_grad=True)
|
|
126
|
-
>>>
|
|
127
|
-
>>>
|
|
128
|
-
>>>
|
|
129
|
-
>>>
|
|
130
|
-
|
|
131
|
-
>>> projector = ParallelProjectorFunction.apply
|
|
132
|
-
>>> sinogram = projector(image, angles, num_detectors, detector_spacing)
|
|
133
|
-
>>> # Compute loss and gradients
|
|
134
|
-
>>> loss = sinogram.sum()
|
|
135
|
-
>>> loss.backward()
|
|
136
|
-
>>> print(f"Gradient shape: {image.grad.shape}") # (128, 128)
|
|
127
|
+
>>> ray_dir, det_origin, det_u_vec = circular_trajectory_2d_parallel(180, device='cuda')
|
|
128
|
+
>>> sinogram = ParallelProjectorFunction.apply(image, ray_dir, det_origin, det_u_vec, 192, 1.0)
|
|
129
|
+
>>> sinogram.sum().backward()
|
|
130
|
+
>>> image.grad.shape
|
|
131
|
+
torch.Size([128, 128])
|
|
137
132
|
"""
|
|
138
133
|
@staticmethod
|
|
134
|
+
@_on_device_of("image")
|
|
139
135
|
def forward(ctx, image, ray_dir, det_origin, det_u_vec, num_detectors, detector_spacing=1.0, voxel_spacing=1.0):
|
|
140
136
|
"""Compute the 2D parallel beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
141
137
|
|
|
@@ -254,16 +250,17 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
|
|
|
254
250
|
Examples
|
|
255
251
|
--------
|
|
256
252
|
>>> import torch
|
|
257
|
-
>>> from diffct
|
|
253
|
+
>>> from diffct import ParallelBackprojectorFunction, circular_trajectory_2d_parallel
|
|
258
254
|
>>>
|
|
259
|
-
>>> sinogram = torch.randn(180,
|
|
260
|
-
>>>
|
|
261
|
-
>>>
|
|
262
|
-
>>>
|
|
263
|
-
>>>
|
|
264
|
-
|
|
255
|
+
>>> sinogram = torch.randn(180, 192, device='cuda', requires_grad=True)
|
|
256
|
+
>>> ray_dir, det_origin, det_u_vec = circular_trajectory_2d_parallel(180, device='cuda')
|
|
257
|
+
>>> image = ParallelBackprojectorFunction.apply(sinogram, ray_dir, det_origin, det_u_vec, 1.0, 128, 128)
|
|
258
|
+
>>> image.sum().backward()
|
|
259
|
+
>>> sinogram.grad.shape
|
|
260
|
+
torch.Size([180, 192])
|
|
265
261
|
"""
|
|
266
262
|
@staticmethod
|
|
263
|
+
@_on_device_of("sinogram")
|
|
267
264
|
def forward(ctx, sinogram, ray_dir, det_origin, det_u_vec, detector_spacing=1.0, H=128, W=128, voxel_spacing=1.0):
|
|
268
265
|
"""Compute the 2D parallel beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
269
266
|
|
|
@@ -384,16 +381,17 @@ class FanProjectorFunction(torch.autograd.Function):
|
|
|
384
381
|
Examples
|
|
385
382
|
--------
|
|
386
383
|
>>> import torch
|
|
387
|
-
>>> from diffct
|
|
384
|
+
>>> from diffct import FanProjectorFunction, circular_trajectory_2d_fan
|
|
388
385
|
>>>
|
|
389
386
|
>>> image = torch.randn(256, 256, device='cuda', requires_grad=True)
|
|
390
|
-
>>>
|
|
391
|
-
>>> sinogram = FanProjectorFunction.apply(image,
|
|
392
|
-
>>>
|
|
393
|
-
>>>
|
|
394
|
-
|
|
387
|
+
>>> src_pos, det_center, det_u_vec = circular_trajectory_2d_fan(360, sid=1000.0, sdd=1500.0, device='cuda')
|
|
388
|
+
>>> sinogram = FanProjectorFunction.apply(image, src_pos, det_center, det_u_vec, 512, 1.0)
|
|
389
|
+
>>> sinogram.sum().backward()
|
|
390
|
+
>>> image.grad.shape
|
|
391
|
+
torch.Size([256, 256])
|
|
395
392
|
"""
|
|
396
393
|
@staticmethod
|
|
394
|
+
@_on_device_of("image")
|
|
397
395
|
def forward(ctx, image, src_pos, det_center, det_u_vec, num_detectors, detector_spacing, voxel_spacing=1.0):
|
|
398
396
|
"""Compute the 2D fan beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
399
397
|
|
|
@@ -511,16 +509,17 @@ class FanBackprojectorFunction(torch.autograd.Function):
|
|
|
511
509
|
Examples
|
|
512
510
|
--------
|
|
513
511
|
>>> import torch
|
|
514
|
-
>>> from diffct
|
|
512
|
+
>>> from diffct import FanBackprojectorFunction, circular_trajectory_2d_fan
|
|
515
513
|
>>>
|
|
516
514
|
>>> sinogram = torch.randn(360, 512, device='cuda', requires_grad=True)
|
|
517
|
-
>>>
|
|
518
|
-
>>>
|
|
519
|
-
>>>
|
|
520
|
-
>>>
|
|
521
|
-
|
|
515
|
+
>>> src_pos, det_center, det_u_vec = circular_trajectory_2d_fan(360, sid=1000.0, sdd=1500.0, device='cuda')
|
|
516
|
+
>>> image = FanBackprojectorFunction.apply(sinogram, src_pos, det_center, det_u_vec, 1.0, 256, 256)
|
|
517
|
+
>>> image.sum().backward()
|
|
518
|
+
>>> sinogram.grad.shape
|
|
519
|
+
torch.Size([360, 512])
|
|
522
520
|
"""
|
|
523
521
|
@staticmethod
|
|
522
|
+
@_on_device_of("sinogram")
|
|
524
523
|
def forward(ctx, sinogram, src_pos, det_center, det_u_vec, detector_spacing, H, W, voxel_spacing=1.0):
|
|
525
524
|
"""Compute the 2D fan beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
526
525
|
|
|
@@ -639,16 +638,19 @@ class ConeProjectorFunction(torch.autograd.Function):
|
|
|
639
638
|
Examples
|
|
640
639
|
--------
|
|
641
640
|
>>> import torch
|
|
642
|
-
>>> from diffct
|
|
641
|
+
>>> from diffct import ConeProjectorFunction, circular_trajectory_3d
|
|
643
642
|
>>>
|
|
644
643
|
>>> volume = torch.randn(128, 128, 128, device='cuda', requires_grad=True)
|
|
645
|
-
>>>
|
|
646
|
-
>>> projections = ConeProjectorFunction.apply(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
>>>
|
|
644
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(360, sid=1000.0, sdd=1500.0, device='cuda')
|
|
645
|
+
>>> projections = ConeProjectorFunction.apply(
|
|
646
|
+
... volume, src_pos, det_center, det_u_vec, det_v_vec, 256, 256, 1.0, 1.0
|
|
647
|
+
... )
|
|
648
|
+
>>> projections.sum().backward()
|
|
649
|
+
>>> volume.grad.shape
|
|
650
|
+
torch.Size([128, 128, 128])
|
|
650
651
|
"""
|
|
651
652
|
@staticmethod
|
|
653
|
+
@_on_device_of("volume")
|
|
652
654
|
def forward(ctx, volume, src_pos, det_center, det_u_vec, det_v_vec, det_u, det_v, du, dv, voxel_spacing=1.0):
|
|
653
655
|
"""Compute the 3D cone beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
654
656
|
|
|
@@ -784,20 +786,19 @@ class ConeBackprojectorFunction(torch.autograd.Function):
|
|
|
784
786
|
Examples
|
|
785
787
|
--------
|
|
786
788
|
>>> import torch
|
|
787
|
-
>>> from diffct
|
|
789
|
+
>>> from diffct import ConeBackprojectorFunction, circular_trajectory_3d
|
|
788
790
|
>>>
|
|
789
791
|
>>> projections = torch.randn(360, 256, 256, device='cuda', requires_grad=True)
|
|
790
|
-
>>>
|
|
791
|
-
>>>
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
>>>
|
|
795
|
-
>>>
|
|
796
|
-
|
|
797
|
-
>>> loss.backward()
|
|
798
|
-
>>> print(f"Projection gradient shape: {projections.grad.shape}") # (360, 256, 256)
|
|
792
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(360, sid=1000.0, sdd=1500.0, device='cuda')
|
|
793
|
+
>>> volume = ConeBackprojectorFunction.apply(
|
|
794
|
+
... projections, src_pos, det_center, det_u_vec, det_v_vec, 128, 128, 128, 1.0, 1.0
|
|
795
|
+
... )
|
|
796
|
+
>>> volume.sum().backward()
|
|
797
|
+
>>> projections.grad.shape
|
|
798
|
+
torch.Size([360, 256, 256])
|
|
799
799
|
"""
|
|
800
800
|
@staticmethod
|
|
801
|
+
@_on_device_of("sinogram")
|
|
801
802
|
def forward(ctx, sinogram, src_pos, det_center, det_u_vec, det_v_vec, D, H, W, du, dv, voxel_spacing=1.0):
|
|
802
803
|
"""Compute the 3D cone beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
803
804
|
|
diffct/utils.py
CHANGED
|
@@ -5,7 +5,12 @@ PyTorch-CUDA bridging, stream caching, trigonometric table generation,
|
|
|
5
5
|
memory layout validation, and CUDA grid computation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import atexit
|
|
9
|
+
import contextlib
|
|
10
|
+
import functools
|
|
11
|
+
import inspect
|
|
8
12
|
import math
|
|
13
|
+
import weakref
|
|
9
14
|
import numpy as np
|
|
10
15
|
import torch
|
|
11
16
|
from numba import cuda
|
|
@@ -150,6 +155,68 @@ def _get_numba_external_stream_for(pt_stream=None):
|
|
|
150
155
|
return numba_stream
|
|
151
156
|
|
|
152
157
|
|
|
158
|
+
@contextlib.contextmanager
|
|
159
|
+
def _cuda_context(device):
|
|
160
|
+
"""Make ``device`` the current device for PyTorch and Numba.
|
|
161
|
+
|
|
162
|
+
Kernel launches then use the CUDA context and the current stream of the
|
|
163
|
+
device that holds the data, not of the device that happens to be current.
|
|
164
|
+
"""
|
|
165
|
+
device = torch.device(device)
|
|
166
|
+
with torch.cuda.device(device), cuda.gpus[device.index]:
|
|
167
|
+
yield
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _on_device_of(name):
|
|
171
|
+
"""Run the decorated function in the CUDA context of its argument ``name``.
|
|
172
|
+
|
|
173
|
+
The argument may be passed by position or by keyword.
|
|
174
|
+
"""
|
|
175
|
+
def decorate(fn):
|
|
176
|
+
signature = inspect.signature(fn)
|
|
177
|
+
|
|
178
|
+
@functools.wraps(fn)
|
|
179
|
+
def wrapper(*args, **kwargs):
|
|
180
|
+
bound = signature.bind_partial(*args, **kwargs).arguments
|
|
181
|
+
tensor = bound[name] if name in bound else None
|
|
182
|
+
if not (isinstance(tensor, torch.Tensor) and tensor.is_cuda):
|
|
183
|
+
return fn(*args, **kwargs)
|
|
184
|
+
with _cuda_context(tensor.device):
|
|
185
|
+
return fn(*args, **kwargs)
|
|
186
|
+
return wrapper
|
|
187
|
+
return decorate
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _keep_cuda_modules_at_exit():
|
|
191
|
+
"""Stop numba-cuda from unloading CUDA modules while the interpreter exits.
|
|
192
|
+
|
|
193
|
+
numba-cuda queues module unloads and other deallocations, and flushes a
|
|
194
|
+
queue once it passes a count or byte limit. After kernels were loaded on
|
|
195
|
+
several GPUs, a flush during interpreter shutdown can unload the same
|
|
196
|
+
library twice and corrupt the heap, so the process crashes after all work
|
|
197
|
+
is done. The process exit releases these resources anyway.
|
|
198
|
+
|
|
199
|
+
Raising the limits is not enough: the byte limit is
|
|
200
|
+
``int(capacity * CUDA_DEALLOCS_RATIO)``, and queues with no capacity set
|
|
201
|
+
have a limit of 0 bytes for any finite ratio. numba-cuda skips the flush
|
|
202
|
+
while a queue is disabled, so disable every queue for the rest of the exit.
|
|
203
|
+
"""
|
|
204
|
+
from numba.cuda.cudadrv.driver import _PendingDeallocs
|
|
205
|
+
|
|
206
|
+
_PendingDeallocs.is_disabled = property(lambda self: True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _ExitOrderAnchor:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# weakref.finalize registers its atexit hook with its first finalizer. Create one
|
|
214
|
+
# now, so that the handler below is registered later and runs before it.
|
|
215
|
+
_EXIT_ORDER_ANCHOR = _ExitOrderAnchor()
|
|
216
|
+
weakref.finalize(_EXIT_ORDER_ANCHOR, lambda: None)
|
|
217
|
+
atexit.register(_keep_cuda_modules_at_exit)
|
|
218
|
+
|
|
219
|
+
|
|
153
220
|
# ============================================================================
|
|
154
221
|
# GPU-aware Trigonometric Table Generation
|
|
155
222
|
# ============================================================================
|
|
@@ -185,6 +252,8 @@ def _trig_tables(angles, dtype=_DTYPE, device=None):
|
|
|
185
252
|
"""
|
|
186
253
|
if isinstance(angles, torch.Tensor):
|
|
187
254
|
device = angles.device if device is None else device
|
|
255
|
+
if not isinstance(dtype, torch.dtype):
|
|
256
|
+
dtype = torch.from_numpy(np.zeros(0, dtype=dtype)).dtype
|
|
188
257
|
# Compute both cos and sin in one call to avoid redundant kernel launches
|
|
189
258
|
angles_device = angles.to(dtype=dtype, device=device)
|
|
190
259
|
cos = torch.cos(angles_device)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: diffct
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
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/
|
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
<h1 align="center">diffct</h1>
|
|
33
33
|
|
|
34
|
-
<p align="center">English · <a href="README.zh.md">简体中文</a></p>
|
|
34
|
+
<p align="center">English · <a href="https://github.com/sypsyp97/diffct/blob/main/README.zh.md">简体中文</a></p>
|
|
35
35
|
|
|
36
36
|
<p align="center">
|
|
37
37
|
Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.
|
|
@@ -43,15 +43,15 @@ Description-Content-Type: text/markdown
|
|
|
43
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
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
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>
|
|
46
|
+
<a href="https://deepwiki.com/sypsyp97/diffct"><img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/deepwiki-badge.svg" alt="Ask DeepWiki"></a>
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
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">
|
|
50
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/diffct_intro.gif" width="100%" alt="diffct intro: projection, sinogram, trajectories, multi-GPU split, reconstruction and geometry calibration">
|
|
51
51
|
</p>
|
|
52
52
|
|
|
53
53
|
<p align="center">
|
|
54
|
-
<a href="docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
|
|
54
|
+
<a href="https://github.com/sypsyp97/diffct/blob/main/docs/assets/diffct_intro.mp4">Intro video (MP4)</a> ·
|
|
55
55
|
<a href="https://www.preprints.org/manuscript/202605.1446/v1">Technical report</a> ·
|
|
56
56
|
<a href="https://doi.org/10.20944/preprints202605.1446.v1">DOI</a>
|
|
57
57
|
</p>
|
|
@@ -65,7 +65,9 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
<a href="#citation">Citation</a>
|
|
66
66
|
</p>
|
|
67
67
|
|
|
68
|
-
> **
|
|
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
|
+
> **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).
|
|
69
71
|
> For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
|
|
70
72
|
|
|
71
73
|
## Why diffct
|
|
@@ -76,18 +78,20 @@ To our knowledge, diffct is the only open-source GPU CT library that combines ar
|
|
|
76
78
|
- **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
79
|
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or one process per GPU with torchrun and NCCL. Views are partitioned and the volume is replicated. Speedup depends on workload and communication costs.
|
|
78
80
|
- **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
|
|
|
81
|
-
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](docs/REFERENCE.md#capabilities-and-limits).
|
|
82
|
+
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
|
|
82
83
|
|
|
83
84
|
## Install
|
|
84
85
|
|
|
85
|
-
You need
|
|
86
|
+
You need an NVIDIA GPU. Install PyTorch for your CUDA version first. Then install diffct with the extra for the same CUDA major version:
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
|
-
pip install "diffct[cu12]" #
|
|
89
|
+
pip install "diffct[cu12]" # PyTorch built for CUDA 12
|
|
90
|
+
pip install "diffct[cu13]" # PyTorch built for CUDA 13
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
The extra installs the CUDA compiler libraries that Numba CUDA needs to compile the kernels. A plain `pip install diffct` does not install them. Use it only if a CUDA Toolkit is already installed on the system.
|
|
94
|
+
|
|
91
95
|
From source, with the examples:
|
|
92
96
|
|
|
93
97
|
```bash
|
|
@@ -130,11 +134,11 @@ loss = 0.5 * (A.project(x) - sinogram).square().sum()
|
|
|
130
134
|
loss.backward() # x.grad = A^T (A x - y)
|
|
131
135
|
```
|
|
132
136
|
|
|
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).
|
|
137
|
+
**Custom or calibrated trajectories.** Pass calibrated tensors directly instead of a generator; no circular fit is needed. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#custom-or-calibrated-trajectories).
|
|
134
138
|
|
|
135
|
-
**Geometry gradients.** Set `requires_grad=True` on trajectory tensors before you build `Projector`. See [REFERENCE](docs/REFERENCE.md#geometry-gradients).
|
|
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).
|
|
136
140
|
|
|
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).
|
|
141
|
+
**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](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).
|
|
138
142
|
|
|
139
143
|
```python
|
|
140
144
|
A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, devices=[0, 1, 2, 3])
|
|
@@ -144,8 +148,8 @@ A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, dev
|
|
|
144
148
|
|
|
145
149
|
<p align="center">
|
|
146
150
|
<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">
|
|
151
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
|
|
152
|
+
<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, 64^3 and 128^3">
|
|
149
153
|
</picture>
|
|
150
154
|
</p>
|
|
151
155
|
|
|
@@ -155,7 +159,7 @@ A = Projector(trajectory, (128, 128, 128), (384, 256), detector_spacing=0.8, dev
|
|
|
155
159
|
| Adjoint (backprojection) | 20.66 ms | 5.89 ms (3.5×) | 4.62 ms (4.5×) |
|
|
156
160
|
| CGLS iteration | 30.08 ms | 8.08 ms (3.7×) | 5.59 ms (5.4×) |
|
|
157
161
|
|
|
158
|
-
Circular trajectory, 360 views, detector (2n, 1.5n) cells at pitch 1.25, A100 64 GB
|
|
162
|
+
Circular trajectory, 360 views, detector (2n, 1.5n) cells at pitch 1.25, A100 64 GB GPUs. 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](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
|
|
159
163
|
|
|
160
164
|
To measure one against several GPUs on your machine (default scan of the script, not the table setup):
|
|
161
165
|
|
|
@@ -168,7 +172,7 @@ python examples/benchmark_projector.py --devices 0 1
|
|
|
168
172
|
**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
173
|
|
|
170
174
|
<p align="center">
|
|
171
|
-
<img src="docs/assets/walnut_measured.png" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
|
|
175
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_measured.png" width="100%" alt="Measured walnut: FDK, SIRT, CGLS and TV reconstructions">
|
|
172
176
|
</p>
|
|
173
177
|
|
|
174
178
|
```bash
|
|
@@ -178,7 +182,7 @@ python examples/walnut_reconstruction.py --figure out.png
|
|
|
178
182
|
**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
183
|
|
|
180
184
|
<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">
|
|
185
|
+
<img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/walnut_helical.png" width="100%" alt="Simulated helical scan of the walnut: FDK, CGLS, SIRT and TV reconstructions">
|
|
182
186
|
</p>
|
|
183
187
|
|
|
184
188
|
```bash
|
|
@@ -186,23 +190,22 @@ python examples/walnut_reconstruction.py --algorithms --save-volume walnut256.np
|
|
|
186
190
|
python examples/iterative_reconstruction.py --size 256 --views 720 --trajectory helical --noise 0.01 --phantom walnut256.npy --figure out.png
|
|
187
191
|
```
|
|
188
192
|
|
|
189
|
-
Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](examples/data/NOTICE).
|
|
193
|
+
Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](https://github.com/sypsyp97/diffct/blob/main/examples/data/NOTICE).
|
|
190
194
|
|
|
191
195
|
## Examples
|
|
192
196
|
|
|
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
|
|
197
|
+
`quickstart.py`, `analytical_reconstruction.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
194
198
|
|
|
195
199
|
## Documentation
|
|
196
200
|
|
|
197
201
|
| Document | Contents |
|
|
198
202
|
|---|---|
|
|
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/
|
|
203
|
-
| [docs/
|
|
204
|
-
| [
|
|
205
|
-
| [CHANGELOG.md](CHANGELOG.md) | Release history |
|
|
203
|
+
| [Trajectory guide](https://github.com/sypsyp97/diffct/blob/main/docs/source/trajectories.rst) | Trajectory tuples and `Projector` usage |
|
|
204
|
+
| [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md) | Capabilities and limits, custom trajectories, geometry gradients, multi-GPU details, geometry and units |
|
|
205
|
+
| [docs/DISTRIBUTED.md](https://github.com/sypsyp97/diffct/blob/main/docs/DISTRIBUTED.md) | Execution/memory choices, distributed loss rules, Slurm and cross-node checks |
|
|
206
|
+
| [docs/MIGRATION.md](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md) | Moving from the circular-only API |
|
|
207
|
+
| [docs/video/README.md](https://github.com/sypsyp97/diffct/blob/main/docs/video/README.md) | How the intro video is rendered |
|
|
208
|
+
| [CHANGELOG.md](https://github.com/sypsyp97/diffct/blob/main/CHANGELOG.md) | Release history |
|
|
206
209
|
|
|
207
210
|
Run the test suite on a CUDA host with `python -m pytest tests/ -q`.
|
|
208
211
|
|
|
@@ -234,7 +237,7 @@ If you use this library in your research, please cite the software and the techn
|
|
|
234
237
|
|
|
235
238
|
## License and acknowledgements
|
|
236
239
|
|
|
237
|
-
Apache 2.0, see [LICENSE](LICENSE). The project draws on
|
|
240
|
+
Apache 2.0, see [LICENSE](https://github.com/sypsyp97/diffct/blob/main/LICENSE). The project draws on
|
|
238
241
|
[PYRO-NN](https://github.com/csyben/PYRO-NN) and
|
|
239
242
|
[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).
|
|
243
|
+
Issues and pull requests are welcome. The walnut data are CC BY 4.0 (Meaney 2022); see [NOTICE](https://github.com/sypsyp97/diffct/blob/main/NOTICE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
diffct/__init__.py,sha256=pt0oFJ4ntPNI3sDcV-2chCYoySpJ9lpCXHoYT7c0cDY,2191
|
|
2
|
+
diffct/analytical.py,sha256=cDJFMJOYzSaHLr9TUkiKtu5R6TR2d9b7GPZnCI48-VY,25206
|
|
3
|
+
diffct/constants.py,sha256=IKC_0u9aoNCG4M13KcRrl2plO0dTiokac-2aCYJJMb4,3060
|
|
4
|
+
diffct/differentiable.py,sha256=cFirhqXRpUemL7y7Sl_0uqZKGJNj__ptQEcTwYNKzys,1879
|
|
5
|
+
diffct/geometry.py,sha256=ZFXqrn2b1kmwzlXqDFGHPVOhHkuAsjRuFbUF8slnOYQ,38154
|
|
6
|
+
diffct/operators.py,sha256=UK3AoUSpt1n910QybmEt2MxoawNBkv4CHqyszRrQAGc,24922
|
|
7
|
+
diffct/projectors.py,sha256=C7Jp4IGuAtGkJQ6Z7ywpUpCsyptC6z1LRa9MSQrWU-E,41273
|
|
8
|
+
diffct/utils.py,sha256=wBAVsXBaBqNhyk4G0Q7qdCa-KQOOjZAd-ojvImJaK58,14496
|
|
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.2.dist-info/METADATA,sha256=viWNaRkii8-GcApKeTdj1vAjUvN9tBrP66VsXVkboWs,13315
|
|
15
|
+
diffct-2.0.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
diffct-2.0.2.dist-info/licenses/LICENSE,sha256=a1fjzGs44LpDnV_LmA2Tzp8z-eJKoNVVdLOnh-F6J6E,11344
|
|
17
|
+
diffct-2.0.2.dist-info/licenses/NOTICE,sha256=du9I2sDF5IDfiY9vF64WWzMJh-MarpVXyLfWQmhbvmI,714
|
|
18
|
+
diffct-2.0.2.dist-info/RECORD,,
|
diffct-2.0.0.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|