1use std::collections::HashMap;
18
19use crate::error::{GraphError, Result};
20
21fn graph_stats(
28 edges: &[(usize, usize, f64)],
29 n_nodes: usize,
30 communities: &[usize],
31) -> (f64, f64, Vec<f64>, usize) {
32 let n_comms = communities.iter().max().copied().unwrap_or(0) + 1;
33 let mut degree = vec![0.0f64; n_nodes];
34 let mut total_w = 0.0f64;
35 let mut intra_w = 0.0f64;
36
37 for &(u, v, w) in edges {
38 if u >= n_nodes || v >= n_nodes {
39 continue;
40 }
41 degree[u] += w;
42 if u != v {
43 degree[v] += w;
44 }
45 total_w += 2.0 * w;
46 if communities[u] == communities[v] {
47 intra_w += 2.0 * w;
48 }
49 }
50 (total_w, intra_w, degree, n_comms)
51}
52
53pub fn modularity(edges: &[(usize, usize, f64)], n_nodes: usize, communities: &[usize]) -> f64 {
66 if n_nodes == 0 || communities.len() != n_nodes {
67 return 0.0;
68 }
69 let (two_m, intra_w, degree, n_comms) = graph_stats(edges, n_nodes, communities);
70 if two_m == 0.0 {
71 return 0.0;
72 }
73
74 let mut comm_degree = vec![0.0f64; n_comms];
76 for i in 0..n_nodes {
77 if communities[i] < n_comms {
78 comm_degree[communities[i]] += degree[i];
79 }
80 }
81 let sq_sum: f64 = comm_degree.iter().map(|&d| d * d).sum();
82
83 (intra_w / two_m) - (sq_sum / (two_m * two_m))
84}
85
86pub fn conductance(
104 edges: &[(usize, usize, f64)],
105 n_nodes: usize,
106 community: &[usize],
107) -> Result<f64> {
108 if community.is_empty() {
109 return Ok(1.0);
110 }
111 let in_community: std::collections::HashSet<usize> = community.iter().cloned().collect();
112
113 let mut degree = vec![0.0f64; n_nodes];
114 let mut cut = 0.0f64;
115
116 for &(u, v, w) in edges {
117 if u >= n_nodes || v >= n_nodes {
118 continue;
119 }
120 degree[u] += w;
121 if u != v {
122 degree[v] += w;
123 }
124 let u_in = in_community.contains(&u);
126 let v_in = in_community.contains(&v);
127 if u_in != v_in {
128 cut += w; }
130 }
131
132 let vol_s: f64 = community
133 .iter()
134 .filter(|&&n| n < n_nodes)
135 .map(|&n| degree[n])
136 .sum();
137 let vol_total: f64 = degree.iter().sum();
138 let vol_s_bar = vol_total - vol_s;
139
140 let min_vol = vol_s.min(vol_s_bar);
141 if min_vol == 0.0 {
142 return Ok(1.0);
143 }
144 Ok(cut / min_vol)
145}
146
147pub fn coverage(edges: &[(usize, usize, f64)], n_nodes: usize, communities: &[usize]) -> f64 {
160 if edges.is_empty() || communities.len() != n_nodes {
161 return 0.0;
162 }
163 let mut total = 0.0f64;
164 let mut intra = 0.0f64;
165 for &(u, v, w) in edges {
166 if u >= n_nodes || v >= n_nodes {
167 continue;
168 }
169 total += w;
170 if communities[u] == communities[v] {
171 intra += w;
172 }
173 }
174 if total == 0.0 {
175 0.0
176 } else {
177 intra / total
178 }
179}
180
181pub fn normalized_cut(edges: &[(usize, usize, f64)], n_nodes: usize, communities: &[usize]) -> f64 {
197 if communities.len() != n_nodes {
198 return 0.0;
199 }
200 let n_comms = communities.iter().max().copied().unwrap_or(0) + 1;
201
202 let mut degree = vec![0.0f64; n_nodes];
203 for &(u, v, w) in edges {
204 if u < n_nodes && v < n_nodes {
205 degree[u] += w;
206 if u != v {
207 degree[v] += w;
208 }
209 }
210 }
211
212 let mut vol_comm = vec![0.0f64; n_comms];
213 for i in 0..n_nodes {
214 if communities[i] < n_comms {
215 vol_comm[communities[i]] += degree[i];
216 }
217 }
218
219 let mut cut_comm = vec![0.0f64; n_comms];
220 for &(u, v, w) in edges {
221 if u >= n_nodes || v >= n_nodes {
222 continue;
223 }
224 if communities[u] != communities[v] {
225 cut_comm[communities[u]] += w;
226 cut_comm[communities[v]] += w;
227 }
228 }
229
230 let ncut: f64 = (0..n_comms)
231 .filter(|&c| vol_comm[c] > 0.0)
232 .map(|c| cut_comm[c] / vol_comm[c])
233 .sum();
234 ncut
235}
236
237pub fn nmi(true_labels: &[usize], pred_labels: &[usize]) -> Result<f64> {
251 let n = true_labels.len();
252 if n != pred_labels.len() {
253 return Err(GraphError::InvalidParameter {
254 param: "pred_labels".into(),
255 value: format!("len={}", pred_labels.len()),
256 expected: format!("len={n}"),
257 context: "nmi".into(),
258 });
259 }
260 if n == 0 {
261 return Ok(1.0);
262 }
263
264 let fn64 = n as f64;
265
266 let mut contingency: HashMap<(usize, usize), usize> = HashMap::new();
268 let mut true_counts: HashMap<usize, usize> = HashMap::new();
269 let mut pred_counts: HashMap<usize, usize> = HashMap::new();
270
271 for i in 0..n {
272 *contingency
273 .entry((true_labels[i], pred_labels[i]))
274 .or_insert(0) += 1;
275 *true_counts.entry(true_labels[i]).or_insert(0) += 1;
276 *pred_counts.entry(pred_labels[i]).or_insert(0) += 1;
277 }
278
279 let mi: f64 = contingency
281 .iter()
282 .map(|(&(t, p), &cnt)| {
283 let n_tp = cnt as f64;
284 let n_t = *true_counts.get(&t).unwrap_or(&1) as f64;
285 let n_p = *pred_counts.get(&p).unwrap_or(&1) as f64;
286 if n_tp > 0.0 {
287 n_tp / fn64 * (n_tp * fn64 / (n_t * n_p)).ln()
288 } else {
289 0.0
290 }
291 })
292 .sum();
293
294 let h_true: f64 = true_counts
296 .values()
297 .map(|&c| {
298 let p = c as f64 / fn64;
299 if p > 0.0 {
300 -p * p.ln()
301 } else {
302 0.0
303 }
304 })
305 .sum();
306
307 let h_pred: f64 = pred_counts
308 .values()
309 .map(|&c| {
310 let p = c as f64 / fn64;
311 if p > 0.0 {
312 -p * p.ln()
313 } else {
314 0.0
315 }
316 })
317 .sum();
318
319 let denom = h_true + h_pred;
320 if denom == 0.0 {
321 Ok(1.0)
322 } else {
323 Ok((2.0 * mi / denom).clamp(0.0, 1.0))
324 }
325}
326
327pub fn adjusted_rand_index(true_labels: &[usize], pred_labels: &[usize]) -> Result<f64> {
341 let n = true_labels.len();
342 if n != pred_labels.len() {
343 return Err(GraphError::InvalidParameter {
344 param: "pred_labels".into(),
345 value: format!("len={}", pred_labels.len()),
346 expected: format!("len={n}"),
347 context: "adjusted_rand_index".into(),
348 });
349 }
350 if n == 0 {
351 return Ok(1.0);
352 }
353
354 let n_true = true_labels.iter().max().copied().unwrap_or(0) + 1;
356 let n_pred = pred_labels.iter().max().copied().unwrap_or(0) + 1;
357 let mut contingency = vec![vec![0u64; n_pred]; n_true];
358 for i in 0..n {
359 let t = true_labels[i];
360 let p = pred_labels[i];
361 if t < n_true && p < n_pred {
362 contingency[t][p] += 1;
363 }
364 }
365
366 let a: Vec<u64> = (0..n_true).map(|i| contingency[i].iter().sum()).collect();
368 let b: Vec<u64> = (0..n_pred)
369 .map(|j| (0..n_true).map(|i| contingency[i][j]).sum())
370 .collect();
371
372 let comb2 = |x: u64| -> f64 { (x * x.saturating_sub(1)) as f64 / 2.0 };
374
375 let sum_comb_c: f64 = contingency
376 .iter()
377 .flat_map(|row| row.iter())
378 .map(|&x| comb2(x))
379 .sum();
380 let sum_comb_a: f64 = a.iter().map(|&x| comb2(x)).sum();
381 let sum_comb_b: f64 = b.iter().map(|&x| comb2(x)).sum();
382 let comb_n = comb2(n as u64);
383
384 if comb_n == 0.0 {
385 return Ok(1.0);
387 }
388
389 let expected = sum_comb_a * sum_comb_b / comb_n;
390 let max_val = (sum_comb_a + sum_comb_b) / 2.0;
391 let denom = max_val - expected;
392 if denom.abs() < 1e-15 {
393 return Ok(1.0);
395 }
396
397 Ok(((sum_comb_c - expected) / denom).clamp(-1.0, 1.0))
398}
399
400#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn two_clique_edges(k: usize) -> (Vec<(usize, usize, f64)>, usize) {
409 let n = 2 * k;
410 let mut edges = Vec::new();
411 for i in 0..k {
412 for j in (i + 1)..k {
413 edges.push((i, j, 1.0));
414 edges.push((k + i, k + j, 1.0));
415 }
416 }
417 edges.push((0, k, 0.05));
418 (edges, n)
419 }
420
421 #[test]
422 fn test_modularity_perfect() {
423 let (edges, n) = two_clique_edges(4);
424 let perfect: Vec<usize> = (0..8).map(|i| if i < 4 { 0 } else { 1 }).collect();
425 let q = modularity(&edges, n, &perfect);
426 assert!(q > 0.0, "modularity should be positive: {q}");
427 }
428
429 #[test]
430 fn test_modularity_single_community() {
431 let (edges, n) = two_clique_edges(3);
432 let single = vec![0usize; 6];
433 let q = modularity(&edges, n, &single);
434 assert!(
435 q <= 0.0 + 1e-10,
436 "single-community modularity should be ≤ 0: {q}"
437 );
438 }
439
440 #[test]
441 fn test_conductance_perfect_cut() {
442 let (edges, n) = two_clique_edges(4);
443 let community: Vec<usize> = (0..4).collect();
444 let phi = conductance(&edges, n, &community).expect("conductance");
445 assert!(phi < 1.0, "conductance should be < 1: {phi}");
447 assert!(phi >= 0.0);
448 }
449
450 #[test]
451 fn test_conductance_empty_community() {
452 let phi = conductance(&[], 4, &[]).expect("conductance empty");
453 assert_eq!(phi, 1.0);
454 }
455
456 #[test]
457 fn test_coverage_perfect_partition() {
458 let (edges, n) = two_clique_edges(4);
459 let perfect: Vec<usize> = (0..8).map(|i| if i < 4 { 0 } else { 1 }).collect();
460 let cov = coverage(&edges, n, &perfect);
461 assert!(cov > 0.9, "coverage should be high: {cov}");
463 }
464
465 #[test]
466 fn test_coverage_single_community() {
467 let (edges, n) = two_clique_edges(3);
468 let single = vec![0usize; 6];
469 let cov = coverage(&edges, n, &single);
470 assert!(
471 (cov - 1.0).abs() < 1e-9,
472 "single community coverage = 1: {cov}"
473 );
474 }
475
476 #[test]
477 fn test_normalized_cut_perfect_partition() {
478 let (edges, n) = two_clique_edges(4);
479 let perfect: Vec<usize> = (0..8).map(|i| if i < 4 { 0 } else { 1 }).collect();
480 let ncut = normalized_cut(&edges, n, &perfect);
481 assert!(ncut >= 0.0);
482 assert!(
483 ncut < 1.0,
484 "normalized cut for near-perfect partition should be small: {ncut}"
485 );
486 }
487
488 #[test]
489 fn test_nmi_perfect_agreement() {
490 let labels = vec![0, 0, 1, 1, 2, 2];
491 let nmi_val = nmi(&labels, &labels).expect("nmi perfect");
492 assert!(
493 (nmi_val - 1.0).abs() < 1e-9,
494 "NMI perfect agreement = 1: {nmi_val}"
495 );
496 }
497
498 #[test]
499 fn test_nmi_length_mismatch() {
500 assert!(nmi(&[0, 1], &[0]).is_err());
501 }
502
503 #[test]
504 fn test_nmi_empty() {
505 let v: Vec<usize> = vec![];
506 let val = nmi(&v, &v).expect("nmi empty");
507 assert_eq!(val, 1.0);
508 }
509
510 #[test]
511 fn test_ari_perfect_agreement() {
512 let labels = vec![0, 0, 0, 1, 1, 1];
513 let ari = adjusted_rand_index(&labels, &labels).expect("ari perfect");
514 assert!((ari - 1.0).abs() < 1e-9, "ARI perfect = 1: {ari}");
515 }
516
517 #[test]
518 fn test_ari_random_labels() {
519 let true_l = vec![0, 0, 1, 1, 2, 2];
521 let pred_l = vec![0, 1, 2, 0, 1, 2]; let ari = adjusted_rand_index(&true_l, &pred_l).expect("ari random");
523 assert!(
524 ari < 0.5,
525 "ARI for dissimilar partitions should be small: {ari}"
526 );
527 }
528
529 #[test]
530 fn test_ari_length_mismatch() {
531 assert!(adjusted_rand_index(&[0, 1], &[0]).is_err());
532 }
533
534 #[test]
535 fn test_ari_empty() {
536 let v: Vec<usize> = vec![];
537 let ari = adjusted_rand_index(&v, &v).expect("ari empty");
538 assert_eq!(ari, 1.0);
539 }
540}