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::{CreateSpec, 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        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Assign craftsmen to tasks over work periods subject to availability and exact pairwise requirements",
20        fields: TimetableDesignCreateSpec::FIELDS,
21    }
22}
23
24/// The Timetable Design problem.
25///
26/// A configuration is a flattened binary tensor `f(c,t,h)` in craftsman-major,
27/// task-next, period-last order:
28/// `idx = ((c * num_tasks) + t) * num_periods + h`.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TimetableDesign {
31    num_periods: usize,
32    num_craftsmen: usize,
33    num_tasks: usize,
34    craftsman_avail: Vec<Vec<bool>>,
35    task_avail: Vec<Vec<bool>>,
36    requirements: Vec<Vec<i64>>,
37}
38
39#[derive(Debug, Deserialize, crate::CreateSpec)]
40struct TimetableDesignCreateSpec {
41    /// Number of work periods.
42    num_periods: usize,
43    /// Number of craftsmen.
44    num_craftsmen: usize,
45    /// Number of tasks.
46    num_tasks: usize,
47    /// Craftsman availability matrix.
48    craftsman_avail: Vec<Vec<bool>>,
49    /// Task availability matrix.
50    task_avail: Vec<Vec<bool>>,
51    /// Required work periods for each craftsman-task pair.
52    requirements: Vec<Vec<i64>>,
53}
54impl TryFrom<TimetableDesignCreateSpec> for TimetableDesign {
55    type Error = crate::registry::ConstructionError;
56    fn try_from(spec: TimetableDesignCreateSpec) -> Result<Self, Self::Error> {
57        if spec.craftsman_avail.len() != spec.num_craftsmen {
58            return Err(format!(
59                "craftsman_avail has {} rows, expected {}",
60                spec.craftsman_avail.len(),
61                spec.num_craftsmen
62            )
63            .into());
64        }
65        if let Some((index, row)) = spec
66            .craftsman_avail
67            .iter()
68            .enumerate()
69            .find(|(_, row)| row.len() != spec.num_periods)
70        {
71            return Err(format!(
72                "craftsman_avail row {index} has {} periods, expected {}",
73                row.len(),
74                spec.num_periods
75            )
76            .into());
77        }
78        if spec.task_avail.len() != spec.num_tasks {
79            return Err(format!(
80                "task_avail has {} rows, expected {}",
81                spec.task_avail.len(),
82                spec.num_tasks
83            )
84            .into());
85        }
86        if let Some((index, row)) = spec
87            .task_avail
88            .iter()
89            .enumerate()
90            .find(|(_, row)| row.len() != spec.num_periods)
91        {
92            return Err(format!(
93                "task_avail row {index} has {} periods, expected {}",
94                row.len(),
95                spec.num_periods
96            )
97            .into());
98        }
99        if spec.requirements.len() != spec.num_craftsmen {
100            return Err(format!(
101                "requirements has {} rows, expected {}",
102                spec.requirements.len(),
103                spec.num_craftsmen
104            )
105            .into());
106        }
107        if let Some((index, row)) = spec
108            .requirements
109            .iter()
110            .enumerate()
111            .find(|(_, row)| row.len() != spec.num_tasks)
112        {
113            return Err(format!(
114                "requirements row {index} has {} tasks, expected {}",
115                row.len(),
116                spec.num_tasks
117            )
118            .into());
119        }
120        Ok(Self::new(
121            spec.num_periods,
122            spec.num_craftsmen,
123            spec.num_tasks,
124            spec.craftsman_avail,
125            spec.task_avail,
126            spec.requirements,
127        ))
128    }
129}
130
131impl TimetableDesign {
132    /// Create a new Timetable Design instance.
133    ///
134    /// # Panics
135    ///
136    /// Panics if any matrix dimensions do not match the declared counts.
137    pub fn new(
138        num_periods: usize,
139        num_craftsmen: usize,
140        num_tasks: usize,
141        craftsman_avail: Vec<Vec<bool>>,
142        task_avail: Vec<Vec<bool>>,
143        requirements: Vec<Vec<i64>>,
144    ) -> Self {
145        assert_eq!(
146            craftsman_avail.len(),
147            num_craftsmen,
148            "craftsman_avail has {} rows, expected {}",
149            craftsman_avail.len(),
150            num_craftsmen
151        );
152        for (craftsman, row) in craftsman_avail.iter().enumerate() {
153            assert_eq!(
154                row.len(),
155                num_periods,
156                "craftsman {} availability has {} periods, expected {}",
157                craftsman,
158                row.len(),
159                num_periods
160            );
161        }
162
163        assert_eq!(
164            task_avail.len(),
165            num_tasks,
166            "task_avail has {} rows, expected {}",
167            task_avail.len(),
168            num_tasks
169        );
170        for (task, row) in task_avail.iter().enumerate() {
171            assert_eq!(
172                row.len(),
173                num_periods,
174                "task {} availability has {} periods, expected {}",
175                task,
176                row.len(),
177                num_periods
178            );
179        }
180
181        assert_eq!(
182            requirements.len(),
183            num_craftsmen,
184            "requirements has {} rows, expected {}",
185            requirements.len(),
186            num_craftsmen
187        );
188        for (craftsman, row) in requirements.iter().enumerate() {
189            assert_eq!(
190                row.len(),
191                num_tasks,
192                "requirements row {} has {} tasks, expected {}",
193                craftsman,
194                row.len(),
195                num_tasks
196            );
197        }
198
199        Self {
200            num_periods,
201            num_craftsmen,
202            num_tasks,
203            craftsman_avail,
204            task_avail,
205            requirements,
206        }
207    }
208
209    /// Get the number of periods.
210    pub fn num_periods(&self) -> usize {
211        self.num_periods
212    }
213
214    /// Get the number of craftsmen.
215    pub fn num_craftsmen(&self) -> usize {
216        self.num_craftsmen
217    }
218
219    /// Get the number of tasks.
220    pub fn num_tasks(&self) -> usize {
221        self.num_tasks
222    }
223
224    /// Get craftsman availability.
225    pub fn craftsman_avail(&self) -> &[Vec<bool>] {
226        &self.craftsman_avail
227    }
228
229    /// Get task availability.
230    pub fn task_avail(&self) -> &[Vec<bool>] {
231        &self.task_avail
232    }
233
234    /// Get the pairwise work requirements.
235    pub fn requirements(&self) -> &[Vec<i64>] {
236        &self.requirements
237    }
238
239    fn config_len(&self) -> usize {
240        self.num_craftsmen * self.num_tasks * self.num_periods
241    }
242
243    fn index(&self, craftsman: usize, task: usize, period: usize) -> usize {
244        ((craftsman * self.num_tasks) + task) * self.num_periods + period
245    }
246
247    pub(crate) fn solve_via_required_assignments(&self) -> Option<Vec<Vec<Vec<bool>>>> {
248        #[derive(Clone)]
249        struct PairRequirement {
250            craftsman: usize,
251            task: usize,
252            required: usize,
253            allowed_periods: Vec<usize>,
254        }
255
256        let mut craftsman_demand = vec![0usize; self.num_craftsmen];
257        let mut task_demand = vec![0usize; self.num_tasks];
258        let mut pairs = Vec::new();
259
260        for (craftsman, requirement_row) in self.requirements.iter().enumerate() {
261            for (task, required_i64) in requirement_row.iter().enumerate() {
262                let required = usize::try_from(*required_i64).ok()?;
263                craftsman_demand[craftsman] += required;
264                task_demand[task] += required;
265
266                if required == 0 {
267                    continue;
268                }
269
270                let allowed_periods = (0..self.num_periods)
271                    .filter(|&period| {
272                        self.craftsman_avail[craftsman][period] && self.task_avail[task][period]
273                    })
274                    .collect::<Vec<_>>();
275
276                if allowed_periods.len() < required {
277                    return None;
278                }
279
280                pairs.push(PairRequirement {
281                    craftsman,
282                    task,
283                    required,
284                    allowed_periods,
285                });
286            }
287        }
288
289        if craftsman_demand
290            .iter()
291            .zip(&self.craftsman_avail)
292            .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count())
293        {
294            return None;
295        }
296
297        if task_demand
298            .iter()
299            .zip(&self.task_avail)
300            .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count())
301        {
302            return None;
303        }
304
305        pairs.sort_by_key(|pair| (pair.allowed_periods.len(), pair.required));
306
307        struct SearchState<'a> {
308            problem: &'a TimetableDesign,
309            pairs: &'a [PairRequirement],
310            craftsman_busy: Vec<Vec<bool>>,
311            task_busy: Vec<Vec<bool>>,
312            config: Vec<usize>,
313        }
314
315        impl SearchState<'_> {
316            fn search_pair(
317                &mut self,
318                pair_index: usize,
319                period_offset: usize,
320                remaining: usize,
321            ) -> bool {
322                if pair_index == self.pairs.len() {
323                    return true;
324                }
325
326                let pair = &self.pairs[pair_index];
327                if remaining == 0 {
328                    return self.search_pair(
329                        pair_index + 1,
330                        0,
331                        self.pairs
332                            .get(pair_index + 1)
333                            .map_or(0, |next| next.required),
334                    );
335                }
336
337                let feasible_remaining = pair.allowed_periods[period_offset..]
338                    .iter()
339                    .filter(|&&period| {
340                        !self.craftsman_busy[pair.craftsman][period]
341                            && !self.task_busy[pair.task][period]
342                    })
343                    .count();
344                if feasible_remaining < remaining {
345                    return false;
346                }
347
348                for candidate_index in period_offset..pair.allowed_periods.len() {
349                    let period = pair.allowed_periods[candidate_index];
350                    if self.craftsman_busy[pair.craftsman][period]
351                        || self.task_busy[pair.task][period]
352                    {
353                        continue;
354                    }
355
356                    self.craftsman_busy[pair.craftsman][period] = true;
357                    self.task_busy[pair.task][period] = true;
358                    self.config[self.problem.index(pair.craftsman, pair.task, period)] = 1;
359
360                    if self.search_pair(pair_index, candidate_index + 1, remaining - 1) {
361                        return true;
362                    }
363
364                    self.config[self.problem.index(pair.craftsman, pair.task, period)] = 0;
365                    self.task_busy[pair.task][period] = false;
366                    self.craftsman_busy[pair.craftsman][period] = false;
367                }
368
369                false
370            }
371        }
372
373        let mut state = SearchState {
374            problem: self,
375            pairs: &pairs,
376            craftsman_busy: vec![vec![false; self.num_periods]; self.num_craftsmen],
377            task_busy: vec![vec![false; self.num_periods]; self.num_tasks],
378            config: vec![0; self.config_len()],
379        };
380
381        if state.search_pair(0, 0, pairs.first().map_or(0, |pair| pair.required)) {
382            Some(
383                (0..self.num_craftsmen)
384                    .map(|craftsman| {
385                        (0..self.num_tasks)
386                            .map(|task| {
387                                (0..self.num_periods)
388                                    .map(|period| {
389                                        state.config[self.index(craftsman, task, period)] == 1
390                                    })
391                                    .collect()
392                            })
393                            .collect()
394                    })
395                    .collect(),
396            )
397        } else {
398            None
399        }
400    }
401}
402
403impl Problem for TimetableDesign {
404    const NAME: &'static str = "TimetableDesign";
405    type Solution = Vec<Vec<Vec<bool>>>;
406    type Value = crate::types::Or;
407
408    crate::problem_parameters![
409        ("num_craftsmen", num_craftsmen),
410        ("num_periods", num_periods),
411        ("num_tasks", num_tasks),
412    ];
413
414    fn evaluate(
415        &self,
416        solution: &Self::Solution,
417    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
418        if solution.len() != self.num_craftsmen
419            || solution.iter().any(|craftsman| {
420                craftsman.len() != self.num_tasks
421                    || craftsman.iter().any(|task| task.len() != self.num_periods)
422            })
423        {
424            return Err(crate::traits::EvaluationError::InvalidConfiguration(
425                "timetable dimensions do not match the instance".into(),
426            ));
427        }
428        let config = solution
429            .iter()
430            .flatten()
431            .flatten()
432            .copied()
433            .collect::<Vec<_>>();
434        Ok({
435            crate::types::Or({
436                if config.len() != self.config_len() {
437                    return Ok(crate::types::Or(false));
438                }
439                let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen];
440                let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks];
441                let mut pair_counts = vec![vec![0i64; self.num_tasks]; self.num_craftsmen];
442
443                for craftsman in 0..self.num_craftsmen {
444                    for task in 0..self.num_tasks {
445                        for period in 0..self.num_periods {
446                            if !config[self.index(craftsman, task, period)] {
447                                continue;
448                            }
449
450                            if !self.craftsman_avail[craftsman][period]
451                                || !self.task_avail[task][period]
452                            {
453                                return Ok(crate::types::Or(false));
454                            }
455
456                            if craftsman_busy[craftsman][period] || task_busy[task][period] {
457                                return Ok(crate::types::Or(false));
458                            }
459
460                            craftsman_busy[craftsman][period] = true;
461                            task_busy[task][period] = true;
462                            pair_counts[craftsman][task] += 1;
463                        }
464                    }
465                }
466
467                pair_counts == self.requirements
468            })
469        })
470    }
471
472    fn variant() -> Vec<(&'static str, &'static str)> {
473        crate::variant_params![]
474    }
475}
476
477impl crate::solvers::BruteForceProblem for TimetableDesign {
478    fn dimensions(&self) -> Vec<usize> {
479        vec![2; self.config_len()]
480    }
481}
482
483crate::declare_variants! {
484    default TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)" create TimetableDesignCreateSpec,
485}
486
487crate::register_brute_force! {
488    TimetableDesign decode |problem: &TimetableDesign, indices: Vec<usize>| (0..problem.num_craftsmen()).map(|craftsman| (0..problem.num_tasks()).map(|task| (0..problem.num_periods()).map(|period| indices[problem.index(craftsman, task, period)] != 0).collect()).collect()).collect(),
489}
490
491#[cfg(any(test, feature = "example-db"))]
492const ISSUE_EXAMPLE_ASSIGNMENTS: &[(usize, usize, usize)] = &[
493    (0, 0, 0),
494    (1, 4, 0),
495    (1, 1, 1),
496    (2, 3, 1),
497    (0, 2, 2),
498    (3, 4, 2),
499    (4, 1, 2),
500];
501
502#[cfg(any(test, feature = "example-db"))]
503fn issue_example_problem() -> TimetableDesign {
504    TimetableDesign::new(
505        3,
506        5,
507        5,
508        vec![
509            vec![true, true, true],
510            vec![true, true, false],
511            vec![false, true, true],
512            vec![true, false, true],
513            vec![true, true, true],
514        ],
515        vec![
516            vec![true, true, false],
517            vec![false, true, true],
518            vec![true, false, true],
519            vec![true, true, true],
520            vec![true, true, true],
521        ],
522        vec![
523            vec![1, 0, 1, 0, 0],
524            vec![0, 1, 0, 0, 1],
525            vec![0, 0, 0, 1, 0],
526            vec![0, 0, 0, 0, 1],
527            vec![0, 1, 0, 0, 0],
528        ],
529    )
530}
531
532#[cfg(any(test, feature = "example-db"))]
533fn issue_example_config() -> Vec<Vec<Vec<bool>>> {
534    let problem = issue_example_problem();
535    let mut config = vec![
536        vec![vec![false; problem.num_periods()]; problem.num_tasks()];
537        problem.num_craftsmen()
538    ];
539    for &(craftsman, task, period) in ISSUE_EXAMPLE_ASSIGNMENTS {
540        config[craftsman][task][period] = true;
541    }
542    config
543}
544
545#[cfg(feature = "example-db")]
546pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
547    vec![crate::example_db::specs::ModelExampleSpec {
548        id: "timetable_design",
549        instance: Box::new(issue_example_problem()),
550        optimal_config: serde_json::json!(issue_example_config()),
551        optimal_value: serde_json::json!(true),
552    }]
553}
554
555#[cfg(test)]
556#[path = "../../unit_tests/models/misc/timetable_design.rs"]
557mod tests;