slide2vec 6.3.4__tar.gz → 7.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. {slide2vec-6.3.4 → slide2vec-7.0.0}/PKG-INFO +3 -3
  2. {slide2vec-6.3.4 → slide2vec-7.0.0}/pyproject.toml +5 -5
  3. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/__init__.py +3 -1
  4. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/api.py +43 -23
  5. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/artifacts.py +38 -18
  6. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/configs/default.yaml +2 -2
  7. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/configs/resources.py +0 -7
  8. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/data/tile_reader.py +0 -1
  9. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/direct_embed_worker.py +10 -29
  10. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/image_worker.py +11 -1
  11. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/pipeline_worker.py +5 -11
  12. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/artifacts_collect.py +10 -30
  13. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/batching.py +1 -14
  14. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_image_shard.py +5 -5
  15. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_shard.py +12 -12
  16. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/embedding_pipeline.py +54 -86
  17. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/feature_identity.py +70 -31
  18. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/image_shard.py +31 -55
  19. slide2vec-7.0.0/slide2vec/runtime/image_stage.py +433 -0
  20. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/persist_callbacks.py +20 -33
  21. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/registry.py +0 -11
  22. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/serialization.py +2 -0
  23. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/tiling.py +0 -5
  24. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/worker_io.py +0 -8
  25. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/config.py +7 -3
  26. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/PKG-INFO +3 -3
  27. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/SOURCES.txt +1 -0
  28. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/requires.txt +2 -2
  29. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_benchmark_tooling.py +57 -17
  30. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_image_resume.py +7 -8
  31. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_image_shard.py +4 -3
  32. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_regions.py +4 -4
  33. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_shard.py +5 -3
  34. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_stage.py +8 -5
  35. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_feature_identity.py +59 -22
  36. slide2vec-7.0.0/tests/test_hierarchical_encoding.py +281 -0
  37. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_hs2p5_integration.py +41 -0
  38. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_image_shard.py +23 -50
  39. slide2vec-7.0.0/tests/test_image_stage.py +1557 -0
  40. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_on_slide_persisted.py +74 -5
  41. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_pooled_identity_api.py +73 -10
  42. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_regression_core.py +88 -5
  43. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_regression_inference.py +169 -125
  44. slide2vec-6.3.4/slide2vec/runtime/image_stage.py +0 -197
  45. slide2vec-6.3.4/tests/test_image_stage.py +0 -427
  46. {slide2vec-6.3.4 → slide2vec-7.0.0}/LICENSE +0 -0
  47. {slide2vec-6.3.4 → slide2vec-7.0.0}/README.md +0 -0
  48. {slide2vec-6.3.4 → slide2vec-7.0.0}/setup.cfg +0 -0
  49. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/__main__.py +0 -0
  50. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/_cufile.py +0 -0
  51. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/cli.py +0 -0
  52. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/configs/__init__.py +0 -0
  53. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/cufile.json +0 -0
  54. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/data/__init__.py +0 -0
  55. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/data/dataset.py +0 -0
  56. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/data/tile_store.py +0 -0
  57. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/__init__.py +0 -0
  58. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/dense_image_worker.py +0 -0
  59. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/dense_worker.py +0 -0
  60. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/pin_gpu.py +0 -0
  61. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/distributed/worker_entry.py +0 -0
  62. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/__init__.py +0 -0
  63. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/base.py +0 -0
  64. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/__init__.py +0 -0
  65. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/conch.py +0 -0
  66. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/dinov2.py +0 -0
  67. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/dinov3.py +0 -0
  68. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/genbio.py +0 -0
  69. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/gigapath.py +0 -0
  70. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/gpfm.py +0 -0
  71. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/hibou.py +0 -0
  72. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/hoptimus.py +0 -0
  73. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/isight.py +0 -0
  74. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/lunit.py +0 -0
  75. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/mettle.py +0 -0
  76. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/midnight.py +0 -0
  77. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  78. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  79. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  80. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  81. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
  82. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  83. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/mstar.py +0 -0
  84. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/musk.py +0 -0
  85. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/phikon.py +0 -0
  86. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/prism.py +0 -0
  87. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/prism2.py +0 -0
  88. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/prost40m.py +0 -0
  89. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
  90. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/titan.py +0 -0
  91. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/uni.py +0 -0
  92. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/virchow.py +0 -0
  93. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/models/waiv.py +0 -0
  94. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/registry.py +0 -0
  95. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/encoders/validation.py +0 -0
  96. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/inference.py +0 -0
  97. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/progress.py +0 -0
  98. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/__init__.py +0 -0
  99. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/cpu_budget.py +0 -0
  100. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_encode.py +0 -0
  101. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_encoder_input.py +0 -0
  102. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_image_reading.py +0 -0
  103. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
  104. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_image_stage.py +0 -0
  105. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_regions.py +0 -0
  106. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_sliding.py +0 -0
  107. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/dense_stage.py +0 -0
  108. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/distributed.py +0 -0
  109. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/distributed_stage.py +0 -0
  110. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
  111. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/embedding.py +0 -0
  112. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/embedding_persist.py +0 -0
  113. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/encoder_input_contract.py +0 -0
  114. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/hierarchical.py +0 -0
  115. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/image_specs.py +0 -0
  116. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/manifest.py +0 -0
  117. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/model_settings.py +0 -0
  118. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/patient_pipeline.py +0 -0
  119. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/persistence.py +0 -0
  120. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  121. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/preprocessing.py +0 -0
  122. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/process_list.py +0 -0
  123. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/progress_bridge.py +0 -0
  124. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/sharding.py +0 -0
  125. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/slide_encode.py +0 -0
  126. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  127. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/runtime/types.py +0 -0
  128. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/__init__.py +0 -0
  129. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/coordinates.py +0 -0
  130. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/log_utils.py +0 -0
  131. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/tiling_io.py +0 -0
  132. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec/utils/utils.py +0 -0
  133. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  134. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/entry_points.txt +0 -0
  135. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/not-zip-safe +0 -0
  136. {slide2vec-6.3.4 → slide2vec-7.0.0}/slide2vec.egg-info/top_level.txt +0 -0
  137. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_attention_extraction.py +0 -0
  138. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_cli_noninteractive.py +0 -0
  139. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_cufile_compat_mode.py +0 -0
  140. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_encode_kit.py +0 -0
  141. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_encoder_input.py +0 -0
  142. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_extraction.py +0 -0
  143. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_image_reading.py +0 -0
  144. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_image_stage.py +0 -0
  145. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_sliding.py +0 -0
  146. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_source_spacing.py +0 -0
  147. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dense_worker.py +0 -0
  148. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dinov2_natimage.py +0 -0
  149. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_dinov3_natimage.py +0 -0
  150. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_capabilities.py +0 -0
  151. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_input_contract.py +0 -0
  152. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_plugins.py +0 -0
  153. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_preprocessing.py +0 -0
  154. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_provider_failures.py +0 -0
  155. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_encoder_registry.py +0 -0
  156. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_gigapath.py +0 -0
  157. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_gpfm_genbio_heavy.py +0 -0
  158. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_hs2p_package_cutover.py +0 -0
  159. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_inference_benchmark.py +0 -0
  160. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_isight.py +0 -0
  161. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_load_model_hf_auth.py +0 -0
  162. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_mascaret.py +0 -0
  163. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_mettle.py +0 -0
  164. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_output_consistency.py +0 -0
  165. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_patch_size_metadata.py +0 -0
  166. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_patient_annotation_sampling.py +0 -0
  167. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_patient_manifest.py +0 -0
  168. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_patient_tiling_only.py +0 -0
  169. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_phaet.py +0 -0
  170. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_pooled_encoder_input.py +0 -0
  171. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_pooled_geometry.py +0 -0
  172. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_prism.py +0 -0
  173. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_prism2.py +0 -0
  174. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_process_list_performance.py +0 -0
  175. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_progress.py +0 -0
  176. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_regression_models.py +0 -0
  177. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_rudolfv2.py +0 -0
  178. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_runtime_batching.py +0 -0
  179. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_sharding.py +0 -0
  180. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_slide_coordinate_preparation.py +0 -0
  181. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_slide_latents.py +0 -0
  182. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_soma_migration.py +0 -0
  183. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_tile_store.py +0 -0
  184. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_tiling_pipeline.py +0 -0
  185. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_titan.py +0 -0
  186. {slide2vec-6.3.4 → slide2vec-7.0.0}/tests/test_worker_gpu_pinning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 6.3.4
3
+ Version: 7.0.0
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=5.1.0
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=6.0.0
19
19
  Requires-Dist: omegaconf
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: numpy<2
@@ -75,7 +75,7 @@ Requires-Dist: numpy<2; extra == "fm"
75
75
  Requires-Dist: pandas; extra == "fm"
76
76
  Requires-Dist: pillow; extra == "fm"
77
77
  Requires-Dist: rich; extra == "fm"
78
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=5.1.0; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=6.0.0; extra == "fm"
79
79
  Requires-Dist: wandb; extra == "fm"
80
80
  Requires-Dist: torch<2.8,>=2.3; extra == "fm"
81
81
  Requires-Dist: torchvision>=0.18.0; extra == "fm"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "6.3.4"
7
+ version = "7.0.0"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.13",
22
22
  ]
23
23
  dependencies = [
24
- "hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=5.1.0",
24
+ "hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=6.0.0",
25
25
  "omegaconf",
26
26
  "matplotlib",
27
27
  "numpy<2",
@@ -100,7 +100,7 @@ fm = [
100
100
  "pandas",
101
101
  "pillow",
102
102
  "rich",
103
- "hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=5.1.0",
103
+ "hs2p[asap,cucim,openslide,sam2,tifffile,vips]>=6.0.0",
104
104
  "wandb",
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
@@ -150,7 +150,7 @@ include = ["slide2vec*"]
150
150
 
151
151
  [tool.setuptools.package-data]
152
152
  slide2vec = ["py.typed", "cufile.json"]
153
- "slide2vec.configs" = ["*.yaml", "models/*.yaml", "preprocessing/*.yaml"]
153
+ "slide2vec.configs" = ["*.yaml"]
154
154
 
155
155
  [tool.pytest.ini_options]
156
156
  addopts = "--cov=slide2vec -m 'not gpu_integration'"
@@ -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.4"
183
+ current_version = "7.0.0"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false
186
186
  tag = false
@@ -25,13 +25,14 @@ from slide2vec.artifacts import (
25
25
  TileEmbeddingArtifact,
26
26
  )
27
27
  from slide2vec.runtime.dense_encode import DenseEncodeGeometry, DenseEncodeKit
28
+ from slide2vec.runtime.feature_identity import MISSING_FIELD
28
29
  from slide2vec.encoders import (
29
30
  EncoderProviderDiagnostic,
30
31
  list_encoder_provider_diagnostics,
31
32
  )
32
33
 
33
34
 
34
- __version__ = "6.3.4"
35
+ __version__ = "7.0.0"
35
36
 
36
37
  __all__ = [
37
38
  "Model",
@@ -56,5 +57,6 @@ __all__ = [
56
57
  "DenseRegionArtifact",
57
58
  "DenseImageArtifact",
58
59
  "ImageEmbeddingArtifact",
60
+ "MISSING_FIELD",
59
61
  "__version__",
60
62
  ]
@@ -66,19 +66,6 @@ SlideSequence = Sequence[SlideInput]
66
66
  TilingResultsInput = Sequence[Any] | Mapping[str, Any]
67
67
 
68
68
 
69
- #: Default annotation-mask vocabulary — plain binary tissue tiling. Mirrors hs2p's
70
- #: shipped default ``{background: 0, tissue: 1}``; leaving it untouched keeps a run
71
- #: behaving exactly as a tissue-only run. ``min_coverage.tissue`` is the single source
72
- #: of truth for the tissue threshold (the standalone ``tissue_threshold`` knob is gone).
73
- #: A :class:`PreprocessingConfig` ``masks`` value is deep-merged over this default, so
74
- #: callers only state what they override (e.g. ``{"min_coverage": {"tissue": 0.1}}``).
75
- DEFAULT_MASKS: dict[str, Any] = {
76
- "output_mode": "per_annotation",
77
- "pixel_mapping": {"background": 0, "tissue": 1},
78
- "colors": {"background": None, "tissue": [157, 219, 129]},
79
- "min_coverage": {"background": None, "tissue": 0.01},
80
- }
81
-
82
69
  _REQUESTED_TILE_SIZE_INTERPOLATION = "${tiling.params.requested_tile_size_px}"
83
70
 
84
71
 
@@ -92,6 +79,7 @@ def _load_default_preprocessing() -> dict[str, dict[str, Any]]:
92
79
  ("segmentation", "seg_params"),
93
80
  ("filtering", "filter_params"),
94
81
  ("preview", "preview"),
82
+ ("masks", "masks"),
95
83
  ):
96
84
  section = OmegaConf.to_container(getattr(tiling, config_name), resolve=False)
97
85
  if not isinstance(section, dict):
@@ -107,6 +95,13 @@ def _load_default_preprocessing() -> dict[str, dict[str, Any]]:
107
95
  #: ``configs/default.yaml`` so Python and YAML entry points share one source.
108
96
  DEFAULT_PREPROCESSING = _load_default_preprocessing()
109
97
 
98
+ #: Default annotation-mask vocabulary, loaded from ``configs/default.yaml``: plain
99
+ #: binary tissue tiling (``{background: 0, tissue: 1}``) with
100
+ #: ``min_coverage.tissue = 0.1``. ``min_coverage.tissue`` is the single source of truth
101
+ #: for the tissue threshold. A :class:`PreprocessingConfig` ``masks`` value is
102
+ #: deep-merged over this default, so callers only state what they override.
103
+ DEFAULT_MASKS: dict[str, Any] = DEFAULT_PREPROCESSING["masks"]
104
+
110
105
 
111
106
  def _deep_merge_dicts(base: Mapping[str, Any], override: Mapping[str, Any]) -> dict[str, Any]:
112
107
  """Deep-merge *override* onto a copy of *base* (nested dicts merge key-by-key)."""
@@ -213,7 +208,7 @@ class PreprocessingConfig:
213
208
  #: Annotation-mask vocabulary forwarded to hs2p's sampling resolver. Keys:
214
209
  #: ``output_mode``, ``pixel_mapping``, ``colors``, ``min_coverage``. A partial
215
210
  #: mapping is deep-merged over :data:`DEFAULT_MASKS`, so callers only state what
216
- #: they override (e.g. ``{"min_coverage": {"tissue": 0.1}}``). The default
211
+ #: they override (e.g. ``{"min_coverage": {"tissue": 0.5}}``). The default
217
212
  #: ``{background, tissue}`` block is plain tissue tiling; ``min_coverage.tissue``
218
213
  #: is the single source of truth for the tissue threshold.
219
214
  masks: dict[str, Any] = field(default_factory=dict)
@@ -311,6 +306,9 @@ class PreprocessingConfig:
311
306
  return replace(self, mask_backend=mask_backend)
312
307
 
313
308
 
309
+ #: Accepted values of :attr:`ExecutionOptions.on_image_mismatch`.
310
+ IMAGE_MISMATCH_POLICIES = ("raise", "reencode")
311
+
314
312
 
315
313
  @dataclass(frozen=True, kw_only=True)
316
314
  class ExecutionOptions:
@@ -347,6 +345,10 @@ class ExecutionOptions:
347
345
  #: Return and persist the slide encoder's latent representations (PRISM). Encoders
348
346
  #: without latents ignore it.
349
347
  save_latents: bool = False
348
+ #: :meth:`Model.embed_images` only: what to do when an existing image embedding
349
+ #: records a different source ``image_path`` for its ``sample_id``. ``"raise"``
350
+ #: (default) refuses to resume; ``"reencode"`` replaces that image's artifacts.
351
+ on_image_mismatch: str = "raise"
350
352
 
351
353
  @classmethod
352
354
  def from_config(cls, cfg: Any, *, run_on_cpu: bool = False) -> "ExecutionOptions":
@@ -380,8 +382,18 @@ class ExecutionOptions:
380
382
  object.__setattr__(self, "output_dtype", normalize_output_dtype(self.output_dtype))
381
383
  if resolved_num_gpus < 1:
382
384
  raise ValueError("ExecutionOptions.num_gpus must be at least 1")
385
+ if self.batch_size < 1:
386
+ raise ValueError("ExecutionOptions.batch_size must be at least 1")
383
387
  if self.prefetch_factor < 1:
384
388
  raise ValueError("ExecutionOptions.prefetch_factor must be at least 1")
389
+ if self.num_workers_per_gpu is not None and self.num_workers_per_gpu < 0:
390
+ raise ValueError("ExecutionOptions.num_workers_per_gpu must be non-negative")
391
+ if self.on_image_mismatch not in IMAGE_MISMATCH_POLICIES:
392
+ raise ValueError(
393
+ "ExecutionOptions.on_image_mismatch must be one of "
394
+ f"{', '.join(repr(policy) for policy in IMAGE_MISMATCH_POLICIES)}; "
395
+ f"got {self.on_image_mismatch!r}"
396
+ )
385
397
  cap = cpu_worker_limit()
386
398
  cpu_count = os.cpu_count() or 1
387
399
  slurm_limit = slurm_cpu_limit()
@@ -699,22 +711,26 @@ class Model:
699
711
  *,
700
712
  preprocessing: PreprocessingConfig | None,
701
713
  execution: ExecutionOptions | None = None,
714
+ encoded_pixels: bool = True,
702
715
  ) -> dict[str, tuple[Any, Any]]:
703
716
  """Compare a pooled sidecar's ``compatibility`` block with the current recipe.
704
717
 
705
- Returns ``{field: (recorded, current)}`` for differing fields held by both
706
- identities. Missing fields retain the extraction resume semantics: they
707
- cannot be verified and are accepted. The caller owns the policy for an
708
- unrecorded identity or a difference.
718
+ Returns ``{field: (recorded, current)}`` for every required field that the
719
+ record holds with a different value or does not hold. A field the record does
720
+ not hold is reported as :data:`slide2vec.MISSING_FIELD`, which is distinct
721
+ from a recorded ``None``. An empty result means the record proves the current
722
+ identity. The caller owns the policy for a difference.
709
723
 
710
724
  Explicitly pass ``preprocessing=None`` for pre-cropped images, whose shipped
711
725
  transform determines geometry. Pass a :class:`PreprocessingConfig` for
712
726
  tiles read from slides; its unset fields resolve as in pooled extraction.
713
727
  ``execution`` defaults to the model's recommended precision and needs no
714
- output directory.
728
+ output directory. The ``transform`` is required unless ``encoded_pixels`` is
729
+ false: pass ``encoded_pixels=False`` for a zero-tile slide, which encodes no
730
+ pixels and records no transform.
715
731
 
716
- Only a recorded ``transform`` loads weights, on an isolated CPU model.
717
- Slide and patient models load their registered tile encoder and tile output
732
+ Comparing the ``transform`` loads weights, on an isolated CPU model. Slide
733
+ and patient models load their registered tile encoder and tile output
718
734
  variant, without loading aggregation weights. This method does not change
719
735
  this model's input contract or loaded backend, encode pixels, or write files.
720
736
  """
@@ -729,6 +745,7 @@ class Model:
729
745
  dict(recorded),
730
746
  execution=resolved_execution,
731
747
  preprocessing=resolved_preprocessing,
748
+ encoded_pixels=encoded_pixels,
732
749
  )
733
750
 
734
751
  def prepare_dense_encoder(
@@ -1024,8 +1041,11 @@ class Model:
1024
1041
  decoded, preprocessed with the encoder's **shipped** transform, encoded, and written
1025
1042
  to ``image_embeddings/<sample_id>.pt`` plus a provenance sidecar. The run splits its
1026
1043
  images across all visible GPUs (``execution.num_gpus``); ``num_gpus=1`` encodes
1027
- fully in-process. Resume is automatic — images whose sidecar already exists are
1028
- skipped, and a sidecar that records a different feature identity raises. Returns
1044
+ fully in-process. Resume is automatic: an image is skipped only when its sidecar
1045
+ records this ``output_format``, the requested ``image_path`` and the full feature
1046
+ identity. A different recorded feature identity raises; a different recorded
1047
+ ``image_path`` raises unless ``execution.on_image_mismatch="reencode"``; missing
1048
+ provenance re-encodes. Returns
1029
1049
  one :class:`~slide2vec.artifacts.ImageEmbeddingArtifact` per input image, in input
1030
1050
  order.
1031
1051
 
@@ -137,7 +137,8 @@ class ImageEmbeddingArtifact:
137
137
  return load_metadata(self.metadata_path)
138
138
 
139
139
 
140
- def _validate_output_format(output_format: str) -> str:
140
+ def normalize_output_format(output_format: str) -> str:
141
+ """The canonical (lowercase) spelling of a supported ``output_format``."""
141
142
  normalized = output_format.lower()
142
143
  if normalized not in {"pt", "npz"}:
143
144
  raise ValueError(f"Unsupported output format: {output_format}")
@@ -403,27 +404,46 @@ def write_dense_image(
403
404
  )
404
405
 
405
406
 
406
- def image_embedding_paths(
407
- output_dir: str | Path, *, sample_id: str, output_format: str
408
- ) -> tuple[Path, Path]:
409
- """``(payload_path, sidecar_path)`` for one given-geometry image.
407
+ #: Payload formats an image embedding can be written in; they share one sidecar.
408
+ IMAGE_EMBEDDING_FORMATS = ("pt", "npz")
410
409
 
411
- ``image_embeddings/<sample_id>.{pt,npz}`` plus ``image_embeddings/<sample_id>.meta.json``.
412
- A given-geometry run has no slide, no coordinate and no annotation class — the caller's
413
- ``sample_id`` is the whole identity — so the layout is flat and the resume check needs
414
- nothing but the sample id.
410
+
411
+ def image_embeddings_dir(output_dir: str | Path) -> Path:
412
+ """The resolved ``image_embeddings/`` directory, refusing one that escapes ``output_dir``.
413
+
414
+ A run resolves it once and names each image with :func:`image_embedding_names`.
415
415
  """
416
416
  output_root = Path(output_dir).expanduser().resolve()
417
- sample_component = _validate_path_component(sample_id, field="sample_id")
418
417
  embeddings_dir = (output_root / "image_embeddings").resolve()
419
418
  if not embeddings_dir.is_relative_to(output_root):
420
419
  raise ValueError("Image artifact path must stay within output_dir")
420
+ return embeddings_dir
421
+
422
+
423
+ def image_embedding_names(sample_id: str, *, output_format: str) -> tuple[str, str]:
424
+ """``(payload_name, sidecar_name)`` of one image inside :func:`image_embeddings_dir`."""
425
+ sample_component = _validate_path_component(sample_id, field="sample_id")
421
426
  return (
422
- embeddings_dir / f"{sample_component}.{_validate_output_format(output_format)}",
423
- embeddings_dir / f"{sample_component}.meta.json",
427
+ f"{sample_component}.{normalize_output_format(output_format)}",
428
+ f"{sample_component}.meta.json",
424
429
  )
425
430
 
426
431
 
432
+ def image_embedding_paths(
433
+ output_dir: str | Path, *, sample_id: str, output_format: str
434
+ ) -> tuple[Path, Path]:
435
+ """``(payload_path, sidecar_path)`` for one given-geometry image.
436
+
437
+ ``image_embeddings/<sample_id>.{pt,npz}`` plus ``image_embeddings/<sample_id>.meta.json``.
438
+ A given-geometry run has no slide, no coordinate and no annotation class — the caller's
439
+ ``sample_id`` is the whole identity — so the layout is flat. Both payload formats share
440
+ the sidecar, which records the ``format`` it certifies.
441
+ """
442
+ payload_name, sidecar_name = image_embedding_names(sample_id, output_format=output_format)
443
+ embeddings_dir = image_embeddings_dir(output_dir)
444
+ return embeddings_dir / payload_name, embeddings_dir / sidecar_name
445
+
446
+
427
447
  def write_image_embedding(
428
448
  embedding,
429
449
  *,
@@ -440,7 +460,7 @@ def write_image_embedding(
440
460
  ROI write follows (:func:`write_dense_region`), and the reason a rank can be killed
441
461
  mid-shard without poisoning the output.
442
462
  """
443
- output_format = _validate_output_format(output_format)
463
+ output_format = normalize_output_format(output_format)
444
464
  payload_path, metadata_path = image_embedding_paths(
445
465
  output_dir, sample_id=sample_id, output_format=output_format
446
466
  )
@@ -520,7 +540,7 @@ def write_tile_embeddings(
520
540
  tile_index: Any | None = None,
521
541
  annotation: str | None = None,
522
542
  ) -> TileEmbeddingArtifact:
523
- output_format = _validate_output_format(output_format)
543
+ output_format = normalize_output_format(output_format)
524
544
  artifact_path, metadata_path = _setup_artifact_paths(
525
545
  output_dir, tile_embeddings_subdir(annotation), sample_id, output_format
526
546
  )
@@ -563,7 +583,7 @@ def write_tile_embedding_metadata(
563
583
  metadata: dict[str, Any] | None = None,
564
584
  annotation: str | None = None,
565
585
  ) -> Path:
566
- output_format = _validate_output_format(output_format)
586
+ output_format = normalize_output_format(output_format)
567
587
  _, metadata_path = _setup_artifact_paths(
568
588
  output_dir, tile_embeddings_subdir(annotation), sample_id, output_format
569
589
  )
@@ -589,7 +609,7 @@ def write_slide_embeddings(
589
609
  latents: Any | None = None,
590
610
  annotation: str | None = None,
591
611
  ) -> SlideEmbeddingArtifact:
592
- output_format = _validate_output_format(output_format)
612
+ output_format = normalize_output_format(output_format)
593
613
  annotation = structural_artifact_annotation(annotation)
594
614
  artifact_path, metadata_path = _setup_artifact_paths(
595
615
  output_dir, slide_embeddings_subdir(annotation), sample_id, output_format
@@ -638,7 +658,7 @@ def write_patient_embeddings(
638
658
  metadata: dict[str, Any] | None = None,
639
659
  num_slides: int = 0,
640
660
  ) -> PatientEmbeddingArtifact:
641
- output_format = _validate_output_format(output_format)
661
+ output_format = normalize_output_format(output_format)
642
662
  artifact_path, metadata_path = _setup_artifact_paths(
643
663
  output_dir, "patient_embeddings", patient_id, output_format
644
664
  )
@@ -677,7 +697,7 @@ def write_hierarchical_embeddings(
677
697
  metadata: dict[str, Any] | None = None,
678
698
  annotation: str | None = None,
679
699
  ) -> HierarchicalEmbeddingArtifact:
680
- output_format = _validate_output_format(output_format)
700
+ output_format = normalize_output_format(output_format)
681
701
  annotation = structural_artifact_annotation(annotation)
682
702
  artifact_path, metadata_path = _setup_artifact_paths(
683
703
  output_dir, hierarchical_embeddings_subdir(annotation), sample_id, output_format
@@ -9,7 +9,7 @@ save_tiles: false # save extracted tiles as {sample_id}.tiles.tar in addition to
9
9
  seed: 0 # seed for reproducibility
10
10
 
11
11
  model:
12
- name: # preset model name; see docs/models.md for supported request strings
12
+ name: # preset model name; see docs/models.rst for supported request strings
13
13
  output_variant: # requested output variant for presets that expose multiple outputs
14
14
  batch_size: 32
15
15
  save_tile_embeddings: false # whether to save tile embeddings alongside the pooled slide embedding when level is "slide"
@@ -18,7 +18,7 @@ model:
18
18
  allow_non_recommended_settings: false # when true, non-recommended spacing / tile size / precision combinations warn instead of erroring
19
19
 
20
20
  tiling:
21
- on_the_fly: true # read tiles directly from WSI during embedding (requires cucim backend)
21
+ on_the_fly: true # read tiles directly from the WSI during embedding; when false, embedding reads tiles from {sample_id}.tiles.tar
22
22
  gpu_decode: false # GPU-accelerated batch decoding via device="cuda" in cucim read_region; set true to opt in when the runtime is configured for it (experimental, may not be stable across all environments)
23
23
  adaptive_batching: false # when true, vary batch size to align with super tile boundaries (avoids redundant reads but batch size fluctuates)
24
24
  use_supertiles: true # group tiles into 8x8/4x4/2x2 super tile reads to reduce WSI read calls (on-the-fly path only)
@@ -1,6 +1,4 @@
1
- from contextlib import contextmanager
2
1
  from pathlib import Path
3
- from typing import Iterator
4
2
 
5
3
 
6
4
  def config_resource(*parts: str):
@@ -16,8 +14,3 @@ def load_config(*parts: str):
16
14
  resource = config_resource(*parts)
17
15
  with resource.open("r", encoding="utf-8") as handle:
18
16
  return OmegaConf.load(handle)
19
-
20
-
21
- @contextmanager
22
- def config_path(*parts: str) -> Iterator[Path]:
23
- yield config_resource(*parts)
@@ -429,7 +429,6 @@ class OnTheFlyHierarchicalBatchCollator:
429
429
  ):
430
430
  self._region_index = np.asarray(region_index, dtype=np.int32)
431
431
  self._subtile_index_within_region = np.asarray(subtile_index_within_region, dtype=np.int32)
432
- self._tiles_per_region = int(self._subtile_index_within_region.max()) + 1 if len(self._subtile_index_within_region) else 0
433
432
  self._read_tile_size_px = int(read_tile_size_px)
434
433
  self._requested_tile_size_px = int(requested_tile_size_px)
435
434
  self._reader = WSIRegionReader(
@@ -17,8 +17,12 @@ def main(argv=None) -> int:
17
17
  import torch
18
18
 
19
19
  import slide2vec.distributed as distributed
20
- import slide2vec.inference as inference
21
20
  from slide2vec.api import Model
21
+ from slide2vec.runtime.embedding_pipeline import (
22
+ compute_embedded_slides,
23
+ compute_hierarchical_embedding_shard_for_slide,
24
+ compute_tile_embeddings_for_slide,
25
+ )
22
26
  from slide2vec.runtime.hierarchical import (
23
27
  build_hierarchical_index,
24
28
  is_hierarchical_preprocessing,
@@ -56,11 +60,9 @@ def main(argv=None) -> int:
56
60
  )
57
61
  from slide2vec.runtime.embedding import tiling_result_annotation
58
62
  from slide2vec.runtime.feature_identity import transform_record
63
+ from slide2vec.runtime.manifest import load_successful_tiled_slides
59
64
 
60
- load_successful_tiled_slides_fn = getattr(inference, "load_successful_tiled_slides", None)
61
- if not callable(load_successful_tiled_slides_fn):
62
- from slide2vec.runtime.manifest import load_successful_tiled_slides as load_successful_tiled_slides_fn
63
- slide_records, tiling_results = load_successful_tiled_slides_fn(output_dir)
65
+ slide_records, tiling_results = load_successful_tiled_slides(output_dir)
64
66
  # Key by the composite (sample_id, annotation) work unit so a multi-class slide's sibling
65
67
  # classes never overwrite each other; flat units collapse to the bare sample_id key.
66
68
  paired_by_unit = {
@@ -95,16 +97,7 @@ def main(argv=None) -> int:
95
97
  tile_size_lv0=int(geometry["tile_size_lv0"]),
96
98
  )
97
99
  flat_indices = np.array_split(index.flat_index, world_size)[global_rank]
98
- compute_hierarchical_embedding_shard_for_slide_fn = getattr(
99
- inference,
100
- "_compute_hierarchical_embedding_shard_for_slide",
101
- None,
102
- )
103
- if not callable(compute_hierarchical_embedding_shard_for_slide_fn):
104
- from slide2vec.runtime.embedding_pipeline import (
105
- compute_hierarchical_embedding_shard_for_slide as compute_hierarchical_embedding_shard_for_slide_fn,
106
- )
107
- shard_indices, tile_embeddings = compute_hierarchical_embedding_shard_for_slide_fn(
100
+ shard_indices, tile_embeddings = compute_hierarchical_embedding_shard_for_slide(
108
101
  loaded,
109
102
  slide,
110
103
  tiling_result,
@@ -122,16 +115,7 @@ def main(argv=None) -> int:
122
115
  else:
123
116
  num_tiles = len(tiling_result.x)
124
117
  tile_indices = np.array_split(np.arange(num_tiles, dtype=np.int64), world_size)[global_rank]
125
- compute_tile_embeddings_for_slide_fn = getattr(
126
- inference,
127
- "_compute_tile_embeddings_for_slide",
128
- None,
129
- )
130
- if not callable(compute_tile_embeddings_for_slide_fn):
131
- from slide2vec.runtime.embedding_pipeline import (
132
- compute_tile_embeddings_for_slide as compute_tile_embeddings_for_slide_fn,
133
- )
134
- tile_embeddings = compute_tile_embeddings_for_slide_fn(
118
+ tile_embeddings = compute_tile_embeddings_for_slide(
135
119
  loaded,
136
120
  model,
137
121
  slide,
@@ -170,10 +154,7 @@ def main(argv=None) -> int:
170
154
  )
171
155
  torch.save(payload, coordination_dir / f"{stem}.embedded.pt")
172
156
 
173
- compute_embedded_slides_fn = getattr(inference, "_compute_embedded_slides", None)
174
- if not callable(compute_embedded_slides_fn):
175
- from slide2vec.runtime.embedding_pipeline import compute_embedded_slides as compute_embedded_slides_fn
176
- compute_embedded_slides_fn(
157
+ compute_embedded_slides(
177
158
  model,
178
159
  assigned_slides,
179
160
  assigned_tiling_results,
@@ -55,7 +55,7 @@ def main(argv=None) -> int:
55
55
  def _on_batch(count: int) -> None:
56
56
  emit_progress("images.batch.finished", rank=rank.global_rank, images=int(count))
57
57
 
58
- run_image_shard(
58
+ artifacts = run_image_shard(
59
59
  shard,
60
60
  loaded=loaded,
61
61
  out_dir=output_dir,
@@ -67,7 +67,17 @@ def main(argv=None) -> int:
67
67
  num_workers=execution.resolved_image_num_workers_per_gpu(),
68
68
  prefetch_factor=int(execution.prefetch_factor),
69
69
  on_batch=_on_batch,
70
+ # The parent planned what to invalidate; the shard loop deletes each image's
71
+ # files right before writing it, so a failed launch, load or sibling rank costs
72
+ # at most the images in flight.
73
+ stale=request["stale"],
70
74
  )
75
+ # The parent collects artifacts without reading sidecars: report the vector width.
76
+ result_path = Path(request["result_dir"]) / f"image_result.rank{rank.global_rank}.json"
77
+ result_path.write_text(
78
+ json.dumps({"feature_dim": int(artifacts[0].feature_dim), "images": len(artifacts)}),
79
+ encoding="utf-8",
80
+ )
71
81
  return 0
72
82
 
73
83
 
@@ -16,8 +16,9 @@ def get_args_parser(add_help: bool = True) -> argparse.ArgumentParser:
16
16
 
17
17
  def main(argv=None) -> int:
18
18
  import slide2vec.distributed as distributed
19
- import slide2vec.inference as inference
20
19
  from slide2vec.api import Model
20
+ from slide2vec.runtime.embedding_pipeline import compute_embedded_slides
21
+ from slide2vec.runtime.manifest import load_successful_tiled_slides
21
22
  from slide2vec.runtime.persist_callbacks import build_incremental_persist_callback
22
23
  from slide2vec.progress import JsonlProgressReporter, activate_progress_reporter
23
24
  from slide2vec.runtime.serialization import deserialize_execution, deserialize_preprocessing
@@ -44,10 +45,7 @@ def main(argv=None) -> int:
44
45
  execution = deserialize_execution(request["execution"])
45
46
  model._declare_encoder_input(preprocessing, emit_run_info=False)
46
47
  tiling_input_dir = Path(request.get("tiling_input_dir", str(output_dir)))
47
- load_successful_tiled_slides_fn = getattr(inference, "load_successful_tiled_slides", None)
48
- if not callable(load_successful_tiled_slides_fn):
49
- from slide2vec.runtime.manifest import load_successful_tiled_slides as load_successful_tiled_slides_fn
50
- slide_records, tiling_results = load_successful_tiled_slides_fn(tiling_input_dir)
48
+ slide_records, tiling_results = load_successful_tiled_slides(tiling_input_dir)
51
49
  # Each (sample_id, annotation) row is an independent work unit; key by the composite so a
52
50
  # multi-class slide's sibling classes never overwrite each other. Flat units (None / tissue /
53
51
  # merged) encode to the bare sample_id, byte-identical to pre-#168 single-class runs.
@@ -83,17 +81,13 @@ def main(argv=None) -> int:
83
81
  )
84
82
  context = activate_progress_reporter(reporter) if reporter is not None else nullcontext()
85
83
  with context:
86
- build_incremental_persist_callback_fn = getattr(inference, "_build_incremental_persist_callback", build_incremental_persist_callback)
87
- persist_callback, _, _ = build_incremental_persist_callback_fn(
84
+ persist_callback, _, _ = build_incremental_persist_callback(
88
85
  model=model,
89
86
  preprocessing=preprocessing,
90
87
  execution=execution,
91
88
  process_list_path=None,
92
89
  )
93
- compute_embedded_slides_fn = getattr(inference, "_compute_embedded_slides", None)
94
- if not callable(compute_embedded_slides_fn):
95
- from slide2vec.runtime.embedding_pipeline import compute_embedded_slides as compute_embedded_slides_fn
96
- compute_embedded_slides_fn(
90
+ compute_embedded_slides(
97
91
  model,
98
92
  assigned_slides,
99
93
  assigned_tiling_results,
@@ -7,7 +7,7 @@ import pandas as pd
7
7
  from hs2p import SlideSpec
8
8
  from hs2p.fileops import read_csv_keyed_by_sample_id
9
9
 
10
- from slide2vec.api import EmbeddedSlide, ExecutionOptions, PreprocessingConfig
10
+ from slide2vec.api import ExecutionOptions, PreprocessingConfig
11
11
  from slide2vec.artifacts import (
12
12
  HierarchicalEmbeddingArtifact,
13
13
  normalize_artifact_annotation,
@@ -15,7 +15,6 @@ from slide2vec.artifacts import (
15
15
  TileEmbeddingArtifact,
16
16
  )
17
17
  from slide2vec.runtime.distributed_stage import run_distributed_embedding_stage
18
- from slide2vec.runtime.embedding_persist import persist_embedded_slide
19
18
  from slide2vec.runtime.feature_identity import (
20
19
  deferred_transform_record,
21
20
  pooled_feature_identity,
@@ -34,34 +33,6 @@ from slide2vec.runtime.process_list import resolved_process_list_output_variant
34
33
  from slide2vec.progress import emit_progress
35
34
 
36
35
 
37
- def collect_local_pipeline_artifacts(
38
- *,
39
- model,
40
- embedded_slides: Sequence[EmbeddedSlide],
41
- tiling_results,
42
- preprocessing: PreprocessingConfig,
43
- execution: ExecutionOptions,
44
- ) -> tuple[list[TileEmbeddingArtifact], list[HierarchicalEmbeddingArtifact], list[SlideEmbeddingArtifact]]:
45
- tile_artifacts: list[TileEmbeddingArtifact] = []
46
- hierarchical_artifacts: list[HierarchicalEmbeddingArtifact] = []
47
- slide_artifacts: list[SlideEmbeddingArtifact] = []
48
- for embedded_slide, tiling_result in zip(embedded_slides, tiling_results):
49
- tile_artifact, slide_artifact = persist_embedded_slide(
50
- model,
51
- embedded_slide,
52
- tiling_result,
53
- preprocessing=preprocessing,
54
- execution=execution,
55
- )
56
- if isinstance(tile_artifact, HierarchicalEmbeddingArtifact):
57
- hierarchical_artifacts.append(tile_artifact)
58
- elif tile_artifact is not None:
59
- tile_artifacts.append(tile_artifact)
60
- if slide_artifact is not None:
61
- slide_artifacts.append(slide_artifact)
62
- return tile_artifacts, hierarchical_artifacts, slide_artifacts
63
-
64
-
65
36
  def _embeddable_annotation_groups(
66
37
  process_list_path: Path,
67
38
  ) -> dict[str, list[str | None]]:
@@ -216,6 +187,11 @@ def collect_distributed_pipeline_artifacts(
216
187
  )
217
188
  slide_by_sample_id = {slide.sample_id: slide for slide in successful_slides}
218
189
  live_updated_sample_ids: set[str] = set()
190
+ # Pending classes not yet reported finished, per sample. Resume can requeue a class whose
191
+ # payload is still on disk (unverifiable provenance), so file existence alone is not completion.
192
+ unfinished_annotations: dict[str, set[str | None]] = {}
193
+ for slide, annotation in zip(pending_slides, pending_annotations):
194
+ unfinished_annotations.setdefault(slide.sample_id, set()).add(annotation)
219
195
 
220
196
  def _update_process_list_for_finished_slide(event) -> None:
221
197
  if getattr(event, "kind", None) != "embedding.slide.finished":
@@ -225,6 +201,10 @@ def collect_distributed_pipeline_artifacts(
225
201
  slide = slide_by_sample_id.get(sample_id)
226
202
  if slide is None or sample_id in live_updated_sample_ids:
227
203
  return
204
+ unfinished = unfinished_annotations.get(sample_id, set())
205
+ unfinished.discard(_normalized_row_annotation(payload.get("annotation")))
206
+ if unfinished:
207
+ return
228
208
  # A multi-class slide emits one finished event per (sample_id, annotation) item, and the
229
209
  # other classes may still be running on another rank. Wait until *every* annotation row for
230
210
  # the sample is persisted on disk before the whole-sample rewrite, so we never (a) load a