diffct 1.2.3__tar.gz → 1.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. diffct-1.2.5/.github/workflows/release.yml +20 -0
  2. {diffct-1.2.3 → diffct-1.2.5}/PKG-INFO +1 -1
  3. {diffct-1.2.3 → diffct-1.2.5}/diffct/differentiable.py +85 -110
  4. {diffct-1.2.3 → diffct-1.2.5}/examples/fbp_fan.py +3 -2
  5. {diffct-1.2.3 → diffct-1.2.5}/examples/fbp_parallel.py +3 -2
  6. {diffct-1.2.3 → diffct-1.2.5}/examples/fdk_cone.py +3 -2
  7. {diffct-1.2.3 → diffct-1.2.5}/pyproject.toml +1 -1
  8. {diffct-1.2.3 → diffct-1.2.5}/.github/workflows/docs.yml +0 -0
  9. {diffct-1.2.3 → diffct-1.2.5}/.gitignore +0 -0
  10. {diffct-1.2.3 → diffct-1.2.5}/LICENSE +0 -0
  11. {diffct-1.2.3 → diffct-1.2.5}/README.md +0 -0
  12. {diffct-1.2.3 → diffct-1.2.5}/diffct/__init__.py +0 -0
  13. {diffct-1.2.3 → diffct-1.2.5}/docs/Makefile +0 -0
  14. {diffct-1.2.3 → diffct-1.2.5}/docs/source/_static/.gitkeep +0 -0
  15. {diffct-1.2.3 → diffct-1.2.5}/docs/source/api.rst +0 -0
  16. {diffct-1.2.3 → diffct-1.2.5}/docs/source/conf.py +0 -0
  17. {diffct-1.2.3 → diffct-1.2.5}/docs/source/examples.rst +0 -0
  18. {diffct-1.2.3 → diffct-1.2.5}/docs/source/fbp_fan_example.rst +0 -0
  19. {diffct-1.2.3 → diffct-1.2.5}/docs/source/fbp_parallel_example.rst +0 -0
  20. {diffct-1.2.3 → diffct-1.2.5}/docs/source/fdk_cone_example.rst +0 -0
  21. {diffct-1.2.3 → diffct-1.2.5}/docs/source/getting_started.rst +0 -0
  22. {diffct-1.2.3 → diffct-1.2.5}/docs/source/index.rst +0 -0
  23. {diffct-1.2.3 → diffct-1.2.5}/docs/source/iterative_reco_cone_example.rst +0 -0
  24. {diffct-1.2.3 → diffct-1.2.5}/docs/source/iterative_reco_fan_example.rst +0 -0
  25. {diffct-1.2.3 → diffct-1.2.5}/docs/source/iterative_reco_parallel_example.rst +0 -0
  26. {diffct-1.2.3 → diffct-1.2.5}/examples/iterative_reco_cone.py +0 -0
  27. {diffct-1.2.3 → diffct-1.2.5}/examples/iterative_reco_fan.py +0 -0
  28. {diffct-1.2.3 → diffct-1.2.5}/examples/iterative_reco_parallel.py +0 -0
  29. {diffct-1.2.3 → diffct-1.2.5}/requirements.txt +0 -0
@@ -0,0 +1,20 @@
1
+ name: Create Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ name: Create Release
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: write
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+ - name: Create Release
18
+ uses: softprops/action-gh-release@v2
19
+ with:
20
+ generate_release_notes: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffct
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: A CUDA-based library for computed tomography (CT) projection and reconstruction with differentiable operators
5
5
  Project-URL: Homepage, https://github.com/sypsyp97/diffct
6
6
  Author-email: Yipeng Sun <yipeng.sun@fau.de>
@@ -19,6 +19,8 @@ _TPB_3D = (8, 8, 8)
19
19
  # Trades numerical precision for performance in ray-tracing calculations
20
20
  # Safe for CT reconstruction where slight precision loss is acceptable for speed gains
21
21
  _FASTMATH_DECORATOR = cuda.jit(cache=True, fastmath=True)
22
+ # Disable fastmath for backward kernels to ensure gradient correctness
23
+ _NON_FASTMATH_DECORATOR = cuda.jit(cache=True, fastmath=False)
22
24
  _INF = _DTYPE(np.inf)
23
25
  _EPSILON = _DTYPE(1e-6)
24
26
  # === Device Management Utilities ===
@@ -106,36 +108,9 @@ class TorchCUDABridge:
106
108
  raise ValueError("Tensor must be on CUDA device")
107
109
  return cuda.as_cuda_array(tensor.detach())
108
110
 
109
- @staticmethod
110
- def cuda_array_to_tensor(cuda_array, tensor_template):
111
- """Convert a Numba CUDA array to a PyTorch tensor.
112
-
113
- Wrap a Numba CUDA DeviceNDArray as a PyTorch tensor with matching device
114
- and dtype from a template tensor, sharing underlying memory.
115
-
116
- Parameters
117
- ----------
118
- cuda_array : numba.cuda.cudadrv.devicearray.DeviceNDArray
119
- Numba CUDA array to wrap.
120
- tensor_template : torch.Tensor
121
- Template tensor specifying device and dtype.
122
-
123
- Returns
124
- -------
125
- torch.Tensor
126
- PyTorch tensor sharing data with the CUDA array on the template's
127
- device and dtype.
128
-
129
- Examples
130
- --------
131
- >>> arr = cuda.device_array((10,), dtype=np.float32)
132
- >>> t = torch.zeros(10, device='cuda')
133
- >>> new_t = TorchCUDABridge.cuda_array_to_tensor(arr, t)
134
- """
135
- return torch.as_tensor(cuda_array, device=tensor_template.device, dtype=tensor_template.dtype)
136
111
 
137
112
  # === GPU-aware Trigonometric Table Generation ===
138
- def _trig_tables(angles, dtype=_DTYPE):
113
+ def _trig_tables(angles, dtype=_DTYPE, device=None):
139
114
  """Compute cosine and sine tables for input angles.
140
115
 
141
116
  Precompute cosine and sine values and return as torch tensors on the
@@ -163,7 +138,7 @@ def _trig_tables(angles, dtype=_DTYPE):
163
138
  device(type='cuda', index=0)
164
139
  """
165
140
  if isinstance(angles, torch.Tensor):
166
- device = angles.device
141
+ device = angles.device if device is None else device
167
142
  cos = torch.cos(angles).to(dtype=dtype)
168
143
  sin = torch.sin(angles).to(dtype=dtype)
169
144
  return cos.to(device), sin.to(device)
@@ -182,7 +157,10 @@ def _trig_tables(angles, dtype=_DTYPE):
182
157
  angles_cpu = torch.tensor(angles, dtype=torch_dtype)
183
158
  cos_cpu = torch.cos(angles_cpu)
184
159
  sin_cpu = torch.sin(angles_cpu)
185
- return cos_cpu, sin_cpu
160
+ if device is not None:
161
+ return cos_cpu.to(device), sin_cpu.to(device)
162
+ else:
163
+ return cos_cpu, sin_cpu
186
164
 
187
165
 
188
166
  # ############################################################################
@@ -191,14 +169,14 @@ def _trig_tables(angles, dtype=_DTYPE):
191
169
 
192
170
  def _validate_3d_memory_layout(tensor, expected_order='DHW'):
193
171
  """Validate 3D tensor memory layout to prevent coordinate system inconsistencies.
194
-
172
+
195
173
  Parameters
196
174
  ----------
197
175
  tensor : torch.Tensor
198
176
  3D tensor to validate
199
177
  expected_order : str, optional
200
178
  Expected memory order ('DHW', 'VHW', etc.). Default is 'DHW'.
201
-
179
+
202
180
  Raises
203
181
  ------
204
182
  ValueError
@@ -206,53 +184,50 @@ def _validate_3d_memory_layout(tensor, expected_order='DHW'):
206
184
  """
207
185
  if len(tensor.shape) != 3:
208
186
  raise ValueError(f"Expected 3D tensor, got {len(tensor.shape)}D")
209
-
187
+
210
188
  # Check if tensor is contiguous to avoid memory duplication
211
189
  if not tensor.is_contiguous():
212
190
  raise ValueError(
213
191
  "Input tensor must be contiguous. Call .contiguous() before passing to "
214
192
  "cone beam functions to avoid memory duplication and ensure correct results."
215
193
  )
216
-
217
- # Validate expected memory order based on stride patterns
218
- strides = tensor.stride()
219
-
220
- # Map expected orders to stride patterns
221
- order_mapping = {
222
- 'DHW': (0, 1, 2), # Depth, Height, Width
223
- 'VHW': (0, 1, 2), # Views, Height, Width (for sinograms)
224
- 'WHD': (2, 1, 0), # Width, Height, Depth (internal WHD format)
225
- }
226
-
227
- if expected_order not in order_mapping:
228
- raise ValueError(f"Unsupported expected_order: {expected_order}")
229
-
230
- expected_stride_order = order_mapping[expected_order]
231
-
232
- # Check if actual strides match expected order
233
- sorted_strides = sorted(enumerate(strides), key=lambda x: x[1], reverse=True)
234
- actual_order = tuple(idx for idx, _ in sorted_strides)
235
-
236
- if actual_order != expected_stride_order:
237
- # Create appropriate error message based on context
238
- if expected_order == 'VHW':
239
- actual_str = f"({tensor.shape[0]}, {tensor.shape[1]}, {tensor.shape[2]})"
240
- expected_str = "(Views, Height, Width)"
241
- fix_str = "ensure your sinogram has shape (num_views, det_v, det_u)"
242
- elif expected_order == 'DHW':
243
- actual_str = f"({tensor.shape[0]}, {tensor.shape[1]}, {tensor.shape[2]})"
244
- expected_str = "(Depth, Height, Width)"
245
- fix_str = "ensure your volume has shape (D, H, W)"
246
- else:
247
- actual_str = str(tuple(tensor.shape))
248
- expected_str = expected_order
249
- fix_str = "check tensor dimensions"
250
-
251
- raise ValueError(
252
- f"Memory layout mismatch: expected {expected_str} order, "
253
- f"but tensor has shape {actual_str}. Please {fix_str} and ensure "
254
- f"the tensor is contiguous (.contiguous()) before passing to the function."
255
- )
194
+
195
+ # Only check memory order for DHW and VHW, not for internal WHD layout
196
+ if expected_order in ('DHW', 'VHW'):
197
+ strides = tensor.stride()
198
+ order_mapping = {
199
+ 'DHW': (0, 1, 2), # Depth, Height, Width
200
+ 'VHW': (0, 1, 2), # Views, Height, Width (for sinograms)
201
+ }
202
+ if expected_order not in order_mapping:
203
+ raise ValueError(f"Unsupported expected_order: {expected_order}")
204
+
205
+ expected_stride_order = order_mapping[expected_order]
206
+ # Check if actual strides match expected order
207
+ sorted_strides = sorted(enumerate(strides), key=lambda x: x[1], reverse=True)
208
+ actual_order = tuple(idx for idx, _ in sorted_strides)
209
+
210
+ if actual_order != expected_stride_order:
211
+ # Create appropriate error message based on context
212
+ if expected_order == 'VHW':
213
+ actual_str = f"({tensor.shape[0]}, {tensor.shape[1]}, {tensor.shape[2]})"
214
+ expected_str = "(Views, Height, Width)"
215
+ fix_str = "ensure your sinogram has shape (num_views, det_v, det_u)"
216
+ elif expected_order == 'DHW':
217
+ actual_str = f"({tensor.shape[0]}, {tensor.shape[1]}, {tensor.shape[2]})"
218
+ expected_str = "(Depth, Height, Width)"
219
+ fix_str = "ensure your volume has shape (D, H, W)"
220
+ else:
221
+ actual_str = str(tuple(tensor.shape))
222
+ expected_str = expected_order
223
+ fix_str = "check tensor dimensions"
224
+
225
+ raise ValueError(
226
+ f"Memory layout mismatch: expected {expected_str} order, "
227
+ f"but tensor has shape {actual_str}. Please {fix_str} and ensure "
228
+ f"the tensor is contiguous (.contiguous()) before passing to the function."
229
+ )
230
+ # For 'WHD' (internal layout), skip stride check entirely
256
231
 
257
232
 
258
233
  def _grid_2d(n1, n2, tpb=_TPB_2D):
@@ -392,7 +367,7 @@ def _parallel_2d_forward_kernel(
392
367
  cos_a = d_cos[iang] # Precomputed cosine of projection angle
393
368
  sin_a = d_sin[iang] # Precomputed sine of projection angle
394
369
  # Normalize all physical distances to voxel units
395
- u = (idet - (n_det - 1) * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
370
+ u = (idet - n_det * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
396
371
 
397
372
  # Define ray direction and starting point for parallel beam geometry
398
373
  # Ray direction is perpendicular to detector array (cos_a, sin_a)
@@ -471,8 +446,8 @@ def _parallel_2d_forward_kernel(
471
446
  dx, dy = mid_x - ix0, mid_y - iy0 # Fractional parts: distance from base voxel center [0,1]
472
447
 
473
448
  # Clamp indices to stay in-bounds during interpolation
474
- ix0 = min(ix0, Nx - 2)
475
- iy0 = min(iy0, Ny - 2)
449
+ ix0 = max(0, min(ix0, Nx - 2))
450
+ iy0 = max(0, min(iy0, Ny - 2))
476
451
 
477
452
  # === BILINEAR INTERPOLATION WEIGHT CALCULATION ===
478
453
  # Mathematical basis: Bilinear interpolation formula f(x,y) = Σ f(xi,yi) * wi(x,y)
@@ -501,7 +476,7 @@ def _parallel_2d_forward_kernel(
501
476
 
502
477
  d_sino[iang, idet] = accum
503
478
 
504
- @_FASTMATH_DECORATOR
479
+ @_NON_FASTMATH_DECORATOR
505
480
  def _parallel_2d_backward_kernel(
506
481
  d_sino, n_ang, n_det,
507
482
  d_image, Nx, Ny,
@@ -554,7 +529,7 @@ def _parallel_2d_backward_kernel(
554
529
  cos_a = d_cos[iang] # Precomputed cosine of projection angle
555
530
  sin_a = d_sin[iang] # Precomputed sine of projection angle
556
531
  # Normalize all physical distances to voxel units
557
- u = (idet - (n_det - 1) * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
532
+ u = (idet - n_det * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
558
533
 
559
534
  # Define ray direction and starting point for parallel beam geometry
560
535
  dir_x, dir_y = cos_a, sin_a
@@ -599,8 +574,8 @@ def _parallel_2d_backward_kernel(
599
574
  dx, dy = mid_x - ix0, mid_y - iy0
600
575
 
601
576
  # Clamp indices to stay in-bounds during interpolation
602
- ix0 = min(ix0, Nx - 2)
603
- iy0 = min(iy0, Ny - 2)
577
+ ix0 = max(0, min(ix0, Nx - 2))
578
+ iy0 = max(0, min(iy0, Ny - 2))
604
579
 
605
580
  # === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
606
581
  # Distribute contribution weighted by segment length and interpolation weights
@@ -686,7 +661,7 @@ def _fan_2d_forward_kernel(
686
661
  cos_a = d_cos[iang] # Precomputed cosine of projection angle
687
662
  sin_a = d_sin[iang] # Precomputed sine of projection angle
688
663
  # Normalize all physical distances to voxel units
689
- u = (idet - (n_det - 1) * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
664
+ u = (idet - n_det * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
690
665
  sid_v = sid / voxel_spacing # Source-to-isocenter distance in voxel units
691
666
  sdd_v = sdd / voxel_spacing # Source-to-detector distance in voxel units
692
667
 
@@ -757,8 +732,8 @@ def _fan_2d_forward_kernel(
757
732
  dx, dy = mid_x - ix0, mid_y - iy0
758
733
 
759
734
  # Clamp indices to stay in-bounds during interpolation
760
- ix0 = min(ix0, Nx - 2)
761
- iy0 = min(iy0, Ny - 2)
735
+ ix0 = max(0, min(ix0, Nx - 2))
736
+ iy0 = max(0, min(iy0, Ny - 2))
762
737
 
763
738
  # Bilinear interpolation (identical to parallel beam)
764
739
  val = (
@@ -781,7 +756,7 @@ def _fan_2d_forward_kernel(
781
756
 
782
757
  d_sino[iang, idet] = accum
783
758
 
784
- @_FASTMATH_DECORATOR
759
+ @_NON_FASTMATH_DECORATOR
785
760
  def _fan_2d_backward_kernel(
786
761
  d_sino, n_ang, n_det,
787
762
  d_image, Nx, Ny,
@@ -839,7 +814,7 @@ def _fan_2d_backward_kernel(
839
814
  cos_a = d_cos[iang] # Precomputed cosine of projection angle
840
815
  sin_a = d_sin[iang] # Precomputed sine of projection angle
841
816
  # Normalize all physical distances to voxel units
842
- u = (idet - (n_det - 1) * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
817
+ u = (idet - n_det * 0.5) * det_spacing / voxel_spacing # Detector coordinate in voxel units
843
818
  sid_v = sid / voxel_spacing # Source-to-isocenter distance in voxel units
844
819
  sdd_v = sdd / voxel_spacing # Source-to-detector distance in voxel units
845
820
 
@@ -901,8 +876,8 @@ def _fan_2d_backward_kernel(
901
876
  dx, dy = mid_x - ix0, mid_y - iy0
902
877
 
903
878
  # Clamp indices to stay in-bounds during interpolation
904
- ix0 = min(ix0, Nx - 2)
905
- iy0 = min(iy0, Ny - 2)
879
+ ix0 = max(0, min(ix0, Nx - 2))
880
+ iy0 = max(0, min(iy0, Ny - 2))
906
881
 
907
882
  # === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
908
883
  # Distribute contribution weighted by segment length and interpolation weights
@@ -995,8 +970,8 @@ def _cone_3d_forward_kernel(
995
970
  # === 3D CONE BEAM GEOMETRY SETUP ===
996
971
  cos_a, sin_a = d_cos[iview], d_sin[iview] # Projection angle trigonometry
997
972
  # Normalize all physical distances to voxel units
998
- u = (iu - (n_u - 1) * 0.5) * du / voxel_spacing # Detector u-coordinate in voxel units
999
- v = (iv - (n_v - 1) * 0.5) * dv / voxel_spacing # Detector v-coordinate in voxel units
973
+ u = (iu - n_u * 0.5) * du / voxel_spacing # Detector u-coordinate in voxel units
974
+ v = (iv - n_v * 0.5) * dv / voxel_spacing # Detector v-coordinate in voxel units
1000
975
  sid_v = sid / voxel_spacing # Source-to-isocenter distance in voxel units
1001
976
  sdd_v = sdd / voxel_spacing # Source-to-detector distance in voxel units
1002
977
 
@@ -1091,9 +1066,9 @@ def _cone_3d_forward_kernel(
1091
1066
  dx, dy, dz = mid_x - ix0, mid_y - iy0, mid_z - iz0 # Fractional parts: distance from base voxel center [0,1]
1092
1067
 
1093
1068
  # Clamp indices to stay in-bounds during interpolation
1094
- ix0 = min(ix0, Nx - 2)
1095
- iy0 = min(iy0, Ny - 2)
1096
- iz0 = min(iz0, Nz - 2)
1069
+ ix0 = max(0, min(ix0, Nx - 2))
1070
+ iy0 = max(0, min(iy0, Ny - 2))
1071
+ iz0 = max(0, min(iz0, Nz - 2))
1097
1072
 
1098
1073
  # === TRILINEAR INTERPOLATION WEIGHT CALCULATION ===
1099
1074
  # Mathematical basis: Trilinear interpolation formula f(x,y,z) = Σ f(xi,yi,zi) * wi(x,y,z)
@@ -1131,7 +1106,7 @@ def _cone_3d_forward_kernel(
1131
1106
 
1132
1107
  d_sino[iview, iu, iv] = accum
1133
1108
 
1134
- @_FASTMATH_DECORATOR
1109
+ @_NON_FASTMATH_DECORATOR
1135
1110
  def _cone_3d_backward_kernel(
1136
1111
  d_sino, n_views, n_u, n_v,
1137
1112
  d_vol, Nx, Ny, Nz,
@@ -1196,8 +1171,8 @@ def _cone_3d_backward_kernel(
1196
1171
  g = d_sino[iview, iu, iv] # Sinogram value to backproject along this ray
1197
1172
  cos_a, sin_a = d_cos[iview], d_sin[iview] # Projection angle trigonometry
1198
1173
  # Normalize all physical distances to voxel units
1199
- u = (iu - (n_u - 1) * 0.5) * du / voxel_spacing # Detector u-coordinate in voxel units
1200
- v = (iv - (n_v - 1) * 0.5) * dv / voxel_spacing # Detector v-coordinate in voxel units
1174
+ u = (iu - n_u * 0.5) * du / voxel_spacing # Detector u-coordinate in voxel units
1175
+ v = (iv - n_v * 0.5) * dv / voxel_spacing # Detector v-coordinate in voxel units
1201
1176
  sid_v = sid / voxel_spacing # Source-to-isocenter distance in voxel units
1202
1177
  sdd_v = sdd / voxel_spacing # Source-to-detector distance in voxel units
1203
1178
 
@@ -1281,9 +1256,9 @@ def _cone_3d_backward_kernel(
1281
1256
  dx, dy, dz = mid_x - ix0, mid_y - iy0, mid_z - iz0 # Fractional parts for 3D weights
1282
1257
 
1283
1258
  # Clamp indices to stay in-bounds during interpolation
1284
- ix0 = min(ix0, Nx - 2)
1285
- iy0 = min(iy0, Ny - 2)
1286
- iz0 = min(iz0, Nz - 2)
1259
+ ix0 = max(0, min(ix0, Nx - 2))
1260
+ iy0 = max(0, min(iy0, Ny - 2))
1261
+ iz0 = max(0, min(iz0, Nz - 2))
1287
1262
 
1288
1263
  # === ATOMIC BACKPROJECTION WITH TRILINEAR WEIGHTS ===
1289
1264
  # Distribute contribution weighted by segment length and interpolation weights
@@ -1408,7 +1383,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
1408
1383
  sinogram = torch.zeros((n_angles, num_detectors), dtype=image.dtype, device=device)
1409
1384
 
1410
1385
  # Prepare trigonometric tables on the correct device
1411
- d_cos, d_sin = _trig_tables(angles, dtype=image.dtype)
1386
+ d_cos, d_sin = _trig_tables(angles, dtype=image.dtype, device=device)
1412
1387
 
1413
1388
  # Get Numba CUDA array views for kernel
1414
1389
  d_image = TorchCUDABridge.tensor_to_cuda_array(image)
@@ -1441,7 +1416,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
1441
1416
 
1442
1417
  n_angles = angles.shape[0]
1443
1418
  grad_image = torch.zeros((Ny, Nx), dtype=grad_sinogram.dtype, device=device)
1444
- d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype)
1419
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype, device=device)
1445
1420
 
1446
1421
  d_grad_sino = TorchCUDABridge.tensor_to_cuda_array(grad_sinogram)
1447
1422
  d_img_grad = TorchCUDABridge.tensor_to_cuda_array(grad_image)
@@ -1541,7 +1516,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
1541
1516
  reco = torch.zeros((Ny, Nx), dtype=sinogram.dtype, device=device)
1542
1517
 
1543
1518
  # Prepare trigonometric tables on the correct device
1544
- d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype)
1519
+ d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype, device=device)
1545
1520
 
1546
1521
  # Get Numba CUDA array views for kernel
1547
1522
  d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
@@ -1578,7 +1553,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
1578
1553
  grad_sino = torch.zeros((n_ang, n_det), dtype=grad_output.dtype, device=device)
1579
1554
 
1580
1555
  # Prepare trigonometric tables on the correct device
1581
- d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype)
1556
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype, device=device)
1582
1557
 
1583
1558
  # Get Numba CUDA array views for kernel
1584
1559
  d_grad_out = TorchCUDABridge.tensor_to_cuda_array(grad_output)
@@ -1679,7 +1654,7 @@ class FanProjectorFunction(torch.autograd.Function):
1679
1654
  n_ang = angles.shape[0]
1680
1655
 
1681
1656
  sinogram = torch.zeros((n_ang, num_detectors), dtype=image.dtype, device=device)
1682
- d_cos, d_sin = _trig_tables(angles, dtype=image.dtype)
1657
+ d_cos, d_sin = _trig_tables(angles, dtype=image.dtype, device=device)
1683
1658
 
1684
1659
  d_image = TorchCUDABridge.tensor_to_cuda_array(image)
1685
1660
  d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
@@ -1713,7 +1688,7 @@ class FanProjectorFunction(torch.autograd.Function):
1713
1688
 
1714
1689
  n_ang = angles.shape[0]
1715
1690
  grad_img = torch.zeros((Ny, Nx), dtype=grad_sinogram.dtype, device=device)
1716
- d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype)
1691
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype, device=device)
1717
1692
 
1718
1693
  d_grad_sino = TorchCUDABridge.tensor_to_cuda_array(grad_sinogram)
1719
1694
  d_img_grad = TorchCUDABridge.tensor_to_cuda_array(grad_img)
@@ -1817,7 +1792,7 @@ class FanBackprojectorFunction(torch.autograd.Function):
1817
1792
  Ny, Nx = H, W
1818
1793
 
1819
1794
  reco = torch.zeros((Ny, Nx), dtype=sinogram.dtype, device=device)
1820
- d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype)
1795
+ d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype, device=device)
1821
1796
 
1822
1797
  d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
1823
1798
  d_reco = TorchCUDABridge.tensor_to_cuda_array(reco)
@@ -1851,7 +1826,7 @@ class FanBackprojectorFunction(torch.autograd.Function):
1851
1826
  Ny, Nx = grad_output.shape
1852
1827
 
1853
1828
  grad_sino = torch.zeros((n_ang, n_det), dtype=grad_output.dtype, device=device)
1854
- d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype)
1829
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype, device=device)
1855
1830
 
1856
1831
  d_grad_out = TorchCUDABridge.tensor_to_cuda_array(grad_output)
1857
1832
  d_sino_grad = TorchCUDABridge.tensor_to_cuda_array(grad_sino)
@@ -1959,7 +1934,7 @@ class ConeProjectorFunction(torch.autograd.Function):
1959
1934
  _validate_3d_memory_layout(volume, expected_order='DHW')
1960
1935
 
1961
1936
  sino = torch.zeros((n_views, det_u, det_v), dtype=volume.dtype, device=device)
1962
- d_cos, d_sin = _trig_tables(angles, dtype=volume.dtype)
1937
+ d_cos, d_sin = _trig_tables(angles, dtype=volume.dtype, device=device)
1963
1938
 
1964
1939
  volume_perm = volume.permute(2, 1, 0).contiguous()
1965
1940
  d_vol = TorchCUDABridge.tensor_to_cuda_array(volume_perm)
@@ -1997,7 +1972,7 @@ class ConeProjectorFunction(torch.autograd.Function):
1997
1972
  n_views = angles.shape[0]
1998
1973
 
1999
1974
  grad_vol_perm = torch.zeros((W, H, D), dtype=grad_sinogram.dtype, device=device)
2000
- d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype)
1975
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_sinogram.dtype, device=device)
2001
1976
 
2002
1977
  d_grad_sino = TorchCUDABridge.tensor_to_cuda_array(grad_sinogram)
2003
1978
  d_vol_grad = TorchCUDABridge.tensor_to_cuda_array(grad_vol_perm)
@@ -2116,7 +2091,7 @@ class ConeBackprojectorFunction(torch.autograd.Function):
2116
2091
  _validate_3d_memory_layout(sinogram, expected_order='VHW')
2117
2092
 
2118
2093
  vol_perm = torch.zeros((W, H, D), dtype=sinogram.dtype, device=device)
2119
- d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype)
2094
+ d_cos, d_sin = _trig_tables(angles, dtype=sinogram.dtype, device=device)
2120
2095
 
2121
2096
  d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
2122
2097
  d_reco = TorchCUDABridge.tensor_to_cuda_array(vol_perm)
@@ -2153,7 +2128,7 @@ class ConeBackprojectorFunction(torch.autograd.Function):
2153
2128
  n_views = angles.shape[0]
2154
2129
 
2155
2130
  grad_sino = torch.zeros((n_views, n_u, n_v), dtype=grad_output.dtype, device=device)
2156
- d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype)
2131
+ d_cos, d_sin = _trig_tables(angles, dtype=grad_output.dtype, device=device)
2157
2132
 
2158
2133
  grad_output_perm = grad_output.permute(2, 1, 0).contiguous()
2159
2134
  d_grad_out = TorchCUDABridge.tensor_to_cuda_array(grad_output_perm)
@@ -2,6 +2,7 @@ import math
2
2
  import numpy as np
3
3
  import torch
4
4
  import matplotlib.pyplot as plt
5
+ import torch.nn.functional as F
5
6
  from diffct.differentiable import FanProjectorFunction, FanBackprojectorFunction
6
7
 
7
8
 
@@ -76,9 +77,9 @@ def main():
76
77
  sino_weighted = sinogram * weights
77
78
  sinogram_filt = ramp_filter(sino_weighted)
78
79
 
79
- reconstruction = FanBackprojectorFunction.apply(sinogram_filt, angles_torch,
80
+ reconstruction = F.relu(FanBackprojectorFunction.apply(sinogram_filt, angles_torch,
80
81
  detector_spacing, Ny, Nx,
81
- sdd, sid, voxel_spacing)
82
+ sdd, sid, voxel_spacing)) # ReLU to ensure non-negativity
82
83
 
83
84
  # --- FBP normalization ---
84
85
  # The backprojection is a sum over all angles. To approximate the integral,
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import torch
3
3
  import matplotlib.pyplot as plt
4
+ import torch.nn.functional as F
4
5
  from diffct.differentiable import ParallelProjectorFunction, ParallelBackprojectorFunction
5
6
 
6
7
 
@@ -64,8 +65,8 @@ def main():
64
65
 
65
66
  sinogram_filt = ramp_filter(sinogram)
66
67
 
67
- reconstruction = ParallelBackprojectorFunction.apply(sinogram_filt, angles_torch,
68
- detector_spacing, Ny, Nx, voxel_spacing)
68
+ reconstruction = F.relu(ParallelBackprojectorFunction.apply(sinogram_filt, angles_torch,
69
+ detector_spacing, Ny, Nx, voxel_spacing)) # ReLU to ensure non-negativity
69
70
 
70
71
  # --- FBP normalization ---
71
72
  # The backprojection is a sum over all angles. To approximate the integral,
@@ -2,6 +2,7 @@ import math
2
2
  import numpy as np
3
3
  import torch
4
4
  import matplotlib.pyplot as plt
5
+ import torch.nn.functional as F
5
6
  from diffct.differentiable import ConeProjectorFunction, ConeBackprojectorFunction
6
7
 
7
8
 
@@ -110,8 +111,8 @@ def main():
110
111
  sino_weighted = sinogram * weights
111
112
  sinogram_filt = ramp_filter_3d(sino_weighted).contiguous()
112
113
 
113
- reconstruction = ConeBackprojectorFunction.apply(sinogram_filt, angles_torch, Nz, Ny, Nx,
114
- du, dv, sdd, sid, voxel_spacing)
114
+ reconstruction = F.relu(ConeBackprojectorFunction.apply(sinogram_filt, angles_torch, Nz, Ny, Nx,
115
+ du, dv, sdd, sid, voxel_spacing)) # ReLU to ensure non-negativity
115
116
 
116
117
  # --- FDK normalization ---
117
118
  # The backprojection is a sum over all angles. To approximate the integral,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "diffct"
7
- version = "1.2.3"
7
+ version = "1.2.5"
8
8
  description = "A CUDA-based library for computed tomography (CT) projection and reconstruction with differentiable operators"
9
9
  readme = "README.md"
10
10
  authors = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes