slide2vec 5.8.2__tar.gz → 5.9.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 (162) hide show
  1. {slide2vec-5.8.2 → slide2vec-5.9.0}/PKG-INFO +3 -3
  2. {slide2vec-5.8.2 → slide2vec-5.9.0}/pyproject.toml +13 -9
  3. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/__init__.py +1 -1
  4. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/api.py +9 -9
  5. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/data/tile_reader.py +0 -1
  6. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/__init__.py +0 -3
  7. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/gigapath.py +2 -7
  8. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/slide.py +9 -17
  9. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/inference.py +19 -53
  10. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/artifacts_collect.py +7 -8
  11. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/batching.py +1 -9
  12. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_sliding.py +2 -7
  13. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/model_settings.py +2 -6
  14. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/persist_callbacks.py +3 -0
  15. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/log_utils.py +4 -9
  16. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/tiling_io.py +0 -2
  17. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/PKG-INFO +3 -3
  18. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/SOURCES.txt +2 -0
  19. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/requires.txt +2 -2
  20. slide2vec-5.9.0/tests/test_architecture_runtime_split.py +19 -0
  21. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_sliding.py +4 -7
  22. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_hs2p_package_cutover.py +3 -3
  23. slide2vec-5.9.0/tests/test_load_model_hf_auth.py +113 -0
  24. slide2vec-5.9.0/tests/test_on_slide_persisted.py +383 -0
  25. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_regression_inference.py +1 -5
  26. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_regression_models.py +1 -1
  27. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_soma_migration.py +0 -40
  28. slide2vec-5.8.2/tests/test_architecture_runtime_split.py +0 -60
  29. {slide2vec-5.8.2 → slide2vec-5.9.0}/LICENSE +0 -0
  30. {slide2vec-5.8.2 → slide2vec-5.9.0}/README.md +0 -0
  31. {slide2vec-5.8.2 → slide2vec-5.9.0}/setup.cfg +0 -0
  32. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/__main__.py +0 -0
  33. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/artifacts.py +0 -0
  34. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/cli.py +0 -0
  35. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/configs/__init__.py +0 -0
  36. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/configs/default.yaml +0 -0
  37. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/configs/resources.py +0 -0
  38. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/data/__init__.py +0 -0
  39. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/data/dataset.py +0 -0
  40. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/data/tile_store.py +0 -0
  41. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/dense_image_worker.py +0 -0
  42. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/dense_worker.py +0 -0
  43. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
  44. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/image_worker.py +0 -0
  45. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/pipeline_worker.py +0 -0
  46. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/distributed/worker_entry.py +0 -0
  47. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/__init__.py +0 -0
  48. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/base.py +0 -0
  49. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/__init__.py +0 -0
  50. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/conch.py +0 -0
  51. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/dinov2.py +0 -0
  52. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/genbio.py +0 -0
  53. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/gpfm.py +0 -0
  54. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/hibou.py +0 -0
  55. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/hoptimus.py +0 -0
  56. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/isight.py +0 -0
  57. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/lunit.py +0 -0
  58. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/midnight.py +0 -0
  59. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  60. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  61. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  62. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  63. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  64. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/mstar.py +0 -0
  65. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/musk.py +0 -0
  66. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/phikon.py +0 -0
  67. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/prism.py +0 -0
  68. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/prism2.py +0 -0
  69. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/prost40m.py +0 -0
  70. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
  71. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/titan.py +0 -0
  72. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/uni.py +0 -0
  73. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/virchow.py +0 -0
  74. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/models/waiv.py +0 -0
  75. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/registry.py +0 -0
  76. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/encoders/validation.py +0 -0
  77. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/progress.py +0 -0
  78. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/__init__.py +0 -0
  79. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/cpu_budget.py +0 -0
  80. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_encode.py +0 -0
  81. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_encoder_input.py +0 -0
  82. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_reading.py +0 -0
  83. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
  84. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_shard.py +0 -0
  85. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_stage.py +0 -0
  86. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_regions.py +0 -0
  87. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_shard.py +0 -0
  88. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/dense_stage.py +0 -0
  89. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/distributed.py +0 -0
  90. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/distributed_stage.py +0 -0
  91. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
  92. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/embedding.py +0 -0
  93. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/embedding_persist.py +0 -0
  94. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
  95. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/encoder_input_contract.py +0 -0
  96. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/hierarchical.py +0 -0
  97. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/image_shard.py +0 -0
  98. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/image_specs.py +0 -0
  99. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/image_stage.py +0 -0
  100. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/manifest.py +0 -0
  101. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/patient_pipeline.py +0 -0
  102. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/persistence.py +0 -0
  103. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  104. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/preprocessing.py +0 -0
  105. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/process_list.py +0 -0
  106. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/progress_bridge.py +0 -0
  107. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/registry.py +0 -0
  108. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/serialization.py +0 -0
  109. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/sharding.py +0 -0
  110. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/slide_encode.py +0 -0
  111. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/tiling.py +0 -0
  112. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  113. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/types.py +0 -0
  114. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/runtime/worker_io.py +0 -0
  115. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/__init__.py +0 -0
  116. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/config.py +0 -0
  117. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/coordinates.py +0 -0
  118. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec/utils/utils.py +0 -0
  119. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  120. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/entry_points.txt +0 -0
  121. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/not-zip-safe +0 -0
  122. {slide2vec-5.8.2 → slide2vec-5.9.0}/slide2vec.egg-info/top_level.txt +0 -0
  123. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_attention_extraction.py +0 -0
  124. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_encode_kit.py +0 -0
  125. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_encoder_input.py +0 -0
  126. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_extraction.py +0 -0
  127. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_image_reading.py +0 -0
  128. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_image_resume.py +0 -0
  129. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_image_shard.py +0 -0
  130. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_image_stage.py +0 -0
  131. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_regions.py +0 -0
  132. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_shard.py +0 -0
  133. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_source_spacing.py +0 -0
  134. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_stage.py +0 -0
  135. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dense_worker.py +0 -0
  136. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_dinov2_natimage.py +0 -0
  137. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_encoder_capabilities.py +0 -0
  138. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_encoder_input_contract.py +0 -0
  139. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_encoder_plugins.py +0 -0
  140. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_encoder_provider_failures.py +0 -0
  141. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_encoder_registry.py +0 -0
  142. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_gpfm_genbio_heavy.py +0 -0
  143. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_image_shard.py +0 -0
  144. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_image_stage.py +0 -0
  145. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_isight.py +0 -0
  146. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_mascaret.py +0 -0
  147. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_output_consistency.py +0 -0
  148. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_patch_size_metadata.py +0 -0
  149. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_patient_manifest.py +0 -0
  150. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_phaet.py +0 -0
  151. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_pooled_encoder_input.py +0 -0
  152. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_pooled_geometry.py +0 -0
  153. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_prism2.py +0 -0
  154. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_progress.py +0 -0
  155. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_regression_core.py +0 -0
  156. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_rudolfv2.py +0 -0
  157. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_runtime_batching.py +0 -0
  158. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_sharding.py +0 -0
  159. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_slide_coordinate_preparation.py +0 -0
  160. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_tile_store.py +0 -0
  161. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_tiling_pipeline.py +0 -0
  162. {slide2vec-5.8.2 → slide2vec-5.9.0}/tests/test_titan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 5.8.2
3
+ Version: 5.9.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,vips]>=4.4.2
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3
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,vips]>=4.4.2; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3; 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 = "5.8.2"
7
+ version = "5.9.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,vips]>=4.4.2",
24
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3",
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,vips]>=4.4.2",
103
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3",
104
104
  "wandb",
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
@@ -158,7 +158,7 @@ testpaths = [
158
158
  "tests",
159
159
  ]
160
160
  markers = [
161
- "heavy: real-weight foundation-model inference on CPU; minutes per test. Excluded from the PR suite via `-m 'not heavy'`; run on the scheduled/manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
161
+ "heavy: real-weight foundation-model inference on CPU; minutes per test. Included in the PR suite and available separately through the manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
162
162
  "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`.",
163
163
  ]
164
164
 
@@ -180,14 +180,15 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "5.8.2"
183
+ current_version = "5.9.0"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
- commit = false # We do version bumping in CI, not as a commit
186
- tag = false # Git tag already exists — we don't auto-tag
187
- push = false # Don't push anything in CI
185
+ commit = false
186
+ tag = false
187
+ push = false
188
188
  files = [
189
189
  "pyproject.toml",
190
- "slide2vec/__init__.py"
190
+ "slide2vec/__init__.py",
191
+ "docs/conf.py",
191
192
  ]
192
193
 
193
194
  [tool.bumpver.file_patterns]
@@ -198,3 +199,6 @@ files = [
198
199
  "slide2vec/__init__.py" = [
199
200
  '^__version__ = "{version}"$',
200
201
  ]
202
+ "docs/conf.py" = [
203
+ '^release = "{version}"$',
204
+ ]
@@ -27,7 +27,7 @@ from slide2vec.encoders import (
27
27
  )
28
28
 
29
29
 
30
- __version__ = "5.8.2"
30
+ __version__ = "5.9.0"
31
31
 
32
32
  __all__ = [
33
33
  "Model",
@@ -7,7 +7,7 @@ import warnings
7
7
  from dataclasses import dataclass, field, replace
8
8
  from contextlib import contextmanager
9
9
  from pathlib import Path
10
- from typing import Any, Mapping, Protocol, Sequence
10
+ from typing import Any, Callable, Mapping, Protocol, Sequence
11
11
 
12
12
  import torch
13
13
  from hs2p import SlideSpec
@@ -128,15 +128,12 @@ def resolve_masks(masks: Mapping[str, Any] | None) -> dict[str, Any]:
128
128
 
129
129
  def _masks_to_plain_dict(node: Any) -> dict[str, Any]:
130
130
  """Normalize a masks config node (OmegaConf, mapping, or namespace) to a plain dict."""
131
+ from omegaconf import OmegaConf
132
+
131
133
  if node is None:
132
134
  return {}
133
- try:
134
- from omegaconf import OmegaConf
135
-
136
- if OmegaConf.is_config(node):
137
- return copy.deepcopy(OmegaConf.to_container(node, resolve=True)) # type: ignore[return-value]
138
- except ImportError:
139
- pass
135
+ if OmegaConf.is_config(node):
136
+ return copy.deepcopy(OmegaConf.to_container(node, resolve=True)) # type: ignore[return-value]
140
137
  if isinstance(node, Mapping):
141
138
  return copy.deepcopy(dict(node))
142
139
  return copy.deepcopy(dict(vars(node)))
@@ -243,7 +240,6 @@ class PreprocessingConfig:
243
240
  "preview",
244
241
  _deep_merge_dicts(DEFAULT_PREPROCESSING["preview"], self.preview),
245
242
  )
246
- # Complete a (possibly partial) masks mapping against the shipped default.
247
243
  object.__setattr__(self, "masks", resolve_masks(self.masks))
248
244
 
249
245
  @classmethod
@@ -710,6 +706,7 @@ class Model:
710
706
  *,
711
707
  preprocessing: PreprocessingConfig | None = None,
712
708
  execution: ExecutionOptions | None = None,
709
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
713
710
  ) -> list[TileEmbeddingArtifact] | list[HierarchicalEmbeddingArtifact]:
714
711
  from slide2vec.inference import embed_tiles
715
712
 
@@ -724,6 +721,7 @@ class Model:
724
721
  tiling_results,
725
722
  execution=resolved,
726
723
  preprocessing=resolved_preprocessing,
724
+ on_slide_persisted=on_slide_persisted,
727
725
  )
728
726
 
729
727
  def aggregate_tiles(
@@ -1217,6 +1215,7 @@ class Pipeline:
1217
1215
  coordinates_dir: str | Path,
1218
1216
  *,
1219
1217
  slides: SlideSequence | None = None,
1218
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
1220
1219
  ) -> RunResult:
1221
1220
  from slide2vec.inference import run_pipeline_with_coordinates
1222
1221
 
@@ -1229,6 +1228,7 @@ class Pipeline:
1229
1228
  slides=slides,
1230
1229
  preprocessing=resolved_preprocessing,
1231
1230
  execution=self.execution,
1231
+ on_slide_persisted=on_slide_persisted,
1232
1232
  )
1233
1233
 
1234
1234
 
@@ -52,7 +52,6 @@ def _open_wsi_backend(image_path: str, backend: str, gpu_decode: bool):
52
52
  return VIPSReader(image_path)
53
53
  elif backend == "asap":
54
54
  from hs2p.wsi.backends.asap import ASAPReader
55
- from slide2vec.utils.log_utils import suppress_c_stderr
56
55
  with suppress_c_stderr():
57
56
  return ASAPReader(image_path)
58
57
  else:
@@ -133,7 +133,6 @@ class _TorchDistributedEnvironment:
133
133
 
134
134
  # Single node job with preset environment (i.e. torchrun)
135
135
  def _set_from_preset_env(self):
136
- # logger.info("Initialization from preset environment")
137
136
  self.rank = int(os.environ["RANK"])
138
137
  self.world_size = int(os.environ["WORLD_SIZE"])
139
138
  assert self.rank < self.world_size
@@ -143,7 +142,6 @@ class _TorchDistributedEnvironment:
143
142
 
144
143
  # Single node and GPU job (i.e. local script run)
145
144
  def _set_from_local(self):
146
- # logger.info("Initialization from local")
147
145
  self.rank = 0
148
146
  self.world_size = 1
149
147
  self.local_rank = 0
@@ -196,7 +194,6 @@ def enable(
196
194
  if set_cuda_current_device:
197
195
  torch.cuda.set_device(torch_env.local_rank)
198
196
 
199
- # Finalize setup
200
197
  _RANK = torch_env.rank
201
198
  _WORLD_SIZE = torch_env.world_size
202
199
  _LOCAL_RANK = torch_env.local_rank
@@ -48,13 +48,8 @@ class GigaPath(TimmTileEncoder):
48
48
  )
49
49
 
50
50
  def get_transform(self) -> Callable:
51
- # POOLED transform only: center-crops the 256px tile to the model's 224px
52
- # native input (paper recipe, center 224 @ native 0.5 mpp). Dense extraction
53
- # must NOT route through this — it needs the full uncropped tile so the grid
54
- # covers the whole source tile. The dense path supplies its own no-crop
55
- # transform (Resize(256), no CenterCrop) → a 16x16 grid over the full tile;
56
- # encode_tiles_dense itself is transform-agnostic (inherited from
57
- # TimmTileEncoder) and operates on whatever batch the dense pipeline feeds.
51
+ # Pooled recipe: center 224px at native spacing. Dense extraction uses
52
+ # get_normalization_transform() to preserve the caller's tile geometry.
58
53
  return v2.Compose([
59
54
  v2.ToImage(),
60
55
  v2.Resize(256, interpolation=v2.InterpolationMode.BICUBIC, antialias=True),
@@ -50,7 +50,7 @@ class MOOZYSlideEncoder(nn.Module):
50
50
  dropout=dropout,
51
51
  attn_dropout=attn_dropout,
52
52
  alibi=self.pos_bias,
53
- drop_path_rate=dpr[i] if i < len(dpr) else 0.0,
53
+ drop_path_rate=dpr[i],
54
54
  qk_norm=qk_norm,
55
55
  layerscale_init=layerscale_init,
56
56
  )
@@ -109,26 +109,18 @@ class MOOZYSlideEncoder(nn.Module):
109
109
  x = torch.cat(tokens, dim=1)
110
110
 
111
111
  coords_xy = coords_xy.to(device=x.device, dtype=torch.float32).reshape(bsz, n_tokens, 2)
112
- zeros_cls = torch.zeros(bsz, 1, 2, dtype=coords_xy.dtype, device=coords_xy.device)
113
- if self.num_registers > 0:
114
- zeros_reg = torch.zeros(bsz, self.num_registers, 2, dtype=coords_xy.dtype, device=coords_xy.device)
115
- positions = torch.cat([zeros_cls, zeros_reg, coords_xy], dim=1)
116
- else:
117
- positions = torch.cat([zeros_cls, coords_xy], dim=1)
118
-
119
- valid_flat = ~invalid_flat
120
- reg_valid = (
121
- torch.ones(bsz, self.num_registers, dtype=torch.bool, device=x.device)
122
- if self.num_registers > 0
123
- else torch.zeros(bsz, 0, dtype=torch.bool, device=x.device)
112
+ num_prefix = 1 + self.num_registers
113
+ prefix_positions = torch.zeros(
114
+ bsz, num_prefix, 2, dtype=coords_xy.dtype, device=coords_xy.device
124
115
  )
125
- valid_with_cls = torch.cat([torch.ones(bsz, 1, dtype=torch.bool, device=x.device), reg_valid, valid_flat], dim=1)
116
+ positions = torch.cat([prefix_positions, coords_xy], dim=1)
117
+ prefix_valid = torch.ones(bsz, num_prefix, dtype=torch.bool, device=x.device)
118
+ valid_with_cls = torch.cat([prefix_valid, ~invalid_flat], dim=1)
126
119
 
127
120
  if mask is None and bsz == 1:
128
121
  keep = valid_with_cls[0]
129
- if keep.sum() > 0:
130
- x = x[:, keep, :]
131
- positions = positions[:, keep, :]
122
+ x = x[:, keep, :]
123
+ positions = positions[:, keep, :]
132
124
  attn_mask = None
133
125
  else:
134
126
  pair_valid = valid_with_cls.unsqueeze(2) & valid_with_cls.unsqueeze(1)
@@ -1,26 +1,14 @@
1
- import json
2
- import importlib
3
1
  import os
4
- import tempfile
5
- import threading
6
- import time
7
- from contextlib import contextmanager, nullcontext
8
- from dataclasses import replace
9
2
  from pathlib import Path
10
- from types import SimpleNamespace
11
3
  from typing import Any, Callable, Sequence
12
4
 
13
- import logging
14
- import pandas as pd
15
5
  import torch
16
- from hs2p import SlideSpec, tile_slides
17
- from hs2p.utils.stderr import run_with_filtered_stderr
6
+ from hs2p import SlideSpec
18
7
 
19
8
  from slide2vec.runtime import (
20
9
  artifacts_collect,
21
10
  batching,
22
11
  cpu_budget,
23
- distributed,
24
12
  distributed_stage,
25
13
  embedding,
26
14
  embedding_persist,
@@ -31,11 +19,9 @@ from slide2vec.runtime import (
31
19
  persist_callbacks,
32
20
  persistence,
33
21
  process_list,
34
- serialization,
35
22
  slide_encode,
36
23
  tiling,
37
24
  tiling_pipeline,
38
- worker_io,
39
25
  )
40
26
  from slide2vec.api import (
41
27
  EmbeddedPatient,
@@ -43,43 +29,21 @@ from slide2vec.api import (
43
29
  ExecutionOptions,
44
30
  PreprocessingConfig,
45
31
  RunResult,
46
- _resolve_hierarchical_preprocessing,
47
32
  )
48
33
  from slide2vec.artifacts import (
49
34
  HierarchicalEmbeddingArtifact,
50
- PatientEmbeddingArtifact,
51
35
  SlideEmbeddingArtifact,
52
36
  TileEmbeddingArtifact,
53
- write_hierarchical_embeddings,
54
37
  load_array,
55
- write_patient_embeddings,
56
- write_tile_embedding_metadata,
57
38
  )
58
39
  from slide2vec.encoders.registry import (
59
40
  encoder_registry,
60
- resolve_encoder_output,
61
41
  resolve_patch_size,
62
- resolve_preprocessing_defaults,
63
42
  )
64
43
  from slide2vec.runtime.model_settings import canonicalize_model_name
65
44
  from slide2vec.runtime.types import LoadedModel
66
45
  from slide2vec.runtime.encoder_input_contract import EncoderInputContract
67
- from slide2vec.progress import (
68
- emit_progress,
69
- read_tiling_progress_snapshot,
70
- )
71
- from slide2vec.utils.log_utils import suppress_c_stderr
72
- from slide2vec.data.dataset import BatchTileCollator, TileIndexDataset
73
- from slide2vec.data.tile_reader import OnTheFlyBatchTileCollator, OnTheFlyHierarchicalBatchCollator
74
- from slide2vec.utils.tiling_io import (
75
- load_embedding_process_df,
76
- load_patient_id_mapping,
77
- load_slide_manifest,
78
- load_tiling_process_df,
79
- load_tiling_result_from_row,
80
- _optional_float,
81
- )
82
- from slide2vec.utils.utils import cpu_worker_limit, slurm_cpu_limit
46
+ from slide2vec.progress import emit_progress
83
47
 
84
48
  from slide2vec.runtime.hierarchical import num_embedding_items
85
49
 
@@ -126,13 +90,15 @@ def load_model(
126
90
  info = encoder_registry.info(name)
127
91
  resolved_level = info["level"]
128
92
 
129
- if token is None and "HF_TOKEN" in os.environ:
130
- token = os.environ["HF_TOKEN"]
131
-
93
+ # Never call ``huggingface_hub.login()`` here. Every encoder resolves its auth
94
+ # through the process-global ``huggingface_hub.get_token()``, which reads
95
+ # ``HF_TOKEN`` from the environment ahead of any stored token file, so an
96
+ # exported ``HF_TOKEN`` is all the hub needs. ``login()`` additionally rewrites a
97
+ # shared ``stored_tokens`` file without a lock; under distributed extraction every
98
+ # rank re-runs load_model per chunk and the concurrent truncate/re-read races,
99
+ # crashing runs with ``ValueError: Token ... not found``.
132
100
  if token is not None:
133
- from huggingface_hub import login as hf_login
134
-
135
- hf_login(token=token, add_to_git_credential=False)
101
+ os.environ["HF_TOKEN"] = token
136
102
 
137
103
  encoder_cls = encoder_registry.require(name)
138
104
  # Pass allow_non_recommended_settings ONLY to encoders whose constructor accepts it
@@ -625,6 +591,7 @@ def embed_tiles(
625
591
  *,
626
592
  execution: ExecutionOptions,
627
593
  preprocessing: PreprocessingConfig | None = None,
594
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
628
595
  ) -> list[TileEmbeddingArtifact] | list[HierarchicalEmbeddingArtifact]:
629
596
  if execution.output_dir is None:
630
597
  raise ValueError("ExecutionOptions.output_dir is required to persist tile embeddings")
@@ -694,6 +661,8 @@ def embed_tiles(
694
661
  annotation=embedding.tiling_result_annotation(tiling_result),
695
662
  )
696
663
  artifacts.append(artifact)
664
+ if on_slide_persisted is not None:
665
+ on_slide_persisted(artifact)
697
666
  return artifacts
698
667
 
699
668
 
@@ -714,11 +683,7 @@ def aggregate_tiles(
714
683
  outputs: list[SlideEmbeddingArtifact] = []
715
684
  for artifact in tile_artifacts:
716
685
  metadata = artifact.metadata
717
- if "coordinates_npz_path" not in metadata or "coordinates_meta_path" not in metadata:
718
- raise ValueError(
719
- f"Tile artifact for {artifact.sample_id} is missing tiling metadata paths required for slide aggregation"
720
- )
721
- if not metadata["coordinates_npz_path"] or not metadata["coordinates_meta_path"]:
686
+ if not metadata.get("coordinates_npz_path") or not metadata.get("coordinates_meta_path"):
722
687
  raise ValueError(
723
688
  f"Tile artifact for {artifact.sample_id} is missing tiling metadata paths required for slide aggregation"
724
689
  )
@@ -735,13 +700,12 @@ def aggregate_tiles(
735
700
  tiling_result,
736
701
  execution=execution,
737
702
  )
738
- latents = None
739
703
  slide_artifact = embedding.write_slide_embedding_artifact(
740
704
  artifact.sample_id,
741
705
  slide_embedding,
742
706
  execution=execution,
743
707
  metadata=embedding.build_slide_embedding_metadata(model, image_path=metadata["image_path"]),
744
- latents=latents,
708
+ latents=None,
745
709
  )
746
710
  outputs.append(slide_artifact)
747
711
  return outputs
@@ -921,9 +885,8 @@ def run_pipeline(
921
885
  execution=execution,
922
886
  process_list_path=process_list_path,
923
887
  )
924
- embedded_slides: list[EmbeddedSlide] = []
925
888
  if pending_slides:
926
- embedded_slides = embedding_pipeline.compute_embedded_slides(
889
+ embedding_pipeline.compute_embedded_slides(
927
890
  model,
928
891
  pending_slides,
929
892
  pending_tiling_results,
@@ -972,6 +935,7 @@ def run_pipeline_with_coordinates(
972
935
  slides=None,
973
936
  preprocessing: PreprocessingConfig | None = None,
974
937
  execution: ExecutionOptions,
938
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
975
939
  ) -> RunResult:
976
940
  if execution.output_dir is None:
977
941
  raise ValueError("ExecutionOptions.output_dir is required for Pipeline.run_with_coordinates(...)")
@@ -1031,6 +995,7 @@ def run_pipeline_with_coordinates(
1031
995
  execution=execution,
1032
996
  output_dir=output_dir,
1033
997
  tiling_input_dir=Path(coordinates_dir),
998
+ on_slide_persisted=on_slide_persisted,
1034
999
  )
1035
1000
  return RunResult(
1036
1001
  tile_artifacts=tile_artifacts,
@@ -1043,6 +1008,7 @@ def run_pipeline_with_coordinates(
1043
1008
  preprocessing=resolved_preprocessing,
1044
1009
  execution=execution,
1045
1010
  process_list_path=process_list_path,
1011
+ on_artifact=on_slide_persisted,
1046
1012
  )
1047
1013
  embedding_pipeline.compute_embedded_slides(
1048
1014
  model,
@@ -1,7 +1,7 @@
1
1
  """Collect pipeline-level tile/slide/hierarchical artifacts (local + distributed paths)."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Sequence
4
+ from typing import Callable, Sequence
5
5
 
6
6
  import pandas as pd
7
7
  from hs2p import SlideSpec
@@ -154,6 +154,7 @@ def collect_distributed_pipeline_artifacts(
154
154
  execution: ExecutionOptions,
155
155
  output_dir: Path,
156
156
  tiling_input_dir: Path | None = None,
157
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
157
158
  ) -> tuple[
158
159
  list[TileEmbeddingArtifact],
159
160
  list[HierarchicalEmbeddingArtifact],
@@ -170,12 +171,10 @@ def collect_distributed_pipeline_artifacts(
170
171
  or include_slide_embeddings
171
172
  or persist_hierarchical_embeddings
172
173
  )
173
- annotation_groups = (
174
- _embeddable_annotation_groups(process_list_path) if annotation_aware else {}
175
- )
176
174
  # The embedding stage must fan out per (sample_id, annotation) for every level so each class's
177
175
  # tiles are embedded independently.
178
176
  stage_annotation_groups = _embeddable_annotation_groups(process_list_path)
177
+ annotation_groups = stage_annotation_groups if annotation_aware else {}
179
178
  # One annotation per ``successful_slides`` entry (the tiling spine fans out per class). Carry each
180
179
  # on a lightweight placeholder so the resume gate keys by (sample_id, annotation) and the surviving
181
180
  # pending entries hand the stage the right per-class work units.
@@ -258,6 +257,9 @@ def collect_distributed_pipeline_artifacts(
258
257
  slide_artifacts=slide_artifacts,
259
258
  )
260
259
  live_updated_sample_ids.add(sample_id)
260
+ if on_slide_persisted is not None:
261
+ for artifact in [*tile_artifacts, *hierarchical_artifacts]:
262
+ on_slide_persisted(artifact)
261
263
 
262
264
  run_distributed_embedding_stage(
263
265
  model=model,
@@ -269,9 +271,6 @@ def collect_distributed_pipeline_artifacts(
269
271
  annotations=pending_annotations,
270
272
  on_progress_event=_update_process_list_for_finished_slide,
271
273
  )
272
- # ``successful_slides`` is already one entry per (sample_id, annotation) row, so resolve a
273
- # parallel annotations list (not an expansion) to re-read each entry's namespaced artifact.
274
- annotations_for_collect = _annotations_parallel_to_slides(successful_slides, annotation_groups)
275
274
  tile_artifacts, hierarchical_artifacts, slide_artifacts = collect_pipeline_artifacts(
276
275
  successful_slides,
277
276
  output_dir=output_dir,
@@ -279,7 +278,7 @@ def collect_distributed_pipeline_artifacts(
279
278
  include_tile_embeddings=include_tile_embeddings,
280
279
  include_hierarchical_embeddings=persist_hierarchical_embeddings,
281
280
  include_slide_embeddings=include_slide_embeddings,
282
- annotations=annotations_for_collect if annotation_aware else None,
281
+ annotations=stage_annotations if annotation_aware else None,
283
282
  )
284
283
  update_process_list_after_embedding(
285
284
  process_list_path,
@@ -17,6 +17,7 @@ import torch
17
17
 
18
18
  from slide2vec.progress import emit_progress
19
19
  from slide2vec.runtime.types import LoadedModel
20
+ from slide2vec.runtime.worker_io import uses_cuda_runtime
20
21
  from slide2vec.utils.log_utils import suppress_c_stderr
21
22
 
22
23
  from .preprocessing import (
@@ -29,17 +30,8 @@ from .preprocessing import (
29
30
  from .types import PreparedBatch
30
31
 
31
32
 
32
- def uses_cuda_runtime(device) -> bool:
33
- return str(device).startswith("cuda") and torch.cuda.is_available()
34
-
35
-
36
-
37
33
  def should_suppress_cucim_dataloader_stderr(dataloader) -> bool:
38
- if dataloader is None:
39
- return False
40
34
  collate_fn = getattr(dataloader, "collate_fn", None)
41
- if collate_fn is None:
42
- return False
43
35
  return bool(getattr(collate_fn, "_suppress_cucim_stderr", False))
44
36
 
45
37
 
@@ -73,11 +73,6 @@ def cover_origins(extent: int, size: int, stride: int) -> list[int]:
73
73
  return starts
74
74
 
75
75
 
76
- def _window_starts(extent: int, win: int, stride: int) -> list[int]:
77
- """Patch-aligned encoder-window starts — the token-space use of :func:`cover_origins`."""
78
- return cover_origins(extent, win, stride)
79
-
80
-
81
76
  def resolve_window_geometry(
82
77
  geometry: DenseGridGeometry, *, window_size: int | None, overlap: float
83
78
  ) -> tuple[tuple[int, int], tuple[int, int], list[int], list[int]]:
@@ -99,8 +94,8 @@ def resolve_window_geometry(
99
94
  keep = 1.0 - float(overlap)
100
95
  stride_h = min(win_h, _round_to(win_h * keep, ph))
101
96
  stride_w = min(win_w, _round_to(win_w * keep, pw))
102
- starts_h = _window_starts(enc_h, win_h, stride_h)
103
- starts_w = _window_starts(enc_w, win_w, stride_w)
97
+ starts_h = cover_origins(enc_h, win_h, stride_h)
98
+ starts_w = cover_origins(enc_w, win_w, stride_w)
104
99
  return (win_h, win_w), (stride_h, stride_w), starts_h, starts_w
105
100
 
106
101
 
@@ -1,8 +1,5 @@
1
- import logging
2
1
  from typing import Any
3
2
 
4
- logger = logging.getLogger("slide2vec")
5
-
6
3
 
7
4
  PRECISION_ALIASES = {
8
5
  "fp32": "fp32",
@@ -66,8 +63,8 @@ def resolve_output_precision(output_dtype: Any, compute_precision: Any) -> str:
66
63
  """Resolve the concrete on-disk feature precision (``"fp16"`` or ``"fp32"``).
67
64
 
68
65
  ``output_dtype is None`` follows ``compute_precision``: an fp16 forward keeps fp16
69
- features, while bf16 / fp32 (and an unset or unknown compute precision) widen to fp32
70
- fp32 is bf16's lossless container and the only float dtype a numpy artifact can hold.
66
+ features, while bf16 / fp32 (and an unset compute precision) widen to fp32.
67
+ NumPy has no bfloat16 dtype, so fp32 is its lossless storage container.
71
68
  A non-null ``output_dtype`` is honored verbatim (after :func:`normalize_output_dtype`).
72
69
  This is the single source of truth shared by the pooled write path and the dense
73
70
  ``iter_regions_dense`` path.
@@ -98,4 +95,3 @@ def output_torch_dtype(precision: str):
98
95
  def canonicalize_model_name(name: str) -> str:
99
96
  normalized = name.strip().lower()
100
97
  return MODEL_NAME_ALIASES.get(normalized, normalized)
101
-
@@ -187,6 +187,7 @@ def build_incremental_persist_callback(
187
187
  preprocessing: PreprocessingConfig,
188
188
  execution: ExecutionOptions,
189
189
  process_list_path: Path | None = None,
190
+ on_artifact: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
190
191
  ) -> tuple[
191
192
  Callable[[SlideSpec, Any, EmbeddedSlide], None] | None,
192
193
  list[TileEmbeddingArtifact] | list[HierarchicalEmbeddingArtifact],
@@ -250,6 +251,8 @@ def build_incremental_persist_callback(
250
251
  )
251
252
  if tile_artifact is not None:
252
253
  tile_artifacts.append(tile_artifact)
254
+ if on_artifact is not None:
255
+ on_artifact(tile_artifact)
253
256
  if slide_artifact is not None:
254
257
  slide_artifacts.append(slide_artifact)
255
258
  # Buffer this completion; a slide with no successful artifact is still
@@ -7,12 +7,6 @@ import sys
7
7
  from slide2vec.progress import emit_progress_log
8
8
 
9
9
 
10
- def _distributed_module():
11
- import slide2vec.distributed as distributed
12
-
13
- return distributed
14
-
15
-
16
10
  @contextlib.contextmanager
17
11
  def suppress_c_stderr():
18
12
  """Temporarily redirect C-level stderr to /dev/null.
@@ -62,16 +56,17 @@ def _configure_logger(
62
56
  level: The logging level to use.
63
57
  output: A file name or a directory to save log. If None, will not save log file.
64
58
  If ends with ".txt" or ".log", assumed to be a file name.
65
- Otherwise, logs will be saved to `output/log.txt`.
59
+ Otherwise, logs will be saved to `output/logs/log.txt`.
66
60
 
67
61
  Returns:
68
62
  The configured logger.
69
63
  """
70
64
 
65
+ import slide2vec.distributed as distributed
66
+
71
67
  logger = logging.getLogger(name)
72
68
  logger.setLevel(level)
73
69
  logger.propagate = False
74
- distributed = _distributed_module()
75
70
 
76
71
  # Loosely match Google glog format:
77
72
  # [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
@@ -127,7 +122,7 @@ def setup_logging(
127
122
  output: A file name or a directory to save log files. If None, log
128
123
  files will not be saved. If output ends with ".txt" or ".log", it
129
124
  is assumed to be a file name.
130
- Otherwise, logs will be saved to `output/log.txt`.
125
+ Otherwise, logs will be saved to `output/logs/log.txt`.
131
126
  name: The name of the logger to configure, by default the root logger.
132
127
  level: The logging level to use.
133
128
  capture_warnings: Whether warnings should be captured as logs.
@@ -211,8 +211,6 @@ def _load_base_process_df(process_list_path: str | Path) -> pd.DataFrame:
211
211
  df["requested_mask_backend"] = [None] * len(df)
212
212
  if "mask_backend" not in df.columns:
213
213
  df["mask_backend"] = [None] * len(df)
214
- if "annotation" not in df.columns:
215
- df["annotation"] = ["tissue"] * len(df)
216
214
  reconstructed_output_mode = df["annotation"].map(
217
215
  lambda annotation: "merged" if str(annotation) == "merged" else None
218
216
  )