Skip to main content

geographdb_core/algorithms/
persistence.rs

1//! Temporal persistence barcode for 4D graphs.
2//!
3//! Sweeps a temporal window across the time axis and tracks how the strongly
4//! connected component landscape evolves. The result is a persistence barcode:
5//! each component has a (birth_t, death_t) lifetime, forming the discrete
6//! analogue of 0-dimensional persistent homology with time as the filtration
7//! parameter.
8
9use crate::algorithms::four_d::{
10    strongly_connected_components_4d, GraphNode4D, SpatialRegion, TemporalWindow,
11    TraversalContext4D,
12};
13use glam::Vec3;
14
15/// One measurement point in the temporal persistence sweep.
16#[derive(Debug, Clone)]
17pub struct TemporalPersistencePoint {
18    /// Centre of the temporal window at this measurement.
19    pub t: u64,
20    /// Nodes active under the (sphere, time) constraints.
21    pub active: usize,
22    /// Number of strongly connected components.
23    pub n_components: usize,
24    /// Size of the largest SCC.
25    pub largest_size: usize,
26    /// `largest_size / active`, or 0.0 when `active == 0`.
27    pub fraction_largest: f32,
28}
29
30/// One bar in the persistence barcode.
31///
32/// Represents a connected component that was first observed at `birth` and
33/// last observed at `death` (or still alive at sweep end when `death` is
34/// `None`).
35#[derive(Debug, Clone, PartialEq)]
36pub struct TemporalBarcode {
37    /// Time slice where this component first appeared.
38    pub birth: u64,
39    /// Time slice where this component was last seen, or `None` if it
40    /// survived to the end of the sweep.
41    pub death: Option<u64>,
42    /// Largest size this component reached across its lifetime.
43    pub peak_size: usize,
44}
45
46/// Sweep the temporal window across `time_slices`, computing the SCC landscape
47/// at each step.
48///
49/// The spatial query is a sphere of `sphere_radius` centred at `sphere_center`.
50/// `time_half_width` forms the window `[t - half, t + half]` around each slice.
51pub fn temporal_persistence_sweep(
52    nodes: &[GraphNode4D],
53    sphere_center: Vec3,
54    sphere_radius: f32,
55    time_slices: &[u64],
56    time_half_width: u64,
57) -> Vec<TemporalPersistencePoint> {
58    time_slices
59        .iter()
60        .map(|&t| {
61            let ctx = TraversalContext4D {
62                spatial_region: Some(SpatialRegion::Sphere {
63                    center: sphere_center,
64                    radius: sphere_radius,
65                }),
66                time_window: Some(TemporalWindow {
67                    start: t.saturating_sub(time_half_width),
68                    end: t.saturating_add(time_half_width),
69                }),
70                ..TraversalContext4D::default()
71            };
72
73            let sccs = strongly_connected_components_4d(nodes, &ctx);
74            let active: usize = sccs.iter().map(|c| c.len()).sum();
75            let n_components = sccs.len();
76            let largest_size = sccs.iter().map(|c| c.len()).max().unwrap_or(0);
77            let fraction_largest = if active == 0 {
78                0.0
79            } else {
80                largest_size as f32 / active as f32
81            };
82
83            TemporalPersistencePoint {
84                t,
85                active,
86                n_components,
87                largest_size,
88                fraction_largest,
89            }
90        })
91        .collect()
92}
93
94/// Derive a persistence barcode from a sweep.
95///
96/// Tracks the number of components across time slices. Each time the count
97/// rises, new components are "born"; each time it falls, existing ones "die"
98/// (merge or disappear). Returns one `TemporalBarcode` per birth event.
99///
100/// The `points` must be ordered by increasing `t` (as produced by
101/// `temporal_persistence_sweep`).
102pub fn compute_temporal_barcode(points: &[TemporalPersistencePoint]) -> Vec<TemporalBarcode> {
103    // Track open bars: one entry per "live" component count slot.
104    // Each birth event opens a bar; each death event closes the oldest open bar.
105    let mut open: Vec<(u64, usize)> = Vec::new(); // (birth_t, peak_size)
106    let mut bars: Vec<TemporalBarcode> = Vec::new();
107
108    let mut prev_count = 0usize;
109
110    for pt in points {
111        let curr = pt.n_components;
112
113        // New components born this step.
114        if curr > prev_count {
115            for _ in 0..(curr - prev_count) {
116                open.push((pt.t, pt.largest_size));
117            }
118        }
119
120        // Update peak sizes for all open bars at each step.
121        for ob in &mut open {
122            ob.1 = ob.1.max(pt.largest_size);
123        }
124
125        // Components that died this step — close the most recently opened bars.
126        if curr < prev_count {
127            let died = prev_count - curr;
128            let close_from = open.len().saturating_sub(died);
129            for (birth, peak) in open.drain(close_from..) {
130                bars.push(TemporalBarcode {
131                    birth,
132                    death: Some(pt.t),
133                    peak_size: peak,
134                });
135            }
136        }
137
138        prev_count = curr;
139    }
140
141    // Remaining open bars survive to end of sweep.
142    for (birth, peak) in open {
143        bars.push(TemporalBarcode {
144            birth,
145            death: None,
146            peak_size: peak,
147        });
148    }
149
150    bars.sort_by_key(|b| b.birth);
151    bars
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::algorithms::four_d::{GraphProperties, TemporalEdge};
158
159    fn node(id: u64, x: f32, successors: Vec<TemporalEdge>) -> GraphNode4D {
160        GraphNode4D {
161            id,
162            x,
163            y: 0.0,
164            z: 0.0,
165            begin_ts: 0,
166            end_ts: 100,
167            properties: GraphProperties::new(),
168            successors,
169        }
170    }
171
172    fn edge(dst: u64) -> TemporalEdge {
173        TemporalEdge {
174            dst,
175            weight: 1.0,
176            begin_ts: 0,
177            end_ts: 100,
178        }
179    }
180
181    fn big_sphere() -> Vec3 {
182        Vec3::ZERO
183    }
184
185    // ── temporal_persistence_sweep ───────────────────────────────────────────
186
187    #[test]
188    fn test_empty_nodes_zero_components() {
189        let pts = temporal_persistence_sweep(&[], big_sphere(), 1000.0, &[50], 10);
190        assert_eq!(pts.len(), 1);
191        assert_eq!(pts[0].active, 0);
192        assert_eq!(pts[0].n_components, 0);
193    }
194
195    #[test]
196    fn test_disconnected_nodes_n_components_equals_n_active() {
197        // 4 isolated nodes → 4 SCCs (each node is its own component).
198        let nodes: Vec<GraphNode4D> = (0..4).map(|i| node(i, i as f32, vec![])).collect();
199        let pts = temporal_persistence_sweep(&nodes, big_sphere(), 1000.0, &[50], 10);
200        assert_eq!(pts[0].n_components, 4);
201        assert_eq!(pts[0].active, 4);
202        assert_eq!(pts[0].largest_size, 1);
203    }
204
205    #[test]
206    fn test_fully_connected_cycle_is_one_component() {
207        // 0 → 1 → 2 → 0 forms a single SCC.
208        let nodes = vec![
209            node(0, 0.0, vec![edge(1)]),
210            node(1, 1.0, vec![edge(2)]),
211            node(2, 2.0, vec![edge(0)]),
212        ];
213        let pts = temporal_persistence_sweep(&nodes, big_sphere(), 1000.0, &[50], 10);
214        assert_eq!(pts[0].n_components, 1);
215        assert_eq!(pts[0].largest_size, 3);
216        assert!((pts[0].fraction_largest - 1.0).abs() < 1e-6);
217    }
218
219    #[test]
220    fn test_time_filter_outside_node_validity_gives_zero() {
221        // Nodes valid in [0, 100]; window centred at t=500 → nothing active.
222        let nodes: Vec<GraphNode4D> = (0..3).map(|i| node(i, i as f32, vec![])).collect();
223        let pts = temporal_persistence_sweep(&nodes, big_sphere(), 1000.0, &[500], 10);
224        assert_eq!(pts[0].active, 0);
225        assert_eq!(pts[0].n_components, 0);
226        assert_eq!(pts[0].fraction_largest, 0.0);
227    }
228
229    // ── compute_temporal_barcode ─────────────────────────────────────────────
230
231    #[test]
232    fn test_barcode_birth_and_death() {
233        // Simulate: at t=10 one component appears, at t=50 it merges away.
234        let points = vec![
235            TemporalPersistencePoint {
236                t: 0,
237                active: 0,
238                n_components: 0,
239                largest_size: 0,
240                fraction_largest: 0.0,
241            },
242            TemporalPersistencePoint {
243                t: 10,
244                active: 3,
245                n_components: 1,
246                largest_size: 3,
247                fraction_largest: 1.0,
248            },
249            TemporalPersistencePoint {
250                t: 30,
251                active: 3,
252                n_components: 1,
253                largest_size: 3,
254                fraction_largest: 1.0,
255            },
256            TemporalPersistencePoint {
257                t: 50,
258                active: 0,
259                n_components: 0,
260                largest_size: 0,
261                fraction_largest: 0.0,
262            },
263        ];
264        let bars = compute_temporal_barcode(&points);
265        assert_eq!(bars.len(), 1);
266        assert_eq!(bars[0].birth, 10);
267        assert_eq!(bars[0].death, Some(50));
268        assert_eq!(bars[0].peak_size, 3);
269    }
270
271    #[test]
272    fn test_barcode_surviving_component_has_no_death() {
273        // Component born at t=10, never dies → death = None.
274        let points = vec![
275            TemporalPersistencePoint {
276                t: 0,
277                active: 0,
278                n_components: 0,
279                largest_size: 0,
280                fraction_largest: 0.0,
281            },
282            TemporalPersistencePoint {
283                t: 10,
284                active: 2,
285                n_components: 1,
286                largest_size: 2,
287                fraction_largest: 1.0,
288            },
289            TemporalPersistencePoint {
290                t: 20,
291                active: 2,
292                n_components: 1,
293                largest_size: 2,
294                fraction_largest: 1.0,
295            },
296        ];
297        let bars = compute_temporal_barcode(&points);
298        assert_eq!(bars.len(), 1);
299        assert_eq!(bars[0].birth, 10);
300        assert_eq!(bars[0].death, None);
301    }
302}