use crate::algorithms::four_d::{
reachable_4d, GraphNode4D, SpatialRegion, TemporalWindow, TraversalContext4D,
};
use glam::Vec3;
#[derive(Debug, Clone)]
pub struct PercolationPoint {
pub radius: f32,
pub time: u64,
pub active: usize,
pub reachable: usize,
pub fraction: f32,
}
pub fn percolation_sweep(
nodes: &[GraphNode4D],
center_id: u64,
sphere_center: Vec3,
radii: &[f32],
time_slices: &[u64],
time_half_width: u64,
) -> Vec<PercolationPoint> {
let mut out = Vec::with_capacity(radii.len() * time_slices.len());
for &time in time_slices {
let t_start = time.saturating_sub(time_half_width);
let t_end = time.saturating_add(time_half_width);
let time_window = TemporalWindow {
start: t_start,
end: t_end,
};
for &radius in radii {
let ctx = TraversalContext4D {
spatial_region: Some(SpatialRegion::Sphere {
center: sphere_center,
radius,
}),
time_window: Some(time_window),
..TraversalContext4D::default()
};
let active = count_active(nodes, &ctx);
let reachable = reachable_4d(nodes, center_id, &ctx).len();
let fraction = if active == 0 {
0.0
} else {
reachable as f32 / active as f32
};
out.push(PercolationPoint {
radius,
time,
active,
reachable,
fraction,
});
}
}
out
}
pub fn find_critical_radius(
points: &[PercolationPoint],
time: u64,
threshold: f32,
min_active: usize,
) -> Option<f32> {
points
.iter()
.filter(|p| p.time == time && p.active >= min_active && p.fraction >= threshold)
.map(|p| p.radius)
.reduce(f32::min)
}
fn count_active(nodes: &[GraphNode4D], ctx: &TraversalContext4D) -> usize {
nodes
.iter()
.filter(|n| {
let time_ok = ctx
.time_window
.map(|w| w.overlaps(n.begin_ts, n.end_ts))
.unwrap_or(true);
let space_ok = match ctx.spatial_region {
Some(SpatialRegion::Sphere { center, radius }) => {
n.position().distance(center) <= radius
}
Some(SpatialRegion::Aabb { min, max }) => {
let p = n.position();
p.x >= min.x
&& p.x <= max.x
&& p.y >= min.y
&& p.y <= max.y
&& p.z >= min.z
&& p.z <= max.z
}
None => true,
};
time_ok && space_ok
})
.count()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
fn node(id: u64, x: f32, y: f32, z: f32, successors: Vec<TemporalEdge>) -> GraphNode4D {
GraphNode4D {
id,
x,
y,
z,
begin_ts: 0,
end_ts: 100,
properties: GraphProperties::new(),
successors,
}
}
fn edge(dst: u64) -> TemporalEdge {
TemporalEdge {
dst,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}
}
fn line_graph(n: u64) -> Vec<GraphNode4D> {
(0..n)
.map(|i| {
let succs = if i + 1 < n { vec![edge(i + 1)] } else { vec![] };
node(i, i as f32, 0.0, 0.0, succs)
})
.collect()
}
#[test]
fn test_zero_radius_gives_zero_fraction() {
let nodes = line_graph(5);
let pts = percolation_sweep(
&nodes,
0,
Vec3::new(-10.0, 0.0, 0.0), &[0.001],
&[50],
10,
);
assert_eq!(pts.len(), 1);
assert_eq!(
pts[0].active, 0,
"no node should fall inside radius 0.001 far from origin"
);
assert_eq!(pts[0].fraction, 0.0);
}
#[test]
fn test_huge_radius_gives_full_fraction() {
let nodes = line_graph(5);
let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &[1000.0], &[50], 10);
assert_eq!(pts.len(), 1);
assert_eq!(pts[0].active, 5);
assert_eq!(pts[0].reachable, 5);
assert!((pts[0].fraction - 1.0).abs() < 1e-6);
}
#[test]
fn test_fraction_monotone_with_radius() {
let nodes = line_graph(10);
let radii: Vec<f32> = (1..=20).map(|i| i as f32 * 0.5).collect();
let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &radii, &[50], 10);
let fracs: Vec<f32> = pts
.iter()
.filter(|p| p.time == 50)
.map(|p| p.fraction)
.collect();
for w in fracs.windows(2) {
assert!(
w[1] >= w[0] - 1e-6,
"fraction not monotone: {} then {}",
w[0],
w[1]
);
}
}
#[test]
fn test_time_filter_reduces_active_nodes() {
let nodes = line_graph(5);
let pts = percolation_sweep(
&nodes,
0,
Vec3::ZERO,
&[1000.0],
&[500], 10,
);
assert_eq!(pts[0].active, 0);
assert_eq!(pts[0].fraction, 0.0);
}
#[test]
fn test_critical_radius_found_in_range() {
let nodes = line_graph(10);
let radii: Vec<f32> = (1..=20).map(|i| i as f32 * 0.5).collect();
let pts = percolation_sweep(&nodes, 0, Vec3::ZERO, &radii, &[50], 10);
let r_star = find_critical_radius(&pts, 50, 0.5, 1);
assert!(r_star.is_some(), "critical radius should be found");
let r = r_star.unwrap();
assert!(
(0.5..=10.0).contains(&r),
"r* = {} out of expected range",
r
);
}
#[test]
fn test_find_critical_radius_none_when_never_crosses() {
let isolated: Vec<GraphNode4D> = (0..5)
.map(|i| node(i, i as f32, 0.0, 0.0, vec![]))
.collect();
let pts2 = percolation_sweep(&isolated, 0, Vec3::ZERO, &[1000.0], &[50], 10);
let r_star = find_critical_radius(&pts2, 50, 0.5, 1);
assert!(
r_star.is_none(),
"threshold 0.5 should not be crossed when fraction = 0.2"
);
}
#[test]
fn test_count_active_no_filter() {
let nodes = line_graph(7);
let ctx = TraversalContext4D::default();
assert_eq!(count_active(&nodes, &ctx), 7);
}
#[test]
fn test_count_active_sphere_filter() {
let nodes = line_graph(10); let ctx = TraversalContext4D {
spatial_region: Some(SpatialRegion::Sphere {
center: Vec3::ZERO,
radius: 3.5,
}),
..TraversalContext4D::default()
};
assert_eq!(count_active(&nodes, &ctx), 4);
}
}