Skip to content

Leiden 0.7.0 - #7

Merged
ianfd merged 2 commits into
mainfrom
leiden-0.7.0
Aug 1, 2026
Merged

Leiden 0.7.0#7
ianfd merged 2 commits into
mainfrom
leiden-0.7.0

Conversation

@ianfd

@ianfd ianfd commented Aug 1, 2026

Copy link
Copy Markdown
Member

some old performance improvements i had lying around

Ian and others added 2 commits August 1, 2026 12:54
publish.yml fired on every push to main as well as on tags, with no test job in
front of it — so any commit bumping the version published it. crates.io versions
can be yanked but never replaced.

Now tags only, behind fmt/clippy/tests, a tag-vs-Cargo.toml version check and a
cargo package dry run. The publish step sits in a `crates-io` environment so a
required reviewer can be added for a manual confirm.

ci.yml runs the core with --no-default-features (fmt, clippy, tests in debug and
release), the kNN feature separately so an hnsw breakage doesn't take the core
red, and cargo-semver-checks.

Also gitignore /data, which holds downloaded h5ad fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.6.1 produced wrong, irreproducible clusterings, quadratically slowly. Six
distinct bugs, each now with a regression test:

- aggregate() halved the degree sum, because a self-loop added its weight once
  instead of twice to strength — the null model was off above level 0
- the RB null term divided by 2m instead of 4m, so resolution meant 2x what it
  does in scanpy/leidenalg
- candidates were iterated out of a HashSet, so seed: Some(42) still drifted
- the "only singletons may merge" guard in refinement hung off the wrong
  statement, losing the internal-connectivity guarantee
- diff_move was O(n) per call rather than O(degree)
- n_iterations and tolerance were dead config fields

Rewritten: Partition keeps every aggregate incrementally in O(1) per move;
NeighborWeights replaces the per-call HashSet and get_group_members allocation;
Objective is a trait over RB and CPM; local moving is queue-based
remove-then-insert; refinement matches libleidenalg's semantics with the
randomised selection from the paper.

Storage went to u32 ids and f32 weights (16 bytes per undirected edge), plus a
zero-copy from_csr_parts that takes a connectivity matrix without a copy.

20k nodes: 44.1s -> 18ms. 1M: 1.75s. 8M: 20.1s. 1.33x faster than igraph native
on synthetic graphs, 2.5-3x on pbmc3k. Modularity is within noise of leidenalg
(-0.09% mean single-seed over 160 committed fixtures) and on pbmc3k matches the
author cell types as well as scanpy does (ARI 0.8599 vs 0.8609).

Verification is five independent layers: invariants, known answers, differential
fixtures generated from leidenalg and committed, exhaustive brute-force
optimality on small graphs, and randomised property tests. 90 tests.

Drops the petgraph dependency and puts the kNN stack behind a default-on feature
so the core builds where hnsw does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ianfd
ianfd merged commit f10d633 into main Aug 1, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant