diffct 1.3.3__tar.gz → 1.3.4__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 (58) hide show
  1. {diffct-1.3.3 → diffct-1.3.4}/CHANGELOG.md +31 -0
  2. {diffct-1.3.3 → diffct-1.3.4}/PKG-INFO +1 -1
  3. {diffct-1.3.3 → diffct-1.3.4}/diffct/differentiable.py +78 -258
  4. {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_cone.py +12 -14
  5. {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_fan.py +8 -9
  6. {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_parallel.py +7 -8
  7. {diffct-1.3.3 → diffct-1.3.4}/pyproject.toml +1 -1
  8. {diffct-1.3.3 → diffct-1.3.4}/tests/test_fdk_cone_accuracy.py +6 -4
  9. {diffct-1.3.3 → diffct-1.3.4}/.git +0 -0
  10. {diffct-1.3.3 → diffct-1.3.4}/.github/workflows/docs.yml +0 -0
  11. {diffct-1.3.3 → diffct-1.3.4}/.github/workflows/release.yml +0 -0
  12. {diffct-1.3.3 → diffct-1.3.4}/.gitignore +0 -0
  13. {diffct-1.3.3 → diffct-1.3.4}/LICENSE +0 -0
  14. {diffct-1.3.3 → diffct-1.3.4}/README.md +0 -0
  15. {diffct-1.3.3 → diffct-1.3.4}/README.zh.md +0 -0
  16. {diffct-1.3.3 → diffct-1.3.4}/diffct/__init__.py +0 -0
  17. {diffct-1.3.3 → diffct-1.3.4}/docs/Makefile +0 -0
  18. {diffct-1.3.3 → diffct-1.3.4}/docs/source/_static/.gitkeep +0 -0
  19. {diffct-1.3.3 → diffct-1.3.4}/docs/source/api.rst +0 -0
  20. {diffct-1.3.3 → diffct-1.3.4}/docs/source/conf.py +0 -0
  21. {diffct-1.3.3 → diffct-1.3.4}/docs/source/examples.rst +0 -0
  22. {diffct-1.3.3 → diffct-1.3.4}/docs/source/fbp_fan_example.rst +0 -0
  23. {diffct-1.3.3 → diffct-1.3.4}/docs/source/fbp_parallel_example.rst +0 -0
  24. {diffct-1.3.3 → diffct-1.3.4}/docs/source/fdk_cone_example.rst +0 -0
  25. {diffct-1.3.3 → diffct-1.3.4}/docs/source/getting_started.rst +0 -0
  26. {diffct-1.3.3 → diffct-1.3.4}/docs/source/index.rst +0 -0
  27. {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_cone_example.rst +0 -0
  28. {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_fan_example.rst +0 -0
  29. {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_parallel_example.rst +0 -0
  30. {diffct-1.3.3 → diffct-1.3.4}/examples/data/NOTICE +0 -0
  31. {diffct-1.3.3 → diffct-1.3.4}/examples/data/preprocess_walnut.py +0 -0
  32. {diffct-1.3.3 → diffct-1.3.4}/examples/data/walnut_cone.npz +0 -0
  33. {diffct-1.3.3 → diffct-1.3.4}/examples/data/walnut_reco.png +0 -0
  34. {diffct-1.3.3 → diffct-1.3.4}/examples/fbp_fan.py +0 -0
  35. {diffct-1.3.3 → diffct-1.3.4}/examples/fbp_parallel.py +0 -0
  36. {diffct-1.3.3 → diffct-1.3.4}/examples/fdk_cone.py +0 -0
  37. {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fbp_fan.py +0 -0
  38. {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fbp_parallel.py +0 -0
  39. {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fdk_cone.py +0 -0
  40. {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_walnut_fdk.py +0 -0
  41. {diffct-1.3.3 → diffct-1.3.4}/pytest.ini +0 -0
  42. {diffct-1.3.3 → diffct-1.3.4}/requirements.txt +0 -0
  43. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/README.md +0 -0
  44. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/__init__.py +0 -0
  45. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/_common.py +0 -0
  46. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_cone.py +0 -0
  47. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_fan.py +0 -0
  48. {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_parallel.py +0 -0
  49. {diffct-1.3.3 → diffct-1.3.4}/tests/test_adjoint_inner_product.py +0 -0
  50. {diffct-1.3.3 → diffct-1.3.4}/tests/test_cone_projector_autograd.py +0 -0
  51. {diffct-1.3.3 → diffct-1.3.4}/tests/test_cuda_smoke.py +0 -0
  52. {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_fan_accuracy.py +0 -0
  53. {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_fan_offsets.py +0 -0
  54. {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_parallel_accuracy.py +0 -0
  55. {diffct-1.3.3 → diffct-1.3.4}/tests/test_fdk_cone_offsets.py +0 -0
  56. {diffct-1.3.3 → diffct-1.3.4}/tests/test_gradcheck.py +0 -0
  57. {diffct-1.3.3 → diffct-1.3.4}/tests/test_ramp_filter_windows.py +0 -0
  58. {diffct-1.3.3 → diffct-1.3.4}/tests/test_weights.py +0 -0
@@ -5,6 +5,37 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.4] - 2026-04-23
9
+
10
+ ### Changed
11
+ - **Siddon projectors switched to cell-constant integration.** The 2D
12
+ parallel, 2D fan, and 3D cone Siddon forward/backward kernels in
13
+ `diffct/differentiable.py` now integrate each ray by accumulating
14
+ `d_image[iy, ix] * seg_len` on the traversed pixel/voxel instead of
15
+ sampling the bi-/tri-linear interpolant at the segment midpoint.
16
+ This simplifies the inner loop, removes the interpolation weight
17
+ bookkeeping, and keeps the forward / adjoint pair matched by
18
+ construction (still verified by
19
+ `tests/test_adjoint_inner_product.py`). Kernel docstrings and the
20
+ `"siddon"` backend blurbs in `examples/iterative_reco_*.py` have
21
+ been updated to say *cell-constant Siddon*.
22
+ - **Iterative examples clamp the reconstruction in-place** instead of
23
+ wrapping the forward pass in `nn.ReLU`. `examples/iterative_reco_parallel.py`,
24
+ `iterative_reco_fan.py`, and `iterative_reco_cone.py` now call
25
+ `self.model.reco.clamp_(min=0.0)` under `torch.no_grad()` after each
26
+ optimizer step, so gradients flow through the unclamped volume while
27
+ the stored state stays non-negative.
28
+ - **`examples/iterative_reco_fan.py` default backend** is now
29
+ `"siddon"` (was `"sf"`), matching the parallel and cone iterative
30
+ examples.
31
+
32
+ ### Fixed
33
+ - **FDK cone accuracy test tolerance.** `tests/test_fdk_cone_accuracy.py`
34
+ raises the maximum central z-profile error bound from `0.6` to `0.65`
35
+ to reflect the slightly sharper one-sample transition error at
36
+ ellipsoid boundaries under cell-constant forward projection; global
37
+ FDK RMSE is unchanged.
38
+
8
39
  ## [1.3.3] - 2026-04-23
9
40
 
10
41
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffct
3
- Version: 1.3.3
3
+ Version: 1.3.4
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>
@@ -618,7 +618,7 @@ def _parallel_2d_forward_kernel(
618
618
  ):
619
619
  """Compute the 2D parallel beam forward projection.
620
620
 
621
- This CUDA kernel implements the Siddon ray-tracing method with interpolation for
621
+ This CUDA kernel implements cell-constant Siddon ray tracing for
622
622
  2D parallel beam forward projection.
623
623
 
624
624
  Parameters
@@ -650,11 +650,10 @@ def _parallel_2d_forward_kernel(
650
650
 
651
651
  Notes
652
652
  -----
653
- The Siddon method with interpolation provides accurate ray-volume intersection by:
654
- - Calculating ray-volume boundary intersections to define traversal limits.
655
- - Iterating through voxels along the ray path via parametric equations.
656
- - Determining bilinear interpolation weights for sub-voxel sampling.
657
- - Aggregating weighted voxel values based on ray segment lengths.
653
+ The cell-constant Siddon method provides accurate ray-volume intersection by:
654
+ - Calculating ray-volume boundary intersections to define traversal limits.
655
+ - Iterating through voxels along the ray path via parametric equations.
656
+ - Aggregating each traversed pixel value based on ray segment lengths.
658
657
  """
659
658
  # CUDA THREAD ORGANIZATION: 2D grid maps directly to ray geometry
660
659
  # Each thread processes one ray defined by (projection_angle, detector_element) pair
@@ -731,49 +730,15 @@ def _parallel_2d_forward_kernel(
731
730
  tx = (np.float32(next_ix) - cx - pnt_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
732
731
  ty = (np.float32(next_iy) - cy - pnt_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
733
732
 
734
- # === MAIN RAY TRAVERSAL LOOP ===
735
- # Step through voxels along ray path, accumulating weighted contributions
736
- while t < t_max:
737
- # Check if current voxel indices are within valid interpolation bounds
738
- if 0 <= ix < Nx and 0 <= iy < Ny:
739
- # Determine next voxel boundary crossing (minimum of x, y boundaries or ray exit)
740
- t_next = min(tx, ty, t_max)
741
- seg_len = t_next - t # Length of ray segment within current voxel region
742
-
743
- if seg_len > _EPSILON: # Only process segments with meaningful length (avoid numerical noise)
744
- # === BILINEAR INTERPOLATION SAMPLING ===
745
- # Sample volume at ray segment midpoint for accurate integration
746
- # Mathematical basis: Midpoint rule for numerical integration along ray segments
747
- t_mid = t + seg_len * _HALF
748
- mid_x = pnt_x + t_mid * dir_x + cx # Midpoint x-coordinate in image space
749
- mid_y = pnt_y + t_mid * dir_y + cy # Midpoint y-coordinate in image space
750
-
751
- # Convert continuous coordinates to discrete voxel indices and fractional weights
752
- # Floor operation gives base voxel index, fractional part gives interpolation weights
753
- ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y)) # Base voxel indices (bottom-left corner)
754
- dx = mid_x - np.float32(ix0) # Fractional parts: distance from base voxel center [0,1]
755
- dy = mid_y - np.float32(iy0)
756
-
757
- # Clamp indices to stay in-bounds during interpolation
758
- ix0 = max(0, min(ix0, Nx - 2))
759
- iy0 = max(0, min(iy0, Ny - 2))
760
-
761
- # === BILINEAR INTERPOLATION WEIGHT CALCULATION ===
762
- # Mathematical basis: Bilinear interpolation formula f(x,y) = Σ f(xi,yi) * wi(x,y)
763
- # where wi(x,y) are the bilinear basis functions for each corner voxel
764
- # Weights are products of 1D linear interpolation weights: (1-dx) or dx, (1-dy) or dy
765
- one_minus_dx = _ONE - dx
766
- one_minus_dy = _ONE - dy
767
- v00 = d_image[iy0, ix0]
768
- v10 = d_image[iy0, ix0 + 1]
769
- v01 = d_image[iy0 + 1, ix0]
770
- v11 = d_image[iy0 + 1, ix0 + 1]
771
- row0 = (v00 * one_minus_dx + v10 * dx) * one_minus_dy
772
- row1 = (v01 * one_minus_dx + v11 * dx) * dy
773
- val = row0 + row1
774
- # Accumulate contribution weighted by ray segment length (discrete line integral approximation)
775
- # This implements the Radon transform: integral of f(x,y) along the ray path
776
- accum += val * seg_len
733
+ # === MAIN RAY TRAVERSAL LOOP ===
734
+ # Step through voxels along ray path, accumulating cell-constant contributions.
735
+ while t < t_max:
736
+ if 0 <= ix < Nx and 0 <= iy < Ny:
737
+ # Determine next voxel boundary crossing (minimum of x, y boundaries or ray exit)
738
+ t_next = min(tx, ty, t_max)
739
+ seg_len = t_next - t # Length of ray segment within current voxel region
740
+ if seg_len > _EPSILON: # Only process segments with meaningful length (avoid numerical noise)
741
+ accum += d_image[iy, ix] * seg_len
777
742
 
778
743
  # === VOXEL BOUNDARY CROSSING LOGIC ===
779
744
  # Advance to next voxel based on which boundary is crossed first
@@ -797,7 +762,7 @@ def _parallel_2d_backward_kernel(
797
762
  ):
798
763
  """Compute the 2D parallel beam backprojection.
799
764
 
800
- This CUDA kernel implements the Siddon ray-tracing method with interpolation for
765
+ This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
801
766
  2D parallel beam backprojection.
802
767
 
803
768
  Parameters
@@ -878,39 +843,14 @@ def _parallel_2d_backward_kernel(
878
843
  tx = (np.float32(next_ix) - cx - pnt_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
879
844
  ty = (np.float32(next_iy) - cy - pnt_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
880
845
 
881
- # === BACKPROJECTION TRAVERSAL LOOP ===
882
- # Distribute sinogram value along ray path using bilinear interpolation
883
- while t < t_max:
884
- if 0 <= ix < Nx and 0 <= iy < Ny:
885
- t_next = min(tx, ty, t_max)
886
- seg_len = t_next - t
887
- if seg_len > _EPSILON:
888
- # Sample at ray segment midpoint (same as forward projection)
889
- t_mid = t + seg_len * _HALF
890
- mid_x = pnt_x + t_mid * dir_x + cx
891
- mid_y = pnt_y + t_mid * dir_y + cy
892
- ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
893
- dx = mid_x - np.float32(ix0)
894
- dy = mid_y - np.float32(iy0)
895
-
896
- # Clamp indices to stay in-bounds during interpolation
897
- ix0 = max(0, min(ix0, Nx - 2))
898
- iy0 = max(0, min(iy0, Ny - 2))
899
-
900
- # === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
901
- # Distribute contribution weighted by segment length and interpolation weights
902
- # CUDA ATOMIC OPERATIONS: Essential for thread safety in backprojection
903
- # Multiple threads (rays) can write to the same voxel simultaneously, causing race conditions
904
- # Atomic add operations serialize these writes, ensuring correct accumulation of contributions
905
- # Performance impact: Atomic operations are slower than regular writes but necessary for correctness
906
- # Memory access pattern: Global memory atomics with potential bank conflicts, but unavoidable
907
- cval = val * seg_len # Contribution value for this ray segment
908
- one_minus_dx = _ONE - dx
909
- one_minus_dy = _ONE - dy
910
- cuda.atomic.add(d_image, (iy0, ix0), cval * one_minus_dx * one_minus_dy)
911
- cuda.atomic.add(d_image, (iy0, ix0 + 1), cval * dx * one_minus_dy)
912
- cuda.atomic.add(d_image, (iy0 + 1, ix0), cval * one_minus_dx * dy)
913
- cuda.atomic.add(d_image, (iy0 + 1, ix0 + 1), cval * dx * dy)
846
+ # === BACKPROJECTION TRAVERSAL LOOP ===
847
+ # Adjoint of the cell-constant Siddon forward projection.
848
+ while t < t_max:
849
+ if 0 <= ix < Nx and 0 <= iy < Ny:
850
+ t_next = min(tx, ty, t_max)
851
+ seg_len = t_next - t
852
+ if seg_len > _EPSILON:
853
+ cuda.atomic.add(d_image, (iy, ix), val * seg_len)
914
854
 
915
855
  # Advance to next voxel (identical logic to forward projection)
916
856
  if tx <= ty:
@@ -936,7 +876,7 @@ def _fan_2d_forward_kernel(
936
876
  ):
937
877
  """Compute the 2D fan beam forward projection.
938
878
 
939
- This CUDA kernel implements the Siddon ray-tracing method with interpolation for
879
+ This CUDA kernel implements cell-constant Siddon ray tracing for
940
880
  2D fan beam forward projection.
941
881
 
942
882
  Parameters
@@ -1046,35 +986,13 @@ def _fan_2d_forward_kernel(
1046
986
  tx = (np.float32(next_ix) - cx - src_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
1047
987
  ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
1048
988
 
1049
- # Main traversal loop with bilinear interpolation (identical to parallel beam)
1050
- while t < t_max:
1051
- if 0 <= ix < Nx and 0 <= iy < Ny:
1052
- t_next = min(tx, ty, t_max)
1053
- seg_len = t_next - t
1054
- if seg_len > _EPSILON:
1055
- # Sample at midpoint using source as ray origin
1056
- t_mid = t + seg_len * _HALF
1057
- mid_x = src_x + t_mid * dir_x + cx
1058
- mid_y = src_y + t_mid * dir_y + cy
1059
- ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
1060
- dx = mid_x - np.float32(ix0)
1061
- dy = mid_y - np.float32(iy0)
1062
-
1063
- # Clamp indices to stay in-bounds during interpolation
1064
- ix0 = max(0, min(ix0, Nx - 2))
1065
- iy0 = max(0, min(iy0, Ny - 2))
1066
-
1067
- # Bilinear interpolation (identical to parallel beam)
1068
- one_minus_dx = _ONE - dx
1069
- one_minus_dy = _ONE - dy
1070
- v00 = d_image[iy0, ix0]
1071
- v10 = d_image[iy0, ix0 + 1]
1072
- v01 = d_image[iy0 + 1, ix0]
1073
- v11 = d_image[iy0 + 1, ix0 + 1]
1074
- row0 = (v00 * one_minus_dx + v10 * dx) * one_minus_dy
1075
- row1 = (v01 * one_minus_dx + v11 * dx) * dy
1076
- val = row0 + row1
1077
- accum += val * seg_len
989
+ # Main traversal loop with cell-constant Siddon integration.
990
+ while t < t_max:
991
+ if 0 <= ix < Nx and 0 <= iy < Ny:
992
+ t_next = min(tx, ty, t_max)
993
+ seg_len = t_next - t
994
+ if seg_len > _EPSILON:
995
+ accum += d_image[iy, ix] * seg_len
1078
996
 
1079
997
  # Voxel boundary crossing logic (identical to parallel beam)
1080
998
  if tx <= ty:
@@ -1098,7 +1016,7 @@ def _fan_2d_backward_kernel(
1098
1016
  ):
1099
1017
  """Pure adjoint ``P^T`` of the fan-beam forward projector.
1100
1018
 
1101
- Siddon ray-driven scatter with bilinear interpolation weights and no
1019
+ Cell-constant Siddon ray-driven scatter with no
1102
1020
  distance weighting. Used by ``FanProjectorFunction.backward`` and
1103
1021
  ``FanBackprojectorFunction.forward``. The analytical FBP path lives
1104
1022
  in ``_fan_2d_fbp_backproject_kernel`` / ``fan_weighted_backproject``
@@ -1165,37 +1083,14 @@ def _fan_2d_backward_kernel(
1165
1083
  tx = (np.float32(next_ix) - cx - src_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
1166
1084
  ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
1167
1085
 
1168
- # === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
1169
- # Distribute sinogram value along divergent ray path using bilinear interpolation
1170
- while t < t_max:
1171
- if 0 <= ix < Nx and 0 <= iy < Ny:
1172
- t_next = min(tx, ty, t_max)
1173
- seg_len = t_next - t
1174
- if seg_len > _EPSILON:
1175
- # Sample at ray segment midpoint using source as ray origin
1176
- t_mid = t + seg_len * _HALF
1177
- mid_x = src_x + t_mid * dir_x + cx
1178
- mid_y = src_y + t_mid * dir_y + cy
1179
- ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
1180
- dx = mid_x - np.float32(ix0)
1181
- dy = mid_y - np.float32(iy0)
1182
-
1183
- # Clamp indices to stay in-bounds during interpolation
1184
- ix0 = max(0, min(ix0, Nx - 2))
1185
- iy0 = max(0, min(iy0, Ny - 2))
1186
-
1187
- # === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
1188
- # Pure adjoint scatter: contribution is just val * seg_len,
1189
- # distributed onto the four nearest pixels with bilinear
1190
- # weights. No distance weighting - that belongs to the
1191
- # analytical FBP kernel, not this adjoint.
1192
- cval = val * seg_len
1193
- one_minus_dx = _ONE - dx
1194
- one_minus_dy = _ONE - dy
1195
- cuda.atomic.add(d_image, (iy0, ix0), cval * one_minus_dx * one_minus_dy)
1196
- cuda.atomic.add(d_image, (iy0, ix0 + 1), cval * dx * one_minus_dy)
1197
- cuda.atomic.add(d_image, (iy0 + 1, ix0), cval * one_minus_dx * dy)
1198
- cuda.atomic.add(d_image, (iy0 + 1, ix0 + 1), cval * dx * dy)
1086
+ # === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
1087
+ # Adjoint of the cell-constant Siddon forward projection.
1088
+ while t < t_max:
1089
+ if 0 <= ix < Nx and 0 <= iy < Ny:
1090
+ t_next = min(tx, ty, t_max)
1091
+ seg_len = t_next - t
1092
+ if seg_len > _EPSILON:
1093
+ cuda.atomic.add(d_image, (iy, ix), val * seg_len)
1199
1094
 
1200
1095
  # === VOXEL BOUNDARY CROSSING LOGIC ===
1201
1096
  # Advance to next voxel based on which boundary is crossed first
@@ -1533,7 +1428,7 @@ def _cone_3d_forward_kernel(
1533
1428
  ):
1534
1429
  """Compute the 3D cone-beam forward projection.
1535
1430
 
1536
- This CUDA kernel implements the Siddon ray-tracing method with interpolation for
1431
+ This CUDA kernel implements cell-constant Siddon ray tracing for
1537
1432
  3D cone-beam forward projection.
1538
1433
 
1539
1434
  Parameters
@@ -1577,9 +1472,9 @@ def _cone_3d_forward_kernel(
1577
1472
 
1578
1473
  Notes
1579
1474
  -----
1580
- Cone-beam geometry extends the fan-beam configuration to 3D by employing
1581
- a 2D detector array and trilinear interpolation for accurate volumetric
1582
- sampling.
1475
+ Cone-beam geometry extends the fan-beam configuration to 3D by employing
1476
+ a 2D detector array and integrating each ray through piecewise-constant
1477
+ voxel cells.
1583
1478
  """
1584
1479
  # Put detector-v on threadIdx.x; d_sino[view, u, v] is row-major with v
1585
1480
  # as the stride-1 axis, and adjacent v rays also tend to walk nearby z cells.
@@ -1673,52 +1568,14 @@ def _cone_3d_forward_kernel(
1673
1568
  ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
1674
1569
  tz = (np.float32(next_iz) - cz - src_z) * inv_dir_z if abs(dir_z) > _EPSILON else _INF
1675
1570
 
1676
- # === 3D TRAVERSAL LOOP WITH TRILINEAR INTERPOLATION ===
1677
- while t < t_max:
1678
- # Check if current 3D voxel indices are within valid interpolation bounds
1679
- if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
1680
- # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
1681
- t_next = min(tx, ty, tz, t_max)
1682
- seg_len = t_next - t
1683
- if seg_len > _EPSILON:
1684
- # === TRILINEAR INTERPOLATION SAMPLING ===
1685
- # Sample 3D volume at ray segment midpoint for accurate integration
1686
- # Mathematical basis: Midpoint rule for numerical integration along 3D ray segments
1687
- t_mid = t + seg_len * _HALF
1688
- mid_x = src_x + t_mid * dir_x + cx # Midpoint x-coordinate in volume space
1689
- mid_y = src_y + t_mid * dir_y + cy # Midpoint y-coordinate in volume space
1690
- mid_z = src_z + t_mid * dir_z + cz # Midpoint z-coordinate in volume space
1691
-
1692
- # Convert continuous 3D coordinates to discrete voxel indices and fractional weights
1693
- ix0, iy0, iz0 = int(math.floor(mid_x)), int(math.floor(mid_y)), int(math.floor(mid_z))
1694
- dx = mid_x - np.float32(ix0)
1695
- dy = mid_y - np.float32(iy0)
1696
- dz = mid_z - np.float32(iz0)
1697
-
1698
- # Clamp indices to stay in-bounds during interpolation
1699
- ix0 = max(0, min(ix0, Nx - 2))
1700
- iy0 = max(0, min(iy0, Ny - 2))
1701
- iz0 = max(0, min(iz0, Nz - 2))
1702
-
1703
- # Precompute complements
1704
- omdx = _ONE - dx
1705
- omdy = _ONE - dy
1706
- omdz = _ONE - dz
1707
-
1708
- # === TRILINEAR INTERPOLATION WEIGHT CALCULATION ===
1709
- val = (
1710
- d_vol[ix0, iy0, iz0] * omdx*omdy*omdz +
1711
- d_vol[ix0 + 1, iy0, iz0] * dx *omdy*omdz +
1712
- d_vol[ix0, iy0 + 1, iz0] * omdx*dy *omdz +
1713
- d_vol[ix0, iy0, iz0 + 1] * omdx*omdy*dz +
1714
- d_vol[ix0 + 1, iy0 + 1, iz0] * dx *dy *omdz +
1715
- d_vol[ix0 + 1, iy0, iz0 + 1] * dx *omdy*dz +
1716
- d_vol[ix0, iy0 + 1, iz0 + 1] * omdx*dy *dz +
1717
- d_vol[ix0 + 1, iy0 + 1, iz0 + 1] * dx *dy *dz
1718
- )
1719
- # Accumulate contribution weighted by 3D ray segment length (discrete line integral approximation)
1720
- # This implements the 3D Radon transform: integral of f(x,y,z) along the ray path
1721
- accum += val * seg_len
1571
+ # === 3D TRAVERSAL LOOP WITH CELL-CONSTANT SIDDON INTEGRATION ===
1572
+ while t < t_max:
1573
+ if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
1574
+ # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
1575
+ t_next = min(tx, ty, tz, t_max)
1576
+ seg_len = t_next - t
1577
+ if seg_len > _EPSILON:
1578
+ accum += d_vol[ix, iy, iz] * seg_len
1722
1579
 
1723
1580
  # === 3D VOXEL BOUNDARY CROSSING LOGIC ===
1724
1581
  # Advance to next voxel based on which boundary is crossed first in 3D
@@ -1748,7 +1605,7 @@ def _cone_3d_backward_kernel(
1748
1605
  ):
1749
1606
  """Pure adjoint ``P^T`` of the cone-beam forward projector.
1750
1607
 
1751
- Siddon ray-driven scatter with trilinear interpolation weights and no
1608
+ Cell-constant Siddon ray-driven scatter with no
1752
1609
  distance weighting. Used by ``ConeProjectorFunction.backward`` and
1753
1610
  ``ConeBackprojectorFunction.forward``. The analytical FDK path lives
1754
1611
  in ``_cone_3d_fdk_backproject_kernel`` / ``cone_weighted_backproject``
@@ -1836,51 +1693,15 @@ def _cone_3d_backward_kernel(
1836
1693
  ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
1837
1694
  tz = (np.float32(next_iz) - cz - src_z) * inv_dir_z if abs(dir_z) > _EPSILON else _INF
1838
1695
 
1839
- # === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
1840
- # Distribute sinogram value along divergent 3D ray path using trilinear interpolation
1841
- while t < t_max:
1842
- # Check if current 3D voxel indices are within valid interpolation bounds
1843
- if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
1844
- # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
1845
- t_next = min(tx, ty, tz, t_max)
1846
- seg_len = t_next - t
1847
- if seg_len > _EPSILON:
1848
- # === TRILINEAR INTERPOLATION SAMPLING ===
1849
- # Sample 3D volume at ray segment midpoint using source as ray origin
1850
- t_mid = t + seg_len * _HALF
1851
- mid_x = src_x + t_mid * dir_x + cx
1852
- mid_y = src_y + t_mid * dir_y + cy
1853
- mid_z = src_z + t_mid * dir_z + cz
1854
-
1855
- # Convert continuous 3D coordinates to voxel indices and interpolation weights
1856
- ix0, iy0, iz0 = int(math.floor(mid_x)), int(math.floor(mid_y)), int(math.floor(mid_z))
1857
- dx = mid_x - np.float32(ix0)
1858
- dy = mid_y - np.float32(iy0)
1859
- dz = mid_z - np.float32(iz0)
1860
-
1861
- # Clamp indices to stay in-bounds during interpolation
1862
- ix0 = max(0, min(ix0, Nx - 2))
1863
- iy0 = max(0, min(iy0, Ny - 2))
1864
- iz0 = max(0, min(iz0, Nz - 2))
1865
-
1866
- # Precompute complements and contribution. Pure adjoint:
1867
- # cval = g * seg_len, distributed onto the eight nearest
1868
- # voxels with trilinear weights. Distance weighting lives
1869
- # in the analytical FDK kernel, not here.
1870
- omdx = _ONE - dx
1871
- omdy = _ONE - dy
1872
- omdz = _ONE - dz
1873
- cval = g * seg_len
1874
-
1875
- # === ATOMIC BACKPROJECTION WITH TRILINEAR WEIGHTS ===
1876
- cuda.atomic.add(d_vol, (ix0, iy0, iz0), cval * omdx*omdy*omdz)
1877
- cuda.atomic.add(d_vol, (ix0 + 1, iy0, iz0), cval * dx *omdy*omdz)
1878
- cuda.atomic.add(d_vol, (ix0, iy0 + 1, iz0), cval * omdx*dy *omdz)
1879
- cuda.atomic.add(d_vol, (ix0, iy0, iz0 + 1), cval * omdx*omdy*dz)
1880
- cuda.atomic.add(d_vol, (ix0 + 1, iy0 + 1, iz0), cval * dx *dy *omdz)
1881
- cuda.atomic.add(d_vol, (ix0 + 1, iy0, iz0 + 1), cval * dx *omdy*dz)
1882
- cuda.atomic.add(d_vol, (ix0, iy0 + 1, iz0 + 1), cval * omdx*dy *dz)
1883
- cuda.atomic.add(d_vol, (ix0 + 1, iy0 + 1, iz0 + 1), cval * dx *dy *dz)
1696
+ # === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
1697
+ # Adjoint of the cell-constant Siddon forward projection.
1698
+ while t < t_max:
1699
+ if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
1700
+ # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
1701
+ t_next = min(tx, ty, tz, t_max)
1702
+ seg_len = t_next - t
1703
+ if seg_len > _EPSILON:
1704
+ cuda.atomic.add(d_vol, (ix, iy, iz), g * seg_len)
1884
1705
 
1885
1706
  # === 3D VOXEL BOUNDARY CROSSING LOGIC ===
1886
1707
  # Advance to next voxel based on which boundary is crossed first in 3D
@@ -3708,7 +3529,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
3708
3529
  Notes
3709
3530
  -----
3710
3531
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
3711
- method with interpolation for parallel beam CT geometry. The forward pass computes
3532
+ method with a cell-constant image basis for parallel beam CT geometry. The forward pass computes
3712
3533
  the sinogram from a 2D image using parallel beam geometry. The backward pass
3713
3534
  computes gradients using the adjoint backprojection operation. Requires
3714
3535
  CUDA-capable hardware and a properly configured CUDA environment; all input
@@ -3770,7 +3591,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
3770
3591
  -----
3771
3592
  - All input tensors must be on the same CUDA device.
3772
3593
  - The operation is fully differentiable and supports autograd.
3773
- - Uses the Siddon method with interpolation for accurate ray tracing and bilinear interpolation.
3594
+ - Uses cell-constant Siddon ray tracing.
3774
3595
 
3775
3596
  Examples
3776
3597
  --------
@@ -3888,7 +3709,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
3888
3709
  Notes
3889
3710
  -----
3890
3711
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
3891
- method with interpolation for parallel beam backprojection. The forward pass computes a 2D
3712
+ method with a cell-constant image basis for parallel beam backprojection. The forward pass computes a 2D
3892
3713
  reconstruction from sinogram data using parallel beam backprojection, and the
3893
3714
  backward pass computes gradients via forward projection as the adjoint operation.
3894
3715
  Requires CUDA-capable hardware and consistent device placements.
@@ -3946,7 +3767,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
3946
3767
  -----
3947
3768
  - All input tensors must be on the same CUDA device.
3948
3769
  - The operation is fully differentiable and supports autograd.
3949
- - Uses the Siddon method with interpolation for accurate ray tracing and bilinear interpolation.
3770
+ - Uses the adjoint of cell-constant Siddon ray tracing.
3950
3771
 
3951
3772
  Examples
3952
3773
  --------
@@ -4070,7 +3891,7 @@ class FanProjectorFunction(torch.autograd.Function):
4070
3891
  Notes
4071
3892
  -----
4072
3893
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
4073
- method with interpolation for fan beam geometry, where rays diverge from a point
3894
+ method with a cell-constant image basis for fan beam geometry, where rays diverge from a point
4074
3895
  X-ray source to a linear detector array. The forward pass computes sinograms
4075
3896
  using divergent beam geometry. The backward pass returns the **pure adjoint**
4076
3897
  ``P^T`` (Siddon scatter, no distance weighting) so that it is the correct
@@ -4112,8 +3933,7 @@ class FanProjectorFunction(torch.autograd.Function):
4112
3933
  ----------
4113
3934
  backend : str, optional
4114
3935
  Forward projector backend. ``"siddon"`` (default) is the existing
4115
- ray-driven Siddon kernel with bilinear interpolation at segment
4116
- midpoints. ``"sf"`` is a prototype separable-footprint (SF-TR)
3936
+ ray-driven cell-constant Siddon kernel. ``"sf"`` is a prototype separable-footprint (SF-TR)
4117
3937
  voxel-driven projector that projects each voxel's footprint as a
4118
3938
  trapezoid on the detector and integrates it closed-form over each
4119
3939
  detector cell; it is mass-conserving and closer to the physical
@@ -4149,7 +3969,7 @@ class FanProjectorFunction(torch.autograd.Function):
4149
3969
  - All input tensors must be on the same CUDA device.
4150
3970
  - The operation is fully differentiable and supports autograd.
4151
3971
  - Fan beam geometry uses divergent rays from a point source to the detector.
4152
- - Uses the Siddon method with interpolation for accurate ray tracing and bilinear interpolation.
3972
+ - Uses cell-constant Siddon ray tracing.
4153
3973
 
4154
3974
  Examples
4155
3975
  --------
@@ -4301,9 +4121,9 @@ class FanBackprojectorFunction(torch.autograd.Function):
4301
4121
  Notes
4302
4122
  -----
4303
4123
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
4304
- method with interpolation for fan beam backprojection. The forward pass runs
4124
+ method with a cell-constant image basis for fan beam backprojection. The forward pass runs
4305
4125
  the **pure adjoint** ``P^T`` of the fan forward projector (Siddon scatter
4306
- with bilinear interpolation, no distance weighting), and the backward pass
4126
+ with no distance weighting), and the backward pass
4307
4127
  computes gradients via forward projection. The forward/backward pair is
4308
4128
  therefore a self-consistent autograd adjoint pair. Analytical FBP distance
4309
4129
  weighting lives in ``fan_weighted_backproject`` and is *not* applied here.
@@ -4378,7 +4198,7 @@ class FanBackprojectorFunction(torch.autograd.Function):
4378
4198
  - All input tensors must be on the same CUDA device.
4379
4199
  - The operation is fully differentiable and supports autograd.
4380
4200
  - Fan beam geometry uses divergent rays from a point source to the detector.
4381
- - Uses the Siddon method with interpolation for accurate ray tracing and bilinear interpolation.
4201
+ - Uses the adjoint of cell-constant Siddon ray tracing.
4382
4202
 
4383
4203
  Examples
4384
4204
  --------
@@ -4530,11 +4350,11 @@ class ConeProjectorFunction(torch.autograd.Function):
4530
4350
  Notes
4531
4351
  -----
4532
4352
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
4533
- method with interpolation for 3D cone beam geometry. Rays emanate from a point
4353
+ method with a cell-constant voxel basis for 3D cone beam geometry. Rays emanate from a point
4534
4354
  X-ray source to a 2D detector array capturing volumetric projection data.
4535
4355
  The forward pass computes 3D projections. The backward pass returns the
4536
4356
  **pure adjoint** ``P^T`` of the forward projector - a Siddon ray-driven
4537
- scatter with trilinear interpolation weights and no distance weighting -
4357
+ scatter with no distance weighting -
4538
4358
  so that ``P^T`` is the mathematically correct gradient of ``y = P(x)``
4539
4359
  with respect to ``x``. Analytical FDK distance weighting is handled
4540
4360
  separately in ``cone_weighted_backproject``, not here.
@@ -4621,7 +4441,7 @@ class ConeProjectorFunction(torch.autograd.Function):
4621
4441
  - All input tensors must be on the same CUDA device.
4622
4442
  - The operation is fully differentiable and supports autograd.
4623
4443
  - Cone beam geometry uses a point source and a 2D detector array.
4624
- - Uses the Siddon method with interpolation for accurate 3D ray tracing and trilinear interpolation.
4444
+ - Uses cell-constant Siddon ray tracing.
4625
4445
 
4626
4446
  Examples
4627
4447
  --------
@@ -4814,9 +4634,9 @@ class ConeBackprojectorFunction(torch.autograd.Function):
4814
4634
  Notes
4815
4635
  -----
4816
4636
  Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
4817
- method with interpolation for 3D cone beam backprojection. The forward pass
4637
+ method with a cell-constant voxel basis for 3D cone beam backprojection. The forward pass
4818
4638
  runs the **pure adjoint** ``P^T`` of the cone forward projector: a Siddon
4819
- ray-driven scatter with trilinear interpolation weights and no distance
4639
+ ray-driven scatter with no distance
4820
4640
  weighting. The backward pass computes gradients via 3D cone beam forward
4821
4641
  projection, which is exactly the adjoint of this pure ``P^T`` - so the
4822
4642
  forward/backward pair is self-consistent for autograd. Analytical FDK
@@ -4910,7 +4730,7 @@ class ConeBackprojectorFunction(torch.autograd.Function):
4910
4730
  - All input tensors must be on the same CUDA device.
4911
4731
  - The operation is fully differentiable and supports autograd.
4912
4732
  - Cone beam geometry uses a point source and a 2D detector array.
4913
- - Uses the Siddon method with interpolation for accurate 3D ray tracing and trilinear interpolation.
4733
+ - Uses the adjoint of cell-constant Siddon ray tracing.
4914
4734
 
4915
4735
  Examples
4916
4736
  --------
@@ -71,11 +71,8 @@ class IterativeRecoModel(nn.Module):
71
71
  self.dv = dv
72
72
  self.sdd = sdd
73
73
  self.sid = sid
74
- # Output clamp for display; the projector uses the unconstrained
75
- # volume so gradients keep flowing through negative updates.
76
- self.relu = nn.ReLU()
77
- self.voxel_spacing = voxel_spacing
78
- self.backend = backend
74
+ self.voxel_spacing = voxel_spacing
75
+ self.backend = backend
79
76
 
80
77
  def forward(self, x):
81
78
  updated_reco = x + self.reco
@@ -94,7 +91,7 @@ class IterativeRecoModel(nn.Module):
94
91
  0.0, 0.0, 0.0, # center_offset_x, y, z
95
92
  self.backend,
96
93
  )
97
- return current_sino, self.relu(updated_reco)
94
+ return current_sino, updated_reco
98
95
 
99
96
  class Pipeline:
100
97
  def __init__(self, lr, volume_shape, angles,
@@ -115,10 +112,12 @@ class Pipeline:
115
112
  for epoch in range(self.epoches):
116
113
  self.optimizer.zero_grad()
117
114
  predictions, current_reco = self.model(input)
118
- loss_value = self.loss(predictions, label)
119
- loss_value.backward()
120
- self.optimizer.step()
121
- loss_values.append(loss_value.item())
115
+ loss_value = self.loss(predictions, label)
116
+ loss_value.backward()
117
+ self.optimizer.step()
118
+ with torch.no_grad():
119
+ self.model.reco.clamp_(min=0.0)
120
+ loss_values.append(loss_value.item())
122
121
 
123
122
  if epoch % 10 == 0:
124
123
  print(f"Epoch {epoch}, Loss: {loss_value.item()}")
@@ -145,10 +144,9 @@ def main():
145
144
  # byte-for-byte (matched scatter/gather kernel pair, verified by
146
145
  # tests/test_adjoint_inner_product.py). Options:
147
146
  #
148
- # "siddon" - 3D ray-driven Siddon with trilinear
149
- # interpolation. Fastest per-iteration step.
150
- # Good default when iteration count is the
151
- # bottleneck.
147
+ # "siddon" - 3D ray-driven cell-constant Siddon. Fastest
148
+ # per-iteration step. Good default when
149
+ # iteration count is the bottleneck.
152
150
  # "sf_tr" - 3D SF with trapezoidal transaxial and
153
151
  # rectangular axial footprint. Mass-
154
152
  # conserving per voxel, closed-form cell
@@ -47,9 +47,6 @@ class IterativeRecoModel(nn.Module):
47
47
  self.detector_spacing = detector_spacing
48
48
  self.sdd = sdd
49
49
  self.sid = sid
50
- # Output clamp for display; the projector uses the unconstrained
51
- # image so gradients keep flowing through negative updates.
52
- self.relu = nn.ReLU()
53
50
  self.voxel_spacing = voxel_spacing
54
51
  self.backend = backend
55
52
 
@@ -72,7 +69,7 @@ class IterativeRecoModel(nn.Module):
72
69
  0.0, # center_offset_y
73
70
  self.backend,
74
71
  )
75
- return current_sino, self.relu(updated_reco)
72
+ return current_sino, updated_reco
76
73
 
77
74
  class Pipeline:
78
75
  def __init__(self, lr, volume_shape, angles,
@@ -96,6 +93,8 @@ class Pipeline:
96
93
  loss_value = self.loss(predictions, label)
97
94
  loss_value.backward()
98
95
  self.optimizer.step()
96
+ with torch.no_grad():
97
+ self.model.reco.clamp_(min=0.0)
99
98
  loss_values.append(loss_value.item())
100
99
 
101
100
  if epoch % 10 == 0:
@@ -123,9 +122,9 @@ def main():
123
122
  # byte-for-byte (guaranteed by the matched scatter/gather kernel
124
123
  # pair, verified by tests/test_adjoint_inner_product.py). Options:
125
124
  #
126
- # "siddon" - ray-driven Siddon with bilinear voxel
127
- # interpolation. Fastest. Good default when
128
- # you want the shortest iteration step.
125
+ # "siddon" - ray-driven cell-constant Siddon. Fastest.
126
+ # Good default when you want the shortest
127
+ # iteration step.
129
128
  # "sf" - voxel-driven separable-footprint projector
130
129
  # (Long et al. SF-TR). Mass-conserving per
131
130
  # voxel, closed-form cell integral, ~3x
@@ -133,7 +132,7 @@ def main():
133
132
  # want a physically-principled cell-
134
133
  # integrated forward model and don't mind
135
134
  # the per-iteration cost.
136
- projector_backend = "sf"
135
+ projector_backend = "siddon"
137
136
 
138
137
  device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
139
138
  phantom_torch = torch.tensor(phantom_cpu, device=device, dtype=torch.float32)
@@ -185,4 +184,4 @@ def main():
185
184
  plt.show()
186
185
 
187
186
  if __name__ == "__main__":
188
- main()
187
+ main()
@@ -43,15 +43,12 @@ class IterativeRecoModel(nn.Module):
43
43
  self.num_detectors = num_detectors
44
44
  self.detector_spacing = detector_spacing
45
45
  self.voxel_spacing = voxel_spacing
46
- # Output clamp for display; the projector uses the unconstrained
47
- # image so gradients keep flowing through negative updates.
48
- self.relu = nn.ReLU()
49
46
 
50
47
  def forward(self, x):
51
48
  updated_reco = x + self.reco
52
49
  current_sino = ParallelProjectorFunction.apply(updated_reco, self.angles,
53
50
  self.num_detectors, self.detector_spacing, self.voxel_spacing)
54
- return current_sino, self.relu(updated_reco)
51
+ return current_sino, updated_reco
55
52
 
56
53
  class Pipeline:
57
54
  def __init__(self, lr, volume_shape, angles, num_detectors, detector_spacing,
@@ -69,10 +66,12 @@ class Pipeline:
69
66
  for epoch in range(self.epoches):
70
67
  self.optimizer.zero_grad()
71
68
  predictions, current_reco = self.model(input)
72
- loss_value = self.loss(predictions, label)
73
- loss_value.backward()
74
- self.optimizer.step()
75
- loss_values.append(loss_value.item())
69
+ loss_value = self.loss(predictions, label)
70
+ loss_value.backward()
71
+ self.optimizer.step()
72
+ with torch.no_grad():
73
+ self.model.reco.clamp_(min=0.0)
74
+ loss_values.append(loss_value.item())
76
75
 
77
76
  if epoch % 10 == 0:
78
77
  print(f"Epoch {epoch}, Loss: {loss_value.item()}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "diffct"
7
- version = "1.3.3"
7
+ version = "1.3.4"
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 = [
@@ -186,10 +186,12 @@ def test_fdk_cone_shepp_logan_rmse_is_small():
186
186
 
187
187
  # z-profile along the central column: catches gross amplitude drift
188
188
  # along the axial direction (a common symptom of wrong FDK constants).
189
- z_profile_err = torch.abs(
190
- reco_raw[:, Ny // 2, Nx // 2] - phantom_torch[:, Ny // 2, Nx // 2]
191
- )
192
- assert z_profile_err.max().item() < 0.6
189
+ z_profile_err = torch.abs(
190
+ reco_raw[:, Ny // 2, Nx // 2] - phantom_torch[:, Ny // 2, Nx // 2]
191
+ )
192
+ # Cell-constant forward projection gives a slightly sharper one-sample
193
+ # transition error at ellipsoid boundaries while preserving global FDK RMSE.
194
+ assert z_profile_err.max().item() < 0.65
193
195
 
194
196
  # Amplitude sanity: phantom is clipped to [0, 1]. Real FDK output has
195
197
  # a small negative ringing lobe near sharp edges, so we only assert the
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
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