thyra 2.2.1__tar.gz → 2.2.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-2.2.1 → thyra-2.2.3}/PKG-INFO +1 -1
- {thyra-2.2.1 → thyra-2.2.3}/pyproject.toml +1 -1
- {thyra-2.2.1 → thyra-2.2.3}/thyra/__init__.py +1 -1
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/extractors/imzml_extractor.py +84 -14
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/data_characteristics.py +14 -1
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/instrument_detectors.py +102 -11
- {thyra-2.2.1 → thyra-2.2.3}/LICENSE +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/README.md +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/__main__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/alignment/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/alignment/affine.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/alignment/teaching_points.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/config.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/convert.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/spatialdata_2d_converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/spatialdata_3d_converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/core/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/core/base_converter.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/core/base_extractor.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/core/base_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/core/registry.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/types.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/metadata/validator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/preview.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/common_axis.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/constants.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/decision_tree.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/gaps.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/strategies/base.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/tic.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/resampling/types.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/tools/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/tools/check_ontology.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/tools/make_example_data.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/utils/__init__.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/utils/logging_config.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/utils/windows_paths.py +0 -0
- {thyra-2.2.1 → thyra-2.2.3}/thyra/utils/zarr_atomic_write.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.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 = ["Theodoros Visvikis <t.visvikis@maastrichtuniversity.nl>"]
|
|
10
10
|
maintainers = ["Theodoros Visvikis <t.visvikis@maastrichtuniversity.nl>"]
|
|
@@ -9,7 +9,7 @@ from numpy.typing import NDArray
|
|
|
9
9
|
from pyimzml.ImzMLParser import ImzMLParser
|
|
10
10
|
|
|
11
11
|
from ...core.base_extractor import MetadataExtractor
|
|
12
|
-
from ...resampling.constants import ImzMLAccessions, SpectrumType
|
|
12
|
+
from ...resampling.constants import BinaryDataType, ImzMLAccessions, SpectrumType
|
|
13
13
|
from ..types import ComprehensiveMetadata, EssentialMetadata
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
@@ -131,7 +131,7 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
131
131
|
if param_by_name is None or not isinstance(param_by_name, dict):
|
|
132
132
|
return False
|
|
133
133
|
|
|
134
|
-
return
|
|
134
|
+
return BinaryDataType.CONTINUOUS in param_by_name
|
|
135
135
|
except Exception:
|
|
136
136
|
return False
|
|
137
137
|
|
|
@@ -522,19 +522,27 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
522
522
|
return None
|
|
523
523
|
|
|
524
524
|
def _detect_centroid_spectrum(self) -> Optional[str]:
|
|
525
|
-
"""Detect spectrum type by looking for MS:1000127 (centroid) or MS:1000128 (profile).
|
|
525
|
+
"""Detect spectrum type by looking for MS:1000127 (centroid) or MS:1000128 (profile).
|
|
526
|
+
|
|
527
|
+
What the file *declares* wins, wherever it is written. Only when the
|
|
528
|
+
file declares nothing does Thyra guess -- see
|
|
529
|
+
:meth:`_guess_spectrum_type_from_storage_mode` for what that guess is
|
|
530
|
+
worth.
|
|
531
|
+
"""
|
|
526
532
|
try:
|
|
527
|
-
#
|
|
528
|
-
result = self.
|
|
533
|
+
# 1. The declaration, from metadata the parser already holds.
|
|
534
|
+
result = self._check_parser_metadata_for_spectrum_type()
|
|
529
535
|
if result:
|
|
530
536
|
return result
|
|
531
537
|
|
|
532
|
-
#
|
|
538
|
+
# 2. The same declaration, wherever else in the document it sits.
|
|
539
|
+
# Streams the XML, so it is tried only if step 1 came up empty.
|
|
533
540
|
result = self._check_xml_for_spectrum_type()
|
|
534
541
|
if result:
|
|
535
542
|
return result
|
|
536
543
|
|
|
537
|
-
|
|
544
|
+
# 3. Nothing declared. Only now is a guess appropriate.
|
|
545
|
+
return self._guess_spectrum_type_from_storage_mode()
|
|
538
546
|
except Exception as e:
|
|
539
547
|
logger.debug(f"Could not detect spectrum type: {e}")
|
|
540
548
|
return None
|
|
@@ -607,8 +615,13 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
607
615
|
logger.warning("defusedxml not available, using xml.etree.ElementTree")
|
|
608
616
|
return ET
|
|
609
617
|
|
|
610
|
-
def
|
|
611
|
-
"""
|
|
618
|
+
def _file_description_params(self) -> Optional[Dict[str, Any]]:
|
|
619
|
+
"""Return the fileDescription cvParams the parser already parsed.
|
|
620
|
+
|
|
621
|
+
``param_by_name`` maps a term's CV *name* to its parsed value, and a
|
|
622
|
+
valueless cvParam -- which is what a flag like ``profile spectrum``
|
|
623
|
+
is -- parses to ``True``.
|
|
624
|
+
"""
|
|
612
625
|
if not (hasattr(self.parser, "metadata") and self.parser.metadata):
|
|
613
626
|
return None
|
|
614
627
|
|
|
@@ -616,13 +629,70 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
616
629
|
return None
|
|
617
630
|
|
|
618
631
|
file_desc = self.parser.metadata.file_description
|
|
619
|
-
|
|
632
|
+
params = getattr(file_desc, "param_by_name", None)
|
|
633
|
+
if not isinstance(params, dict):
|
|
634
|
+
return None
|
|
635
|
+
|
|
636
|
+
return params
|
|
637
|
+
|
|
638
|
+
def _check_parser_metadata_for_spectrum_type(self) -> Optional[str]:
|
|
639
|
+
"""Read the declared spectrum representation out of the fileDescription.
|
|
640
|
+
|
|
641
|
+
Cheap: the parser has already read this, so no XML pass is needed.
|
|
642
|
+
``SpectrumType.CENTROID`` and ``SpectrumType.PROFILE`` are the CV
|
|
643
|
+
names of ``MS:1000127`` and ``MS:1000128``, which is what
|
|
644
|
+
``param_by_name`` is keyed on.
|
|
645
|
+
"""
|
|
646
|
+
params = self._file_description_params()
|
|
647
|
+
if params is None:
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
if params.get(SpectrumType.CENTROID, False):
|
|
651
|
+
logger.info(
|
|
652
|
+
f"Detected centroid spectrum from declared "
|
|
653
|
+
f"{ImzMLAccessions.CENTROID_SPECTRUM} in fileDescription"
|
|
654
|
+
)
|
|
655
|
+
return SpectrumType.CENTROID
|
|
656
|
+
|
|
657
|
+
if params.get(SpectrumType.PROFILE, False):
|
|
658
|
+
logger.info(
|
|
659
|
+
f"Detected profile spectrum from declared "
|
|
660
|
+
f"{ImzMLAccessions.PROFILE_SPECTRUM} in fileDescription"
|
|
661
|
+
)
|
|
662
|
+
return SpectrumType.PROFILE
|
|
663
|
+
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
def _guess_spectrum_type_from_storage_mode(self) -> Optional[str]:
|
|
667
|
+
"""Last-resort guess: assume a processed-mode file is centroided.
|
|
668
|
+
|
|
669
|
+
Processed and centroid are orthogonal. ``IMS:1000031`` says each
|
|
670
|
+
spectrum carries its own m/z array; it says nothing about whether
|
|
671
|
+
the peaks in it are centroided or profile. Plenty of instruments
|
|
672
|
+
export profile spectra in processed mode -- ``bellini.imzML`` is
|
|
673
|
+
processed *and* declares ``MS:1000128 profile spectrum``.
|
|
674
|
+
|
|
675
|
+
This used to run before the declared accession was ever read, so
|
|
676
|
+
every processed file was reported as centroid whatever it said about
|
|
677
|
+
itself. It now runs only when the file declares neither
|
|
678
|
+
``MS:1000127`` nor ``MS:1000128`` anywhere, where a guess is all
|
|
679
|
+
there is. Peak lists are the common case for processed exports, so
|
|
680
|
+
centroid is the better guess -- but it is still a guess, hence the
|
|
681
|
+
warning.
|
|
682
|
+
"""
|
|
683
|
+
params = self._file_description_params()
|
|
684
|
+
if params is None:
|
|
620
685
|
return None
|
|
621
686
|
|
|
622
|
-
params
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
687
|
+
if params.get(BinaryDataType.PROCESSED, False):
|
|
688
|
+
logger.warning(
|
|
689
|
+
"This imzML declares neither %s (centroid) nor %s (profile). "
|
|
690
|
+
"Assuming centroid, because it is stored in processed mode -- "
|
|
691
|
+
"but the two are independent, so check the result if the "
|
|
692
|
+
"spectra are actually profile.",
|
|
693
|
+
ImzMLAccessions.CENTROID_SPECTRUM,
|
|
694
|
+
ImzMLAccessions.PROFILE_SPECTRUM,
|
|
695
|
+
)
|
|
626
696
|
return SpectrumType.CENTROID
|
|
627
697
|
|
|
628
698
|
return None
|
|
@@ -66,6 +66,14 @@ class DataCharacteristics:
|
|
|
66
66
|
|
|
67
67
|
Profile data typically has >5000 points per spectrum,
|
|
68
68
|
indicating continuous signal rather than centroid peaks.
|
|
69
|
+
|
|
70
|
+
This says how densely the spectra are sampled. It says nothing about
|
|
71
|
+
which instrument produced them, so it **must not be used to pick a
|
|
72
|
+
resampling strategy or an axis type**: those encode assumptions about
|
|
73
|
+
the acquisition's physics. ``RapiflexDetector`` used to match on this
|
|
74
|
+
alone, which handed MALDI-TOF treatment to any dense profile data --
|
|
75
|
+
TOF-SIMS, Orbitrap, anything -- with no check that the data was
|
|
76
|
+
MALDI-TOF at all.
|
|
69
77
|
"""
|
|
70
78
|
avg = self.avg_peaks_per_spectrum
|
|
71
79
|
return (
|
|
@@ -76,7 +84,12 @@ class DataCharacteristics:
|
|
|
76
84
|
|
|
77
85
|
@property
|
|
78
86
|
def is_maldi_tof(self) -> bool:
|
|
79
|
-
"""Check if this is MALDI-TOF data.
|
|
87
|
+
"""Check if this is MALDI-TOF data.
|
|
88
|
+
|
|
89
|
+
Carries the same caveat as :attr:`is_high_density_profile`, which its
|
|
90
|
+
last clause consults: density plus a vendor name is not a modality.
|
|
91
|
+
No detector calls this.
|
|
92
|
+
"""
|
|
80
93
|
return (
|
|
81
94
|
self.is_rapiflex_format
|
|
82
95
|
or self.instrument_type == "MALDI-TOF"
|
|
@@ -52,6 +52,30 @@ class InstrumentDetector(ABC):
|
|
|
52
52
|
"""Get the recommended mass axis type for this instrument."""
|
|
53
53
|
pass
|
|
54
54
|
|
|
55
|
+
@property
|
|
56
|
+
def source_grid_law(self) -> Optional[AxisType]:
|
|
57
|
+
"""Spacing law of the *source* m/z grid, when it is known.
|
|
58
|
+
|
|
59
|
+
``tic_preserving`` interpolates onto the target axis and then applies
|
|
60
|
+
one global scaling factor. That composite operator is exact only when
|
|
61
|
+
the source grid follows the same spacing law as the target axis; off
|
|
62
|
+
the diagonal it distorts the ratio between two peaks by exactly
|
|
63
|
+
``(m_hi / m_lo) ** (p_target - p_source)``, with ``p`` = 0, 0.5, 1,
|
|
64
|
+
1.5, 2 for constant / linear_tof / reflector_tof / orbitrap / fticr.
|
|
65
|
+
|
|
66
|
+
SCiLS Lab gates its own TIC-preserving resampling on the same
|
|
67
|
+
condition -- "If all axis types are identical, a TIC preserving
|
|
68
|
+
resampling is applied, otherwise a linear interpolation is performed"
|
|
69
|
+
(SCiLS Lab 2026b User Guide, p.80).
|
|
70
|
+
|
|
71
|
+
``None`` -- the default -- means Thyra does not know the source law.
|
|
72
|
+
That is the honest answer for every detector that matches on
|
|
73
|
+
something other than a vendor format whose grid Thyra itself lays
|
|
74
|
+
out, and :class:`InstrumentDetectorChain` refuses ``TIC_PRESERVING``
|
|
75
|
+
for such a detector.
|
|
76
|
+
"""
|
|
77
|
+
return None
|
|
78
|
+
|
|
55
79
|
|
|
56
80
|
class CentroidImzMLDetector(InstrumentDetector):
|
|
57
81
|
"""Detector for centroid ImzML data.
|
|
@@ -91,23 +115,33 @@ class RapiflexDetector(InstrumentDetector):
|
|
|
91
115
|
return "Rapiflex MALDI-TOF"
|
|
92
116
|
|
|
93
117
|
def matches(self, characteristics: DataCharacteristics) -> bool:
|
|
94
|
-
"""Check if data
|
|
118
|
+
"""Check if data came from Bruker flexImaging/Rapiflex.
|
|
119
|
+
|
|
120
|
+
Both branches below are written by one producer, the Rapiflex
|
|
121
|
+
metadata extractor, so a match here means the spectra arrive on the
|
|
122
|
+
uniform-in-m/z grid ``RapiflexReader`` builds.
|
|
123
|
+
|
|
124
|
+
Peak density is deliberately *not* consulted. It used to be: any
|
|
125
|
+
profile data averaging more than
|
|
126
|
+
``Thresholds.PROFILE_PEAK_DENSITY`` points per spectrum was handed
|
|
127
|
+
MALDI-TOF treatment regardless of what instrument produced it. That
|
|
128
|
+
made the detector modality-blind -- a dense TOF-SIMS or Orbitrap
|
|
129
|
+
profile acquisition would have been resampled by MALDI-TOF logic
|
|
130
|
+
onto a MALDI-shaped axis, silently. SCiLS Lab does not guess
|
|
131
|
+
modality either; its importer takes ``--project TIMSTOF|TOF|FT`` as
|
|
132
|
+
an argument (2026b User Guide, p.81). Unknown-provenance profile
|
|
133
|
+
data now falls through to :class:`DefaultDetector`, which bins
|
|
134
|
+
counts rather than interpolating and so is safe for any modality.
|
|
135
|
+
"""
|
|
95
136
|
# Direct Rapiflex format detection
|
|
96
137
|
if characteristics.is_rapiflex_format:
|
|
97
138
|
return True
|
|
98
139
|
|
|
99
140
|
# Bruker MALDI-TOF detection
|
|
100
|
-
|
|
141
|
+
return (
|
|
101
142
|
characteristics.instrument_type == "MALDI-TOF"
|
|
102
143
|
and characteristics.manufacturer == "Bruker"
|
|
103
|
-
)
|
|
104
|
-
return True
|
|
105
|
-
|
|
106
|
-
# Profile data with high peak density (likely MALDI-TOF)
|
|
107
|
-
if characteristics.is_high_density_profile:
|
|
108
|
-
return True
|
|
109
|
-
|
|
110
|
-
return False
|
|
144
|
+
)
|
|
111
145
|
|
|
112
146
|
def get_resampling_method(self) -> ResamplingMethod:
|
|
113
147
|
"""Return TIC-preserving for profile MALDI-TOF data."""
|
|
@@ -117,6 +151,19 @@ class RapiflexDetector(InstrumentDetector):
|
|
|
117
151
|
"""Return constant axis matching SCiLS Lab convention."""
|
|
118
152
|
return AxisType.CONSTANT
|
|
119
153
|
|
|
154
|
+
@property
|
|
155
|
+
def source_grid_law(self) -> Optional[AxisType]:
|
|
156
|
+
"""Report the constant law of the flexImaging source grid.
|
|
157
|
+
|
|
158
|
+
``RapiflexReader.get_common_mass_axis`` lays every spectrum out with
|
|
159
|
+
``np.linspace(mass_start, mass_end, n_points)``, so the source
|
|
160
|
+
spacing is constant in m/z -- the same law as the ``constant`` target
|
|
161
|
+
axis :meth:`get_axis_type` asks for. Source law equal to target law
|
|
162
|
+
is what makes ``tic_preserving`` exact here, and it is the reason
|
|
163
|
+
this is the only route on which the chain permits it.
|
|
164
|
+
"""
|
|
165
|
+
return AxisType.CONSTANT
|
|
166
|
+
|
|
120
167
|
|
|
121
168
|
class TimsTOFDetector(InstrumentDetector):
|
|
122
169
|
"""Detector for Bruker timsTOF data.
|
|
@@ -273,6 +320,9 @@ class InstrumentDetectorChain:
|
|
|
273
320
|
) -> ResamplingMethod:
|
|
274
321
|
"""Get resampling method for the detected instrument.
|
|
275
322
|
|
|
323
|
+
``TIC_PRESERVING`` additionally has to clear the matching-axis-law
|
|
324
|
+
gate; see :meth:`_gate_tic_preserving`.
|
|
325
|
+
|
|
276
326
|
Args:
|
|
277
327
|
characteristics: Data characteristics to match
|
|
278
328
|
|
|
@@ -280,10 +330,51 @@ class InstrumentDetectorChain:
|
|
|
280
330
|
Recommended resampling method for the instrument
|
|
281
331
|
"""
|
|
282
332
|
detector = self.detect(characteristics)
|
|
283
|
-
method =
|
|
333
|
+
method = self._gate_tic_preserving(detector)
|
|
284
334
|
logger.info(f"Selected resampling method: {method.name}")
|
|
285
335
|
return method
|
|
286
336
|
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _gate_tic_preserving(detector: InstrumentDetector) -> ResamplingMethod:
|
|
339
|
+
"""Allow ``TIC_PRESERVING`` only when source and target laws agree.
|
|
340
|
+
|
|
341
|
+
SCiLS Lab applies TIC-preserving resampling to profile data only
|
|
342
|
+
when all the mass axes being combined are of the same type, and
|
|
343
|
+
linear interpolation otherwise (2026b User Guide, p.80). That is
|
|
344
|
+
also precisely the condition under which Thyra's operator --
|
|
345
|
+
interpolate, then rescale by one global factor -- is exact. See
|
|
346
|
+
:attr:`InstrumentDetector.source_grid_law` for the error off the
|
|
347
|
+
diagonal.
|
|
348
|
+
|
|
349
|
+
A detector that has not declared its source grid law does not clear
|
|
350
|
+
the gate. Auto-selection therefore cannot reach the interpolating
|
|
351
|
+
path on data whose acquisition Thyra has not actually identified.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
detector: The detector that matched.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
The detector's method, or ``NEAREST_NEIGHBOR`` in its place.
|
|
358
|
+
"""
|
|
359
|
+
method = detector.get_resampling_method()
|
|
360
|
+
if method is not ResamplingMethod.TIC_PRESERVING:
|
|
361
|
+
return method
|
|
362
|
+
|
|
363
|
+
axis_type = detector.get_axis_type()
|
|
364
|
+
source_law = detector.source_grid_law
|
|
365
|
+
if source_law is axis_type:
|
|
366
|
+
return method
|
|
367
|
+
|
|
368
|
+
logger.info(
|
|
369
|
+
"%s asks for TIC_PRESERVING onto a %s axis, but the source grid "
|
|
370
|
+
"law is %s. TIC-preserving resampling is exact only when the two "
|
|
371
|
+
"match, so NEAREST_NEIGHBOR is used instead.",
|
|
372
|
+
detector.name,
|
|
373
|
+
axis_type.name,
|
|
374
|
+
"unknown" if source_law is None else source_law.name,
|
|
375
|
+
)
|
|
376
|
+
return ResamplingMethod.NEAREST_NEIGHBOR
|
|
377
|
+
|
|
287
378
|
def get_axis_type(self, characteristics: DataCharacteristics) -> AxisType:
|
|
288
379
|
"""Get axis type for the detected instrument.
|
|
289
380
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|