diffct 2.4.2__tar.gz → 2.4.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 (182) hide show
  1. {diffct-2.4.2 → diffct-2.4.4}/.gitignore +5 -0
  2. {diffct-2.4.2 → diffct-2.4.4}/CHANGELOG.md +38 -1
  3. {diffct-2.4.2 → diffct-2.4.4}/CITATION.cff +2 -2
  4. {diffct-2.4.2 → diffct-2.4.4}/NOTICE +2 -0
  5. {diffct-2.4.2 → diffct-2.4.4}/PKG-INFO +12 -9
  6. {diffct-2.4.2 → diffct-2.4.4}/README.md +11 -8
  7. {diffct-2.4.2 → diffct-2.4.4}/diffct/__init__.py +1 -1
  8. {diffct-2.4.2 → diffct-2.4.4}/diffct/_solver_state.py +12 -9
  9. {diffct-2.4.2 → diffct-2.4.4}/diffct/_solver_tv.py +3 -2
  10. {diffct-2.4.2 → diffct-2.4.4}/diffct/analytical.py +12 -12
  11. {diffct-2.4.2 → diffct-2.4.4}/diffct/analytical_execution.py +24 -3
  12. {diffct-2.4.2 → diffct-2.4.4}/diffct/backends.py +513 -113
  13. {diffct-2.4.2 → diffct-2.4.4}/diffct/chunking.py +4 -2
  14. {diffct-2.4.2 → diffct-2.4.4}/diffct/constants.py +12 -0
  15. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/analytical_transpose.py +40 -16
  16. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/cone_beam.py +117 -63
  17. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/fan_beam.py +49 -33
  18. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/geometry_vjp.py +153 -51
  19. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/parallel_beam.py +45 -15
  20. diffct-2.4.4/diffct/kernels/spacing.py +79 -0
  21. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/surface_geometry.py +22 -9
  22. {diffct-2.4.2 → diffct-2.4.4}/diffct/operators.py +300 -106
  23. {diffct-2.4.2 → diffct-2.4.4}/diffct/projectors.py +13 -6
  24. {diffct-2.4.2 → diffct-2.4.4}/diffct/reconstruction.py +7 -4
  25. {diffct-2.4.2 → diffct-2.4.4}/diffct/solvers.py +22 -11
  26. {diffct-2.4.2 → diffct-2.4.4}/diffct/surfaces.py +20 -14
  27. {diffct-2.4.2 → diffct-2.4.4}/diffct/utils.py +10 -0
  28. diffct-2.4.4/docs/assets/gallery/NOTICE +29 -0
  29. diffct-2.4.4/docs/search/tests/.gitignore +2 -0
  30. diffct-2.4.4/docs/search/tests/fixtures/make_tiny_model.py +89 -0
  31. diffct-2.4.4/docs/search/tests/fixtures/tiny-model/config.json +15 -0
  32. diffct-2.4.4/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx +0 -0
  33. diffct-2.4.4/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx_data +0 -0
  34. diffct-2.4.4/docs/search/tests/fixtures/tiny-model/tokenizer.json +140 -0
  35. diffct-2.4.4/docs/search/tests/fixtures/tiny-model/tokenizer_config.json +6 -0
  36. diffct-2.4.4/docs/search/tests/full-model.spec.mjs +59 -0
  37. diffct-2.4.4/docs/search/tests/harness.mjs +473 -0
  38. diffct-2.4.4/docs/search/tests/item1-ordering.spec.mjs +106 -0
  39. diffct-2.4.4/docs/search/tests/item2-single-load.spec.mjs +71 -0
  40. diffct-2.4.4/docs/search/tests/item3-cancel.spec.mjs +93 -0
  41. diffct-2.4.4/docs/search/tests/item4-leaks.spec.mjs +123 -0
  42. diffct-2.4.4/docs/search/tests/item5-errors.spec.mjs +115 -0
  43. diffct-2.4.4/docs/search/tests/item6-a11y.spec.mjs +186 -0
  44. diffct-2.4.4/docs/search/tests/item7-security.spec.mjs +101 -0
  45. diffct-2.4.4/docs/search/tests/playwright.config.mjs +17 -0
  46. {diffct-2.4.2 → diffct-2.4.4}/examples/README.md +3 -0
  47. {diffct-2.4.2 → diffct-2.4.4}/formal/README.md +25 -12
  48. {diffct-2.4.2 → diffct-2.4.4}/pyproject.toml +1 -1
  49. diffct-2.4.4/tests/_multiprocessing.py +26 -0
  50. diffct-2.4.4/tests/_native_per_axis_spacing.py +261 -0
  51. diffct-2.4.4/tests/fixtures/native_scalar_spacing_main.npz +0 -0
  52. {diffct-2.4.2 → diffct-2.4.4}/tests/test_allocator_capacity.py +7 -7
  53. {diffct-2.4.2 → diffct-2.4.4}/tests/test_batched_surfaces.py +55 -2
  54. {diffct-2.4.2 → diffct-2.4.4}/tests/test_block_storage.py +2 -2
  55. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cgls_state_admission.py +2 -2
  56. {diffct-2.4.2 → diffct-2.4.4}/tests/test_chunked_projector.py +5 -5
  57. {diffct-2.4.2 → diffct-2.4.4}/tests/test_completed_transfer_reuse.py +2 -2
  58. {diffct-2.4.2 → diffct-2.4.4}/tests/test_detector_surfaces.py +2 -2
  59. {diffct-2.4.2 → diffct-2.4.4}/tests/test_distributed_training_transforms.py +2 -7
  60. {diffct-2.4.2 → diffct-2.4.4}/tests/test_execution_buffers.py +67 -4
  61. {diffct-2.4.2 → diffct-2.4.4}/tests/test_execution_features.py +2 -2
  62. diffct-2.4.4/tests/test_gpu_performance_contracts.py +229 -0
  63. diffct-2.4.4/tests/test_multiprocessing.py +78 -0
  64. diffct-2.4.4/tests/test_native_per_axis_admission.py +99 -0
  65. diffct-2.4.4/tests/test_native_per_axis_extremes.py +139 -0
  66. diffct-2.4.4/tests/test_native_per_axis_helical.py +93 -0
  67. diffct-2.4.4/tests/test_native_per_axis_spacing.py +505 -0
  68. diffct-2.4.4/tests/test_native_per_axis_spacing_gpu.py +237 -0
  69. {diffct-2.4.2 → diffct-2.4.4}/tests/test_native_volume_layout_distributed.py +2 -2
  70. {diffct-2.4.2 → diffct-2.4.4}/tests/test_parallel3d.py +2 -2
  71. diffct-2.4.4/tests/test_portable_bounded_autograd.py +945 -0
  72. diffct-2.4.4/tests/test_portable_cuda_budget.py +996 -0
  73. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_distributed_geometry.py +2 -2
  74. diffct-2.4.4/tests/test_pr42_review_regressions.py +301 -0
  75. diffct-2.4.4/tests/test_reconstruction_work_reuse.py +231 -0
  76. {diffct-2.4.2 → diffct-2.4.4}/tests/test_reference_backend.py +12 -4
  77. {diffct-2.4.2 → diffct-2.4.4}/tests/test_solver_admission_regressions.py +2 -2
  78. {diffct-2.4.2 → diffct-2.4.4}/tests/test_solver_collective_failures.py +7 -5
  79. diffct-2.4.4/tests/test_solver_device_scalars.py +255 -0
  80. {diffct-2.4.2 → diffct-2.4.4}/tests/test_spatial_partition.py +2 -2
  81. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_collective_failures.py +2 -2
  82. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_compute_shards.py +3 -3
  83. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_geometry_cudasim.py +4 -2
  84. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_pipeline_contracts.py +10 -6
  85. diffct-2.4.4/tests/test_surface_saved_snapshot_budget.py +274 -0
  86. diffct-2.4.4/tests/test_surface_snapshot_admission.py +209 -0
  87. {diffct-2.4.2 → diffct-2.4.4}/tests/test_trajectory_surface_integration.py +8 -4
  88. {diffct-2.4.2 → diffct-2.4.4}/tests/test_transfer_pipeline.py +2 -2
  89. {diffct-2.4.2 → diffct-2.4.4}/tests/test_transfer_retirement_lifetimes.py +2 -2
  90. {diffct-2.4.2 → diffct-2.4.4}/LICENSE +0 -0
  91. {diffct-2.4.2 → diffct-2.4.4}/diffct/differentiable.py +0 -0
  92. {diffct-2.4.2 → diffct-2.4.4}/diffct/distribution.py +0 -0
  93. {diffct-2.4.2 → diffct-2.4.4}/diffct/geometry.py +0 -0
  94. {diffct-2.4.2 → diffct-2.4.4}/diffct/kernels/__init__.py +0 -0
  95. {diffct-2.4.2 → diffct-2.4.4}/diffct/pipeline.py +0 -0
  96. {diffct-2.4.2 → diffct-2.4.4}/diffct/storage.py +0 -0
  97. {diffct-2.4.2 → diffct-2.4.4}/diffct/streaming.py +0 -0
  98. {diffct-2.4.2 → diffct-2.4.4}/diffct/surface_gradients.py +0 -0
  99. {diffct-2.4.2 → diffct-2.4.4}/docs/video/README.md +0 -0
  100. {diffct-2.4.2 → diffct-2.4.4}/examples/_block_cgls.py +0 -0
  101. {diffct-2.4.2 → diffct-2.4.4}/examples/_common.py +0 -0
  102. {diffct-2.4.2 → diffct-2.4.4}/examples/data/NOTICE +0 -0
  103. {diffct-2.4.2 → diffct-2.4.4}/examples/data/preprocess_walnut.py +0 -0
  104. {diffct-2.4.2 → diffct-2.4.4}/examples/walnut_reconstruction.py +0 -0
  105. {diffct-2.4.2 → diffct-2.4.4}/pytest.ini +0 -0
  106. {diffct-2.4.2 → diffct-2.4.4}/tests/__init__.py +0 -0
  107. {diffct-2.4.2 → diffct-2.4.4}/tests/_phantoms.py +0 -0
  108. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/README.md +0 -0
  109. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/__init__.py +0 -0
  110. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/_common.py +0 -0
  111. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/test_bench_cone.py +0 -0
  112. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/test_bench_fan.py +0 -0
  113. {diffct-2.4.2 → diffct-2.4.4}/tests/benchmarks/test_bench_parallel.py +0 -0
  114. {diffct-2.4.2 → diffct-2.4.4}/tests/distributed_projector_check.py +0 -0
  115. {diffct-2.4.2 → diffct-2.4.4}/tests/test_adjoint_inner_product.py +0 -0
  116. {diffct-2.4.2 → diffct-2.4.4}/tests/test_analytical_autograd.py +0 -0
  117. {diffct-2.4.2 → diffct-2.4.4}/tests/test_analytical_distribution.py +0 -0
  118. {diffct-2.4.2 → diffct-2.4.4}/tests/test_analytical_regressions.py +0 -0
  119. {diffct-2.4.2 → diffct-2.4.4}/tests/test_analytical_weight_distribution.py +0 -0
  120. {diffct-2.4.2 → diffct-2.4.4}/tests/test_angular_low_precision.py +0 -0
  121. {diffct-2.4.2 → diffct-2.4.4}/tests/test_axis_aligned_chords.py +0 -0
  122. {diffct-2.4.2 → diffct-2.4.4}/tests/test_batch_execution_edges.py +0 -0
  123. {diffct-2.4.2 → diffct-2.4.4}/tests/test_block_cgls.py +0 -0
  124. {diffct-2.4.2 → diffct-2.4.4}/tests/test_call_trajectory.py +0 -0
  125. {diffct-2.4.2 → diffct-2.4.4}/tests/test_checkpoint_metadata_validation.py +0 -0
  126. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cone_backprojection_transpose.py +0 -0
  127. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cone_projector_autograd.py +0 -0
  128. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cpu_install.py +0 -0
  129. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cuda_exit_deallocations.py +0 -0
  130. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cuda_sink_lifetimes.py +0 -0
  131. {diffct-2.4.2 → diffct-2.4.4}/tests/test_cuda_smoke.py +0 -0
  132. {diffct-2.4.2 → diffct-2.4.4}/tests/test_detector_surface_dtypes.py +0 -0
  133. {diffct-2.4.2 → diffct-2.4.4}/tests/test_detector_surface_mapping.py +0 -0
  134. {diffct-2.4.2 → diffct-2.4.4}/tests/test_detector_surface_units.py +0 -0
  135. {diffct-2.4.2 → diffct-2.4.4}/tests/test_example_psnr.py +0 -0
  136. {diffct-2.4.2 → diffct-2.4.4}/tests/test_execution_schedules.py +0 -0
  137. {diffct-2.4.2 → diffct-2.4.4}/tests/test_fbp_fan_accuracy.py +0 -0
  138. {diffct-2.4.2 → diffct-2.4.4}/tests/test_fbp_fan_offsets.py +0 -0
  139. {diffct-2.4.2 → diffct-2.4.4}/tests/test_fbp_parallel_accuracy.py +0 -0
  140. {diffct-2.4.2 → diffct-2.4.4}/tests/test_fdk_cone_accuracy.py +0 -0
  141. {diffct-2.4.2 → diffct-2.4.4}/tests/test_fdk_cone_offsets.py +0 -0
  142. {diffct-2.4.2 → diffct-2.4.4}/tests/test_geometry_autograd.py +0 -0
  143. {diffct-2.4.2 → diffct-2.4.4}/tests/test_geometry_conversions.py +0 -0
  144. {diffct-2.4.2 → diffct-2.4.4}/tests/test_geometry_gradients.py +0 -0
  145. {diffct-2.4.2 → diffct-2.4.4}/tests/test_gradcheck.py +0 -0
  146. {diffct-2.4.2 → diffct-2.4.4}/tests/test_lazy_native_observers.py +0 -0
  147. {diffct-2.4.2 → diffct-2.4.4}/tests/test_live_trajectory_validation.py +0 -0
  148. {diffct-2.4.2 → diffct-2.4.4}/tests/test_multi_gpu_exit.py +0 -0
  149. {diffct-2.4.2 → diffct-2.4.4}/tests/test_native_endpoint_precision.py +0 -0
  150. {diffct-2.4.2 → diffct-2.4.4}/tests/test_native_surface_transforms.py +0 -0
  151. {diffct-2.4.2 → diffct-2.4.4}/tests/test_native_volume_layout.py +0 -0
  152. {diffct-2.4.2 → diffct-2.4.4}/tests/test_output_pipeline_overlap.py +0 -0
  153. {diffct-2.4.2 → diffct-2.4.4}/tests/test_parallel3d_surface_oracle.py +0 -0
  154. {diffct-2.4.2 → diffct-2.4.4}/tests/test_partition_lifecycle.py +0 -0
  155. {diffct-2.4.2 → diffct-2.4.4}/tests/test_plan_memory_bounds.py +0 -0
  156. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_endpoint_precision.py +0 -0
  157. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_execution_bounds.py +0 -0
  158. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_numeric_regressions.py +0 -0
  159. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_sampler_snapshot.py +0 -0
  160. {diffct-2.4.2 → diffct-2.4.4}/tests/test_portable_surface_admission.py +0 -0
  161. {diffct-2.4.2 → diffct-2.4.4}/tests/test_projector_api.py +0 -0
  162. {diffct-2.4.2 → diffct-2.4.4}/tests/test_projector_degenerate_geometry.py +0 -0
  163. {diffct-2.4.2 → diffct-2.4.4}/tests/test_projector_geometry_devices.py +0 -0
  164. {diffct-2.4.2 → diffct-2.4.4}/tests/test_ramp_filter_windows.py +0 -0
  165. {diffct-2.4.2 → diffct-2.4.4}/tests/test_reconstruction_api.py +0 -0
  166. {diffct-2.4.2 → diffct-2.4.4}/tests/test_release_2_0_1.py +0 -0
  167. {diffct-2.4.2 → diffct-2.4.4}/tests/test_review_230.py +0 -0
  168. {diffct-2.4.2 → diffct-2.4.4}/tests/test_review_regressions.py +0 -0
  169. {diffct-2.4.2 → diffct-2.4.4}/tests/test_solver_api.py +0 -0
  170. {diffct-2.4.2 → diffct-2.4.4}/tests/test_solver_checkpoint_edges.py +0 -0
  171. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_composition_kernel.py +0 -0
  172. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_gpu_execution.py +0 -0
  173. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_gradient_replay.py +0 -0
  174. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_host_sampling_budget.py +0 -0
  175. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_native_regressions.py +0 -0
  176. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_parameter_updates.py +0 -0
  177. {diffct-2.4.2 → diffct-2.4.4}/tests/test_surface_trajectory_transform.py +0 -0
  178. {diffct-2.4.2 → diffct-2.4.4}/tests/test_training_transforms.py +0 -0
  179. {diffct-2.4.2 → diffct-2.4.4}/tests/test_trajectory_admission_cost.py +0 -0
  180. {diffct-2.4.2 → diffct-2.4.4}/tests/test_tv_zero_dependency.py +0 -0
  181. {diffct-2.4.2 → diffct-2.4.4}/tests/test_two_far_endpoints.py +0 -0
  182. {diffct-2.4.2 → diffct-2.4.4}/tests/test_weights.py +0 -0
@@ -193,3 +193,8 @@ projectBrief.md
193
193
  docs/video/media/
194
194
  docs/video/scenes.txt
195
195
  docs/video/palette.png
196
+
197
+ # Sphinx autosummary stubs
198
+ docs/source/generated/
199
+ docs/search/node_modules/
200
+ docs/search/tests/test-results/
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.4.4] - 2026-10-25
11
+
12
+ ### Added
13
+
14
+ - Native per-axis voxel spacing for projection, matched backprojection, geometry gradients and analytical FBP/FDK. Tensor axis order and physical length units match the PyTorch backend. Native pitches must be in `[1e-6, 1e6]`, with a maximum/minimum ratio of at most `1e4`. Outside these bounds, `numba` raises and `auto` selects PyTorch. On NVIDIA CUDA, the fallback warning names the reason. Scalar native arithmetic and admission are unchanged.
15
+
16
+ ### Fixed
17
+
18
+ - Portable (`backend="torch"`) autograd no longer records every ray batch and volume tile. Data gradients apply the matched adjoint, and geometry gradients recompute one ray batch and tile at a time. Saved autograd memory no longer grows with the number of batches, also inside `torch.vmap` and `torch.func.jvp` with differentiable inputs or tangents. Geometry backward splits large leading batches like the forward call and accepts empty leading batches. First-order values are unchanged.
19
+ - Second derivatives with respect to the geometry now raise an error on the single-process portable path, as documented. Before, they returned a value.
20
+ - Forward-mode derivatives (`torch.func.jvp`, `jacfwd`) with respect to `Projector` trajectory tensors now raise an error. Before, they returned zero tangents.
21
+ - Distributed operators reject trajectory dtype differences between ranks before gradient reduction, including per-call trajectory overrides.
22
+
23
+ ### Changed
24
+
25
+ - The portable PyTorch backend sizes its automatic volume tiles, view batches and pixel batches from CUDA memory when the compute devices are CUDA or ROCm devices. One call uses at most a quarter of the memory that the PyTorch allocator can still use when the call starts. This quarter covers work arrays and reduction buffers; geometry backward recomputes each pair with half of that limit. Geometry backward may also keep one cotangent-sized buffer outside that quarter, for a contiguous copy or a distributed sum. With replicated distributed outputs, geometry backward may hold up to three cotangent-sized buffers outside that quarter during the cotangent reduction. CPU inputs, outputs and host stores with CUDA compute devices stage at most the smaller of a quarter of the free host memory and the CUDA limit, but never less than 8 MiB. One work array stays below 2^31 elements. CPU calls keep the 8 MiB ceiling and their previous plans. Distributed ranks agree on the smallest limits. Results change only in floating-point summation order.
26
+ - Portable projection sets up the rays of a view/pixel batch once and reuses them for every volume tile. Projections, backprojections and their gradients are bitwise unchanged for the same chunks. Distributed backward through a backprojection keeps the previous tile order and holds one summed cotangent tile at a time.
27
+ - Portable outputs accumulate in place, so a call no longer holds a second full-size output. Execution statistics report the loop order that ran.
28
+ - `Projector(backend="auto")` emits one `UserWarning` when it selects the portable backend on an NVIDIA CUDA machine. The warning names the reasons. `backend="torch"` silences it.
29
+ - `backend="auto"` selects native NVIDIA execution for per-axis spacing in fast mode with views/space ownership. Strict precision modes and hybrid ownership continue to use PyTorch.
30
+
31
+ ## [2.4.3] - 2026-10-22
32
+
33
+ ### Changed
34
+
35
+ - Reduce GPU transfers and host synchronization in native geometry admission: validity flags are reduced on the device and flat detector corners are admitted in one batch. Invalid-geometry errors and their order are unchanged (#42).
36
+ - Compose streamed sampled and per-view detector surfaces for a complete admitted view/pixel batch in one launch, with a bounded fallback workspace. Eligible full legacy-surface calls keep their geometry snapshots on the input GPU (#42).
37
+ - Read the two CGLS scalar norms together, keep tensor-store updates and TV reductions on their device, and reuse SIRT/TV residuals when no callback can change the volume or operator (#42).
38
+ - Reuse filtered view batches across volume tiles in single-rank analytical reconstruction (#42).
39
+ - Add `examples/benchmark_gpu_overheads.py`, a synthetic benchmark for launch and transfer overheads (#42).
40
+
41
+ ### Fixed
42
+
43
+ - Geometry-gradient replay uses the surface workspace that admission selected. Surface admission charges the larger of the composition and replay totals, the largest allocation of either phase, and a reservation bound that counts both phases (#42).
44
+ - Spawned multi-process test workers exit after a successful teardown, which removes intermittent `terminate called without an active exception` aborts in CI (#43).
45
+ - Add a formal check that the union reservation bounds every modelled composition/replay allocation order (#44).
46
+
10
47
  ## [2.4.2] - 2026-10-21
11
48
 
12
49
  ### Fixed
@@ -238,7 +275,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
238
275
 
239
276
  Version 2.0.0 replaces the circular-orbit API of 1.x with arbitrary
240
277
  per-view trajectories. It breaks the 1.x interface; see
241
- [docs/MIGRATION.md](docs/MIGRATION.md).
278
+ [docs/MIGRATION.md](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
242
279
 
243
280
  ### Breaking
244
281
 
@@ -13,8 +13,8 @@ authors:
13
13
  given-names: Yipeng
14
14
  email: yipeng.sun@fau.de
15
15
  alias: sypsyp97
16
- version: 2.4.2
17
- date-released: "2026-10-21"
16
+ version: 2.4.4
17
+ date-released: "2026-10-25"
18
18
  license: Apache-2.0
19
19
  repository-code: "https://github.com/sypsyp97/diffct"
20
20
  url: "https://sypsyp97.github.io/diffct/"
@@ -16,3 +16,5 @@ https://creativecommons.org/licenses/by/4.0/). The walnut images in
16
16
  docs/assets/ (walnut_measured.png, walnut_helical.png, diffct_intro.gif,
17
17
  diffct_intro.mp4) are reconstructions from these data. Credit the dataset as
18
18
  above when you reuse them. examples/data/NOTICE describes the preprocessing.
19
+
20
+ The documentation figures use third-party CT data; see docs/assets/gallery/NOTICE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: diffct
3
- Version: 2.4.2
3
+ Version: 2.4.4
4
4
  Summary: Differentiable CT projection and reconstruction: precision modes, streaming, distributed execution and geometry gradients
5
5
  Project-URL: Homepage, https://github.com/sypsyp97/diffct
6
6
  Project-URL: Documentation, https://sypsyp97.github.io/diffct/
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
68
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](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
69
69
  > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
70
70
 
71
- This README describes the source checkout, including [unreleased APIs](CHANGELOG.md#unreleased). Per-call trajectories, angle-tensor circular helpers, `transform_trajectory`, `parallel3d` and `native_volume_layout` require installation from source.
71
+ This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
72
72
 
73
73
  ## Why diffct
74
74
 
@@ -90,6 +90,7 @@ Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com
90
90
  For the native path, install NVIDIA CUDA PyTorch first, then diffct with the matching CUDA major extra:
91
91
 
92
92
  ```bash
93
+ python -c "import torch; print(torch.version.cuda)" # 12.x or 13.x
93
94
  pip install "diffct[cu12]" # PyTorch built for CUDA 12
94
95
  pip install "diffct[cu13]" # PyTorch built for CUDA 13
95
96
  ```
@@ -142,6 +143,8 @@ loss.backward() # x.grad = A^T (A x - y)
142
143
 
143
144
  **Geometry gradients.** Reuse one projector with `A.project(volume, trajectory=current)` or `A.backproject(sinogram, trajectory=current)`. Circular helpers accept learnable angle tensors, and `transform_trajectory` applies differentiable rigid transforms. Omitting the override keeps the constructor trajectory. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
144
145
 
146
+ **Per-axis voxel spacing.** Set `voxel_spacing=(dy, dx)` or `(dz, dy, dx)` in tensor axis order. Both backends support these pitches for projection, projection geometry gradients and FBP/FDK with fixed geometry.
147
+
145
148
  **3D parallel beams.** Use `circular_trajectory_3d_parallel(...)` and `beam="parallel3d"`. The PyTorch backend provides matched projection and backprojection on CPU and CUDA.
146
149
 
147
150
  **Native volume layout.** For unbatched, flat-detector cone operations, set `native_volume_layout=True` to avoid volume layout copies. This applies to the native full-CUDA path on one compute device. Shapes stay `(D, H, W)`; the returned strides follow the kernel layout. The default is `False`. See [chunked execution](docs/REFERENCE.md#chunked-execution) for limits.
@@ -177,18 +180,18 @@ volume slabs use `--partition space` under torchrun. Run
177
180
 
178
181
  <p align="center">
179
182
  <picture>
180
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.1">
181
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.1" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
183
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
184
+ <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, 512^3 volume">
182
185
  </picture>
183
186
  </p>
184
187
 
185
188
  | 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
186
189
  |---|---:|---:|---:|
187
- | Forward projection | 569.99 ms | 151.02 ms (3.8×) | 81.50 ms (7.0×) |
188
- | Adjoint (backprojection) | 1061.36 ms | 278.75 ms (3.8×) | 220.34 ms (4.8×) |
189
- | CGLS iteration | 1756.03 ms | 468.05 ms (3.8×) | 333.08 ms (5.3×) |
190
+ | Forward projection | 566.79 ms | 150.76 ms (3.8×) | 81.58 ms (6.9×) |
191
+ | Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
192
+ | CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
190
193
 
191
- diffct 2.4.1, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
194
+ FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
192
195
 
193
196
  Projection times are medians of seven samples after three warmups. CGLS uses three ten-iteration solves after a two-iteration warmup; the median includes initialization and is divided by ten. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
194
197
 
@@ -228,7 +231,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
228
231
 
229
232
  ## Examples
230
233
 
231
- `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.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).
234
+ `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `benchmark_surface_geometry.py`, `benchmark_gpu_overheads.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.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).
232
235
 
233
236
  ## Documentation
234
237
 
@@ -37,7 +37,7 @@
37
37
  > **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).
38
38
  > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
39
39
 
40
- This README describes the source checkout, including [unreleased APIs](CHANGELOG.md#unreleased). Per-call trajectories, angle-tensor circular helpers, `transform_trajectory`, `parallel3d` and `native_volume_layout` require installation from source.
40
+ This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
41
41
 
42
42
  ## Why diffct
43
43
 
@@ -59,6 +59,7 @@ Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com
59
59
  For the native path, install NVIDIA CUDA PyTorch first, then diffct with the matching CUDA major extra:
60
60
 
61
61
  ```bash
62
+ python -c "import torch; print(torch.version.cuda)" # 12.x or 13.x
62
63
  pip install "diffct[cu12]" # PyTorch built for CUDA 12
63
64
  pip install "diffct[cu13]" # PyTorch built for CUDA 13
64
65
  ```
@@ -111,6 +112,8 @@ loss.backward() # x.grad = A^T (A x - y)
111
112
 
112
113
  **Geometry gradients.** Reuse one projector with `A.project(volume, trajectory=current)` or `A.backproject(sinogram, trajectory=current)`. Circular helpers accept learnable angle tensors, and `transform_trajectory` applies differentiable rigid transforms. Omitting the override keeps the constructor trajectory. See [REFERENCE](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#geometry-gradients).
113
114
 
115
+ **Per-axis voxel spacing.** Set `voxel_spacing=(dy, dx)` or `(dz, dy, dx)` in tensor axis order. Both backends support these pitches for projection, projection geometry gradients and FBP/FDK with fixed geometry.
116
+
114
117
  **3D parallel beams.** Use `circular_trajectory_3d_parallel(...)` and `beam="parallel3d"`. The PyTorch backend provides matched projection and backprojection on CPU and CUDA.
115
118
 
116
119
  **Native volume layout.** For unbatched, flat-detector cone operations, set `native_volume_layout=True` to avoid volume layout copies. This applies to the native full-CUDA path on one compute device. Shapes stay `(D, H, W)`; the returned strides follow the kernel layout. The default is `False`. See [chunked execution](docs/REFERENCE.md#chunked-execution) for limits.
@@ -146,18 +149,18 @@ volume slabs use `--partition space` under torchrun. Run
146
149
 
147
150
  <p align="center">
148
151
  <picture>
149
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.1">
150
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.1" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
152
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
153
+ <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, 512^3 volume">
151
154
  </picture>
152
155
  </p>
153
156
 
154
157
  | 512³ volume | 1 GPU | 4 GPUs, 1 node | 8 GPUs, 2 nodes |
155
158
  |---|---:|---:|---:|
156
- | Forward projection | 569.99 ms | 151.02 ms (3.8×) | 81.50 ms (7.0×) |
157
- | Adjoint (backprojection) | 1061.36 ms | 278.75 ms (3.8×) | 220.34 ms (4.8×) |
158
- | CGLS iteration | 1756.03 ms | 468.05 ms (3.8×) | 333.08 ms (5.3×) |
159
+ | Forward projection | 566.79 ms | 150.76 ms (3.8×) | 81.58 ms (6.9×) |
160
+ | Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
161
+ | CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
159
162
 
160
- diffct 2.4.1, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
163
+ FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
161
164
 
162
165
  Projection times are medians of seven samples after three warmups. CGLS uses three ten-iteration solves after a two-iteration warmup; the median includes initialization and is divided by ten. Timings use synchronized wall time and the slowest rank. Raw samples and configuration: [scaling.json](https://github.com/sypsyp97/diffct/blob/main/docs/assets/scaling.json).
163
166
 
@@ -197,7 +200,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
197
200
 
198
201
  ## Examples
199
202
 
200
- `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.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).
203
+ `quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `benchmark_trajectory_admission.py`, `benchmark_surface_geometry.py`, `benchmark_gpu_overheads.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.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).
201
204
 
202
205
  ## Documentation
203
206
 
@@ -64,7 +64,7 @@ try:
64
64
  except ImportError:
65
65
  pass
66
66
 
67
- __version__ = '2.4.2'
67
+ __version__ = '2.4.4'
68
68
 
69
69
  __all__ = [
70
70
  'Projector',
@@ -53,30 +53,32 @@ def owns_values(operator, kind):
53
53
  )
54
54
 
55
55
 
56
- def scalar_sum(operator, value, kind):
57
- """Count physical rays/voxels once, including view/space replicas."""
56
+ def scalar_sum(operator, value, kind, *, return_tensor=False):
57
+ """Count physical rays/voxels once, optionally retaining the reduction tensor."""
58
58
  world = getattr(operator, "world_size", 1)
59
+ result_device = value.device if isinstance(value, torch.Tensor) else torch.device("cpu")
59
60
  value = value if owns_values(operator, kind) else 0.
60
61
  if world > 1:
61
62
  group = operator.process_group
62
63
  device = torch.device("cuda", torch.cuda.current_device()) if dist.get_backend(group) == "nccl" else torch.device("cpu")
63
64
  accumulator = torch.as_tensor(value, dtype=torch.float64, device=device).clone()
64
65
  dist.all_reduce(accumulator, group=group)
65
- return accumulator.item()
66
+ return accumulator if return_tensor else accumulator.item()
67
+ if return_tensor:
68
+ return torch.as_tensor(value, dtype=torch.float64, device=result_device)
66
69
  return float(value)
67
70
 
68
71
 
69
- def squared_norm(operator, value, kind, elements=1048576):
72
+ def squared_norm(operator, value, kind, elements=1048576, *, return_tensor=False):
70
73
  store = TensorStore(value) if isinstance(value, torch.Tensor) else value
71
- total = 0.
74
+ total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
72
75
  if owns_values(operator, kind):
73
- total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
74
76
  for selected in _fragments(tuple(store.shape), elements):
75
77
  # Clone even an FP64 view: norms must never alter caller measurements.
76
78
  block = store.read(selected).to(dtype=torch.float64).clone()
77
79
  # Preserve the block reductions and their sequential FP64 order.
78
80
  total.add_(block.square_().sum().to(device=total.device))
79
- return scalar_sum(operator, total, kind)
81
+ return scalar_sum(operator, total, kind, return_tensor=return_tensor)
80
82
 
81
83
 
82
84
  def copy_blocks(source, target, elements):
@@ -91,9 +93,10 @@ def fill_blocks(target, elements):
91
93
 
92
94
 
93
95
  def update_blocks(target, source, factor, elements, scale=1.):
96
+ device = getattr(target, "device", "cpu")
94
97
  for selected in _fragments(tuple(target.shape), elements):
95
- block = target.read(selected).to(device="cpu", dtype=target.dtype).clone()
96
- block.mul_(scale).add_(source.read(selected).to(device="cpu", dtype=target.dtype), alpha=factor)
98
+ block = target.read(selected).to(device=device, dtype=target.dtype).clone()
99
+ block.mul_(scale).add_(source.read(selected).to(device=device, dtype=target.dtype), alpha=factor)
97
100
  target.write(selected, block)
98
101
 
99
102
 
@@ -104,7 +104,8 @@ def total_variation(volume, *, operator=None, voxel_spacing=1., eps=1e-6, reduct
104
104
  total = local
105
105
  elements = volume.numel()
106
106
  if operator is not None:
107
- global_value = scalar_sum(operator, local.detach().item(), "volume")
108
- total = local + (local.new_tensor(global_value) - local.detach())
107
+ if getattr(operator, "world_size", 1) > 1:
108
+ global_value = scalar_sum(operator, local.detach(), "volume", return_tensor=True)
109
+ total = local + (global_value.to(local) - local.detach())
109
110
  elements = math.prod(operator.global_volume_shape)
110
111
  return total if reduction == "sum" else total / max(elements, 1)
@@ -27,7 +27,7 @@ import math
27
27
 
28
28
  import torch
29
29
 
30
- from .constants import _DTYPE
30
+ from .constants import _DTYPE, _native_spacing
31
31
  from .utils import (
32
32
  DeviceManager,
33
33
  TorchCUDABridge,
@@ -431,7 +431,7 @@ def _parallel_weighted_backproject_raw(sinogram, ray_dir, det_origin, det_u_vec,
431
431
  _resolve_kernel('_parallel_2d_fbp_backproject_kernel')[grid, tpb, numba_stream](
432
432
  d_sino, n_views, n_det, d_reco, Nx, Ny,
433
433
  _DTYPE(detector_spacing), d_ray_dir, d_det_origin, d_det_u_vec,
434
- cx, cy, _DTYPE(voxel_spacing),
434
+ cx, cy, _native_spacing(voxel_spacing),
435
435
  )
436
436
 
437
437
  return reco * (1.0 / (2.0 * math.pi))
@@ -522,9 +522,9 @@ def _fan_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec,
522
522
  _resolve_kernel('_fan_2d_fbp_backproject_kernel')[grid, tpb, numba_stream](
523
523
  d_sino, n_views, n_det, d_reco, Nx, Ny,
524
524
  _DTYPE(detector_spacing), d_src_pos, d_det_center, d_det_u_vec,
525
- cx, cy, _DTYPE(voxel_spacing),
526
- _DTYPE(isocenter[0].item() / voxel_spacing),
527
- _DTYPE(isocenter[1].item() / voxel_spacing),
525
+ cx, cy, _native_spacing(voxel_spacing),
526
+ *(_DTYPE(value if isinstance(voxel_spacing, (tuple, list)) else value / voxel_spacing)
527
+ for value in isocenter.tolist()),
528
528
  )
529
529
 
530
530
  return reco
@@ -585,10 +585,9 @@ def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det
585
585
  d_sino, n_views, n_u, n_v, d_reco, Nx, Ny, Nz,
586
586
  _DTYPE(du), _DTYPE(dv),
587
587
  d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
588
- cx, cy, cz, _DTYPE(voxel_spacing),
589
- _DTYPE(isocenter[0].item() / voxel_spacing),
590
- _DTYPE(isocenter[1].item() / voxel_spacing),
591
- _DTYPE(isocenter[2].item() / voxel_spacing),
588
+ cx, cy, cz, _native_spacing(voxel_spacing),
589
+ *(_DTYPE(value if isinstance(voxel_spacing, (tuple, list)) else value / voxel_spacing)
590
+ for value in isocenter.tolist()),
592
591
  )
593
592
 
594
593
  # Convert back to (D, H, W) layout for the user.
@@ -640,7 +639,8 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
640
639
  scale = (sdd_n / (2.0 * math.pi * sid_n)).view(
641
640
  (-1,) + (1,) * (len(sinogram_shape) - 1)
642
641
  )
643
- iso = tuple(_DTYPE(value.item() / parameters[-1]) for value in isocenter)
642
+ iso = tuple(_DTYPE(value if isinstance(parameters[-1], (tuple, list)) else value / parameters[-1])
643
+ for value in isocenter.tolist())
644
644
 
645
645
  if beam == "cone":
646
646
  D, H, W, du, dv, spacing = parameters
@@ -650,7 +650,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
650
650
  _resolve_kernel('_cone_3d_fdk_transpose_kernel')[grid, tpb, stream](
651
651
  d_sino, *sinogram_shape, d_image, W, H, D, _DTYPE(du), _DTYPE(dv),
652
652
  *d_geometry, _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5),
653
- _DTYPE(spacing), *iso,
653
+ _native_spacing(spacing), *iso,
654
654
  )
655
655
  else:
656
656
  pitch, H, W, spacing = parameters
@@ -660,7 +660,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
660
660
  else '_fan_2d_fbp_transpose_kernel')
661
661
  kernel[grid, tpb, stream](
662
662
  d_sino, *sinogram_shape, d_image, W, H, _DTYPE(pitch), *d_geometry,
663
- _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(spacing), *iso,
663
+ _DTYPE(W * 0.5), _DTYPE(H * 0.5), _native_spacing(spacing), *iso,
664
664
  )
665
665
  return sinogram * scale
666
666
 
@@ -16,6 +16,7 @@ from .analytical import (
16
16
  fan_weighted_backproject, parallel_weighted_backproject,
17
17
  )
18
18
  from .backends import _zero_dependency, select_backend, voxel_spacing_tuple
19
+ from .constants import _native_spacing
19
20
  from .chunking import _memory_budget, _pixel_tiles
20
21
  from .distribution import Partition, preflight
21
22
  from .operators import _detector_spacing, _normalize_devices, _positive_int, _shape, _validate_trajectory
@@ -53,6 +54,8 @@ def _sum(value, group, count):
53
54
 
54
55
 
55
56
  def _sum_blocks(value, group, count, block_elements):
57
+ if count == 1:
58
+ return value
56
59
  flat = value.reshape(-1)
57
60
  blocks = [_sum(flat.narrow(0, start, min(block_elements, flat.numel() - start)), group, count)
58
61
  for start in range(0, flat.numel(), block_elements)]
@@ -141,6 +144,9 @@ class AnalyticalExecution:
141
144
  self.rank = 3 if beam == "cone" else 2
142
145
  self.global_volume_shape = _shape(volume_shape, self.rank, "volume_shape")
143
146
  self.spacing = voxel_spacing_tuple(voxel_spacing, self.rank)
147
+ self.voxel_spacing = self.spacing if isinstance(voxel_spacing, (tuple, list)) else self.spacing[0]
148
+ if self.backend == "numba" and isinstance(self.voxel_spacing, tuple):
149
+ _native_spacing(self.voxel_spacing)
144
150
  self.pitch = _detector_spacing(detector_spacing, beam)
145
151
  self.devices = _normalize_devices(devices)
146
152
  if not isinstance(source, torch.Tensor):
@@ -334,14 +340,14 @@ class AnalyticalExecution:
334
340
  device=data.device, dtype=self.dtype)
335
341
  if self.beam == "parallel":
336
342
  geometry = (geometry[0], geometry[1] - centre, geometry[2])
337
- return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.spacing[0])
343
+ return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.voxel_spacing)
338
344
  geometry = (geometry[0] - centre, geometry[1] - centre, *geometry[2:])
339
345
  isocenter = self.isocenter.to(data.device) - centre
340
346
  if self.beam == "fan":
341
347
  return fan_weighted_backproject(data, *geometry, self.pitch[0], *shape,
342
- self.spacing[0], isocenter=isocenter)
348
+ self.voxel_spacing, isocenter=isocenter)
343
349
  return cone_weighted_backproject(data, *geometry, *shape, *self.pitch,
344
- self.spacing[0], isocenter=isocenter)
350
+ self.voxel_spacing, isocenter=isocenter)
345
351
 
346
352
  def _tile(self, spatial):
347
353
  shape = tuple(part.stop - part.start for part in spatial)
@@ -385,6 +391,21 @@ class AnalyticalExecution:
385
391
  def tensor_result(self):
386
392
  self._prepare_gradients()
387
393
  result = torch.zeros(self.volume_shape, device=self.output_device, dtype=self.dtype)
394
+ inputs = (self.source, self.projection_weights, self.angular_weights, self.filter_response)
395
+ active_gradients = torch.is_grad_enabled() and any(value is not None and value.requires_grad
396
+ for value in inputs)
397
+ if self.partition.world_size == 1 and not active_gradients:
398
+ # Each tile still accumulates view batches in their original order.
399
+ # The numerical result itself holds the partial sums between batches.
400
+ for views, device in self._view_batches():
401
+ filtered, geometry = self._filtered(views, device)
402
+ for spatial, _ in _pixel_tiles(self.volume_shape, self.chunk_shape):
403
+ partial = (self._native_gather(filtered, geometry, spatial) if self.backend == "numba"
404
+ else self._portable_gather(filtered, geometry, views, spatial))
405
+ result[spatial].add_(partial.to(self.output_device))
406
+ del partial
407
+ del filtered, geometry
408
+ return result
388
409
  # Every owned tile's backward collective precedes the bundled input
389
410
  # transpose, even when this rank owns no data views. Empty space groups
390
411
  # have no tile collectives but still contribute zero input cotangents.