Skip to main content

u_schedule/models/
activity.rs

1//! Activity (operation) model.
2//!
3//! An activity is the smallest schedulable unit of work. It belongs to a task,
4//! requires resources, has a duration, and may have precedence constraints.
5//!
6//! # Duration Model
7//!
8//! Each activity has three time components:
9//! - **Setup**: Preparation time (may depend on previous activity via TransitionMatrix)
10//! - **Process**: Core work time
11//! - **Teardown**: Cleanup/cooldown time
12//!
13//! # Reference
14//! Pinedo (2016), "Scheduling: Theory, Algorithms, and Systems", Ch. 2
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19/// An activity (operation) to be scheduled.
20///
21/// Represents a single processing step that requires one or more resources
22/// for a specified duration. Activities within a task are linked by
23/// precedence constraints.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Activity {
26    /// Unique activity identifier.
27    pub id: String,
28    /// Parent task identifier.
29    pub task_id: String,
30    /// Position within the task (0-indexed).
31    pub sequence: i32,
32    /// Time required to complete this activity.
33    pub duration: ActivityDuration,
34    /// Resources needed (type + quantity + candidates).
35    pub resource_requirements: Vec<ResourceRequirement>,
36    /// IDs of activities that must complete before this one starts.
37    pub predecessors: Vec<String>,
38    /// Whether this activity can be preempted and resumed later.
39    pub splittable: bool,
40    /// Minimum duration (ms) of each split segment.
41    pub min_split_ms: i64,
42    /// Domain-specific metadata.
43    pub attributes: HashMap<String, String>,
44}
45
46impl Activity {
47    /// Creates a new activity.
48    pub fn new(id: impl Into<String>, task_id: impl Into<String>, sequence: i32) -> Self {
49        Self {
50            id: id.into(),
51            task_id: task_id.into(),
52            sequence,
53            duration: ActivityDuration::default(),
54            resource_requirements: Vec::new(),
55            predecessors: Vec::new(),
56            splittable: false,
57            min_split_ms: 0,
58            attributes: HashMap::new(),
59        }
60    }
61
62    /// Sets the duration.
63    pub fn with_duration(mut self, duration: ActivityDuration) -> Self {
64        self.duration = duration;
65        self
66    }
67
68    /// Sets the processing time (setup=0, teardown=0).
69    pub fn with_process_time(mut self, process_ms: i64) -> Self {
70        self.duration = ActivityDuration::fixed(process_ms);
71        self
72    }
73
74    /// Adds a resource requirement.
75    pub fn with_requirement(mut self, req: ResourceRequirement) -> Self {
76        self.resource_requirements.push(req);
77        self
78    }
79
80    /// Adds a predecessor activity ID.
81    pub fn with_predecessor(mut self, predecessor_id: impl Into<String>) -> Self {
82        self.predecessors.push(predecessor_id.into());
83        self
84    }
85
86    /// Enables preemption with a minimum split size.
87    pub fn with_splitting(mut self, min_split_ms: i64) -> Self {
88        self.splittable = true;
89        self.min_split_ms = min_split_ms;
90        self
91    }
92
93    /// Returns all candidate resource IDs across all requirements.
94    pub fn candidate_resources(&self) -> Vec<&str> {
95        self.resource_requirements
96            .iter()
97            .flat_map(|r| r.candidates.iter().map(|s| s.as_str()))
98            .collect()
99    }
100}
101
102/// Time components of an activity.
103///
104/// # Components
105/// - **Setup**: Preparation before processing (e.g., machine changeover).
106///   May be overridden by `TransitionMatrix` for sequence-dependent setups.
107/// - **Process**: Core work time (the actual operation).
108/// - **Teardown**: Cleanup after processing (e.g., cooling, inspection).
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ActivityDuration {
111    /// Setup/preparation time (ms).
112    pub setup_ms: i64,
113    /// Core processing time (ms).
114    pub process_ms: i64,
115    /// Teardown/cleanup time (ms).
116    pub teardown_ms: i64,
117}
118
119impl ActivityDuration {
120    /// Creates a duration with all three components.
121    pub fn new(setup_ms: i64, process_ms: i64, teardown_ms: i64) -> Self {
122        Self {
123            setup_ms,
124            process_ms,
125            teardown_ms,
126        }
127    }
128
129    /// Creates a fixed-duration activity (setup=0, teardown=0).
130    pub fn fixed(process_ms: i64) -> Self {
131        Self::new(0, process_ms, 0)
132    }
133
134    /// Total duration (setup + process + teardown).
135    pub fn total_ms(&self) -> i64 {
136        self.setup_ms + self.process_ms + self.teardown_ms
137    }
138}
139
140impl Default for ActivityDuration {
141    fn default() -> Self {
142        Self::fixed(0)
143    }
144}
145
146/// A resource requirement for an activity.
147///
148/// Specifies what type and quantity of resources are needed,
149/// with optional candidate filtering and skill requirements.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ResourceRequirement {
152    /// Required resource type (e.g., "Machine", "Operator").
153    pub resource_type: String,
154    /// Number of resource units needed simultaneously.
155    pub quantity: i32,
156    /// Specific resource IDs that can fulfill this requirement.
157    /// Empty = any resource of the correct type.
158    pub candidates: Vec<String>,
159    /// Required skills (matched against `Resource.skills`).
160    pub required_skills: Vec<String>,
161}
162
163impl ResourceRequirement {
164    /// Creates a new requirement for one unit of a resource type.
165    pub fn new(resource_type: impl Into<String>) -> Self {
166        Self {
167            resource_type: resource_type.into(),
168            quantity: 1,
169            candidates: Vec::new(),
170            required_skills: Vec::new(),
171        }
172    }
173
174    /// Sets the required quantity.
175    pub fn with_quantity(mut self, quantity: i32) -> Self {
176        self.quantity = quantity;
177        self
178    }
179
180    /// Adds candidate resource IDs.
181    pub fn with_candidates(mut self, candidates: Vec<String>) -> Self {
182        self.candidates = candidates;
183        self
184    }
185
186    /// Adds a required skill.
187    pub fn with_skill(mut self, skill: impl Into<String>) -> Self {
188        self.required_skills.push(skill.into());
189        self
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_activity_builder() {
199        let act = Activity::new("O1", "J1", 0)
200            .with_duration(ActivityDuration::new(100, 500, 50))
201            .with_requirement(ResourceRequirement::new("Machine").with_quantity(1))
202            .with_predecessor("O0")
203            .with_splitting(200);
204
205        assert_eq!(act.id, "O1");
206        assert_eq!(act.task_id, "J1");
207        assert_eq!(act.sequence, 0);
208        assert_eq!(act.duration.total_ms(), 650);
209        assert_eq!(act.resource_requirements.len(), 1);
210        assert_eq!(act.predecessors, vec!["O0"]);
211        assert!(act.splittable);
212        assert_eq!(act.min_split_ms, 200);
213    }
214
215    #[test]
216    fn test_activity_duration_fixed() {
217        let d = ActivityDuration::fixed(1000);
218        assert_eq!(d.setup_ms, 0);
219        assert_eq!(d.process_ms, 1000);
220        assert_eq!(d.teardown_ms, 0);
221        assert_eq!(d.total_ms(), 1000);
222    }
223
224    #[test]
225    fn test_activity_duration_components() {
226        let d = ActivityDuration::new(100, 500, 50);
227        assert_eq!(d.total_ms(), 650);
228    }
229
230    #[test]
231    fn test_resource_requirement() {
232        let req = ResourceRequirement::new("CNC")
233            .with_quantity(2)
234            .with_candidates(vec!["M1".into(), "M2".into(), "M3".into()])
235            .with_skill("milling");
236
237        assert_eq!(req.resource_type, "CNC");
238        assert_eq!(req.quantity, 2);
239        assert_eq!(req.candidates.len(), 3);
240        assert_eq!(req.required_skills, vec!["milling"]);
241    }
242
243    #[test]
244    fn test_candidate_resources() {
245        let act = Activity::new("O1", "J1", 0)
246            .with_requirement(
247                ResourceRequirement::new("Machine").with_candidates(vec!["M1".into(), "M2".into()]),
248            )
249            .with_requirement(
250                ResourceRequirement::new("Operator").with_candidates(vec!["W1".into()]),
251            );
252
253        let candidates = act.candidate_resources();
254        assert_eq!(candidates.len(), 3);
255        assert!(candidates.contains(&"M1"));
256        assert!(candidates.contains(&"W1"));
257    }
258}