1use 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#[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 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 pub fn num_periods(&self) -> usize {
125 self.num_periods
126 }
127
128 pub fn num_craftsmen(&self) -> usize {
130 self.num_craftsmen
131 }
132
133 pub fn num_tasks(&self) -> usize {
135 self.num_tasks
136 }
137
138 pub fn craftsman_avail(&self) -> &[Vec<bool>] {
140 &self.craftsman_avail
141 }
142
143 pub fn task_avail(&self) -> &[Vec<bool>] {
145 &self.task_avail
146 }
147
148 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;