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.
- {thyra-4.6.0 → thyra-4.6.2}/PKG-INFO +17 -28
- {thyra-4.6.0 → thyra-4.6.2}/README.md +14 -26
- {thyra-4.6.0 → thyra-4.6.2}/pyproject.toml +4 -3
- {thyra-4.6.0 → thyra-4.6.2}/thyra/__init__.py +1 -1
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/base_spatialdata_converter.py +68 -4
- thyra-4.6.2/thyra/core/positions.py +165 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/bruker_extractor.py +65 -21
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +93 -20
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/solarix/solarix_reader.py +60 -33
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/timstof_reader.py +83 -32
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/imzml_reader.py +18 -9
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/chunk_decoding.py +21 -1
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/mzpeak_reader.py +53 -14
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/event_stream.py +11 -1
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/mass_axis.py +8 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/phi_header.py +10 -2
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/phi_reader.py +39 -7
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/imaging_grid.py +33 -4
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/waters_reader.py +111 -7
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/memory_guard.py +58 -1
- {thyra-4.6.0 → thyra-4.6.2}/.gitignore +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/LICENSE +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/__main__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/affine.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/config.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/convert.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/acquisition_frame.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/optical_image.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/root_attrs.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_converter.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/base_reader.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/conversion_state.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/drop_tally.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/frames.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/mass_axis.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/mobility.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/msms.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/region_map.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/core/registry.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/errors.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/mass_axis_plan.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/constants.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/document.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/imzml_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/mzpeak_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/axis_fingerprint.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/msi_metadata_schema_v0_14.json +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/types.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/metadata/validator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/preview.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/py.typed +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/reader_options.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/lockmass.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/regions.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/binning.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/constants.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/gaps.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/plan.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/tic.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/resampling/types.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/__init__.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/imzml_coordinate_base.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/imzml_spectrum_terms.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/logging_config.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/ms_levels.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/output_files.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.6.0 → thyra-4.6.2}/thyra/utils/store_lifecycle.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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"
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|