Skip to main content

problemreductions/models/misc/
staff_scheduling.rs

1//! Staff Scheduling problem implementation.
2//!
3//! Given a collection of schedule patterns, period staffing requirements, and a
4//! worker budget, determine whether workers can be assigned to schedules so that
5//! all requirements are met without exceeding the budget.
6
7use 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/// The Staff Scheduling problem.
29///
30/// Each variable represents how many workers adopt a particular schedule
31/// pattern. A configuration is satisfying iff the total assigned workers does
32/// not exceed `num_workers` and every period's staffing requirement is met.
33#[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    /// Create a new Staff Scheduling instance.
43    ///
44    /// # Panics
45    ///
46    /// Panics if `num_workers` does not fit in `usize`, if any schedule has a
47    /// different number of periods than `requirements.len()`, or if any
48    /// schedule has a number of active periods different from
49    /// `shifts_per_schedule`.
50    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    /// Get the number of periods.
88    pub fn num_periods(&self) -> usize {
89        self.requirements.len()
90    }
91
92    /// Get the required number of active periods per schedule.
93    pub fn shifts_per_schedule(&self) -> usize {
94        self.shifts_per_schedule
95    }
96
97    /// Get the schedule patterns.
98    pub fn schedules(&self) -> &[Vec<bool>] {
99        &self.schedules
100    }
101
102    /// Get the staffing requirements.
103    pub fn requirements(&self) -> &[u64] {
104        &self.requirements
105    }
106
107    /// Get the worker budget.
108    pub fn num_workers(&self) -> u64 {
109        self.num_workers
110    }
111
112    /// Get the number of schedule patterns.
113    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;