use geographdb_core::{
articulation_points_4d, astar_find_path_4d, bridges_4d, earliest_arrival_path_4d,
fastest_temporal_path_4d, reachable_4d, strongly_connected_components_4d,
time_dependent_dijkstra_4d, GraphNode4D, SpatialRegion, TemporalEdge, TemporalWindow,
TraversalContext4D,
};
use glam::Vec3;
fn test_graph() -> Vec<GraphNode4D> {
vec![
GraphNode4D {
id: 1,
x: 0.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 2,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
},
TemporalEdge {
dst: 4,
weight: 10.0,
begin_ts: 0,
end_ts: 100,
},
],
},
GraphNode4D {
id: 2,
x: 1.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 3,
weight: 1.0,
begin_ts: 0,
end_ts: 40,
}],
},
GraphNode4D {
id: 3,
x: 2.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 1,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}],
},
GraphNode4D {
id: 4,
x: 50.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
]
}
#[test]
fn reachable_4d_filters_by_time_and_space() {
let ctx = TraversalContext4D {
time_window: Some(TemporalWindow { start: 50, end: 60 }),
spatial_region: Some(SpatialRegion::Sphere {
center: Vec3::ZERO,
radius: 5.0,
}),
graph_weight: 1.0,
spatial_weight: 0.0,
temporal_weight: 0.0,
};
let reachable = reachable_4d(&test_graph(), 1, &ctx);
assert_eq!(reachable, vec![1, 2]);
}
#[test]
fn astar_4d_uses_edge_time_validity_and_spatial_cost() {
let ctx = TraversalContext4D {
time_window: Some(TemporalWindow { start: 10, end: 20 }),
spatial_region: None,
graph_weight: 1.0,
spatial_weight: 0.5,
temporal_weight: 0.0,
};
let path = astar_find_path_4d(&test_graph(), 1, 3, &ctx).expect("path should exist");
assert_eq!(path.node_ids, vec![1, 2, 3]);
assert!(path.total_cost > 0.0);
}
#[test]
fn astar_4d_rejects_paths_when_edge_is_outside_time_window() {
let ctx = TraversalContext4D {
time_window: Some(TemporalWindow { start: 50, end: 60 }),
spatial_region: None,
graph_weight: 1.0,
spatial_weight: 0.0,
temporal_weight: 0.0,
};
assert!(astar_find_path_4d(&test_graph(), 1, 3, &ctx).is_none());
}
#[test]
fn astar_4d_can_prefer_spatially_local_path_over_lower_graph_weight() {
let nodes = vec![
GraphNode4D {
id: 1,
x: 0.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 2,
weight: 5.0,
begin_ts: 0,
end_ts: 100,
},
TemporalEdge {
dst: 4,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
},
],
},
GraphNode4D {
id: 2,
x: 1.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 3,
weight: 5.0,
begin_ts: 0,
end_ts: 100,
}],
},
GraphNode4D {
id: 3,
x: 2.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
GraphNode4D {
id: 4,
x: 100.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 3,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}],
},
];
let graph_only = TraversalContext4D {
graph_weight: 1.0,
spatial_weight: 0.0,
..TraversalContext4D::default()
};
let spatial_aware = TraversalContext4D {
graph_weight: 1.0,
spatial_weight: 1.0,
..TraversalContext4D::default()
};
let graph_path = astar_find_path_4d(&nodes, 1, 3, &graph_only).unwrap();
let spatial_path = astar_find_path_4d(&nodes, 1, 3, &spatial_aware).unwrap();
assert_eq!(graph_path.node_ids, vec![1, 4, 3]);
assert_eq!(spatial_path.node_ids, vec![1, 2, 3]);
}
#[test]
fn scc_4d_respects_temporal_edge_filtering() {
let early = TraversalContext4D {
time_window: Some(TemporalWindow { start: 10, end: 20 }),
spatial_region: None,
graph_weight: 1.0,
spatial_weight: 0.0,
temporal_weight: 0.0,
};
let late = TraversalContext4D {
time_window: Some(TemporalWindow { start: 50, end: 60 }),
spatial_region: None,
graph_weight: 1.0,
spatial_weight: 0.0,
temporal_weight: 0.0,
};
let early_components = strongly_connected_components_4d(&test_graph(), &early);
let late_components = strongly_connected_components_4d(&test_graph(), &late);
assert!(early_components.iter().any(|c| c == &vec![1, 2, 3]));
assert!(!late_components.iter().any(|c| c == &vec![1, 2, 3]));
}
fn temporal_delivery_graph() -> Vec<GraphNode4D> {
vec![
GraphNode4D {
id: 1,
x: 0.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 2,
weight: 4.0,
begin_ts: 10,
end_ts: 30,
},
TemporalEdge {
dst: 3,
weight: 4.0,
begin_ts: 10,
end_ts: 18,
},
],
},
GraphNode4D {
id: 2,
x: 1.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 6,
weight: 4.0,
begin_ts: 10,
end_ts: 14,
},
TemporalEdge {
dst: 5,
weight: 4.0,
begin_ts: 22,
end_ts: 26,
},
],
},
GraphNode4D {
id: 3,
x: 0.0,
y: 2.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 4,
weight: 5.0,
begin_ts: 14,
end_ts: 25,
}],
},
GraphNode4D {
id: 4,
x: 0.0,
y: 4.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 6,
weight: 4.0,
begin_ts: 23,
end_ts: 40,
}],
},
GraphNode4D {
id: 5,
x: 2.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 6,
weight: 4.0,
begin_ts: 26,
end_ts: 40,
}],
},
GraphNode4D {
id: 6,
x: 3.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
]
}
#[test]
fn earliest_arrival_path_4d_respects_causal_edge_windows() {
let journey = earliest_arrival_path_4d(&temporal_delivery_graph(), 1, 6, 10, None)
.expect("temporal journey should exist");
assert_eq!(journey.node_ids, vec![1, 3, 4, 6]);
assert_eq!(journey.departure_time, 10);
assert_eq!(journey.arrival_time, 27);
assert_eq!(journey.duration, 17);
}
#[test]
fn earliest_arrival_path_4d_rejects_edge_that_expires_before_arrival() {
let journey = earliest_arrival_path_4d(&temporal_delivery_graph(), 1, 6, 10, None).unwrap();
assert_ne!(journey.node_ids, vec![1, 2, 6]);
}
#[test]
fn fastest_temporal_path_4d_can_depart_later_than_earliest_arrival() {
let journey = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 10, 20, None)
.expect("fastest journey should exist");
assert_eq!(journey.node_ids, vec![1, 2, 5, 6]);
assert_eq!(journey.departure_time, 18);
assert_eq!(journey.arrival_time, 30);
assert_eq!(journey.duration, 12);
}
#[test]
fn fastest_temporal_path_4d_rejects_inverted_window() {
let result = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 50, 10, None);
assert!(result.is_none());
}
#[test]
fn fastest_temporal_path_4d_rejects_oversized_window() {
let result = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 0, u64::MAX, None);
assert!(result.is_none());
}
fn temporal_bottleneck_graph() -> Vec<GraphNode4D> {
vec![
GraphNode4D {
id: 1,
x: 0.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 2,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
},
TemporalEdge {
dst: 4,
weight: 1.0,
begin_ts: 40,
end_ts: 100,
},
],
},
GraphNode4D {
id: 2,
x: 1.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 3,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}],
},
GraphNode4D {
id: 3,
x: 2.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 6,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}],
},
GraphNode4D {
id: 4,
x: 0.0,
y: 2.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 5,
weight: 1.0,
begin_ts: 0,
end_ts: 100,
}],
},
GraphNode4D {
id: 5,
x: 1.0,
y: 2.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 3,
weight: 1.0,
begin_ts: 40,
end_ts: 100,
},
TemporalEdge {
dst: 6,
weight: 1.0,
begin_ts: 40,
end_ts: 100,
},
],
},
GraphNode4D {
id: 6,
x: 3.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
]
}
fn bottleneck_context(start: u64, end: u64) -> TraversalContext4D {
TraversalContext4D {
time_window: Some(TemporalWindow { start, end }),
spatial_region: Some(SpatialRegion::Sphere {
center: Vec3::ZERO,
radius: 10.0,
}),
..TraversalContext4D::default()
}
}
#[test]
fn articulation_points_4d_change_across_temporal_windows() {
let early = articulation_points_4d(&temporal_bottleneck_graph(), &bottleneck_context(10, 20));
let late = articulation_points_4d(&temporal_bottleneck_graph(), &bottleneck_context(50, 60));
assert_eq!(early, vec![2, 3]);
assert!(late.is_empty());
}
#[test]
fn bridges_4d_change_across_temporal_windows() {
let early = bridges_4d(&temporal_bottleneck_graph(), &bottleneck_context(10, 20));
let late = bridges_4d(&temporal_bottleneck_graph(), &bottleneck_context(50, 60));
assert_eq!(early, vec![(1, 2), (2, 3), (3, 6), (4, 5)]);
assert!(late.is_empty());
}
fn signal_propagation_graph() -> Vec<GraphNode4D> {
vec![
GraphNode4D {
id: 1,
x: 0.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![
TemporalEdge {
dst: 2,
weight: 4.0,
begin_ts: 10,
end_ts: 30,
},
TemporalEdge {
dst: 4,
weight: 6.0,
begin_ts: 10,
end_ts: 30,
},
],
},
GraphNode4D {
id: 2,
x: 1.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 3,
weight: 5.0,
begin_ts: 14,
end_ts: 30,
}],
},
GraphNode4D {
id: 3,
x: 2.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 6,
weight: 4.0,
begin_ts: 10,
end_ts: 18,
}],
},
GraphNode4D {
id: 4,
x: 0.0,
y: 2.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![TemporalEdge {
dst: 5,
weight: 8.0,
begin_ts: 16,
end_ts: 40,
}],
},
GraphNode4D {
id: 5,
x: 1.0,
y: 2.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
GraphNode4D {
id: 6,
x: 3.0,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 100,
properties: Default::default(),
successors: vec![],
},
]
}
#[test]
fn time_dependent_dijkstra_4d_computes_arrival_wave() {
let result = time_dependent_dijkstra_4d(&signal_propagation_graph(), 1, 10, None)
.expect("start node should be usable");
let arrivals: Vec<(u64, u64, Vec<u64>)> = result
.reachable
.iter()
.map(|arrival| (arrival.node_id, arrival.arrival_time, arrival.path.clone()))
.collect();
assert_eq!(
arrivals,
vec![
(1, 10, vec![1]),
(2, 14, vec![1, 2]),
(4, 16, vec![1, 4]),
(3, 19, vec![1, 2, 3]),
(5, 24, vec![1, 4, 5]),
]
);
assert_eq!(result.unreachable, vec![6]);
}