1use std::collections::BTreeMap;
2use std::num::NonZeroUsize;
3
4use serde::{Deserialize, Serialize};
5
6use crate::{Graph, GraphError, NodeId, NonNegativeFiniteF64, PositiveFiniteF64};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
10pub struct LouvainOptions {
11 pub resolution: PositiveFiniteF64,
13 pub threshold: NonNegativeFiniteF64,
15 pub seed: u64,
17 pub max_levels: NonZeroUsize,
19}
20
21impl Default for LouvainOptions {
22 fn default() -> Self {
23 Self {
24 resolution: PositiveFiniteF64::new(1.0).expect("one is positive"),
25 threshold: NonNegativeFiniteF64::new(1e-4).expect("threshold is non-negative"),
26 seed: 42,
27 max_levels: NonZeroUsize::new(10).expect("10 is non-zero"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34pub struct Community {
35 pub id: NodeId,
37 pub node_ids: Vec<NodeId>,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct CommunityResult {
44 pub communities: Vec<Community>,
46 pub modularity: f64,
48 pub levels: usize,
50}
51
52#[derive(Debug, Clone, Copy)]
53pub(super) struct LevelEdge {
54 pub(super) source: usize,
55 pub(super) target: usize,
56 pub(super) weight: f64,
57}
58
59pub(super) struct LevelGraph {
60 pub(super) node_members: Vec<Vec<usize>>,
61 pub(super) edges: Vec<LevelEdge>,
62 pub(super) adjacency: Vec<Vec<(usize, f64)>>,
63 pub(super) degrees: Vec<f64>,
64 pub(super) total_weight: f64,
65}
66
67impl LevelGraph {
68 pub(super) fn from_original(graph: &Graph) -> Self {
69 let edges = graph
70 .edges()
71 .iter()
72 .map(|edge| LevelEdge {
73 source: graph
74 .node_index(&edge.source)
75 .expect("validated edge source exists"),
76 target: graph
77 .node_index(&edge.target)
78 .expect("validated edge target exists"),
79 weight: edge.weight.unwrap_or(1.0),
80 })
81 .collect::<Vec<_>>();
82 Self::new(
83 (0..graph.node_count()).map(|node| vec![node]).collect(),
84 edges,
85 )
86 }
87
88 pub(super) fn new(node_members: Vec<Vec<usize>>, edges: Vec<LevelEdge>) -> Self {
89 let mut adjacency = vec![Vec::new(); node_members.len()];
90 let mut degrees = vec![0.0; node_members.len()];
91 let mut total_weight = 0.0;
92 for edge in &edges {
93 total_weight += edge.weight;
94 if edge.source == edge.target {
95 degrees[edge.source] += edge.weight * 2.0;
96 } else {
97 degrees[edge.source] += edge.weight;
98 degrees[edge.target] += edge.weight;
99 adjacency[edge.source].push((edge.target, edge.weight));
100 adjacency[edge.target].push((edge.source, edge.weight));
101 }
102 }
103 for row in &mut adjacency {
104 row.sort_by_key(|(neighbor, _)| *neighbor);
105 }
106 Self {
107 node_members,
108 edges,
109 adjacency,
110 degrees,
111 total_weight,
112 }
113 }
114
115 fn one_level(&self, resolution: f64, seed: &mut u64) -> Vec<usize> {
116 self.local_move(
117 &(0..self.node_members.len()).collect::<Vec<_>>(),
118 resolution,
119 seed,
120 usize::MAX,
121 )
122 .0
123 }
124
125 pub(super) fn local_move(
126 &self,
127 initial: &[usize],
128 resolution: f64,
129 seed: &mut u64,
130 max_iterations: usize,
131 ) -> (Vec<usize>, bool) {
132 let node_count = self.node_members.len();
133 assert_eq!(initial.len(), node_count, "one community per level node");
134 let mut communities = compress_communities(initial);
135 if self.total_weight == 0.0 {
136 return (communities, false);
137 }
138 let mut community_degrees = vec![0.0; node_count];
139 for (node, community) in communities.iter().enumerate() {
140 community_degrees[*community] += self.degrees[node];
141 }
142 let mut order = (0..node_count).collect::<Vec<_>>();
143 shuffle(&mut order, seed);
144 let mut moved_any = false;
145 for _ in 0..max_iterations {
146 let mut moved = false;
147 for node in &order {
148 let old_community = communities[*node];
149 let degree = self.degrees[*node];
150 let mut weights_by_community = BTreeMap::<usize, f64>::new();
151 for (neighbor, weight) in &self.adjacency[*node] {
152 *weights_by_community
153 .entry(communities[*neighbor])
154 .or_default() += *weight;
155 }
156 community_degrees[old_community] -= degree;
157 let denominator = 2.0 * self.total_weight * self.total_weight;
158 let remove_cost = -weights_by_community
159 .get(&old_community)
160 .copied()
161 .unwrap_or(0.0)
162 / self.total_weight
163 + resolution * community_degrees[old_community] * degree / denominator;
164 let mut best_community = old_community;
165 let mut best_gain = 0.0;
166 for (candidate, edge_weight) in weights_by_community {
167 let gain = remove_cost + edge_weight / self.total_weight
168 - resolution * community_degrees[candidate] * degree / denominator;
169 if gain > best_gain
170 || (gain == best_gain && gain > 0.0 && candidate < best_community)
171 {
172 best_gain = gain;
173 best_community = candidate;
174 }
175 }
176 community_degrees[best_community] += degree;
177 if best_community != old_community {
178 communities[*node] = best_community;
179 moved = true;
180 moved_any = true;
181 }
182 }
183 if !moved {
184 break;
185 }
186 }
187 (compress_communities(&communities), moved_any)
188 }
189
190 pub(super) fn aggregate(&self, communities: &[usize]) -> Self {
191 let community_count = communities.iter().max().map_or(0, |max| max + 1);
192 let mut members = vec![Vec::new(); community_count];
193 for (node, community) in communities.iter().enumerate() {
194 members[*community].extend(self.node_members[node].iter().copied());
195 }
196 let mut edge_weights = BTreeMap::<(usize, usize), f64>::new();
197 for edge in &self.edges {
198 let source = communities[edge.source];
199 let target = communities[edge.target];
200 let pair = if source <= target {
201 (source, target)
202 } else {
203 (target, source)
204 };
205 *edge_weights.entry(pair).or_default() += edge.weight;
206 }
207 Self::new(
208 members,
209 edge_weights
210 .into_iter()
211 .map(|((source, target), weight)| LevelEdge {
212 source,
213 target,
214 weight,
215 })
216 .collect(),
217 )
218 }
219}
220
221impl Graph {
222 pub fn louvain_communities(
224 &self,
225 options: LouvainOptions,
226 ) -> Result<CommunityResult, GraphError> {
227 validate_community_graph(self, "Louvain")?;
228 if self.node_count() == 0 {
229 return Ok(CommunityResult {
230 communities: Vec::new(),
231 modularity: 0.0,
232 levels: 0,
233 });
234 }
235
236 let original_edges = LevelGraph::from_original(self).edges;
237 let mut level = LevelGraph::from_original(self);
238 let mut seed = options.seed;
239 let mut final_assignment = (0..self.node_count()).collect::<Vec<_>>();
240 let mut previous_modularity = f64::NEG_INFINITY;
241 let mut final_modularity = 0.0;
242 let mut levels = 0;
243 for _ in 0..options.max_levels.get() {
244 let communities = level.one_level(options.resolution.get(), &mut seed);
245 levels += 1;
246 final_assignment =
247 assignment_for_original_nodes(self.node_count(), &level.node_members, &communities);
248 final_modularity = modularity(
249 self.node_count(),
250 &original_edges,
251 &final_assignment,
252 options.resolution.get(),
253 );
254 let community_count = communities.iter().max().map_or(0, |max| max + 1);
255 if community_count == level.node_members.len()
256 || (previous_modularity.is_finite()
257 && final_modularity - previous_modularity <= options.threshold.get())
258 {
259 break;
260 }
261 previous_modularity = final_modularity;
262 level = level.aggregate(&communities);
263 }
264
265 Ok(CommunityResult {
266 communities: canonical_communities(self, &final_assignment),
267 modularity: final_modularity,
268 levels,
269 })
270 }
271}
272
273pub(super) fn assignment_for_original_nodes(
274 original_node_count: usize,
275 members: &[Vec<usize>],
276 communities: &[usize],
277) -> Vec<usize> {
278 let mut assignment = vec![0; original_node_count];
279 for (node, originals) in members.iter().enumerate() {
280 for original in originals {
281 assignment[*original] = communities[node];
282 }
283 }
284 assignment
285}
286
287pub(super) fn compress_communities(communities: &[usize]) -> Vec<usize> {
288 let mut remap = BTreeMap::new();
289 let mut next = 0;
290 communities
291 .iter()
292 .map(|community| {
293 *remap.entry(*community).or_insert_with(|| {
294 let current = next;
295 next += 1;
296 current
297 })
298 })
299 .collect()
300}
301
302pub(super) fn modularity(
303 node_count: usize,
304 edges: &[LevelEdge],
305 communities: &[usize],
306 resolution: f64,
307) -> f64 {
308 let total_weight = edges.iter().map(|edge| edge.weight).sum::<f64>();
309 if total_weight == 0.0 {
310 return 0.0;
311 }
312 let community_count = communities.iter().max().map_or(0, |max| max + 1);
313 let mut internal_weight = vec![0.0; community_count];
314 let mut degree = vec![0.0; node_count];
315 for edge in edges {
316 if edge.source == edge.target {
317 degree[edge.source] += edge.weight * 2.0;
318 } else {
319 degree[edge.source] += edge.weight;
320 degree[edge.target] += edge.weight;
321 }
322 if communities[edge.source] == communities[edge.target] {
323 internal_weight[communities[edge.source]] += edge.weight;
324 }
325 }
326 let mut community_degree = vec![0.0; community_count];
327 for (node, node_degree) in degree.into_iter().enumerate() {
328 community_degree[communities[node]] += node_degree;
329 }
330 (0..community_count)
331 .map(|community| {
332 internal_weight[community] / total_weight
333 - resolution * (community_degree[community] / (2.0 * total_weight)).powi(2)
334 })
335 .sum()
336}
337
338pub(super) fn next_random(seed: &mut u64) -> u64 {
339 *seed = seed
340 .wrapping_mul(6_364_136_223_846_793_005)
341 .wrapping_add(1_442_695_040_888_963_407);
342 *seed
343}
344
345pub(super) fn shuffle(values: &mut [usize], seed: &mut u64) {
346 for index in (1..values.len()).rev() {
347 let other = (next_random(seed) as usize) % (index + 1);
348 values.swap(index, other);
349 }
350}
351
352pub(super) fn validate_community_graph(graph: &Graph, algorithm: &str) -> Result<(), GraphError> {
353 if graph.is_directed() {
354 return Err(GraphError::InvalidOption(format!(
355 "{algorithm} requires an explicit undirected graph view"
356 )));
357 }
358 Ok(())
359}
360
361pub(super) fn canonical_communities(graph: &Graph, assignment: &[usize]) -> Vec<Community> {
362 let assignment = compress_communities(assignment);
363 let community_count = assignment.iter().max().map_or(0, |max| max + 1);
364 let mut members = vec![Vec::new(); community_count];
365 for (node, community) in assignment.into_iter().enumerate() {
366 members[community].push(graph.node_id(node).clone());
367 }
368 for community in &mut members {
369 community.sort();
370 }
371 let mut communities = members
372 .into_iter()
373 .map(|node_ids| Community {
374 id: node_ids[0].clone(),
375 node_ids,
376 })
377 .collect::<Vec<_>>();
378 communities.sort_by(|left, right| left.id.cmp(&right.id));
379 communities
380}
381
382#[cfg(test)]
383mod tests {
384 use approx::assert_abs_diff_eq;
385
386 use super::*;
387 use crate::{Edge, GraphKind, Node};
388
389 fn bridged_triangles() -> Graph {
390 Graph::new(
391 GraphKind::Graph,
392 ["a", "b", "c", "d", "e", "f"].into_iter().map(Node::new),
393 [
394 Edge::new("ab", "a", "b"),
395 Edge::new("bc", "b", "c"),
396 Edge::new("ca", "c", "a"),
397 Edge::new("de", "d", "e"),
398 Edge::new("ef", "e", "f"),
399 Edge::new("fd", "f", "d"),
400 Edge::new("cd", "c", "d").with_weight(0.1),
401 ],
402 )
403 .unwrap()
404 }
405
406 #[test]
407 fn louvain_finds_dense_groups_and_is_seed_deterministic() {
408 let graph = bridged_triangles();
409 let first = graph
410 .louvain_communities(LouvainOptions::default())
411 .unwrap();
412 let second = graph
413 .louvain_communities(LouvainOptions::default())
414 .unwrap();
415 assert_eq!(first, second);
416 assert_eq!(first.communities.len(), 2);
417 assert_eq!(first.communities[0].node_ids, ["a", "b", "c"]);
418 assert_eq!(first.communities[1].node_ids, ["d", "e", "f"]);
419 assert!(first.modularity > 0.0);
420 assert_abs_diff_eq!(first.modularity, 0.4836065573770493, epsilon = 1e-12);
421 }
422
423 #[test]
424 fn louvain_keeps_isolated_nodes_as_singletons() {
425 let graph = Graph::new(GraphKind::Graph, [Node::new("a"), Node::new("b")], []).unwrap();
426 let result = graph
427 .louvain_communities(LouvainOptions::default())
428 .unwrap();
429 assert_eq!(result.communities.len(), 2);
430 assert_eq!(result.modularity, 0.0);
431 }
432
433 #[test]
434 fn louvain_requires_undirected_graph_and_options_reject_invalid_values() {
435 let graph = Graph::new(GraphKind::DiGraph, [Node::new("a")], []).unwrap();
436 assert!(matches!(
437 graph.louvain_communities(LouvainOptions::default()),
438 Err(GraphError::InvalidOption(_))
439 ));
440 assert!(PositiveFiniteF64::new(0.0).is_err());
441 assert!(NonNegativeFiniteF64::new(-1.0).is_err());
442 }
443
444 #[test]
445 fn louvain_respects_resolution_threshold_level_cap_and_parallel_weights() {
446 let graph = bridged_triangles();
447 let high_resolution = graph
448 .louvain_communities(LouvainOptions {
449 resolution: PositiveFiniteF64::new(2.0).unwrap(),
450 ..LouvainOptions::default()
451 })
452 .unwrap();
453 assert_abs_diff_eq!(
454 high_resolution.modularity,
455 -0.016393442622950727,
456 epsilon = 1e-12
457 );
458 let capped = graph
459 .louvain_communities(LouvainOptions {
460 threshold: NonNegativeFiniteF64::new(1.0).unwrap(),
461 max_levels: NonZeroUsize::new(1).unwrap(),
462 ..LouvainOptions::default()
463 })
464 .unwrap();
465 assert_eq!(capped.levels, 1);
466
467 let parallel = Graph::new(
468 GraphKind::MultiGraph,
469 [Node::new("a"), Node::new("b"), Node::new("c")],
470 [
471 Edge::new("ab1", "a", "b").with_weight(2.0),
472 Edge::new("ab2", "a", "b").with_weight(3.0),
473 Edge::new("bc", "b", "c").with_weight(0.1),
474 Edge::new("aa", "a", "a").with_weight(0.5),
475 ],
476 )
477 .unwrap();
478 let result = parallel
479 .louvain_communities(LouvainOptions::default())
480 .unwrap();
481 assert_eq!(result.communities[0].node_ids, ["a", "b", "c"]);
482 }
483}