thyra 4.3.1__tar.gz → 4.3.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.3.1 → thyra-4.3.2}/PKG-INFO +1 -1
- {thyra-4.3.1 → thyra-4.3.2}/pyproject.toml +1 -1
- {thyra-4.3.1 → thyra-4.3.2}/thyra/__init__.py +1 -1
- {thyra-4.3.1 → thyra-4.3.2}/thyra/convert.py +6 -3
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/imzml_extractor.py +137 -116
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/mzpeak_extractor.py +103 -15
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/mzpeak/chunk_decoding.py +13 -1
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/mzpeak/mzpeak_reader.py +463 -30
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/instrument_detectors.py +5 -2
- {thyra-4.3.1 → thyra-4.3.2}/.gitignore +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/LICENSE +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/README.md +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/__main__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/alignment/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/alignment/affine.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/config.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/optical_image.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/root_attrs.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/base_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/base_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/base_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/conversion_state.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/drop_tally.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/frames.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/mass_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/mobility.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/msms.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/core/registry.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/errors.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/constants.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/document.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/msi_metadata_schema_v0_10.json +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/types.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/metadata/validator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/preview.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/py.typed +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/solarix/solarix_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/phi/phi_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/binning.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/constants.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/gaps.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/tic.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/resampling/types.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/tools/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/imzml_coordinate_base.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/logging_config.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.3.1 → thyra-4.3.2}/thyra/utils/windows_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thyra
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "4.3.
|
|
7
|
+
version = "4.3.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" }]
|
|
@@ -358,9 +358,12 @@ def _determine_pixel_size(
|
|
|
358
358
|
essential_metadata = reader.get_essential_metadata()
|
|
359
359
|
|
|
360
360
|
if essential_metadata.pixel_size is None:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
raise ConversionRefused(
|
|
361
|
+
# Said once: the refusal is logged where it is caught. Logging it
|
|
362
|
+
# here as well printed the same line twice.
|
|
363
|
+
raise ConversionRefused(
|
|
364
|
+
"Pixel size not found in metadata. Pass it with --pixel-size "
|
|
365
|
+
"(for example --pixel-size 25)."
|
|
366
|
+
)
|
|
364
367
|
|
|
365
368
|
final_pixel_size = essential_metadata.pixel_size[0] # Use X size
|
|
366
369
|
logger.info(f"Detected pixel size: {final_pixel_size:.1f} um")
|
|
@@ -159,6 +159,138 @@ def _family_from_model_text(text: str) -> Optional[str]:
|
|
|
159
159
|
return None
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def instrument_info_from_configurations(configs: List[Any]) -> Dict[str, Any]:
|
|
163
|
+
"""Instrument model, serial number, analyzer and family of a run.
|
|
164
|
+
|
|
165
|
+
``configs`` are instrument configurations shaped as pyimzml parses
|
|
166
|
+
them: each has ``param_by_accession`` and ``param_by_name`` dicts, a
|
|
167
|
+
valueless term reads as ``True``, and ``components`` lists objects with
|
|
168
|
+
a ``type`` and their own ``param_by_accession``. The imzML extractor
|
|
169
|
+
passes pyimzml's own; the mzPeak extractor builds the same shape from
|
|
170
|
+
the archive's JSON.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
``instrument_model``, ``instrument_serial_number``, ``analyzer`` and
|
|
174
|
+
``instrument_type``, each only when the configurations state it.
|
|
175
|
+
"""
|
|
176
|
+
info: Dict[str, Any] = {}
|
|
177
|
+
if not configs:
|
|
178
|
+
return info
|
|
179
|
+
|
|
180
|
+
model = _instrument_model(configs)
|
|
181
|
+
if model:
|
|
182
|
+
info["instrument_model"] = model
|
|
183
|
+
|
|
184
|
+
serial = _config_param(configs, "MS:1000529")
|
|
185
|
+
if isinstance(serial, str) and serial.strip():
|
|
186
|
+
info["instrument_serial_number"] = serial.strip()
|
|
187
|
+
|
|
188
|
+
analyzer_accession = _analyzer_accession(configs)
|
|
189
|
+
if analyzer_accession is not None:
|
|
190
|
+
info["analyzer"] = ONTOLOGY.terms[analyzer_accession][0]
|
|
191
|
+
|
|
192
|
+
family = _analyzer_family(configs, analyzer_accession, model)
|
|
193
|
+
if family is not None:
|
|
194
|
+
info["instrument_type"] = family
|
|
195
|
+
|
|
196
|
+
return info
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _config_param(configs: List[Any], accession: str) -> Any:
|
|
200
|
+
"""The first value any configuration declares for ``accession``."""
|
|
201
|
+
for config in configs:
|
|
202
|
+
params = getattr(config, "param_by_accession", None)
|
|
203
|
+
if isinstance(params, dict) and accession in params:
|
|
204
|
+
return params[accession]
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _instrument_model(configs: List[Any]) -> Optional[str]:
|
|
209
|
+
"""The declared instrument model, from the shape the exporter chose.
|
|
210
|
+
|
|
211
|
+
Vendors write the model two ways: generic ``MS:1000031 instrument
|
|
212
|
+
model`` carrying the name as free text, or the model's own child term
|
|
213
|
+
as a valueless flag (``MS:1001549 solariX``, ``MS:1003124 timsTOF
|
|
214
|
+
fleX``). The explicit value wins; a known model accession resolves
|
|
215
|
+
through the shipped ontology table; and a term newer than the table is
|
|
216
|
+
still usable when its *name* identifies a family.
|
|
217
|
+
"""
|
|
218
|
+
value = _config_param(configs, "MS:1000031")
|
|
219
|
+
if isinstance(value, str) and value.strip():
|
|
220
|
+
return value.strip()
|
|
221
|
+
|
|
222
|
+
for config in configs:
|
|
223
|
+
params = getattr(config, "param_by_accession", None)
|
|
224
|
+
if not isinstance(params, dict):
|
|
225
|
+
continue
|
|
226
|
+
for accession in params:
|
|
227
|
+
if accession in _MODEL_FAMILIES:
|
|
228
|
+
return ONTOLOGY.terms[accession][0]
|
|
229
|
+
|
|
230
|
+
for config in configs:
|
|
231
|
+
params = getattr(config, "param_by_name", None)
|
|
232
|
+
if not isinstance(params, dict):
|
|
233
|
+
continue
|
|
234
|
+
for name, value in params.items():
|
|
235
|
+
if (
|
|
236
|
+
isinstance(name, str)
|
|
237
|
+
and value is True
|
|
238
|
+
and _family_from_model_text(name) is not None
|
|
239
|
+
):
|
|
240
|
+
return name
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _analyzer_accession(configs: List[Any]) -> Optional[str]:
|
|
246
|
+
"""The highest-priority analyzer term declared on any componentList."""
|
|
247
|
+
declared: set = set()
|
|
248
|
+
for config in configs:
|
|
249
|
+
for component in getattr(config, "components", None) or []:
|
|
250
|
+
if getattr(component, "type", None) != "analyzer":
|
|
251
|
+
continue
|
|
252
|
+
params = getattr(component, "param_by_accession", None)
|
|
253
|
+
if isinstance(params, dict):
|
|
254
|
+
declared.update(params)
|
|
255
|
+
|
|
256
|
+
for accession in _ANALYZER_ACCESSIONS:
|
|
257
|
+
if accession in declared:
|
|
258
|
+
return accession
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _analyzer_family(
|
|
263
|
+
configs: List[Any],
|
|
264
|
+
analyzer_accession: Optional[str],
|
|
265
|
+
model: Optional[str],
|
|
266
|
+
) -> Optional[str]:
|
|
267
|
+
"""Resolve the axis family, preferring the analyzer declaration.
|
|
268
|
+
|
|
269
|
+
The analyzer cvParam states the physics outright, so it outranks the
|
|
270
|
+
model term, which outranks a substring match on free-form model text.
|
|
271
|
+
A file that declares none of the three resolves to ``None``.
|
|
272
|
+
"""
|
|
273
|
+
if analyzer_accession in _ANALYZER_FAMILIES:
|
|
274
|
+
return _ANALYZER_FAMILIES[analyzer_accession]
|
|
275
|
+
if analyzer_accession is not None:
|
|
276
|
+
# A recognised analyzer outside the family table (TOF, ion trap)
|
|
277
|
+
# is a real declaration; do not second-guess it from the model.
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
for config in configs:
|
|
281
|
+
params = getattr(config, "param_by_accession", None)
|
|
282
|
+
if not isinstance(params, dict):
|
|
283
|
+
continue
|
|
284
|
+
for accession in params:
|
|
285
|
+
family = _MODEL_FAMILIES.get(accession)
|
|
286
|
+
if family is not None:
|
|
287
|
+
return family
|
|
288
|
+
|
|
289
|
+
if model is not None:
|
|
290
|
+
return _family_from_model_text(model)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
162
294
|
class ImzMLMetadataExtractor(MetadataExtractor):
|
|
163
295
|
"""ImzML-specific metadata extractor with optimized two-phase extraction."""
|
|
164
296
|
|
|
@@ -824,29 +956,12 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
824
956
|
chain matches on (``"FT-ICR"`` / ``"Orbitrap"``), when the analyzer
|
|
825
957
|
or the model resolves to a family the chain distinguishes. Absent
|
|
826
958
|
otherwise: an unstated analyzer stays unstated.
|
|
827
|
-
"""
|
|
828
|
-
info: Dict[str, Any] = {}
|
|
829
|
-
configs = self._instrument_configurations()
|
|
830
|
-
if not configs:
|
|
831
|
-
return info
|
|
832
|
-
|
|
833
|
-
model = self._instrument_model(configs)
|
|
834
|
-
if model:
|
|
835
|
-
info["instrument_model"] = model
|
|
836
959
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if analyzer_accession is not None:
|
|
843
|
-
info["analyzer"] = ONTOLOGY.terms[analyzer_accession][0]
|
|
844
|
-
|
|
845
|
-
family = self._analyzer_family(configs, analyzer_accession, model)
|
|
846
|
-
if family is not None:
|
|
847
|
-
info["instrument_type"] = family
|
|
848
|
-
|
|
849
|
-
return info
|
|
960
|
+
The rules live in :func:`instrument_info_from_configurations`, which
|
|
961
|
+
the mzPeak extractor calls too, so an archive and the imzML it was
|
|
962
|
+
made from resolve to the same instrument.
|
|
963
|
+
"""
|
|
964
|
+
return instrument_info_from_configurations(self._instrument_configurations())
|
|
850
965
|
|
|
851
966
|
def _instrument_configurations(self) -> List[Any]:
|
|
852
967
|
"""The parsed instrumentConfiguration ParamGroups, in document order."""
|
|
@@ -856,100 +971,6 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
856
971
|
return []
|
|
857
972
|
return list(configs.values())
|
|
858
973
|
|
|
859
|
-
@staticmethod
|
|
860
|
-
def _config_param(configs: List[Any], accession: str) -> Any:
|
|
861
|
-
"""The first value any configuration declares for ``accession``."""
|
|
862
|
-
for config in configs:
|
|
863
|
-
params = getattr(config, "param_by_accession", None)
|
|
864
|
-
if isinstance(params, dict) and accession in params:
|
|
865
|
-
return params[accession]
|
|
866
|
-
return None
|
|
867
|
-
|
|
868
|
-
def _instrument_model(self, configs: List[Any]) -> Optional[str]:
|
|
869
|
-
"""The declared instrument model, from the shape the exporter chose.
|
|
870
|
-
|
|
871
|
-
Vendors write the model two ways: generic ``MS:1000031 instrument
|
|
872
|
-
model`` carrying the name as free text, or the model's own child term
|
|
873
|
-
as a valueless flag (``MS:1001549 solariX``, ``MS:1003124 timsTOF
|
|
874
|
-
fleX``). The explicit value wins; a known model accession resolves
|
|
875
|
-
through the shipped ontology table; and a term newer than the table is
|
|
876
|
-
still usable when its *name* identifies a family.
|
|
877
|
-
"""
|
|
878
|
-
value = self._config_param(configs, "MS:1000031")
|
|
879
|
-
if isinstance(value, str) and value.strip():
|
|
880
|
-
return value.strip()
|
|
881
|
-
|
|
882
|
-
for config in configs:
|
|
883
|
-
params = getattr(config, "param_by_accession", None)
|
|
884
|
-
if not isinstance(params, dict):
|
|
885
|
-
continue
|
|
886
|
-
for accession in params:
|
|
887
|
-
if accession in _MODEL_FAMILIES:
|
|
888
|
-
return ONTOLOGY.terms[accession][0]
|
|
889
|
-
|
|
890
|
-
for config in configs:
|
|
891
|
-
params = getattr(config, "param_by_name", None)
|
|
892
|
-
if not isinstance(params, dict):
|
|
893
|
-
continue
|
|
894
|
-
for name, value in params.items():
|
|
895
|
-
if (
|
|
896
|
-
isinstance(name, str)
|
|
897
|
-
and value is True
|
|
898
|
-
and _family_from_model_text(name) is not None
|
|
899
|
-
):
|
|
900
|
-
return name
|
|
901
|
-
|
|
902
|
-
return None
|
|
903
|
-
|
|
904
|
-
@staticmethod
|
|
905
|
-
def _analyzer_accession(configs: List[Any]) -> Optional[str]:
|
|
906
|
-
"""The highest-priority analyzer term declared on any componentList."""
|
|
907
|
-
declared: set = set()
|
|
908
|
-
for config in configs:
|
|
909
|
-
for component in getattr(config, "components", None) or []:
|
|
910
|
-
if getattr(component, "type", None) != "analyzer":
|
|
911
|
-
continue
|
|
912
|
-
params = getattr(component, "param_by_accession", None)
|
|
913
|
-
if isinstance(params, dict):
|
|
914
|
-
declared.update(params)
|
|
915
|
-
|
|
916
|
-
for accession in _ANALYZER_ACCESSIONS:
|
|
917
|
-
if accession in declared:
|
|
918
|
-
return accession
|
|
919
|
-
return None
|
|
920
|
-
|
|
921
|
-
def _analyzer_family(
|
|
922
|
-
self,
|
|
923
|
-
configs: List[Any],
|
|
924
|
-
analyzer_accession: Optional[str],
|
|
925
|
-
model: Optional[str],
|
|
926
|
-
) -> Optional[str]:
|
|
927
|
-
"""Resolve the axis family, preferring the analyzer declaration.
|
|
928
|
-
|
|
929
|
-
The analyzer cvParam states the physics outright, so it outranks the
|
|
930
|
-
model term, which outranks a substring match on free-form model text.
|
|
931
|
-
A file that declares none of the three resolves to ``None``.
|
|
932
|
-
"""
|
|
933
|
-
if analyzer_accession in _ANALYZER_FAMILIES:
|
|
934
|
-
return _ANALYZER_FAMILIES[analyzer_accession]
|
|
935
|
-
if analyzer_accession is not None:
|
|
936
|
-
# A recognised analyzer outside the family table (TOF, ion trap)
|
|
937
|
-
# is a real declaration; do not second-guess it from the model.
|
|
938
|
-
return None
|
|
939
|
-
|
|
940
|
-
for config in configs:
|
|
941
|
-
params = getattr(config, "param_by_accession", None)
|
|
942
|
-
if not isinstance(params, dict):
|
|
943
|
-
continue
|
|
944
|
-
for accession in params:
|
|
945
|
-
family = _MODEL_FAMILIES.get(accession)
|
|
946
|
-
if family is not None:
|
|
947
|
-
return family
|
|
948
|
-
|
|
949
|
-
if model is not None:
|
|
950
|
-
return _family_from_model_text(model)
|
|
951
|
-
return None
|
|
952
|
-
|
|
953
974
|
def _extract_raw_metadata(self) -> Dict[str, Any]:
|
|
954
975
|
"""Extract raw metadata from imzmldict and spectrum cvParams."""
|
|
955
976
|
raw_metadata = {}
|
|
@@ -12,14 +12,20 @@ from __future__ import annotations
|
|
|
12
12
|
import logging
|
|
13
13
|
import math
|
|
14
14
|
from pathlib import Path
|
|
15
|
+
from types import SimpleNamespace
|
|
15
16
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
|
|
19
20
|
from ...core.base_extractor import MetadataExtractor
|
|
20
21
|
from ..constants import ImzMLAccessions, SpectrumType
|
|
22
|
+
from ..ontology.cache import ONTOLOGY
|
|
21
23
|
from ..types import ComprehensiveMetadata, EssentialMetadata
|
|
22
|
-
from .imzml_extractor import
|
|
24
|
+
from .imzml_extractor import (
|
|
25
|
+
UM_PER_UNIT,
|
|
26
|
+
has_old_pixel_size_name,
|
|
27
|
+
instrument_info_from_configurations,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
25
31
|
# Imported for typing only. A runtime import here would be a cycle:
|
|
@@ -84,10 +90,11 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
84
90
|
coordinates = index.coordinates
|
|
85
91
|
raw = index.raw_positions
|
|
86
92
|
|
|
87
|
-
# Grid is sized from what the file actually contains
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
93
|
+
# Grid is sized from what the file actually contains, from index 0
|
|
94
|
+
# (the base, as D14 sets it for imzML) to the largest position.
|
|
95
|
+
# mzPeak also declares IMS:1000042/43, but a declared extent that
|
|
96
|
+
# disagrees with the positions would silently pad or clip the
|
|
97
|
+
# output, so the declaration is kept as provenance only.
|
|
91
98
|
dimensions = (
|
|
92
99
|
int(coordinates[:, 0].max()) + 1,
|
|
93
100
|
int(coordinates[:, 1].max()) + 1,
|
|
@@ -123,12 +130,20 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
123
130
|
data: the metadata member has one row per spectrum, so this is a
|
|
124
131
|
9-row read on a 36k-point file and stays a one-row-per-spectrum read
|
|
125
132
|
at any scale.
|
|
133
|
+
|
|
134
|
+
A spectrum that is not converted (on no pixel, or an MSn spectrum
|
|
135
|
+
beside MS1) does not widen the range.
|
|
126
136
|
"""
|
|
127
137
|
table = self.archive.parquet("spectrum", "metadata").read(
|
|
128
|
-
columns=["lowest_observed_mz", "highest_observed_mz"]
|
|
138
|
+
columns=["index", "lowest_observed_mz", "highest_observed_mz"]
|
|
129
139
|
)
|
|
130
140
|
low = np.asarray(table.column("lowest_observed_mz").to_numpy(), dtype=float)
|
|
131
141
|
high = np.asarray(table.column("highest_observed_mz").to_numpy(), dtype=float)
|
|
142
|
+
index = self.archive.spatial_index()
|
|
143
|
+
if not index.complete:
|
|
144
|
+
rows = np.asarray(table.column("index").to_numpy(), dtype=np.int64)
|
|
145
|
+
kept = np.isin(rows, index.spectrum_indices)
|
|
146
|
+
low, high = low[kept], high[kept]
|
|
132
147
|
low = low[np.isfinite(low)]
|
|
133
148
|
high = high[np.isfinite(high)]
|
|
134
149
|
if low.size == 0 or high.size == 0:
|
|
@@ -174,7 +189,12 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
174
189
|
x = parameters.get(ImzMLAccessions.PIXEL_SIZE_X)
|
|
175
190
|
y = parameters.get(ImzMLAccessions.PIXEL_SIZE_Y)
|
|
176
191
|
if x is None and y is None:
|
|
177
|
-
logger.info(
|
|
192
|
+
logger.info(
|
|
193
|
+
"%s states no pixel size: its scan settings hold neither %s " "nor %s.",
|
|
194
|
+
self.data_path.name,
|
|
195
|
+
ImzMLAccessions.PIXEL_SIZE_X,
|
|
196
|
+
ImzMLAccessions.PIXEL_SIZE_Y,
|
|
197
|
+
)
|
|
178
198
|
return None
|
|
179
199
|
if x is not None and y is not None and not self._has_old_name(x):
|
|
180
200
|
return self._declared_pair(x, y)
|
|
@@ -446,6 +466,11 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
446
466
|
nulls = self.archive.null_count()
|
|
447
467
|
if not nulls:
|
|
448
468
|
return declared
|
|
469
|
+
if not index.complete:
|
|
470
|
+
# The padding count covers the whole member and cannot be split
|
|
471
|
+
# by spectrum, so with spectra left out the count keeps its
|
|
472
|
+
# padding: an upper bound rather than a figure too low.
|
|
473
|
+
return declared
|
|
449
474
|
return max(0, declared - int(nulls))
|
|
450
475
|
|
|
451
476
|
# ------------------------------------------------------------------
|
|
@@ -507,11 +532,74 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
507
532
|
}
|
|
508
533
|
|
|
509
534
|
def _instrument_info(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
510
|
-
"""
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
535
|
+
"""The instrument as the imzML route reads it, and the raw lists.
|
|
536
|
+
|
|
537
|
+
Model, serial number, analyzer and family are resolved by the
|
|
538
|
+
function the imzML extractor uses, so an archive converts on the
|
|
539
|
+
same mass axis as the imzML it was made from. The family is what
|
|
540
|
+
picks the axis: without it an Orbitrap archive was binned as an
|
|
541
|
+
unknown instrument.
|
|
542
|
+
"""
|
|
543
|
+
configurations = metadata.get("instrument_configuration_list")
|
|
544
|
+
info = instrument_info_from_configurations(
|
|
545
|
+
_as_parsed_configurations(configurations)
|
|
546
|
+
)
|
|
547
|
+
info.update(
|
|
548
|
+
{
|
|
549
|
+
"instrument_configuration_list": configurations,
|
|
550
|
+
"software_list": metadata.get("software_list"),
|
|
551
|
+
"data_processing_method_list": metadata.get(
|
|
552
|
+
"data_processing_method_list"
|
|
553
|
+
),
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
return info
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _as_parsed_parameters(parameters: Any) -> SimpleNamespace:
|
|
560
|
+
"""One mzPeak parameter list in the shape pyimzml gives a ParamGroup.
|
|
561
|
+
|
|
562
|
+
As pyimzml does, a term without a value reads as ``True`` and a known
|
|
563
|
+
accession is named by the ontology, not by the name the writer gave.
|
|
564
|
+
"""
|
|
565
|
+
by_accession: Dict[str, Any] = {}
|
|
566
|
+
by_name: Dict[str, Any] = {}
|
|
567
|
+
for parameter in parameters if isinstance(parameters, list) else []:
|
|
568
|
+
if not isinstance(parameter, dict):
|
|
569
|
+
continue
|
|
570
|
+
value = parameter.get("value")
|
|
571
|
+
value = True if value is None else value
|
|
572
|
+
accession = parameter.get("accession")
|
|
573
|
+
name = parameter.get("name")
|
|
574
|
+
if isinstance(accession, str) and accession:
|
|
575
|
+
by_accession.setdefault(accession, value)
|
|
576
|
+
known = ONTOLOGY.terms.get(accession)
|
|
577
|
+
if known:
|
|
578
|
+
name = known[0]
|
|
579
|
+
if isinstance(name, str) and name:
|
|
580
|
+
by_name.setdefault(name, value)
|
|
581
|
+
return SimpleNamespace(param_by_accession=by_accession, param_by_name=by_name)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _as_parsed_configurations(configurations: Any) -> List[SimpleNamespace]:
|
|
585
|
+
"""The archive's instrument configurations, shaped as pyimzml parses them.
|
|
586
|
+
|
|
587
|
+
See :func:`~.imzml_extractor.instrument_info_from_configurations`.
|
|
588
|
+
mzPeak writes a component's kind as ``component_type`` ("analyzer",
|
|
589
|
+
"ionsource", "detector"); pyimzml calls it ``type``.
|
|
590
|
+
"""
|
|
591
|
+
parsed: List[SimpleNamespace] = []
|
|
592
|
+
for configuration in configurations if isinstance(configurations, list) else []:
|
|
593
|
+
if not isinstance(configuration, dict):
|
|
594
|
+
continue
|
|
595
|
+
group = _as_parsed_parameters(configuration.get("parameters"))
|
|
596
|
+
components = []
|
|
597
|
+
for component in configuration.get("components") or []:
|
|
598
|
+
if not isinstance(component, dict):
|
|
599
|
+
continue
|
|
600
|
+
parsed_component = _as_parsed_parameters(component.get("parameters"))
|
|
601
|
+
parsed_component.type = component.get("component_type")
|
|
602
|
+
components.append(parsed_component)
|
|
603
|
+
group.components = components
|
|
604
|
+
parsed.append(group)
|
|
605
|
+
return parsed
|
|
@@ -79,6 +79,17 @@ REFUSED_TERMS: Dict[str, str] = {
|
|
|
79
79
|
"MS:9999002": "timsTOF m/z grid of the reference implementation",
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
#: Grid models the default timsTOF (TDF) lane of mzpeak-convert writes.
|
|
83
|
+
TIMSTOF_GRID_TYPES = ("MS:9999001", "MS:9999002")
|
|
84
|
+
|
|
85
|
+
#: What a refusal of those models adds: the option of that converter that
|
|
86
|
+
#: writes the m/z themselves (checked against the help of 0.16.0).
|
|
87
|
+
TIMSTOF_GRID_HINT = (
|
|
88
|
+
" mzpeak-convert writes this model for a timsTOF .d by default. Convert "
|
|
89
|
+
"the .d again with --no-ims-compact to store m/z Thyra reads, or "
|
|
90
|
+
"convert the .d with Thyra directly."
|
|
91
|
+
)
|
|
92
|
+
|
|
82
93
|
#: Transforms of the intensity array, by the column that holds them.
|
|
83
94
|
INTENSITY_TRANSFORMS: Dict[str, str] = {
|
|
84
95
|
"intensity_numpress_slof_bytes": "MS:1002314",
|
|
@@ -208,10 +219,11 @@ def validate_encodings(
|
|
|
208
219
|
return
|
|
209
220
|
for grid_type in grid_types:
|
|
210
221
|
if grid_type not in DECODED_GRID_TYPES:
|
|
222
|
+
hint = TIMSTOF_GRID_HINT if grid_type in TIMSTOF_GRID_TYPES else ""
|
|
211
223
|
raise ConversionRefused(
|
|
212
224
|
f"{source} stores m/z on a grid of type "
|
|
213
225
|
f"{describe_term(grid_type)}, which Thyra does not decode. "
|
|
214
|
-
f"It decodes {_decoded(DECODED_GRID_TYPES)}."
|
|
226
|
+
f"It decodes {_decoded(DECODED_GRID_TYPES)}.{hint}"
|
|
215
227
|
)
|
|
216
228
|
|
|
217
229
|
|