thyra 4.3.4__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thyra-4.3.4 → thyra-4.4.0}/PKG-INFO +22 -1
- {thyra-4.3.4 → thyra-4.4.0}/README.md +20 -0
- {thyra-4.3.4 → thyra-4.4.0}/pyproject.toml +2 -1
- {thyra-4.3.4 → thyra-4.4.0}/thyra/__init__.py +1 -1
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/base_spatialdata_converter.py +2 -2
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/optical_image.py +13 -1
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/root_attrs.py +13 -11
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/streaming_converter.py +20 -2
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/uns_assembler.py +6 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/base_reader.py +74 -3
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/constants.py +35 -1
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/imzml_extractor.py +64 -2
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/imzml_header_extractor.py +10 -1
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/mzpeak_extractor.py +120 -3
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/timstof_reader.py +38 -10
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/imzml/header.py +20 -9
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/imzml/imzml_reader.py +48 -16
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/mzpeak/chunk_decoding.py +108 -21
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/mzpeak/mzpeak_reader.py +526 -19
- {thyra-4.3.4 → thyra-4.4.0}/.gitignore +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/LICENSE +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/__main__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/alignment/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/alignment/affine.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/config.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/convert.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/base_converter.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/base_extractor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/conversion_state.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/drop_tally.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/frames.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/mass_axis.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/mobility.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/msms.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/core/registry.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/errors.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/document.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/msi_metadata_schema_v0_10.json +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/types.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/metadata/validator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/preview.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/py.typed +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/solarix/solarix_reader.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/phi/phi_reader.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/binning.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/constants.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/gaps.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/tic.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/resampling/types.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/tools/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/__init__.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/imzml_coordinate_base.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/logging_config.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.3.4 → thyra-4.4.0}/thyra/utils/windows_paths.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thyra
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
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
9
|
Project-URL: Bug Tracker, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/issues
|
|
9
10
|
Project-URL: Discussions, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/discussions
|
|
10
11
|
Project-URL: Changelog, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CHANGELOG.md
|
|
@@ -172,6 +173,26 @@ MIT -- see [LICENSE](LICENSE).
|
|
|
172
173
|
|
|
173
174
|
## Citation
|
|
174
175
|
|
|
176
|
+
If you use Thyra, please cite the paper:
|
|
177
|
+
|
|
178
|
+
> Visvikis, T.; Vierdag, W.-M.; Marconato, L.; Birmpili, A.; Heeren, R. M. A.;
|
|
179
|
+
> Cuypers, E. Native, Memory-Bounded Conversion of Mass Spectrometry Imaging
|
|
180
|
+
> Data into the SpatialData Ecosystem. *J. Am. Soc. Mass Spectrom.* **2026**.
|
|
181
|
+
> [doi:10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169)
|
|
182
|
+
|
|
183
|
+
```bibtex
|
|
184
|
+
@article{visvikis2026thyra,
|
|
185
|
+
title = {Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the {SpatialData} Ecosystem},
|
|
186
|
+
author = {Visvikis, Theodoros and Vierdag, Wouter-Michiel and Marconato, Luca and Birmpili, Angeliki and Heeren, Ron M. A. and Cuypers, Eva},
|
|
187
|
+
journal = {Journal of the American Society for Mass Spectrometry},
|
|
188
|
+
year = {2026},
|
|
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
|
+
|
|
175
196
|
```bibtex
|
|
176
197
|
@software{thyra2024,
|
|
177
198
|
title = {Thyra: Modern Mass Spectrometry Imaging Data Conversion},
|
|
@@ -119,6 +119,26 @@ MIT -- see [LICENSE](LICENSE).
|
|
|
119
119
|
|
|
120
120
|
## Citation
|
|
121
121
|
|
|
122
|
+
If you use Thyra, please cite the paper:
|
|
123
|
+
|
|
124
|
+
> Visvikis, T.; Vierdag, W.-M.; Marconato, L.; Birmpili, A.; Heeren, R. M. A.;
|
|
125
|
+
> Cuypers, E. Native, Memory-Bounded Conversion of Mass Spectrometry Imaging
|
|
126
|
+
> Data into the SpatialData Ecosystem. *J. Am. Soc. Mass Spectrom.* **2026**.
|
|
127
|
+
> [doi:10.1021/jasms.6c00169](https://doi.org/10.1021/jasms.6c00169)
|
|
128
|
+
|
|
129
|
+
```bibtex
|
|
130
|
+
@article{visvikis2026thyra,
|
|
131
|
+
title = {Native, Memory-Bounded Conversion of Mass Spectrometry Imaging Data into the {SpatialData} Ecosystem},
|
|
132
|
+
author = {Visvikis, Theodoros and Vierdag, Wouter-Michiel and Marconato, Luca and Birmpili, Angeliki and Heeren, Ron M. A. and Cuypers, Eva},
|
|
133
|
+
journal = {Journal of the American Society for Mass Spectrometry},
|
|
134
|
+
year = {2026},
|
|
135
|
+
doi = {10.1021/jasms.6c00169}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The same entry is in [CITATION.cff](CITATION.cff), which GitHub's "Cite this
|
|
140
|
+
repository" button reads. To cite the software itself:
|
|
141
|
+
|
|
122
142
|
```bibtex
|
|
123
143
|
@software{thyra2024,
|
|
124
144
|
title = {Thyra: Modern Mass Spectrometry Imaging Data Conversion},
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.4.0"
|
|
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" }]
|
|
@@ -243,6 +243,7 @@ dependencies = [
|
|
|
243
243
|
Homepage = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra"
|
|
244
244
|
Repository = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra"
|
|
245
245
|
Documentation = "https://M4i-Imaging-Mass-Spectrometry.github.io/thyra/"
|
|
246
|
+
Paper = "https://doi.org/10.1021/jasms.6c00169"
|
|
246
247
|
"Bug Tracker" = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/issues"
|
|
247
248
|
Discussions = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/discussions"
|
|
248
249
|
Changelog = "https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CHANGELOG.md"
|
|
@@ -892,7 +892,7 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
892
892
|
Otherwise, shapes use physical (micrometer) coordinates.
|
|
893
893
|
|
|
894
894
|
**Footprints are two-dimensional, including on a multi-slice
|
|
895
|
-
volume.** A slice's depth is carried by the TIC image's
|
|
895
|
+
volume.** A slice's depth is carried by the TIC image's transform
|
|
896
896
|
and by ``obs["spatial_z"]``; it is deliberately not also put on
|
|
897
897
|
the polygon geometry.
|
|
898
898
|
|
|
@@ -1042,7 +1042,7 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
1042
1042
|
half_y_um = self.pixel_size_y_um / 2
|
|
1043
1043
|
|
|
1044
1044
|
# Footprints are flat, on every route including volumes. A
|
|
1045
|
-
# slice's depth lives on the TIC image's
|
|
1045
|
+
# slice's depth lives on the TIC image's transform and in
|
|
1046
1046
|
# obs["spatial_z"]; see the docstring for why it is not also
|
|
1047
1047
|
# put on the geometry. Built through shapely's vectorised box
|
|
1048
1048
|
# constructor -- one C call for the whole table instead of one
|
|
@@ -1183,6 +1183,18 @@ class OpticalImages:
|
|
|
1183
1183
|
transformation that maps MSI raster coordinates to optical image
|
|
1184
1184
|
pixel coordinates.
|
|
1185
1185
|
"""
|
|
1186
|
+
# A source that states its own registration (an mzPeak archive with
|
|
1187
|
+
# a teach_points affine) hands it over whole.
|
|
1188
|
+
stated = getattr(self.reader, "get_image_alignment", None)
|
|
1189
|
+
alignment = stated() if callable(stated) else None
|
|
1190
|
+
if alignment is not None:
|
|
1191
|
+
self._alignment = alignment
|
|
1192
|
+
logger.info(
|
|
1193
|
+
"Optical alignment stated by the source, with "
|
|
1194
|
+
f"{len(alignment.region_mappings)} region mappings"
|
|
1195
|
+
)
|
|
1196
|
+
return
|
|
1197
|
+
|
|
1186
1198
|
# Check if reader has FlexImaging-specific metadata
|
|
1187
1199
|
if not hasattr(self.reader, "mis_metadata"):
|
|
1188
1200
|
logger.debug("Reader does not have mis_metadata, skipping alignment")
|
|
@@ -1399,7 +1411,7 @@ class OpticalImages:
|
|
|
1399
1411
|
primary = resolve() if callable(resolve) else None
|
|
1400
1412
|
if primary is not None:
|
|
1401
1413
|
self.primary_filename = Path(primary).stem.lower()
|
|
1402
|
-
logger.info(f"Primary alignment image
|
|
1414
|
+
logger.info(f"Primary alignment image named by the reader: {primary.name}")
|
|
1403
1415
|
|
|
1404
1416
|
def _is_primary_optical(self, image_path: Path) -> bool:
|
|
1405
1417
|
"""Check if an image file is the primary alignment image from .mis.
|
|
@@ -232,12 +232,13 @@ class RootAttrsBuilder:
|
|
|
232
232
|
Two variants are emitted depending on whether FlexImaging optical
|
|
233
233
|
alignment was applied during conversion:
|
|
234
234
|
|
|
235
|
-
- No alignment (``global = micrometer``): the TIC image carries
|
|
236
|
-
``Scale(pixel_size_um)
|
|
237
|
-
|
|
238
|
-
``
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
- No alignment (``global = micrometer``): the TIC image carries
|
|
236
|
+
a half-cell ``Translation`` then ``Scale(pixel_size_um)``, and
|
|
237
|
+
pixel-polygon shapes are stored in micrometers with
|
|
238
|
+
``Identity``. Both elements agree at ``global``.
|
|
239
|
+
``pixel_size_um_x/y`` are filled with the MSI grid pixel size,
|
|
240
|
+
since "global" is in physical micrometers and there is no
|
|
241
|
+
canonical raster image other than the MSI itself.
|
|
241
242
|
|
|
242
243
|
- With alignment (``global = pixel``): the TIC image carries an
|
|
243
244
|
``Affine`` mapping raster indices to optical-image pixels and
|
|
@@ -311,9 +312,10 @@ class RootAttrsBuilder:
|
|
|
311
312
|
# the explicit 3x3 row-major affine from TIC raster indices to
|
|
312
313
|
# "global" (the same mapping the TIC element's transform
|
|
313
314
|
# expresses), so a consumer that reads only attrs still gets
|
|
314
|
-
# the full placement. In
|
|
315
|
-
#
|
|
316
|
-
#
|
|
315
|
+
# the full placement. In both variants it takes TIC coordinates,
|
|
316
|
+
# where cell i spans [i, i + 1): its centre i + 0.5 lands on the
|
|
317
|
+
# spot (D31), or on obs["spatial_x"] = i * px in micrometres
|
|
318
|
+
# (D32). `coordinate_offsets_px` preserves the
|
|
317
319
|
# source's raw acquisition-index offsets, which 0-based
|
|
318
320
|
# normalisation otherwise erases; `stage_offset_um` is their
|
|
319
321
|
# physical equivalent, written only when "global" is in
|
|
@@ -325,8 +327,8 @@ class RootAttrsBuilder:
|
|
|
325
327
|
]
|
|
326
328
|
else:
|
|
327
329
|
global_cs["raster_to_global_affine"] = [
|
|
328
|
-
[px, 0.0, 0.
|
|
329
|
-
[0.0, py, 0.
|
|
330
|
+
[px, 0.0, -0.5 * px],
|
|
331
|
+
[0.0, py, -0.5 * py],
|
|
330
332
|
[0.0, 0.0, 1.0],
|
|
331
333
|
]
|
|
332
334
|
offsets = self._source_coordinate_offsets()
|
|
@@ -47,6 +47,8 @@ from anndata import AnnData
|
|
|
47
47
|
from numpy.typing import NDArray
|
|
48
48
|
from spatialdata.models import Image2DModel, Image3DModel, TableModel
|
|
49
49
|
from spatialdata.transformations import Affine, Scale
|
|
50
|
+
from spatialdata.transformations import Sequence as SequenceTransform
|
|
51
|
+
from spatialdata.transformations import Translation
|
|
50
52
|
from tqdm import tqdm
|
|
51
53
|
|
|
52
54
|
from ...core.conversion_state import ConversionState
|
|
@@ -62,6 +64,17 @@ logger = logging.getLogger(__name__)
|
|
|
62
64
|
_NOT_ACQUIRED = np.iinfo(np.int64).max
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
def _cell_centred_scale(pitches: List[float], axes: Tuple[str, ...]) -> Any:
|
|
68
|
+
"""Raster cells to micrometres, each cell centred on its own position.
|
|
69
|
+
|
|
70
|
+
Cell ``i`` spans ``[i, i + 1)``; moving it back half a cell first puts
|
|
71
|
+
its centre on ``i * pitch``, the position ``obs`` records (D32).
|
|
72
|
+
"""
|
|
73
|
+
return SequenceTransform(
|
|
74
|
+
[Translation([-0.5] * len(axes), axes=axes), Scale(pitches, axes=axes)]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
65
78
|
class _TableUnit:
|
|
66
79
|
"""One table of the store in the making: a z plane, or the whole volume.
|
|
67
80
|
|
|
@@ -1127,6 +1140,11 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
1127
1140
|
frame -- optical-image pixels when FlexImaging alignment is
|
|
1128
1141
|
applied, physical micrometres otherwise, so that ``"global"``
|
|
1129
1142
|
agrees with the pixel-polygon shapes.
|
|
1143
|
+
|
|
1144
|
+
SpatialData draws cell ``i`` over ``[i, i + 1)``. In micrometres
|
|
1145
|
+
the cell is moved back half a step before it is scaled, so its
|
|
1146
|
+
centre lands on ``obs["spatial_x"] = i * pixel_size_um``, where
|
|
1147
|
+
the pixel's polygon is centred (D32).
|
|
1130
1148
|
"""
|
|
1131
1149
|
n_z = self._dimensions[2]
|
|
1132
1150
|
if unit.plane is None and n_z > 1:
|
|
@@ -1134,7 +1152,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
1134
1152
|
# spacing (see BaseMSIConverter._resolve_z_spacing). Scale
|
|
1135
1153
|
# pairs values with axis *names*, so ("x", "y", "z") against
|
|
1136
1154
|
# a (c, z, y, x) image is deliberate.
|
|
1137
|
-
transform: Any =
|
|
1155
|
+
transform: Any = _cell_centred_scale(
|
|
1138
1156
|
[self.pixel_size_um, self.pixel_size_y_um, self.z_spacing_um],
|
|
1139
1157
|
axes=("x", "y", "z"),
|
|
1140
1158
|
)
|
|
@@ -1155,7 +1173,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
1155
1173
|
output_axes=("x", "y"),
|
|
1156
1174
|
)
|
|
1157
1175
|
else:
|
|
1158
|
-
transform =
|
|
1176
|
+
transform = _cell_centred_scale(
|
|
1159
1177
|
[self.pixel_size_um, self.pixel_size_y_um], axes=("x", "y")
|
|
1160
1178
|
)
|
|
1161
1179
|
return Image2DModel.parse(
|
|
@@ -37,6 +37,7 @@ every CSC scratch memmap the finalize path's frames still hold (issue
|
|
|
37
37
|
|
|
38
38
|
import json
|
|
39
39
|
import logging
|
|
40
|
+
import numbers
|
|
40
41
|
from dataclasses import dataclass
|
|
41
42
|
from typing import (
|
|
42
43
|
TYPE_CHECKING,
|
|
@@ -548,6 +549,11 @@ class UnsAssembler:
|
|
|
548
549
|
and getattr(self.reader, "file_type", None) == "tdf"
|
|
549
550
|
):
|
|
550
551
|
conversion_parameters["tdf_spectrum"] = str(tdf_spectrum)
|
|
552
|
+
# Peaks below it are not in the store, so a store made with one is
|
|
553
|
+
# not the source's whole signal, and must say so (D34).
|
|
554
|
+
intensity_threshold = getattr(self.reader, "intensity_threshold", None)
|
|
555
|
+
if isinstance(intensity_threshold, numbers.Real):
|
|
556
|
+
conversion_parameters["intensity_threshold"] = float(intensity_threshold)
|
|
551
557
|
steps = [
|
|
552
558
|
ProcessingStep(
|
|
553
559
|
name="conversion",
|
|
@@ -69,6 +69,7 @@ from numpy.typing import NDArray
|
|
|
69
69
|
from ..metadata.types import ComprehensiveMetadata, EssentialMetadata
|
|
70
70
|
|
|
71
71
|
if TYPE_CHECKING:
|
|
72
|
+
from ..alignment import AreaAlignmentResult
|
|
72
73
|
from .base_extractor import MetadataExtractor
|
|
73
74
|
from .frames import FrameScans
|
|
74
75
|
from .mobility import MobilityAxis
|
|
@@ -78,6 +79,53 @@ from .mass_axis import validate_max_mass_axis_length
|
|
|
78
79
|
|
|
79
80
|
logger = logging.getLogger(__name__)
|
|
80
81
|
|
|
82
|
+
#: Two m/z of one spectrum this close, relative, are one peak listed twice.
|
|
83
|
+
#: A source that lists one point per mobility scan repeats a peak's m/z
|
|
84
|
+
#: (an archive's chunk bound can differ from the decoded value in the last
|
|
85
|
+
#: bit), while the closest distinct peaks of any analyser are some 1e-6
|
|
86
|
+
#: apart.
|
|
87
|
+
SAME_PEAK_RELATIVE = 1e-12
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def summed_peak_mask(
|
|
91
|
+
mzs: NDArray[np.float64], intensities: NDArray[np.float64], threshold: float
|
|
92
|
+
) -> NDArray[np.bool_]:
|
|
93
|
+
"""Which points belong to a peak whose summed intensity reaches ``threshold``.
|
|
94
|
+
|
|
95
|
+
The intensity threshold's one unit (D34): a peak's intensity summed over
|
|
96
|
+
every point that lists its m/z. A spectrum that lists each m/z once is
|
|
97
|
+
tested point by point, as before; one that lists a peak once per mobility
|
|
98
|
+
scan keeps or drops all of that peak's points together, so the threshold
|
|
99
|
+
acts on the peak the store holds, not on one scan's share of it.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
mzs: m/z of each point, in any order.
|
|
103
|
+
intensities: Intensity of each point.
|
|
104
|
+
threshold: The lowest summed intensity kept.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A mask over the points, in their order.
|
|
108
|
+
"""
|
|
109
|
+
if mzs.size < 2:
|
|
110
|
+
return intensities >= threshold
|
|
111
|
+
steps = np.diff(mzs)
|
|
112
|
+
if np.all(steps > SAME_PEAK_RELATIVE * np.abs(mzs[1:])):
|
|
113
|
+
# Strictly ascending with no repeat: every point is its own peak.
|
|
114
|
+
return intensities >= threshold
|
|
115
|
+
order = np.argsort(mzs, kind="stable")
|
|
116
|
+
ordered = mzs[order]
|
|
117
|
+
starts = np.concatenate(
|
|
118
|
+
([True], np.diff(ordered) > SAME_PEAK_RELATIVE * np.abs(ordered[1:]))
|
|
119
|
+
)
|
|
120
|
+
# argsort puts NaN last; each NaN point is its own peak, never part of
|
|
121
|
+
# the largest real one.
|
|
122
|
+
starts |= np.isnan(ordered)
|
|
123
|
+
group = np.cumsum(starts) - 1
|
|
124
|
+
sums = np.bincount(group, weights=intensities[order])
|
|
125
|
+
keep = np.empty(mzs.size, dtype=bool)
|
|
126
|
+
keep[order] = sums[group] >= threshold
|
|
127
|
+
return keep
|
|
128
|
+
|
|
81
129
|
|
|
82
130
|
class OpticalImageLabel(NamedTuple):
|
|
83
131
|
"""What a store calls an optical image whose file name is not the source's.
|
|
@@ -194,6 +242,20 @@ class BaseMSIReader(ABC):
|
|
|
194
242
|
"""
|
|
195
243
|
return None
|
|
196
244
|
|
|
245
|
+
def get_image_alignment(self) -> Optional["AreaAlignmentResult"]:
|
|
246
|
+
"""The registration the source states for its alignment image.
|
|
247
|
+
|
|
248
|
+
Default implementation returns None. A format whose file states
|
|
249
|
+
where each pixel sits on its alignment image overrides it; the
|
|
250
|
+
converter then places the pixels by it, as it places a FlexImaging
|
|
251
|
+
run's by its ``.mis``. The image is the one
|
|
252
|
+
:meth:`get_primary_optical_image_path` names.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The alignment, or None when the source states none.
|
|
256
|
+
"""
|
|
257
|
+
return None
|
|
258
|
+
|
|
197
259
|
def get_optical_image_label(self, path: Path) -> Optional[OpticalImageLabel]:
|
|
198
260
|
"""Name an optical image this reader copied out of its source.
|
|
199
261
|
|
|
@@ -265,20 +327,29 @@ class BaseMSIReader(ABC):
|
|
|
265
327
|
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
266
328
|
"""Apply intensity threshold filtering to spectrum data.
|
|
267
329
|
|
|
330
|
+
A peak is kept when its intensity, summed over every point that
|
|
331
|
+
lists its m/z, reaches the threshold (:func:`summed_peak_mask`).
|
|
332
|
+
|
|
268
333
|
Args:
|
|
269
334
|
mzs: m/z values array
|
|
270
335
|
intensities: Intensity values array
|
|
271
336
|
|
|
272
337
|
Returns:
|
|
273
|
-
Tuple of (filtered_mzs, filtered_intensities) with
|
|
274
|
-
threshold removed. Returns original arrays if no
|
|
338
|
+
Tuple of (filtered_mzs, filtered_intensities) with the peaks
|
|
339
|
+
below the threshold removed. Returns original arrays if no
|
|
340
|
+
threshold is set.
|
|
275
341
|
"""
|
|
276
342
|
if self._intensity_threshold is None:
|
|
277
343
|
return mzs, intensities
|
|
278
344
|
|
|
279
|
-
mask = intensities
|
|
345
|
+
mask = summed_peak_mask(mzs, intensities, self._intensity_threshold)
|
|
280
346
|
return mzs[mask], intensities[mask]
|
|
281
347
|
|
|
348
|
+
@property
|
|
349
|
+
def intensity_threshold(self) -> Optional[float]:
|
|
350
|
+
"""The ``--intensity-threshold`` this reader applies, ``None`` for none."""
|
|
351
|
+
return self._intensity_threshold
|
|
352
|
+
|
|
282
353
|
@staticmethod
|
|
283
354
|
def map_mz_to_common_axis(
|
|
284
355
|
mzs: NDArray[np.float64],
|
|
@@ -15,7 +15,7 @@ module re-exports every name here, so both spellings keep working;
|
|
|
15
15
|
``Thresholds``, the one genuine resampling number in it, stayed there.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from typing import Dict, Optional
|
|
18
|
+
from typing import Dict, Iterable, Optional
|
|
19
19
|
|
|
20
20
|
from ..errors import ConversionRefused
|
|
21
21
|
|
|
@@ -27,6 +27,10 @@ class ImzMLAccessions:
|
|
|
27
27
|
CENTROID_SPECTRUM = "MS:1000127"
|
|
28
28
|
PROFILE_SPECTRUM = "MS:1000128"
|
|
29
29
|
|
|
30
|
+
# Scan polarity (MS ontology)
|
|
31
|
+
POSITIVE_SCAN = "MS:1000130"
|
|
32
|
+
NEGATIVE_SCAN = "MS:1000129"
|
|
33
|
+
|
|
30
34
|
# Binary data type (imzML ontology)
|
|
31
35
|
CONTINUOUS_BINARY = "IMS:1000030"
|
|
32
36
|
PROCESSED_BINARY = "IMS:1000031"
|
|
@@ -99,6 +103,36 @@ def normalize_spectrum_type(value: Optional[object]) -> Optional[str]:
|
|
|
99
103
|
)
|
|
100
104
|
|
|
101
105
|
|
|
106
|
+
#: The polarity each scan polarity term states.
|
|
107
|
+
POLARITY_OF_ACCESSION: Dict[str, str] = {
|
|
108
|
+
ImzMLAccessions.POSITIVE_SCAN: "positive",
|
|
109
|
+
ImzMLAccessions.NEGATIVE_SCAN: "negative",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#: What an extractor records for a source that states both polarities:
|
|
114
|
+
#: pyimzml's word for it. No polarity term matches it, so the document's
|
|
115
|
+
#: field stays unset, and the file-level terms cannot fill it instead.
|
|
116
|
+
MIXED_POLARITY = "mixed"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def agreed_polarity(stated: Iterable[str]) -> Optional[str]:
|
|
120
|
+
"""The polarity every statement of a source agrees on (D35).
|
|
121
|
+
|
|
122
|
+
``stated`` holds ``"positive"`` or ``"negative"`` once for each place
|
|
123
|
+
the source states one: its file-level terms, a parameter group, a
|
|
124
|
+
spectrum. One value is the answer, and None means the source says
|
|
125
|
+
nothing. Both give :data:`MIXED_POLARITY`: the source alternated or
|
|
126
|
+
contradicts itself, which has no single truthful value.
|
|
127
|
+
"""
|
|
128
|
+
values = set(stated)
|
|
129
|
+
if not values:
|
|
130
|
+
return None
|
|
131
|
+
if len(values) > 1:
|
|
132
|
+
return MIXED_POLARITY
|
|
133
|
+
return next(iter(values))
|
|
134
|
+
|
|
135
|
+
|
|
102
136
|
class BinaryDataType:
|
|
103
137
|
"""Binary data type string constants."""
|
|
104
138
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import gc
|
|
3
3
|
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
from numpy.typing import NDArray
|
|
@@ -13,9 +13,12 @@ from ...errors import ConversionRefused
|
|
|
13
13
|
from ...utils.imzml_coordinate_base import coordinate_bases, recorded_offsets
|
|
14
14
|
from ...utils.pyimzml_direct import read_spectrum_mzs_only
|
|
15
15
|
from ..constants import (
|
|
16
|
+
MIXED_POLARITY,
|
|
17
|
+
POLARITY_OF_ACCESSION,
|
|
16
18
|
BinaryDataType,
|
|
17
19
|
ImzMLAccessions,
|
|
18
20
|
SpectrumType,
|
|
21
|
+
agreed_polarity,
|
|
19
22
|
normalize_spectrum_type,
|
|
20
23
|
)
|
|
21
24
|
from ..ontology.cache import ONTOLOGY
|
|
@@ -911,7 +914,7 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
911
914
|
|
|
912
915
|
def _extract_acquisition_params(self) -> Dict[str, Any]:
|
|
913
916
|
"""Extract acquisition parameters from XML metadata."""
|
|
914
|
-
params = {}
|
|
917
|
+
params: Dict[str, Any] = {}
|
|
915
918
|
|
|
916
919
|
# Extract pixel size with full XML parsing if not found in fast
|
|
917
920
|
# extraction
|
|
@@ -921,6 +924,12 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
921
924
|
params["pixel_size_x_um"] = pixel_size[0]
|
|
922
925
|
params["pixel_size_y_um"] = pixel_size[1]
|
|
923
926
|
|
|
927
|
+
# Only when the file states one. "mixed" when it states both, so
|
|
928
|
+
# the builder cannot fall back to the file-level terms alone (D35).
|
|
929
|
+
polarity = self._extract_polarity()
|
|
930
|
+
if polarity is not None:
|
|
931
|
+
params["polarity"] = polarity
|
|
932
|
+
|
|
924
933
|
# Add other acquisition parameters from imzmldict
|
|
925
934
|
if hasattr(self.parser, "imzmldict") and self.parser.imzmldict:
|
|
926
935
|
acquisition_keys = [
|
|
@@ -937,6 +946,59 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
937
946
|
|
|
938
947
|
return params
|
|
939
948
|
|
|
949
|
+
def _extract_polarity(self) -> Optional[str]:
|
|
950
|
+
"""The polarity every statement in the file agrees on (D35).
|
|
951
|
+
|
|
952
|
+
An imzML can state it in three places: the ``fileContent`` terms,
|
|
953
|
+
a referenceable parameter group, and each spectrum. Writers differ
|
|
954
|
+
(pyimzml's writer uses the group), so all three are read. The
|
|
955
|
+
spectra's terms come from the parser's own pass over them. A file
|
|
956
|
+
that states both values, anywhere, is left unset and says so.
|
|
957
|
+
"""
|
|
958
|
+
stated: List[str] = []
|
|
959
|
+
metadata = getattr(self.parser, "metadata", None)
|
|
960
|
+
groups = [getattr(metadata, "file_description", None)]
|
|
961
|
+
param_groups = getattr(metadata, "referenceable_param_groups", None)
|
|
962
|
+
if isinstance(param_groups, dict):
|
|
963
|
+
groups.extend(param_groups.values())
|
|
964
|
+
for group in groups:
|
|
965
|
+
by_accession = getattr(group, "param_by_accession", None)
|
|
966
|
+
if isinstance(by_accession, dict):
|
|
967
|
+
stated.extend(
|
|
968
|
+
polarity
|
|
969
|
+
for accession, polarity in POLARITY_OF_ACCESSION.items()
|
|
970
|
+
if accession in by_accession
|
|
971
|
+
)
|
|
972
|
+
stated.extend(
|
|
973
|
+
POLARITY_OF_ACCESSION[accession]
|
|
974
|
+
for accession in self._spectrum_polarity_accessions()
|
|
975
|
+
)
|
|
976
|
+
polarity = agreed_polarity(stated)
|
|
977
|
+
if polarity == MIXED_POLARITY:
|
|
978
|
+
logger.warning(
|
|
979
|
+
"%s states both positive and negative scan polarity; no "
|
|
980
|
+
"polarity is recorded.",
|
|
981
|
+
self.imzml_path.name,
|
|
982
|
+
)
|
|
983
|
+
return polarity
|
|
984
|
+
|
|
985
|
+
def _spectrum_polarity_accessions(self) -> Set[str]:
|
|
986
|
+
"""The scan polarity terms any spectrum states itself.
|
|
987
|
+
|
|
988
|
+
Collected by the parser during its one pass over the spectra, when
|
|
989
|
+
it was built with ``include_spectra_metadata`` as the reader builds
|
|
990
|
+
it. A parser built without it gives none, and the polarity then
|
|
991
|
+
rests on ``fileContent`` and the groups.
|
|
992
|
+
"""
|
|
993
|
+
fields = getattr(self.parser, "spectrum_metadata_fields", None)
|
|
994
|
+
if not isinstance(fields, dict):
|
|
995
|
+
return set()
|
|
996
|
+
return {
|
|
997
|
+
accession
|
|
998
|
+
for accession in POLARITY_OF_ACCESSION
|
|
999
|
+
if any(value is not None for value in fields.get(accession, ()))
|
|
1000
|
+
}
|
|
1001
|
+
|
|
940
1002
|
def _extract_instrument_info(self) -> Dict[str, Any]:
|
|
941
1003
|
"""Extract instrument information from the instrumentConfiguration blocks.
|
|
942
1004
|
|
|
@@ -38,7 +38,7 @@ from __future__ import annotations
|
|
|
38
38
|
|
|
39
39
|
import logging
|
|
40
40
|
from pathlib import Path
|
|
41
|
-
from typing import Any, Optional, Tuple, cast
|
|
41
|
+
from typing import Any, Optional, Set, Tuple, cast
|
|
42
42
|
|
|
43
43
|
from ...readers.imzml.header import ImzMLHeaderParser, scan_observed_mz_range
|
|
44
44
|
from ..types import EssentialMetadata
|
|
@@ -148,6 +148,15 @@ class ImzMLHeaderExtractor(ImzMLMetadataExtractor):
|
|
|
148
148
|
"""The count the file declares, not one taken over coordinates."""
|
|
149
149
|
return int(self.header.n_spectra or 0)
|
|
150
150
|
|
|
151
|
+
def _spectrum_polarity_accessions(self) -> Set[str]:
|
|
152
|
+
"""The polarity terms the first spectrum states, as pyimzml reads it.
|
|
153
|
+
|
|
154
|
+
The head ends at the second spectrum. A conversion reads every
|
|
155
|
+
spectrum's terms, so the two differ only for a file whose later
|
|
156
|
+
spectra state the other polarity (D35).
|
|
157
|
+
"""
|
|
158
|
+
return set(cast(ImzMLHeaderParser, self.parser).first_spectrum_polarity)
|
|
159
|
+
|
|
151
160
|
|
|
152
161
|
def head_shortfall(parser: ImzMLHeaderParser) -> Optional[str]:
|
|
153
162
|
"""Why the head cannot describe this file, or ``None`` if it can.
|