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 CHANGED
@@ -49,7 +49,7 @@ try:
49
49
  except ImportError:
50
50
  pass
51
51
 
52
- __version__ = '2.0.0'
52
+ __version__ = '2.0.2'
53
53
 
54
54
  __all__ = [
55
55
  'Projector',
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
- >>> # Define a custom figure-8 trajectory
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) * torch.sin(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.differentiable import ParallelProjectorFunction
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
- >>> # Define projection parameters
127
- >>> angles = torch.linspace(0, torch.pi, 180, device='cuda')
128
- >>> num_detectors = 128
129
- >>> detector_spacing = 1.0
130
- >>> # Compute forward projection
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.differentiable import ParallelBackprojectorFunction
253
+ >>> from diffct import ParallelBackprojectorFunction, circular_trajectory_2d_parallel
258
254
  >>>
259
- >>> sinogram = torch.randn(180, 128, device='cuda', requires_grad=True)
260
- >>> angles = torch.linspace(0, torch.pi, 180, device='cuda')
261
- >>> recon = ParallelBackprojectorFunction.apply(sinogram, angles, 1.0, 128, 128)
262
- >>> loss = recon.sum()
263
- >>> loss.backward()
264
- >>> print(sinogram.grad.shape) # (180, 128)
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.differentiable import FanProjectorFunction
384
+ >>> from diffct import FanProjectorFunction, circular_trajectory_2d_fan
388
385
  >>>
389
386
  >>> image = torch.randn(256, 256, device='cuda', requires_grad=True)
390
- >>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
391
- >>> sinogram = FanProjectorFunction.apply(image, angles, 512, 1.0, 1500.0, 1000.0)
392
- >>> loss = sinogram.sum()
393
- >>> loss.backward()
394
- >>> print(image.grad.shape) # (256, 256)
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.differentiable import FanBackprojectorFunction
512
+ >>> from diffct import FanBackprojectorFunction, circular_trajectory_2d_fan
515
513
  >>>
516
514
  >>> sinogram = torch.randn(360, 512, device='cuda', requires_grad=True)
517
- >>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
518
- >>> recon = FanBackprojectorFunction.apply(sinogram, angles, 1.0, 256, 256, 1500.0, 1000.0)
519
- >>> loss = recon.sum()
520
- >>> loss.backward()
521
- >>> print(sinogram.grad.shape) # (360, 512)
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.differentiable import ConeProjectorFunction
641
+ >>> from diffct import ConeProjectorFunction, circular_trajectory_3d
643
642
  >>>
644
643
  >>> volume = torch.randn(128, 128, 128, device='cuda', requires_grad=True)
645
- >>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
646
- >>> projections = ConeProjectorFunction.apply(volume, angles, 256, 256, 1.0, 1.0, 1500.0, 1000.0)
647
- >>> loss = projections.sum()
648
- >>> loss.backward()
649
- >>> print(volume.grad.shape) # (128, 128, 128)
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.differentiable import ConeBackprojectorFunction
789
+ >>> from diffct import ConeBackprojectorFunction, circular_trajectory_3d
788
790
  >>>
789
791
  >>> projections = torch.randn(360, 256, 256, device='cuda', requires_grad=True)
790
- >>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
791
- >>> D, H, W = 128, 128, 128
792
- >>> du, dv = 1.0, 1.0
793
- >>> sdd, sid = 1500.0, 1000.0
794
- >>> backprojector = ConeBackprojectorFunction.apply
795
- >>> volume = backprojector(projections, angles, D, H, W, du, dv, sdd, sid)
796
- >>> loss = volume.sum()
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.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
- > **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).
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 a CUDA GPU and PyTorch. Install PyTorch for your CUDA version first.
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]" # [cu13] for CUDA 13; install PyTorch for your CUDA first
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 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).
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, distributed-loss rules and measured results are in [examples/README.md](examples/README.md).
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/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 |
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,,
@@ -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