sctopo
Find loops in single-cell data, and the genes that follow them.
Some populations of cells are arranged in a circle in expression space -- cycling
cells going round the cell cycle is the classic case. sctopo finds these loops
with persistent homology, one population at a time. It gives every cell an angle
around its loop, and ranks every gene by how strongly its expression follows that
angle. Those genes are the loop's topogenes, and they are how you tell what a
loop is.
Install
pip install sctopo
In Google Colab, put this in the first cell:
!pip install sctopo
Python 3.10 or newer. No GPU and no PyTorch needed.
Start with the tutorial
The tutorial notebook, example_tutorial.ipynb, builds a population with a loop
planted in it, gene by gene, and walks through finding it, checking it against a
held-out label, and recovering the planted genes. It needs no dataset, runs in Colab
or locally, and takes a couple of minutes. Read it before you run sctopo on real
data.
Quick start
import scanpy as sc
import sctopo.pipeline as tppl
import sctopo.viz as tpv
# one population, with PCA fitted inside it (not across all cell types)
tcells = adata[adata.obs["cell_type"] == "T cell"].copy()
sc.pp.pca(tcells, n_comps=30)
# 1. detect: the 10 most persistent loops, measured by effective resistance
det = tppl.detect_loops_spectral(tcells, "T cell", cluster_key="cell_type",
source_key="X_pca",
n_neighbors=int(0.1 * tcells.n_obs),
n_candidates=10)
# 2. characterize: an angle per cell, and descriptive numbers, for every loop
bundle = tppl.characterize_all_loops(det["adata_cluster"], det["loops"],
compute_topogenes=False)
# 3. look at a loop, then score its genes
tpv.plot_cycle_plane_mpl(bundle["loops"][0], det["adata_cluster"].obsm["X_pca"])
scored = tppl.score_topogenes_for_loops(det["adata_cluster"], bundle["loops"],
bundle["cc"], loop_indices=[0])
print(scored["results"][0]["topogenes"][:20])
How to read the results
These matter more than any parameter.
1. Detection returns a shortlist, not a verdict. n_candidates=10 returns 10
loops whether or not the data contains any. It is a compute budget. Nothing in the
output says "this loop is real".
2. Read the persistence profile, not the count. A real loop's persistence stands far above the candidates below it. On a sphere, which has no loop at all, the candidates come out at similar heights. In the tutorial the planted loop stands 33x above the next candidate, and the sphere's top candidate 1.1x.
3. Decide what a loop is by reading it. Two things settle it:
- Its topogenes. A cell-cycle loop is made of replication and mitosis genes. A technical loop is made of mitochondrial or ribosomal genes, or follows library size.
- Labels you held out of detection. Score cell-cycle phase (or anything else you
expect a loop to order), keep it out of detection, then check whether it advances
in order around the loop's angle.
sctopo.cell_cycledoes the phase scoring.
No geometric or statistical number in this package separates a real loop from a technical one. The genes do.
Mistakes that have misled real analyses:
- A technical axis that looks like a loop. Mitochondrial content, ribosomal genes
and library size can each form a tall, well-populated ring. Read
pct_counts_mtandtotal_countsagainst the angle, the same way you would read a held-out label. - A position or differentiation axis bent into a circle. If a label such as tissue region or pseudotime runs around the loop, each value on its own arc, the loop may just be that axis. If every value spreads right round, the loop is independent of it.
- Mixing populations. Run on one cell type at a time. On a mixture, the largest "loops" are the gaps between cell types.
- Filtering away the signal. QC filters can remove cells non-randomly. A mitochondrial cutoff tuned for blood once removed most of a gut epithelium, and left it enriched for cycling cells.
Choosing an embedding
- Fit it inside the population. A PCA fitted across many cell types spends its
leading components on the differences between them. Use
detect_loops(..., use_local_embedding=True), or fit PCA on each population yourself. - Use effective resistance.
detect_loops_spectral(kind="eff", the default) measures distance along a nearest-neighbour graph, and recovers loops that plain Euclidean distance misses in high dimensions.n_neighborsaround 10% of the population size is a sound start. - Be careful with integrated embeddings. Batch-corrected latents (scVI, Harmony, CONCORD) can remove part of a real loop along with the batch effect. They tend to lose the G1 stretch of the cell cycle in particular. Compare against a local PCA before trusting one.
- Never a 2-D UMAP. Squeezing data into two dimensions creates holes that are not there.
What is in the package
| module | what it does |
|---|---|
sctopo.pipeline |
Start here. detect_loops, detect_loops_spectral, characterize_all_loops, score_topogenes_for_loops, filter_loops_by_support_size |
sctopo.pp |
Embeddings: recompute_subset_embedding (local PCA), spectral_embedding (effective resistance, diffusion, Laplacian eigenmaps, DPT), spectral_extend, PcaPolicy |
sctopo.cells |
critical_edge_cycles (the loop finder), loop geometry, batch_confound_test |
sctopo.genes |
get_circular_coords, topo_scores_periodicity, topo_scores_harmonics, cycle_coverage_entropy |
sctopo.viz |
plot_cycle_plane_mpl, plot_harmonic, plot_pers_diag_ripser, plot_3d_scatter_by_cluster |
sctopo.cell_cycle |
load_phase_genes, score_cell_cycle_phases: Revelio markers, mouse symbols |
sctopo.synthetic |
Shapes with a known number of loops (circle, linked_circles, torus, sphere, ...), for testing |
How a loop is found. Persistent homology on the population's embedding gives
every candidate hole a persistence. The n_candidates most persistent are kept. For
each one, the edge that creates the hole is closed by the shortest path around it,
which gives a representative cycle of cells. Cells near that cycle become its
topocells. DREiMac then assigns each topocell an angle, and each gene is scored by
the strength of its one-peak-per-turn pattern along that angle.
Changes from 0.1
0.2 keeps only the current method:
- Detection uses a candidate budget (
n_candidates), not a permutation test. The test checked whether data had more structure than shuffled copies of itself, which a loop-free sphere also passes. Thecbthreshold parameter is gone with it: a threshold is in the embedding's own units, so a value cannot be reused between embeddings, while a count can.detect_loopsreturns the cutoff the budget produced aspersistence_cutoff. - Topogene scoring is periodicity only. The gradient method and its PyTorch dependency are gone.
- Several quality scores were removed (
circ_var,coverage,passes_quality, the gap score, p-values). On real data they did not separate real loops from technical ones. critical_edge_method_significantis nowcritical_edge_cycles, andsctopo.datasetsis nowsctopo.cell_cycle.
Citing
@software{sctopo2026,
author = {Shao, Binglun},
title = {sctopo: topological analysis of single-cell data},
year = {2026},
url = {https://github.com/binglunshao/sctopo},
version = {0.2.0},
}
Please also cite ripser and DREiMac, which do the persistent homology and the circular coordinates.
Acknowledgements
sctopo grew out of an earlier prototype,
totopos, developed with Emanuel Flores
Bautista.
License
BSD 3-Clause. See LICENSE.
Metadata
Release files for sctopo 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sctopo-0.2.0.tar.gz | 114.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sctopo-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 203.5 kB
Release files / sctopo-0.2.0.tar.gz
| Download URL | sctopo-0.2.0.tar.gz |
|---|---|
| Size | 114.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c1da144b663617dbafedd68143af108bd271e5c4a82f37119273fae806457af4
|
|
BLAKE2b-256 checksum How to use checksums |
ce4fe8ed6e1dd1318c6f780109e31c7a1ff9df78015e94f92b22498a8a35ac49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / sctopo-0.2.0-py3-none-any.whl
| Download URL | sctopo-0.2.0-py3-none-any.whl |
|---|---|
| Size | 89.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2b941897ff76baad95d9bc60009c609e2fc3101f382b67f106deac7658dd60c8
|
|
BLAKE2b-256 checksum How to use checksums |
ba1b3a8faed7f6f7a3b5cdbab0d068e773463db42cf90e4468483db499b1a571
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for sctopo 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sctopo-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.