Skip to main content

rust_robotics_planning/
bug_planning.rs

1//! Bug algorithm family for path planning.
2//!
3//! Simple reactive planners that move greedily toward the goal on an integer
4//! grid and follow obstacle boundaries when blocked.
5//!
6//! Three variants are provided:
7//!
8//! - **Bug0** -- greedy move toward goal; on hitting an obstacle, follow the
9//!   boundary until the direct path is clear again.
10//! - **Bug1** -- circumnavigate the entire obstacle, record the closest point
11//!   to the goal, backtrack to that point, then resume greedy motion.
12//! - **Bug2** -- precompute the start-goal line; follow the obstacle boundary
13//!   until re-intersecting that line closer to the goal.
14//!
15//! Reference: <https://web.archive.org/web/20201103052224/https://sites.google.com/site/ece452bugalgorithms/>
16
17use std::collections::HashSet;
18
19/// Which Bug algorithm variant to use.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum BugVariant {
22    Bug0,
23    Bug1,
24    Bug2,
25}
26
27/// Configuration for the Bug planner.
28#[derive(Debug, Clone)]
29pub struct BugConfig {
30    /// Algorithm variant.
31    pub variant: BugVariant,
32    /// Maximum number of iterations (safety limit).
33    pub max_iterations: usize,
34}
35
36impl Default for BugConfig {
37    fn default() -> Self {
38        Self {
39            variant: BugVariant::Bug0,
40            max_iterations: 1_000_000,
41        }
42    }
43}
44
45/// Result of a Bug planning run.
46#[derive(Debug, Clone)]
47pub struct BugResult {
48    /// X coordinates of the path.
49    pub path_x: Vec<f64>,
50    /// Y coordinates of the path.
51    pub path_y: Vec<f64>,
52}
53
54/// Bug family path planner operating on an integer grid.
55///
56/// Obstacles are specified as a set of occupied grid cells. The planner
57/// precomputes the *boundary* cells (free cells that are 8-connected
58/// neighbours of at least one obstacle cell).
59pub struct BugPlanner {
60    goal_x: f64,
61    goal_y: f64,
62    obs: HashSet<(i64, i64)>,
63    boundary: HashSet<(i64, i64)>,
64    path_x: Vec<f64>,
65    path_y: Vec<f64>,
66}
67
68impl BugPlanner {
69    /// Create a new planner instance.
70    ///
71    /// `obs_x` / `obs_y` are the coordinates of every occupied grid cell.
72    pub fn new(
73        start_x: f64,
74        start_y: f64,
75        goal_x: f64,
76        goal_y: f64,
77        obs_x: &[f64],
78        obs_y: &[f64],
79    ) -> Self {
80        let obs: HashSet<(i64, i64)> = obs_x
81            .iter()
82            .zip(obs_y.iter())
83            .map(|(&x, &y)| (x as i64, y as i64))
84            .collect();
85
86        // Boundary = free cells that are 8-connected to an obstacle cell.
87        let dirs: [(i64, i64); 8] = [
88            (1, 1),
89            (0, 1),
90            (-1, 1),
91            (-1, 0),
92            (-1, -1),
93            (0, -1),
94            (1, -1),
95            (1, 0),
96        ];
97        let mut boundary = HashSet::new();
98        for &(ox, oy) in &obs {
99            for &(dx, dy) in &dirs {
100                let cx = ox + dx;
101                let cy = oy + dy;
102                if !obs.contains(&(cx, cy)) {
103                    boundary.insert((cx, cy));
104                }
105            }
106        }
107
108        Self {
109            goal_x,
110            goal_y,
111            obs,
112            boundary,
113            path_x: vec![start_x],
114            path_y: vec![start_y],
115        }
116    }
117
118    /// Run the planner with the given configuration.
119    pub fn plan(mut self, config: &BugConfig) -> Option<BugResult> {
120        let ok = match config.variant {
121            BugVariant::Bug0 => self.bug0(config.max_iterations),
122            BugVariant::Bug1 => self.bug1(config.max_iterations),
123            BugVariant::Bug2 => self.bug2(config.max_iterations),
124        };
125        if ok {
126            Some(BugResult {
127                path_x: self.path_x,
128                path_y: self.path_y,
129            })
130        } else {
131            None
132        }
133    }
134
135    // ---- helpers ----
136
137    /// Current position (last entry in path).
138    fn cur(&self) -> (f64, f64) {
139        (*self.path_x.last().unwrap(), *self.path_y.last().unwrap())
140    }
141
142    /// Greedy step toward the goal (sign-based, up to 1 cell per axis).
143    fn mov_normal(&self) -> (f64, f64) {
144        let (cx, cy) = self.cur();
145        (cx + sign(self.goal_x - cx), cy + sign(self.goal_y - cy))
146    }
147
148    /// Move to the next unvisited boundary cell (4-connected neighbours).
149    /// Returns `(x, y, back_to_start)`. If no unvisited boundary neighbour
150    /// is found, the robot stays in place and `back_to_start` is `true`.
151    fn mov_to_next_obs(&self, visited: &HashSet<(i64, i64)>) -> (f64, f64, bool) {
152        let (cx, cy) = self.cur();
153        let cx = cx as i64;
154        let cy = cy as i64;
155        for &(dx, dy) in &[(1i64, 0i64), (0, 1), (-1, 0), (0, -1)] {
156            let nx = cx + dx;
157            let ny = cy + dy;
158            if self.boundary.contains(&(nx, ny)) && !visited.contains(&(nx, ny)) {
159                return (nx as f64, ny as f64, false);
160            }
161        }
162        (cx as f64, cy as f64, true)
163    }
164
165    fn at_goal(&self) -> bool {
166        let (cx, cy) = self.cur();
167        cx == self.goal_x && cy == self.goal_y
168    }
169
170    fn push(&mut self, x: f64, y: f64) {
171        self.path_x.push(x);
172        self.path_y.push(y);
173    }
174
175    /// Is (x,y) on the obstacle boundary?
176    fn is_boundary(&self, x: f64, y: f64) -> bool {
177        self.boundary.contains(&(x as i64, y as i64))
178    }
179
180    /// Is (x,y) an obstacle cell?
181    fn is_obs(&self, x: f64, y: f64) -> bool {
182        self.obs.contains(&(x as i64, y as i64))
183    }
184
185    // ---- Bug0 ----
186
187    fn bug0(&mut self, max_iter: usize) -> bool {
188        let mut mode = if self.is_boundary(self.cur().0, self.cur().1) {
189            Mode::Obs
190        } else {
191            Mode::Normal
192        };
193        let mut visited = HashSet::new();
194
195        for _ in 0..max_iter {
196            if self.at_goal() {
197                return true;
198            }
199
200            let (cand_x, cand_y) = if mode == Mode::Normal {
201                self.mov_normal()
202            } else {
203                let (x, y, _) = self.mov_to_next_obs(&visited);
204                (x, y)
205            };
206
207            match mode {
208                Mode::Normal => {
209                    if self.is_boundary(cand_x, cand_y) {
210                        self.push(cand_x, cand_y);
211                        visited.clear();
212                        visited.insert((cand_x as i64, cand_y as i64));
213                        mode = Mode::Obs;
214                    } else {
215                        self.push(cand_x, cand_y);
216                    }
217                }
218                Mode::Obs => {
219                    let (nx, ny) = self.mov_normal();
220                    if !self.is_obs(nx, ny) {
221                        mode = Mode::Normal;
222                    } else {
223                        self.push(cand_x, cand_y);
224                        visited.insert((cand_x as i64, cand_y as i64));
225                    }
226                }
227            }
228        }
229        false
230    }
231
232    // ---- Bug1 ----
233
234    fn bug1(&mut self, max_iter: usize) -> bool {
235        let mut mode = if self.is_boundary(self.cur().0, self.cur().1) {
236            Mode::Obs
237        } else {
238            Mode::Normal
239        };
240        let mut visited = HashSet::new();
241        let mut dist = f64::INFINITY;
242        let mut exit_x = f64::NEG_INFINITY;
243        let mut exit_y = f64::NEG_INFINITY;
244        let mut back_to_start;
245        let mut second_round = false;
246
247        for _ in 0..max_iter {
248            if self.at_goal() {
249                return true;
250            }
251
252            let (cand_x, cand_y) = if mode == Mode::Normal {
253                let c = self.mov_normal();
254                back_to_start = false;
255                c
256            } else {
257                let (x, y, b) = self.mov_to_next_obs(&visited);
258                back_to_start = b;
259                (x, y)
260            };
261
262            match mode {
263                Mode::Normal => {
264                    if self.is_boundary(cand_x, cand_y) {
265                        self.push(cand_x, cand_y);
266                        visited.clear();
267                        visited.insert((cand_x as i64, cand_y as i64));
268                        mode = Mode::Obs;
269                        dist = f64::INFINITY;
270                        second_round = false;
271                    } else {
272                        self.push(cand_x, cand_y);
273                    }
274                }
275                Mode::Obs => {
276                    let d =
277                        ((cand_x - self.goal_x).powi(2) + (cand_y - self.goal_y).powi(2)).sqrt();
278                    if d < dist && !second_round {
279                        exit_x = cand_x;
280                        exit_y = cand_y;
281                        dist = d;
282                    }
283                    if back_to_start && !second_round {
284                        second_round = true;
285                        // Remove the first-round boundary trace; we will
286                        // re-walk to exit_x/exit_y.
287                        let n = visited.len();
288                        let len = self.path_x.len();
289                        self.path_x.truncate(len.saturating_sub(n));
290                        self.path_y.truncate(len.saturating_sub(n));
291                        visited.clear();
292                    }
293                    self.push(cand_x, cand_y);
294                    visited.insert((cand_x as i64, cand_y as i64));
295                    if cand_x == exit_x && cand_y == exit_y && second_round {
296                        mode = Mode::Normal;
297                    }
298                }
299            }
300        }
301        false
302    }
303
304    // ---- Bug2 ----
305
306    fn bug2(&mut self, max_iter: usize) -> bool {
307        // Precompute the start-goal line and find boundary hit points.
308        let (sx, sy) = self.cur();
309        let mut hit_points: Vec<(f64, f64)> = Vec::new();
310        {
311            let mut lx = sx;
312            let mut ly = sy;
313            loop {
314                if lx == self.goal_x && ly == self.goal_y {
315                    break;
316                }
317                let cx = lx + sign(self.goal_x - lx);
318                let cy = ly + sign(self.goal_y - ly);
319                if self.is_boundary(cx, cy) {
320                    hit_points.push((cx, cy));
321                }
322                lx = cx;
323                ly = cy;
324            }
325        }
326
327        let mut mode = if self.is_boundary(sx, sy) {
328            Mode::Obs
329        } else {
330            Mode::Normal
331        };
332        let mut visited = HashSet::new();
333
334        for _ in 0..max_iter {
335            if self.at_goal() {
336                return true;
337            }
338
339            let (cand_x, cand_y) = if mode == Mode::Normal {
340                self.mov_normal()
341            } else {
342                let (x, y, _) = self.mov_to_next_obs(&visited);
343                (x, y)
344            };
345
346            match mode {
347                Mode::Normal => {
348                    if self.is_boundary(cand_x, cand_y) {
349                        self.push(cand_x, cand_y);
350                        visited.clear();
351                        visited.insert((cand_x as i64, cand_y as i64));
352                        // Remove the first matching hit point.
353                        if let Some(idx) = hit_points
354                            .iter()
355                            .position(|&(hx, hy)| hx == cand_x && hy == cand_y)
356                        {
357                            hit_points.remove(idx);
358                        }
359                        mode = Mode::Obs;
360                    } else {
361                        self.push(cand_x, cand_y);
362                    }
363                }
364                Mode::Obs => {
365                    self.push(cand_x, cand_y);
366                    visited.insert((cand_x as i64, cand_y as i64));
367                    if let Some(idx) = hit_points
368                        .iter()
369                        .position(|&(hx, hy)| hx == cand_x && hy == cand_y)
370                    {
371                        hit_points.remove(idx);
372                        mode = Mode::Normal;
373                    }
374                }
375            }
376        }
377        false
378    }
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382enum Mode {
383    Normal,
384    Obs,
385}
386
387/// Signum returning -1.0, 0.0, or 1.0.
388fn sign(v: f64) -> f64 {
389    if v > 0.0 {
390        1.0
391    } else if v < 0.0 {
392        -1.0
393    } else {
394        0.0
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    /// Build the same obstacle field used in the PythonRobotics reference.
403    fn build_obstacles() -> (Vec<f64>, Vec<f64>) {
404        let mut ox = Vec::new();
405        let mut oy = Vec::new();
406        let ranges: &[(i64, i64, i64, i64)] = &[
407            (20, 40, 20, 40),
408            (60, 100, 40, 80),
409            (120, 140, 80, 100),
410            (80, 140, 0, 20),
411            (0, 20, 60, 100),
412            (20, 40, 80, 100),
413            (120, 160, 40, 60),
414        ];
415        for &(x0, x1, y0, y1) in ranges {
416            for i in x0..x1 {
417                for j in y0..y1 {
418                    ox.push(i as f64);
419                    oy.push(j as f64);
420                }
421            }
422        }
423        (ox, oy)
424    }
425
426    #[test]
427    fn test_bug0_reaches_goal() {
428        let (ox, oy) = build_obstacles();
429        let planner = BugPlanner::new(0.0, 0.0, 167.0, 50.0, &ox, &oy);
430        let config = BugConfig {
431            variant: BugVariant::Bug0,
432            ..Default::default()
433        };
434        let result = planner.plan(&config).expect("Bug0 should find a path");
435        assert_eq!(*result.path_x.last().unwrap(), 167.0);
436        assert_eq!(*result.path_y.last().unwrap(), 50.0);
437        assert!(result.path_x.len() > 2);
438    }
439
440    #[test]
441    fn test_bug1_reaches_goal() {
442        let (ox, oy) = build_obstacles();
443        let planner = BugPlanner::new(0.0, 0.0, 167.0, 50.0, &ox, &oy);
444        let config = BugConfig {
445            variant: BugVariant::Bug1,
446            ..Default::default()
447        };
448        let result = planner.plan(&config).expect("Bug1 should find a path");
449        assert_eq!(*result.path_x.last().unwrap(), 167.0);
450        assert_eq!(*result.path_y.last().unwrap(), 50.0);
451    }
452
453    #[test]
454    fn test_bug2_reaches_goal() {
455        let (ox, oy) = build_obstacles();
456        let planner = BugPlanner::new(0.0, 0.0, 167.0, 50.0, &ox, &oy);
457        let config = BugConfig {
458            variant: BugVariant::Bug2,
459            ..Default::default()
460        };
461        let result = planner.plan(&config).expect("Bug2 should find a path");
462        assert_eq!(*result.path_x.last().unwrap(), 167.0);
463        assert_eq!(*result.path_y.last().unwrap(), 50.0);
464    }
465
466    #[test]
467    fn test_straight_path_no_obstacles() {
468        let planner = BugPlanner::new(0.0, 0.0, 5.0, 5.0, &[], &[]);
469        let config = BugConfig {
470            variant: BugVariant::Bug0,
471            ..Default::default()
472        };
473        let result = planner.plan(&config).expect("should reach goal");
474        assert_eq!(*result.path_x.last().unwrap(), 5.0);
475        assert_eq!(*result.path_y.last().unwrap(), 5.0);
476        // Diagonal steps: 5 steps + start = 6 points
477        assert_eq!(result.path_x.len(), 6);
478    }
479
480    #[test]
481    fn test_config_default() {
482        let config = BugConfig::default();
483        assert_eq!(config.variant, BugVariant::Bug0);
484        assert_eq!(config.max_iterations, 1_000_000);
485    }
486}