∫ Optimal Transport & Wasserstein Distance in Single-Cell Biology

Mathematical & Algorithmic Foundations for Cellular Distribution Analysis

A rigorous guide to optimal transport theory and Wasserstein metrics in computational biology. From mathematical foundations (Monge, Kantorovich, entropic regularization) to modern applications in trajectory inference, multi-omics integration, and distributional comparison. Covering landmark methods (Waddington-OT, moscot, SCOT) and theoretical advances (2019-2025).

Cartoon fennec builder holding a blue brick beside a sorting tray, with a loose scatter of coloured bricks on the left and a neatly stacked target wall on the right, dashed lines linking the two piles
🧱 LEGO analogy · 02.4 The Great Brick Move

Move the whole city, not one brick at a time.

Optimal transport is the problem of relocating an entire LEGO city from one table to another for the least total effort. In single-cell terms, the two tables are two distributions of cells - two timepoints, two conditions, two modalities - and the cheapest move is a coupling that says how much mass from each source cell should land on each target cell. The total cost of that cheapest plan is the Wasserstein distance.

1
Survey both tables: you need the full pile on each side, not a handful of sample bricks. OT matches whole distributions, so the answer depends on the shape and weight of both populations rather than on any individual cell.
2
Price every trip: build the cost matrix. Distances must be measured in a space where they mean something - PCA space, a latent embedding, or a graph metric - because Euclidean distance on raw counts is dominated by depth and noise.
3
Plan the whole move, not each brick: the optimum is global, and greedy nearest-neighbour choices do not add up to the cheapest plan. Entropic regularisation (Sinkhorn) makes the global solve tractable, at the cost of a blurrier, more diffuse coupling as epsilon grows.
4
Allow for bricks gained and lost: cells divide and die, so the two piles need not weigh the same. Unbalanced OT relaxes the exact mass-conservation constraint with KL penalties, letting the plan create and destroy mass where growth or death demands it.
Three poses of the fennec builder: surveying a full sorting tray of red, blue, yellow and green bricks; inspecting a single brick in a magnified callout; and pointing at a finished multi-colour wall with arrows showing bricks arriving
Survey Take both distributions in full, with their marginal weights.
Price Compute pairwise cost in a shared, meaningful embedding.
Move Solve for the globally cheapest coupling, unbalanced if mass changes.

🎯 Core Concepts

What is Optimal Transport?

Optimal transport theory provides a mathematical framework for comparing probability distributions by computing the minimal "cost" of transforming one distribution into another. Originally formulated by Gaspard Monge in 1781 and generalized by Leonid Kantorovich in 1942, the theory asks: What is the most efficient way to move a pile of dirt from one location to match a desired shape elsewhere?

The Monge Problem: Moving Mass Optimally

Source μ Initial distribution Optimal Transport Plan min ∫ c(x,y) dπ(x,y) Target ν Target distribution Transport Cost W(μ,ν) = optimal cost • Distance moved • Mass transported • Efficiency metric

Intuition: Each mass point (circle) must be transported to the target distribution along optimal paths (arrows) that minimize total cost.

The Wasserstein Distance

The Wasserstein distance (also called Earth Mover's Distance) quantifies the dissimilarity between two probability distributions as the minimal cost to transport mass from one to the other. For single-cell biology:

Wasserstein Distance: Measuring Distribution Dissimilarity

Similar Distributions Small W-distance ✓ Similar cells/populations Different Distributions Large W-distance ✗ Dissimilar cells/populations

Key Insight: Wasserstein distance is proportional to transport path lengths—small distance for similar distributions, large for dissimilar ones.

Why Optimal Transport for Biology?

Single-Cell Biology Applications

Cell-Cell Similarity Cell A Cell B Gene Expression W(A,B) = cell distance Population Alignment Dataset 1 Coupling π Dataset 2 Match cell populations Trajectory Inference t₀ t₁ t₂ Temporal cell dynamics Cross-Modality Alignment (Gromov-Wasserstein) scRNA-seq Genes: GAPDH, TP53... Compare internal distances, not features directly scATAC-seq Peaks: chr1:1000... Entropic Regularization (Sinkhorn) Classical OT (ε=0) Sparse, O(n³) +entropy Entropic OT (ε>0) Smooth, O(n²log n) Key Advantages for Single-Cell Biology ✓ Handles unpaired data • ✓ Cross-modality integration • ✓ Preserves manifold geometry ✓ Probabilistic interpretations • ✓ Scalable algorithms • ✓ Natural for temporal dynamics

Summary: OT provides a unified mathematical framework for diverse single-cell problems: comparing cells, aligning populations, inferring trajectories, and integrating multi-omics data.

Key Challenges Addressed

Four-panel LEGO comic of Sinkhorn iteration: an exact brick-by-brick move is abandoned as too slow, a blurred approximate plan is drawn instead, rows and columns are alternately rebalanced until the plan settles, and the finished coupling is close enough to move the whole city
Sinkhorn trades an exact answer for a reachable one: add a little entropy, then rebalance rows and columns in turn until the plan converges — the larger the epsilon, the faster the solve and the blurrier the coupling.

📈 Major Application Domains

1. Trajectory Inference & Temporal Dynamics

Waddington-OT: Developmental Trajectory Inference

2019 Cell Temporal OT

Pioneering application of OT to trajectory inference from time-series scRNA-seq. Models cellular differentiation as probability distributions flowing through expression space, explicitly accounting for differential growth/death rates.

🔬 How Optimal Transport is Applied:

Unbalanced Kantorovich Formulation: Extends classical OT by adding growth/death terms through KL divergence penalties (λ₁=1, λ₂=50), allowing cell populations to change mass between timepoints.

Entropic Regularization (ε=0.05): Uses Sinkhorn algorithm to efficiently solve OT problems in 30D PCA space, trading exact optimality for computational tractability.

Temporal Coupling Maps: Computes transport couplings π(t,t+Δt) between consecutive timepoints using squared Euclidean distance as the cost function, representing probabilistic cell fate relationships.

Iterative Growth Rate Refinement: Initializes from proliferation/apoptosis gene signatures, then iteratively updates growth rates based on transport maps to ensure consistency with observed cell count changes.

  • 315,000 cells across 39 timepoints during iPSC reprogramming
  • Revealed diverse developmental programs: stromal, pluripotent, trophoblast, neural
  • Validated predictions: Obox6 TF and GDF9 cytokine enhance reprogramming 2-5 fold
  • Interpolation accuracy near batch-to-batch baseline variation

PRESCIENT: Generative Modeling of Cell Trajectories

2021 Nature Comm Generative OT

Models differentiation as diffusion process over potential landscape. Uses regularized Wasserstein loss to fit neural network-parameterized drift and growth terms, enabling in silico perturbation experiments.

🔬 How Optimal Transport is Applied:

Wasserstein-2 Loss for Neural Network Training: Minimizes W₂² distance between predicted and observed cell distributions using squared Euclidean cost in 50D PCA space.

Potential Landscape Parameterization: Neural networks learn drift F(x) = -∇U(x) + h(x) and growth g(x) terms that minimize the Wasserstein distance to empirical timepoints.

Proliferation Weighting: Each cell weighted by expected number of descendants at final time, computed via forward integration of learned growth function g(x).

Stochastic Sampling for Prediction: Generates cell trajectories via Euler-Maruyama simulation of SDE dx = F(x)dt + σdW, enabling in silico perturbation screens by modifying F or g.

  • Potential landscape framework connects to Waddington's epigenetic landscape
  • Incorporates cell proliferation by weighting based on expected descendants
  • Outperforms alternatives on fate bias prediction with lineage tracing validation
  • Enables large-scale combinatorial perturbation screens

TIGON: Growth and Dynamic Trajectories

2023 Nature Mach Intel WFR Distance

Uses Wasserstein-Fisher-Rao distance to simultaneously capture gene expression velocity and population growth. Neural ODEs solve high-dimensional OT efficiently with meshless formulation.

🔬 How Optimal Transport is Applied:

Wasserstein-Fisher-Rao (WFR) Metric: Combines transport (W₂) with growth/death (Fisher-Rao) using parameter α to balance contribution: d_WFR = √(W₂² + α·FR²).

Neural ODE Velocity Field: Parameterizes velocity v(x,t) with neural networks, solving continuity equation ∂ₜρ + ∇·(vρ) = g·ρ where g represents growth rate.

Dynamic Formulation (Benamou-Brenier): Minimizes kinetic energy ∫₀ᵀ ∫|v(x,t)|²ρ(x,t)dxdt subject to matching marginals at observed timepoints.

Meshless Particle-Based Solver: Discretizes distributions as weighted particles moving along learned velocity field, scaling to 100K+ cells without spatial discretization.

  • WFR distance separates transport (Wasserstein) from growth (Fisher-Rao)
  • Infers time-varying gene regulatory networks via velocity field gradients
  • Reconstructs未测时间点 expression and cell-cell communication dynamics
  • Preserves trajectories and pseudo-time with high fidelity

moscot: Unified Framework for Temporal-Spatial Mapping

2025 Nature Scalable OT

Scalable unified framework supporting temporal, spatial, and spatiotemporal OT with multimodal integration. Linear memory complexity enables atlas-scale analysis.

🔬 How Optimal Transport is Applied:

Modular Problem Composition: Constructs cost matrices C from multiple sources (expression, spatial coordinates, lineage, time) and combines via C_total = Σᵢ αᵢCᵢ for flexible problem specification.

Low-Rank Sinkhorn (ε=0.005-0.01): Factorizes coupling matrix as π ≈ diag(u)KL^T diag(v) with rank r=50-100, reducing memory from O(n²) to O(nr).

Quadratic Problems (Gromov-Wasserstein): For cross-modality (RNA→ATAC), minimizes ∑ᵢⱼₖₗ (C^X_ik - C^Y_jl)²πᵢⱼπₖₗ using entropic regularization with ε_quad=0.1.

Temporal & Spatial Extensions: Handles temporal couplings with growth models (unbalanced OT with τ=0.5-1.0) and spatial problems using Euclidean/geodesic costs with fused GW (α=0.5).

  • Processes 1.7M cells (mouse embryogenesis) where competitors fail
  • 10-100× faster with linear memory through online cost evaluation
  • Supports W-type, GW-type, FGW-type formulations in unified API
  • Validated NEUROD2 as epsilon cell regulator in human iPSC-islets

2. Multi-Omics Integration & Cross-Modality Alignment

SCOT: Gromov-Wasserstein for Multi-Omics Alignment

2020 ICML Workshop GW-OT

First application of Gromov-Wasserstein to single-cell multi-omics. Aligns datasets with unmatched features by comparing k-NN graph geodesic distances instead of feature values.

🔬 How Optimal Transport is Applied:

Gromov-Wasserstein (GW) for Feature Mismatch: Minimizes ∑ᵢⱼₖₗ |d_X(xᵢ,xₖ) - d_Y(yⱼ,yₗ)|²πᵢⱼπₖₗ where d_X and d_Y are intra-dataset distances, enabling cross-modality alignment (e.g., scRNA-seq ↔ scATAC-seq).

k-NN Graph Geodesic Distances: Constructs k=30 nearest neighbor graphs in each modality, computes shortest path distances as cost matrices C^X and C^Y to capture local manifold geometry.

Entropic GW Solver (ε=0.1): Uses projected gradient descent with Sinkhorn iterations for inner entropic OT subproblems, converging in 100-500 iterations.

Self-Tuning for Unbalanced Data: Employs KL divergence marginal penalties with λ=0.1 to handle different numbers of cells per dataset without strict mass conservation.

  • 15-50× faster than alternatives with only 2 hyperparameters vs 4-5
  • Lowest FOSCTTM on all 3 simulated datasets
  • Successfully aligns RNA-ATAC, RNA-methylation without common features
  • Probabilistic coupling matrix enables uncertainty-aware downstream analysis

Pamona: Partial Manifold Alignment

2022 Bioinformatics Partial GW

Extends GW-OT to partial alignment scenarios where datasets have distinct cell types. Virtual points absorb dataset-specific cells while aligning shared structures.

🔬 How Optimal Transport is Applied:

Partial Gromov-Wasserstein (PGW): Extends GW by adding virtual "dust bins" with mass s (0

SPL Curve for Mass Selection: Sweeps s from 0 to 1, plots SPL = ∑ᵢⱼ πᵢⱼ·similarity(i,j). Sharp drop indicates dataset-specific cells; plateau gives optimal shared mass.

Manifold-Preserving Costs: Uses diffusion distance (t=1, k=15 neighbors) to compute C^X and C^Y, capturing global manifold structure beyond local k-NN graphs.

Iterative Refinement with sc-GEM: Alternates between (1) partial GW alignment and (2) guided entry of gene modules to improve biological consistency of coupling.

  • First method to explicitly handle dataset-specific cell types
  • SPL curve estimates shared cell number automatically
  • Alignment Score 0.834-0.907 on partial alignment benchmarks
  • Identified DNMT3B as key regulator in iPSC reprogramming (sc-GEM)

Labeled GWOT: Cross-Modality Perturbation Prediction

2025 AISTATS Label-Aware OT

Incorporates perturbation labels as constraints in GW/COOT formulations. L-fold speedup by restricting coupling to label-compatible pairs, enabling accurate cross-modality prediction.

🔬 How Optimal Transport is Applied:

Label-Constrained Coupling Set: Defines C^l_{p,q} = {π ∈ C_{p,q} | πᵢⱼ > 0 ⟹ label(xᵢ)=label(yⱼ)}, restricting transport to same-perturbation cells.

Entropic Labeled GWOT (ε=0.05): Solves min_π ∑ᵢⱼₖₗ |C^X_ik - C^Y_jl|²πᵢⱼπₖₗ + εH(π) s.t. π ∈ C^l, achieving L-fold speedup where L = # labels.

Labeled COOT Variant: Extends Co-Optimal Transport by learning global feature transport G with per-label sample transport {πₗ}, combining linear and quadratic OT.

Predictor Training: Uses learned coupling π* to construct barycentric projection: ŷᵢ = ∑ⱼ (πᵢⱼ*/∑ₖπᵢₖ*)yⱼ, then trains regression model f: X→Y on aligned pairs.

  • Label-compatible coupling: T_ij > 0 only if labels match
  • O(nm/L) complexity per Sinkhorn iteration vs O(nm)
  • Predicts RNA from ATAC for 11 kinase inhibitors with dose-response
  • Enriches true protein-RNA pairs in coupling matrix

3. Cell Similarity & Clustering

OT-scOmics: Improved Cell-Cell Similarity

2022 Bioinformatics Sinkhorn Divergence

Uses debiased Sinkhorn divergence as cell-cell similarity metric. Outperforms Euclidean, Pearson, and Cosine across 13 datasets (scRNA-seq, scATAC-seq, methylation).

🔬 How Optimal Transport is Applied:

Entropic OT Cell Distance (ε=0.5): For cell pair (l,m), computes W̄_C,ε(aₗ,aₘ) where cells are normalized to probability distributions aᵢ = xᵢ/||xᵢ||₁ over 10K most variable features.

Debiased Sinkhorn Divergence: W̄(a,b) = W_ε(a,b) - [W_ε(a,a) + W_ε(b,b)]/2 ensures zero distance for identical cells and satisfies metric axioms.

Data-Driven Ground Cost: Computes gene-gene distance matrix C using Pearson correlation (RNA-seq/methylation) or Cosine similarity (ATAC-seq), capturing feature relationships.

GPU-Accelerated Sinkhorn: PyTorch implementation with automatic differentiation computes all pairwise distances for 10K cells × 10K features in minutes on single GPU.

  • Higher C-index and Silhouette scores than all baselines
  • Particularly strong for overlapping clusters and rare populations
  • GPU-accelerated PyTorch implementation for scalability
  • Generalizes across modalities without adaptation

4. Neural Optimal Transport & Deep Learning

CellOT: Neural Optimal Transport for Perturbations

2023 Nature Methods Neural OT

Uses Input Convex Neural Networks (ICNNs) to learn transport maps between control and perturbed distributions. Predicts single-cell responses from unpaired data.

🔬 How Optimal Transport is Applied:

Monge Map Parameterization: Learns transport map T_k: ρ_control → ρ_perturbed as T_k(x) = x + ∇g_θ(x) where g_θ is Input Convex Neural Network (ICNN).

Wasserstein-2 Training Objective: Minimizes W₂²(T_k#ρ_c, ρ_k) = 𝔼[||T_k(x) - y||²] over transported control cells and observed perturbed cells.

ICNN Architecture: Ensures g_θ is convex through non-negative weights between layers and convex activation functions, guaranteeing T_k is optimal transport map.

Unpaired Data Handling: No cell-cell correspondence needed; learns population-level transformation from separate control/treated measurements, enabling cross-patient generalization.

  • ICNNs ensure convexity for guaranteed optimality of learned maps
  • Handles unpaired data - no matched control/treated cells needed
  • Predicts individual cell trajectories, not just population means
  • Generalizes to holdout patients and cross-species transfer

GENOT: Entropic (Gromov)-Wasserstein Flow Matching

2024 NeurIPS Flow Matching

Learns conditional transport plans via flow matching on noise-to-target couplings. Supports linear, GW, and FGW formulations with unbalanced variants.

🔬 How Optimal Transport is Applied:

Conditional Flow Matching: Learns velocity field v_θ(x,t|x₀,x₁) that transports noise distribution to target via ODE dx/dt = v_θ(x,t), matching entropic OT couplings estimated with Sinkhorn.

Entropic Linear OT (GENOT-L): For trajectory inference, uses W_ε with geodesic cost on cell manifold, preserving biologically plausible paths (TSI metric improvement).

Unbalanced OT (U-GENOT, τ<1): For perturbation with cell death, relaxes mass conservation: marginal constraints become KL(π·1|p) ≤ τ·KL(p|p), allowing learned growth/death.

Fused GW (GENOT-F, α=0.5): For cross-modality (ATAC→RNA), combines feature cost (Euclidean) and structural cost (GW) as C_total = (1-α)C_lin + αC_quad, reducing GW non-uniqueness.

  • Stochastic trajectories capture biological variability
  • Arbitrary cost functions (geodesic, graph-based) for domain-specific priors
  • Unbalanced mass support for noisy/growing cell populations
  • 50% improvement in perturbation discrimination on Tahoe-100M

GRouNdGAN: GRN-Guided Simulation with Causal GANs

2024 Nature Comm Simulation

Causal GAN that generates realistic scRNA-seq data while imposing user-defined gene regulatory networks. Uses Wasserstein GAN framework for stable training.

🔬 How Optimal Transport is Applied:

Wasserstein GAN with Gradient Penalty (WGAN-GP): Trains causal controller to generate TF expression by minimizing Earth Mover's Distance approximated via Kantorovich-Rubinstein duality: W(p_real, p_gen) = sup_||f||_L≤1 𝔼[f(x_real)] - 𝔼[f(x_gen)].

Two-Stage Training: (1) Pre-train WGAN-GP causal controller on TF expressions independently of GRN; (2) Train target generators with architectural constraints per GRN topology.

Critic as Wasserstein Distance Estimator: Lipschitz-constrained critic network provides gradient signal from estimated W-distance between simulated and real data distributions.

Causality via Architecture: Each target gene generator receives only its regulating TFs (per GRN), ensuring causal edges are structurally imposed, not just learned correlations—validated via knockout perturbations.

  • Bridges simulation-experiment benchmark gap for GRN inference
  • Architectural constraints impose causal structure (not just correlations)
  • 66.5% of imposed TF-gene edges show significant knockout effects
  • Preserves cell trajectories, pseudo-time, and biological/technical noise

📚 Method Comparison

Method Year OT Type Primary Use Case Key Innovation Scale
Waddington-OT 2019 Unbalanced W Trajectory Inference Temporal coupling with growth modeling 315K cells, 39 timepoints
SCOT 2020 GW Multi-Omics Alignment k-NN graph geodesic distances ~1K cells
PRESCIENT 2021 Regularized W Generative Trajectories Potential landscape + perturbations ~100K cells
Pamona 2022 Partial GW Partial Alignment Virtual points for dataset-specific cells ~5K cells
OT-scOmics 2022 Sinkhorn Div Cell Similarity Superior clustering metric ~10K cells
CellOT 2023 Neural W Perturbation Prediction ICNN-parameterized transport maps ~50K cells
TIGON 2023 WFR Growth + Trajectories Separates transport from growth dynamics ~5K cells
GENOT 2024 Flow Match W/GW/FGW Stochastic Trajectories Conditional flows with arbitrary costs ~100K cells
GRouNdGAN 2024 WGAN GRN Simulation Causal GAN with regulatory constraints ~100K cells
Labeled GWOT 2025 Label-GW/COOT Cross-Modality Prediction Label constraints for L-fold speedup ~10K cells
moscot 2025 W/GW/FGW Atlas-Scale Mapping Linear memory, unified framework 1.7M cells

💡 Practical Implementation Guide

Common Pitfalls & Best Practices

Software Ecosystem

📖 Learning Resources

Theoretical Foundations

Tutorials & Code Examples

Benchmark Datasets

🛠️ Hands-On Practice

The walkthrough below runs a complete optimal transport analysis on a single-cell time course — from a preprocessed AnnData object with a day annotation, through a PCA-space cost matrix, an entropic-regularized Sinkhorn solve, and finally to ancestor/descendant probabilities for a chosen cell population. It uses POT directly for the low-level mechanics (so the coupling is visible as a plain matrix) and moscot for the scalable, biology-aware version of the same computation. Everything is retrospective and purely computational: the inputs are existing count matrices, the outputs are transport plans and probabilities.

Environment & packages

POT provides the core solvers (Sinkhorn, unbalanced, Gromov-Wasserstein); moscot wraps them in an OTT-JAX backend with single-cell problem classes (TemporalProblem, SpatialAlignmentProblem, TranslationProblem). Scanpy handles the preprocessing that produces the latent space the cost is computed in. Install into a clean environment — moscot pins JAX/OTT versions and will fight an existing deep-learning stack.

# conda / mamba recommended
conda create -n otsc python=3.10 -y
conda activate otsc

pip install scanpy anndata
pip install pot                 # POT: ot.sinkhorn, ot.gromov, ot.unbalanced
pip install moscot              # pulls in ott-jax + jax (CPU build by default)

# optional GPU backend for large problems (CUDA 12)
# pip install --upgrade "jax[cuda12]"

# optional: the original Waddington-OT implementation
# pip install wot

Hardware. The dense coupling is n × m in float64, so two timepoints of 5,000 cells each is ~200 MB and fits comfortably on a laptop; 30,000 × 30,000 is ~7 GB and needs a compute node. moscot's low-rank solvers (rank=) and the OTT-JAX GPU backend are what make 105-cell problems tractable — a GPU is optional for the toy case below but effectively required for a full atlas-scale time course.

Data structures & formats

Minimal code walkthrough

Load the time-course object, reduce to a shared PCA space, build and normalize the cost matrix between two adjacent days, solve the entropic problem with Sinkhorn, read ancestor/descendant probabilities off the coupling, then repeat the whole thing at scale with moscot's TemporalProblem including growth-aware unbalanced marginals.

import numpy as np
import scanpy as sc
import ot                      # POT

# 1. Load a preprocessed time course. adata.obs["day"] must be NUMERIC:
#    OT needs an ordering, and moscot uses the gap between timepoints to
#    scale the growth term.
adata = sc.read_h5ad("timecourse.h5ad")
adata.obs["day"] = adata.obs["day"].astype(float)

# 2. Build ONE shared latent space across all timepoints. Never compute a
#    cost on raw counts: Euclidean distance on counts is dominated by library
#    size and by a handful of high-expression genes.
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
sc.pp.pca(adata, n_comps=30, use_highly_variable=True)

# 3. Take two adjacent timepoints as source (t0) and target (t1)
t0, t1 = 2.0, 4.0
src = adata[adata.obs["day"] == t0]
tgt = adata[adata.obs["day"] == t1]
X0 = src.obsm["X_pca"]
X1 = tgt.obsm["X_pca"]
n, m = X0.shape[0], X1.shape[0]
print(f"day {t0}: {n} cells  ->  day {t1}: {m} cells")

# 4. Cost matrix: squared Euclidean in PCA space, then rescale to [0, 1].
#    Rescaling matters because the meaning of `reg` below is relative to the
#    magnitude of M -- an unnormalised M makes epsilon dataset-dependent.
M = ot.dist(X0, X1, metric="sqeuclidean")
M /= M.max()

# 5. Marginals. Uniform is the assumption "no cell is more likely than any
#    other to have a descendant" -- revisited in step 8.
a = np.ones(n) / n
b = np.ones(m) / m

# 6. Entropic-regularised OT (Sinkhorn). reg is epsilon: small = sharp,
#    near-deterministic plan but slow / numerically unstable; large = blurred
#    plan where every cell maps a little onto every other cell.
#    Use the log-domain solver -- it is the one that survives small reg.
pi = ot.sinkhorn(a, b, M, reg=0.05, method="sinkhorn_log", numItermax=2000)
print("coupling shape:", pi.shape, " mass:", pi.sum())   # ~1.0

# 7. Read biology off the coupling.
#    Row-normalise -> descendant distribution of each day-2 cell.
#    Column-normalise -> ancestor distribution of each day-4 cell.
descendants = pi / pi.sum(axis=1, keepdims=True)
ancestors   = pi / pi.sum(axis=0, keepdims=True)

# Ancestor profile of one terminal cluster at t1: how much of its mass
# came from each source cell type?
mask_t1 = (tgt.obs["cell_type"] == "Neuron").to_numpy()
anc_mass = ancestors[:, mask_t1].sum(axis=1)
anc_mass /= anc_mass.sum()
src_ct = src.obs["cell_type"].to_numpy()
for ct in np.unique(src_ct):
    print(f"  {ct:<20s} {anc_mass[src_ct == ct].sum():.3f}")

# 8. Growth and death break the "equal mass in, equal mass out" assumption:
#    proliferating cells contribute MORE than 1/n of the day-4 mass, apoptotic
#    cells less. Unbalanced OT relaxes the marginal constraints; reg_m controls
#    how strongly (small reg_m = marginals free to change, large = back to balanced).
g = src.obs["proliferation_score"].to_numpy()   # e.g. sc.tl.score_genes on a cycle signature
a_growth = g / g.sum()
pi_unb = ot.unbalanced.sinkhorn_unbalanced(
    a_growth, b, M, reg=0.05, reg_m=1.0, numItermax=2000
)
print("unbalanced transported mass:", pi_unb.sum())     # != 1.0 by design

# ---------------------------------------------------------------------------
# 9. The same analysis at scale, with moscot: solves ALL consecutive day pairs,
#    estimates growth rates from proliferation/apoptosis signatures, and keeps
#    couplings on GPU via the OTT-JAX backend.
# ---------------------------------------------------------------------------
from moscot.problems.time import TemporalProblem

tp = TemporalProblem(adata)
tp = tp.score_genes_for_marginals(
    gene_set_proliferation="human", gene_set_apoptosis="human"
)
tp = tp.prepare(time_key="day", joint_attr="X_pca")
tp = tp.solve(epsilon=1e-2, tau_a=0.95, tau_b=0.999)   # tau < 1 => unbalanced

# Pull a terminal population backwards through the whole chain of couplings
tp.pull(source=t0, target=t1, data="cell_type", subset="Neuron")
sc.pl.embedding(adata, basis="umap", color="pull")

# 10. Wasserstein distance as a summary statistic: how far apart are two
#     conditions in expression space? Use the un-regularised (exact) cost of
#     the Sinkhorn plan, or ot.emd2 for small problems.
w2 = np.sum(pi * M)
print(f"Entropic W2 (squared, normalised cost) between {t0} and {t1}: {w2:.4f}")

Common pitfalls & tips