1use std::cmp::Ordering;
7use std::collections::{BinaryHeap, HashMap, HashSet};
8
9use rust_robotics_core::{RoboticsError, RoboticsResult};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct StlTimedCell {
14 pub x: i32,
15 pub y: i32,
16 pub t: u64,
17}
18
19impl StlTimedCell {
20 pub fn new(x: i32, y: i32, t: u64) -> Self {
21 Self { x, y, t }
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct StlCbsAgent {
28 pub id: usize,
29 pub start: (i32, i32),
30 pub goal: (i32, i32),
31}
32
33impl StlCbsAgent {
34 pub fn new(id: usize, start: (i32, i32), goal: (i32, i32)) -> Self {
35 Self { id, start, goal }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct StlCbsConfig {
42 pub width: i32,
43 pub height: i32,
44 pub obstacle_map: Vec<Vec<bool>>,
45 pub max_time: u64,
46 pub max_cbs_nodes: usize,
47 pub allow_wait: bool,
48}
49
50impl StlCbsConfig {
51 pub fn new(width: i32, height: i32) -> Self {
52 Self {
53 width,
54 height,
55 obstacle_map: vec![vec![false; height.max(0) as usize]; width.max(0) as usize],
56 max_time: 64,
57 max_cbs_nodes: 512,
58 allow_wait: true,
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct StlCbsPath {
66 pub agent_id: usize,
67 pub waypoints: Vec<StlTimedCell>,
68}
69
70impl StlCbsPath {
71 pub fn arrival_time(&self) -> u64 {
72 self.waypoints.last().map_or(0, |waypoint| waypoint.t)
73 }
74
75 pub fn position_at(&self, t: u64) -> StlTimedCell {
76 self.waypoints
77 .iter()
78 .find(|waypoint| waypoint.t == t)
79 .copied()
80 .or_else(|| {
81 self.waypoints
82 .iter()
83 .rev()
84 .find(|waypoint| waypoint.t < t)
85 .copied()
86 })
87 .unwrap_or_else(|| {
88 *self
89 .waypoints
90 .first()
91 .expect("validated STL-CBS path is non-empty")
92 })
93 }
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct StlCbsPlan {
99 pub paths: Vec<StlCbsPath>,
100 pub total_cost: u64,
101 pub conflicts_resolved: usize,
102 pub high_level_nodes_expanded: usize,
103 pub min_pairwise_separation_robustness: f64,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq)]
108pub struct StlRectangle2D {
109 pub min_x: f64,
110 pub max_x: f64,
111 pub min_y: f64,
112 pub max_y: f64,
113}
114
115impl StlRectangle2D {
116 pub fn new(min_x: f64, max_x: f64, min_y: f64, max_y: f64) -> RoboticsResult<Self> {
117 if !min_x.is_finite()
118 || !max_x.is_finite()
119 || !min_y.is_finite()
120 || !max_y.is_finite()
121 || min_x > max_x
122 || min_y > max_y
123 {
124 return Err(RoboticsError::InvalidParameter(
125 "STL rectangle bounds must be finite and ordered".to_string(),
126 ));
127 }
128 Ok(Self {
129 min_x,
130 max_x,
131 min_y,
132 max_y,
133 })
134 }
135
136 pub fn inside_robustness(&self, x: f64, y: f64) -> f64 {
137 (x - self.min_x)
138 .min(self.max_x - x)
139 .min(y - self.min_y)
140 .min(self.max_y - y)
141 }
142
143 pub fn avoid_robustness(&self, x: f64, y: f64) -> f64 {
144 -self.inside_robustness(x, y)
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct StlTimeInterval {
151 pub start: u64,
152 pub end: u64,
153}
154
155impl StlTimeInterval {
156 pub fn new(start: u64, end: u64) -> RoboticsResult<Self> {
157 if start > end {
158 return Err(RoboticsError::InvalidParameter(
159 "STL time interval start must be <= end".to_string(),
160 ));
161 }
162 Ok(Self { start, end })
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub struct StlCbsConflict {
169 pub agent_a: usize,
170 pub agent_b: usize,
171 pub kind: StlCbsConflictKind,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum StlCbsConflictKind {
176 Vertex {
177 x: i32,
178 y: i32,
179 t: u64,
180 },
181 Edge {
182 ax0: i32,
183 ay0: i32,
184 ax1: i32,
185 ay1: i32,
186 bx0: i32,
187 by0: i32,
188 bx1: i32,
189 by1: i32,
190 t: u64,
191 },
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195enum StlCbsConstraintKind {
196 Vertex {
197 x: i32,
198 y: i32,
199 t: u64,
200 },
201 Edge {
202 from_x: i32,
203 from_y: i32,
204 to_x: i32,
205 to_y: i32,
206 t: u64,
207 },
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211struct StlCbsConstraint {
212 agent_id: usize,
213 kind: StlCbsConstraintKind,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217struct CbsNode {
218 constraints: Vec<StlCbsConstraint>,
219 paths: Vec<StlCbsPath>,
220 total_cost: u64,
221 conflicts_resolved: usize,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225struct CbsOpenEntry {
226 node_index: usize,
227 total_cost: u64,
228 conflicts_resolved: usize,
229}
230
231impl Ord for CbsOpenEntry {
232 fn cmp(&self, other: &Self) -> Ordering {
233 other
234 .total_cost
235 .cmp(&self.total_cost)
236 .then_with(|| other.conflicts_resolved.cmp(&self.conflicts_resolved))
237 .then_with(|| other.node_index.cmp(&self.node_index))
238 }
239}
240
241impl PartialOrd for CbsOpenEntry {
242 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
243 Some(self.cmp(other))
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248struct SearchState {
249 x: i32,
250 y: i32,
251 t: u64,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255struct SearchOpenEntry {
256 state: SearchState,
257 f: u64,
258 g: u64,
259}
260
261impl Ord for SearchOpenEntry {
262 fn cmp(&self, other: &Self) -> Ordering {
263 other
264 .f
265 .cmp(&self.f)
266 .then_with(|| other.g.cmp(&self.g))
267 .then_with(|| other.state.t.cmp(&self.state.t))
268 }
269}
270
271impl PartialOrd for SearchOpenEntry {
272 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
273 Some(self.cmp(other))
274 }
275}
276
277#[derive(Debug, Clone, PartialEq)]
279pub struct StlCbsPlanner {
280 config: StlCbsConfig,
281 motions: Vec<(i32, i32)>,
282}
283
284impl StlCbsPlanner {
285 pub fn new(config: StlCbsConfig) -> RoboticsResult<Self> {
286 validate_config(&config)?;
287 let mut motions = vec![(1, 0), (0, 1), (-1, 0), (0, -1)];
288 if config.allow_wait {
289 motions.push((0, 0));
290 }
291 Ok(Self { config, motions })
292 }
293
294 pub fn plan_independent(&self, agents: &[StlCbsAgent]) -> RoboticsResult<Vec<StlCbsPath>> {
295 self.validate_agents(agents)?;
296 agents
297 .iter()
298 .map(|agent| self.plan_agent(*agent, &[]))
299 .collect()
300 }
301
302 pub fn plan(&self, agents: &[StlCbsAgent]) -> RoboticsResult<StlCbsPlan> {
303 self.validate_agents(agents)?;
304 let root_paths = self.plan_independent(agents)?;
305 let root = CbsNode {
306 total_cost: total_path_cost(&root_paths),
307 constraints: Vec::new(),
308 paths: root_paths,
309 conflicts_resolved: 0,
310 };
311
312 let mut nodes = vec![root];
313 let mut open = BinaryHeap::new();
314 open.push(CbsOpenEntry {
315 node_index: 0,
316 total_cost: nodes[0].total_cost,
317 conflicts_resolved: 0,
318 });
319 let mut expanded = 0;
320
321 while let Some(entry) = open.pop() {
322 expanded += 1;
323 if expanded > self.config.max_cbs_nodes {
324 return Err(RoboticsError::PlanningError(
325 "STL-CBS exceeded high-level node limit".to_string(),
326 ));
327 }
328 let node = nodes[entry.node_index].clone();
329 if let Some(conflict) = first_conflict(&node.paths, self.config.max_time) {
330 for constrained_agent in [conflict.agent_a, conflict.agent_b] {
331 let mut constraints = node.constraints.clone();
332 constraints.push(constraint_from_conflict(conflict, constrained_agent));
333 let agent = *agents
334 .iter()
335 .find(|agent| agent.id == constrained_agent)
336 .expect("validated conflict references a known agent");
337 let agent_constraints = constraints_for_agent(&constraints, constrained_agent);
338 let Ok(replanned_path) = self.plan_agent(agent, &agent_constraints) else {
339 continue;
340 };
341 let mut paths = node.paths.clone();
342 let path_index = paths
343 .iter()
344 .position(|path| path.agent_id == constrained_agent)
345 .expect("validated node contains all agent paths");
346 paths[path_index] = replanned_path;
347 let child = CbsNode {
348 total_cost: total_path_cost(&paths),
349 constraints,
350 paths,
351 conflicts_resolved: node.conflicts_resolved + 1,
352 };
353 let node_index = nodes.len();
354 open.push(CbsOpenEntry {
355 node_index,
356 total_cost: child.total_cost,
357 conflicts_resolved: child.conflicts_resolved,
358 });
359 nodes.push(child);
360 }
361 } else {
362 return Ok(StlCbsPlan {
363 total_cost: node.total_cost,
364 min_pairwise_separation_robustness: stl_pairwise_separation_robustness(
365 &node.paths,
366 1.0,
367 StlTimeInterval {
368 start: 0,
369 end: self.config.max_time,
370 },
371 )?,
372 paths: node.paths,
373 conflicts_resolved: node.conflicts_resolved,
374 high_level_nodes_expanded: expanded,
375 });
376 }
377 }
378
379 Err(RoboticsError::PlanningError(
380 "STL-CBS could not find a conflict-free plan".to_string(),
381 ))
382 }
383
384 fn plan_agent(
385 &self,
386 agent: StlCbsAgent,
387 constraints: &[StlCbsConstraintKind],
388 ) -> RoboticsResult<StlCbsPath> {
389 if self.violates_vertex_constraint(agent.start.0, agent.start.1, 0, constraints) {
390 return Err(RoboticsError::PlanningError(
391 "agent start violates a CBS vertex constraint".to_string(),
392 ));
393 }
394 let start = SearchState {
395 x: agent.start.0,
396 y: agent.start.1,
397 t: 0,
398 };
399 let mut open = BinaryHeap::new();
400 let mut best_g = HashMap::new();
401 let mut parent: HashMap<SearchState, Option<SearchState>> = HashMap::new();
402
403 best_g.insert(start, 0);
404 parent.insert(start, None);
405 open.push(SearchOpenEntry {
406 state: start,
407 g: 0,
408 f: manhattan(agent.start, agent.goal),
409 });
410
411 while let Some(entry) = open.pop() {
412 if entry.state.t > self.config.max_time {
413 continue;
414 }
415 if entry.g > *best_g.get(&entry.state).unwrap_or(&u64::MAX) {
416 continue;
417 }
418 if (entry.state.x, entry.state.y) == agent.goal {
419 return Ok(StlCbsPath {
420 agent_id: agent.id,
421 waypoints: reconstruct_timed_path(&parent, entry.state, agent.id),
422 });
423 }
424 let next_t = entry.state.t + 1;
425 if next_t > self.config.max_time {
426 continue;
427 }
428 for &(dx, dy) in &self.motions {
429 let nx = entry.state.x + dx;
430 let ny = entry.state.y + dy;
431 if !self.is_free(nx, ny) {
432 continue;
433 }
434 if self.violates_vertex_constraint(nx, ny, next_t, constraints)
435 || self.violates_edge_constraint(
436 entry.state.x,
437 entry.state.y,
438 nx,
439 ny,
440 next_t,
441 constraints,
442 )
443 {
444 continue;
445 }
446 let next = SearchState {
447 x: nx,
448 y: ny,
449 t: next_t,
450 };
451 let next_g = entry.g + 1;
452 if next_g < *best_g.get(&next).unwrap_or(&u64::MAX) {
453 best_g.insert(next, next_g);
454 parent.insert(next, Some(entry.state));
455 open.push(SearchOpenEntry {
456 state: next,
457 g: next_g,
458 f: next_g + manhattan((nx, ny), agent.goal),
459 });
460 }
461 }
462 }
463
464 Err(RoboticsError::PlanningError(format!(
465 "no path found for agent {}",
466 agent.id
467 )))
468 }
469
470 fn validate_agents(&self, agents: &[StlCbsAgent]) -> RoboticsResult<()> {
471 if agents.is_empty() {
472 return Err(RoboticsError::InvalidParameter(
473 "STL-CBS requires at least one agent".to_string(),
474 ));
475 }
476 let mut ids = HashSet::new();
477 for agent in agents {
478 if !ids.insert(agent.id) {
479 return Err(RoboticsError::InvalidParameter(
480 "STL-CBS agent ids must be unique".to_string(),
481 ));
482 }
483 if !self.is_free(agent.start.0, agent.start.1)
484 || !self.is_free(agent.goal.0, agent.goal.1)
485 {
486 return Err(RoboticsError::InvalidParameter(format!(
487 "agent {} start/goal must be free grid cells",
488 agent.id
489 )));
490 }
491 }
492 Ok(())
493 }
494
495 fn is_free(&self, x: i32, y: i32) -> bool {
496 x >= 0
497 && y >= 0
498 && x < self.config.width
499 && y < self.config.height
500 && !self.config.obstacle_map[x as usize][y as usize]
501 }
502
503 fn violates_vertex_constraint(
504 &self,
505 x: i32,
506 y: i32,
507 t: u64,
508 constraints: &[StlCbsConstraintKind],
509 ) -> bool {
510 constraints.iter().any(|constraint| {
511 matches!(
512 constraint,
513 StlCbsConstraintKind::Vertex { x: cx, y: cy, t: ct }
514 if *cx == x && *cy == y && *ct == t
515 )
516 })
517 }
518
519 fn violates_edge_constraint(
520 &self,
521 from_x: i32,
522 from_y: i32,
523 to_x: i32,
524 to_y: i32,
525 t: u64,
526 constraints: &[StlCbsConstraintKind],
527 ) -> bool {
528 constraints.iter().any(|constraint| {
529 matches!(
530 constraint,
531 StlCbsConstraintKind::Edge {
532 from_x: cx0,
533 from_y: cy0,
534 to_x: cx1,
535 to_y: cy1,
536 t: ct,
537 } if *cx0 == from_x
538 && *cy0 == from_y
539 && *cx1 == to_x
540 && *cy1 == to_y
541 && *ct == t
542 )
543 })
544 }
545}
546
547pub fn stl_eventually_reach_robustness(
549 path: &StlCbsPath,
550 region: StlRectangle2D,
551 interval: StlTimeInterval,
552) -> RoboticsResult<f64> {
553 validate_path(path)?;
554 let mut robustness = f64::NEG_INFINITY;
555 for t in interval.start..=interval.end {
556 let position = path.position_at(t);
557 robustness = robustness.max(region.inside_robustness(position.x as f64, position.y as f64));
558 }
559 Ok(robustness)
560}
561
562pub fn stl_always_avoid_robustness(
564 path: &StlCbsPath,
565 region: StlRectangle2D,
566 interval: StlTimeInterval,
567) -> RoboticsResult<f64> {
568 validate_path(path)?;
569 let mut robustness = f64::INFINITY;
570 for t in interval.start..=interval.end {
571 let position = path.position_at(t);
572 robustness = robustness.min(region.avoid_robustness(position.x as f64, position.y as f64));
573 }
574 Ok(robustness)
575}
576
577pub fn stl_pairwise_separation_robustness(
579 paths: &[StlCbsPath],
580 min_distance: f64,
581 interval: StlTimeInterval,
582) -> RoboticsResult<f64> {
583 if paths.len() < 2 {
584 return Ok(f64::INFINITY);
585 }
586 if min_distance < 0.0 || !min_distance.is_finite() {
587 return Err(RoboticsError::InvalidParameter(
588 "STL min_distance must be finite and non-negative".to_string(),
589 ));
590 }
591 for path in paths {
592 validate_path(path)?;
593 }
594 let mut robustness = f64::INFINITY;
595 for t in interval.start..=interval.end {
596 for i in 0..paths.len() {
597 for j in i + 1..paths.len() {
598 let a = paths[i].position_at(t);
599 let b = paths[j].position_at(t);
600 robustness = robustness.min(euclidean((a.x, a.y), (b.x, b.y)) - min_distance);
601 }
602 }
603 }
604 Ok(robustness)
605}
606
607pub fn first_conflict(paths: &[StlCbsPath], max_time: u64) -> Option<StlCbsConflict> {
608 for t in 0..=max_time {
609 for i in 0..paths.len() {
610 for j in i + 1..paths.len() {
611 let a = paths[i].position_at(t);
612 let b = paths[j].position_at(t);
613 if a.x == b.x && a.y == b.y {
614 return Some(StlCbsConflict {
615 agent_a: paths[i].agent_id,
616 agent_b: paths[j].agent_id,
617 kind: StlCbsConflictKind::Vertex { x: a.x, y: a.y, t },
618 });
619 }
620 if t > 0 {
621 let a_prev = paths[i].position_at(t - 1);
622 let b_prev = paths[j].position_at(t - 1);
623 if a_prev.x == b.x && a_prev.y == b.y && b_prev.x == a.x && b_prev.y == a.y {
624 return Some(StlCbsConflict {
625 agent_a: paths[i].agent_id,
626 agent_b: paths[j].agent_id,
627 kind: StlCbsConflictKind::Edge {
628 ax0: a_prev.x,
629 ay0: a_prev.y,
630 ax1: a.x,
631 ay1: a.y,
632 bx0: b_prev.x,
633 by0: b_prev.y,
634 bx1: b.x,
635 by1: b.y,
636 t,
637 },
638 });
639 }
640 }
641 }
642 }
643 }
644 None
645}
646
647fn constraint_from_conflict(
648 conflict: StlCbsConflict,
649 constrained_agent: usize,
650) -> StlCbsConstraint {
651 let kind = match conflict.kind {
652 StlCbsConflictKind::Vertex { x, y, t } => StlCbsConstraintKind::Vertex { x, y, t },
653 StlCbsConflictKind::Edge {
654 ax0,
655 ay0,
656 ax1,
657 ay1,
658 bx0,
659 by0,
660 bx1,
661 by1,
662 t,
663 } => {
664 if constrained_agent == conflict.agent_a {
665 StlCbsConstraintKind::Edge {
666 from_x: ax0,
667 from_y: ay0,
668 to_x: ax1,
669 to_y: ay1,
670 t,
671 }
672 } else {
673 StlCbsConstraintKind::Edge {
674 from_x: bx0,
675 from_y: by0,
676 to_x: bx1,
677 to_y: by1,
678 t,
679 }
680 }
681 }
682 };
683 StlCbsConstraint {
684 agent_id: constrained_agent,
685 kind,
686 }
687}
688
689fn constraints_for_agent(
690 constraints: &[StlCbsConstraint],
691 agent_id: usize,
692) -> Vec<StlCbsConstraintKind> {
693 constraints
694 .iter()
695 .filter(|constraint| constraint.agent_id == agent_id)
696 .map(|constraint| constraint.kind)
697 .collect()
698}
699
700fn total_path_cost(paths: &[StlCbsPath]) -> u64 {
701 paths.iter().map(StlCbsPath::arrival_time).sum()
702}
703
704fn reconstruct_timed_path(
705 parent: &HashMap<SearchState, Option<SearchState>>,
706 mut current: SearchState,
707 agent_id: usize,
708) -> Vec<StlTimedCell> {
709 let mut reversed = Vec::new();
710 loop {
711 reversed.push(StlTimedCell::new(current.x, current.y, current.t));
712 match parent.get(¤t).copied().flatten() {
713 Some(previous) => current = previous,
714 None => break,
715 }
716 }
717 reversed.reverse();
718 let _ = agent_id;
719 reversed
720}
721
722fn manhattan(a: (i32, i32), b: (i32, i32)) -> u64 {
723 (a.0 - b.0).unsigned_abs() as u64 + (a.1 - b.1).unsigned_abs() as u64
724}
725
726fn euclidean(a: (i32, i32), b: (i32, i32)) -> f64 {
727 let dx = (a.0 - b.0) as f64;
728 let dy = (a.1 - b.1) as f64;
729 (dx * dx + dy * dy).sqrt()
730}
731
732fn validate_config(config: &StlCbsConfig) -> RoboticsResult<()> {
733 if config.width <= 0 || config.height <= 0 {
734 return Err(RoboticsError::InvalidParameter(
735 "STL-CBS width and height must be positive".to_string(),
736 ));
737 }
738 if config.max_time == 0 || config.max_cbs_nodes == 0 {
739 return Err(RoboticsError::InvalidParameter(
740 "STL-CBS max_time and max_cbs_nodes must be positive".to_string(),
741 ));
742 }
743 if config.obstacle_map.len() != config.width as usize {
744 return Err(RoboticsError::InvalidParameter(
745 "STL-CBS obstacle_map x-dimension must match width".to_string(),
746 ));
747 }
748 for column in &config.obstacle_map {
749 if column.len() != config.height as usize {
750 return Err(RoboticsError::InvalidParameter(
751 "STL-CBS obstacle_map y-dimension must match height".to_string(),
752 ));
753 }
754 }
755 Ok(())
756}
757
758fn validate_path(path: &StlCbsPath) -> RoboticsResult<()> {
759 if path.waypoints.is_empty() {
760 return Err(RoboticsError::InvalidParameter(
761 "STL-CBS path must contain at least one waypoint".to_string(),
762 ));
763 }
764 for window in path.waypoints.windows(2) {
765 if window[1].t <= window[0].t {
766 return Err(RoboticsError::InvalidParameter(
767 "STL-CBS path times must be strictly increasing".to_string(),
768 ));
769 }
770 }
771 Ok(())
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 fn corridor_planner() -> StlCbsPlanner {
779 StlCbsPlanner::new(StlCbsConfig {
780 max_time: 12,
781 ..StlCbsConfig::new(5, 3)
782 })
783 .unwrap()
784 }
785
786 #[test]
787 fn independent_paths_have_a_conflict() {
788 let planner = corridor_planner();
789 let agents = [
790 StlCbsAgent::new(0, (0, 1), (4, 1)),
791 StlCbsAgent::new(1, (4, 1), (0, 1)),
792 ];
793 let paths = planner.plan_independent(&agents).unwrap();
794
795 assert!(matches!(
796 first_conflict(&paths, 12).unwrap().kind,
797 StlCbsConflictKind::Vertex { .. } | StlCbsConflictKind::Edge { .. }
798 ));
799 }
800
801 #[test]
802 fn cbs_resolves_two_agent_swap() {
803 let planner = corridor_planner();
804 let agents = [
805 StlCbsAgent::new(0, (0, 1), (4, 1)),
806 StlCbsAgent::new(1, (4, 1), (0, 1)),
807 ];
808 let plan = planner.plan(&agents).unwrap();
809
810 assert!(plan.conflicts_resolved > 0);
811 assert!(first_conflict(&plan.paths, 12).is_none());
812 assert!(plan.min_pairwise_separation_robustness >= 0.0);
813 }
814
815 #[test]
816 fn stl_goal_and_avoid_robustness_have_expected_signs() {
817 let path = StlCbsPath {
818 agent_id: 0,
819 waypoints: vec![
820 StlTimedCell::new(0, 0, 0),
821 StlTimedCell::new(1, 0, 1),
822 StlTimedCell::new(2, 0, 2),
823 ],
824 };
825 let goal = StlRectangle2D::new(1.5, 2.5, -0.5, 0.5).unwrap();
826 let hazard = StlRectangle2D::new(0.8, 1.2, -0.2, 0.2).unwrap();
827 let interval = StlTimeInterval::new(0, 3).unwrap();
828
829 assert!(stl_eventually_reach_robustness(&path, goal, interval).unwrap() > 0.0);
830 assert!(stl_always_avoid_robustness(&path, hazard, interval).unwrap() < 0.0);
831 }
832}