diffct 2.4.3__tar.gz → 2.5.0__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 (185) hide show
  1. {diffct-2.4.3 → diffct-2.5.0}/.gitignore +5 -0
  2. {diffct-2.4.3 → diffct-2.5.0}/CHANGELOG.md +49 -2
  3. {diffct-2.4.3 → diffct-2.5.0}/CITATION.cff +5 -4
  4. {diffct-2.4.3 → diffct-2.5.0}/NOTICE +2 -0
  5. {diffct-2.4.3 → diffct-2.5.0}/PKG-INFO +13 -9
  6. {diffct-2.4.3 → diffct-2.5.0}/README.md +11 -7
  7. {diffct-2.4.3 → diffct-2.5.0}/diffct/__init__.py +1 -1
  8. {diffct-2.4.3 → diffct-2.5.0}/diffct/_solver_state.py +34 -7
  9. {diffct-2.4.3 → diffct-2.5.0}/diffct/analytical.py +12 -9
  10. {diffct-2.4.3 → diffct-2.5.0}/diffct/analytical_execution.py +9 -3
  11. {diffct-2.4.3 → diffct-2.5.0}/diffct/backends.py +544 -121
  12. {diffct-2.4.3 → diffct-2.5.0}/diffct/chunking.py +3 -2
  13. {diffct-2.4.3 → diffct-2.5.0}/diffct/constants.py +12 -0
  14. {diffct-2.4.3 → diffct-2.5.0}/diffct/geometry.py +57 -5
  15. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/analytical_transpose.py +40 -16
  16. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/cone_beam.py +117 -63
  17. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/fan_beam.py +49 -33
  18. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/geometry_vjp.py +153 -51
  19. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/parallel_beam.py +45 -15
  20. diffct-2.5.0/diffct/kernels/spacing.py +79 -0
  21. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/surface_geometry.py +15 -4
  22. {diffct-2.4.3 → diffct-2.5.0}/diffct/operators.py +102 -28
  23. {diffct-2.4.3 → diffct-2.5.0}/diffct/projectors.py +13 -6
  24. {diffct-2.4.3 → diffct-2.5.0}/diffct/reconstruction.py +22 -7
  25. {diffct-2.4.3 → diffct-2.5.0}/diffct/streaming.py +7 -0
  26. {diffct-2.4.3 → diffct-2.5.0}/diffct/surfaces.py +13 -4
  27. {diffct-2.4.3 → diffct-2.5.0}/diffct/utils.py +20 -0
  28. diffct-2.5.0/docs/assets/gallery/NOTICE +29 -0
  29. diffct-2.5.0/docs/search/tests/.gitignore +2 -0
  30. diffct-2.5.0/docs/search/tests/fixtures/make_tiny_model.py +89 -0
  31. diffct-2.5.0/docs/search/tests/fixtures/tiny-model/config.json +15 -0
  32. diffct-2.5.0/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx +0 -0
  33. diffct-2.5.0/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx_data +0 -0
  34. diffct-2.5.0/docs/search/tests/fixtures/tiny-model/tokenizer.json +140 -0
  35. diffct-2.5.0/docs/search/tests/fixtures/tiny-model/tokenizer_config.json +6 -0
  36. diffct-2.5.0/docs/search/tests/full-model.spec.mjs +59 -0
  37. diffct-2.5.0/docs/search/tests/harness.mjs +473 -0
  38. diffct-2.5.0/docs/search/tests/item1-ordering.spec.mjs +106 -0
  39. diffct-2.5.0/docs/search/tests/item2-single-load.spec.mjs +71 -0
  40. diffct-2.5.0/docs/search/tests/item3-cancel.spec.mjs +93 -0
  41. diffct-2.5.0/docs/search/tests/item4-leaks.spec.mjs +123 -0
  42. diffct-2.5.0/docs/search/tests/item5-errors.spec.mjs +115 -0
  43. diffct-2.5.0/docs/search/tests/item6-a11y.spec.mjs +186 -0
  44. diffct-2.5.0/docs/search/tests/item7-security.spec.mjs +101 -0
  45. diffct-2.5.0/docs/search/tests/playwright.config.mjs +17 -0
  46. {diffct-2.4.3 → diffct-2.5.0}/formal/README.md +41 -12
  47. diffct-2.5.0/formal/tla/README.md +131 -0
  48. {diffct-2.4.3 → diffct-2.5.0}/pyproject.toml +2 -2
  49. diffct-2.5.0/tests/_native_per_axis_spacing.py +261 -0
  50. diffct-2.5.0/tests/fixtures/native_scalar_spacing_main.npz +0 -0
  51. diffct-2.5.0/tests/test_geometry_input_validation.py +179 -0
  52. diffct-2.5.0/tests/test_mps_backend.py +431 -0
  53. diffct-2.5.0/tests/test_native_per_axis_admission.py +99 -0
  54. diffct-2.5.0/tests/test_native_per_axis_extremes.py +139 -0
  55. diffct-2.5.0/tests/test_native_per_axis_helical.py +93 -0
  56. diffct-2.5.0/tests/test_native_per_axis_spacing.py +505 -0
  57. diffct-2.5.0/tests/test_native_per_axis_spacing_gpu.py +237 -0
  58. diffct-2.5.0/tests/test_portable_bounded_autograd.py +945 -0
  59. diffct-2.5.0/tests/test_portable_cuda_budget.py +996 -0
  60. {diffct-2.4.3 → diffct-2.5.0}/tests/test_reference_backend.py +12 -4
  61. {diffct-2.4.3 → diffct-2.5.0}/LICENSE +0 -0
  62. {diffct-2.4.3 → diffct-2.5.0}/diffct/_solver_tv.py +0 -0
  63. {diffct-2.4.3 → diffct-2.5.0}/diffct/differentiable.py +0 -0
  64. {diffct-2.4.3 → diffct-2.5.0}/diffct/distribution.py +0 -0
  65. {diffct-2.4.3 → diffct-2.5.0}/diffct/kernels/__init__.py +0 -0
  66. {diffct-2.4.3 → diffct-2.5.0}/diffct/pipeline.py +0 -0
  67. {diffct-2.4.3 → diffct-2.5.0}/diffct/solvers.py +0 -0
  68. {diffct-2.4.3 → diffct-2.5.0}/diffct/storage.py +0 -0
  69. {diffct-2.4.3 → diffct-2.5.0}/diffct/surface_gradients.py +0 -0
  70. {diffct-2.4.3 → diffct-2.5.0}/docs/video/README.md +0 -0
  71. {diffct-2.4.3 → diffct-2.5.0}/examples/README.md +0 -0
  72. {diffct-2.4.3 → diffct-2.5.0}/examples/_block_cgls.py +0 -0
  73. {diffct-2.4.3 → diffct-2.5.0}/examples/_common.py +0 -0
  74. {diffct-2.4.3 → diffct-2.5.0}/examples/data/NOTICE +0 -0
  75. {diffct-2.4.3 → diffct-2.5.0}/examples/data/preprocess_walnut.py +0 -0
  76. {diffct-2.4.3 → diffct-2.5.0}/examples/walnut_reconstruction.py +0 -0
  77. {diffct-2.4.3 → diffct-2.5.0}/pytest.ini +0 -0
  78. {diffct-2.4.3 → diffct-2.5.0}/tests/__init__.py +0 -0
  79. {diffct-2.4.3 → diffct-2.5.0}/tests/_multiprocessing.py +0 -0
  80. {diffct-2.4.3 → diffct-2.5.0}/tests/_phantoms.py +0 -0
  81. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/README.md +0 -0
  82. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/__init__.py +0 -0
  83. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/_common.py +0 -0
  84. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/test_bench_cone.py +0 -0
  85. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/test_bench_fan.py +0 -0
  86. {diffct-2.4.3 → diffct-2.5.0}/tests/benchmarks/test_bench_parallel.py +0 -0
  87. {diffct-2.4.3 → diffct-2.5.0}/tests/distributed_projector_check.py +0 -0
  88. {diffct-2.4.3 → diffct-2.5.0}/tests/test_adjoint_inner_product.py +0 -0
  89. {diffct-2.4.3 → diffct-2.5.0}/tests/test_allocator_capacity.py +0 -0
  90. {diffct-2.4.3 → diffct-2.5.0}/tests/test_analytical_autograd.py +0 -0
  91. {diffct-2.4.3 → diffct-2.5.0}/tests/test_analytical_distribution.py +0 -0
  92. {diffct-2.4.3 → diffct-2.5.0}/tests/test_analytical_regressions.py +0 -0
  93. {diffct-2.4.3 → diffct-2.5.0}/tests/test_analytical_weight_distribution.py +0 -0
  94. {diffct-2.4.3 → diffct-2.5.0}/tests/test_angular_low_precision.py +0 -0
  95. {diffct-2.4.3 → diffct-2.5.0}/tests/test_axis_aligned_chords.py +0 -0
  96. {diffct-2.4.3 → diffct-2.5.0}/tests/test_batch_execution_edges.py +0 -0
  97. {diffct-2.4.3 → diffct-2.5.0}/tests/test_batched_surfaces.py +0 -0
  98. {diffct-2.4.3 → diffct-2.5.0}/tests/test_block_cgls.py +0 -0
  99. {diffct-2.4.3 → diffct-2.5.0}/tests/test_block_storage.py +0 -0
  100. {diffct-2.4.3 → diffct-2.5.0}/tests/test_call_trajectory.py +0 -0
  101. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cgls_state_admission.py +0 -0
  102. {diffct-2.4.3 → diffct-2.5.0}/tests/test_checkpoint_metadata_validation.py +0 -0
  103. {diffct-2.4.3 → diffct-2.5.0}/tests/test_chunked_projector.py +0 -0
  104. {diffct-2.4.3 → diffct-2.5.0}/tests/test_completed_transfer_reuse.py +0 -0
  105. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cone_backprojection_transpose.py +0 -0
  106. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cone_projector_autograd.py +0 -0
  107. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cpu_install.py +0 -0
  108. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cuda_exit_deallocations.py +0 -0
  109. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cuda_sink_lifetimes.py +0 -0
  110. {diffct-2.4.3 → diffct-2.5.0}/tests/test_cuda_smoke.py +0 -0
  111. {diffct-2.4.3 → diffct-2.5.0}/tests/test_detector_surface_dtypes.py +0 -0
  112. {diffct-2.4.3 → diffct-2.5.0}/tests/test_detector_surface_mapping.py +0 -0
  113. {diffct-2.4.3 → diffct-2.5.0}/tests/test_detector_surface_units.py +0 -0
  114. {diffct-2.4.3 → diffct-2.5.0}/tests/test_detector_surfaces.py +0 -0
  115. {diffct-2.4.3 → diffct-2.5.0}/tests/test_distributed_training_transforms.py +0 -0
  116. {diffct-2.4.3 → diffct-2.5.0}/tests/test_example_psnr.py +0 -0
  117. {diffct-2.4.3 → diffct-2.5.0}/tests/test_execution_buffers.py +0 -0
  118. {diffct-2.4.3 → diffct-2.5.0}/tests/test_execution_features.py +0 -0
  119. {diffct-2.4.3 → diffct-2.5.0}/tests/test_execution_schedules.py +0 -0
  120. {diffct-2.4.3 → diffct-2.5.0}/tests/test_fbp_fan_accuracy.py +0 -0
  121. {diffct-2.4.3 → diffct-2.5.0}/tests/test_fbp_fan_offsets.py +0 -0
  122. {diffct-2.4.3 → diffct-2.5.0}/tests/test_fbp_parallel_accuracy.py +0 -0
  123. {diffct-2.4.3 → diffct-2.5.0}/tests/test_fdk_cone_accuracy.py +0 -0
  124. {diffct-2.4.3 → diffct-2.5.0}/tests/test_fdk_cone_offsets.py +0 -0
  125. {diffct-2.4.3 → diffct-2.5.0}/tests/test_geometry_autograd.py +0 -0
  126. {diffct-2.4.3 → diffct-2.5.0}/tests/test_geometry_conversions.py +0 -0
  127. {diffct-2.4.3 → diffct-2.5.0}/tests/test_geometry_gradients.py +0 -0
  128. {diffct-2.4.3 → diffct-2.5.0}/tests/test_gpu_performance_contracts.py +0 -0
  129. {diffct-2.4.3 → diffct-2.5.0}/tests/test_gradcheck.py +0 -0
  130. {diffct-2.4.3 → diffct-2.5.0}/tests/test_lazy_native_observers.py +0 -0
  131. {diffct-2.4.3 → diffct-2.5.0}/tests/test_live_trajectory_validation.py +0 -0
  132. {diffct-2.4.3 → diffct-2.5.0}/tests/test_multi_gpu_exit.py +0 -0
  133. {diffct-2.4.3 → diffct-2.5.0}/tests/test_multiprocessing.py +0 -0
  134. {diffct-2.4.3 → diffct-2.5.0}/tests/test_native_endpoint_precision.py +0 -0
  135. {diffct-2.4.3 → diffct-2.5.0}/tests/test_native_surface_transforms.py +0 -0
  136. {diffct-2.4.3 → diffct-2.5.0}/tests/test_native_volume_layout.py +0 -0
  137. {diffct-2.4.3 → diffct-2.5.0}/tests/test_native_volume_layout_distributed.py +0 -0
  138. {diffct-2.4.3 → diffct-2.5.0}/tests/test_output_pipeline_overlap.py +0 -0
  139. {diffct-2.4.3 → diffct-2.5.0}/tests/test_parallel3d.py +0 -0
  140. {diffct-2.4.3 → diffct-2.5.0}/tests/test_parallel3d_surface_oracle.py +0 -0
  141. {diffct-2.4.3 → diffct-2.5.0}/tests/test_partition_lifecycle.py +0 -0
  142. {diffct-2.4.3 → diffct-2.5.0}/tests/test_plan_memory_bounds.py +0 -0
  143. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_distributed_geometry.py +0 -0
  144. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_endpoint_precision.py +0 -0
  145. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_execution_bounds.py +0 -0
  146. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_numeric_regressions.py +0 -0
  147. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_sampler_snapshot.py +0 -0
  148. {diffct-2.4.3 → diffct-2.5.0}/tests/test_portable_surface_admission.py +0 -0
  149. {diffct-2.4.3 → diffct-2.5.0}/tests/test_pr42_review_regressions.py +0 -0
  150. {diffct-2.4.3 → diffct-2.5.0}/tests/test_projector_api.py +0 -0
  151. {diffct-2.4.3 → diffct-2.5.0}/tests/test_projector_degenerate_geometry.py +0 -0
  152. {diffct-2.4.3 → diffct-2.5.0}/tests/test_projector_geometry_devices.py +0 -0
  153. {diffct-2.4.3 → diffct-2.5.0}/tests/test_ramp_filter_windows.py +0 -0
  154. {diffct-2.4.3 → diffct-2.5.0}/tests/test_reconstruction_api.py +0 -0
  155. {diffct-2.4.3 → diffct-2.5.0}/tests/test_reconstruction_work_reuse.py +0 -0
  156. {diffct-2.4.3 → diffct-2.5.0}/tests/test_release_2_0_1.py +0 -0
  157. {diffct-2.4.3 → diffct-2.5.0}/tests/test_review_230.py +0 -0
  158. {diffct-2.4.3 → diffct-2.5.0}/tests/test_review_regressions.py +0 -0
  159. {diffct-2.4.3 → diffct-2.5.0}/tests/test_solver_admission_regressions.py +0 -0
  160. {diffct-2.4.3 → diffct-2.5.0}/tests/test_solver_api.py +0 -0
  161. {diffct-2.4.3 → diffct-2.5.0}/tests/test_solver_checkpoint_edges.py +0 -0
  162. {diffct-2.4.3 → diffct-2.5.0}/tests/test_solver_collective_failures.py +0 -0
  163. {diffct-2.4.3 → diffct-2.5.0}/tests/test_solver_device_scalars.py +0 -0
  164. {diffct-2.4.3 → diffct-2.5.0}/tests/test_spatial_partition.py +0 -0
  165. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_collective_failures.py +0 -0
  166. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_composition_kernel.py +0 -0
  167. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_compute_shards.py +0 -0
  168. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_geometry_cudasim.py +0 -0
  169. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_gpu_execution.py +0 -0
  170. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_gradient_replay.py +0 -0
  171. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_host_sampling_budget.py +0 -0
  172. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_native_regressions.py +0 -0
  173. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_parameter_updates.py +0 -0
  174. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_pipeline_contracts.py +0 -0
  175. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_saved_snapshot_budget.py +0 -0
  176. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_snapshot_admission.py +0 -0
  177. {diffct-2.4.3 → diffct-2.5.0}/tests/test_surface_trajectory_transform.py +0 -0
  178. {diffct-2.4.3 → diffct-2.5.0}/tests/test_training_transforms.py +0 -0
  179. {diffct-2.4.3 → diffct-2.5.0}/tests/test_trajectory_admission_cost.py +0 -0
  180. {diffct-2.4.3 → diffct-2.5.0}/tests/test_trajectory_surface_integration.py +0 -0
  181. {diffct-2.4.3 → diffct-2.5.0}/tests/test_transfer_pipeline.py +0 -0
  182. {diffct-2.4.3 → diffct-2.5.0}/tests/test_transfer_retirement_lifetimes.py +0 -0
  183. {diffct-2.4.3 → diffct-2.5.0}/tests/test_tv_zero_dependency.py +0 -0
  184. {diffct-2.4.3 → diffct-2.5.0}/tests/test_two_far_endpoints.py +0 -0
  185. {diffct-2.4.3 → diffct-2.5.0}/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/
@@ -5,7 +5,54 @@ 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
- ## [Unreleased]
8
+ ## [2.5.0] - 2026-10-26
9
+
10
+ ### Added
11
+
12
+ - Apple Silicon GPU (MPS) support for the portable PyTorch backend, including
13
+ the solvers, detector surfaces and streamed stores. MPS has no FP64, so ray
14
+ clipping and entry setup run in FP32 there; `precision="float64"` raises a
15
+ `ValueError` for MPS data, outputs or trajectories. See `docs/EXECUTION.md`
16
+ for the FP32 error bound. Thanks to @cslht11
17
+ ([#57](https://github.com/sypsyp97/diffct/pull/57)).
18
+ - TLA+ protocol models with TLC/Apalache checks and execution trace validation
19
+ for memory budgets, native admission and distributed collectives, run in a
20
+ separate CI workflow. They live in `formal/` and do not change library
21
+ behavior ([#55](https://github.com/sypsyp97/diffct/pull/55)).
22
+
23
+ ### Fixed
24
+
25
+ - Trajectory constructors reject degenerate inputs instead of building NaN or empty geometry: zero or non-finite `ray_dir_fn` directions in `custom_trajectory_2d_parallel`, `random_trajectory_3d` sources at the isocenter, and view counts that are not positive integers (`TypeError` for floats and booleans, `ValueError` for counts below 1). Explicit angle tensors are still accepted. Thanks to @cslht11 ([#56](https://github.com/sypsyp97/diffct/pull/56)).
26
+
27
+ ### Documentation
28
+
29
+ - README, documentation landing page, `llms.txt` summary, `CITATION.cff` and package keywords name the native CUDA kernels and the portable backend on CPU, ROCm and Apple Silicon. The landing quickstart has an Apple Silicon (MPS) tab ([#60](https://github.com/sypsyp97/diffct/pull/60)).
30
+ - Every documentation page uses the same three-column layout with the site navigation on the left ([#58](https://github.com/sypsyp97/diffct/pull/58)).
31
+
32
+ ### Changed
33
+
34
+ - Continuous integration: pull requests that change only `docs/` skip the package, test and formal jobs; the documentation build still runs. Pushes to `main` and release tags run every job ([#59](https://github.com/sypsyp97/diffct/pull/59)).
35
+
36
+ ## [2.4.4] - 2026-10-25
37
+
38
+ ### Added
39
+
40
+ - 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.
41
+
42
+ ### Fixed
43
+
44
+ - 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.
45
+ - Second derivatives with respect to the geometry now raise an error on the single-process portable path, as documented. Before, they returned a value.
46
+ - Forward-mode derivatives (`torch.func.jvp`, `jacfwd`) with respect to `Projector` trajectory tensors now raise an error. Before, they returned zero tangents.
47
+ - Distributed operators reject trajectory dtype differences between ranks before gradient reduction, including per-call trajectory overrides.
48
+
49
+ ### Changed
50
+
51
+ - 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.
52
+ - 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.
53
+ - Portable outputs accumulate in place, so a call no longer holds a second full-size output. Execution statistics report the loop order that ran.
54
+ - `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.
55
+ - `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.
9
56
 
10
57
  ## [2.4.3] - 2026-10-22
11
58
 
@@ -254,7 +301,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
254
301
 
255
302
  Version 2.0.0 replaces the circular-orbit API of 1.x with arbitrary
256
303
  per-view trajectories. It breaks the 1.x interface; see
257
- [docs/MIGRATION.md](docs/MIGRATION.md).
304
+ [docs/MIGRATION.md](https://github.com/sypsyp97/diffct/blob/main/docs/MIGRATION.md).
258
305
 
259
306
  ### Breaking
260
307
 
@@ -2,8 +2,9 @@ cff-version: 1.2.0
2
2
  message: "If you use this software, please cite both the software (below) and the technical report listed under preferred-citation."
3
3
  title: "diffct: Differentiable Computed Tomography Reconstruction with CUDA"
4
4
  abstract: >-
5
- Differentiable CUDA projectors for computed tomography (CT) with PyTorch
6
- autograd. Each view has its own source and detector geometry, so circular,
5
+ Differentiable projectors for computed tomography (CT) with PyTorch
6
+ autograd: native NVIDIA CUDA kernels and a portable PyTorch backend for
7
+ CPU, ROCm and Apple Silicon (MPS). Each view has its own source and detector geometry, so circular,
7
8
  helical, saddle and calibrated scans use the same matched Siddon operator.
8
9
  Projections run on one GPU, several GPUs in one process, or across nodes,
9
10
  and trajectory tensors receive geometry gradients for calibration.
@@ -13,8 +14,8 @@ authors:
13
14
  given-names: Yipeng
14
15
  email: yipeng.sun@fau.de
15
16
  alias: sypsyp97
16
- version: 2.4.3
17
- date-released: "2026-10-22"
17
+ version: 2.5.0
18
+ date-released: "2026-10-26"
18
19
  license: Apache-2.0
19
20
  repository-code: "https://github.com/sypsyp97/diffct"
20
21
  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.3
3
+ Version: 2.5.0
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/
@@ -10,7 +10,7 @@ Author-email: Yipeng Sun <yipeng.sun@fau.de>
10
10
  License-Expression: Apache-2.0
11
11
  License-File: LICENSE
12
12
  License-File: NOTICE
13
- Keywords: CT reconstruction,CUDA,PyTorch,ROCm,computed tomography,differentiable programming
13
+ Keywords: Apple Silicon,CT reconstruction,CUDA,MPS,PyTorch,ROCm,computed tomography,differentiable programming
14
14
  Classifier: Development Status :: 5 - Production/Stable
15
15
  Classifier: Environment :: GPU :: NVIDIA CUDA
16
16
  Classifier: Intended Audience :: Science/Research
@@ -34,7 +34,8 @@ Description-Content-Type: text/markdown
34
34
  <p align="center">English · <a href="https://github.com/sypsyp97/diffct/blob/main/README.zh.md">简体中文</a></p>
35
35
 
36
36
  <p align="center">
37
- Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.
37
+ Differentiable CT projectors: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.<br>
38
+ Native NVIDIA CUDA kernels, plus a portable PyTorch backend for CPU, ROCm and Apple Silicon (MPS).
38
39
  </p>
39
40
 
40
41
  <p align="center">
@@ -66,7 +67,7 @@ Description-Content-Type: text/markdown
66
67
  </p>
67
68
 
68
69
  > **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
- > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
70
+ > Apple Silicon runs through the built-in MPS support (`backend="torch"`); a community [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX) also exists.
70
71
 
71
72
  This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
72
73
 
@@ -79,7 +80,7 @@ To our knowledge, diffct is the only open-source GPU CT library that combines ar
79
80
  - **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
80
81
  - **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
81
82
  - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Views, space and hybrid partitions control replication of volumes and projections. Speedup depends on workload and communication costs.
82
- - **Precision and training.** Explicit native/PyTorch backends, FP32/FP64 modes, per-axis voxel spacing, leading batch/channel dimensions and data `torch.func` transforms. The portable path supports CPU and PyTorch CUDA/ROCm devices; see [execution contracts](docs/EXECUTION.md).
83
+ - **Precision and training.** Explicit native/PyTorch backends, FP32/FP64 modes, per-axis voxel spacing, leading batch/channel dimensions and data `torch.func` transforms. The portable path supports CPU, PyTorch CUDA/ROCm devices and Apple Silicon GPUs (MPS, `backend="torch"`); see [execution contracts](docs/EXECUTION.md).
83
84
  - **Solver API.** `cgls`, `sirt` and `tv_reconstruction` report residual history, tolerances and callbacks, with scan/data fingerprints and complete checkpoint state. `cgls_into` keeps state in bounded mapped blocks.
84
85
  - **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
85
86
 
@@ -90,11 +91,12 @@ Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com
90
91
  For the native path, install NVIDIA CUDA PyTorch first, then diffct with the matching CUDA major extra:
91
92
 
92
93
  ```bash
94
+ python -c "import torch; print(torch.version.cuda)" # 12.x or 13.x
93
95
  pip install "diffct[cu12]" # PyTorch built for CUDA 12
94
96
  pip install "diffct[cu13]" # PyTorch built for CUDA 13
95
97
  ```
96
98
 
97
- The extra installs the CUDA compiler libraries Numba needs. For CPU or PyTorch ROCm execution, install the appropriate PyTorch build and plain `pip install diffct`, then select `backend="torch"`. Importing diffct and using this backend require no CUDA runtime; Numba CUDA initializes when native execution is used. Plain installation also works for native execution when a compatible CUDA Toolkit is already present.
99
+ The extra installs the CUDA compiler libraries Numba needs. For CPU, PyTorch ROCm or Apple Silicon (MPS) execution, install the appropriate PyTorch build and plain `pip install diffct`, then select `backend="torch"`. Importing diffct and using this backend require no CUDA runtime; Numba CUDA initializes when native execution is used. Plain installation also works for native execution when a compatible CUDA Toolkit is already present.
98
100
 
99
101
  From source, with the examples:
100
102
 
@@ -142,6 +144,8 @@ loss.backward() # x.grad = A^T (A x - y)
142
144
 
143
145
  **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
146
 
147
+ **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.
148
+
145
149
  **3D parallel beams.** Use `circular_trajectory_3d_parallel(...)` and `beam="parallel3d"`. The PyTorch backend provides matched projection and backprojection on CPU and CUDA.
146
150
 
147
151
  **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,8 +181,8 @@ volume slabs use `--partition space` under torchrun. Run
177
181
 
178
182
  <p align="center">
179
183
  <picture>
180
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.3">
181
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.3" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
184
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
185
+ <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
186
  </picture>
183
187
  </p>
184
188
 
@@ -188,7 +192,7 @@ volume slabs use `--partition space` under torchrun. Run
188
192
  | Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
189
193
  | CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
190
194
 
191
- diffct 2.4.3, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
195
+ 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
196
 
193
197
  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
198
 
@@ -3,7 +3,8 @@
3
3
  <p align="center">English · <a href="https://github.com/sypsyp97/diffct/blob/main/README.zh.md">简体中文</a></p>
4
4
 
5
5
  <p align="center">
6
- Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.
6
+ Differentiable CT projectors: arbitrary trajectories, multi-GPU, multi-node, geometry gradients.<br>
7
+ Native NVIDIA CUDA kernels, plus a portable PyTorch backend for CPU, ROCm and Apple Silicon (MPS).
7
8
  </p>
8
9
 
9
10
  <p align="center">
@@ -35,7 +36,7 @@
35
36
  </p>
36
37
 
37
38
  > **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
- > For Apple Silicon, use the [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX).
39
+ > Apple Silicon runs through the built-in MPS support (`backend="torch"`); a community [MLX port by Linda-Sophie Schneider](https://github.com/Linda-SophieSchneider/DiffCT-MLX) also exists.
39
40
 
40
41
  This README describes the source checkout. Features listed under [Unreleased](CHANGELOG.md#unreleased) require installation from source.
41
42
 
@@ -48,7 +49,7 @@ To our knowledge, diffct is the only open-source GPU CT library that combines ar
48
49
  - **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
49
50
  - **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
50
51
  - **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Views, space and hybrid partitions control replication of volumes and projections. Speedup depends on workload and communication costs.
51
- - **Precision and training.** Explicit native/PyTorch backends, FP32/FP64 modes, per-axis voxel spacing, leading batch/channel dimensions and data `torch.func` transforms. The portable path supports CPU and PyTorch CUDA/ROCm devices; see [execution contracts](docs/EXECUTION.md).
52
+ - **Precision and training.** Explicit native/PyTorch backends, FP32/FP64 modes, per-axis voxel spacing, leading batch/channel dimensions and data `torch.func` transforms. The portable path supports CPU, PyTorch CUDA/ROCm devices and Apple Silicon GPUs (MPS, `backend="torch"`); see [execution contracts](docs/EXECUTION.md).
52
53
  - **Solver API.** `cgls`, `sirt` and `tv_reconstruction` report residual history, tolerances and callbacks, with scan/data fingerprints and complete checkpoint state. `cgls_into` keeps state in bounded mapped blocks.
53
54
  - **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
54
55
 
@@ -59,11 +60,12 @@ Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com
59
60
  For the native path, install NVIDIA CUDA PyTorch first, then diffct with the matching CUDA major extra:
60
61
 
61
62
  ```bash
63
+ python -c "import torch; print(torch.version.cuda)" # 12.x or 13.x
62
64
  pip install "diffct[cu12]" # PyTorch built for CUDA 12
63
65
  pip install "diffct[cu13]" # PyTorch built for CUDA 13
64
66
  ```
65
67
 
66
- The extra installs the CUDA compiler libraries Numba needs. For CPU or PyTorch ROCm execution, install the appropriate PyTorch build and plain `pip install diffct`, then select `backend="torch"`. Importing diffct and using this backend require no CUDA runtime; Numba CUDA initializes when native execution is used. Plain installation also works for native execution when a compatible CUDA Toolkit is already present.
68
+ The extra installs the CUDA compiler libraries Numba needs. For CPU, PyTorch ROCm or Apple Silicon (MPS) execution, install the appropriate PyTorch build and plain `pip install diffct`, then select `backend="torch"`. Importing diffct and using this backend require no CUDA runtime; Numba CUDA initializes when native execution is used. Plain installation also works for native execution when a compatible CUDA Toolkit is already present.
67
69
 
68
70
  From source, with the examples:
69
71
 
@@ -111,6 +113,8 @@ loss.backward() # x.grad = A^T (A x - y)
111
113
 
112
114
  **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
115
 
116
+ **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.
117
+
114
118
  **3D parallel beams.** Use `circular_trajectory_3d_parallel(...)` and `beam="parallel3d"`. The PyTorch backend provides matched projection and backprojection on CPU and CUDA.
115
119
 
116
120
  **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,8 +150,8 @@ volume slabs use `--partition space` under torchrun. Run
146
150
 
147
151
  <p align="center">
148
152
  <picture>
149
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png?v=2.4.3">
150
- <img src="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_light.png?v=2.4.3" width="100%" alt="Time per forward, adjoint and CGLS iteration on 1, 4 and 8 GPUs, 512^3 volume">
153
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sypsyp97/diffct/main/docs/assets/scaling_dark.png">
154
+ <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
155
  </picture>
152
156
  </p>
153
157
 
@@ -157,7 +161,7 @@ volume slabs use `--partition space` under torchrun. Run
157
161
  | Adjoint (backprojection) | 1060.77 ms | 279.37 ms (3.8×) | 224.04 ms (4.7×) |
158
162
  | CGLS iteration | 1753.73 ms | 468.09 ms (3.7×) | 337.39 ms (5.2×) |
159
163
 
160
- diffct 2.4.3, FP32, A100 64 GB GPUs; circular cone beam, 360 views, 1024×768 detector, pitch 1.25. Default memory settings, without an imposed allocator cap.
164
+ 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
165
 
162
166
  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
167
 
@@ -64,7 +64,7 @@ try:
64
64
  except ImportError:
65
65
  pass
66
66
 
67
- __version__ = '2.4.3'
67
+ __version__ = '2.5.0'
68
68
 
69
69
  __all__ = [
70
70
  'Projector',
@@ -12,6 +12,7 @@ import torch.distributed as dist
12
12
  from .pipeline import _fragments
13
13
  from .storage import NpyStore, TensorStore
14
14
  from .distribution import control_status, preflight
15
+ from .utils import _work_dtype
15
16
 
16
17
 
17
18
  def count(value, name, positive=False):
@@ -65,20 +66,46 @@ def scalar_sum(operator, value, kind, *, return_tensor=False):
65
66
  dist.all_reduce(accumulator, group=group)
66
67
  return accumulator if return_tensor else accumulator.item()
67
68
  if return_tensor:
68
- return torch.as_tensor(value, dtype=torch.float64, device=result_device)
69
+ return torch.as_tensor(value, dtype=_work_dtype(result_device), device=result_device)
69
70
  return float(value)
70
71
 
71
72
 
72
73
  def squared_norm(operator, value, kind, elements=1048576, *, return_tensor=False):
73
74
  store = TensorStore(value) if isinstance(value, torch.Tensor) else value
74
- total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
75
+ # Scale every block by its own peak before squaring and track the
76
+ # running maximum on the device: unscaled FP32 squares overflow for
77
+ # large measurements and underflow for tiny geometries.
78
+ device = getattr(store, "device", torch.device("cpu"))
79
+ if _work_dtype(device) == torch.float64:
80
+ total = torch.zeros((), dtype=torch.float64, device=device)
81
+ if owns_values(operator, kind):
82
+ for selected in _fragments(tuple(store.shape), elements):
83
+ # Clone even an FP64 view: norms must never alter caller measurements.
84
+ block = store.read(selected).to(dtype=torch.float64).clone()
85
+ # Preserve the block reductions and their sequential FP64 order.
86
+ total.add_(block.square_().sum().to(device=total.device))
87
+ return scalar_sum(operator, total, kind, return_tensor=return_tensor)
88
+ # FP32 devices (MPS): squares of large measurements overflow and tiny
89
+ # geometries underflow, so every block is rescaled by its peak and the
90
+ # running sum follows the largest peak seen. All block math stays on the
91
+ # device; the two closing scalar reads fit the per-iteration reduction
92
+ # budget, and their product leaves FP32 before it can overflow.
93
+ peak = torch.zeros((), device=device, dtype=torch.float32)
94
+ total = torch.zeros((), device=device, dtype=torch.float32)
75
95
  if owns_values(operator, kind):
76
96
  for selected in _fragments(tuple(store.shape), elements):
77
- # Clone even an FP64 view: norms must never alter caller measurements.
78
- block = store.read(selected).to(dtype=torch.float64).clone()
79
- # Preserve the block reductions and their sequential FP64 order.
80
- total.add_(block.square_().sum().to(device=total.device))
81
- return scalar_sum(operator, total, kind, return_tensor=return_tensor)
97
+ block = store.read(selected)
98
+ if not block.numel():
99
+ continue
100
+ block_peak = block.detach().abs().amax()
101
+ safe = torch.where(block_peak > 0, block_peak, torch.ones_like(block_peak))
102
+ combined = torch.maximum(peak, block_peak)
103
+ guard = torch.where(combined > 0, combined, torch.ones_like(combined))
104
+ total = (total * (peak / guard).square()
105
+ + (block.detach() / safe).square().sum() * (block_peak / guard).square())
106
+ peak = combined
107
+ return scalar_sum(operator, peak.item() ** 2 * total.item(), kind,
108
+ return_tensor=return_tensor)
82
109
 
83
110
 
84
111
  def copy_blocks(source, target, elements):
@@ -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,8 +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(value / voxel_spacing) for value in isocenter.tolist()),
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()),
527
528
  )
528
529
 
529
530
  return reco
@@ -584,8 +585,9 @@ def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det
584
585
  d_sino, n_views, n_u, n_v, d_reco, Nx, Ny, Nz,
585
586
  _DTYPE(du), _DTYPE(dv),
586
587
  d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
587
- cx, cy, cz, _DTYPE(voxel_spacing),
588
- *(_DTYPE(value / voxel_spacing) for value in isocenter.tolist()),
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()),
589
591
  )
590
592
 
591
593
  # Convert back to (D, H, W) layout for the user.
@@ -637,7 +639,8 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
637
639
  scale = (sdd_n / (2.0 * math.pi * sid_n)).view(
638
640
  (-1,) + (1,) * (len(sinogram_shape) - 1)
639
641
  )
640
- iso = tuple(_DTYPE(value / parameters[-1]) for value in isocenter.tolist())
642
+ iso = tuple(_DTYPE(value if isinstance(parameters[-1], (tuple, list)) else value / parameters[-1])
643
+ for value in isocenter.tolist())
641
644
 
642
645
  if beam == "cone":
643
646
  D, H, W, du, dv, spacing = parameters
@@ -647,7 +650,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
647
650
  _resolve_kernel('_cone_3d_fdk_transpose_kernel')[grid, tpb, stream](
648
651
  d_sino, *sinogram_shape, d_image, W, H, D, _DTYPE(du), _DTYPE(dv),
649
652
  *d_geometry, _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5),
650
- _DTYPE(spacing), *iso,
653
+ _native_spacing(spacing), *iso,
651
654
  )
652
655
  else:
653
656
  pitch, H, W, spacing = parameters
@@ -657,7 +660,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
657
660
  else '_fan_2d_fbp_transpose_kernel')
658
661
  kernel[grid, tpb, stream](
659
662
  d_sino, *sinogram_shape, d_image, W, H, _DTYPE(pitch), *d_geometry,
660
- _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(spacing), *iso,
663
+ _DTYPE(W * 0.5), _DTYPE(H * 0.5), _native_spacing(spacing), *iso,
661
664
  )
662
665
  return sinogram * scale
663
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
@@ -143,6 +144,9 @@ class AnalyticalExecution:
143
144
  self.rank = 3 if beam == "cone" else 2
144
145
  self.global_volume_shape = _shape(volume_shape, self.rank, "volume_shape")
145
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)
146
150
  self.pitch = _detector_spacing(detector_spacing, beam)
147
151
  self.devices = _normalize_devices(devices)
148
152
  if not isinstance(source, torch.Tensor):
@@ -156,6 +160,8 @@ class AnalyticalExecution:
156
160
  raise TypeError("sinogram store must have dtype float32 or float64")
157
161
  self.source = source
158
162
  self.output_device = torch.device(getattr(source, "device", "cpu"))
163
+ if self.dtype == torch.float64 and self.output_device.type == "mps":
164
+ raise ValueError("precision must be 'fast' or 'float32' on MPS; MPS provides no float64")
159
165
  self.detector_shape = tuple(source.shape[1:])
160
166
  for component in trajectory:
161
167
  _fixed(component, "trajectory")
@@ -336,14 +342,14 @@ class AnalyticalExecution:
336
342
  device=data.device, dtype=self.dtype)
337
343
  if self.beam == "parallel":
338
344
  geometry = (geometry[0], geometry[1] - centre, geometry[2])
339
- return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.spacing[0])
345
+ return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.voxel_spacing)
340
346
  geometry = (geometry[0] - centre, geometry[1] - centre, *geometry[2:])
341
347
  isocenter = self.isocenter.to(data.device) - centre
342
348
  if self.beam == "fan":
343
349
  return fan_weighted_backproject(data, *geometry, self.pitch[0], *shape,
344
- self.spacing[0], isocenter=isocenter)
350
+ self.voxel_spacing, isocenter=isocenter)
345
351
  return cone_weighted_backproject(data, *geometry, *shape, *self.pitch,
346
- self.spacing[0], isocenter=isocenter)
352
+ self.voxel_spacing, isocenter=isocenter)
347
353
 
348
354
  def _tile(self, spatial):
349
355
  shape = tuple(part.stop - part.start for part in spatial)