API#

Import as:

import mantispy as mt

Every function that modifies an object takes copy. With copy=False (the default) it mutates in place and returns None; with copy=True it returns a modified copy and leaves the input alone. Each call records its arguments under uns["mantispy"]["params"][<function name>], so a finished object says how it was made.

The Stores column in the tables below lists the keys each function writes.

Reading and writing#

io.read_profiles(paths[, metadata_prefixes, ...])

Read profiles into an AnnData of observations by features.

io.read_plate(path[, plate, batch, layout, ...])

Read one plate of images, segmentations and measurements into a SpatialData object.

io.read_jump(paths[, annotate])

Read JUMP plate profiles, optionally joining the annotation.

io.read(path[, backed, migrate_schema])

Read a mantispy h5ad or zarr store, checking its schema version.

io.write(adata, path)

Validate and write adata as h5ad (default) or zarr (.zarr suffix).

io.validate(adata, *[, raise_on_error])

Check adata against the mantispy schema.

io.read_profiles reads the output of a profiling pipeline, and the input type decides how. CSV, TSV or parquet files are stacked, a CellProfiler ExportToSpreadsheet directory is joined across its objects, and a directory of CytoTable parquet parts is read as single cells. io.read_plate reads the images and segmentations behind the profiles into SpatialData, from a Cell Painting Gallery source or an ExportForSpatialData plate folder, and needs the spatial extra:

pip install 'mantispy[spatial]'

Function

Stores

io.read_profiles

X, obs (Metadata_*, and from an export directory Metadata_Center_X/_Y), var (parsed annotation), uns["mantispy"]: schema_version, resolution, channels, params, and image_table from an export directory

io.read_plate

returns SpatialData: fields of view as Images, segmentations as Labels, wells as Shapes; the cells and wells Tables of a gallery source follow the contract below

io.read_jump

as io.read_profiles, plus obs: Metadata_JCP2022, Metadata_Perturbation, Metadata_InChIKey, Metadata_Control

io.write

validates first, then writes h5ad (or zarr for a .zarr suffix)

io.validate returns a report rather than raising, unless raise_on_error=True:

class mantispy._core.schema.ValidationReport(errors=<factory>, warnings=<factory>)[source]#

Result of validate(). Truthy when there are no errors.

property ok: bool#

Whether the object satisfies the schema.

Preprocessing#

pp.annotate_controls(adata[, negcon, ...])

Mark negative (and optionally positive) controls.

pp.annotate_jump(adata[, kind, copy])

Join the JUMP annotation onto profiles read from the Cell Painting Gallery.

pp.find_perturbation_key(adata[, ...])

Resolve which obs column holds the perturbation identity.

pp.calculate_qc_metrics(adata[, ...])

Compute per-cell and per-feature QC metrics.

pp.filter_cells(adata[, min_cells_per_well, ...])

Drop cells that fail QC or sit in under-populated wells.

pp.filter_features(adata[, drop_nan, ...])

Drop all-NaN, low-variance and blocklisted features.

pp.normalize(adata[, method, by, reference, ...])

Normalize features within groups, optionally fitting on reference rows only.

pp.feature_select(adata[, operations, ...])

Flag the features worth keeping.

pp.subset_features(adata[, key])

Return a new object holding only the features flagged by var[key].

pp.outliers(adata[, method, contamination, ...])

Flag outlying cells.

pp.image_qc(adata[, metrics, channel, ...])

Flag low-quality images and broadcast the verdict onto their cells.

pp.filter_images(adata[, copy])

Drop every cell belonging to an image that failed image_qc().

pp.well_qc(adata[, min_cells, ...])

Flag wells with too few cells, too much missing data, or unstable controls.

pp.standardize_feature_names(adata[, ...])

Rename features to target grammar, keeping the original in var.

pp.sphere(adata[, method, reference, ...])

Whiten profiles with a transform fitted on the reference rows.

pp.correct_plate_position(adata[, method, ...])

Remove row and column position effects, per plate and per feature.

pp.regress_out(adata[, keys, by, key_added, ...])

Regress confounders out of every feature, within groups.

pp.harmony(adata[, batch_key, use_rep, ...])

Correct an embedding for batch with Harmony.

pp.rank_int(adata[, by, c, stochastic, ...])

Replace every feature by the normal quantile of its rank.

pp.downsample(adata[, n_per_group, groupby, ...])

Return at most n_per_group rows from each group.

pp.feature_select_chatterjee(adata[, ...])

Keep features whose values depend on the group, monotonically or otherwise.

pp.feature_reproducibility(adata[, groupby, ...])

Score each feature by how consistently replicates of a perturbation agree on it.

pp.feature_batch_sensitivity(adata[, ...])

Test each feature for dependence on the batch, after whatever correction was applied.

Function

Stores

pp.annotate_controls

obs["Metadata_Control"], and obs["Metadata_Control_Type"] when poscon is given

pp.annotate_jump

obs: Metadata_JCP2022, Metadata_Perturbation, Metadata_InChIKey, Metadata_Control

pp.calculate_qc_metrics

obs: qc_n_nan_features, qc_nan_fraction, qc_is_border, qc_area_outlier, qc_pass; var: qc_n_nan, qc_variance, qc_n_unique

pp.filter_cells

subsets obs in place

pp.filter_features

subsets var in place

pp.normalize

X, or layers[key_added]; layers["raw"] when keep_raw=True

pp.feature_select

var[key_added], uns["mantispy"]["feature_select"]

pp.outliers

obs[key_added], obs[key_added + "_score"]

pp.image_qc

uns["mantispy"]["image_qc"], obs["qc_image_pass"]

pp.well_qc

uns["mantispy"]["well_qc"], obs["qc_well_pass"]

pp.standardize_feature_names

var_names, var["original_name"]

pp.sphere

X, or layers[key_added]

pp.correct_plate_position

X or layers[key_added], uns["mantispy"]["plate_position"]

pp.regress_out

X, or layers[key_added]

pp.harmony

obsm[key_added]; needs mantispy[harmony]

pp.rank_int

X, or layers[key_added]

pp.downsample

returns a new object holding the sampled rows

pp.feature_select_chatterjee

var[key_added], var["chatterjee_xi"]

pp.feature_reproducibility

var[key_added], var[key_added + "_selected"]

pp.feature_batch_sensitivity

var[key_added + "_pvalue"], _qvalue, _sensitive

pp.normalize also writes var["degenerate_scale"], flagging features with no spread in some group. Those are divided by epsilon rather than by zero and come back at ~1e17; drop them before computing anything from distances.

Tools#

tl.aggregate(adata[, by, func, min_cells, layer])

Aggregate adata to one profile per group.

tl.map(adata[, pos_sameby, pos_diffby, ...])

Mean average precision per group, with a permutation null.

tl.similarity(adata[, metric, use_rep, ...])

Store pairwise profile similarity in obsp[key_added].

tl.percent_replicating(adata[, groupby, ...])

Median replicate correlation against a non-replicate null.

tl.grit(adata[, groupby, reference, metric, ...])

Similarity of each replicate to its group, z-scored against its similarity to the controls.

tl.consensus(adata[, by, method, ...])

One profile per group, weighting replicates by how well they agree.

tl.effect_size(adata[, groupby, reference, ...])

Per-feature effect size of each group against the reference.

tl.wasserstein_features(adata[, groupby, ...])

Wasserstein-1 distance per feature between each group and the reference.

tl.differential_features(adata[, groupby, ...])

Moderated t-test per feature, per group, with wells as the replicates.

tl.feature_signature(adata[, key, by, statistic])

Collapse a differential table into a perturbation-by-feature-family matrix.

tl.hit_calling(adata[, groupby, reference, ...])

Call hits by testing each group's distance from the controls.

tl.edistance(adata[, groupby, reference, ...])

Energy distance between each group and the controls, or between every pair.

tl.transport(adata[, by, groupby, ...])

Test whether each perturbation's effect reproduces across settings.

tl.dose_response(adata[, compound_key, ...])

Test whether each compound's response grows with concentration.

tl.nn_moa_classify(adata[, moa_key, metric, ...])

Leave-one-out nearest-neighbor mechanism assignment.

tl.moa_enrichment(adata[, moa_key, groupby, ...])

Test which mechanisms are over-represented among each profile's nearest neighbors.

tl.feature_sets(adata[, by])

Build a decoupler network from the parsed feature annotation.

tl.enrich(adata[, net, by, method, ...])

Score every profile against every feature set.

tl.rank_features(adata, groupby[, method, ...])

Rank features by how well they separate each group, with the annotation attached.

tl.rank_sets(adata, groupby[, score_key, ...])

Rank feature sets by how far each group's score sits from the rest.

tl.cluster_composition(adata[, cluster_key, ...])

Fraction of each well's cells in each cluster, as a well-level object.

tl.subpopulation_hits(adata[, cluster_key, ...])

Test each perturbation against the controls within each cluster.

tl.cell_cycle_phase(adata[, dna_feature, ...])

Assign G1, S or G2M from integrated DNA intensity.

tl.neighbors_local_density(adata[, k, by, ...])

Mean distance to the k nearest cells in the same field of view.

tl.replicate_saturation(adata[, groupby, ...])

Score how much a group's signature improves with each additional replicate.

tl.cytotoxicity(adata[, groupby, reference, ...])

Flag perturbations that both lost cells and moved away from the controls.

tl.gene_sets([source, organism])

Fetch a gene-set network, or read one from a GMT file.

tl.pathway_coherence(adata, net[, gene_key, ...])

Score how similar the profiles of each gene set's genes are.

tl.enrich_hits(adata, net[, gene_key, ...])

Test which gene sets are over-represented among the hits.

Function

Stores

tl.aggregate

returns a new object: obs gains Metadata_CellCount; uns["mantispy"] gains aggregated_from and a resolution of "well" or "perturbation"

tl.map

uns["mantispy"][key_added], obs[key_added], obs[key_added + "_qvalue"]

tl.similarity

obsp[key_added]

tl.percent_replicating

uns["mantispy"][key_added] and ..._summary

tl.grit

obs[key_added], uns["mantispy"][key_added]

tl.consensus

returns a new object at "perturbation" resolution; obs["Metadata_ReplicateCount"], uns["mantispy"]["consensus_weights"]

tl.effect_size

varm[key_added], uns["mantispy"][key_added] and ..._groups

tl.wasserstein_features

varm[key_added], uns["mantispy"][key_added] and ..._groups

tl.hit_calling

uns["mantispy"][key_added], obs[key_added + "_distance"], obs[key_added + "_qvalue"]

tl.edistance

uns["mantispy"][key_added], or ..._pairwise when reference=None

tl.transport

uns["mantispy"][key_added] and ..._units, obs[key_added + "_agreement"]

tl.dose_response

uns["mantispy"][key_added]

tl.nn_moa_classify

obs[key_added + "_predicted"], uns["mantispy"][key_added] and ..._confusion

tl.moa_enrichment

uns["mantispy"][key_added]

tl.feature_sets

returns a decoupler network; stores nothing

tl.enrich

obsm["score_<method>"], obsm["padj_<method>"] (written by decoupler)

tl.rank_features

uns["mantispy"][key_added]

tl.rank_sets

uns["mantispy"][key_added]

tl.cluster_composition

returns a new object: wells by clusters; uns["mantispy"]["composition_test"]

tl.subpopulation_hits

uns["mantispy"][key_added]

tl.cell_cycle_phase

obs[key_added]

tl.neighbors_local_density

obs[key_added]

tl.replicate_saturation

uns["mantispy"][key_added]

tl.cytotoxicity

uns["mantispy"][key_added], obs[key_added + "_suspect"]

tl.gene_sets

returns a gene-set network; stores nothing

tl.pathway_coherence

uns["mantispy"][key_added], sorted by coherence

tl.enrich_hits

uns["mantispy"][key_added]

tl.map has four modes, named after the questions the field asks. "activity" matches the copairs reference implementation (0.9267 against 0.9267 over 301 JUMP compounds):

mode

question

needs

"activity"

is this perturbation distinguishable from the negative controls?

controls

"consistency"

do perturbations sharing an annotation look alike, against those that do not?

annotation_key

"replicability"

do a perturbation’s replicates retrieve each other against everything else?

"cross_plate"

the same, counting only replicates from a different plate

Metadata_Plate

Metrics#

metrics.silhouette_label(adata, label_key[, ...])

How well separated the biological labels are, rescaled to [0, 1].

metrics.silhouette_batch(adata, label_key, ...)

How well mixed the batches are within each biological label.

metrics.lisi(adata, key[, use_rep, ...])

Median LISI over rows.

metrics.pc_regression(adata, key[, use_rep, ...])

Variance-weighted R^2 of the principal components on key.

metrics.batch_variance_explained(adata, keys)

pc_regression() for several covariates, stacked into one frame.

metrics.evaluate_correction(adata[, reps, ...])

Run every metric for every representation and stack the results.

metrics.diagnose_testing(adata[, groupby, ...])

Check whether differential testing is calibrated on this screen.

Metrics do not modify the object. Each returns a tidy frame with metric, representation, key and value, so results from several calls stack. evaluate_correction adds a better column giving the direction of improvement for each metric, because batch mixing and biological separation trade off against each other and neither is meaningful alone.

Accessors#

get.features(adata[, object, feature_group, ...])

Names of the features matching every filter given.

get.to_dataframe(adata[, layer, metadata, ...])

Wide, pycytominer-shaped frame: Metadata_ columns first, then features.

get.controls(adata[, kind])

Boolean mask of control rows.

get.obs_df(adata[, keys, obsm_keys, layer, ...])

Return values for observations in adata.

get.var_df(adata[, keys, varm_keys, layer])

Return values for observations in adata.

Accessors do not modify the object. get.to_dataframe returns the flat table that pycytominer and similar tools expect.

Plotting#

pl.plate(adata, color[, plate, agg, ax, cmap])

Well-grid heatmap of color, one panel per plate.

pl.cell_counts(adata[, groupby, ax])

Distribution of cells per well, split by groupby.

pl.feature_distributions(adata, features[, ...])

Per-feature distributions, before and after normalization when layer_before exists.

pl.nan_matrix(adata[, max_features, ax])

Fraction of missing values per feature, per plate.

pl.qc(adata[, figsize])

Two-by-two summary of the QC metrics calculate_qc_metrics() writes.

pl.plate_effects(adata[, feature, axes])

Row and column medians per plate, for spotting plate position artifacts.

pl.image_qc(adata[, ax])

Image quality score per image, with the flagged images marked.

pl.control_drift(adata[, groupby, ...])

Control wells projected onto principal components fitted on the controls alone.

pl.outliers(adata[, key, axes])

Outlier score distribution, and the flagged fraction per plate.

pl.feature_correlation(adata[, key, ...])

Correlation heatmap with features ordered by their annotation.

pl.feature_groups(adata[, key, ax])

How many features each group contributes, split by channel.

pl.feature_signature(adata[, groupby, top, ...])

Heatmap of perturbations by feature families.

pl.map(adata[, key, label_top, ax])

Mean average precision against significance, with the strongest groups labeled.

pl.replicate_correlation(adata[, key, ax])

Observed replicate correlation against each group's permutation threshold.

pl.hits(adata[, key, label_top, ax])

Distance from the controls against significance, with the most distant groups labeled.

pl.effect_sizes(adata, group[, key, top, ax])

The largest effects for one group, colored by feature family.

pl.feature_volcano(adata, group[, key, ...])

Effect against significance, per feature, for one group.

pl.dose_response(adata, compound[, key, ...])

One compound's response against dose, with the fitted curve when there is one.

pl.moa_confusion(adata[, key, normalize, ax])

The confusion matrix of nn_moa_classify(), as a heatmap.

pl.moa_enrichment(adata, group[, key, top, ax])

Which mechanisms one profile's neighborhood is enriched for.

pl.distance_heatmap(adata[, key, groupby, ax])

The group-by-group distance matrix, ordered so related groups sit together.

pl.setting_agreement(adata[, key, by, ...])

Settings against settings: which plates, batches or laboratories agree with each other.

pl.transport(adata[, key, level, top, ax])

Agreement per perturbation, ranked, with the ones that reproduce colored.

pl.sets_heatmap(adata, groupby[, score_key, ...])

Mean enrichment score per group per feature set.

pl.cluster_composition(composition[, ...])

Stacked bars of cell-state fractions, averaged within each group.

pl.cell_cycle(adata, dna_feature[, by, key, ...])

Log DNA intensity per group, colored by assigned phase.

pl.density(adata, feature[, groupby, key, ...])

Local cell density against a feature, per group.

pl.subpopulation_hits(adata[, key, top, ax])

Cluster by group heatmap of significance, so an effect in one cell state stands out.

pl.replicate_saturation(adata[, key, ax])

The saturation curve with its spread across draws.

pl.cytotoxicity(adata[, key, label_top, ax])

Distance from the controls against viability, with the suspect groups marked.

pl.pathway_coherence(adata[, key, top, ax])

Coherence per gene set, the significant ones marked.

pl.batch_variance(adata, keys[, use_rep, ...])

R^2 of each principal component on each covariate.

pl.metrics(table[, ax])

Grouped bars of an evaluate_correction() table.

pl.similarity(adata[, key, groupby, max_obs, ax])

Profile-by-profile similarity, ordered by groupby so blocks are visible.

There is no dedicated function for a plate map of a per-well flag, which is mt.pl.plate(adata, color="qc_well_pass"), or for embeddings side by side, which is a loop over sc.pl.embedding.

Plotting functions return Matplotlib axes and do not modify the object.

Datasets#

ds.synthetic_plate([n_plates, n_wells, ...])

Generate a synthetic plate with known ground truth.

ds.blobs(*[, n_wells, n_sites, n_cells, ...])

A small synthetic Cell Painting plate, laid out like one mantispy.io.read_plate() returns.

ds.bbbc021([cache_dir])

BBBC021, MCF-7 cells treated with small molecules, the standard mechanism-of-action benchmark.

ds.rohban([plates, cache_dir])

The Rohban 2017 ORF overexpression screen, with the genes and cell counts that BBBC021 lacks.

ds.pki([plates, cache_dir])

Kinase inhibitors over a dose series, from the JUMP pilot.

ds.jump_target2([plates, annotate, cache_dir])

JUMP-Target-2, one 384-well plate map run at many sites.

ds.agnp([cache_dir])

Silver nanoparticles, 180 wells at four sizes and three doses.

ds.amish([cache_dir])

An Amish cohort, 468 wells varying seeding density and timepoint.

ds.chroma([cache_dir])

Alternative dyes, 3,455 wells across eight channels.

ds.jump_crispr([cache_dir])

The assembled JUMP CRISPR arm, 51,185 wells of knockouts.

ds.luad([cache_dir])

LUAD alleles, 6,144 wells of wild-type against mutant.

ds.miami([cache_dir])

MIAMI, 2,775 wells of compounds in U2OS cells.

ds.neuropainting([cache_dir])

Astrocytes and neurons, 1,691 wells imaged at 20x and 63x.

ds.oasis_pilot([cache_dir])

OASIS pilot, 4,604 wells in U2OS and HepaRG.

ds.pooled_rare([cache_dir])

Pooled rare variants, 290 barcodes in a pooled screen.

By default io.read_profiles keeps features from the three compartments (Cells, Cytoplasm, Nuclei), matching pycytominer.infer_cp_features. Whole-field Image_ measurements are excluded (a JUMP plate has 1077 of them against 3634 per-cell features); objects=None keeps them.

synthetic_plate and blobs are generated locally. synthetic_plate is a single-cell profile table with injected artifacts for quality control to find; blobs is a small SpatialData plate of images, labels and tables. The other datasets download once, checked against a pinned sha256, into mt.settings.cache_dir (set MANTISPY_CACHE_DIR to change it). Four of them carry the annotations the analysis functions need:

dataset

download

perturbations

carries

bbbc021

~10 MB

39 compounds

MOA labels, the classic retrieval benchmark

rohban

~27 MB

194 overexpressed genes

cell counts, ~10 replicates per gene

pki

~71 MB

15 kinase inhibitors x 7 doses

cell counts, MOA labels, 32-64 replicates

jump_target2

~40 MB

302 compounds, one shared plate map

the same plate run at two sites, so any difference between them is technical

The other nine are further gallery accessions, normalized and feature-selected by their authors and read with the io.read_profiles defaults. Use them to run a method across a range of screens.

Check anything tuned on one dataset against the others. Cutoffs that looked universal on BBBC021 turned out to be dataset-dependent on rohban and pki.

Settings#

settings

Allows users to customize settings for the mantispy package.

settings.override(*[, cache_dir, verbosity])

Provides local override via keyword arguments as a context manager.

settings.reset(*names)

Reset passed settings to their default values.

Both settings, verbosity and cache_dir, are also read from MANTISPY_VERBOSITY and MANTISPY_CACHE_DIR, and with mt.settings.override(verbosity=2): changes one for a block.

Relative to scmorph#

scmorph is the other AnnData-based morphological profiling package. mantispy covers its functionality with two exceptions:

scmorph

here

reading CellProfiler CSV output

io.read_profiles, on an ExportToSpreadsheet directory or on published tables

reading a CellProfiler SQLite database

not yet (planned)

quality control, batch correction, aggregation

pp and tl.aggregate

feature selection

pp.feature_select (pycytominer-equivalent), pp.feature_select_chatterjee, pp.feature_reproducibility

trajectory inference (slingshot, differential progression)

out of scope: a trajectory through morphology space needs an ordering the assay rarely justifies, and scanpy’s sc.tl.paga and sc.tl.dpt already work on these objects

Hit calling, effect sizes, dose response, mechanism retrieval, feature-set and pathway enrichment, cell-state composition, replicate power and cytotoxicity have no counterpart in scmorph.

Not reimplemented here#

Dimensionality reduction, neighborhood graphs, embeddings and clustering come from scanpy. Call them directly on the same object:

import scanpy as sc

sc.pp.pca(wells, n_comps=50)
sc.pp.neighbors(wells)
sc.tl.umap(wells)

Harmony is wrapped as pp.harmony. It is the last step of the JUMP consortium’s recipe [CCG+24] and the best performer in Arevalo et al. [ASE+24]. It needs the optional extra:

pip install 'mantispy[harmony]'

It corrects an embedding rather than the features, so run sc.pp.pca first. The other corrections are pp.sphere, pp.correct_plate_position and pp.regress_out.

The data contract#

X is float32, one row per profile. obs carries Metadata_ columns identifying where each profile came from; var carries the parsed feature annotation (object, feature_group, feature, channel, scale, angle, gray_levels, radial_bin, params, is_feature). uns["mantispy"] holds the schema version, the resolution, the channel vocabulary and provenance.

Resolution ("cell", "well", "perturbation") is advisory. It sets which identifier columns io.validate requires, and functions warn instead of raising when they get a resolution they do not expect.

The machine-readable contract is published as spec/schema-1.0.json, alongside the earlier spec/schema-0.1.json.

Seeing what mantispy is doing#

mantispy logs what it drops, skips and shrinks through the standard library’s logging. Warnings print by default; raise the verbosity to see the rest:

import mantispy as mt

mt.settings.verbosity = 2  # 0 errors, 1 warnings (default), 2 info, 3 debug

Set it to 2 when you first run a new screen. Several functions discard features or wells, and they log it at that level.

Working backed#

mt.io.read(path, backed="r") leaves X on disk. obs and var stay in memory, so metadata, QC flags and feature selection work unchanged. Functions that rewrite X need copy=True and raise a ValueError saying so when it is missing.

operation

backed

pp.calculate_qc_metrics, pp.outliers, pp.feature_select, pp.well_qc

yes (they write obs/var)

tl.aggregate, tl.consensus, tl.map, tl.hit_calling, the metrics

yes (they return new objects or tables)

pp.normalize, pp.sphere, pp.correct_plate_position, pp.regress_out

with copy=True; the result is materialized

writing X in place

no; the file is opened read-only

Grouped reductions read one group at a time when the matrix is on disk, and the tests assert that the results are identical to the in-memory path. For a per-plate median on 100 000 cells x 500 features, backed mode peaks at 44 MB against a 200 MB resident matrix in memory, and takes 3.5 s against 0.8 s. That is about four times the runtime for four times less memory, so use it only on data that does not fit in memory.

Performance#

benchmarks/ holds a suite that is not part of the test run; run it with pytest benchmarks -s. Measured on a laptop, 100 000 cells by 500 features (X is 200 MB):

operation

time

peak

np.median over the whole matrix (the floor)

0.86 s

200 MB

pp.normalize, per plate

2.06 s

232 MB

pp.feature_select

0.41 s

458 MB

tl.aggregate to 7680 wells

0.39 s

78 MB

pp.sphere on those wells

0.48 s

227 MB

tl.hit_calling, 1000 permutations

0.21 s

7 MB

tl.map (copairs)

65 s

2465 MB

At 500 000 cells the preprocessing scales linearly: normalize 12.7 s, aggregate 2.0 s. tl.map is by far the most expensive step, in both time and memory.

Stability#

The schema is frozen at 1.0. The names in spec/schema-1.0.json (the required and reserved obs columns, the var annotation, the uns["mantispy"] keys and the result tables) are stable, as are the public signatures in the tables above.

  • Anything removed gets a DeprecationWarning for two minor releases first.

  • mt.io.read migrates a file written against an older schema on read. A schema version this build does not know raises an error.

  • _core and other _-prefixed modules are private.

io.read_plate and ds.blobs bring images and segmentations in as SpatialData on top of this contract, without changing it.