Skip to content

fix: symmetric gauss connectivities for pynndescent / sparse distances - #4423

Open
syahra712 wants to merge 3 commits into
scverse:mainfrom
syahra712:fix/gauss-sparse-symmetric
Open

syahra712 wants to merge 3 commits into
scverse:mainfrom
syahra712:fix/gauss-sparse-symmetric

Conversation

@syahra712

Copy link
Copy Markdown

LLM usage

The sparse branch of _connectivity.gauss relied on _get_indices_distances_from_sparse_matrix returning neighbours sorted by distance, with the cell itself in column 0. The pynndescent distance matrix does not meet either assumption: every row stores the cell itself as an explicit 0, holds one more neighbour than sklearn, and is ordered by column index.

_has_self_column only requires one row to start with itself. As a result:

  • indices[:, 1:] drops an arbitrary neighbour instead of the cell itself.
  • Kernel weights were computed over every stored entry of d_sq, but only the truncated indices were mirrored.

The result was one-directional edges (max|W - W.T| = 0.655 in the issue) and self-loops with weight 1 on the diagonal.

The sparse branch now takes, for every row, the n_neighbors - 1 closest entries that are not the cell itself. It computes the kernel on exactly those edges and mirrors the missing reverse edges, which matches what the dense knn=True branch does. The new code is vectorised and replaces the per-row Python loop and the LIL conversion.

On the issue's reproducer (9000 cells):

before after
pynndescent max|W - W.T| 0.655 0.0
pynndescent max diag(W) 1.0 0.0
shared edges with sklearn — 99.4 % (rest is approximate kNN)

The sklearn path gives the same output as before; the existing gauss tests pass unchanged.

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@8ed52d9). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4423   +/-   ##
=======================================
  Coverage        ?   84.31%           
=======================================
  Files           ?      134           
  Lines           ?    12701           
  Branches        ?        0           
=======================================
  Hits            ?    10709           
  Misses          ?     1992           
  Partials        ?        0           
Flag Coverage Δ
hatch-test.low-vers 81.07% <100.00%> (?)
hatch-test.pre 84.11% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/scanpy/neighbors/_connectivity.py 89.74% <100.00%> (ø)

@syahra712

Copy link
Copy Markdown
Author

The "Check title, milestone, and labels" job needs a milestone, which I can't set as an outside contributor. Could a maintainer add one, or the label you use to skip it?

This branch has not been deployed

No deployments
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.

neighbors(method="gauss") returns a non-symmetric connectivity matrix on the sparse (pynndescent / transformer) path

1 participant