use super::graph::TemporalGraph;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DynamicCommunity {
pub node_memberships: Vec<Vec<usize>>,
pub n_snapshots: usize,
pub n_nodes: usize,
pub community_counts: Vec<usize>,
}
impl DynamicCommunity {
pub fn new(node_memberships: Vec<Vec<usize>>) -> Self {
let n_snapshots = node_memberships.len();
let n_nodes = node_memberships.first().map(|v| v.len()).unwrap_or(0);
let community_counts = node_memberships
.iter()
.map(|snap| snap.iter().copied().max().map(|m| m + 1).unwrap_or(0))
.collect();
DynamicCommunity {
node_memberships,
n_snapshots,
n_nodes,
community_counts,
}
}
pub fn membership(&self, t: usize, v: usize) -> Option<usize> {
self.node_memberships
.get(t)
.and_then(|snap| snap.get(v))
.copied()
}
pub fn temporal_stability(&self) -> f64 {
if self.n_snapshots < 2 {
return 1.0;
}
let mut total = 0.0;
let count = (self.n_snapshots - 1) as f64;
for t in 0..(self.n_snapshots - 1) {
total +=
nmi_between_snapshots(&self.node_memberships[t], &self.node_memberships[t + 1]);
}
total / count
}
}
fn nmi_between_snapshots(labels_a: &[usize], labels_b: &[usize]) -> f64 {
let n = labels_a.len().min(labels_b.len());
if n == 0 {
return 1.0;
}
let mut joint: HashMap<(usize, usize), usize> = HashMap::new();
let mut freq_a: HashMap<usize, usize> = HashMap::new();
let mut freq_b: HashMap<usize, usize> = HashMap::new();
for i in 0..n {
let a = labels_a[i];
let b = labels_b[i];
*joint.entry((a, b)).or_insert(0) += 1;
*freq_a.entry(a).or_insert(0) += 1;
*freq_b.entry(b).or_insert(0) += 1;
}
let n_f = n as f64;
let mut h_ab = 0.0;
let mut h_a = 0.0;
let mut h_b = 0.0;
for &cnt in joint.values() {
let p = cnt as f64 / n_f;
if p > 0.0 {
h_ab -= p * p.ln();
}
}
for &cnt in freq_a.values() {
let p = cnt as f64 / n_f;
if p > 0.0 {
h_a -= p * p.ln();
}
}
for &cnt in freq_b.values() {
let p = cnt as f64 / n_f;
if p > 0.0 {
h_b -= p * p.ln();
}
}
let mi = h_a + h_b - h_ab;
let denom = (h_a + h_b) / 2.0;
if denom <= 0.0 {
1.0
} else {
mi / denom
}
}
pub fn evolutionary_clustering(
tg: &mut TemporalGraph,
n_snapshots: usize,
alpha: f64,
) -> DynamicCommunity {
let n = tg.nodes;
if n == 0 || n_snapshots == 0 {
return DynamicCommunity::new(vec![vec![0usize; n]; n_snapshots.max(1)]);
}
tg.ensure_sorted();
let alpha = alpha.clamp(0.0, 1.0);
let (t_min, t_max) = if tg.edges.is_empty() {
(0.0, 1.0)
} else {
(
tg.edges.first().map(|e| e.timestamp).unwrap_or(0.0),
tg.edges.last().map(|e| e.timestamp).unwrap_or(1.0),
)
};
let window_width = if (t_max - t_min).abs() < 1e-12 {
1.0
} else {
(t_max - t_min) / n_snapshots as f64
};
let mut all_memberships: Vec<Vec<usize>> = Vec::with_capacity(n_snapshots);
let mut prev_memberships: Option<Vec<usize>> = None;
for snap_idx in 0..n_snapshots {
let t_start = t_min + snap_idx as f64 * window_width;
let t_end = if snap_idx + 1 == n_snapshots {
t_max + 1.0 } else {
t_min + (snap_idx + 1) as f64 * window_width
};
let adj = build_snapshot_adjacency(tg, n, t_start, t_end);
let memberships =
label_propagation_with_smoothness(&adj, n, prev_memberships.as_deref(), alpha);
prev_memberships = Some(memberships.clone());
all_memberships.push(memberships);
}
DynamicCommunity::new(all_memberships)
}
fn build_snapshot_adjacency(
tg: &TemporalGraph,
n: usize,
t_start: f64,
t_end: f64,
) -> Vec<Vec<(usize, f64)>> {
let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
let mut weight_map: HashMap<(usize, usize), f64> = HashMap::new();
for e in &tg.edges {
if e.timestamp < t_start || e.timestamp >= t_end {
continue;
}
let key = (e.source.min(e.target), e.source.max(e.target));
*weight_map.entry(key).or_insert(0.0) += e.weight;
}
for ((u, v), w) in weight_map {
adj[u].push((v, w));
adj[v].push((u, w));
}
adj
}
fn label_propagation_with_smoothness(
adj: &[Vec<(usize, f64)>],
n: usize,
prev: Option<&[usize]>,
alpha: f64,
) -> Vec<usize> {
let mut labels: Vec<usize> = if let Some(p) = prev {
p.to_vec()
} else {
(0..n).collect()
};
let max_iter = 20usize;
let mut changed = true;
let mut iter = 0;
while changed && iter < max_iter {
changed = false;
let order: Vec<usize> = (0..n).collect();
for &v in &order {
let mut vote: HashMap<usize, f64> = HashMap::new();
for &(nbr, w) in &adj[v] {
*vote.entry(labels[nbr]).or_insert(0.0) += w;
}
if vote.is_empty() {
continue;
}
if let Some(p) = prev {
if v < p.len() {
*vote.entry(p[v]).or_insert(0.0) += alpha * 1.0;
}
}
let best_label = vote
.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(&l, _)| l);
if let Some(best) = best_label {
if best != labels[v] {
labels[v] = best;
changed = true;
}
}
}
iter += 1;
}
compact_labels(&mut labels);
labels
}
fn compact_labels(labels: &mut [usize]) {
let mut mapping: HashMap<usize, usize> = HashMap::new();
let mut next_id = 0usize;
for l in labels.iter_mut() {
let id = mapping.entry(*l).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
*l = *id;
}
}
#[allow(dead_code)]
fn compute_modularity(adj: &[Vec<(usize, f64)>], labels: &[usize]) -> f64 {
let n = adj.len();
let mut total_weight = 0.0;
let mut degree: Vec<f64> = vec![0.0; n];
for (u, neighbors) in adj.iter().enumerate() {
for &(_, w) in neighbors {
degree[u] += w;
total_weight += w;
}
}
total_weight /= 2.0;
if total_weight <= 0.0 {
return 0.0;
}
let mut q = 0.0;
for u in 0..n {
for &(v, w) in &adj[u] {
if labels[u] == labels[v] {
q += w - degree[u] * degree[v] / (2.0 * total_weight);
}
}
}
q / (2.0 * total_weight)
}
#[cfg(test)]
mod tests {
use super::super::graph::TemporalEdge;
use super::*;
fn make_two_community_graph() -> TemporalGraph {
let mut tg = TemporalGraph::new(6);
for t in 1..=3 {
tg.add_edge(TemporalEdge::with_weight(0, 1, t as f64, 2.0));
tg.add_edge(TemporalEdge::with_weight(1, 2, t as f64 + 0.1, 2.0));
tg.add_edge(TemporalEdge::with_weight(0, 2, t as f64 + 0.2, 2.0));
}
for t in 4..=6 {
tg.add_edge(TemporalEdge::with_weight(3, 4, t as f64, 2.0));
tg.add_edge(TemporalEdge::with_weight(4, 5, t as f64 + 0.1, 2.0));
tg.add_edge(TemporalEdge::with_weight(3, 5, t as f64 + 0.2, 2.0));
}
tg.add_edge(TemporalEdge::with_weight(2, 3, 7.0, 0.1));
tg
}
#[test]
fn test_evolutionary_clustering_basic() {
let mut tg = make_two_community_graph();
let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
assert_eq!(dyn_com.n_snapshots, 3);
assert_eq!(dyn_com.n_nodes, 6);
assert_eq!(dyn_com.node_memberships.len(), 3);
for snap in &dyn_com.node_memberships {
assert_eq!(snap.len(), 6);
}
}
#[test]
fn test_evolutionary_clustering_stability() {
let mut tg = make_two_community_graph();
let dyn_com = evolutionary_clustering(&mut tg, 4, 0.9);
let stability = dyn_com.temporal_stability();
assert!(
(0.0..=1.0).contains(&stability),
"stability should be in [0,1], got {stability}"
);
}
#[test]
fn test_evolutionary_clustering_empty_graph() {
let mut tg = TemporalGraph::new(5);
let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
assert_eq!(dyn_com.n_snapshots, 3);
assert_eq!(dyn_com.n_nodes, 5);
}
#[test]
fn test_dynamic_community_membership_access() {
let mut tg = make_two_community_graph();
let dyn_com = evolutionary_clustering(&mut tg, 2, 0.5);
for t in 0..dyn_com.n_snapshots {
for v in 0..dyn_com.n_nodes {
let m = dyn_com.membership(t, v);
assert!(m.is_some(), "membership({t},{v}) should be Some");
}
}
}
#[test]
fn test_community_counts_nonnegative() {
let mut tg = make_two_community_graph();
let dyn_com = evolutionary_clustering(&mut tg, 3, 0.5);
for &cnt in &dyn_com.community_counts {
assert!(cnt >= 1, "each snapshot should have at least 1 community");
}
}
#[test]
fn test_compact_labels() {
let mut labels = vec![5, 10, 5, 20, 10, 20];
compact_labels(&mut labels);
let max_label = labels.iter().copied().max().unwrap_or(0);
assert!(
max_label <= 2,
"compact labels should be 0-indexed, max={max_label}"
);
}
#[test]
fn test_temporal_stability_single_snapshot() {
let dc = DynamicCommunity::new(vec![vec![0, 1, 0, 1]]);
assert_eq!(dc.temporal_stability(), 1.0);
}
}