Skip to main content

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_cycle does 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_mt and total_counts against 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_neighbors around 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. The cb threshold 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_loops returns the cutoff the budget produced as persistence_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_significant is now critical_edge_cycles, and sctopo.datasets is now sctopo.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)

Source distribution for sctopo 0.2.0
File Size Uploaded Provenance
sctopo-0.2.0.tar.gz 114.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sctopo 0.2.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page