geographdb_core/algorithms/
percolation.rs1use crate::algorithms::four_d::{
8 reachable_4d, GraphNode4D, SpatialRegion, TemporalWindow, TraversalContext4D,
9};
10use glam::Vec3;
11
12#[derive(Debug, Clone)]
14pub struct PercolationPoint {
15 pub radius: f32,
17 pub time: u64,
19 pub active: usize,
21 pub reachable: usize,
23 pub fraction: f32,
25}
26
27pub fn percolation_sweep(
36 nodes: &[GraphNode4D],
37 center_id: u64,
38 sphere_center: Vec3,
39 radii: &[f32],
40 time_slices: &[u64],
41 time_half_width: u64,
42) -> Vec<PercolationPoint> {
43 let mut out = Vec::with_capacity(radii.len() * time_slices.len());
44
45 for &time in time_slices {
46 let t_start = time.saturating_sub(time_half_width);
47 let t_end = time.saturating_add(time_half_width);
48 let time_window = TemporalWindow {
49 start: t_start,
50 end: t_end,
51 };
52
53 for &radius in radii {
54 let ctx = TraversalContext4D {
55 spatial_region: Some(SpatialRegion::Sphere {
56 center: sphere_center,
57 radius,
58 }),
59 time_window: Some(time_window),
60 ..TraversalContext4D::default()
61 };
62
63 let active = count_active(nodes, &ctx);
64 let reachable = reachable_4d(nodes, center_id, &ctx).len();
65 let fraction = if active == 0 {
66 0.0
67 } else {
68 reachable as f32 / active as f32
69 };
70
71 out.push(PercolationPoint {
72 radius,
73 time,
74 active,
75 reachable,
76 fraction,
77 });
78 }
79 }
80
81 out
82}
83
84pub fn find_critical_radius(
93 points: &[PercolationPoint],
94 time: u64,
95 threshold: f32,
96 min_active: usize,
97) -> Option<f32> {
98 points
99 .iter()
100 .filter(|p| p.time == time && p.active >= min_active && p.fraction >= threshold)
101 .map(|p| p.radius)
102 .reduce(f32::min)
103}
104
105fn count_active(nodes: &[GraphNode4D], ctx: &TraversalContext4D) -> usize {
109 nodes
110 .iter()
111 .filter(|n| {
112 let time_ok = ctx
113 .time_window
114 .map(|w| w.overlaps(n.begin_ts, n.end_ts))
115 .unwrap_or(true);
116 let space_ok = match ctx.spatial_region {
117 Some(SpatialRegion::Sphere { center, radius }) => {
118 n.position().distance(center) <= radius
119 }
120 Some(SpatialRegion::Aabb { min, max }) => {
121 let p = n.position();
122 p.x >= min.x
123 && p.x <= max.x
124 && p.y >= min.y
125 && p.y <= max.y
126 && p.z >= min.z
127 && p.z <= max.z
128 }
129 None => true,
130 };
131 time_ok && space_ok
132 })
133 .count()
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
140
141 fn node(id: u64, x: f32, y: f32, z: f32, successors: Vec<TemporalEdge>) -> GraphNode4D {
142 GraphNode4D {
143 id,
144 x,
145 y,
146 z,
147 begin_ts: 0,
148 end_ts: 100,
149 properties: GraphProperties::new(),
150 successors,
151 }
152 }
153
154 fn edge(dst: u64) -> TemporalEdge {
155 TemporalEdge {
156 dst,
157 weight: 1.0,
158 begin_ts: 0,
159 end_ts: 100,
160 }
161 }
162
163 fn line_graph(n: u64) -> Vec<GraphNode4D> {
165 (0..n)
166 .map(|i| {
167 let succs = if i + 1 < n { vec![edge(i + 1)] } else { vec![] };
168 node(i, i as f32, 0.0, 0.0, succs)
169 })
170 .collect()
171 }
172
173 #[test]
176 fn test_zero_radius_gives_zero_fraction() {
177 let nodes = line_graph(5);
179 let pts = percolation_sweep(
180 &nodes,
181 0,
182 Vec3::new(-10.0, 0.0, 0.0), &[0.001],
184 &[50],
185 10,
186 );
187 assert_eq!(pts.len(), 1);
188 assert_eq!(
189 pts[0].active, 0,
190 "no node should fall inside radius 0.001 far from origin"
191 );
192 assert_eq!(pts[0].fraction, 0.0);
193 }
194
195 #[test]
196 fn test_huge_radius_gives_full_fraction() {
197 let nodes = line_graph(5);
199 let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &[1000.0], &[50], 10);
200 assert_eq!(pts.len(), 1);
201 assert_eq!(pts[0].active, 5);
202 assert_eq!(pts[0].reachable, 5);
203 assert!((pts[0].fraction - 1.0).abs() < 1e-6);
204 }
205
206 #[test]
207 fn test_fraction_monotone_with_radius() {
208 let nodes = line_graph(10);
210 let radii: Vec<f32> = (1..=20).map(|i| i as f32 * 0.5).collect();
211 let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &radii, &[50], 10);
212
213 let fracs: Vec<f32> = pts
214 .iter()
215 .filter(|p| p.time == 50)
216 .map(|p| p.fraction)
217 .collect();
218 for w in fracs.windows(2) {
219 assert!(
220 w[1] >= w[0] - 1e-6,
221 "fraction not monotone: {} then {}",
222 w[0],
223 w[1]
224 );
225 }
226 }
227
228 #[test]
229 fn test_time_filter_reduces_active_nodes() {
230 let nodes = line_graph(5);
232 let pts = percolation_sweep(
233 &nodes,
234 0,
235 Vec3::ZERO,
236 &[1000.0],
237 &[500], 10,
239 );
240 assert_eq!(pts[0].active, 0);
241 assert_eq!(pts[0].fraction, 0.0);
242 }
243
244 #[test]
245 fn test_critical_radius_found_in_range() {
246 let nodes = line_graph(10);
249 let radii: Vec<f32> = (1..=20).map(|i| i as f32 * 0.5).collect();
250 let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &radii, &[50], 10);
251 let r_star = find_critical_radius(&pts, 50, 0.5, 1);
252 assert!(r_star.is_some(), "critical radius should be found");
253 let r = r_star.unwrap();
254 assert!(
255 (0.5..=10.0).contains(&r),
256 "r* = {} out of expected range",
257 r
258 );
259 }
260
261 #[test]
262 fn test_find_critical_radius_none_when_never_crosses() {
263 let isolated: Vec<GraphNode4D> = (0..5)
265 .map(|i| node(i, i as f32, 0.0, 0.0, vec![]))
266 .collect();
267 let pts2 = percolation_sweep(&isolated, 0, Vec3::ZERO, &[1000.0], &[50], 10);
268 let r_star = find_critical_radius(&pts2, 50, 0.5, 1);
270 assert!(
271 r_star.is_none(),
272 "threshold 0.5 should not be crossed when fraction = 0.2"
273 );
274 }
275
276 #[test]
279 fn test_count_active_no_filter() {
280 let nodes = line_graph(7);
281 let ctx = TraversalContext4D::default();
282 assert_eq!(count_active(&nodes, &ctx), 7);
283 }
284
285 #[test]
286 fn test_count_active_sphere_filter() {
287 let nodes = line_graph(10); let ctx = TraversalContext4D {
289 spatial_region: Some(SpatialRegion::Sphere {
290 center: Vec3::ZERO,
291 radius: 3.5,
292 }),
293 ..TraversalContext4D::default()
294 };
295 assert_eq!(count_active(&nodes, &ctx), 4);
297 }
298}