slide2vec 6.3.3__tar.gz → 6.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. {slide2vec-6.3.3 → slide2vec-6.3.4}/PKG-INFO +4 -4
  2. {slide2vec-6.3.3 → slide2vec-6.3.4}/pyproject.toml +5 -5
  3. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/__init__.py +1 -1
  4. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/api.py +17 -6
  5. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/base.py +49 -9
  6. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/registry.py +40 -0
  7. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_encode.py +1 -21
  8. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_regions.py +11 -4
  9. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/PKG-INFO +4 -4
  10. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/requires.txt +3 -3
  11. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_encode_kit.py +15 -0
  12. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_image_stage.py +30 -1
  13. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_regions.py +68 -5
  14. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_stage.py +31 -0
  15. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_capabilities.py +34 -0
  16. {slide2vec-6.3.3 → slide2vec-6.3.4}/LICENSE +0 -0
  17. {slide2vec-6.3.3 → slide2vec-6.3.4}/README.md +0 -0
  18. {slide2vec-6.3.3 → slide2vec-6.3.4}/setup.cfg +0 -0
  19. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/__main__.py +0 -0
  20. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/_cufile.py +0 -0
  21. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/artifacts.py +0 -0
  22. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/cli.py +0 -0
  23. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/configs/__init__.py +0 -0
  24. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/configs/default.yaml +0 -0
  25. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/configs/resources.py +0 -0
  26. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/cufile.json +0 -0
  27. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/data/__init__.py +0 -0
  28. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/data/dataset.py +0 -0
  29. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/data/tile_reader.py +0 -0
  30. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/data/tile_store.py +0 -0
  31. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/__init__.py +0 -0
  32. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/dense_image_worker.py +0 -0
  33. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/dense_worker.py +0 -0
  34. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/direct_embed_worker.py +0 -0
  35. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/image_worker.py +0 -0
  36. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/pin_gpu.py +0 -0
  37. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/pipeline_worker.py +0 -0
  38. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/distributed/worker_entry.py +0 -0
  39. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/__init__.py +0 -0
  40. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/__init__.py +0 -0
  41. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/conch.py +0 -0
  42. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/dinov2.py +0 -0
  43. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/dinov3.py +0 -0
  44. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/genbio.py +0 -0
  45. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/gigapath.py +0 -0
  46. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/gpfm.py +0 -0
  47. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/hibou.py +0 -0
  48. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/hoptimus.py +0 -0
  49. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/isight.py +0 -0
  50. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/lunit.py +0 -0
  51. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/mettle.py +0 -0
  52. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/midnight.py +0 -0
  53. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  54. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  55. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/case.py +0 -0
  56. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/loading.py +0 -0
  57. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/slide.py +0 -0
  58. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/moozy/types.py +0 -0
  59. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/mstar.py +0 -0
  60. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/musk.py +0 -0
  61. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/phikon.py +0 -0
  62. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/prism.py +0 -0
  63. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/prism2.py +0 -0
  64. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/prost40m.py +0 -0
  65. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/rudolfv2.py +0 -0
  66. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/titan.py +0 -0
  67. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/uni.py +0 -0
  68. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/virchow.py +0 -0
  69. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/models/waiv.py +0 -0
  70. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/encoders/validation.py +0 -0
  71. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/inference.py +0 -0
  72. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/progress.py +0 -0
  73. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/__init__.py +0 -0
  74. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/artifacts_collect.py +0 -0
  75. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/batching.py +0 -0
  76. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/cpu_budget.py +0 -0
  77. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_encoder_input.py +0 -0
  78. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_image_reading.py +0 -0
  79. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_image_recipe.py +0 -0
  80. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_image_shard.py +0 -0
  81. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_image_stage.py +0 -0
  82. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_shard.py +0 -0
  83. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_sliding.py +0 -0
  84. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/dense_stage.py +0 -0
  85. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/distributed.py +0 -0
  86. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/distributed_stage.py +0 -0
  87. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/effective_encoder_input.py +0 -0
  88. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/embedding.py +0 -0
  89. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/embedding_persist.py +0 -0
  90. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/embedding_pipeline.py +0 -0
  91. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/encoder_input_contract.py +0 -0
  92. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/feature_identity.py +0 -0
  93. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/hierarchical.py +0 -0
  94. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/image_shard.py +0 -0
  95. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/image_specs.py +0 -0
  96. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/image_stage.py +0 -0
  97. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/manifest.py +0 -0
  98. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/model_settings.py +0 -0
  99. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/patient_pipeline.py +0 -0
  100. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/persist_callbacks.py +0 -0
  101. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/persistence.py +0 -0
  102. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  103. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/preprocessing.py +0 -0
  104. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/process_list.py +0 -0
  105. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/progress_bridge.py +0 -0
  106. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/registry.py +0 -0
  107. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/serialization.py +0 -0
  108. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/sharding.py +0 -0
  109. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/slide_encode.py +0 -0
  110. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/tiling.py +0 -0
  111. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/tiling_pipeline.py +0 -0
  112. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/types.py +0 -0
  113. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/runtime/worker_io.py +0 -0
  114. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/__init__.py +0 -0
  115. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/config.py +0 -0
  116. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/coordinates.py +0 -0
  117. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/log_utils.py +0 -0
  118. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/tiling_io.py +0 -0
  119. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec/utils/utils.py +0 -0
  120. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/SOURCES.txt +0 -0
  121. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/dependency_links.txt +0 -0
  122. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/entry_points.txt +0 -0
  123. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/not-zip-safe +0 -0
  124. {slide2vec-6.3.3 → slide2vec-6.3.4}/slide2vec.egg-info/top_level.txt +0 -0
  125. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_attention_extraction.py +0 -0
  126. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_benchmark_tooling.py +0 -0
  127. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_cli_noninteractive.py +0 -0
  128. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_cufile_compat_mode.py +0 -0
  129. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_encoder_input.py +0 -0
  130. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_extraction.py +0 -0
  131. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_image_reading.py +0 -0
  132. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_image_resume.py +0 -0
  133. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_image_shard.py +0 -0
  134. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_shard.py +0 -0
  135. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_sliding.py +0 -0
  136. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_source_spacing.py +0 -0
  137. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dense_worker.py +0 -0
  138. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dinov2_natimage.py +0 -0
  139. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_dinov3_natimage.py +0 -0
  140. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_input_contract.py +0 -0
  141. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_plugins.py +0 -0
  142. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_preprocessing.py +0 -0
  143. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_provider_failures.py +0 -0
  144. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_encoder_registry.py +0 -0
  145. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_feature_identity.py +0 -0
  146. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_gigapath.py +0 -0
  147. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_gpfm_genbio_heavy.py +0 -0
  148. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_hs2p5_integration.py +0 -0
  149. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_hs2p_package_cutover.py +0 -0
  150. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_image_shard.py +0 -0
  151. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_image_stage.py +0 -0
  152. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_inference_benchmark.py +0 -0
  153. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_isight.py +0 -0
  154. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_load_model_hf_auth.py +0 -0
  155. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_mascaret.py +0 -0
  156. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_mettle.py +0 -0
  157. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_on_slide_persisted.py +0 -0
  158. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_output_consistency.py +0 -0
  159. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_patch_size_metadata.py +0 -0
  160. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_patient_annotation_sampling.py +0 -0
  161. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_patient_manifest.py +0 -0
  162. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_patient_tiling_only.py +0 -0
  163. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_phaet.py +0 -0
  164. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_pooled_encoder_input.py +0 -0
  165. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_pooled_geometry.py +0 -0
  166. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_pooled_identity_api.py +0 -0
  167. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_prism.py +0 -0
  168. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_prism2.py +0 -0
  169. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_process_list_performance.py +0 -0
  170. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_progress.py +0 -0
  171. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_regression_core.py +0 -0
  172. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_regression_inference.py +0 -0
  173. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_regression_models.py +0 -0
  174. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_rudolfv2.py +0 -0
  175. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_runtime_batching.py +0 -0
  176. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_sharding.py +0 -0
  177. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_slide_coordinate_preparation.py +0 -0
  178. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_slide_latents.py +0 -0
  179. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_soma_migration.py +0 -0
  180. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_tile_store.py +0 -0
  181. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_tiling_pipeline.py +0 -0
  182. {slide2vec-6.3.3 → slide2vec-6.3.4}/tests/test_titan.py +0 -0
  183. {slide2vec-6.3.3 → slide2vec-6.3.4}/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.3
3
+ Version: 6.3.4
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
@@ -27,18 +27,18 @@ Requires-Dist: torchvision
27
27
  Requires-Dist: transformers
28
28
  Requires-Dist: wandb
29
29
  Requires-Dist: einops
30
- Requires-Dist: timm
30
+ Requires-Dist: timm>=1.0.3
31
31
  Requires-Dist: huggingface_hub
32
32
  Provides-Extra: hoptimus
33
33
  Requires-Dist: torch>=2.0; extra == "hoptimus"
34
34
  Requires-Dist: torchvision>=0.15.0; extra == "hoptimus"
35
35
  Requires-Dist: xformers>=0.0.18; extra == "hoptimus"
36
36
  Provides-Extra: virchow
37
- Requires-Dist: timm>=0.9.11; extra == "virchow"
37
+ Requires-Dist: timm>=1.0.3; extra == "virchow"
38
38
  Requires-Dist: torch>=2.0; extra == "virchow"
39
39
  Provides-Extra: uni
40
40
  Requires-Dist: torch>=2.0; extra == "uni"
41
- Requires-Dist: timm>=0.9.8; extra == "uni"
41
+ Requires-Dist: timm>=1.0.3; extra == "uni"
42
42
  Requires-Dist: xformers>=0.0.18; extra == "uni"
43
43
  Provides-Extra: prism
44
44
  Requires-Dist: transformers~=4.53.0; extra == "prism"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "6.3.3"
7
+ version = "6.3.4"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -33,7 +33,7 @@ dependencies = [
33
33
  "transformers",
34
34
  "wandb",
35
35
  "einops",
36
- "timm",
36
+ "timm>=1.0.3",
37
37
  "huggingface_hub",
38
38
  ]
39
39
 
@@ -51,12 +51,12 @@ hoptimus = [
51
51
  "xformers>=0.0.18",
52
52
  ]
53
53
  virchow = [
54
- "timm>=0.9.11",
54
+ "timm>=1.0.3",
55
55
  "torch>=2.0",
56
56
  ]
57
57
  uni = [
58
58
  "torch>=2.0",
59
- "timm>=0.9.8",
59
+ "timm>=1.0.3",
60
60
  "xformers>=0.0.18",
61
61
  ]
62
62
  prism = [
@@ -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.3"
183
+ current_version = "6.3.4"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false
186
186
  tag = false
@@ -31,7 +31,7 @@ from slide2vec.encoders import (
31
31
  )
32
32
 
33
33
 
34
- __version__ = "6.3.3"
34
+ __version__ = "6.3.4"
35
35
 
36
36
  __all__ = [
37
37
  "Model",
@@ -26,6 +26,7 @@ from slide2vec.encoders.registry import (
26
26
  encoder_registry,
27
27
  list_encoder_provider_diagnostics,
28
28
  resolve_preprocessing_fields,
29
+ validate_feature_kind_capability,
29
30
  )
30
31
  from slide2vec.encoders.validation import validate_encoder_config
31
32
  from slide2vec.runtime.model_settings import (
@@ -457,8 +458,10 @@ class DenseOptions:
457
458
  #: Fractional window overlap in ``[0, 1)`` for the sliding path (ignored when
458
459
  #: ``window_size is None``).
459
460
  overlap: float = 0.0
460
- #: ``"patch_features"`` (the patch-token grid) or ``"cls_attention"`` (CLS/register
461
- #: self-attention grid).
461
+ #: ``"patch_features"`` (the patch-token grid after the backbone's final norm),
462
+ #: ``"patch_features_prenorm"`` (the same grid tapped *before* the final norm, timm's
463
+ #: ``features_only`` tap; timm ViT encoders only) or ``"cls_attention"``
464
+ #: (CLS/register self-attention grid).
462
465
  feature_kind: str = "patch_features"
463
466
  #: Transformer blocks whose CLS attention is read (``cls_attention`` only).
464
467
  attention_blocks: tuple[int, ...] = (-1,)
@@ -508,8 +511,10 @@ class DenseImageOptions:
508
511
  #: Fractional window overlap in ``[0, 1)`` for the sliding path (ignored when
509
512
  #: ``window_size is None``).
510
513
  overlap: float = 0.0
511
- #: ``"patch_features"`` (the patch-token grid) or ``"cls_attention"`` (CLS/register
512
- #: self-attention grid).
514
+ #: ``"patch_features"`` (the patch-token grid after the backbone's final norm),
515
+ #: ``"patch_features_prenorm"`` (the same grid tapped *before* the final norm, timm's
516
+ #: ``features_only`` tap; timm ViT encoders only) or ``"cls_attention"``
517
+ #: (CLS/register self-attention grid).
513
518
  feature_kind: str = "patch_features"
514
519
  #: Transformer blocks whose CLS attention is read (``cls_attention`` only).
515
520
  attention_blocks: tuple[int, ...] = (-1,)
@@ -1091,14 +1096,20 @@ class Model:
1091
1096
  parent stage and every torchrun rank — declares for itself.
1092
1097
 
1093
1098
  *dense* is a :class:`DenseOptions` (ROIs on a slide) or a :class:`DenseImageOptions`
1094
- (pre-cropped images); only the supervision geometry is read here, and the two state
1095
- it the same way.
1099
+ (pre-cropped images); only the supervision geometry and the requested feature kind
1100
+ are read here, and the two state them the same way.
1101
+
1102
+ The feature kind is checked against the registered encoder class at the same
1103
+ point, so a tap this encoder does not implement (``patch_features_prenorm`` on a
1104
+ non-timm backbone, say) fails before any backend is loaded and before resume
1105
+ inspects or invalidates an existing artifact.
1096
1106
  """
1097
1107
  contract = EncoderInputContract.declared_dense(
1098
1108
  self.name,
1099
1109
  target_size_px=dense.target_size,
1100
1110
  window_size=None if dense.window_size is None else int(dense.window_size),
1101
1111
  )
1112
+ validate_feature_kind_capability(self.name, feature_kind=str(dense.feature_kind))
1102
1113
  self._encoder_input = contract
1103
1114
  plan = contract.plan
1104
1115
  if emit_run_info and plan.requires_variable_model_input:
@@ -462,6 +462,21 @@ class TileEncoder(Encoder):
462
462
  "tokens can be reshaped into a spatial grid."
463
463
  )
464
464
 
465
+ def encode_tiles_dense_prenorm(self, batch: Tensor) -> Tensor:
466
+ """Dense grid tapped before the backbone's final normalisation. (B, C, H, W) -> (B, d, h, w).
467
+
468
+ Same grid as :meth:`encode_tiles_dense` but from the last block's output,
469
+ *before* the final LayerNorm — the tap timm's ``features_only`` backbones
470
+ expose (``forward_intermediates(norm=False)``) and that kaiko-ai/eva's
471
+ segmentation decoders were trained on. Default: unsupported; the timm ViT
472
+ base overrides it.
473
+ """
474
+ raise NotImplementedError(
475
+ f"{type(self).__name__} does not support pre-norm dense feature "
476
+ "extraction (feature_kind='patch_features_prenorm'). The tap is "
477
+ "available for timm ViT tile encoders only."
478
+ )
479
+
465
480
  def encode_tiles_attention(
466
481
  self,
467
482
  batch: Tensor,
@@ -634,15 +649,8 @@ class TimmTileEncoder(TileEncoder):
634
649
  """Number of leading non-patch tokens (CLS + register tokens)."""
635
650
  return int(self._model.num_prefix_tokens)
636
651
 
637
- def encode_tiles_dense(self, batch: Tensor) -> Tensor:
638
- """Encode tiles into a dense spatial grid. (B, C, H, W) -> (B, d, h, w).
639
-
640
- Runs the frozen backbone's ``forward_features`` and folds the patch-token
641
- sequence back into its spatial grid (CLS/register tokens discarded). The
642
- backbone must accept ``batch`` at its current spatial size (timm ViTs need
643
- ``dynamic_img_size=True`` for sizes other than their native input), and
644
- ``H, W`` must be divisible by the patch size.
645
- """
652
+ def _check_dense_batch(self, batch: Tensor) -> tuple[int, int]:
653
+ """Validate a dense ``(B, C, H, W)`` batch; return ``(H, W)``."""
646
654
  if batch.ndim != 4:
647
655
  raise ValueError(
648
656
  "encode_tiles_dense expects a (B, C, H, W) batch, got shape "
@@ -656,6 +664,19 @@ class TimmTileEncoder(TileEncoder):
656
664
  f"divisible by the patch size: got {height}x{width}, patch "
657
665
  f"{patch_h}x{patch_w}. Pad the tile up to a patch multiple first."
658
666
  )
667
+ return int(height), int(width)
668
+
669
+ def encode_tiles_dense(self, batch: Tensor) -> Tensor:
670
+ """Encode tiles into a dense spatial grid. (B, C, H, W) -> (B, d, h, w).
671
+
672
+ Runs the frozen backbone's ``forward_features`` and folds the patch-token
673
+ sequence back into its spatial grid (CLS/register tokens discarded). The
674
+ backbone must accept ``batch`` at its current spatial size (timm ViTs need
675
+ ``dynamic_img_size=True`` for sizes other than their native input), and
676
+ ``H, W`` must be divisible by the patch size.
677
+ """
678
+ height, width = self._check_dense_batch(batch)
679
+ patch_h, patch_w = self._dense_patch_size()
659
680
  tokens = self._model.forward_features(batch)
660
681
  return reshape_tokens_to_grid(
661
682
  tokens,
@@ -665,6 +686,25 @@ class TimmTileEncoder(TileEncoder):
665
686
  encoder_name=type(self).__name__,
666
687
  )
667
688
 
689
+ def encode_tiles_dense_prenorm(self, batch: Tensor) -> Tensor:
690
+ """Pre-norm dense grid via timm's ``forward_intermediates(norm=False)``.
691
+
692
+ ``indices=1`` selects the last block, ``output_fmt="NCHW"`` folds the patch
693
+ tokens into the grid (prefix tokens dropped), ``norm=False`` skips the final
694
+ LayerNorm — exactly timm's ``features_only=True, out_indices=1`` path.
695
+ """
696
+ self._check_dense_batch(batch)
697
+ forward_intermediates = getattr(self._model, "forward_intermediates", None)
698
+ if forward_intermediates is None:
699
+ raise NotImplementedError(
700
+ f"{type(self).__name__} backbone has no forward_intermediates; the "
701
+ "pre-norm dense tap needs a timm ViT-family model."
702
+ )
703
+ grids = forward_intermediates(
704
+ batch, indices=1, norm=False, intermediates_only=True, output_fmt="NCHW"
705
+ )
706
+ return grids[-1]
707
+
668
708
  def encode_tiles_attention(
669
709
  self,
670
710
  batch: Tensor,
@@ -149,6 +149,7 @@ class EncoderCapabilities:
149
149
  level: str
150
150
  pooled: bool
151
151
  dense: bool
152
+ dense_prenorm: bool
152
153
  attention: bool
153
154
  slide: bool
154
155
  patient: bool
@@ -164,6 +165,7 @@ def resolve_encoder_capabilities(encoder_name: str) -> EncoderCapabilities:
164
165
  _validate_encoder_capability_contract(encoder_name, encoder_cls, metadata)
165
166
  level = resolve_encoder_level(encoder_name, metadata)
166
167
  dense = all(_dense_class_contract(encoder_cls))
168
+ dense_prenorm = dense and _supports_dense_prenorm(encoder_cls)
167
169
  attention = _supports_attention(encoder_cls)
168
170
  tile_encoder = None
169
171
  tile_encoder_output_variant = None
@@ -189,6 +191,7 @@ def resolve_encoder_capabilities(encoder_name: str) -> EncoderCapabilities:
189
191
  level=level,
190
192
  pooled=issubclass(encoder_cls, TileEncoder),
191
193
  dense=dense,
194
+ dense_prenorm=dense_prenorm,
192
195
  attention=attention,
193
196
  slide=issubclass(encoder_cls, (SlideEncoder, PatientEncoder)),
194
197
  patient=issubclass(encoder_cls, PatientEncoder),
@@ -239,6 +242,43 @@ def _dense_class_contract(encoder_cls: type) -> tuple[bool, ...]:
239
242
  )
240
243
 
241
244
 
245
+ def _supports_dense_prenorm(encoder_cls: type) -> bool:
246
+ return (
247
+ issubclass(encoder_cls, TileEncoder)
248
+ and getattr(encoder_cls, "encode_tiles_dense_prenorm")
249
+ is not TileEncoder.encode_tiles_dense_prenorm
250
+ )
251
+
252
+
253
+ #: Dense feature kind -> the ``TileEncoder`` method that produces it.
254
+ _FEATURE_KIND_METHODS = {
255
+ "patch_features": "encode_tiles_dense",
256
+ "patch_features_prenorm": "encode_tiles_dense_prenorm",
257
+ "cls_attention": "encode_tiles_attention",
258
+ }
259
+
260
+
261
+ def validate_feature_kind_capability(encoder_name: str, *, feature_kind: str) -> None:
262
+ """Reject a dense feature kind whose method the encoder class leaves unimplemented.
263
+
264
+ Resolved from the registered class, so it runs before any backend is loaded and before
265
+ any on-disk artifact is inspected or invalidated. Every dense route (persisted ROI,
266
+ persisted image, and live kit) goes through it via the dense contract declaration.
267
+ """
268
+ encoder_cls = encoder_registry.require(encoder_name)
269
+ method_name = _FEATURE_KIND_METHODS.get(str(feature_kind))
270
+ if method_name is None:
271
+ raise ValueError(
272
+ f"unsupported feature_kind {feature_kind!r}; expected one of "
273
+ + ", ".join(repr(kind) for kind in _FEATURE_KIND_METHODS)
274
+ )
275
+ if getattr(encoder_cls, method_name) is getattr(TileEncoder, method_name):
276
+ raise ValueError(
277
+ f"Encoder {encoder_name!r} does not support {feature_kind}; choose a "
278
+ "registered dense feature kind implemented by this encoder."
279
+ )
280
+
281
+
242
282
  def _supports_attention(encoder_cls: type) -> bool:
243
283
  return (
244
284
  issubclass(encoder_cls, TileEncoder)
@@ -7,8 +7,7 @@ from typing import TYPE_CHECKING, Any, Callable, cast
7
7
 
8
8
  import torch
9
9
 
10
- from slide2vec.encoders.base import TileEncoder
11
- from slide2vec.encoders.registry import encoder_registry, resolve_patch_size
10
+ from slide2vec.encoders.registry import resolve_patch_size
12
11
  from slide2vec.runtime.dense_regions import (
13
12
  DenseGridGeometry,
14
13
  DenseGridEncoder,
@@ -94,10 +93,6 @@ class _DenseEncodePlan:
94
93
  attention_blocks=dense.attention_blocks,
95
94
  attention_include_registers=dense.attention_include_registers,
96
95
  )
97
- _validate_registered_feature_capability(
98
- contract_plan.tile_encoder_name,
99
- feature_kind=str(dense.feature_kind),
100
- )
101
96
  if execution.precision is None:
102
97
  raise ValueError("Live dense encoder precision must be resolved before loading")
103
98
  output_precision = resolve_output_precision(
@@ -122,21 +117,6 @@ class _DenseEncodePlan:
122
117
  )
123
118
 
124
119
 
125
- def _validate_registered_feature_capability(
126
- encoder_name: str, *, feature_kind: str
127
- ) -> None:
128
- """Reject a feature method left at ``TileEncoder``'s unsupported default."""
129
- encoder_cls = encoder_registry.require(encoder_name)
130
- method_name = {
131
- "patch_features": "encode_tiles_dense",
132
- "cls_attention": "encode_tiles_attention",
133
- }[feature_kind]
134
- if getattr(encoder_cls, method_name) is getattr(TileEncoder, method_name):
135
- raise ValueError(
136
- f"Encoder {encoder_name!r} does not support {feature_kind}; choose a "
137
- "registered dense feature kind implemented by this encoder."
138
- )
139
-
140
120
 
141
121
  @dataclass(frozen=True, kw_only=True)
142
122
  class _DenseItemPreprocessor:
@@ -51,6 +51,10 @@ from slide2vec.runtime.slide_encode import slide_encode_autocast_ctx
51
51
  from slide2vec.runtime.tiling import resolve_slide_backend
52
52
 
53
53
 
54
+ #: Dense feature kinds a request may ask for, each backed by one ``TileEncoder`` method.
55
+ DENSE_FEATURE_KINDS: tuple[str, ...] = ("patch_features", "patch_features_prenorm", "cls_attention")
56
+
57
+
54
58
  def _resolve_output_dtype(output_dtype: "torch.dtype | None", precision: str) -> "torch.dtype":
55
59
  """Resolve the dtype emitted grids are materialized in.
56
60
 
@@ -152,10 +156,10 @@ def validate_dense_request_settings(
152
156
  f"dimension; target_size={geometry.target_size} requires bottom/right "
153
157
  f"pad={geometry.pad}. Choose a larger target or another pad_mode."
154
158
  )
155
- if feature_kind not in {"patch_features", "cls_attention"}:
159
+ if feature_kind not in DENSE_FEATURE_KINDS:
156
160
  raise ValueError(
157
- f"unsupported feature_kind {feature_kind!r}; "
158
- "expected 'patch_features' or 'cls_attention'"
161
+ f"unsupported feature_kind {feature_kind!r}; expected one of "
162
+ + ", ".join(repr(kind) for kind in DENSE_FEATURE_KINDS)
159
163
  )
160
164
  if window_size is not None:
161
165
  if int(window_size) <= 0:
@@ -210,6 +214,8 @@ def _resolve_encode_fn(
210
214
  ) -> Callable[[torch.Tensor], torch.Tensor]:
211
215
  if feature_kind == "patch_features":
212
216
  return model.encode_tiles_dense
217
+ if feature_kind == "patch_features_prenorm":
218
+ return model.encode_tiles_dense_prenorm
213
219
  if feature_kind == "cls_attention":
214
220
  blocks = tuple(int(b) for b in attention_blocks)
215
221
  include_registers = bool(attention_include_registers)
@@ -221,7 +227,8 @@ def _resolve_encode_fn(
221
227
 
222
228
  return encode_fn
223
229
  raise ValueError(
224
- f"unsupported feature_kind {feature_kind!r}; expected 'patch_features' or 'cls_attention'"
230
+ f"unsupported feature_kind {feature_kind!r}; expected one of "
231
+ + ", ".join(repr(kind) for kind in DENSE_FEATURE_KINDS)
225
232
  )
226
233
 
227
234
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 6.3.3
3
+ Version: 6.3.4
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
@@ -27,18 +27,18 @@ Requires-Dist: torchvision
27
27
  Requires-Dist: transformers
28
28
  Requires-Dist: wandb
29
29
  Requires-Dist: einops
30
- Requires-Dist: timm
30
+ Requires-Dist: timm>=1.0.3
31
31
  Requires-Dist: huggingface_hub
32
32
  Provides-Extra: hoptimus
33
33
  Requires-Dist: torch>=2.0; extra == "hoptimus"
34
34
  Requires-Dist: torchvision>=0.15.0; extra == "hoptimus"
35
35
  Requires-Dist: xformers>=0.0.18; extra == "hoptimus"
36
36
  Provides-Extra: virchow
37
- Requires-Dist: timm>=0.9.11; extra == "virchow"
37
+ Requires-Dist: timm>=1.0.3; extra == "virchow"
38
38
  Requires-Dist: torch>=2.0; extra == "virchow"
39
39
  Provides-Extra: uni
40
40
  Requires-Dist: torch>=2.0; extra == "uni"
41
- Requires-Dist: timm>=0.9.8; extra == "uni"
41
+ Requires-Dist: timm>=1.0.3; extra == "uni"
42
42
  Requires-Dist: xformers>=0.0.18; extra == "uni"
43
43
  Provides-Extra: prism
44
44
  Requires-Dist: transformers~=4.53.0; extra == "prism"
@@ -10,7 +10,7 @@ torchvision
10
10
  transformers
11
11
  wandb
12
12
  einops
13
- timm
13
+ timm>=1.0.3
14
14
  huggingface_hub
15
15
 
16
16
  [docs]
@@ -96,11 +96,11 @@ transformers==4.46.0
96
96
 
97
97
  [uni]
98
98
  torch>=2.0
99
- timm>=0.9.8
99
+ timm>=1.0.3
100
100
  xformers>=0.0.18
101
101
 
102
102
  [virchow]
103
- timm>=0.9.11
103
+ timm>=1.0.3
104
104
  torch>=2.0
105
105
 
106
106
  [waiv]
@@ -247,6 +247,21 @@ def test_prepare_dense_encoder_rejects_unsupported_attention_before_loading(
247
247
  assert load_attempted is False
248
248
 
249
249
 
250
+ @pytest.mark.parametrize(("model_name", "target_size"), [("phikon", 224), ("hibou-b", 224)])
251
+ def test_prepare_dense_encoder_rejects_unsupported_prenorm_before_loading(
252
+ model_name, target_size, monkeypatch
253
+ ):
254
+ model = Model(name=model_name, device="cpu")
255
+ monkeypatch.setattr(
256
+ model, "_load_backend", lambda: (_ for _ in ()).throw(AssertionError("must not load"))
257
+ )
258
+ with pytest.raises(ValueError, match="does not support patch_features_prenorm"):
259
+ model.prepare_dense_encoder(
260
+ dense=_dense_image(target_size=target_size, feature_kind="patch_features_prenorm"),
261
+ execution=ExecutionOptions(num_gpus=1, precision="fp32"),
262
+ )
263
+
264
+
250
265
  def test_prepare_dense_encoder_rejects_non_tile_models_before_loading(monkeypatch):
251
266
  model = Model(name="gigapath-slide", device="cpu")
252
267
  load_attempted = False
@@ -22,7 +22,7 @@ PIL = pytest.importorskip("PIL")
22
22
 
23
23
  from PIL import Image # noqa: E402
24
24
 
25
- from slide2vec.api import DenseImageOptions, ExecutionOptions, ImageSpec # noqa: E402
25
+ from slide2vec.api import DenseImageOptions, ExecutionOptions, ImageSpec, Model # noqa: E402
26
26
  from slide2vec.encoders.base import TimmTileEncoder # noqa: E402
27
27
  from slide2vec.runtime import dense_image_stage # noqa: E402
28
28
  from slide2vec.runtime.types import LoadedModel # noqa: E402
@@ -1085,3 +1085,32 @@ def test_worker_encodes_only_its_rank_shard(tmp_path, monkeypatch):
1085
1085
  # World of 2 ranks: plan_contiguous_shards([a,b,c,d], 2) => rank 1 owns [c, d].
1086
1086
  dense_dir = tmp_path / "out" / "dense_image_embeddings"
1087
1087
  assert sorted(p.name for p in dense_dir.glob("*.pt")) == ["c.pt", "d.pt"]
1088
+
1089
+
1090
+ def test_embed_images_dense_rejects_unsupported_prenorm_before_loading_or_resume(
1091
+ tmp_path, monkeypatch
1092
+ ):
1093
+ """An unsupported feature kind fails from registry metadata on the persisted image route.
1094
+
1095
+ Phikon is not a timm ViT, so ``patch_features_prenorm`` is rejected before the backend
1096
+ loads and before resume inspects the output dir: an existing post-norm payload and its
1097
+ sidecar are left untouched (the earlier behaviour deleted the sidecar first).
1098
+ """
1099
+ model = Model(name="phikon", device="cpu")
1100
+ monkeypatch.setattr(
1101
+ model, "_load_backend", lambda: (_ for _ in ()).throw(AssertionError("must not load"))
1102
+ )
1103
+ specs = _images(tmp_path, ["a"])
1104
+ out_dir = tmp_path / "out"
1105
+ existing = out_dir / "dense_image_embeddings"
1106
+ existing.mkdir(parents=True)
1107
+ (existing / "a.pt").write_bytes(b"payload")
1108
+ (existing / "a.meta.json").write_text("{}")
1109
+ with pytest.raises(ValueError, match="does not support patch_features_prenorm"):
1110
+ model.embed_images_dense(
1111
+ specs,
1112
+ dense=_dense(feature_kind="patch_features_prenorm"),
1113
+ execution=ExecutionOptions(num_gpus=1, precision="fp32", output_dir=out_dir),
1114
+ )
1115
+ assert (existing / "a.pt").read_bytes() == b"payload"
1116
+ assert (existing / "a.meta.json").read_text() == "{}"
@@ -161,7 +161,7 @@ def _make_tiling_result(
161
161
  )
162
162
 
163
163
 
164
- @pytest.mark.parametrize("feature_kind", ["patch_features", "cls_attention"])
164
+ @pytest.mark.parametrize("feature_kind", ["patch_features", "patch_features_prenorm", "cls_attention"])
165
165
  @pytest.mark.parametrize("window_size", [None, 32], ids=["whole", "window32"])
166
166
  def test_iter_regions_dense_yields_grid_per_coordinate_in_order(fake_backend, window_size, feature_kind):
167
167
  enc = _encoder()
@@ -235,6 +235,8 @@ def _reference_grid(
235
235
  batch = padded.unsqueeze(0)
236
236
  if feature_kind == "patch_features":
237
237
  encode_fn = enc.encode_tiles_dense
238
+ elif feature_kind == "patch_features_prenorm":
239
+ encode_fn = enc.encode_tiles_dense_prenorm
238
240
  else:
239
241
  encode_fn = enc.encode_tiles_attention
240
242
  with torch.inference_mode():
@@ -248,7 +250,7 @@ def _reference_grid(
248
250
  return out.detach().float().cpu().numpy()[0]
249
251
 
250
252
 
251
- @pytest.mark.parametrize("feature_kind", ["patch_features", "cls_attention"])
253
+ @pytest.mark.parametrize("feature_kind", ["patch_features", "patch_features_prenorm", "cls_attention"])
252
254
  @pytest.mark.parametrize("window_size", [None, 32], ids=["whole", "window32"])
253
255
  def test_iter_regions_dense_matches_direct_encode(fake_backend, window_size, feature_kind):
254
256
  """Each yielded grid is byte-identical to a hand-rolled transform+pad+encode.
@@ -276,7 +278,7 @@ def test_iter_regions_dense_matches_direct_encode(fake_backend, window_size, fea
276
278
  np.testing.assert_array_equal(grid, ref)
277
279
 
278
280
 
279
- @pytest.mark.parametrize("feature_kind", ["patch_features", "cls_attention"])
281
+ @pytest.mark.parametrize("feature_kind", ["patch_features", "patch_features_prenorm", "cls_attention"])
280
282
  def test_iter_regions_dense_area_resizes_when_read_differs_from_requested(fake_backend, feature_kind):
281
283
  """When ``read_tile_size_px != requested_tile_size_px`` the region is area-resized.
282
284
 
@@ -333,7 +335,7 @@ def test_iter_regions_dense_empty_coordinates_yields_nothing(fake_backend):
333
335
  assert fake_backend.open_count == 0 # nothing read -> slide never opened
334
336
 
335
337
 
336
- @pytest.mark.parametrize("feature_kind", ["patch_features", "cls_attention"])
338
+ @pytest.mark.parametrize("feature_kind", ["patch_features", "patch_features_prenorm", "cls_attention"])
337
339
  @pytest.mark.parametrize("window_size", [None, 32], ids=["whole", "window32"])
338
340
  def test_iter_regions_dense_streams_one_batch_at_a_time(fake_backend, window_size, feature_kind):
339
341
  """Reads advance one batch at a time; first grids land before all coords are read.
@@ -369,7 +371,7 @@ def test_iter_regions_dense_streams_one_batch_at_a_time(fake_backend, window_siz
369
371
  assert len(backend.locations_read) == len(coords) # total reads never exceed the coordinate count
370
372
 
371
373
 
372
- @pytest.mark.parametrize("feature_kind", ["patch_features", "cls_attention"])
374
+ @pytest.mark.parametrize("feature_kind", ["patch_features", "patch_features_prenorm", "cls_attention"])
373
375
  def test_iter_regions_dense_is_batch_invariant(fake_backend, feature_kind):
374
376
  """Composition is irrelevant: only ``B`` matters, not how coords are grouped (adr/0002).
375
377
 
@@ -448,3 +450,64 @@ def test_iter_regions_dense_rejects_bfloat16_output_eagerly(fake_backend):
448
450
  )
449
451
  assert fake_backend.backend.locations_read == []
450
452
  assert fake_backend.open_count == 0
453
+
454
+
455
+ @pytest.mark.parametrize("model_name", ["vit_tiny_patch16_224", "vit_small_patch14_reg4_dinov2"])
456
+ def test_prenorm_dense_grid_is_the_last_block_output_before_the_final_norm(model_name):
457
+ """``encode_tiles_dense_prenorm`` is ``encode_tiles_dense`` minus the backbone's final norm.
458
+
459
+ Applying the backbone's own ``norm`` to the pre-norm grid reproduces the post-norm grid
460
+ exactly (timm's ``features_only`` tap), for a plain ViT and a register-token ViT.
461
+ """
462
+ enc = TimmTileEncoder(model_name, pretrained=False, num_classes=0, dynamic_img_size=True)
463
+ patch = enc.patch_size[0]
464
+ batch = torch.randn(2, 3, 4 * patch, 3 * patch, generator=torch.Generator().manual_seed(0))
465
+ with torch.inference_mode():
466
+ post = enc.encode_tiles_dense(batch)
467
+ pre = enc.encode_tiles_dense_prenorm(batch)
468
+ normed = enc._model.norm(pre.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
469
+ assert tuple(pre.shape) == tuple(post.shape) == (2, enc.encode_dim, 4, 3)
470
+ assert not torch.allclose(pre, post)
471
+ assert torch.allclose(normed, post, atol=1e-5)
472
+
473
+
474
+ def test_prenorm_dense_grid_has_literal_values_for_a_fixed_backbone():
475
+ """Literal oracle: with every weight zeroed, each block adds only its MLP output bias.
476
+
477
+ Penultimate block bias 5, last block bias 3, final LayerNorm bias 2: the pre-norm grid
478
+ must be the last block's output ``5 + 3 = 8`` everywhere (not the penultimate block's
479
+ 5, and not the normed 2), and the post-norm grid must be the LayerNorm bias 2 (a
480
+ constant input has zero variance, so only the bias survives).
481
+ """
482
+ enc = _encoder()
483
+ with torch.no_grad():
484
+ for parameter in enc._model.parameters():
485
+ parameter.zero_()
486
+ enc._model.blocks[-2].mlp.fc2.bias.fill_(5.0)
487
+ enc._model.blocks[-1].mlp.fc2.bias.fill_(3.0)
488
+ enc._model.norm.bias.fill_(2.0)
489
+ batch = torch.ones(2, 3, 32, 48)
490
+ with torch.inference_mode():
491
+ pre = enc.encode_tiles_dense_prenorm(batch)
492
+ post = enc.encode_tiles_dense(batch)
493
+ assert tuple(pre.shape) == tuple(post.shape) == (2, enc.encode_dim, 2, 3)
494
+ torch.testing.assert_close(pre, torch.full_like(pre, 8.0))
495
+ torch.testing.assert_close(post, torch.full_like(post, 2.0))
496
+
497
+
498
+ def test_prenorm_dense_rejects_batches_not_divisible_by_the_patch_size():
499
+ enc = _encoder()
500
+ with pytest.raises(ValueError, match="divisible by the patch size"):
501
+ enc.encode_tiles_dense_prenorm(torch.randn(1, 3, 40, 32))
502
+
503
+
504
+ def test_validate_dense_request_settings_rejects_unknown_feature_kind():
505
+ from slide2vec.runtime.dense_regions import validate_dense_request_settings
506
+
507
+ geometry = compute_dense_geometry(target_size=32, patch_size=(16, 16))
508
+ with pytest.raises(ValueError, match="unsupported feature_kind 'tokens'"):
509
+ validate_dense_request_settings(
510
+ geometry, pad_mode="reflect", window_size=None,
511
+ overlap=0.0, feature_kind="tokens", attention_blocks=(-1,),
512
+ attention_include_registers=False,
513
+ )
@@ -533,3 +533,34 @@ def test_region_specs_round_trip_through_request(tmp_path):
533
533
  )
534
534
  assert request["slides"][1]["read_plan"] == specs[2].read_plan.to_dict()
535
535
  assert dense_stage.region_specs_from_request(request) == specs
536
+
537
+
538
+ def test_embed_regions_dense_rejects_unsupported_prenorm_before_loading_or_resume(
539
+ tmp_path, monkeypatch
540
+ ):
541
+ """The persisted ROI route rejects an unimplemented feature kind from registry metadata.
542
+
543
+ Nothing is read, loaded or partitioned for resume: an existing artifact directory is
544
+ left exactly as it was.
545
+ """
546
+ from slide2vec.api import Model
547
+
548
+ model = Model(name="phikon", device="cpu")
549
+ monkeypatch.setattr(
550
+ model, "_load_backend", lambda: (_ for _ in ()).throw(AssertionError("must not load"))
551
+ )
552
+ monkeypatch.setattr(
553
+ dense_stage,
554
+ "partition_regions_by_resume",
555
+ lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("must not resume")),
556
+ )
557
+ out_dir = tmp_path / "out"
558
+ (out_dir / "dense_embeddings" / "s0").mkdir(parents=True)
559
+ (out_dir / "dense_embeddings" / "s0" / "0_0.meta.json").write_text("{}")
560
+ with pytest.raises(ValueError, match="does not support patch_features_prenorm"):
561
+ model.embed_regions_dense(
562
+ [_regions()],
563
+ dense=_dense(feature_kind="patch_features_prenorm"),
564
+ execution=ExecutionOptions(num_gpus=1, precision="fp32", output_dir=out_dir),
565
+ )
566
+ assert (out_dir / "dense_embeddings" / "s0" / "0_0.meta.json").read_text() == "{}"