From 1828efb75f236c386c26ec321d8cfbccc9c9e8cd Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 8 May 2026 12:15:46 +0200 Subject: [PATCH 1/2] fix: use `get_igraph_from_adjacency` for graph construction for now. --- src/scanpy/_utils/__init__.py | 11 ++++++++++- src/scanpy/metrics/_metrics.py | 7 +++---- src/scanpy/neighbors/__init__.py | 2 +- src/scanpy/plotting/_tools/paga.py | 2 +- src/scanpy/tools/_draw_graph.py | 2 +- src/scanpy/tools/_paga.py | 2 +- tests/test_metrics.py | 2 +- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/scanpy/_utils/__init__.py b/src/scanpy/_utils/__init__.py index ebaa7d985b..8cf0bfae7e 100644 --- a/src/scanpy/_utils/__init__.py +++ b/src/scanpy/_utils/__init__.py @@ -70,6 +70,7 @@ "compute_association_matrix_of_groups", "descend_classes_and_funcs", "ensure_igraph", + "get_igraph_from_adjacency", "get_literal_vals", "indent", "is_backed_type", @@ -273,10 +274,18 @@ def check_use_raw( # -------------------------------------------------------------------------------- -def get_igraph_from_adjacency(adjacency: CSBase, *, directed: bool = False) -> Graph: +def get_igraph_from_adjacency(adjacency: CSBase, *, directed: bool) -> Graph: """Get igraph graph from adjacency matrix.""" import igraph as ig + import scanpy as sc + + if sc.settings.preset is sc.Preset.ScanpyV2Preview: + # TODO: replace all call sites with this line + return ig.Graph.Weighted_Adjacency( + adjacency, mode=ig.ADJ_DIRECTED if directed else ig.ADJ_UNDIRECTED + ) + sources, targets = adjacency.nonzero() weights = dematrix(adjacency[sources, targets]).ravel() if len(sources) else [] g = ig.Graph(directed=directed) diff --git a/src/scanpy/metrics/_metrics.py b/src/scanpy/metrics/_metrics.py index 7fce2bac91..c3a9f86653 100644 --- a/src/scanpy/metrics/_metrics.py +++ b/src/scanpy/metrics/_metrics.py @@ -10,7 +10,7 @@ from natsort import natsorted from pandas.api.types import CategoricalDtype -from .._utils import NeighborsView +from .._utils import NeighborsView, get_igraph_from_adjacency if TYPE_CHECKING: from collections.abc import Sequence @@ -203,15 +203,14 @@ def modularity_array( connectivities: AnyArrayLike | SpBase, /, *, labels: AnyArrayLike, is_directed: bool ) -> float: try: - import igraph as ig + import igraph # noqa: F401 except ImportError as e: # pragma: no cover e.add_note( "`igraph` is required for computing modularity. " "Please install `igraph` and try again." ) raise - igraph_mode: str = ig.ADJ_DIRECTED if is_directed else ig.ADJ_UNDIRECTED - graph: ig.Graph = ig.Graph.Weighted_Adjacency(connectivities, mode=igraph_mode) + graph = get_igraph_from_adjacency(connectivities, directed=is_directed) return graph.modularity(_codes(labels), "weight") diff --git a/src/scanpy/neighbors/__init__.py b/src/scanpy/neighbors/__init__.py index 3622c02576..7bc2470df3 100644 --- a/src/scanpy/neighbors/__init__.py +++ b/src/scanpy/neighbors/__init__.py @@ -578,7 +578,7 @@ def distances_dpt(self) -> OnFlySymMatrix: def to_igraph(self) -> Graph: """Generate igraph from connectiviies.""" - return _utils.get_igraph_from_adjacency(self.connectivities) + return _utils.get_igraph_from_adjacency(self.connectivities, directed=False) @_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep) @_accepts_legacy_random_state(0) diff --git a/src/scanpy/plotting/_tools/paga.py b/src/scanpy/plotting/_tools/paga.py index 36c0c8978d..d5100f1338 100644 --- a/src/scanpy/plotting/_tools/paga.py +++ b/src/scanpy/plotting/_tools/paga.py @@ -243,7 +243,7 @@ def _compute_pos( # noqa: PLR0912 ctx = nullcontext() else: ctx = _set_igraph_rng(rng) - g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) + g = _sc_utils.get_igraph_from_adjacency(adjacency_solid, directed=False) with ctx: if "rt" in layout: g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) diff --git a/src/scanpy/tools/_draw_graph.py b/src/scanpy/tools/_draw_graph.py index bdbb37d0be..ae209599d1 100644 --- a/src/scanpy/tools/_draw_graph.py +++ b/src/scanpy/tools/_draw_graph.py @@ -150,7 +150,7 @@ def draw_graph( # noqa: PLR0913 if layout == "fa": positions = np.array(fa2_positions(adjacency, init_coords, **kwds)) else: - g = _utils.get_igraph_from_adjacency(adjacency) + g = _utils.get_igraph_from_adjacency(adjacency, directed=False) with _igraph_rng_compat(rng): if layout in {"fr", "drl", "kk", "grid_fr"}: ig_layout = g.layout(layout, seed=init_coords.tolist(), **kwds) diff --git a/src/scanpy/tools/_paga.py b/src/scanpy/tools/_paga.py index f2130b5787..b01cd6e0f0 100644 --- a/src/scanpy/tools/_paga.py +++ b/src/scanpy/tools/_paga.py @@ -212,7 +212,7 @@ def _compute_connectivities_v1_0(self): ones = self._neighbors.connectivities.copy() ones.data = np.ones(len(ones.data)) - g = _utils.get_igraph_from_adjacency(ones) + g = _utils.get_igraph_from_adjacency(ones, directed=False) vc = igraph.VertexClustering( g, membership=self._adata.obs[self._groups_key].cat.codes.values ) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 7fe7fe1fa3..2de76b07e0 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -334,7 +334,7 @@ def test_modularity_adata( assert 0 <= s <= 1 for (n0, s0), (n1, s1) in combinations(scores.items(), 2): with subtests.test("equality", l=n0, r=n1): - assert pytest.approx(s0, rel=1e-6) == s1 + assert s0 == s1 with subtests.test("update"): assert adata.uns["leiden"]["modularity"] is scores["update"] From 69f919563095f8294e4f4cd52a1535430365a2b3 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 8 May 2026 12:39:49 +0200 Subject: [PATCH 2/2] test with new one as well --- src/scanpy/tools/_utils.py | 13 +++++++------ src/testing/scanpy/_pytest/__init__.py | 1 + tests/test_metrics.py | 5 ++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/scanpy/tools/_utils.py b/src/scanpy/tools/_utils.py index 4190907e27..80083a7023 100644 --- a/src/scanpy/tools/_utils.py +++ b/src/scanpy/tools/_utils.py @@ -55,12 +55,13 @@ def _get_pca_or_small_x(adata: AnnData, n_pcs: int | None) -> np.ndarray | CSRBa logg.info(" using data matrix X directly") return adata.X - if "X_pca" in adata.obsm: - if n_pcs is not None and n_pcs > adata.obsm["X_pca"].shape[1]: - msg = "`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`." + pca_key = next((k for k in ("pca", "X_pca") if k in adata.obsm), None) + if pca_key is not None: + if n_pcs is not None and n_pcs > adata.obsm[pca_key].shape[1]: + msg = f"`{pca_key}` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`." raise ValueError(msg) - x = adata.obsm["X_pca"][:, :n_pcs] - logg.info(f" using 'X_pca' with n_pcs = {x.shape[1]}") + x = adata.obsm[pca_key][:, :n_pcs] + logg.info(f" using {pca_key!r} with n_pcs = {x.shape[1]}") return x from ..preprocessing import pca @@ -73,7 +74,7 @@ def _get_pca_or_small_x(adata: AnnData, n_pcs: int | None) -> np.ndarray | CSRBa warn(msg, UserWarning) n_pcs_pca = n_pcs if n_pcs is not None else settings.N_PCS pca(adata, n_comps=n_pcs_pca) - return adata.obsm["X_pca"] + return adata.obsm[settings.preset.pca.key_added or "X_pca"] def get_init_pos_from_paga( diff --git a/src/testing/scanpy/_pytest/__init__.py b/src/testing/scanpy/_pytest/__init__.py index d519ba5411..0c07046a48 100644 --- a/src/testing/scanpy/_pytest/__init__.py +++ b/src/testing/scanpy/_pytest/__init__.py @@ -58,6 +58,7 @@ def original_settings( }) setup() + sc.settings.preset = sc.Preset.ScanpyV1 if pkg_version("anndata") >= Version("0.12"): ad.settings.zarr_write_format = 3 # default in anndata 0.13, warns otherwise sc.settings.logfile = sys.stderr diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 2de76b07e0..f1aafba30a 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -310,10 +310,13 @@ def test_modularity_adj_errors(labels: object, is_directed: object, pat: str) -> @needs.igraph +@pytest.mark.parametrize("preset", [sc.Preset.ScanpyV1, sc.Preset.ScanpyV2Preview]) def test_modularity_adata( - monkeypatch: pytest.MonkeyPatch, subtests: pytest.Subtests + monkeypatch: pytest.MonkeyPatch, subtests: pytest.Subtests, preset: sc.Preset ) -> None: """Test domain and API of modularity score.""" + # get_igraph_from_adjacency works very slightly differently in scanpy 2 + sc.settings.preset = preset adata = pbmc3k() sc.pp.pca(adata) sc.pp.neighbors(adata)