rust-NMF
A modern Rust port of R's NMF package plus
2019-2024 SOTA algorithms (HALS, E-HALS, RcppML-style diagonalised NMF). Six
algorithms are bit-equivalent to R within f64 round-off; four are modern
single-cell-tuned methods. Parallelised with rayon + matrixmultiply, on
omicverse pbmc8k it's 76-280× faster than R depending on configuration.
Quick decision table
| Use case | Recommended | Why |
|---|---|---|
| Single-cell exploratory (default) | lee + NNDSVD, 25 iter |
ARI 0.89 vs cell types — best biology and bit-eq R |
| Pure speed with strong biology | dnmf + NNDSVD, 25 iter |
ARI 0.85, ~26% faster than lee+NNDSVD |
Reproduce existing R NMF::nmf() analysis |
lee, brunet, offset, nsNMF, snmf/r, snmf/l |
bit-eq R within 1e-12 |
| Largest speed (loss only matters) | hals + NNDSVD, 25 iter |
~280× R, ~3× sklearn cd; ARI lower (0.55) |
| Synthetic / ground-truth recovery | lee or brunet, 100+ iter |
lowest Amari error |
| Sparse factors with bit-eq R | snmf/r or snmf/l |
FCNNLS port + L1² regularisation |
| Missing-value imputation | ls-nmf with weight=mask |
weight=0 drops entries from loss |
| Large atlas (>500k cells) | lee + NNDSVD or dnmf + NNDSVD |
NNDSVD warm-start dominant at scale |
Key empirical finding (from examples/benchmark_scaling.ipynb init-pairing section,
real pbmc8k 3000 cells × 1500 HVG, rank 8): NNDSVD init is the magic, not the algorithm.
With NNDSVD seeding, every algorithm reaches its biological-signal ceiling in ~25
iterations. Random init takes 100 iters and still ends ~30% worse on ARI. Pair NNDSVD
with lee/brunet/dnmf and you get the best of both worlds (R-equivalence + best
biology, or speed + best biology).
Four one-liner recipes
import numpy as np
from nmf_rs import nmf, nndsvd_init
# 1. EXPLORATORY single-cell, best ARI vs cell types (0.89), still bit-eq R
W0, H0 = nndsvd_init(V, rank=10, fill='mean')
res = nmf(V, rank=10, method='lee', # 'brunet' also reaches ARI 0.86
W0=W0, H0=H0, max_iter=25, num_threads=16)
# 2. SPEED + biology, modern algorithm (ARI 0.85, 26% faster than recipe 1)
W0, H0 = nndsvd_init(V, rank=10, fill='mean')
res = nmf(V, rank=10, method='dnmf',
W0=W0, H0=H0, max_iter=25, num_threads=16)
# 3. REPRODUCE R analysis exactly — bit-eq, max|ΔW| < 1e-12
res = nmf(V, rank=10, method='brunet', # or 'lee', 'offset', 'nsNMF', 'snmf/r', 'snmf/l'
W0=W0_from_R, H0=H0_from_R, max_iter=200, num_threads=16)
# 4. PUREST SPEED at atlas scale (>500k cells), ARI tradeoff
W0, H0 = nndsvd_init(V, rank=20, fill='mean')
res = nmf(V, rank=20, method='hals',
W0=W0, H0=H0, max_iter=25, num_threads=16)
Algorithm catalogue
method |
Origin | Year | Update rule | Bit-eq R | Best for |
|---|---|---|---|---|---|
brunet (alias KL) |
Brunet et al. PNAS | 2004 | KL divergence multiplicative | ✅ 1e-12 | R reproduction; count-like data |
lee (alias Frobenius) |
Lee & Seung | 2001 | Frobenius multiplicative | ✅ 1e-12 | R reproduction; ground-truth recovery |
offset |
Badea | 2008 | Lee + per-feature baseline | ✅ 1e-12 | per-gene background subtraction |
nsNMF |
Pascual-Montano | 2006 | Brunet + smoothing S | ✅ 1e-12 | sparser, more interpretable factors |
ls-nmf (alias lsnmf) |
Wang, Kossenkov, Ochs | 2006 | Weighted Frobenius; needs weight= |
≈ (BLAS variance) | missing-value imputation |
snmf/r |
Kim & Park | 2007 | FCNNLS-based ANLS; sparse H | ✅ 1e-12 | sparse coefficients (cells) |
snmf/l |
Kim & Park | 2007 | FCNNLS-based ANLS; sparse W | ✅ 1e-12 | sparse basis (genes) |
hals |
Cichocki & Phan | 2009 | Block-coord least-squares | ❌ (custom) | speed |
ehals |
Andersen-Ang & Gillis | 2019 | HALS + Nesterov extrapolation | ❌ | speed + slight per-iter accuracy |
dnmf (alias rcppml) |
DeBruine (RcppML) | 2024 | Diagonalised W·diag(d)·H + L1 | ❌ | single-cell biological signal |
The six "✅ 1e-12" rows are bitwise-identical to R NMF within f64 round-off
on every parity test (pytest tests/).
Speed (omicverse pbmc8k, HVG=2000, cells=7750, rank=10)
| config | iters | 16t (s) | speed-up vs R lee (125 s) |
|---|---|---|---|
R lee + R seed |
100 | 125 s | 1× (baseline) |
lee + matched seed |
100 | 1.65 s | 76× ✅ bit-eq |
brunet + matched seed |
100 | 4.30 s | 18× ✅ bit-eq |
hals + random |
100 | 1.61 s | 78× |
dnmf + NNDSVD |
25 | ~0.5 s | ~250× |
hals + NNDSVD |
25 | 0.58 s | 215× |
hals + NNDSVD (loose) |
10 | 0.45 s | 280× |
| sklearn cd (BLAS, 17t) | 100 | 0.79 s | 158× |
See examples/benchmark_pbmc8k.ipynb and examples/comparison_vs_cnmf.ipynb.
Objective accuracy (anchor-free, no algorithm bias)
The standard NMF evaluation metrics from Brunet 2004 and Kim & Park 2007 on real pbmc8k (3000 cells × 1500 HVG, rank 8, K=5 random inits per algorithm):
ARI vs cell types (real pbmc8k, 3k cells × 1.5k HVG, rank 8)
Pivoted by (algorithm × init) to expose the NNDSVD effect:
| algo | NNDSVD (25 iter, K=1) | random (K=5 runs) |
|---|---|---|
lee |
🥇 0.889 | 0.568 |
brunet |
0.864 | 0.621 |
dnmf |
🥈 0.854 | 0.726 |
hals |
0.547 | 0.428 |
ehals |
0.486 | 0.539 |
Wall-clock at the same configuration (16 threads)
| algo | NNDSVD@25 (s) | random×5 (mean s/run) |
|---|---|---|
lee |
0.19 | 0.39 |
brunet |
0.41 | 0.96 |
dnmf |
0.14 | 0.24 |
hals |
0.13 | 0.26 |
ehals |
0.12 | 0.26 |
Three takeaways
-
NNDSVD init dominates the choice of algorithm. Every algorithm gets higher ARI with NNDSVD than with random — typically 30-60% higher. So the question "which algorithm" is downstream of "use NNDSVD".
-
lee + NNDSVDis the gold standard for biological signal. On real single-cell data withpredicted_celltypelabels,lee + NNDSVD@25reaches ARI = 0.89 — the best across all (algorithm × init) combinations. Andleeis bit-equivalent to RNMF::nmf(method="lee")so you can reproduce R analyses exactly. -
dnmf + NNDSVDis the fast alternative. Same NNDSVD trick on the modern (DeBruine 2024) diagonalised algorithm: 0.14 s, ARI 0.85. Trades 0.04 ARI for 26% speed.
Caveat: on synthetic data without label structure, the same recipes
undertrain in 25 iters — for ground-truth recovery prefer lee/brunet
with 100+ iters (medium tier in examples/benchmark_scaling.ipynb).
Tutorial roadmap
| Notebook | Audience | What it covers |
|---|---|---|
tutorial_quickstart.ipynb |
New users | API basics, all 4 R-bit-eq algorithms, stop criteria |
benchmark_vs_R.ipynb |
R NMF users |
Drop-in parity check vs R, max|ΔW|<1e-12 |
benchmark_pbmc8k.ipynb |
Single-cell users | Real pbmc8k thread sweep, factor inspection |
hals_nndsvd_demo.ipynb |
Speed-focused users | HALS + NNDSVD convergence trace, factor quality |
comparison_vs_cnmf.ipynb |
cNMF users | sklearn / cNMF apples-to-apples, factor correlation |
benchmark_scaling.ipynb |
Anyone choosing an algorithm | 3 scales × 7 algos × 6 objective metrics + init pairing |
tutorial_full.ipynb |
Comprehensive | R baseline → all algorithms → init strategies → threading → production recipe |
Suggested reading order for new users:
- Start with
tutorial_quickstart.ipynbfor API. - Then
benchmark_scaling.ipynbto pick the right algorithm for your scale + use case. - If reproducing an R analysis,
benchmark_vs_R.ipynb. - If you've used cNMF before,
comparison_vs_cnmf.ipynb. - For deep-dive:
tutorial_full.ipynb.
Install
# Sherlock or any host with rust toolchain:
module load rust/1.90.0
maturin develop --release --manifest-path rust/Cargo.toml
# Or via pip into an active venv (release wheels coming to PyPI as `nmf-rs`):
VIRTUAL_ENV=/path/to/venv pip install -e .
Reproducing an R NMF analysis
# 1. Generate W0, H0 in R with set.seed(s); export as TSV.
# 2. Load same V, W0, H0 in Python:
import numpy as np
from nmf_rs import nmf
V = np.loadtxt("V.tsv")
W0 = np.loadtxt("W0.tsv")
H0 = np.loadtxt("H0.tsv")
res = nmf(V, rank=W0.shape[1], W0=W0, H0=H0, method="brunet", max_iter=200)
W_R = np.loadtxt("W_from_R.tsv")
H_R = np.loadtxt("H_from_R.tsv")
np.testing.assert_allclose(res.W, W_R, atol=1e-12) # passes exactly
np.testing.assert_allclose(res.H, H_R, atol=1e-12)
See tests/test_parity.py and tests/test_snmf_parity.py for the
auto-regenerated reference fixtures + R-parity asserts.
License
GPL ≥ 2 (inherited from R NMF).
Metadata
Release files for nmf-rs 0.1.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 | |
|---|---|---|---|---|
| nmf_rs-0.1.0.tar.gz | 36.7 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| nmf_rs-0.1.0-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| nmf_rs-0.1.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.9 | abi3 | macOS 10.12+ universal2 (ARM64, x86-64), macOS 11.0+ ARM64, macOS 10.12+ x86-64 | Details |
Total release size: 2.1 MB
Release files / nmf_rs-0.1.0.tar.gz
| Download URL | nmf_rs-0.1.0.tar.gz |
|---|---|
| Size | 36.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
008cc4b8032afc0f88668e4ff2701244aed0bce3cf3b472cf2045bb5c9bccf61
|
|
BLAKE2b-256 checksum How to use checksums |
53d749c1f3516245c06da14895667ae549f080f697f7150e916b8a1a45f92f78
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 8, 2026.
Transparency logRelease files / nmf_rs-0.1.0-cp39-abi3-win_amd64.whl
| Download URL | nmf_rs-0.1.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 347.9 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
aa1db26e64c06a760ba835bef194e20a484171562944c9a3523df9121b5be0da
|
|
BLAKE2b-256 checksum How to use checksums |
992f32e061603cd422cc480b7efefd5f4d6983d624a3d2a4096c058dd7af302e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 8, 2026.
Transparency logRelease files / nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_x86_64.whl
| Download URL | nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 477.2 kB |
| Tags | CPython 3.9 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
93608cb2f4ab78c9ece2fcdc2b7feb14536f913db01e8f8cb4f186a970c0eed3
|
|
BLAKE2b-256 checksum How to use checksums |
c7a60d1aef67e0ba64eb908f3a8efcf76f2ef877028468837ef54b78fa900870
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 8, 2026.
Transparency logRelease files / nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_aarch64.whl
| Download URL | nmf_rs-0.1.0-cp39-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 443.5 kB |
| Tags | CPython 3.9 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
9ad81407bbc9335219f31a0b6746264761733494c6b16697e060729fa749dd17
|
|
BLAKE2b-256 checksum How to use checksums |
333cf47bb2fa049de0887f0f36c6acc62bcbf10c1f1da9d3f3de2736f69fbbe8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 8, 2026.
Transparency logRelease files / nmf_rs-0.1.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | nmf_rs-0.1.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 823.6 kB |
| Tags | CPython 3.9 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
92a60270238ed3eb0e61266c40fbbc86d7526417cde06425c1c8d1f2b1cc86ac
|
|
BLAKE2b-256 checksum How to use checksums |
208093bded0b8e1db491a1c4e3cdde3098ac289d7bc25aa9f01cfb6f76034bb7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 8, 2026.
Transparency logSecurity for nmf-rs 0.1.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 5 files in this release have been uploaded with provenance metadata.
Attestation for nmf_rs-0.1.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 May 8, 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.