11from __future__ import annotations
22
3- from typing import TYPE_CHECKING
3+ from typing import TYPE_CHECKING , cast
44
55import numpy as np
66import pandas as pd
2929 MutableVertexPartition .__module__ = "leidenalg.VertexPartition"
3030
3131
32- def leiden ( # noqa: PLR0912, PLR0913, PLR0915
32+ def leiden ( # noqa: PLR0913
3333 adata : AnnData ,
3434 resolution : float = 1 ,
3535 * ,
@@ -47,7 +47,7 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
4747 flavor : Literal ["leidenalg" , "igraph" ] | None = None ,
4848 ** clustering_args ,
4949) -> AnnData | None :
50- """Cluster cells into subgroups :cite:p:`Traag2019`.
50+ r """Cluster cells into subgroups :cite:p:`Traag2019`.
5151
5252 Cluster cells using the Leiden algorithm :cite:p:`Traag2019`,
5353 an improved version of the Louvain algorithm :cite:p:`Blondel2008`.
@@ -120,34 +120,15 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
120120 A dict with the values for the parameters `resolution`, `random_state`,
121121 and `n_iterations`.
122122
123+ `adata.uns['leiden' | key_added]['modularity']` : :class:`float`
124+ The modularity score of the final clustering,
125+ as calculated by the `flavor`.
126+ Use :func:`scanpy.metrics.modularity`\ `(adata, mode='calculate' | 'update')`
127+ to calculate a score independent of `flavor`.
128+
123129 """
124- if flavor is None :
125- flavor = "leidenalg"
126- msg = (
127- "In the future, the default backend for leiden will be igraph instead of leidenalg. "
128- "To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. "
129- "`directed` must also be `False` to work with igraph’s implementation."
130- )
131- warn (msg , FutureWarning )
132- if flavor not in {"igraph" , "leidenalg" }:
133- msg = (
134- f"flavor must be either 'igraph' or 'leidenalg', but { flavor !r} was passed"
135- )
136- raise ValueError (msg )
130+ flavor = _validate_flavor (flavor , partition_type = partition_type , directed = directed )
137131 _utils .ensure_igraph ()
138- if flavor == "igraph" :
139- if directed :
140- msg = "Cannot use igraph’s leiden implementation with a directed graph."
141- raise ValueError (msg )
142- if partition_type is not None :
143- msg = "Do not pass in partition_type argument when using igraph."
144- raise ValueError (msg )
145- else :
146- try :
147- import leidenalg
148- except ImportError as e :
149- msg = "Please install the leiden algorithm: `conda install -c conda-forge leidenalg` or `pip install leidenalg`."
150- raise ImportError (msg ) from e
151132 clustering_args = dict (clustering_args )
152133
153134 start = logg .info ("running Leiden clustering" )
@@ -169,6 +150,8 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
169150 # (in the case of a partition variant that doesn't take it on input)
170151 clustering_args ["n_iterations" ] = n_iterations
171152 if flavor == "leidenalg" :
153+ import leidenalg
154+
172155 if resolution is not None :
173156 clustering_args ["resolution_parameter" ] = resolution
174157 directed = True if directed is None else directed
@@ -178,7 +161,10 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
178161 if use_weights :
179162 clustering_args ["weights" ] = np .array (g .es ["weight" ]).astype (np .float64 )
180163 clustering_args ["seed" ] = random_state
181- part = leidenalg .find_partition (g , partition_type , ** clustering_args )
164+ part = cast (
165+ "MutableVertexPartition" ,
166+ leidenalg .find_partition (g , partition_type , ** clustering_args ),
167+ )
182168 else :
183169 g = _utils .get_igraph_from_adjacency (adjacency , directed = False )
184170 if use_weights :
@@ -212,6 +198,7 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
212198 random_state = random_state ,
213199 n_iterations = n_iterations ,
214200 )
201+ adata .uns [key_added ]["modularity" ] = part .modularity
215202 logg .info (
216203 " finished" ,
217204 time = start ,
@@ -221,3 +208,38 @@ def leiden( # noqa: PLR0912, PLR0913, PLR0915
221208 ),
222209 )
223210 return adata if copy else None
211+
212+
213+ def _validate_flavor (
214+ flavor : str | None , * , partition_type : object | None , directed : bool | None
215+ ) -> Literal ["igraph" , "leidenalg" ]:
216+ match flavor :
217+ case "igraph" :
218+ if directed :
219+ msg = "Cannot use igraph’s leiden implementation with a directed graph."
220+ raise ValueError (msg )
221+ if partition_type is not None :
222+ msg = "Do not pass in partition_type argument when using igraph."
223+ raise ValueError (msg )
224+ case None | "leidenalg" :
225+ msg = (
226+ "The `igraph` implementation of leiden clustering is *orders of magnitude faster*. "
227+ "Set the flavor argument to (and install if needed) 'igraph' to use it."
228+ )
229+ if flavor is None :
230+ msg += (
231+ "\n In the future, the default backend for leiden will be igraph instead of leidenalg. "
232+ "To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. "
233+ "`directed` must also be `False` to work with igraph’s implementation."
234+ )
235+ warn (msg , FutureWarning if flavor is None else UserWarning )
236+ try :
237+ import leidenalg # noqa: F401
238+ except ImportError as e :
239+ msg = "Please install the leiden algorithm: `conda install -c conda-forge leidenalg` or `pip install leidenalg`."
240+ raise ImportError (msg ) from e
241+ flavor = "leidenalg"
242+ case _:
243+ msg = f"flavor must be either 'igraph' or 'leidenalg', but { flavor !r} was passed"
244+ raise ValueError (msg )
245+ return flavor
0 commit comments