From 241593b9d37b885ffb334bcced0a5dc4d334e9ea Mon Sep 17 00:00:00 2001 From: yzhuang-els Date: Mon, 11 May 2026 15:24:25 +0200 Subject: [PATCH] fix: prevent infinite loop in Leiden refinement on symmetric subgraphs --- sknetwork/clustering/leiden.py | 2 +- sknetwork/clustering/leiden_core.pyx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sknetwork/clustering/leiden.py b/sknetwork/clustering/leiden.py index 36b918f0..8d8e8412 100644 --- a/sknetwork/clustering/leiden.py +++ b/sknetwork/clustering/leiden.py @@ -163,7 +163,7 @@ def _optimize_refine(self, labels, adjacency, out_weights, in_weights): labels_refined = np.arange(len(labels)).astype(np.int32) return optimize_refine_core(labels, labels_refined, indices, indptr, data, out_weights, in_weights, out_cluster_weights, in_cluster_weights, cluster_weights, self_loops, - self.resolution) + self.resolution, self.tol_optimization) @staticmethod def _aggregate_refine(labels, labels_refined, adjacency, out_weights, in_weights): diff --git a/sknetwork/clustering/leiden_core.pyx b/sknetwork/clustering/leiden_core.pyx index e8814569..03d87fe0 100644 --- a/sknetwork/clustering/leiden_core.pyx +++ b/sknetwork/clustering/leiden_core.pyx @@ -13,7 +13,8 @@ ctypedef fused int_or_long: @cython.wraparound(False) def optimize_refine_core(int_or_long[:] labels, int_or_long[:] labels_refined, int_or_long[:] indices, int_or_long[:] indptr, float[:] data, float[:] out_weights, float[:] in_weights, float[:] out_cluster_weights, - float[:] in_cluster_weights, float[:] cluster_weights, float[:] self_loops, float resolution): # pragma: no cover + float[:] in_cluster_weights, float[:] cluster_weights, float[:] self_loops, float resolution, + float tol_optimization): # pragma: no cover """Refine clusters while maximizing modularity. Parameters @@ -42,6 +43,8 @@ def optimize_refine_core(int_or_long[:] labels, int_or_long[:] labels_refined, i Weights of self loops. resolution : Resolution parameter (positive). + tol_optimization : + Minimum increase in modularity to enter a new optimization pass. Returns ------- @@ -102,7 +105,7 @@ def optimize_refine_core(int_or_long[:] labels, int_or_long[:] labels_refined, i delta_local -= resolution * out_weight * in_cluster_weights[label_target] delta_local -= resolution * in_weight * out_cluster_weights[label_target] delta_local -= delta - if delta_local > 0: + if delta_local > tol_optimization: label_target_set.insert(label_target) cluster_weights[label_target] = 0