1use std::cmp::Ordering;
14use std::collections::{BinaryHeap, HashSet};
15
16use rust_robotics_core::{RoboticsError, RoboticsResult};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct Position {
25 pub x: i32,
26 pub y: i32,
27}
28
29impl Position {
30 pub fn new(x: i32, y: i32) -> Self {
31 Self { x, y }
32 }
33
34 fn add(self, other: Self) -> Self {
35 Self {
36 x: self.x + other.x,
37 y: self.y + other.y,
38 }
39 }
40
41 fn manhattan_distance(self, other: Self) -> i32 {
42 (self.x - other.x).abs() + (self.y - other.y).abs()
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct Interval {
49 pub start_time: i32,
50 pub end_time: i32,
51}
52
53impl Interval {
54 pub fn new(start: i32, end: i32) -> Self {
55 Self {
56 start_time: start,
57 end_time: end,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum ObstacleArrangement {
69 Arrangement1,
71 NarrowCorridor,
73}
74
75#[derive(Debug, Clone)]
80pub struct Grid {
81 pub width: i32,
82 pub height: i32,
83 pub time_limit: i32,
84 reservation: Vec<Vec<Vec<i32>>>,
86 pub obstacle_paths: Vec<Vec<Position>>,
88}
89
90impl Grid {
91 pub fn new(
93 width: i32,
94 height: i32,
95 num_obstacles: usize,
96 arrangement: ObstacleArrangement,
97 avoid_points: &[Position],
98 time_limit: i32,
99 ) -> RoboticsResult<Self> {
100 if width <= 0 || height <= 0 || time_limit <= 0 {
101 return Err(RoboticsError::InvalidParameter(
102 "Grid dimensions and time_limit must be positive".into(),
103 ));
104 }
105
106 let mut reservation =
107 vec![vec![vec![0i32; time_limit as usize]; height as usize]; width as usize];
108
109 let obstacle_paths = match arrangement {
110 ObstacleArrangement::Arrangement1 => {
111 Self::arrangement_1(width, height, num_obstacles, time_limit, avoid_points)
112 }
113 ObstacleArrangement::NarrowCorridor => {
114 Self::narrow_corridor(width, height, num_obstacles, time_limit)
115 }
116 };
117
118 for (i, path) in obstacle_paths.iter().enumerate() {
120 let obs_id = (i + 1) as i32;
121 for (t, &pos) in path.iter().enumerate() {
122 reservation[pos.x as usize][pos.y as usize][t] = obs_id;
123 if t > 0 {
124 let prev = path[t - 1];
125 reservation[prev.x as usize][prev.y as usize][t] = obs_id;
126 }
127 }
128 }
129
130 Ok(Self {
131 width,
132 height,
133 time_limit,
134 reservation,
135 obstacle_paths,
136 })
137 }
138
139 pub fn empty(width: i32, height: i32, time_limit: i32) -> RoboticsResult<Self> {
141 if width <= 0 || height <= 0 || time_limit <= 0 {
142 return Err(RoboticsError::InvalidParameter(
143 "Grid dimensions and time_limit must be positive".into(),
144 ));
145 }
146 Ok(Self {
147 width,
148 height,
149 time_limit,
150 reservation: vec![
151 vec![vec![0i32; time_limit as usize]; height as usize];
152 width as usize
153 ],
154 obstacle_paths: vec![],
155 })
156 }
157
158 pub fn inside_bounds(&self, pos: Position) -> bool {
159 pos.x >= 0 && pos.x < self.width && pos.y >= 0 && pos.y < self.height
160 }
161
162 pub fn is_free(&self, pos: Position, t: i32) -> bool {
163 if !self.inside_bounds(pos) {
164 return false;
165 }
166 if t < 0 || t >= self.time_limit {
167 return false;
168 }
169 self.reservation[pos.x as usize][pos.y as usize][t as usize] == 0
170 }
171
172 pub fn reserve(&mut self, pos: Position, agent_id: i32, interval: Interval) {
174 for t in interval.start_time..=interval.end_time.min(self.time_limit - 1) {
175 self.reservation[pos.x as usize][pos.y as usize][t as usize] = agent_id;
176 }
177 }
178
179 pub fn reserve_path(&mut self, path: &NodePath, agent_id: i32) {
181 for (i, node) in path.nodes.iter().enumerate() {
182 let end = if i + 1 < path.nodes.len() {
183 path.nodes[i + 1].time
184 } else {
185 node.time + 1
186 };
187 self.reserve(node.position, agent_id, Interval::new(node.time, end));
188 }
189 }
190
191 pub fn clear_reservation(&mut self, pos: Position, agent_id: i32) {
193 for t in 0..self.time_limit as usize {
194 if self.reservation[pos.x as usize][pos.y as usize][t] == agent_id {
195 self.reservation[pos.x as usize][pos.y as usize][t] = 0;
196 }
197 }
198 }
199
200 pub fn safe_intervals_at(&self, pos: Position) -> Vec<Interval> {
202 let tl = self.time_limit as usize;
203 let col = &self.reservation[pos.x as usize][pos.y as usize];
204 let mut intervals = Vec::new();
205 let mut start: Option<usize> = None;
206 for (t, &cell) in col.iter().enumerate().take(tl) {
207 if cell == 0 {
208 if start.is_none() {
209 start = Some(t);
210 }
211 } else if let Some(s) = start {
212 if t - 1 > s {
213 intervals.push(Interval::new(s as i32, (t - 1) as i32));
215 }
216 start = None;
217 }
218 }
219 if let Some(s) = start {
220 if tl - 1 > s {
221 intervals.push(Interval::new(s as i32, (tl - 1) as i32));
222 }
223 }
224 intervals
225 }
226
227 fn arrangement_1(
230 width: i32,
231 height: i32,
232 num_obstacles: usize,
233 time_limit: i32,
234 _avoid: &[Position],
235 ) -> Vec<Vec<Position>> {
236 let half_x = width / 2;
237 let half_y = height / 2;
238 let count = num_obstacles.min(height as usize);
239 let mut paths = Vec::with_capacity(count);
240 for y_idx in 0..count as i32 {
241 let mut moving_right = y_idx < half_y;
242 let mut pos = Position::new(half_x, y_idx);
243 let mut path = vec![pos];
244 for t in 1..time_limit - 1 {
245 if t % 2 == 0 {
246 path.push(pos);
247 continue;
248 }
249 if (moving_right && pos.x == width - 1) || (!moving_right && pos.x == 0) {
250 moving_right = !moving_right;
251 }
252 pos = Position::new(pos.x + if moving_right { 1 } else { -1 }, pos.y);
253 path.push(pos);
254 }
255 paths.push(path);
256 }
257 paths
258 }
259
260 fn narrow_corridor(
261 width: i32,
262 height: i32,
263 num_obstacles: usize,
264 time_limit: i32,
265 ) -> Vec<Vec<Position>> {
266 let mid_y = height / 2;
267 let x = width / 2;
268 let mut paths = Vec::new();
269 for y in 0..height.min(num_obstacles as i32) {
270 if y == mid_y {
271 continue;
272 }
273 let pos = Position::new(x, y);
274 let path = vec![pos; (time_limit - 1) as usize];
275 paths.push(path);
276 }
277 paths
278 }
279}
280
281#[derive(Debug, Clone, Copy, Eq, PartialEq)]
287pub struct Node {
288 pub position: Position,
289 pub time: i32,
290 pub heuristic: i32,
291 pub parent_index: i32,
292}
293
294impl Node {
295 pub fn priority(&self) -> i32 {
296 self.time + self.heuristic
297 }
298}
299
300impl Ord for Node {
301 fn cmp(&self, other: &Self) -> Ordering {
302 other.priority().cmp(&self.priority())
304 }
305}
306
307impl PartialOrd for Node {
308 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
309 Some(self.cmp(other))
310 }
311}
312
313impl std::hash::Hash for Node {
314 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
315 self.position.hash(state);
316 self.time.hash(state);
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct NodePath {
323 pub nodes: Vec<Node>,
324 pub expanded_count: usize,
325}
326
327impl NodePath {
328 pub fn new(nodes: Vec<Node>, expanded_count: usize) -> Self {
329 Self {
330 nodes,
331 expanded_count,
332 }
333 }
334
335 pub fn position_at(&self, time: i32) -> Option<Position> {
337 if self.nodes.is_empty() {
338 return None;
339 }
340 for (i, node) in self.nodes.iter().enumerate() {
341 let next_time = if i + 1 < self.nodes.len() {
342 self.nodes[i + 1].time
343 } else {
344 node.time + 1
345 };
346 if time >= node.time && time < next_time {
347 return Some(node.position);
348 }
349 }
350 self.nodes.last().map(|n| n.position)
352 }
353
354 pub fn goal_reached_time(&self) -> i32 {
356 self.nodes.last().map_or(0, |n| n.time)
357 }
358}
359
360const DIFFS: [Position; 5] = [
365 Position { x: 0, y: 0 }, Position { x: 1, y: 0 },
367 Position { x: -1, y: 0 },
368 Position { x: 0, y: 1 },
369 Position { x: 0, y: -1 },
370];
371
372pub struct SpaceTimeAStar;
381
382impl SpaceTimeAStar {
383 pub fn plan(grid: &Grid, start: Position, goal: Position) -> RoboticsResult<NodePath> {
385 let mut open: BinaryHeap<Node> = BinaryHeap::new();
386 open.push(Node {
387 position: start,
388 time: 0,
389 heuristic: start.manhattan_distance(goal),
390 parent_index: -1,
391 });
392
393 let mut expanded_list: Vec<Node> = Vec::new();
394 let mut expanded_set: HashSet<(Position, i32)> = HashSet::new();
395
396 while let Some(node) = open.pop() {
397 if node.time + 1 >= grid.time_limit {
398 continue;
399 }
400 if node.position == goal {
401 let path = Self::reconstruct(&expanded_list, node);
402 return Ok(NodePath::new(path, expanded_set.len()));
403 }
404
405 let parent_idx = expanded_list.len() as i32;
406 expanded_list.push(node);
407 expanded_set.insert((node.position, node.time));
408
409 for &diff in &DIFFS {
410 let new_pos = node.position.add(diff);
411 let new_time = node.time + 1;
412 let new_node = Node {
413 position: new_pos,
414 time: new_time,
415 heuristic: new_pos.manhattan_distance(goal),
416 parent_index: parent_idx,
417 };
418
419 if expanded_set.contains(&(new_pos, new_time)) {
420 continue;
421 }
422
423 if grid.is_free(new_pos, new_time) && grid.is_free(new_pos, new_time + 1) {
425 open.push(new_node);
426 }
427 }
428 }
429
430 Err(RoboticsError::PlanningError("No path found".into()))
431 }
432
433 fn reconstruct(expanded: &[Node], goal_node: Node) -> Vec<Node> {
434 let mut path = vec![goal_node];
435 let mut walker = goal_node;
436 while walker.parent_index >= 0 {
437 walker = expanded[walker.parent_index as usize];
438 path.push(walker);
439 }
440 path.reverse();
441 path
442 }
443}
444
445#[derive(Debug, Clone, Copy, Eq, PartialEq)]
450struct SippNode {
451 position: Position,
452 time: i32,
453 heuristic: i32,
454 parent_index: i32,
455 interval: Interval,
456}
457
458impl SippNode {
459 fn priority(&self) -> i32 {
460 self.time + self.heuristic
461 }
462}
463
464impl Ord for SippNode {
465 fn cmp(&self, other: &Self) -> Ordering {
466 other.priority().cmp(&self.priority())
467 }
468}
469
470impl PartialOrd for SippNode {
471 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
472 Some(self.cmp(other))
473 }
474}
475
476pub struct SafeIntervalPlanner;
485
486impl SafeIntervalPlanner {
487 pub fn plan(grid: &Grid, start: Position, goal: Position) -> RoboticsResult<NodePath> {
489 let intervals = Self::compute_all_intervals(grid);
491
492 let first_interval = intervals[start.x as usize][start.y as usize]
493 .first()
494 .copied()
495 .ok_or_else(|| {
496 RoboticsError::PlanningError("Start position has no safe interval".into())
497 })?;
498
499 let mut open: BinaryHeap<SippNode> = BinaryHeap::new();
500 open.push(SippNode {
501 position: start,
502 time: 0,
503 heuristic: start.manhattan_distance(goal),
504 parent_index: -1,
505 interval: first_interval,
506 });
507
508 let mut expanded_list: Vec<SippNode> = Vec::new();
509 let mut visited: Vec<Vec<Vec<(i32, Interval)>>> =
511 vec![vec![vec![]; grid.height as usize]; grid.width as usize];
512
513 while let Some(node) = open.pop() {
514 if node.time + 1 >= grid.time_limit {
515 continue;
516 }
517 if node.position == goal {
518 let path = Self::reconstruct(&expanded_list, &node);
519 return Ok(NodePath::new(path, expanded_list.len()));
520 }
521
522 let parent_idx = expanded_list.len() as i32;
523 expanded_list.push(node);
524
525 let vlist = &mut visited[node.position.x as usize][node.position.y as usize];
527 let already = vlist.iter_mut().find(|(_, iv)| *iv == node.interval);
528 match already {
529 Some(entry) => {
530 entry.0 = entry.0.min(node.time);
531 }
532 None => {
533 vlist.push((node.time, node.interval));
534 }
535 }
536
537 for child in Self::successors(grid, goal, &node, parent_idx, &intervals, &visited) {
538 open.push(child);
539 }
540 }
541
542 Err(RoboticsError::PlanningError("No path found".into()))
543 }
544
545 fn successors(
546 grid: &Grid,
547 goal: Position,
548 parent: &SippNode,
549 parent_idx: i32,
550 intervals: &[Vec<Vec<Interval>>],
551 visited: &[Vec<Vec<(i32, Interval)>>],
552 ) -> Vec<SippNode> {
553 let mut result = Vec::new();
554 for &diff in &DIFFS {
555 let new_pos = parent.position.add(diff);
556 if !grid.inside_bounds(new_pos) {
557 continue;
558 }
559 let cell_intervals = &intervals[new_pos.x as usize][new_pos.y as usize];
560 for &iv in cell_intervals {
561 if iv.start_time > parent.interval.end_time {
562 break;
563 }
564 if iv.end_time < parent.interval.start_time {
565 continue;
566 }
567 let dominated = visited[new_pos.x as usize][new_pos.y as usize]
569 .iter()
570 .any(|&(et, ref vis_iv)| *vis_iv == iv && et <= parent.time + 1);
571 if dominated {
572 continue;
573 }
574 let t_lo = (parent.time + 1).max(iv.start_time);
576 let t_hi = parent.interval.end_time.min(iv.end_time);
577 for t in t_lo..t_hi {
578 if grid.is_free(new_pos, t) {
579 result.push(SippNode {
580 position: new_pos,
581 time: (parent.time + 1).max(iv.start_time),
582 heuristic: new_pos.manhattan_distance(goal),
583 parent_index: parent_idx,
584 interval: iv,
585 });
586 break;
587 }
588 }
589 }
590 }
591 result
592 }
593
594 fn compute_all_intervals(grid: &Grid) -> Vec<Vec<Vec<Interval>>> {
595 let mut all = vec![vec![vec![]; grid.height as usize]; grid.width as usize];
596 for x in 0..grid.width {
597 for y in 0..grid.height {
598 all[x as usize][y as usize] = grid.safe_intervals_at(Position::new(x, y));
599 }
600 }
601 all
602 }
603
604 fn reconstruct(expanded: &[SippNode], goal_node: &SippNode) -> Vec<Node> {
605 let mut path = vec![Node {
606 position: goal_node.position,
607 time: goal_node.time,
608 heuristic: goal_node.heuristic,
609 parent_index: goal_node.parent_index,
610 }];
611 let mut walker = *goal_node;
612 while walker.parent_index >= 0 {
613 walker = expanded[walker.parent_index as usize];
614 path.push(Node {
615 position: walker.position,
616 time: walker.time,
617 heuristic: walker.heuristic,
618 parent_index: walker.parent_index,
619 });
620 }
621 path.reverse();
622 path
623 }
624}
625
626#[derive(Debug, Clone)]
632pub struct AgentTask {
633 pub id: i32,
634 pub start: Position,
635 pub goal: Position,
636}
637
638impl AgentTask {
639 pub fn new(id: i32, start: Position, goal: Position) -> Self {
640 Self { id, start, goal }
641 }
642
643 fn distance_sq(&self) -> i32 {
644 let dx = self.goal.x - self.start.x;
645 let dy = self.goal.y - self.start.y;
646 dx * dx + dy * dy
647 }
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
652pub enum SingleAgentAlgorithm {
653 SpaceTimeAStar,
654 SafeInterval,
655}
656
657pub struct PriorityBasedPlanner;
662
663impl PriorityBasedPlanner {
664 pub fn plan(
667 grid: &mut Grid,
668 tasks: &mut [AgentTask],
669 algorithm: SingleAgentAlgorithm,
670 ) -> RoboticsResult<Vec<NodePath>> {
671 tasks.sort_by_key(|b| std::cmp::Reverse(b.distance_sq()));
673
674 for task in tasks.iter() {
676 grid.reserve(
677 task.start,
678 task.id,
679 Interval::new(0, 10.min(grid.time_limit - 1)),
680 );
681 }
682
683 let mut paths = Vec::with_capacity(tasks.len());
684 for task in tasks.iter() {
685 grid.clear_reservation(task.start, task.id);
686 let path = match algorithm {
687 SingleAgentAlgorithm::SpaceTimeAStar => {
688 SpaceTimeAStar::plan(grid, task.start, task.goal)?
689 }
690 SingleAgentAlgorithm::SafeInterval => {
691 SafeIntervalPlanner::plan(grid, task.start, task.goal)?
692 }
693 };
694 grid.reserve_path(&path, task.id);
695 paths.push(path);
696 }
697 Ok(paths)
698 }
699}
700
701#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn empty_grid(size: i32) -> Grid {
711 Grid::empty(size, size, 100).unwrap()
712 }
713
714 #[test]
719 fn test_sta_star_straight_line() {
720 let grid = empty_grid(10);
721 let start = Position::new(0, 0);
722 let goal = Position::new(5, 0);
723 let path = SpaceTimeAStar::plan(&grid, start, goal).unwrap();
724 assert_eq!(path.nodes.first().unwrap().position, start);
725 assert_eq!(path.nodes.last().unwrap().position, goal);
726 assert_eq!(path.goal_reached_time(), 5);
727 }
728
729 #[test]
730 fn test_sta_star_diagonal() {
731 let grid = empty_grid(10);
732 let start = Position::new(0, 0);
733 let goal = Position::new(3, 3);
734 let path = SpaceTimeAStar::plan(&grid, start, goal).unwrap();
735 assert_eq!(path.nodes.last().unwrap().position, goal);
736 assert_eq!(path.goal_reached_time(), 6);
738 }
739
740 #[test]
741 fn test_sta_star_same_start_goal() {
742 let grid = empty_grid(10);
743 let pos = Position::new(5, 5);
744 let path = SpaceTimeAStar::plan(&grid, pos, pos).unwrap();
745 assert_eq!(path.nodes.last().unwrap().position, pos);
746 }
747
748 #[test]
749 fn test_sta_star_with_obstacles() {
750 let grid = Grid::new(
751 11,
752 11,
753 5,
754 ObstacleArrangement::Arrangement1,
755 &[Position::new(0, 0), Position::new(10, 10)],
756 100,
757 )
758 .unwrap();
759 let result = SpaceTimeAStar::plan(&grid, Position::new(0, 0), Position::new(10, 10));
760 assert!(result.is_ok() || result.is_err());
763 }
764
765 #[test]
770 fn test_sipp_straight_line() {
771 let grid = empty_grid(10);
772 let start = Position::new(0, 0);
773 let goal = Position::new(5, 0);
774 let path = SafeIntervalPlanner::plan(&grid, start, goal).unwrap();
775 assert_eq!(path.nodes.first().unwrap().position, start);
776 assert_eq!(path.nodes.last().unwrap().position, goal);
777 assert_eq!(path.goal_reached_time(), 5);
778 }
779
780 #[test]
781 fn test_sipp_same_start_goal() {
782 let grid = empty_grid(10);
783 let pos = Position::new(3, 3);
784 let path = SafeIntervalPlanner::plan(&grid, pos, pos).unwrap();
785 assert_eq!(path.nodes.last().unwrap().position, pos);
786 }
787
788 #[test]
793 fn test_grid_empty_all_free() {
794 let grid = empty_grid(5);
795 for x in 0..5 {
796 for y in 0..5 {
797 assert!(grid.is_free(Position::new(x, y), 0));
798 }
799 }
800 }
801
802 #[test]
803 fn test_grid_bounds_check() {
804 let grid = empty_grid(5);
805 assert!(!grid.inside_bounds(Position::new(-1, 0)));
806 assert!(!grid.inside_bounds(Position::new(0, 5)));
807 assert!(grid.inside_bounds(Position::new(0, 0)));
808 assert!(grid.inside_bounds(Position::new(4, 4)));
809 }
810
811 #[test]
812 fn test_safe_intervals_empty_grid() {
813 let grid = empty_grid(5);
814 let ivs = grid.safe_intervals_at(Position::new(2, 2));
815 assert!(!ivs.is_empty());
817 assert_eq!(ivs[0].start_time, 0);
818 }
819
820 #[test]
821 fn test_reserve_and_check() {
822 let mut grid = empty_grid(5);
823 let pos = Position::new(2, 2);
824 assert!(grid.is_free(pos, 5));
825 grid.reserve(pos, 1, Interval::new(5, 10));
826 assert!(!grid.is_free(pos, 5));
827 assert!(!grid.is_free(pos, 10));
828 assert!(grid.is_free(pos, 11));
829 }
830
831 #[test]
832 fn test_clear_reservation() {
833 let mut grid = empty_grid(5);
834 let pos = Position::new(1, 1);
835 grid.reserve(pos, 42, Interval::new(0, 20));
836 assert!(!grid.is_free(pos, 10));
837 grid.clear_reservation(pos, 42);
838 assert!(grid.is_free(pos, 10));
839 }
840
841 #[test]
846 fn test_node_path_position_at() {
847 let nodes = vec![
848 Node {
849 position: Position::new(0, 0),
850 time: 0,
851 heuristic: 3,
852 parent_index: -1,
853 },
854 Node {
855 position: Position::new(1, 0),
856 time: 1,
857 heuristic: 2,
858 parent_index: 0,
859 },
860 Node {
861 position: Position::new(2, 0),
862 time: 2,
863 heuristic: 1,
864 parent_index: 1,
865 },
866 ];
867 let np = NodePath::new(nodes, 3);
868 assert_eq!(np.position_at(0), Some(Position::new(0, 0)));
869 assert_eq!(np.position_at(1), Some(Position::new(1, 0)));
870 assert_eq!(np.position_at(2), Some(Position::new(2, 0)));
871 assert_eq!(np.position_at(10), Some(Position::new(2, 0)));
873 }
874
875 #[test]
880 fn test_priority_planner_two_agents() {
881 let mut grid = empty_grid(10);
882 let mut tasks = vec![
883 AgentTask::new(1, Position::new(0, 0), Position::new(5, 0)),
884 AgentTask::new(2, Position::new(0, 1), Position::new(5, 1)),
885 ];
886 let paths =
887 PriorityBasedPlanner::plan(&mut grid, &mut tasks, SingleAgentAlgorithm::SpaceTimeAStar)
888 .unwrap();
889 assert_eq!(paths.len(), 2);
890 for (task, path) in tasks.iter().zip(paths.iter()) {
891 assert_eq!(path.nodes.last().unwrap().position, task.goal);
892 }
893 }
894
895 #[test]
900 fn test_arrangement1_creates_obstacles() {
901 let grid = Grid::new(11, 11, 5, ObstacleArrangement::Arrangement1, &[], 50).unwrap();
902 assert_eq!(grid.obstacle_paths.len(), 5);
903 }
904
905 #[test]
906 fn test_narrow_corridor_creates_obstacles() {
907 let grid = Grid::new(11, 11, 20, ObstacleArrangement::NarrowCorridor, &[], 50).unwrap();
908 assert_eq!(grid.obstacle_paths.len(), 10);
910 }
911
912 #[test]
913 fn test_invalid_grid_params() {
914 assert!(Grid::empty(0, 5, 100).is_err());
915 assert!(Grid::empty(5, -1, 100).is_err());
916 assert!(Grid::empty(5, 5, 0).is_err());
917 }
918}