1use 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#[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 num_periods: usize,
43 num_craftsmen: usize,
45 num_tasks: usize,
47 craftsman_avail: Vec<Vec<bool>>,
49 task_avail: Vec<Vec<bool>>,
51 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 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 pub fn num_periods(&self) -> usize {
211 self.num_periods
212 }
213
214 pub fn num_craftsmen(&self) -> usize {
216 self.num_craftsmen
217 }
218
219 pub fn num_tasks(&self) -> usize {
221 self.num_tasks
222 }
223
224 pub fn craftsman_avail(&self) -> &[Vec<bool>] {
226 &self.craftsman_avail
227 }
228
229 pub fn task_avail(&self) -> &[Vec<bool>] {
231 &self.task_avail
232 }
233
234 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;