Metadata-Version: 2.4
Name: manifolds-rs
Version: 0.4.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Dist: numpy>=1.23
Requires-Dist: beartype>=0.19
Requires-Dist: anndata>=0.10 ; extra == 'anndata'
Provides-Extra: anndata
Summary: UMAP, t-SNE, PHATE, PaCMAP, diffusion maps and ForceAtlas2 for single-cell and computational biology, in Rust.
Author-email: Gregor Lueg <gregorlueg@me.com>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/GregorLueg/manifolds-rs/blob/main/python/CHANGELOG.md
Project-URL: Documentation, https://gregorlueg.github.io/manifolds-rs/
Project-URL: Repository, https://github.com/GregorLueg/manifolds-rs

[![PyPI](https://img.shields.io/pypi/v/manifolds-rs.svg)](https://pypi.org/project/manifolds-rs/)
[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://gregorlueg.github.io/manifolds-rs/)
[![CI](https://github.com/GregorLueg/manifolds-rs/actions/workflows/python-test.yml/badge.svg)](https://github.com/GregorLueg/manifolds-rs/actions/workflows/python-test.yml)

# manifolds-rs

Dimensionality reduction for single-cell and computational biology. The
[Rust crate](https://github.com/GregorLueg/manifolds-rs) does the work; this is
a scikit-learn shaped layer over it.

Nine algorithms on the CPU, four of them with GPU variants where the neighbour
search and the optimiser (UMAP's Adam, t-SNE's three-kernel FFT) move to the
device. Scanpy-style `mf.umap(adata)` and `mf.tsne(adata)` for `AnnData`. No
CUDA runtime to install: the GPU backend is wgpu, so it runs on Metal, Vulkan or
DX12 and ships in the ordinary wheel.

**Documentation: <https://gregorlueg.github.io/manifolds-rs/>**

This package versions separately from the Rust crate. Changes here are in
[`python/CHANGELOG.md`](https://github.com/GregorLueg/manifolds-rs/blob/main/python/CHANGELOG.md);
the crate's are in
[`CHANGELOG.md`](https://github.com/GregorLueg/manifolds-rs/blob/main/CHANGELOG.md).

## Install

```bash
uv pip install manifolds-rs
```

Wheels are built for Linux x86_64 and macOS on both architectures, against
Python 3.10 and up.

## Thirty seconds

```python
import manifolds_rs as mf

X, labels = mf.datasets.clustered(20_000, dim=50, n_clusters=12)

embedding = mf.UMAP(n_neighbors=15, min_dist=0.1).fit_transform(X)
```

Every estimator takes its parameters in the constructor, its data in `fit`, and
hands back an `(n_samples, n_components)` array. `get_params` and `set_params`
are there, so `clone` and `Pipeline` work without scikit-learn being an install
requirement.

## What's here

| Class | What it is for |
| --- | --- |
| `UMAP` | The default choice. Local structure, decent global structure from the spectral init. |
| `DensMAP` | UMAP with relative density preserved, so a dense region stays dense. |
| `TSNE` | Local structure above all else. Barnes-Hut, the faster quick-and-dirty `approx="qd"`, or FFT-interpolated `approx="fft"` / `"fft_3k"` for large N. 2-D only. |
| `DensNE` | t-SNE with the same density correction. |
| `DREAMS` | t-SNE pulled towards a global reference (PCA, or any layout you pass), so the clusters keep a sensible arrangement. |
| `PHATE` | Continuous structure. Trajectories and branch points survive this. |
| `PaCMAP` | Global structure without leaning on a spectral init. Three pair types. |
| `DiffusionMaps` | The spectral embedding PHATE is built on. |
| `ForceAtlas2` | Gephi's force-directed layout on the kNN graph, as in scanpy's `draw_graph`. 2-D only. |
| `UMAPGpu`, `DensMAPGpu`, `TSNEGpu`, `DREAMSGpu` | The same, with the neighbour search on the device. `TSNEGpu(approx="fft_3k_gpu")` runs the optimiser there too. |

## AnnData

With `uv pip install 'manifolds-rs[anndata]'`, `mf.umap` and `mf.tsne` drop in
for `sc.tl.umap` and `sc.tl.tsne`: same slots, so `sc.pl.umap` just works.

```python
import scanpy as sc
import manifolds_rs as mf

sc.pp.pca(adata)
sc.pp.neighbors(adata)
mf.umap(adata)  # obsm["X_umap"], uns["umap"]
mf.tsne(adata)  # obsm["X_tsne"], uns["tsne"]
sc.pl.umap(adata, color="leiden")
```

`device="gpu"` switches to the GPU estimators. On 25,000 cells on an M1 Max,
`mf.umap` ran in 1.5s against 10.6s for `sc.tl.umap`, and `mf.tsne` in 9.7s
(4.9s with `device="gpu", approx="fft_3k_gpu"`) against 47.2s for a 10-thread
`sc.tl.tsne`. The [AnnData page](https://gregorlueg.github.io/manifolds-rs/anndata/)
has the full table, cluster separation included, and the caveats.

## Parameters

The knobs people actually turn are ordinary constructor arguments. The rest live
in frozen dataclasses, one per group, and anything you leave alone keeps the
crate's default:

```python
mf.UMAP(
    n_neighbors=30,
    ann="hnsw",
    nn_params=mf.NeighbourParams(m=32, ef_search=200),
    optim_params=mf.UmapOptim(gamma=1.5, neg_sample_rate=10),
)
```

Every parameter has exactly one home. If it is a constructor argument it is not
in the group, so there is never a question of which wins.

## Reusing the neighbour graph

Running several embeddings over the same data? Build the graph once. On anything
large the search is most of the runtime.

```python
ind, dist = mf.knn_graph(X, k=15, ann="hnsw")

a = mf.UMAP().fit_transform(X, knn_indices=ind, knn_distances=dist)
b = mf.PaCMAP().fit_transform(X, knn_indices=ind, knn_distances=dist)
```

## No transform

None of these projects new points, so there is no `transform`. That is the
crate's position rather than a gap in the bindings: embedding new data means
refitting on the combined set, which moves the existing coordinates too. Anyone
telling you otherwise is doing something to the new points that is not the same
operation the old ones went through.

## Precision

float32 in, float32 throughout; float64 in, float64 throughout. Anything else is
promoted to float64 rather than narrowed, so precision is never lost by
accident. The GPU estimators are the exception and cast to float32, because WGSL
has no float64 and the alternative is a failure inside a kernel.

## What is not in the wheel

The MLX backends for Apple Silicon. They link mlx-c dynamically, so they need a
source build: `brew install mlx-c`, then `maturin develop --release --features
mlx`. See the GPU page of the docs.

Parametric UMAP is in the crate but not yet bound.

