1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct Schedule {
18 pub assignments: Vec<Assignment>,
20 pub violations: Vec<Violation>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Assignment {
30 pub activity_id: String,
32 pub task_id: String,
34 pub resource_id: String,
36 pub start_ms: i64,
38 pub end_ms: i64,
40 pub setup_ms: i64,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Violation {
47 pub violation_type: ViolationType,
49 pub entity_id: String,
51 pub message: String,
53 pub severity: i32,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum ViolationType {
60 DeadlineMiss,
62 CapacityExceeded,
64 PrecedenceViolation,
66 ResourceUnavailable,
68 SkillMismatch,
70 Custom(String),
72}
73
74impl Assignment {
75 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 pub fn with_setup(mut self, setup_ms: i64) -> Self {
95 self.setup_ms = setup_ms;
96 self
97 }
98
99 #[inline]
101 pub fn duration_ms(&self) -> i64 {
102 self.end_ms - self.start_ms
103 }
104
105 #[inline]
107 pub fn process_ms(&self) -> i64 {
108 self.duration_ms() - self.setup_ms
109 }
110}
111
112impl Violation {
113 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 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 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 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn add_assignment(&mut self, assignment: Assignment) {
155 self.assignments.push(assignment);
156 }
157
158 pub fn add_violation(&mut self, violation: Violation) {
160 self.violations.push(violation);
161 }
162
163 pub fn is_valid(&self) -> bool {
165 self.violations.is_empty()
166 }
167
168 pub fn makespan_ms(&self) -> i64 {
170 self.assignments.iter().map(|a| a.end_ms).max().unwrap_or(0)
171 }
172
173 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 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 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 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 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 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 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); }
304
305 #[test]
306 fn test_resource_utilization() {
307 let s = sample_schedule();
308 let util = s.resource_utilization("M1", 8000).unwrap();
310 assert!((util - 1.0).abs() < 1e-10);
311
312 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)); 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}