use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, RngExt, SeedableRng};
use std::fmt::Debug;
use crate::distance::EuclideanDistance;
use crate::error::{SpatialError, SpatialResult};
use crate::kdtree::KDTree;
use crate::pathplanning::astar::Path;
type CollisionCheckFn = Box<dyn Fn(&Array1<f64>, &Array1<f64>) -> bool>;
#[derive(Clone, Debug)]
pub struct RRTConfig {
pub max_iterations: usize,
pub step_size: f64,
pub goal_bias: f64,
pub seed: Option<u64>,
pub use_rrt_star: bool,
pub neighborhood_radius: Option<f64>,
pub bidirectional: bool,
}
impl Default for RRTConfig {
fn default() -> Self {
RRTConfig {
max_iterations: 10000,
step_size: 0.5,
goal_bias: 0.05,
seed: None,
use_rrt_star: false,
neighborhood_radius: None,
bidirectional: false,
}
}
}
#[derive(Clone, Debug)]
struct RRTNode {
position: Array1<f64>,
parent: Option<usize>,
cost: f64,
}
pub struct RRTPlanner {
config: RRTConfig,
collision_checker: Option<CollisionCheckFn>,
rng: StdRng,
dimension: usize,
bounds: Option<(Array1<f64>, Array1<f64>)>,
}
impl Debug for RRTPlanner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RRTPlanner")
.field("config", &self.config)
.field("dimension", &self.dimension)
.field("bounds", &self.bounds)
.field("collision_checker", &"<function>")
.finish()
}
}
impl Clone for RRTPlanner {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
collision_checker: None, rng: StdRng::seed_from_u64(scirs2_core::random::random()), dimension: self.dimension,
bounds: self.bounds.clone(),
}
}
}
impl RRTPlanner {
pub fn new(config: RRTConfig, dimension: usize) -> Self {
let seed = config.seed.unwrap_or_else(scirs2_core::random::random);
let rng = StdRng::seed_from_u64(seed);
RRTPlanner {
config,
collision_checker: None,
rng,
dimension,
bounds: None,
}
}
pub fn with_collision_checker<F>(mut self, collisionchecker: F) -> Self
where
F: Fn(&Array1<f64>, &Array1<f64>) -> bool + 'static,
{
self.collision_checker = Some(Box::new(collisionchecker));
self
}
pub fn with_bounds(
mut self,
min_bounds: Array1<f64>,
max_bounds: Array1<f64>,
) -> SpatialResult<Self> {
if min_bounds.len() != self.dimension || max_bounds.len() != self.dimension {
return Err(SpatialError::DimensionError(format!(
"Bounds dimensions ({}, {}) don't match planner dimension ({})",
min_bounds.len(),
max_bounds.len(),
self.dimension
)));
}
for i in 0..self.dimension {
if min_bounds[i] >= max_bounds[i] {
return Err(SpatialError::ValueError(format!(
"Min bound {} is not less than max bound {} at index {}",
min_bounds[i], max_bounds[i], i
)));
}
}
self.bounds = Some((min_bounds, max_bounds));
Ok(self)
}
fn sample_random_point(&mut self) -> SpatialResult<Array1<f64>> {
let (min_bounds, max_bounds) = self.bounds.as_ref().ok_or_else(|| {
SpatialError::ValueError("Bounds must be set before sampling".to_string())
})?;
let mut point = Array1::zeros(self.dimension);
for i in 0..self.dimension {
point[i] = self.rng.random_range(min_bounds[i]..max_bounds[i]);
}
Ok(point)
}
fn sample_with_goal_bias(&mut self, goal: &ArrayView1<f64>) -> SpatialResult<Array1<f64>> {
if self.rng.random_range(0.0..1.0) < self.config.goal_bias {
Ok(goal.to_owned())
} else {
self.sample_random_point()
}
}
fn find_nearest_node(
&self,
point: &ArrayView1<f64>,
_nodes: &[RRTNode],
kdtree: &KDTree<f64, EuclideanDistance<f64>>,
) -> SpatialResult<usize> {
let point_vec = point.to_vec();
let (indices, _) = kdtree.query(point_vec.as_slice(), 1)?;
Ok(indices[0])
}
fn steer(&self, nearest: &ArrayView1<f64>, randompoint: &ArrayView1<f64>) -> Array1<f64> {
let mut direction = randompoint - nearest;
let norm = direction.iter().map(|&x| x * x).sum::<f64>().sqrt();
if norm < 1e-10 {
return nearest.to_owned();
}
if norm > self.config.step_size {
direction *= self.config.step_size / norm;
}
nearest + direction
}
fn is_valid_connection(&self, from: &ArrayView1<f64>, to: &ArrayView1<f64>) -> bool {
if let Some(ref collision_checker) = self.collision_checker {
!collision_checker(&from.to_owned(), &to.to_owned())
} else {
true }
}
pub fn find_path(
&mut self,
start: ArrayView1<f64>,
goal: ArrayView1<f64>,
goal_threshold: f64,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
if start.len() != self.dimension || goal.len() != self.dimension {
return Err(SpatialError::DimensionError(format!(
"Start or goal dimensions ({}, {}) don't match planner dimension ({})",
start.len(),
goal.len(),
self.dimension
)));
}
if self.bounds.is_none() {
return Err(SpatialError::ValueError(
"Bounds must be set before planning".to_string(),
));
}
if self.config.bidirectional {
self.find_path_bidirectional(start, goal, goal_threshold)
} else if self.config.use_rrt_star {
self.find_path_rrt_star(start, goal, goal_threshold)
} else {
self.find_path_basic_rrt(start, goal, goal_threshold)
}
}
fn find_path_basic_rrt(
&mut self,
start: ArrayView1<f64>,
goal: ArrayView1<f64>,
goal_threshold: f64,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
let mut nodes = vec![RRTNode {
position: start.to_owned(),
parent: None,
cost: 0.0,
}];
for _ in 0..self.config.max_iterations {
let randompoint = self.sample_with_goal_bias(&goal)?;
let points: Vec<_> = nodes.iter().map(|node| node.position.clone()).collect();
let points_array = scirs2_core::ndarray::stack(
scirs2_core::ndarray::Axis(0),
&points.iter().map(|p| p.view()).collect::<Vec<_>>(),
)
.expect("Failed to stack points");
let kdtree = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array)
.expect("Failed to build KDTree");
let nearest_idx = self.find_nearest_node(&randompoint.view(), &nodes, &kdtree)?;
let nearest_position = nodes[nearest_idx].position.clone();
let nearest_cost = nodes[nearest_idx].cost;
let new_point = self.steer(&nearest_position.view(), &randompoint.view());
if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
let new_node = RRTNode {
position: new_point.clone(),
parent: Some(nearest_idx),
cost: nearest_cost
+ euclidean_distance(&nearest_position.view(), &new_point.view()),
};
nodes.push(new_node);
if euclidean_distance(&new_point.view(), &goal) <= goal_threshold {
return Ok(Some(RRTPlanner::extract_path(&nodes, nodes.len() - 1)));
}
}
}
Ok(None)
}
fn find_path_rrt_star(
&mut self,
start: ArrayView1<f64>,
goal: ArrayView1<f64>,
goal_threshold: f64,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
let mut nodes = vec![RRTNode {
position: start.to_owned(),
parent: None,
cost: 0.0,
}];
let mut goalidx: Option<usize> = None;
let neighborhood_radius = self
.config
.neighborhood_radius
.unwrap_or(self.config.step_size * 2.0);
for _ in 0..self.config.max_iterations {
let randompoint = self.sample_with_goal_bias(&goal)?;
let points: Vec<_> = nodes.iter().map(|node| node.position.clone()).collect();
let points_array = scirs2_core::ndarray::stack(
scirs2_core::ndarray::Axis(0),
&points.iter().map(|p| p.view()).collect::<Vec<_>>(),
)
.expect("Failed to stack points");
let kdtree = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array)
.expect("Failed to build KDTree");
let nearest_idx = self.find_nearest_node(&randompoint.view(), &nodes, &kdtree)?;
let nearest_position = nodes[nearest_idx].position.clone();
let new_point = self.steer(&nearest_position.view(), &randompoint.view());
if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
let (parent_idx, cost_from_parent) = self.find_best_parent(
&new_point,
&nodes,
&kdtree,
nearest_idx,
neighborhood_radius,
);
let new_node_idx = nodes.len();
let parent_cost = nodes[parent_idx].cost;
let new_node = RRTNode {
position: new_point.clone(),
parent: Some(parent_idx),
cost: parent_cost + cost_from_parent,
};
nodes.push(new_node);
self.rewire_tree(&mut nodes, new_node_idx, &kdtree, neighborhood_radius);
let dist_to_goal = euclidean_distance(&new_point.view(), &goal);
if dist_to_goal <= goal_threshold {
let new_cost = nodes[new_node_idx].cost + dist_to_goal;
if let Some(idx) = goalidx {
if new_cost < nodes[idx].cost {
goalidx = Some(new_node_idx);
}
} else {
goalidx = Some(new_node_idx);
}
}
}
}
if let Some(idx) = goalidx {
Ok(Some(RRTPlanner::extract_path(&nodes, idx)))
} else {
Ok(None)
}
}
fn find_best_parent(
&self,
new_point: &Array1<f64>,
nodes: &[RRTNode],
kdtree: &KDTree<f64, EuclideanDistance<f64>>,
nearest_idx: usize,
radius: f64,
) -> (usize, f64) {
let mut best_parent_idx = nearest_idx;
let mut best_cost = nodes[nearest_idx].cost
+ euclidean_distance(&nodes[nearest_idx].position.view(), &new_point.view());
let (near_indices, near_distances) = kdtree
.query_radius(new_point.as_slice().expect("Operation failed"), radius)
.expect("KDTree query failed");
for (_idx, &nodeidx) in near_indices.iter().enumerate() {
let node = &nodes[nodeidx];
let dist = near_distances[_idx];
let cost_from_start = node.cost + dist;
if cost_from_start < best_cost
&& self.is_valid_connection(&node.position.view(), &new_point.view())
{
best_parent_idx = nodeidx;
best_cost = cost_from_start;
}
}
let cost_from_parent =
euclidean_distance(&nodes[best_parent_idx].position.view(), &new_point.view());
(best_parent_idx, cost_from_parent)
}
fn rewire_tree(
&self,
nodes: &mut [RRTNode],
new_node_idx: usize,
kdtree: &KDTree<f64, EuclideanDistance<f64>>,
radius: f64,
) {
let new_point = nodes[new_node_idx].position.clone();
let new_cost = nodes[new_node_idx].cost;
let (near_indices, near_distances) = kdtree
.query_radius(new_point.as_slice().expect("Operation failed"), radius)
.expect("KDTree query failed");
for (_idx, &nodeidx) in near_indices.iter().enumerate() {
if nodeidx == new_node_idx {
continue;
}
let dist = near_distances[_idx];
let cost_through_new = new_cost + dist;
let node_position = nodes[nodeidx].position.clone();
if cost_through_new < nodes[nodeidx].cost
&& self.is_valid_connection(&new_point.view(), &node_position.view())
{
nodes[nodeidx].parent = Some(new_node_idx);
nodes[nodeidx].cost = cost_through_new;
RRTPlanner::update_subtree_costs(nodes, nodeidx);
}
}
}
#[allow(clippy::only_used_in_recursion)]
fn update_subtree_costs(nodes: &mut [RRTNode], nodeidx: usize) {
let children: Vec<usize> = nodes
.iter()
.enumerate()
.filter(|(_, node)| node.parent == Some(nodeidx))
.map(|(idx, _)| idx)
.collect();
for &child_idx in &children {
let parent_cost = nodes[nodeidx].cost;
let parent_position = nodes[nodeidx].position.clone();
let child_position = nodes[child_idx].position.clone();
let edge_cost = euclidean_distance(&parent_position.view(), &child_position.view());
nodes[child_idx].cost = parent_cost + edge_cost;
Self::update_subtree_costs(nodes, child_idx);
}
}
fn find_path_bidirectional(
&mut self,
start: ArrayView1<f64>,
goal: ArrayView1<f64>,
goal_threshold: f64,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
let mut start_tree = vec![RRTNode {
position: start.to_owned(),
parent: None,
cost: 0.0,
}];
let mut goal_tree = vec![RRTNode {
position: goal.to_owned(),
parent: None,
cost: 0.0,
}];
let mut tree_a = &mut start_tree;
let mut tree_b = &mut goal_tree;
let mut a_is_start = true;
let mut connection: Option<(usize, usize)> = None;
for _ in 0..self.config.max_iterations {
std::mem::swap(&mut tree_a, &mut tree_b);
a_is_start = !a_is_start;
let target = if a_is_start {
goal.to_owned()
} else {
start.to_owned()
};
let randompoint = self.sample_with_goal_bias(&target.view())?;
let points_a: Vec<_> = tree_a.iter().map(|node| node.position.clone()).collect();
let points_array_a = scirs2_core::ndarray::stack(
scirs2_core::ndarray::Axis(0),
&points_a.iter().map(|p| p.view()).collect::<Vec<_>>(),
)
.expect("Failed to stack points");
let kdtree_a = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array_a)
.expect("Failed to build KDTree");
let nearest_idxa = self.find_nearest_node(&randompoint.view(), tree_a, &kdtree_a)?;
let nearest_position = tree_a[nearest_idxa].position.clone();
let nearest_cost = tree_a[nearest_idxa].cost;
let new_point = self.steer(&nearest_position.view(), &randompoint.view());
if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
let new_cost =
nearest_cost + euclidean_distance(&nearest_position.view(), &new_point.view());
let new_node_idx_a = tree_a.len();
tree_a.push(RRTNode {
position: new_point.clone(),
parent: Some(nearest_idxa),
cost: new_cost,
});
let points_b: Vec<_> = tree_b.iter().map(|node| node.position.clone()).collect();
let points_array_b = scirs2_core::ndarray::stack(
scirs2_core::ndarray::Axis(0),
&points_b.iter().map(|p| p.view()).collect::<Vec<_>>(),
)
.expect("Failed to stack points");
let kdtree_b = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array_b)
.expect("Failed to build KDTree");
let nearest_idxb = self.find_nearest_node(&new_point.view(), tree_b, &kdtree_b)?;
let nearest_position_b = tree_b[nearest_idxb].position.clone();
let dist_between_trees =
euclidean_distance(&new_point.view(), &nearest_position_b.view());
if dist_between_trees <= goal_threshold
&& self.is_valid_connection(&new_point.view(), &nearest_position_b.view())
{
connection = if a_is_start {
Some((new_node_idx_a, nearest_idxb))
} else {
Some((nearest_idxb, new_node_idx_a))
};
break;
}
}
}
if let Some((start_idx, goalidx)) = connection {
let path = self.extract_bidirectional_path(&start_tree, &goal_tree, start_idx, goalidx);
Ok(Some(path))
} else {
Ok(None)
}
}
fn extract_bidirectional_path(
&self,
start_tree: &[RRTNode],
goal_tree: &[RRTNode],
start_idx: usize,
goalidx: usize,
) -> Path<Array1<f64>> {
let mut forward_path = Vec::new();
let mut current_idx = Some(start_idx);
while let Some(_idx) = current_idx {
forward_path.push(start_tree[_idx].position.clone());
current_idx = start_tree[_idx].parent;
}
forward_path.reverse();
let mut backward_path = Vec::new();
let mut current_idx = Some(goalidx);
while let Some(_idx) = current_idx {
backward_path.push(goal_tree[_idx].position.clone());
current_idx = goal_tree[_idx].parent;
}
let mut full_path = forward_path;
full_path.extend(backward_path);
let mut total_cost = 0.0;
for i in 1..full_path.len() {
total_cost += euclidean_distance(&full_path[i - 1].view(), &full_path[i].view());
}
Path::new(full_path, total_cost)
}
fn extract_path(nodes: &[RRTNode], goalidx: usize) -> Path<Array1<f64>> {
let mut path = Vec::new();
let mut current_idx = Some(goalidx);
let cost = nodes[goalidx].cost;
while let Some(_idx) = current_idx {
path.push(nodes[_idx].position.clone());
current_idx = nodes[_idx].parent;
}
path.reverse();
Path::new(path, cost)
}
}
#[allow(dead_code)]
fn euclidean_distance(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
let mut sum = 0.0;
for i in 0..a.len() {
let diff = a[i] - b[i];
sum += diff * diff;
}
sum.sqrt()
}
#[derive(Clone)]
pub struct RRT2DPlanner {
planner: RRTPlanner,
obstacles: Vec<Vec<[f64; 2]>>,
_collision_step_size: f64,
}
impl RRT2DPlanner {
pub fn new(
config: RRTConfig,
obstacles: Vec<Vec<[f64; 2]>>,
min_bounds: [f64; 2],
max_bounds: [f64; 2],
collision_step_size: f64,
) -> SpatialResult<Self> {
let mut planner = RRTPlanner::new(config, 2);
planner = planner.with_bounds(
scirs2_core::ndarray::arr1(&min_bounds),
scirs2_core::ndarray::arr1(&max_bounds),
)?;
let obstacles_clone = obstacles.clone();
planner = planner.with_collision_checker(move |from, to| {
Self::check_collision_with_obstacles(from, to, &obstacles_clone, collision_step_size)
});
Ok(RRT2DPlanner {
planner,
obstacles: obstacles.clone(),
_collision_step_size: collision_step_size,
})
}
pub fn find_path(
&mut self,
start: [f64; 2],
goal: [f64; 2],
goal_threshold: f64,
) -> SpatialResult<Option<Path<[f64; 2]>>> {
let start_arr = scirs2_core::ndarray::arr1(&start);
let goal_arr = scirs2_core::ndarray::arr1(&goal);
for obstacle in &self.obstacles {
if Self::point_in_polygon(&start, obstacle) {
return Err(SpatialError::ValueError(
"Start point is inside an obstacle".to_string(),
));
}
if Self::point_in_polygon(&goal, obstacle) {
return Err(SpatialError::ValueError(
"Goal point is inside an obstacle".to_string(),
));
}
}
let result = self
.planner
.find_path(start_arr.view(), goal_arr.view(), goal_threshold)?;
if let Some(path) = result {
let nodes: Vec<[f64; 2]> = path.nodes.iter().map(|p| [p[0], p[1]]).collect();
Ok(Some(Path::new(nodes, path.cost)))
} else {
Ok(None)
}
}
fn check_collision_with_obstacles(
from: &Array1<f64>,
to: &Array1<f64>,
obstacles: &[Vec<[f64; 2]>],
step_size: f64,
) -> bool {
let from_point = [from[0], from[1]];
let to_point = [to[0], to[1]];
for obstacle in obstacles {
if Self::point_in_polygon(&from_point, obstacle)
|| Self::point_in_polygon(&to_point, obstacle)
{
return true;
}
}
let dx = to[0] - from[0];
let dy = to[1] - from[1];
let distance = (dx * dx + dy * dy).sqrt();
if distance < 1e-6 {
return false; }
let steps = (distance / step_size).ceil() as usize;
for i in 1..steps {
let t = i as f64 / steps as f64;
let x = from[0] + dx * t;
let y = from[1] + dy * t;
let point = [x, y];
for obstacle in obstacles {
if Self::point_in_polygon(&point, obstacle) {
return true;
}
}
}
false
}
fn point_in_polygon(point: &[f64; 2], polygon: &[[f64; 2]]) -> bool {
if polygon.len() < 3 {
return false;
}
let mut inside = false;
let mut j = polygon.len() - 1;
for i in 0..polygon.len() {
let xi = polygon[i][0];
let yi = polygon[i][1];
let xj = polygon[j][0];
let yj = polygon[j][1];
let intersect = ((yi > point[1]) != (yj > point[1]))
&& (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
if intersect {
inside = !inside;
}
j = i;
}
inside
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rrt_empty_space() {
let config = RRTConfig {
max_iterations: 1000,
step_size: 0.5,
goal_bias: 0.1,
seed: Some(42), use_rrt_star: false,
neighborhood_radius: None,
bidirectional: false,
};
let mut planner = RRT2DPlanner::new(
config,
vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
.expect("Operation failed");
let start = [1.0, 1.0];
let goal = [9.0, 9.0];
let goal_threshold = 0.5;
let path = planner
.find_path(start, goal, goal_threshold)
.expect("Operation failed");
assert!(path.is_some());
let path = path.expect("Operation failed");
assert_eq!(path.nodes[0], start);
let last_node = path.nodes.last().expect("Operation failed");
let dist_to_goal =
((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
assert!(dist_to_goal <= goal_threshold);
}
#[test]
fn test_rrt_star_optimization() {
let config = RRTConfig {
max_iterations: 1000,
step_size: 0.5,
goal_bias: 0.1,
seed: Some(42), use_rrt_star: true,
neighborhood_radius: Some(1.0),
bidirectional: false,
};
let mut planner = RRT2DPlanner::new(
config,
vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
.expect("Operation failed");
let start = [1.0, 1.0];
let goal = [9.0, 9.0];
let goal_threshold = 0.5;
let path = planner
.find_path(start, goal, goal_threshold)
.expect("Operation failed");
assert!(path.is_some());
let path = path.expect("Operation failed");
assert_eq!(path.nodes[0], start);
let last_node = path.nodes.last().expect("Operation failed");
let dist_to_goal =
((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
assert!(dist_to_goal <= goal_threshold);
let direct_distance = ((goal[0] - start[0]).powi(2) + (goal[1] - start[1]).powi(2)).sqrt();
assert!(path.cost <= direct_distance * 1.5);
}
#[test]
fn test_rrt_bidirectional() {
let config = RRTConfig {
max_iterations: 1000,
step_size: 0.5,
goal_bias: 0.1,
seed: Some(42), use_rrt_star: false,
neighborhood_radius: None,
bidirectional: true,
};
let mut planner = RRT2DPlanner::new(
config,
vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
.expect("Operation failed");
let start = [1.0, 1.0];
let goal = [9.0, 9.0];
let goal_threshold = 0.5;
let path = planner
.find_path(start, goal, goal_threshold)
.expect("Operation failed");
assert!(path.is_some());
let path = path.expect("Operation failed");
assert_eq!(path.nodes[0], start);
let last_node = path.nodes.last().expect("Operation failed");
let dist_to_goal =
((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
assert!(dist_to_goal <= goal_threshold);
}
#[test]
fn test_rrt_with_obstacles() {
let config = RRTConfig {
max_iterations: 2000,
step_size: 0.3,
goal_bias: 0.1,
seed: Some(42), use_rrt_star: false,
neighborhood_radius: None,
bidirectional: false,
};
let obstacles = vec![vec![[4.0, 0.0], [5.0, 0.0], [5.0, 8.0], [4.0, 8.0]]];
let mut planner = RRT2DPlanner::new(
config,
obstacles,
[0.0, 0.0], [10.0, 10.0], 0.1, )
.expect("Operation failed");
let start = [2.0, 5.0];
let goal = [7.0, 5.0];
let goal_threshold = 0.5;
let path = planner
.find_path(start, goal, goal_threshold)
.expect("Operation failed");
assert!(path.is_some());
let path = path.expect("Operation failed");
assert_eq!(path.nodes[0], start);
let last_node = path.nodes.last().expect("Operation failed");
let dist_to_goal =
((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
assert!(dist_to_goal <= goal_threshold);
for node in &path.nodes {
assert!(!(node[0] >= 4.0 && node[0] <= 5.0 && node[1] >= 0.0 && node[1] <= 8.0));
}
}
}