1use std::cmp::Ordering;
12use std::collections::{BinaryHeap, HashMap};
13
14use rust_robotics_core::{RoboticsError, RoboticsResult};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Interval {
19 pub start: u64,
20 pub end: u64,
21}
22
23impl Interval {
24 pub fn new(start: u64, end: u64) -> Self {
25 Self { start, end }
26 }
27
28 pub fn infinite() -> Self {
30 Self {
31 start: 0,
32 end: u64::MAX,
33 }
34 }
35
36 pub fn contains(&self, t: u64) -> bool {
37 t >= self.start && t < self.end
38 }
39
40 pub fn is_empty(&self) -> bool {
41 self.start >= self.end
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct DynamicObstacle {
48 pub x: i32,
49 pub y: i32,
50 pub interval: Interval,
52}
53
54#[derive(Debug, Clone)]
56pub struct SippConfig {
57 pub width: i32,
58 pub height: i32,
59 pub obstacle_map: Vec<Vec<bool>>,
61 pub dynamic_obstacles: Vec<DynamicObstacle>,
63 pub allow_diagonal: bool,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct TimedWaypoint {
70 pub x: i32,
71 pub y: i32,
72 pub t: u64,
73}
74
75pub type SippPath = Vec<TimedWaypoint>;
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84struct StateKey {
85 x: i32,
86 y: i32,
87 interval_idx: usize,
88}
89
90struct OpenEntry {
92 key: StateKey,
93 f: u64,
94}
95
96impl Eq for OpenEntry {}
97impl PartialEq for OpenEntry {
98 fn eq(&self, other: &Self) -> bool {
99 self.f == other.f
100 }
101}
102impl Ord for OpenEntry {
103 fn cmp(&self, other: &Self) -> Ordering {
104 other.f.cmp(&self.f)
106 }
107}
108impl PartialOrd for OpenEntry {
109 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
110 Some(self.cmp(other))
111 }
112}
113
114pub struct SippPlanner {
120 width: i32,
121 height: i32,
122 obstacle_map: Vec<Vec<bool>>,
123 safe_intervals: Vec<Vec<Interval>>,
125 motions: Vec<(i32, i32, u64)>,
126}
127
128impl SippPlanner {
129 pub fn new(config: SippConfig) -> RoboticsResult<Self> {
131 if config.width <= 0 || config.height <= 0 {
132 return Err(RoboticsError::InvalidParameter(
133 "width and height must be positive".to_string(),
134 ));
135 }
136 if config.obstacle_map.len() != config.width as usize {
137 return Err(RoboticsError::InvalidParameter(format!(
138 "obstacle_map x-dimension ({}) must match width ({})",
139 config.obstacle_map.len(),
140 config.width
141 )));
142 }
143 for col in &config.obstacle_map {
144 if col.len() != config.height as usize {
145 return Err(RoboticsError::InvalidParameter(
146 "obstacle_map y-dimension must match height".to_string(),
147 ));
148 }
149 }
150
151 let motions = if config.allow_diagonal {
152 vec![
153 (1, 0, 1),
154 (0, 1, 1),
155 (-1, 0, 1),
156 (0, -1, 1),
157 (1, 1, 1),
158 (1, -1, 1),
159 (-1, 1, 1),
160 (-1, -1, 1),
161 ]
162 } else {
163 vec![(1, 0, 1), (0, 1, 1), (-1, 0, 1), (0, -1, 1)]
164 };
165
166 let safe_intervals = Self::compute_safe_intervals(
167 &config.obstacle_map,
168 &config.dynamic_obstacles,
169 config.width,
170 config.height,
171 );
172
173 Ok(Self {
174 width: config.width,
175 height: config.height,
176 obstacle_map: config.obstacle_map,
177 safe_intervals,
178 motions,
179 })
180 }
181
182 pub fn plan(&self, sx: i32, sy: i32, gx: i32, gy: i32) -> RoboticsResult<SippPath> {
184 self.validate_pos(sx, sy, "start")?;
185 self.validate_pos(gx, gy, "goal")?;
186
187 if self.obstacle_map[sx as usize][sy as usize] {
188 return Err(RoboticsError::PlanningError(
189 "start is inside a static obstacle".to_string(),
190 ));
191 }
192 if self.obstacle_map[gx as usize][gy as usize] {
193 return Err(RoboticsError::PlanningError(
194 "goal is inside a static obstacle".to_string(),
195 ));
196 }
197
198 let start_intervals = &self.safe_intervals[self.cell_index(sx, sy)];
199 let start_interval_idx = match start_intervals.iter().position(|iv| iv.contains(0)) {
200 Some(idx) => idx,
201 None => {
202 return Err(RoboticsError::PlanningError(
203 "start cell is not safe at time 0".to_string(),
204 ));
205 }
206 };
207
208 let start_key = StateKey {
209 x: sx,
210 y: sy,
211 interval_idx: start_interval_idx,
212 };
213
214 let h0 = Self::heuristic(sx, sy, gx, gy);
215
216 let mut open = BinaryHeap::new();
217 let mut best_g: HashMap<StateKey, u64> = HashMap::new();
218 let mut parent_map: HashMap<StateKey, Option<StateKey>> = HashMap::new();
219
220 best_g.insert(start_key, 0);
221 parent_map.insert(start_key, None);
222 open.push(OpenEntry {
223 key: start_key,
224 f: h0,
225 });
226
227 while let Some(current) = open.pop() {
228 let g = match best_g.get(¤t.key) {
229 Some(&g) => g,
230 None => continue,
231 };
232
233 if current.f > g + Self::heuristic(current.key.x, current.key.y, gx, gy) {
235 continue;
236 }
237
238 if current.key.x == gx && current.key.y == gy {
240 return Ok(Self::reconstruct_path(&parent_map, &best_g, current.key));
241 }
242
243 let current_interval = self.safe_intervals
244 [self.cell_index(current.key.x, current.key.y)][current.key.interval_idx];
245
246 for &(dx, dy, cost) in &self.motions {
248 let nx = current.key.x + dx;
249 let ny = current.key.y + dy;
250
251 if !self.in_bounds(nx, ny) || self.obstacle_map[nx as usize][ny as usize] {
252 continue;
253 }
254
255 let arrival = g + cost;
256 let neighbor_intervals = &self.safe_intervals[self.cell_index(nx, ny)];
257
258 for (ni, niv) in neighbor_intervals.iter().enumerate() {
259 if arrival >= niv.end {
261 continue;
262 }
263 let effective_arrival = if arrival >= niv.start {
267 arrival
268 } else {
269 let depart = niv.start - cost;
272 if depart < g {
273 continue;
275 }
276 if depart >= current_interval.end {
277 continue;
279 }
280 niv.start
281 };
282
283 let nkey = StateKey {
284 x: nx,
285 y: ny,
286 interval_idx: ni,
287 };
288
289 if let Some(&prev_g) = best_g.get(&nkey) {
290 if effective_arrival >= prev_g {
291 continue;
292 }
293 }
294
295 best_g.insert(nkey, effective_arrival);
296 parent_map.insert(nkey, Some(current.key));
297 let f = effective_arrival + Self::heuristic(nx, ny, gx, gy);
298 open.push(OpenEntry { key: nkey, f });
299 }
300 }
301
302 let cell_intervals =
305 &self.safe_intervals[self.cell_index(current.key.x, current.key.y)];
306 if current.key.interval_idx + 1 < cell_intervals.len() {
307 let next_idx = current.key.interval_idx + 1;
308 let next_iv = cell_intervals[next_idx];
309 let _ = (next_idx, next_iv); }
324 }
325
326 Err(RoboticsError::PlanningError("no path found".to_string()))
327 }
328
329 pub fn get_safe_intervals(&self, x: i32, y: i32) -> &[Interval] {
331 &self.safe_intervals[self.cell_index(x, y)]
332 }
333
334 fn cell_index(&self, x: i32, y: i32) -> usize {
339 (y as usize) * (self.width as usize) + (x as usize)
340 }
341
342 fn in_bounds(&self, x: i32, y: i32) -> bool {
343 x >= 0 && y >= 0 && x < self.width && y < self.height
344 }
345
346 fn validate_pos(&self, x: i32, y: i32, label: &str) -> RoboticsResult<()> {
347 if self.in_bounds(x, y) {
348 Ok(())
349 } else {
350 Err(RoboticsError::InvalidParameter(format!(
351 "{} ({}, {}) is out of bounds ({}x{})",
352 label, x, y, self.width, self.height
353 )))
354 }
355 }
356
357 fn heuristic(x: i32, y: i32, gx: i32, gy: i32) -> u64 {
358 let dx = (x - gx).unsigned_abs() as u64;
360 let dy = (y - gy).unsigned_abs() as u64;
361 dx.max(dy)
362 }
363
364 fn compute_safe_intervals(
371 obstacle_map: &[Vec<bool>],
372 dynamic_obstacles: &[DynamicObstacle],
373 width: i32,
374 height: i32,
375 ) -> Vec<Vec<Interval>> {
376 let n = (width as usize) * (height as usize);
377 let mut unsafe_map: HashMap<usize, Vec<Interval>> = HashMap::new();
378
379 for dob in dynamic_obstacles {
380 if dob.x < 0 || dob.y < 0 || dob.x >= width || dob.y >= height {
381 continue;
382 }
383 let idx = (dob.y as usize) * (width as usize) + (dob.x as usize);
384 unsafe_map.entry(idx).or_default().push(dob.interval);
385 }
386
387 let mut safe = Vec::with_capacity(n);
388 for y in 0..height {
389 for x in 0..width {
390 let idx = (y as usize) * (width as usize) + (x as usize);
391 if obstacle_map[x as usize][y as usize] {
392 safe.push(Vec::new());
393 } else if let Some(unsafes) = unsafe_map.get(&idx) {
394 safe.push(Self::complement_intervals(unsafes));
395 } else {
396 safe.push(vec![Interval::infinite()]);
397 }
398 }
399 }
400 safe
401 }
402
403 fn complement_intervals(unsafe_intervals: &[Interval]) -> Vec<Interval> {
406 if unsafe_intervals.is_empty() {
407 return vec![Interval::infinite()];
408 }
409
410 let mut sorted: Vec<(u64, u64)> = unsafe_intervals
412 .iter()
413 .filter(|iv| !iv.is_empty())
414 .map(|iv| (iv.start, iv.end))
415 .collect();
416 sorted.sort_by_key(|&(s, _)| s);
417
418 let mut merged: Vec<(u64, u64)> = Vec::new();
419 for (s, e) in sorted {
420 if let Some(last) = merged.last_mut() {
421 if s <= last.1 {
422 last.1 = last.1.max(e);
423 continue;
424 }
425 }
426 merged.push((s, e));
427 }
428
429 let mut result = Vec::new();
431 let mut cursor: u64 = 0;
432 for (s, e) in &merged {
433 if cursor < *s {
434 result.push(Interval::new(cursor, *s));
435 }
436 cursor = *e;
437 }
438 if cursor < u64::MAX {
439 result.push(Interval::new(cursor, u64::MAX));
440 }
441 result
442 }
443
444 fn reconstruct_path(
445 parent_map: &HashMap<StateKey, Option<StateKey>>,
446 best_g: &HashMap<StateKey, u64>,
447 goal_key: StateKey,
448 ) -> SippPath {
449 let mut path = Vec::new();
450 let mut current = Some(goal_key);
451
452 while let Some(key) = current {
453 let t = best_g[&key];
454 path.push(TimedWaypoint {
455 x: key.x,
456 y: key.y,
457 t,
458 });
459 current = parent_map.get(&key).copied().flatten();
460 }
461
462 path.reverse();
463 path
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 fn empty_map(width: i32, height: i32) -> Vec<Vec<bool>> {
473 vec![vec![false; height as usize]; width as usize]
474 }
475
476 fn simple_config(width: i32, height: i32, obstacles: &[(i32, i32)]) -> SippConfig {
478 let mut map = empty_map(width, height);
479 for &(x, y) in obstacles {
480 map[x as usize][y as usize] = true;
481 }
482 SippConfig {
483 width,
484 height,
485 obstacle_map: map,
486 dynamic_obstacles: vec![],
487 allow_diagonal: false,
488 }
489 }
490
491 #[test]
496 fn test_straight_line_no_obstacles() {
497 let config = simple_config(5, 5, &[]);
498 let planner = SippPlanner::new(config).unwrap();
499 let path = planner.plan(0, 0, 4, 0).unwrap();
500
501 assert_eq!(path.len(), 5);
502 assert_eq!(path.first().unwrap(), &TimedWaypoint { x: 0, y: 0, t: 0 });
503 assert_eq!(path.last().unwrap(), &TimedWaypoint { x: 4, y: 0, t: 4 });
504 }
505
506 #[test]
507 fn test_static_obstacle_detour() {
508 let obstacles: Vec<(i32, i32)> = (0..3).map(|y| (2, y)).collect();
510 let config = simple_config(5, 5, &obstacles);
511 let planner = SippPlanner::new(config).unwrap();
512 let path = planner.plan(0, 0, 4, 0).unwrap();
513
514 assert!(path.len() > 5, "path must detour around the wall");
516 assert_eq!(path.first().unwrap().x, 0);
517 assert_eq!(path.first().unwrap().y, 0);
518 assert_eq!(path.last().unwrap().x, 4);
519 assert_eq!(path.last().unwrap().y, 0);
520
521 for wp in &path {
523 assert!(
524 !obstacles.contains(&(wp.x, wp.y)),
525 "path must not pass through static obstacles"
526 );
527 }
528 }
529
530 #[test]
531 fn test_static_only_behaves_like_astar() {
532 let config = simple_config(10, 10, &[]);
535 let planner = SippPlanner::new(config).unwrap();
536 let path = planner.plan(0, 0, 9, 9).unwrap();
537
538 assert_eq!(path.len(), 19);
540 assert_eq!(path.last().unwrap().t, 18);
541 }
542
543 #[test]
548 fn test_dynamic_obstacle_blocks_corridor() {
549 let config = SippConfig {
555 width: 5,
556 height: 1,
557 obstacle_map: empty_map(5, 1),
558 dynamic_obstacles: vec![DynamicObstacle {
559 x: 2,
560 y: 0,
561 interval: Interval::new(2, 5),
562 }],
563 allow_diagonal: false,
564 };
565
566 let planner = SippPlanner::new(config).unwrap();
567 let path = planner.plan(0, 0, 4, 0).unwrap();
568
569 let at_2 = path.iter().find(|wp| wp.x == 2 && wp.y == 0).unwrap();
571 assert!(
572 at_2.t >= 5,
573 "robot should not be at (2,0) before t=5, got t={}",
574 at_2.t
575 );
576
577 assert_eq!(path.last().unwrap().x, 4);
578 assert_eq!(path.last().unwrap().y, 0);
579 }
580
581 #[test]
582 fn test_wait_and_go() {
583 let config = SippConfig {
587 width: 3,
588 height: 1,
589 obstacle_map: empty_map(3, 1),
590 dynamic_obstacles: vec![DynamicObstacle {
591 x: 1,
592 y: 0,
593 interval: Interval::new(0, 10),
594 }],
595 allow_diagonal: false,
596 };
597
598 let planner = SippPlanner::new(config).unwrap();
599 let path = planner.plan(0, 0, 2, 0).unwrap();
600
601 assert_eq!(path.first().unwrap(), &TimedWaypoint { x: 0, y: 0, t: 0 });
602
603 let at_1 = path.iter().find(|wp| wp.x == 1).unwrap();
604 assert!(
605 at_1.t >= 10,
606 "robot should reach (1,0) at t>=10, got t={}",
607 at_1.t
608 );
609
610 assert_eq!(path.last().unwrap().x, 2);
611 assert_eq!(path.last().unwrap().y, 0);
612 assert!(path.last().unwrap().t >= 11);
613 }
614
615 #[test]
616 fn test_multiple_dynamic_obstacles() {
617 let config = SippConfig {
620 width: 7,
621 height: 1,
622 obstacle_map: empty_map(7, 1),
623 dynamic_obstacles: vec![
624 DynamicObstacle {
625 x: 2,
626 y: 0,
627 interval: Interval::new(2, 5),
628 },
629 DynamicObstacle {
630 x: 4,
631 y: 0,
632 interval: Interval::new(4, 8),
633 },
634 ],
635 allow_diagonal: false,
636 };
637
638 let planner = SippPlanner::new(config).unwrap();
639 let path = planner.plan(0, 0, 6, 0).unwrap();
640
641 for wp in &path {
643 if wp.x == 2 && wp.y == 0 {
644 assert!(wp.t < 2 || wp.t >= 5, "(2,0) blocked during [2,5)");
645 }
646 if wp.x == 4 && wp.y == 0 {
647 assert!(wp.t < 4 || wp.t >= 8, "(4,0) blocked during [4,8)");
648 }
649 }
650 assert_eq!(path.last().unwrap().x, 6);
651 }
652
653 #[test]
658 fn test_safe_intervals_no_dynamic() {
659 let config = simple_config(3, 3, &[]);
660 let planner = SippPlanner::new(config).unwrap();
661 let ivs = planner.get_safe_intervals(1, 1);
662 assert_eq!(ivs.len(), 1);
663 assert_eq!(ivs[0], Interval::infinite());
664 }
665
666 #[test]
667 fn test_safe_intervals_with_dynamic() {
668 let config = SippConfig {
669 width: 3,
670 height: 3,
671 obstacle_map: empty_map(3, 3),
672 dynamic_obstacles: vec![DynamicObstacle {
673 x: 1,
674 y: 1,
675 interval: Interval::new(5, 10),
676 }],
677 allow_diagonal: false,
678 };
679 let planner = SippPlanner::new(config).unwrap();
680 let ivs = planner.get_safe_intervals(1, 1);
681 assert_eq!(ivs.len(), 2);
682 assert_eq!(ivs[0], Interval::new(0, 5));
683 assert_eq!(ivs[1], Interval::new(10, u64::MAX));
684 }
685
686 #[test]
687 fn test_safe_intervals_static_obstacle() {
688 let config = simple_config(3, 3, &[(1, 1)]);
689 let planner = SippPlanner::new(config).unwrap();
690 let ivs = planner.get_safe_intervals(1, 1);
691 assert!(ivs.is_empty(), "static obstacle has no safe intervals");
692 }
693
694 #[test]
699 fn test_start_equals_goal() {
700 let config = simple_config(3, 3, &[]);
701 let planner = SippPlanner::new(config).unwrap();
702 let path = planner.plan(1, 1, 1, 1).unwrap();
703 assert_eq!(path.len(), 1);
704 assert_eq!(path[0], TimedWaypoint { x: 1, y: 1, t: 0 });
705 }
706
707 #[test]
708 fn test_no_path_due_to_static_wall() {
709 let obstacles: Vec<(i32, i32)> = (0..5).map(|y| (2, y)).collect();
711 let config = simple_config(5, 5, &obstacles);
712 let planner = SippPlanner::new(config).unwrap();
713 let result = planner.plan(0, 0, 4, 0);
714 assert!(result.is_err());
715 }
716
717 #[test]
718 fn test_diagonal_movement() {
719 let config = SippConfig {
720 width: 5,
721 height: 5,
722 obstacle_map: empty_map(5, 5),
723 dynamic_obstacles: vec![],
724 allow_diagonal: true,
725 };
726 let planner = SippPlanner::new(config).unwrap();
727 let path = planner.plan(0, 0, 4, 4).unwrap();
728
729 assert_eq!(path.len(), 5);
731 assert_eq!(path.last().unwrap().t, 4);
732 }
733
734 #[test]
735 fn test_invalid_start_goal() {
736 let config = simple_config(5, 5, &[]);
737 let planner = SippPlanner::new(config).unwrap();
738
739 assert!(planner.plan(-1, 0, 4, 0).is_err());
740 assert!(planner.plan(0, 0, 5, 0).is_err());
741 }
742}