Skip to main content

problemreductions/models/misc/
timetable_design.rs

1//! Timetable Design problem implementation.
2//!
3//! Decide whether craftsmen can be assigned to tasks across work periods while
4//! respecting availability, per-period exclusivity, and exact pairwise work
5//! requirements.
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "TimetableDesign",
14        display_name: "Timetable Design",
15        aliases: &[],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Assign craftsmen to tasks over work periods subject to availability and exact pairwise requirements",
19        fields: &[
20            FieldInfo { name: "num_periods", type_name: "usize", description: "Number of work periods |H|" },
21            FieldInfo { name: "num_craftsmen", type_name: "usize", description: "Number of craftsmen |C|" },
22            FieldInfo { name: "num_tasks", type_name: "usize", description: "Number of tasks |T|" },
23            FieldInfo { name: "craftsman_avail", type_name: "Vec<Vec<bool>>", description: "Availability matrix A(c) for craftsmen (|C| x |H|)" },
24            FieldInfo { name: "task_avail", type_name: "Vec<Vec<bool>>", description: "Availability matrix A(t) for tasks (|T| x |H|)" },
25            FieldInfo { name: "requirements", type_name: "Vec<Vec<u64>>", description: "Required work periods R(c,t) for each craftsman-task pair (|C| x |T|)" },
26        ],
27    }
28}
29
30/// The Timetable Design problem.
31///
32/// A configuration is a flattened binary tensor `f(c,t,h)` in craftsman-major,
33/// task-next, period-last order:
34/// `idx = ((c * num_tasks) + t) * num_periods + h`.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TimetableDesign {
37    num_periods: usize,
38    num_craftsmen: usize,
39    num_tasks: usize,
40    craftsman_avail: Vec<Vec<bool>>,
41    task_avail: Vec<Vec<bool>>,
42    requirements: Vec<Vec<u64>>,
43}
44
45impl TimetableDesign {
46    /// Create a new Timetable Design instance.
47    ///
48    /// # Panics
49    ///
50    /// Panics if any matrix dimensions do not match the declared counts.
51    pub fn new(
52        num_periods: usize,
53        num_craftsmen: usize,
54        num_tasks: usize,
55        craftsman_avail: Vec<Vec<bool>>,
56        task_avail: Vec<Vec<bool>>,
57        requirements: Vec<Vec<u64>>,
58    ) -> Self {
59        assert_eq!(
60            craftsman_avail.len(),
61            num_craftsmen,
62            "craftsman_avail has {} rows, expected {}",
63            craftsman_avail.len(),
64            num_craftsmen
65        );
66        for (craftsman, row) in craftsman_avail.iter().enumerate() {
67            assert_eq!(
68                row.len(),
69                num_periods,
70                "craftsman {} availability has {} periods, expected {}",
71                craftsman,
72                row.len(),
73                num_periods
74            );
75        }
76
77        assert_eq!(
78            task_avail.len(),
79            num_tasks,
80            "task_avail has {} rows, expected {}",
81            task_avail.len(),
82            num_tasks
83        );
84        for (task, row) in task_avail.iter().enumerate() {
85            assert_eq!(
86                row.len(),
87                num_periods,
88                "task {} availability has {} periods, expected {}",
89                task,
90                row.len(),
91                num_periods
92            );
93        }
94
95        assert_eq!(
96            requirements.len(),
97            num_craftsmen,
98            "requirements has {} rows, expected {}",
99            requirements.len(),
100            num_craftsmen
101        );
102        for (craftsman, row) in requirements.iter().enumerate() {
103            assert_eq!(
104                row.len(),
105                num_tasks,
106                "requirements row {} has {} tasks, expected {}",
107                craftsman,
108                row.len(),
109                num_tasks
110            );
111        }
112
113        Self {
114            num_periods,
115            num_craftsmen,
116            num_tasks,
117            craftsman_avail,
118            task_avail,
119            requirements,
120        }
121    }
122
123    /// Get the number of periods.
124    pub fn num_periods(&self) -> usize {
125        self.num_periods
126    }
127
128    /// Get the number of craftsmen.
129    pub fn num_craftsmen(&self) -> usize {
130        self.num_craftsmen
131    }
132
133    /// Get the number of tasks.
134    pub fn num_tasks(&self) -> usize {
135        self.num_tasks
136    }
137
138    /// Get craftsman availability.
139    pub fn craftsman_avail(&self) -> &[Vec<bool>] {
140        &self.craftsman_avail
141    }
142
143    /// Get task availability.
144    pub fn task_avail(&self) -> &[Vec<bool>] {
145        &self.task_avail
146    }
147
148    /// Get the pairwise work requirements.
149    pub fn requirements(&self) -> &[Vec<u64>] {
150        &self.requirements
151    }
152
153    fn config_len(&self) -> usize {
154        self.num_craftsmen * self.num_tasks * self.num_periods
155    }
156
157    fn index(&self, craftsman: usize, task: usize, period: usize) -> usize {
158        ((craftsman * self.num_tasks) + task) * self.num_periods + period
159    }
160
161    #[cfg(feature = "ilp-solver")]
162    pub(crate) fn solve_via_required_assignments(&self) -> Option<Vec<usize>> {
163        #[derive(Clone)]
164        struct PairRequirement {
165            craftsman: usize,
166            task: usize,
167            required: usize,
168            allowed_periods: Vec<usize>,
169        }
170
171        let mut craftsman_demand = vec![0usize; self.num_craftsmen];
172        let mut task_demand = vec![0usize; self.num_tasks];
173        let mut pairs = Vec::new();
174
175        for (craftsman, requirement_row) in self.requirements.iter().enumerate() {
176            for (task, required_u64) in requirement_row.iter().enumerate() {
177                let required = usize::try_from(*required_u64).ok()?;
178                craftsman_demand[craftsman] += required;
179                task_demand[task] += required;
180
181                if required == 0 {
182                    continue;
183                }
184
185                let allowed_periods = (0..self.num_periods)
186                    .filter(|&period| {
187                        self.craftsman_avail[craftsman][period] && self.task_avail[task][period]
188                    })
189                    .collect::<Vec<_>>();
190
191                if allowed_periods.len() < required {
192                    return None;
193                }
194
195                pairs.push(PairRequirement {
196                    craftsman,
197                    task,
198                    required,
199                    allowed_periods,
200                });
201            }
202        }
203
204        if craftsman_demand
205            .iter()
206            .zip(&self.craftsman_avail)
207            .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count())
208        {
209            return None;
210        }
211
212        if task_demand
213            .iter()
214            .zip(&self.task_avail)
215            .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count())
216        {
217            return None;
218        }
219
220        pairs.sort_by_key(|pair| (pair.allowed_periods.len(), pair.required));
221
222        struct SearchState<'a> {
223            problem: &'a TimetableDesign,
224            pairs: &'a [PairRequirement],
225            craftsman_busy: Vec<Vec<bool>>,
226            task_busy: Vec<Vec<bool>>,
227            config: Vec<usize>,
228        }
229
230        impl SearchState<'_> {
231            fn search_pair(
232                &mut self,
233                pair_index: usize,
234                period_offset: usize,
235                remaining: usize,
236            ) -> bool {
237                if pair_index == self.pairs.len() {
238                    return true;
239                }
240
241                let pair = &self.pairs[pair_index];
242                if remaining == 0 {
243                    return self.search_pair(
244                        pair_index + 1,
245                        0,
246                        self.pairs
247                            .get(pair_index + 1)
248                            .map_or(0, |next| next.required),
249                    );
250                }
251
252                let feasible_remaining = pair.allowed_periods[period_offset..]
253                    .iter()
254                    .filter(|&&period| {
255                        !self.craftsman_busy[pair.craftsman][period]
256                            && !self.task_busy[pair.task][period]
257                    })
258                    .count();
259                if feasible_remaining < remaining {
260                    return false;
261                }
262
263                for candidate_index in period_offset..pair.allowed_periods.len() {
264                    let period = pair.allowed_periods[candidate_index];
265                    if self.craftsman_busy[pair.craftsman][period]
266                        || self.task_busy[pair.task][period]
267                    {
268                        continue;
269                    }
270
271                    self.craftsman_busy[pair.craftsman][period] = true;
272                    self.task_busy[pair.task][period] = true;
273                    self.config[self.problem.index(pair.craftsman, pair.task, period)] = 1;
274
275                    if self.search_pair(pair_index, candidate_index + 1, remaining - 1) {
276                        return true;
277                    }
278
279                    self.config[self.problem.index(pair.craftsman, pair.task, period)] = 0;
280                    self.task_busy[pair.task][period] = false;
281                    self.craftsman_busy[pair.craftsman][period] = false;
282                }
283
284                false
285            }
286        }
287
288        let mut state = SearchState {
289            problem: self,
290            pairs: &pairs,
291            craftsman_busy: vec![vec![false; self.num_periods]; self.num_craftsmen],
292            task_busy: vec![vec![false; self.num_periods]; self.num_tasks],
293            config: vec![0; self.config_len()],
294        };
295
296        if state.search_pair(0, 0, pairs.first().map_or(0, |pair| pair.required)) {
297            Some(state.config)
298        } else {
299            None
300        }
301    }
302}
303
304impl Problem for TimetableDesign {
305    const NAME: &'static str = "TimetableDesign";
306    type Value = crate::types::Or;
307
308    fn dims(&self) -> Vec<usize> {
309        vec![2; self.config_len()]
310    }
311
312    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
313        crate::types::Or({
314            if config.len() != self.config_len() {
315                return crate::types::Or(false);
316            }
317            if config.iter().any(|&value| value > 1) {
318                return crate::types::Or(false);
319            }
320
321            let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen];
322            let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks];
323            let mut pair_counts = vec![vec![0u64; self.num_tasks]; self.num_craftsmen];
324
325            for craftsman in 0..self.num_craftsmen {
326                for task in 0..self.num_tasks {
327                    for period in 0..self.num_periods {
328                        if config[self.index(craftsman, task, period)] == 0 {
329                            continue;
330                        }
331
332                        if !self.craftsman_avail[craftsman][period]
333                            || !self.task_avail[task][period]
334                        {
335                            return crate::types::Or(false);
336                        }
337
338                        if craftsman_busy[craftsman][period] || task_busy[task][period] {
339                            return crate::types::Or(false);
340                        }
341
342                        craftsman_busy[craftsman][period] = true;
343                        task_busy[task][period] = true;
344                        pair_counts[craftsman][task] += 1;
345                    }
346                }
347            }
348
349            pair_counts == self.requirements
350        })
351    }
352
353    fn variant() -> Vec<(&'static str, &'static str)> {
354        crate::variant_params![]
355    }
356}
357
358crate::declare_variants! {
359    default TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)",
360}
361
362#[cfg(any(test, feature = "example-db"))]
363const ISSUE_EXAMPLE_ASSIGNMENTS: &[(usize, usize, usize)] = &[
364    (0, 0, 0),
365    (1, 4, 0),
366    (1, 1, 1),
367    (2, 3, 1),
368    (0, 2, 2),
369    (3, 4, 2),
370    (4, 1, 2),
371];
372
373#[cfg(any(test, feature = "example-db"))]
374fn issue_example_problem() -> TimetableDesign {
375    TimetableDesign::new(
376        3,
377        5,
378        5,
379        vec![
380            vec![true, true, true],
381            vec![true, true, false],
382            vec![false, true, true],
383            vec![true, false, true],
384            vec![true, true, true],
385        ],
386        vec![
387            vec![true, true, false],
388            vec![false, true, true],
389            vec![true, false, true],
390            vec![true, true, true],
391            vec![true, true, true],
392        ],
393        vec![
394            vec![1, 0, 1, 0, 0],
395            vec![0, 1, 0, 0, 1],
396            vec![0, 0, 0, 1, 0],
397            vec![0, 0, 0, 0, 1],
398            vec![0, 1, 0, 0, 0],
399        ],
400    )
401}
402
403#[cfg(any(test, feature = "example-db"))]
404fn issue_example_config() -> Vec<usize> {
405    let problem = issue_example_problem();
406    let mut config = vec![0; problem.config_len()];
407    for &(craftsman, task, period) in ISSUE_EXAMPLE_ASSIGNMENTS {
408        config[problem.index(craftsman, task, period)] = 1;
409    }
410    config
411}
412
413#[cfg(feature = "example-db")]
414pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
415    vec![crate::example_db::specs::ModelExampleSpec {
416        id: "timetable_design",
417        instance: Box::new(issue_example_problem()),
418        optimal_config: issue_example_config(),
419        optimal_value: serde_json::json!(true),
420    }]
421}
422
423#[cfg(test)]
424#[path = "../../unit_tests/models/misc/timetable_design.rs"]
425mod tests;