Advanced Features¶
This page documents optional input files and parameters that activate advanced modes of CardamomOT. All optional files are placed in my_project/Data/ (or set directly in adata.obs).
Project directory structure (full)¶
my_project/
├── Data/
│ ├── data.h5ad # required
│ ├── gene_list.txt # optional — restrict to a gene subset
│ ├── stimulus_schedule.txt # optional — custom stimulus values per timepoint
│ ├── stimulus_schedule_simul.txt # optional — overrides schedule for simulation only
│ ├── ref_network.csv # optional — prior interaction graph (sparsity mask)
│ ├── basal_init.npy / .csv # optional — warm-start for basal parameters
│ ├── inter_init.npy / .csv # optional — warm-start for interactions
│ ├── basal_ref.npy / .csv # optional — regularisation target for basal
│ ├── inter_ref.npy / .csv # optional — regularisation target for interactions
│ ├── KO_OV_inference.txt # optional — per-sample KO/OV prior (requires dataset_id)
│ ├── KO_OV_simulate.txt # optional — in-silico perturbations to simulate
│ └── transition_rates.csv # optional — cell-type transition cost matrix for OT
└── cardamomOT/ # generated by the pipeline
├── basal.npy # (n_samples, G, n_networks)
├── inter.npy # (G, G, n_networks)
└── basal_ref_mask.npy # (n_samples, G) bool — from KO_OV_inference.txt
Gene subset (Data/gene_list.txt)¶
By default select_DEgenes_and_split.py selects differentially expressed genes automatically. To fix the gene set manually, place a plain-text file with one gene name per line:
CHGA
STMN2
PHOX2B
HAND2
...
cardamomot step select_DEgenes_and_split -i my_project -s full -c 0 --mean-forcing 0.5 --force-basins 1.0 --temporal-basins 1
When gene_list.txt is present, those genes are added to the final selection — they are not a replacement. The full behaviour depends on the -c flag:
-c 1: genes variable across time, conditions, and cell types are selected automatically, then the genes ingene_list.txtare appended (provided they are non-constant in time).-c 0: no automatic differential selection is performed; only the genes ingene_list.txtthat are non-constant in time are kept.
Read-depth correction (infer_rd)¶
When cells have heterogeneous sequencing depths, run this optional step before gene selection:
cardamomot step infer_rd -i my_project
This identifies Poisson-like genes (Chronocell method, Fang et al. 2024) and stores a per-cell read-depth factor in adata.obs['rd']. Subsequent steps (infer_mixture, infer_network_structure) use it automatically when the column is present.
Key parameters: --var_threshold 1.2 (Poisson variance threshold), --min_mean 0.1.
Requires at least 1 000 genes. Below this threshold the step is silently skipped.
Prior interaction network (prepare_reference_network)¶
To bias network inference toward known biology, first build a prior network from public databases (OmniPath, etc.):
cardamomot step prepare_reference_network -i my_project -d 3
This generates Data/ref_network.csv, a gene × gene binary (or weighted) interaction matrix. The --prior parameter then controls how strongly edges absent from this prior are penalised.
|
Effect |
|---|---|
|
No penalisation — all edges equally possible |
|
Soft penalisation — absent edges discouraged |
|
Hard mask — only edges present in the prior are allowed |
--prior must be passed to both steps — it constrains inference in infer_network_structure and the simulation reference network in infer_network_simul. Always use the same value in both.
# Easiest: let the pipeline handle it (prior network with default depth 3)
cardamomot pipeline -i my_project -s full --ref --prior 0.5
# With a shallower graph (faster):
cardamomot pipeline -i my_project -s full --ref --ref-depth 1 --prior 0.5
# Or step by step — pass --prior to both:
cardamomot step infer_network_structure -i my_project -s full --prior 0.5
cardamomot step infer_network_simul -i my_project -s full --prior 0.5
Custom stimulus schedule (Data/stimulus_schedule.txt)¶
By default one stimulus is 0 at the first timepoint and 1 at all subsequent ones. To define a custom schedule, create a tab/space-separated file where rows = timepoints and columns = stimulus channels:
# stimulus_schedule.txt
0.0 0.0
1.0 0.0
1.0 1.0
Values between 0 and 1 are allowed (partial stimulus strength).
If fewer rows than timepoints are provided, the last row is repeated.
To use a different schedule for simulation (e.g. a novel protocol), place
Data/stimulus_schedule_simul.txt; it takes priority overstimulus_schedule.txtduringsimulate_networkandsimulate_network_KOV.
The --stimulus parameter controls how strongly the stimulus regulates genes in the inferred network:
|
Effect |
|---|---|
|
Full stimulus influence |
|
Stimulus has no regulatory influence |
Multiple experimental samples (dataset_id)¶
If your experiment contains several biological conditions that share a gene regulatory network but have different basal transcription rates (e.g. different cell lines, donors, or perturbation backgrounds):
adata.obs['dataset_id'] = ... # string or integer label per cell
CardamomOT then:
Builds one set of per-sample basal parameters
θ_basal(s)for each sample.Runs a single joint optimisation over shared interaction weights and all per-sample basals simultaneously.
Saves
basal.npywith shape(n_samples, G, n_networks).
Keeping per-sample basals close (constrain_basal_uniform): set model.constrain_basal_uniform = λ (e.g. λ = 100–1000) to add an L2 penalty pushing each sample’s basals toward their common mean. Genes with a non-zero basal_ref entry for a given sample (e.g. a KO prior) are excluded from the penalty for that sample.
Regularisation targets (basal_ref, inter_ref)¶
Place reference arrays in Data/ to anchor the optimiser:
File |
Shape |
Role |
|---|---|---|
|
|
Regularisation target for basal parameters |
|
|
Regularisation target for interactions |
|
same as |
Warm-start values (not regularised) |
|
same as |
Warm-start values for interactions |
CSV files must have gene names matching adata.var_names as row and column indices.
KO/OV inference with per-sample priors (Data/KO_OV_inference.txt)¶
When dataset_id is present, you can encode prior knowledge about which genes are knocked out or overexpressed in each sample:
# KO_OV_inference.txt (tab-separated, header required)
sample_id KO OV
wt 0 0
ko_CHGA CHGA 0
ov_STMN2 0 STMN2
ko_CHGA_ov_STMN2 CHGA STMN2
sample_idmust matchadata.obs['dataset_id'].KOgenes are forced tobasal = −100(silent during inference).OVgenes are forced tobasal = +100(always active).Multiple genes per cell:
CHGA,POSTN.
During inference, this replaces basal_ref for the affected genes/samples and saves cardamomOT/basal_ref_mask.npy for use by simulate_network_KOV.
cardamomot step infer_network_structure -i my_project -s full
# KO_OV_inference.txt is read automatically when present
In-silico perturbation simulation (Data/KO_OV_simulate.txt)¶
After training, simulate arbitrary KO/OV combinations by defining them in:
# KO_OV_simulate.txt (tab-separated, header required)
KO OV
none none
CHGA STMN2
POSTN S100B,STMN2
CHGA-80 STMN2-60
Each row produces one independent simulation saved as cardamomOT/adata_sim_KO_*_OV_*_stim*.h5ad.
Partial KO/OV (GENE-X syntax): append -X (0 < X < 100) for a partial perturbation of strength X%:
Mode |
Creation rate factor |
Effect |
|---|---|---|
|
|
Reduced production → lower steady-state |
|
|
Increased production → higher steady-state |
Output files use the pctX suffix to distinguish partial from complete perturbations (KO_CHGApct80_OV_STMN2pct60).
cardamomot step simulate_network_KOV -i my_project -s full
Visualise results with:
from CardamomOT import plot_results_sim_kov, compare_cell_types
p, stim, prior = "my_project/", 1.0, 1.0
compare_cell_types(p, "KO_CHGA_OV_none", split="full", stim=stim, prior=prior)
plot_results_sim_kov(p, "KO_CHGA_OV_none", stim=stim, prior=prior)
Population dynamics: proliferation, death, and transition rates¶
Per-cell proliferation/death (adata.obs)¶
adata.obs['prolif_rate'] = ... # net proliferation rate per cell
adata.obs['death_rate'] = ... # net death rate per cell
When both columns are present, the OT marginals are corrected so that faster-growing cells carry proportionally more weight as trajectory sources. This implements demographically corrected OT (as in Waddington OT, Schiebinger et al. 2019).
Cell-type transition rates (Data/transition_rates.csv)¶
To bias the OT cost toward biologically plausible transitions:
# transition_rates.csv — rows = source type at t1, cols = target type at t2
,TypeA,TypeB,TypeC
TypeA , 0.3, 0.1, 0.01
TypeB , 0.05, 0.2, 0.05
TypeC , 0.01, 0.05, 0.3
Row/column names must match adata.obs['cell_type']. At each consecutive timepoint pair, transition probabilities are computed as exp(rate × Δt) and rescaled so that the mean weight per row equals 1 — transitions with weight > 1 become cheaper (preferred), weight < 1 become more expensive (penalised).
Proliferation-aware simulation (--proliferation)¶
CardamomOT can learn and simulate net proliferation rates (R = birth − death) directly from the inferred optimal-transport couplings, without requiring external annotations.
How it works¶
Learning R from the OT coupling.
During trajectory inference (infer_network_structure), at each consecutive timepoint pair (t → t+1), the row marginals of the optimal coupling encode the effective mass transported out of each source cell. The optimal net growth rate per cell is recovered via the self-consistent zero-mean inversion of the WOT marginal formula:
m_n = (1/N) · exp(R_n · Δt/2) / C (WOT: source marginal, normalised)
R_n = (2/Δt) · (log(m_n) − mean_k(log(m_k)))
The factor 2/Δt comes from the WOT convention where both the source and the target marginals are corrected by exp(R · Δt/2) each — the formula recovers the true net rate R from the source marginal alone. With balanced OT (default) all m_n = 1/N so R_n = 0 exactly; with unbalanced OT (model.unbalanced_reg > 0) they vary and encode genuine differential growth. The resulting R_opt array (one value per cell per timepoint transition) is always saved to cardamomOT/data_R_opt.npy.
Training the ProliferationMLP.
infer_network_simul --proliferation reads data_R_opt.npy and trains a lightweight two-hidden-layer MLP (64 units, Tanh activation, MSE loss):
R(P) : protein levels → net proliferation rate
The network is trained on (prot[:, ns:], R_opt) pairs after any recomputation of protein trajectories, so it captures the optimal R consistent with the final inferred network. The weights are saved to cardamomOT/prolif_network.pt.
Branching simulation.
simulate_network --proliferation (and simulate_network_KOV --proliferation) loads the MLP and applies a trapezoidal forward+backward resampling after each simulated interval — mirroring the structure of the WOT correction used during inference (which corrects both source and target marginals):
log_weight_n = (R(P_start_n) + R(P_end_n)) / 2 · Δt
P_start and P_end are the protein states of cell n at the beginning and end of the interval. The N cells are then resampled multinomially (with replacement, weighted by exp(log_weight_n)). This avoids the catastrophic variance of Poisson sampling (which would kill ~37 % of cells per step even at R = 0) and is consistent with the inference-time coupling.
Pipeline usage¶
# Step 4 — adapt parameters and learn R
cardamomot step infer_network_simul -i my_project -s full --proliferation
# Step 5 — simulate with branching
cardamomot step simulate_network -i my_project -s full --proliferation
# KO/OV perturbations with branching
cardamomot step simulate_network_KOV -i my_project -s full --proliferation
Or directly:
python infer_network_simul.py -i my_project -s full --proliferation
python simulate_network.py -i my_project -s full --proliferation
Notes¶
--proliferationis off by default.infer_network_structurealways computes and savesR_optas a by-product regardless — enabling the flag later has no cost.The flag must be passed to both
infer_network_simul(trains the MLP) andsimulate_network/simulate_network_KOV(uses it). Passing it only to the simulation scripts whileprolif_network.ptis absent prints a warning and falls back to standard simulation.Resampling is currently applied to the PDMP stochastic mode only; ODE mode ignores it silently.
For balanced OT the estimated R values are exactly zero for all cells; meaningful proliferation signals require unbalanced OT (
model.unbalanced_reg > 0) or prior annotations viaadata.obs['prolif_rate']/adata.obs['death_rate'](see section above).The trapezoidal rule
(R_start + R_end)/2naturally handles cells whose protein levels — and thus growth rate — change significantly within the interval.