scirs2_spatial/pathplanning/
potentialfield.rs

1//! Potential Field implementation for pathfinding with obstacles
2//!
3//! This module provides an implementation of the Potential Field algorithm
4//! for path planning. It creates an artificial potential field where obstacles
5//! generate repulsive forces and the goal generates an attractive force.
6//! The agent moves along the gradient of this potential field to reach the goal.
7//!
8//! # Examples
9//!
10//! ```
11//! use ndarray::Array1;
12//! use scirs2_spatial::pathplanning::{PotentialFieldPlanner, PotentialConfig};
13//!
14//! // Create a configuration for the potential field planner
15//! let config = PotentialConfig::new()
16//!     .with_attractive_gain(1.0)
17//!     .with_repulsive_gain(100.0)
18//!     .with_influence_radius(5.0)
19//!     .with_step_size(0.1)
20//!     .with_max_iterations(1000);
21//!
22//! // Create a potential field planner
23//! let mut planner = PotentialFieldPlanner::new_2d(config);
24//!
25//! // Add circular obstacles
26//! planner.add_circular_obstacle([5.0, 5.0], 2.0);
27//!
28//! // Find a path from start to goal
29//! let start = Array1::from_vec(vec![1.0, 1.0]);
30//! let goal = Array1::from_vec(vec![9.0, 9.0]);
31//!
32//! let path = planner.find_path(&start, &goal);
33//!
34//! match path {
35//!     Ok(Some(path)) => {
36//!         println!("Path found with {} points", path.nodes.len());
37//!     },
38//!     Ok(None) => println!("No path found"),
39//!     Err(e) => println!("Error: {}", e),
40//! }
41//! ```
42
43use ndarray::{Array1, ArrayView1};
44use num_traits::Float;
45// use rand::rngs::StdRng;
46// use rand::{Rng, SeedableRng};
47// use std::collections::HashMap;
48
49use crate::error::{SpatialError, SpatialResult};
50use crate::pathplanning::astar::Path;
51// use crate::transform::rigid_transform::RigidTransform;
52
53/// Type alias for distance calculation function
54#[allow(dead_code)]
55type DistanceFn = Box<dyn Fn(&ArrayView1<f64>) -> f64>;
56
57/// Configuration for potential field pathfinding
58#[derive(Debug, Clone)]
59pub struct PotentialConfig {
60    /// Gain parameter for attractive forces (goal)
61    pub attractive_gain: f64,
62    /// Gain parameter for repulsive forces (obstacles)
63    pub repulsive_gain: f64,
64    /// Influence radius for obstacles
65    pub influence_radius: f64,
66    /// Maximum step size for path following
67    pub step_size: f64,
68    /// Maximum number of iterations
69    pub max_iterations: usize,
70    /// Random seed for any stochastic components
71    pub seed: Option<u64>,
72    /// Goal threshold - how close to consider goal reached
73    pub goal_threshold: f64,
74    /// Fast path option - try direct path first
75    pub use_fast_path: bool,
76    /// Minimum force threshold for detecting local minima
77    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    /// Create a new default potential field configuration
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Set the attractive gain (force towards goal)
103    pub fn with_attractive_gain(mut self, gain: f64) -> Self {
104        self.attractive_gain = gain;
105        self
106    }
107
108    /// Set the repulsive gain (force away from obstacles)
109    pub fn with_repulsive_gain(mut self, gain: f64) -> Self {
110        self.repulsive_gain = gain;
111        self
112    }
113
114    /// Set the influence radius of obstacles
115    pub fn with_influence_radius(mut self, radius: f64) -> Self {
116        self.influence_radius = radius;
117        self
118    }
119
120    /// Set the step size for path following
121    pub fn with_step_size(mut self, stepsize: f64) -> Self {
122        self.step_size = stepsize;
123        self
124    }
125
126    /// Set the maximum number of iterations
127    pub fn with_max_iterations(mut self, maxiterations: usize) -> Self {
128        self.max_iterations = maxiterations;
129        self
130    }
131
132    /// Set the minimum force threshold for detecting local minima
133    pub fn with_min_force_threshold(mut self, threshold: f64) -> Self {
134        self.min_force_threshold = threshold;
135        self
136    }
137
138    /// Set random seed
139    pub fn with_seed(mut self, seed: u64) -> Self {
140        self.seed = Some(seed);
141        self
142    }
143
144    /// Set goal threshold distance
145    pub fn with_goal_threshold(mut self, threshold: f64) -> Self {
146        self.goal_threshold = threshold;
147        self
148    }
149
150    /// Enable/disable fast path option
151    pub fn with_use_fast_path(mut self, use_fastpath: bool) -> Self {
152        self.use_fast_path = use_fastpath;
153        self
154    }
155}
156
157/// Obstacle trait for potential field planning
158pub trait Obstacle {
159    /// Calculate the distance from a point to the obstacle
160    fn distance(&self, point: &ArrayView1<f64>) -> f64;
161    /// Calculate the repulsive force at a point
162    fn repulsive_force(&self, point: &ArrayView1<f64>, config: &PotentialConfig) -> Array1<f64>;
163}
164
165/// Circular obstacle representation
166pub struct CircularObstacle {
167    center: Array1<f64>,
168    radius: f64,
169}
170
171impl CircularObstacle {
172    /// Create a new circular obstacle
173    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
199/// Polygon obstacle representation
200pub struct PolygonObstacle {
201    /// Vertices of the polygon
202    vertices: Vec<Array1<f64>>,
203}
204
205impl PolygonObstacle {
206    /// Create a new polygon obstacle
207    pub fn new(vertices: Vec<Array1<f64>>) -> Self {
208        Self { vertices }
209    }
210
211    /// Check if a point is inside the polygon using the ray casting algorithm
212    fn is_point_inside(&self, point: &ArrayView1<f64>) -> bool {
213        if self.vertices.len() < 3 || point.len() != 2 {
214            return false; // Only support 2D polygons
215        }
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    /// Calculate the minimum distance from a point to any edge of the polygon
238    fn distance_to_polygon_boundary(&self, point: &ArrayView1<f64>) -> f64 {
239        if self.vertices.len() < 2 || point.len() != 2 {
240            return 0.0; // Only support 2D polygons
241        }
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    /// Calculate the distance from a point to a line segment
257    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            // Line segment is actually a point
276            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        // Parameter t represents position along the line segment
282        let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
283        let t_clamped = t.clamp(0.0, 1.0);
284
285        // Find the closest point on the line segment
286        let closest_x = x1 + t_clamped * dx;
287        let closest_y = y1 + t_clamped * dy;
288
289        // Calculate distance to closest point
290        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    /// Find the closest point on the polygon boundary to the given point
296    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    /// Find the closest point on a line segment to the given point
322    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            // Line segment is actually a point
341            return line_start.clone();
342        }
343
344        // Parameter t represents position along the line segment
345        let t = ((px - x1) * dx + (py - y1) * dy) / length_squared;
346        let t_clamped = t.clamp(0.0, 1.0);
347
348        // Find the closest point on the line segment
349        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; // Only support 2D polygons
360        }
361
362        let boundary_distance = self.distance_to_polygon_boundary(point);
363
364        // If point is inside the polygon, return 0 (collision)
365        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()); // Only support 2D polygons
375        }
376
377        let distance = self.distance_to_polygon_boundary(point);
378
379        // No force if point is too far away or inside the polygon
380        if distance > config.influence_radius || self.is_point_inside(point) {
381            return Array1::zeros(point.len());
382        }
383
384        // Find the closest point on the polygon boundary
385        let closest_point = self.closest_point_on_boundary(point);
386
387        // Calculate direction from closest point to the point (repulsive direction)
388        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            // Point is exactly on the boundary, push in arbitrary direction
394            return Array1::from_vec(vec![1.0, 0.0]) * config.repulsive_gain;
395        }
396
397        // Normalize direction vector
398        let unit_direction = Array1::from_vec(vec![
399            direction_x / direction_magnitude,
400            direction_y / direction_magnitude,
401        ]);
402
403        // Calculate force magnitude based on distance
404        // Force increases as distance decreases, following potential field formula
405        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 // Very large force when very close
409        };
410
411        // Apply force in the repulsive direction
412        unit_direction * force_magnitude.max(0.0)
413    }
414}
415
416/// Generic potential field planner for n-dimensional space
417pub struct PotentialFieldPlanner {
418    /// Configuration for the planner
419    #[allow(dead_code)]
420    config: PotentialConfig,
421    /// List of obstacles in the environment
422    obstacles: Vec<Box<dyn Obstacle>>,
423    /// Dimensionality of the planning space
424    dim: usize,
425}
426
427impl PotentialFieldPlanner {
428    /// Create a new 2D potential field planner
429    pub fn new_2d(config: PotentialConfig) -> Self {
430        Self {
431            config,
432            obstacles: Vec::new(),
433            dim: 2,
434        }
435    }
436
437    /// Add a circular obstacle
438    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    /// Add a polygon obstacle
445    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    /// Calculate the attractive force towards the goal
455    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    /// Calculate the total repulsive force from all obstacles
468    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    /// Calculate the total force (attractive + repulsive) at a point
478    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    /// Calculate the distance between two points
485    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    /// Check if the point is in collision with any obstacle
491    fn is_collision(&self, point: &Array1<f64>) -> bool {
492        for obstacle in &self.obstacles {
493            // Use distance to check collision - if distance is very small, consider it inside
494            let dist = obstacle.distance(&point.view());
495            if dist < 1e-6 {
496                return true;
497            }
498        }
499        false
500    }
501
502    /// Find a path from start to goal using potential field method
503    pub fn plan(
504        &self,
505        start: &Array1<f64>,
506        goal: &Array1<f64>,
507    ) -> SpatialResult<Option<Path<Array1<f64>>>> {
508        // Validate dimensions
509        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        // Check if start or goal are in collision
517        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        // Try fast path first if enabled
529        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        // Initialize path tracking
536        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            // Check if goal is reached
547            let goal_distance = PotentialFieldPlanner::distance(&current_pos, goal);
548            if goal_distance < self.config.goal_threshold {
549                path_points.push(goal.clone());
550                total_distance += PotentialFieldPlanner::distance(&current_pos, goal);
551                let path = Path::new(path_points, total_distance);
552                return Ok(Some(path));
553            }
554
555            // Calculate total force at current position
556            let force = self.total_force(&current_pos, goal);
557            let force_magnitude = force.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
558
559            // Check for local minimum (very small force)
560            if force_magnitude < self.config.min_force_threshold {
561                // Try to escape local minimum by adding random perturbation
562                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; // Could not escape local minimum
570                }
571                stuck_counter = 0;
572                continue;
573            }
574
575            // Normalize force and take a step
576            let force_unit = &force / force_magnitude;
577            let step = &force_unit * self.config.step_size;
578            let next_pos = &current_pos + &step;
579
580            // Check if next position is in collision
581            if self.is_collision(&next_pos) {
582                // Try to move along the boundary or find alternative direction
583                let adjusted_pos = self.adjust_for_collision(&current_pos, &step, &force_unit);
584                if self.is_collision(&adjusted_pos) {
585                    // Still in collision, try escape mechanism
586                    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            // Check if we're stuck (moving very little)
604            let movement = PotentialFieldPlanner::distance(&current_pos, &previous_pos);
605            if movement < self.config.step_size * 0.1 {
606                stuck_counter += 1;
607                if stuck_counter > 10 {
608                    // Try escape mechanism
609                    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            // Add point to path and update distance
625            total_distance += PotentialFieldPlanner::distance(&previous_pos, &current_pos);
626            path_points.push(current_pos.clone());
627            previous_pos = current_pos.clone();
628        }
629
630        // Return partial path if we couldn't reach the goal
631        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    /// Check if a direct path from start to goal is clear of obstacles
640    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    /// Attempt to escape a local minimum by trying alternative directions
653    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 rand::Rng;
661        let mut rng = rand::rng();
662
663        // Try multiple random directions
664        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            // Normalize the random direction
671            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            // Try a larger step in the random direction
681            let escape_step = random_direction * (self.config.step_size * 3.0);
682            let candidate_pos = &*current_pos + &escape_step;
683
684            // Check if this position is valid and makes progress toward goal
685            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                // Accept if it gets us closer to the goal or at least doesn't move us much farther
690                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 // Could not escape
700    }
701
702    /// Adjust movement direction to avoid collision
703    fn adjust_for_collision(
704        &self,
705        current_pos: &Array1<f64>,
706        step: &Array1<f64>,
707        force_unit: &Array1<f64>,
708    ) -> Array1<f64> {
709        // Try moving with a smaller step
710        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        // Try moving perpendicular to the force direction (wall following)
717        if current_pos.len() == 2 {
718            // For 2D, rotate force vector by 90 degrees
719            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            // Try the other perpendicular direction
728            let candidate3 = current_pos - &side_step;
729            if !self.is_collision(&candidate3) {
730                return candidate3;
731            }
732        }
733
734        // If all else fails, return current position (no movement)
735        current_pos.clone()
736    }
737
738    /// Alias for plan() for API compatibility
739    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
748/// Specialized 2D potential field planner
749pub struct PotentialField2DPlanner {
750    internal_planner: PotentialFieldPlanner,
751}
752
753impl PotentialField2DPlanner {
754    /// Create a new 2D potential field planner
755    pub fn new(config: PotentialConfig) -> Self {
756        Self {
757            internal_planner: PotentialFieldPlanner::new_2d(config),
758        }
759    }
760
761    /// Add a circular obstacle
762    pub fn add_circular_obstacle(&mut self, center: [f64; 2], radius: f64) {
763        self.internal_planner.add_circular_obstacle(center, radius);
764    }
765
766    /// Add a polygon obstacle
767    pub fn add_polygon_obstacle(&mut self, vertices: Vec<[f64; 2]>) {
768        self.internal_planner.add_polygon_obstacle(vertices);
769    }
770
771    /// Find a path from start to goal
772    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}