Skip to main content

rust_robotics_planning/
d_star.rs

1#![allow(dead_code, clippy::legacy_numeric_constants)]
2
3//! D\* (original) path planning algorithm
4//!
5//! The original D\* algorithm by Anthony Stentz (1994) for optimal path planning
6//! with dynamic obstacle detection. Unlike D\* Lite, this uses the classic OPEN list
7//! with NEW/OPEN/CLOSED state tags.
8//!
9//! Reference: <https://en.wikipedia.org/wiki/D*>
10
11use std::collections::HashSet;
12
13/// Tag describing the state of a cell in the D\* search.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum Tag {
16    New,
17    Open,
18    Closed,
19}
20
21/// Whether a cell is free or an obstacle.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum CellKind {
24    Free,
25    Obstacle,
26}
27
28/// Internal cell state for the D\* algorithm.
29#[derive(Debug, Clone)]
30struct Cell {
31    tag: Tag,
32    kind: CellKind,
33    /// Cost-to-goal estimate (h value in Stentz's paper).
34    h: f64,
35    /// Key value used for priority ordering.
36    k: f64,
37    /// Parent cell index (row * cols + col), or `usize::MAX` if none.
38    parent: usize,
39}
40
41/// Grid map used by the D\* planner.
42#[derive(Debug)]
43struct Map {
44    rows: usize,
45    cols: usize,
46    cells: Vec<Cell>,
47}
48
49impl Map {
50    fn new(rows: usize, cols: usize) -> Self {
51        let cells = vec![
52            Cell {
53                tag: Tag::New,
54                kind: CellKind::Free,
55                h: 0.0,
56                k: 0.0,
57                parent: usize::MAX,
58            };
59            rows * cols
60        ];
61        Map { rows, cols, cells }
62    }
63
64    #[inline]
65    fn idx(&self, r: usize, c: usize) -> usize {
66        r * self.cols + c
67    }
68
69    #[inline]
70    fn pos(&self, idx: usize) -> (usize, usize) {
71        (idx / self.cols, idx % self.cols)
72    }
73
74    fn neighbors(&self, idx: usize) -> Vec<usize> {
75        let (r, c) = self.pos(idx);
76        let mut result = Vec::with_capacity(8);
77        for dr in [-1i32, 0, 1] {
78            for dc in [-1i32, 0, 1] {
79                if dr == 0 && dc == 0 {
80                    continue;
81                }
82                let nr = r as i32 + dr;
83                let nc = c as i32 + dc;
84                if nr >= 0 && (nr as usize) < self.rows && nc >= 0 && (nc as usize) < self.cols {
85                    result.push(self.idx(nr as usize, nc as usize));
86                }
87            }
88        }
89        result
90    }
91
92    /// Movement cost between two adjacent cells. Returns `f64::MAX` if either is an obstacle.
93    fn cost(&self, a: usize, b: usize) -> f64 {
94        if self.cells[a].kind == CellKind::Obstacle || self.cells[b].kind == CellKind::Obstacle {
95            return f64::MAX;
96        }
97        let (ar, ac) = self.pos(a);
98        let (br, bc) = self.pos(b);
99        let dr = (ar as f64) - (br as f64);
100        let dc = (ac as f64) - (bc as f64);
101        (dr * dr + dc * dc).sqrt()
102    }
103
104    fn set_obstacle(&mut self, r: usize, c: usize) {
105        if r < self.rows && c < self.cols {
106            let idx = self.idx(r, c);
107            self.cells[idx].kind = CellKind::Obstacle;
108        }
109    }
110}
111
112/// D\* (original) path planner.
113///
114/// Constructs a grid from obstacle coordinates, plans a shortest path from
115/// start to goal, and supports dynamic re-planning when new obstacles are
116/// detected along the current path.
117#[derive(Debug)]
118pub struct DStar {
119    map: Map,
120    open_list: HashSet<usize>,
121    x_min_world: i32,
122    y_min_world: i32,
123}
124
125impl DStar {
126    /// Create a new D\* planner from obstacle boundary coordinates.
127    ///
128    /// `ox` and `oy` define obstacle (or boundary) positions in world coordinates.
129    pub fn new(ox: &[i32], oy: &[i32]) -> Self {
130        let x_min = *ox.iter().min().unwrap_or(&0);
131        let y_min = *oy.iter().min().unwrap_or(&0);
132        let x_max = *ox.iter().max().unwrap_or(&0);
133        let y_max = *oy.iter().max().unwrap_or(&0);
134
135        let rows = (x_max - x_min + 1) as usize;
136        let cols = (y_max - y_min + 1) as usize;
137
138        let mut map = Map::new(rows, cols);
139
140        for (&x, &y) in ox.iter().zip(oy.iter()) {
141            let r = (x - x_min) as usize;
142            let c = (y - y_min) as usize;
143            map.set_obstacle(r, c);
144        }
145
146        DStar {
147            map,
148            open_list: HashSet::new(),
149            x_min_world: x_min,
150            y_min_world: y_min,
151        }
152    }
153
154    /// Return the cell index with the minimum k value in the open list, or `None`.
155    fn min_state(&self) -> Option<usize> {
156        self.open_list.iter().copied().min_by(|&a, &b| {
157            self.map.cells[a]
158                .k
159                .partial_cmp(&self.map.cells[b].k)
160                .unwrap_or(std::cmp::Ordering::Equal)
161        })
162    }
163
164    /// Return the minimum k value in the open list, or `-1.0` if empty.
165    fn get_kmin(&self) -> f64 {
166        self.open_list
167            .iter()
168            .map(|&i| self.map.cells[i].k)
169            .fold(f64::INFINITY, f64::min)
170            .min(f64::INFINITY)
171            * if self.open_list.is_empty() { 0.0 } else { 1.0 }
172            + if self.open_list.is_empty() { -1.0 } else { 0.0 }
173    }
174
175    /// Insert a state into the open list with a new h value.
176    fn insert(&mut self, idx: usize, h_new: f64) {
177        let cell = &mut self.map.cells[idx];
178        match cell.tag {
179            Tag::New => cell.k = h_new,
180            Tag::Open => cell.k = cell.k.min(h_new),
181            Tag::Closed => cell.k = cell.h.min(h_new),
182        }
183        cell.h = h_new;
184        cell.tag = Tag::Open;
185        self.open_list.insert(idx);
186    }
187
188    /// Remove a state from the open list (mark as CLOSED).
189    fn remove(&mut self, idx: usize) {
190        if self.map.cells[idx].tag == Tag::Open {
191            self.map.cells[idx].tag = Tag::Closed;
192        }
193        self.open_list.remove(&idx);
194    }
195
196    /// Core D\* state processing step. Returns the minimum k value after processing,
197    /// or `-1.0` if the open list is empty.
198    fn process_state(&mut self) -> f64 {
199        let x = match self.min_state() {
200            Some(idx) => idx,
201            None => return -1.0,
202        };
203
204        let k_old = self.get_kmin();
205        self.remove(x);
206
207        let neighbors = self.map.neighbors(x);
208        let x_h = self.map.cells[x].h;
209
210        if k_old < x_h {
211            // RAISE state
212            for &y in &neighbors {
213                let y_h = self.map.cells[y].h;
214                let cost_xy = self.map.cost(x, y);
215                if y_h <= k_old && x_h > y_h + cost_xy {
216                    self.map.cells[x].parent = y;
217                    self.map.cells[x].h = y_h + cost_xy;
218                }
219            }
220        }
221
222        // Re-read x_h after potential update above.
223        let x_h = self.map.cells[x].h;
224
225        if (k_old - x_h).abs() < 1e-10 {
226            // LOWER state
227            for &y in &neighbors {
228                let y_tag = self.map.cells[y].tag;
229                let y_h = self.map.cells[y].h;
230                let y_parent = self.map.cells[y].parent;
231                let cost_xy = self.map.cost(x, y);
232                if y_tag == Tag::New
233                    || (y_parent == x && (y_h - (x_h + cost_xy)).abs() > 1e-10)
234                    || (y_parent != x && y_h > x_h + cost_xy)
235                {
236                    self.map.cells[y].parent = x;
237                    self.insert(y, x_h + cost_xy);
238                }
239            }
240        } else {
241            // RAISE state (propagation)
242            for &y in &neighbors {
243                let y_tag = self.map.cells[y].tag;
244                let y_h = self.map.cells[y].h;
245                let y_parent = self.map.cells[y].parent;
246                let cost_xy = self.map.cost(x, y);
247                if y_tag == Tag::New || (y_parent == x && (y_h - (x_h + cost_xy)).abs() > 1e-10) {
248                    self.map.cells[y].parent = x;
249                    self.insert(y, x_h + cost_xy);
250                } else if y_parent != x && y_h > x_h + cost_xy {
251                    self.insert(x, x_h);
252                } else if y_parent != x
253                    && x_h > y_h + cost_xy
254                    && y_tag == Tag::Closed
255                    && y_h > k_old
256                {
257                    self.insert(y, y_h);
258                }
259            }
260        }
261
262        self.get_kmin()
263    }
264
265    /// Trigger re-planning from a cell whose parent became an obstacle.
266    ///
267    /// If the current parent is an obstacle, find the best non-obstacle
268    /// neighbor as a new parent before re-inserting into the open list.
269    fn modify_cost(&mut self, idx: usize) {
270        if self.map.cells[idx].tag == Tag::Closed {
271            let parent = self.map.cells[idx].parent;
272            let cost = self.map.cost(idx, parent);
273            if cost < f64::MAX {
274                // Parent is still valid
275                let parent_h = self.map.cells[parent].h;
276                self.insert(idx, parent_h + cost);
277            } else {
278                // Parent is an obstacle — find the best alternative neighbor
279                let neighbors = self.map.neighbors(idx);
280                let mut best_h = f64::MAX;
281                let mut best_neighbor = usize::MAX;
282                for &n in &neighbors {
283                    if self.map.cells[n].kind == CellKind::Obstacle {
284                        continue;
285                    }
286                    let c = self.map.cost(idx, n);
287                    let candidate = self.map.cells[n].h + c;
288                    if candidate < best_h {
289                        best_h = candidate;
290                        best_neighbor = n;
291                    }
292                }
293                if best_neighbor != usize::MAX {
294                    self.map.cells[idx].parent = best_neighbor;
295                    self.insert(idx, best_h);
296                } else {
297                    // Completely surrounded — insert with MAX to propagate RAISE
298                    self.insert(idx, f64::MAX);
299                }
300            }
301        }
302    }
303
304    fn modify(&mut self, idx: usize) {
305        self.modify_cost(idx);
306        loop {
307            let k_min = self.process_state();
308            if k_min >= self.map.cells[idx].h || k_min < 0.0 {
309                break;
310            }
311        }
312    }
313
314    /// Plan a path from `(sx, sy)` to `(gx, gy)` in world coordinates.
315    ///
316    /// Returns a vector of `(x, y)` world-coordinate waypoints if a path exists.
317    pub fn plan_xy(&mut self, sx: i32, sy: i32, gx: i32, gy: i32) -> Option<Vec<(i32, i32)>> {
318        let start_r = (sx - self.x_min_world) as usize;
319        let start_c = (sy - self.y_min_world) as usize;
320        let goal_r = (gx - self.x_min_world) as usize;
321        let goal_c = (gy - self.y_min_world) as usize;
322
323        if start_r >= self.map.rows
324            || start_c >= self.map.cols
325            || goal_r >= self.map.rows
326            || goal_c >= self.map.cols
327        {
328            return None;
329        }
330
331        let start = self.map.idx(start_r, start_c);
332        let goal = self.map.idx(goal_r, goal_c);
333
334        // Initial search: insert goal with h=0, expand until start is CLOSED.
335        self.insert(goal, 0.0);
336
337        loop {
338            let k = self.process_state();
339            if self.map.cells[start].tag == Tag::Closed || k < 0.0 {
340                break;
341            }
342        }
343
344        if self.map.cells[start].tag != Tag::Closed || !self.map.cells[start].h.is_finite() {
345            return None; // No path found
346        }
347
348        // Trace the path following parent pointers.
349        self.extract_path(start, goal)
350    }
351
352    /// Plan a path, then dynamically add obstacles and re-plan.
353    ///
354    /// `new_obstacles` are `(x, y)` world-coordinate pairs of obstacles discovered
355    /// after the initial plan. The planner re-routes around them.
356    pub fn plan_with_new_obstacles(
357        &mut self,
358        sx: i32,
359        sy: i32,
360        gx: i32,
361        gy: i32,
362        new_obstacles: &[(i32, i32)],
363    ) -> Option<Vec<(i32, i32)>> {
364        let start_r = (sx - self.x_min_world) as usize;
365        let start_c = (sy - self.y_min_world) as usize;
366        let goal_r = (gx - self.x_min_world) as usize;
367        let goal_c = (gy - self.y_min_world) as usize;
368
369        if start_r >= self.map.rows
370            || start_c >= self.map.cols
371            || goal_r >= self.map.rows
372            || goal_c >= self.map.cols
373        {
374            return None;
375        }
376
377        let start = self.map.idx(start_r, start_c);
378        let goal = self.map.idx(goal_r, goal_c);
379
380        // Initial search
381        self.insert(goal, 0.0);
382        loop {
383            let k = self.process_state();
384            if self.map.cells[start].tag == Tag::Closed || k < 0.0 {
385                break;
386            }
387        }
388
389        if self.map.cells[start].tag != Tag::Closed || !self.map.cells[start].h.is_finite() {
390            return None;
391        }
392
393        // Add new obstacles
394        for &(ox, oy) in new_obstacles {
395            let r = (ox - self.x_min_world) as usize;
396            let c = (oy - self.y_min_world) as usize;
397            if r < self.map.rows && c < self.map.cols {
398                self.map.set_obstacle(r, c);
399            }
400        }
401
402        // Walk the path; when hitting an obstacle, re-plan from that cell.
403        let mut path = Vec::new();
404        let mut current = start;
405
406        let max_steps = self.map.rows * self.map.cols;
407        let mut steps = 0;
408
409        while current != goal {
410            if steps > max_steps {
411                return None; // prevent infinite loop
412            }
413            steps += 1;
414
415            let (r, c) = self.map.pos(current);
416            path.push((r as i32 + self.x_min_world, c as i32 + self.y_min_world));
417
418            let parent = self.map.cells[current].parent;
419            if parent == usize::MAX {
420                return None;
421            }
422
423            if self.map.cells[parent].kind == CellKind::Obstacle {
424                // Parent is blocked, trigger re-plan
425                self.modify(current);
426                // After re-plan, continue from current
427                continue;
428            }
429
430            current = parent;
431        }
432
433        let (r, c) = self.map.pos(goal);
434        path.push((r as i32 + self.x_min_world, c as i32 + self.y_min_world));
435
436        Some(path)
437    }
438
439    /// Add a single obstacle at world coordinates and trigger local re-planning.
440    pub fn add_obstacle(&mut self, x: i32, y: i32) {
441        let r = (x - self.x_min_world) as usize;
442        let c = (y - self.y_min_world) as usize;
443        if r < self.map.rows && c < self.map.cols {
444            let idx = self.map.idx(r, c);
445            self.map.set_obstacle(r, c);
446            // Notify affected neighbors
447            let neighbors = self.map.neighbors(idx);
448            for n in neighbors {
449                if self.map.cells[n].parent == idx && self.map.cells[n].tag == Tag::Closed {
450                    self.modify(n);
451                }
452            }
453        }
454    }
455
456    /// Extract path by following parent pointers from start to goal.
457    fn extract_path(&self, start: usize, goal: usize) -> Option<Vec<(i32, i32)>> {
458        let mut path = Vec::new();
459        let mut current = start;
460        let max_steps = self.map.rows * self.map.cols;
461
462        for _ in 0..max_steps {
463            let (r, c) = self.map.pos(current);
464            path.push((r as i32 + self.x_min_world, c as i32 + self.y_min_world));
465
466            if current == goal {
467                return Some(path);
468            }
469
470            let parent = self.map.cells[current].parent;
471            if parent == usize::MAX || parent == current {
472                return None;
473            }
474            current = parent;
475        }
476
477        None
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    /// Build a simple box boundary: walls at x=-1,11 and y=-1,11.
486    fn make_box_obstacles() -> (Vec<i32>, Vec<i32>) {
487        let mut ox = Vec::new();
488        let mut oy = Vec::new();
489        for i in -1..=11 {
490            ox.push(i);
491            oy.push(-1); // bottom wall
492            ox.push(i);
493            oy.push(11); // top wall
494            ox.push(-1);
495            oy.push(i); // left wall
496            ox.push(11);
497            oy.push(i); // right wall
498        }
499        (ox, oy)
500    }
501
502    #[test]
503    fn test_dstar_creation() {
504        let (ox, oy) = make_box_obstacles();
505        let dstar = DStar::new(&ox, &oy);
506        assert!(dstar.map.rows > 0);
507        assert!(dstar.map.cols > 0);
508    }
509
510    #[test]
511    fn test_dstar_simple_path() {
512        let (ox, oy) = make_box_obstacles();
513        let mut dstar = DStar::new(&ox, &oy);
514        let path = dstar.plan_xy(0, 0, 5, 5);
515        assert!(path.is_some());
516        let path = path.unwrap();
517        assert_eq!(path.first(), Some(&(0, 0)));
518        assert_eq!(path.last(), Some(&(5, 5)));
519        assert!(path.len() >= 2);
520    }
521
522    #[test]
523    fn test_dstar_start_equals_goal() {
524        let (ox, oy) = make_box_obstacles();
525        let mut dstar = DStar::new(&ox, &oy);
526        let path = dstar.plan_xy(3, 3, 3, 3);
527        assert!(path.is_some());
528        let path = path.unwrap();
529        assert_eq!(path.len(), 1);
530        assert_eq!(path[0], (3, 3));
531    }
532
533    #[test]
534    fn test_dstar_path_avoids_obstacle() {
535        let (mut ox, mut oy) = make_box_obstacles();
536        // Add a vertical wall at x=5 from y=0 to y=8
537        for y in 0..=8 {
538            ox.push(5);
539            oy.push(y);
540        }
541        let mut dstar = DStar::new(&ox, &oy);
542        let path = dstar.plan_xy(2, 2, 8, 2);
543        assert!(path.is_some());
544        let path = path.unwrap();
545        // Path must not pass through the wall
546        for &(px, py) in &path {
547            if px == 5 {
548                assert!(py > 8, "path should go around the wall");
549            }
550        }
551        assert_eq!(path.first(), Some(&(2, 2)));
552        assert_eq!(path.last(), Some(&(8, 2)));
553    }
554
555    #[test]
556    fn test_dstar_no_path_blocked() {
557        // Start == goal but surrounded by obstacles should still find trivial path,
558        // so instead test truly disconnected components:
559        // place start and goal on opposite sides of a complete wall
560        let mut ox = Vec::new();
561        let mut oy = Vec::new();
562        // Outer boundary 10x10
563        for i in -1..=10 {
564            ox.push(i);
565            oy.push(-1);
566            ox.push(i);
567            oy.push(10);
568            ox.push(-1);
569            oy.push(i);
570            ox.push(10);
571            oy.push(i);
572        }
573        // Complete wall at x=5 from y=-1 to y=10
574        for j in -1..=10 {
575            ox.push(5);
576            oy.push(j);
577        }
578        let mut dstar = DStar::new(&ox, &oy);
579        let path = dstar.plan_xy(2, 5, 8, 5);
580        // Start and goal are on opposite sides of a complete wall
581        assert!(
582            path.is_none(),
583            "goal should be unreachable through complete wall"
584        );
585    }
586
587    #[test]
588    fn test_dstar_with_new_obstacles() {
589        let (ox, oy) = make_box_obstacles();
590        let mut dstar = DStar::new(&ox, &oy);
591
592        // New obstacles: partial wall at x=5 from y=0 to y=6, leaving gap at y=7..9
593        let new_obs: Vec<(i32, i32)> = (0..=6).map(|i| (5, i)).collect();
594
595        let path = dstar.plan_with_new_obstacles(2, 5, 8, 5, &new_obs);
596        assert!(path.is_some(), "should find path through gap above wall");
597        let path = path.unwrap();
598        assert_eq!(path.first(), Some(&(2, 5)));
599        assert_eq!(path.last(), Some(&(8, 5)));
600    }
601
602    #[test]
603    fn test_dstar_out_of_bounds() {
604        let (ox, oy) = make_box_obstacles();
605        let mut dstar = DStar::new(&ox, &oy);
606        let path = dstar.plan_xy(-100, -100, 200, 200);
607        assert!(path.is_none());
608    }
609
610    #[test]
611    fn test_dstar_adjacent_cells() {
612        let (ox, oy) = make_box_obstacles();
613        let mut dstar = DStar::new(&ox, &oy);
614        let path = dstar.plan_xy(3, 3, 4, 4);
615        assert!(path.is_some());
616        let path = path.unwrap();
617        assert_eq!(path.len(), 2); // direct diagonal neighbor
618    }
619
620    #[test]
621    fn test_dstar_straight_line() {
622        let (ox, oy) = make_box_obstacles();
623        let mut dstar = DStar::new(&ox, &oy);
624        let path = dstar.plan_xy(1, 1, 1, 9);
625        assert!(path.is_some());
626        let path = path.unwrap();
627        assert_eq!(path.first(), Some(&(1, 1)));
628        assert_eq!(path.last(), Some(&(1, 9)));
629        // All waypoints should have x=1 (straight line)
630        for &(px, _) in &path {
631            assert_eq!(px, 1);
632        }
633    }
634
635    #[test]
636    fn test_map_neighbors() {
637        let map = Map::new(5, 5);
638        // Corner cell (0,0) should have 3 neighbors
639        let n = map.neighbors(map.idx(0, 0));
640        assert_eq!(n.len(), 3);
641        // Edge cell (0,2) should have 5 neighbors
642        let n = map.neighbors(map.idx(0, 2));
643        assert_eq!(n.len(), 5);
644        // Center cell (2,2) should have 8 neighbors
645        let n = map.neighbors(map.idx(2, 2));
646        assert_eq!(n.len(), 8);
647    }
648
649    #[test]
650    fn test_map_cost_with_obstacle() {
651        let mut map = Map::new(5, 5);
652        let a = map.idx(1, 1);
653        let b = map.idx(1, 2);
654        // Normal cost
655        let cost = map.cost(a, b);
656        assert!((cost - 1.0).abs() < 1e-10);
657        // After making b an obstacle
658        map.set_obstacle(1, 2);
659        let cost = map.cost(a, b);
660        assert_eq!(cost, f64::MAX);
661    }
662}