problemreductions/models/misc/
staff_scheduling.rs1use crate::registry::{FieldInfo, 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 module_path: module_path!(),
18 description: "Assign workers to schedule patterns to satisfy per-period staffing requirements within a worker budget",
19 fields: &[
20 FieldInfo { name: "shifts_per_schedule", type_name: "usize", description: "Required number of active periods in each schedule pattern" },
21 FieldInfo { name: "schedules", type_name: "Vec<Vec<bool>>", description: "Binary schedule patterns available to workers" },
22 FieldInfo { name: "requirements", type_name: "Vec<u64>", description: "Minimum staffing requirement for each period" },
23 FieldInfo { name: "num_workers", type_name: "u64", description: "Maximum number of workers available" },
24 ],
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct StaffScheduling {
35 shifts_per_schedule: usize,
36 schedules: Vec<Vec<bool>>,
37 requirements: Vec<u64>,
38 num_workers: u64,
39}
40
41impl StaffScheduling {
42 pub fn new(
51 shifts_per_schedule: usize,
52 schedules: Vec<Vec<bool>>,
53 requirements: Vec<u64>,
54 num_workers: u64,
55 ) -> Self {
56 assert!(
57 num_workers < usize::MAX as u64,
58 "num_workers must fit in usize so dims() can encode 0..=num_workers"
59 );
60
61 let num_periods = requirements.len();
62 for (index, schedule) in schedules.iter().enumerate() {
63 assert_eq!(
64 schedule.len(),
65 num_periods,
66 "schedule {} has {} periods, expected {}",
67 index,
68 schedule.len(),
69 num_periods
70 );
71 let ones = schedule.iter().filter(|&&active| active).count();
72 assert_eq!(
73 ones, shifts_per_schedule,
74 "schedule {} has {} active periods, expected {}",
75 index, ones, shifts_per_schedule
76 );
77 }
78
79 Self {
80 shifts_per_schedule,
81 schedules,
82 requirements,
83 num_workers,
84 }
85 }
86
87 pub fn num_periods(&self) -> usize {
89 self.requirements.len()
90 }
91
92 pub fn shifts_per_schedule(&self) -> usize {
94 self.shifts_per_schedule
95 }
96
97 pub fn schedules(&self) -> &[Vec<bool>] {
99 &self.schedules
100 }
101
102 pub fn requirements(&self) -> &[u64] {
104 &self.requirements
105 }
106
107 pub fn num_workers(&self) -> u64 {
109 self.num_workers
110 }
111
112 pub fn num_schedules(&self) -> usize {
114 self.schedules.len()
115 }
116
117 fn worker_limit(&self) -> usize {
118 self.num_workers as usize
119 }
120
121 fn worker_counts_valid(&self, config: &[usize]) -> bool {
122 config.iter().all(|&count| count <= self.worker_limit())
123 }
124
125 fn within_budget(&self, config: &[usize]) -> bool {
126 config.iter().map(|&count| count as u128).sum::<u128>() <= self.num_workers as u128
127 }
128
129 fn meets_requirements(&self, config: &[usize]) -> bool {
130 let mut coverage = vec![0u128; self.num_periods()];
131
132 for (count, schedule) in config.iter().zip(&self.schedules) {
133 if *count == 0 {
134 continue;
135 }
136 let count = *count as u128;
137 for (period, active) in schedule.iter().enumerate() {
138 if *active {
139 coverage[period] += count;
140 }
141 }
142 }
143
144 coverage
145 .iter()
146 .zip(&self.requirements)
147 .all(|(covered, required)| *covered >= *required as u128)
148 }
149}
150
151impl Problem for StaffScheduling {
152 const NAME: &'static str = "StaffScheduling";
153 type Value = crate::types::Or;
154
155 fn dims(&self) -> Vec<usize> {
156 vec![self.worker_limit() + 1; self.num_schedules()]
157 }
158
159 fn evaluate(&self, config: &[usize]) -> crate::types::Or {
160 crate::types::Or({
161 if config.len() != self.num_schedules() {
162 return crate::types::Or(false);
163 }
164 self.worker_counts_valid(config)
165 && self.within_budget(config)
166 && self.meets_requirements(config)
167 })
168 }
169
170 fn variant() -> Vec<(&'static str, &'static str)> {
171 crate::variant_params![]
172 }
173}
174
175crate::declare_variants! {
176 default StaffScheduling => "(num_workers + 1)^num_schedules",
177}
178
179#[cfg(feature = "example-db")]
180pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
181 vec![crate::example_db::specs::ModelExampleSpec {
182 id: "staff_scheduling",
183 instance: Box::new(StaffScheduling::new(
184 5,
185 vec![
186 vec![true, true, true, true, true, false, false],
187 vec![false, true, true, true, true, true, false],
188 vec![false, false, true, true, true, true, true],
189 vec![true, false, false, true, true, true, true],
190 vec![true, true, false, false, true, true, true],
191 ],
192 vec![2, 2, 2, 3, 3, 2, 1],
193 4,
194 )),
195 optimal_config: vec![1, 1, 1, 1, 0],
196 optimal_value: serde_json::json!(true),
197 }]
198}
199
200#[cfg(test)]
201#[path = "../../unit_tests/models/misc/staff_scheduling.rs"]
202mod tests;