API Reference¶
In plain words
Change how Thyra converts shows the common settings from Python. This page documents every function.
Thyra's Python API centres on two functions: convert_msi, which does the
work, and preview_msi, which tells you what an input is without converting
it. For most use cases those are all you need. The remaining sections document
configuration types, metadata objects, and base classes for advanced users who
want to inspect results or extend Thyra with new formats.
Converting Data¶
The primary entry point. Detects the input format, reads metadata, and writes a SpatialData/Zarr directory.
Basic usage¶
from thyra import convert_msi
# Minimal -- auto-detects format and pixel size
success = convert_msi("input.imzML", "output.zarr")
# With explicit parameters
success = convert_msi(
"data/experiment.d",
"output/experiment.zarr",
dataset_id="hippocampus",
pixel_size_um=10.0,
)
The Python API does not resample by default
resampling_config defaults to None, and nothing builds one for you, so
the call above keeps the original mass axis. The thyra command-line tool
is the opposite -- it resamples unless you pass --no-resample, because
the default is applied in the CLI layer rather than in convert_msi. Pass
a resampling_config explicitly to get the CLI's behaviour from Python.
With resampling configuration¶
success = convert_msi(
"input.imzML",
"output.zarr",
resampling_config={
"method": "nearest_neighbor",
"axis_type": "orbitrap",
"target_bins": 50000,
},
)
Multi-region dataset (select one region)¶
region takes either a .mis Area Name or a DB RegionNumber. A string is
matched against the area names first and only parsed as a number if no name
matches, so the two forms below are not interchangeable -- area '03' need not
be RegionNumber 3.
# By the name flexImaging gives the area
success = convert_msi(
"data/slide.d",
"output/tissue_only.zarr",
region="03",
)
# By the database's own region number, which starts at 0
success = convert_msi(
"data/slide.d",
"output/tissue_only.zarr",
region=0,
)
Thyra logs the RegionNumber-to-Area-Name mapping at INFO when it opens a
multi-region dataset. See --region for the detail.
Only the timsTOF reader can select a region. Every other format refuses
region, and the call returns False.
Large datasets¶
Nothing to switch on: every conversion makes two passes over the source and
writes each table from memory-mapped arrays, so the matrix is never held in
RAM. The streaming keyword older code passes is accepted and selects
nothing.
Full signature¶
convert_msi(input_path: Union[str, Path], output_path: Union[str, Path], format_type: str = 'spatialdata', dataset_id: str = 'msi_dataset', pixel_size_um: Optional[float] = None, handle_3d: bool = False, z_spacing_um: Optional[float] = None, resampling_config: Optional[Union[Dict[str, Any], ResamplingConfig, MassAxisPlan]] = None, reader_options: Optional[Dict[str, Any]] = None, include_optical: bool = True, apply_optical_alignment: bool = True, streaming: Union[bool, Literal['auto']] = 'auto', region: Optional[Union[int, str]] = None, sample_id: Optional[str] = None, sample_name: Optional[str] = None, **kwargs: Any) -> bool
¶
Convert MSI data to the specified format.
Provides automatic pixel size detection from metadata or accepts user-specified values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Union[str, Path]
|
Path to input MSI data file or directory |
required |
output_path
|
Union[str, Path]
|
Path for output file |
required |
format_type
|
str
|
Output format type (default: "spatialdata") |
'spatialdata'
|
dataset_id
|
str
|
Identifier for the dataset |
'msi_dataset'
|
pixel_size_um
|
Optional[float]
|
In-plane pixel size in micrometers (None for auto) |
None
|
handle_3d
|
bool
|
Whether to process as 3D data (default: False) |
False
|
z_spacing_um
|
Optional[float]
|
Distance between consecutive slices in micrometers.
Only meaningful together with |
None
|
resampling_config
|
Optional[Union[Dict[str, Any], ResamplingConfig, MassAxisPlan]]
|
Optional resampling configuration: a dict, a
|
None
|
reader_options
|
Optional[Dict[str, Any]]
|
Optional format-specific reader options: - intensity_threshold: float - Minimum intensity to include. Default: None (no filtering). - use_recalibrated_state: bool - For Bruker data, use active/recalibrated calibration (default True). - tdf_spectrum: "scan_sum" | "vendor_centroid" - For Bruker TDF (TIMS) data, how a frame's mobility scans are collapsed into one spectrum per pixel (default "scan_sum"). - use_centroid: bool - For Waters .raw, whether MassLynx hands back the vendor centroid (True) or the profile trace (False). Default None: the profile trace on a SELECT SERIES MRT, the vendor centroid on every other Waters instrument. The CLI spells it --waters-spectrum. |
None
|
**kwargs
|
Any
|
Forwarded to the converter. |
{}
|
include_optical
|
bool
|
Include optical images (default: True) |
True
|
apply_optical_alignment
|
bool
|
If True (default) and the MSI source
carries FlexImaging Area metadata, MSI elements are placed
in optical-image pixel space at |
True
|
streaming
|
Union[bool, Literal['auto']]
|
Kept for compatibility; selects nothing. Every
conversion streams -- two passes over the source into
memory-mapped arrays, the matrix never held in RAM -- since
the in-memory converter was folded in (v3.23). |
'auto'
|
region
|
Optional[Union[int, str]]
|
Select one region of a multi-region Bruker timsTOF
|
None
|
sample_id
|
Optional[str]
|
Your identifier for the sample, written to
|
None
|
sample_name
|
Optional[str]
|
Your name for the sample, written to
|
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the conversion completed and the store was written, False |
bool
|
otherwise. Every failure comes back this way, refusals included: |
bool
|
a refusal Thyra planned for is logged once at |
bool
|
whole explanation and its traceback kept for |
bool
|
234), so a caller that wants the exception itself has to use the¶ |
bool
|
converter classes directly. |
bool
|
On failure the destination is cleared. Anything written to |
bool
|
|
bool
|
|
bool
|
there), so the same call can be retried without the caller |
bool
|
deleting a directory first, and a half-written store is not left |
bool
|
where a finished one is expected -- it would not open, but it |
bool
|
looks like one. When the rename itself fails -- a live handle on |
bool
|
the directory is the usual reason on Windows -- the partial store |
bool
|
stays put and the failure says so at |
bool
|
to delete; a retry is then refused until it is gone. An |
bool
|
|
bool
|
refused untouched, never renamed. See |
bool
|
func: |
Previewing an Input¶
preview_msi answers "what is this file?" without converting it. It detects
the format, builds the reader, and reads metadata only -- no spectra are
decoded and no store is written -- so it is cheap enough to call while a user
waits. It is the entry point behind the Ousia import wizard's per-sample
preview card.
Two properties make it usable directly from UI code:
- It never raises. Any failure -- a path that does not exist, an
unrecognised format, a truncated file -- comes back as an
MsiPreviewwithreadable=Falseanderrorset to the message. Checkreadablebefore reading the numeric fields; they hold zeroes when it isFalse. - It is fast. The design budget is under 500 ms for inputs up to about
50 GB, because it never touches the spectra. Each format answers from its
header: a Bruker
.dfromanalysis.tdf, an imzML from the block before<run>-- 0.4 ms whether that document is 29 MB or 2.0 GiB. One step is bounded by the file rather than by its header, and it is worth knowing about: a processed-mode imzML records its mass range per spectrum, so reading it costs a pass over the XML (3.9 s on a 2.0 GiB document, 53 ms on a 29 MB one). A continuous-mode one shares a single m/z array across every spectrum, so its first spectrum states the range and the pass stops there. Neither pass opens the.ibd.
from pathlib import Path
from thyra import preview_msi
p = preview_msi(Path("example_data/synthetic_brain.imzML"))
if p.readable:
print(p.grid_dims) # (48, 36)
print(p.n_pixels) # 1728
print(p.mz_range) # (250.0, 1200.0)
print(p.pixel_size_um) # 25.0
print(p.instrument_type) # AxisType.CONSTANT
else:
print("cannot read:", p.error)
Failure looks the same shape, which is the point:
p = preview_msi(Path("no/such/file.imzML"))
print(p.readable, p.error)
# False Path does not exist: no\such\file.imzML
Two fields can come back as None rather than a number, and both mean "the
file does not say, and a preview will not decode spectra to find out":
n_pixels for a PHI acquisition, whose occupied pixels are a property of the
ion-event stream; and mz_range for an imzML written without the observed-m/z
cvParams MS:1000528 / MS:1000527, which IONTOF SurfaceLab omits. Neither is
filled in with a plausible substitute -- taking an imzML's range from its first
spectrum was measured 28 Da narrow on a real file.
instrument_type is the AxisType the resampling decision tree would pick for
this input, so a caller can show the default before the user commits to it.
tof_law is the (A, B) width-law pair the instrument declares, or None.
Pass one pair as tof_a/tof_b to every conversion that should share a
tof axis.
has_escdat_folder reports whether <path>/EscDat/ exists, which is how a
downstream tool decides whether EscDat-derived registration is available.
preview_msi(path: Path) -> MsiPreview
¶
Return a metadata-only snapshot of an MSI input.
This is the entry point used by the Ousia Import Wizard to drive
the per-sample preview card in step 2. It detects the format,
constructs the appropriate reader, and calls
:meth:BaseMSIReader.get_essential_metadata (plus
:meth:get_comprehensive_metadata for the instrument-type guess).
No spectra are decoded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Filesystem path to an |
required |
Returns:
| Type | Description |
|---|---|
MsiPreview
|
class: |
MsiPreview
|
attr: |
Example
from thyra import preview_msi p = preview_msi(Path("slice_01.imzML")) if p.readable: ... print(p.grid_dims, p.mz_range)
MsiPreview(mz_range: Optional[Tuple[float, float]], n_pixels: Optional[int], grid_dims: Tuple[int, int], instrument_type: Optional[AxisType], pixel_size_um: Optional[float], has_escdat_folder: bool, readable: bool, error: Optional[str] = None, resampling_method: Optional[ResamplingMethod] = None, regions: Optional[List[Dict[str, Any]]] = None, tof_law: Optional[Tuple[float, float]] = None)
dataclass
¶
Metadata-only snapshot of an MSI input.
Attributes:
| Name | Type | Description |
|---|---|---|
mz_range |
Optional[Tuple[float, float]]
|
|
n_pixels |
Optional[int]
|
Number of spectra (pixels) the dataset actually holds --
the positions that carry a measurement, not the extent of the
raster, which is :attr: |
grid_dims |
Tuple[int, int]
|
Grid dimensions as |
instrument_type |
Optional[AxisType]
|
The :class: |
pixel_size_um |
Optional[float]
|
Pixel pitch in micrometres. Thyra stores a
|
has_escdat_folder |
bool
|
|
readable |
bool
|
|
error |
Optional[str]
|
The exception's |
resampling_method |
Optional[ResamplingMethod]
|
The :class: |
regions |
Optional[List[Dict[str, Any]]]
|
One entry per acquisition region for a MULTI-region
input -- a Bruker Read this before trusting :attr:
|
tof_law |
Optional[Tuple[float, float]]
|
The |
Planning One Mass Axis¶
plan_mass_axis reads the header of each source (no spectra) and decides one
mass axis for all of them. The returned MassAxisPlan is passed to
convert_msi as resampling_config, and can be saved to a JSON file for the
--mass-axis-plan command-line option. See
One axis for several data sets.
plan_mass_axis(sources: Union[str, Path, Sequence[Union[str, Path]]], *, axis_type: Optional[Union[str, AxisType]] = None, min_mz: Optional[float] = None, max_mz: Optional[float] = None, width_at_mz: Optional[float] = None, reference_mz: Optional[float] = None, tof_a: Optional[float] = None, tof_b: Optional[float] = None, bins_per_fwhm: Optional[float] = None, method: Optional[Union[str, ResamplingMethod]] = None) -> MassAxisPlan
¶
Plan one mass axis for several sources.
Each source is read the way :func:thyra.preview_msi reads it: its
header only, no spectra. Every setting you pass is used as given. The
rest are decided from the sources, and the plan records where each came
from in origins and what it costs a source in warnings.
- Axis type: every source's detector agrees, else refused.
- Range: the intersection of the sources' ranges. An imzML source's
range is its header text moved out by one unit of the last digit it
states (
100.123457by 1e-6,1000by 1), because the header rounds it and would otherwise cut the source's edge samples. - Width (non-
tofaxes): the widest width any source declares, each first stated atreference_mzunder the axis law, so that on the shared axis no source's peaks scatter over bins finer than they are. The narrower sources lose resolution and are named in the warnings. tofaxes: the width law every declaring source agrees on.
The plan fixes the axis only. Each conversion keeps its own detector's
resampling method unless you force method. Pass the plan as
resampling_config to :func:thyra.convert_msi; the conversion
refuses when the axis it builds hashes differently from the plan's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sources
|
Union[str, Path, Sequence[Union[str, Path]]]
|
Paths of the sources, or one path. |
required |
axis_type
|
Optional[Union[str, AxisType]]
|
Force the axis type (a name or an |
None
|
min_mz
|
Optional[float]
|
Force the lower bound of the range. |
None
|
max_mz
|
Optional[float]
|
Force the upper bound of the range. |
None
|
width_at_mz
|
Optional[float]
|
Force the bin width in Da at |
None
|
reference_mz
|
Optional[float]
|
The m/z the width is stated at. Default: 300 for a
|
None
|
tof_a
|
Optional[float]
|
Force |
None
|
tof_b
|
Optional[float]
|
Force |
None
|
bins_per_fwhm
|
Optional[float]
|
Force the bins per peak width of a |
None
|
method
|
Optional[Union[str, ResamplingMethod]]
|
Force one resampling method for every conversion. |
None
|
Returns:
| Type | Description |
|---|---|
MassAxisPlan
|
The plan. |
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
If a source cannot be read or states no m/z range
(and |
MassAxisPlan(axis_type: AxisType, min_mz: float, max_mz: float, reference_mz: float, n_bins: int, sha256: str, width_at_mz: Optional[float] = None, tof_a: Optional[float] = None, tof_b: Optional[float] = None, bins_per_fwhm: Optional[float] = None, method: Optional[ResamplingMethod] = None, origins: Dict[str, str] = dict(), warnings: Tuple[str, ...] = (), sources: Tuple[str, ...] = ())
dataclass
¶
One mass axis, fixed for several sources.
Attributes:
| Name | Type | Description |
|---|---|---|
axis_type |
AxisType
|
The spacing law of the bins. |
min_mz |
float
|
Lower end of the range the bins span. |
max_mz |
float
|
Upper end of it. |
reference_mz |
float
|
The m/z at which the bin width is stated. |
n_bins |
int
|
How many bins the axis has. |
sha256 |
str
|
The fingerprint of the axis (D42). A conversion given this plan refuses when the axis it builds hashes differently. |
width_at_mz |
Optional[float]
|
Bin width in Da at |
tof_a |
Optional[float]
|
|
tof_b |
Optional[float]
|
|
bins_per_fwhm |
Optional[float]
|
Bins per peak width ( |
method |
Optional[ResamplingMethod]
|
The resampling method, only when the caller forced one.
|
origins |
Dict[str, str]
|
One plain line per setting saying where the value came from. |
warnings |
Tuple[str, ...]
|
What the plan costs a source, each naming its source. |
sources |
Tuple[str, ...]
|
The names of the sources the plan was made from. |
from_dict(data: Dict[str, Any]) -> MassAxisPlan
classmethod
¶
Rebuild a plan from :meth:to_dict output.
A plan file is edited by hand, so what would be ignored is refused:
a key :meth:to_dict does not write, a version that is not the
whole number 1, an inverted range, and an n_bins that the
settings do not lay (it is a result, not a setting).
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
If |
load(path: Union[str, Path]) -> MassAxisPlan
classmethod
¶
Read a plan written by :meth:save.
The bytes go to json.loads, which tells UTF-8, UTF-16 and UTF-32
apart and skips a byte-order mark, as a plan saved through Windows
PowerShell 5.1 may carry. Text in any other encoding is refused.
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
If the file cannot be read, is not JSON in one of those encodings, or is not a plan. |
save(path: Union[str, Path]) -> None
¶
Write the plan as UTF-8 JSON.
to_dict() -> Dict[str, Any]
¶
A JSON-safe dict: enums as their values, tuples as lists.
to_resampling_config() -> Dict[str, Any]
¶
The explicit request that rebuilds this axis, as a config dict.
Never holds both target_bins and width_at_mz: the bin count
follows from the settings, and a conversion refuses the pair. The
fingerprint travels as mass_axis_sha256.
Listing Regions¶
list_regions names the regions of an input without converting it: each
region's number, .mis Area name, spectrum count and box in the store's pixel
grid. An input with one region, or a format without regions, gives one entry
for the whole raster. To write one store per region of a timsTOF slide:
import thyra
for region in thyra.list_regions("data/slide.d"):
number = region["region_number"]
thyra.convert_msi("data/slide.d", f"output/region_{number}.zarr", region=number)
list_regions(path: Union[str, Path]) -> List[Dict[str, Any]]
¶
List the acquisition regions of an MSI input without converting it.
Each entry has four keys:
region_number: the value the store writes toobs["region_number"]for that region. On a multi-region timsTOF.dit is the DB RegionNumber, whichregion=accepts.name: the.misArea name of a region of a multi-region input, orNonewhen the source names none. The whole-run entry of a single-region input has no name.n_spectra: how many spectra the region holds. A position measured twice counts twice.Nonewhen the format cannot count without reading spectra.bounds:(x_min, y_min, x_max, y_max), 0-based and inclusive, in the pixel grid of the converted store.Nonewhen the reader could not read a region's box.
An input with one region, or a format without regions, gives one entry that covers the whole raster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Union[str, Path]
|
The input, as :func: |
required |
Returns:
| Type | Description |
|---|---|
List[Dict[str, Any]]
|
One dict per region, sorted by |
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
When the input cannot be read. |
Resampling Configuration¶
When you pass resampling_config to convert_msi, the dictionary keys map
to the fields of ResamplingConfig. You can pass a plain dict (as shown in
the examples above) or construct the dataclass directly:
from thyra.resampling.types import ResamplingConfig, ResamplingMethod, AxisType
config = ResamplingConfig(
method=ResamplingMethod.NEAREST_NEIGHBOR,
axis_type=AxisType.ORBITRAP,
target_bins=50000,
)
success = convert_msi("input.imzML", "output.zarr", resampling_config=config)
ResamplingConfig(method: Optional[ResamplingMethod] = None, axis_type: Optional[AxisType] = None, target_bins: Optional[int] = None, mass_width_da: Optional[float] = None, reference_mz: float = DEFAULT_REFERENCE_MZ, min_mz: Optional[float] = None, max_mz: Optional[float] = None, gap_tolerance_da: Optional[float] = None, tof_a: Optional[float] = None, tof_b: Optional[float] = None, bins_per_fwhm: Optional[float] = None, mass_axis_sha256: Optional[str] = None)
dataclass
¶
Configuration for resampling operations.
All fields default to None (auto-detect from instrument metadata).
You can override individual fields while leaving the rest automatic.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
Optional[ResamplingMethod]
|
Resampling algorithm. |
axis_type |
Optional[AxisType]
|
Mass axis spacing model. |
target_bins |
Optional[int]
|
Number of bins in the resampled axis. |
mass_width_da |
Optional[float]
|
Bin width in Daltons at |
reference_mz |
float
|
Reference m/z for |
min_mz |
Optional[float]
|
Override the lower bound of the mass range. |
max_mz |
Optional[float]
|
Override the upper bound of the mass range. |
gap_tolerance_da |
Optional[float]
|
How far, in Daltons, a target bin may sit from the
nearest source m/z before |
tof_a |
Optional[float]
|
|
tof_b |
Optional[float]
|
|
bins_per_fwhm |
Optional[float]
|
Bins per peak width for |
mass_axis_sha256 |
Optional[str]
|
The SHA-256 of the mass axis this conversion is
expected to build (:func: |
ResamplingMethod
¶
Bases: Enum
Available resampling methods.
Attributes:
| Name | Type | Description |
|---|---|---|
NONE |
No resampling -- keep the original mass axis. |
|
NEAREST_NEIGHBOR |
Snap each peak to the nearest target bin. |
|
TIC_PRESERVING |
Redistribute intensity so the total ion count is preserved after rebinning (recommended for quantitative work). |
AxisType
¶
Bases: Enum
Mass axis spacing model, determined by the analyser physics.
The axis type controls how target bins are distributed across the
mass range. When set to None in :class:ResamplingConfig, the
type is auto-detected from instrument metadata.
Attributes:
| Name | Type | Description |
|---|---|---|
CONSTANT |
Equidistant spacing (constant Da per bin). |
|
LINEAR_TOF |
Linear TOF -- spacing proportional to
|
|
REFLECTOR_TOF |
Reflector TOF -- spacing proportional to |
|
TOF |
General time-of-flight -- spacing proportional to a measured
peak width |
|
ORBITRAP |
Orbitrap -- spacing proportional to |
|
FTICR |
FTICR -- spacing proportional to |
|
UNKNOWN |
No analyser identified. Nothing in Thyra ever produces this
member -- an undetected analyser is left as |
CONSTANT = 'constant'
class-attribute
instance-attribute
¶
FTICR = 'fticr'
class-attribute
instance-attribute
¶
LINEAR_TOF = 'linear_tof'
class-attribute
instance-attribute
¶
ORBITRAP = 'orbitrap'
class-attribute
instance-attribute
¶
REFLECTOR_TOF = 'reflector_tof'
class-attribute
instance-attribute
¶
TOF = 'tof'
class-attribute
instance-attribute
¶
UNKNOWN = 'unknown'
class-attribute
instance-attribute
¶
Metadata Types¶
Readers expose metadata through two dataclasses. EssentialMetadata contains
everything needed for conversion decisions (grid size, mass range, memory
estimate). ComprehensiveMetadata wraps essential metadata and adds
vendor-specific details for provenance and QC.
from thyra.readers.imzml import ImzMLReader
with ImzMLReader("sample.imzML") as reader:
meta = reader.get_essential_metadata()
print(f"Grid: {meta.dimensions}")
print(f"m/z range: {meta.mass_range}")
print(f"Spectra: {meta.n_spectra}")
print(f"Peaks: {meta.total_peaks}")
EssentialMetadata(dimensions: Tuple[int, int, int], coordinate_bounds: Tuple[float, float, float, float], mass_range: Tuple[float, float], pixel_size: Optional[Tuple[float, float]], n_spectra: int, total_peaks: int, source_path: str, coordinate_offsets: Optional[Tuple[int, int, int]] = None, spectrum_type: Optional[str] = None, z_spacing_um: Optional[float] = None, n_spectra_counted: bool = True, mass_range_known: bool = True, spectrum_type_guessed: bool = False, stage_origin_um: Optional[Tuple[float, float]] = None)
dataclass
¶
Critical metadata for processing decisions and interpolation setup.
Attributes:
| Name | Type | Description |
|---|---|---|
dimensions |
Tuple[int, int, int]
|
Grid dimensions as |
coordinate_bounds |
Tuple[float, float, float, float]
|
Spatial extent as |
mass_range |
Tuple[float, float]
|
Mass-to-charge range as |
pixel_size |
Optional[Tuple[float, float]]
|
In-plane pixel dimensions as |
n_spectra |
int
|
Total number of spectra in the dataset -- the ones
actually present, not the positions the raster covers.
Meaningless unless |
n_spectra_counted |
bool
|
Whether |
total_peaks |
int
|
Total number of peaks across all spectra (used for sparse matrix pre-allocation). |
mass_range_known |
bool
|
Whether |
source_path |
str
|
Absolute path to the source data. |
coordinate_offsets |
Optional[Tuple[int, int, int]]
|
Raw coordinate offsets |
stage_origin_um |
Optional[Tuple[float, float]]
|
Where grid index |
spectrum_type |
Optional[str]
|
Spectrum type string (e.g. |
z_spacing_um |
Optional[float]
|
Distance between consecutive slices in micrometres,
or |
spectrum_type_guessed |
bool
|
True when the reader inferred the spectrum
type rather than reading it from the file or the user. The value
is still used for resampling, but it is not stated in
|
ComprehensiveMetadata(essential: EssentialMetadata, format_specific: Dict[str, Any], acquisition_params: Dict[str, Any], instrument_info: Dict[str, Any], raw_metadata: Dict[str, Any])
dataclass
¶
Complete metadata including format-specific details.
Wraps :class:EssentialMetadata and adds vendor-specific information
that is not needed for conversion but useful for provenance and QC.
Attributes:
| Name | Type | Description |
|---|---|---|
essential |
EssentialMetadata
|
Core metadata required for conversion. |
format_specific |
Dict[str, Any]
|
Vendor-specific metadata (e.g. ImzML CV params, Bruker property tables). |
acquisition_params |
Dict[str, Any]
|
Acquisition parameters such as polarity, scan range, and laser settings. |
instrument_info |
Dict[str, Any]
|
Instrument model, serial number, and software version. |
raw_metadata |
Dict[str, Any]
|
Unprocessed metadata as read from the source file,
preserved for round-trip fidelity -- less the fields that name
a person, and with a path the vendor recorded reduced to its
file name, as in every vendor dictionary here (see
:mod: |
coordinate_bounds: Tuple[float, float, float, float]
property
¶
Convenience access to coordinate bounds from essential metadata.
dimensions: Tuple[int, int, int]
property
¶
Convenience access to dimensions from essential metadata.
pixel_size: Optional[Tuple[float, float]]
property
¶
Convenience access to pixel size from essential metadata.
Metadata Schema¶
The versioned, ontology-mapped uns["msi_metadata"] block every store
carries. See Metadata Schema for the storage
contract, the CLI (thyra metadata, thyra validate,
thyra export-metaspace), and the versioning rules.
read_metadata_document builds the block from a raw source instead of
reading it back out of a store, so the same document is available before
a conversion and for a source that will never be converted at all.
from thyra import read_metadata_document
from thyra.metadata.schema import (
read_msi_metadata_blocks,
to_metaspace,
validate_document,
)
blocks = read_msi_metadata_blocks("output.zarr")
meta, issues = validate_document(blocks["msi_dataset_z0"])
submission, warnings = to_metaspace(meta)
# The same document, from the raw file rather than the store
document = read_metadata_document("raw_data.d")
MSIMetadata
¶
Bases: _SchemaModel
The versioned MSI metadata document.
sample and preparation cannot be auto-populated from raw
files and default to empty; Thyra fills sample.sample_id and
sample.sample_name only when the conversion was given them;
ms_analysis and provenance are written by the converter for
every store; acquisition, calibration and alignment are
each written when the reader reports at least one of their facts and
are absent -- not empty -- otherwise.
The emitted JSON Schema carries $id and $schema so that the
committed artifact names its own published address; they are added
here, on the root model, rather than pasted into the file, so the
sync test that compares the file with model_json_schema() keeps
holding.
to_uns_dict() -> Dict[str, Any]
¶
Serialise for storage in table.uns.
None fields are dropped (which is what leaves an unset
acquisition, calibration or alignment section out), and
the sample / preparation / processing sections are
omitted entirely when empty --
following the store convention that a section the source has
nothing for is omitted rather than written empty, so consumers
can tell "not available" from "available and empty".
processing is stored as a JSON string: it is a list of
objects, which AnnData/zarr cannot round-trip (the same reason
uns["regions"] is JSON). read_msi_metadata_blocks and
validate_document both decode it transparently. The lists in
:data:PACKED_OBJECT_LISTS -- the isolation windows and the
teaching points -- are lists of objects too and get the same
treatment.
validate_document(doc: Any) -> Tuple[Optional[MSIMetadata], List[ValidationIssue]]
¶
Validate one metadata document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Any
|
The parsed document (normally a dict read from a store's
|
required |
Returns:
| Type | Description |
|---|---|
Optional[MSIMetadata]
|
|
List[ValidationIssue]
|
validation failed; issues carry everything found, errors first |
Tuple[Optional[MSIMetadata], List[ValidationIssue]]
|
is not guaranteed -- filter on |
read_msi_metadata_blocks(store_path: Union[str, Path]) -> Dict[str, Dict[str, Any]]
¶
Read every table's msi_metadata block from a SpatialData store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
Union[str, Path]
|
Path to a converted |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Dict[str, Any]]
|
Mapping of table name to the block as a plain dict. Tables |
Dict[str, Dict[str, Any]]
|
without a block are skipped; a store written by a Thyra version |
Dict[str, Dict[str, Any]]
|
that predates the schema therefore returns an empty mapping. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the path is not a SpatialData store (no
|
read_metadata_document(path: Path | str) -> Dict[str, Any]
¶
Build the msi_metadata document for a raw MSI source.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path | str
|
Path to an input in any format Thyra reads -- the same
paths :func: |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
The document as a plain dict, in the shape |
Dict[str, Any]
|
func: |
Dict[str, Any]
|
returns for a converted store. |
Dict[str, Any]
|
has been done to the data. |
Dict[str, Any]
|
absent when the source states no raster pitch, which makes the |
Dict[str, Any]
|
document invalid against schema 0.6.0 -- the caller reports that |
Dict[str, Any]
|
rather than this function hiding it. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the format cannot be detected or has no reader. The registry's own message names what Thyra does read. |
Exception
|
Whatever the reader raises for a file it cannot open.
Deliberately not swallowed, unlike :func: |
to_metaspace(meta: MSIMetadata) -> Tuple[Dict[str, Any], List[str]]
¶
Render the METASPACE submission metadata document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
meta
|
MSIMetadata
|
A validated MSI metadata document. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
|
List[str]
|
one warning per required field that had to be left empty. |
build_msi_metadata(comprehensive: Optional[ComprehensiveMetadata], *, pixel_size_um: Tuple[float, float], pixel_size_source: Optional[str] = None, source_format: Optional[str] = None, processing: Optional[List[ProcessingStep]] = None, mobility_resolved_table: Optional[str] = None, mobility_grid: Optional[Dict[str, Any]] = None, fragmentation: Any = None, msms_resolved_table: Optional[str] = None, mass_axis: Optional[Dict[str, Any]] = None, sample_id: Optional[str] = None, sample_name: Optional[str] = None) -> MSIMetadata
¶
Build an :class:MSIMetadata document from extracted metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
comprehensive
|
Optional[ComprehensiveMetadata]
|
The reader's comprehensive metadata, or |
required |
pixel_size_um
|
Tuple[float, float]
|
Resolved in-plane pixel pitch |
required |
pixel_size_source
|
Optional[str]
|
How the pixel size was determined
( |
None
|
source_format
|
Optional[str]
|
Detected input format name ( |
None
|
processing
|
Optional[List[ProcessingStep]]
|
Ordered processing steps performed so far, oldest
first (see :class: |
None
|
mobility_resolved_table
|
Optional[str]
|
Element key of the mobility-resolved sibling table written beside the summed table, when one was. |
None
|
mobility_grid
|
Optional[Dict[str, Any]]
|
The common mobility grid that table was binned
onto, as
:meth: |
None
|
fragmentation
|
Any
|
What the reader reported about fragmentation, as
:meth: |
None
|
msms_resolved_table
|
Optional[str]
|
Element key of the demultiplexed MS/MS sibling table written beside the summed table, when one was. |
None
|
mass_axis
|
Optional[Dict[str, Any]]
|
The store's mass axis fingerprint, |
None
|
sample_id
|
Optional[str]
|
The user's identifier for the sample, or |
None
|
sample_name
|
Optional[str]
|
The user's name for the sample, or |
None
|
Returns:
| Type | Description |
|---|---|
MSIMetadata
|
The populated document. Fields the source does not report are |
MSIMetadata
|
left unset, and the |
MSIMetadata
|
|
MSIMetadata
|
none of their facts. |
Reader Base Class¶
All format readers (ImzML, Bruker, Waters, PHI) inherit from this base class.
If you are writing a custom reader for a new format, subclass BaseMSIReader
and implement the abstract methods below. See
Supported Formats for which optional methods are worth
implementing and what each one buys you.
BaseMSIReader(data_path: Path, intensity_threshold: Optional[float] = None, **kwargs: object)
¶
Bases: ABC
Abstract base class for reading MSI data formats.
Initialize the reader with the path to the data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_path
|
Path
|
Path to the data file or directory |
required |
intensity_threshold
|
Optional[float]
|
Minimum intensity value to include. Values below this threshold are filtered out during iteration. Useful for removing detector noise in continuous mode data. Default: None (no filtering, include all values). |
None
|
**kwargs
|
object
|
Additional reader-specific parameters.
|
{}
|
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
When |
has_acquisition_order: bool
property
¶
Whether the source says in which order its spectra were acquired.
has_shared_mass_axis: bool
property
¶
Check if all spectra share the same m/z axis.
For continuous ImzML data, all spectra have identical m/z values, so get_common_mass_axis() only needs to read the first spectrum. For processed/centroid data, each spectrum may have different m/z values, requiring iteration through all spectra.
Returns:
| Type | Description |
|---|---|
bool
|
True if all spectra share the same m/z axis (continuous mode), |
bool
|
False if each spectrum has different m/z values (processed mode). |
has_ion_mobility: bool
property
¶
Whether the source carries a mobility dimension at all.
has_frame_scans: bool
property
¶
Whether :meth:iter_frame_scans is available on this source.
get_essential_metadata() -> EssentialMetadata
¶
Get essential metadata for processing.
get_comprehensive_metadata() -> ComprehensiveMetadata
¶
Get complete metadata.
get_common_mass_axis() -> NDArray[np.float64]
abstractmethod
¶
Return the common mass axis for all spectra.
This method must always return a valid array. If no common mass axis can be created, implementations should raise an exception.
get_mass_axis_annotations() -> Optional[dict]
¶
Get extra per-channel columns to store alongside the m/z axis.
Returns a mapping of column name to an array with one entry per
entry of :meth:get_common_mass_axis. These are written into the
table's var next to mz.
This exists so a format whose native axis is not m/z can keep that axis in the output. Time-of-flight instruments measure flight time and derive m/z from a calibration, so storing the flight time makes the conversion reversible without the reader: a later recalibration can be applied to the stored times directly.
Annotations are dropped if their length does not match the axis the converter actually writes, which is what happens when resampling is enabled and the axis is rebuilt.
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Mapping of column name to per-channel values, or None. |
get_optical_image_paths() -> List[Path]
¶
Get paths to optical/microscopy images associated with this data.
Returns the image files that hold optical images of the sample -- TIFF, JPEG, PNG or BMP. These images can be stored alongside MSI data in SpatialData output for multimodal analysis.
Default implementation returns empty list. Subclasses should override to return paths to optical images specific to their format.
Returns:
| Type | Description |
|---|---|
List[Path]
|
Paths to optical images, empty if none are available. When one of |
List[Path]
|
them is the alignment image (see |
List[Path]
|
meth: |
get_primary_optical_image_path() -> Optional[Path]
¶
Get the optical image the source designates as the alignment image.
Some formats say which of an acquisition's images the registration is
stated against -- FlexImaging names it in the .mis <ImageFile>
element -- and that one gets the identity transform while the others
are scaled into its pixel space. Without it the converter has no way
to tell an alignment scan from a slide overview that happens to sit
in the same folder.
Default implementation returns None, which leaves every optical image
equal. Override alongside :meth:get_optical_image_paths when the
format designates one; it must be a member of that list.
Returns:
| Type | Description |
|---|---|
Optional[Path]
|
Path to the designated image, or None if the format does not |
Optional[Path]
|
designate one or the designated file is not present. |
iter_spectra() -> Generator[Tuple[Tuple[int, int, int], NDArray[np.float64], NDArray[np.float64]], None, None]
abstractmethod
¶
Iterate through spectra with optional batch processing.
Yields:
| Type | Description |
|---|---|
Tuple[Tuple[int, int, int], NDArray[float64], NDArray[float64]]
|
Tuple containing:
|
Note
Subclasses should apply intensity threshold filtering by calling _apply_intensity_filter() on the intensities before yielding.
iter_spectra_with_acquisition_order() -> Generator[Tuple[Tuple[int, int, int], int, NDArray[np.float64], NDArray[np.float64]], None, None]
¶
:meth:iter_spectra, with each spectrum's acquisition order.
Yields ((x, y, z), order, mzs, intensities): the spectra
:meth:iter_spectra yields, in its order, with its coordinates and
arrays. order grows with acquisition time and no two spectra
share one. It is the source's own number for the spectrum where the
source numbers them (a Bruker Frames.Id), else the spectrum's
0-based position in the sequence the source lists them in. So it
need not start at 0 or be contiguous: sort by it, never index with
it.
The converter reads the spectra through this method whenever
:attr:has_acquisition_order is True, so a subclass that changes
what :meth:iter_spectra yields has to change this as well.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
When :attr: |
get_region_map() -> Optional[dict]
¶
Get per-pixel region mapping for multi-region datasets.
Returns a dictionary mapping normalized (0-based) (x, y) coordinate tuples to integer region numbers. This enables the converter to annotate each pixel with its acquisition region in obs["region_number"].
Default implementation returns None (single-region or no region info). Subclasses should override when region information is available.
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict mapping (x, y) tuples to region numbers, or None if |
Optional[dict]
|
region information is not available. |
get_region_info() -> Optional[list]
¶
Get summary information about acquisition regions.
Returns a list of dictionaries, each describing one region with at minimum: {"region_number": int, "n_spectra": int}. Additional keys (e.g. "name") are format-specific and optional.
Default implementation returns None (single-region or no region info). Subclasses should override when region information is available.
Returns:
| Type | Description |
|---|---|
Optional[list]
|
List of region summary dicts, or None if region information |
Optional[list]
|
is not available. |
get_mobility_axis() -> Optional[MobilityAxis]
¶
Describe the mobility dimension, or None when there is none.
iter_mobility_spectra() -> Generator[Tuple[Tuple[int, int, int], NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]], None, None]
¶
Iterate through spectra with their per-point mobility values.
Yields ((x, y, z), mzs, mobilities, intensities) per spectrum:
the raw (m/z, mobility) point cloud, unbinned, with the three arrays
parallel, at the same coordinates :meth:iter_spectra gives it. A
source may place two spectra on one position. Readers whose source
has no mobility dimension do not implement this.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If the reader exposes no mobility dimension. |
get_fragmentation() -> Optional[FragmentationSchedule]
¶
Describe the fragmentation, or None when the reader cannot.
None exactly when :attr:has_fragmentation is False.
iter_precursor_spectra() -> Generator[Tuple[Tuple[int, int, int], int, NDArray[np.float64], NDArray[np.float64]], None, None]
¶
Iterate the fragment spectra of each precursor separately.
Yields ((x, y, z), window_index, mzs, intensities): one
spectrum per pixel per precursor, where window_index is the
position of that precursor in
:meth:get_fragmentation's windows. Only a source that
separates its precursors within a pixel implements this -- Bruker
PASEF gives each isolation window its own slice of the mobility
ramp, so the split is a filter on the scan number rather than a
deconvolution.
A pixel-precursor pair with no ion current is not yielded.
:meth:iter_spectra is unaffected and keeps yielding the summed
spectrum.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If the reader cannot separate precursors. |
iter_frame_scans() -> Generator[FrameScans, None, None]
¶
Iterate the frames as records that derive every view of a pixel.
Yields one :class:~thyra.core.frames.FrameScans per frame, in
the same order and with the same coordinates as
:meth:iter_spectra, including frames whose summed spectrum is
empty (their :meth:~thyra.core.frames.FrameScans.spectrum is
None), so a consumer can still count them for the sinks that
do not depend on the summed spectrum.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
When :attr: |
close() -> None
abstractmethod
¶
Close all open file handles.
A reader whose summed spectrum, mobility point cloud and precursor spectra all derive from one raw read per pixel can hand that read over as a record, so a conversion that writes several tables reads the source once per pass (design decision D5). The Bruker TDF reader does; the record's contract is:
FrameScans
¶
Bases: Protocol
One pixel's raw read, with every view of it derived on demand.
Each method answers exactly as the corresponding reader iterator would have yielded for this frame -- the same arrays, the same filtering, the same "nothing" -- so a consumer fed from records is fed what its own pass would have read.
acquisition_order: Optional[int]
property
¶
The frame's order as iter_spectra_with_acquisition_order yields it.
None when the reader does not know the order.
mobility_points() -> Optional[MobilityPoints]
¶
(mzs, mobility, intensities) as iter_mobility_spectra yields, or None.
mobility_points_indexed() -> Optional[IndexedMobilityPoints]
¶
The same points with the m/z left factored, or None for no points.
(unique_mz, inverse, mobility, intensities) where
unique_mz[inverse] is exactly the mzs of
:meth:mobility_points, element for element, and the three other
arrays are that method's own. A digitizer reports the points of
one frame at far fewer distinct m/z values than it reports
points, so a consumer that has to map m/z onto an axis can map
unique_mz once and gather by inverse for the same bins.
None exactly where :meth:mobility_points returns None:
the frame holds no points. A record whose points do not factor
this way does not offer the method at all -- consumers reach for
it with :func:getattr and fall back to the flat view -- so
None never means "ask the other way".
precursor_spectra() -> List[PrecursorSpectrum]
¶
[(window_index, mzs, intensities), ...] as iter_precursor_spectra yields.
Empty when the acquisition has no separable precursor schedule or the frame holds no isolated points.
spectrum() -> Optional[Spectrum]
¶
(mzs, intensities) as :meth:BaseMSIReader.iter_spectra yields, or None.
None where the iterator would have skipped the frame (an
empty spectrum, or one emptied by the intensity threshold).
Converter Base Class¶
All output converters inherit from this base class. Currently only
SpatialData output is supported, but the architecture allows adding new output
formats by subclassing BaseMSIConverter.
BaseMSIConverter(reader: BaseMSIReader, output_path: Union[str, Path, PathLike[str]], dataset_id: str = 'msi_dataset', pixel_size_um: float = 1.0, pixel_size_source: PixelSizeSource = PixelSizeSource.DEFAULT, compression_level: int = 5, handle_3d: bool = False, z_spacing_um: Optional[float] = None, **kwargs: Any)
¶
Bases: ABC
Base class for MSI data converters with shared functionality.
Implements common processing steps while allowing format-specific customization.
Initialize the MSI converter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reader
|
BaseMSIReader
|
MSI data reader instance |
required |
output_path
|
Union[str, Path, PathLike[str]]
|
Path for output file |
required |
dataset_id
|
str
|
Identifier for the dataset |
'msi_dataset'
|
pixel_size_um
|
float
|
In-plane pixel pitch in micrometers along
x. The y pitch starts equal to it and is replaced by
the detected one when the source declares an anisotropic
raster -- see :attr: |
1.0
|
pixel_size_source
|
PixelSizeSource
|
How pixel size was determined |
DEFAULT
|
compression_level
|
int
|
Compression level for output |
5
|
handle_3d
|
bool
|
Whether to process as 3D data |
False
|
z_spacing_um
|
Optional[float]
|
Distance between consecutive slices in
micrometers. Only meaningful with |
None
|
**kwargs
|
Any
|
Additional keyword arguments |
{}
|
Raises:
| Type | Description |
|---|---|
ConversionRefused
|
If |
pixel_size_um = pixel_size_um
instance-attribute
¶
pixel_size_source = pixel_size_source
instance-attribute
¶
dataset_id = dataset_id
instance-attribute
¶
handle_3d = handle_3d
instance-attribute
¶
convert() -> bool
¶
Template method defining the conversion workflow.
An interrupt is a failed conversion, not a separate kind of exit.
KeyboardInterrupt is caught here so the caller gets False
and the CLI's _handle_post_conversion runs, renaming the
partial store to .failed and leaving the output path free for
a retry -- which is what docs/cli.md promises of any failed
conversion. Before this it propagated past except Exception,
click printed Aborted!, and an interrupted run left an
unopenable store where a finished one belongs, a blocked retry,
and (on a whole-slide mobility grid) 18 GB of scratch memmaps
nothing came back to remove (issue #245).
The one exception is an interrupt that lands after the store is
complete (:attr:store_complete): the store holds every row and
opens, so it is kept and the conversion reports success
(issue #500).
Returns:
| Type | Description |
|---|---|
bool
|
True if the conversion succeeded, False otherwise. |
Format Detection and Plugin Registry¶
Thyra uses a registry to map file extensions and directory structures to the correct reader and converter classes. The public functions below let you detect formats programmatically or register your own reader/converter.
Detecting a format¶
from pathlib import Path
from thyra.core.registry import detect_format
fmt = detect_format(Path("experiment.imzML")) # "imzml"
fmt = detect_format(Path("data.d")) # "bruker" or "rapiflex"
fmt = detect_format(Path("data.raw")) # "waters"
Registering a custom reader¶
from thyra.core.registry import register_reader
from thyra.core.base_reader import BaseMSIReader
@register_reader("my_format")
class MyFormatReader(BaseMSIReader):
...
The decorator runs when your module is imported, and the format is
available from that point on. Thyra's own readers work the same way, but
nothing imports them up front: the registry holds a table of
format -> module and imports a format's module the first time that
format is looked up or detected. So import thyra loads no reader, no
converter and no spatialdata, and importing the metadata schema on its
own costs a pydantic model rather than the whole imaging stack.
detect_format(input_path: Path) -> str
¶
Detect MSI format from input path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Path
|
Path to MSI data file or directory |
required |
Returns:
| Type | Description |
|---|---|
str
|
Format name ('imzml', 'bruker', 'rapiflex', 'solarix', 'waters', |
str
|
or 'phi') |
register_reader(format_name: str) -> Callable[[Type[_ReaderT]], Type[_ReaderT]]
¶
Decorator for reader registration.
register_converter(format_name: str) -> Callable[[Type[_ConverterT]], Type[_ConverterT]]
¶
Decorator for converter registration.