Repository navigation
find_landmarks: build-only kNN graph by default (fixes a large-data stall), exact and order-invariant options - #35
Open
settylab-dotto-bot[bot] wants to merge 2 commits into
Open
settylab-dotto-bot[bot] wants to merge 2 commits into
settylab-dotto-bot[bot] wants to merge 2 commits into
Conversation
…riant options build_graph built a pynndescent index and then queried it for every training point. On large data with low intrinsic dimension and strongly varying density that query is orders of magnitude slower than the build (synthetic branched curve, 19 dims, 8 CPUs: 1.5 s vs 217 s at 200k cells, 10 s vs > 44 min at 1.1M). The default graph is now the index's own neighbor graph, with no re-query; it is also slightly more accurate. - knn_method="nndescent" (default): build-only pynndescent graph. - knn_method="exact": scipy cKDTree kNN; deterministic and independent of row order and thread count, but slow on high-intrinsic-dimension data. - knn_method="nndescent_query": the 0.9.0 graph, to reproduce landmarks. - find_landmarks(order_invariant=True): canonical row sort + exact graph, indices mapped back; requires an int random_state. - Edge list built without a Python loop; returned as an (n_edges, 2) array. - First Leiden resolution clamped to the search bounds [0.01, 1000]. Changes which landmarks are selected relative to 0.9.0; see CHANGELOG.
…ce guarantee The clamp kept the first Leiden resolution inside [0.01, 1000], guarding a bisection branch (a first step with too FEW clusters at resolution > 1000) that does not occur on kNN graphs: such a resolution always yields too many clusters. It was the only thing that stopped knn_method="nndescent_query" from reproducing 0.9.0 landmarks above 1000 cells per landmark. Without it, nndescent_query output is bit-identical to 0.9.0 (edges, landmark indices and coordinates) at 1 500 to 6 000 cells per landmark as well. order_invariant's guarantee is now stated as holding for the same numpy, scipy and python-igraph versions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
find_landmarks→build_graphbuilt a pynndescent index and then queried that index for every training point, although the build had already produced a neighbor graph. On large data with low intrinsic dimension and strongly varying density, which is the geometry of diffusion components and other trajectory-like embeddings, that query takes orders of magnitude longer than the build, and its cost grows much faster than n. This PR makes the build-only graph the default and adds an exact, order-invariant option for users who need landmarks reproducible across machines and row orders.Related: #34 (the exact centroid snap, the previous change to
find_landmarks). There is no existing issue for the slowdown.The slowdown, reproduced on synthetic data
19 dimensions, 8 CPUs, k = 15, one run per cell on several cluster nodes, so read these as orders of magnitude:
t = u**4). Similar curves at uniform density are slower than the mixture but far less extreme (query 53 s at 200k, 98 s at 500k). Exact duplicate rows (2% or 20%) and 100× tighter clusters do not slow the query.build_graphend to end.What changes
knn_method="nndescent"(default):NNDescent(...).neighbor_graph, with no re-query.knn_method="exact":scipy.spatial.cKDTree. It depends only on the data: not on the seed, the thread count or (underorder_invariant) the row order.knn_method="nndescent_query": the 0.9.0 graph, kept so that old landmarks can be reproduced.find_landmarks(..., order_invariant=True):random_state, since Leiden is otherwise unseeded, and raises with a non-exactknn_method, since the pynndescent graph changes with the numba thread count;(n_edges, 2)array instead of a list of tuples. pynndescent's-1padding entries are dropped.exact_snap=Falseraises withknn_method="exact", which builds no index to snap with.Compatibility
This changes which landmarks are selected relative to 0.9.0 (CHANGELOG,
[Unreleased]).On a 200k-cell mixture with
n_clusters=200andrandom_state=0, the new default and the 0.9.0 graph share 65 landmarks (of 178 and 175). For scale, changing only the seed shares 61 (of 178 and 192), and a same-seed rerun reproduces the landmarks exactly.knn_method="nndescent_query"with the same seed. Verified bit-identical against the 0.9.0 code (edges, landmark indices and coordinates), including above 1000 cells per landmark.exact_snap=False.da(),de()and the differential classes choose landmarks with mellon, notfind_landmarks, so they are unaffected. This follows the convention of #34: the default changes, the old behaviour stays one keyword away, and the CHANGELOG says so.Tests
tests/test_find_landmarks_graph.py(12 tests):neighbor_graph, andnndescent_queryequals the 0.9.0 graph;Every new test fails on
main. The exact-graph test fails onmainon a 12.6% edge mismatch, not on a type difference.Mutation checks:
Full suite locally (Python 3.12, numpy 2.5.3, scipy 1.18.1, pynndescent 0.6.0, python-igraph 0.11.9): 2 215 passed, 36 skipped, 0 failed. One existing test was updated: the approximate-snap opt-out now passes
knn_method="nndescent_query".Timing check (not added to CI)
Not merged by me; for the next release.