thyra 4.6.0__tar.gz → 4.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {thyra-4.6.0 → thyra-4.6.2}/PKG-INFO +17 -28
  2. {thyra-4.6.0 → thyra-4.6.2}/README.md +14 -26
  3. {thyra-4.6.0 → thyra-4.6.2}/pyproject.toml +4 -3
  4. {thyra-4.6.0 → thyra-4.6.2}/thyra/__init__.py +1 -1
  5. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/base_spatialdata_converter.py +68 -4
  6. thyra-4.6.2/thyra/core/positions.py +165 -0
  7. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/bruker_extractor.py +65 -21
  8. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +93 -20
  9. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/solarix/solarix_reader.py +60 -33
  10. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/timstof_reader.py +83 -32
  11. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/imzml_reader.py +18 -9
  12. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/chunk_decoding.py +21 -1
  13. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/mzpeak_reader.py +53 -14
  14. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/event_stream.py +11 -1
  15. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/mass_axis.py +8 -0
  16. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/phi_header.py +10 -2
  17. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/phi_reader.py +39 -7
  18. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/imaging_grid.py +33 -4
  19. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/waters_reader.py +111 -7
  20. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/memory_guard.py +58 -1
  21. {thyra-4.6.0 → thyra-4.6.2}/.gitignore +0 -0
  22. {thyra-4.6.0 → thyra-4.6.2}/LICENSE +0 -0
  23. {thyra-4.6.0 → thyra-4.6.2}/thyra/__main__.py +0 -0
  24. {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/__init__.py +0 -0
  25. {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/affine.py +0 -0
  26. {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/teaching_points.py +0 -0
  27. {thyra-4.6.0 → thyra-4.6.2}/thyra/config.py +0 -0
  28. {thyra-4.6.0 → thyra-4.6.2}/thyra/convert.py +0 -0
  29. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/__init__.py +0 -0
  30. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/__init__.py +0 -0
  31. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/_chunking.py +0 -0
  32. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/acquisition_frame.py +0 -0
  33. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/converter.py +0 -0
  34. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/csc_assembly.py +0 -0
  35. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/fused_passes.py +0 -0
  36. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
  37. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/mobility_table.py +0 -0
  38. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/msms_table.py +0 -0
  39. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/optical_image.py +0 -0
  40. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/root_attrs.py +0 -0
  41. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/sibling_tables.py +0 -0
  42. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/streaming_converter.py +0 -0
  43. {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/uns_assembler.py +0 -0
  44. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/__init__.py +0 -0
  45. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_converter.py +0 -0
  46. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_extractor.py +0 -0
  47. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_reader.py +0 -0
  48. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/conversion_state.py +0 -0
  49. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/drop_tally.py +0 -0
  50. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/frames.py +0 -0
  51. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/mass_axis.py +0 -0
  52. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/mobility.py +0 -0
  53. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/msms.py +0 -0
  54. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/region_map.py +0 -0
  55. {thyra-4.6.0 → thyra-4.6.2}/thyra/core/registry.py +0 -0
  56. {thyra-4.6.0 → thyra-4.6.2}/thyra/errors.py +0 -0
  57. {thyra-4.6.0 → thyra-4.6.2}/thyra/mass_axis_plan.py +0 -0
  58. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/__init__.py +0 -0
  59. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/constants.py +0 -0
  60. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/document.py +0 -0
  61. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/__init__.py +0 -0
  62. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/imzml_extractor.py +0 -0
  63. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
  64. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/mzpeak_extractor.py +0 -0
  65. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/phi_extractor.py +0 -0
  66. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/solarix_extractor.py +0 -0
  67. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/waters_extractor.py +0 -0
  68. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/__init__.py +0 -0
  69. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_ims.py +0 -0
  70. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_ms.py +0 -0
  71. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_uo.py +0 -0
  72. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/cache.py +0 -0
  73. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/dump_obo_files.py +0 -0
  74. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/personal_data.py +0 -0
  75. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/__init__.py +0 -0
  76. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/axis_fingerprint.py +0 -0
  77. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/builder.py +0 -0
  78. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/cli.py +0 -0
  79. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/generate.py +0 -0
  80. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/metaspace.py +0 -0
  81. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/models.py +0 -0
  82. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
  83. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/msi_metadata_schema_v0_14.json +0 -0
  84. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/store_io.py +0 -0
  85. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/validate.py +0 -0
  86. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/vocab.py +0 -0
  87. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/types.py +0 -0
  88. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/uns_compat.py +0 -0
  89. {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/validator.py +0 -0
  90. {thyra-4.6.0 → thyra-4.6.2}/thyra/preview.py +0 -0
  91. {thyra-4.6.0 → thyra-4.6.2}/thyra/py.typed +0 -0
  92. {thyra-4.6.0 → thyra-4.6.2}/thyra/reader_options.py +0 -0
  93. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/__init__.py +0 -0
  94. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/__init__.py +0 -0
  95. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/base_bruker_reader.py +0 -0
  96. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/folder_structure.py +0 -0
  97. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/mis_parser.py +0 -0
  98. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
  99. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/solarix/__init__.py +0 -0
  100. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/__init__.py +0 -0
  101. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
  102. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
  103. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
  104. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
  105. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
  106. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
  107. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
  108. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
  109. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/vendor_db.py +0 -0
  110. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/__init__.py +0 -0
  111. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
  112. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/header.py +0 -0
  113. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/mobility_array.py +0 -0
  114. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/__init__.py +0 -0
  115. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/__init__.py +0 -0
  116. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/__init__.py +0 -0
  117. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/instrument.py +0 -0
  118. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/MLReader.dll +0 -0
  119. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
  120. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/libMLReader.so +0 -0
  121. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
  122. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lockmass.py +0 -0
  123. {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/masslynx_lib.py +0 -0
  124. {thyra-4.6.0 → thyra-4.6.2}/thyra/regions.py +0 -0
  125. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/__init__.py +0 -0
  126. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/axis_planner.py +0 -0
  127. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/binning.py +0 -0
  128. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/common_axis.py +0 -0
  129. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/constants.py +0 -0
  130. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/data_characteristics.py +0 -0
  131. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/decision_tree.py +0 -0
  132. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/gaps.py +0 -0
  133. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/instrument_detectors.py +0 -0
  134. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/interpolation.py +0 -0
  135. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/__init__.py +0 -0
  136. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/base_generator.py +0 -0
  137. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
  138. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/linear_generator.py +0 -0
  139. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
  140. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
  141. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
  142. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/tof_generator.py +0 -0
  143. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mobility_grid.py +0 -0
  144. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/plan.py +0 -0
  145. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/__init__.py +0 -0
  146. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/base.py +0 -0
  147. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
  148. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/tic_preserving.py +0 -0
  149. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/tic.py +0 -0
  150. {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/types.py +0 -0
  151. {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/__init__.py +0 -0
  152. {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/check_ontology.py +0 -0
  153. {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/make_example_data.py +0 -0
  154. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/__init__.py +0 -0
  155. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/bruker_exceptions.py +0 -0
  156. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/imzml_coordinate_base.py +0 -0
  157. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/imzml_spectrum_terms.py +0 -0
  158. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/logging_config.py +0 -0
  159. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/ms_levels.py +0 -0
  160. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/output_files.py +0 -0
  161. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/pyimzml_direct.py +0 -0
  162. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/store_lifecycle.py +0 -0
  163. {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/windows_paths.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thyra
3
- Version: 4.6.0
3
+ Version: 4.6.2
4
4
  Summary: A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics
5
5
  Project-URL: Homepage, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
6
6
  Project-URL: Repository, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
7
7
  Project-URL: Documentation, https://M4i-Imaging-Mass-Spectrometry.github.io/thyra/
8
- Project-URL: Paper, https://doi.org/10.1021/jasms.6c00169
8
+ Project-URL: Paper, https://doi.org/10.1038/s41592-026-03305-5
9
+ Project-URL: Paper (JASMS), https://doi.org/10.1021/jasms.6c00169
9
10
  Project-URL: Bug Tracker, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/issues
10
11
  Project-URL: Discussions, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/discussions
11
12
  Project-URL: Changelog, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CHANGELOG.md
@@ -223,7 +224,15 @@ MIT -- see [LICENSE](LICENSE).
223
224
 
224
225
  ## Citation
225
226
 
226
- If you use Thyra, please cite the paper:
227
+ If you use Thyra, please cite:
228
+
229
+ > Visvikis, T.; Hendriks, T. F. E.; Vierdag, W.-M.; Marconato, L.; Heeren, R. M. A.;
230
+ > Cuypers, E. Thyra: bridging mass spectrometry imaging and SpatialData for
231
+ > unified multimodal analysis. *Nat. Methods* **2026**.
232
+ > [doi:10.1038/s41592-026-03305-5](https://doi.org/10.1038/s41592-026-03305-5)
233
+
234
+ If your work relies on how Thyra reads vendor files or keeps memory bounded
235
+ while converting, please also cite:
227
236
 
228
237
  > Visvikis, T.; Vierdag, W.-M.; Marconato, L.; Birmpili, A.; Heeren, R. M. A.;
229
238
  > Cuypers, E. Native, Memory-Bounded Conversion of Mass Spectrometry Imaging
@@ -231,39 +240,19 @@ If you use Thyra, please cite the paper:
231
240
  > *37* (10), 2379-2387.
232
241
  > [doi:10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169)
233
242
 
234
- ```bibtex
235
- @article{visvikis2026thyra,
236
- title = {Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the {SpatialData} Ecosystem},
237
- author = {Visvikis, Theodoros and Vierdag, Wouter-Michiel and Marconato, Luca and Birmpili, Angeliki and Heeren, Ron M. A. and Cuypers, Eva},
238
- journal = {Journal of the American Society for Mass Spectrometry},
239
- year = {2026},
240
- volume = {37},
241
- number = {10},
242
- pages = {2379--2387},
243
- doi = {10.1021/jasms.6c00169}
244
- }
245
- ```
246
-
247
- The same entry is in [CITATION.cff](CITATION.cff), which GitHub's "Cite this
248
- repository" button reads. To cite the software itself:
249
-
250
- ```bibtex
251
- @software{thyra2024,
252
- title = {Thyra: Modern Mass Spectrometry Imaging Data Conversion},
253
- author = {Visvikis, Theodoros},
254
- year = {2024},
255
- url = {https://github.com/M4i-Imaging-Mass-Spectrometry/thyra}
256
- }
257
- ```
243
+ BibTeX for both is in [CITATION.bib](CITATION.bib). GitHub's "Cite this
244
+ repository" button reads [CITATION.cff](CITATION.cff), which shows the Nature
245
+ Methods paper and lists the JASMS paper under its references.
258
246
 
259
247
  ### Papers
260
248
 
261
249
  | Paper | Published | DOI |
262
250
  |---|---|---|
251
+ | Thyra: bridging mass spectrometry imaging and SpatialData for unified multimodal analysis (*Nat. Methods*) | 2026-10 | [10.1038/s41592-026-03305-5](https://doi.org/10.1038/s41592-026-03305-5) |
263
252
  | Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the SpatialData Ecosystem (*J. Am. Soc. Mass Spectrom.*) | 2026-09 | [10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169) |
264
253
  | Thyra: Bridging Mass Spectrometry Imaging and SpatialData for Unified Multi-Modal Analysis (bioRxiv preprint) | 2026-01 | [10.64898/2026.01.26.700792](https://doi.org/10.64898/2026.01.26.700792) |
265
254
 
266
- Ion mobility and MS/MS support came after both papers. If you use them,
255
+ Ion mobility and MS/MS support came after the JASMS paper and the preprint. If you use them,
267
256
  please also name the Thyra version you used.
268
257
 
269
258
  ## Acknowledgments
@@ -169,7 +169,15 @@ MIT -- see [LICENSE](LICENSE).
169
169
 
170
170
  ## Citation
171
171
 
172
- If you use Thyra, please cite the paper:
172
+ If you use Thyra, please cite:
173
+
174
+ > Visvikis, T.; Hendriks, T. F. E.; Vierdag, W.-M.; Marconato, L.; Heeren, R. M. A.;
175
+ > Cuypers, E. Thyra: bridging mass spectrometry imaging and SpatialData for
176
+ > unified multimodal analysis. *Nat. Methods* **2026**.
177
+ > [doi:10.1038/s41592-026-03305-5](https://doi.org/10.1038/s41592-026-03305-5)
178
+
179
+ If your work relies on how Thyra reads vendor files or keeps memory bounded
180
+ while converting, please also cite:
173
181
 
174
182
  > Visvikis, T.; Vierdag, W.-M.; Marconato, L.; Birmpili, A.; Heeren, R. M. A.;
175
183
  > Cuypers, E. Native, Memory-Bounded Conversion of Mass Spectrometry Imaging
@@ -177,39 +185,19 @@ If you use Thyra, please cite the paper:
177
185
  > *37* (10), 2379-2387.
178
186
  > [doi:10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169)
179
187
 
180
- ```bibtex
181
- @article{visvikis2026thyra,
182
- title = {Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the {SpatialData} Ecosystem},
183
- author = {Visvikis, Theodoros and Vierdag, Wouter-Michiel and Marconato, Luca and Birmpili, Angeliki and Heeren, Ron M. A. and Cuypers, Eva},
184
- journal = {Journal of the American Society for Mass Spectrometry},
185
- year = {2026},
186
- volume = {37},
187
- number = {10},
188
- pages = {2379--2387},
189
- doi = {10.1021/jasms.6c00169}
190
- }
191
- ```
192
-
193
- The same entry is in [CITATION.cff](CITATION.cff), which GitHub's "Cite this
194
- repository" button reads. To cite the software itself:
195
-
196
- ```bibtex
197
- @software{thyra2024,
198
- title = {Thyra: Modern Mass Spectrometry Imaging Data Conversion},
199
- author = {Visvikis, Theodoros},
200
- year = {2024},
201
- url = {https://github.com/M4i-Imaging-Mass-Spectrometry/thyra}
202
- }
203
- ```
188
+ BibTeX for both is in [CITATION.bib](CITATION.bib). GitHub's "Cite this
189
+ repository" button reads [CITATION.cff](CITATION.cff), which shows the Nature
190
+ Methods paper and lists the JASMS paper under its references.
204
191
 
205
192
  ### Papers
206
193
 
207
194
  | Paper | Published | DOI |
208
195
  |---|---|---|
196
+ | Thyra: bridging mass spectrometry imaging and SpatialData for unified multimodal analysis (*Nat. Methods*) | 2026-10 | [10.1038/s41592-026-03305-5](https://doi.org/10.1038/s41592-026-03305-5) |
209
197
  | Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the SpatialData Ecosystem (*J. Am. Soc. Mass Spectrom.*) | 2026-09 | [10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169) |
210
198
  | Thyra: Bridging Mass Spectrometry Imaging and SpatialData for Unified Multi-Modal Analysis (bioRxiv preprint) | 2026-01 | [10.64898/2026.01.26.700792](https://doi.org/10.64898/2026.01.26.700792) |
211
199
 
212
- Ion mobility and MS/MS support came after both papers. If you use them,
200
+ Ion mobility and MS/MS support came after the JASMS paper and the preprint. If you use them,
213
201
  please also name the Thyra version you used.
214
202
 
215
203
  ## Acknowledgments
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thyra"
7
- version = "4.6.0"
7
+ version = "4.6.2"
8
8
  description = "A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics"
9
9
  authors = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
10
10
  maintainers = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
@@ -241,14 +241,15 @@ dependencies = [
241
241
  # Floor is scikit-image's own, which is also comfortably above the 9.5
242
242
  # that `Image.has_transparency_data` needs; no upper bound, nothing here
243
243
  # has tested one.
244
- "pillow>=10.1", # tested 12.3.0
244
+ "pillow>=10.1", # tested 13.0.0
245
245
  ]
246
246
 
247
247
  [project.urls]
248
248
  Homepage = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra"
249
249
  Repository = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra"
250
250
  Documentation = "https://M4i-Imaging-Mass-Spectrometry.github.io/thyra/"
251
- Paper = "https://doi.org/10.1021/jasms.6c00169"
251
+ Paper = "https://doi.org/10.1038/s41592-026-03305-5"
252
+ "Paper (JASMS)" = "https://doi.org/10.1021/jasms.6c00169"
252
253
  "Bug Tracker" = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/issues"
253
254
  Discussions = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/discussions"
254
255
  Changelog = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CHANGELOG.md"
@@ -70,7 +70,7 @@ warnings.filterwarnings(
70
70
  category=FutureWarning,
71
71
  )
72
72
 
73
- __version__ = "4.6.0"
73
+ __version__ = "4.6.2"
74
74
 
75
75
  #: Public name -> the module that defines it, imported on first access.
76
76
  _LAZY_ATTRS = {
@@ -43,6 +43,7 @@ log capture, which is the consumer that met it (issue #249).
43
43
  import asyncio
44
44
  import errno
45
45
  import logging
46
+ import math
46
47
  import warnings
47
48
  from abc import ABC, abstractmethod
48
49
  from contextlib import contextmanager
@@ -637,6 +638,65 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
637
638
  """Apply :meth:`build_uns_metadata` to an AnnData about to be written."""
638
639
  self.uns.apply(adata, self._uns_context())
639
640
 
641
+ def _refuse_a_pitch_past_float(self) -> None:
642
+ """Refuse a pitch whose product with the grid is not a finite number.
643
+
644
+ A finite pitch passes every earlier check, but ``1e308`` um times
645
+ the third pixel is infinity: ``obs["spatial_x"]``, the pixel boxes
646
+ and the affines then held ``inf`` at exit 0, and ``thyra validate``
647
+ passed the store (issue #527). Checked once the grid and every
648
+ pitch are settled, so a detected pitch and a stated one, 2D and 3D,
649
+ all meet it. The far edge is the largest index plus half a pixel,
650
+ counted from the source's own origin, which the store also scales.
651
+ """
652
+ assert self._dimensions is not None
653
+ offsets = tuple(self._source_coordinate_offsets or (0, 0, 0)) + (0, 0, 0)
654
+ axes = [
655
+ ("x", "pixel size", self.pixel_size_um, 0),
656
+ ("y", "pixel size", self.pixel_size_y_um, 1),
657
+ ]
658
+ if self.handle_3d:
659
+ axes.append(("z", "z spacing", self.z_spacing_um, 2))
660
+ for axis, name, pitch, i in axes:
661
+ n = int(self._dimensions[i])
662
+ edge = (abs(float(offsets[i])) + n + 0.5) * float(pitch)
663
+ if not math.isfinite(edge):
664
+ raise ConversionRefused(
665
+ f"A {name} of {pitch:g} um across {n:,} pixels along {axis} "
666
+ "places the far edge of the image past the largest number "
667
+ "a float holds. Give a pixel size in micrometres "
668
+ "(--pixel-size, --z-spacing)."
669
+ )
670
+
671
+ def _refuse_a_grid_past_memory(self) -> None:
672
+ """Refuse a pixel grid whose per-position arrays will not fit.
673
+
674
+ Asked before the mass axis and the tables, so a grid of 10**12
675
+ positions for two spectra is refused in one line rather than found
676
+ by ``np.zeros`` (issue #527). Every plane is counted, as the
677
+ per-plane route may hold them all at once. The free memory read
678
+ here is the one the mass-axis check compares with too.
679
+
680
+ Raises:
681
+ ConversionRefused: When the projection exceeds the fraction of
682
+ free memory a conversion may take.
683
+ """
684
+ from ...resampling.memory_guard import available_memory_gb, pixel_grid_refusal
685
+
686
+ assert self._dimensions is not None
687
+ free = getattr(self, "_axis_free_memory_gb", None)
688
+ if free is None:
689
+ free = available_memory_gb()
690
+ self._axis_free_memory_gb = free
691
+ refusal = pixel_grid_refusal(
692
+ self._dimensions,
693
+ int(self._n_spectra or 0),
694
+ bool(getattr(self.reader, "has_acquisition_order", False)),
695
+ available_gb=free,
696
+ )
697
+ if refusal is not None:
698
+ raise ConversionRefused(refusal)
699
+
640
700
  def _initialize_conversion(self) -> None:
641
701
  """Override parent initialization to preserve resampled mass axis."""
642
702
  logger.info("Loading essential dataset information...")
@@ -670,6 +730,8 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
670
730
 
671
731
  # After pixel size, because the fallback is the pixel size.
672
732
  self._resolve_z_spacing(essential)
733
+ self._refuse_a_pitch_past_float()
734
+ self._refuse_a_grid_past_memory()
673
735
 
674
736
  # Handle mass axis setup
675
737
  self._setup_mass_axis()
@@ -1021,7 +1083,7 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1021
1083
  )
1022
1084
  return unique.astype(indices.dtype, copy=False), summed
1023
1085
 
1024
- def build_region_numbers(self, x_values, y_values) -> NDArray[np.int32]:
1086
+ def build_region_numbers(self, x_values, y_values) -> NDArray[np.int64]:
1025
1087
  """``obs["region_number"]`` for the given pixel positions, in row order.
1026
1088
 
1027
1089
  Written on every dataset for a consistent schema, so a consumer
@@ -1043,16 +1105,18 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1043
1105
  y_values: Pixel y index per obs row.
1044
1106
 
1045
1107
  Returns:
1046
- Region number per obs row.
1108
+ Region number per obs row, as int64: a source may number its
1109
+ regions past 2**31, which an int32 column ended in an
1110
+ ``OverflowError`` (issue #527).
1047
1111
  """
1048
1112
  n = len(x_values)
1049
1113
  region_map = getattr(self, "_region_map", None)
1050
1114
  if region_map is None:
1051
- return np.ones(n, dtype=np.int32)
1115
+ return np.ones(n, dtype=np.int64)
1052
1116
 
1053
1117
  keys = zip(x_values.tolist(), y_values.tolist())
1054
1118
  return np.fromiter(
1055
- (region_map.get(key, -1) for key in keys), dtype=np.int32, count=n
1119
+ (region_map.get(key, -1) for key in keys), dtype=np.int64, count=n
1056
1120
  )
1057
1121
 
1058
1122
  def _create_mass_dataframe(self) -> pd.DataFrame:
@@ -0,0 +1,165 @@
1
+ """Position, index and region columns as whole numbers, or a refusal.
2
+
3
+ Vendor files state pixel positions, spectrum numbers and region numbers in
4
+ whatever type their writer chose: ``uint64`` in a Parquet column, ``REAL``
5
+ or text in a sqlite one, a float a writer computed as ``0.3 / 0.1``. Each
6
+ reader used to cast with ``astype(np.int64)`` or ``int()``, so a float just
7
+ below a whole number was cut down onto its neighbour's pixel, a ``uint64``
8
+ of ``2**63`` or more wrapped negative, and text or ``NULL`` ended in a
9
+ traceback (issue #527).
10
+
11
+ :func:`whole_numbers` is the one door they go through instead. A value is
12
+ accepted when it is a whole number within int64 once floating-point noise
13
+ is allowed for: a float within :data:`WHOLE_TOLERANCE` of a whole number is
14
+ that number (``2.9999999999999996`` is 3), anything further off is not a
15
+ position. Everything else is refused in one line that names the column,
16
+ how many values fail and the first of them.
17
+
18
+ The tolerance is for a column whose type is a float, as mzPeak allows for
19
+ positions: there a computed value carries writer noise. A vendor sqlite
20
+ column declared INTEGER that holds ``100.9999999`` is damage, not noise,
21
+ and its readers pass ``tolerance=0``.
22
+ """
23
+
24
+ import numbers
25
+ from typing import Any, Optional
26
+
27
+ import numpy as np
28
+ from numpy.typing import NDArray
29
+
30
+ from ..errors import ConversionRefused
31
+
32
+ __all__ = ["WHOLE_TOLERANCE", "whole_number", "whole_numbers", "whole_sql"]
33
+
34
+ #: How far a float may sit from a whole number and still be that number.
35
+ #: Writer noise (``0.3 / 0.1``) is ~1e-15 of a pixel; a value 1e-6 away
36
+ #: is still that pixel by any reading, and 0.5 away is no pixel at all.
37
+ WHOLE_TOLERANCE = 1e-6
38
+
39
+ _INT64_MAX = np.iinfo(np.int64).max
40
+ _INT64_MIN = np.iinfo(np.int64).min
41
+
42
+
43
+ def _refuse(what: str, source: Any, n_bad: int, n_all: int, first: Any) -> None:
44
+ where = f" in {source}" if source is not None else ""
45
+ raise ConversionRefused(
46
+ f"{n_bad} of {n_all} {what}{where} are not whole numbers within "
47
+ f"int64 (the first is {first!r}), so no pixel can be placed from them"
48
+ )
49
+
50
+
51
+ def _from_objects(
52
+ values: NDArray[Any], what: str, source: Any, tolerance: float
53
+ ) -> NDArray[np.int64]:
54
+ """Python objects (a sqlite column): ints, floats, or something else."""
55
+ out = np.empty(values.size, dtype=np.int64)
56
+ bad = []
57
+ for i, value in enumerate(values.tolist()):
58
+ number: Optional[int] = None
59
+ if isinstance(value, bool):
60
+ number = None
61
+ elif isinstance(value, numbers.Integral):
62
+ number = int(value)
63
+ elif isinstance(value, numbers.Real):
64
+ number = _whole_float(float(value), tolerance)
65
+ if number is None or not _INT64_MIN <= number <= _INT64_MAX:
66
+ bad.append(value)
67
+ continue
68
+ out[i] = number
69
+ if bad:
70
+ _refuse(what, source, len(bad), values.size, bad[0])
71
+ return out
72
+
73
+
74
+ def _whole_float(value: float, tolerance: float) -> Optional[int]:
75
+ if not np.isfinite(value):
76
+ return None
77
+ nearest = round(value)
78
+ if abs(value - nearest) > tolerance:
79
+ return None
80
+ return int(nearest)
81
+
82
+
83
+ def whole_number(value: Any) -> Optional[int]:
84
+ """One value of a column declared INTEGER, or ``None`` when it is not whole.
85
+
86
+ For a reader that skips a bad record rather than refusing the file: an
87
+ ``int``, or a float with no fraction, within int64. Text, ``None``, a
88
+ bool and a float with any fraction at all give ``None``.
89
+ """
90
+ if isinstance(value, bool):
91
+ return None
92
+ if isinstance(value, numbers.Integral):
93
+ number = int(value)
94
+ elif isinstance(value, numbers.Real):
95
+ found = _whole_float(float(value), 0.0)
96
+ if found is None:
97
+ return None
98
+ number = found
99
+ else:
100
+ return None
101
+ return number if _INT64_MIN <= number <= _INT64_MAX else None
102
+
103
+
104
+ def whole_sql(column: str) -> str:
105
+ """SQL that holds where a sqlite ``column`` stores a whole number.
106
+
107
+ An INTEGER, or a REAL with no fraction; text, ``NULL``, a REAL with a
108
+ fraction and a REAL past int64 fail it, the last because ``CAST``
109
+ saturates. The same rule as :func:`whole_number`, for a query that
110
+ should leave such rows out.
111
+ """
112
+ return f"({column} = CAST({column} AS INTEGER))"
113
+
114
+
115
+ def whole_numbers(
116
+ values: Any, what: str, source: Any = None, tolerance: float = WHOLE_TOLERANCE
117
+ ) -> NDArray[np.int64]:
118
+ """``values`` as int64 whole numbers, or a refusal naming the first bad one.
119
+
120
+ Args:
121
+ values: A column of positions, indices or region numbers: any numpy
122
+ array or sequence, of any integer, float or object dtype.
123
+ what: What the values are, plural, as it should read mid-sentence --
124
+ ``"x positions"``, ``"spectrum indices"``.
125
+ source: The file or archive, named in the refusal.
126
+ tolerance: How far a float may sit from a whole number and still
127
+ be read as it; 0 for a column declared as an integer.
128
+
129
+ Returns:
130
+ The values as an int64 array of the same length.
131
+
132
+ Raises:
133
+ ConversionRefused: If a value is not a whole number within int64:
134
+ ``NULL``, text, ``nan``, ``inf``, a float further than
135
+ :data:`WHOLE_TOLERANCE` from a whole number, or an unsigned
136
+ value of ``2**63`` or more.
137
+ """
138
+ array = np.asarray(values)
139
+ if array.ndim != 1:
140
+ array = array.reshape(-1)
141
+ if array.size == 0:
142
+ return array.astype(np.int64)
143
+ kind = array.dtype.kind
144
+ if kind == "b":
145
+ _refuse(what, source, array.size, array.size, array.flat[0])
146
+ if kind == "i":
147
+ return array.astype(np.int64, copy=False)
148
+ if kind == "u":
149
+ over = np.nonzero(array > np.uint64(_INT64_MAX))[0]
150
+ if over.size:
151
+ _refuse(what, source, over.size, array.size, int(array[over[0]]))
152
+ return array.astype(np.int64)
153
+ if kind == "f":
154
+ as_float = array.astype(np.float64, copy=False)
155
+ nearest = np.rint(as_float)
156
+ with np.errstate(invalid="ignore"):
157
+ # 2**63 is exactly representable and already out of range;
158
+ # every float below it in magnitude fits.
159
+ bad = ~np.isfinite(as_float) | (np.abs(as_float - nearest) > tolerance)
160
+ bad |= (nearest >= 2.0**63) | (nearest < -(2.0**63))
161
+ if bad.any():
162
+ first = np.nonzero(bad)[0][0]
163
+ _refuse(what, source, int(bad.sum()), array.size, float(as_float[first]))
164
+ return nearest.astype(np.int64)
165
+ return _from_objects(array, what, source, tolerance)
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Any, Dict, List, Optional, Tuple
7
7
 
8
8
  from ...core.base_extractor import MetadataExtractor
9
+ from ...core.positions import whole_sql
9
10
  from ...errors import ConversionRefused
10
11
  from ..types import ComprehensiveMetadata, EssentialMetadata
11
12
 
@@ -30,6 +31,19 @@ _CALIBRATION_INFO_TEXT = (
30
31
  )
31
32
 
32
33
 
34
+ def _mismatch_reading(raster: Tuple[float, float], beam: Tuple[float, float]) -> str:
35
+ """What a Raster step that differs from BeamScanSize says, by direction.
36
+
37
+ Oversampled only when the beam scans more than one step, on some axis
38
+ and on none less; a step larger than the beam leaves gaps between spots.
39
+ """
40
+ if all(b >= r for r, b in zip(raster, beam)):
41
+ return "The beam scan is larger than the step: the acquisition is oversampled."
42
+ if all(b <= r for r, b in zip(raster, beam)):
43
+ return "The beam scan is smaller than the step: the spots leave gaps."
44
+ return "The beam scan is larger than the step on one axis and smaller on the other."
45
+
46
+
33
47
  def _float64_count(blob: Any) -> Optional[int]:
34
48
  """How many little-endian float64 values a ``CalibrationInfo`` blob holds."""
35
49
  if not isinstance(blob, (bytes, bytearray)) or len(blob) % 8:
@@ -298,26 +312,30 @@ class BrukerMetadataExtractor(MetadataExtractor):
298
312
 
299
313
  When a region is selected, only frames from that region are
300
314
  included in the bounds and count.
315
+
316
+ The bounds come from the frames whose position and region number
317
+ are whole numbers: the reader skips the others, and a text ``MAX`` ended in ``int()``
318
+ while a REAL just below a whole number was cut down (issue #527).
319
+ The count stays every frame, as it was.
301
320
  """
321
+ whole = " AND ".join(
322
+ whole_sql(column) for column in ("XIndexPos", "YIndexPos", "RegionNumber")
323
+ )
324
+ bounds = (
325
+ f"MIN(CASE WHEN {whole} THEN XIndexPos END), "
326
+ f"MAX(CASE WHEN {whole} THEN XIndexPos END), "
327
+ f"MIN(CASE WHEN {whole} THEN YIndexPos END), "
328
+ f"MAX(CASE WHEN {whole} THEN YIndexPos END), "
329
+ "COUNT(*) as frame_count"
330
+ )
302
331
  if self._region is not None:
303
- frame_query = """
304
- SELECT
305
- MIN(XIndexPos), MAX(XIndexPos),
306
- MIN(YIndexPos), MAX(YIndexPos),
307
- COUNT(*) as frame_count
308
- FROM MaldiFrameInfo
309
- WHERE RegionNumber = ?
310
- """
311
- cursor.execute(frame_query, (self._region,))
332
+ where = " WHERE RegionNumber = ?"
333
+ cursor.execute(
334
+ f"SELECT {bounds} FROM MaldiFrameInfo{where}", # nosec B608
335
+ (self._region,),
336
+ )
312
337
  else:
313
- frame_query = """
314
- SELECT
315
- MIN(XIndexPos), MAX(XIndexPos),
316
- MIN(YIndexPos), MAX(YIndexPos),
317
- COUNT(*) as frame_count
318
- FROM MaldiFrameInfo
319
- """
320
- cursor.execute(frame_query)
338
+ cursor.execute(f"SELECT {bounds} FROM MaldiFrameInfo") # nosec B608
321
339
  return cursor.fetchone()
322
340
 
323
341
  def _query_frame_count(self, cursor) -> int:
@@ -581,6 +599,33 @@ class BrukerMetadataExtractor(MetadataExtractor):
581
599
  mis_path = find_mis_file_for_d_folder(self.data_path)
582
600
  return parse_mis_file(mis_path) if mis_path is not None else None
583
601
 
602
+ def _beam_scan_size(
603
+ self, beam_x: object, beam_y: object
604
+ ) -> Optional[Tuple[float, float]]:
605
+ """The beam scan size as two finite numbers above zero, or ``None``.
606
+
607
+ sqlite stores whatever was written, so a column declared REAL can
608
+ hold ``'20 um'``. A value that is not a number is treated like a
609
+ missing one, with a warning naming it, instead of ending the
610
+ conversion in ``float()`` even when ``--pixel-size`` was given.
611
+ """
612
+ if not beam_x or not beam_y:
613
+ return None
614
+ try:
615
+ beam = (float(beam_x), float(beam_y)) # type: ignore[arg-type]
616
+ except (TypeError, ValueError):
617
+ beam = (math.nan, math.nan)
618
+ if all(math.isfinite(v) and v > 0 for v in beam):
619
+ return beam
620
+ logger.warning(
621
+ "MaldiFrameLaserInfo BeamScanSize (%r, %r) in %s is not two numbers "
622
+ "above zero, so it is treated as missing",
623
+ beam_x,
624
+ beam_y,
625
+ self.data_path.name,
626
+ )
627
+ return None
628
+
584
629
  def _resolve_pixel_size_um(
585
630
  self,
586
631
  beam_x: Optional[float],
@@ -612,20 +657,19 @@ class BrukerMetadataExtractor(MetadataExtractor):
612
657
  ):
613
658
  raster = (float(raster_xy[0]), float(raster_xy[1]))
614
659
 
615
- beam: Optional[Tuple[float, float]] = (
616
- (float(beam_x), float(beam_y)) if beam_x and beam_y else None
617
- )
660
+ beam = self._beam_scan_size(beam_x, beam_y)
618
661
 
619
662
  if raster is not None and beam is not None:
620
663
  if raster != beam:
621
664
  logger.warning(
622
665
  "Pixel size mismatch: .mis Raster=(%g, %g) um, "
623
- "BeamScanSize=(%g, %g) um. Acquisition is oversampled. "
666
+ "BeamScanSize=(%g, %g) um. %s "
624
667
  "Using Raster step. Override with --pixel-size if needed.",
625
668
  raster[0],
626
669
  raster[1],
627
670
  beam[0],
628
671
  beam[1],
672
+ _mismatch_reading(raster, beam),
629
673
  )
630
674
  return raster
631
675