geographdb_core/algorithms/
persistence.rs1use crate::algorithms::four_d::{
10 strongly_connected_components_4d, GraphNode4D, SpatialRegion, TemporalWindow,
11 TraversalContext4D,
12};
13use glam::Vec3;
14
15#[derive(Debug, Clone)]
17pub struct TemporalPersistencePoint {
18 pub t: u64,
20 pub active: usize,
22 pub n_components: usize,
24 pub largest_size: usize,
26 pub fraction_largest: f32,
28}
29
30#[derive(Debug, Clone, PartialEq)]
36pub struct TemporalBarcode {
37 pub birth: u64,
39 pub death: Option<u64>,
42 pub peak_size: usize,
44}
45
46pub 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
94pub fn compute_temporal_barcode(points: &[TemporalPersistencePoint]) -> Vec<TemporalBarcode> {
103 let mut open: Vec<(u64, usize)> = Vec::new(); 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 if curr > prev_count {
115 for _ in 0..(curr - prev_count) {
116 open.push((pt.t, pt.largest_size));
117 }
118 }
119
120 for ob in &mut open {
122 ob.1 = ob.1.max(pt.largest_size);
123 }
124
125 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 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 #[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 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 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 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 #[test]
232 fn test_barcode_birth_and_death() {
233 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 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}