1919 doc_rng ,
2020)
2121from .._settings import Default
22+ from .._settings .presets import LeidenPreset
2223from .._utils import _doc_params
2324from .._utils .random import _accepts_legacy_random_state , _LegacyRng , _set_igraph_rng
2425from ..get ._out import _out_key , deprecated_copy
3536
3637if TYPE_CHECKING :
3738 from collections .abc import Sequence
38- from typing import Literal
3939
4040 from anndata import AnnData
41+ from igraph import Graph
4142
4243 from .._compat import CSBase
4344 from .._settings .presets import LeidenFlavor
@@ -85,7 +86,7 @@ def leiden( # noqa: PLR0913
8586 * ,
8687 restrict_to : tuple [str , Sequence [str ]] | None = None ,
8788 rng : SeedLike | RNGLike | None = None ,
88- directed : bool | None = None ,
89+ directed : bool | Default | None = Default ( preset = ( "leiden" , "directed" )) ,
8990 use_weights : bool = True ,
9091 n_iterations : int = - 1 ,
9192 partition_type : type [MutableVertexPartition ] | None = None ,
@@ -123,6 +124,7 @@ def leiden( # noqa: PLR0913
123124 {restrict_to}
124125 directed
125126 Whether to treat the graph as directed or undirected.
127+ `None` means directed for `flavor='leidenalg'` and undirected for `flavor='igraph'`.
126128 use_weights
127129 If `True`, edge weights from the graph are used in the computation
128130 (placing more emphasis on stronger edges).
@@ -169,7 +171,9 @@ def leiden( # noqa: PLR0913
169171 to calculate a score independent of `flavor`.
170172
171173 """
172- flavor = _validate_flavor (flavor , partition_type = partition_type , directed = directed )
174+ flavor , directed = _validate_flavor (
175+ flavor , partition_type = partition_type , directed = directed
176+ )
173177 _utils .ensure_igraph ()
174178 clustering_args = dict (clustering_args )
175179 rng = np .random .default_rng (rng )
@@ -205,7 +209,6 @@ def leiden( # noqa: PLR0913
205209
206210 if resolution is not None :
207211 clustering_args ["resolution_parameter" ] = resolution
208- directed = True if directed is None else directed
209212 g = _utils .get_igraph_from_adjacency (adjacency , directed = directed )
210213 if partition_type is None :
211214 partition_type = leidenalg .RBConfigurationVertexPartition
@@ -222,12 +225,20 @@ def leiden( # noqa: PLR0913
222225 leidenalg .find_partition (g , partition_type , seed = seed , ** clustering_args ),
223226 )
224227 else :
225- g = _utils .get_igraph_from_adjacency (adjacency , directed = False )
228+ g = _utils .get_igraph_from_adjacency (adjacency , directed = directed )
226229 if use_weights :
227230 clustering_args ["weights" ] = "weight"
228231 if resolution is not None :
229232 clustering_args ["resolution" ] = resolution
230233 clustering_args .setdefault ("objective_function" , "modularity" )
234+ if (
235+ directed
236+ and clustering_args ["objective_function" ] == "modularity"
237+ and clustering_args .keys ().isdisjoint ({"node_weights" , "node_in_weights" })
238+ ):
239+ clustering_args |= _directed_node_weights (
240+ g , weights = clustering_args .get ("weights" )
241+ )
231242 with _set_igraph_rng (rng ):
232243 part = g .community_leiden (** clustering_args )
233244 # store output into adata.obs
@@ -265,18 +276,30 @@ def leiden( # noqa: PLR0913
265276 return None
266277
267278
279+ def _directed_node_weights (
280+ g : Graph , * , weights : str | Sequence [float ] | None
281+ ) -> dict [str , list [float ]]:
282+ """Get in- and out-degrees as node weights; python-igraph otherwise only counts out-degrees."""
283+ return dict (
284+ node_weights = g .strength (mode = "out" , weights = weights ),
285+ node_in_weights = g .strength (mode = "in" , weights = weights ),
286+ )
287+
288+
268289def _validate_flavor (
269- flavor : str | None , * , partition_type : object | None , directed : bool | None
270- ) -> Literal ["igraph" , "leidenalg" ]:
271- if was_default := (flavor is None or isinstance (flavor , Default )):
272- from scanpy import settings
290+ flavor : str | None ,
291+ * ,
292+ partition_type : object | None ,
293+ directed : bool | Default | None ,
294+ ) -> LeidenPreset :
295+ from scanpy import settings
273296
297+ if was_default := (flavor is None or isinstance (flavor , Default )):
274298 flavor = settings .preset .leiden .flavor
299+ if isinstance (directed , Default ):
300+ directed = settings .preset .leiden .directed
275301 match flavor :
276302 case "igraph" :
277- if directed :
278- msg = "Cannot use igraph’s leiden implementation with a directed graph."
279- raise ValueError (msg )
280303 if partition_type is not None :
281304 msg = "Do not pass in partition_type argument when using igraph."
282305 raise ValueError (msg )
@@ -288,8 +311,7 @@ def _validate_flavor(
288311 if was_default :
289312 msg += (
290313 "\n In the future, the default backend for leiden will be igraph instead of leidenalg. "
291- "To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. "
292- "`directed` must also be `False` to work with igraph’s implementation."
314+ "To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`."
293315 )
294316 warn (msg , FutureWarning if was_default else UserWarning )
295317 try :
@@ -302,4 +324,6 @@ def _validate_flavor(
302324 case _:
303325 msg = f"flavor must be either 'igraph' or 'leidenalg', but { flavor !r} was passed."
304326 raise ValueError (msg )
305- return flavor
327+ if directed is None :
328+ directed = flavor == "leidenalg"
329+ return LeidenPreset (flavor , directed )
0 commit comments