1use crate::types::{CommunityResult, CsrGraph};
9use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
21pub struct ModularityScore {
22 metadata: KernelMetadata,
23}
24
25impl ModularityScore {
26 #[must_use]
28 pub fn new() -> Self {
29 Self {
30 metadata: KernelMetadata::batch("graph/modularity-score", Domain::GraphAnalytics)
31 .with_description("Modularity score Q calculation")
32 .with_throughput(100_000)
33 .with_latency_us(10.0),
34 }
35 }
36
37 pub fn compute(graph: &CsrGraph, communities: &[u64]) -> f64 {
41 let n = graph.num_nodes;
42 if n == 0 || graph.num_edges == 0 {
43 return 0.0;
44 }
45
46 let m = graph.num_edges as f64;
47 let two_m = 2.0 * m;
48
49 let degrees: Vec<f64> = (0..n).map(|i| graph.out_degree(i as u64) as f64).collect();
51
52 let mut q = 0.0;
53
54 for i in 0..n {
56 let ci = communities[i];
57 let ki = degrees[i];
58
59 for &j in graph.neighbors(i as u64) {
60 let j = j as usize;
61 let cj = communities[j];
62
63 if ci == cj {
64 let kj = degrees[j];
65 q += 1.0 - (ki * kj) / two_m;
67 }
68 }
69 }
70
71 q / two_m
72 }
73
74 pub fn delta_modularity(
76 graph: &CsrGraph,
77 node: usize,
78 old_community: u64,
79 new_community: u64,
80 communities: &[u64],
81 community_degrees: &HashMap<u64, f64>,
82 m: f64,
83 ) -> f64 {
84 if old_community == new_community {
85 return 0.0;
86 }
87
88 let ki = graph.out_degree(node as u64) as f64;
89 let two_m = 2.0 * m;
90
91 let mut edges_to_old = 0.0;
93 let mut edges_to_new = 0.0;
94
95 for &neighbor in graph.neighbors(node as u64) {
96 let nc = communities[neighbor as usize];
97 if nc == old_community {
98 edges_to_old += 1.0;
99 } else if nc == new_community {
100 edges_to_new += 1.0;
101 }
102 }
103
104 let sigma_old = community_degrees
105 .get(&old_community)
106 .copied()
107 .unwrap_or(0.0);
108 let sigma_new = community_degrees
109 .get(&new_community)
110 .copied()
111 .unwrap_or(0.0);
112
113 let delta = (edges_to_new - sigma_new * ki / two_m)
115 - (edges_to_old - (sigma_old - ki) * ki / two_m);
116
117 delta / m
118 }
119}
120
121impl Default for ModularityScore {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl GpuKernel for ModularityScore {
128 fn metadata(&self) -> &KernelMetadata {
129 &self.metadata
130 }
131}
132
133#[derive(Debug, Clone)]
141pub struct LouvainCommunity {
142 metadata: KernelMetadata,
143}
144
145impl LouvainCommunity {
146 #[must_use]
148 pub fn new() -> Self {
149 Self {
150 metadata: KernelMetadata::batch("graph/louvain-community", Domain::GraphAnalytics)
151 .with_description("Louvain community detection (modularity optimization)")
152 .with_throughput(10_000)
153 .with_latency_us(100.0),
154 }
155 }
156
157 pub fn compute(
164 graph: &CsrGraph,
165 max_iterations: u32,
166 min_modularity_gain: f64,
167 ) -> CommunityResult {
168 let n = graph.num_nodes;
169 if n == 0 {
170 return CommunityResult {
171 assignments: Vec::new(),
172 num_communities: 0,
173 modularity: 0.0,
174 };
175 }
176
177 let mut communities: Vec<u64> = (0..n).map(|i| i as u64).collect();
179
180 let m = graph.num_edges as f64;
182 if m == 0.0 {
183 return CommunityResult {
184 assignments: communities,
185 num_communities: n,
186 modularity: 0.0,
187 };
188 }
189
190 let mut community_degrees: HashMap<u64, f64> = HashMap::new();
192 for i in 0..n {
193 let degree = graph.out_degree(i as u64) as f64;
194 *community_degrees.entry(i as u64).or_insert(0.0) += degree;
195 }
196
197 let mut community_internal_edges: HashMap<u64, f64> = HashMap::new();
199
200 let mut improved = true;
201 let mut iteration = 0;
202
203 while improved && iteration < max_iterations {
204 improved = false;
205 iteration += 1;
206 let mut total_gain = 0.0;
207
208 for node in 0..n {
210 let current_community = communities[node];
211 let node_degree = graph.out_degree(node as u64) as f64;
212
213 let mut neighbor_communities: HashMap<u64, f64> = HashMap::new();
215 for &neighbor in graph.neighbors(node as u64) {
216 let nc = communities[neighbor as usize];
217 *neighbor_communities.entry(nc).or_insert(0.0) += 1.0;
218 }
219
220 let mut best_community = current_community;
222 let mut best_gain = 0.0;
223
224 for (&comm, &edges_to_comm) in &neighbor_communities {
225 if comm == current_community {
226 continue;
227 }
228
229 let sigma_comm = community_degrees.get(&comm).copied().unwrap_or(0.0);
230 let sigma_current = community_degrees
231 .get(¤t_community)
232 .copied()
233 .unwrap_or(0.0);
234 let edges_to_current = neighbor_communities
235 .get(¤t_community)
236 .copied()
237 .unwrap_or(0.0);
238
239 let gain = (edges_to_comm - edges_to_current)
241 - node_degree * (sigma_comm - sigma_current + node_degree) / (2.0 * m);
242
243 if gain > best_gain {
244 best_gain = gain;
245 best_community = comm;
246 }
247 }
248
249 if best_gain > min_modularity_gain {
251 if let Some(d) = community_degrees.get_mut(¤t_community) {
253 *d -= node_degree;
254 }
255 *community_degrees.entry(best_community).or_insert(0.0) += node_degree;
256
257 let edges_to_current = neighbor_communities
259 .get(¤t_community)
260 .copied()
261 .unwrap_or(0.0);
262 if let Some(e) = community_internal_edges.get_mut(¤t_community) {
263 *e -= edges_to_current;
264 }
265 let edges_to_best = neighbor_communities
266 .get(&best_community)
267 .copied()
268 .unwrap_or(0.0);
269 *community_internal_edges
270 .entry(best_community)
271 .or_insert(0.0) += edges_to_best;
272
273 communities[node] = best_community;
274 improved = true;
275 total_gain += best_gain;
276 }
277 }
278
279 if total_gain < min_modularity_gain {
281 break;
282 }
283 }
284
285 let mut community_map: HashMap<u64, u64> = HashMap::new();
287 let mut next_id = 0u64;
288
289 for c in &mut communities {
290 let new_id = *community_map.entry(*c).or_insert_with(|| {
291 let id = next_id;
292 next_id += 1;
293 id
294 });
295 *c = new_id;
296 }
297
298 let modularity = ModularityScore::compute(graph, &communities);
300
301 CommunityResult {
302 assignments: communities,
303 num_communities: next_id as usize,
304 modularity,
305 }
306 }
307}
308
309impl Default for LouvainCommunity {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl GpuKernel for LouvainCommunity {
316 fn metadata(&self) -> &KernelMetadata {
317 &self.metadata
318 }
319}
320
321#[derive(Debug, Clone)]
330pub struct LabelPropagation {
331 metadata: KernelMetadata,
332}
333
334impl LabelPropagation {
335 #[must_use]
337 pub fn new() -> Self {
338 Self {
339 metadata: KernelMetadata::batch("graph/label-propagation", Domain::GraphAnalytics)
340 .with_description("Label propagation community detection")
341 .with_throughput(100_000)
342 .with_latency_us(10.0),
343 }
344 }
345
346 pub fn compute(graph: &CsrGraph, max_iterations: u32) -> CommunityResult {
352 let n = graph.num_nodes;
353 if n == 0 {
354 return CommunityResult {
355 assignments: Vec::new(),
356 num_communities: 0,
357 modularity: 0.0,
358 };
359 }
360
361 let mut labels: Vec<u64> = (0..n).map(|i| i as u64).collect();
363
364 for _ in 0..max_iterations {
365 let mut changed = false;
366
367 for node in 0..n {
369 let mut label_counts: HashMap<u64, usize> = HashMap::new();
371
372 for &neighbor in graph.neighbors(node as u64) {
373 *label_counts.entry(labels[neighbor as usize]).or_insert(0) += 1;
374 }
375
376 if let Some((&best_label, _)) = label_counts.iter().max_by_key(|(_, count)| *count)
378 {
379 if labels[node] != best_label {
380 labels[node] = best_label;
381 changed = true;
382 }
383 }
384 }
385
386 if !changed {
387 break;
388 }
389 }
390
391 let mut label_map: HashMap<u64, u64> = HashMap::new();
393 let mut next_id = 0u64;
394
395 for label in &mut labels {
396 let new_id = *label_map.entry(*label).or_insert_with(|| {
397 let id = next_id;
398 next_id += 1;
399 id
400 });
401 *label = new_id;
402 }
403
404 let modularity = ModularityScore::compute(graph, &labels);
405
406 CommunityResult {
407 assignments: labels,
408 num_communities: next_id as usize,
409 modularity,
410 }
411 }
412}
413
414impl Default for LabelPropagation {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420impl GpuKernel for LabelPropagation {
421 fn metadata(&self) -> &KernelMetadata {
422 &self.metadata
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn create_two_cliques() -> CsrGraph {
431 CsrGraph::from_edges(
436 6,
437 &[
438 (0, 1),
440 (1, 0),
441 (0, 2),
442 (2, 0),
443 (1, 2),
444 (2, 1),
445 (3, 4),
447 (4, 3),
448 (3, 5),
449 (5, 3),
450 (4, 5),
451 (5, 4),
452 (2, 3),
454 (3, 2),
455 ],
456 )
457 }
458
459 #[test]
460 fn test_modularity_score_metadata() {
461 let kernel = ModularityScore::new();
462 assert_eq!(kernel.metadata().id, "graph/modularity-score");
463 assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
464 }
465
466 #[test]
467 fn test_modularity_perfect_partition() {
468 let graph = create_two_cliques();
469
470 let communities = vec![0, 0, 0, 1, 1, 1];
472 let q = ModularityScore::compute(&graph, &communities);
473
474 assert!(q > 0.0, "Expected positive modularity, got {}", q);
476 }
477
478 #[test]
479 fn test_modularity_single_community() {
480 let graph = create_two_cliques();
481
482 let communities = vec![0, 0, 0, 0, 0, 0];
484 let q_single = ModularityScore::compute(&graph, &communities);
485
486 let communities_perfect = vec![0, 0, 0, 1, 1, 1];
488 let q_perfect = ModularityScore::compute(&graph, &communities_perfect);
489
490 assert!(
493 q_single.is_finite(),
494 "Single community modularity should be finite"
495 );
496 assert!(
497 q_perfect.is_finite(),
498 "Perfect partition modularity should be finite"
499 );
500 assert!(
501 q_perfect > 0.0,
502 "Perfect partition should have positive modularity"
503 );
504 }
505
506 #[test]
507 fn test_louvain_finds_communities() {
508 let graph = create_two_cliques();
509 let result = LouvainCommunity::compute(&graph, 100, 1e-6);
510
511 assert_eq!(
513 result.num_communities, 2,
514 "Expected 2 communities, got {}",
515 result.num_communities
516 );
517
518 assert_eq!(result.assignments[0], result.assignments[1]);
520 assert_eq!(result.assignments[1], result.assignments[2]);
521 assert_eq!(result.assignments[3], result.assignments[4]);
522 assert_eq!(result.assignments[4], result.assignments[5]);
523
524 assert_ne!(result.assignments[0], result.assignments[3]);
526
527 assert!(result.modularity > 0.0);
529 }
530
531 #[test]
532 fn test_label_propagation_finds_communities() {
533 let graph = create_two_cliques();
534 let result = LabelPropagation::compute(&graph, 100);
535
536 assert!(
541 result.num_communities >= 1 && result.num_communities <= 2,
542 "Expected 1-2 communities, got {}",
543 result.num_communities
544 );
545
546 assert_eq!(result.assignments[0], result.assignments[1]);
548 assert_eq!(result.assignments[1], result.assignments[2]);
549 assert_eq!(result.assignments[3], result.assignments[4]);
550 assert_eq!(result.assignments[4], result.assignments[5]);
551
552 assert!(result.modularity >= 0.0);
554 }
555}