use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::numeric::Float;
use crate::error::{SpatialError, SpatialResult};
use crate::pathplanning::astar::Path;
#[allow(dead_code)]
type DistanceFn = Box<dyn Fn(&ArrayView1<f64>) -> f64>;
#[derive(Debug, Clone)]
pub struct PotentialConfig {
pub attractive_gain: f64,
pub repulsive_gain: f64,
pub influence_radius: f64,
pub step_size: f64,
pub max_iterations: usize,
pub seed: Option<u64>,
pub goal_threshold: f64,
pub use_fast_path: bool,
pub min_force_threshold: f64,
}
impl Default for PotentialConfig {
fn default() -> Self {
Self {
attractive_gain: 1.0,
repulsive_gain: 100.0,
influence_radius: 5.0,
step_size: 0.1,
max_iterations: 1000,
seed: None,
goal_threshold: 0.5,
use_fast_path: true,
min_force_threshold: 0.01,
}
}
}
impl PotentialConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_attractive_gain(mut self, gain: f64) -> Self {
self.attractive_gain = gain;
self
}
pub fn with_repulsive_gain(mut self, gain: f64) -> Self {
self.repulsive_gain = gain;
self
}
pub fn with_influence_radius(mut self, radius: f64) -> Self {
self.influence_radius = radius;
self
}
pub fn with_step_size(mut self, stepsize: f64) -> Self {
self.step_size = stepsize;
self
}
pub fn with_max_iterations(mut self, maxiterations: usize) -> Self {
self.max_iterations = maxiterations;
self
}
pub fn with_min_force_threshold(mut self, threshold: f64) -> Self {
self.min_force_threshold = threshold;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_goal_threshold(mut self, threshold: f64) -> Self {
self.goal_threshold = threshold;
self
}
pub fn with_use_fast_path(mut self, use_fastpath: bool) -> Self {
self.use_fast_path = use_fastpath;
self
}
}
pub trait Obstacle {
fn distance(&self, point: &ArrayView1<f64>) -> f64;
fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64>;
}
pub struct CircularObstacle {
center: Array1<f64>,
radius: f64,
}
impl CircularObstacle {
pub fn new(center: Array1<f64>, radius: f64) -> Self {
Self { center, radius }
}
}
impl Obstacle for CircularObstacle {
fn distance(&self, point: &ArrayView1<f64>) -> f64 {
let diff = &self.center - point;
let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
(dist - self.radius).max(0.0)
}
fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64> {
let diff = point.to_owned() - &self.center;
let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
if dist <= self.radius || dist > config.influence_radius {
return Array1::zeros(point.len());
}
let force_magnitude = config.repulsive_gain * (1.0 / dist - 1.0 / config.influence_radius);
let unit_vec = &diff / dist;
unit_vec * force_magnitude
}
}
pub struct PolygonObstacle {
vertices: Vec<Array1<f64>>,
}
impl PolygonObstacle {
pub fn new(vertices: Vec<Array1<f64>>) -> Self {
Self { vertices }
}
fn is_point_inside(&self, point: &ArrayView1<f64>) -> bool {
if self.vertices.len() < 3 || point.len() != 2 {
return false; }
let px = point[0];
let py = point[1];
let mut inside = false;
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let xi = self.vertices[i][0];
let yi = self.vertices[i][1];
let xj = self.vertices[j][0];
let yj = self.vertices[j][1];
if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
inside = !inside;
}
}
inside
}
fn distance_to_polygon_boundary(&self, point: &ArrayView1<f64>) -> f64 {
if self.vertices.len() < 2 || point.len() != 2 {
return 0.0; }
let mut min_distance = f64::INFINITY;
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let edge_dist =
self.distance_point_to_line_segment(point, &self.vertices[i], &self.vertices[j]);
min_distance = min_distance.min(edge_dist);
}
min_distance
}
fn distance_point_to_line_segment(
&self,
point: &ArrayView1<f64>,
line_start: &Array1<f64>,
line_end: &Array1<f64>,
) -> f64 {
let px = point[0];
let py = point[1];
let x1 = line_start[0];
let y1 = line_start[1];
let x2 = line_end[0];
let y2 = line_end[1];
let dx = x2 - x1;
let dy = y2 - y1;
let length_squared = dx * dx + dy * dy;
if length_squared < 1e-10 {
let diff_x = px - x1;
let diff_y = py - y1;
return (diff_x * diff_x + diff_y * diff_y).sqrt();
}
let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
let t_clamped = t.clamp(0.0, 1.0);
let closest_x = x1 + t_clamped * dx;
let closest_y = y1 + t_clamped * dy;
let diff_x = px - closest_x;
let diff_y = py - closest_y;
(diff_x * diff_x + diff_y * diff_y).sqrt()
}
fn closest_point_on_boundary(&self, point: &ArrayView1<f64>) -> Array1<f64> {
if self.vertices.len() < 2 || point.len() != 2 {
return Array1::from_vec(vec![0.0, 0.0]);
}
let mut min_distance = f64::INFINITY;
let mut closest_point = Array1::from_vec(vec![0.0, 0.0]);
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let edge_point =
self.closest_point_on_line_segment(point, &self.vertices[i], &self.vertices[j]);
let dist =
self.distance_point_to_line_segment(point, &self.vertices[i], &self.vertices[j]);
if dist < min_distance {
min_distance = dist;
closest_point = edge_point;
}
}
closest_point
}
fn closest_point_on_line_segment(
&self,
point: &ArrayView1<f64>,
line_start: &Array1<f64>,
line_end: &Array1<f64>,
) -> Array1<f64> {
let px = point[0];
let py = point[1];
let x1 = line_start[0];
let y1 = line_start[1];
let x2 = line_end[0];
let y2 = line_end[1];
let dx = x2 - x1;
let dy = y2 - y1;
let length_squared = dx * dx + dy * dy;
if length_squared < 1e-10 {
return line_start.clone();
}
let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
let t_clamped = t.clamp(0.0, 1.0);
let closest_x = x1 + t_clamped * dx;
let closest_y = y1 + t_clamped * dy;
Array1::from_vec(vec![closest_x, closest_y])
}
}
impl Obstacle for PolygonObstacle {
fn distance(&self, point: &ArrayView1<f64>) -> f64 {
if point.len() != 2 || self.vertices.len() < 3 {
return 0.0; }
let boundary_distance = self.distance_to_polygon_boundary(point);
if self.is_point_inside(point) {
0.0
} else {
boundary_distance
}
}
fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64> {
if point.len() != 2 || self.vertices.len() < 3 {
return Array1::zeros(point.len()); }
let distance = self.distance_to_polygon_boundary(point);
if distance > config.influence_radius || self.is_point_inside(point) {
return Array1::zeros(point.len());
}
let closest_point = self.closest_point_on_boundary(point);
let direction_x = point[0] - closest_point[0];
let direction_y = point[1] - closest_point[1];
let direction_magnitude = (direction_x * direction_x + direction_y * direction_y).sqrt();
if direction_magnitude < 1e-10 {
return Array1::from_vec(vec![1.0, 0.0]) * config.repulsive_gain;
}
let unit_direction = Array1::from_vec(vec![
direction_x / direction_magnitude,
direction_y / direction_magnitude,
]);
let force_magnitude = if distance > 1e-6 {
config.repulsive_gain * (1.0 / distance - 1.0 / config.influence_radius)
} else {
config.repulsive_gain * 1000.0 };
unit_direction * force_magnitude.max(0.0)
}
}
pub struct PotentialFieldPlanner {
#[allow(dead_code)]
config: PotentialConfig,
obstacles: Vec<Box<dyn Obstacle>>,
dim: usize,
}
impl PotentialFieldPlanner {
pub fn new_2d(config: PotentialConfig) -> Self {
Self {
config,
obstacles: Vec::new(),
dim: 2,
}
}
pub fn add_circular_obstacle(&mut self, center: [f64; 2], radius: f64) {
let center_array = Array1::from_vec(center.to_vec());
self.obstacles
.push(Box::new(CircularObstacle::new(center_array, radius)));
}
pub fn add_polygon_obstacle(&mut self, vertices: Vec<[f64; 2]>) {
let vertices_array = vertices
.into_iter()
.map(|v| Array1::from_vec(v.to_vec()))
.collect();
self.obstacles
.push(Box::new(PolygonObstacle::new(vertices_array)));
}
fn attractive_force(&self, point: &Array1<f64>, goal: &Array1<f64>) -> Array1<f64> {
let diff = goal - point;
let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
let force_magnitude = self.config.attractive_gain * dist;
if dist < 1e-6 {
Array1::zeros(point.len())
} else {
let unit_vec = &diff / dist;
unit_vec * force_magnitude
}
}
fn repulsive_force(&self, point: &Array1<f64>) -> Array1<f64> {
let mut total_force = Array1::zeros(point.len());
for obstacle in &self.obstacles {
let force = obstacle.repulsive_force(&point.view(), &self.config);
total_force = total_force + force;
}
total_force
}
fn total_force(&self, point: &Array1<f64>, goal: &Array1<f64>) -> Array1<f64> {
let attractive = self.attractive_force(point, goal);
let repulsive = self.repulsive_force(point);
attractive + repulsive
}
fn distance(p1: &Array1<f64>, p2: &Array1<f64>) -> f64 {
let diff = p1 - p2;
diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
}
fn is_collision(&self, point: &Array1<f64>) -> bool {
for obstacle in &self.obstacles {
let dist = obstacle.distance(&point.view());
if dist < 1e-6 {
return true;
}
}
false
}
pub fn plan(
&self,
start: &Array1<f64>,
goal: &Array1<f64>,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
if start.len() != self.dim || goal.len() != self.dim {
return Err(SpatialError::DimensionError(format!(
"Start and goal dimensions must match the planner dimension ({})",
self.dim
)));
}
if self.is_collision(start) {
return Err(SpatialError::ValueError(
"Start position is in collision with obstacle".to_string(),
));
}
if self.is_collision(goal) {
return Err(SpatialError::ValueError(
"Goal position is in collision with obstacle".to_string(),
));
}
if self.config.use_fast_path && self.is_direct_path_clear(start, goal) {
let distance = PotentialFieldPlanner::distance(start, goal);
let path = Path::new(vec![start.clone(), goal.clone()], distance);
return Ok(Some(path));
}
let mut path_points = vec![start.clone()];
let mut current_pos = start.clone();
let mut total_distance = 0.0;
let mut iteration = 0;
let mut stuck_counter = 0;
let mut previous_pos = current_pos.clone();
while iteration < self.config.max_iterations {
iteration += 1;
let goal_distance = PotentialFieldPlanner::distance(¤t_pos, goal);
if goal_distance < self.config.goal_threshold {
path_points.push(goal.clone());
total_distance += PotentialFieldPlanner::distance(¤t_pos, goal);
let path = Path::new(path_points, total_distance);
return Ok(Some(path));
}
let force = self.total_force(¤t_pos, goal);
let force_magnitude = force.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
if force_magnitude < self.config.min_force_threshold {
let escape_success = self.escape_local_minimum(
&mut current_pos,
goal,
&mut path_points,
&mut total_distance,
);
if !escape_success {
break; }
stuck_counter = 0;
continue;
}
let force_unit = &force / force_magnitude;
let step = &force_unit * self.config.step_size;
let next_pos = ¤t_pos + &step;
if self.is_collision(&next_pos) {
let adjusted_pos = self.adjust_for_collision(¤t_pos, &step, &force_unit);
if self.is_collision(&adjusted_pos) {
let escape_success = self.escape_local_minimum(
&mut current_pos,
goal,
&mut path_points,
&mut total_distance,
);
if !escape_success {
break;
}
continue;
} else {
current_pos = adjusted_pos;
}
} else {
current_pos = next_pos;
}
let movement = PotentialFieldPlanner::distance(¤t_pos, &previous_pos);
if movement < self.config.step_size * 0.1 {
stuck_counter += 1;
if stuck_counter > 10 {
let escape_success = self.escape_local_minimum(
&mut current_pos,
goal,
&mut path_points,
&mut total_distance,
);
if !escape_success {
break;
}
stuck_counter = 0;
}
} else {
stuck_counter = 0;
}
total_distance += PotentialFieldPlanner::distance(&previous_pos, ¤t_pos);
path_points.push(current_pos.clone());
previous_pos = current_pos.clone();
}
if !path_points.is_empty() {
let path = Path::new(path_points, total_distance);
Ok(Some(path))
} else {
Ok(None)
}
}
fn is_direct_path_clear(&self, start: &Array1<f64>, goal: &Array1<f64>) -> bool {
let num_checks = 20;
for i in 0..=num_checks {
let t = i as f64 / num_checks as f64;
let point = start * (1.0 - t) + goal * t;
if self.is_collision(&point) {
return false;
}
}
true
}
fn escape_local_minimum(
&self,
current_pos: &mut Array1<f64>,
goal: &Array1<f64>,
path_points: &mut Vec<Array1<f64>>,
total_distance: &mut f64,
) -> bool {
use scirs2_core::random::{Rng, RngExt};
let mut rng = scirs2_core::random::rng();
for _ in 0..8 {
let mut random_direction = Array1::zeros(current_pos.len());
for i in 0..random_direction.len() {
random_direction[i] = rng.random_range(-1.0..1.0);
}
let magnitude = random_direction
.iter()
.map(|x| x.powi(2))
.sum::<f64>()
.sqrt();
if magnitude > 1e-6 {
random_direction /= magnitude;
}
let escape_step = random_direction * (self.config.step_size * 3.0);
let candidate_pos = &*current_pos + &escape_step;
if !self.is_collision(&candidate_pos) {
let old_goal_distance = PotentialFieldPlanner::distance(current_pos, goal);
let new_goal_distance = PotentialFieldPlanner::distance(&candidate_pos, goal);
if new_goal_distance <= old_goal_distance * 1.2 {
*total_distance += PotentialFieldPlanner::distance(current_pos, &candidate_pos);
*current_pos = candidate_pos;
path_points.push(current_pos.clone());
return true;
}
}
}
false }
fn adjust_for_collision(
&self,
current_pos: &Array1<f64>,
step: &Array1<f64>,
force_unit: &Array1<f64>,
) -> Array1<f64> {
let reduced_step = step * 0.5;
let candidate1 = current_pos + &reduced_step;
if !self.is_collision(&candidate1) {
return candidate1;
}
if current_pos.len() == 2 {
let perpendicular = Array1::from_vec(vec![-force_unit[1], force_unit[0]]);
let side_step = &perpendicular * self.config.step_size * 0.5;
let candidate2 = current_pos + &side_step;
if !self.is_collision(&candidate2) {
return candidate2;
}
let candidate3 = current_pos - &side_step;
if !self.is_collision(&candidate3) {
return candidate3;
}
}
current_pos.clone()
}
pub fn find_path(
&self,
start: &Array1<f64>,
goal: &Array1<f64>,
) -> SpatialResult<Option<Path<Array1<f64>>>> {
self.plan(start, goal)
}
}
pub struct PotentialField2DPlanner {
internal_planner: PotentialFieldPlanner,
}
impl PotentialField2DPlanner {
pub fn new(config: PotentialConfig) -> Self {
Self {
internal_planner: PotentialFieldPlanner::new_2d(config),
}
}
pub fn add_circular_obstacle(&mut self, center: [f64; 2], radius: f64) {
self.internal_planner.add_circular_obstacle(center, radius);
}
pub fn add_polygon_obstacle(&mut self, vertices: Vec<[f64; 2]>) {
self.internal_planner.add_polygon_obstacle(vertices);
}
pub fn plan(
&self,
start: [f64; 2],
goal: [f64; 2],
) -> SpatialResult<Option<Path<Array1<f64>>>> {
let start_array = Array1::from_vec(start.to_vec());
let goal_array = Array1::from_vec(goal.to_vec());
self.internal_planner.plan(&start_array, &goal_array)
}
}