Data Schemas¶
This page documents the canonical data schemas that FOCUS writes at each pipeline stage. Use this reference when integrating FOCUS outputs programmatically, writing custom downstream analyses, or implementing new modality handlers.
Note
All coordinate arrays use physical units of micrometers (µm) unless stated otherwise. Sparse matrices use SciPy CSR format. All floating-point data are stored as float32 unless stated otherwise.
1. Preprocessing outputs¶
Image modalities (microscopy, raman) → OME-TIFF outputs¶
- Microscopy preprocessing writes a multi-resolution OME-TIFF pyramid using
tifffilewithzlibcompression and a predictor matched to the storage dtype (3 forfloat32, 2 for integers). The storage dtype follows the source file:uint8/uint16pass through, float sources stayfloat32, and any other integer depth becomesuint16. Integer output is the internally normalised image scaled by the dtype maximum and rounded;float32output is written on that normalised scale unchanged. - Raman preprocessing writes the OME-TIFF pyramid produced by the ASHLAR stitching step (via the external
FOCUS_ASHLARenvironment):uint8, one channel per spectral channel in scan order, Adobe-Deflate compressed with a horizontal predictor, with successive 2× levels. Its OME-XML holds the physical pixel size in µm; it carries no channel names and no wavenumber values, so the spectral axis cannot be read back from the file.
In both cases the full-resolution level is at index 0 and higher indices are progressively downsampled, but they are stored differently: microscopy writes one TIFF series per level, while ASHLAR stores the Raman levels as SubIFDs of a single series (tifffile.TiffFile(...).series[0].levels). OME-TIFF files can be read with tifffile.TiffFile, bioformats, QuPath, or Napari.
Omics modalities (msi, st) → AnnData .h5ad¶
MSI (lipidomics)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
sparse CSR float32 | \((n_\text{spots},\ n_\text{mz})\) | Ion intensities after normalization |
.layers['raw'] |
sparse CSR float32 | \((n_\text{spots},\ n_\text{mz})\) | Interpolated intensities before normalization |
.obsm['spatial'] |
float32 ndarray | \((n_\text{spots},\ 2)\) | Physical coordinates in µm \((x, y)\) |
.obsm['raster_coordinates'] |
int32 ndarray | \((n_\text{spots},\ 2,\ 2)\) | Raster-cell bounding boxes \([[x_\text{tl}, y_\text{tl}], [x_\text{br}, y_\text{br}]]\) in µm |
.obs['sample_id'] |
categorical | \((n_\text{spots},)\) | Sample identifier (directory name) |
.obs['foreground'] |
categorical | \((n_\text{spots},)\) | Tissue/background mask stored as categorical boolean values. Always present; all True when background detection did not run |
.obs['cluster'] |
categorical | \((n_\text{spots},)\) | Per-sample cluster label, used only for spot colouring in the alignment GUI |
.var['mz'] |
float32 ndarray | \((n_\text{mz},)\) | Consensus m/z values |
.var['mz_mode'] |
categorical | \((n_\text{mz},)\) | Ion mode for each m/z ('pos' or 'neg') |
.var['lipid_annotation'] |
categorical | \((n_\text{mz},)\) | Lipid annotation string: "; "-separated db_name hits, or "Unannotated" when no match/database is provided |
.uns['spot_size'] |
list of 2 floats | - | Spot size \([x,\ y]\) in µm, derived from MSI raster metadata. In the merged file this is instead a dict keyed by sample_id |
.var_names are the stringified column indices "0" … "n_mz-1", not the m/z values. Read .var['mz'] for the masses.
Dual ion mode
When both positive and negative ion mode acquisitions are present, their features are concatenated along var (columns), and mode identity is tracked in .var['mz_mode'].
spot_size
For MSI, spot_size is propagated from parsed raster metadata and used during spot_interpolation registration to determine the Gaussian kernel width.
Spatial transcriptomics (ST)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
sparse CSR | \((n_\text{spots},\ n_\text{genes})\) | Expression counts. Raw unless total_counts_normalize or log1p_transform was enabled; dtype follows the input file |
.layers['raw'] |
sparse CSR | \((n_\text{spots},\ n_\text{genes})\) | Raw counts before normalization. Present only when .X was normalized |
.obsm['spatial'] |
float32 ndarray | \((n_\text{spots},\ 2)\) | Spot coordinates in µm \((x, y)\) |
.obs_names |
object | \((n_\text{spots},)\) | Spot names, prefixed <sample_id>_ |
.obs['sample_id'] |
categorical | \((n_\text{spots},)\) | Sample identifier |
.obs['cluster'] |
categorical | \((n_\text{spots},)\) | Per-sample cluster label, used only for spot colouring in the alignment GUI |
.obs QC |
float / int | \((n_\text{spots},)\) | total_counts, n_genes_by_counts, total_counts_mt, pct_counts_mt and their log1p_ variants |
.var['mt'] |
bool | \((n_\text{genes},)\) | True for mitochondrial genes (case-insensitive MT- / MT. name prefix) |
.var QC |
float / int | \((n_\text{genes},)\) | n_cells_by_counts, mean_counts, total_counts, pct_dropout_by_counts and their log1p_ variants |
.uns['spot_size'] |
float32 ndarray | \((2,)\) | Spot diameter \([x,\ y]\) in µm, read from the input AnnData's .uns['spot_size']; defaults to [1.0, 1.0] µm when absent. In the merged file this is instead a dict keyed by sample_id |
spot_size
For ST modalities, spot_size is read from the input AnnData file's .uns['spot_size'] field during preprocessing. If this field is absent, FOCUS applies the default [1.0, 1.0] µm. This value is critical for accurate spot interpolation during registration; ensure your input ST AnnData has the correct spot diameter.
2. Alignment output (reference modality AnnData)¶
The Alignment stage does not produce a new file for each non-reference modality. Instead, it adds coordinate keys to the reference modality AnnData. After alignment, the reference AnnData contains:
| Attribute | Type | Shape | Description |
|---|---|---|---|
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | Reference modality coordinates (unchanged) |
.obsm['{non_ref_name}_spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | Reference spots expressed in the non-reference modality's coordinate space |
One .obsm['{non_ref_name}_spatial'] key is added per non-reference modality that was aligned. For example, if the reference is "st" and non-reference modalities are "msi" and "microscopy", the aligned reference AnnData will contain .obsm['spatial'], .obsm['msi_spatial'], and .obsm['microscopy_spatial'].
Pre-aligned modalities
Modalities configured with alignment_strategy: "pre_aligned" store passthrough coordinates in .obsm['{reference_modality_name}_spatial'] (a copy of .obsm['spatial']).
3. Registration output (all modalities → per-modality AnnData)¶
Registration produces one AnnData per modality per sample, plus a merged AnnData combining all samples. All registration outputs share the same observation index as the reference modality, so row \(i\) in every modality's registered AnnData corresponds to the same reference spot.
Every registered file, whatever the mode, carries:
| Attribute | Type | Shape | Description |
|---|---|---|---|
.obs['sample_id'] |
categorical | \((n_\text{ref},)\) | Sample identifier |
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | Positions the features were read at, in the target modality's coordinate frame, not the reference's own frame. See each mode below |
.uns['registration_type'] |
str | - | The mode that produced the file; the cache is only reused when this matches the configured registration_type |
In the merged file, .obs_names are rewritten as {sample_id}_{row_index}.
Feature extraction registration (feature_extraction)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
float32 ndarray (dense) | \((n_\text{ref},\ n_\text{dims})\) | Patch embeddings (1536-dim for Prov-GigaPath). Rows whose patch was classified as background are all-zero |
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | The actual patch centres in image pixel coordinates. Patch origins are clamped so each patch fits inside the image, so a centre near a border differs from the anchor coordinate that was requested |
.var_names |
Index | \((n_\text{dims},)\) | Positional strings "0" … "n_dims-1"; the embedding has no feature metadata |
No .layers are written.
Embedding dimensionality
The backbone is Prov-GigaPath (HuggingFace), which outputs 1536-dimensional embeddings. The width is whatever the model returns and is not hard-coded, so it would change if the backbone were swapped. Swapping the backbone requires a code change and is not exposed as a configuration option.
Spot interpolation registration (spot_interpolation)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
float32 ndarray (dense) | \((n_\text{ref},\ n_\text{features})\) | Gaussian-weighted average of the target spots inside each anchor footprint. Anchors with no target spot inside are all-zero |
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | Anchor coordinates in the target modality's frame (obsm['{target}_spatial'] of the aligned reference) |
.var / .var_names |
DataFrame / Index | \((n_\text{features},)\) | Copied from the target modality (m/z values or gene names) |
.layers['…'] |
float32 ndarray (dense) | \((n_\text{ref},\ n_\text{features})\) | Every target layer, interpolated with the same kernel |
The footprint and the kernel come from .uns['spot_size'] of the anchor (aligned reference) AnnData, not from the target's. The footprint is the rectangle \(s_x \times s_y\) centred on the anchor, and the Gaussian bandwidth is \(\sigma = \sqrt{s_x s_y}/2\). A missing spot_size falls back to [1.0, 1.0].
Spot aggregation registration (spot_aggregation)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
sparse CSR float32 | \((n_\text{ref},\ n_\text{features})\) | Equal-weight sum of the target spots inside each anchor footprint, unnormalized. Anchors with no target spot inside are all-zero rows |
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | As for spot_interpolation |
.var / .var_names |
DataFrame / Index | \((n_\text{features},)\) | Copied from the target modality |
.layers['…'] |
sparse CSR float32 | \((n_\text{ref},\ n_\text{features})\) | Every target layer, summed with the same membership |
Same footprint rule and spot_size source as spot_interpolation; no kernel and no normalization.
Raman pixel interpolation registration (raman_pixel_interpolation)¶
| Attribute | Type | Shape | Description |
|---|---|---|---|
.X |
float32 ndarray (dense) | \((n_\text{ref},\ n_\text{channels})\) | Gaussian-weighted average of the Raman pixels inside each anchor footprint, using the same kernel as spot_interpolation. Anchors with no pixel inside are all-zero |
.obsm['spatial'] |
float32 ndarray | \((n_\text{ref},\ 2)\) | Anchor coordinates in the Raman image's pixel frame |
.var_names |
Index | \((n_\text{channels},)\) | Channel names from the OME-XML, falling back to Channel_0 … Channel_{C-1} |
No .layers are written.
4. Final MuData output (.h5mu)¶
The final output is written to {dataset_path}/merged/multimodal_dataset.h5mu.
| Attribute | Type | Description |
|---|---|---|
.mod['{modality_name}'] |
AnnData | Per-modality AnnData (schema as in Section 3), except that obsm['spatial'] and uns['spot_size'] are removed (they live at the top level) and var_names are namespaced as {modality}:{name} |
.obsm['spatial'] |
float32 ndarray | Shared reference coordinates, shape \((n_\text{obs},\ 2)\) |
.obs['sample_id'] |
categorical | Shared sample identifiers across all modalities |
.obs['spatial_annotation'] |
categorical | Region labels transferred from GeoJSON annotations (present only when annotation transfer is enabled) |
.uns['spot_size'] |
float32 ndarray | Length-2 [width, height] in µm, copied verbatim from the reference (anchor) merged AnnData; present only if the reference had it |
Observation alignment
.obs_names in each included .mod AnnData are harmonized so that .mod['{modality_name}'][i] and .mod['{other_modality}'][i] refer to the same reference spot location. A modality is included only if its rows align to the reference. Its observation count must match and its sample_id sequence must match the reference element-wise; otherwise it is skipped with a warning. Reference spots that are uncovered (all-zero features) in any modality are dropped from every modality, so n_obs may be smaller than the reference spot count. See Compilation.
5. Reading FOCUS outputs with Python¶
import mudata as md
import scanpy as sc
mdata = md.read_h5mu("path/to/merged/multimodal_dataset.h5mu")
# Access individual modalities
st_adata = mdata.mod["st"]
msi_adata = mdata.mod["msi"]
# Shared spatial coordinates (n_spots, 2) in µm
coords = mdata.obsm["spatial"]
# Sample identifiers
sample_ids = mdata.obs["sample_id"]
# Spatial region annotations (if annotations were enabled)
annotations = mdata.obs["spatial_annotation"]
# Downstream analysis with squidpy (feature names are namespaced as {modality}:{name})
import squidpy as sq
sq.pl.spatial_scatter(mdata.mod["st"], color="st:CD3E")
# Access feature embeddings from microscopy registration
microscopy_embeddings = mdata.mod["microscopy"].X # (n_spots, 1536)
# Concatenate all modalities for joint embedding (e.g., MOFA+)
import numpy as np
joint_matrix = np.hstack([
mdata.mod["st"].X.toarray(),
mdata.mod["msi"].X,
mdata.mod["microscopy"].X,
])
Working with sparse matrices
MSI and ST .X matrices are stored as sparse CSR arrays. Call .toarray() to convert to dense NumPy arrays when needed. For large datasets, prefer operating on the sparse representation directly (e.g., with scipy.sparse or scanpy functions that accept sparse input).