1use super::graph::TemporalGraph;
28use std::collections::HashMap;
29
30#[derive(Debug, Clone)]
40pub struct DynamicCommunity {
41 pub node_memberships: Vec<Vec<usize>>,
43 pub n_snapshots: usize,
45 pub n_nodes: usize,
47 pub community_counts: Vec<usize>,
49}
50
51impl DynamicCommunity {
52 pub fn new(node_memberships: Vec<Vec<usize>>) -> Self {
54 let n_snapshots = node_memberships.len();
55 let n_nodes = node_memberships.first().map(|v| v.len()).unwrap_or(0);
56 let community_counts = node_memberships
57 .iter()
58 .map(|snap| snap.iter().copied().max().map(|m| m + 1).unwrap_or(0))
59 .collect();
60 DynamicCommunity {
61 node_memberships,
62 n_snapshots,
63 n_nodes,
64 community_counts,
65 }
66 }
67
68 pub fn membership(&self, t: usize, v: usize) -> Option<usize> {
70 self.node_memberships
71 .get(t)
72 .and_then(|snap| snap.get(v))
73 .copied()
74 }
75
76 pub fn temporal_stability(&self) -> f64 {
79 if self.n_snapshots < 2 {
80 return 1.0;
81 }
82
83 let mut total = 0.0;
84 let count = (self.n_snapshots - 1) as f64;
85 for t in 0..(self.n_snapshots - 1) {
86 total +=
87 nmi_between_snapshots(&self.node_memberships[t], &self.node_memberships[t + 1]);
88 }
89 total / count
90 }
91}
92
93fn nmi_between_snapshots(labels_a: &[usize], labels_b: &[usize]) -> f64 {
95 let n = labels_a.len().min(labels_b.len());
96 if n == 0 {
97 return 1.0;
98 }
99
100 let mut joint: HashMap<(usize, usize), usize> = HashMap::new();
101 let mut freq_a: HashMap<usize, usize> = HashMap::new();
102 let mut freq_b: HashMap<usize, usize> = HashMap::new();
103
104 for i in 0..n {
105 let a = labels_a[i];
106 let b = labels_b[i];
107 *joint.entry((a, b)).or_insert(0) += 1;
108 *freq_a.entry(a).or_insert(0) += 1;
109 *freq_b.entry(b).or_insert(0) += 1;
110 }
111
112 let n_f = n as f64;
113 let mut h_ab = 0.0;
114 let mut h_a = 0.0;
115 let mut h_b = 0.0;
116
117 for &cnt in joint.values() {
118 let p = cnt as f64 / n_f;
119 if p > 0.0 {
120 h_ab -= p * p.ln();
121 }
122 }
123 for &cnt in freq_a.values() {
124 let p = cnt as f64 / n_f;
125 if p > 0.0 {
126 h_a -= p * p.ln();
127 }
128 }
129 for &cnt in freq_b.values() {
130 let p = cnt as f64 / n_f;
131 if p > 0.0 {
132 h_b -= p * p.ln();
133 }
134 }
135
136 let mi = h_a + h_b - h_ab;
137 let denom = (h_a + h_b) / 2.0;
138 if denom <= 0.0 {
139 1.0
140 } else {
141 mi / denom
142 }
143}
144
145pub fn evolutionary_clustering(
166 tg: &mut TemporalGraph,
167 n_snapshots: usize,
168 alpha: f64,
169) -> DynamicCommunity {
170 let n = tg.nodes;
171 if n == 0 || n_snapshots == 0 {
172 return DynamicCommunity::new(vec![vec![0usize; n]; n_snapshots.max(1)]);
173 }
174
175 tg.ensure_sorted();
176 let alpha = alpha.clamp(0.0, 1.0);
177
178 let (t_min, t_max) = if tg.edges.is_empty() {
179 (0.0, 1.0)
180 } else {
181 (
182 tg.edges.first().map(|e| e.timestamp).unwrap_or(0.0),
183 tg.edges.last().map(|e| e.timestamp).unwrap_or(1.0),
184 )
185 };
186
187 let window_width = if (t_max - t_min).abs() < 1e-12 {
188 1.0
189 } else {
190 (t_max - t_min) / n_snapshots as f64
191 };
192
193 let mut all_memberships: Vec<Vec<usize>> = Vec::with_capacity(n_snapshots);
194 let mut prev_memberships: Option<Vec<usize>> = None;
195
196 for snap_idx in 0..n_snapshots {
197 let t_start = t_min + snap_idx as f64 * window_width;
198 let t_end = if snap_idx + 1 == n_snapshots {
199 t_max + 1.0 } else {
201 t_min + (snap_idx + 1) as f64 * window_width
202 };
203
204 let adj = build_snapshot_adjacency(tg, n, t_start, t_end);
206
207 let memberships =
209 label_propagation_with_smoothness(&adj, n, prev_memberships.as_deref(), alpha);
210
211 prev_memberships = Some(memberships.clone());
212 all_memberships.push(memberships);
213 }
214
215 DynamicCommunity::new(all_memberships)
216}
217
218fn build_snapshot_adjacency(
220 tg: &TemporalGraph,
221 n: usize,
222 t_start: f64,
223 t_end: f64,
224) -> Vec<Vec<(usize, f64)>> {
225 let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
226 let mut weight_map: HashMap<(usize, usize), f64> = HashMap::new();
228
229 for e in &tg.edges {
230 if e.timestamp < t_start || e.timestamp >= t_end {
231 continue;
232 }
233 let key = (e.source.min(e.target), e.source.max(e.target));
234 *weight_map.entry(key).or_insert(0.0) += e.weight;
235 }
236
237 for ((u, v), w) in weight_map {
238 adj[u].push((v, w));
239 adj[v].push((u, w));
240 }
241
242 adj
243}
244
245fn label_propagation_with_smoothness(
252 adj: &[Vec<(usize, f64)>],
253 n: usize,
254 prev: Option<&[usize]>,
255 alpha: f64,
256) -> Vec<usize> {
257 let mut labels: Vec<usize> = if let Some(p) = prev {
259 p.to_vec()
260 } else {
261 (0..n).collect()
262 };
263
264 let max_iter = 20usize;
265 let mut changed = true;
266 let mut iter = 0;
267
268 while changed && iter < max_iter {
269 changed = false;
270 let order: Vec<usize> = (0..n).collect();
272
273 for &v in &order {
274 let mut vote: HashMap<usize, f64> = HashMap::new();
276
277 for &(nbr, w) in &adj[v] {
278 *vote.entry(labels[nbr]).or_insert(0.0) += w;
279 }
280
281 if vote.is_empty() {
282 continue;
283 }
284
285 if let Some(p) = prev {
287 if v < p.len() {
288 *vote.entry(p[v]).or_insert(0.0) += alpha * 1.0;
289 }
290 }
291
292 let best_label = vote
294 .iter()
295 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
296 .map(|(&l, _)| l);
297
298 if let Some(best) = best_label {
299 if best != labels[v] {
300 labels[v] = best;
301 changed = true;
302 }
303 }
304 }
305
306 iter += 1;
307 }
308
309 compact_labels(&mut labels);
311 labels
312}
313
314fn compact_labels(labels: &mut [usize]) {
316 let mut mapping: HashMap<usize, usize> = HashMap::new();
317 let mut next_id = 0usize;
318 for l in labels.iter_mut() {
319 let id = mapping.entry(*l).or_insert_with(|| {
320 let id = next_id;
321 next_id += 1;
322 id
323 });
324 *l = *id;
325 }
326}
327
328#[allow(dead_code)]
335fn compute_modularity(adj: &[Vec<(usize, f64)>], labels: &[usize]) -> f64 {
336 let n = adj.len();
337 let mut total_weight = 0.0;
338 let mut degree: Vec<f64> = vec![0.0; n];
339
340 for (u, neighbors) in adj.iter().enumerate() {
341 for &(_, w) in neighbors {
342 degree[u] += w;
343 total_weight += w;
344 }
345 }
346 total_weight /= 2.0; if total_weight <= 0.0 {
349 return 0.0;
350 }
351
352 let mut q = 0.0;
353 for u in 0..n {
354 for &(v, w) in &adj[u] {
355 if labels[u] == labels[v] {
356 q += w - degree[u] * degree[v] / (2.0 * total_weight);
357 }
358 }
359 }
360
361 q / (2.0 * total_weight)
362}
363
364#[cfg(test)]
369mod tests {
370 use super::super::graph::TemporalEdge;
371 use super::*;
372
373 fn make_two_community_graph() -> TemporalGraph {
376 let mut tg = TemporalGraph::new(6);
377 for t in 1..=3 {
379 tg.add_edge(TemporalEdge::with_weight(0, 1, t as f64, 2.0));
380 tg.add_edge(TemporalEdge::with_weight(1, 2, t as f64 + 0.1, 2.0));
381 tg.add_edge(TemporalEdge::with_weight(0, 2, t as f64 + 0.2, 2.0));
382 }
383 for t in 4..=6 {
385 tg.add_edge(TemporalEdge::with_weight(3, 4, t as f64, 2.0));
386 tg.add_edge(TemporalEdge::with_weight(4, 5, t as f64 + 0.1, 2.0));
387 tg.add_edge(TemporalEdge::with_weight(3, 5, t as f64 + 0.2, 2.0));
388 }
389 tg.add_edge(TemporalEdge::with_weight(2, 3, 7.0, 0.1));
391 tg
392 }
393
394 #[test]
395 fn test_evolutionary_clustering_basic() {
396 let mut tg = make_two_community_graph();
397 let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
398
399 assert_eq!(dyn_com.n_snapshots, 3);
400 assert_eq!(dyn_com.n_nodes, 6);
401 assert_eq!(dyn_com.node_memberships.len(), 3);
402
403 for snap in &dyn_com.node_memberships {
405 assert_eq!(snap.len(), 6);
406 }
407 }
408
409 #[test]
410 fn test_evolutionary_clustering_stability() {
411 let mut tg = make_two_community_graph();
412 let dyn_com = evolutionary_clustering(&mut tg, 4, 0.9);
414 let stability = dyn_com.temporal_stability();
415 assert!(
417 stability >= 0.0 && stability <= 1.0,
418 "stability should be in [0,1], got {stability}"
419 );
420 }
421
422 #[test]
423 fn test_evolutionary_clustering_empty_graph() {
424 let mut tg = TemporalGraph::new(5);
425 let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
426 assert_eq!(dyn_com.n_snapshots, 3);
427 assert_eq!(dyn_com.n_nodes, 5);
428 }
429
430 #[test]
431 fn test_dynamic_community_membership_access() {
432 let mut tg = make_two_community_graph();
433 let dyn_com = evolutionary_clustering(&mut tg, 2, 0.5);
434 for t in 0..dyn_com.n_snapshots {
436 for v in 0..dyn_com.n_nodes {
437 let m = dyn_com.membership(t, v);
438 assert!(m.is_some(), "membership({t},{v}) should be Some");
439 }
440 }
441 }
442
443 #[test]
444 fn test_community_counts_nonnegative() {
445 let mut tg = make_two_community_graph();
446 let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
447 for &cnt in &dyn_com.community_counts {
448 assert!(cnt >= 1, "each snapshot should have at least 1 community");
449 }
450 }
451
452 #[test]
453 fn test_compact_labels() {
454 let mut labels = vec![5, 10, 5, 20, 10, 20];
455 compact_labels(&mut labels);
456 let max_label = labels.iter().copied().max().unwrap_or(0);
458 assert!(
459 max_label <= 2,
460 "compact labels should be 0-indexed, max={max_label}"
461 );
462 }
463
464 #[test]
465 fn test_temporal_stability_single_snapshot() {
466 let dc = DynamicCommunity::new(vec![vec![0, 1, 0, 1]]);
467 assert_eq!(dc.temporal_stability(), 1.0);
468 }
469}