[WIP] Louvain algorithm for undirected graphs - #1277
Conversation
…for partition instead of `Vec`
IvanIsCoding
left a comment
There was a problem hiding this comment.
Overall this is a great start! I would call it more than a draft given it already has good tests.
I left some Rust comments, I need to study the algorithm more myself to give more "algorithmic" advice
| } | ||
|
|
||
| #[test] | ||
| fn test_louvain_karate_club_graph() { |
There was a problem hiding this comment.
Friendly reminder to myself that we should add a Karate club graph generator. It could be a separate PR
There was a problem hiding this comment.
Once #1280 gets merged this test will hopefully be much shorter
| // happen to get the same result as: | ||
| // import networkx as nx | ||
| // g = nx.karate_club_graph() | ||
| // communities = nx.community.louvain_communities(g, weight='weight', seed=12) |
There was a problem hiding this comment.
I think this is a brittle test (https://testing.googleblog.com/2024/04/how-i-learned-to-stop-writing-brittle.html) but leave a TODO there and we can revisit it later. I am glad it passes though
There was a problem hiding this comment.
I'm not sure yet what to do with this. I can adjust the resolution parameter so that we consistently get two output communities. However, there is still a lot of dependence on the seed (in networkx I get at least 5 different partitions with the same parameters)
| }; | ||
|
|
||
| let m: f64 = total_edge_weight(self.graph); | ||
| sigma_internal / m - resolution * sigma_total_squared / (m * m) |
There was a problem hiding this comment.
We might want to reorganize this expression to avoid floating point erros, specially because of the common factor 1/m
There was a problem hiding this comment.
I started doing this in 58857fd. Other than the common factor what are the possible issues? I think sigma_internal, sigma_total_squared / m, and m are all about the same order of magnitude but could be missing something.
It looks as if the CI is failing because I used associated type bounds. |
Indeed our current MSRV is Rust 1.70 from June 2023. Rust 1.79 is from June 2024. In the past we used to follow Debian stable's Rustc which believe it or not is in 1.63 still. I'd try rewriting it before we discuss bumping MSR, 1.79 might break a couple people that install from source |
Pull Request Test Coverage Report for Build 10877937563Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
rustworkx upstream has no community detection algorithm at all (Louvain or Leiden) - confirmed via issue Qiskit#1141 (open since 2024-03, unclaimed) and its draft PR Qiskit#1277 (stale since 2024-09, never merged). Rather than writing Leiden from scratch, vendors it from network_partitions (MIT, part of graspologic-org/graspologic-native), re-verified live before use: petgraph interop support (feature = "petgraph"), MIT/Microsoft, actively maintained, not on crates.io so pulled as a git dependency pinned by commit in rustworkx-core/Cargo.toml. Required a newer Rust toolchain than this repo's own declared MSRV (1.85): network_partitions' 2024-edition source uses a let-chains pattern that only compiles on a newer stable release (tested with 1.98). rustworkx's own code is unaffected - this is purely a build requirement for the vendored dependency. rustworkx-core::community::leiden is generic over any IntoNodeIdentifiers + IntoEdgeReferences graph plus an edge weight_fn closure (matching this crate's existing weighted-algorithm convention, e.g. shortest_path's edge_cost). Its job is entirely adaptation: build a fresh, densely-indexed petgraph::graph::UnGraph<f64,f64> from the caller's graph (network_partitions' PetgraphNetworkView requires contiguous 0..n indices, which an arbitrary StableGraph with removed nodes wouldn't have), compute each node's weighted degree as its node weight (required for modularity mode - CPM mode gets 1.0 uniformly), run leiden_view(), then translate cluster assignments back to the caller's own node identifiers - never assumes network_partitions' internal indices mean anything outside this function. Exposed to Python as graph_leiden(graph, weight_fn=None, default_weight=1.0, resolution=1.0, iterations=2, randomness=None, use_modularity=True, seed=None) -> dict[int, int], following the same weight_fn/default_weight calling convention as the rest of this codebase's weighted algorithms (weight_callable/edge_weights_from_callable). Verified: rustworkx-core unit tests (two-clique split via the generic wrapper - not just network_partitions' own PetgraphNetworkView tests, to exercise this crate's actual conversion code; empty-graph and negative-weight error handling; same-seed determinism) and end-to-end Python tests (two-clique split, default-weight fallback without weight_fn, determinism, empty-graph/negative-weight errors, a complete graph collapsing to one community, an edgeless graph producing all singletons). Full existing test suites unaffected: rustworkx-core 373 Rust tests and 2,388 Python tests (11 pre-existing skips) all still pass.
I finally had some time to work on #1141, so far only on the Rust side and only for undirected graphs. Opening this for feedback on how the Rust code is organized, and maybe to restart the discussion on whether this should go here or in petgraph.
The big additions here are:
metrics.rsModularity: a trait for graphs for which we can compute modularity (by extension this means we can apply the Louvain method)modularity: computes the modularity, given a graph and a set of subsets.Partition: a struct holding a graph partition. This mainly exists to keep the implementation ofmodularityorganized.louvain.rsInnerGraph: at each level of the algorithm we work with an aggregated graph, where each node of this graph corresponds to one of the communities that were identified in the previous level).InnerGraphkeeps track of this correspondence.LouvainAlgo: helper functions for updating thePartitionat each level of the algorithm.one_level_undirected: most of the business logic is in here since it constructs the new communities at each level, roughly equivalent to_one_levelin the networkx implementation.louvain_communities: repeatedly callsone_level_undirected, stopping either after a fixed number of iterations or if the modularity improvement falls below the given threshold.I realize this is a lot to review at once and can go into more detail about each piece if necessary.