Skip to main content

u_schedule/models/
schedule.rs

1//! Schedule (solution) model.
2//!
3//! A schedule is a complete assignment of activities to resources and
4//! time slots. It may include constraint violations for infeasible
5//! or suboptimal solutions.
6//!
7//! # Reference
8//! Pinedo (2016), "Scheduling: Theory, Algorithms, and Systems", Ch. 3
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// A complete schedule (solution to a scheduling problem).
14///
15/// Contains activity-resource-time assignments and any constraint violations.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct Schedule {
18    /// Activity assignments (activity → resource × time).
19    pub assignments: Vec<Assignment>,
20    /// Constraint violations detected in this schedule.
21    pub violations: Vec<Violation>,
22}
23
24/// An activity-resource-time assignment.
25///
26/// Records that a specific activity is scheduled on a specific resource
27/// during a specific time interval.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Assignment {
30    /// Assigned activity ID.
31    pub activity_id: String,
32    /// Parent task ID (denormalized for query convenience).
33    pub task_id: String,
34    /// Assigned resource ID.
35    pub resource_id: String,
36    /// Start time (ms).
37    pub start_ms: i64,
38    /// End time (ms).
39    pub end_ms: i64,
40    /// Setup time portion (ms). Included in [start_ms, start_ms + setup_ms).
41    pub setup_ms: i64,
42}
43
44/// A constraint violation.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Violation {
47    /// Type of violation.
48    pub violation_type: ViolationType,
49    /// Related entity ID (task, resource, or activity).
50    pub entity_id: String,
51    /// Human-readable description.
52    pub message: String,
53    /// Severity (0-100, higher = worse).
54    pub severity: i32,
55}
56
57/// Classification of constraint violations.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum ViolationType {
60    /// Task completed after its deadline.
61    DeadlineMiss,
62    /// Resource allocated beyond its capacity.
63    CapacityExceeded,
64    /// Activity started before its predecessor finished.
65    PrecedenceViolation,
66    /// Activity scheduled when resource is unavailable.
67    ResourceUnavailable,
68    /// Resource lacks a required skill.
69    SkillMismatch,
70    /// Domain-specific violation.
71    Custom(String),
72}
73
74impl Assignment {
75    /// Creates a new assignment.
76    pub fn new(
77        activity_id: impl Into<String>,
78        task_id: impl Into<String>,
79        resource_id: impl Into<String>,
80        start_ms: i64,
81        end_ms: i64,
82    ) -> Self {
83        Self {
84            activity_id: activity_id.into(),
85            task_id: task_id.into(),
86            resource_id: resource_id.into(),
87            start_ms,
88            end_ms,
89            setup_ms: 0,
90        }
91    }
92
93    /// Sets the setup time.
94    pub fn with_setup(mut self, setup_ms: i64) -> Self {
95        self.setup_ms = setup_ms;
96        self
97    }
98
99    /// Total duration (end - start) in ms.
100    #[inline]
101    pub fn duration_ms(&self) -> i64 {
102        self.end_ms - self.start_ms
103    }
104
105    /// Processing duration excluding setup (ms).
106    #[inline]
107    pub fn process_ms(&self) -> i64 {
108        self.duration_ms() - self.setup_ms
109    }
110}
111
112impl Violation {
113    /// Creates a deadline miss violation.
114    pub fn deadline_miss(task_id: impl Into<String>, message: impl Into<String>) -> Self {
115        Self {
116            violation_type: ViolationType::DeadlineMiss,
117            entity_id: task_id.into(),
118            message: message.into(),
119            severity: 80,
120        }
121    }
122
123    /// Creates a capacity exceeded violation.
124    pub fn capacity_exceeded(resource_id: impl Into<String>, message: impl Into<String>) -> Self {
125        Self {
126            violation_type: ViolationType::CapacityExceeded,
127            entity_id: resource_id.into(),
128            message: message.into(),
129            severity: 90,
130        }
131    }
132
133    /// Creates a precedence violation.
134    pub fn precedence_violation(
135        activity_id: impl Into<String>,
136        message: impl Into<String>,
137    ) -> Self {
138        Self {
139            violation_type: ViolationType::PrecedenceViolation,
140            entity_id: activity_id.into(),
141            message: message.into(),
142            severity: 95,
143        }
144    }
145}
146
147impl Schedule {
148    /// Creates an empty schedule.
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Adds an assignment.
154    pub fn add_assignment(&mut self, assignment: Assignment) {
155        self.assignments.push(assignment);
156    }
157
158    /// Adds a violation.
159    pub fn add_violation(&mut self, violation: Violation) {
160        self.violations.push(violation);
161    }
162
163    /// Whether the schedule has no violations.
164    pub fn is_valid(&self) -> bool {
165        self.violations.is_empty()
166    }
167
168    /// Makespan: latest end time across all assignments (ms).
169    pub fn makespan_ms(&self) -> i64 {
170        self.assignments.iter().map(|a| a.end_ms).max().unwrap_or(0)
171    }
172
173    /// Finds the assignment for a given activity.
174    pub fn assignment_for_activity(&self, activity_id: &str) -> Option<&Assignment> {
175        self.assignments
176            .iter()
177            .find(|a| a.activity_id == activity_id)
178    }
179
180    /// Returns all assignments for a given task.
181    pub fn assignments_for_task(&self, task_id: &str) -> Vec<&Assignment> {
182        self.assignments
183            .iter()
184            .filter(|a| a.task_id == task_id)
185            .collect()
186    }
187
188    /// Returns all assignments for a given resource.
189    pub fn assignments_for_resource(&self, resource_id: &str) -> Vec<&Assignment> {
190        self.assignments
191            .iter()
192            .filter(|a| a.resource_id == resource_id)
193            .collect()
194    }
195
196    /// Computes resource utilization: busy_time / horizon.
197    ///
198    /// Returns `None` if `horizon_ms` is zero.
199    pub fn resource_utilization(&self, resource_id: &str, horizon_ms: i64) -> Option<f64> {
200        if horizon_ms <= 0 {
201            return None;
202        }
203        let busy: i64 = self
204            .assignments_for_resource(resource_id)
205            .iter()
206            .map(|a| a.duration_ms())
207            .sum();
208        Some(busy as f64 / horizon_ms as f64)
209    }
210
211    /// Computes utilization for all resources that have assignments.
212    ///
213    /// Uses makespan as the horizon.
214    pub fn all_utilizations(&self) -> HashMap<String, f64> {
215        let horizon = self.makespan_ms();
216        if horizon <= 0 {
217            return HashMap::new();
218        }
219
220        let mut resource_busy: HashMap<String, i64> = HashMap::new();
221        for a in &self.assignments {
222            *resource_busy.entry(a.resource_id.clone()).or_insert(0) += a.duration_ms();
223        }
224
225        resource_busy
226            .into_iter()
227            .map(|(id, busy)| (id, busy as f64 / horizon as f64))
228            .collect()
229    }
230
231    /// Completion time for a task (latest end of its assignments).
232    pub fn task_completion_time(&self, task_id: &str) -> Option<i64> {
233        self.assignments_for_task(task_id)
234            .iter()
235            .map(|a| a.end_ms)
236            .max()
237    }
238
239    /// Number of assignments.
240    pub fn assignment_count(&self) -> usize {
241        self.assignments.len()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn sample_schedule() -> Schedule {
250        let mut s = Schedule::new();
251        s.add_assignment(Assignment::new("O1", "J1", "M1", 0, 5000).with_setup(500));
252        s.add_assignment(Assignment::new("O2", "J1", "M2", 1000, 4000));
253        s.add_assignment(Assignment::new("O3", "J2", "M1", 5000, 8000));
254        s
255    }
256
257    #[test]
258    fn test_schedule_makespan() {
259        let s = sample_schedule();
260        assert_eq!(s.makespan_ms(), 8000);
261    }
262
263    #[test]
264    fn test_schedule_is_valid() {
265        let s = sample_schedule();
266        assert!(s.is_valid());
267
268        let mut s2 = sample_schedule();
269        s2.add_violation(Violation::deadline_miss("J1", "Late by 1000ms"));
270        assert!(!s2.is_valid());
271    }
272
273    #[test]
274    fn test_assignment_duration() {
275        let a = Assignment::new("O1", "J1", "M1", 0, 5000).with_setup(500);
276        assert_eq!(a.duration_ms(), 5000);
277        assert_eq!(a.process_ms(), 4500);
278        assert_eq!(a.setup_ms, 500);
279    }
280
281    #[test]
282    fn test_assignment_for_activity() {
283        let s = sample_schedule();
284        let a = s.assignment_for_activity("O1").unwrap();
285        assert_eq!(a.resource_id, "M1");
286        assert!(s.assignment_for_activity("O99").is_none());
287    }
288
289    #[test]
290    fn test_assignments_for_task() {
291        let s = sample_schedule();
292        let j1 = s.assignments_for_task("J1");
293        assert_eq!(j1.len(), 2);
294        let j2 = s.assignments_for_task("J2");
295        assert_eq!(j2.len(), 1);
296    }
297
298    #[test]
299    fn test_assignments_for_resource() {
300        let s = sample_schedule();
301        let m1 = s.assignments_for_resource("M1");
302        assert_eq!(m1.len(), 2); // O1 and O3
303    }
304
305    #[test]
306    fn test_resource_utilization() {
307        let s = sample_schedule();
308        // M1: busy 5000 + 3000 = 8000 over horizon 8000 → 1.0
309        let util = s.resource_utilization("M1", 8000).unwrap();
310        assert!((util - 1.0).abs() < 1e-10);
311
312        // M2: busy 3000 over horizon 8000 → 0.375
313        let util2 = s.resource_utilization("M2", 8000).unwrap();
314        assert!((util2 - 0.375).abs() < 1e-10);
315    }
316
317    #[test]
318    fn test_task_completion_time() {
319        let s = sample_schedule();
320        assert_eq!(s.task_completion_time("J1"), Some(5000)); // max(5000, 4000)
321        assert_eq!(s.task_completion_time("J2"), Some(8000));
322        assert_eq!(s.task_completion_time("J99"), None);
323    }
324
325    #[test]
326    fn test_all_utilizations() {
327        let s = sample_schedule();
328        let utils = s.all_utilizations();
329        assert!((utils["M1"] - 1.0).abs() < 1e-10);
330        assert!((utils["M2"] - 0.375).abs() < 1e-10);
331    }
332
333    #[test]
334    fn test_empty_schedule() {
335        let s = Schedule::new();
336        assert_eq!(s.makespan_ms(), 0);
337        assert!(s.is_valid());
338        assert_eq!(s.assignment_count(), 0);
339    }
340
341    #[test]
342    fn test_violation_factories() {
343        let v1 = Violation::deadline_miss("J1", "Late");
344        assert_eq!(v1.violation_type, ViolationType::DeadlineMiss);
345        assert_eq!(v1.entity_id, "J1");
346
347        let v2 = Violation::capacity_exceeded("M1", "Over capacity");
348        assert_eq!(v2.violation_type, ViolationType::CapacityExceeded);
349
350        let v3 = Violation::precedence_violation("O2", "Started before O1");
351        assert_eq!(v3.violation_type, ViolationType::PrecedenceViolation);
352    }
353}