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)]
30#[serde(try_from = "TimetableDesignCreateSpec")]
31pub struct TimetableDesign {
32    num_periods: usize,
33    num_craftsmen: usize,
34    num_tasks: usize,
35    craftsman_avail: Vec<Vec<bool>>,
36    task_avail: Vec<Vec<bool>>,
37    requirements: Vec<Vec<i64>>,
38}
39
40#[derive(Debug, Deserialize, crate::CreateSpec)]
41struct TimetableDesignCreateSpec {
42    /// Number of work periods.
43    num_periods: usize,
44    /// Number of craftsmen.
45    num_craftsmen: usize,
46    /// Number of tasks.
47    num_tasks: usize,
48    /// Craftsman availability matrix.
49    craftsman_avail: Vec<Vec<bool>>,
50    /// Task availability matrix.
51    task_avail: Vec<Vec<bool>>,
52    /// Required work periods for each craftsman-task pair.
53    requirements: Vec<Vec<i64>>,
54}
55impl TryFrom<TimetableDesignCreateSpec> for TimetableDesign {
56    type Error = crate::registry::ConstructionError;
57    fn try_from(spec: TimetableDesignCreateSpec) -> Result<Self, Self::Error> {
58        Self::try_new(
59            spec.num_periods,
60            spec.num_craftsmen,
61            spec.num_tasks,
62            spec.craftsman_avail,
63            spec.task_avail,
64            spec.requirements,
65        )
66    }
67}
68
69impl TimetableDesign {
70    /// Create a new Timetable Design instance.
71    ///
72    /// # Panics
73    ///
74    /// Panics if any matrix dimensions do not match the declared counts.
75    pub fn new(
76        num_periods: usize,
77        num_craftsmen: usize,
78        num_tasks: usize,
79        craftsman_avail: Vec<Vec<bool>>,
80        task_avail: Vec<Vec<bool>>,
81        requirements: Vec<Vec<i64>>,
82    ) -> Self {
83        Self::try_new(
84            num_periods,
85            num_craftsmen,
86            num_tasks,
87            craftsman_avail,
88            task_avail,
89            requirements,
90        )
91        .unwrap_or_else(|error| panic!("{error}"))
92    }
93
94    fn try_new(
95        num_periods: usize,
96        num_craftsmen: usize,
97        num_tasks: usize,
98        craftsman_avail: Vec<Vec<bool>>,
99        task_avail: Vec<Vec<bool>>,
100        requirements: Vec<Vec<i64>>,
101    ) -> Result<Self, crate::registry::ConstructionError> {
102        if craftsman_avail.len() != num_craftsmen {
103            return Err(format!(
104                "craftsman_avail has {} rows, expected {}",
105                craftsman_avail.len(),
106                num_craftsmen
107            )
108            .into());
109        }
110        for (craftsman, row) in craftsman_avail.iter().enumerate() {
111            if row.len() != num_periods {
112                return Err(format!(
113                    "craftsman {} availability has {} periods, expected {}",
114                    craftsman,
115                    row.len(),
116                    num_periods
117                )
118                .into());
119            }
120        }
121
122        if task_avail.len() != num_tasks {
123            return Err(format!(
124                "task_avail has {} rows, expected {}",
125                task_avail.len(),
126                num_tasks
127            )
128            .into());
129        }
130        for (task, row) in task_avail.iter().enumerate() {
131            if row.len() != num_periods {
132                return Err(format!(
133                    "task {} availability has {} periods, expected {}",
134                    task,
135                    row.len(),
136                    num_periods
137                )
138                .into());
139            }
140        }
141
142        if requirements.len() != num_craftsmen {
143            return Err(format!(
144                "requirements has {} rows, expected {}",
145                requirements.len(),
146                num_craftsmen
147            )
148            .into());
149        }
150        for (craftsman, row) in requirements.iter().enumerate() {
151            if row.len() != num_tasks {
152                return Err(format!(
153                    "requirements row {} has {} tasks, expected {}",
154                    craftsman,
155                    row.len(),
156                    num_tasks
157                )
158                .into());
159            }
160        }
161
162        Ok(Self {
163            num_periods,
164            num_craftsmen,
165            num_tasks,
166            craftsman_avail,
167            task_avail,
168            requirements,
169        })
170    }
171
172    /// Get the number of periods.
173    pub fn num_periods(&self) -> usize {
174        self.num_periods
175    }
176
177    /// Get the number of craftsmen.
178    pub fn num_craftsmen(&self) -> usize {
179        self.num_craftsmen
180    }
181
182    /// Get the number of tasks.
183    pub fn num_tasks(&self) -> usize {
184        self.num_tasks
185    }
186
187    /// Get craftsman availability.
188    pub fn craftsman_avail(&self) -> &[Vec<bool>] {
189        &self.craftsman_avail
190    }
191
192    /// Get task availability.
193    pub fn task_avail(&self) -> &[Vec<bool>] {
194        &self.task_avail
195    }
196
197    /// Get the pairwise work requirements.
198    pub fn requirements(&self) -> &[Vec<i64>] {
199        &self.requirements
200    }
201
202    fn config_len(&self) -> usize {
203        self.num_craftsmen * self.num_tasks * self.num_periods
204    }
205
206    fn index(&self, craftsman: usize, task: usize, period: usize) -> usize {
207        ((craftsman * self.num_tasks) + task) * self.num_periods + period
208    }
209
210    /// Available triples belonging to pairs with positive demand.
211    pub fn num_available_assignments(&self) -> usize {
212        self.requirements
213            .iter()
214            .enumerate()
215            .map(|(c, row)| {
216                row.iter()
217                    .enumerate()
218                    .filter(|(_, r)| **r > 0)
219                    .map(|(t, _)| {
220                        self.craftsman_avail[c]
221                            .iter()
222                            .zip(&self.task_avail[t])
223                            .filter(|(a, b)| **a && **b)
224                            .count()
225                    })
226                    .sum::<usize>()
227            })
228            .sum()
229    }
230
231    /// Pair equations needed, including impossible negative demands.
232    pub fn num_nonzero_requirements(&self) -> usize {
233        self.requirements
234            .iter()
235            .flatten()
236            .filter(|&&r| r != 0)
237            .count()
238    }
239
240    /// Bit length of the number of periods.
241    pub fn period_count_bits(&self) -> u64 {
242        crate::types::max_numeric_magnitude_bits([self.num_periods])
243    }
244}
245
246impl Problem for TimetableDesign {
247    const NAME: &'static str = "TimetableDesign";
248    type Solution = Vec<Vec<Vec<bool>>>;
249    type Value = crate::types::Or;
250
251    crate::problem_parameters![
252        ("num_craftsmen", num_craftsmen),
253        ("num_available_assignments", num_available_assignments),
254        ("num_nonzero_requirements", num_nonzero_requirements),
255        ("period_count_bits", period_count_bits),
256        ("num_periods", num_periods),
257        ("num_tasks", num_tasks),
258    ];
259
260    fn evaluate(
261        &self,
262        solution: &Self::Solution,
263    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
264        if solution.len() != self.num_craftsmen
265            || solution.iter().any(|craftsman| {
266                craftsman.len() != self.num_tasks
267                    || craftsman.iter().any(|task| task.len() != self.num_periods)
268            })
269        {
270            return Err(crate::traits::EvaluationError::InvalidConfiguration(
271                "timetable dimensions do not match the instance".into(),
272            ));
273        }
274        let config = solution
275            .iter()
276            .flatten()
277            .flatten()
278            .copied()
279            .collect::<Vec<_>>();
280        Ok({
281            crate::types::Or({
282                if config.len() != self.config_len() {
283                    return Ok(crate::types::Or(false));
284                }
285                let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen];
286                let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks];
287                let mut pair_counts = vec![vec![0i64; self.num_tasks]; self.num_craftsmen];
288
289                for craftsman in 0..self.num_craftsmen {
290                    for task in 0..self.num_tasks {
291                        for period in 0..self.num_periods {
292                            if !config[self.index(craftsman, task, period)] {
293                                continue;
294                            }
295
296                            if !self.craftsman_avail[craftsman][period]
297                                || !self.task_avail[task][period]
298                            {
299                                return Ok(crate::types::Or(false));
300                            }
301
302                            if craftsman_busy[craftsman][period] || task_busy[task][period] {
303                                return Ok(crate::types::Or(false));
304                            }
305
306                            craftsman_busy[craftsman][period] = true;
307                            task_busy[task][period] = true;
308                            pair_counts[craftsman][task] += 1;
309                        }
310                    }
311                }
312
313                pair_counts == self.requirements
314            })
315        })
316    }
317
318    fn variant() -> Vec<(&'static str, &'static str)> {
319        crate::variant_params![]
320    }
321}
322
323impl crate::solvers::BruteForceProblem for TimetableDesign {
324    fn dimensions(&self) -> Vec<usize> {
325        vec![2; self.config_len()]
326    }
327}
328
329crate::declare_variants! {
330    default TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)" create TimetableDesignCreateSpec,
331}
332
333crate::register_brute_force! {
334    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(),
335}
336
337#[cfg(any(test, feature = "example-db"))]
338const ISSUE_EXAMPLE_ASSIGNMENTS: &[(usize, usize, usize)] = &[
339    (0, 0, 0),
340    (1, 4, 0),
341    (1, 1, 1),
342    (2, 3, 1),
343    (0, 2, 2),
344    (3, 4, 2),
345    (4, 1, 2),
346];
347
348#[cfg(any(test, feature = "example-db"))]
349fn issue_example_problem() -> TimetableDesign {
350    TimetableDesign::new(
351        3,
352        5,
353        5,
354        vec![
355            vec![true, true, true],
356            vec![true, true, false],
357            vec![false, true, true],
358            vec![true, false, true],
359            vec![true, true, true],
360        ],
361        vec![
362            vec![true, true, false],
363            vec![false, true, true],
364            vec![true, false, true],
365            vec![true, true, true],
366            vec![true, true, true],
367        ],
368        vec![
369            vec![1, 0, 1, 0, 0],
370            vec![0, 1, 0, 0, 1],
371            vec![0, 0, 0, 1, 0],
372            vec![0, 0, 0, 0, 1],
373            vec![0, 1, 0, 0, 0],
374        ],
375    )
376}
377
378#[cfg(any(test, feature = "example-db"))]
379fn issue_example_config() -> Vec<Vec<Vec<bool>>> {
380    let problem = issue_example_problem();
381    let mut config = vec![
382        vec![vec![false; problem.num_periods()]; problem.num_tasks()];
383        problem.num_craftsmen()
384    ];
385    for &(craftsman, task, period) in ISSUE_EXAMPLE_ASSIGNMENTS {
386        config[craftsman][task][period] = true;
387    }
388    config
389}
390
391#[cfg(feature = "example-db")]
392pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
393    vec![crate::example_db::specs::ModelExampleSpec {
394        id: "timetable_design",
395        instance: Box::new(issue_example_problem()),
396        optimal_config: serde_json::json!(issue_example_config()),
397        optimal_value: serde_json::json!(true),
398    }]
399}
400
401#[cfg(test)]
402#[path = "../../unit_tests/models/misc/timetable_design.rs"]
403mod tests;