1use crate::{CsrGraph, NodeId};
11use anyhow::Result;
12use aprender::graph::Graph as AprenderGraph;
13
14#[derive(Debug, Clone)]
16pub struct CommunityDetectionResult {
17 pub communities: Vec<Vec<NodeId>>,
19
20 pub num_communities: usize,
22
23 pub modularity: f64,
25}
26
27impl CommunityDetectionResult {
28 #[must_use]
32 pub fn get_community(&self, node: NodeId) -> Option<usize> {
33 for (comm_id, community) in self.communities.iter().enumerate() {
34 if community.contains(&node) {
35 return Some(comm_id);
36 }
37 }
38 None
39 }
40
41 #[must_use]
43 pub fn get_community_nodes(&self, comm_id: usize) -> Option<&[NodeId]> {
44 self.communities.get(comm_id).map(Vec::as_slice)
45 }
46
47 #[must_use]
49 pub fn community_size(&self, comm_id: usize) -> Option<usize> {
50 self.communities.get(comm_id).map(Vec::len)
51 }
52}
53
54pub fn louvain(graph: &CsrGraph) -> Result<CommunityDetectionResult> {
88 let aprender_graph = convert_to_aprender(graph);
90
91 let communities = aprender_graph.louvain();
93
94 let modularity = aprender_graph.modularity(&communities);
96
97 let converted_communities: Vec<Vec<NodeId>> = communities
99 .into_iter()
100 .map(|community| {
101 community
102 .into_iter()
103 .filter_map(|node_id| u32::try_from(node_id).ok().map(NodeId))
104 .collect()
105 })
106 .filter(|community: &Vec<NodeId>| !community.is_empty()) .collect();
108
109 let num_communities = converted_communities.len();
110
111 Ok(CommunityDetectionResult { communities: converted_communities, num_communities, modularity })
112}
113
114fn convert_to_aprender(graph: &CsrGraph) -> AprenderGraph {
116 let mut edges = Vec::new();
118
119 for (src, targets, weights) in graph.iter_adjacency() {
120 for (dst, weight) in targets.iter().zip(weights.iter()) {
121 edges.push((src.0 as usize, *dst as usize, f64::from(*weight)));
122 }
123 }
124
125 AprenderGraph::from_weighted_edges(&edges, false)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_louvain_empty_graph() {
135 let graph = CsrGraph::new();
136 let result = louvain(&graph).unwrap();
137
138 assert_eq!(result.num_communities, 0);
139 assert_eq!(result.communities.len(), 0);
140 }
141
142 #[test]
143 fn test_louvain_single_triangle() {
144 let mut graph = CsrGraph::new();
146 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
147 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
148 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
149
150 let result = louvain(&graph).unwrap();
151
152 assert_eq!(result.num_communities, 1, "Triangle should form 1 community");
153
154 let comm_0 = result.get_community(NodeId(0));
156 let comm_1 = result.get_community(NodeId(1));
157 let comm_2 = result.get_community(NodeId(2));
158
159 assert_eq!(comm_0, comm_1);
160 assert_eq!(comm_1, comm_2);
161 }
162
163 #[test]
164 fn test_louvain_two_triangles_connected() {
165 let mut graph = CsrGraph::new();
170
171 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
173 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
174 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
175
176 graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
178 graph.add_edge(NodeId(4), NodeId(5), 1.0).unwrap();
179 graph.add_edge(NodeId(5), NodeId(3), 1.0).unwrap();
180
181 graph.add_edge(NodeId(2), NodeId(3), 1.0).unwrap();
183
184 let result = louvain(&graph).unwrap();
185
186 assert!(result.num_communities >= 1, "Should find at least 1 community");
188
189 assert!(result.modularity > 0.0, "Modularity should be positive for community structure");
191 }
192
193 #[test]
194 fn test_louvain_disconnected_components() {
195 let mut graph = CsrGraph::new();
199
200 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
202 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
203 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
204
205 graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
207 graph.add_edge(NodeId(4), NodeId(5), 1.0).unwrap();
208 graph.add_edge(NodeId(5), NodeId(3), 1.0).unwrap();
209
210 let result = louvain(&graph).unwrap();
211
212 assert_eq!(
214 result.num_communities, 2,
215 "Should find 2 communities for 2 disconnected components"
216 );
217
218 let comm_0 = result.get_community(NodeId(0));
220 let comm_3 = result.get_community(NodeId(3));
221
222 assert!(comm_0.is_some() && comm_3.is_some());
223 assert_ne!(comm_0, comm_3, "Disconnected components should be in different communities");
224 }
225
226 #[test]
227 fn test_community_detection_result_api() {
228 let result = CommunityDetectionResult {
230 communities: vec![vec![NodeId(0), NodeId(1), NodeId(2)], vec![NodeId(3), NodeId(4)]],
231 num_communities: 2,
232 modularity: 0.42,
233 };
234
235 assert_eq!(result.get_community(NodeId(0)), Some(0));
237 assert_eq!(result.get_community(NodeId(3)), Some(1));
238 assert_eq!(result.get_community(NodeId(99)), None);
239
240 assert_eq!(
242 result.get_community_nodes(0),
243 Some(&[NodeId(0), NodeId(1), NodeId(2)] as &[NodeId])
244 );
245 assert_eq!(result.get_community_nodes(1), Some(&[NodeId(3), NodeId(4)] as &[NodeId]));
246 assert_eq!(result.get_community_nodes(2), None);
247
248 assert_eq!(result.community_size(0), Some(3));
250 assert_eq!(result.community_size(1), Some(2));
251 assert_eq!(result.community_size(2), None);
252 }
253
254 #[test]
255 fn test_louvain_all_nodes_assigned() {
256 let mut graph = CsrGraph::new();
258
259 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
261 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
262 graph.add_edge(NodeId(2), NodeId(3), 1.0).unwrap();
263 graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
264
265 let result = louvain(&graph).unwrap();
266
267 let mut assigned_nodes = std::collections::HashSet::new();
269 for community in &result.communities {
270 for &node in community {
271 assigned_nodes.insert(node);
272 }
273 }
274
275 assert_eq!(assigned_nodes.len(), 5);
277 assert!(assigned_nodes.contains(&NodeId(0)));
278 assert!(assigned_nodes.contains(&NodeId(1)));
279 assert!(assigned_nodes.contains(&NodeId(2)));
280 assert!(assigned_nodes.contains(&NodeId(3)));
281 assert!(assigned_nodes.contains(&NodeId(4)));
282 }
283}