thyra 4.2.0__tar.gz → 4.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thyra-4.2.0 → thyra-4.3.0}/PKG-INFO +1 -1
- {thyra-4.2.0 → thyra-4.3.0}/pyproject.toml +1 -1
- {thyra-4.2.0 → thyra-4.3.0}/thyra/__init__.py +1 -1
- {thyra-4.2.0 → thyra-4.3.0}/thyra/__main__.py +12 -2
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/streaming_converter.py +83 -21
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/base_reader.py +48 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/frames.py +7 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/imzml_extractor.py +67 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/mzpeak_extractor.py +239 -42
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/__init__.py +2 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/builder.py +27 -6
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/models.py +18 -2
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/msi_metadata.linkml.yaml +2 -2
- thyra-4.2.0/thyra/metadata/schema/msi_metadata_schema_v0_9.json → thyra-4.3.0/thyra/metadata/schema/msi_metadata_schema_v0_10.json +2 -2
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +5 -1
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/solarix/solarix_reader.py +45 -1
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/timstof_reader.py +191 -78
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/imzml/imzml_reader.py +50 -3
- thyra-4.3.0/thyra/readers/mzpeak/chunk_decoding.py +787 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/mzpeak/mzpeak_reader.py +366 -68
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/phi/phi_reader.py +4 -1
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/waters_reader.py +47 -1
- {thyra-4.2.0 → thyra-4.3.0}/.gitignore +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/LICENSE +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/README.md +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/alignment/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/alignment/affine.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/config.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/convert.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/optical_image.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/root_attrs.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/base_converter.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/base_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/conversion_state.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/drop_tally.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/mass_axis.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/mobility.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/msms.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/core/registry.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/errors.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/constants.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/document.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/types.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/metadata/validator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/preview.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/py.typed +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/binning.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/constants.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/gaps.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/tic.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/resampling/types.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/tools/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/utils/__init__.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/utils/imzml_coordinate_base.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/utils/logging_config.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.2.0 → thyra-4.3.0}/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
|
+
Version: 4.3.0
|
|
4
4
|
Summary: A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics
|
|
5
5
|
Project-URL: Homepage, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
6
6
|
Project-URL: Repository, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.0"
|
|
8
8
|
description = "A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics"
|
|
9
9
|
authors = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
10
10
|
maintainers = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
@@ -331,7 +331,14 @@ def _display_calibration_info(input: Path, use_recalibrated: bool) -> None:
|
|
|
331
331
|
f"{'s' if n_recalibrations > 1 else ''} since acquisition"
|
|
332
332
|
)
|
|
333
333
|
for state in states:
|
|
334
|
-
|
|
334
|
+
# With --no-recalibrated, Bruker's library applies none of the
|
|
335
|
+
# states, the newest included: it takes the analysis database's
|
|
336
|
+
# own calibration instead.
|
|
337
|
+
active_marker = (
|
|
338
|
+
" (active/will be used)"
|
|
339
|
+
if use_recalibrated and state["id"] == active_id
|
|
340
|
+
else ""
|
|
341
|
+
)
|
|
335
342
|
click.echo(
|
|
336
343
|
f" State {state['id']}: {state['datetime']} "
|
|
337
344
|
f"[{state['source']}]{active_marker}"
|
|
@@ -340,7 +347,10 @@ def _display_calibration_info(input: Path, use_recalibrated: bool) -> None:
|
|
|
340
347
|
if use_recalibrated:
|
|
341
348
|
click.echo(f"\nUsing active calibration state (State {active_id})")
|
|
342
349
|
else:
|
|
343
|
-
click.echo(
|
|
350
|
+
click.echo(
|
|
351
|
+
"\nUsing original calibration (--no-recalibrated flag set): "
|
|
352
|
+
"none of these states"
|
|
353
|
+
)
|
|
344
354
|
|
|
345
355
|
click.echo(
|
|
346
356
|
"\nNote: this lists the states only; one cannot be chosen here. "
|
|
@@ -51,21 +51,27 @@ from tqdm import tqdm
|
|
|
51
51
|
|
|
52
52
|
from ...core.conversion_state import ConversionState
|
|
53
53
|
from ...errors import ConversionRefused
|
|
54
|
+
from ...metadata.schema import MSI_OBS_ACQUISITION_ORDER_COLUMN
|
|
54
55
|
from .base_spatialdata_converter import BaseSpatialDataConverter
|
|
55
56
|
from .csc_assembly import CscAssembly, index_dtype
|
|
56
57
|
|
|
57
58
|
logger = logging.getLogger(__name__)
|
|
58
59
|
|
|
60
|
+
#: The acquisition order of a grid position no spectrum has reached yet:
|
|
61
|
+
#: larger than any real one, so the first spectrum's order replaces it.
|
|
62
|
+
_NOT_ACQUIRED = np.iinfo(np.int64).max
|
|
63
|
+
|
|
59
64
|
|
|
60
65
|
class _TableUnit:
|
|
61
66
|
"""One table of the store in the making: a z plane, or the whole volume.
|
|
62
67
|
|
|
63
68
|
Holds what the two passes accumulate for it -- the column counts and
|
|
64
69
|
the scattered matrix (``assembly``), which grid positions carry a
|
|
65
|
-
spectrum (``occupancy``)
|
|
66
|
-
|
|
67
|
-
row
|
|
68
|
-
|
|
70
|
+
spectrum (``occupancy``), the TIC raster and, when the reader knows
|
|
71
|
+
it, each position's acquisition order -- and, once pass 1 is done,
|
|
72
|
+
the row layout: ``kept_grid`` is the grid index of each table row and
|
|
73
|
+
``row_of_grid`` the table row of each grid position (-1 when dropped
|
|
74
|
+
as empty).
|
|
69
75
|
|
|
70
76
|
Args:
|
|
71
77
|
key: The table's element key (``{id}_z{z}`` or ``{id}``).
|
|
@@ -75,6 +81,8 @@ class _TableUnit:
|
|
|
75
81
|
n_cols: Length of the mass axis.
|
|
76
82
|
tic_shape: ``(n_y, n_x)`` for a plane, ``(n_z, n_y, n_x)`` for a
|
|
77
83
|
volume.
|
|
84
|
+
records_order: Whether every spectrum comes with its acquisition
|
|
85
|
+
order (the reader's ``has_acquisition_order``).
|
|
78
86
|
"""
|
|
79
87
|
|
|
80
88
|
def __init__(
|
|
@@ -85,6 +93,7 @@ class _TableUnit:
|
|
|
85
93
|
n_grid: int,
|
|
86
94
|
n_cols: int,
|
|
87
95
|
tic_shape: Tuple[int, ...],
|
|
96
|
+
records_order: bool = False,
|
|
88
97
|
) -> None:
|
|
89
98
|
"""Allocate the pass-1 accumulators; the matrix comes after pass 1."""
|
|
90
99
|
self.key = key
|
|
@@ -104,14 +113,31 @@ class _TableUnit:
|
|
|
104
113
|
#: coordinate to name if it does not come out as pass 1's.
|
|
105
114
|
self.observed: Dict[int, Tuple[float, Tuple[int, int, int]]] = {}
|
|
106
115
|
self.tic = np.zeros(tic_shape, dtype=np.float64)
|
|
116
|
+
#: Per grid position, the smallest acquisition order among the
|
|
117
|
+
#: spectra it got; ``None`` when the reader does not know the
|
|
118
|
+
#: order. A position no spectrum reached keeps the sentinel and
|
|
119
|
+
#: never becomes a row, so no row can read it.
|
|
120
|
+
self.acquisition_order: Optional[NDArray[np.int64]] = (
|
|
121
|
+
np.full(self.n_grid, _NOT_ACQUIRED, dtype=np.int64)
|
|
122
|
+
if records_order
|
|
123
|
+
else None
|
|
124
|
+
)
|
|
107
125
|
self.kept_grid: Optional[NDArray[np.int64]] = None
|
|
108
126
|
self.row_of_grid: Optional[NDArray[np.int64]] = None
|
|
109
127
|
self.n_rows = 0
|
|
110
128
|
|
|
111
129
|
def count(
|
|
112
|
-
self,
|
|
130
|
+
self,
|
|
131
|
+
grid: int,
|
|
132
|
+
mz_indices: NDArray[np.int_],
|
|
133
|
+
values: NDArray[np.float64],
|
|
134
|
+
order: Optional[int] = None,
|
|
113
135
|
) -> None:
|
|
114
|
-
"""Pass 1: one spectrum at grid position ``grid``.
|
|
136
|
+
"""Pass 1: one spectrum at grid position ``grid``.
|
|
137
|
+
|
|
138
|
+
``order`` is the spectrum's acquisition order, required when the
|
|
139
|
+
unit records one.
|
|
140
|
+
"""
|
|
115
141
|
self.assembly.count(grid, mz_indices)
|
|
116
142
|
if self.occupancy[grid]:
|
|
117
143
|
# A pixel the source measured twice. One position is one row,
|
|
@@ -122,6 +148,16 @@ class _TableUnit:
|
|
|
122
148
|
self.assembly.merge_duplicates = True
|
|
123
149
|
self.occupancy[grid] = True
|
|
124
150
|
self.tic.reshape(-1)[grid] += values.sum()
|
|
151
|
+
if self.acquisition_order is not None:
|
|
152
|
+
if order is None:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"{self.key}: a spectrum reached the table without its "
|
|
155
|
+
"acquisition order"
|
|
156
|
+
)
|
|
157
|
+
# A row summed from two measurements was first measured at the
|
|
158
|
+
# earlier one, whatever order the reader yields them in.
|
|
159
|
+
if order < self.acquisition_order[grid]:
|
|
160
|
+
self.acquisition_order[grid] = order
|
|
125
161
|
|
|
126
162
|
def finish_counting(self) -> None:
|
|
127
163
|
"""Decide which grid positions become rows, and in what order.
|
|
@@ -436,6 +472,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
436
472
|
raise ValueError("Common mass axis is not initialized")
|
|
437
473
|
n_x, n_y, n_z = self._dimensions
|
|
438
474
|
n_cols = len(self._common_mass_axis)
|
|
475
|
+
records_order = bool(self.reader.has_acquisition_order)
|
|
439
476
|
if self.handle_3d:
|
|
440
477
|
return [
|
|
441
478
|
_TableUnit(
|
|
@@ -445,6 +482,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
445
482
|
n_x * n_y * n_z,
|
|
446
483
|
n_cols,
|
|
447
484
|
(n_z, n_y, n_x),
|
|
485
|
+
records_order,
|
|
448
486
|
)
|
|
449
487
|
]
|
|
450
488
|
return [
|
|
@@ -455,6 +493,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
455
493
|
n_x * n_y,
|
|
456
494
|
n_cols,
|
|
457
495
|
(n_y, n_x),
|
|
496
|
+
records_order,
|
|
458
497
|
)
|
|
459
498
|
for z in range(n_z)
|
|
460
499
|
]
|
|
@@ -533,22 +572,36 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
533
572
|
return units[0], z * n_x * n_y + y * n_x + x
|
|
534
573
|
return units[z], y * n_x + x
|
|
535
574
|
|
|
536
|
-
def _iter_pass_spectra(
|
|
537
|
-
|
|
538
|
-
|
|
575
|
+
def _iter_pass_spectra(self, passes: Any, phase: str) -> Generator[
|
|
576
|
+
Tuple[Tuple[int, int, int], Optional[int], Any, Any, Any],
|
|
577
|
+
None,
|
|
578
|
+
None,
|
|
579
|
+
]:
|
|
539
580
|
"""The summed spectra of one pass, with the frame record they came from.
|
|
540
581
|
|
|
541
|
-
Yields ``(coords, mzs, intensities, frame)``: ``
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
582
|
+
Yields ``(coords, order, mzs, intensities, frame)``: ``order`` is
|
|
583
|
+
the spectrum's acquisition order, or ``None`` when the reader does
|
|
584
|
+
not know it, and ``frame`` is the record the sinks are fed from,
|
|
585
|
+
or ``None`` when the pass reads the summed spectra directly (no
|
|
586
|
+
sinks, or a reader without records). A frame whose summed spectrum
|
|
587
|
+
is empty is not yielded, exactly as ``iter_spectra`` never yields
|
|
588
|
+
it, but the sinks of ``phase`` (``"count"`` or ``"scatter"``) still
|
|
589
|
+
see it with no row, which is what their own pass would have shown
|
|
590
|
+
them.
|
|
548
591
|
"""
|
|
592
|
+
ordered = bool(self.reader.has_acquisition_order)
|
|
549
593
|
if passes is None or passes.empty:
|
|
594
|
+
if ordered:
|
|
595
|
+
for (
|
|
596
|
+
coords,
|
|
597
|
+
order,
|
|
598
|
+
mzs,
|
|
599
|
+
intensities,
|
|
600
|
+
) in self.reader.iter_spectra_with_acquisition_order():
|
|
601
|
+
yield coords, order, mzs, intensities, None
|
|
602
|
+
return
|
|
550
603
|
for coords, mzs, intensities in self.reader.iter_spectra():
|
|
551
|
-
yield coords, mzs, intensities, None
|
|
604
|
+
yield coords, None, mzs, intensities, None
|
|
552
605
|
return
|
|
553
606
|
feed = getattr(passes, phase)
|
|
554
607
|
for frame in self.reader.iter_frame_scans():
|
|
@@ -556,7 +609,8 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
556
609
|
if spectrum is None:
|
|
557
610
|
feed(frame, None)
|
|
558
611
|
continue
|
|
559
|
-
|
|
612
|
+
order = frame.acquisition_order if ordered else None
|
|
613
|
+
yield frame.coords, order, spectrum[0], spectrum[1], frame
|
|
560
614
|
|
|
561
615
|
def _process_spectra(self, state: ConversionState) -> None:
|
|
562
616
|
"""Run both passes: count, size the arrays, scatter.
|
|
@@ -762,7 +816,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
762
816
|
desc="Pre-scan" if passes is None else "Pre-scan + sibling tables",
|
|
763
817
|
unit="spectrum",
|
|
764
818
|
) as pbar:
|
|
765
|
-
for coords, mzs, intensities, frame in self._iter_pass_spectra(
|
|
819
|
+
for coords, order, mzs, intensities, frame in self._iter_pass_spectra(
|
|
766
820
|
passes, "count"
|
|
767
821
|
):
|
|
768
822
|
x, y, z = coords
|
|
@@ -791,7 +845,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
791
845
|
# never writes -- explicit zeros, out of order, which
|
|
792
846
|
# scipy reports as a non-canonical matrix.
|
|
793
847
|
if unit is not None:
|
|
794
|
-
unit.count(grid, mz_indices, values)
|
|
848
|
+
unit.count(grid, mz_indices, values, order)
|
|
795
849
|
|
|
796
850
|
# Behind the same check, and for the same reason
|
|
797
851
|
# the per-table average is taken over rows: the
|
|
@@ -876,7 +930,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
876
930
|
desc="Scatter to CSC" if passes is None else "Scatter to CSC + siblings",
|
|
877
931
|
unit="spectrum",
|
|
878
932
|
) as pbar:
|
|
879
|
-
for coords, mzs, intensities, frame in self._iter_pass_spectra(
|
|
933
|
+
for coords, _, mzs, intensities, frame in self._iter_pass_spectra(
|
|
880
934
|
passes, "scatter"
|
|
881
935
|
):
|
|
882
936
|
x, y, z = coords
|
|
@@ -1010,6 +1064,9 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
1010
1064
|
the in-plane pitch so the table lands in the same micrometre frame
|
|
1011
1065
|
as the TIC volume's ``Scale``. Column order and dtypes are the ones
|
|
1012
1066
|
the two in-memory converters wrote, so a store reads back the same.
|
|
1067
|
+
``acquisition_order`` follows, int64, when the reader knows the
|
|
1068
|
+
order (design decision D26); the rows stay in grid order either
|
|
1069
|
+
way.
|
|
1013
1070
|
"""
|
|
1014
1071
|
if unit.kept_grid is None or self._dimensions is None:
|
|
1015
1072
|
raise RuntimeError("The row layout is not decided yet")
|
|
@@ -1053,6 +1110,11 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
|
|
|
1053
1110
|
obs.set_index("instance_id", inplace=True)
|
|
1054
1111
|
# Always add per-pixel region numbers for a consistent schema.
|
|
1055
1112
|
obs["region_number"] = self.build_region_numbers(x_idx, y_idx)
|
|
1113
|
+
# Indexed by the same kept grid as every column above, so a row's
|
|
1114
|
+
# order is its own position's, region filtering included: the
|
|
1115
|
+
# filter removes frames before pass 1 ever sees them.
|
|
1116
|
+
if unit.acquisition_order is not None:
|
|
1117
|
+
obs[MSI_OBS_ACQUISITION_ORDER_COLUMN] = unit.acquisition_order[kept]
|
|
1056
1118
|
return obs
|
|
1057
1119
|
|
|
1058
1120
|
def _tic_image(self, unit: _TableUnit) -> Any:
|
|
@@ -319,6 +319,54 @@ class BaseMSIReader(ABC):
|
|
|
319
319
|
"""
|
|
320
320
|
return None
|
|
321
321
|
|
|
322
|
+
# ------------------------------------------------------------------
|
|
323
|
+
# Acquisition order
|
|
324
|
+
#
|
|
325
|
+
# When a spectrum was recorded, relative to the others. ``iter_spectra``
|
|
326
|
+
# says where each spectrum sits and nothing about when, and the rows a
|
|
327
|
+
# converter writes follow the grid, which a serpentine scan or a slide
|
|
328
|
+
# of several regions does not. A drift plotted against the rows is
|
|
329
|
+
# plotted against raster order.
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def has_acquisition_order(self) -> bool:
|
|
334
|
+
"""Whether the source says in which order its spectra were acquired."""
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def iter_spectra_with_acquisition_order(self) -> Generator[
|
|
338
|
+
Tuple[
|
|
339
|
+
Tuple[int, int, int],
|
|
340
|
+
int,
|
|
341
|
+
NDArray[np.float64],
|
|
342
|
+
NDArray[np.float64],
|
|
343
|
+
],
|
|
344
|
+
None,
|
|
345
|
+
None,
|
|
346
|
+
]:
|
|
347
|
+
""":meth:`iter_spectra`, with each spectrum's acquisition order.
|
|
348
|
+
|
|
349
|
+
Yields ``((x, y, z), order, mzs, intensities)``: the spectra
|
|
350
|
+
:meth:`iter_spectra` yields, in its order, with its coordinates and
|
|
351
|
+
arrays. ``order`` grows with acquisition time and no two spectra
|
|
352
|
+
share one. It is the source's own number for the spectrum where the
|
|
353
|
+
source numbers them (a Bruker ``Frames.Id``), else the spectrum's
|
|
354
|
+
0-based position in the sequence the source lists them in. So it
|
|
355
|
+
need not start at 0 or be contiguous: sort by it, never index with
|
|
356
|
+
it.
|
|
357
|
+
|
|
358
|
+
The converter reads the spectra through this method whenever
|
|
359
|
+
:attr:`has_acquisition_order` is True, so a subclass that changes
|
|
360
|
+
what :meth:`iter_spectra` yields has to change this as well.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
NotImplementedError: When :attr:`has_acquisition_order` is False.
|
|
364
|
+
"""
|
|
365
|
+
raise NotImplementedError(
|
|
366
|
+
f"{type(self).__name__} does not know the order its spectra were "
|
|
367
|
+
"acquired in"
|
|
368
|
+
)
|
|
369
|
+
|
|
322
370
|
# ------------------------------------------------------------------
|
|
323
371
|
# One read per frame for every table
|
|
324
372
|
#
|
|
@@ -46,6 +46,13 @@ class FrameScans(Protocol):
|
|
|
46
46
|
|
|
47
47
|
coords: Coords
|
|
48
48
|
|
|
49
|
+
@property
|
|
50
|
+
def acquisition_order(self) -> Optional[int]:
|
|
51
|
+
"""The frame's order as ``iter_spectra_with_acquisition_order`` yields it.
|
|
52
|
+
|
|
53
|
+
``None`` when the reader does not know the order.
|
|
54
|
+
"""
|
|
55
|
+
|
|
49
56
|
def spectrum(self) -> Optional[Spectrum]:
|
|
50
57
|
"""``(mzs, intensities)`` as :meth:`BaseMSIReader.iter_spectra` yields, or ``None``.
|
|
51
58
|
|
|
@@ -33,6 +33,18 @@ UM_PER_UNIT = {
|
|
|
33
33
|
"UO:0000018": 0.001, # nanometer
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
# The name IMS:1000046 carried until imagingMS.obo commit 421481e of
|
|
37
|
+
# 2017-09-07. Under that name the value is the AREA of a pixel, and
|
|
38
|
+
# IMS:1000047 is "image shape", which is not a size at all. Files written
|
|
39
|
+
# under the old vocabulary are still published.
|
|
40
|
+
OLD_PIXEL_SIZE_NAME = "pixel size"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def has_old_pixel_size_name(name: object) -> bool:
|
|
44
|
+
"""Whether IMS:1000046 is spelled as it was when it gave an area."""
|
|
45
|
+
return isinstance(name, str) and name.strip().lower() == OLD_PIXEL_SIZE_NAME
|
|
46
|
+
|
|
47
|
+
|
|
36
48
|
# Analyzer-component terms, in preference order. The first one found under a
|
|
37
49
|
# <componentList><analyzer> is surfaced as ``instrument_info["analyzer"]``, so
|
|
38
50
|
# on a hybrid that declares several analyzers the highest-resolution stage --
|
|
@@ -533,12 +545,21 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
533
545
|
nanometre pixel size as micrometres, 1000x too large, and
|
|
534
546
|
``convert_msi`` still returned ``True``. The unit survives on the
|
|
535
547
|
ParamGroup path, so it is read from there and the value converted.
|
|
548
|
+
|
|
549
|
+
The name the file gave the term survives there too. Under its old
|
|
550
|
+
name the value is an area, so it is not read, see
|
|
551
|
+
:meth:`_is_old_pixel_size`.
|
|
536
552
|
"""
|
|
537
553
|
if hasattr(self.parser, "imzmldict") and self.parser.imzmldict:
|
|
538
554
|
# Check for pixel size parameters in the parsed dictionary
|
|
539
555
|
x_size = self.parser.imzmldict.get("pixel size x")
|
|
540
556
|
y_size = self.parser.imzmldict.get("pixel size y")
|
|
541
557
|
|
|
558
|
+
if x_size is not None and self._is_old_pixel_size(
|
|
559
|
+
self._pixel_size_raw_name(), x_size
|
|
560
|
+
):
|
|
561
|
+
return None
|
|
562
|
+
|
|
542
563
|
if x_size is not None and y_size is not None:
|
|
543
564
|
try:
|
|
544
565
|
x_size, y_size = float(x_size), float(y_size)
|
|
@@ -582,6 +603,44 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
582
603
|
units[accession] = unit_accession
|
|
583
604
|
return units
|
|
584
605
|
|
|
606
|
+
def _pixel_size_raw_name(self) -> Optional[str]:
|
|
607
|
+
"""The name the file itself gives IMS:1000046, or ``None``.
|
|
608
|
+
|
|
609
|
+
pyimzml renames the term to its current name while parsing, so
|
|
610
|
+
``name`` always reads "pixel size (x)". ``raw_name`` is what the
|
|
611
|
+
document says. First declaration wins, as for the unit.
|
|
612
|
+
"""
|
|
613
|
+
metadata = getattr(self.parser, "metadata", None)
|
|
614
|
+
scan_settings = getattr(metadata, "scan_settings", None)
|
|
615
|
+
if not isinstance(scan_settings, dict):
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
for group in scan_settings.values():
|
|
619
|
+
for param in getattr(group, "cv_params", []):
|
|
620
|
+
if param[1] == ImzMLAccessions.PIXEL_SIZE_X:
|
|
621
|
+
raw_name = param[3]
|
|
622
|
+
return raw_name if isinstance(raw_name, str) else None
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
def _is_old_pixel_size(self, name: object, value: object) -> bool:
|
|
626
|
+
"""Whether IMS:1000046 is written under its name of before 2017.
|
|
627
|
+
|
|
628
|
+
Under that name the value is the area of a pixel. Read as a length
|
|
629
|
+
it is wrong by its own square root: 10000 for a 100 um pixel. So it
|
|
630
|
+
is not read, the log says why, and the conversion asks for
|
|
631
|
+
``--pixel-size``.
|
|
632
|
+
"""
|
|
633
|
+
if not has_old_pixel_size_name(name):
|
|
634
|
+
return False
|
|
635
|
+
logger.warning(
|
|
636
|
+
'%s names IMS:1000046 "pixel size", as the vocabulary did before '
|
|
637
|
+
"2017, when the value (%r) was the area of a pixel. It is not "
|
|
638
|
+
"read as a pixel size. Pass --pixel-size.",
|
|
639
|
+
Path(self.imzml_path).name,
|
|
640
|
+
value,
|
|
641
|
+
)
|
|
642
|
+
return True
|
|
643
|
+
|
|
585
644
|
def _pixel_size_to_um(self, value: float, unit_accession: Optional[str]) -> float:
|
|
586
645
|
"""Convert a declared pixel size to micrometres, or refuse.
|
|
587
646
|
|
|
@@ -1170,11 +1229,15 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
1170
1229
|
The unit conversion happens outside the ``try`` so a refused unit
|
|
1171
1230
|
propagates as loudly as it does on the fast path, while a merely
|
|
1172
1231
|
unparseable document still degrades to "no pixel size".
|
|
1232
|
+
|
|
1233
|
+
IMS:1000046 under its old name is not read here either, see
|
|
1234
|
+
:meth:`_is_old_pixel_size`.
|
|
1173
1235
|
"""
|
|
1174
1236
|
x_size = None
|
|
1175
1237
|
y_size = None
|
|
1176
1238
|
x_unit = None
|
|
1177
1239
|
y_unit = None
|
|
1240
|
+
x_name = None
|
|
1178
1241
|
|
|
1179
1242
|
try:
|
|
1180
1243
|
if not hasattr(self.parser, "metadata") or not hasattr(
|
|
@@ -1196,6 +1259,7 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
1196
1259
|
if accession == ImzMLAccessions.PIXEL_SIZE_X:
|
|
1197
1260
|
x_size = float(cvparam.get("value", 0))
|
|
1198
1261
|
x_unit = cvparam.get("unitAccession")
|
|
1262
|
+
x_name = cvparam.get("name")
|
|
1199
1263
|
elif accession == ImzMLAccessions.PIXEL_SIZE_Y:
|
|
1200
1264
|
y_size = float(cvparam.get("value", 0))
|
|
1201
1265
|
y_unit = cvparam.get("unitAccession")
|
|
@@ -1204,6 +1268,9 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
1204
1268
|
logger.warning(f"Failed to parse XML metadata for pixel size: {e}")
|
|
1205
1269
|
return None
|
|
1206
1270
|
|
|
1271
|
+
if x_size is not None and self._is_old_pixel_size(x_name, x_size):
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1207
1274
|
if x_size is not None and y_size is not None:
|
|
1208
1275
|
x_um = self._pixel_size_to_um(x_size, x_unit)
|
|
1209
1276
|
y_um = self._pixel_size_to_um(y_size, y_unit)
|