Skip to main content

waremax_map/
reservation.rs

1//! Reservation-based traffic control for proactive conflict prevention
2//!
3//! This module provides time-windowed resource reservations that allow robots
4//! to reserve edges and nodes ahead of time, preventing conflicts proactively
5//! rather than reactively.
6
7use std::collections::HashMap;
8use waremax_core::{EdgeId, NodeId, RobotId, SimTime};
9
10/// A resource that can be reserved (edge or node)
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub enum ReservableResource {
13    /// An edge between two nodes
14    Edge(EdgeId),
15    /// A node in the warehouse map
16    Node(NodeId),
17}
18
19/// A time-windowed reservation of a resource
20#[derive(Clone, Debug)]
21pub struct Reservation {
22    /// Robot that holds this reservation
23    pub robot_id: RobotId,
24    /// Resource being reserved
25    pub resource: ReservableResource,
26    /// Start time of the reservation window
27    pub start_time: SimTime,
28    /// End time of the reservation window
29    pub end_time: SimTime,
30}
31
32impl Reservation {
33    /// Create a new reservation
34    pub fn new(
35        robot_id: RobotId,
36        resource: ReservableResource,
37        start_time: SimTime,
38        end_time: SimTime,
39    ) -> Self {
40        Self {
41            robot_id,
42            resource,
43            start_time,
44            end_time,
45        }
46    }
47
48    /// Check if this reservation overlaps with a time window
49    pub fn overlaps(&self, start: SimTime, end: SimTime) -> bool {
50        self.start_time < end && self.end_time > start
51    }
52}
53
54/// Error when a reservation conflicts with existing reservations
55#[derive(Clone, Debug)]
56pub struct ReservationConflict {
57    /// The resource that has a conflict
58    pub resource: ReservableResource,
59    /// The robot that holds the conflicting reservation
60    pub conflicting_robot: RobotId,
61    /// Start of the conflicting time window
62    pub conflict_start: SimTime,
63    /// End of the conflicting time window
64    pub conflict_end: SimTime,
65}
66
67/// Manages time-windowed reservations for edges and nodes
68///
69/// Allows robots to reserve resources for specific time windows,
70/// enabling proactive conflict detection and prevention.
71#[derive(Clone, Default)]
72pub struct ReservationManager {
73    /// Map from resource to list of reservations
74    reservations: HashMap<ReservableResource, Vec<Reservation>>,
75    /// Whether reservation system is enabled
76    pub enabled: bool,
77}
78
79impl ReservationManager {
80    /// Create a new reservation manager (disabled by default)
81    pub fn new() -> Self {
82        Self {
83            reservations: HashMap::new(),
84            enabled: false,
85        }
86    }
87
88    /// Create a new enabled reservation manager
89    pub fn new_enabled() -> Self {
90        Self {
91            reservations: HashMap::new(),
92            enabled: true,
93        }
94    }
95
96    /// Check if a resource can be reserved for a time window
97    ///
98    /// Returns true if no conflicting reservations exist, or if the
99    /// reservation system is disabled.
100    pub fn can_reserve(
101        &self,
102        resource: &ReservableResource,
103        robot: RobotId,
104        start: SimTime,
105        end: SimTime,
106    ) -> bool {
107        if !self.enabled {
108            return true;
109        }
110
111        if let Some(reservations) = self.reservations.get(resource) {
112            !reservations
113                .iter()
114                .any(|r| r.robot_id != robot && r.overlaps(start, end))
115        } else {
116            true
117        }
118    }
119
120    /// Reserve a resource for a time window
121    ///
122    /// Returns Ok(()) if the reservation was successful, or an error
123    /// describing the conflict if another robot has a conflicting reservation.
124    pub fn reserve(
125        &mut self,
126        resource: ReservableResource,
127        robot: RobotId,
128        start: SimTime,
129        end: SimTime,
130    ) -> Result<(), ReservationConflict> {
131        if !self.enabled {
132            return Ok(());
133        }
134
135        // Check for conflicts
136        if let Some(reservations) = self.reservations.get(&resource) {
137            for r in reservations {
138                if r.robot_id != robot && r.overlaps(start, end) {
139                    return Err(ReservationConflict {
140                        resource: resource.clone(),
141                        conflicting_robot: r.robot_id,
142                        conflict_start: r.start_time,
143                        conflict_end: r.end_time,
144                    });
145                }
146            }
147        }
148
149        // Add reservation
150        self.reservations
151            .entry(resource.clone())
152            .or_default()
153            .push(Reservation::new(robot, resource, start, end));
154        Ok(())
155    }
156
157    /// Release all reservations for a robot
158    ///
159    /// Called when a robot completes its task or aborts.
160    pub fn release_all(&mut self, robot: RobotId) {
161        for reservations in self.reservations.values_mut() {
162            reservations.retain(|r| r.robot_id != robot);
163        }
164    }
165
166    /// Release reservations for a robot on a specific resource
167    pub fn release(&mut self, resource: &ReservableResource, robot: RobotId) {
168        if let Some(reservations) = self.reservations.get_mut(resource) {
169            reservations.retain(|r| r.robot_id != robot);
170        }
171    }
172
173    /// Clean up expired reservations (those that ended before current_time)
174    ///
175    /// Should be called periodically to prevent memory growth.
176    pub fn cleanup_expired(&mut self, current_time: SimTime) {
177        for reservations in self.reservations.values_mut() {
178            reservations.retain(|r| r.end_time > current_time);
179        }
180    }
181
182    /// Get all conflicts for a proposed reservation
183    ///
184    /// Returns a list of reservations that would conflict with the proposed
185    /// reservation window.
186    pub fn get_conflicts(
187        &self,
188        resource: &ReservableResource,
189        robot: RobotId,
190        start: SimTime,
191        end: SimTime,
192    ) -> Vec<&Reservation> {
193        if let Some(reservations) = self.reservations.get(resource) {
194            reservations
195                .iter()
196                .filter(|r| r.robot_id != robot && r.overlaps(start, end))
197                .collect()
198        } else {
199            Vec::new()
200        }
201    }
202
203    /// Get all reservations for a specific robot
204    pub fn get_robot_reservations(&self, robot: RobotId) -> Vec<&Reservation> {
205        self.reservations
206            .values()
207            .flat_map(|v| v.iter())
208            .filter(|r| r.robot_id == robot)
209            .collect()
210    }
211
212    /// Get the number of active reservations
213    pub fn reservation_count(&self) -> usize {
214        self.reservations.values().map(|v| v.len()).sum()
215    }
216
217    /// Check if a resource has any reservations in a time window
218    pub fn has_reservations(
219        &self,
220        resource: &ReservableResource,
221        start: SimTime,
222        end: SimTime,
223    ) -> bool {
224        if let Some(reservations) = self.reservations.get(resource) {
225            reservations.iter().any(|r| r.overlaps(start, end))
226        } else {
227            false
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    fn t(seconds: f64) -> SimTime {
237        SimTime::from_seconds(seconds)
238    }
239
240    #[test]
241    fn test_reservation_overlaps() {
242        let res = Reservation::new(
243            RobotId(1),
244            ReservableResource::Edge(EdgeId(1)),
245            t(10.0),
246            t(20.0),
247        );
248
249        // Overlapping cases
250        assert!(res.overlaps(t(15.0), t(25.0))); // Overlaps at end
251        assert!(res.overlaps(t(5.0), t(15.0))); // Overlaps at start
252        assert!(res.overlaps(t(12.0), t(18.0))); // Fully inside
253        assert!(res.overlaps(t(5.0), t(25.0))); // Fully contains
254
255        // Non-overlapping cases
256        assert!(!res.overlaps(t(0.0), t(10.0))); // Ends exactly at start
257        assert!(!res.overlaps(t(20.0), t(30.0))); // Starts exactly at end
258        assert!(!res.overlaps(t(0.0), t(5.0))); // Before
259        assert!(!res.overlaps(t(25.0), t(30.0))); // After
260    }
261
262    #[test]
263    fn test_reservation_manager_disabled() {
264        let mut mgr = ReservationManager::new();
265        assert!(!mgr.enabled);
266
267        // Can always reserve when disabled
268        assert!(mgr.can_reserve(
269            &ReservableResource::Edge(EdgeId(1)),
270            RobotId(1),
271            t(0.0),
272            t(10.0)
273        ));
274
275        // Reserve succeeds even with conflicts when disabled
276        mgr.reserve(
277            ReservableResource::Edge(EdgeId(1)),
278            RobotId(1),
279            t(0.0),
280            t(10.0),
281        )
282        .unwrap();
283
284        // Overlapping reservation by different robot succeeds when disabled
285        mgr.reserve(
286            ReservableResource::Edge(EdgeId(1)),
287            RobotId(2),
288            t(5.0),
289            t(15.0),
290        )
291        .unwrap();
292    }
293
294    #[test]
295    fn test_reservation_manager_enabled() {
296        let mut mgr = ReservationManager::new_enabled();
297        assert!(mgr.enabled);
298
299        // First reservation succeeds
300        mgr.reserve(
301            ReservableResource::Edge(EdgeId(1)),
302            RobotId(1),
303            t(0.0),
304            t(10.0),
305        )
306        .unwrap();
307
308        // Same robot can reserve overlapping window
309        assert!(mgr.can_reserve(
310            &ReservableResource::Edge(EdgeId(1)),
311            RobotId(1),
312            t(5.0),
313            t(15.0)
314        ));
315
316        // Different robot cannot reserve overlapping window
317        assert!(!mgr.can_reserve(
318            &ReservableResource::Edge(EdgeId(1)),
319            RobotId(2),
320            t(5.0),
321            t(15.0)
322        ));
323
324        // Different robot can reserve non-overlapping window
325        assert!(mgr.can_reserve(
326            &ReservableResource::Edge(EdgeId(1)),
327            RobotId(2),
328            t(10.0),
329            t(20.0)
330        ));
331    }
332
333    #[test]
334    fn test_reservation_conflict() {
335        let mut mgr = ReservationManager::new_enabled();
336
337        // First reservation
338        mgr.reserve(
339            ReservableResource::Edge(EdgeId(1)),
340            RobotId(1),
341            t(0.0),
342            t(10.0),
343        )
344        .unwrap();
345
346        // Conflicting reservation fails
347        let result = mgr.reserve(
348            ReservableResource::Edge(EdgeId(1)),
349            RobotId(2),
350            t(5.0),
351            t(15.0),
352        );
353
354        assert!(result.is_err());
355        let conflict = result.unwrap_err();
356        assert_eq!(conflict.conflicting_robot, RobotId(1));
357        assert_eq!(conflict.conflict_start, t(0.0));
358        assert_eq!(conflict.conflict_end, t(10.0));
359    }
360
361    #[test]
362    fn test_release_all() {
363        let mut mgr = ReservationManager::new_enabled();
364
365        // Robot 1 reserves multiple resources
366        mgr.reserve(
367            ReservableResource::Edge(EdgeId(1)),
368            RobotId(1),
369            t(0.0),
370            t(10.0),
371        )
372        .unwrap();
373        mgr.reserve(
374            ReservableResource::Edge(EdgeId(2)),
375            RobotId(1),
376            t(10.0),
377            t(20.0),
378        )
379        .unwrap();
380        mgr.reserve(
381            ReservableResource::Node(NodeId(5)),
382            RobotId(1),
383            t(0.0),
384            t(5.0),
385        )
386        .unwrap();
387
388        assert_eq!(mgr.reservation_count(), 3);
389
390        // Release all for robot 1
391        mgr.release_all(RobotId(1));
392
393        assert_eq!(mgr.reservation_count(), 0);
394    }
395
396    #[test]
397    fn test_release_specific() {
398        let mut mgr = ReservationManager::new_enabled();
399
400        let edge1 = ReservableResource::Edge(EdgeId(1));
401        let edge2 = ReservableResource::Edge(EdgeId(2));
402
403        mgr.reserve(edge1.clone(), RobotId(1), t(0.0), t(10.0))
404            .unwrap();
405        mgr.reserve(edge2.clone(), RobotId(1), t(0.0), t(10.0))
406            .unwrap();
407
408        assert_eq!(mgr.reservation_count(), 2);
409
410        // Release only edge1
411        mgr.release(&edge1, RobotId(1));
412
413        assert_eq!(mgr.reservation_count(), 1);
414
415        // Robot 2 can now reserve edge1
416        assert!(mgr.can_reserve(&edge1, RobotId(2), t(0.0), t(10.0)));
417        // But not edge2
418        assert!(!mgr.can_reserve(&edge2, RobotId(2), t(0.0), t(10.0)));
419    }
420
421    #[test]
422    fn test_cleanup_expired() {
423        let mut mgr = ReservationManager::new_enabled();
424
425        // Add reservations at different times
426        mgr.reserve(
427            ReservableResource::Edge(EdgeId(1)),
428            RobotId(1),
429            t(0.0),
430            t(10.0),
431        )
432        .unwrap();
433        mgr.reserve(
434            ReservableResource::Edge(EdgeId(2)),
435            RobotId(1),
436            t(5.0),
437            t(15.0),
438        )
439        .unwrap();
440        mgr.reserve(
441            ReservableResource::Edge(EdgeId(3)),
442            RobotId(1),
443            t(10.0),
444            t(20.0),
445        )
446        .unwrap();
447
448        assert_eq!(mgr.reservation_count(), 3);
449
450        // Cleanup at t=10 should remove first reservation
451        mgr.cleanup_expired(t(10.0));
452
453        assert_eq!(mgr.reservation_count(), 2);
454
455        // Cleanup at t=15 should remove second reservation
456        mgr.cleanup_expired(t(15.0));
457
458        assert_eq!(mgr.reservation_count(), 1);
459    }
460
461    #[test]
462    fn test_get_conflicts() {
463        let mut mgr = ReservationManager::new_enabled();
464
465        mgr.reserve(
466            ReservableResource::Edge(EdgeId(1)),
467            RobotId(1),
468            t(0.0),
469            t(10.0),
470        )
471        .unwrap();
472        mgr.reserve(
473            ReservableResource::Edge(EdgeId(1)),
474            RobotId(2),
475            t(15.0),
476            t(25.0),
477        )
478        .unwrap();
479
480        let edge1 = ReservableResource::Edge(EdgeId(1));
481
482        // Robot 3 checking window that overlaps robot 1
483        let conflicts = mgr.get_conflicts(&edge1, RobotId(3), t(5.0), t(12.0));
484        assert_eq!(conflicts.len(), 1);
485        assert_eq!(conflicts[0].robot_id, RobotId(1));
486
487        // Robot 3 checking window that overlaps both
488        let conflicts = mgr.get_conflicts(&edge1, RobotId(3), t(5.0), t(20.0));
489        assert_eq!(conflicts.len(), 2);
490
491        // Robot 1 checking its own reservation (no conflict with self)
492        let conflicts = mgr.get_conflicts(&edge1, RobotId(1), t(0.0), t(10.0));
493        assert_eq!(conflicts.len(), 0);
494    }
495
496    #[test]
497    fn test_node_reservations() {
498        let mut mgr = ReservationManager::new_enabled();
499
500        let node = ReservableResource::Node(NodeId(5));
501
502        mgr.reserve(node.clone(), RobotId(1), t(0.0), t(10.0))
503            .unwrap();
504
505        // Different robot cannot reserve same node at overlapping time
506        assert!(!mgr.can_reserve(&node, RobotId(2), t(5.0), t(15.0)));
507
508        // But can reserve after
509        assert!(mgr.can_reserve(&node, RobotId(2), t(10.0), t(20.0)));
510    }
511
512    #[test]
513    fn test_get_robot_reservations() {
514        let mut mgr = ReservationManager::new_enabled();
515
516        mgr.reserve(
517            ReservableResource::Edge(EdgeId(1)),
518            RobotId(1),
519            t(0.0),
520            t(10.0),
521        )
522        .unwrap();
523        mgr.reserve(
524            ReservableResource::Edge(EdgeId(2)),
525            RobotId(1),
526            t(10.0),
527            t(20.0),
528        )
529        .unwrap();
530        mgr.reserve(
531            ReservableResource::Edge(EdgeId(3)),
532            RobotId(2),
533            t(0.0),
534            t(10.0),
535        )
536        .unwrap();
537
538        let robot1_res = mgr.get_robot_reservations(RobotId(1));
539        assert_eq!(robot1_res.len(), 2);
540
541        let robot2_res = mgr.get_robot_reservations(RobotId(2));
542        assert_eq!(robot2_res.len(), 1);
543
544        let robot3_res = mgr.get_robot_reservations(RobotId(3));
545        assert_eq!(robot3_res.len(), 0);
546    }
547}