thyra 4.3.1__tar.gz → 4.3.3__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.3}/PKG-INFO +1 -1
- {thyra-4.3.1 → thyra-4.3.3}/pyproject.toml +1 -1
- {thyra-4.3.1 → thyra-4.3.3}/thyra/__init__.py +1 -1
- {thyra-4.3.1 → thyra-4.3.3}/thyra/convert.py +6 -3
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/optical_image.py +38 -12
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/base_reader.py +42 -1
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/imzml_extractor.py +143 -119
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/mzpeak_extractor.py +104 -16
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/mzpeak/chunk_decoding.py +13 -1
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/mzpeak/mzpeak_reader.py +612 -32
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/instrument_detectors.py +5 -2
- {thyra-4.3.1 → thyra-4.3.3}/thyra/utils/imzml_coordinate_base.py +85 -1
- {thyra-4.3.1 → thyra-4.3.3}/.gitignore +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/LICENSE +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/README.md +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/__main__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/alignment/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/alignment/affine.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/config.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/root_attrs.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/base_converter.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/base_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/conversion_state.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/drop_tally.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/frames.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/mass_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/mobility.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/msms.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/core/registry.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/errors.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/constants.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/document.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/msi_metadata_schema_v0_10.json +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/types.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/metadata/validator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/preview.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/py.typed +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/solarix/solarix_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/phi/phi_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/binning.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/constants.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/gaps.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/tic.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/resampling/types.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/tools/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/utils/__init__.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/utils/logging_config.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.3.1 → thyra-4.3.3}/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.3
|
|
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.3"
|
|
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")
|
|
@@ -126,7 +126,7 @@ from ._chunking import image_chunks
|
|
|
126
126
|
|
|
127
127
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
128
128
|
from ...alignment.teaching_points import RegionMapping
|
|
129
|
-
from ...core.base_reader import BaseMSIReader
|
|
129
|
+
from ...core.base_reader import BaseMSIReader, OpticalImageLabel
|
|
130
130
|
|
|
131
131
|
logger = logging.getLogger(__name__)
|
|
132
132
|
|
|
@@ -944,6 +944,11 @@ def _calc_optical_scale_factors(
|
|
|
944
944
|
return factors
|
|
945
945
|
|
|
946
946
|
|
|
947
|
+
def _clean_stem(stem: str) -> str:
|
|
948
|
+
"""A file stem made fit for an element name: lowercase, at most 30 long."""
|
|
949
|
+
return stem.lower().replace(" ", "_").replace("-", "_")[:30]
|
|
950
|
+
|
|
951
|
+
|
|
947
952
|
class OpticalImages:
|
|
948
953
|
"""The optical images of one conversion: alignment, declaration, streaming.
|
|
949
954
|
|
|
@@ -1459,9 +1464,11 @@ class OpticalImages:
|
|
|
1459
1464
|
images: The store's images, to add the placeholder to
|
|
1460
1465
|
"""
|
|
1461
1466
|
# Generate a clean name for the image layer
|
|
1462
|
-
|
|
1467
|
+
label = self._reader_label(image_path)
|
|
1468
|
+
image_name = self._element_name(image_path, label)
|
|
1469
|
+
source_file = label.source_file if label is not None else image_path.name
|
|
1463
1470
|
|
|
1464
|
-
logger.info(f"Loading optical image: {
|
|
1471
|
+
logger.info(f"Loading optical image: {source_file} as '{image_name}'")
|
|
1465
1472
|
|
|
1466
1473
|
# Only the page header is read here. The pixels never enter this
|
|
1467
1474
|
# process whole: the element is declared to SpatialData as a lazy
|
|
@@ -1584,8 +1591,10 @@ class OpticalImages:
|
|
|
1584
1591
|
# rewrites the stem (_0000 -> highres, and a stem over 30
|
|
1585
1592
|
# characters is truncated), and the alignment image is only
|
|
1586
1593
|
# distinguishable by its transform, and only when the alignment
|
|
1587
|
-
# was applied. Recorded here, written by the root attrs.
|
|
1588
|
-
|
|
1594
|
+
# was applied. Recorded here, written by the root attrs. For an
|
|
1595
|
+
# image a reader copied out of a container, the member it came
|
|
1596
|
+
# from, not the copy.
|
|
1597
|
+
self._sources[image_name] = source_file
|
|
1589
1598
|
if is_primary:
|
|
1590
1599
|
self._alignment_element = image_name
|
|
1591
1600
|
|
|
@@ -1688,18 +1697,39 @@ class OpticalImages:
|
|
|
1688
1697
|
"global": {**systems["global"], "reference_element": None},
|
|
1689
1698
|
}
|
|
1690
1699
|
|
|
1691
|
-
def
|
|
1700
|
+
def _reader_label(self, image_path: Path) -> Optional[OpticalImageLabel]:
|
|
1701
|
+
"""The reader's name for an image it copied out of its source.
|
|
1702
|
+
|
|
1703
|
+
Asked for with ``getattr`` for the reason
|
|
1704
|
+
:meth:`_adopt_reader_primary_optical` gives: a reader here need not
|
|
1705
|
+
inherit from ``BaseMSIReader``.
|
|
1706
|
+
"""
|
|
1707
|
+
resolve = getattr(self.reader, "get_optical_image_label", None)
|
|
1708
|
+
if not callable(resolve):
|
|
1709
|
+
return None
|
|
1710
|
+
label: Optional[OpticalImageLabel] = resolve(image_path)
|
|
1711
|
+
return label
|
|
1712
|
+
|
|
1713
|
+
def _element_name(
|
|
1714
|
+
self, image_path: Path, label: Optional[OpticalImageLabel] = None
|
|
1715
|
+
) -> str:
|
|
1692
1716
|
"""Generate a clean name for an optical image layer.
|
|
1693
1717
|
|
|
1694
1718
|
The suffix is dropped, so the same acquisition exported as a .tif or
|
|
1695
|
-
a .jpg lands under the same element name.
|
|
1719
|
+
a .jpg lands under the same element name. The ``_0000`` / ``_0001``
|
|
1720
|
+
rule is for files named by the acquisition software; an image a
|
|
1721
|
+
reader copied out of a container is named by the reader's label.
|
|
1696
1722
|
|
|
1697
1723
|
Args:
|
|
1698
1724
|
image_path: Path to the optical image
|
|
1725
|
+
label: The reader's name for the image, if it gave one.
|
|
1699
1726
|
|
|
1700
1727
|
Returns:
|
|
1701
1728
|
Clean name for the image layer (e.g., 'optical_0000', 'optical_deriv')
|
|
1702
1729
|
"""
|
|
1730
|
+
if label is not None:
|
|
1731
|
+
return f"{self.dataset_id}_optical_{_clean_stem(label.name)}"
|
|
1732
|
+
|
|
1703
1733
|
stem = image_path.stem.lower()
|
|
1704
1734
|
|
|
1705
1735
|
# Extract meaningful suffix from filename
|
|
@@ -1710,11 +1740,7 @@ class OpticalImages:
|
|
|
1710
1740
|
elif "deriv" in stem:
|
|
1711
1741
|
suffix = "overview"
|
|
1712
1742
|
else:
|
|
1713
|
-
|
|
1714
|
-
suffix = stem.replace(" ", "_").replace("-", "_")
|
|
1715
|
-
# Truncate if too long
|
|
1716
|
-
if len(suffix) > 30:
|
|
1717
|
-
suffix = suffix[:30]
|
|
1743
|
+
suffix = _clean_stem(stem)
|
|
1718
1744
|
|
|
1719
1745
|
return f"{self.dataset_id}_optical_{suffix}"
|
|
1720
1746
|
|
|
@@ -52,7 +52,16 @@ for all seven readers at once, so the next change to it is a visible one.
|
|
|
52
52
|
import logging
|
|
53
53
|
from abc import ABC, abstractmethod
|
|
54
54
|
from pathlib import Path
|
|
55
|
-
from typing import
|
|
55
|
+
from typing import (
|
|
56
|
+
TYPE_CHECKING,
|
|
57
|
+
Dict,
|
|
58
|
+
Generator,
|
|
59
|
+
List,
|
|
60
|
+
NamedTuple,
|
|
61
|
+
Optional,
|
|
62
|
+
Tuple,
|
|
63
|
+
Union,
|
|
64
|
+
)
|
|
56
65
|
|
|
57
66
|
import numpy as np
|
|
58
67
|
from numpy.typing import NDArray
|
|
@@ -70,6 +79,22 @@ from .mass_axis import validate_max_mass_axis_length
|
|
|
70
79
|
logger = logging.getLogger(__name__)
|
|
71
80
|
|
|
72
81
|
|
|
82
|
+
class OpticalImageLabel(NamedTuple):
|
|
83
|
+
"""What a store calls an optical image whose file name is not the source's.
|
|
84
|
+
|
|
85
|
+
A reader that copies images out of a container hands the converter files
|
|
86
|
+
it named itself. Their names say nothing, and the name rule written for
|
|
87
|
+
vendor folders (``_0000`` is the high resolution scan) would read meaning
|
|
88
|
+
into them.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
"""The element name after ``<dataset_id>_optical_``."""
|
|
93
|
+
|
|
94
|
+
source_file: str
|
|
95
|
+
"""What the store records the element came from."""
|
|
96
|
+
|
|
97
|
+
|
|
73
98
|
class BaseMSIReader(ABC):
|
|
74
99
|
"""Abstract base class for reading MSI data formats."""
|
|
75
100
|
|
|
@@ -169,6 +194,22 @@ class BaseMSIReader(ABC):
|
|
|
169
194
|
"""
|
|
170
195
|
return None
|
|
171
196
|
|
|
197
|
+
def get_optical_image_label(self, path: Path) -> Optional[OpticalImageLabel]:
|
|
198
|
+
"""Name an optical image this reader copied out of its source.
|
|
199
|
+
|
|
200
|
+
Default implementation returns None: the file is the source's own,
|
|
201
|
+
and its name is what the store is named after. Override alongside
|
|
202
|
+
:meth:`get_optical_image_paths` when the reader names the files.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: One of the paths :meth:`get_optical_image_paths` returned.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The element name and the source file to record, or None to name
|
|
209
|
+
the element after the file.
|
|
210
|
+
"""
|
|
211
|
+
return None
|
|
212
|
+
|
|
172
213
|
@abstractmethod
|
|
173
214
|
def get_common_mass_axis(self) -> NDArray[np.float64]:
|
|
174
215
|
"""Return the common mass axis for all spectra.
|
|
@@ -10,7 +10,7 @@ from pyimzml.ImzMLParser import ImzMLParser
|
|
|
10
10
|
|
|
11
11
|
from ...core.base_extractor import MetadataExtractor
|
|
12
12
|
from ...errors import ConversionRefused
|
|
13
|
-
from ...utils.imzml_coordinate_base import coordinate_bases
|
|
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
16
|
BinaryDataType,
|
|
@@ -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
|
|
|
@@ -220,8 +352,11 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
220
352
|
# store can say where its origin came from: the converter
|
|
221
353
|
# writes it to coordinate_systems.global.coordinate_offsets_px.
|
|
222
354
|
# (1, 1, 1) for an ordinary 1-based file, (0, 0, ...) for a
|
|
223
|
-
# 0-based one -- see _coordinate_bases() (issue #244).
|
|
224
|
-
|
|
355
|
+
# 0-based one -- see _coordinate_bases() (issue #244). z is 0
|
|
356
|
+
# when the file states none (D30).
|
|
357
|
+
coordinate_offsets=recorded_offsets(
|
|
358
|
+
self._coordinate_bases(coords), coords[:, 2], self.imzml_path
|
|
359
|
+
),
|
|
225
360
|
spectrum_type=spectrum_type,
|
|
226
361
|
)
|
|
227
362
|
|
|
@@ -824,29 +959,12 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
824
959
|
chain matches on (``"FT-ICR"`` / ``"Orbitrap"``), when the analyzer
|
|
825
960
|
or the model resolves to a family the chain distinguishes. Absent
|
|
826
961
|
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
|
-
|
|
837
|
-
serial = self._config_param(configs, "MS:1000529")
|
|
838
|
-
if isinstance(serial, str) and serial.strip():
|
|
839
|
-
info["instrument_serial_number"] = serial.strip()
|
|
840
|
-
|
|
841
|
-
analyzer_accession = self._analyzer_accession(configs)
|
|
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
962
|
|
|
849
|
-
|
|
963
|
+
The rules live in :func:`instrument_info_from_configurations`, which
|
|
964
|
+
the mzPeak extractor calls too, so an archive and the imzML it was
|
|
965
|
+
made from resolve to the same instrument.
|
|
966
|
+
"""
|
|
967
|
+
return instrument_info_from_configurations(self._instrument_configurations())
|
|
850
968
|
|
|
851
969
|
def _instrument_configurations(self) -> List[Any]:
|
|
852
970
|
"""The parsed instrumentConfiguration ParamGroups, in document order."""
|
|
@@ -856,100 +974,6 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
856
974
|
return []
|
|
857
975
|
return list(configs.values())
|
|
858
976
|
|
|
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
977
|
def _extract_raw_metadata(self) -> Dict[str, Any]:
|
|
954
978
|
"""Extract raw metadata from imzmldict and spectrum cvParams."""
|
|
955
979
|
raw_metadata = {}
|