diff --git a/src/scanpy/_utils/__init__.py b/src/scanpy/_utils/__init__.py index ebaa7d985b..9cabf05e24 100644 --- a/src/scanpy/_utils/__init__.py +++ b/src/scanpy/_utils/__init__.py @@ -42,7 +42,6 @@ from typing import Any from anndata import AnnData - from igraph import Graph from numpy.typing import ArrayLike, NDArray from pandas._typing import Dtype as PdDtype @@ -268,30 +267,6 @@ def check_use_raw( return adata.raw is not None -# -------------------------------------------------------------------------------- -# Graph stuff -# -------------------------------------------------------------------------------- - - -def get_igraph_from_adjacency(adjacency: CSBase, *, directed: bool = False) -> Graph: - """Get igraph graph from adjacency matrix.""" - import igraph as ig - - sources, targets = adjacency.nonzero() - weights = dematrix(adjacency[sources, targets]).ravel() if len(sources) else [] - g = ig.Graph(directed=directed) - g.add_vertices(adjacency.shape[0]) # this adds adjacency.shape[0] vertices - g.add_edges(list(zip(sources, targets, strict=True))) - with suppress(KeyError): - g.es["weight"] = weights - if g.vcount() != adjacency.shape[0]: - logg.warning( - f"The constructed graph has only {g.vcount()} nodes. " - "Your adjacency matrix contained redundant nodes." - ) - return g - - # -------------------------------------------------------------------------------- # Group stuff # -------------------------------------------------------------------------------- diff --git a/src/scanpy/neighbors/__init__.py b/src/scanpy/neighbors/__init__.py index 3622c02576..e6875865a7 100644 --- a/src/scanpy/neighbors/__init__.py +++ b/src/scanpy/neighbors/__init__.py @@ -13,7 +13,6 @@ from packaging.version import Version from scipy import sparse -from .. import _utils from .. import logging as logg from .._compat import CSBase, CSRBase, SpBase, pkg_version, warn from .._docs import doc_rng @@ -578,7 +577,9 @@ def distances_dpt(self) -> OnFlySymMatrix: def to_igraph(self) -> Graph: """Generate igraph from connectiviies.""" - return _utils.get_igraph_from_adjacency(self.connectivities) + import igraph as ig + + return ig.Graph.Weighted_Adjacency(self.connectivities, mode=ig.ADJ_UNDIRECTED) @_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..376fb8ffc7 100644 --- a/src/scanpy/plotting/_tools/paga.py +++ b/src/scanpy/plotting/_tools/paga.py @@ -238,15 +238,21 @@ def _compute_pos( # noqa: PLR0912 ) raise ValueError(msg) else: # igraph layouts + import igraph as ig + if isinstance(rng, _LegacyRng): # backwards compat random.seed(rng.bytes(8)) ctx = nullcontext() else: ctx = _set_igraph_rng(rng) - g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) + g: ig.Graph = ig.Graph.Weighted_Adjacency( + adjacency_solid, mode=ig.ADJ_UNDIRECTED + ) with ctx: if "rt" in layout: - g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) + g_tree: ig.Graph = ig.Graph.Weighted_Adjacency( + adj_tree, mode=ig.ADJ_UNDIRECTED + ) pos_list = g_tree.layout( layout, root=root if isinstance(root, list) else [root] ).coords diff --git a/src/scanpy/tools/_draw_graph.py b/src/scanpy/tools/_draw_graph.py index bdbb37d0be..8d6aa92946 100644 --- a/src/scanpy/tools/_draw_graph.py +++ b/src/scanpy/tools/_draw_graph.py @@ -5,7 +5,6 @@ import numpy as np -from .. import _utils from .. import logging as logg from .._docs import doc_rng from .._utils import _choose_graph, _doc_params, get_literal_vals @@ -150,7 +149,9 @@ 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) + import igraph as ig + + g: ig.Graph = ig.Graph.Weighted_Adjacency(adjacency, mode=ig.ADJ_UNDIRECTED) 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/_leiden.py b/src/scanpy/tools/_leiden.py index 2f789acbaf..14de63277a 100644 --- a/src/scanpy/tools/_leiden.py +++ b/src/scanpy/tools/_leiden.py @@ -138,8 +138,10 @@ def leiden( # noqa: PLR0913 to calculate a score independent of `flavor`. """ - flavor = _validate_flavor(flavor, partition_type=partition_type, directed=directed) _utils.ensure_igraph() + import igraph as ig + + flavor = _validate_flavor(flavor, partition_type=partition_type, directed=directed) clustering_args = dict(clustering_args) rng = np.random.default_rng(rng) meta_random_state = ( @@ -170,7 +172,9 @@ def leiden( # noqa: PLR0913 if resolution is not None: clustering_args["resolution_parameter"] = resolution directed = True if directed is None else directed - g = _utils.get_igraph_from_adjacency(adjacency, directed=directed) + g: ig.Graph = ig.Graph.Weighted_Adjacency( + adjacency, mode=ig.ADJ_DIRECTED if directed else ig.ADJ_UNDIRECTED + ) if partition_type is None: partition_type = leidenalg.RBConfigurationVertexPartition if use_weights: @@ -186,7 +190,7 @@ def leiden( # noqa: PLR0913 leidenalg.find_partition(g, partition_type, seed=seed, **clustering_args), ) else: - g = _utils.get_igraph_from_adjacency(adjacency, directed=False) + g: ig.Graph = ig.Graph.Weighted_Adjacency(adjacency, mode=ig.ADJ_UNDIRECTED) if use_weights: clustering_args["weights"] = "weight" if resolution is not None: diff --git a/src/scanpy/tools/_louvain.py b/src/scanpy/tools/_louvain.py index 19fb50bd47..8c3d6b3b0d 100644 --- a/src/scanpy/tools/_louvain.py +++ b/src/scanpy/tools/_louvain.py @@ -9,7 +9,6 @@ from packaging.version import Version from scverse_misc import Deprecation, deprecated -from .. import _utils from .. import logging as logg from .._compat import pkg_version from .._utils import _choose_graph, _doc_params @@ -143,13 +142,17 @@ def louvain( # noqa: PLR0912, PLR0913, PLR0915 adjacency=adjacency, ) if flavor in {"vtraag", "igraph"}: + import igraph as ig + if flavor == "igraph" and resolution is not None: logg.warning('`resolution` parameter has no effect for flavor "igraph"') if directed and flavor == "igraph": directed = False if not directed: logg.debug(" using the undirected graph") - g = _utils.get_igraph_from_adjacency(adjacency, directed=directed) + g: ig.Graph = ig.Graph.Weighted_Adjacency( + adjacency, mode=ig.ADJ_DIRECTED if directed else ig.ADJ_UNDIRECTED + ) weights = np.array(g.es["weight"]).astype(np.float64) if use_weights else None if flavor == "vtraag": import louvain diff --git a/src/scanpy/tools/_paga.py b/src/scanpy/tools/_paga.py index f2130b5787..dac585ee0f 100644 --- a/src/scanpy/tools/_paga.py +++ b/src/scanpy/tools/_paga.py @@ -180,7 +180,9 @@ def _compute_connectivities_v1_2(self): ones = self._neighbors.distances.copy() ones.data = np.ones(len(ones.data)) # should be directed if we deal with distances - g = _utils.get_igraph_from_adjacency(ones, directed=True) + g: igraph.Graph = igraph.Graph.Weighted_Adjacency( + ones, mode=igraph.ADJ_DIRECTED + ) vc = igraph.VertexClustering( g, membership=self._adata.obs[self._groups_key].cat.codes.values ) @@ -212,7 +214,9 @@ 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: igraph.Graph = igraph.Graph.Weighted_Adjacency( + ones, mode=igraph.ADJ_DIRECTED + ) vc = igraph.VertexClustering( g, membership=self._adata.obs[self._groups_key].cat.codes.values ) @@ -289,9 +293,8 @@ def compute_transitions(self): # raise ValueError(msg) import igraph - g = _utils.get_igraph_from_adjacency( - self._adata.uns[vkey].astype("bool"), - directed=True, + g: igraph.Graph = igraph.Graph.Adjacency( + self._adata.uns[vkey].astype("bool"), mode=igraph.ADJ_DIRECTED ) vc = igraph.VertexClustering( g, membership=self._adata.obs[self._groups_key].cat.codes.values @@ -324,9 +327,8 @@ def compute_transitions(self): def compute_transitions_old(self): import igraph - g = _utils.get_igraph_from_adjacency( - self._adata.uns["velocyto_transitions"], - directed=True, + g: igraph.Graph = igraph.Graph.Weighted_Adjacency( + self._adata.uns["velocyto_transitions"], mode=igraph.ADJ_DIRECTED ) vc = igraph.VertexClustering( g, membership=self._adata.obs[self._groups_key].cat.codes.values @@ -334,9 +336,9 @@ def compute_transitions_old(self): # this stores all single-cell edges in the cluster graph cg_full = vc.cluster_graph(combine_edges=False) # this is the boolean version that simply counts edges in the clustered graph - g_bool = _utils.get_igraph_from_adjacency( + g_bool = igraph.Graph.Adjacency( self._adata.uns["velocyto_transitions"].astype("bool"), - directed=True, + mode=igraph.ADJ_DIRECTED, ) vc_bool = igraph.VertexClustering( g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values diff --git a/tests/_images/heatmap_var_as_dict/expected.png b/tests/_images/heatmap_var_as_dict/expected.png index e2fb15b1c3..9249144963 100644 Binary files a/tests/_images/heatmap_var_as_dict/expected.png and b/tests/_images/heatmap_var_as_dict/expected.png differ diff --git a/tests/_images/paga/expected.png b/tests/_images/paga/expected.png index ffca4b8e0e..43a3083a7a 100644 Binary files a/tests/_images/paga/expected.png and b/tests/_images/paga/expected.png differ diff --git a/tests/_images/paga_continuous/expected.png b/tests/_images/paga_continuous/expected.png index a674273748..f4eacd1106 100644 Binary files a/tests/_images/paga_continuous/expected.png and b/tests/_images/paga_continuous/expected.png differ diff --git a/tests/_images/paga_continuous_multiple/expected.png b/tests/_images/paga_continuous_multiple/expected.png index 0bbda8ca49..de8252619c 100644 Binary files a/tests/_images/paga_continuous_multiple/expected.png and b/tests/_images/paga_continuous_multiple/expected.png differ diff --git a/tests/_images/paga_continuous_obs/expected.png b/tests/_images/paga_continuous_obs/expected.png index 8c2a3d5bbb..f9f936d18f 100644 Binary files a/tests/_images/paga_continuous_obs/expected.png and b/tests/_images/paga_continuous_obs/expected.png differ diff --git a/tests/_images/paga_pie/expected.png b/tests/_images/paga_pie/expected.png index 7a428c8ce7..997853b9f4 100644 Binary files a/tests/_images/paga_pie/expected.png and b/tests/_images/paga_pie/expected.png differ diff --git a/tests/test_clustering.py b/tests/test_clustering.py index 51fa863d9e..1a5a02a2e1 100644 --- a/tests/test_clustering.py +++ b/tests/test_clustering.py @@ -84,7 +84,7 @@ def test_leiden_random_state( n_iterations=n_iterations, **{rng_arg: seed}, ) - for seed in (1, 1, 42) + for seed in (1, 1, 3) ) with subtests.test("reproducible"): pd.testing.assert_series_equal( 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"]