problemreductions/models/misc/
staff_scheduling.rs1use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12 ProblemSchemaEntry {
13 name: "StaffScheduling",
14 display_name: "Staff Scheduling",
15 aliases: &[],
16 dimensions: &[],
17 category: crate::registry::ProblemCategory::Misc,
18 module_path: module_path!(),
19 description: "Assign workers to schedule patterns to satisfy per-period staffing requirements within a worker budget",
20 fields: StaffSchedulingCreateSpec::FIELDS,
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(try_from = "StaffSchedulingData")]
31pub struct StaffScheduling {
32 shifts_per_schedule: usize,
33 schedules: Vec<Vec<bool>>,
34 requirements: Vec<i64>,
35 num_workers: i64,
36}
37
38#[derive(Deserialize)]
39struct StaffSchedulingData {
40 shifts_per_schedule: usize,
41 schedules: Vec<Vec<bool>>,
42 requirements: Vec<i64>,
43 num_workers: i64,
44}
45
46impl TryFrom<StaffSchedulingData> for StaffScheduling {
47 type Error = crate::registry::ConstructionError;
48
49 fn try_from(data: StaffSchedulingData) -> Result<Self, Self::Error> {
50 Self::try_new(
51 data.shifts_per_schedule,
52 data.schedules,
53 data.requirements,
54 data.num_workers,
55 )
56 }
57}
58
59#[derive(Debug, Deserialize, crate::CreateSpec)]
60struct StaffSchedulingCreateSpec {
61 k: usize,
63 schedules: Vec<Vec<bool>>,
65 requirements: Vec<i64>,
67 num_workers: i64,
69}
70
71impl TryFrom<StaffSchedulingCreateSpec> for StaffScheduling {
72 type Error = crate::registry::ConstructionError;
73
74 fn try_from(spec: StaffSchedulingCreateSpec) -> Result<Self, Self::Error> {
75 Self::try_new(spec.k, spec.schedules, spec.requirements, spec.num_workers)
76 }
77}
78
79impl StaffScheduling {
80 pub fn new(
89 shifts_per_schedule: usize,
90 schedules: Vec<Vec<bool>>,
91 requirements: Vec<i64>,
92 num_workers: i64,
93 ) -> Self {
94 Self::try_new(shifts_per_schedule, schedules, requirements, num_workers)
95 .unwrap_or_else(|error| panic!("{error}"))
96 }
97
98 fn try_new(
99 shifts_per_schedule: usize,
100 schedules: Vec<Vec<bool>>,
101 requirements: Vec<i64>,
102 num_workers: i64,
103 ) -> Result<Self, crate::registry::ConstructionError> {
104 if usize::try_from(num_workers)
105 .ok()
106 .and_then(|workers| workers.checked_add(1))
107 .is_none()
108 {
109 return Err("num_workers must be nonnegative and encodable by dims()".into());
110 }
111
112 let num_periods = requirements.len();
113 for (index, schedule) in schedules.iter().enumerate() {
114 if schedule.len() != num_periods {
115 return Err(format!(
116 "schedule {} has {} periods, expected {}",
117 index,
118 schedule.len(),
119 num_periods
120 )
121 .into());
122 }
123 let ones = schedule.iter().filter(|&&active| active).count();
124 if ones != shifts_per_schedule {
125 return Err(format!(
126 "schedule {} has {} active periods, expected {}",
127 index, ones, shifts_per_schedule
128 )
129 .into());
130 }
131 }
132
133 Ok(Self {
134 shifts_per_schedule,
135 schedules,
136 requirements,
137 num_workers,
138 })
139 }
140
141 pub fn num_periods(&self) -> usize {
143 self.requirements.len()
144 }
145
146 pub fn shifts_per_schedule(&self) -> usize {
148 self.shifts_per_schedule
149 }
150
151 pub fn schedules(&self) -> &[Vec<bool>] {
153 &self.schedules
154 }
155
156 pub fn requirements(&self) -> &[i64] {
158 &self.requirements
159 }
160
161 pub fn num_workers(&self) -> i64 {
163 self.num_workers
164 }
165
166 pub fn num_schedules(&self) -> usize {
168 self.schedules.len()
169 }
170
171 fn worker_limit(&self) -> usize {
172 usize::try_from(self.num_workers)
173 .expect("validated nonnegative worker count must fit usize")
174 }
175
176 fn worker_counts_valid(&self, config: &[usize]) -> bool {
177 config.iter().all(|&count| count <= self.worker_limit())
178 }
179
180 fn within_budget(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
181 let total = config.iter().try_fold(0_i64, |total, &count| {
182 let count = i64::try_from(count).map_err(|_| {
183 crate::traits::EvaluationError::IntegerOverflow(
184 "converting assigned worker count to i64".into(),
185 )
186 })?;
187 total.checked_add(count).ok_or_else(|| {
188 crate::traits::EvaluationError::IntegerOverflow(
189 "summing assigned worker counts".into(),
190 )
191 })
192 })?;
193 Ok(total <= self.num_workers)
194 }
195
196 fn meets_requirements(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
197 let mut coverage = vec![0_i64; self.num_periods()];
198
199 for (count, schedule) in config.iter().zip(&self.schedules) {
200 if *count == 0 {
201 continue;
202 }
203 let count = i64::try_from(*count).map_err(|_| {
204 crate::traits::EvaluationError::IntegerOverflow(
205 "converting scheduled worker count to i64".into(),
206 )
207 })?;
208 for (period, active) in schedule.iter().enumerate() {
209 if *active {
210 coverage[period] = coverage[period].checked_add(count).ok_or_else(|| {
211 crate::traits::EvaluationError::IntegerOverflow(
212 "summing staffing coverage".into(),
213 )
214 })?;
215 }
216 }
217 }
218
219 Ok(coverage
220 .iter()
221 .zip(&self.requirements)
222 .all(|(covered, required)| covered >= required))
223 }
224}
225
226impl Problem for StaffScheduling {
227 const NAME: &'static str = "StaffScheduling";
228 type Solution = Vec<usize>;
229 type Value = crate::types::Or;
230
231 crate::problem_parameters![
232 ("num_periods", num_periods),
233 ("num_schedules", num_schedules),
234 ("num_workers", num_workers),
235 ];
236
237 fn evaluate(
238 &self,
239 config: &Self::Solution,
240 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
241 Ok({
242 crate::types::Or({
243 if config.len() != self.num_schedules() {
244 return Err(crate::traits::EvaluationError::InvalidConfiguration(
245 "staffing vector length does not match the schedules".into(),
246 ));
247 }
248 self.worker_counts_valid(config)
249 && self.within_budget(config)?
250 && self.meets_requirements(config)?
251 })
252 })
253 }
254
255 fn variant() -> Vec<(&'static str, &'static str)> {
256 crate::variant_params![]
257 }
258}
259
260impl crate::solvers::BruteForceProblem for StaffScheduling {
261 fn dimensions(&self) -> Vec<usize> {
262 vec![self.worker_limit() + 1; self.num_schedules()]
263 }
264}
265
266crate::declare_variants! {
267 default StaffScheduling => "(num_workers + 1)^num_schedules" create StaffSchedulingCreateSpec,
268}
269
270crate::register_brute_force! {
271 StaffScheduling,
272}
273
274#[cfg(feature = "example-db")]
275pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
276 vec![crate::example_db::specs::ModelExampleSpec {
277 id: "staff_scheduling",
278 instance: Box::new(StaffScheduling::new(
279 5,
280 vec![
281 vec![true, true, true, true, true, false, false],
282 vec![false, true, true, true, true, true, false],
283 vec![false, false, true, true, true, true, true],
284 vec![true, false, false, true, true, true, true],
285 vec![true, true, false, false, true, true, true],
286 ],
287 vec![2, 2, 2, 3, 3, 2, 1],
288 4,
289 )),
290 optimal_config: serde_json::json!(vec![1, 1, 1, 1, 0]),
291 optimal_value: serde_json::json!(true),
292 }]
293}
294
295#[cfg(test)]
296#[path = "../../unit_tests/models/misc/staff_scheduling.rs"]
297mod tests;