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.
- {diffct-2.5.0 → diffct-2.5.1}/CHANGELOG.md +28 -0
- {diffct-2.5.0 → diffct-2.5.1}/CITATION.cff +2 -2
- {diffct-2.5.0 → diffct-2.5.1}/PKG-INFO +1 -1
- {diffct-2.5.0 → diffct-2.5.1}/diffct/__init__.py +1 -1
- {diffct-2.5.0 → diffct-2.5.1}/diffct/_solver_state.py +5 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/geometry.py +13 -7
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/full-model.spec.mjs +2 -1
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/harness.mjs +20 -2
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item1-ordering.spec.mjs +2 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item2-single-load.spec.mjs +8 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item3-cancel.spec.mjs +9 -4
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item4-leaks.spec.mjs +2 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item5-errors.spec.mjs +2 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item6-a11y.spec.mjs +2 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/item7-security.spec.mjs +2 -0
- diffct-2.5.1/docs/search/tests/semantic-opt-in.spec.mjs +88 -0
- {diffct-2.5.0 → diffct-2.5.1}/pyproject.toml +1 -1
- diffct-2.5.1/tests/test_geometry_input_validation.py +393 -0
- diffct-2.5.1/tests/test_portable_accuracy.py +141 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_checkpoint_edges.py +77 -0
- diffct-2.5.0/tests/test_geometry_input_validation.py +0 -179
- {diffct-2.5.0 → diffct-2.5.1}/.gitignore +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/LICENSE +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/NOTICE +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/_solver_tv.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/analytical.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/analytical_execution.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/backends.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/chunking.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/constants.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/differentiable.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/distribution.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/__init__.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/analytical_transpose.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/cone_beam.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/fan_beam.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/geometry_vjp.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/parallel_beam.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/spacing.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/kernels/surface_geometry.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/operators.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/pipeline.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/projectors.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/reconstruction.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/solvers.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/storage.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/streaming.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/surface_gradients.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/surfaces.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/diffct/utils.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/assets/gallery/NOTICE +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/.gitignore +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/make_tiny_model.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/config.json +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/onnx/model_quantized.onnx_data +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/tokenizer.json +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/fixtures/tiny-model/tokenizer_config.json +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/search/tests/playwright.config.mjs +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/docs/video/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/_block_cgls.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/_common.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/data/NOTICE +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/data/preprocess_walnut.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/examples/walnut_reconstruction.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/formal/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/formal/tla/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/pytest.ini +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/__init__.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/_multiprocessing.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/_native_per_axis_spacing.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/_phantoms.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/README.md +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/_common.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/distributed_projector_check.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/fixtures/native_scalar_spacing_main.npz +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_allocator_capacity.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_autograd.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_distribution.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_analytical_weight_distribution.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_angular_low_precision.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_axis_aligned_chords.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_batch_execution_edges.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_batched_surfaces.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_block_cgls.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_block_storage.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_call_trajectory.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cgls_state_admission.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_checkpoint_metadata_validation.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_chunked_projector.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_completed_transfer_reuse.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cone_backprojection_transpose.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cpu_install.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_sink_lifetimes.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_dtypes.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_mapping.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surface_units.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_detector_surfaces.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_distributed_training_transforms.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_example_psnr.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_buffers.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_features.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_execution_schedules.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_conversions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_gpu_performance_contracts.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_gradcheck.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_lazy_native_observers.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_live_trajectory_validation.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_mps_backend.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_multiprocessing.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_endpoint_precision.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_admission.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_extremes.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_helical.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_spacing.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_per_axis_spacing_gpu.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_surface_transforms.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_volume_layout.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_native_volume_layout_distributed.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_output_pipeline_overlap.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_parallel3d.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_parallel3d_surface_oracle.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_partition_lifecycle.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_plan_memory_bounds.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_bounded_autograd.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_cuda_budget.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_distributed_geometry.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_endpoint_precision.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_execution_bounds.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_numeric_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_sampler_snapshot.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_portable_surface_admission.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_pr42_review_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_api.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_reconstruction_api.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_reconstruction_work_reuse.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_reference_backend.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_release_2_0_1.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_review_230.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_review_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_admission_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_api.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_collective_failures.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_solver_device_scalars.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_spatial_partition.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_collective_failures.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_composition_kernel.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_compute_shards.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_geometry_cudasim.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_gpu_execution.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_gradient_replay.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_host_sampling_budget.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_native_regressions.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_parameter_updates.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_pipeline_contracts.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_saved_snapshot_budget.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_snapshot_admission.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_surface_trajectory_transform.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_training_transforms.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_trajectory_admission_cost.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_trajectory_surface_integration.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_transfer_pipeline.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_transfer_retirement_lifetimes.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_tv_zero_dependency.py +0 -0
- {diffct-2.5.0 → diffct-2.5.1}/tests/test_two_far_endpoints.py +0 -0
- {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.
|
|
18
|
-
date-released: "2026-
|
|
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.
|
|
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/
|
|
@@ -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.
|
|
310
|
-
det_center += torch.
|
|
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':
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
|
86
|
+
await expect(enableSemanticButton(ui.host)).toBeVisible();
|
|
82
87
|
hub.clear(); // later downloads go through at once
|
|
83
|
-
await ui.host
|
|
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
|
|
@@ -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.
|
|
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"
|