A small CLI for treating the cell cycle as a limit cycle in single-cell data, then aligning other modalities (for example scATAC-seq) to the inferred phase.
This repo is meant to be practical: it ingests data into AnnData, infers a circular phase coordinate θ using S and G2/M programs, and bins per-cell signals by phase.
-
Ingest scRNA-seq into AnnData
- Builds a single
adatawith counts plus metadata (timepoint,sample_id, optionalgenotype) - Optionally imports author-provided embeddings if present
- Builds a single
-
Infer a limit-cycle phase
- Scores S and G2/M programs per cell
- Fits a circle in one or more bases (configurable) and computes:
thetain[0, 2π)radiusand fit diagnostics
- Persists results into
adata.obs
-
Align and bin other signals by phase
- Given a per-cell matrix (for example ATAC accessibility), aggregates by θ bins
- Saves a phase-resolved pseudobulk object into
adata.unsand exports tables
- Python 3.10+ recommended
- Works on Linux, macOS, and WSL2
- If using large sparse matrices, you will want enough RAM for PCA and OT-style operations
python -m venv .venv
source .venv/bin/activate
pip install -U pip wheel setuptools
pip install -r requirements.txt
pip install -e .Sanity check:
limitcycle-landscape --helpBy default the repo expects:
data/
raw/
<dataset_name>/
... your raw files ...
interim/
processed/
outputs/
figures/
tables/
logs/
configs/
params.yaml
You control paths via configs/params.yaml.
Edit configs/params.yaml to point to your raw data and choose options.
Minimal example:
paths:
raw_root: "data/raw"
interim_root: "data/interim"
processed_root: "data/processed"
outputs_root: "outputs"
dataset:
name: "gse154989" # or "generic_h5ad"
input_h5ad: null # only used when name == "generic_h5ad"
preprocess:
min_genes_per_cell: 200
min_cells_per_gene: 3
n_hvgs: 2000
pca_n_components: 50
cycle:
s_genes_file: "configs/genes_s.txt"
g2m_genes_file: "configs/genes_g2m.txt"
score_method: "scanpy" # implementation-dependent
circle_fit_basis: "S_G2M" # options depend on code, keep default unless you know why
theta_key: "theta"
radius_key: "radius"
atac:
default_layer: "X_atac"
n_bins: 60
output_uns_key: "atac_pseudobulk"Notes:
- If you are not using the built-in ingestion adapter, set
dataset.name: generic_h5adand providedataset.input_h5adpointing to an existing.h5adthat already containsobsmetadata andXcounts. - The ATAC binning step assumes your ATAC signal is already aligned to the same cells as the scRNA object. If it is in a separate AnnData, you need a merge step before
bin-atac.
- Put raw data where
paths.raw_rootpoints. - Run ingestion.
- Fit the cycle.
- Bin ATAC (or any other layer) by phase.
limitcycle-landscape ingest-scrna
limitcycle-landscape fit-cycle
limitcycle-landscape bin-atac --layer X_atac --bins 60Expected artifacts:
data/interim/mm_timecourse.h5ad(or dataset-specific name)data/processed/mm_timecourse_with_theta.h5adoutputs/tables/atac_phase_bins.csvoutputs/figures/cycle_fit.pngoutputs/figures/theta_umap.png
If your CLI supports a config override, use:
limitcycle-landscape ingest-scrna --config configs/params.yaml| Command | Output | Purpose |
|---|---|---|
ingest-scrna |
data/interim/*.h5ad |
Build AnnData with metadata + counts. |
fit-cycle |
data/processed/*_with_theta.h5ad |
Score S/G2M, fit circle, write obs[theta]. |
bin-atac |
tables + adata.uns[...] |
Aggregate a layer by θ bins. |
Run limitcycle-landscape <command> --help to see the arguments supported by your local version.
-
Compute module scores for S genes and G2/M genes.
-
Optionally z-score the scores and/or project into a basis used for circle fitting.
-
Fit a circle to obtain a phase angle θ per cell.
-
Store:
adata.obs["theta"]in radians[0, 2π)adata.obs["radius"]- Fit diagnostics in
adata.uns["cycle_fit"](if implemented)
This is meant to be compatible with a landscape and flux view of nonequilibrium dynamics:
- The inferred θ is a coordinate along a cyclic trajectory (limit cycle).
- Phase-binned signals can be interpreted as observables along that cyclic flow.
bin-atac expects an AnnData layer containing a matrix shaped (n_cells, n_features) aligned to the same cells used for θ.
Common patterns:
adata.layers["X_atac"]is sparse and has the same row order asadata.X- Peaks or topics live in
adata.var_namesor in a separate feature index
The command will:
-
Bin cells by θ into
n_binsequal-width bins. -
Aggregate ATAC signal within each bin (mean or sum, depending on implementation).
-
Save:
- bin edges
- counts per bin
- aggregated matrix
If your ATAC data are in a separate object, merge them first so that the row order and cell IDs match.
This repo can be applied to any time course dataset that contains cycling cells, including the Marjanovic 2020 LUAD evolution dataset, but only if:
- you subset to epithelial tumor cells that are cycling, or at least not dominated by non-cycling programs
- you validate that S and G2/M programs show the expected periodic structure
Practical advice:
- Run
fit-cycleon a cycling-enriched subset. - Use the phase coordinate θ as an alignment axis for comparing accessibility, TF activity, or other modalities within the cycling compartment.
Do not claim “limit cycle” unless the fitted circle is supported by marker periodicity and stable across samples.
- You likely forgot
pip install -e .or your venv is not active.
-
Confirm the layer exists:
- In Python:
print(adata.layers.keys())
- In Python:
-
Or pass the correct layer name:
limitcycle-landscape bin-atac --layer <your_layer>
- Check that your S/G2M gene lists match the organism and gene naming convention.
- Check you have enough cycling cells. If most cells are quiescent, circle fitting will fail silently or produce nonsense.
- Plot S score vs G2/M score before fitting. You should see a roughly circular or annular structure.
Zhu and Wang (2025), landscape and flux perspective:
@article{zhu2025quantifying,
title={Quantifying landscape and flux from single-cell omics: unraveling the physical mechanisms of cell function},
author={Zhu, Ligang and Wang, Jin},
journal={JACS Au},
volume={5},
number={8},
pages={3738--3757},
year={2025},
publisher={ACS Publications}
}Marjanovic et al. (2020), LUAD evolution and HPCS:
@article{marjanovic2020emergence,
title={Emergence of a high-plasticity cell state during lung cancer evolution},
author={Marjanovic, Nemanja Despot and Hofree, Matan and Chan, Jason E and others},
journal={Cancer Cell},
volume={38},
number={2},
pages={229--246},
year={2020},
publisher={Elsevier}
}