slide2vec 7.1.2__tar.gz → 7.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slide2vec-7.1.2 → slide2vec-7.1.4}/PKG-INFO +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.4}/pyproject.toml +3 -3
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/__init__.py +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/api.py +44 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/__init__.py +2 -0
- slide2vec-7.1.4/slide2vec/encoders/code.py +102 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/inference.py +7 -2
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_image_recipe.py +58 -2
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_image_stage.py +3 -28
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/feature_identity.py +81 -10
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/PKG-INFO +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/SOURCES.txt +3 -0
- slide2vec-7.1.4/tests/test_dense_identity_api.py +430 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_image_resume.py +5 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_image_shard.py +2 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_stage.py +16 -0
- slide2vec-7.1.4/tests/test_encoder_code.py +241 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_plugins.py +9 -1
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_feature_identity.py +106 -13
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_output_consistency.py +3 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_pooled_identity_api.py +12 -1
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_soma_migration.py +1 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/LICENSE +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/README.md +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/setup.cfg +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/__main__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/_cufile.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/artifacts.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/cli.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/configs/resources.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/cufile.json +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/data/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/data/dataset.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/pin_gpu.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/base.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/dinov3.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/mettle.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/progress.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/types.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/config.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec/utils/utils.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_attention_extraction.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_cli_noninteractive.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_cufile_compat_mode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_extraction.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_regions.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_sliding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dense_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_dinov3_natimage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_drop_in_encoders.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_capabilities.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_preprocessing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_provider_failures.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_encoder_registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_execution_options.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_gigapath.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_hierarchical_encoding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_hs2p5_integration.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_isight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_mascaret.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_mettle.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_patient_annotation_sampling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_patient_manifest.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_patient_tiling_only.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_phaet.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_prism.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_prism2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_process_list_performance.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_progress.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_regression_core.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_regression_inference.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_regression_models.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_rudolfv2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_runtime_batching.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_sharding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_slide_latents.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_tile_store.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_tiling_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_titan.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.4}/tests/test_worker_gpu_pinning.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "7.1.
|
|
7
|
+
version = "7.1.4"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -218,7 +218,7 @@ testpaths = [
|
|
|
218
218
|
"tests",
|
|
219
219
|
]
|
|
220
220
|
markers = [
|
|
221
|
-
"heavy: real-weight foundation-model inference on CPU; minutes per test.
|
|
221
|
+
"heavy: real-weight foundation-model inference on CPU; minutes per test. Excluded from the PR suite except the PRISM consistency test; run all of them through the manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
|
|
222
222
|
"gpu_integration: real one-GPU versus multi-GPU parity; requires at least two visible CUDA devices. Run explicitly with `CUDA_VISIBLE_DEVICES=0,1 python -m pytest -m gpu_integration --no-cov`.",
|
|
223
223
|
]
|
|
224
224
|
|
|
@@ -240,7 +240,7 @@ no_implicit_reexport = true
|
|
|
240
240
|
max-line-length = 160
|
|
241
241
|
|
|
242
242
|
[tool.bumpver]
|
|
243
|
-
current_version = "7.1.
|
|
243
|
+
current_version = "7.1.4"
|
|
244
244
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
245
245
|
commit = false
|
|
246
246
|
tag = false
|
|
@@ -708,6 +708,45 @@ class Model:
|
|
|
708
708
|
encoded_pixels=encoded_pixels,
|
|
709
709
|
)
|
|
710
710
|
|
|
711
|
+
def dense_identity_differences(
|
|
712
|
+
self,
|
|
713
|
+
recorded: Mapping[str, Any],
|
|
714
|
+
*,
|
|
715
|
+
dense: "DenseOptions | DenseImageOptions",
|
|
716
|
+
execution: ExecutionOptions | None = None,
|
|
717
|
+
) -> dict[str, tuple[Any, Any]]:
|
|
718
|
+
"""Compare a dense sidecar's ``compatibility`` block with the current request.
|
|
719
|
+
|
|
720
|
+
The dense counterpart of :meth:`pooled_identity_differences`. Pass the
|
|
721
|
+
``dense`` options and ``execution`` of :meth:`embed_regions_dense` (a
|
|
722
|
+
:class:`DenseOptions`) or :meth:`embed_images_dense` (a
|
|
723
|
+
:class:`DenseImageOptions`). Returns ``{field: (recorded, current)}`` for every
|
|
724
|
+
request-wide field that the record holds with a different value or does not
|
|
725
|
+
hold. A field the record does not hold is reported as
|
|
726
|
+
:data:`slide2vec.MISSING_FIELD`. Per-sample fields (the sample, its path and its
|
|
727
|
+
read plan) are not compared. An empty result means the record proves the
|
|
728
|
+
current request-wide identity. The caller owns the policy for a difference.
|
|
729
|
+
|
|
730
|
+
An invalid request raises as it does in the embed methods. ``execution``
|
|
731
|
+
defaults to the model's recommended precision and needs no output directory.
|
|
732
|
+
Comparing the ``transform`` loads weights, on an isolated CPU model. This method
|
|
733
|
+
does not change this model's input contract or loaded backend, encode pixels,
|
|
734
|
+
or write files.
|
|
735
|
+
"""
|
|
736
|
+
from slide2vec.runtime.feature_identity import dense_identity_differences
|
|
737
|
+
|
|
738
|
+
if not isinstance(dense, (DenseOptions, DenseImageOptions)):
|
|
739
|
+
raise TypeError(
|
|
740
|
+
"Model.dense_identity_differences(dense=...) expects DenseOptions "
|
|
741
|
+
f"or DenseImageOptions, got {type(dense).__name__}"
|
|
742
|
+
)
|
|
743
|
+
return dense_identity_differences(
|
|
744
|
+
self,
|
|
745
|
+
dict(recorded),
|
|
746
|
+
dense=dense,
|
|
747
|
+
execution=_coerce_execution_options(execution, model=self),
|
|
748
|
+
)
|
|
749
|
+
|
|
711
750
|
def prepare_dense_encoder(
|
|
712
751
|
self,
|
|
713
752
|
*,
|
|
@@ -1085,6 +1124,11 @@ class Model:
|
|
|
1085
1124
|
non-timm backbone, say) fails before any backend is loaded and before resume
|
|
1086
1125
|
inspects or invalidates an existing artifact.
|
|
1087
1126
|
"""
|
|
1127
|
+
if self.level != "tile":
|
|
1128
|
+
raise ValueError(
|
|
1129
|
+
"Dense extraction requires a tile-level foundation encoder; model "
|
|
1130
|
+
f"{self.name!r} is registered at level {self.level!r}."
|
|
1131
|
+
)
|
|
1088
1132
|
contract = EncoderInputContract.declared_dense(
|
|
1089
1133
|
self.name,
|
|
1090
1134
|
target_size_px=dense.target_size,
|
|
@@ -16,6 +16,7 @@ from slide2vec.encoders.base import (
|
|
|
16
16
|
resolve_recommended_dynamic_img_size,
|
|
17
17
|
resolve_requested_output_variant,
|
|
18
18
|
)
|
|
19
|
+
from slide2vec.encoders.code import encoder_code
|
|
19
20
|
from slide2vec.encoders.registry import (
|
|
20
21
|
EncoderCapabilities,
|
|
21
22
|
EncoderProviderDiagnostic,
|
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
|
42
43
|
"reshape_tokens_to_grid",
|
|
43
44
|
"resolve_recommended_dynamic_img_size",
|
|
44
45
|
"resolve_requested_output_variant",
|
|
46
|
+
"encoder_code",
|
|
45
47
|
"encoder_registry",
|
|
46
48
|
"list_encoder_provider_diagnostics",
|
|
47
49
|
"normalize_patch_size",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""The code a registered encoder runs, as the feature identity records it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
import functools
|
|
7
|
+
import hashlib
|
|
8
|
+
from importlib import metadata as importlib_metadata
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from slide2vec.encoders.registry import encoder_registry
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def encoder_code(name: str) -> dict[str, Any]:
|
|
20
|
+
"""Return the code block of the registered encoder *name*.
|
|
21
|
+
|
|
22
|
+
``object`` is the encoder class's ``module.qualname``. ``fingerprint`` is the
|
|
23
|
+
version of the non-editable installed distribution, other than slide2vec, whose
|
|
24
|
+
``RECORD`` lists the file that defines the class; otherwise the SHA-256 of that
|
|
25
|
+
file, hashed on every call; ``None`` when there is no file (a class defined in a
|
|
26
|
+
notebook cell). ``source`` is the registry's ``source``. Reads the registry entry
|
|
27
|
+
only: the encoder is not instantiated and no weights are loaded.
|
|
28
|
+
"""
|
|
29
|
+
encoder_cls = encoder_registry.require(name)
|
|
30
|
+
return {
|
|
31
|
+
"object": f"{encoder_cls.__module__}.{encoder_cls.__qualname__}",
|
|
32
|
+
"fingerprint": _fingerprint(encoder_cls),
|
|
33
|
+
"source": encoder_registry.info(name)["source"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fingerprint(encoder_cls: type) -> str | None:
|
|
38
|
+
path = _source_file(encoder_cls)
|
|
39
|
+
if path is None:
|
|
40
|
+
return None
|
|
41
|
+
distribution = _installed_distribution(encoder_cls.__module__, path)
|
|
42
|
+
return _sha256(path) if distribution is None else distribution.version
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _installed_distribution(module: str, path: Path) -> importlib_metadata.Distribution | None:
|
|
46
|
+
"""The non-editable installed distribution, other than slide2vec, whose ``RECORD``
|
|
47
|
+
lists *path*, or ``None``.
|
|
48
|
+
|
|
49
|
+
slide2vec is left out so that a built-in encoder's identity changes only when a
|
|
50
|
+
release changes its file. The distributions of a namespace package share its
|
|
51
|
+
top-level name and its install directory, so the top-level name only narrows the
|
|
52
|
+
candidates.
|
|
53
|
+
"""
|
|
54
|
+
top = module.split(".", 1)[0]
|
|
55
|
+
for name in _packages_distributions(tuple(sys.path)).get(top, ()):
|
|
56
|
+
if re.sub(r"[-_.]+", "-", name).lower() == "slide2vec":
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
distribution = importlib_metadata.distribution(name)
|
|
60
|
+
except importlib_metadata.PackageNotFoundError:
|
|
61
|
+
continue
|
|
62
|
+
if distribution.read_text("RECORD") is None or _is_editable(distribution):
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
relative = path.relative_to(Path(distribution.locate_file("")).resolve())
|
|
66
|
+
except ValueError:
|
|
67
|
+
continue
|
|
68
|
+
if relative.as_posix() in {file.as_posix() for file in distribution.files or ()}:
|
|
69
|
+
return distribution
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_editable(distribution) -> bool:
|
|
74
|
+
"""Whether *distribution* is an editable install, as its ``direct_url.json`` records."""
|
|
75
|
+
try:
|
|
76
|
+
payload = json.loads(distribution.read_text("direct_url.json") or "{}")
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
return False
|
|
79
|
+
return isinstance(payload, dict) and bool((payload.get("dir_info") or {}).get("editable"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@functools.lru_cache(maxsize=1)
|
|
83
|
+
def _packages_distributions(path: tuple[str, ...]) -> Mapping[str, list[str]]:
|
|
84
|
+
"""Top-level package -> distributions on the import *path*, scanned once per path."""
|
|
85
|
+
return importlib_metadata.packages_distributions()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _source_file(encoder_cls: type) -> Path | None:
|
|
89
|
+
"""The file that defines *encoder_cls*, or ``None`` (a notebook cell)."""
|
|
90
|
+
try:
|
|
91
|
+
path = Path(inspect.getsourcefile(encoder_cls) or inspect.getfile(encoder_cls))
|
|
92
|
+
except (TypeError, OSError):
|
|
93
|
+
return None
|
|
94
|
+
return path.resolve() if path.is_file() else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sha256(path: Path) -> str:
|
|
98
|
+
digest = hashlib.sha256()
|
|
99
|
+
with path.open("rb") as handle:
|
|
100
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
101
|
+
digest.update(chunk)
|
|
102
|
+
return digest.hexdigest()
|
|
@@ -725,8 +725,8 @@ def aggregate_tiles(
|
|
|
725
725
|
execution=execution,
|
|
726
726
|
)
|
|
727
727
|
latents = None
|
|
728
|
-
# Aggregation encodes no tile: the tile geometry and
|
|
729
|
-
# recorded with the tile artifact.
|
|
728
|
+
# Aggregation encodes no tile: the tile geometry, transform and tile encoder code
|
|
729
|
+
# are the ones recorded with the tile artifact.
|
|
730
730
|
tile_identity = metadata.get("compatibility") or {}
|
|
731
731
|
compatibility = {
|
|
732
732
|
**feature_identity.pooled_feature_identity(model, execution=execution),
|
|
@@ -736,6 +736,11 @@ def aggregate_tiles(
|
|
|
736
736
|
if field in tile_identity
|
|
737
737
|
},
|
|
738
738
|
}
|
|
739
|
+
# A slide run's tile sidecar records the tile encoder's code under `tile_encoder_code`.
|
|
740
|
+
compatibility.pop("tile_encoder_code", None)
|
|
741
|
+
tile_code = tile_identity.get("tile_encoder_code") or tile_identity.get("encoder_code")
|
|
742
|
+
if tile_code is not None:
|
|
743
|
+
compatibility["tile_encoder_code"] = tile_code
|
|
739
744
|
slide_artifact = embedding.write_slide_embedding_artifact(
|
|
740
745
|
artifact.sample_id,
|
|
741
746
|
slide_embedding,
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass, fields
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
|
-
from slide2vec.encoders.
|
|
10
|
+
from slide2vec.encoders.code import encoder_code
|
|
11
|
+
from slide2vec.encoders.registry import resolve_encoder_output, resolve_preprocessing_defaults
|
|
12
|
+
from slide2vec.encoders.validation import validate_encoder_config
|
|
10
13
|
from slide2vec.runtime.model_settings import output_dtype_name, resolve_output_precision
|
|
11
14
|
|
|
12
15
|
|
|
@@ -67,6 +70,8 @@ class DenseImageRecipe:
|
|
|
67
70
|
"""Request-wide extraction facts combined with one parent-resolved image read plan."""
|
|
68
71
|
|
|
69
72
|
encoder_name: str
|
|
73
|
+
#: The encoder's code block (:func:`slide2vec.encoders.encoder_code`).
|
|
74
|
+
encoder_code: dict[str, Any]
|
|
70
75
|
output_variant: str
|
|
71
76
|
reader_regime: str
|
|
72
77
|
spacing_source: str
|
|
@@ -99,6 +104,7 @@ class DenseImageRecipe:
|
|
|
99
104
|
def to_dict(self) -> dict[str, Any]:
|
|
100
105
|
payload = {
|
|
101
106
|
"encoder_name": self.encoder_name,
|
|
107
|
+
"encoder_code": self.encoder_code,
|
|
102
108
|
"output_variant": self.output_variant,
|
|
103
109
|
"reader_regime": self.reader_regime,
|
|
104
110
|
"spacing_source": self.spacing_source,
|
|
@@ -130,6 +136,20 @@ class DenseImageRecipe:
|
|
|
130
136
|
payload["transform"] = self.transform
|
|
131
137
|
return payload
|
|
132
138
|
|
|
139
|
+
def request_identity(self) -> dict[str, Any]:
|
|
140
|
+
"""The request-wide fields of :meth:`for_image`: the recipe without per-image fields.
|
|
141
|
+
|
|
142
|
+
The per-image fields are the read plan's, which :meth:`for_image` replaces, and
|
|
143
|
+
the read sizes.
|
|
144
|
+
"""
|
|
145
|
+
from slide2vec.runtime.dense_image_reading import DenseImageReadPlan
|
|
146
|
+
|
|
147
|
+
per_image = {field.name for field in fields(DenseImageReadPlan)} | {
|
|
148
|
+
"read_tile_size_px",
|
|
149
|
+
"requested_tile_size_px",
|
|
150
|
+
}
|
|
151
|
+
return {key: value for key, value in self.to_dict().items() if key not in per_image}
|
|
152
|
+
|
|
133
153
|
def for_image(self, spec, read_plan=None) -> dict[str, Any]:
|
|
134
154
|
"""Add the normalized source identity to this request-wide recipe."""
|
|
135
155
|
if read_plan is None:
|
|
@@ -142,6 +162,7 @@ class DenseImageRecipe:
|
|
|
142
162
|
)
|
|
143
163
|
recipe = self.to_dict()
|
|
144
164
|
encoder_name = recipe.pop("encoder_name")
|
|
165
|
+
code = recipe.pop("encoder_code")
|
|
145
166
|
output_variant = recipe.pop("output_variant")
|
|
146
167
|
for field in read_plan.to_dict():
|
|
147
168
|
recipe.pop(field, None)
|
|
@@ -149,6 +170,7 @@ class DenseImageRecipe:
|
|
|
149
170
|
"sample_id": str(spec.sample_id),
|
|
150
171
|
"image_path": str(Path(spec.image_path).expanduser().resolve()),
|
|
151
172
|
"encoder_name": encoder_name,
|
|
173
|
+
"encoder_code": code,
|
|
152
174
|
"output_variant": output_variant,
|
|
153
175
|
**read_plan.to_dict(),
|
|
154
176
|
**recipe,
|
|
@@ -158,6 +180,7 @@ class DenseImageRecipe:
|
|
|
158
180
|
def from_dict(cls, payload: dict[str, Any]) -> "DenseImageRecipe":
|
|
159
181
|
return cls(
|
|
160
182
|
encoder_name=str(payload["encoder_name"]),
|
|
183
|
+
encoder_code=dict(payload["encoder_code"]),
|
|
161
184
|
output_variant=str(payload["output_variant"]),
|
|
162
185
|
reader_regime=str(payload["reader_regime"]),
|
|
163
186
|
spacing_source=str(payload["spacing_source"]),
|
|
@@ -218,6 +241,38 @@ class DenseImageRecipe:
|
|
|
218
241
|
)
|
|
219
242
|
|
|
220
243
|
|
|
244
|
+
def resolve_dense_image_spacing(model, dense) -> tuple[float, str]:
|
|
245
|
+
"""Resolve and validate the run spacing of *dense*: ``(spacing_um, spacing_source)``.
|
|
246
|
+
|
|
247
|
+
An omitted spacing resolves the encoder's single registry default.
|
|
248
|
+
"""
|
|
249
|
+
spacing_source = "explicit"
|
|
250
|
+
if dense.spacing_um is None:
|
|
251
|
+
try:
|
|
252
|
+
spacing_um = float(
|
|
253
|
+
resolve_preprocessing_defaults(model.name)["spacing_um"]
|
|
254
|
+
)
|
|
255
|
+
except (KeyError, ValueError) as exc:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"DenseImageOptions.spacing_um must be explicit when encoder "
|
|
258
|
+
f"{model.name!r} has no single resolvable model default: {exc}"
|
|
259
|
+
) from exc
|
|
260
|
+
spacing_source = "model_default"
|
|
261
|
+
else:
|
|
262
|
+
spacing_um = float(dense.spacing_um)
|
|
263
|
+
if not math.isfinite(spacing_um) or spacing_um <= 0:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
"DenseImageOptions.spacing_um must be a positive, finite value or None."
|
|
266
|
+
)
|
|
267
|
+
validate_encoder_config(
|
|
268
|
+
model.name,
|
|
269
|
+
requested_spacing_um=spacing_um,
|
|
270
|
+
allow_non_recommended=bool(model.allow_non_recommended_settings),
|
|
271
|
+
require_known_spacing=True,
|
|
272
|
+
)
|
|
273
|
+
return spacing_um, spacing_source
|
|
274
|
+
|
|
275
|
+
|
|
221
276
|
def resolve_dense_image_recipe(
|
|
222
277
|
*,
|
|
223
278
|
model,
|
|
@@ -270,6 +325,7 @@ def resolve_dense_image_recipe(
|
|
|
270
325
|
)
|
|
271
326
|
return DenseImageRecipe(
|
|
272
327
|
encoder_name=str(model.name),
|
|
328
|
+
encoder_code=encoder_code(model.name),
|
|
273
329
|
output_variant=str(output["output_variant"]),
|
|
274
330
|
reader_regime=str(reader_regime),
|
|
275
331
|
spacing_source=(
|
|
@@ -29,7 +29,6 @@ from __future__ import annotations
|
|
|
29
29
|
|
|
30
30
|
import json
|
|
31
31
|
import logging
|
|
32
|
-
import math
|
|
33
32
|
from dataclasses import replace
|
|
34
33
|
from pathlib import Path
|
|
35
34
|
from subprocess import Popen
|
|
@@ -37,8 +36,6 @@ from typing import Callable, Sequence
|
|
|
37
36
|
|
|
38
37
|
from slide2vec.api import DenseImageOptions, ImageSpec
|
|
39
38
|
from slide2vec.artifacts import DenseImageArtifact
|
|
40
|
-
from slide2vec.encoders.registry import resolve_preprocessing_defaults
|
|
41
|
-
from slide2vec.encoders.validation import validate_encoder_config
|
|
42
39
|
from slide2vec.progress import emit_progress
|
|
43
40
|
from slide2vec.runtime.dense_image_shard import (
|
|
44
41
|
dense_image_artifact_from_disk,
|
|
@@ -48,6 +45,7 @@ from slide2vec.runtime.dense_image_shard import (
|
|
|
48
45
|
from slide2vec.runtime.dense_image_recipe import (
|
|
49
46
|
DenseImageRecipe,
|
|
50
47
|
resolve_dense_image_recipe,
|
|
48
|
+
resolve_dense_image_spacing,
|
|
51
49
|
)
|
|
52
50
|
from slide2vec.runtime.dense_image_reading import DenseImageReadPlan, resolve_spacing_read_plan
|
|
53
51
|
from slide2vec.runtime.dense_stage import resolve_output_torch_dtype
|
|
@@ -115,31 +113,8 @@ def embed_images_dense(
|
|
|
115
113
|
artifact_location="dense_image_embeddings/<sample_id>",
|
|
116
114
|
)
|
|
117
115
|
reader_regime, probed_auto_backends = validate_dense_image_reader_regime(specs)
|
|
118
|
-
spacing_source =
|
|
119
|
-
|
|
120
|
-
try:
|
|
121
|
-
spacing_um = float(
|
|
122
|
-
resolve_preprocessing_defaults(model.name)["spacing_um"]
|
|
123
|
-
)
|
|
124
|
-
except (KeyError, ValueError) as exc:
|
|
125
|
-
raise ValueError(
|
|
126
|
-
"DenseImageOptions.spacing_um must be explicit when encoder "
|
|
127
|
-
f"{model.name!r} has no single resolvable model default: {exc}"
|
|
128
|
-
) from exc
|
|
129
|
-
spacing_source = "model_default"
|
|
130
|
-
dense = replace(dense, spacing_um=spacing_um)
|
|
131
|
-
else:
|
|
132
|
-
spacing_um = float(dense.spacing_um)
|
|
133
|
-
if spacing_um is not None and (not math.isfinite(spacing_um) or spacing_um <= 0):
|
|
134
|
-
raise ValueError(
|
|
135
|
-
"DenseImageOptions.spacing_um must be a positive, finite value or None."
|
|
136
|
-
)
|
|
137
|
-
validate_encoder_config(
|
|
138
|
-
model.name,
|
|
139
|
-
requested_spacing_um=spacing_um,
|
|
140
|
-
allow_non_recommended=bool(model.allow_non_recommended_settings),
|
|
141
|
-
require_known_spacing=True,
|
|
142
|
-
)
|
|
116
|
+
spacing_um, spacing_source = resolve_dense_image_spacing(model, dense)
|
|
117
|
+
dense = replace(dense, spacing_um=spacing_um)
|
|
143
118
|
# Declare the effective encoder input before anything is decoded, sharded or launched: an
|
|
144
119
|
# image geometry this encoder cannot accept must fail here, not on the first forward pass
|
|
145
120
|
# of a torchrun rank. Idempotent, so every rank re-declares for itself (dense_image_worker).
|
|
@@ -11,6 +11,7 @@ from PIL import Image
|
|
|
11
11
|
from transformers.image_processing_utils import BaseImageProcessor
|
|
12
12
|
|
|
13
13
|
from slide2vec.encoders.base import ProcessorTransform
|
|
14
|
+
from slide2vec.encoders.code import encoder_code
|
|
14
15
|
from slide2vec.encoders.registry import encoder_registry
|
|
15
16
|
from slide2vec.runtime.model_settings import output_dtype_name, resolve_output_precision
|
|
16
17
|
from slide2vec.runtime.preprocessing import iter_transform_steps
|
|
@@ -35,6 +36,7 @@ def pooled_feature_identity(
|
|
|
35
36
|
if model.level != "tile" and model.name in encoder_registry:
|
|
36
37
|
info = encoder_registry.info(model.name)
|
|
37
38
|
identity["tile_encoder"] = info["tile_encoder"]
|
|
39
|
+
identity["tile_encoder_code"] = encoder_code(info["tile_encoder"])
|
|
38
40
|
identity["tile_encoder_output_variant"] = info["tile_encoder_output_variant"]
|
|
39
41
|
if info.get("aggregation_revision") is not None:
|
|
40
42
|
identity["aggregation_revision"] = info["aggregation_revision"]
|
|
@@ -74,15 +76,16 @@ def dense_feature_identity(model, *, dense, execution) -> dict[str, Any]:
|
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
def _encoder_identity(model, execution) -> dict[str, Any]:
|
|
77
|
-
"""The fields every path records: the encoder, its output variant, the precision."""
|
|
79
|
+
"""The fields every path records: the encoder, its code, its output variant, the precision."""
|
|
78
80
|
from slide2vec.runtime.process_list import resolved_process_list_output_variant
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
identity: dict[str, Any] = {"encoder_name": model.name}
|
|
83
|
+
if model.name in encoder_registry:
|
|
84
|
+
identity["encoder_code"] = encoder_code(model.name)
|
|
85
|
+
identity["output_variant"] = resolved_process_list_output_variant(model)
|
|
86
|
+
# An unset inference precision runs without autocast, which is fp32.
|
|
87
|
+
identity["precision"] = execution.precision or "fp32"
|
|
88
|
+
return identity
|
|
86
89
|
|
|
87
90
|
|
|
88
91
|
def deferred_transform_record(
|
|
@@ -222,6 +225,69 @@ def pooled_identity_differences(
|
|
|
222
225
|
)
|
|
223
226
|
|
|
224
227
|
|
|
228
|
+
def dense_identity_differences(
|
|
229
|
+
model,
|
|
230
|
+
recorded: dict[str, Any],
|
|
231
|
+
*,
|
|
232
|
+
dense,
|
|
233
|
+
execution,
|
|
234
|
+
) -> dict[str, tuple[Any, Any]]:
|
|
235
|
+
"""Compare a recorded dense identity with the request-wide identity of the current request.
|
|
236
|
+
|
|
237
|
+
A :class:`~slide2vec.api.DenseImageOptions` request is compared with the image recipe,
|
|
238
|
+
any other with the region identity. The request is validated as the embed methods
|
|
239
|
+
validate it, on an isolated CPU copy of *model* that declares the dense encoder-input
|
|
240
|
+
contract, so *model* keeps its contract and backend. The transform is that copy's
|
|
241
|
+
``get_normalization_transform()``, as the shards record it.
|
|
242
|
+
"""
|
|
243
|
+
from slide2vec.api import DenseImageOptions, Model
|
|
244
|
+
|
|
245
|
+
source = Model.from_preset(
|
|
246
|
+
model.name,
|
|
247
|
+
device="cpu",
|
|
248
|
+
output_variant=model._output_variant,
|
|
249
|
+
allow_non_recommended_settings=model.allow_non_recommended_settings,
|
|
250
|
+
)
|
|
251
|
+
if isinstance(dense, DenseImageOptions):
|
|
252
|
+
from slide2vec.runtime.dense_image_recipe import (
|
|
253
|
+
resolve_dense_image_recipe,
|
|
254
|
+
resolve_dense_image_spacing,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
resolve_dense_image_spacing(source, dense) # validates; the spacing is per-sample
|
|
258
|
+
contract = source._declare_dense_encoder_input(dense, emit_run_info=False)
|
|
259
|
+
requested = resolve_dense_image_recipe(
|
|
260
|
+
model=source, contract=contract, dense=dense, execution=execution
|
|
261
|
+
).request_identity()
|
|
262
|
+
else:
|
|
263
|
+
from slide2vec.encoders.registry import resolve_patch_size
|
|
264
|
+
from slide2vec.runtime.dense_regions import (
|
|
265
|
+
compute_dense_geometry,
|
|
266
|
+
validate_dense_request_settings,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
plan = source._declare_dense_encoder_input(dense, emit_run_info=False).plan
|
|
270
|
+
# The region shards validate the same settings when they encode.
|
|
271
|
+
validate_dense_request_settings(
|
|
272
|
+
compute_dense_geometry(
|
|
273
|
+
target_size=plan.target_size_px,
|
|
274
|
+
patch_size=resolve_patch_size(plan.tile_encoder_name),
|
|
275
|
+
),
|
|
276
|
+
pad_mode=dense.pad_mode,
|
|
277
|
+
window_size=dense.window_size,
|
|
278
|
+
overlap=dense.overlap,
|
|
279
|
+
feature_kind=dense.feature_kind,
|
|
280
|
+
attention_blocks=dense.attention_blocks,
|
|
281
|
+
attention_include_registers=dense.attention_include_registers,
|
|
282
|
+
)
|
|
283
|
+
requested = dense_feature_identity(source, dense=dense, execution=execution)
|
|
284
|
+
|
|
285
|
+
def resolve_transform() -> dict[str, Any]:
|
|
286
|
+
return transform_record(source._load_backend().model.get_normalization_transform())
|
|
287
|
+
|
|
288
|
+
return differing_fields(recorded, requested, resolve_transform=resolve_transform)
|
|
289
|
+
|
|
290
|
+
|
|
225
291
|
class PooledResumeCheck:
|
|
226
292
|
"""Compare the completed pooled artifacts of one resume with the run's feature identity.
|
|
227
293
|
|
|
@@ -253,11 +319,16 @@ class PooledResumeCheck:
|
|
|
253
319
|
for field, values in differing.items()
|
|
254
320
|
for leaf, old, new in _leaf_differences(field, *values)
|
|
255
321
|
)
|
|
322
|
+
# A caller can request a recorded value, but not recorded code.
|
|
323
|
+
remedies = (
|
|
324
|
+
"Re-run into a new output_dir or delete the stale artifacts."
|
|
325
|
+
if differing.keys() & {"encoder_code", "tile_encoder_code"}
|
|
326
|
+
else "Re-run into a new output_dir, delete the stale artifacts, or request "
|
|
327
|
+
"the recorded values."
|
|
328
|
+
)
|
|
256
329
|
raise ValueError(
|
|
257
330
|
f"Cannot resume '{sample_id}': the existing {kind} embeddings at {path} "
|
|
258
|
-
f"were computed with a different feature identity: {details}.
|
|
259
|
-
"a new output_dir, delete the stale artifacts, or request the recorded "
|
|
260
|
-
"values."
|
|
331
|
+
f"were computed with a different feature identity: {details}. {remedies}"
|
|
261
332
|
)
|
|
262
333
|
return not missing
|
|
263
334
|
|
|
@@ -34,6 +34,7 @@ slide2vec/distributed/pipeline_worker.py
|
|
|
34
34
|
slide2vec/distributed/worker_entry.py
|
|
35
35
|
slide2vec/encoders/__init__.py
|
|
36
36
|
slide2vec/encoders/base.py
|
|
37
|
+
slide2vec/encoders/code.py
|
|
37
38
|
slide2vec/encoders/registry.py
|
|
38
39
|
slide2vec/encoders/validation.py
|
|
39
40
|
slide2vec/encoders/models/__init__.py
|
|
@@ -122,6 +123,7 @@ tests/test_cufile_compat_mode.py
|
|
|
122
123
|
tests/test_dense_encode_kit.py
|
|
123
124
|
tests/test_dense_encoder_input.py
|
|
124
125
|
tests/test_dense_extraction.py
|
|
126
|
+
tests/test_dense_identity_api.py
|
|
125
127
|
tests/test_dense_image_reading.py
|
|
126
128
|
tests/test_dense_image_resume.py
|
|
127
129
|
tests/test_dense_image_shard.py
|
|
@@ -136,6 +138,7 @@ tests/test_dinov2_natimage.py
|
|
|
136
138
|
tests/test_dinov3_natimage.py
|
|
137
139
|
tests/test_drop_in_encoders.py
|
|
138
140
|
tests/test_encoder_capabilities.py
|
|
141
|
+
tests/test_encoder_code.py
|
|
139
142
|
tests/test_encoder_input_contract.py
|
|
140
143
|
tests/test_encoder_plugins.py
|
|
141
144
|
tests/test_encoder_preprocessing.py
|