slide2vec 7.1.2__tar.gz → 7.1.3__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.3}/PKG-INFO +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.3}/pyproject.toml +3 -3
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/__init__.py +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/__init__.py +2 -0
- slide2vec-7.1.3/slide2vec/encoders/code.py +102 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/inference.py +7 -2
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_recipe.py +8 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/feature_identity.py +18 -10
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/PKG-INFO +1 -1
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/SOURCES.txt +2 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_resume.py +5 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_shard.py +2 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_stage.py +16 -0
- slide2vec-7.1.3/tests/test_encoder_code.py +241 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_plugins.py +9 -1
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_feature_identity.py +106 -13
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_output_consistency.py +3 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_identity_api.py +12 -1
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_soma_migration.py +1 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/LICENSE +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/README.md +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/setup.cfg +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/__main__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/_cufile.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/api.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/artifacts.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/cli.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/configs/resources.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/cufile.json +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/dataset.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/pin_gpu.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/base.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/dinov3.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/mettle.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/progress.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/types.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/config.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec/utils/utils.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_attention_extraction.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_cli_noninteractive.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_cufile_compat_mode.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_extraction.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_regions.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_sliding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dense_worker.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_dinov3_natimage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_drop_in_encoders.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_capabilities.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_preprocessing.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_provider_failures.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_encoder_registry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_execution_options.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_gigapath.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hierarchical_encoding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hs2p5_integration.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_image_shard.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_image_stage.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_isight.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_mascaret.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_mettle.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_annotation_sampling.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_manifest.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_patient_tiling_only.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_phaet.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_prism.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_prism2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_process_list_performance.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_progress.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_core.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_inference.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_regression_models.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_rudolfv2.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_runtime_batching.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_sharding.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_slide_latents.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_tile_store.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_tiling_pipeline.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/tests/test_titan.py +0 -0
- {slide2vec-7.1.2 → slide2vec-7.1.3}/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.3"
|
|
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.3"
|
|
244
244
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
245
245
|
commit = false
|
|
246
246
|
tag = false
|
|
@@ -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,
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from slide2vec.encoders.code import encoder_code
|
|
9
10
|
from slide2vec.encoders.registry import resolve_encoder_output
|
|
10
11
|
from slide2vec.runtime.model_settings import output_dtype_name, resolve_output_precision
|
|
11
12
|
|
|
@@ -67,6 +68,8 @@ class DenseImageRecipe:
|
|
|
67
68
|
"""Request-wide extraction facts combined with one parent-resolved image read plan."""
|
|
68
69
|
|
|
69
70
|
encoder_name: str
|
|
71
|
+
#: The encoder's code block (:func:`slide2vec.encoders.encoder_code`).
|
|
72
|
+
encoder_code: dict[str, Any]
|
|
70
73
|
output_variant: str
|
|
71
74
|
reader_regime: str
|
|
72
75
|
spacing_source: str
|
|
@@ -99,6 +102,7 @@ class DenseImageRecipe:
|
|
|
99
102
|
def to_dict(self) -> dict[str, Any]:
|
|
100
103
|
payload = {
|
|
101
104
|
"encoder_name": self.encoder_name,
|
|
105
|
+
"encoder_code": self.encoder_code,
|
|
102
106
|
"output_variant": self.output_variant,
|
|
103
107
|
"reader_regime": self.reader_regime,
|
|
104
108
|
"spacing_source": self.spacing_source,
|
|
@@ -142,6 +146,7 @@ class DenseImageRecipe:
|
|
|
142
146
|
)
|
|
143
147
|
recipe = self.to_dict()
|
|
144
148
|
encoder_name = recipe.pop("encoder_name")
|
|
149
|
+
code = recipe.pop("encoder_code")
|
|
145
150
|
output_variant = recipe.pop("output_variant")
|
|
146
151
|
for field in read_plan.to_dict():
|
|
147
152
|
recipe.pop(field, None)
|
|
@@ -149,6 +154,7 @@ class DenseImageRecipe:
|
|
|
149
154
|
"sample_id": str(spec.sample_id),
|
|
150
155
|
"image_path": str(Path(spec.image_path).expanduser().resolve()),
|
|
151
156
|
"encoder_name": encoder_name,
|
|
157
|
+
"encoder_code": code,
|
|
152
158
|
"output_variant": output_variant,
|
|
153
159
|
**read_plan.to_dict(),
|
|
154
160
|
**recipe,
|
|
@@ -158,6 +164,7 @@ class DenseImageRecipe:
|
|
|
158
164
|
def from_dict(cls, payload: dict[str, Any]) -> "DenseImageRecipe":
|
|
159
165
|
return cls(
|
|
160
166
|
encoder_name=str(payload["encoder_name"]),
|
|
167
|
+
encoder_code=dict(payload["encoder_code"]),
|
|
161
168
|
output_variant=str(payload["output_variant"]),
|
|
162
169
|
reader_regime=str(payload["reader_regime"]),
|
|
163
170
|
spacing_source=str(payload["spacing_source"]),
|
|
@@ -270,6 +277,7 @@ def resolve_dense_image_recipe(
|
|
|
270
277
|
)
|
|
271
278
|
return DenseImageRecipe(
|
|
272
279
|
encoder_name=str(model.name),
|
|
280
|
+
encoder_code=encoder_code(model.name),
|
|
273
281
|
output_variant=str(output["output_variant"]),
|
|
274
282
|
reader_regime=str(reader_regime),
|
|
275
283
|
spacing_source=(
|
|
@@ -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(
|
|
@@ -253,11 +256,16 @@ class PooledResumeCheck:
|
|
|
253
256
|
for field, values in differing.items()
|
|
254
257
|
for leaf, old, new in _leaf_differences(field, *values)
|
|
255
258
|
)
|
|
259
|
+
# A caller can request a recorded value, but not recorded code.
|
|
260
|
+
remedies = (
|
|
261
|
+
"Re-run into a new output_dir or delete the stale artifacts."
|
|
262
|
+
if differing.keys() & {"encoder_code", "tile_encoder_code"}
|
|
263
|
+
else "Re-run into a new output_dir, delete the stale artifacts, or request "
|
|
264
|
+
"the recorded values."
|
|
265
|
+
)
|
|
256
266
|
raise ValueError(
|
|
257
267
|
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."
|
|
268
|
+
f"were computed with a different feature identity: {details}. {remedies}"
|
|
261
269
|
)
|
|
262
270
|
return not missing
|
|
263
271
|
|
|
@@ -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
|
|
@@ -136,6 +137,7 @@ tests/test_dinov2_natimage.py
|
|
|
136
137
|
tests/test_dinov3_natimage.py
|
|
137
138
|
tests/test_drop_in_encoders.py
|
|
138
139
|
tests/test_encoder_capabilities.py
|
|
140
|
+
tests/test_encoder_code.py
|
|
139
141
|
tests/test_encoder_input_contract.py
|
|
140
142
|
tests/test_encoder_plugins.py
|
|
141
143
|
tests/test_encoder_preprocessing.py
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
|
|
10
10
|
from slide2vec.api import DenseImageOptions, ExecutionOptions, ImageSpec, Model
|
|
11
11
|
from slide2vec.artifacts import dense_image_paths
|
|
12
|
+
from slide2vec.encoders import encoder_code
|
|
12
13
|
from slide2vec.runtime.dense_image_stage import partition_dense_images_by_resume
|
|
13
14
|
from slide2vec.runtime.dense_image_recipe import resolve_dense_image_recipe
|
|
14
15
|
from slide2vec.runtime.dense_image_reading import DenseImageReadPlan
|
|
@@ -80,6 +81,7 @@ def test_dense_image_recipe_contains_the_complete_canonical_identity(tmp_path):
|
|
|
80
81
|
)
|
|
81
82
|
assert first.to_dict() == {
|
|
82
83
|
"encoder_name": "virchow2",
|
|
84
|
+
"encoder_code": encoder_code("virchow2"),
|
|
83
85
|
"output_variant": "cls",
|
|
84
86
|
"reader_regime": "raster",
|
|
85
87
|
"spacing_source": "explicit",
|
|
@@ -444,6 +446,7 @@ def test_every_recorded_compatibility_field_participates_in_resume(tmp_path):
|
|
|
444
446
|
"sample_id",
|
|
445
447
|
"image_path",
|
|
446
448
|
"encoder_name",
|
|
449
|
+
"encoder_code",
|
|
447
450
|
"output_variant",
|
|
448
451
|
"reader_regime",
|
|
449
452
|
"spacing_source",
|
|
@@ -493,6 +496,8 @@ def test_every_recorded_compatibility_field_participates_in_resume(tmp_path):
|
|
|
493
496
|
recorded[field] = f"{value}-different"
|
|
494
497
|
elif isinstance(value, list):
|
|
495
498
|
recorded[field] = [value[0] + 1, *value[1:]]
|
|
499
|
+
elif isinstance(value, dict):
|
|
500
|
+
recorded[field] = {**value, "fingerprint": "different"}
|
|
496
501
|
else:
|
|
497
502
|
recorded[field] = value + 1
|
|
498
503
|
out_dir = tmp_path / field
|
|
@@ -97,6 +97,7 @@ def _recipe(
|
|
|
97
97
|
geometry = compute_dense_geometry(target_size=target_size, patch_size=(16, 16))
|
|
98
98
|
return DenseImageRecipe(
|
|
99
99
|
encoder_name="fake-encoder",
|
|
100
|
+
encoder_code={"object": "tests.FakeEncoder", "fingerprint": None, "source": ""},
|
|
100
101
|
output_variant="default",
|
|
101
102
|
reader_regime="raster",
|
|
102
103
|
spacing_source="explicit",
|
|
@@ -317,6 +318,7 @@ def test_run_dense_image_shard_sidecar_records_extraction_geometry(tmp_path):
|
|
|
317
318
|
"sample_id": "a",
|
|
318
319
|
"image_path": str(Path(specs[0].image_path).resolve()),
|
|
319
320
|
"encoder_name": "fake-encoder",
|
|
321
|
+
"encoder_code": {"object": "tests.FakeEncoder", "fingerprint": None, "source": ""},
|
|
320
322
|
"output_variant": "default",
|
|
321
323
|
"reader_regime": "raster",
|
|
322
324
|
"spacing_source": "explicit",
|
|
@@ -385,6 +385,22 @@ def test_dense_region_sidecar_records_the_feature_identity(fake_backend, stub_re
|
|
|
385
385
|
assert compatibility["read_level"] == 0 # the read plan is still recorded alongside
|
|
386
386
|
|
|
387
387
|
|
|
388
|
+
def test_dense_region_sidecar_records_the_code_of_a_registered_encoder(
|
|
389
|
+
fake_backend, stub_read_plan, tmp_path
|
|
390
|
+
):
|
|
391
|
+
from slide2vec.encoders import encoder_code
|
|
392
|
+
|
|
393
|
+
[artifact] = dense_stage.embed_regions_dense(
|
|
394
|
+
_FakeModel(_encoder(), name="virchow2"),
|
|
395
|
+
[_regions(coords=((0, 0),))],
|
|
396
|
+
dense=_dense(),
|
|
397
|
+
execution=ExecutionOptions(output_dir=tmp_path, num_gpus=1, precision="fp32"),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
compatibility = json.loads(artifact.metadata_path.read_text())["compatibility"]
|
|
401
|
+
assert compatibility["encoder_code"] == encoder_code("virchow2")
|
|
402
|
+
|
|
403
|
+
|
|
388
404
|
def test_embed_regions_dense_resume_recomputes_regions_from_a_different_encoder(
|
|
389
405
|
fake_backend, stub_read_plan, tmp_path
|
|
390
406
|
):
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""The encoder code block the feature identity records under ``encoder_code``.
|
|
2
|
+
|
|
3
|
+
Ways it can go wrong, each covered below:
|
|
4
|
+
|
|
5
|
+
* computing the block instantiates the encoder or loads its weights;
|
|
6
|
+
* an edit to the file that defines the encoder leaves the block, or the comparison of
|
|
7
|
+
a recorded identity, unchanged;
|
|
8
|
+
* an installed encoder records its file's digest instead of its distribution's version,
|
|
9
|
+
or a namespace package shared by several distributions is attributed by its
|
|
10
|
+
top-level name instead of by the ``RECORD`` that lists the file;
|
|
11
|
+
* an editable install, or a file slide2vec itself installed, records a version that
|
|
12
|
+
does not change with the file;
|
|
13
|
+
* a class with no file (a notebook cell) raises instead of recording no fingerprint.
|
|
14
|
+
|
|
15
|
+
The installed-plugin path (a real ``pip install``) is covered in ``test_encoder_plugins.py``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import importlib
|
|
20
|
+
import importlib.util
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import sys
|
|
24
|
+
import types
|
|
25
|
+
|
|
26
|
+
from PIL import Image
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
from slide2vec import ExecutionOptions, ImageSpec, Model
|
|
30
|
+
from slide2vec.artifacts import load_metadata
|
|
31
|
+
from slide2vec.encoders import encoder_code
|
|
32
|
+
|
|
33
|
+
ENCODER_MODULE = '''
|
|
34
|
+
from slide2vec import TorchTileEncoder, register_encoder
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@register_encoder(
|
|
38
|
+
"{name}",
|
|
39
|
+
encode_dim=8,
|
|
40
|
+
input_size=32,
|
|
41
|
+
supports_variable_input_size=False,
|
|
42
|
+
supported_spacing_um=0.5,
|
|
43
|
+
precision="fp32",
|
|
44
|
+
source="/models/{name}.pt",
|
|
45
|
+
)
|
|
46
|
+
class CustomEncoder(TorchTileEncoder):
|
|
47
|
+
def __init__(self, *, output_variant=None):
|
|
48
|
+
raise AssertionError("encoder_code must not instantiate the encoder")
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
WORKING_ENCODER_MODULE = '''
|
|
53
|
+
import torch
|
|
54
|
+
|
|
55
|
+
from slide2vec import TorchTileEncoder, register_encoder
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register_encoder(
|
|
59
|
+
"{name}",
|
|
60
|
+
encode_dim=3,
|
|
61
|
+
input_size=32,
|
|
62
|
+
supports_variable_input_size=False,
|
|
63
|
+
supported_spacing_um=0.5,
|
|
64
|
+
precision="fp32",
|
|
65
|
+
source="/models/{name}.pt",
|
|
66
|
+
)
|
|
67
|
+
class CustomEncoder(TorchTileEncoder):
|
|
68
|
+
def __init__(self, *, output_variant=None):
|
|
69
|
+
model = torch.nn.Sequential(torch.nn.AdaptiveAvgPool2d(1), torch.nn.Flatten())
|
|
70
|
+
super().__init__(
|
|
71
|
+
model, encode_dim=3, input_size=32, mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5),
|
|
72
|
+
output_variant=output_variant,
|
|
73
|
+
)
|
|
74
|
+
'''
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _import_module_file(path: Path, module_name: str, monkeypatch) -> None:
|
|
78
|
+
"""Import *path* as *module_name*, as a script importing its encoder module does."""
|
|
79
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
80
|
+
module = importlib.util.module_from_spec(spec)
|
|
81
|
+
monkeypatch.setitem(sys.modules, module_name, module)
|
|
82
|
+
spec.loader.exec_module(module)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _sha256(path: Path) -> str:
|
|
86
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_encoder_code_of_a_module_file_encoder_names_its_class_file_digest_and_source(
|
|
90
|
+
tmp_path, monkeypatch, isolated_encoder_registry
|
|
91
|
+
):
|
|
92
|
+
path = tmp_path / "custom_encoders.py"
|
|
93
|
+
path.write_text(ENCODER_MODULE.format(name="custom-code"), encoding="utf-8")
|
|
94
|
+
_import_module_file(path, "custom_encoders", monkeypatch)
|
|
95
|
+
|
|
96
|
+
assert encoder_code("custom-code") == {
|
|
97
|
+
"object": "custom_encoders.CustomEncoder",
|
|
98
|
+
"fingerprint": _sha256(path),
|
|
99
|
+
"source": "/models/custom-code.pt",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_an_edit_to_a_module_file_encoder_is_a_feature_identity_difference(
|
|
104
|
+
tmp_path, monkeypatch, isolated_encoder_registry
|
|
105
|
+
):
|
|
106
|
+
path = tmp_path / "edited_encoders.py"
|
|
107
|
+
path.write_text(WORKING_ENCODER_MODULE.format(name="edited-code"), encoding="utf-8")
|
|
108
|
+
_import_module_file(path, "edited_encoders", monkeypatch)
|
|
109
|
+
image_path = tmp_path / "image.png"
|
|
110
|
+
Image.new("RGB", (48, 48), color=(255, 255, 255)).save(image_path)
|
|
111
|
+
model = Model.from_preset("edited-code", device="cpu")
|
|
112
|
+
execution = ExecutionOptions(
|
|
113
|
+
output_dir=tmp_path / "out", precision="fp32", num_gpus=1, num_workers_per_gpu=0
|
|
114
|
+
)
|
|
115
|
+
(artifact,) = model.embed_images(
|
|
116
|
+
[ImageSpec(sample_id="image", image_path=image_path)], execution=execution
|
|
117
|
+
)
|
|
118
|
+
recorded = load_metadata(artifact.metadata_path)["compatibility"]
|
|
119
|
+
recorded_code = {
|
|
120
|
+
"object": "edited_encoders.CustomEncoder",
|
|
121
|
+
"fingerprint": _sha256(path),
|
|
122
|
+
"source": "/models/edited-code.pt",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assert recorded["encoder_code"] == recorded_code
|
|
126
|
+
assert model.pooled_identity_differences(
|
|
127
|
+
recorded, preprocessing=None, execution=execution
|
|
128
|
+
) == {}
|
|
129
|
+
|
|
130
|
+
path.write_text(path.read_text(encoding="utf-8") + "\n# An edit.\n", encoding="utf-8")
|
|
131
|
+
|
|
132
|
+
assert model.pooled_identity_differences(
|
|
133
|
+
recorded, preprocessing=None, execution=execution
|
|
134
|
+
) == {"encoder_code": (recorded_code, {**recorded_code, "fingerprint": _sha256(path)})}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
#: A namespace package (no ``__init__.py``) that two installed distributions share, as
|
|
138
|
+
#: ``protobuf`` and ``googleapis-common-protos`` share ``google``.
|
|
139
|
+
NAMESPACE = "slide2vec_test_namespace"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _install(site: Path, distribution: str, version: str, module: str, *, direct_url=None):
|
|
143
|
+
"""Install ``<NAMESPACE>/<module>.py`` into *site* as *distribution*, listed in its RECORD."""
|
|
144
|
+
package = site / NAMESPACE
|
|
145
|
+
package.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
(package / f"{module}.py").write_text(
|
|
147
|
+
ENCODER_MODULE.format(name=f"installed-{module}"), encoding="utf-8"
|
|
148
|
+
)
|
|
149
|
+
info = site / f"{distribution.replace('-', '_')}-{version}.dist-info"
|
|
150
|
+
info.mkdir()
|
|
151
|
+
(info / "METADATA").write_text(
|
|
152
|
+
f"Metadata-Version: 2.1\nName: {distribution}\nVersion: {version}\n", encoding="utf-8"
|
|
153
|
+
)
|
|
154
|
+
(info / "RECORD").write_text(
|
|
155
|
+
f"{NAMESPACE}/{module}.py,,\n{info.name}/METADATA,,\n{info.name}/RECORD,,\n",
|
|
156
|
+
encoding="utf-8",
|
|
157
|
+
)
|
|
158
|
+
if direct_url is not None:
|
|
159
|
+
(info / "direct_url.json").write_text(json.dumps(direct_url), encoding="utf-8")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.fixture
|
|
163
|
+
def site(tmp_path, monkeypatch, isolated_encoder_registry):
|
|
164
|
+
"""An extra site directory on ``sys.path``; its namespace modules are unloaded after."""
|
|
165
|
+
site = tmp_path / "site-packages"
|
|
166
|
+
site.mkdir()
|
|
167
|
+
monkeypatch.syspath_prepend(str(site))
|
|
168
|
+
yield site
|
|
169
|
+
for name in [name for name in sys.modules if name.split(".", 1)[0] == NAMESPACE]:
|
|
170
|
+
sys.modules.pop(name)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _import_installed(module: str) -> None:
|
|
174
|
+
importlib.import_module(f"{NAMESPACE}.{module}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_encoder_code_of_an_installed_encoder_is_the_version_of_the_distribution_listing_it(site):
|
|
178
|
+
"""Distributions of a namespace package share its top-level name: their RECORD decides."""
|
|
179
|
+
_install(site, "slide2vec-test-alpha", "1.0", "alpha")
|
|
180
|
+
_install(site, "slide2vec-test-beta", "2.0", "beta")
|
|
181
|
+
(site / NAMESPACE / "gamma.py").write_text(
|
|
182
|
+
ENCODER_MODULE.format(name="installed-gamma"), encoding="utf-8"
|
|
183
|
+
)
|
|
184
|
+
for module in ("alpha", "beta", "gamma"):
|
|
185
|
+
_import_installed(module)
|
|
186
|
+
|
|
187
|
+
assert {
|
|
188
|
+
module: encoder_code(f"installed-{module}")["fingerprint"]
|
|
189
|
+
for module in ("alpha", "beta", "gamma")
|
|
190
|
+
} == {
|
|
191
|
+
"alpha": "1.0",
|
|
192
|
+
"beta": "2.0",
|
|
193
|
+
# A file no distribution lists is not installed code: its digest pins it.
|
|
194
|
+
"gamma": _sha256(site / NAMESPACE / "gamma.py"),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_encoder_code_of_an_editable_install_is_the_digest_of_its_file(site):
|
|
199
|
+
"""An editable install runs the work tree: its version does not change with an edit."""
|
|
200
|
+
_install(
|
|
201
|
+
site, "slide2vec-test-editable", "3.0", "delta",
|
|
202
|
+
direct_url={"url": site.as_uri(), "dir_info": {"editable": True}},
|
|
203
|
+
)
|
|
204
|
+
_import_installed("delta")
|
|
205
|
+
|
|
206
|
+
assert encoder_code("installed-delta")["fingerprint"] == _sha256(site / NAMESPACE / "delta.py")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_encoder_code_of_a_file_slide2vec_installed_is_its_digest(site):
|
|
210
|
+
"""A built-in's identity changes when a release changes its file, not on every release."""
|
|
211
|
+
_install(site, "slide2vec", "99.0", "epsilon")
|
|
212
|
+
_import_installed("epsilon")
|
|
213
|
+
|
|
214
|
+
assert encoder_code("installed-epsilon")["fingerprint"] == _sha256(
|
|
215
|
+
site / NAMESPACE / "epsilon.py"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_encoder_code_of_a_built_in_encoder_is_the_digest_of_its_file():
|
|
220
|
+
import slide2vec.encoders.models.virchow as virchow
|
|
221
|
+
|
|
222
|
+
assert encoder_code("virchow2") == {
|
|
223
|
+
"object": "slide2vec.encoders.models.virchow.Virchow2",
|
|
224
|
+
"fingerprint": _sha256(Path(virchow.__file__)),
|
|
225
|
+
"source": "paige-ai/Virchow2",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_encoder_code_of_an_encoder_with_no_file_records_no_fingerprint(
|
|
230
|
+
monkeypatch, isolated_encoder_registry
|
|
231
|
+
):
|
|
232
|
+
"""A class defined in a notebook cell lives in a module that has no file."""
|
|
233
|
+
module = types.ModuleType("notebook_cell")
|
|
234
|
+
monkeypatch.setitem(sys.modules, "notebook_cell", module)
|
|
235
|
+
exec(compile(ENCODER_MODULE.format(name="cell-code"), "<cell>", "exec"), module.__dict__)
|
|
236
|
+
|
|
237
|
+
assert encoder_code("cell-code") == {
|
|
238
|
+
"object": "notebook_cell.CustomEncoder",
|
|
239
|
+
"fingerprint": None,
|
|
240
|
+
"source": "/models/cell-code.pt",
|
|
241
|
+
}
|
|
@@ -155,7 +155,7 @@ from threading import Barrier
|
|
|
155
155
|
|
|
156
156
|
import slide2vec
|
|
157
157
|
from slide2vec import Model, list_models
|
|
158
|
-
from slide2vec.encoders import encoder_registry
|
|
158
|
+
from slide2vec.encoders import encoder_code, encoder_registry
|
|
159
159
|
from slide2vec.runtime.serialization import serialize_model
|
|
160
160
|
|
|
161
161
|
discovered = Path(os.environ["PLUGIN_DISCOVERED_SENTINEL"])
|
|
@@ -201,6 +201,14 @@ assert encoder_registry.info("private-beta") == {
|
|
|
201
201
|
"source": "private-org/private-beta",
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
# The installed distribution pins its encoders' code, without constructing them.
|
|
205
|
+
assert encoder_code("private-alpha") == {
|
|
206
|
+
"object": "private_encoder_plugin.register_presets.<locals>.PrivateAlpha",
|
|
207
|
+
"fingerprint": "1.0.0",
|
|
208
|
+
"source": "/models/private-alpha.pt",
|
|
209
|
+
}
|
|
210
|
+
assert not constructed.exists()
|
|
211
|
+
|
|
204
212
|
model = Model.from_preset("private-alpha", device="cpu")
|
|
205
213
|
assert model.name == "private-alpha"
|
|
206
214
|
assert model.level == "tile"
|