Skip to main content

u_schedule/scheduler/
simple.rs

1//! Simple priority-driven greedy scheduler.
2//!
3//! # Algorithm
4//!
5//! 1. Sort tasks by dispatching rule (or priority if no rule engine).
6//! 2. For each task, process activities sequentially.
7//! 3. For each activity, select the earliest-available candidate resource.
8//! 4. Apply sequence-dependent setup times from transition matrices.
9//!
10//! # Complexity
11//! O(n * m * c) where n=tasks, m=activities/task, c=candidate resources.
12//!
13//! # Reference
14//! Pinedo (2016), "Scheduling", Ch. 4: Priority Dispatching
15
16use std::collections::HashMap;
17
18use crate::dispatching::{RuleEngine, SchedulingContext};
19use crate::models::{Assignment, Resource, Schedule, Task, TransitionMatrixCollection};
20
21/// Input container for scheduling.
22#[derive(Debug, Clone)]
23pub struct ScheduleRequest {
24    /// Tasks to schedule.
25    pub tasks: Vec<Task>,
26    /// Available resources.
27    pub resources: Vec<Resource>,
28    /// Schedule start time (ms).
29    pub start_time_ms: i64,
30    /// Sequence-dependent setup time matrices.
31    pub transition_matrices: TransitionMatrixCollection,
32}
33
34impl ScheduleRequest {
35    /// Creates a new schedule request.
36    pub fn new(tasks: Vec<Task>, resources: Vec<Resource>) -> Self {
37        Self {
38            tasks,
39            resources,
40            start_time_ms: 0,
41            transition_matrices: TransitionMatrixCollection::new(),
42        }
43    }
44
45    /// Sets the schedule start time.
46    pub fn with_start_time(mut self, start_time_ms: i64) -> Self {
47        self.start_time_ms = start_time_ms;
48        self
49    }
50
51    /// Sets transition matrices.
52    pub fn with_transition_matrices(mut self, matrices: TransitionMatrixCollection) -> Self {
53        self.transition_matrices = matrices;
54        self
55    }
56}
57
58/// Simple priority-driven greedy scheduler.
59///
60/// Schedules tasks by priority (or dispatching rule), assigning each
61/// activity to the earliest-available candidate resource. Supports
62/// sequence-dependent setup times via transition matrices.
63///
64/// # Example
65///
66/// ```
67/// use u_schedule::scheduler::{SimpleScheduler, ScheduleRequest};
68/// use u_schedule::models::{Task, Resource, ResourceType, Activity, ActivityDuration, ResourceRequirement};
69///
70/// let tasks = vec![
71///     Task::new("J1").with_activity(
72///         Activity::new("O1", "J1", 0)
73///             .with_duration(ActivityDuration::fixed(1000))
74///             .with_requirement(
75///                 ResourceRequirement::new("Machine")
76///                     .with_candidates(vec!["M1".into()])
77///             )
78///     ),
79/// ];
80/// let resources = vec![Resource::new("M1", ResourceType::Primary)];
81/// let request = ScheduleRequest::new(tasks, resources);
82///
83/// let scheduler = SimpleScheduler::new();
84/// let schedule = scheduler.schedule_request(&request);
85/// assert_eq!(schedule.assignment_count(), 1);
86/// ```
87#[derive(Debug, Clone)]
88pub struct SimpleScheduler {
89    transition_matrices: TransitionMatrixCollection,
90    rule_engine: Option<RuleEngine>,
91}
92
93impl SimpleScheduler {
94    /// Creates a new scheduler.
95    pub fn new() -> Self {
96        Self {
97            transition_matrices: TransitionMatrixCollection::new(),
98            rule_engine: None,
99        }
100    }
101
102    /// Sets transition matrices.
103    pub fn with_transition_matrices(mut self, matrices: TransitionMatrixCollection) -> Self {
104        self.transition_matrices = matrices;
105        self
106    }
107
108    /// Sets a rule engine for task ordering.
109    ///
110    /// When set, tasks are sorted by the rule engine instead of by priority.
111    pub fn with_rule_engine(mut self, engine: RuleEngine) -> Self {
112        self.rule_engine = Some(engine);
113        self
114    }
115
116    /// Schedules tasks on resources.
117    ///
118    /// # Algorithm
119    /// 1. Sort tasks by rule engine or priority (descending).
120    /// 2. For each task, schedule activities in sequence order.
121    /// 3. For each activity, find the earliest-available candidate resource.
122    /// 4. Apply setup time from transition matrices.
123    pub fn schedule(&self, tasks: &[Task], resources: &[Resource], start_time_ms: i64) -> Schedule {
124        let mut schedule = Schedule::new();
125        let mut resource_available: HashMap<String, i64> = HashMap::new();
126        let mut last_category: HashMap<String, String> = HashMap::new();
127
128        // Initialize resource availability
129        for resource in resources {
130            resource_available.insert(resource.id.clone(), start_time_ms);
131        }
132
133        // Determine task order
134        let task_order = self.sort_tasks(tasks, start_time_ms);
135
136        // Schedule each task
137        for &task_idx in &task_order {
138            let task = &tasks[task_idx];
139            let mut task_start = task
140                .release_time
141                .unwrap_or(start_time_ms)
142                .max(start_time_ms);
143
144            for activity in &task.activities {
145                let candidates = activity.candidate_resources();
146                if candidates.is_empty() {
147                    continue;
148                }
149
150                // Select resource with earliest availability
151                let mut best_resource: Option<&str> = None;
152                let mut best_start = i64::MAX;
153
154                for candidate in &candidates {
155                    if let Some(&available) = resource_available.get(*candidate) {
156                        let actual_start = available.max(task_start);
157                        if actual_start < best_start {
158                            best_start = actual_start;
159                            best_resource = Some(candidate);
160                        }
161                    }
162                }
163
164                if let Some(resource_id) = best_resource {
165                    // Calculate setup time from transition matrices
166                    let setup_time = if let Some(prev_cat) = last_category.get(resource_id) {
167                        self.transition_matrices.get_transition_time(
168                            resource_id,
169                            prev_cat,
170                            &task.category,
171                        )
172                    } else {
173                        0
174                    };
175
176                    let start = best_start;
177                    let end = start + setup_time + activity.duration.process_ms;
178
179                    let assignment =
180                        Assignment::new(&activity.id, &task.id, resource_id, start, end)
181                            .with_setup(setup_time);
182
183                    schedule.add_assignment(assignment);
184
185                    // Update state
186                    resource_available.insert(resource_id.to_string(), end);
187                    last_category.insert(resource_id.to_string(), task.category.clone());
188                    task_start = end; // Enforce intra-task precedence
189                }
190            }
191        }
192
193        schedule
194    }
195
196    /// Schedules from a request.
197    pub fn schedule_request(&self, request: &ScheduleRequest) -> Schedule {
198        let scheduler = Self {
199            transition_matrices: request.transition_matrices.clone(),
200            rule_engine: self.rule_engine.clone(),
201        };
202        scheduler.schedule(&request.tasks, &request.resources, request.start_time_ms)
203    }
204
205    /// Returns task indices sorted by rule engine or priority.
206    fn sort_tasks(&self, tasks: &[Task], start_time_ms: i64) -> Vec<usize> {
207        if let Some(ref engine) = self.rule_engine {
208            let ctx = SchedulingContext::at_time(start_time_ms);
209            engine.sort_indices(tasks, &ctx)
210        } else {
211            // Default: sort by priority descending
212            let mut indices: Vec<usize> = (0..tasks.len()).collect();
213            indices.sort_by(|&a, &b| tasks[b].priority.cmp(&tasks[a].priority));
214            indices
215        }
216    }
217}
218
219impl Default for SimpleScheduler {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::dispatching::rules;
229    use crate::models::{
230        Activity, ActivityDuration, Resource, ResourceRequirement, ResourceType, TransitionMatrix,
231    };
232
233    fn make_resource(id: &str) -> Resource {
234        Resource::new(id, ResourceType::Primary)
235    }
236
237    fn make_task_with_resource(
238        id: &str,
239        duration_ms: i64,
240        resource_id: &str,
241        priority: i32,
242    ) -> Task {
243        Task::new(id)
244            .with_priority(priority)
245            .with_category("default")
246            .with_activity(
247                Activity::new(format!("{id}_O1"), id, 0)
248                    .with_duration(ActivityDuration::fixed(duration_ms))
249                    .with_requirement(
250                        ResourceRequirement::new("Machine")
251                            .with_candidates(vec![resource_id.into()]),
252                    ),
253            )
254    }
255
256    #[test]
257    fn test_simple_single_task() {
258        let tasks = vec![make_task_with_resource("J1", 1000, "M1", 0)];
259        let resources = vec![make_resource("M1")];
260        let scheduler = SimpleScheduler::new();
261
262        let schedule = scheduler.schedule(&tasks, &resources, 0);
263        assert_eq!(schedule.assignment_count(), 1);
264
265        let a = schedule.assignment_for_activity("J1_O1").unwrap();
266        assert_eq!(a.start_ms, 0);
267        assert_eq!(a.end_ms, 1000);
268        assert_eq!(a.resource_id, "M1");
269    }
270
271    #[test]
272    fn test_priority_ordering() {
273        let tasks = vec![
274            make_task_with_resource("low", 1000, "M1", 1),
275            make_task_with_resource("high", 1000, "M1", 10),
276        ];
277        let resources = vec![make_resource("M1")];
278        let scheduler = SimpleScheduler::new();
279
280        let schedule = scheduler.schedule(&tasks, &resources, 0);
281
282        // High priority scheduled first
283        let high_a = schedule.assignment_for_activity("high_O1").unwrap();
284        let low_a = schedule.assignment_for_activity("low_O1").unwrap();
285        assert!(high_a.start_ms < low_a.start_ms);
286    }
287
288    #[test]
289    fn test_two_resources() {
290        let tasks = vec![
291            make_task_with_resource("J1", 2000, "M1", 10),
292            make_task_with_resource("J2", 1000, "M1", 5),
293        ];
294        // Only M1 → J1 first (priority), then J2 at 2000
295        let resources = vec![make_resource("M1")];
296        let scheduler = SimpleScheduler::new();
297
298        let schedule = scheduler.schedule(&tasks, &resources, 0);
299        let j1 = schedule.assignment_for_activity("J1_O1").unwrap();
300        let j2 = schedule.assignment_for_activity("J2_O1").unwrap();
301        assert_eq!(j1.start_ms, 0);
302        assert_eq!(j1.end_ms, 2000);
303        assert_eq!(j2.start_ms, 2000);
304        assert_eq!(j2.end_ms, 3000);
305    }
306
307    #[test]
308    fn test_parallel_resources() {
309        // J1→M1, J2→M2 can run in parallel
310        let tasks = vec![
311            make_task_with_resource("J1", 2000, "M1", 10),
312            make_task_with_resource("J2", 1000, "M2", 5),
313        ];
314        let resources = vec![make_resource("M1"), make_resource("M2")];
315        let scheduler = SimpleScheduler::new();
316
317        let schedule = scheduler.schedule(&tasks, &resources, 0);
318        let j1 = schedule.assignment_for_activity("J1_O1").unwrap();
319        let j2 = schedule.assignment_for_activity("J2_O1").unwrap();
320        // Both start at 0 since they use different resources
321        assert_eq!(j1.start_ms, 0);
322        assert_eq!(j2.start_ms, 0);
323    }
324
325    #[test]
326    fn test_multi_activity_task() {
327        let task = Task::new("J1")
328            .with_priority(1)
329            .with_category("TypeA")
330            .with_activity(
331                Activity::new("O1", "J1", 0)
332                    .with_duration(ActivityDuration::fixed(1000))
333                    .with_requirement(
334                        ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
335                    ),
336            )
337            .with_activity(
338                Activity::new("O2", "J1", 1)
339                    .with_duration(ActivityDuration::fixed(2000))
340                    .with_requirement(
341                        ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
342                    ),
343            );
344
345        let resources = vec![make_resource("M1")];
346        let scheduler = SimpleScheduler::new();
347        let schedule = scheduler.schedule(&[task], &resources, 0);
348
349        let o1 = schedule.assignment_for_activity("O1").unwrap();
350        let o2 = schedule.assignment_for_activity("O2").unwrap();
351        // O2 must start after O1 ends (intra-task precedence)
352        assert_eq!(o1.end_ms, 1000);
353        assert!(o2.start_ms >= o1.end_ms);
354        assert_eq!(o2.end_ms, 3000);
355    }
356
357    #[test]
358    fn test_transition_matrix_setup() {
359        let mut tm = TransitionMatrix::new("changeover", "M1").with_default(500);
360        tm.set_transition("TypeA", "TypeB", 1000);
361
362        let matrices = TransitionMatrixCollection::new().with_matrix(tm);
363
364        let tasks = vec![
365            Task::new("J1")
366                .with_priority(10)
367                .with_category("TypeA")
368                .with_activity(
369                    Activity::new("O1", "J1", 0)
370                        .with_duration(ActivityDuration::fixed(1000))
371                        .with_requirement(
372                            ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
373                        ),
374                ),
375            Task::new("J2")
376                .with_priority(5)
377                .with_category("TypeB")
378                .with_activity(
379                    Activity::new("O2", "J2", 0)
380                        .with_duration(ActivityDuration::fixed(1000))
381                        .with_requirement(
382                            ResourceRequirement::new("Machine").with_candidates(vec!["M1".into()]),
383                        ),
384                ),
385        ];
386
387        let resources = vec![make_resource("M1")];
388        let scheduler = SimpleScheduler::new().with_transition_matrices(matrices);
389
390        let schedule = scheduler.schedule(&tasks, &resources, 0);
391        let o2 = schedule.assignment_for_activity("O2").unwrap();
392        // J1 ends at 1000, setup A→B = 1000, J2 starts at 1000, ends at 1000+1000+1000 = 3000
393        assert_eq!(o2.start_ms, 1000);
394        assert_eq!(o2.setup_ms, 1000);
395        assert_eq!(o2.end_ms, 3000);
396    }
397
398    #[test]
399    fn test_with_rule_engine() {
400        // Use SPT rule → shorter task first regardless of priority
401        let tasks = vec![
402            make_task_with_resource("long", 5000, "M1", 100), // High priority but long
403            make_task_with_resource("short", 1000, "M1", 1),  // Low priority but short
404        ];
405        let resources = vec![make_resource("M1")];
406        let engine = RuleEngine::new().with_rule(rules::Spt);
407        let scheduler = SimpleScheduler::new().with_rule_engine(engine);
408
409        let schedule = scheduler.schedule(&tasks, &resources, 0);
410        let short_a = schedule.assignment_for_activity("short_O1").unwrap();
411        let long_a = schedule.assignment_for_activity("long_O1").unwrap();
412        // SPT orders short first despite lower priority
413        assert_eq!(short_a.start_ms, 0);
414        assert!(long_a.start_ms >= short_a.end_ms);
415    }
416
417    #[test]
418    fn test_schedule_request() {
419        let tasks = vec![make_task_with_resource("J1", 1000, "M1", 0)];
420        let resources = vec![make_resource("M1")];
421        let request = ScheduleRequest::new(tasks, resources).with_start_time(5000);
422
423        let scheduler = SimpleScheduler::new();
424        let schedule = scheduler.schedule_request(&request);
425
426        let a = schedule.assignment_for_activity("J1_O1").unwrap();
427        assert_eq!(a.start_ms, 5000);
428        assert_eq!(a.end_ms, 6000);
429    }
430
431    #[test]
432    fn test_release_time_respected() {
433        let mut task = make_task_with_resource("J1", 1000, "M1", 0);
434        task.release_time = Some(5000);
435        let resources = vec![make_resource("M1")];
436        let scheduler = SimpleScheduler::new();
437
438        let schedule = scheduler.schedule(&[task], &resources, 0);
439        let a = schedule.assignment_for_activity("J1_O1").unwrap();
440        // Must not start before release_time
441        assert_eq!(a.start_ms, 5000);
442    }
443
444    #[test]
445    fn test_empty_input() {
446        let scheduler = SimpleScheduler::new();
447        let schedule = scheduler.schedule(&[], &[], 0);
448        assert_eq!(schedule.assignment_count(), 0);
449        assert_eq!(schedule.makespan_ms(), 0);
450    }
451
452    #[test]
453    fn test_no_candidate_resources() {
454        // Activity with no resource requirement → skipped
455        let task = Task::new("J1").with_priority(1).with_activity(
456            Activity::new("O1", "J1", 0).with_duration(ActivityDuration::fixed(1000)),
457            // No resource requirement
458        );
459        let resources = vec![make_resource("M1")];
460        let scheduler = SimpleScheduler::new();
461        let schedule = scheduler.schedule(&[task], &resources, 0);
462        assert_eq!(schedule.assignment_count(), 0);
463    }
464}