slide2vec 6.3.1__tar.gz → 6.3.2__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-6.3.1 → slide2vec-6.3.2}/PKG-INFO +1 -1
- {slide2vec-6.3.1 → slide2vec-6.3.2}/pyproject.toml +2 -2
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/__init__.py +1 -1
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/api.py +38 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/feature_identity.py +36 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/PKG-INFO +1 -1
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/SOURCES.txt +1 -0
- slide2vec-6.3.2/tests/test_pooled_identity_api.py +283 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/LICENSE +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/README.md +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/setup.cfg +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/__main__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/_cufile.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/artifacts.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/cli.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/configs/resources.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/cufile.json +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/data/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/data/dataset.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/pin_gpu.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/base.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/dinov3.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/mettle.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/registry.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/inference.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/progress.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_image_recipe.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_image_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/types.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/config.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec/utils/utils.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_attention_extraction.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_cli_noninteractive.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_cufile_compat_mode.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_extraction.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_image_resume.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_image_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_image_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_regions.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_sliding.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dense_worker.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_dinov3_natimage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_capabilities.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_plugins.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_preprocessing.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_provider_failures.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_encoder_registry.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_feature_identity.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_gigapath.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_hs2p5_integration.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_image_shard.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_image_stage.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_isight.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_mascaret.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_mettle.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_output_consistency.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_patient_annotation_sampling.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_patient_manifest.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_phaet.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_prism.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_prism2.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_process_list_performance.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_progress.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_regression_core.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_regression_inference.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_regression_models.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_rudolfv2.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_runtime_batching.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_sharding.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_slide_latents.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_soma_migration.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_tile_store.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_tiling_pipeline.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/tests/test_titan.py +0 -0
- {slide2vec-6.3.1 → slide2vec-6.3.2}/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 = "6.3.
|
|
7
|
+
version = "6.3.2"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -180,7 +180,7 @@ no_implicit_reexport = true
|
|
|
180
180
|
max-line-length = 160
|
|
181
181
|
|
|
182
182
|
[tool.bumpver]
|
|
183
|
-
current_version = "6.3.
|
|
183
|
+
current_version = "6.3.2"
|
|
184
184
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
185
185
|
commit = false
|
|
186
186
|
tag = false
|
|
@@ -688,6 +688,44 @@ class Model:
|
|
|
688
688
|
# Construction fact, not an encode: see _load_backend_without_transform.
|
|
689
689
|
return int(self._load_backend_without_transform().feature_dim)
|
|
690
690
|
|
|
691
|
+
def pooled_identity_differences(
|
|
692
|
+
self,
|
|
693
|
+
recorded: Mapping[str, Any],
|
|
694
|
+
*,
|
|
695
|
+
preprocessing: PreprocessingConfig | None,
|
|
696
|
+
execution: ExecutionOptions | None = None,
|
|
697
|
+
) -> dict[str, tuple[Any, Any]]:
|
|
698
|
+
"""Compare a pooled sidecar's ``compatibility`` block with the current recipe.
|
|
699
|
+
|
|
700
|
+
Returns ``{field: (recorded, current)}`` for differing fields held by both
|
|
701
|
+
identities. Missing fields retain the extraction resume semantics: they
|
|
702
|
+
cannot be verified and are accepted. The caller owns the policy for an
|
|
703
|
+
unrecorded identity or a difference.
|
|
704
|
+
|
|
705
|
+
Explicitly pass ``preprocessing=None`` for pre-cropped images, whose shipped
|
|
706
|
+
transform determines geometry. Pass a :class:`PreprocessingConfig` for
|
|
707
|
+
tiles read from slides; its unset fields resolve as in pooled extraction.
|
|
708
|
+
``execution`` defaults to the model's recommended precision and needs no
|
|
709
|
+
output directory.
|
|
710
|
+
|
|
711
|
+
Only a recorded ``transform`` loads weights, on an isolated CPU model.
|
|
712
|
+
Slide and patient models load their registered tile encoder and tile output
|
|
713
|
+
variant, without loading aggregation weights. This method does not change
|
|
714
|
+
this model's input contract or loaded backend, encode pixels, or write files.
|
|
715
|
+
"""
|
|
716
|
+
from slide2vec.runtime.feature_identity import pooled_identity_differences
|
|
717
|
+
|
|
718
|
+
resolved_execution = _coerce_execution_options(execution, model=self)
|
|
719
|
+
resolved_preprocessing = (
|
|
720
|
+
None if preprocessing is None else _resolve_direct_api_preprocessing(self, preprocessing)
|
|
721
|
+
)
|
|
722
|
+
return pooled_identity_differences(
|
|
723
|
+
self,
|
|
724
|
+
dict(recorded),
|
|
725
|
+
execution=resolved_execution,
|
|
726
|
+
preprocessing=resolved_preprocessing,
|
|
727
|
+
)
|
|
728
|
+
|
|
691
729
|
def prepare_dense_encoder(
|
|
692
730
|
self,
|
|
693
731
|
*,
|
|
@@ -136,6 +136,42 @@ def differing_fields(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def pooled_identity_differences(
|
|
140
|
+
model,
|
|
141
|
+
recorded: dict[str, Any],
|
|
142
|
+
*,
|
|
143
|
+
execution,
|
|
144
|
+
preprocessing=None,
|
|
145
|
+
) -> dict[str, tuple[Any, Any]]:
|
|
146
|
+
"""Compare a recorded pooled identity with the resolved current request."""
|
|
147
|
+
|
|
148
|
+
def resolve_transform() -> dict[str, Any]:
|
|
149
|
+
from slide2vec.api import Model
|
|
150
|
+
|
|
151
|
+
encoder_name, output_variant = model.name, model._output_variant
|
|
152
|
+
if model.level != "tile":
|
|
153
|
+
info = encoder_registry.info(model.name)
|
|
154
|
+
encoder_name = info["tile_encoder"]
|
|
155
|
+
output_variant = info["tile_encoder_output_variant"]
|
|
156
|
+
source = Model.from_preset(
|
|
157
|
+
encoder_name,
|
|
158
|
+
device="cpu",
|
|
159
|
+
output_variant=output_variant,
|
|
160
|
+
allow_non_recommended_settings=model.allow_non_recommended_settings,
|
|
161
|
+
)
|
|
162
|
+
if preprocessing is None:
|
|
163
|
+
source._declare_given_encoder_input(emit_run_info=False)
|
|
164
|
+
else:
|
|
165
|
+
source._declare_encoder_input(preprocessing, emit_run_info=False)
|
|
166
|
+
return transform_record(source._load_backend().transforms)
|
|
167
|
+
|
|
168
|
+
return differing_fields(
|
|
169
|
+
recorded,
|
|
170
|
+
pooled_feature_identity(model, execution=execution, preprocessing=preprocessing),
|
|
171
|
+
resolve_transform=resolve_transform,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
139
175
|
class PooledResumeCheck:
|
|
140
176
|
"""Compare the completed pooled artifacts of one resume with the run's feature identity.
|
|
141
177
|
|
|
@@ -160,6 +160,7 @@ tests/test_patient_manifest.py
|
|
|
160
160
|
tests/test_phaet.py
|
|
161
161
|
tests/test_pooled_encoder_input.py
|
|
162
162
|
tests/test_pooled_geometry.py
|
|
163
|
+
tests/test_pooled_identity_api.py
|
|
163
164
|
tests/test_prism.py
|
|
164
165
|
tests/test_prism2.py
|
|
165
166
|
tests/test_process_list_performance.py
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""The public comparison of recorded pooled identities with the current extraction recipe.
|
|
2
|
+
|
|
3
|
+
Failure modes: wrong metadata differences, eager or GPU model loading, using the
|
|
4
|
+
wrong input regime or tile dependency, changing the caller's model, and requiring
|
|
5
|
+
artifact writes for a metadata comparison. Expected differences are literal records.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import torch
|
|
10
|
+
from PIL import Image
|
|
11
|
+
from torchvision.transforms import v2
|
|
12
|
+
|
|
13
|
+
from slide2vec import ExecutionOptions, ImageSpec, Model, PreprocessingConfig
|
|
14
|
+
from slide2vec.encoders import PatientEncoder, SlideEncoder, TileEncoder, encoder_registry, register_encoder
|
|
15
|
+
|
|
16
|
+
TILE_ENCODER = "identity-api-tile"
|
|
17
|
+
SHIPPED_TRANSFORM = {
|
|
18
|
+
"normalize": {"mean": [0.5, 0.5, 0.5], "std": [0.5, 0.5, 0.5]},
|
|
19
|
+
"resize": {"size": [40], "interpolation": "bicubic"},
|
|
20
|
+
"center_crop": {"size": [32, 32]},
|
|
21
|
+
}
|
|
22
|
+
OLD_TRANSFORM = {
|
|
23
|
+
"normalize": {"mean": [0.25, 0.5, 0.5], "std": [0.5, 0.5, 0.5]},
|
|
24
|
+
"resize": {"size": [40], "interpolation": "bicubic"},
|
|
25
|
+
"center_crop": {"size": [32, 32]},
|
|
26
|
+
}
|
|
27
|
+
NORMALIZATION_ONLY = {
|
|
28
|
+
"normalize": {"mean": [0.5, 0.5, 0.5], "std": [0.5, 0.5, 0.5]},
|
|
29
|
+
"resize": None,
|
|
30
|
+
"center_crop": None,
|
|
31
|
+
}
|
|
32
|
+
ALTERNATE_NORMALIZATION = {
|
|
33
|
+
"normalize": {"mean": [0.25, 0.5, 0.5], "std": [0.5, 0.5, 0.5]},
|
|
34
|
+
"resize": None,
|
|
35
|
+
"center_crop": None,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def tile_encoder(monkeypatch):
|
|
41
|
+
"""A weight-free provider with known transforms and observable device selection."""
|
|
42
|
+
monkeypatch.setattr(encoder_registry, "_entries", dict(encoder_registry._entries))
|
|
43
|
+
|
|
44
|
+
@register_encoder(
|
|
45
|
+
TILE_ENCODER,
|
|
46
|
+
output_variants={"default": {"encode_dim": 3}, "alternate": {"encode_dim": 3}},
|
|
47
|
+
default_output_variant="default",
|
|
48
|
+
input_size=32,
|
|
49
|
+
supports_variable_input_size=True,
|
|
50
|
+
supported_spacing_um=[0.5],
|
|
51
|
+
default_spacing_um=0.5,
|
|
52
|
+
precision="fp16",
|
|
53
|
+
)
|
|
54
|
+
class TestTileEncoder(TileEncoder):
|
|
55
|
+
variants = []
|
|
56
|
+
devices = []
|
|
57
|
+
encoded_images = 0
|
|
58
|
+
|
|
59
|
+
def __init__(self, *, output_variant=None, allow_non_recommended_settings=False):
|
|
60
|
+
self._device = torch.device("cpu")
|
|
61
|
+
self.output_variant = output_variant or "default"
|
|
62
|
+
type(self).variants.append(self.output_variant)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def encode_dim(self):
|
|
66
|
+
return 3
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def device(self):
|
|
70
|
+
return self._device
|
|
71
|
+
|
|
72
|
+
def to(self, device):
|
|
73
|
+
self._device = torch.device(device)
|
|
74
|
+
type(self).devices.append(str(self._device))
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def get_normalization_transform(self):
|
|
78
|
+
mean = [0.25, 0.5, 0.5] if self.output_variant == "alternate" else [0.5, 0.5, 0.5]
|
|
79
|
+
return v2.Compose([
|
|
80
|
+
v2.ToImage(),
|
|
81
|
+
v2.ToDtype(torch.float32, scale=True),
|
|
82
|
+
v2.Normalize(mean=mean, std=[0.5, 0.5, 0.5]),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
def get_transform(self):
|
|
86
|
+
return v2.Compose([
|
|
87
|
+
v2.Resize(40, interpolation=v2.InterpolationMode.BICUBIC),
|
|
88
|
+
v2.CenterCrop(32),
|
|
89
|
+
self.get_normalization_transform(),
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
def encode_tiles(self, batch):
|
|
93
|
+
type(self).encoded_images += int(batch.shape[0])
|
|
94
|
+
return batch.mean(dim=(-1, -2))
|
|
95
|
+
|
|
96
|
+
return TestTileEncoder
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.fixture
|
|
100
|
+
def aggregate_encoders(tile_encoder):
|
|
101
|
+
class AggregationLifecycle:
|
|
102
|
+
def __init__(self, **kwargs):
|
|
103
|
+
raise AssertionError("Identity comparison must not load aggregation weights")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def encode_dim(self):
|
|
107
|
+
return 3
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def device(self):
|
|
111
|
+
return torch.device("cpu")
|
|
112
|
+
|
|
113
|
+
def to(self, device):
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def encode_slide(self, tile_features, coordinates=None, *, tile_size_lv0=None):
|
|
117
|
+
return tile_features.mean(dim=0)
|
|
118
|
+
|
|
119
|
+
class TestSlideEncoder(AggregationLifecycle, SlideEncoder):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
class TestPatientEncoder(AggregationLifecycle, PatientEncoder):
|
|
123
|
+
def encode_patient(self, slide_embeddings):
|
|
124
|
+
return slide_embeddings.mean(dim=0)
|
|
125
|
+
|
|
126
|
+
names = {"slide": "identity-api-slide", "patient": "identity-api-patient"}
|
|
127
|
+
for level, cls in (("slide", TestSlideEncoder), ("patient", TestPatientEncoder)):
|
|
128
|
+
register_encoder(
|
|
129
|
+
names[level],
|
|
130
|
+
level=level,
|
|
131
|
+
tile_encoder=TILE_ENCODER,
|
|
132
|
+
tile_encoder_output_variant="alternate",
|
|
133
|
+
output_variants={"default": {"encode_dim": 3}},
|
|
134
|
+
default_output_variant="default",
|
|
135
|
+
supported_spacing_um=[0.5],
|
|
136
|
+
default_spacing_um=0.5,
|
|
137
|
+
precision="fp32",
|
|
138
|
+
)(cls)
|
|
139
|
+
return names
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_pooled_identity_comparison_reports_recorded_and_current_metadata():
|
|
143
|
+
model = Model.from_preset("virchow2", device="cuda")
|
|
144
|
+
recorded = {
|
|
145
|
+
"encoder_name": "older-encoder",
|
|
146
|
+
"output_variant": "cls",
|
|
147
|
+
"precision": "bf16",
|
|
148
|
+
"feature_dtype": "fp16",
|
|
149
|
+
"older_field": "accepted",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
differences = model.pooled_identity_differences(
|
|
153
|
+
recorded,
|
|
154
|
+
preprocessing=None,
|
|
155
|
+
execution=ExecutionOptions(precision="fp32", output_dtype="fp32", num_gpus=1),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert differences == {
|
|
159
|
+
"encoder_name": ("older-encoder", "virchow2"),
|
|
160
|
+
"output_variant": ("cls", "cls_patch_mean"),
|
|
161
|
+
"precision": ("bf16", "fp32"),
|
|
162
|
+
"feature_dtype": ("fp16", "fp32"),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_pooled_identity_comparison_reads_the_shipped_transform_on_cpu(tile_encoder):
|
|
167
|
+
model = Model.from_preset(TILE_ENCODER, device="cuda")
|
|
168
|
+
|
|
169
|
+
differences = model.pooled_identity_differences(
|
|
170
|
+
{"transform": OLD_TRANSFORM},
|
|
171
|
+
preprocessing=None,
|
|
172
|
+
execution=ExecutionOptions(precision="fp32", num_gpus=1),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assert differences == {"transform": (OLD_TRANSFORM, SHIPPED_TRANSFORM)}
|
|
176
|
+
assert tile_encoder.devices == ["cpu"]
|
|
177
|
+
assert tile_encoder.encoded_images == 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_pooled_identity_comparison_reads_the_declared_transform(tile_encoder):
|
|
181
|
+
differences = Model.from_preset(TILE_ENCODER).pooled_identity_differences(
|
|
182
|
+
{"transform": SHIPPED_TRANSFORM},
|
|
183
|
+
preprocessing=PreprocessingConfig(requested_tile_size_px=32, requested_spacing_um=0.5),
|
|
184
|
+
execution=ExecutionOptions(precision="fp32", num_gpus=1),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert differences == {"transform": (SHIPPED_TRANSFORM, NORMALIZATION_ONLY)}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.parametrize("level", ["slide", "patient"])
|
|
191
|
+
def test_pooled_identity_comparison_uses_the_registered_tile_dependency(
|
|
192
|
+
tile_encoder, aggregate_encoders, level
|
|
193
|
+
):
|
|
194
|
+
model = Model.from_preset(aggregate_encoders[level], device="cuda")
|
|
195
|
+
|
|
196
|
+
differences = model.pooled_identity_differences(
|
|
197
|
+
{"tile_encoder_output_variant": "default", "transform": NORMALIZATION_ONLY},
|
|
198
|
+
preprocessing=PreprocessingConfig(requested_tile_size_px=32, requested_spacing_um=0.5),
|
|
199
|
+
execution=ExecutionOptions(precision="fp32", num_gpus=1),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert differences == {
|
|
203
|
+
"tile_encoder_output_variant": ("default", "alternate"),
|
|
204
|
+
"transform": (NORMALIZATION_ONLY, ALTERNATE_NORMALIZATION),
|
|
205
|
+
}
|
|
206
|
+
assert tile_encoder.variants == ["alternate"]
|
|
207
|
+
assert tile_encoder.devices == ["cpu"]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_pooled_identity_comparison_accepts_missing_fields_without_loading_weights(tile_encoder):
|
|
211
|
+
differences = Model.from_preset(TILE_ENCODER, device="cuda").pooled_identity_differences(
|
|
212
|
+
{},
|
|
213
|
+
preprocessing=None,
|
|
214
|
+
execution=ExecutionOptions(precision="fp32", num_gpus=1),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
assert differences == {}
|
|
218
|
+
assert tile_encoder.variants == []
|
|
219
|
+
assert tile_encoder.devices == []
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@pytest.mark.parametrize("execution", [None, ExecutionOptions(num_gpus=1)])
|
|
223
|
+
def test_pooled_identity_comparison_resolves_the_extraction_precision_default(tile_encoder, execution):
|
|
224
|
+
differences = Model.from_preset(TILE_ENCODER).pooled_identity_differences(
|
|
225
|
+
{"precision": "fp32", "feature_dtype": "fp32"},
|
|
226
|
+
preprocessing=None,
|
|
227
|
+
execution=execution,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert differences == {"precision": ("fp32", "fp16"), "feature_dtype": ("fp32", "fp16")}
|
|
231
|
+
assert tile_encoder.variants == []
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_pooled_identity_comparison_resolves_hierarchical_geometry(tile_encoder):
|
|
235
|
+
differences = Model.from_preset(TILE_ENCODER).pooled_identity_differences(
|
|
236
|
+
{
|
|
237
|
+
"requested_tile_size_px": 64,
|
|
238
|
+
"encoder_input_size_px": 64,
|
|
239
|
+
"region_tile_multiple": 3,
|
|
240
|
+
"requested_region_size_px": 96,
|
|
241
|
+
},
|
|
242
|
+
preprocessing=PreprocessingConfig(region_tile_multiple=2),
|
|
243
|
+
execution=ExecutionOptions(precision="fp32", num_gpus=1),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert differences == {
|
|
247
|
+
"requested_tile_size_px": (64, 32),
|
|
248
|
+
"encoder_input_size_px": (64, 32),
|
|
249
|
+
"region_tile_multiple": (3, 2),
|
|
250
|
+
"requested_region_size_px": (96, 64),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_pooled_identity_comparison_preserves_a_model_used_for_image_extraction(tmp_path, tile_encoder):
|
|
255
|
+
image_path = tmp_path / "image.png"
|
|
256
|
+
Image.new("RGB", (48, 48), color=(255, 255, 255)).save(image_path)
|
|
257
|
+
model = Model.from_preset(TILE_ENCODER, device="cpu")
|
|
258
|
+
execution = ExecutionOptions(
|
|
259
|
+
output_dir=tmp_path / "artifacts",
|
|
260
|
+
precision="fp32",
|
|
261
|
+
num_gpus=1,
|
|
262
|
+
num_workers_per_gpu=0,
|
|
263
|
+
)
|
|
264
|
+
(artifact,) = model.embed_images([ImageSpec(sample_id="first", image_path=image_path)], execution=execution)
|
|
265
|
+
sidecar_before = artifact.metadata_path.read_bytes()
|
|
266
|
+
|
|
267
|
+
differences = model.pooled_identity_differences(
|
|
268
|
+
artifact.metadata["compatibility"],
|
|
269
|
+
preprocessing=PreprocessingConfig(requested_tile_size_px=32, requested_spacing_um=0.5),
|
|
270
|
+
execution=execution,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
assert differences == {"transform": (SHIPPED_TRANSFORM, NORMALIZATION_ONLY)}
|
|
274
|
+
assert tile_encoder.encoded_images == 1
|
|
275
|
+
assert artifact.metadata_path.read_bytes() == sidecar_before
|
|
276
|
+
|
|
277
|
+
(next_artifact,) = model.embed_images([ImageSpec(sample_id="second", image_path=image_path)], execution=execution)
|
|
278
|
+
|
|
279
|
+
# One original backend and one comparison copy; extraction reuses the original.
|
|
280
|
+
assert tile_encoder.variants == ["default", "default"]
|
|
281
|
+
assert tile_encoder.encoded_images == 2
|
|
282
|
+
assert next_artifact.metadata["compatibility"]["transform"] == SHIPPED_TRANSFORM
|
|
283
|
+
assert torch.equal(torch.load(next_artifact.path, weights_only=True), torch.tensor([1.0, 1.0, 1.0]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|