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