Skip to main content

scirs2_graph/dynamic/
snapshot.rs

1//! Snapshot-based temporal graph representation.
2
3use std::collections::HashMap;
4
5/// A single temporal snapshot of a graph.
6#[derive(Debug, Clone)]
7pub struct GraphSnapshot {
8    /// Timestamp for this snapshot.
9    pub time: f64,
10    /// Nodes present at this time.
11    pub nodes: Vec<usize>,
12    /// Edges at this time: (src, dst, weight).
13    pub edges: Vec<(usize, usize, f64)>,
14    /// Per-node attribute maps.
15    pub node_attributes: HashMap<usize, HashMap<String, f64>>,
16}
17
18impl GraphSnapshot {
19    /// Create a new empty snapshot at the given time.
20    pub fn new(time: f64) -> Self {
21        Self {
22            time,
23            nodes: Vec::new(),
24            edges: Vec::new(),
25            node_attributes: HashMap::new(),
26        }
27    }
28
29    /// Add a node, ignoring duplicates.
30    pub fn add_node(&mut self, node: usize) {
31        if !self.nodes.contains(&node) {
32            self.nodes.push(node);
33        }
34    }
35
36    /// Add an edge, adding both endpoints if absent.
37    pub fn add_edge(&mut self, src: usize, dst: usize, weight: f64) {
38        self.add_node(src);
39        self.add_node(dst);
40        self.edges.push((src, dst, weight));
41    }
42
43    /// Degree of a node (undirected count).
44    pub fn degree(&self, node: usize) -> usize {
45        self.edges
46            .iter()
47            .filter(|(s, d, _)| *s == node || *d == node)
48            .count()
49    }
50
51    /// Density of the snapshot (directed normalisation: n*(n-1)).
52    pub fn density(&self) -> f64 {
53        let n = self.nodes.len();
54        if n < 2 {
55            return 0.0;
56        }
57        self.edges.len() as f64 / (n * (n - 1)) as f64
58    }
59
60    /// Number of nodes.
61    pub fn n_nodes(&self) -> usize {
62        self.nodes.len()
63    }
64
65    /// Number of edges.
66    pub fn n_edges(&self) -> usize {
67        self.edges.len()
68    }
69}
70
71/// Sequence of graph snapshots forming a temporal graph.
72#[derive(Debug, Clone)]
73pub struct SnapshotGraph {
74    /// Ordered list of snapshots.
75    pub snapshots: Vec<GraphSnapshot>,
76    /// Optional time-window width used when building snapshots.
77    pub time_window: Option<f64>,
78}
79
80impl SnapshotGraph {
81    /// Create an empty snapshot graph.
82    pub fn new() -> Self {
83        Self {
84            snapshots: Vec::new(),
85            time_window: None,
86        }
87    }
88
89    /// Attach a time-window parameter.
90    pub fn with_time_window(mut self, window: f64) -> Self {
91        self.time_window = Some(window);
92        self
93    }
94
95    /// Insert a snapshot, maintaining chronological order.
96    pub fn add_snapshot(&mut self, snapshot: GraphSnapshot) {
97        self.snapshots.push(snapshot);
98        self.snapshots.sort_by(|a, b| {
99            a.time
100                .partial_cmp(&b.time)
101                .unwrap_or(std::cmp::Ordering::Equal)
102        });
103    }
104
105    /// Number of snapshots.
106    pub fn n_snapshots(&self) -> usize {
107        self.snapshots.len()
108    }
109
110    /// Get the snapshot whose timestamp is closest to `t`.
111    pub fn snapshot_at(&self, t: f64) -> Option<&GraphSnapshot> {
112        self.snapshots.iter().min_by(|a, b| {
113            (a.time - t)
114                .abs()
115                .partial_cmp(&(b.time - t).abs())
116                .unwrap_or(std::cmp::Ordering::Equal)
117        })
118    }
119
120    /// All unique node identifiers across all snapshots.
121    pub fn all_nodes(&self) -> Vec<usize> {
122        let mut nodes: Vec<usize> = self
123            .snapshots
124            .iter()
125            .flat_map(|s| s.nodes.iter().cloned())
126            .collect();
127        nodes.sort_unstable();
128        nodes.dedup();
129        nodes
130    }
131
132    /// Temporal degree sequence: (time, degree) for every snapshot.
133    pub fn temporal_degree(&self, node: usize) -> Vec<(f64, usize)> {
134        self.snapshots
135            .iter()
136            .map(|s| (s.time, s.degree(node)))
137            .collect()
138    }
139
140    /// Burstiness coefficient B = (σ-μ)/(σ+μ) for inter-event times of edge (src,dst).
141    ///
142    /// Returns 0.0 when there are fewer than two activations.
143    pub fn burstiness(&self, src: usize, dst: usize) -> f64 {
144        let event_times: Vec<f64> = self
145            .snapshots
146            .iter()
147            .filter(|s| {
148                s.edges
149                    .iter()
150                    .any(|(s_, d_, _)| (*s_ == src && *d_ == dst) || (*s_ == dst && *d_ == src))
151            })
152            .map(|s| s.time)
153            .collect();
154
155        if event_times.len() < 2 {
156            return 0.0;
157        }
158
159        let inter_events: Vec<f64> = event_times.windows(2).map(|w| w[1] - w[0]).collect();
160        let n = inter_events.len() as f64;
161        let mu = inter_events.iter().sum::<f64>() / n;
162        let sigma = (inter_events.iter().map(|x| (x - mu).powi(2)).sum::<f64>() / n).sqrt();
163
164        if sigma + mu < 1e-10 {
165            0.0
166        } else {
167            (sigma - mu) / (sigma + mu)
168        }
169    }
170
171    /// Graph-level temporal autocorrelation (Jaccard similarity of edge sets at lag `lag`).
172    pub fn temporal_correlation(&self, lag: usize) -> f64 {
173        if self.snapshots.len() <= lag {
174            return 0.0;
175        }
176
177        let n = self.snapshots.len() - lag;
178        let mut similarity_sum = 0.0;
179
180        for i in 0..n {
181            let s1 = &self.snapshots[i];
182            let s2 = &self.snapshots[i + lag];
183
184            let e1: std::collections::HashSet<(usize, usize)> = s1
185                .edges
186                .iter()
187                .map(|(a, b, _)| (*a.min(b), *a.max(b)))
188                .collect();
189            let e2: std::collections::HashSet<(usize, usize)> = s2
190                .edges
191                .iter()
192                .map(|(a, b, _)| (*a.min(b), *a.max(b)))
193                .collect();
194
195            let intersection = e1.intersection(&e2).count();
196            let union_size = e1.union(&e2).count();
197
198            similarity_sum += if union_size > 0 {
199                intersection as f64 / union_size as f64
200            } else {
201                0.0
202            };
203        }
204
205        similarity_sum / n as f64
206    }
207}
208
209impl Default for SnapshotGraph {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_snapshot_density() {
221        let mut s = GraphSnapshot::new(1.0);
222        s.add_edge(0, 1, 1.0);
223        s.add_edge(0, 2, 1.0);
224        // 3 nodes, 2 edges => density = 2 / (3*2) ≈ 0.333
225        let d = s.density();
226        assert!((d - 1.0 / 3.0).abs() < 1e-9, "density={d}");
227    }
228
229    #[test]
230    fn test_temporal_correlation_identical() {
231        let mut sg = SnapshotGraph::new();
232        for t in 0..4 {
233            let mut snap = GraphSnapshot::new(t as f64);
234            snap.add_edge(0, 1, 1.0);
235            snap.add_edge(1, 2, 1.0);
236            sg.add_snapshot(snap);
237        }
238        // All snapshots have the same edges => correlation at any lag should be 1.0
239        let corr = sg.temporal_correlation(1);
240        assert!((corr - 1.0).abs() < 1e-9, "corr={corr}");
241    }
242
243    #[test]
244    fn test_burstiness_regular() {
245        // Regular inter-event times → σ = 0 → B = -1
246        let mut sg = SnapshotGraph::new();
247        for t in 0..5 {
248            let mut snap = GraphSnapshot::new(t as f64);
249            snap.add_edge(0, 1, 1.0);
250            sg.add_snapshot(snap);
251        }
252        let b = sg.burstiness(0, 1);
253        assert!(b < -0.5, "b={b}");
254    }
255
256    #[test]
257    fn test_all_nodes() {
258        let mut sg = SnapshotGraph::new();
259        let mut s1 = GraphSnapshot::new(0.0);
260        s1.add_edge(0, 1, 1.0);
261        let mut s2 = GraphSnapshot::new(1.0);
262        s2.add_edge(1, 2, 1.0);
263        sg.add_snapshot(s1);
264        sg.add_snapshot(s2);
265        let nodes = sg.all_nodes();
266        assert_eq!(nodes, vec![0, 1, 2]);
267    }
268}