Skip to main content

problemreductions/rules/
ksatisfiability_timetabledesign.rs

1//! Reduction from KSatisfiability (3-SAT) to TimetableDesign.
2//!
3//! The issue sketch for #486 is not directly implementable against this
4//! repository's `TimetableDesign` model: the sketch relies on pair-specific
5//! availability and per-clause optional work, while the model exposes only
6//! craftsman/task availability sets and exact pairwise requirements.
7//!
8//! This implementation uses a fully specified chain instead:
9//! 1. Eliminate pure literals from the source formula.
10//! 2. Apply Tovey's bounded-occurrence cloning so every remaining variable
11//!    appears at most three times and every clause has length two or three.
12//! 3. Build the explicit bipartite list-edge-coloring instance from Marx's
13//!    outerplanar reduction, padding missing literal-occurrence colors with
14//!    dummy colors when a variable occurs only `1+1`, `2+1`, or `1+2` times.
15//! 4. Compile the list instance to a core  edge-coloring instance with blocked
16//!    colors on auxiliary vertices, following Marx's precoloring gadgets.
17//! 5. Encode blocked colors as dedicated dummy assignments in `TimetableDesign`.
18//!
19//! A timetable witness therefore yields a proper coloring of the core gadget
20//! graph; the colors on the two special variable edges recover the satisfying
21//! truth assignment.
22
23use crate::models::formula::{CNFClause, KSatisfiability};
24use crate::models::misc::TimetableDesign;
25use crate::reduction;
26use crate::rules::sat_helpers::SatVariableAllocator;
27use crate::rules::traits::{ReduceTo, ReductionResult};
28use crate::variant::K3;
29use std::collections::VecDeque;
30
31#[derive(Debug, Clone)]
32struct NormalizedFormula {
33    clauses: Vec<CNFClause>,
34    transformed_to_original: Vec<usize>,
35    pure_assignments: Vec<Option<usize>>,
36    source_num_vars: usize,
37}
38
39#[derive(Debug, Clone)]
40struct CoreGraph {
41    edges: Vec<(usize, usize)>,
42    blocked_colors: Vec<Vec<usize>>,
43}
44
45impl CoreGraph {
46    fn new() -> Self {
47        Self {
48            edges: Vec::new(),
49            blocked_colors: Vec::new(),
50        }
51    }
52
53    fn add_vertex(&mut self) -> usize {
54        let id = self.blocked_colors.len();
55        self.blocked_colors.push(Vec::new());
56        id
57    }
58
59    fn add_edge(&mut self, u: usize, v: usize) -> usize {
60        self.edges.push((u, v));
61        self.edges.len() - 1
62    }
63
64    fn block_color(&mut self, vertex: usize, color: usize) {
65        let blocked = &mut self.blocked_colors[vertex];
66        if !blocked.contains(&color) {
67            blocked.push(color);
68        }
69    }
70}
71
72#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
73#[derive(Debug, Clone)]
74enum EdgeEncoding {
75    Direct {
76        edge: usize,
77        allowed: Vec<usize>,
78    },
79    TwoList {
80        left_outer: usize,
81        middle: usize,
82        right_outer: usize,
83        first: usize,
84        second: usize,
85    },
86}
87
88#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
89#[derive(Debug, Clone)]
90struct VariableEncoding {
91    vb: EdgeEncoding,
92    vd: EdgeEncoding,
93    ab: EdgeEncoding,
94    bc: EdgeEncoding,
95    cd: EdgeEncoding,
96    de: EdgeEncoding,
97    neg2: usize,
98}
99
100#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
101#[derive(Debug, Clone)]
102struct ClauseEncoding {
103    edge: EdgeEncoding,
104}
105
106#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
107#[derive(Debug, Clone)]
108struct ReductionLayout {
109    source_num_vars: usize,
110    num_periods: usize,
111    craftsman_avail: Vec<Vec<bool>>,
112    task_avail: Vec<Vec<bool>>,
113    requirements: Vec<Vec<i64>>,
114    pure_assignments: Vec<Option<usize>>,
115    transformed_to_original: Vec<usize>,
116    normalized_clauses: Vec<CNFClause>,
117    variable_encodings: Vec<VariableEncoding>,
118    clause_encodings: Vec<ClauseEncoding>,
119    edge_pairs: Vec<(usize, usize)>,
120}
121
122/// Result of reducing KSatisfiability<K3> to TimetableDesign.
123#[derive(Debug, Clone)]
124pub struct Reduction3SATToTimetableDesign {
125    target: TimetableDesign,
126    layout: ReductionLayout,
127}
128
129fn literal_var_index(literal: i64) -> usize {
130    usize::try_from(literal.unsigned_abs()).expect("SAT literal magnitude must fit usize") - 1
131}
132
133#[cfg(any(test, feature = "example-db"))]
134fn evaluate_clause(clause: &CNFClause, assignment: &[bool]) -> bool {
135    clause.literals.iter().any(|&literal| {
136        let value = assignment[literal_var_index(literal)];
137        if literal > 0 {
138            value
139        } else {
140            !value
141        }
142    })
143}
144
145fn eliminate_pure_literals(source: &KSatisfiability<K3>) -> (Vec<CNFClause>, Vec<Option<usize>>) {
146    let mut clauses = source.clauses().to_vec();
147    let mut assignments = vec![None; source.num_vars()];
148
149    loop {
150        let mut positive = vec![0usize; source.num_vars()];
151        let mut negative = vec![0usize; source.num_vars()];
152        for clause in &clauses {
153            for &literal in &clause.literals {
154                let var = literal_var_index(literal);
155                if literal > 0 {
156                    positive[var] += 1;
157                } else {
158                    negative[var] += 1;
159                }
160            }
161        }
162
163        let mut changed = false;
164        for var in 0..source.num_vars() {
165            if assignments[var].is_some() {
166                continue;
167            }
168            match (positive[var] > 0, negative[var] > 0) {
169                (true, false) => {
170                    assignments[var] = Some(1);
171                    changed = true;
172                }
173                (false, true) => {
174                    assignments[var] = Some(0);
175                    changed = true;
176                }
177                _ => {}
178            }
179        }
180
181        if !changed {
182            break;
183        }
184
185        clauses.retain(|clause| {
186            !clause.literals.iter().any(|&literal| {
187                let var = literal_var_index(literal);
188                match assignments[var] {
189                    Some(1) => literal > 0,
190                    Some(0) => literal < 0,
191                    None => false,
192                    Some(_) => unreachable!(),
193                }
194            })
195        });
196    }
197
198    (clauses, assignments)
199}
200
201fn normalize_formula(source: &KSatisfiability<K3>) -> NormalizedFormula {
202    let (mut clauses, pure_assignments) = eliminate_pure_literals(source);
203    let source_num_vars = source.num_vars();
204    let mut transformed_to_original = Vec::new();
205    let mut variables = SatVariableAllocator::new(
206        "KSatisfiability -> TimetableDesign normalization",
207        source_num_vars,
208    )
209    .unwrap_or_else(|message| panic!("{message}"));
210
211    for original_var in 1..=source_num_vars {
212        let mut occurrences = Vec::new();
213        for (clause_idx, clause) in clauses.iter().enumerate() {
214            for (lit_idx, &literal) in clause.literals.iter().enumerate() {
215                if literal_var_index(literal) + 1 == original_var {
216                    occurrences.push((clause_idx, lit_idx, literal > 0));
217                }
218            }
219        }
220
221        if occurrences.is_empty() {
222            continue;
223        }
224
225        if occurrences.len() <= 3 {
226            let replacement = variables
227                .allocate()
228                .unwrap_or_else(|message| panic!("{message}"));
229            transformed_to_original.push(original_var - 1);
230            for (clause_idx, lit_idx, is_positive) in occurrences {
231                clauses[clause_idx].literals[lit_idx] = if is_positive {
232                    replacement
233                } else {
234                    -replacement
235                };
236            }
237            continue;
238        }
239
240        let replacements = variables
241            .allocate_many(occurrences.len())
242            .unwrap_or_else(|message| panic!("{message}"));
243        transformed_to_original.extend(std::iter::repeat_n(original_var - 1, replacements.len()));
244
245        for ((clause_idx, lit_idx, is_positive), replacement) in
246            occurrences.into_iter().zip(replacements.iter().copied())
247        {
248            clauses[clause_idx].literals[lit_idx] = if is_positive {
249                replacement
250            } else {
251                -replacement
252            };
253        }
254
255        for idx in 0..replacements.len() {
256            let current = replacements[idx];
257            let next = replacements[(idx + 1) % replacements.len()];
258            clauses.push(CNFClause::new(vec![current, -next]));
259        }
260    }
261
262    for clause in &mut clauses {
263        for literal in &mut clause.literals {
264            let sign = if *literal < 0 { -1 } else { 1 };
265            let temp_var = usize::try_from(literal.unsigned_abs())
266                .expect("SAT literal magnitude must fit usize");
267            debug_assert!(
268                temp_var > source_num_vars,
269                "all residual literals should have been replaced by transformed variables"
270            );
271            let compact_var = temp_var - source_num_vars;
272            *literal = sign
273                * i64::try_from(compact_var)
274                    .expect("checked normalized SAT variable count fits i64");
275        }
276    }
277
278    for transformed_var in 1..=transformed_to_original.len() {
279        let mut positive = 0usize;
280        let mut negative = 0usize;
281        for clause in &clauses {
282            for &literal in &clause.literals {
283                if literal_var_index(literal) + 1 == transformed_var {
284                    if literal > 0 {
285                        positive += 1;
286                    } else {
287                        negative += 1;
288                    }
289                }
290            }
291        }
292        debug_assert!(
293            positive <= 2,
294            "normalized variable {transformed_var} has {positive} positive occurrences"
295        );
296        debug_assert!(
297            negative <= 2,
298            "normalized variable {transformed_var} has {negative} negative occurrences"
299        );
300        debug_assert!(
301            positive > 0 && negative > 0,
302            "pure literals should have been eliminated before gadget construction"
303        );
304    }
305
306    NormalizedFormula {
307        clauses,
308        transformed_to_original,
309        pure_assignments,
310        source_num_vars,
311    }
312}
313
314#[cfg(any(test, feature = "example-db"))]
315fn choose_clause_edge_color(
316    clause: &CNFClause,
317    assignment: &[bool],
318    colors: &[usize],
319) -> Option<usize> {
320    clause
321        .literals
322        .iter()
323        .zip(colors.iter().copied())
324        .find_map(|(&literal, color)| {
325            let value = assignment[literal_var_index(literal)];
326            let satisfied = if literal > 0 { value } else { !value };
327            satisfied.then_some(color)
328        })
329}
330
331fn add_two_list_edge(
332    graph: &mut CoreGraph,
333    all_colors: &[usize],
334    x: usize,
335    y: usize,
336    first: usize,
337    second: usize,
338) -> EdgeEncoding {
339    let x_prime = graph.add_vertex();
340    let y_prime = graph.add_vertex();
341
342    let left_outer = graph.add_edge(x, x_prime);
343    let middle = graph.add_edge(x_prime, y_prime);
344    let right_outer = graph.add_edge(y_prime, y);
345
346    for &color in all_colors {
347        if color != first && color != second {
348            graph.block_color(x_prime, color);
349            graph.block_color(y_prime, color);
350        }
351    }
352
353    EdgeEncoding::TwoList {
354        left_outer,
355        middle,
356        right_outer,
357        first,
358        second,
359    }
360}
361
362fn add_direct_clause_edge(
363    graph: &mut CoreGraph,
364    all_colors: &[usize],
365    x: usize,
366    y: usize,
367    allowed: Vec<usize>,
368) -> EdgeEncoding {
369    let edge = graph.add_edge(x, y);
370    for &color in all_colors {
371        if !allowed.contains(&color) {
372            graph.block_color(y, color);
373        }
374    }
375    EdgeEncoding::Direct { edge, allowed }
376}
377
378fn core_edge_color(solution: &[Vec<Vec<bool>>], pair: (usize, usize), num_periods: usize) -> usize {
379    (0..num_periods)
380        .find(|&period| solution[pair.0][pair.1][period])
381        .expect("each required pair should be scheduled exactly once")
382}
383
384#[cfg(any(test, feature = "example-db"))]
385fn encode_edge_color(colors: &mut [Option<usize>], encoding: &EdgeEncoding, chosen_color: usize) {
386    match encoding {
387        EdgeEncoding::Direct { edge, allowed } => {
388            assert!(
389                allowed.contains(&chosen_color),
390                "chosen color {chosen_color} must belong to direct edge list {allowed:?}"
391            );
392            colors[*edge] = Some(chosen_color);
393        }
394        EdgeEncoding::TwoList {
395            left_outer,
396            middle,
397            right_outer,
398            first,
399            second,
400        } => {
401            assert!(
402                chosen_color == *first || chosen_color == *second,
403                "chosen color {chosen_color} must belong to two-list edge {{{first}, {second}}}"
404            );
405            let other = if chosen_color == *first {
406                *second
407            } else {
408                *first
409            };
410            colors[*left_outer] = Some(chosen_color);
411            colors[*right_outer] = Some(chosen_color);
412            colors[*middle] = Some(other);
413        }
414    }
415}
416
417#[cfg(any(test, feature = "example-db"))]
418fn edge_from_assignment(encoding: &EdgeEncoding, choose_first: bool) -> usize {
419    match encoding {
420        EdgeEncoding::Direct { allowed, .. } => allowed[usize::from(!choose_first)],
421        EdgeEncoding::TwoList { first, second, .. } => {
422            if choose_first {
423                *first
424            } else {
425                *second
426            }
427        }
428    }
429}
430
431fn bipartition(graph: &CoreGraph) -> Vec<bool> {
432    let mut side = vec![None; graph.blocked_colors.len()];
433    let mut adjacency = vec![Vec::new(); graph.blocked_colors.len()];
434    for &(u, v) in &graph.edges {
435        adjacency[u].push(v);
436        adjacency[v].push(u);
437    }
438
439    for start in 0..graph.blocked_colors.len() {
440        if side[start].is_some() {
441            continue;
442        }
443        side[start] = Some(false);
444        let mut queue = VecDeque::from([start]);
445        while let Some(vertex) = queue.pop_front() {
446            let current = side[vertex].expect("vertex has been assigned a side");
447            for &next in &adjacency[vertex] {
448                match side[next] {
449                    Some(existing) => {
450                        assert_ne!(existing, current, "core graph must remain bipartite");
451                    }
452                    None => {
453                        side[next] = Some(!current);
454                        queue.push_back(next);
455                    }
456                }
457            }
458        }
459    }
460
461    side.into_iter()
462        .map(|entry| entry.expect("every vertex should receive a bipartition side"))
463        .collect()
464}
465
466fn build_layout(source: &KSatisfiability<K3>) -> ReductionLayout {
467    let normalized = normalize_formula(source);
468    let num_transformed_vars = normalized.transformed_to_original.len();
469    if num_transformed_vars == 0 && normalized.clauses.is_empty() {
470        return ReductionLayout {
471            source_num_vars: normalized.source_num_vars,
472            num_periods: 1,
473            craftsman_avail: Vec::new(),
474            task_avail: Vec::new(),
475            requirements: Vec::new(),
476            pure_assignments: normalized.pure_assignments,
477            transformed_to_original: normalized.transformed_to_original,
478            normalized_clauses: normalized.clauses,
479            variable_encodings: Vec::new(),
480            clause_encodings: Vec::new(),
481            edge_pairs: Vec::new(),
482        };
483    }
484
485    let num_periods = 4 * num_transformed_vars.max(1);
486    let all_colors: Vec<usize> = (0..num_periods).collect();
487
488    let mut occurrences_by_var: Vec<Vec<(usize, usize, bool)>> =
489        vec![Vec::new(); num_transformed_vars];
490    for (clause_idx, clause) in normalized.clauses.iter().enumerate() {
491        for (lit_idx, &literal) in clause.literals.iter().enumerate() {
492            occurrences_by_var[literal_var_index(literal)].push((clause_idx, lit_idx, literal > 0));
493        }
494    }
495
496    let mut clause_literal_colors: Vec<Vec<usize>> = normalized
497        .clauses
498        .iter()
499        .map(|clause| vec![usize::MAX; clause.literals.len()])
500        .collect();
501
502    let mut variable_colors = Vec::with_capacity(num_transformed_vars);
503    for (index, occurrences) in occurrences_by_var.iter().enumerate() {
504        let base = 4 * index;
505        let neg2 = base;
506        let neg1 = base + 1;
507        let pos2 = base + 2;
508        let pos1 = base + 3;
509
510        let mut positive_occurrences = Vec::new();
511        let mut negative_occurrences = Vec::new();
512        for &(clause_idx, lit_idx, is_positive) in occurrences {
513            if is_positive {
514                positive_occurrences.push((clause_idx, lit_idx));
515            } else {
516                negative_occurrences.push((clause_idx, lit_idx));
517            }
518        }
519
520        if let Some(&(clause_idx, lit_idx)) = positive_occurrences.first() {
521            clause_literal_colors[clause_idx][lit_idx] = pos1;
522        }
523        if let Some(&(clause_idx, lit_idx)) = positive_occurrences.get(1) {
524            clause_literal_colors[clause_idx][lit_idx] = pos2;
525        }
526        if let Some(&(clause_idx, lit_idx)) = negative_occurrences.first() {
527            clause_literal_colors[clause_idx][lit_idx] = neg1;
528        }
529        if let Some(&(clause_idx, lit_idx)) = negative_occurrences.get(1) {
530            clause_literal_colors[clause_idx][lit_idx] = neg2;
531        }
532
533        variable_colors.push((pos1, pos2, neg1, neg2));
534    }
535
536    let mut graph = CoreGraph::new();
537    let center = graph.add_vertex();
538    let mut variable_encodings = Vec::with_capacity(num_transformed_vars);
539
540    for &(pos1, pos2, neg1, neg2) in &variable_colors {
541        let a = graph.add_vertex();
542        let b = graph.add_vertex();
543        let c = graph.add_vertex();
544        let d = graph.add_vertex();
545        let e = graph.add_vertex();
546
547        let ab = add_two_list_edge(&mut graph, &all_colors, a, b, pos1, neg2);
548        let bc = add_two_list_edge(&mut graph, &all_colors, b, c, pos2, pos1);
549        let cd = add_two_list_edge(&mut graph, &all_colors, c, d, pos1, pos2);
550        let de = add_two_list_edge(&mut graph, &all_colors, d, e, pos2, neg1);
551        let vb = add_two_list_edge(&mut graph, &all_colors, center, b, neg2, pos2);
552        let vd = add_two_list_edge(&mut graph, &all_colors, center, d, neg1, pos1);
553
554        variable_encodings.push(VariableEncoding {
555            vb,
556            vd,
557            ab,
558            bc,
559            cd,
560            de,
561            neg2,
562        });
563    }
564
565    let mut clause_encodings = Vec::with_capacity(normalized.clauses.len());
566    for (clause_idx, _clause) in normalized.clauses.iter().enumerate() {
567        let clause_vertex = graph.add_vertex();
568        let colors = clause_literal_colors[clause_idx].clone();
569        debug_assert!(colors.iter().all(|&color| color != usize::MAX));
570
571        let edge = match colors.len() {
572            1 => add_direct_clause_edge(&mut graph, &all_colors, center, clause_vertex, colors),
573            2 => add_two_list_edge(
574                &mut graph,
575                &all_colors,
576                center,
577                clause_vertex,
578                colors[0],
579                colors[1],
580            ),
581            3 => add_direct_clause_edge(&mut graph, &all_colors, center, clause_vertex, colors),
582            len => panic!("expected clause size 1, 2, or 3 after normalization, got {len}"),
583        };
584
585        clause_encodings.push(ClauseEncoding { edge });
586    }
587
588    let side = bipartition(&graph);
589    let mut vertex_to_craftsman = vec![None; graph.blocked_colors.len()];
590    let mut vertex_to_task = vec![None; graph.blocked_colors.len()];
591
592    let mut num_craftsmen = 0usize;
593    let mut num_tasks = 0usize;
594    for (vertex, is_task_side) in side.iter().copied().enumerate() {
595        if is_task_side {
596            vertex_to_task[vertex] = Some(num_tasks);
597            num_tasks += 1;
598        } else {
599            vertex_to_craftsman[vertex] = Some(num_craftsmen);
600            num_craftsmen += 1;
601        }
602    }
603
604    let mut craftsman_avail = vec![vec![true; num_periods]; num_craftsmen];
605    let mut task_avail = vec![vec![true; num_periods]; num_tasks];
606    let mut requirements = vec![vec![0i64; num_tasks]; num_craftsmen];
607    let mut edge_pairs = vec![(usize::MAX, usize::MAX); graph.edges.len()];
608
609    for (edge_idx, &(u, v)) in graph.edges.iter().enumerate() {
610        let (craft, task) = if !side[u] {
611            (
612                vertex_to_craftsman[u].expect("left vertex has craftsman index"),
613                vertex_to_task[v].expect("right vertex has task index"),
614            )
615        } else {
616            (
617                vertex_to_craftsman[v].expect("left vertex has craftsman index"),
618                vertex_to_task[u].expect("right vertex has task index"),
619            )
620        };
621        requirements[craft][task] = 1;
622        edge_pairs[edge_idx] = (craft, task);
623    }
624
625    // A blocked color on a core-graph vertex translates directly to
626    // removing that period from the corresponding craftsman/task's
627    // availability. Semantically equivalent to adding a dedicated
628    // blocker pair (same `*_busy` slot gets consumed), but avoids the
629    // O(L²) blowup in craftsman/task counts.
630    for (vertex, blocked_colors) in graph.blocked_colors.iter().enumerate() {
631        for &color in blocked_colors {
632            if side[vertex] {
633                let task = vertex_to_task[vertex].expect("right vertex has task index");
634                task_avail[task][color] = false;
635            } else {
636                let craft = vertex_to_craftsman[vertex].expect("left vertex has craftsman index");
637                craftsman_avail[craft][color] = false;
638            }
639        }
640    }
641
642    ReductionLayout {
643        source_num_vars: normalized.source_num_vars,
644        num_periods,
645        craftsman_avail,
646        task_avail,
647        requirements,
648        pure_assignments: normalized.pure_assignments,
649        transformed_to_original: normalized.transformed_to_original,
650        normalized_clauses: normalized.clauses,
651        variable_encodings,
652        clause_encodings,
653        edge_pairs,
654    }
655}
656
657impl Reduction3SATToTimetableDesign {
658    #[cfg(any(test, feature = "example-db"))]
659    fn construct_target_solution(&self, source_assignment: &[bool]) -> Option<Vec<Vec<Vec<bool>>>> {
660        if source_assignment.len() != self.layout.source_num_vars {
661            return None;
662        }
663        if self
664            .layout
665            .pure_assignments
666            .iter()
667            .enumerate()
668            .any(|(var, fixed)| fixed.is_some_and(|value| source_assignment[var] != (value == 1)))
669        {
670            return None;
671        }
672
673        let transformed_assignment: Vec<bool> = self
674            .layout
675            .transformed_to_original
676            .iter()
677            .map(|&original| source_assignment[original])
678            .collect();
679
680        if self
681            .layout
682            .normalized_clauses
683            .iter()
684            .any(|clause| !evaluate_clause(clause, &transformed_assignment))
685        {
686            return None;
687        }
688
689        let mut colors = vec![None; self.layout.edge_pairs.len()];
690
691        for (transformed_var, encoding) in self.layout.variable_encodings.iter().enumerate() {
692            let choose_first = transformed_assignment[transformed_var];
693            for edge in [
694                &encoding.ab,
695                &encoding.bc,
696                &encoding.cd,
697                &encoding.de,
698                &encoding.vb,
699                &encoding.vd,
700            ] {
701                let chosen = edge_from_assignment(edge, choose_first);
702                encode_edge_color(&mut colors, edge, chosen);
703            }
704        }
705
706        for (clause, encoding) in self
707            .layout
708            .normalized_clauses
709            .iter()
710            .zip(self.layout.clause_encodings.iter())
711        {
712            let allowed = match &encoding.edge {
713                EdgeEncoding::Direct { allowed, .. } => allowed.clone(),
714                EdgeEncoding::TwoList { first, second, .. } => vec![*first, *second],
715            };
716            let chosen = choose_clause_edge_color(clause, &transformed_assignment, &allowed)?;
717            encode_edge_color(&mut colors, &encoding.edge, chosen);
718        }
719
720        if colors.iter().any(Option::is_none) {
721            return None;
722        }
723
724        let num_tasks = self.target.num_tasks();
725        let num_periods = self.target.num_periods();
726        let mut config =
727            vec![vec![vec![false; num_periods]; num_tasks]; self.target.num_craftsmen()];
728
729        for (edge_idx, color) in colors.into_iter().enumerate() {
730            let (craft, task) = self.layout.edge_pairs[edge_idx];
731            let color = color.expect("all core edges should be colored");
732            config[craft][task][color] = true;
733        }
734
735        Some(config)
736    }
737}
738
739impl ReductionResult for Reduction3SATToTimetableDesign {
740    type Source = KSatisfiability<K3>;
741    type Target = TimetableDesign;
742
743    fn target_problem(&self) -> &Self::Target {
744        &self.target
745    }
746
747    fn extract_solution(
748        &self,
749        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
750    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
751        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
752
753        Ok({
754            let num_periods = self.target.num_periods();
755
756            let mut transformed_assignment = vec![false; self.layout.transformed_to_original.len()];
757            for (index, encoding) in self.layout.variable_encodings.iter().enumerate() {
758                let vb_pair = match &encoding.vb {
759                    EdgeEncoding::Direct { edge, .. } => self.layout.edge_pairs[*edge],
760                    EdgeEncoding::TwoList { left_outer, .. } => self.layout.edge_pairs[*left_outer],
761                };
762                let vb_color = core_edge_color(target_solution, vb_pair, num_periods);
763                transformed_assignment[index] = vb_color == encoding.neg2;
764            }
765
766            let mut source_assignment = vec![false; self.layout.source_num_vars];
767            for (var, fixed) in self.layout.pure_assignments.iter().copied().enumerate() {
768                if let Some(value) = fixed {
769                    source_assignment[var] = value == 1;
770                }
771            }
772
773            let mut seen_transformed = vec![false; self.layout.source_num_vars];
774            for (value, &original_var) in transformed_assignment
775                .iter()
776                .zip(self.layout.transformed_to_original.iter())
777            {
778                if !seen_transformed[original_var] {
779                    source_assignment[original_var] = *value;
780                    seen_transformed[original_var] = true;
781                }
782            }
783
784            source_assignment
785        })
786    }
787}
788
789#[reduction(
790    transform = upper_bound {
791        num_periods = "4 * num_literals",
792        num_craftsmen = "24 * num_literals + 1",
793        num_tasks = "24 * num_literals + 1",
794    }
795)]
796impl ReduceTo<TimetableDesign> for KSatisfiability<K3> {
797    type Result = Reduction3SATToTimetableDesign;
798
799    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
800        let layout = build_layout(self);
801        let target = TimetableDesign::new(
802            layout.num_periods,
803            layout.craftsman_avail.len(),
804            layout.task_avail.len(),
805            layout.craftsman_avail.clone(),
806            layout.task_avail.clone(),
807            layout.requirements.clone(),
808        );
809
810        Ok(Reduction3SATToTimetableDesign { target, layout })
811    }
812}
813
814#[cfg(any(test, feature = "example-db"))]
815#[allow(dead_code)]
816pub(super) fn construct_timetable_from_assignment(
817    target: &TimetableDesign,
818    assignment: &[bool],
819    source: &KSatisfiability<K3>,
820) -> Option<Vec<Vec<Vec<bool>>>> {
821    let reduction =
822        ReduceTo::<TimetableDesign>::reduce_to(source).expect("reduction should succeed");
823    if reduction.target_problem().num_periods() != target.num_periods()
824        || reduction.target_problem().num_craftsmen() != target.num_craftsmen()
825        || reduction.target_problem().num_tasks() != target.num_tasks()
826        || reduction.target_problem().craftsman_avail() != target.craftsman_avail()
827        || reduction.target_problem().task_avail() != target.task_avail()
828        || reduction.target_problem().requirements() != target.requirements()
829    {
830        return None;
831    }
832    reduction.construct_target_solution(assignment)
833}
834
835#[cfg(feature = "example-db")]
836pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
837    use crate::export::SolutionPair;
838    use crate::models::formula::CNFClause;
839
840    vec![crate::example_db::specs::RuleExampleSpec {
841        id: "ksatisfiability_to_timetabledesign",
842        build: || {
843            let source = KSatisfiability::<K3>::new(
844                3,
845                vec![
846                    CNFClause::new(vec![1, 2, 3]),
847                    CNFClause::new(vec![-1, -2, -3]),
848                ],
849            );
850            let reduction =
851                ReduceTo::<TimetableDesign>::reduce_to(&source).expect("reduction should succeed");
852            let source_config = vec![true, false, false];
853            let target_config = reduction
854                .construct_target_solution(&source_config)
855                .expect("canonical satisfying assignment should lift to timetable");
856
857            crate::example_db::specs::rule_example_with_witness::<_, TimetableDesign>(
858                source,
859                SolutionPair {
860                    source_config: serde_json::to_value(source_config)
861                        .expect("solution serialization must succeed"),
862                    target_config: serde_json::to_value(target_config)
863                        .expect("solution serialization must succeed"),
864                },
865            )
866        },
867    }]
868}
869
870#[cfg(test)]
871#[path = "../unit_tests/rules/ksatisfiability_timetabledesign.rs"]
872mod tests;