diff --git a/docs/release-notes/4031.fix.md b/docs/release-notes/4031.fix.md new file mode 100644 index 0000000000..7bf2a2692a --- /dev/null +++ b/docs/release-notes/4031.fix.md @@ -0,0 +1 @@ +{func}`scanpy.tl.umap` no longer silently mutates `adata.obsp['connectivities']` via a shared sparse buffer {smaller}`N Justice` diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index c3d59e5537..2d9b65d931 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -205,7 +205,7 @@ def umap( # noqa: PLR0913 n_epochs = default_epochs if maxiter is None else maxiter x_umap, _ = simplicial_set_embedding( data=x, - graph=neighbors["connectivities"].tocoo(), + graph=neighbors["connectivities"].tocoo(copy=True), n_components=n_components, initial_alpha=alpha, a=a, diff --git a/tests/test_embedding.py b/tests/test_embedding.py index 10c09f932a..c92e1205ab 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -80,6 +80,21 @@ def test_umap_init_paga(layout): sc.tl.umap(pbmc, init_pos="paga") +def test_umap_preserves_connectivities(): + # https://github.com/scverse/scanpy/issues/4028 + pbmc = pbmc68k_reduced()[:100, :].copy() + conn = pbmc.obsp["connectivities"] + data_before = conn.data.copy() + nnz_before = conn.nnz + + sc.tl.umap(pbmc) + + assert_array_equal(data_before, conn.data) + assert conn.nnz == nnz_before + assert (conn.data == 0).sum() == 0, "CSR should have no explicit zeros" + assert "X_umap" in pbmc.obsm + + @pytest.mark.parametrize("rng_arg", ["rng", "random_state"]) def test_diffmap( subtests: pytest.Subtests, rng_arg: Literal["rng", "random_state"]