Skip to main content

u_schedule/models/
resource.rs

1//! Resource model.
2//!
3//! Resources are the entities that perform activities: machines, workers,
4//! tools, rooms, vehicles. Each resource has a type, capacity, skills,
5//! and an optional availability calendar.
6//!
7//! # Reference
8//! Pinedo (2016), "Scheduling: Theory, Algorithms, and Systems", Ch. 1.2
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use super::Calendar;
14
15/// A resource that can be assigned to activities.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Resource {
18    /// Unique resource identifier.
19    pub id: String,
20    /// Human-readable name.
21    pub name: String,
22    /// Resource classification.
23    pub resource_type: ResourceType,
24    /// Number of units available simultaneously (default: 1).
25    pub capacity: i32,
26    /// Work rate multiplier (1.0 = normal, <1.0 = slower, >1.0 = faster).
27    pub efficiency: f64,
28    /// Availability schedule.
29    pub calendar: Option<Calendar>,
30    /// Skills with proficiency levels.
31    pub skills: Vec<Skill>,
32    /// Economic cost per hour (optional, for cost optimization).
33    pub cost_per_hour: Option<f64>,
34    /// Domain-specific metadata.
35    pub attributes: HashMap<String, String>,
36}
37
38/// Resource type classification.
39///
40/// Determines scheduling semantics (e.g., consumable resources deplete,
41/// human resources have shift constraints).
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ResourceType {
44    /// Main processing resource (e.g., machine, operating room).
45    Primary,
46    /// Support resource (e.g., tool, fixture, jig).
47    Secondary,
48    /// Human resource (e.g., operator, doctor, driver).
49    Human,
50    /// Depleting resource (e.g., raw material, energy budget).
51    Consumable,
52    /// Domain-specific type.
53    Custom(String),
54}
55
56/// A skill with proficiency level.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Skill {
59    /// Skill name (e.g., "welding", "milling", "anesthesia").
60    pub name: String,
61    /// Proficiency level (0.0 to 1.0, where 1.0 = expert).
62    pub level: f64,
63}
64
65impl Resource {
66    /// Creates a new primary resource.
67    pub fn new(id: impl Into<String>, resource_type: ResourceType) -> Self {
68        Self {
69            id: id.into(),
70            name: String::new(),
71            resource_type,
72            capacity: 1,
73            efficiency: 1.0,
74            calendar: None,
75            skills: Vec::new(),
76            cost_per_hour: None,
77            attributes: HashMap::new(),
78        }
79    }
80
81    /// Creates a primary resource.
82    pub fn primary(id: impl Into<String>) -> Self {
83        Self::new(id, ResourceType::Primary)
84    }
85
86    /// Creates a human resource.
87    pub fn human(id: impl Into<String>) -> Self {
88        Self::new(id, ResourceType::Human)
89    }
90
91    /// Creates a secondary resource.
92    pub fn secondary(id: impl Into<String>) -> Self {
93        Self::new(id, ResourceType::Secondary)
94    }
95
96    /// Sets the resource name.
97    pub fn with_name(mut self, name: impl Into<String>) -> Self {
98        self.name = name.into();
99        self
100    }
101
102    /// Sets the capacity.
103    pub fn with_capacity(mut self, capacity: i32) -> Self {
104        self.capacity = capacity;
105        self
106    }
107
108    /// Sets the efficiency multiplier.
109    pub fn with_efficiency(mut self, efficiency: f64) -> Self {
110        self.efficiency = efficiency;
111        self
112    }
113
114    /// Sets the availability calendar.
115    pub fn with_calendar(mut self, calendar: Calendar) -> Self {
116        self.calendar = Some(calendar);
117        self
118    }
119
120    /// Adds a skill.
121    pub fn with_skill(mut self, name: impl Into<String>, level: f64) -> Self {
122        self.skills.push(Skill {
123            name: name.into(),
124            level: level.clamp(0.0, 1.0),
125        });
126        self
127    }
128
129    /// Sets the hourly cost.
130    pub fn with_cost(mut self, cost_per_hour: f64) -> Self {
131        self.cost_per_hour = Some(cost_per_hour);
132        self
133    }
134
135    /// Adds a domain-specific attribute.
136    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
137        self.attributes.insert(key.into(), value.into());
138        self
139    }
140
141    /// Whether this resource has a given skill.
142    pub fn has_skill(&self, name: &str) -> bool {
143        self.skills.iter().any(|s| s.name == name)
144    }
145
146    /// Returns the proficiency level for a skill (0.0 if not found).
147    pub fn skill_level(&self, name: &str) -> f64 {
148        self.skills
149            .iter()
150            .find(|s| s.name == name)
151            .map(|s| s.level)
152            .unwrap_or(0.0)
153    }
154
155    /// Checks availability at a given time (ms).
156    ///
157    /// Returns `true` if no calendar is set (always available)
158    /// or if the calendar indicates working time.
159    pub fn is_available_at(&self, time_ms: i64) -> bool {
160        match &self.calendar {
161            None => true,
162            Some(cal) => cal.is_working_time(time_ms),
163        }
164    }
165}
166
167impl Skill {
168    /// Creates a new skill.
169    pub fn new(name: impl Into<String>, level: f64) -> Self {
170        Self {
171            name: name.into(),
172            level: level.clamp(0.0, 1.0),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_resource_builder() {
183        let r = Resource::primary("M1")
184            .with_name("CNC Machine 1")
185            .with_capacity(1)
186            .with_efficiency(1.2)
187            .with_skill("milling", 0.9)
188            .with_skill("drilling", 0.7)
189            .with_cost(50.0)
190            .with_attribute("location", "Shop Floor A");
191
192        assert_eq!(r.id, "M1");
193        assert_eq!(r.name, "CNC Machine 1");
194        assert_eq!(r.resource_type, ResourceType::Primary);
195        assert_eq!(r.capacity, 1);
196        assert!((r.efficiency - 1.2).abs() < 1e-10);
197        assert!(r.has_skill("milling"));
198        assert!(!r.has_skill("welding"));
199        assert!((r.skill_level("milling") - 0.9).abs() < 1e-10);
200        assert!((r.skill_level("unknown") - 0.0).abs() < 1e-10);
201        assert_eq!(r.cost_per_hour, Some(50.0));
202    }
203
204    #[test]
205    fn test_resource_types() {
206        let m = Resource::primary("M1");
207        assert_eq!(m.resource_type, ResourceType::Primary);
208
209        let w = Resource::human("W1");
210        assert_eq!(w.resource_type, ResourceType::Human);
211
212        let t = Resource::secondary("T1");
213        assert_eq!(t.resource_type, ResourceType::Secondary);
214    }
215
216    #[test]
217    fn test_resource_availability_no_calendar() {
218        let r = Resource::primary("M1");
219        assert!(r.is_available_at(0));
220        assert!(r.is_available_at(1_000_000));
221    }
222
223    #[test]
224    fn test_skill_clamping() {
225        let r = Resource::primary("M1")
226            .with_skill("over", 1.5)
227            .with_skill("under", -0.5);
228
229        assert!((r.skill_level("over") - 1.0).abs() < 1e-10);
230        assert!((r.skill_level("under") - 0.0).abs() < 1e-10);
231    }
232}