Skip to content

Python API

The package is installed from PyPI with pip installm3-sc and imported as import m3. It has a one-to-one counterpart in the R API — both drive the same engine and produce identical results.

The public API has two pieces:

  1. an input facade (m3.read_h5, m3.read_h5ad, m3.from_anndata, m3.read_matrix, m3.concat, and m3.datasets.*) that turns common single-cell formats into one canonical Dataset; and
  2. the m3.M3 model object, whose methods cover representation learning, donor-level disease prediction, integrated-gradients attribution, and generative augmentation.

A typical run is three lines:

import m3

data  = m3.datasets.liu_demo()                          # 1. load a Dataset
model = m3.M3(data, condition_keys=["cond_group"],      # 2. declare column roles
              celltype_key="mergedcelltype",
              donor_key="sample_id").train()    # 3. train
emb   = model.embedding(part="bio")                     #    read out

Capability gating

m3 is staged, and which read-outs are available depends on how the model was built and trained. Every trained model unlocks the integration-level read-outs (embedding, reconstruct, generate, augment). The donor-level read-outs (predict_donors, donor_embedding, attribute) additionally require the donor predictor, which trains automatically when both donor_key and celltype_key were supplied at construction (and is skipped when you pass donor_prediction=False). Calling a read-out before its capability is unlocked raises m3.M3CapabilityError. Inspect model.capabilities at any time:

model.capabilities
# {'embedding': True, 'reconstruct': True, 'predict_donors': True}

Input facade

All readers return a Dataset holding raw counts. m3 normalises internally, so do not pre–log-normalise the input.

m3.read_h5

m3.read_h5(*, rna: str | None = None, adt: str | None = None,
           atac: str | None = None, metadata: str,
           batch: str = "batch0") -> Dataset

Read one batch from paper-format HDF5 count matrices plus a metadata CSV. Pass the path to each modality you have (at least one of rna / adt / atac) and the per-cell metadata CSV. Matrix orientation (cells×features vs. features×cells) is resolved automatically against the feature count.

m3.read_h5ad

m3.read_h5ad(path: str, *, batch: str = "batch0",
             modality: str = "rna") -> Dataset

Read one batch from an AnnData .h5ad. If adata.var has a feature_types column (Gene Expression / Antibody Capture / Peaks), the matrix is split into modalities; otherwise the whole matrix is taken as modality.

m3.from_anndata

m3.from_anndata(adata, *, batch: str = "batch0",
                modality: str = "rna") -> Dataset

Build one batch from an in-memory AnnData. Splitting by feature_types behaves as in read_h5ad.

m3.read_matrix

m3.read_matrix(*, counts: dict, obs: 'pd.DataFrame', var: dict,
               batch: str = "batch0") -> Dataset

Build one batch from in-memory per-modality count arrays.

  • counts — {modality -> array | sparse} of raw counts, shape [n_cells, n_features]
  • obs — per-cell DataFrame, n_cells rows
  • var — {modality -> list/Index} of feature names, aligned to each matrix's columns

m3.concat

m3.concat(datasets: 'list[Dataset]') -> Dataset

Stack single-batch Datasets into one multi-batch Dataset. Features per modality are harmonised by name intersection (the first batch's order is kept); a batch missing a whole modality is zero-filled and flagged in present (mosaic design). Batch labels must be unique across inputs.

m3.datasets.liu_demo

m3.datasets.liu_demo(n_cells: int | None = None) -> Dataset

Load the built-in Liu et al. COVID-19 demo — a ~30 k-cell (30,534) stratified subsample of the full ~135 k-cell dataset, shipped inside the wheel so every tutorial runs with no downloads.

>>> data = m3.datasets.liu_demo()
>>> print(data)
Dataset(n_cells=30534, batches=['B1', 'B2', 'B3'], modalities=[rna:1000, adt:192])

obs carries the columns batch (B1 / B2 / B3), sample_id, Donor, cond_group (HC / Severe), Age_interval, and mergedcelltype.

Pass n_cells (2,250 to 21,489) to load a smaller, fixed subsample instead of all 30,534 cells. Training time is proportional to the number of cells, so this is the quickest way to try cell-level integration on a CPU:

>>> m3.datasets.liu_demo(n_cells=6000)
Dataset(n_cells=6000, batches=['B1', 'B2', 'B3'], modalities=[rna:1000, adt:192])

The same cells are returned on every call. Each batch contributes equally, and every cell type of every batch keeps at least 50 cells (or all it has), so no cell type is lost. Donor-level prediction and attribution need enough cells per donor, so use the full demo there.


The Dataset container

The readers return an immutable, multimodal, multi-batch raw-count container. You normally obtain one from a reader rather than constructing it by hand. Useful read-only members:

Member Meaning
data.modalities {name -> scipy.sparse.csr_matrix} of raw counts
data.obs per-cell DataFrame (always contains a batch column)
data.var {name -> pd.Index} of feature names per modality
data.present {name -> bool ndarray} per-cell modality presence (mosaic)
data.n_cells number of cells
data.modality_names list of modality names, e.g. ['rna', 'adt']
data.batches list of batch labels
>>> data.modality_names
['rna', 'adt']
>>> data.obs["mergedcelltype"].value_counts().head(3)

m3.M3

m3.M3(
    dataset,
    *,
    condition_keys,                  # str | list[str]  (required)
    target_condition: str | None = None,
    donor_key: str | None = None,
    celltype_key: str | None = None,
    batch_key: str = "batch",
    held_out: list | None = None,
    held_out_samples: list | None = None,
    hvg: dict | None = None,
    embedding_dim: int = 30,
)

Construct a model and freeze the column-role contract against dataset.obs. Nothing is trained yet — call train.

Argument Role
condition_keys one or more obs columns the model is made aware of; each gets its own latent factor. m3 is condition-aware, so this is required.
target_condition the single condition used for prediction / attribution (defaults to the first of condition_keys).
donor_key obs column identifying a donor/sample. Required for donor-level prediction, attribution, and augment.
celltype_key obs column with cell-type labels. Required for train.
batch_key obs column with the batch label (default "batch", written by every reader). It is the integration / held-out unit, the site the donor adversary removes, and the stratifier for augment(batch=...). After training, cell_metadata["batch"] holds this column's values. A single-batch dataset trains without batch correction and warns.
held_out batch label(s) to hold out as an unlabelled query set — their target_condition labels are masked during training and recovered by predict_donors.
held_out_samples donor/sample ID(s) to hold out as the query set instead of a whole batch — may span batches; requires donor_key and is mutually exclusive with held_out. Their target_condition labels are masked during training and recovered by predict_donors.
hvg {modality -> n_top} highly-variable-feature selection, e.g. {"rna": 1000}. Omit to use all features as given.
embedding_dim size of the shared biological latent (default 30).

After construction, model.contract records the frozen roles, including model.contract["reference_vocab"] (the condition values observed in the reference batches).

M3.train

M3.train(
    *,
    max_epochs: int = 300,
    lr: float = 1e-5,
    batch_size: int = 256,
    early_stop_patience: int = 300,
    min_delta: float = 0.0,
    val_percentage: float = 0.1,
    weight_batch_ae: float = 1.0,
    balance_batches: bool = True,
    donor_prediction: bool | None = None,
    donor_predictor: dict | None = None,
) -> M3

Train the integration VAE and, when applicable, the donor predictor on top. Returns the model (so you can chain .train()). Uses CUDA automatically when available; otherwise CPU.

  • donor_prediction — None (default) auto-enables the donor predictor when donor_key and celltype_key are set. Pass False for pure representation learning, True to require it.
  • balance_batches — when True (default), the VAE trains on a batch-balanced subset; with no held-out query, downstream steps are automatically rebuilt on the full reference so sparse cell types are not biased out.
  • donor_predictor — a dict tuning the Stage-2 adversarial corrector. Recognised keys (defaults): enc_lr (1e-5), glr (5e-3), n_epochs (30), patient_w (3.0), adv_max (1.0), adv_warmup (10), n_disc (1), hidden (64), min_cells (5).

Read-outs

m3.embedding

M3.embedding(part: str = "bio") -> np.ndarray

Cell-level latent, row-aligned to cell_metadata — not necessarily to the input. Rows are grouped by batch; with held_out / held_out_samples they are reference cells then query cells. Align on the obs_names column that from_anndata / read_h5ad carry through, never by position:

emb = pd.DataFrame(model.embedding("intrinsic"),
                   index=model.cell_metadata["obs_names"])
adata.obsm["X_m3"] = emb.reindex(adata.obs_names).to_numpy()

part selects which factor(s):

  • "bio" — the intrinsic biological latent plus every condition factor (the integration embedding for clustering / UMAP)
  • "intrinsic" — the intrinsic biological latent only
  • "batch" — the 2-d batch factor (should carry batch signal, not biology)
  • any name in condition_keys — that condition's 2-d factor

m3.reconstruct

M3.reconstruct(*, remove_batch: bool = True) -> dict

Posterior-mean decode of every cell, returned as {modality -> ndarray} ([n_cells, n_features]). With remove_batch=True (default) the batch factor is zeroed before decoding, yielding a batch-corrected matrix suitable for cross-batch differential expression.

m3.predict_donors

M3.predict_donors(*, include_reference: bool = False) -> pd.DataFrame

Donor-level prediction of target_condition. Requires the donor predictor. Returns one row per donor with columns donor, is_reference, predicted_label, and a prob_<label> column per class. By default only the held-out query donors are returned; pass include_reference=True to include the reference donors too.

m3.donor_embedding

M3.donor_embedding() -> pd.DataFrame

The corrector-corrected per-donor vector used for prediction — a DataFrame indexed by donor, with an is_reference column followed by embedding dimensions (m3_0, m3_1, …). Join with predict_donors(include_reference=True) for labels, or feed straight into UMAP for a patient-level map. Requires the donor predictor.

m3.generate

M3.generate(*, tau: float = 0.8, seed: int = 42) -> dict

Posterior-resampled synthetic cells (one per reference cell, 1:1), returned as {modality -> ndarray}. tau scales the sampling temperature. Handy for noise-augmenting a training set.

m3.augment

M3.augment(*, conditions: list, n_donors: list, tau: float = 0.8,
           batch: str | None = None, seed: int = 42) -> dict

Synthesise new donors per condition by posterior-resampling real donor templates. conditions lists the target_condition values to generate and n_donors how many synthetic donors for each (same length). Returns {"expression": {modality -> ndarray}, "obs": DataFrame}. When batch is given (a value of batch_key), templates are drawn only from that batch — useful for reproducing batch-stratified figures. Requires donor_key + celltype_key.

m3.cell_metadata

M3.cell_metadata  # property -> pd.DataFrame

Row-aligned metadata for embedding / reconstruct (reference cells first, then query cells). Data read with from_anndata / read_h5ad carries the input barcodes in an obs_names column; for read_matrix / read_h5, put an identifier column in obs yourself.


Attribution

m3.attribute

M3.attribute(*, reference_labels: list, target_class: int | None = None,
             n_steps: int = 50) -> Attribution

End-to-end integrated-gradients attribution of target_condition, returning an Attribution. Requires the donor predictor.

  • reference_labels — the healthy/baseline label(s) of target_condition (e.g. ["HC"]). These set the IG baseline at two levels: the mean expression of reference cells (cell-level IG) and the mean patient vector over reference donors (donor-level IG). Supplying them is what keeps housekeeping / pan–cell-type genes from dominating the ranking.
  • target_class — which non-reference class to attribute toward; defaults to the first class not in reference_labels.
  • n_steps — IG interpolation steps (default 50; lower is faster but biased).

The Attribution object

Returned by attribute. Holds several views of importance:

Member Type Meaning
attr.genes DataFrame(feature, importance) raw mean(\|IG\|) over all cells — quick, unfiltered
attr.celltypes DataFrame(celltype, importance) per–cell-type importance
attr.donors DataFrame(donor, attribution) or None per-donor attribution
attr.cells ndarray signed per-cell importance
attr.attribution ndarray [n_cells, n_features] full cell × feature IG matrix
attr.gene_celltype_matrix ndarray [n_celltypes, n_features] signed per–(cell-type, feature) attribution
attr.target_label str the attributed (non-reference) condition

Attribution.top_genes

Attribution.top_genes(n: int = 100, *, min_cells_per_condition: int = 200,
                      exclude_regex: str | None = <housekeeping>,
                      modality: str | None = None) -> pd.DataFrame

The publication ranking recipe. Drops cell types where either the reference or the target condition has fewer than min_cells_per_condition cells, scores each feature as mean(|gene_celltype_matrix|) across the kept cell types, removes housekeeping / ribosomal genes by name (exclude_regex, default matches ^MT-, ^RPL, ^RPS, …; pass None to skip), optionally restricts to one modality ("rna" / "adt" / "atac"), and returns the top-n ranked DataFrame with columns feature, modality, score, n_celltypes_used.

Attribution.top_celltypes

Attribution.top_celltypes(*, min_cells_per_condition: int = 200) -> pd.DataFrame

Cell-type importance ranking, filtered to types with at least min_cells_per_condition cells in both the reference and the target condition (default 200; set 0 for the raw celltypes table).


Exceptions

  • m3.M3CapabilityError — a read-out was requested before its capability was unlocked (subclasses RuntimeError). The message names what is missing and how to obtain it.

Versioning

The installed version is available as m3.__version__.


R ⇄ Python correspondence

Every verb has a 1:1 counterpart in the R API. The R package wraps the same engine, so a script translated line-for-line produces identical numbers.

Python R
m3.datasets.liu_demo() m3_demo()
m3.read_h5() / m3.read_h5ad() m3_read_h5() / m3_read_h5ad()
m3.from_anndata() / m3.read_matrix() m3_dataset()
m3.concat() m3_concat()
m3.M3(...).train() m3_train(...)
M3.capabilities m3_capabilities()
M3.embedding() m3_embedding()
M3.cell_metadata m3_cell_metadata()
M3.reconstruct() m3_reconstruct()
M3.predict_donors() m3_predict_donors()
M3.donor_embedding() m3_donor_embedding()
M3.generate() m3_generate()
M3.augment() m3_augment()
M3.attribute() m3_attribute()
Attribution.top_genes() / .top_celltypes() m3_top_genes() / m3_top_celltypes()
— m3_shutdown() (R only — releases the engine session)

See also