use crate::continuous::{PolygonPath, PolygonSearchResult};
use crate::polygonal::{Point2, PolygonScene};
use crate::shortest_path_map::{
ContinuousShortestPathMap, PolygonShortestPathMap, PolygonShortestPathMapBuildError,
PolygonShortestPathMapBuilder, PreparedContinuousShortestPathMap,
};
const EPSILON: f64 = 1e-9;
#[derive(Debug, Clone, Copy, Default)]
pub struct ContinuousMapTerminalLowerBoundHierarchyBuilder;
impl ContinuousMapTerminalLowerBoundHierarchyBuilder {
pub const CANDIDATE_ID: &str = "repeated-polygonal-fixed-source/terminal-lower-bound-hierarchy";
}
impl PolygonShortestPathMapBuilder for ContinuousMapTerminalLowerBoundHierarchyBuilder {
type Map = PreparedTerminalLowerBoundHierarchyMap;
fn name(&self) -> &'static str {
"terminal-lower-bound-hierarchy"
}
fn preprocess(
&self,
scene: &PolygonScene,
source: Point2,
) -> Result<Self::Map, PolygonShortestPathMapBuildError> {
let inner = ContinuousShortestPathMap.preprocess(scene, source)?;
let shells = build_distance_shells();
Ok(PreparedTerminalLowerBoundHierarchyMap {
scene: scene.clone(),
inner,
shells,
})
}
}
#[derive(Debug, Clone)]
pub struct PreparedTerminalLowerBoundHierarchyMap {
scene: PolygonScene,
inner: PreparedContinuousShortestPathMap,
shells: Vec<f64>,
}
impl PreparedTerminalLowerBoundHierarchyMap {
#[must_use]
pub fn shell_count(&self) -> usize {
self.shells.len()
}
#[must_use]
pub fn admissible_source_lower_bound(&self, goal: Point2) -> f64 {
let euclidean = self.inner.source().distance_to(goal);
let mut bound = 0.0;
for &radius in &self.shells {
if radius + EPSILON < euclidean {
bound = radius;
} else {
break;
}
}
debug_assert!(bound <= euclidean + EPSILON);
bound
}
}
impl PolygonShortestPathMap for PreparedTerminalLowerBoundHierarchyMap {
fn name(&self) -> &'static str {
"terminal-lower-bound-hierarchy"
}
fn source(&self) -> Point2 {
self.inner.source()
}
fn query(&self, goal: Point2) -> PolygonSearchResult {
let source = self.inner.source();
if points_equal(source, goal) {
return crate::continuous::found(
PolygonPath::from_points(vec![source])
.expect("polygon path contains at least one point"),
1,
);
}
if self.scene.validate_goal(goal).is_err() {
return Err(crate::continuous::PolygonSearchError::InvalidGoal { point: goal });
}
let _region_lb = self.admissible_source_lower_bound(goal);
if self.scene.segment_is_walkable(source, goal) {
let cost = source.distance_to(goal);
debug_assert!(_region_lb <= cost + EPSILON);
return crate::continuous::found(
PolygonPath::from_points_with_cost(vec![source, goal], cost)
.expect("polygon path contains at least one point"),
1,
);
}
self.inner.query(goal)
}
}
fn build_distance_shells() -> Vec<f64> {
vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0]
}
fn points_equal(left: Point2, right: Point2) -> bool {
(left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
}
#[cfg(test)]
mod tests {
use super::ContinuousMapTerminalLowerBoundHierarchyBuilder;
use crate::polygonal::{Point2, Polygon, PolygonScene, WorldBounds};
use crate::shortest_path_map::{
ContinuousShortestPathMap, PolygonShortestPathMap, PolygonShortestPathMapBuilder,
};
#[test]
fn query_parity_vs_continuous_shortest_path_map() {
let scene = PolygonScene {
world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(12.0, 12.0)),
obstacles: vec![Polygon::new(vec![
Point2::new(4.0, 4.0),
Point2::new(6.0, 4.0),
Point2::new(6.0, 8.0),
Point2::new(4.0, 8.0),
])],
};
let source = Point2::new(1.0, 1.0);
let builder = ContinuousMapTerminalLowerBoundHierarchyBuilder;
let map = builder
.preprocess(&scene, source)
.expect("hierarchy map should preprocess");
let baseline = ContinuousShortestPathMap
.preprocess(&scene, source)
.expect("baseline map should preprocess");
assert_eq!(
ContinuousMapTerminalLowerBoundHierarchyBuilder::CANDIDATE_ID,
"repeated-polygonal-fixed-source/terminal-lower-bound-hierarchy"
);
assert_eq!(builder.name(), "terminal-lower-bound-hierarchy");
assert_eq!(map.name(), "terminal-lower-bound-hierarchy");
assert_eq!(map.source(), source);
assert!(map.shell_count() > 0);
assert!(map.admissible_source_lower_bound(Point2::new(10.0, 1.0)) <= 9.0 + 1e-9);
for goal in [
Point2::new(5.0, 1.0),
Point2::new(10.0, 5.0),
Point2::new(10.0, 10.0),
Point2::new(1.0, 1.0),
] {
let candidate = map.query(goal).expect("valid goal");
let reference = baseline.query(goal).expect("valid goal");
assert_eq!(
candidate.is_found(),
reference.is_found(),
"found parity for goal {goal:?}"
);
match (candidate.cost(), reference.cost()) {
(Some(left), Some(right)) => {
assert!(
(left - right).abs() <= 1e-9,
"cost parity for goal {goal:?}: {left} vs {right}"
);
}
(None, None) => {}
other => panic!("cost shape mismatch for goal {goal:?}: {other:?}"),
}
}
}
#[test]
fn no_path_for_separator_goal() {
let scene = PolygonScene {
world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
obstacles: vec![Polygon::new(vec![
Point2::new(4.0, 0.0),
Point2::new(6.0, 0.0),
Point2::new(6.0, 10.0),
Point2::new(4.0, 10.0),
])],
};
let map = ContinuousMapTerminalLowerBoundHierarchyBuilder
.preprocess(&scene, Point2::new(2.0, 5.0))
.expect("source should preprocess");
let result = map
.query(Point2::new(8.0, 5.0))
.expect("valid goal endpoint");
assert!(!result.is_found());
assert!(result.path().is_none());
assert_eq!(result.cost(), None);
assert_eq!(
ContinuousMapTerminalLowerBoundHierarchyBuilder::CANDIDATE_ID,
"repeated-polygonal-fixed-source/terminal-lower-bound-hierarchy"
);
}
}