Metadata-Version: 2.4
Name: sctopo
Version: 0.2.0
Summary: Topological analysis of single-cell data: per-cluster persistent homology and periodicity-based topogene discovery.
Author: Binglun Shao
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/binglunshao/sctopo
Project-URL: Repository, https://github.com/binglunshao/sctopo
Project-URL: Issues, https://github.com/binglunshao/sctopo/issues
Keywords: single-cell,scRNA-seq,persistent homology,topological data analysis,TDA,computational biology,circular coordinates,cell cycle
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.22
Requires-Dist: scipy>=1.9
Requires-Dist: pandas>=1.5
Requires-Dist: scikit-learn>=1.1
Requires-Dist: networkx>=2.8
Requires-Dist: tqdm>=4.60
Requires-Dist: matplotlib>=3.5
Requires-Dist: plotly>=5.10
Requires-Dist: anndata>=0.9
Requires-Dist: scanpy>=1.9
Requires-Dist: ripser>=0.6
Requires-Dist: persim>=0.3
Requires-Dist: dreimac>=0.3
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: pytest-cov>=4.0; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: jupyter; extra == "dev"
Requires-Dist: ipykernel; extra == "dev"
Dynamic: license-file

# 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

```bash
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

```python
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

```bibtex
@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](https://joss.theoj.org/papers/10.21105/joss.00925) and
[DREiMac](https://github.com/scikit-tda/DREiMac), which do the persistent homology and
the circular coordinates.

## Acknowledgements

`sctopo` grew out of an earlier prototype,
[`totopos`](https://github.com/manuflores/totopos), developed with Emanuel Flores
Bautista.

## License

BSD 3-Clause. See [LICENSE](LICENSE).
