1use crate::cdcl::Lit;
42use crate::proof::Perm;
43use std::collections::{BTreeSet, HashMap, HashSet};
44
45pub type CanonClauses = Vec<Vec<(u32, bool)>>;
48
49pub type Twist = Vec<(u32, u32, bool)>;
52
53pub fn canon(clauses: &[Vec<Lit>]) -> CanonClauses {
55 canon_raw(
56 &clauses
57 .iter()
58 .map(|c| c.iter().map(|l| (l.var(), l.is_positive())).collect())
59 .collect::<Vec<_>>(),
60 )
61}
62
63pub fn canon_raw(clauses: &[Vec<(u32, bool)>]) -> CanonClauses {
65 let mut out: CanonClauses = clauses
66 .iter()
67 .map(|c| {
68 let mut lits = c.clone();
69 lits.sort_unstable();
70 lits.dedup();
71 lits
72 })
73 .collect();
74 out.sort();
75 out.dedup();
76 out
77}
78
79pub fn cofactor(clauses: &CanonClauses, x: u32, b: bool) -> CanonClauses {
82 canon_raw(
83 &clauses
84 .iter()
85 .filter(|c| !c.iter().any(|&(v, pos)| v == x && pos == b))
86 .map(|c| c.iter().copied().filter(|&(v, _)| v != x).collect())
87 .collect::<Vec<_>>(),
88 )
89}
90
91pub fn is_leaf(clauses: &CanonClauses) -> bool {
93 clauses.iter().any(|c| c.is_empty())
94}
95
96#[derive(Clone, Debug)]
102pub enum Node {
103 Leaf(CanonClauses),
105 Internal { clauses: CanonClauses, var: u32, lo: usize, hi: usize },
107}
108
109pub fn distinct_cofactor_dag(n: usize, clauses: &CanonClauses) -> Option<(usize, Vec<Node>)> {
114 let mut nodes: Vec<Node> = Vec::new();
115 let mut memo: HashMap<(usize, CanonClauses), Option<usize>> = HashMap::new();
116 fn go(
117 depth: usize,
118 n: usize,
119 clauses: CanonClauses,
120 nodes: &mut Vec<Node>,
121 memo: &mut HashMap<(usize, CanonClauses), Option<usize>>,
122 ) -> Option<usize> {
123 if let Some(&hit) = memo.get(&(depth, clauses.clone())) {
124 return hit;
125 }
126 let result = if clauses.iter().any(|c| c.is_empty()) {
127 let id = nodes.len();
128 nodes.push(Node::Leaf(clauses.clone()));
129 Some(id)
130 } else if depth == n {
131 None
132 } else {
133 let x = depth as u32;
134 let lo = go(depth + 1, n, cofactor(&clauses, x, false), nodes, memo);
135 let hi = go(depth + 1, n, cofactor(&clauses, x, true), nodes, memo);
136 match (lo, hi) {
137 (Some(lo), Some(hi)) => {
138 let id = nodes.len();
139 nodes.push(Node::Internal { clauses: clauses.clone(), var: x, lo, hi });
140 Some(id)
141 }
142 _ => None,
143 }
144 };
145 memo.insert((depth, clauses), result);
146 result
147 }
148 let root = go(0, n, clauses.clone(), &mut nodes, &mut memo)?;
149 Some((root, nodes))
150}
151
152pub fn check_distinct_dag(root: usize, nodes: &[Node], expected: &CanonClauses) -> bool {
156 match &nodes[root] {
157 Node::Leaf(c) | Node::Internal { clauses: c, .. } if c != expected => return false,
158 _ => {}
159 }
160 nodes.iter().all(|node| match node {
161 Node::Leaf(c) => c.iter().any(|cl| cl.is_empty()),
162 Node::Internal { clauses, var, lo, hi } => {
163 let want_lo = cofactor(clauses, *var, false);
164 let want_hi = cofactor(clauses, *var, true);
165 let got = |id: usize| match &nodes[id] {
166 Node::Leaf(c) => c,
167 Node::Internal { clauses, .. } => clauses,
168 };
169 *got(*lo) == want_lo && *got(*hi) == want_hi
170 }
171 })
172}
173
174pub fn level_widths(n: usize, root: &CanonClauses) -> Vec<usize> {
177 let mut levels: Vec<HashSet<CanonClauses>> = vec![HashSet::new(); n + 1];
178 let mut visited: HashSet<(usize, CanonClauses)> = HashSet::new();
179 fn go(
180 depth: usize,
181 n: usize,
182 clauses: CanonClauses,
183 levels: &mut Vec<HashSet<CanonClauses>>,
184 visited: &mut HashSet<(usize, CanonClauses)>,
185 ) {
186 if !visited.insert((depth, clauses.clone())) {
187 return;
188 }
189 levels[depth].insert(clauses.clone());
190 if clauses.iter().any(|c| c.is_empty()) || depth == n {
191 return;
192 }
193 let x = depth as u32;
194 go(depth + 1, n, cofactor(&clauses, x, false), levels, visited);
195 go(depth + 1, n, cofactor(&clauses, x, true), levels, visited);
196 }
197 go(0, n, root.clone(), &mut levels, &mut visited);
198 levels.iter().map(|s| s.len()).collect()
199}
200
201pub fn cofactor_set(n: usize, clauses: &CanonClauses) -> BTreeSet<(usize, CanonClauses)> {
207 fn go(depth: usize, n: usize, clauses: CanonClauses, set: &mut BTreeSet<(usize, CanonClauses)>) {
208 if !set.insert((depth, clauses.clone())) {
209 return;
210 }
211 if is_leaf(&clauses) || depth == n {
212 return;
213 }
214 let x = depth as u32;
215 go(depth + 1, n, cofactor(&clauses, x, false), set);
216 go(depth + 1, n, cofactor(&clauses, x, true), set);
217 }
218 let mut set = BTreeSet::new();
219 go(0, n, clauses.clone(), &mut set);
220 set
221}
222
223pub fn distinct_width(n: usize, clauses: &CanonClauses) -> usize {
226 cofactor_set(n, clauses).len()
227}
228
229pub fn quotient_class_count<C: Congruence + ?Sized>(
236 n: usize,
237 clauses: &CanonClauses,
238 cong: &C,
239) -> usize {
240 cofactor_set(n, clauses)
241 .into_iter()
242 .map(|(d, c)| (d, cong.canonicalize(&c).0))
243 .collect::<BTreeSet<_>>()
244 .len()
245}
246
247pub trait Congruence {
257 fn name(&self) -> &str;
259 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist);
262}
263
264pub fn normalize(clauses: &CanonClauses) -> (CanonClauses, Vec<(u32, u32)>) {
267 let mut cur = clauses.clone();
268 let mut total: HashMap<u32, u32> = HashMap::new();
269 for c in clauses.iter().flatten() {
270 total.entry(c.0).or_insert(c.0);
271 }
272 for _ in 0..3 {
273 let mut next_name: u32 = 0;
274 let mut ren: HashMap<u32, u32> = HashMap::new();
275 for c in &cur {
276 for &(v, _) in c {
277 ren.entry(v).or_insert_with(|| {
278 let x = next_name;
279 next_name += 1;
280 x
281 });
282 }
283 }
284 let renamed: Vec<Vec<(u32, bool)>> =
285 cur.iter().map(|c| c.iter().map(|&(v, p)| (ren[&v], p)).collect()).collect();
286 let renamed = canon_raw(&renamed);
287 for (_, tgt) in total.iter_mut() {
288 if let Some(&t2) = ren.get(tgt) {
289 *tgt = t2;
290 }
291 }
292 if renamed == cur {
293 break;
294 }
295 cur = renamed;
296 }
297 (cur.clone(), total.into_iter().collect())
298}
299
300pub fn apply_twist(clauses: &CanonClauses, twist: &Twist) -> Option<CanonClauses> {
302 let map: HashMap<u32, (u32, bool)> = twist.iter().map(|&(a, b, f)| (a, (b, f))).collect();
303 let mut out = Vec::new();
304 for c in clauses {
305 let mut nc = Vec::new();
306 for &(v, pos) in c {
307 let &(v2, f) = map.get(&v)?;
308 nc.push((v2, pos ^ f));
309 }
310 out.push(nc);
311 }
312 Some(canon_raw(&out))
313}
314
315pub fn group_canon(clauses: &CanonClauses, group: &[Perm]) -> (CanonClauses, Twist) {
319 let mut best: Option<(CanonClauses, Twist)> = None;
320 for g in group {
321 let mapped: Vec<Vec<(u32, bool)>> = clauses
322 .iter()
323 .map(|c| {
324 c.iter()
325 .map(|&(v, pos)| {
326 let img = g.apply(Lit::new(v, pos));
327 (img.var(), img.is_positive())
328 })
329 .collect()
330 })
331 .collect();
332 let mapped = canon_raw(&mapped);
333 let (normed, ren) = normalize(&mapped);
334 let ren_map: HashMap<u32, u32> = ren.into_iter().collect();
335 let twist: Twist = clauses
336 .iter()
337 .flatten()
338 .map(|&(v, _)| {
339 let img = g.apply(Lit::pos(v));
340 (v, ren_map[&img.var()], !img.is_positive())
341 })
342 .collect::<BTreeSet<_>>()
343 .into_iter()
344 .collect();
345 if best.as_ref().map_or(true, |(b, _)| normed < *b) {
346 best = Some((normed, twist));
347 }
348 }
349 best.unwrap_or_else(|| (clauses.clone(), Vec::new()))
350}
351
352pub fn iso_canon(clauses: &CanonClauses, cap: usize) -> (CanonClauses, Twist) {
357 let live: Vec<u32> = clauses
358 .iter()
359 .flatten()
360 .map(|&(v, _)| v)
361 .collect::<BTreeSet<_>>()
362 .into_iter()
363 .collect();
364 let k = live.len();
365 if k == 0 {
366 return (clauses.clone(), Vec::new());
367 }
368 if k > cap {
369 let (normed, ren) = normalize(clauses);
370 let twist: Twist = ren
371 .into_iter()
372 .map(|(a, b)| (a, b, false))
373 .collect::<BTreeSet<_>>()
374 .into_iter()
375 .collect();
376 return (normed, twist);
377 }
378 let mut best: Option<(CanonClauses, Twist)> = None;
379 for perm in permutations(k) {
380 for flip_mask in 0u32..(1u32 << k) {
381 let map: HashMap<u32, (u32, bool)> = (0..k)
382 .map(|i| (live[i], (perm[i] as u32, (flip_mask >> i) & 1 == 1)))
383 .collect();
384 let mapped: Vec<Vec<(u32, bool)>> = clauses
385 .iter()
386 .map(|c| {
387 c.iter()
388 .map(|&(v, p)| {
389 let (v2, f) = map[&v];
390 (v2, p ^ f)
391 })
392 .collect()
393 })
394 .collect();
395 let mapped = canon_raw(&mapped);
396 if best.as_ref().map_or(true, |(b, _)| mapped < *b) {
397 let twist: Twist = live
398 .iter()
399 .map(|&v| {
400 let (v2, f) = map[&v];
401 (v, v2, f)
402 })
403 .collect();
404 best = Some((mapped, twist));
405 }
406 }
407 }
408 best.unwrap()
409}
410
411pub fn unit_propagate(clauses: &CanonClauses) -> CanonClauses {
415 let mut cur = clauses.clone();
416 while let Some(&[(v, p)]) = cur.iter().find(|c| c.len() == 1).map(|c| c.as_slice()) {
417 cur = canon_raw(
418 &cur.iter()
419 .filter(|c| !c.iter().any(|&(vv, pp)| vv == v && pp == p))
420 .map(|c| c.iter().copied().filter(|&(vv, _)| vv != v).collect())
421 .collect::<Vec<_>>(),
422 );
423 if cur.iter().any(|c| c.is_empty()) {
424 break;
425 }
426 }
427 cur
428}
429
430fn pure_eliminate(clauses: &CanonClauses) -> CanonClauses {
433 let mut pos: HashSet<u32> = HashSet::new();
434 let mut neg: HashSet<u32> = HashSet::new();
435 for &(v, p) in clauses.iter().flatten() {
436 if p {
437 pos.insert(v);
438 } else {
439 neg.insert(v);
440 }
441 }
442 let pure: HashSet<u32> = pos.symmetric_difference(&neg).copied().collect();
443 if pure.is_empty() {
444 return clauses.clone();
445 }
446 canon_raw(
447 &clauses
448 .iter()
449 .filter(|c| !c.iter().any(|&(v, _)| pure.contains(&v)))
450 .cloned()
451 .collect::<Vec<_>>(),
452 )
453}
454
455fn subsume(clauses: &CanonClauses) -> CanonClauses {
458 canon_raw(
459 &clauses
460 .iter()
461 .filter(|c| {
462 let cset: BTreeSet<(u32, bool)> = c.iter().copied().collect();
463 !clauses.iter().any(|d| d.len() < c.len() && d.iter().all(|l| cset.contains(l)))
464 })
465 .cloned()
466 .collect::<Vec<_>>(),
467 )
468}
469
470pub fn reduce(clauses: &CanonClauses) -> CanonClauses {
474 let mut cur = clauses.clone();
475 loop {
476 let next = subsume(&pure_eliminate(&unit_propagate(&cur)));
477 if is_leaf(&next) || next == cur {
478 return next;
479 }
480 cur = next;
481 }
482}
483
484fn permutations(k: usize) -> Vec<Vec<usize>> {
486 let items: Vec<usize> = (0..k).collect();
487 let mut out = Vec::new();
488 fn rec(remaining: &[usize], acc: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
489 if remaining.is_empty() {
490 out.push(acc.clone());
491 return;
492 }
493 for i in 0..remaining.len() {
494 let mut rest = remaining.to_vec();
495 let x = rest.remove(i);
496 acc.push(x);
497 rec(&rest, acc, out);
498 acc.pop();
499 }
500 }
501 rec(&items, &mut Vec::new(), &mut out);
502 out
503}
504
505pub struct Raw;
508
509impl Congruence for Raw {
510 fn name(&self) -> &str {
511 "raw"
512 }
513 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
514 let twist: Twist = clauses
515 .iter()
516 .flatten()
517 .map(|&(v, _)| (v, v, false))
518 .collect::<BTreeSet<_>>()
519 .into_iter()
520 .collect();
521 (clauses.clone(), twist)
522 }
523}
524
525pub struct Rename;
528
529impl Congruence for Rename {
530 fn name(&self) -> &str {
531 "rename"
532 }
533 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
534 let (normed, ren) = normalize(clauses);
537 let ren_map: HashMap<u32, u32> = ren.into_iter().collect();
538 let twist: Twist = clauses
539 .iter()
540 .flatten()
541 .map(|&(v, _)| (v, ren_map[&v], false))
542 .collect::<BTreeSet<_>>()
543 .into_iter()
544 .collect();
545 (normed, twist)
546 }
547}
548
549pub struct GroupInduced {
551 pub group: Vec<Perm>,
552 pub label: String,
553}
554
555impl Congruence for GroupInduced {
556 fn name(&self) -> &str {
557 &self.label
558 }
559 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
560 group_canon(clauses, &self.group)
561 }
562}
563
564pub struct CofactorIso {
567 pub cap: usize,
568}
569
570impl Congruence for CofactorIso {
571 fn name(&self) -> &str {
572 "cofactor-iso"
573 }
574 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
575 iso_canon(clauses, self.cap)
576 }
577}
578
579pub struct UnitPropIso {
586 pub cap: usize,
587}
588
589impl Congruence for UnitPropIso {
590 fn name(&self) -> &str {
591 "unitprop-iso"
592 }
593 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
594 iso_canon(&unit_propagate(clauses), self.cap)
595 }
596}
597
598pub struct ReduceIso {
603 pub cap: usize,
604}
605
606impl Congruence for ReduceIso {
607 fn name(&self) -> &str {
608 "reduce-iso"
609 }
610 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
611 iso_canon(&reduce(clauses), self.cap)
612 }
613}
614
615fn is_non_resolution_route(r: &crate::solve::Route) -> bool {
619 use crate::solve::Route::*;
620 matches!(r, Parity | ModP | ModM | ExactCover | Collapse | HybridXor | Sos | Nullstellensatz | Pigeonhole)
621}
622
623fn non_res_crushed() -> CanonClauses {
626 vec![vec![(u32::MAX, true)]]
627}
628
629pub struct StructuredReduceIso {
637 pub cap: usize,
638}
639
640impl Congruence for StructuredReduceIso {
641 fn name(&self) -> &str {
642 "struct-reduce-iso"
643 }
644 fn canonicalize(&self, clauses: &CanonClauses) -> (CanonClauses, Twist) {
645 let r = reduce(clauses);
646 match structured_leaf(&r) {
647 Some(route) if is_non_resolution_route(&route) => (non_res_crushed(), Vec::new()),
648 _ => iso_canon(&r, self.cap),
649 }
650 }
651}
652
653#[derive(Clone, Debug)]
656pub enum QNode {
657 Leaf(CanonClauses),
658 Internal { clauses: CanonClauses, var: u32, lo: usize, hi: usize, lo_twist: Twist, hi_twist: Twist },
659}
660
661pub struct QuotientDag {
664 pub root: usize,
665 pub nodes: Vec<QNode>,
666 pub visits: usize,
667}
668
669impl QuotientDag {
670 pub fn width(&self) -> usize {
672 self.nodes.len()
673 }
674}
675
676pub fn quotient_dag<C: Congruence + ?Sized>(
680 n: usize,
681 clauses: &CanonClauses,
682 cong: &C,
683) -> Option<QuotientDag> {
684 let mut nodes: Vec<QNode> = Vec::new();
685 let mut memo: HashMap<(usize, CanonClauses), Option<usize>> = HashMap::new();
686 fn go<C: Congruence + ?Sized>(
687 depth: usize,
688 n: usize,
689 clauses: CanonClauses,
690 nodes: &mut Vec<QNode>,
691 memo: &mut HashMap<(usize, CanonClauses), Option<usize>>,
692 cong: &C,
693 ) -> Option<usize> {
694 if let Some(&hit) = memo.get(&(depth, clauses.clone())) {
695 return hit;
696 }
697 let result = if clauses.iter().any(|c| c.is_empty()) {
698 let id = nodes.len();
699 nodes.push(QNode::Leaf(clauses.clone()));
700 Some(id)
701 } else if clauses.is_empty() || depth > n {
702 None
703 } else {
704 let x = clauses.iter().flatten().map(|&(v, _)| v).min().unwrap();
705 let mut children: Vec<(usize, Twist)> = Vec::new();
706 let mut ok = true;
707 for b in [false, true] {
708 let cof = cofactor(&clauses, x, b);
709 let (cn, twist) = cong.canonicalize(&cof);
710 match go(depth + 1, n, cn, nodes, memo, cong) {
711 Some(id) => children.push((id, twist)),
712 None => {
713 ok = false;
714 break;
715 }
716 }
717 }
718 if ok {
719 let id = nodes.len();
720 let (lo, lo_twist) = children[0].clone();
721 let (hi, hi_twist) = children[1].clone();
722 nodes.push(QNode::Internal { clauses: clauses.clone(), var: x, lo, hi, lo_twist, hi_twist });
723 Some(id)
724 } else {
725 None
726 }
727 };
728 memo.insert((depth, clauses), result);
729 result
730 }
731 let (root_canon, _) = cong.canonicalize(clauses);
732 let root = go(0, n, root_canon, &mut nodes, &mut memo, cong)?;
733 let visits = memo.len();
734 Some(QuotientDag { root, nodes, visits })
735}
736
737pub fn quotient_width<C: Congruence + ?Sized>(
739 n: usize,
740 clauses: &CanonClauses,
741 cong: &C,
742) -> Option<usize> {
743 quotient_dag(n, clauses, cong).map(|d| d.width())
744}
745
746pub fn check_quotient_dag(nodes: &[QNode]) -> bool {
751 nodes.iter().all(|node| match node {
752 QNode::Leaf(c) => c.iter().any(|cl| cl.is_empty()),
753 QNode::Internal { clauses, var, lo, hi, lo_twist, hi_twist } => {
754 let child = |id: usize| match &nodes[id] {
755 QNode::Leaf(c) => c,
756 QNode::Internal { clauses, .. } => clauses,
757 };
758 let ok = |b: bool, id: usize, tw: &Twist| {
759 apply_twist(&cofactor(clauses, *var, b), tw).map_or(false, |t| t == *child(id))
760 };
761 ok(false, *lo, lo_twist) && ok(true, *hi, hi_twist)
762 }
763 })
764}
765
766fn to_lits(clauses: &CanonClauses) -> Vec<Vec<Lit>> {
771 clauses.iter().map(|c| c.iter().map(|&(v, p)| Lit::new(v, p)).collect()).collect()
772}
773
774pub fn structured_leaf(clauses: &CanonClauses) -> Option<crate::solve::Route> {
781 if is_leaf(clauses) {
782 return None; }
784 let nv = clauses.iter().flatten().map(|&(v, _)| v as usize + 1).max().unwrap_or(0);
785 if nv == 0 {
786 return None; }
788 let solved = crate::solve::structured_prefix(nv, &to_lits(clauses))?;
792 match solved.answer {
793 crate::solve::Answer::Unsat
794 if !matches!(solved.via, crate::solve::Route::Cdcl | crate::solve::Route::Incompressible) =>
795 {
796 Some(solved.via)
797 }
798 _ => None,
799 }
800}
801
802#[derive(Clone, Debug)]
804pub enum SNode {
805 Trivial(CanonClauses),
807 Structured { clauses: CanonClauses, route: crate::solve::Route },
809 Internal { clauses: CanonClauses, var: u32, lo: usize, hi: usize },
811}
812
813impl SNode {
814 fn clauses(&self) -> &CanonClauses {
815 match self {
816 SNode::Trivial(c) | SNode::Structured { clauses: c, .. } | SNode::Internal { clauses: c, .. } => c,
817 }
818 }
819}
820
821pub struct StructuredDag {
826 pub root: usize,
827 pub nodes: Vec<SNode>,
828}
829
830impl StructuredDag {
831 pub fn size(&self) -> usize {
833 self.nodes.len()
834 }
835 pub fn structured_leaves(&self) -> usize {
837 self.nodes.iter().filter(|n| matches!(n, SNode::Structured { .. })).count()
838 }
839}
840
841pub fn structured_leaf_dag(n: usize, clauses: &CanonClauses) -> Option<StructuredDag> {
845 let mut nodes: Vec<SNode> = Vec::new();
846 let mut memo: HashMap<(usize, CanonClauses), Option<usize>> = HashMap::new();
847 fn go(
848 depth: usize,
849 n: usize,
850 clauses: CanonClauses,
851 nodes: &mut Vec<SNode>,
852 memo: &mut HashMap<(usize, CanonClauses), Option<usize>>,
853 ) -> Option<usize> {
854 if let Some(&hit) = memo.get(&(depth, clauses.clone())) {
855 return hit;
856 }
857 let result = if is_leaf(&clauses) {
858 let id = nodes.len();
859 nodes.push(SNode::Trivial(clauses.clone()));
860 Some(id)
861 } else if let Some(route) = structured_leaf(&clauses) {
862 let id = nodes.len();
863 nodes.push(SNode::Structured { clauses: clauses.clone(), route });
864 Some(id)
865 } else if depth == n {
866 None
867 } else {
868 let x = depth as u32;
869 let lo = go(depth + 1, n, cofactor(&clauses, x, false), nodes, memo);
870 let hi = go(depth + 1, n, cofactor(&clauses, x, true), nodes, memo);
871 match (lo, hi) {
872 (Some(lo), Some(hi)) => {
873 let id = nodes.len();
874 nodes.push(SNode::Internal { clauses: clauses.clone(), var: x, lo, hi });
875 Some(id)
876 }
877 _ => None,
878 }
879 };
880 memo.insert((depth, clauses), result);
881 result
882 }
883 let root = go(0, n, clauses.clone(), &mut nodes, &mut memo)?;
884 Some(StructuredDag { root, nodes })
885}
886
887pub fn check_structured_dag(nodes: &[SNode]) -> bool {
893 nodes.iter().all(|node| match node {
894 SNode::Trivial(c) => is_leaf(c),
895 SNode::Structured { clauses, .. } => structured_leaf(clauses).is_some(),
896 SNode::Internal { clauses, var, lo, hi } => {
897 nodes[*lo].clauses() == &cofactor(clauses, *var, false)
898 && nodes[*hi].clauses() == &cofactor(clauses, *var, true)
899 }
900 })
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906 use crate::hypercube::{minimal_cover_orbits, php_perm_symmetries};
907
908 fn php_canon(m: usize) -> (usize, CanonClauses) {
909 let (php, _) = crate::families::php(m);
910 (php.num_vars, canon(&php.clauses))
911 }
912
913 fn php_group(m: usize) -> Vec<Perm> {
915 let nv = m * (m - 1);
916 let gens = php_perm_symmetries(m);
917 let key = |p: &Perm| -> Vec<u32> { (0..nv).map(|v| p.apply(Lit::pos(v as u32)).var()).collect() };
918 let id = Perm::identity(nv);
919 let mut seen: BTreeSet<Vec<u32>> = [key(&id)].into_iter().collect();
920 let mut group = vec![id.clone()];
921 let mut frontier = vec![id];
922 while let Some(p) = frontier.pop() {
923 for g in &gens {
924 let q = p.compose(g);
925 if seen.insert(key(&q)) {
926 group.push(q.clone());
927 frontier.push(q);
928 }
929 }
930 }
931 group
932 }
933
934 fn xor_cycle(k: usize) -> CanonClauses {
935 let mut raw: Vec<Vec<(u32, bool)>> = Vec::new();
936 for i in 0..k {
937 let j = (i + 1) % k;
938 raw.push(vec![(i as u32, true), (j as u32, true)]);
939 raw.push(vec![(i as u32, false), (j as u32, false)]);
940 }
941 canon_raw(&raw)
942 }
943
944 #[test]
948 fn distinct_cofactor_dag_matches_the_prototype_and_the_checker_has_teeth() {
949 let n = 3usize;
950 for cover in minimal_cover_orbits(n) {
951 let clauses = canon(&cover.clauses());
952 let (root, nodes) = distinct_cofactor_dag(n, &clauses).expect("every UNSAT family unfolds");
953 assert!(check_distinct_dag(root, &nodes, &clauses), "the strict DAG re-checks");
954 }
955 let sat = canon(&[vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(2)]]);
957 assert!(distinct_cofactor_dag(n, &sat).is_none(), "a satisfiable formula has no DAG");
958 let (root, mut nodes) = distinct_cofactor_dag(3, &xor_cycle(3)).unwrap();
960 let internal = nodes
961 .iter()
962 .position(|nd| matches!(nd, Node::Internal { lo, hi, .. } if lo != hi))
963 .unwrap();
964 if let Node::Internal { lo, hi, .. } = &mut nodes[internal] {
965 std::mem::swap(lo, hi);
966 }
967 assert!(!check_distinct_dag(root, &nodes, &xor_cycle(3)), "a corrupted DAG is rejected");
968 let widths: Vec<usize> = [5usize, 7, 9, 11, 13]
970 .iter()
971 .map(|&k| *level_widths(k, &xor_cycle(k)).iter().max().unwrap())
972 .collect();
973 assert!(widths.windows(2).all(|w| w[0] == w[1]), "XOR cycle max width constant: {widths:?}");
974 }
975
976 #[test]
980 fn quotient_dag_reproduces_the_locked_pigeonhole_ratchets() {
981 for &(m, plain_expected, fused_expected) in &[(3usize, 25usize, 18usize), (4, 103, 60)] {
983 let (nv, clauses) = php_canon(m);
984 let plain = quotient_dag(nv, &clauses, &Rename).expect("PHP unfolds");
985 let group = GroupInduced { group: php_group(m), label: "php-sym".into() };
986 let fused = quotient_dag(nv, &clauses, &group).expect("PHP unfolds under the group");
987 assert!(check_quotient_dag(&plain.nodes), "m={m}: plain re-checks");
988 assert!(check_quotient_dag(&fused.nodes), "m={m}: fused re-checks, twists verified");
989 assert_eq!(plain.width(), plain_expected, "m={m}: plain quotient width is locked");
990 assert_eq!(fused.width(), fused_expected, "m={m}: fused quotient width is locked");
991 assert!(fused.width() < plain.width(), "m={m}: the group compounds the crush");
992 assert!(fused.visits <= 2 * fused.width() + 2 * nv + 2, "m={m}: work linear in output");
994 }
995 let (nv, clauses) = php_canon(3);
997 let group = GroupInduced { group: php_group(3), label: "php-sym".into() };
998 let mut dag = quotient_dag(nv, &clauses, &group).unwrap();
999 let victim = dag
1000 .nodes
1001 .iter()
1002 .position(|n| matches!(n, QNode::Internal { lo_twist, .. } if !lo_twist.is_empty()))
1003 .expect("a nontrivial twist exists");
1004 if let QNode::Internal { lo_twist, .. } = &mut dag.nodes[victim] {
1005 lo_twist[0].2 = !lo_twist[0].2;
1006 }
1007 assert!(!check_quotient_dag(&dag.nodes), "a corrupted twist is rejected");
1008 }
1009
1010 #[test]
1018 fn the_cofactor_class_ladder_is_monotone_and_bounded_by_the_distinct_floor() {
1019 for m in [3usize, 4] {
1020 let (nv, clauses) = php_canon(m);
1021 let distinct = distinct_width(nv, &clauses);
1022 let raw = quotient_class_count(nv, &clauses, &Raw);
1023 let rename = quotient_class_count(nv, &clauses, &Rename);
1024 let iso = quotient_class_count(nv, &clauses, &CofactorIso { cap: 6 });
1025 let unitprop = quotient_class_count(nv, &clauses, &UnitPropIso { cap: 6 });
1026 let reduceiso = quotient_class_count(nv, &clauses, &ReduceIso { cap: 6 });
1027 assert_eq!(raw, distinct, "m={m}: Raw class count == distinct_width");
1029 assert_eq!(level_widths(nv, &clauses).iter().sum::<usize>(), distinct, "m={m}: Σ widths");
1030 assert!(rename <= raw, "m={m}: rename ≤ raw ({rename} ≤ {raw})");
1034 assert!(iso <= rename, "m={m}: iso ≤ rename ({iso} ≤ {rename})");
1035 assert!(unitprop <= iso, "m={m}: unitprop ≤ iso ({unitprop} ≤ {iso})");
1036 assert!(reduceiso <= unitprop, "m={m}: reduce-iso ≤ unitprop ({reduceiso} ≤ {unitprop})");
1037 assert!(iso <= distinct, "m={m}: every class count ≤ the distinct floor ({iso} ≤ {distinct})");
1038 let dag = quotient_dag(nv, &clauses, &CofactorIso { cap: 6 }).expect("PHP unfolds under iso");
1040 assert!(check_quotient_dag(&dag.nodes), "m={m}: the iso certificate re-checks");
1041 eprintln!(
1042 "cofactor-classes[PHP({m})]: distinct {distinct} → rename {rename} → iso {iso} \
1043 (certificate DAG {} nodes, re-checked)",
1044 dag.width()
1045 );
1046 }
1047 }
1048}