scirs2_spatial/pathplanning/
potentialfield.rs1use num_traits::Float;
44use scirs2_core::ndarray::{Array1, ArrayView1};
45use crate::error::{SpatialError, SpatialResult};
50use crate::pathplanning::astar::Path;
51#[allow(dead_code)]
55type DistanceFn = Box<dyn Fn(&ArrayView1<f64>) -> f64>;
56
57#[derive(Debug, Clone)]
59pub struct PotentialConfig {
60 pub attractive_gain: f64,
62 pub repulsive_gain: f64,
64 pub influence_radius: f64,
66 pub step_size: f64,
68 pub max_iterations: usize,
70 pub seed: Option<u64>,
72 pub goal_threshold: f64,
74 pub use_fast_path: bool,
76 pub min_force_threshold: f64,
78}
79
80impl Default for PotentialConfig {
81 fn default() -> Self {
82 Self {
83 attractive_gain: 1.0,
84 repulsive_gain: 100.0,
85 influence_radius: 5.0,
86 step_size: 0.1,
87 max_iterations: 1000,
88 seed: None,
89 goal_threshold: 0.5,
90 use_fast_path: true,
91 min_force_threshold: 0.01,
92 }
93 }
94}
95
96impl PotentialConfig {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn with_attractive_gain(mut self, gain: f64) -> Self {
104 self.attractive_gain = gain;
105 self
106 }
107
108 pub fn with_repulsive_gain(mut self, gain: f64) -> Self {
110 self.repulsive_gain = gain;
111 self
112 }
113
114 pub fn with_influence_radius(mut self, radius: f64) -> Self {
116 self.influence_radius = radius;
117 self
118 }
119
120 pub fn with_step_size(mut self, stepsize: f64) -> Self {
122 self.step_size = stepsize;
123 self
124 }
125
126 pub fn with_max_iterations(mut self, maxiterations: usize) -> Self {
128 self.max_iterations = maxiterations;
129 self
130 }
131
132 pub fn with_min_force_threshold(mut self, threshold: f64) -> Self {
134 self.min_force_threshold = threshold;
135 self
136 }
137
138 pub fn with_seed(mut self, seed: u64) -> Self {
140 self.seed = Some(seed);
141 self
142 }
143
144 pub fn with_goal_threshold(mut self, threshold: f64) -> Self {
146 self.goal_threshold = threshold;
147 self
148 }
149
150 pub fn with_use_fast_path(mut self, use_fastpath: bool) -> Self {
152 self.use_fast_path = use_fastpath;
153 self
154 }
155}
156
157pub trait Obstacle {
159 fn distance(&self, point: &ArrayView1<f64>) -> f64;
161 fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64>;
163}
164
165pub struct CircularObstacle {
167 center: Array1<f64>,
168 radius: f64,
169}
170
171impl CircularObstacle {
172 pub fn new(center: Array1<f64>, radius: f64) -> Self {
174 Self { center, radius }
175 }
176}
177
178impl Obstacle for CircularObstacle {
179 fn distance(&self, point: &ArrayView1<f64>) -> f64 {
180 let diff = &self.center - point;
181 let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
182 (dist - self.radius).max(0.0)
183 }
184
185 fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64> {
186 let diff = point.to_owned() - &self.center;
187 let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
188
189 if dist <= self.radius || dist > config.influence_radius {
190 return Array1::zeros(point.len());
191 }
192
193 let force_magnitude = config.repulsive_gain * (1.0 / dist - 1.0 / config.influence_radius);
194 let unit_vec = &diff / dist;
195 unit_vec * force_magnitude
196 }
197}
198
199pub struct PolygonObstacle {
201 vertices: Vec<Array1<f64>>,
203}
204
205impl PolygonObstacle {
206 pub fn new(vertices: Vec<Array1<f64>>) -> Self {
208 Self { vertices }
209 }
210
211 fn is_point_inside(&self, point: &ArrayView1<f64>) -> bool {
213 if self.vertices.len() < 3 || point.len() != 2 {
214 return false; }
216
217 let px = point[0];
218 let py = point[1];
219 let mut inside = false;
220 let n = self.vertices.len();
221
222 for i in 0..n {
223 let j = (i + 1) % n;
224 let xi = self.vertices[i][0];
225 let yi = self.vertices[i][1];
226 let xj = self.vertices[j][0];
227 let yj = self.vertices[j][1];
228
229 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
230 inside = !inside;
231 }
232 }
233
234 inside
235 }
236
237 fn distance_to_polygon_boundary(&self, point: &ArrayView1<f64>) -> f64 {
239 if self.vertices.len() < 2 || point.len() != 2 {
240 return 0.0; }
242
243 let mut min_distance = f64::INFINITY;
244 let n = self.vertices.len();
245
246 for i in 0..n {
247 let j = (i + 1) % n;
248 let edge_dist =
249 self.distance_point_to_line_segment(point, &self.vertices[i], &self.vertices[j]);
250 min_distance = min_distance.min(edge_dist);
251 }
252
253 min_distance
254 }
255
256 fn distance_point_to_line_segment(
258 &self,
259 point: &ArrayView1<f64>,
260 line_start: &Array1<f64>,
261 line_end: &Array1<f64>,
262 ) -> f64 {
263 let px = point[0];
264 let py = point[1];
265 let x1 = line_start[0];
266 let y1 = line_start[1];
267 let x2 = line_end[0];
268 let y2 = line_end[1];
269
270 let dx = x2 - x1;
271 let dy = y2 - y1;
272 let length_squared = dx * dx + dy * dy;
273
274 if length_squared < 1e-10 {
275 let diff_x = px - x1;
277 let diff_y = py - y1;
278 return (diff_x * diff_x + diff_y * diff_y).sqrt();
279 }
280
281 let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
283 let t_clamped = t.clamp(0.0, 1.0);
284
285 let closest_x = x1 + t_clamped * dx;
287 let closest_y = y1 + t_clamped * dy;
288
289 let diff_x = px - closest_x;
291 let diff_y = py - closest_y;
292 (diff_x * diff_x + diff_y * diff_y).sqrt()
293 }
294
295 fn closest_point_on_boundary(&self, point: &ArrayView1<f64>) -> Array1<f64> {
297 if self.vertices.len() < 2 || point.len() != 2 {
298 return Array1::from_vec(vec![0.0, 0.0]);
299 }
300
301 let mut min_distance = f64::INFINITY;
302 let mut closest_point = Array1::from_vec(vec![0.0, 0.0]);
303 let n = self.vertices.len();
304
305 for i in 0..n {
306 let j = (i + 1) % n;
307 let edge_point =
308 self.closest_point_on_line_segment(point, &self.vertices[i], &self.vertices[j]);
309
310 let dist =
311 self.distance_point_to_line_segment(point, &self.vertices[i], &self.vertices[j]);
312 if dist < min_distance {
313 min_distance = dist;
314 closest_point = edge_point;
315 }
316 }
317
318 closest_point
319 }
320
321 fn closest_point_on_line_segment(
323 &self,
324 point: &ArrayView1<f64>,
325 line_start: &Array1<f64>,
326 line_end: &Array1<f64>,
327 ) -> Array1<f64> {
328 let px = point[0];
329 let py = point[1];
330 let x1 = line_start[0];
331 let y1 = line_start[1];
332 let x2 = line_end[0];
333 let y2 = line_end[1];
334
335 let dx = x2 - x1;
336 let dy = y2 - y1;
337 let length_squared = dx * dx + dy * dy;
338
339 if length_squared < 1e-10 {
340 return line_start.clone();
342 }
343
344 let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
346 let t_clamped = t.clamp(0.0, 1.0);
347
348 let closest_x = x1 + t_clamped * dx;
350 let closest_y = y1 + t_clamped * dy;
351
352 Array1::from_vec(vec![closest_x, closest_y])
353 }
354}
355
356impl Obstacle for PolygonObstacle {
357 fn distance(&self, point: &ArrayView1<f64>) -> f64 {
358 if point.len() != 2 || self.vertices.len() < 3 {
359 return 0.0; }
361
362 let boundary_distance = self.distance_to_polygon_boundary(point);
363
364 if self.is_point_inside(point) {
366 0.0
367 } else {
368 boundary_distance
369 }
370 }
371
372 fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64> {
373 if point.len() != 2 || self.vertices.len() < 3 {
374 return Array1::zeros(point.len()); }
376
377 let distance = self.distance_to_polygon_boundary(point);
378
379 if distance > config.influence_radius || self.is_point_inside(point) {
381 return Array1::zeros(point.len());
382 }
383
384 let closest_point = self.closest_point_on_boundary(point);
386
387 let direction_x = point[0] - closest_point[0];
389 let direction_y = point[1] - closest_point[1];
390 let direction_magnitude = (direction_x * direction_x + direction_y * direction_y).sqrt();
391
392 if direction_magnitude < 1e-10 {
393 return Array1::from_vec(vec![1.0, 0.0]) * config.repulsive_gain;
395 }
396
397 let unit_direction = Array1::from_vec(vec![
399 direction_x / direction_magnitude,
400 direction_y / direction_magnitude,
401 ]);
402
403 let force_magnitude = if distance > 1e-6 {
406 config.repulsive_gain * (1.0 / distance - 1.0 / config.influence_radius)
407 } else {
408 config.repulsive_gain * 1000.0 };
410
411 unit_direction * force_magnitude.max(0.0)
413 }
414}
415
416pub struct PotentialFieldPlanner {
418 #[allow(dead_code)]
420 config: PotentialConfig,
421 obstacles: Vec<Box<dyn Obstacle>>,
423 dim: usize,
425}
426
427impl PotentialFieldPlanner {
428 pub fn new_2d(config: PotentialConfig) -> Self {
430 Self {
431 config,
432 obstacles: Vec::new(),
433 dim: 2,
434 }
435 }
436
437 pub fn add_circular_obstacle(&mut self, center: [f64; 2], radius: f64) {
439 let center_array = Array1::from_vec(center.to_vec());
440 self.obstacles
441 .push(Box::new(CircularObstacle::new(center_array, radius)));
442 }
443
444 pub fn add_polygon_obstacle(&mut self, vertices: Vec<[f64; 2]>) {
446 let vertices_array = vertices
447 .into_iter()
448 .map(|v| Array1::from_vec(v.to_vec()))
449 .collect();
450 self.obstacles
451 .push(Box::new(PolygonObstacle::new(vertices_array)));
452 }
453
454 fn attractive_force(&self, point: &Array1<f64>, goal: &Array1<f64>) -> Array1<f64> {
456 let diff = goal - point;
457 let dist = diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
458 let force_magnitude = self.config.attractive_gain * dist;
459 if dist < 1e-6 {
460 Array1::zeros(point.len())
461 } else {
462 let unit_vec = &diff / dist;
463 unit_vec * force_magnitude
464 }
465 }
466
467 fn repulsive_force(&self, point: &Array1<f64>) -> Array1<f64> {
469 let mut total_force = Array1::zeros(point.len());
470 for obstacle in &self.obstacles {
471 let force = obstacle.repulsive_force(&point.view(), &self.config);
472 total_force = total_force + force;
473 }
474 total_force
475 }
476
477 fn total_force(&self, point: &Array1<f64>, goal: &Array1<f64>) -> Array1<f64> {
479 let attractive = self.attractive_force(point, goal);
480 let repulsive = self.repulsive_force(point);
481 attractive + repulsive
482 }
483
484 fn distance(p1: &Array1<f64>, p2: &Array1<f64>) -> f64 {
486 let diff = p1 - p2;
487 diff.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
488 }
489
490 fn is_collision(&self, point: &Array1<f64>) -> bool {
492 for obstacle in &self.obstacles {
493 let dist = obstacle.distance(&point.view());
495 if dist < 1e-6 {
496 return true;
497 }
498 }
499 false
500 }
501
502 pub fn plan(
504 &self,
505 start: &Array1<f64>,
506 goal: &Array1<f64>,
507 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
508 if start.len() != self.dim || goal.len() != self.dim {
510 return Err(SpatialError::DimensionError(format!(
511 "Start and goal dimensions must match the planner dimension ({})",
512 self.dim
513 )));
514 }
515
516 if self.is_collision(start) {
518 return Err(SpatialError::ValueError(
519 "Start position is in collision with obstacle".to_string(),
520 ));
521 }
522 if self.is_collision(goal) {
523 return Err(SpatialError::ValueError(
524 "Goal position is in collision with obstacle".to_string(),
525 ));
526 }
527
528 if self.config.use_fast_path && self.is_direct_path_clear(start, goal) {
530 let distance = PotentialFieldPlanner::distance(start, goal);
531 let path = Path::new(vec![start.clone(), goal.clone()], distance);
532 return Ok(Some(path));
533 }
534
535 let mut path_points = vec![start.clone()];
537 let mut current_pos = start.clone();
538 let mut total_distance = 0.0;
539 let mut iteration = 0;
540 let mut stuck_counter = 0;
541 let mut previous_pos = current_pos.clone();
542
543 while iteration < self.config.max_iterations {
544 iteration += 1;
545
546 let goal_distance = PotentialFieldPlanner::distance(¤t_pos, goal);
548 if goal_distance < self.config.goal_threshold {
549 path_points.push(goal.clone());
550 total_distance += PotentialFieldPlanner::distance(¤t_pos, goal);
551 let path = Path::new(path_points, total_distance);
552 return Ok(Some(path));
553 }
554
555 let force = self.total_force(¤t_pos, goal);
557 let force_magnitude = force.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
558
559 if force_magnitude < self.config.min_force_threshold {
561 let escape_success = self.escape_local_minimum(
563 &mut current_pos,
564 goal,
565 &mut path_points,
566 &mut total_distance,
567 );
568 if !escape_success {
569 break; }
571 stuck_counter = 0;
572 continue;
573 }
574
575 let force_unit = &force / force_magnitude;
577 let step = &force_unit * self.config.step_size;
578 let next_pos = ¤t_pos + &step;
579
580 if self.is_collision(&next_pos) {
582 let adjusted_pos = self.adjust_for_collision(¤t_pos, &step, &force_unit);
584 if self.is_collision(&adjusted_pos) {
585 let escape_success = self.escape_local_minimum(
587 &mut current_pos,
588 goal,
589 &mut path_points,
590 &mut total_distance,
591 );
592 if !escape_success {
593 break;
594 }
595 continue;
596 } else {
597 current_pos = adjusted_pos;
598 }
599 } else {
600 current_pos = next_pos;
601 }
602
603 let movement = PotentialFieldPlanner::distance(¤t_pos, &previous_pos);
605 if movement < self.config.step_size * 0.1 {
606 stuck_counter += 1;
607 if stuck_counter > 10 {
608 let escape_success = self.escape_local_minimum(
610 &mut current_pos,
611 goal,
612 &mut path_points,
613 &mut total_distance,
614 );
615 if !escape_success {
616 break;
617 }
618 stuck_counter = 0;
619 }
620 } else {
621 stuck_counter = 0;
622 }
623
624 total_distance += PotentialFieldPlanner::distance(&previous_pos, ¤t_pos);
626 path_points.push(current_pos.clone());
627 previous_pos = current_pos.clone();
628 }
629
630 if !path_points.is_empty() {
632 let path = Path::new(path_points, total_distance);
633 Ok(Some(path))
634 } else {
635 Ok(None)
636 }
637 }
638
639 fn is_direct_path_clear(&self, start: &Array1<f64>, goal: &Array1<f64>) -> bool {
641 let num_checks = 20;
642 for i in 0..=num_checks {
643 let t = i as f64 / num_checks as f64;
644 let point = start * (1.0 - t) + goal * t;
645 if self.is_collision(&point) {
646 return false;
647 }
648 }
649 true
650 }
651
652 fn escape_local_minimum(
654 &self,
655 current_pos: &mut Array1<f64>,
656 goal: &Array1<f64>,
657 path_points: &mut Vec<Array1<f64>>,
658 total_distance: &mut f64,
659 ) -> bool {
660 use scirs2_core::random::Rng;
661 let mut rng = rand::rng();
662
663 for _ in 0..8 {
665 let mut random_direction = Array1::zeros(current_pos.len());
666 for i in 0..random_direction.len() {
667 random_direction[i] = rng.gen_range(-1.0..1.0);
668 }
669
670 let magnitude = random_direction
672 .iter()
673 .map(|x| x.powi(2))
674 .sum::<f64>()
675 .sqrt();
676 if magnitude > 1e-6 {
677 random_direction /= magnitude;
678 }
679
680 let escape_step = random_direction * (self.config.step_size * 3.0);
682 let candidate_pos = &*current_pos + &escape_step;
683
684 if !self.is_collision(&candidate_pos) {
686 let old_goal_distance = PotentialFieldPlanner::distance(current_pos, goal);
687 let new_goal_distance = PotentialFieldPlanner::distance(&candidate_pos, goal);
688
689 if new_goal_distance <= old_goal_distance * 1.2 {
691 *total_distance += PotentialFieldPlanner::distance(current_pos, &candidate_pos);
692 *current_pos = candidate_pos;
693 path_points.push(current_pos.clone());
694 return true;
695 }
696 }
697 }
698
699 false }
701
702 fn adjust_for_collision(
704 &self,
705 current_pos: &Array1<f64>,
706 step: &Array1<f64>,
707 force_unit: &Array1<f64>,
708 ) -> Array1<f64> {
709 let reduced_step = step * 0.5;
711 let candidate1 = current_pos + &reduced_step;
712 if !self.is_collision(&candidate1) {
713 return candidate1;
714 }
715
716 if current_pos.len() == 2 {
718 let perpendicular = Array1::from_vec(vec![-force_unit[1], force_unit[0]]);
720 let side_step = &perpendicular * self.config.step_size * 0.5;
721
722 let candidate2 = current_pos + &side_step;
723 if !self.is_collision(&candidate2) {
724 return candidate2;
725 }
726
727 let candidate3 = current_pos - &side_step;
729 if !self.is_collision(&candidate3) {
730 return candidate3;
731 }
732 }
733
734 current_pos.clone()
736 }
737
738 pub fn find_path(
740 &self,
741 start: &Array1<f64>,
742 goal: &Array1<f64>,
743 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
744 self.plan(start, goal)
745 }
746}
747
748pub struct PotentialField2DPlanner {
750 internal_planner: PotentialFieldPlanner,
751}
752
753impl PotentialField2DPlanner {
754 pub fn new(config: PotentialConfig) -> Self {
756 Self {
757 internal_planner: PotentialFieldPlanner::new_2d(config),
758 }
759 }
760
761 pub fn add_circular_obstacle(&mut self, center: [f64; 2], radius: f64) {
763 self.internal_planner.add_circular_obstacle(center, radius);
764 }
765
766 pub fn add_polygon_obstacle(&mut self, vertices: Vec<[f64; 2]>) {
768 self.internal_planner.add_polygon_obstacle(vertices);
769 }
770
771 pub fn plan(
773 &self,
774 start: [f64; 2],
775 goal: [f64; 2],
776 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
777 let start_array = Array1::from_vec(start.to_vec());
778 let goal_array = Array1::from_vec(goal.to_vec());
779 self.internal_planner.plan(&start_array, &goal_array)
780 }
781}