1use super::{DispatchingRule, RuleScore, SchedulingContext};
18use crate::models::Task;
19
20#[derive(Debug, Clone, Copy)]
30pub struct Spt;
31
32impl DispatchingRule for Spt {
33 fn name(&self) -> &'static str {
34 "SPT"
35 }
36
37 fn evaluate(&self, task: &Task, _context: &SchedulingContext) -> RuleScore {
38 task.total_duration_ms() as f64
39 }
40
41 fn description(&self) -> &'static str {
42 "Shortest Processing Time"
43 }
44}
45
46#[derive(Debug, Clone, Copy)]
51pub struct Lpt;
52
53impl DispatchingRule for Lpt {
54 fn name(&self) -> &'static str {
55 "LPT"
56 }
57
58 fn evaluate(&self, task: &Task, _context: &SchedulingContext) -> RuleScore {
59 -(task.total_duration_ms() as f64)
60 }
61
62 fn description(&self) -> &'static str {
63 "Longest Processing Time"
64 }
65}
66
67#[derive(Debug, Clone, Copy)]
72pub struct Lwkr;
73
74impl DispatchingRule for Lwkr {
75 fn name(&self) -> &'static str {
76 "LWKR"
77 }
78
79 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
80 context
81 .remaining_work
82 .get(&task.id)
83 .copied()
84 .unwrap_or_else(|| task.total_duration_ms()) as f64
85 }
86
87 fn description(&self) -> &'static str {
88 "Least Work Remaining"
89 }
90}
91
92#[derive(Debug, Clone, Copy)]
97pub struct Mwkr;
98
99impl DispatchingRule for Mwkr {
100 fn name(&self) -> &'static str {
101 "MWKR"
102 }
103
104 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
105 let remaining = context
106 .remaining_work
107 .get(&task.id)
108 .copied()
109 .unwrap_or_else(|| task.total_duration_ms());
110 -(remaining as f64)
111 }
112
113 fn description(&self) -> &'static str {
114 "Most Work Remaining"
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
126pub struct Wspt;
127
128impl DispatchingRule for Wspt {
129 fn name(&self) -> &'static str {
130 "WSPT"
131 }
132
133 fn evaluate(&self, task: &Task, _context: &SchedulingContext) -> RuleScore {
134 let processing_time = task.total_duration_ms() as f64;
135 if processing_time <= 0.0 {
136 return f64::MAX;
137 }
138 let weight = 1000.0 / (task.priority as f64 + 1.0);
139 -(weight / processing_time) }
141
142 fn description(&self) -> &'static str {
143 "Weighted Shortest Processing Time"
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
157pub struct Edd;
158
159impl DispatchingRule for Edd {
160 fn name(&self) -> &'static str {
161 "EDD"
162 }
163
164 fn evaluate(&self, task: &Task, _context: &SchedulingContext) -> RuleScore {
165 task.deadline.map(|d| d as f64).unwrap_or(f64::MAX)
166 }
167
168 fn description(&self) -> &'static str {
169 "Earliest Due Date"
170 }
171}
172
173#[derive(Debug, Clone, Copy)]
180pub struct Mst;
181
182impl DispatchingRule for Mst {
183 fn name(&self) -> &'static str {
184 "MST"
185 }
186
187 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
188 let deadline = match task.deadline {
189 Some(d) => d,
190 None => return f64::MAX,
191 };
192
193 let remaining = context
194 .remaining_work
195 .get(&task.id)
196 .copied()
197 .unwrap_or_else(|| task.total_duration_ms());
198
199 let time_until_deadline = deadline - context.current_time_ms;
200 (time_until_deadline - remaining) as f64
201 }
202
203 fn description(&self) -> &'static str {
204 "Minimum Slack Time"
205 }
206}
207
208#[derive(Debug, Clone, Copy)]
217pub struct Cr;
218
219impl DispatchingRule for Cr {
220 fn name(&self) -> &'static str {
221 "CR"
222 }
223
224 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
225 let deadline = match task.deadline {
226 Some(d) => d,
227 None => return f64::MAX,
228 };
229
230 let remaining = context
231 .remaining_work
232 .get(&task.id)
233 .copied()
234 .unwrap_or_else(|| task.total_duration_ms());
235
236 if remaining <= 0 {
237 return f64::MAX; }
239
240 let time_until_deadline = (deadline - context.current_time_ms) as f64;
241 time_until_deadline / remaining as f64
242 }
243
244 fn description(&self) -> &'static str {
245 "Critical Ratio"
246 }
247}
248
249#[derive(Debug, Clone, Copy)]
254pub struct Sro;
255
256impl DispatchingRule for Sro {
257 fn name(&self) -> &'static str {
258 "S/RO"
259 }
260
261 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
262 let deadline = match task.deadline {
263 Some(d) => d,
264 None => return f64::MAX,
265 };
266
267 let remaining_work = context
268 .remaining_work
269 .get(&task.id)
270 .copied()
271 .unwrap_or_else(|| task.total_duration_ms());
272
273 let op_count = task.activity_count().max(1);
274 let slack = (deadline - context.current_time_ms - remaining_work) as f64;
275 slack / op_count as f64
276 }
277
278 fn description(&self) -> &'static str {
279 "Slack per Remaining Operations"
280 }
281}
282
283#[derive(Debug, Clone, Copy)]
294pub struct Atc {
295 pub k: f64,
297}
298
299impl Default for Atc {
300 fn default() -> Self {
301 Self { k: 2.0 }
302 }
303}
304
305impl Atc {
306 pub fn with_k(k: f64) -> Self {
308 Self { k }
309 }
310}
311
312impl DispatchingRule for Atc {
313 fn name(&self) -> &'static str {
314 "ATC"
315 }
316
317 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
318 let processing_time = task.total_duration_ms() as f64;
319 if processing_time <= 0.0 {
320 return f64::MAX;
321 }
322
323 let weight = 1000.0 / (task.priority as f64 + 1.0);
324
325 let deadline = match task.deadline {
326 Some(d) => d as f64,
327 None => return -(weight / processing_time), };
329
330 let slack = deadline - processing_time - context.current_time_ms as f64;
331 let p_avg = context
332 .average_processing_time
333 .unwrap_or(processing_time)
334 .max(1.0);
335
336 let urgency = if slack <= 0.0 {
337 1.0
338 } else {
339 (-slack / (self.k * p_avg)).exp()
340 };
341
342 -(weight / processing_time * urgency) }
344
345 fn description(&self) -> &'static str {
346 "Apparent Tardiness Cost"
347 }
348}
349
350#[derive(Debug, Clone, Copy)]
357pub struct Fifo;
358
359impl DispatchingRule for Fifo {
360 fn name(&self) -> &'static str {
361 "FIFO"
362 }
363
364 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
365 context
366 .arrival_times
367 .get(&task.id)
368 .copied()
369 .unwrap_or_else(|| task.release_time.unwrap_or(0)) as f64
370 }
371
372 fn description(&self) -> &'static str {
373 "First In First Out"
374 }
375}
376
377#[derive(Debug, Clone, Copy)]
382pub struct Winq;
383
384impl DispatchingRule for Winq {
385 fn name(&self) -> &'static str {
386 "WINQ"
387 }
388
389 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
390 context
391 .next_queue_length
392 .get(&task.id)
393 .copied()
394 .unwrap_or(0) as f64
395 }
396
397 fn description(&self) -> &'static str {
398 "Work In Next Queue"
399 }
400}
401
402#[derive(Debug, Clone, Copy)]
407pub struct Lpul;
408
409impl DispatchingRule for Lpul {
410 fn name(&self) -> &'static str {
411 "LPUL"
412 }
413
414 fn evaluate(&self, task: &Task, context: &SchedulingContext) -> RuleScore {
415 if let Some(activity) = task.activities.first() {
416 let min_util = activity
417 .resource_requirements
418 .iter()
419 .flat_map(|req| req.candidates.iter())
420 .filter_map(|res_id| context.resource_utilization.get(res_id))
421 .copied()
422 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
423
424 min_util.unwrap_or(0.0)
425 } else {
426 0.0
427 }
428 }
429
430 fn description(&self) -> &'static str {
431 "Least Planned Utilization Level"
432 }
433}
434
435#[derive(Debug, Clone, Copy)]
442pub struct Priority;
443
444impl DispatchingRule for Priority {
445 fn name(&self) -> &'static str {
446 "PRIORITY"
447 }
448
449 fn evaluate(&self, task: &Task, _context: &SchedulingContext) -> RuleScore {
450 -(task.priority as f64)
451 }
452
453 fn description(&self) -> &'static str {
454 "Task Priority"
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::models::{Activity, ActivityDuration, ResourceRequirement};
462
463 fn make_task(id: &str, duration_ms: i64, deadline: Option<i64>, priority: i32) -> Task {
464 let mut task = Task::new(id).with_priority(priority).with_activity(
465 Activity::new(format!("{id}_O1"), id, 0)
466 .with_duration(ActivityDuration::fixed(duration_ms)),
467 );
468 task.deadline = deadline;
469 task
470 }
471
472 #[test]
473 fn test_spt() {
474 let ctx = SchedulingContext::at_time(0);
475 let short = make_task("short", 1000, None, 0);
476 let long = make_task("long", 5000, None, 0);
477 assert!(Spt.evaluate(&short, &ctx) < Spt.evaluate(&long, &ctx));
478 }
479
480 #[test]
481 fn test_lpt() {
482 let ctx = SchedulingContext::at_time(0);
483 let short = make_task("short", 1000, None, 0);
484 let long = make_task("long", 5000, None, 0);
485 assert!(Lpt.evaluate(&long, &ctx) < Lpt.evaluate(&short, &ctx));
486 }
487
488 #[test]
489 fn test_lwkr_with_context() {
490 let ctx = SchedulingContext::at_time(0)
491 .with_remaining_work("almost_done", 100)
492 .with_remaining_work("lots_left", 5000);
493
494 let t1 = make_task("almost_done", 10000, None, 0);
495 let t2 = make_task("lots_left", 10000, None, 0);
496 assert!(Lwkr.evaluate(&t1, &ctx) < Lwkr.evaluate(&t2, &ctx));
497 }
498
499 #[test]
500 fn test_lwkr_fallback() {
501 let ctx = SchedulingContext::at_time(0); let t1 = make_task("short", 1000, None, 0);
503 let t2 = make_task("long", 5000, None, 0);
504 assert!(Lwkr.evaluate(&t1, &ctx) < Lwkr.evaluate(&t2, &ctx));
505 }
506
507 #[test]
508 fn test_mwkr() {
509 let ctx = SchedulingContext::at_time(0)
510 .with_remaining_work("a", 100)
511 .with_remaining_work("b", 5000);
512 let t1 = make_task("a", 10000, None, 0);
513 let t2 = make_task("b", 10000, None, 0);
514 assert!(Mwkr.evaluate(&t2, &ctx) < Mwkr.evaluate(&t1, &ctx));
515 }
516
517 #[test]
518 fn test_wspt() {
519 let ctx = SchedulingContext::at_time(0);
520 let important_short = make_task("is", 1000, None, 1);
522 let unimportant_long = make_task("ul", 5000, None, 10);
524 assert!(Wspt.evaluate(&important_short, &ctx) < Wspt.evaluate(&unimportant_long, &ctx));
525 }
526
527 #[test]
528 fn test_edd() {
529 let ctx = SchedulingContext::at_time(0);
530 let early = make_task("early", 1000, Some(10_000), 0);
531 let late = make_task("late", 1000, Some(50_000), 0);
532 let none = make_task("none", 1000, None, 0);
533 assert!(Edd.evaluate(&early, &ctx) < Edd.evaluate(&late, &ctx));
534 assert!(Edd.evaluate(&late, &ctx) < Edd.evaluate(&none, &ctx));
535 }
536
537 #[test]
538 fn test_mst() {
539 let ctx = SchedulingContext::at_time(1000);
540 let urgent = make_task("urgent", 3000, Some(5000), 0);
542 let relaxed = make_task("relaxed", 3000, Some(50000), 0);
544 assert!(Mst.evaluate(&urgent, &ctx) < Mst.evaluate(&relaxed, &ctx));
545 }
546
547 #[test]
548 fn test_cr() {
549 let ctx = SchedulingContext::at_time(1000);
550 let on_track = make_task("on_track", 3000, Some(4000), 0);
552 let ahead = make_task("ahead", 3000, Some(10000), 0);
554 assert!(Cr.evaluate(&on_track, &ctx) < Cr.evaluate(&ahead, &ctx));
555 }
556
557 #[test]
558 fn test_cr_behind_schedule() {
559 let ctx = SchedulingContext::at_time(5000);
560 let behind = make_task("behind", 3000, Some(4000), 0);
562 let normal = make_task("normal", 3000, Some(20000), 0);
563 assert!(Cr.evaluate(&behind, &ctx) < Cr.evaluate(&normal, &ctx));
564 }
565
566 #[test]
567 fn test_sro() {
568 let ctx = SchedulingContext::at_time(0);
569 let few_ops = make_task("few", 1000, Some(5000), 0); let mut many_ops = Task::new("many").with_priority(0);
572 many_ops.deadline = Some(5000);
573 for i in 0..3 {
574 many_ops.activities.push(
575 Activity::new(format!("many_O{i}"), "many", i)
576 .with_duration(ActivityDuration::fixed(333)),
577 );
578 }
579 assert!(Sro.evaluate(&many_ops, &ctx) < Sro.evaluate(&few_ops, &ctx));
581 }
582
583 #[test]
584 fn test_atc() {
585 let ctx = SchedulingContext::at_time(0).with_average_processing_time(2000.0);
586 let atc = Atc::default();
587 let urgent = make_task("urgent", 1000, Some(2000), 0); let relaxed = make_task("relaxed", 1000, Some(100_000), 0); assert!(atc.evaluate(&urgent, &ctx) < atc.evaluate(&relaxed, &ctx));
591 }
592
593 #[test]
594 fn test_atc_no_deadline() {
595 let ctx = SchedulingContext::at_time(0);
596 let atc = Atc::default();
597 let no_dl = make_task("no_dl", 1000, None, 0);
598 assert!(atc.evaluate(&no_dl, &ctx).is_finite());
600 }
601
602 #[test]
603 fn test_fifo() {
604 let ctx = SchedulingContext::at_time(5000)
605 .with_arrival_time("first", 1000)
606 .with_arrival_time("second", 3000);
607 let t1 = make_task("first", 2000, None, 0);
608 let t2 = make_task("second", 2000, None, 0);
609 assert!(Fifo.evaluate(&t1, &ctx) < Fifo.evaluate(&t2, &ctx));
610 }
611
612 #[test]
613 fn test_fifo_fallback() {
614 let ctx = SchedulingContext::at_time(0);
615 let mut t1 = make_task("t1", 1000, None, 0);
616 t1.release_time = Some(500);
617 let mut t2 = make_task("t2", 1000, None, 0);
618 t2.release_time = Some(1000);
619 assert!(Fifo.evaluate(&t1, &ctx) < Fifo.evaluate(&t2, &ctx));
620 }
621
622 #[test]
623 fn test_winq() {
624 let ctx = SchedulingContext::at_time(0)
625 .with_next_queue("short_q", 2)
626 .with_next_queue("long_q", 10);
627 let t1 = make_task("short_q", 1000, None, 0);
628 let t2 = make_task("long_q", 1000, None, 0);
629 assert!(Winq.evaluate(&t1, &ctx) < Winq.evaluate(&t2, &ctx));
630 }
631
632 #[test]
633 fn test_lpul() {
634 let ctx = SchedulingContext::at_time(0)
635 .with_utilization("M1", 0.3)
636 .with_utilization("M2", 0.9);
637
638 let t1 = Task::new("t1").with_activity(
639 Activity::new("t1_O1", "t1", 0)
640 .with_process_time(1000)
641 .with_requirement(
642 ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
643 ),
644 );
645 let t2 = Task::new("t2").with_activity(
646 Activity::new("t2_O1", "t2", 0)
647 .with_process_time(1000)
648 .with_requirement(
649 ResourceRequirement::new("Machine").with_candidates(vec!["M2".into()]),
650 ),
651 );
652
653 assert!(Lpul.evaluate(&t1, &ctx) < Lpul.evaluate(&t2, &ctx));
654 }
655
656 #[test]
657 fn test_priority() {
658 let ctx = SchedulingContext::at_time(0);
659 let high = make_task("high", 1000, None, 100);
660 let low = make_task("low", 1000, None, 1);
661 assert!(Priority.evaluate(&high, &ctx) < Priority.evaluate(&low, &ctx));
662 }
663}