thyra 2.2.2__tar.gz → 2.2.4__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.2 → thyra-2.2.4}/PKG-INFO +2 -2
- {thyra-2.2.2 → thyra-2.2.4}/pyproject.toml +11 -2
- {thyra-2.2.2 → thyra-2.2.4}/thyra/__init__.py +1 -1
- thyra-2.2.4/thyra/converters/spatialdata/_chunking.py +89 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/base_spatialdata_converter.py +7 -3
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/extractors/imzml_extractor.py +84 -14
- thyra-2.2.2/thyra/converters/spatialdata/_chunking.py +0 -41
- {thyra-2.2.2 → thyra-2.2.4}/LICENSE +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/README.md +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/__main__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/alignment/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/alignment/affine.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/alignment/teaching_points.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/config.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/convert.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/spatialdata_2d_converter.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/spatialdata_3d_converter.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/core/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/core/base_converter.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/core/base_extractor.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/core/base_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/core/registry.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/types.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/metadata/validator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/preview.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/common_axis.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/constants.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/decision_tree.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/gaps.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/strategies/base.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/tic.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/resampling/types.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/tools/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/tools/check_ontology.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/tools/make_example_data.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/utils/__init__.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/utils/logging_config.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/utils/windows_paths.py +0 -0
- {thyra-2.2.2 → thyra-2.2.4}/thyra/utils/zarr_atomic_write.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thyra
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
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
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -39,7 +39,7 @@ Requires-Dist: pyimzML (>=1.4.0)
|
|
|
39
39
|
Requires-Dist: scipy (>=1.7.0)
|
|
40
40
|
Requires-Dist: spatialdata (>=0.8.0,<0.9)
|
|
41
41
|
Requires-Dist: tqdm (>=4.50.0)
|
|
42
|
-
Requires-Dist: zarr (>=3.
|
|
42
|
+
Requires-Dist: zarr (>=3.1.4,<3.2)
|
|
43
43
|
Project-URL: Bug Tracker, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/issues
|
|
44
44
|
Project-URL: Changelog, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CHANGELOG.md
|
|
45
45
|
Project-URL: Contributing, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra/blob/main/CONTRIBUTING.md
|
|
@@ -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.4"
|
|
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>"]
|
|
@@ -86,7 +86,16 @@ spatialdata = ">=0.8.0, <0.9" # tested 0.8.0
|
|
|
86
86
|
# bit ome_zarr 0.12.x with newer dask, and spatialdata 0.8 requires >= 0.16.
|
|
87
87
|
ome-zarr = ">=0.16.0, <0.19" # tested 0.18.0
|
|
88
88
|
tqdm = ">=4.50.0"
|
|
89
|
-
|
|
89
|
+
# Floor is 3.1.4, and it is load-bearing. anndata >= 0.13 writes every zarr v3
|
|
90
|
+
# array with `shards="auto"`. zarr < 3.1.4 sized the auto chunk with
|
|
91
|
+
# `max_bytes=1024` where its own comment says "aim for a 1MiB chunk" (compare
|
|
92
|
+
# `_auto_partition` in 3.1.3 vs 3.1.4), so one shard -- one FILE -- came out at
|
|
93
|
+
# ~1 KB. Measured on test_data/pea.imzML: 393,310 files for a single table and
|
|
94
|
+
# 434 s of a 445 s conversion spent in the write, against 12 files and 2.7 s on
|
|
95
|
+
# 3.1.6. Nothing in Thyra changed; the anndata 0.13 bump alone did this.
|
|
96
|
+
# 3.1.4 is also where `array.target_shard_size_bytes` arrives, which is the
|
|
97
|
+
# knob _chunking.table_write_config sets to claim the budget from anndata.
|
|
98
|
+
zarr = ">=3.1.4, <3.2" # tested 3.1.6
|
|
90
99
|
imagecodecs = ">=2024.1.1" # For reading compressed TIFF optical images
|
|
91
100
|
# Floor is 0.13.2, not 0.13.0. anndata 0.13.0 imports
|
|
92
101
|
# `typing_extensions.sentinel` (needs typing-extensions >= 4.16) but declares
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Chunk/shard policy for the zarr Thyra writes: rasters and the MSI table.
|
|
2
|
+
|
|
3
|
+
Single source of truth for how Thyra chunks the raster images it writes to
|
|
4
|
+
SpatialData zarr -- and the designated seam for tile-aligned zarr v3 sharding
|
|
5
|
+
once scverse/spatialdata PR #1106 (``raster_write_kwargs``) lands.
|
|
6
|
+
|
|
7
|
+
Cross-repo contract: the viewer (Ousia) morphology tile server streams
|
|
8
|
+
``DEFAULT_TILE_SIZE`` (= 512) edge tiles. Once sharding lands, the zarr INNER
|
|
9
|
+
chunk edge must equal that so one served tile == one decompress. Thyra is a
|
|
10
|
+
separate package and CANNOT import Ousia, so the coupling is DUPLICATED BY
|
|
11
|
+
CONTRACT here -- keep ``INNER_TILE_EDGE`` numerically in sync with Ousia's
|
|
12
|
+
``DEFAULT_TILE_SIZE``; a silent divergence reintroduces cold-tile amplification
|
|
13
|
+
on the viewer. See the Ousia ADR docs/ADR-pyramid-tile-sharding.md.
|
|
14
|
+
|
|
15
|
+
The raster policy above is passed per-array. The TABLE policy below cannot be:
|
|
16
|
+
``SpatialData.write`` hands the table straight to ``anndata.write_elem`` and
|
|
17
|
+
exposes no ``dataset_kwargs`` seam, so the only supported lever is zarr's own
|
|
18
|
+
config. ``table_write_config()`` is that lever, and every writer that reaches
|
|
19
|
+
zarr through spatialdata (the 2D, 3D and streaming-COO converters, all via
|
|
20
|
+
``_save_output``) must hold it open across the write. The streaming-PCS writer
|
|
21
|
+
hand-rolls its ``create_array`` calls and sizes them itself.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from typing import Iterator, Tuple
|
|
26
|
+
|
|
27
|
+
import zarr
|
|
28
|
+
|
|
29
|
+
# Outer chunk edge used TODAY (pre-sharding the chunk IS the outer block): a 4096
|
|
30
|
+
# block means a viewer 512-tile read decompresses at most one chunk per request.
|
|
31
|
+
IMAGE_CHUNK_EDGE = 4096
|
|
32
|
+
|
|
33
|
+
# Future zarr INNER chunk edge once #1106 lands == Ousia DEFAULT_TILE_SIZE (512).
|
|
34
|
+
# Duplicated by contract (Thyra cannot import Ousia).
|
|
35
|
+
INNER_TILE_EDGE = 512
|
|
36
|
+
|
|
37
|
+
# Uncompressed byte budget for ONE SHARD of a table array -- X/data, X/indices,
|
|
38
|
+
# X/indptr and every obs/var column anndata writes. One shard is one FILE, so
|
|
39
|
+
# this is what keeps a 36M-nnz table (test_data/bellini.imzML) in 11 files
|
|
40
|
+
# instead of 394,699.
|
|
41
|
+
#
|
|
42
|
+
# anndata >= 0.13 turns on ``shards="auto"`` for zarr v3 and would otherwise
|
|
43
|
+
# pick its own 1 GB budget; it explicitly yields to a caller-set value (see
|
|
44
|
+
# ``anndata._io.specs.methods.zarr_v3_sharding``). We set a smaller one because
|
|
45
|
+
# a shard is assembled in memory before it is written -- anndata's own comment
|
|
46
|
+
# says so -- and Thyra converts multi-GB acquisitions on workstations, where a
|
|
47
|
+
# 1 GB write buffer is a real cost. 128 MiB is still enormous relative to the
|
|
48
|
+
# file counts that hurt: test_data/pea.imzML (60.1M nnz, 481 MB of X/data)
|
|
49
|
+
# lands in 4 data files, so even a table 100x that size stays in the hundreds.
|
|
50
|
+
#
|
|
51
|
+
# The INNER chunk stays zarr's ~1 MiB auto pick: that is the unit a random read
|
|
52
|
+
# decompresses, and it is the size the non-sharded path used before anndata
|
|
53
|
+
# began auto-sharding, so read amplification is unchanged.
|
|
54
|
+
TABLE_SHARD_TARGET_BYTES = 128 * 1024 * 1024
|
|
55
|
+
|
|
56
|
+
# zarr < 3.1.4 computed the auto chunk with ``max_bytes=1024`` where 1 MiB was
|
|
57
|
+
# meant, so ``shards="auto"`` produced ~1 KB shards -- one file per KB of table.
|
|
58
|
+
# It also predates ``array.target_shard_size_bytes``, making the budget above
|
|
59
|
+
# unenforceable. pyproject pins the floor at 3.1.4 for exactly this reason;
|
|
60
|
+
# this constant is what the regression test checks the floor still buys us.
|
|
61
|
+
MIN_TABLE_SHARD_BYTES = 64 * 1024
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@contextmanager
|
|
65
|
+
def table_write_config() -> Iterator[None]:
|
|
66
|
+
"""Hold Thyra's table shard budget open across a ``SpatialData.write``.
|
|
67
|
+
|
|
68
|
+
Wraps zarr's global config, so it must stay open for the *whole* write --
|
|
69
|
+
the arrays are created lazily inside anndata, not up front.
|
|
70
|
+
"""
|
|
71
|
+
with zarr.config.set({"array.target_shard_size_bytes": TABLE_SHARD_TARGET_BYTES}):
|
|
72
|
+
yield
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def image_chunks(spatial_ndim: int, edge: int = IMAGE_CHUNK_EDGE) -> Tuple[int, ...]:
|
|
76
|
+
"""The zarr chunk tuple for a Thyra-written raster image (leading channel axis).
|
|
77
|
+
|
|
78
|
+
``spatial_ndim == 2`` (c, y, x) -> ``(1, edge, edge)``;
|
|
79
|
+
``spatial_ndim == 3`` (c, z, y, x) -> ``(1, 1, edge, edge)``.
|
|
80
|
+
|
|
81
|
+
This is the ONE place the future inner=``INNER_TILE_EDGE`` + outer-shard policy
|
|
82
|
+
will live when spatialdata PR #1106 ships ``raster_write_kwargs``. Today it
|
|
83
|
+
returns the pre-sharding behaviour, byte-for-byte.
|
|
84
|
+
"""
|
|
85
|
+
if spatial_ndim == 2:
|
|
86
|
+
return (1, edge, edge)
|
|
87
|
+
if spatial_ndim == 3:
|
|
88
|
+
return (1, 1, edge, edge)
|
|
89
|
+
raise ValueError(f"spatial_ndim must be 2 or 3, got {spatial_ndim}")
|
|
@@ -21,7 +21,7 @@ from ...resampling.gaps import zero_across_gaps
|
|
|
21
21
|
from ...resampling.tic import preserved_tic, rescale_to_preserved_tic
|
|
22
22
|
from ...resampling.types import ResamplingConfig
|
|
23
23
|
from ...utils.zarr_atomic_write import install_windows_atomic_write_retry
|
|
24
|
-
from ._chunking import image_chunks
|
|
24
|
+
from ._chunking import image_chunks, table_write_config
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
@@ -1921,8 +1921,12 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
1921
1921
|
# Add metadata
|
|
1922
1922
|
self.add_metadata(sdata)
|
|
1923
1923
|
|
|
1924
|
-
# Write to disk
|
|
1925
|
-
|
|
1924
|
+
# Write to disk. table_write_config() must wrap the write itself,
|
|
1925
|
+
# not just the SpatialData construction: anndata creates the table's
|
|
1926
|
+
# zarr arrays lazily inside sdata.write, and that is where the shard
|
|
1927
|
+
# budget is read. Without it the table lands in ~1 KB shards -- one
|
|
1928
|
+
# file per KB -- and the write dominates conversion wall-clock.
|
|
1929
|
+
with _suppress_upstream_warnings(), table_write_config():
|
|
1926
1930
|
sdata.write(str(self.output_path))
|
|
1927
1931
|
zarr.consolidate_metadata(str(self.output_path))
|
|
1928
1932
|
logger.info(f"Successfully saved SpatialData to {self.output_path}")
|
|
@@ -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
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""Image chunk/shard policy for Thyra-written SpatialData rasters.
|
|
2
|
-
|
|
3
|
-
Single source of truth for how Thyra chunks the raster images it writes to
|
|
4
|
-
SpatialData zarr -- and the designated seam for tile-aligned zarr v3 sharding
|
|
5
|
-
once scverse/spatialdata PR #1106 (``raster_write_kwargs``) lands.
|
|
6
|
-
|
|
7
|
-
Cross-repo contract: the viewer (Ousia) morphology tile server streams
|
|
8
|
-
``DEFAULT_TILE_SIZE`` (= 512) edge tiles. Once sharding lands, the zarr INNER
|
|
9
|
-
chunk edge must equal that so one served tile == one decompress. Thyra is a
|
|
10
|
-
separate package and CANNOT import Ousia, so the coupling is DUPLICATED BY
|
|
11
|
-
CONTRACT here -- keep ``INNER_TILE_EDGE`` numerically in sync with Ousia's
|
|
12
|
-
``DEFAULT_TILE_SIZE``; a silent divergence reintroduces cold-tile amplification
|
|
13
|
-
on the viewer. See the Ousia ADR docs/ADR-pyramid-tile-sharding.md.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from typing import Tuple
|
|
17
|
-
|
|
18
|
-
# Outer chunk edge used TODAY (pre-sharding the chunk IS the outer block): a 4096
|
|
19
|
-
# block means a viewer 512-tile read decompresses at most one chunk per request.
|
|
20
|
-
IMAGE_CHUNK_EDGE = 4096
|
|
21
|
-
|
|
22
|
-
# Future zarr INNER chunk edge once #1106 lands == Ousia DEFAULT_TILE_SIZE (512).
|
|
23
|
-
# Duplicated by contract (Thyra cannot import Ousia).
|
|
24
|
-
INNER_TILE_EDGE = 512
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def image_chunks(spatial_ndim: int, edge: int = IMAGE_CHUNK_EDGE) -> Tuple[int, ...]:
|
|
28
|
-
"""The zarr chunk tuple for a Thyra-written raster image (leading channel axis).
|
|
29
|
-
|
|
30
|
-
``spatial_ndim == 2`` (c, y, x) -> ``(1, edge, edge)``;
|
|
31
|
-
``spatial_ndim == 3`` (c, z, y, x) -> ``(1, 1, edge, edge)``.
|
|
32
|
-
|
|
33
|
-
This is the ONE place the future inner=``INNER_TILE_EDGE`` + outer-shard policy
|
|
34
|
-
will live when spatialdata PR #1106 ships ``raster_write_kwargs``. Today it
|
|
35
|
-
returns the pre-sharding behaviour, byte-for-byte.
|
|
36
|
-
"""
|
|
37
|
-
if spatial_ndim == 2:
|
|
38
|
-
return (1, edge, edge)
|
|
39
|
-
if spatial_ndim == 3:
|
|
40
|
-
return (1, 1, edge, edge)
|
|
41
|
-
raise ValueError(f"spatial_ndim must be 2 or 3, got {spatial_ndim}")
|
|
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
|