diffct 2.5.0__tar.gz → 2.5.1__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 (188) hide show
  1. {diffct-2.5.0 → diffct-2.5.1}/CHANGELOG.md +28 -0
  2. {diffct-2.5.0 → diffct-2.5.1}/CITATION.cff +2 -2
  3. {diffct-2.5.0 → diffct-2.5.1}/PKG-INFO +1 -1
  4. {diffct-2.5.0 → diffct-2.5.1}/diffct/__init__.py +1 -1
  5. {diffct-2.5.0 → diffct-2.5.1}/diffct/_solver_state.py +5 -0
  6. {diffct-2.5.0 → diffct-2.5.1}/diffct/geometry.py +13 -7
  7. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/full-model.spec.mjs +2 -1
  8. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/harness.mjs +20 -2
  9. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item1-ordering.spec.mjs +2 -0
  10. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item2-single-load.spec.mjs +8 -0
  11. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item3-cancel.spec.mjs +9 -4
  12. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item4-leaks.spec.mjs +2 -0
  13. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item5-errors.spec.mjs +2 -0
  14. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item6-a11y.spec.mjs +2 -0
  15. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item7-security.spec.mjs +2 -0
  16. diffct-2.5.1/docs/search/tests/semantic-opt-in.spec.mjs +88 -0
  17. {diffct-2.5.0 → diffct-2.5.1}/pyproject.toml +1 -1
  18. diffct-2.5.1/tests/test_geometry_input_validation.py +393 -0
  19. diffct-2.5.1/tests/test_portable_accuracy.py +141 -0
  20. {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_checkpoint_edges.py +77 -0
  21. diffct-2.5.0/tests/test_geometry_input_validation.py +0 -179
  22. {diffct-2.5.0 → diffct-2.5.1}/.gitignore +0 -0
  23. {diffct-2.5.0 → diffct-2.5.1}/LICENSE +0 -0
  24. {diffct-2.5.0 → diffct-2.5.1}/NOTICE +0 -0
  25. {diffct-2.5.0 → diffct-2.5.1}/README.md +0 -0
  26. {diffct-2.5.0 → diffct-2.5.1}/diffct/_solver_tv.py +0 -0
  27. {diffct-2.5.0 → diffct-2.5.1}/diffct/analytical.py +0 -0
  28. {diffct-2.5.0 → diffct-2.5.1}/diffct/analytical_execution.py +0 -0
  29. {diffct-2.5.0 → diffct-2.5.1}/diffct/backends.py +0 -0
  30. {diffct-2.5.0 → diffct-2.5.1}/diffct/chunking.py +0 -0
  31. {diffct-2.5.0 → diffct-2.5.1}/diffct/constants.py +0 -0
  32. {diffct-2.5.0 → diffct-2.5.1}/diffct/differentiable.py +0 -0
  33. {diffct-2.5.0 → diffct-2.5.1}/diffct/distribution.py +0 -0
  34. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/__init__.py +0 -0
  35. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/analytical_transpose.py +0 -0
  36. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/cone_beam.py +0 -0
  37. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/fan_beam.py +0 -0
  38. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/geometry_vjp.py +0 -0
  39. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/parallel_beam.py +0 -0
  40. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/spacing.py +0 -0
  41. {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/surface_geometry.py +0 -0
  42. {diffct-2.5.0 → diffct-2.5.1}/diffct/operators.py +0 -0
  43. {diffct-2.5.0 → diffct-2.5.1}/diffct/pipeline.py +0 -0
  44. {diffct-2.5.0 → diffct-2.5.1}/diffct/projectors.py +0 -0
  45. {diffct-2.5.0 → diffct-2.5.1}/diffct/reconstruction.py +0 -0
  46. {diffct-2.5.0 → diffct-2.5.1}/diffct/solvers.py +0 -0
  47. {diffct-2.5.0 → diffct-2.5.1}/diffct/storage.py +0 -0
  48. {diffct-2.5.0 → diffct-2.5.1}/diffct/streaming.py +0 -0
  49. {diffct-2.5.0 → diffct-2.5.1}/diffct/surface_gradients.py +0 -0
  50. {diffct-2.5.0 → diffct-2.5.1}/diffct/surfaces.py +0 -0
  51. {diffct-2.5.0 → diffct-2.5.1}/diffct/utils.py +0 -0
  52. {diffct-2.5.0 → diffct-2.5.1}/docs/assets/gallery/NOTICE +0 -0
  53. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/.gitignore +0 -0
  54. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/make_tiny_model.py +0 -0
  55. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/config.json +0 -0
  56. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx +0 -0
  57. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx_data +0 -0
  58. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/tokenizer.json +0 -0
  59. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/tokenizer_config.json +0 -0
  60. {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/playwright.config.mjs +0 -0
  61. {diffct-2.5.0 → diffct-2.5.1}/docs/video/README.md +0 -0
  62. {diffct-2.5.0 → diffct-2.5.1}/examples/README.md +0 -0
  63. {diffct-2.5.0 → diffct-2.5.1}/examples/_block_cgls.py +0 -0
  64. {diffct-2.5.0 → diffct-2.5.1}/examples/_common.py +0 -0
  65. {diffct-2.5.0 → diffct-2.5.1}/examples/data/NOTICE +0 -0
  66. {diffct-2.5.0 → diffct-2.5.1}/examples/data/preprocess_walnut.py +0 -0
  67. {diffct-2.5.0 → diffct-2.5.1}/examples/walnut_reconstruction.py +0 -0
  68. {diffct-2.5.0 → diffct-2.5.1}/formal/README.md +0 -0
  69. {diffct-2.5.0 → diffct-2.5.1}/formal/tla/README.md +0 -0
  70. {diffct-2.5.0 → diffct-2.5.1}/pytest.ini +0 -0
  71. {diffct-2.5.0 → diffct-2.5.1}/tests/__init__.py +0 -0
  72. {diffct-2.5.0 → diffct-2.5.1}/tests/_multiprocessing.py +0 -0
  73. {diffct-2.5.0 → diffct-2.5.1}/tests/_native_per_axis_spacing.py +0 -0
  74. {diffct-2.5.0 → diffct-2.5.1}/tests/_phantoms.py +0 -0
  75. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/README.md +0 -0
  76. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/__init__.py +0 -0
  77. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/_common.py +0 -0
  78. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_cone.py +0 -0
  79. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_fan.py +0 -0
  80. {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_parallel.py +0 -0
  81. {diffct-2.5.0 → diffct-2.5.1}/tests/distributed_projector_check.py +0 -0
  82. {diffct-2.5.0 → diffct-2.5.1}/tests/fixtures/native_scalar_spacing_main.npz +0 -0
  83. {diffct-2.5.0 → diffct-2.5.1}/tests/test_adjoint_inner_product.py +0 -0
  84. {diffct-2.5.0 → diffct-2.5.1}/tests/test_allocator_capacity.py +0 -0
  85. {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_autograd.py +0 -0
  86. {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_distribution.py +0 -0
  87. {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_regressions.py +0 -0
  88. {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_weight_distribution.py +0 -0
  89. {diffct-2.5.0 → diffct-2.5.1}/tests/test_angular_low_precision.py +0 -0
  90. {diffct-2.5.0 → diffct-2.5.1}/tests/test_axis_aligned_chords.py +0 -0
  91. {diffct-2.5.0 → diffct-2.5.1}/tests/test_batch_execution_edges.py +0 -0
  92. {diffct-2.5.0 → diffct-2.5.1}/tests/test_batched_surfaces.py +0 -0
  93. {diffct-2.5.0 → diffct-2.5.1}/tests/test_block_cgls.py +0 -0
  94. {diffct-2.5.0 → diffct-2.5.1}/tests/test_block_storage.py +0 -0
  95. {diffct-2.5.0 → diffct-2.5.1}/tests/test_call_trajectory.py +0 -0
  96. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cgls_state_admission.py +0 -0
  97. {diffct-2.5.0 → diffct-2.5.1}/tests/test_checkpoint_metadata_validation.py +0 -0
  98. {diffct-2.5.0 → diffct-2.5.1}/tests/test_chunked_projector.py +0 -0
  99. {diffct-2.5.0 → diffct-2.5.1}/tests/test_completed_transfer_reuse.py +0 -0
  100. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cone_backprojection_transpose.py +0 -0
  101. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cone_projector_autograd.py +0 -0
  102. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cpu_install.py +0 -0
  103. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_exit_deallocations.py +0 -0
  104. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_sink_lifetimes.py +0 -0
  105. {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_smoke.py +0 -0
  106. {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_dtypes.py +0 -0
  107. {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_mapping.py +0 -0
  108. {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_units.py +0 -0
  109. {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surfaces.py +0 -0
  110. {diffct-2.5.0 → diffct-2.5.1}/tests/test_distributed_training_transforms.py +0 -0
  111. {diffct-2.5.0 → diffct-2.5.1}/tests/test_example_psnr.py +0 -0
  112. {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_buffers.py +0 -0
  113. {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_features.py +0 -0
  114. {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_schedules.py +0 -0
  115. {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_fan_accuracy.py +0 -0
  116. {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_fan_offsets.py +0 -0
  117. {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_parallel_accuracy.py +0 -0
  118. {diffct-2.5.0 → diffct-2.5.1}/tests/test_fdk_cone_accuracy.py +0 -0
  119. {diffct-2.5.0 → diffct-2.5.1}/tests/test_fdk_cone_offsets.py +0 -0
  120. {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_autograd.py +0 -0
  121. {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_conversions.py +0 -0
  122. {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_gradients.py +0 -0
  123. {diffct-2.5.0 → diffct-2.5.1}/tests/test_gpu_performance_contracts.py +0 -0
  124. {diffct-2.5.0 → diffct-2.5.1}/tests/test_gradcheck.py +0 -0
  125. {diffct-2.5.0 → diffct-2.5.1}/tests/test_lazy_native_observers.py +0 -0
  126. {diffct-2.5.0 → diffct-2.5.1}/tests/test_live_trajectory_validation.py +0 -0
  127. {diffct-2.5.0 → diffct-2.5.1}/tests/test_mps_backend.py +0 -0
  128. {diffct-2.5.0 → diffct-2.5.1}/tests/test_multi_gpu_exit.py +0 -0
  129. {diffct-2.5.0 → diffct-2.5.1}/tests/test_multiprocessing.py +0 -0
  130. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_endpoint_precision.py +0 -0
  131. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_admission.py +0 -0
  132. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_extremes.py +0 -0
  133. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_helical.py +0 -0
  134. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_spacing.py +0 -0
  135. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_spacing_gpu.py +0 -0
  136. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_surface_transforms.py +0 -0
  137. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_volume_layout.py +0 -0
  138. {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_volume_layout_distributed.py +0 -0
  139. {diffct-2.5.0 → diffct-2.5.1}/tests/test_output_pipeline_overlap.py +0 -0
  140. {diffct-2.5.0 → diffct-2.5.1}/tests/test_parallel3d.py +0 -0
  141. {diffct-2.5.0 → diffct-2.5.1}/tests/test_parallel3d_surface_oracle.py +0 -0
  142. {diffct-2.5.0 → diffct-2.5.1}/tests/test_partition_lifecycle.py +0 -0
  143. {diffct-2.5.0 → diffct-2.5.1}/tests/test_plan_memory_bounds.py +0 -0
  144. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_bounded_autograd.py +0 -0
  145. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_cuda_budget.py +0 -0
  146. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_distributed_geometry.py +0 -0
  147. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_endpoint_precision.py +0 -0
  148. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_execution_bounds.py +0 -0
  149. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_numeric_regressions.py +0 -0
  150. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_sampler_snapshot.py +0 -0
  151. {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_surface_admission.py +0 -0
  152. {diffct-2.5.0 → diffct-2.5.1}/tests/test_pr42_review_regressions.py +0 -0
  153. {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_api.py +0 -0
  154. {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_degenerate_geometry.py +0 -0
  155. {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_geometry_devices.py +0 -0
  156. {diffct-2.5.0 → diffct-2.5.1}/tests/test_ramp_filter_windows.py +0 -0
  157. {diffct-2.5.0 → diffct-2.5.1}/tests/test_reconstruction_api.py +0 -0
  158. {diffct-2.5.0 → diffct-2.5.1}/tests/test_reconstruction_work_reuse.py +0 -0
  159. {diffct-2.5.0 → diffct-2.5.1}/tests/test_reference_backend.py +0 -0
  160. {diffct-2.5.0 → diffct-2.5.1}/tests/test_release_2_0_1.py +0 -0
  161. {diffct-2.5.0 → diffct-2.5.1}/tests/test_review_230.py +0 -0
  162. {diffct-2.5.0 → diffct-2.5.1}/tests/test_review_regressions.py +0 -0
  163. {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_admission_regressions.py +0 -0
  164. {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_api.py +0 -0
  165. {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_collective_failures.py +0 -0
  166. {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_device_scalars.py +0 -0
  167. {diffct-2.5.0 → diffct-2.5.1}/tests/test_spatial_partition.py +0 -0
  168. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_collective_failures.py +0 -0
  169. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_composition_kernel.py +0 -0
  170. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_compute_shards.py +0 -0
  171. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_geometry_cudasim.py +0 -0
  172. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_gpu_execution.py +0 -0
  173. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_gradient_replay.py +0 -0
  174. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_host_sampling_budget.py +0 -0
  175. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_native_regressions.py +0 -0
  176. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_parameter_updates.py +0 -0
  177. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_pipeline_contracts.py +0 -0
  178. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_saved_snapshot_budget.py +0 -0
  179. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_snapshot_admission.py +0 -0
  180. {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_trajectory_transform.py +0 -0
  181. {diffct-2.5.0 → diffct-2.5.1}/tests/test_training_transforms.py +0 -0
  182. {diffct-2.5.0 → diffct-2.5.1}/tests/test_trajectory_admission_cost.py +0 -0
  183. {diffct-2.5.0 → diffct-2.5.1}/tests/test_trajectory_surface_integration.py +0 -0
  184. {diffct-2.5.0 → diffct-2.5.1}/tests/test_transfer_pipeline.py +0 -0
  185. {diffct-2.5.0 → diffct-2.5.1}/tests/test_transfer_retirement_lifetimes.py +0 -0
  186. {diffct-2.5.0 → diffct-2.5.1}/tests/test_tv_zero_dependency.py +0 -0
  187. {diffct-2.5.0 → diffct-2.5.1}/tests/test_two_far_endpoints.py +0 -0
  188. {diffct-2.5.0 → diffct-2.5.1}/tests/test_weights.py +0 -0
@@ -5,6 +5,34 @@ 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
+ ## [2.5.1] - 2026-11-07
9
+
10
+ ### Fixed
11
+
12
+ - Saving a solver checkpoint into a step that already exists raises a clear
13
+ `ValueError` that names the existing directory, instead of a bare
14
+ `FileExistsError`. The existing checkpoint is not changed. Use a new
15
+ `checkpoint_directory`, or pass `resume=` to continue from it. Thanks to
16
+ @cslht11 ([#63](https://github.com/sypsyp97/diffct/pull/63)).
17
+ - `random_trajectory_3d(seed=...)` draws its noise from a private generator
18
+ and no longer reseeds the process-wide PyTorch generator. Seeded
19
+ trajectories are unchanged. Thanks to @cslht11
20
+ ([#64](https://github.com/sypsyp97/diffct/pull/64)).
21
+
22
+ ### Changed
23
+
24
+ - CPU tests check the FBP, FDK and short-scan Parker accuracy bounds on the
25
+ portable backend, so CI covers them without a GPU. Thanks to @cslht11
26
+ ([#65](https://github.com/sypsyp97/diffct/pull/65)).
27
+ - A `.mailmap` joins the commits of one contributor under one name
28
+ ([#62](https://github.com/sypsyp97/diffct/pull/62)).
29
+
30
+ ### Documentation
31
+
32
+ - Semantic search on the documentation site is off by default. The
33
+ embedding model (about 377 MB) downloads only after the visitor enables it
34
+ ([#66](https://github.com/sypsyp97/diffct/pull/66)).
35
+
8
36
  ## [2.5.0] - 2026-10-26
9
37
 
10
38
  ### Added
@@ -14,8 +14,8 @@ authors:
14
14
  given-names: Yipeng
15
15
  email: yipeng.sun@fau.de
16
16
  alias: sypsyp97
17
- version: 2.5.0
18
- date-released: "2026-10-26"
17
+ version: 2.5.1
18
+ date-released: "2026-11-07"
19
19
  license: Apache-2.0
20
20
  repository-code: "https://github.com/sypsyp97/diffct"
21
21
  url: "https://sypsyp97.github.io/diffct/"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: diffct
3
- Version: 2.5.0
3
+ Version: 2.5.1
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/
@@ -64,7 +64,7 @@ try:
64
64
  except ImportError:
65
65
  pass
66
66
 
67
- __version__ = '2.5.0'
67
+ __version__ = '2.5.1'
68
68
 
69
69
  __all__ = [
70
70
  'Projector',
@@ -288,6 +288,11 @@ def save_checkpoint(operator, directory, states, metadata, iteration, history, m
288
288
  step = Path(directory).resolve() / f"step-{iteration:06d}"
289
289
  step.mkdir(parents=True, exist_ok=True)
290
290
  rank = step / f"rank-{getattr(operator, 'rank', 0):05d}"
291
+ if rank.exists():
292
+ # Replacing a published step in place could destroy it on a
293
+ # failed write, and ranks cannot roll back together.
294
+ raise ValueError(f"checkpoint {rank} already exists; use a new checkpoint_directory "
295
+ "or pass resume= to continue from it")
291
296
  rank.mkdir()
292
297
  for name, source in states.items():
293
298
  source = TensorStore(source.detach()) if isinstance(source, torch.Tensor) else source
@@ -274,18 +274,24 @@ def random_trajectory_3d(n_views, sid_mean, sdd_mean, sid_std=0.0, pos_std=0.0,
274
274
  """
275
275
  import math
276
276
 
277
- if seed is not None:
278
- torch.manual_seed(seed)
279
-
280
277
  # Start with circular trajectory
281
278
  src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(
282
279
  n_views, sid_mean, sdd_mean, device=device, dtype=dtype
283
280
  )
284
281
 
282
+ # Angle tensors override the requested device. Keep seeded draws on that
283
+ # device without changing any process-wide RNG state.
284
+ generator = None
285
+ if seed is not None:
286
+ # No fallback stream: a different generator would change the values.
287
+ generator = torch.Generator(device=src_pos.device)
288
+ # Match torch.manual_seed's conversion of bool, NumPy and tensor seeds.
289
+ generator.manual_seed(int(seed))
290
+
285
291
  # Add random perturbations
286
292
  if sid_std > 0.0:
287
293
  # Random SID variations
288
- sid_perturbations = torch.randn(n_views, device=device, dtype=dtype) * sid_std
294
+ sid_perturbations = torch.randn(n_views, device=device, dtype=dtype, generator=generator) * sid_std
289
295
  sdd_perturbations = sid_perturbations.clone() # Keep SDD-SID distance approximately constant
290
296
 
291
297
  # Compute angles from existing positions
@@ -306,12 +312,12 @@ def random_trajectory_3d(n_views, sid_mean, sdd_mean, sid_std=0.0, pos_std=0.0,
306
312
 
307
313
  if pos_std > 0.0:
308
314
  # Random 3D position offsets
309
- src_pos += torch.randn_like(src_pos) * pos_std
310
- det_center += torch.randn_like(det_center) * pos_std
315
+ src_pos += torch.empty_like(src_pos).normal_(generator=generator) * pos_std
316
+ det_center += torch.empty_like(det_center).normal_(generator=generator) * pos_std
311
317
 
312
318
  if angle_std > 0.0:
313
319
  # Random angular perturbations (rotate detector orientation)
314
- angle_perturbations = torch.randn(n_views, device=device, dtype=dtype) * angle_std
320
+ angle_perturbations = torch.randn(n_views, device=device, dtype=dtype, generator=generator) * angle_std
315
321
  cos_perturb = torch.cos(angle_perturbations)
316
322
  sin_perturb = torch.sin(angle_perturbations)
317
323
 
@@ -7,7 +7,7 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { chromium } from '@playwright/test';
10
- import { test, expect, landingUI, typeQuery, FULL_MODEL } from './harness.mjs';
10
+ import { test, expect, landingUI, typeQuery, enableSemanticButton, FULL_MODEL } from './harness.mjs';
11
11
 
12
12
  const HUB = /(^|\.)(huggingface\.co|hf\.co)$/;
13
13
 
@@ -34,6 +34,7 @@ test('real model: semantic results, then a reload with no second download', asyn
34
34
  await page.goto('/index.html');
35
35
  let ui = landingUI(page);
36
36
  await typeQuery(ui.input, 'how do I use a curved detector');
37
+ await enableSemanticButton(ui.host).click();
37
38
  let lb = await ui.listbox.locator();
38
39
  await expect(lb.getByText(/^(Semantic|Both)$/).first()).toBeVisible({ timeout: 25 * 60_000 });
39
40
  const first = downloads();
@@ -255,6 +255,19 @@ export class FakeHub extends EventEmitter {
255
255
 
256
256
  // ---- page helpers -------------------------------------------------------------------------
257
257
 
258
+ /** Seed consent without overwriting choices made during the test or on reload. */
259
+ export async function seedSemanticPreference(context) {
260
+ await context.addInitScript(() => {
261
+ if (localStorage.getItem('diffct-search') === null) {
262
+ localStorage.setItem('diffct-search', JSON.stringify({ semantic: 'on' }));
263
+ }
264
+ });
265
+ }
266
+
267
+ export function enableSemanticButton(host) {
268
+ return host.getByRole('button', { name: 'Enable semantic search (about 377 MB)', exact: true });
269
+ }
270
+
258
271
  export function landingUI(page) {
259
272
  const input = page.locator('.landing-search input[name="q"]');
260
273
  return { input, host: page.locator('.landing-search .srch-host'), listbox: listboxOf(input) };
@@ -418,7 +431,7 @@ export async function routeFakeHub(context, hub) {
418
431
 
419
432
  /**
420
433
  * The results that `query` shows in a fresh browser context, as a reference.
421
- * mode 'keyword': the model download never finishes, so only keyword results show.
434
+ * mode 'keyword': a fresh visitor has not enabled semantic search.
422
435
  * mode 'semantic': the model loads and the semantic answer for `query` merges in.
423
436
  * where: 'landing' (index.html) or 'dialog' (quickstart.html, Ctrl+K dialog).
424
437
  */
@@ -426,7 +439,7 @@ export async function reference(browser, server, query, mode, where = 'landing')
426
439
  const context = await browser.newContext({ baseURL: server.origin });
427
440
  const hub = new FakeHub();
428
441
  await routeFakeHub(context, hub);
429
- if (mode === 'keyword') hub.hold('onnx/model_quantized.onnx');
442
+ if (mode === 'semantic') await seedSemanticPreference(context);
430
443
  try {
431
444
  const page = await context.newPage();
432
445
  let ui;
@@ -451,6 +464,11 @@ export async function reference(browser, server, query, mode, where = 'landing')
451
464
  // ---- fixtures -----------------------------------------------------------------------------
452
465
 
453
466
  export const test = base.extend({
467
+ semanticSearch: [false, { option: true }],
468
+ page: async ({ page, semanticSearch }, use) => {
469
+ if (semanticSearch) await seedSemanticPreference(page.context());
470
+ await use(page);
471
+ },
454
472
  server: [async ({}, use) => {
455
473
  const server = await startServer(process.env.DOCS_HTML);
456
474
  await use(server);
@@ -5,6 +5,8 @@ import {
5
5
  test, expect, landingUI, dialogUI, typeQuery, snapshot, stableSnapshot, reference, sleep,
6
6
  } from './harness.mjs';
7
7
 
8
+ test.use({ semanticSearch: true });
9
+
8
10
  test.describe('item 1: ordering', () => {
9
11
  test('slow semantic answer of an older query does not replace the newer query results', async ({ browser, page, site, hub }) => {
10
12
  const Q1 = 'cone beam geometry';
@@ -5,6 +5,8 @@ import {
5
5
  test, expect, dialogUI, landingUI, typeQuery, stableSnapshot, trackWorkers, sleep,
6
6
  } from './harness.mjs';
7
7
 
8
+ test.use({ semanticSearch: true });
9
+
8
10
  const WEIGHTS = 'onnx/model_quantized.onnx';
9
11
  const FILES = ['onnx/model_quantized.onnx_data', 'tokenizer.json', 'tokenizer_config.json'];
10
12
 
@@ -58,10 +60,16 @@ test.describe('item 2: single model load', () => {
58
60
  await site.waitProbe('embed', (p) => p.text.endsWith('fan beam'));
59
61
  await stableSnapshot(ui.listbox);
60
62
  const before = hub.downloadLog().length;
63
+ await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('diffct-search')))).toMatchObject({
64
+ semantic: 'on', loaded: true,
65
+ });
61
66
  const sessionsBefore = site.probes('session').length;
62
67
 
63
68
  await page.reload();
64
69
  const again = landingUI(page);
70
+ await again.input.focus();
71
+ await expect.poll(() => site.probes('session').length).toBe(sessionsBefore + 1);
72
+ await expect(again.input).toHaveValue('');
65
73
  await typeQuery(again.input, 'cone beam');
66
74
  await site.waitProbe('embed', (p) => p.text.endsWith('cone beam'));
67
75
  await stableSnapshot(again.listbox);
@@ -2,8 +2,11 @@
2
2
  // the choice persists across reloads; turning semantic search back on resumes cleanly.
3
3
  import {
4
4
  test, expect, landingUI, typeQuery, snapshot, stableSnapshot, statusText, trackWorkers, trackConsole, sleep,
5
+ enableSemanticButton,
5
6
  } from './harness.mjs';
6
7
 
8
+ test.use({ semanticSearch: true });
9
+
7
10
  const WEIGHTS = 'onnx/model_quantized.onnx';
8
11
 
9
12
  test.describe('item 3: cancel', () => {
@@ -19,6 +22,7 @@ test.describe('item 3: cancel', () => {
19
22
  let requestsAtCancel = 0;
20
23
  await test.step('"Keyword only" stops further model requests', async () => {
21
24
  await ui.host.getByRole('button', { name: /keyword only/i }).click();
25
+ expect(await page.evaluate(() => JSON.parse(localStorage.getItem('diffct-search')).semantic)).toBe('off');
22
26
  requestsAtCancel = hub.log.length;
23
27
  held.release(); // the request in flight may finish; nothing new may start
24
28
  await sleep(2000);
@@ -38,13 +42,14 @@ test.describe('item 3: cancel', () => {
38
42
  expect(hub.log.slice(requestsAtCancel).map((r) => r.url), 'model requests after reload').toEqual([]);
39
43
  expect(workers.length - workersBefore, 'model workers started after reload').toBe(0);
40
44
  expect((await stableSnapshot(ui.listbox)).length, 'keyword results').toBeGreaterThan(0);
41
- await expect(ui.host.getByRole('button', { name: /semantic/i })).toBeVisible();
45
+ await expect(enableSemanticButton(ui.host)).toBeVisible();
42
46
  });
43
47
 
44
48
  await test.step('turning semantic search back on resumes cleanly', async () => {
45
49
  const errorsBefore = errors.length;
46
50
  const downloadsBefore = hub.downloads(WEIGHTS);
47
- await ui.host.getByRole('button', { name: /semantic/i }).click();
51
+ await enableSemanticButton(ui.host).click();
52
+ await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('diffct-search')).semantic)).toBe('on');
48
53
  await site.waitProbe('session', () => true, 30_000);
49
54
  await site.waitProbe('embed', (p) => p.text.endsWith('multi GPU'), 30_000);
50
55
  await stableSnapshot(ui.listbox);
@@ -78,9 +83,9 @@ test.describe('item 3: cancel', () => {
78
83
  await typeQuery(ui.input, 'fan beam');
79
84
  await held.seen;
80
85
  await ui.host.getByRole('button', { name: /keyword only/i }).click();
81
- await expect(ui.host.getByRole('button', { name: /semantic/i })).toBeVisible();
86
+ await expect(enableSemanticButton(ui.host)).toBeVisible();
82
87
  hub.clear(); // later downloads go through at once
83
- await ui.host.getByRole('button', { name: /semantic/i }).click();
88
+ await enableSemanticButton(ui.host).click();
84
89
  held.release();
85
90
  await site.waitProbe('embed', (p) => p.text.endsWith('fan beam'), 30_000);
86
91
  await stableSnapshot(ui.listbox);
@@ -6,6 +6,8 @@ import {
6
6
  test, expect, dialogUI, typeQuery, clearQuery, stableSnapshot, cdpWithWorkers, trackWorkers, sleep,
7
7
  } from './harness.mjs';
8
8
 
9
+ test.use({ semanticSearch: true });
10
+
9
11
  // Bounds fixed before the first run. Both measurements are taken in the same UI state
10
12
  // (dialog closed, input empty) after forced garbage collection.
11
13
  const HEAP_BOUND = 1024 * 1024; // bytes, page and worker each
@@ -5,6 +5,8 @@ import {
5
5
  trackWorkers, sleep,
6
6
  } from './harness.mjs';
7
7
 
8
+ test.use({ semanticSearch: true });
9
+
8
10
  const WEIGHTS = 'onnx/model_quantized.onnx';
9
11
  const NEXT = 'cone beam geometry';
10
12
 
@@ -4,6 +4,8 @@ import {
4
4
  test, expect, landingUI, dialogUI, searchPageUI, typeQuery, stableSnapshot, sleep,
5
5
  } from './harness.mjs';
6
6
 
7
+ test.use({ semanticSearch: true });
8
+
7
9
  async function open(page, where) {
8
10
  if (where === 'landing') {
9
11
  await page.goto('/index.html');
@@ -6,6 +6,8 @@ import {
6
6
  test, expect, landingUI, dialogUI, searchPageUI, typeQuery, stableSnapshot, sleep,
7
7
  } from './harness.mjs';
8
8
 
9
+ test.use({ semanticSearch: true });
10
+
9
11
  const PAYLOADS = [
10
12
  '<img src=x onerror=alert(1)>',
11
13
  '<img src=x onerror="window.__xss=1">',
@@ -0,0 +1,88 @@
1
+ // Semantic search requires consent on every search surface. Connection hints do not add
2
+ // another consent state. These tests deliberately use fresh storage, without preference seeding.
3
+ import {
4
+ test, expect, landingUI, dialogUI, searchPageUI, typeQuery, stableSnapshot,
5
+ enableSemanticButton, trackWorkers, sleep,
6
+ } from './harness.mjs';
7
+
8
+ async function openSearch(page, where, reload = false) {
9
+ if (reload) await page.reload();
10
+ else await page.goto(where === 'landing' ? '/index.html' : where === 'dialog' ? '/quickstart.html' : '/search.html');
11
+ if (where === 'dialog') {
12
+ await page.keyboard.press('ControlOrMeta+k');
13
+ const ui = dialogUI(page);
14
+ await expect(ui.dialog).toBeVisible();
15
+ return ui;
16
+ }
17
+ return where === 'landing' ? landingUI(page) : searchPageUI(page);
18
+ }
19
+
20
+ async function expectKeywordOnly(ui, site, hub, workers) {
21
+ expect((await stableSnapshot(ui.listbox)).length, 'keyword results').toBeGreaterThan(0);
22
+ await expect(enableSemanticButton(ui.host)).toBeVisible();
23
+ await expect(ui.host.getByRole('button', { name: 'Download', exact: true })).toHaveCount(0);
24
+ const lb = await ui.listbox.locator();
25
+ await expect(lb.getByText(/^(Semantic|Both)$/)).toHaveCount(0);
26
+ // Observe beyond the query debounce, including any delayed automatic model load.
27
+ await sleep(1500);
28
+ expect(hub.log, 'all Hub requests, including metadata probes').toEqual([]);
29
+ expect(site.probes('pipeline'), 'model load attempts').toEqual([]);
30
+ expect(site.probes('session'), 'model sessions').toEqual([]);
31
+ expect(site.probes('embed'), 'semantic queries').toEqual([]);
32
+ expect(workers, 'workers before consent').toEqual([]);
33
+ }
34
+
35
+ async function expectSemantic(ui) {
36
+ const lb = await ui.listbox.locator();
37
+ await expect(lb.getByText(/^(Semantic|Both)$/).first()).toBeVisible({ timeout: 30_000 });
38
+ }
39
+
40
+ for (const where of ['landing', 'dialog', 'page']) {
41
+ test(`fresh visitor: keyword results and zero model requests (${where})`, async ({ page, site, hub }) => {
42
+ const workers = trackWorkers(page);
43
+ const ui = await openSearch(page, where);
44
+ expect(await page.evaluate(() => localStorage.getItem('diffct-search'))).toBeNull();
45
+ await typeQuery(ui.input, 'fan beam');
46
+ await expectKeywordOnly(ui, site, hub, workers);
47
+ await typeQuery(ui.input, 'cone beam');
48
+ await expectKeywordOnly(ui, site, hub, workers);
49
+ });
50
+
51
+ test(`enable click merges the current query and persists across reload (${where})`, async ({ page, site, hub }) => {
52
+ const workers = trackWorkers(page);
53
+ let ui = await openSearch(page, where);
54
+ await typeQuery(ui.input, 'fan beam');
55
+ await expectKeywordOnly(ui, site, hub, workers);
56
+ await enableSemanticButton(ui.host).click();
57
+ await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('diffct-search')))).toMatchObject({ semantic: 'on' });
58
+ await site.waitProbe('embed', (p) => p.text.endsWith('fan beam'));
59
+ await expectSemantic(ui);
60
+ await expect(ui.input).toHaveValue('fan beam');
61
+ expect(hub.downloads('onnx/model_quantized.onnx')).toBe(1);
62
+ expect(workers.length, 'model loads in one Web Worker').toBe(1);
63
+ expect(site.probes('session').length).toBe(1);
64
+
65
+ ui = await openSearch(page, where, true);
66
+ await typeQuery(ui.input, 'curved detector');
67
+ await site.waitProbe('embed', (p) => p.text.endsWith('curved detector'));
68
+ await expectSemantic(ui);
69
+ expect(site.probes('session').length, 'reload automatically loads a new session').toBe(2);
70
+ expect(await page.evaluate(() => JSON.parse(localStorage.getItem('diffct-search')).semantic)).toBe('on');
71
+ });
72
+ }
73
+
74
+ for (const connection of [{ saveData: true, type: 'wifi' }, { saveData: false, type: 'cellular' }]) {
75
+ test(`connection ${JSON.stringify(connection)} uses the same enable button without Download consent`, async ({ page, site, hub }) => {
76
+ await page.addInitScript((value) => {
77
+ Object.defineProperty(navigator, 'connection', { configurable: true, value });
78
+ }, connection);
79
+ const workers = trackWorkers(page);
80
+ const ui = await openSearch(page, 'landing');
81
+ await typeQuery(ui.input, 'fan beam');
82
+ await expectKeywordOnly(ui, site, hub, workers);
83
+ await enableSemanticButton(ui.host).click();
84
+ await site.waitProbe('embed', (p) => p.text.endsWith('fan beam'));
85
+ await expectSemantic(ui);
86
+ await expect(ui.host.getByRole('button', { name: 'Download', exact: true })).toHaveCount(0);
87
+ });
88
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "diffct"
7
- version = "2.5.0"
7
+ version = "2.5.1"
8
8
  description = "Differentiable CT projection and reconstruction: precision modes, streaming, distributed execution and geometry gradients"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"