1use crate::cdcl::Lit;
27use crate::dimacs::DimacsCnf;
28use crate::proof::Perm;
29use std::collections::{BTreeSet, HashMap};
30
31pub type Corner = u64;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct Subcube {
40 pub n: usize,
41 pub care: u64,
43 pub value: u64,
45}
46
47impl Subcube {
48 pub fn blocker(clause: &[Lit], n: usize) -> Subcube {
52 let mut care = 0u64;
53 let mut value = 0u64;
54 for &lit in clause {
55 let v = lit.var() as u64;
56 care |= 1u64 << v;
57 if !lit.is_positive() {
58 value |= 1u64 << v;
59 }
60 }
61 Subcube { n, care, value }
62 }
63
64 #[inline]
67 pub fn covers(&self, corner: Corner) -> bool {
68 (corner & self.care) == self.value
69 }
70
71 pub fn dimension(&self) -> usize {
73 self.n - self.care.count_ones() as usize
74 }
75
76 pub fn footprint_card(&self) -> u64 {
78 1u64 << self.dimension()
79 }
80
81 pub fn clause_literals(&self) -> Vec<(usize, bool)> {
86 (0..self.n)
87 .filter(|&v| self.care & (1u64 << v) != 0)
88 .map(|v| (v, self.value & (1u64 << v) == 0))
89 .collect()
90 }
91
92 pub fn clause_lp_value(&self, point: &[f64]) -> f64 {
96 self.clause_literals()
97 .iter()
98 .map(|&(v, positive)| if positive { point[v] } else { 1.0 - point[v] })
99 .sum()
100 }
101
102 pub fn resolve(&self, other: &Subcube) -> Option<(usize, Subcube)> {
110 let shared = self.care & other.care;
111 let disagree = shared & (self.value ^ other.value);
112 if disagree.count_ones() != 1 {
113 return None;
114 }
115 let pivot = disagree.trailing_zeros() as usize;
116 let care = (self.care | other.care) & !(1u64 << pivot);
117 let value = (self.value | other.value) & care;
118 Some((pivot, Subcube { n: self.n, care, value }))
119 }
120
121 pub fn footprint(&self) -> Vec<Corner> {
123 let free: Vec<u64> = (0..self.n as u64).filter(|i| self.care & (1u64 << i) == 0).collect();
124 let mut out = Vec::with_capacity(1usize << free.len());
125 for mask in 0..(1u64 << free.len()) {
126 let mut c = self.value;
127 for (j, &i) in free.iter().enumerate() {
128 if mask & (1u64 << j) != 0 {
129 c |= 1u64 << i;
130 }
131 }
132 out.push(c);
133 }
134 out
135 }
136}
137
138#[derive(Clone, Debug)]
141pub struct Cover {
142 pub n: usize,
143 pub blockers: Vec<Subcube>,
144}
145
146impl Cover {
147 pub fn of_cnf(cnf: &DimacsCnf) -> Cover {
149 let n = cnf.num_vars;
150 let blockers = cnf.clauses.iter().map(|c| Subcube::blocker(c, n)).collect();
151 Cover { n, blockers }
152 }
153
154 pub fn vertex_energy(&self, corner: Corner) -> usize {
157 self.blockers.iter().filter(|b| b.covers(corner)).count()
158 }
159
160 pub fn blocks(&self, corner: Corner) -> bool {
162 self.blockers.iter().any(|b| b.covers(corner))
163 }
164
165 pub fn is_tight(&self, corner: Corner) -> bool {
169 self.vertex_energy(corner) == 1
170 }
171
172 pub fn is_redundant(&self, corner: Corner) -> bool {
174 self.vertex_energy(corner) >= 2
175 }
176
177 pub fn essential_blockers(&self) -> Vec<usize> {
182 (0..self.blockers.len())
183 .filter(|&i| self.blockers[i].footprint().iter().any(|&c| self.vertex_energy(c) == 1))
184 .collect()
185 }
186
187 pub fn escaping_corner(&self) -> Option<Corner> {
190 (0u64..(1u64 << self.n)).find(|&c| !self.blocks(c))
191 }
192
193 pub fn is_total(&self) -> bool {
195 self.escaping_corner().is_none()
196 }
197
198 pub fn solution_count(&self) -> u64 {
200 (0u64..(1u64 << self.n)).filter(|&c| !self.blocks(c)).count() as u64
201 }
202
203 pub fn has_no_hole(&self) -> bool {
205 self.is_total()
206 }
207
208 pub fn relaxation_feasible_at_center(&self) -> bool {
215 self.blockers.iter().all(|b| b.care.count_ones() >= 2)
216 }
217
218 pub fn counting_refutation(&self) -> Option<crate::pigeonhole::CountingCert> {
223 crate::pigeonhole::counting_certificate(&self.to_expr()?)
224 }
225
226 pub fn hall_refutation(&self) -> Option<crate::matching::HallWitness> {
230 crate::pigeonhole::hall_refutation(&self.to_expr()?)
231 }
232
233 pub fn has_unique_hole(&self) -> bool {
235 self.solution_count() == 1
236 }
237
238 pub fn has_at_least_holes(&self, k: u64) -> bool {
240 self.solution_count() >= k
241 }
242
243 pub fn separated_by(&self, cut: u64) -> bool {
247 self.blockers.iter().all(|b| (b.care & cut) == b.care || (b.care & cut) == 0)
248 }
249
250 pub fn variable_interaction(&self, i: usize, j: usize) -> bool {
253 i != j
254 && self.blockers.iter().any(|b| b.care & (1u64 << i) != 0 && b.care & (1u64 << j) != 0)
255 }
256
257 pub fn to_expr(&self) -> Option<crate::ProofExpr> {
261 use crate::ProofExpr;
262 let lit = |v: usize, positive: bool| {
263 let a = ProofExpr::Atom(format!("x{v}"));
264 if positive { a } else { ProofExpr::Not(Box::new(a)) }
265 };
266 let mut clauses = Vec::with_capacity(self.blockers.len());
267 for b in &self.blockers {
268 let lits = b.clause_literals();
269 if lits.is_empty() {
270 return None;
271 }
272 let mut it = lits.into_iter();
273 let (v0, p0) = it.next().unwrap();
274 clauses.push(it.fold(lit(v0, p0), |acc, (v, p)| ProofExpr::Or(Box::new(acc), Box::new(lit(v, p)))));
275 }
276 let mut it = clauses.into_iter();
277 let first = it.next()?;
278 Some(it.fold(first, |acc, c| ProofExpr::And(Box::new(acc), Box::new(c))))
279 }
280
281 pub fn prove_total_certified(&self) -> crate::sat::UnsatOutcome {
287 match self.to_expr() {
288 Some(e) => crate::sat::prove_unsat(&e),
289 None => crate::sat::UnsatOutcome::Unsupported,
290 }
291 }
292
293 pub fn neighbors(&self, i: usize) -> Vec<(usize, usize, Subcube)> {
297 (0..self.blockers.len())
298 .filter(|&j| j != i)
299 .filter_map(|j| self.blockers[i].resolve(&self.blockers[j]).map(|(pivot, r)| (j, pivot, r)))
300 .collect()
301 }
302
303 pub fn clauses(&self) -> Vec<Vec<Lit>> {
306 self.blockers
307 .iter()
308 .map(|b| b.clause_literals().into_iter().map(|(v, p)| Lit::new(v as u32, p)).collect())
309 .collect()
310 }
311
312 pub fn blocker_orbits(&self, generators: &[CubeSym]) -> Option<Vec<Vec<usize>>> {
323 let mut index: HashMap<Subcube, usize> = HashMap::new();
324 for (i, b) in self.blockers.iter().enumerate() {
325 index.entry(*b).or_insert(i);
326 }
327 let m = self.blockers.len();
328 let mut seen = vec![false; m];
329 let mut orbits = Vec::new();
330 for start in 0..m {
331 if seen[start] {
332 continue;
333 }
334 let mut orbit = Vec::new();
335 let mut stack = vec![start];
336 seen[start] = true;
337 while let Some(i) = stack.pop() {
338 orbit.push(i);
339 for g in generators {
340 let image = g.map_subcube(&self.blockers[i]);
341 let &j = index.get(&image)?; if !seen[j] {
343 seen[j] = true;
344 stack.push(j);
345 }
346 }
347 }
348 orbit.sort_unstable();
349 orbits.push(orbit);
350 }
351 Some(orbits)
352 }
353
354 pub fn discovered_rule_symmetry(&self) -> RuleSymmetry {
361 let clauses = self.clauses();
362 let generators = crate::symmetry_detect::find_generators(self.n, &clauses);
363 let rule_orbits = clause_orbits(&clauses, &generators).len();
364 RuleSymmetry { n: self.n, blockers: clauses.len(), generators: generators.len(), rule_orbits }
365 }
366}
367
368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub struct RuleSymmetry {
374 pub n: usize,
375 pub blockers: usize,
376 pub generators: usize,
377 pub rule_orbits: usize,
378}
379
380pub fn clause_orbits(clauses: &[Vec<Lit>], generators: &[Perm]) -> Vec<Vec<usize>> {
389 let index: HashMap<Vec<u32>, usize> = clauses
390 .iter()
391 .enumerate()
392 .map(|(i, c)| (crate::symmetry_detect::clause_key(c), i))
393 .collect();
394 let m = clauses.len();
395 let mut seen = vec![false; m];
396 let mut orbits = Vec::new();
397 for start in 0..m {
398 if seen[start] {
399 continue;
400 }
401 let mut orbit = Vec::new();
402 let mut stack = vec![start];
403 seen[start] = true;
404 while let Some(i) = stack.pop() {
405 orbit.push(i);
406 for g in generators {
407 let key = crate::symmetry_detect::clause_key(&g.apply_clause(&clauses[i]));
408 if let Some(&j) = index.get(&key) {
409 if !seen[j] {
410 seen[j] = true;
411 stack.push(j);
412 }
413 }
414 }
415 }
416 orbit.sort_unstable();
417 orbits.push(orbit);
418 }
419 orbits
420}
421
422pub fn php_perm_symmetries(n: usize) -> Vec<Perm> {
425 let holes = n.saturating_sub(1);
426 let num_vars = n * holes;
427 let var = |p: usize, h: usize| p * holes + h;
428 let mut gens = Vec::new();
429 for p in 0..n.saturating_sub(1) {
430 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
431 for h in 0..holes {
432 images.swap(var(p, h), var(p + 1, h));
433 }
434 gens.push(Perm::from_images(images));
435 }
436 for h in 0..holes.saturating_sub(1) {
437 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
438 for p in 0..n {
439 images.swap(var(p, h), var(p, h + 1));
440 }
441 gens.push(Perm::from_images(images));
442 }
443 gens
444}
445
446pub fn pigeonhole_rule_symmetry(n: usize) -> RuleSymmetry {
451 let (cnf, _) = crate::families::php(n);
452 let generators = php_perm_symmetries(n);
453 let rule_orbits = clause_orbits(&cnf.clauses, &generators).len();
454 RuleSymmetry { n, blockers: cnf.clauses.len(), generators: generators.len(), rule_orbits }
455}
456
457#[derive(Clone, Debug, PartialEq, Eq)]
464pub struct AbstractRefutation {
465 pub rule_types: usize,
466 pub invariant: &'static str,
467 pub witness: crate::pigeonhole::CountingCert,
468}
469
470pub fn pigeonhole_abstract_refutation(n: usize) -> Option<AbstractRefutation> {
476 let (cnf, _) = crate::families::php(n);
477 let rule_types = clause_orbits(&cnf.clauses, &php_perm_symmetries(n)).len();
478 let witness = crate::pigeonhole::certify_pigeonhole_unsat(n as u128, n.saturating_sub(1) as u128)?;
479 Some(AbstractRefutation { rule_types, invariant: "Hall/matching: pigeons > holes", witness })
480}
481
482pub fn apply_renaming(clauses: &[Vec<Lit>], flips: &[bool]) -> Vec<Vec<Lit>> {
485 clauses
486 .iter()
487 .map(|c| {
488 c.iter()
489 .map(|l| if flips[l.var() as usize] { l.negated() } else { *l })
490 .collect()
491 })
492 .collect()
493}
494
495pub fn renaming_to_horn(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Vec<bool>> {
502 use crate::twosat::{self, Lit as TLit, TwoSatOutcome};
503 let flit = |l: &Lit| {
504 if l.is_positive() {
505 TLit::pos(l.var() as usize)
506 } else {
507 TLit::neg(l.var() as usize)
508 }
509 };
510 let mut two_sat: Vec<(TLit, TLit)> = Vec::new();
511 for c in clauses {
512 for i in 0..c.len() {
513 for j in (i + 1)..c.len() {
514 two_sat.push((flit(&c[i]), flit(&c[j]))); }
516 }
517 }
518 match twosat::solve(&two_sat, num_vars) {
519 TwoSatOutcome::Sat(flips) => Some(flips),
520 TwoSatOutcome::Unsat(_) => None,
521 }
522}
523
524pub fn automorphism_group_size(num_vars: usize, clauses: &[Vec<Lit>]) -> usize {
527 let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
528 let key = |p: &Perm| -> Vec<(u32, bool)> {
529 (0..num_vars)
530 .map(|v| {
531 let l = p.apply(Lit::pos(v as u32));
532 (l.var(), l.is_positive())
533 })
534 .collect()
535 };
536 let id = Perm::identity(num_vars);
537 let mut seen: BTreeSet<Vec<(u32, bool)>> = [key(&id)].into_iter().collect();
538 let mut group = vec![id];
539 let mut i = 0;
540 while i < group.len() {
541 let g = group[i].clone();
542 i += 1;
543 for s in &generators {
544 let h = s.compose(&g);
545 if seen.insert(key(&h)) {
546 group.push(h);
547 }
548 }
549 if group.len() > 5_000_000 {
550 break;
551 }
552 }
553 group.len()
554}
555
556pub fn symmetry_entropy_bits(num_vars: usize, clauses: &[Vec<Lit>]) -> f64 {
561 (automorphism_group_size(num_vars, clauses) as f64).log2()
562}
563
564pub fn find_random_core(num_vars: usize, clauses: &[Vec<Lit>], max_steps: usize) -> Option<Vec<Vec<Lit>>> {
570 let mut current = clauses.to_vec();
571 for _ in 0..max_steps {
572 let cut = clauses_to_expr(¤t).is_some_and(|e| {
573 crate::pigeonhole::decide_pigeonhole_unsat(&e)
574 || crate::xorsat::refute_via_parity(&e)
575 || crate::pseudo_boolean::refute_clausal(&e)
576 });
577 if cut {
578 return None; }
580 match carve(num_vars, ¤t) {
581 CarveOutcome::Sat | CarveOutcome::Unsat => return None,
582 CarveOutcome::Core { clauses: core, .. } if core.len() < current.len() => {
583 current = core;
584 }
585 CarveOutcome::Core { clauses: core, .. } => {
586 let eliminated = bounded_variable_elimination(num_vars, &core);
587 if eliminated.len() < core.len() {
588 current = eliminated;
589 } else {
590 return Some(core); }
592 }
593 }
594 }
595 Some(current)
596}
597
598#[derive(Clone, Debug, PartialEq, Eq)]
601pub enum AdvanceStatus {
602 Decided(bool),
603 StructurelessResidue { core: usize },
604}
605
606#[derive(Clone, Debug, PartialEq)]
608pub struct AdvanceStep {
609 pub lever: &'static str,
610 pub clauses: usize,
611 pub symmetry_bits: f64,
612}
613
614pub fn auto_advance(
620 num_vars: usize,
621 clauses: &[Vec<Lit>],
622 max_steps: usize,
623) -> (AdvanceStatus, Vec<AdvanceStep>) {
624 let mut current = clauses.to_vec();
625 let mut trace = Vec::new();
626 for _ in 0..max_steps {
627 let bits = symmetry_entropy_bits(num_vars, ¤t);
628 let cut = clauses_to_expr(¤t).is_some_and(|e| {
630 crate::pigeonhole::decide_pigeonhole_unsat(&e)
631 || crate::xorsat::refute_via_parity(&e)
632 || crate::pseudo_boolean::refute_clausal(&e)
633 });
634 if cut {
635 trace.push(AdvanceStep { lever: "certified cut → UNSAT", clauses: current.len(), symmetry_bits: bits });
636 return (AdvanceStatus::Decided(false), trace);
637 }
638 match carve(num_vars, ¤t) {
640 CarveOutcome::Sat => {
641 trace.push(AdvanceStep { lever: "carve → SAT", clauses: 0, symmetry_bits: bits });
642 return (AdvanceStatus::Decided(true), trace);
643 }
644 CarveOutcome::Unsat => {
645 trace.push(AdvanceStep { lever: "carve → UNSAT", clauses: 0, symmetry_bits: bits });
646 return (AdvanceStatus::Decided(false), trace);
647 }
648 CarveOutcome::Core { clauses: core, .. } if core.len() < current.len() => {
649 trace.push(AdvanceStep { lever: "carve (unit/pure/subsume)", clauses: core.len(), symmetry_bits: bits });
650 current = core;
651 continue;
652 }
653 CarveOutcome::Core { clauses: core, .. } => {
654 let eliminated = bounded_variable_elimination(num_vars, &core);
656 if eliminated.len() < core.len() {
657 trace.push(AdvanceStep { lever: "variable elimination (project a dimension)", clauses: eliminated.len(), symmetry_bits: bits });
658 current = eliminated;
659 continue;
660 }
661 trace.push(AdvanceStep { lever: "irreducible core — no structure left (branch)", clauses: core.len(), symmetry_bits: bits });
663 return (AdvanceStatus::StructurelessResidue { core: core.len() }, trace);
664 }
665 }
666 }
667 (AdvanceStatus::StructurelessResidue { core: current.len() }, trace)
668}
669
670#[derive(Clone, Debug, PartialEq)]
673pub struct Diagnosis {
674 pub clauses: usize,
675 pub symmetry_bits: f64,
676 pub rule_quotient: usize,
677 pub cut: Option<Shadow>,
678 pub antipodal: bool,
679 pub renamable_horn: bool,
680 pub components: usize,
681 pub autark_section: bool,
682 pub core_clauses: usize,
683}
684
685pub fn diagnose(num_vars: usize, clauses: &[Vec<Lit>]) -> Diagnosis {
688 let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
689 let rule_quotient = clause_orbits(clauses, &generators).len();
690 let symmetry_bits = symmetry_entropy_bits(num_vars, clauses);
691 let cut = clauses_to_expr(clauses).and_then(|e| {
692 if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
693 Some(Shadow::Counting)
694 } else if crate::xorsat::refute_via_parity(&e) {
695 Some(Shadow::Parity)
696 } else if crate::pseudo_boolean::refute_clausal(&e) {
697 Some(Shadow::CuttingPlanes)
698 } else {
699 None
700 }
701 });
702 let (_, assigned) = pure_literal_reduce(num_vars, clauses);
703 let core_clauses = match carve(num_vars, clauses) {
704 CarveOutcome::Core { clauses: c, .. } => c.len(),
705 _ => 0,
706 };
707 Diagnosis {
708 clauses: clauses.len(),
709 symmetry_bits,
710 rule_quotient,
711 cut,
712 antipodal: is_antipodally_symmetric(clauses),
713 renamable_horn: renaming_to_horn(num_vars, clauses).is_some(),
714 components: components(num_vars, clauses).len(),
715 autark_section: !assigned.is_empty(),
716 core_clauses,
717 }
718}
719
720pub fn applicable_levers(d: &Diagnosis) -> Vec<&'static str> {
723 let mut levers = Vec::new();
724 if let Some(s) = d.cut {
725 levers.push(match s {
726 Shadow::Counting => "counting/Hall cut (one-punch)",
727 Shadow::Parity => "GF(2) parity cut (one-punch)",
728 Shadow::CuttingPlanes => "cutting-planes cut (one-punch)",
729 });
730 }
731 if d.symmetry_bits > 0.0 {
732 levers.push("symmetry breaking (lex-leader prune)");
733 }
734 if d.antipodal {
735 levers.push("antipodal / center-inversion (recursive)");
736 }
737 if d.renamable_horn {
738 levers.push("renamable-Horn (poly via 2-SAT renaming)");
739 }
740 if d.components > 1 {
741 levers.push("component decomposition");
742 }
743 if d.autark_section || d.core_clauses < d.clauses {
744 levers.push("autarky / carving (unit, pure-literal, subsumption)");
745 }
746 if levers.is_empty() {
747 levers.push("no global structure — backdoor + branch the residue (the honest wall)");
748 }
749 levers
750}
751
752#[derive(Clone, Debug, PartialEq, Eq)]
759pub struct StructuralProfile {
760 pub clauses: usize,
761 pub quotient: usize,
762 pub cut: Option<Shadow>,
763 pub core_clauses: usize,
764}
765
766pub fn structural_profile(num_vars: usize, clauses: &[Vec<Lit>]) -> StructuralProfile {
770 let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
771 let quotient = clause_orbits(clauses, &generators).len();
772 let cut = clauses_to_expr(clauses).and_then(|e| {
773 if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
774 Some(Shadow::Counting)
775 } else if crate::xorsat::refute_via_parity(&e) {
776 Some(Shadow::Parity)
777 } else if crate::pseudo_boolean::refute_clausal(&e) {
778 Some(Shadow::CuttingPlanes)
779 } else {
780 None
781 }
782 });
783 let core_clauses = match carve(num_vars, clauses) {
784 CarveOutcome::Sat | CarveOutcome::Unsat => 0,
785 CarveOutcome::Core { clauses: c, .. } => bounded_variable_elimination(num_vars, &c).len(),
786 };
787 StructuralProfile { clauses: clauses.len(), quotient, cut, core_clauses }
788}
789
790#[derive(Clone, Copy, Debug, PartialEq, Eq)]
792pub enum Shadow {
793 Counting,
795 Parity,
797 CuttingPlanes,
799}
800
801pub fn apply_perm_to_model(perm: &Perm, model: &[bool]) -> Vec<bool> {
805 let mut out = model.to_vec();
806 for v in 0..model.len() {
807 let image = perm.apply(Lit::pos(v as u32));
808 out[image.var() as usize] = if image.is_positive() { model[v] } else { !model[v] };
809 }
810 out
811}
812
813pub fn model_orbit(model: &[bool], generators: &[Perm]) -> Vec<Vec<bool>> {
817 let mut seen = BTreeSet::new();
818 seen.insert(model.to_vec());
819 let mut stack = vec![model.to_vec()];
820 while let Some(m) = stack.pop() {
821 for g in generators {
822 let image = apply_perm_to_model(g, &m);
823 if seen.insert(image.clone()) {
824 stack.push(image);
825 }
826 }
827 }
828 seen.into_iter().collect()
829}
830
831pub fn canonical_model(model: &[bool], generators: &[Perm]) -> Vec<bool> {
836 model_orbit(model, generators).into_iter().min().unwrap()
837}
838
839pub fn perm_group_closure(generators: &[Perm], num_vars: usize) -> Vec<Perm> {
843 let mut seen = std::collections::HashSet::new();
844 let id = Perm::identity(num_vars);
845 let mut frontier = vec![id.clone()];
846 seen.insert(id);
847 while let Some(g) = frontier.pop() {
848 for h in generators {
849 let gh = h.compose(&g);
850 if seen.insert(gh.clone()) {
851 frontier.push(gh);
852 }
853 }
854 }
855 seen.into_iter().collect()
856}
857
858pub fn stabilizer(model: &[bool], group: &[Perm]) -> Vec<Perm> {
862 group.iter().filter(|g| apply_perm_to_model(g, model) == model).cloned().collect()
863}
864
865pub fn witness_perspective(model: &[bool], generators: &[Perm], num_vars: usize) -> Vec<(Vec<bool>, Perm)> {
873 let group = perm_group_closure(generators, num_vars);
874 let mut seen = BTreeSet::new();
875 let mut out = Vec::new();
876 let mut by_dest: Vec<(Vec<bool>, Perm)> = Vec::new();
877 for g in &group {
878 let dest = apply_perm_to_model(g, model);
879 if seen.insert(dest.clone()) {
880 by_dest.push((dest, g.clone()));
881 }
882 }
883 by_dest.sort_by(|a, b| a.0.cmp(&b.0));
884 let here = by_dest.iter().position(|(d, _)| d == model).unwrap();
885 out.push((model.to_vec(), Perm::identity(num_vars)));
886 for (i, pair) in by_dest.into_iter().enumerate() {
887 if i != here {
888 out.push(pair);
889 }
890 }
891 out
892}
893
894pub fn burnside_orbit_count(models: &[Vec<bool>], group: &[Perm]) -> usize {
902 let total_fixed: usize = group
903 .iter()
904 .map(|g| models.iter().filter(|m| apply_perm_to_model(g, m.as_slice()) == **m).count())
905 .sum();
906 total_fixed / group.len()
907}
908
909#[derive(Clone, Copy, Debug, PartialEq, Eq)]
919pub enum ProofRung {
920 Trivial,
922 Counting,
925 Parity,
928 ModCount { p: u64 },
934 Nullstellensatz { min_degree: usize },
937 BeyondBudget,
939}
940
941pub fn weakest_crushing_rung(num_vars: usize, clauses: &[Vec<Lit>], ns_budget: usize) -> ProofRung {
950 weakest_crushing_rung_with_char(num_vars, clauses, ns_budget, &[])
951}
952
953pub fn weakest_crushing_rung_with_char(
962 num_vars: usize,
963 clauses: &[Vec<Lit>],
964 ns_budget: usize,
965 primes: &[u64],
966) -> ProofRung {
967 if let CarveOutcome::Unsat = carve(num_vars, clauses) {
968 return ProofRung::Trivial;
969 }
970 let Some(e) = clauses_to_expr(clauses) else { return ProofRung::BeyondBudget };
971 if crate::pigeonhole::counting_certificate(&e).is_some() || crate::pigeonhole::hall_refutation(&e).is_some() {
972 return ProofRung::Counting;
973 }
974 if crate::xorsat::refute_via_parity(&e) {
975 return ProofRung::Parity;
976 }
977 if !primes.is_empty() {
978 if let Some(rec) = crate::modp::recover_from_cnf(num_vars, clauses) {
979 if primes.contains(&rec.modulus) {
980 if let crate::modp::ModpOutcome::Unsat(combo) =
981 crate::modp::solve(&rec.equations, rec.num_vars, rec.modulus)
982 {
983 if crate::modp::is_refutation(&rec.equations, rec.num_vars, rec.modulus, &combo) {
984 return ProofRung::ModCount { p: rec.modulus };
985 }
986 }
987 }
988 }
989 }
990 let cap = ns_budget.min(num_vars);
991 if let Some(d) = (1..=cap).find(|&d| crate::polycalc::nullstellensatz_refutes(num_vars, clauses, d)) {
992 return ProofRung::Nullstellensatz { min_degree: d };
993 }
994 ProofRung::BeyondBudget
995}
996
997pub fn partition_into_orbits(models: &[Vec<bool>], generators: &[Perm]) -> Vec<Vec<Vec<bool>>> {
1002 let model_set: BTreeSet<Vec<bool>> = models.iter().cloned().collect();
1003 let mut assigned: BTreeSet<Vec<bool>> = BTreeSet::new();
1004 let mut orbits = Vec::new();
1005 for m in models {
1006 if assigned.contains(m) {
1007 continue;
1008 }
1009 let orbit: Vec<Vec<bool>> =
1010 model_orbit(m, generators).into_iter().filter(|x| model_set.contains(x)).collect();
1011 for x in &orbit {
1012 assigned.insert(x.clone());
1013 }
1014 orbits.push(orbit);
1015 }
1016 orbits
1017}
1018
1019#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1028pub struct CarveStats {
1029 pub nodes: usize,
1030 pub punches: usize,
1031 pub max_depth: usize,
1032}
1033
1034pub fn autocarve(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> Option<bool> {
1035 autocarve_measured(num_vars, clauses, budget).0
1036}
1037
1038pub fn autocarve_measured(
1040 num_vars: usize,
1041 clauses: &[Vec<Lit>],
1042 budget: usize,
1043) -> (Option<bool>, CarveStats) {
1044 let mut stats = CarveStats::default();
1045 let verdict = autocarve_rec(num_vars, clauses, budget, 0, &mut stats);
1046 (verdict, stats)
1047}
1048
1049fn autocarve_rec(
1050 num_vars: usize,
1051 clauses: &[Vec<Lit>],
1052 budget: usize,
1053 depth: usize,
1054 stats: &mut CarveStats,
1055) -> Option<bool> {
1056 stats.nodes += 1;
1057 stats.max_depth = stats.max_depth.max(depth);
1058 if stats.nodes > budget {
1059 return None;
1060 }
1061 let core = match carve(num_vars, clauses) {
1062 CarveOutcome::Sat => return Some(true),
1063 CarveOutcome::Unsat => return Some(false),
1064 CarveOutcome::Core { clauses, .. } => clauses,
1065 };
1066 for component in components(num_vars, &core) {
1067 let cut = clauses_to_expr(&component).is_some_and(|e| {
1071 crate::pigeonhole::counting_certificate(&e).is_some()
1072 || crate::pigeonhole::decide_pigeonhole_unsat(&e)
1073 || crate::xorsat::refute_via_parity(&e)
1074 || crate::pseudo_boolean::refute_clausal(&e)
1075 });
1076 if cut {
1077 stats.punches += 1;
1078 return Some(false); }
1080 let pivot = component[0][0].var();
1082 let mut component_sat = false;
1083 for value in [false, true] {
1084 let mut branch = component.clone();
1085 branch.push(vec![Lit::new(pivot, value)]);
1086 match autocarve_rec(num_vars, &branch, budget, depth + 1, stats) {
1087 Some(true) => {
1088 component_sat = true;
1089 break;
1090 }
1091 Some(false) => {}
1092 None => return None,
1093 }
1094 }
1095 if !component_sat {
1096 return Some(false); }
1098 }
1099 Some(true)
1100}
1101
1102pub fn crush(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> Option<bool> {
1109 let (core, _) = pure_literal_reduce(num_vars, clauses);
1110 if core.is_empty() {
1111 return Some(true); }
1113 for component in components(num_vars, &core) {
1114 match search_cost(num_vars, &component, true, budget) {
1115 SearchCost::Decided { sat: false, .. } => return Some(false), SearchCost::Decided { sat: true, .. } => {} SearchCost::Exceeded { .. } => return None, }
1119 }
1120 Some(true) }
1122
1123fn resolve_on_var(cp: &[Lit], cn: &[Lit], v: usize) -> Option<Vec<Lit>> {
1124 let mut lits: Vec<Lit> = Vec::new();
1125 for &l in cp.iter().chain(cn.iter()) {
1126 if l.var() as usize != v && !lits.contains(&l) {
1127 lits.push(l);
1128 }
1129 }
1130 if lits.iter().any(|l| lits.contains(&l.negated())) {
1131 return None; }
1133 Some(lits)
1134}
1135
1136pub fn eliminate_variable(v: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Lit>> {
1141 let (pv, nv) = (Lit::new(v as u32, true), Lit::new(v as u32, false));
1142 let mut result: Vec<Vec<Lit>> =
1143 clauses.iter().filter(|c| !c.contains(&pv) && !c.contains(&nv)).cloned().collect();
1144 let pos: Vec<&Vec<Lit>> = clauses.iter().filter(|c| c.contains(&pv)).collect();
1145 let neg: Vec<&Vec<Lit>> = clauses.iter().filter(|c| c.contains(&nv)).collect();
1146 for cp in &pos {
1147 for cn in &neg {
1148 if let Some(resolvent) = resolve_on_var(cp, cn, v) {
1149 result.push(resolvent);
1150 }
1151 }
1152 }
1153 result
1154}
1155
1156pub fn bounded_variable_elimination(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Lit>> {
1161 let mut current = clauses.to_vec();
1162 loop {
1163 let mut eliminated = false;
1164 for v in 0..num_vars {
1165 let pos = current.iter().filter(|c| c.contains(&Lit::new(v as u32, true))).count();
1166 let neg = current.iter().filter(|c| c.contains(&Lit::new(v as u32, false))).count();
1167 if pos == 0 || neg == 0 {
1168 continue;
1169 }
1170 let candidate = eliminate_variable(v, ¤t);
1171 if candidate.len() <= current.len() {
1172 current = candidate;
1173 eliminated = true;
1174 }
1175 }
1176 if !eliminated {
1177 break;
1178 }
1179 }
1180 current
1181}
1182
1183#[derive(Clone, Debug, PartialEq, Eq)]
1185pub enum CarveOutcome {
1186 Sat,
1188 Unsat,
1190 Core { clauses: Vec<Vec<Lit>>, forced: Vec<Lit> },
1192}
1193
1194fn find_pure_literal(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Lit> {
1195 let mut pos = vec![false; num_vars];
1196 let mut neg = vec![false; num_vars];
1197 for c in clauses {
1198 for l in c {
1199 if l.is_positive() {
1200 pos[l.var() as usize] = true;
1201 } else {
1202 neg[l.var() as usize] = true;
1203 }
1204 }
1205 }
1206 (0..num_vars).find_map(|v| match (pos[v], neg[v]) {
1207 (true, false) => Some(Lit::new(v as u32, true)),
1208 (false, true) => Some(Lit::new(v as u32, false)),
1209 _ => None,
1210 })
1211}
1212
1213fn subsume_once(clauses: &mut Vec<Vec<Lit>>) -> bool {
1214 for i in 0..clauses.len() {
1215 for j in 0..clauses.len() {
1216 if i != j
1217 && clauses[i].len() < clauses[j].len()
1218 && clauses[i].iter().all(|l| clauses[j].contains(l))
1219 {
1220 clauses.remove(j);
1221 return true;
1222 }
1223 }
1224 }
1225 false
1226}
1227
1228pub fn carve(num_vars: usize, clauses: &[Vec<Lit>]) -> CarveOutcome {
1234 let mut current: Vec<Vec<Lit>> = clauses.to_vec();
1235 let mut forced: Vec<Lit> = Vec::new();
1236 loop {
1237 if current.iter().any(|c| c.is_empty()) {
1238 return CarveOutcome::Unsat;
1239 }
1240 if current.is_empty() {
1241 return CarveOutcome::Sat;
1242 }
1243 let mut changed = false;
1244 if let Some(unit) = current.iter().find(|c| c.len() == 1).map(|c| c[0]) {
1245 forced.push(unit);
1246 let neg = unit.negated();
1247 current.retain(|c| !c.contains(&unit));
1248 for c in &mut current {
1249 c.retain(|&l| l != neg);
1250 }
1251 changed = true;
1252 } else if let Some(pure) = find_pure_literal(num_vars, ¤t) {
1253 forced.push(pure);
1254 current.retain(|c| !c.contains(&pure));
1255 changed = true;
1256 } else if subsume_once(&mut current) {
1257 changed = true;
1258 }
1259 if !changed {
1260 return CarveOutcome::Core { clauses: current, forced };
1261 }
1262 }
1263}
1264
1265pub fn pure_literal_reduce(num_vars: usize, clauses: &[Vec<Lit>]) -> (Vec<Vec<Lit>>, Vec<Lit>) {
1271 let mut current: Vec<Vec<Lit>> = clauses.to_vec();
1272 let mut assigned = Vec::new();
1273 loop {
1274 let mut pos = vec![false; num_vars];
1275 let mut neg = vec![false; num_vars];
1276 for c in ¤t {
1277 for l in c {
1278 if l.is_positive() {
1279 pos[l.var() as usize] = true;
1280 } else {
1281 neg[l.var() as usize] = true;
1282 }
1283 }
1284 }
1285 let pure = (0..num_vars).find_map(|v| match (pos[v], neg[v]) {
1286 (true, false) => Some(Lit::new(v as u32, true)),
1287 (false, true) => Some(Lit::new(v as u32, false)),
1288 _ => None,
1289 });
1290 let Some(l) = pure else { break };
1291 assigned.push(l);
1292 current.retain(|c| !c.iter().any(|&x| x == l));
1293 }
1294 (current, assigned)
1295}
1296
1297pub fn components(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Vec<Lit>>> {
1302 fn find(parent: &mut [usize], mut x: usize) -> usize {
1303 while parent[x] != x {
1304 parent[x] = parent[parent[x]];
1305 x = parent[x];
1306 }
1307 x
1308 }
1309 let mut parent: Vec<usize> = (0..num_vars.max(1)).collect();
1310 for clause in clauses {
1311 let vars: Vec<usize> = clause.iter().map(|l| l.var() as usize).collect();
1312 for pair in vars.windows(2) {
1313 let (a, b) = (find(&mut parent, pair[0]), find(&mut parent, pair[1]));
1314 parent[a] = b;
1315 }
1316 }
1317 let mut groups: HashMap<usize, Vec<Vec<Lit>>> = HashMap::new();
1318 for clause in clauses {
1319 let root = clause.first().map(|l| find(&mut parent, l.var() as usize)).unwrap_or(0);
1320 groups.entry(root).or_default().push(clause.clone());
1321 }
1322 groups.into_values().collect()
1323}
1324
1325pub fn decompose_and_crush(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
1329 components(num_vars, clauses).iter().any(|comp| {
1330 clauses_to_expr(comp).is_some_and(|e| {
1331 crate::pigeonhole::decide_pigeonhole_unsat(&e)
1332 || crate::xorsat::refute_via_parity(&e)
1333 || crate::pseudo_boolean::refute_clausal(&e)
1334 })
1335 })
1336}
1337
1338pub fn is_antipodally_symmetric(clauses: &[Vec<Lit>]) -> bool {
1344 let key = |c: &[Lit]| -> Vec<u32> {
1345 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
1346 k.sort_unstable();
1347 k.dedup();
1348 k
1349 };
1350 let original: BTreeSet<Vec<u32>> = clauses.iter().map(|c| key(c)).collect();
1351 let flipped: BTreeSet<Vec<u32>> = clauses
1352 .iter()
1353 .map(|c| key(&c.iter().map(|l| l.negated()).collect::<Vec<_>>()))
1354 .collect();
1355 original == flipped
1356}
1357
1358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1361pub struct LadderStats {
1362 pub nodes: usize,
1363 pub max_depth: usize,
1364 pub cut_closures: usize,
1365 pub pruned: usize,
1366}
1367
1368pub fn decide_laddered(num_vars: usize, clauses: &[Vec<Lit>]) -> (bool, LadderStats) {
1382 let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
1383 let sat = ladder(clauses, vec![None; num_vars], 0, &mut stats);
1384 (sat, stats)
1385}
1386
1387pub fn decide_laddered_sym(num_vars: usize, clauses: &[Vec<Lit>], use_cut: bool) -> (bool, LadderStats) {
1394 let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
1395 let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
1396 let sat = ladder_sym(clauses, vec![None; num_vars], 0, &generators, use_cut, &mut stats);
1397 (sat, stats)
1398}
1399
1400pub fn decide_laddered_nocut(num_vars: usize, clauses: &[Vec<Lit>]) -> (bool, LadderStats) {
1403 let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
1404 let sat = ladder_sym(clauses, vec![None; num_vars], 0, &[], false, &mut stats);
1405 (sat, stats)
1406}
1407
1408#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1411pub enum SearchCost {
1412 Decided { sat: bool, nodes: usize },
1413 Exceeded { budget: usize },
1414}
1415
1416pub fn search_cost(num_vars: usize, clauses: &[Vec<Lit>], use_cut: bool, budget: usize) -> SearchCost {
1422 let mut nodes = 0usize;
1423 match cost_rec(clauses, vec![None; num_vars], use_cut, budget, &mut nodes) {
1424 Some(sat) => SearchCost::Decided { sat, nodes },
1425 None => SearchCost::Exceeded { budget },
1426 }
1427}
1428
1429pub fn search_cost_antipodal(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> SearchCost {
1435 let mut nodes = 0usize;
1436 match antipodal_rec(clauses, vec![None; num_vars], budget, &mut nodes) {
1437 Some(sat) => SearchCost::Decided { sat, nodes },
1438 None => SearchCost::Exceeded { budget },
1439 }
1440}
1441
1442fn antipodal_rec(
1443 clauses: &[Vec<Lit>],
1444 assignment: Vec<Option<bool>>,
1445 budget: usize,
1446 nodes: &mut usize,
1447) -> Option<bool> {
1448 *nodes += 1;
1449 if *nodes > budget {
1450 return None;
1451 }
1452 let residual = restrict(clauses, &assignment);
1453 if residual.iter().any(|c| c.is_empty()) {
1454 return Some(false);
1455 }
1456 if residual.is_empty() {
1457 return Some(true);
1458 }
1459 let pivot = residual[0][0].var() as usize;
1460 let values: &[bool] = if is_antipodally_symmetric(&residual) {
1462 &[false]
1463 } else {
1464 &[false, true]
1465 };
1466 for &value in values {
1467 let mut next = assignment.clone();
1468 next[pivot] = Some(value);
1469 match antipodal_rec(clauses, next, budget, nodes) {
1470 Some(true) => return Some(true),
1471 Some(false) => {}
1472 None => return None,
1473 }
1474 }
1475 Some(false)
1476}
1477
1478fn cost_rec(
1479 clauses: &[Vec<Lit>],
1480 assignment: Vec<Option<bool>>,
1481 use_cut: bool,
1482 budget: usize,
1483 nodes: &mut usize,
1484) -> Option<bool> {
1485 *nodes += 1;
1486 if *nodes > budget {
1487 return None;
1488 }
1489 let residual = restrict(clauses, &assignment);
1490 if residual.iter().any(|c| c.is_empty()) {
1491 return Some(false);
1492 }
1493 if residual.is_empty() {
1494 return Some(true);
1495 }
1496 if use_cut {
1497 if let Some(e) = clauses_to_expr(&residual) {
1498 if crate::pigeonhole::decide_pigeonhole_unsat(&e)
1499 || crate::xorsat::refute_via_parity(&e)
1500 || crate::pseudo_boolean::refute_clausal(&e)
1501 {
1502 return Some(false);
1503 }
1504 }
1505 }
1506 let Some(pivot) = assignment.iter().position(|a| a.is_none()) else {
1507 return Some(true);
1508 };
1509 for value in [false, true] {
1510 let mut next = assignment.clone();
1511 next[pivot] = Some(value);
1512 match cost_rec(clauses, next, use_cut, budget, nodes) {
1513 Some(true) => return Some(true),
1514 Some(false) => {}
1515 None => return None,
1516 }
1517 }
1518 Some(false)
1519}
1520
1521fn ladder_sym(
1522 clauses: &[Vec<Lit>],
1523 assignment: Vec<Option<bool>>,
1524 depth: usize,
1525 generators: &[Perm],
1526 use_cut: bool,
1527 stats: &mut LadderStats,
1528) -> bool {
1529 stats.nodes += 1;
1530 stats.max_depth = stats.max_depth.max(depth);
1531 let residual = restrict(clauses, &assignment);
1532 if residual.iter().any(|c| c.is_empty()) {
1533 return false;
1534 }
1535 if residual.is_empty() {
1536 return true;
1537 }
1538 if use_cut {
1539 if let Some(e) = clauses_to_expr(&residual) {
1540 if crate::pigeonhole::decide_pigeonhole_unsat(&e)
1541 || crate::xorsat::refute_via_parity(&e)
1542 || crate::pseudo_boolean::refute_clausal(&e)
1543 {
1544 stats.cut_closures += 1;
1545 return false;
1546 }
1547 }
1548 }
1549 let Some(pivot) = assignment.iter().position(|a| a.is_none()) else {
1551 return true;
1552 };
1553 for value in [false, true] {
1554 let mut next = assignment.clone();
1555 next[pivot] = Some(value);
1556 if violates_lex_leader(&next, generators) {
1557 stats.pruned += 1;
1558 continue; }
1560 if ladder_sym(clauses, next, depth + 1, generators, use_cut, stats) {
1561 return true;
1562 }
1563 }
1564 false
1565}
1566
1567fn violates_lex_leader(a: &[Option<bool>], generators: &[Perm]) -> bool {
1572 let n = a.len();
1573 for sigma in generators {
1574 let mut b = vec![None; n];
1575 for v in 0..n {
1576 if let Some(val) = a[v] {
1577 let image = sigma.apply(Lit::pos(v as u32));
1578 b[image.var() as usize] = Some(if image.is_positive() { val } else { !val });
1579 }
1580 }
1581 if (0..n).any(|v| a[v].is_some() != b[v].is_some()) {
1583 continue;
1584 }
1585 for v in 0..n {
1587 if let (Some(av), Some(bv)) = (a[v], b[v]) {
1588 if av != bv {
1589 if !bv {
1590 return true;
1591 }
1592 break;
1593 }
1594 }
1595 }
1596 }
1597 false
1598}
1599
1600fn ladder(
1601 clauses: &[Vec<Lit>],
1602 assignment: Vec<Option<bool>>,
1603 depth: usize,
1604 stats: &mut LadderStats,
1605) -> bool {
1606 stats.nodes += 1;
1607 stats.max_depth = stats.max_depth.max(depth);
1608 let residual = restrict(clauses, &assignment);
1609 if residual.iter().any(|c| c.is_empty()) {
1610 return false; }
1612 if residual.is_empty() {
1613 return true; }
1615 if let Some(unit) = residual.iter().find(|c| c.len() == 1) {
1617 let l = unit[0];
1618 let mut next = assignment.clone();
1619 next[l.var() as usize] = Some(l.is_positive());
1620 return ladder(clauses, next, depth + 1, stats);
1621 }
1622 if let Some(e) = clauses_to_expr(&residual) {
1624 if crate::pigeonhole::decide_pigeonhole_unsat(&e)
1625 || crate::xorsat::refute_via_parity(&e)
1626 || crate::pseudo_boolean::refute_clausal(&e)
1627 {
1628 stats.cut_closures += 1;
1629 return false;
1630 }
1631 }
1632 let pivot = residual[0][0].var() as usize;
1634 for value in [false, true] {
1635 let mut next = assignment.clone();
1636 next[pivot] = Some(value);
1637 if ladder(clauses, next, depth + 1, stats) {
1638 return true;
1639 }
1640 }
1641 false
1642}
1643
1644#[derive(Clone, Debug, PartialEq, Eq)]
1647pub enum CoverVerdict {
1648 Total { cut: Option<Shadow> },
1652 Escapes,
1654 Unknown,
1656}
1657
1658impl Cover {
1659 pub fn auto_certify(&self) -> CoverVerdict {
1665 let Some(e) = self.to_expr() else { return CoverVerdict::Unknown };
1666 if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
1667 return CoverVerdict::Total { cut: Some(Shadow::Counting) };
1668 }
1669 if crate::xorsat::refute_via_parity(&e) {
1670 return CoverVerdict::Total { cut: Some(Shadow::Parity) };
1671 }
1672 if crate::pseudo_boolean::refute_clausal(&e) {
1673 return CoverVerdict::Total { cut: Some(Shadow::CuttingPlanes) };
1674 }
1675 match crate::sat::prove_unsat(&e) {
1676 crate::sat::UnsatOutcome::Refuted => CoverVerdict::Total { cut: None },
1677 crate::sat::UnsatOutcome::Sat(_) => CoverVerdict::Escapes,
1678 crate::sat::UnsatOutcome::Unsupported => CoverVerdict::Unknown,
1679 }
1680 }
1681}
1682
1683#[derive(Clone, Debug, PartialEq, Eq)]
1687pub struct FamilySignature {
1688 pub num_vars: usize,
1689 pub clauses: usize,
1690 pub rule_types: usize,
1691 pub shadow: Option<Shadow>,
1692}
1693
1694pub fn abstract_signature(num_vars: usize, clauses: &[Vec<Lit>]) -> FamilySignature {
1700 let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
1701 let rule_types = clause_orbits(clauses, &generators).len();
1702 let shadow = clauses_to_expr(clauses).and_then(|e| {
1703 if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
1704 Some(Shadow::Counting)
1705 } else if crate::xorsat::refute_via_parity(&e) {
1706 Some(Shadow::Parity)
1707 } else if crate::pseudo_boolean::refute_clausal(&e) {
1708 Some(Shadow::CuttingPlanes)
1709 } else {
1710 None
1711 }
1712 });
1713 FamilySignature { num_vars, clauses: clauses.len(), rule_types, shadow }
1714}
1715
1716pub fn restrict(clauses: &[Vec<Lit>], assignment: &[Option<bool>]) -> Vec<Vec<Lit>> {
1729 let mut out = Vec::new();
1730 'clause: for c in clauses {
1731 let mut residual = Vec::new();
1732 for &l in c {
1733 match assignment.get(l.var() as usize).copied().flatten() {
1734 Some(value) => {
1735 if value == l.is_positive() {
1736 continue 'clause; }
1738 }
1740 None => residual.push(l),
1741 }
1742 }
1743 out.push(residual);
1744 }
1745 out
1746}
1747
1748fn to_twosat_lit(l: Lit) -> crate::twosat::Lit {
1749 if l.is_positive() {
1750 crate::twosat::Lit::pos(l.var() as usize)
1751 } else {
1752 crate::twosat::Lit::neg(l.var() as usize)
1753 }
1754}
1755
1756pub fn decide_2sat(clauses: &[Vec<Lit>], num_vars: usize) -> bool {
1759 let mut pairs = Vec::with_capacity(clauses.len());
1760 for c in clauses {
1761 match c.as_slice() {
1762 [] => return false,
1763 [a] => pairs.push((to_twosat_lit(*a), to_twosat_lit(*a))),
1764 [a, b] => pairs.push((to_twosat_lit(*a), to_twosat_lit(*b))),
1765 _ => panic!("decide_2sat given a width-{} clause", c.len()),
1766 }
1767 }
1768 matches!(crate::twosat::solve(&pairs, num_vars), crate::twosat::TwoSatOutcome::Sat(_))
1769}
1770
1771pub fn greedy_2sat_backdoor(clauses: &[Vec<Lit>], num_vars: usize) -> Vec<usize> {
1776 let mut chosen = vec![false; num_vars];
1777 let mut backdoor = Vec::new();
1778 loop {
1779 let mut freq = vec![0usize; num_vars];
1780 let mut any_wide = false;
1781 for c in clauses {
1782 let free = c.iter().filter(|l| !chosen[l.var() as usize]).count();
1783 if free > 2 {
1784 any_wide = true;
1785 for l in c {
1786 if !chosen[l.var() as usize] {
1787 freq[l.var() as usize] += 1;
1788 }
1789 }
1790 }
1791 }
1792 if !any_wide {
1793 break;
1794 }
1795 let best = (0..num_vars).max_by_key(|&v| freq[v]).unwrap();
1796 chosen[best] = true;
1797 backdoor.push(best);
1798 }
1799 backdoor.sort_unstable();
1800 backdoor
1801}
1802
1803pub fn is_strong_backdoor_to_2sat(clauses: &[Vec<Lit>], num_vars: usize, backdoor: &[usize]) -> bool {
1806 let k = backdoor.len();
1807 if k > 24 {
1808 return false;
1809 }
1810 for mask in 0u32..(1u32 << k) {
1811 let mut assignment = vec![None; num_vars];
1812 for (i, &v) in backdoor.iter().enumerate() {
1813 assignment[v] = Some(mask & (1 << i) != 0);
1814 }
1815 if restrict(clauses, &assignment).iter().any(|c| c.len() > 2) {
1816 return false;
1817 }
1818 }
1819 true
1820}
1821
1822pub fn decide_sat_via_2sat_backdoor(clauses: &[Vec<Lit>], num_vars: usize, backdoor: &[usize]) -> bool {
1827 for mask in 0u32..(1u32 << backdoor.len()) {
1828 let mut assignment = vec![None; num_vars];
1829 for (i, &v) in backdoor.iter().enumerate() {
1830 assignment[v] = Some(mask & (1 << i) != 0);
1831 }
1832 let residual = restrict(clauses, &assignment);
1833 if decide_2sat(&residual, num_vars) {
1834 return true;
1835 }
1836 }
1837 false
1838}
1839
1840pub fn canonical_blocker(b: &Subcube, generators: &[CubeSym]) -> Subcube {
1844 let mut best = *b;
1845 let mut seen = BTreeSet::new();
1846 seen.insert(*b);
1847 let mut stack = vec![*b];
1848 while let Some(x) = stack.pop() {
1849 for g in generators {
1850 let y = g.map_subcube(&x);
1851 if seen.insert(y) {
1852 if y < best {
1853 best = y;
1854 }
1855 stack.push(y);
1856 }
1857 }
1858 }
1859 best
1860}
1861
1862pub fn blocker_orbit(b: &Subcube, generators: &[CubeSym]) -> Vec<Subcube> {
1866 let mut seen = BTreeSet::new();
1867 seen.insert(*b);
1868 let mut stack = vec![*b];
1869 while let Some(x) = stack.pop() {
1870 for g in generators {
1871 let y = g.map_subcube(&x);
1872 if seen.insert(y) {
1873 stack.push(y);
1874 }
1875 }
1876 }
1877 seen.into_iter().collect()
1878}
1879
1880pub fn symmetric_resolution_closure(
1892 cover: &Cover,
1893 generators: &[CubeSym],
1894 max_rounds: usize,
1895 max_reps: usize,
1896) -> (usize, bool) {
1897 let empty = Subcube { n: cover.n, care: 0, value: 0 };
1898 let mut reps: BTreeSet<Subcube> =
1899 cover.blockers.iter().map(|b| canonical_blocker(b, generators)).collect();
1900 let mut refuted = reps.contains(&empty);
1901 for _ in 0..max_rounds {
1902 if refuted {
1903 break;
1904 }
1905 let current: Vec<Subcube> = reps.iter().copied().collect();
1906 let orbits: Vec<Vec<Subcube>> =
1907 current.iter().map(|d| blocker_orbit(d, generators)).collect();
1908 let mut added = false;
1909 'outer: for c in ¤t {
1910 for orbit in &orbits {
1911 for image in orbit {
1912 if let Some((_, r)) = c.resolve(image) {
1913 let canon = canonical_blocker(&r, generators);
1914 if reps.insert(canon) {
1915 added = true;
1916 if canon == empty {
1917 refuted = true;
1918 break 'outer;
1919 }
1920 if reps.len() > max_reps {
1921 break 'outer;
1922 }
1923 }
1924 }
1925 }
1926 }
1927 }
1928 if !added {
1929 break;
1930 }
1931 }
1932 (reps.len(), refuted)
1933}
1934
1935pub fn symmetric_resolution_growth(
1943 cover: &Cover,
1944 generators: &[CubeSym],
1945 rounds: usize,
1946) -> Vec<(usize, usize)> {
1947 let mut raw: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
1948 let mut out = Vec::new();
1949 for _ in 0..rounds {
1950 let current: Vec<Subcube> = raw.iter().copied().collect();
1951 for i in 0..current.len() {
1952 for j in (i + 1)..current.len() {
1953 if let Some((_, r)) = current[i].resolve(¤t[j]) {
1954 raw.insert(r);
1955 }
1956 }
1957 }
1958 let orbits: BTreeSet<Subcube> =
1959 raw.iter().map(|b| canonical_blocker(b, generators)).collect();
1960 out.push((raw.len(), orbits.len()));
1961 }
1962 out
1963}
1964
1965pub fn backdoor_branch_orbit_count(backdoor: &[usize], generators: &[Perm]) -> u64 {
1971 let k = backdoor.len();
1972 let position: HashMap<usize, usize> = backdoor.iter().enumerate().map(|(i, &v)| (v, i)).collect();
1973 let mut induced: Vec<(Vec<usize>, u32)> = Vec::new();
1974 for g in generators {
1975 let mut perm = vec![0usize; k];
1976 let mut flip = 0u32;
1977 let mut preserves = true;
1978 for (i, &v) in backdoor.iter().enumerate() {
1979 let image = g.apply(Lit::pos(v as u32));
1980 match position.get(&(image.var() as usize)) {
1981 Some(&j) => {
1982 perm[i] = j;
1983 if !image.is_positive() {
1984 flip |= 1 << j;
1985 }
1986 }
1987 None => {
1988 preserves = false;
1989 break;
1990 }
1991 }
1992 }
1993 if preserves {
1994 induced.push((perm, flip));
1995 }
1996 }
1997 let total = 1u32 << k;
1998 let mut seen = vec![false; total as usize];
1999 let mut orbits = 0u64;
2000 for start in 0..total {
2001 if seen[start as usize] {
2002 continue;
2003 }
2004 orbits += 1;
2005 let mut stack = vec![start];
2006 seen[start as usize] = true;
2007 while let Some(m) = stack.pop() {
2008 for (perm, flip) in &induced {
2009 let mut image = 0u32;
2010 for i in 0..k {
2011 if m & (1 << i) != 0 {
2012 image |= 1 << perm[i];
2013 }
2014 }
2015 image ^= flip;
2016 if !seen[image as usize] {
2017 seen[image as usize] = true;
2018 stack.push(image);
2019 }
2020 }
2021 }
2022 }
2023 orbits
2024}
2025
2026pub fn clauses_to_expr(clauses: &[Vec<Lit>]) -> Option<crate::ProofExpr> {
2029 use crate::ProofExpr;
2030 let lit = |l: &Lit| {
2031 let a = ProofExpr::Atom(format!("x{}", l.var()));
2032 if l.is_positive() { a } else { ProofExpr::Not(Box::new(a)) }
2033 };
2034 fn balanced(
2039 mut nodes: Vec<ProofExpr>,
2040 combine: impl Fn(Box<ProofExpr>, Box<ProofExpr>) -> ProofExpr,
2041 ) -> ProofExpr {
2042 while nodes.len() > 1 {
2043 let mut next = Vec::with_capacity((nodes.len() + 1) / 2);
2044 let mut it = nodes.into_iter();
2045 while let Some(a) = it.next() {
2046 match it.next() {
2047 Some(b) => next.push(combine(Box::new(a), Box::new(b))),
2048 None => next.push(a),
2049 }
2050 }
2051 nodes = next;
2052 }
2053 nodes.into_iter().next().expect("balanced() requires a non-empty node list")
2054 }
2055 let mut built = Vec::with_capacity(clauses.len());
2056 for c in clauses {
2057 if c.is_empty() {
2058 return None;
2059 }
2060 let lits: Vec<ProofExpr> = c.iter().map(|l| lit(l)).collect();
2061 built.push(balanced(lits, |a, b| ProofExpr::Or(a, b)));
2062 }
2063 if built.is_empty() {
2064 return None;
2065 }
2066 Some(balanced(built, |a, b| crate::ProofExpr::And(a, b)))
2067}
2068
2069#[derive(Clone, Debug)]
2074pub struct CubeSym {
2075 pub perm: Vec<usize>,
2076 pub flip: Vec<bool>,
2077}
2078
2079impl CubeSym {
2080 pub fn identity(n: usize) -> CubeSym {
2082 CubeSym { perm: (0..n).collect(), flip: vec![false; n] }
2083 }
2084
2085 pub fn map_corner(&self, c: Corner) -> Corner {
2088 let mut out = 0u64;
2089 for j in 0..self.perm.len() {
2090 let mut bit = (c >> j) & 1;
2091 if self.flip[j] {
2092 bit ^= 1;
2093 }
2094 out |= bit << self.perm[j];
2095 }
2096 out
2097 }
2098
2099 pub fn map_subcube(&self, s: &Subcube) -> Subcube {
2102 let mut care = 0u64;
2103 let mut value = 0u64;
2104 for j in 0..self.perm.len() {
2105 if s.care & (1u64 << j) != 0 {
2106 let pj = self.perm[j];
2107 care |= 1u64 << pj;
2108 let mut bit = (s.value >> j) & 1;
2109 if self.flip[j] {
2110 bit ^= 1;
2111 }
2112 value |= bit << pj;
2113 }
2114 }
2115 Subcube { n: s.n, care, value }
2116 }
2117
2118 pub fn map_fractional(&self, point: &[f64]) -> Vec<f64> {
2121 let mut out = vec![0.0; self.perm.len()];
2122 for j in 0..self.perm.len() {
2123 out[self.perm[j]] = if self.flip[j] { 1.0 - point[j] } else { point[j] };
2124 }
2125 out
2126 }
2127
2128 pub fn compose(&self, other: &CubeSym) -> CubeSym {
2130 let n = self.perm.len();
2131 let mut perm = vec![0usize; n];
2132 let mut flip = vec![false; n];
2133 for j in 0..n {
2134 let mid = other.perm[j];
2135 perm[j] = self.perm[mid];
2136 flip[j] = other.flip[j] ^ self.flip[mid];
2137 }
2138 CubeSym { perm, flip }
2139 }
2140
2141 pub fn is_automorphism(&self, cover: &Cover) -> bool {
2145 let original: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
2146 let mapped: BTreeSet<Subcube> = cover.blockers.iter().map(|b| self.map_subcube(b)).collect();
2147 original == mapped
2148 }
2149}
2150
2151fn group_closure(generators: &[CubeSym], n: usize) -> Vec<CubeSym> {
2154 let id = CubeSym::identity(n);
2155 let key = |g: &CubeSym| (g.perm.clone(), g.flip.clone());
2156 let mut seen: BTreeSet<(Vec<usize>, Vec<bool>)> = [key(&id)].into_iter().collect();
2157 let mut group = vec![id];
2158 let mut i = 0;
2159 while i < group.len() {
2160 let g = group[i].clone();
2161 i += 1;
2162 for s in generators {
2163 let h = s.compose(&g);
2164 if seen.insert(key(&h)) {
2165 group.push(h);
2166 }
2167 }
2168 if group.len() > 200_000 {
2169 break;
2170 }
2171 }
2172 group
2173}
2174
2175pub fn burnside_corner_orbits(n: usize, generators: &[CubeSym]) -> u64 {
2179 let group = group_closure(generators, n);
2180 let fixed_total: u128 = group
2181 .iter()
2182 .map(|g| (0u64..(1u64 << n)).filter(|&c| g.map_corner(c) == c).count() as u128)
2183 .sum();
2184 (fixed_total / group.len() as u128) as u64
2185}
2186
2187pub fn walsh_hadamard_energy(cover: &Cover) -> Vec<f64> {
2191 let size = 1usize << cover.n;
2192 let f: Vec<f64> = (0..size as u64).map(|x| cover.vertex_energy(x) as f64).collect();
2193 (0..size)
2194 .map(|s| {
2195 let acc: f64 = (0..size)
2196 .map(|x| {
2197 if ((s & x) as u64).count_ones() % 2 == 0 { f[x] } else { -f[x] }
2198 })
2199 .sum();
2200 acc / size as f64
2201 })
2202 .collect()
2203}
2204
2205pub fn face_vector(cover: &Cover) -> std::collections::BTreeMap<usize, usize> {
2209 let mut fv = std::collections::BTreeMap::new();
2210 for b in &cover.blockers {
2211 *fv.entry(b.dimension()).or_insert(0) += 1;
2212 }
2213 fv
2214}
2215
2216pub fn orbit_representatives(n: usize, generators: &[CubeSym]) -> Vec<Corner> {
2222 let total = 1u64 << n;
2223 let mut seen = vec![false; total as usize];
2224 let mut reps = Vec::new();
2225 for start in 0u64..total {
2226 if seen[start as usize] {
2227 continue;
2228 }
2229 reps.push(start);
2230 let mut stack = vec![start];
2231 seen[start as usize] = true;
2232 while let Some(c) = stack.pop() {
2233 for g in generators {
2234 let d = g.map_corner(c);
2235 if !seen[d as usize] {
2236 seen[d as usize] = true;
2237 stack.push(d);
2238 }
2239 }
2240 }
2241 }
2242 reps
2243}
2244
2245pub fn orbit_count(n: usize, generators: &[CubeSym]) -> u64 {
2247 orbit_representatives(n, generators).len() as u64
2248}
2249
2250pub fn is_total_via_orbits(cover: &Cover, generators: &[CubeSym]) -> Option<bool> {
2255 if !generators.iter().all(|g| g.is_automorphism(cover)) {
2256 return None;
2257 }
2258 let reps = orbit_representatives(cover.n, generators);
2259 Some(reps.iter().all(|&c| cover.blocks(c)))
2260}
2261
2262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2264pub struct CollapseStep {
2265 pub generators_used: usize,
2266 pub orbits: u64,
2267}
2268
2269pub fn collapse_curve(n: usize, generators: &[CubeSym]) -> Vec<CollapseStep> {
2273 let mut steps = Vec::with_capacity(generators.len() + 1);
2274 steps.push(CollapseStep { generators_used: 0, orbits: 1u64 << n });
2275 for k in 1..=generators.len() {
2276 steps.push(CollapseStep {
2277 generators_used: k,
2278 orbits: orbit_count(n, &generators[..k]),
2279 });
2280 }
2281 steps
2282}
2283
2284pub fn php_cover(n: usize) -> Cover {
2287 let (cnf, _) = crate::families::php(n);
2288 Cover::of_cnf(&cnf)
2289}
2290
2291pub fn php_symmetries(n: usize) -> Vec<CubeSym> {
2295 let holes = n.saturating_sub(1);
2296 let num_vars = n * holes;
2297 let var = |p: usize, h: usize| p * holes + h;
2298 let mut gens = Vec::new();
2299 for p in 0..n.saturating_sub(1) {
2301 let mut perm: Vec<usize> = (0..num_vars).collect();
2302 for h in 0..holes {
2303 perm.swap(var(p, h), var(p + 1, h));
2304 }
2305 gens.push(CubeSym { perm, flip: vec![false; num_vars] });
2306 }
2307 for h in 0..holes.saturating_sub(1) {
2309 let mut perm: Vec<usize> = (0..num_vars).collect();
2310 for p in 0..n {
2311 perm.swap(var(p, h), var(p, h + 1));
2312 }
2313 gens.push(CubeSym { perm, flip: vec![false; num_vars] });
2314 }
2315 gens
2316}
2317
2318pub fn hyperoctahedral_generators(n: usize) -> Vec<CubeSym> {
2325 let mut gens = Vec::new();
2326 for i in 0..n.saturating_sub(1) {
2327 let mut perm: Vec<usize> = (0..n).collect();
2328 perm.swap(i, i + 1);
2329 gens.push(CubeSym { perm, flip: vec![false; n] });
2330 }
2331 if n > 0 {
2332 let mut flip = vec![false; n];
2333 flip[0] = true;
2334 gens.push(CubeSym { perm: (0..n).collect(), flip });
2335 }
2336 gens
2337}
2338
2339pub fn cube_group_closure(generators: &[CubeSym], n: usize) -> Vec<CubeSym> {
2343 group_closure(generators, n)
2344}
2345
2346pub fn min_resolution_width(cover: &Cover) -> Option<usize> {
2355 let n = cover.n;
2356 let empty = Subcube { n, care: 0, value: 0 };
2357 for w in 0..=n {
2358 let mut set: BTreeSet<Subcube> = cover
2359 .blockers
2360 .iter()
2361 .copied()
2362 .filter(|b| b.care.count_ones() as usize <= w)
2363 .collect();
2364 if set.contains(&empty) {
2365 return Some(w);
2366 }
2367 loop {
2368 let snapshot: Vec<Subcube> = set.iter().copied().collect();
2369 let mut added = false;
2370 for i in 0..snapshot.len() {
2371 for j in (i + 1)..snapshot.len() {
2372 if let Some((_, r)) = snapshot[i].resolve(&snapshot[j]) {
2373 if r.care.count_ones() as usize <= w && set.insert(r) {
2374 added = true;
2375 }
2376 }
2377 }
2378 }
2379 if set.contains(&empty) {
2380 return Some(w);
2381 }
2382 if !added {
2383 break;
2384 }
2385 }
2386 }
2387 None
2388}
2389
2390fn cover_key(blockers: &[Subcube]) -> Vec<Subcube> {
2393 let mut k = blockers.to_vec();
2394 k.sort_unstable();
2395 k.dedup();
2396 k
2397}
2398
2399fn canonical_key(blockers: &[Subcube], group: &[CubeSym]) -> Vec<Subcube> {
2404 group
2405 .iter()
2406 .map(|g| cover_key(&blockers.iter().map(|b| g.map_subcube(b)).collect::<Vec<_>>()))
2407 .min()
2408 .unwrap_or_else(|| cover_key(blockers))
2409}
2410
2411pub fn canonical_cover(cover: &Cover, generators: &[CubeSym]) -> (Vec<Subcube>, usize) {
2417 let group = group_closure(generators, cover.n);
2418 let images: BTreeSet<Vec<Subcube>> = group
2419 .iter()
2420 .map(|g| cover_key(&cover.blockers.iter().map(|b| g.map_subcube(b)).collect::<Vec<_>>()))
2421 .collect();
2422 let best = images.iter().next().cloned().unwrap_or_else(|| cover_key(&cover.blockers));
2423 (best, images.len())
2424}
2425
2426pub fn minimal_cover_orbits(n: usize) -> Vec<Cover> {
2435 let generators = hyperoctahedral_generators(n);
2436 let group = group_closure(&generators, n); let mut orbits: HashMap<Vec<Subcube>, Cover> = HashMap::new();
2438 let mut visited: BTreeSet<Vec<Subcube>> = BTreeSet::new();
2444 let mut chosen: Vec<Subcube> = Vec::new();
2445 enumerate_minimal_covers(n, &mut chosen, &group, &mut visited, &mut orbits);
2446 let mut out: Vec<Cover> = orbits.into_values().collect();
2447 out.sort_by(|a, b| cover_key(&a.blockers).cmp(&cover_key(&b.blockers)));
2448 out
2449}
2450
2451fn blocker_is_redundant(blockers: &[Subcube], i: usize) -> bool {
2455 blockers[i].footprint().iter().all(|&c| {
2456 blockers
2457 .iter()
2458 .enumerate()
2459 .any(|(j, b)| j != i && b.covers(c))
2460 })
2461}
2462
2463fn enumerate_minimal_covers(
2464 n: usize,
2465 chosen: &mut Vec<Subcube>,
2466 group: &[CubeSym],
2467 visited: &mut BTreeSet<Vec<Subcube>>,
2468 orbits: &mut HashMap<Vec<Subcube>, Cover>,
2469) {
2470 if (0..chosen.len()).any(|i| blocker_is_redundant(chosen, i)) {
2472 return;
2473 }
2474 let canon = canonical_key(chosen, group);
2477 if !visited.insert(canon.clone()) {
2478 return;
2479 }
2480 let cover = Cover { n, blockers: chosen.clone() };
2481 match cover.escaping_corner() {
2482 None => {
2483 orbits.entry(canon).or_insert(cover);
2486 }
2487 Some(c) => {
2488 for care in 1u64..(1u64 << n) {
2495 let blocker = Subcube { n, care, value: c & care };
2496 if !chosen.contains(&blocker) {
2497 chosen.push(blocker);
2498 enumerate_minimal_covers(n, chosen, group, visited, orbits);
2499 chosen.pop();
2500 }
2501 }
2502 }
2503 }
2504}
2505
2506#[cfg(test)]
2507mod tests {
2508 use super::*;
2509 use crate::cdcl::Lit;
2510
2511 #[test]
2522 fn mod_p_one_hot_instances_land_on_the_modcount_rung_of_the_extended_ladder() {
2523 let (_eqs, cnf, _) = crate::families::mod_p_tseitin_expander(4, 3, 0xC0DE);
2524 assert_eq!(
2525 weakest_crushing_rung(cnf.num_vars, &cnf.clauses, 3),
2526 ProofRung::BeyondBudget,
2527 "legacy: the GF(2) ladder cannot place the mod-3 instance (regression pin)"
2528 );
2529 assert_eq!(
2530 weakest_crushing_rung_with_char(cnf.num_vars, &cnf.clauses, 3, &[3]),
2531 ProofRung::ModCount { p: 3 },
2532 "extended: the characteristic rung fires on the recovered, re-checked GF(3) refutation"
2533 );
2534 assert_eq!(
2535 weakest_crushing_rung_with_char(cnf.num_vars, &cnf.clauses, 3, &[5, 7]),
2536 ProofRung::BeyondBudget,
2537 "the rung is per-prime: without p = 3 enabled the verdict is the legacy one"
2538 );
2539 let (php3, _) = crate::families::php(3);
2541 let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = vec![(php3.num_vars, php3.clauses)];
2542 for cover in minimal_cover_orbits(2) {
2543 corpus.push((2, cover.clauses()));
2544 }
2545 for cover in minimal_cover_orbits(3).into_iter().take(12) {
2546 corpus.push((3, cover.clauses()));
2547 }
2548 for (nv, clauses) in &corpus {
2549 let legacy = weakest_crushing_rung(*nv, clauses, *nv);
2550 assert_eq!(
2551 weakest_crushing_rung_with_char(*nv, clauses, *nv, &[]),
2552 legacy,
2553 "no primes ⟹ the extended cascade IS the legacy cascade"
2554 );
2555 assert_eq!(
2556 weakest_crushing_rung_with_char(*nv, clauses, *nv, &[3, 5, 7]),
2557 legacy,
2558 "non-one-hot instances are placed identically with the characteristic rungs enabled"
2559 );
2560 }
2561 }
2562
2563 #[test]
2566 fn blocker_is_exactly_the_falsifying_corners() {
2567 let clause = vec![Lit::new(0, true), Lit::new(2, false)];
2569 let b = Subcube::blocker(&clause, 3);
2570 let blocked: BTreeSet<Corner> = b.footprint().into_iter().collect();
2571
2572 let mut expected = BTreeSet::new();
2573 for c in 0u64..8 {
2574 let x0 = c & 1 != 0;
2575 let x2 = c & 4 != 0;
2576 let clause_true = x0 || !x2;
2577 if !clause_true {
2578 expected.insert(c);
2579 }
2580 }
2581 assert_eq!(blocked, expected, "blocker must be the precise falsifying set");
2582 assert_eq!(b.footprint_card(), expected.len() as u64);
2583 assert_eq!(b.dimension(), 1, "3 vars, 2 fixed ⟹ 1 free coordinate");
2584 }
2585
2586 #[test]
2588 fn blocker_dimension_is_codimension_of_clause_width() {
2589 for n in 4..8 {
2590 for w in 1..=4.min(n) {
2591 let clause: Vec<Lit> = (0..w).map(|v| Lit::new(v as u32, v % 2 == 0)).collect();
2592 let b = Subcube::blocker(&clause, n);
2593 assert_eq!(b.dimension(), n - w);
2594 assert_eq!(b.footprint_card(), 1u64 << (n - w));
2595 }
2596 }
2597 }
2598
2599 #[test]
2603 fn cover_is_total_iff_formula_is_unsat() {
2604 for n in 2..=4 {
2605 let cover = php_cover(n);
2606 assert!(cover.is_total(), "PHP({n}) blockers must cover the whole hypercube");
2607 assert_eq!(cover.escaping_corner(), None);
2608 assert_eq!(cover.solution_count(), 0);
2609 }
2610
2611 let sat = DimacsCnf {
2613 num_vars: 3,
2614 clauses: vec![vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)]],
2615 };
2616 let cover = Cover::of_cnf(&sat);
2617 assert!(!cover.is_total());
2618 let model = cover.escaping_corner().expect("a satisfiable cover must leave a corner free");
2619 assert_eq!(cover.vertex_energy(model), 0, "an escaping corner has energy zero");
2620 assert_eq!(cover.blocks(0), true, "the all-false corner is the unique falsifying corner");
2623 assert_eq!(model, 0b001, "the first escaping corner above the blocked all-false corner");
2624 assert_eq!(cover.solution_count(), 7);
2625 }
2626
2627 #[test]
2629 fn vertex_energy_zero_iff_satisfying() {
2630 let cnf = DimacsCnf {
2631 num_vars: 4,
2632 clauses: vec![
2633 vec![Lit::new(0, true), Lit::new(1, false)],
2634 vec![Lit::new(2, true), Lit::new(3, true)],
2635 vec![Lit::new(0, false), Lit::new(2, false)],
2636 ],
2637 };
2638 let cover = Cover::of_cnf(&cnf);
2639 for c in 0u64..16 {
2640 let satisfies = cnf.clauses.iter().all(|clause| {
2641 clause.iter().any(|lit| {
2642 let bit = (c >> lit.var() as u64) & 1 != 0;
2643 bit == lit.is_positive()
2644 })
2645 });
2646 assert_eq!(satisfies, cover.vertex_energy(c) == 0, "corner {c:04b}");
2647 }
2648 }
2649
2650 #[test]
2653 fn php_symmetries_are_automorphisms() {
2654 for n in 2..=5 {
2655 let cover = php_cover(n);
2656 for (k, g) in php_symmetries(n).iter().enumerate() {
2657 assert!(g.is_automorphism(&cover), "PHP({n}) generator {k} must be an automorphism");
2658 }
2659 }
2660 }
2661
2662 #[test]
2665 fn symmetry_acts_jointly_on_rules_and_solutions() {
2666 let cnf = DimacsCnf {
2668 num_vars: 3,
2669 clauses: vec![
2670 vec![Lit::new(0, true), Lit::new(2, true)],
2671 vec![Lit::new(1, true), Lit::new(2, true)],
2672 ],
2673 };
2674 let cover = Cover::of_cnf(&cnf);
2675 let swap = CubeSym { perm: vec![1, 0, 2], flip: vec![false; 3] };
2676 assert!(swap.is_automorphism(&cover), "swapping x0,x1 preserves the blocker set");
2677
2678 let blk: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
2680 let mapped: BTreeSet<Subcube> = cover.blockers.iter().map(|b| swap.map_subcube(b)).collect();
2681 assert_eq!(blk, mapped);
2682
2683 let solutions: BTreeSet<Corner> = (0u64..8).filter(|&c| !cover.blocks(c)).collect();
2685 let moved: BTreeSet<Corner> = solutions.iter().map(|&c| swap.map_corner(c)).collect();
2686 assert_eq!(solutions, moved, "the cover symmetry permutes solutions among themselves");
2687 }
2688
2689 #[test]
2695 fn paths_to_random_group_by_the_symmetry_of_the_step() {
2696 let php = crate::families::php(3).0;
2697 let n = php.num_vars;
2698 let generators = crate::symmetry_detect::find_generators(n, &php.clauses);
2699
2700 let mut candidates: Vec<Vec<Lit>> = Vec::new();
2702 for v in 0..n as u32 {
2703 for w in (v + 1)..n as u32 {
2704 for &sv in &[true, false] {
2705 for &sw in &[true, false] {
2706 candidates.push(vec![Lit::new(v, sv), Lit::new(w, sw)]);
2707 }
2708 }
2709 }
2710 }
2711
2712 let orbits = clause_orbits(&candidates, &generators);
2714 assert!(
2715 orbits.len() < candidates.len(),
2716 "{} step-orbits group {} candidate steps",
2717 orbits.len(),
2718 candidates.len()
2719 );
2720
2721 for orbit in &orbits {
2723 let auts: Vec<usize> = orbit
2724 .iter()
2725 .map(|&i| {
2726 let mut f = php.clauses.clone();
2727 f.push(candidates[i].clone());
2728 automorphism_group_size(n, &f)
2729 })
2730 .collect();
2731 assert!(
2732 auts.windows(2).all(|w| w[0] == w[1]),
2733 "same-orbit steps give isomorphic results (identical |Aut|): {auts:?}"
2734 );
2735 }
2736 }
2737
2738 #[test]
2743 fn information_theory_of_the_rigidity_cliff() {
2744 fn sm(s: &mut u64) -> u64 {
2745 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
2746 let mut z = *s;
2747 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2748 z ^ (z >> 31)
2749 }
2750 let php = crate::families::php(3).0;
2751 let n = php.num_vars;
2752 let mut clauses = php.clauses.clone();
2753 let mut state = 0x1F0E_0001u64;
2754 let mut bits = Vec::new();
2755 for _ in 0..=4 {
2756 bits.push(symmetry_entropy_bits(n, &clauses));
2757 let mut c: Vec<Lit> = Vec::new();
2758 while c.len() < 2 {
2759 let v = (sm(&mut state) % n as u64) as u32;
2760 if !c.iter().any(|l| l.var() == v) {
2761 c.push(Lit::new(v, sm(&mut state) % 2 == 0));
2762 }
2763 }
2764 clauses.push(c);
2765 }
2766 assert!(bits[0] > 3.0, "PHP(3) carries ~3.58 bits of symmetry: {bits:?}");
2767 assert_eq!(*bits.last().unwrap(), 0.0, "rigid = 0 bits of symmetry (incompressible)");
2768 for w in bits.windows(2) {
2769 assert!(w[1] <= w[0] + 1e-9, "symmetry-information only decreases: {bits:?}");
2770 }
2771 }
2772
2773 #[test]
2780 fn symmetry_bits_are_the_branches_you_can_cut() {
2781 let n = 3;
2782 let cover = php_cover(n); let gens = php_symmetries(n);
2784 let bits = symmetry_entropy_bits(cover.n, &crate::families::php(n).0.clauses);
2785 let orbits = orbit_count(cover.n, &gens);
2786 let full = 1u64 << cover.n;
2787 let reduction = full as f64 / orbits as f64;
2788
2789 assert!(reduction > 1.0, "symmetry cuts branches: {full} corners → {orbits} orbits");
2790 assert!(
2792 reduction.log2() <= bits + 1e-9,
2793 "branch-reduction {:.2} bits ≤ symmetry {bits:.2} bits (orbit-counting bound)",
2794 reduction.log2()
2795 );
2796 }
2797
2798 #[test]
2806 fn the_line_where_symmetry_becomes_rigidity() {
2807 use std::fmt::Write;
2808 fn sm(s: &mut u64) -> u64 {
2809 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
2810 let mut z = *s;
2811 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2812 z ^ (z >> 31)
2813 }
2814 let php = crate::families::php(3).0;
2815 let n = php.num_vars;
2816 let mut clauses = php.clauses.clone();
2817 let mut state = 0x11AE_0001u64;
2818 let mut chart = String::from("asymmetric added |Aut|\n");
2819 chart.push_str("---------------- -----\n");
2820 let mut rigid_at = None;
2821 for added in 0..=5 {
2822 let aut = automorphism_group_size(n, &clauses);
2823 let _ = writeln!(chart, "{added:>16} {aut:>5}");
2824 if aut == 1 && rigid_at.is_none() {
2825 rigid_at = Some(added);
2826 }
2827 let mut c: Vec<Lit> = Vec::new();
2828 while c.len() < 2 {
2829 let v = (sm(&mut state) % n as u64) as u32;
2830 if !c.iter().any(|l| l.var() == v) {
2831 c.push(Lit::new(v, sm(&mut state) % 2 == 0));
2832 }
2833 }
2834 clauses.push(c);
2835 }
2836 assert!(automorphism_group_size(n, &php.clauses) >= 6, "the base is symmetric");
2838 assert!(rigid_at.is_some(), "it becomes rigid (|Aut|=1) — the most asymmetric:\n{chart}");
2839
2840 println!("\n{chart}rigid at {rigid_at:?} asymmetric clauses\n");
2841 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2842 if std::fs::create_dir_all(&dir).is_ok() {
2843 let _ = std::fs::write(
2844 dir.join("rigidity_line.txt"),
2845 format!("THE LINE — |Aut| as asymmetric clauses are added to PHP(3). Symmetry falls off a cliff to\n1 (rigid = the most asymmetric) within a few clauses: every automorphism must fix every clause,\nso a few generic constraints pin the whole structure. rigid at {rigid_at:?} clauses.\n\n{chart}\n"),
2846 );
2847 }
2848 }
2849
2850 #[test]
2856 fn combinatorics_analysis_geometry_invariants_agree() {
2857 let n = 3;
2858 let cover = php_cover(n); let gens = php_symmetries(n);
2860
2861 let burnside = burnside_corner_orbits(cover.n, &gens);
2863 let walked = orbit_count(cover.n, &gens);
2864 assert_eq!(burnside, walked, "Burnside (fixed points) = orbit walk: {burnside} vs {walked}");
2865
2866 let hat = walsh_hadamard_energy(&cover);
2869 for g in &gens {
2870 for s in 0u64..(1 << cover.n) {
2871 let mut gs = 0u64;
2872 for v in 0..cover.n {
2873 if s & (1 << v) != 0 {
2874 gs |= 1 << g.perm[v];
2875 }
2876 }
2877 assert!(
2878 (hat[s as usize] - hat[gs as usize]).abs() < 1e-9,
2879 "Fourier coefficient constant on the symmetry orbit: S={s:06b} σS={gs:06b}"
2880 );
2881 }
2882 }
2883
2884 let fv = face_vector(&cover);
2886 for g in &gens {
2887 let moved = Cover {
2888 n: cover.n,
2889 blockers: cover.blockers.iter().map(|b| g.map_subcube(b)).collect(),
2890 };
2891 assert_eq!(face_vector(&moved), fv, "the f-vector is a geometric invariant under symmetry");
2892 }
2893 }
2894
2895 #[test]
2898 fn symmetry_collapses_the_cover_check_on_pigeonhole() {
2899 let n = 4; let cover = php_cover(n);
2901 let gens = php_symmetries(n);
2902
2903 let via_orbits =
2904 is_total_via_orbits(&cover, &gens).expect("php symmetries must all be automorphisms");
2905 assert_eq!(via_orbits, cover.is_total(), "orbit verdict must match brute force");
2906 assert!(via_orbits, "PHP(4) is UNSAT ⟹ the cover is total");
2907
2908 let orbits = orbit_count(cover.n, &gens);
2909 let corners = 1u64 << cover.n;
2910 assert!(orbits < corners, "{orbits} orbits must be fewer than {corners} corners");
2911 assert!(orbits * 8 < corners, "expected a >8× collapse, got {orbits} of {corners}");
2914 }
2915
2916 #[test]
2921 fn collapse_curve_is_monotone_and_compounds() {
2922 let n = 4;
2923 let cover = php_cover(n);
2924 let gens = php_symmetries(n);
2925 let curve = collapse_curve(cover.n, &gens);
2926
2927 assert_eq!(curve[0].orbits, 1u64 << cover.n, "starts at 2ⁿ with no symmetry");
2928 for w in curve.windows(2) {
2929 assert!(w[1].orbits <= w[0].orbits, "stacking a generator never grows the orbit count");
2930 }
2931 let first = curve[0].orbits;
2932 let last = curve.last().unwrap().orbits;
2933 assert!(last * 8 < first, "the stacked group collapses the check >8×: {first} → {last}");
2934 }
2935
2936 use crate::sat::UnsatOutcome;
2947
2948 #[test]
2953 fn certified_prover_decides_the_cover_both_ways() {
2954 for n in 2..=4 {
2955 let cover = php_cover(n);
2956 assert!(cover.has_no_hole(), "PHP({n}) cover is total");
2957 assert_eq!(
2958 cover.prove_total_certified(),
2959 UnsatOutcome::Refuted,
2960 "our prover must *certify* the PHP({n}) cover total, not just brute-force it"
2961 );
2962 }
2963 for (n, k) in [(3usize, 2usize), (4, 3)] {
2965 let (cnf, _) = crate::families::clique_coloring(n, k);
2966 let cover = Cover::of_cnf(&cnf);
2967 assert!(cover.has_no_hole(), "K_{n} needs >{k} colors ⟹ total cover");
2968 assert_eq!(cover.prove_total_certified(), UnsatOutcome::Refuted);
2969 }
2970 let sat = DimacsCnf { num_vars: 3, clauses: vec![vec![Lit::new(0, true), Lit::new(1, true)]] };
2972 let cover = Cover::of_cnf(&sat);
2973 assert!(!cover.has_no_hole());
2974 match cover.prove_total_certified() {
2975 UnsatOutcome::Sat(model) => {
2976 let mut corner = 0u64;
2977 for (name, val) in &model {
2978 if *val {
2979 let v: usize = name.trim_start_matches('x').parse().unwrap();
2980 corner |= 1 << v;
2981 }
2982 }
2983 assert_eq!(cover.vertex_energy(corner), 0, "the prover's model is an uncovered corner");
2984 }
2985 other => panic!("expected a model for a satisfiable cover, got {other:?}"),
2986 }
2987 }
2988
2989 #[test]
2992 fn three_clause_is_a_clean_three_bit_blocker() {
2993 let clause = vec![Lit::new(1, true), Lit::new(4, false), Lit::new(6, true)];
2994 let b = Subcube::blocker(&clause, 8);
2995 assert_eq!(b.care.count_ones(), 3, "support of a 3-clause is exactly 3 coordinates");
2996 assert_eq!(b.dimension(), 5, "n − 3 free coordinates (Blocker.freeCoordinates_card)");
2997 let recovered: BTreeSet<(usize, bool)> = b.clause_literals().into_iter().collect();
2998 let expected: BTreeSet<(usize, bool)> = [(1, true), (4, false), (6, true)].into_iter().collect();
2999 assert_eq!(recovered, expected, "blocker ↔ clause is a lossless round-trip");
3000 }
3001
3002 #[test]
3005 fn cube_symmetry_is_a_corner_bijection() {
3006 let s = CubeSym { perm: vec![2, 0, 3, 1], flip: vec![false, true, false, true] };
3007 let images: BTreeSet<Corner> = (0u64..16).map(|c| s.map_corner(c)).collect();
3008 assert_eq!(images.len(), 16, "map_corner is injective ⟹ a bijection on the 2ⁿ corners");
3009 assert_eq!(images, (0u64..16).collect::<BTreeSet<_>>());
3010 }
3011
3012 #[test]
3016 fn separator_iff_no_interaction_crosses_the_cut() {
3017 let cnf = DimacsCnf {
3018 num_vars: 5,
3019 clauses: vec![
3020 vec![Lit::new(0, true), Lit::new(1, true)],
3021 vec![Lit::new(2, true), Lit::new(3, false)],
3022 vec![Lit::new(3, true), Lit::new(4, true)],
3023 ],
3024 };
3025 let cover = Cover::of_cnf(&cnf);
3026 for cut in 0u64..(1 << 5) {
3027 let separated = cover.separated_by(cut);
3028 let no_cross = (0..5).all(|i| {
3029 (0..5).all(|j| {
3030 let i_in = cut & (1 << i) != 0;
3031 let j_in = cut & (1 << j) != 0;
3032 !(i_in && !j_in && cover.variable_interaction(i, j))
3033 })
3034 });
3035 assert_eq!(separated, no_cross, "cut {cut:05b}");
3036 }
3037 }
3038
3039 #[test]
3043 fn difficulty_classes_name_the_hole_count() {
3044 let one = DimacsCnf {
3046 num_vars: 3,
3047 clauses: vec![vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)]],
3048 };
3049 let cover = Cover::of_cnf(&one);
3050 assert!(!cover.has_no_hole());
3051 assert!(!cover.has_unique_hole());
3052 assert!(cover.has_at_least_holes(7));
3053 assert!(!cover.has_at_least_holes(8));
3054
3055 let mut clauses = Vec::new();
3057 for forbidden in 0u64..7 {
3058 let lits: Vec<Lit> =
3060 (0..3).map(|v| Lit::new(v as u32, forbidden & (1 << v) == 0)).collect();
3061 clauses.push(lits);
3062 }
3063 let unique = Cover::of_cnf(&DimacsCnf { num_vars: 3, clauses });
3064 assert!(unique.has_unique_hole(), "all corners but 0b111 forbidden ⟹ exactly one hole");
3065 assert_eq!(unique.escaping_corner(), Some(0b111));
3066
3067 assert!(php_cover(3).has_no_hole());
3069 }
3070
3071 #[test]
3081 fn pigeonhole_rules_collapse_to_two_orbits_at_every_scale() {
3082 for n in 2..=12 {
3083 let sig = pigeonhole_rule_symmetry(n);
3084 assert_eq!(
3085 sig.rule_orbits, 2,
3086 "PHP({n}): {} blockers must collapse to 2 rule-orbits, got {}",
3087 sig.blockers, sig.rule_orbits
3088 );
3089 }
3090 let big = pigeonhole_rule_symmetry(12);
3093 assert_eq!(big.n * (big.n - 1), 132, "12 pigeons × 11 holes = 132 variables (2^132 corners)");
3094 assert_eq!(big.rule_orbits, 2);
3095 assert!(big.blockers > 700, "{} blockers — large cover, two essential rules", big.blockers);
3096 }
3097
3098 #[test]
3101 fn the_detector_discovers_the_pigeonhole_rule_symmetry() {
3102 for n in 2..=5 {
3103 let sig = php_cover(n).discovered_rule_symmetry();
3104 assert!(sig.generators >= 1, "PHP({n}): the detector must find the grid symmetry");
3105 assert_eq!(sig.rule_orbits, 2, "PHP({n}): discovered rule-orbits = {}, expected 2", sig.rule_orbits);
3106 }
3107 }
3108
3109 #[test]
3115 fn random_3sat_rules_do_not_collapse_to_a_constant() {
3116 let cnf = crate::families::random_3sat(14, 40, 0xC0FFEE);
3117 let cover = Cover::of_cnf(&cnf);
3118 let sig = cover.discovered_rule_symmetry();
3119 assert!(sig.rule_orbits > 2, "random hardness has no constant-size rule symmetry: {sig:?}");
3120 assert!(
3121 sig.rule_orbits * 2 > sig.blockers,
3122 "random rules barely merge: {} orbits of {} blockers",
3123 sig.rule_orbits,
3124 sig.blockers
3125 );
3126 }
3127
3128 #[test]
3134 fn orbit_rep_engine_refutes_php3_but_does_not_scale() {
3135 let cover = php_cover(3);
3136 let gens = php_symmetries(3);
3137 let (orbit_types, refuted) = symmetric_resolution_closure(&cover, &gens, 40, 40_000);
3138 assert!(refuted, "the orbit-rep engine derives the empty clause for PHP(3)");
3139 assert_eq!(orbit_types, 12, "PHP(3) saturates at 12 orbit-types — the same as the raw closure");
3140 }
3141
3142 #[test]
3150 fn a_blocker_is_one_point_under_its_free_coordinate_symmetry() {
3151 let cover = php_cover(3);
3152 for b in &cover.blockers {
3153 let footprint: BTreeSet<Corner> = b.footprint().into_iter().collect();
3154 let free_bits: Vec<u64> =
3155 (0..b.n as u64).filter(|i| b.care & (1 << i) == 0).collect();
3156 assert_eq!(free_bits.len(), b.dimension(), "free coordinates = the dimension");
3157
3158 let start = *footprint.iter().next().unwrap();
3160 let orbit: BTreeSet<Corner> = (0..(1u64 << b.dimension()))
3161 .map(|subset| {
3162 let mut c = start;
3163 for (j, &fb) in free_bits.iter().enumerate() {
3164 if subset & (1 << j) != 0 {
3165 c ^= 1 << fb;
3166 }
3167 }
3168 c
3169 })
3170 .collect();
3171 assert_eq!(orbit, footprint, "the free-coordinate group orbit = the footprint (many → one)");
3173
3174 let support: Vec<(usize, bool)> = b.clause_literals();
3176 assert_eq!(support.len(), b.care.count_ones() as usize, "the point lives in the support subspace");
3177 }
3178 }
3179
3180 #[test]
3187 fn every_covered_corner_comes_in_a_pair_unless_a_clause_is_full_width() {
3188 let cover = php_cover(3);
3190 for b in &cover.blockers {
3191 let fp = b.footprint_card();
3192 assert!(fp.is_power_of_two() && fp >= 2, "blocker covers a power-of-two ≥ 2 corners: {fp}");
3193 let corners = b.footprint();
3194 for &c in &corners {
3195 assert!(
3196 corners.iter().any(|&c2| (c ^ c2).count_ones() == 1),
3197 "every covered corner has a free-axis partner also covered (covers come in pairs)"
3198 );
3199 }
3200 }
3201
3202 let full = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)], 3);
3204 assert_eq!(full.footprint_card(), 1, "a full-width clause covers exactly one corner — the exception");
3205 assert_eq!(full.dimension(), 0, "its blocker is a 0-dimensional point, no partner");
3206 }
3207
3208 #[test]
3212 fn the_half_center_is_the_symmetry_fixed_point() {
3213 let n = 5;
3214 let center = vec![0.5_f64; n];
3215 let sigma = CubeSym { perm: vec![2, 0, 4, 1, 3], flip: vec![true, false, true, false, true] };
3217 assert_eq!(sigma.map_fractional(¢er), center, "the center is fixed by the symmetry");
3218 for g in php_symmetries(4) {
3220 let c = vec![0.5_f64; 4 * 3];
3221 assert_eq!(g.map_fractional(&c), c, "every pigeonhole automorphism fixes the center");
3222 }
3223 let off = vec![0.1, 0.2, 0.3, 0.4, 0.5];
3225 assert_ne!(sigma.map_fractional(&off), off, "a non-center point is moved");
3226 }
3227
3228 #[test]
3234 fn pigeonhole_is_lp_feasible_at_the_center_yet_integer_unsat() {
3235 for n in 3..=6 {
3236 let cover = php_cover(n.min(8));
3237 assert!(
3238 cover.relaxation_feasible_at_center(),
3239 "PHP({n}) clauses all have width ≥ 2 ⟹ the ½-center is LP-feasible"
3240 );
3241 let cert = cover.counting_refutation().expect("yet it is integer-UNSAT by counting");
3242 assert!(cert.pigeons > cert.holes, "the integer obstruction the ½-center hides");
3243 }
3244 }
3245
3246 #[test]
3253 fn the_cutting_plane_separates_the_symmetric_center() {
3254 let n = 4;
3255 let cover = php_cover(n);
3256 let center = vec![0.5_f64; cover.n];
3257
3258 for b in &cover.blockers {
3260 assert!(b.clause_lp_value(¢er) >= 1.0 - 1e-9, "clause satisfied at the center");
3261 }
3262
3263 let holes = n - 1;
3265 let var = |p: usize, h: usize| p * holes + h;
3266 for h in 0..holes {
3267 let cardinality: f64 = (0..n).map(|p| center[var(p, h)]).sum();
3268 assert!(
3269 cardinality > 1.0 + 1e-9,
3270 "hole {h}: Σ_p x = {cardinality} > 1 — the cutting plane separates the center"
3271 );
3272 }
3273 }
3274
3275 fn mutilated_chessboard(m: usize) -> (usize, Vec<Vec<Lit>>) {
3280 use std::collections::HashMap;
3281 let sq = |r: usize, c: usize| r * m + c;
3282 let removed = |r: usize, c: usize| (r == 0 && c == 0) || (r == m - 1 && c == m - 1);
3283 let color = |r: usize, c: usize| (r + c) % 2;
3284 let neighbors = |r: usize, c: usize| {
3285 let mut v = Vec::new();
3286 if r > 0 { v.push((r - 1, c)); }
3287 if r + 1 < m { v.push((r + 1, c)); }
3288 if c > 0 { v.push((r, c - 1)); }
3289 if c + 1 < m { v.push((r, c + 1)); }
3290 v
3291 };
3292 let mut var_of: HashMap<(usize, usize), u32> = HashMap::new();
3294 for r in 0..m {
3295 for c in 0..m {
3296 if color(r, c) == 1 && !removed(r, c) {
3297 for (nr, nc) in neighbors(r, c) {
3298 if color(nr, nc) == 0 && !removed(nr, nc) {
3299 let next = var_of.len() as u32;
3300 var_of.entry((sq(r, c), sq(nr, nc))).or_insert(next);
3301 }
3302 }
3303 }
3304 }
3305 }
3306 let mut clauses: Vec<Vec<Lit>> = Vec::new();
3307 for r in 0..m {
3308 for c in 0..m {
3309 if color(r, c) == 1 && !removed(r, c) {
3310 let row: Vec<Lit> = neighbors(r, c)
3311 .into_iter()
3312 .filter(|&(nr, nc)| color(nr, nc) == 0 && !removed(nr, nc))
3313 .map(|(nr, nc)| Lit::new(var_of[&(sq(r, c), sq(nr, nc))], true))
3314 .collect();
3315 clauses.push(row);
3316 }
3317 }
3318 }
3319 for r in 0..m {
3320 for c in 0..m {
3321 if color(r, c) == 0 && !removed(r, c) {
3322 let incident: Vec<u32> = neighbors(r, c)
3323 .into_iter()
3324 .filter(|&(nr, nc)| color(nr, nc) == 1 && !removed(nr, nc))
3325 .map(|(nr, nc)| var_of[&(sq(nr, nc), sq(r, c))])
3326 .collect();
3327 for i in 0..incident.len() {
3328 for j in (i + 1)..incident.len() {
3329 clauses.push(vec![Lit::new(incident[i], false), Lit::new(incident[j], false)]);
3330 }
3331 }
3332 }
3333 }
3334 }
3335 (var_of.len(), clauses)
3336 }
3337
3338 fn selected_pigeonholes(a: usize, b: usize) -> (usize, Vec<Vec<Lit>>) {
3342 let (a_holes, b_holes) = (a - 1, b - 1);
3343 let off = 1 + a * a_holes;
3344 let s = Lit::new(0, true);
3345 let var_a = |p: usize, h: usize| Lit::new((1 + p * a_holes + h) as u32, true);
3346 let var_b = |p: usize, h: usize| Lit::new((off + p * b_holes + h) as u32, true);
3347 let mut clauses = Vec::new();
3348 for p in 0..a {
3349 let mut row: Vec<Lit> = (0..a_holes).map(|h| var_a(p, h)).collect();
3350 row.push(s.negated());
3351 clauses.push(row);
3352 }
3353 for h in 0..a_holes {
3354 for p in 0..a {
3355 for q in (p + 1)..a {
3356 clauses.push(vec![var_a(p, h).negated(), var_a(q, h).negated(), s.negated()]);
3357 }
3358 }
3359 }
3360 for p in 0..b {
3361 let mut row: Vec<Lit> = (0..b_holes).map(|h| var_b(p, h)).collect();
3362 row.push(s);
3363 clauses.push(row);
3364 }
3365 for h in 0..b_holes {
3366 for p in 0..b {
3367 for q in (p + 1)..b {
3368 clauses.push(vec![var_b(p, h).negated(), var_b(q, h).negated(), s]);
3369 }
3370 }
3371 }
3372 (off + b * b_holes, clauses)
3373 }
3374
3375 #[test]
3381 fn branching_the_selector_unlocks_the_masked_cut() {
3382 let (num_vars, clauses) = selected_pigeonholes(4, 5);
3383 let e = clauses_to_expr(&clauses).expect("non-empty");
3385 assert!(
3386 !crate::pigeonhole::decide_pigeonhole_unsat(&e),
3387 "the selector masks the bipartite cut at the root"
3388 );
3389 let (sat, stats) = decide_laddered_sym(num_vars, &clauses, true);
3391 assert!(!sat, "selected pigeonholes is UNSAT");
3392 assert!(stats.cut_closures >= 1, "a cut fires after branching the selector: {stats:?}");
3393 assert!(stats.nodes <= 6, "a couple of branches, then crush: {stats:?}");
3394 }
3395
3396 #[test]
3401 fn the_counting_cut_crushes_the_mutilated_chessboard() {
3402 for m in [4usize, 6, 8] {
3403 let (num_vars, clauses) = mutilated_chessboard(m);
3404 let e = clauses_to_expr(&clauses).expect("non-empty board");
3405 assert!(
3406 crate::pigeonhole::decide_pigeonhole_unsat(&e),
3407 "the counting cut crushes the mutilated {m}×{m} board"
3408 );
3409 assert!(
3410 crate::pigeonhole::hall_refutation(&e).is_some(),
3411 "Hall names the over-subscribed majority colour on the {m}×{m} board"
3412 );
3413 let (sat, stats) = decide_laddered(num_vars, &clauses);
3415 assert!(!sat && stats.cut_closures >= 1 && stats.nodes <= 2, "{m}×{m}: {stats:?}");
3416 }
3417 }
3418
3419 #[test]
3424 fn the_full_hall_cut_beats_simple_counting() {
3425 let cover = Cover::of_cnf(&DimacsCnf {
3427 num_vars: 4,
3428 clauses: vec![
3429 vec![Lit::new(0, true)], vec![Lit::new(1, true)], vec![Lit::new(2, true), Lit::new(3, true)], vec![Lit::new(0, false), Lit::new(1, false)], ],
3434 });
3435 assert_eq!(cover.counting_refutation(), None, "items > slots cannot see the subset violation");
3437 let witness = cover.hall_refutation().expect("Hall's theorem refutes the subset");
3439 assert_eq!(witness.items.len(), 2, "two items competing for one slot: {witness:?}");
3440 assert_eq!(witness.slots.len(), 1, "their shared neighborhood is a single slot");
3441 assert_eq!(cover.auto_certify(), CoverVerdict::Total { cut: Some(Shadow::Counting) });
3443 }
3444
3445 #[test]
3448 fn the_counting_crush_generalizes_beyond_pigeonhole() {
3449 let php = Cover::of_cnf(&crate::families::php(5).0);
3451 let pc = php.counting_refutation().expect("pigeonhole crushed");
3452 assert_eq!((pc.pigeons, pc.holes), (5, 4));
3453
3454 let cc = Cover::of_cnf(&crate::families::clique_coloring(4, 3).0);
3456 let ccert = cc.counting_refutation().expect("clique-coloring crushed by the same invariant");
3457 assert!(ccert.pigeons > ccert.holes, "K_4 over 3 colors: {ccert:?}");
3458 assert!(crate::pigeonhole::check_counting_cert(&ccert), "the certificate re-checks");
3459 }
3460
3461 #[test]
3465 fn tight_and_redundant_vertices_identify_the_essential_core() {
3466 let minimal = Cover {
3468 n: 2,
3469 blockers: vec![
3470 Subcube::blocker(&[Lit::new(0, true)], 2), Subcube::blocker(&[Lit::new(0, false)], 2), ],
3473 };
3474 assert!(minimal.is_total(), "the two halves tile the whole cube");
3475 for c in 0u64..4 {
3476 assert!(minimal.is_tight(c), "corner {c} is covered by exactly one blocker");
3477 }
3478 assert_eq!(minimal.essential_blockers(), vec![0, 1], "both halves are essential");
3479
3480 let mut redundant = minimal.clone();
3482 redundant.blockers.push(Subcube::blocker(&[Lit::new(1, true)], 2));
3483 assert!(redundant.is_redundant(0) && redundant.is_redundant(1), "corners 0,1 now overlapped");
3484 assert_eq!(redundant.essential_blockers(), vec![0, 1], "the added blocker joins no core");
3485 }
3486
3487 #[test]
3492 fn laddered_branch_and_cut_crushes_structured_and_brute_forces_the_rest() {
3493 for n in [4usize, 8, 12, 20] {
3495 let (cnf, _) = crate::families::php(n);
3496 let (sat, stats) = decide_laddered(cnf.num_vars, &cnf.clauses);
3497 assert!(!sat, "PHP({n}) is UNSAT");
3498 assert!(
3499 stats.nodes <= 3 && stats.cut_closures >= 1,
3500 "PHP({n}) crushed by a cut at the root: {stats:?}"
3501 );
3502 }
3503 let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
3505 let (tsat, tstats) = decide_laddered(t.num_vars, &t.clauses);
3506 assert!(!tsat && tstats.cut_closures >= 1, "Tseitin crushed by the parity cut: {tstats:?}");
3507
3508 let rnd = crate::families::random_3sat(12, 22, 0xBEEF);
3510 let (sat, _) = decide_laddered(rnd.num_vars, &rnd.clauses);
3511 let e = clauses_to_expr(&rnd.clauses).unwrap();
3512 let prover_sat = !matches!(crate::sat::prove_unsat(&e), crate::sat::UnsatOutcome::Refuted);
3513 assert_eq!(sat, prover_sat, "the ladder agrees with the certified prover on the residual");
3514 }
3515
3516 #[test]
3521 fn symmetry_pruned_ladder_is_sound_against_brute_force() {
3522 for seed in 0..60u64 {
3523 let clauses_n = 14 + (seed % 14) as usize;
3524 let cnf = crate::families::random_3sat(9, clauses_n, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
3525 let brute = (0u64..(1 << cnf.num_vars)).any(|c| {
3527 cnf.clauses.iter().all(|cl| {
3528 cl.iter().any(|l| ((c >> l.var()) & 1 != 0) == l.is_positive())
3529 })
3530 });
3531 let (sym_sat, _) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, true);
3532 assert_eq!(sym_sat, brute, "seed {seed}: symmetry-pruned ladder must match brute force");
3533 let (plain_sat, _) = decide_laddered(cnf.num_vars, &cnf.clauses);
3535 assert_eq!(sym_sat, plain_sat, "seed {seed}: pruning must not change the verdict");
3536 let (nocut_sat, _) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
3538 assert_eq!(nocut_sat, brute, "seed {seed}: cut-free symmetry pruning must match brute force");
3539 }
3540 for n in [4usize, 6, 8] {
3542 let (cnf, _) = crate::families::php(n);
3543 let (sat, stats) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, true);
3544 assert!(!sat && stats.cut_closures >= 1, "PHP({n}) crushed by the cut: {stats:?}");
3545 }
3546 }
3547
3548 #[test]
3554 fn symmetry_pruning_collapses_the_search_with_the_cut_off() {
3555 for n in [3usize, 4] {
3556 let (cnf, _) = crate::families::php(n);
3557 let (sat_pruned, pruned_stats) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
3558 let (sat_plain, plain_stats) = decide_laddered_nocut(cnf.num_vars, &cnf.clauses);
3559 assert!(!sat_pruned && !sat_plain, "PHP({n}) is UNSAT either way");
3560 assert!(
3561 pruned_stats.pruned >= 1,
3562 "symmetry pruning must fire on PHP({n}): {pruned_stats:?}"
3563 );
3564 assert!(
3565 pruned_stats.nodes < plain_stats.nodes,
3566 "PHP({n}): pruned search {} nodes < plain {} nodes",
3567 pruned_stats.nodes,
3568 plain_stats.nodes
3569 );
3570 }
3571 }
3572
3573 #[test]
3577 fn symmetry_aware_counting_collapses_the_count() {
3578 let cnf = crate::families::clique_coloring(3, 3).0;
3579 let nv = cnf.num_vars;
3580 let satisfies = |m: &[bool]| {
3581 cnf.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
3582 };
3583 let models: Vec<Vec<bool>> = (0u64..(1 << nv))
3584 .filter_map(|x| {
3585 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
3586 satisfies(&m).then_some(m)
3587 })
3588 .collect();
3589 let total = models.len();
3590 let generators = crate::symmetry_detect::find_generators(nv, &cnf.clauses);
3591 let orbits = partition_into_orbits(&models, &generators);
3592
3593 assert_eq!(orbits.iter().map(|o| o.len()).sum::<usize>(), total, "orbits partition the solutions");
3594 assert!(orbits.len() < total, "{} orbits ≪ {} solutions — symmetry collapses the count", orbits.len(), total);
3595 for orbit in &orbits {
3596 assert_eq!(model_orbit(&orbit[0], &generators).len(), orbit.len(), "each orbit reconstructs from its rep");
3597 }
3598 }
3599
3600 #[test]
3606 fn renamable_horn_is_a_new_symmetry_for_a_new_class() {
3607 let cl = vec![
3609 vec![Lit::new(0, true), Lit::new(1, true)],
3610 vec![Lit::new(0, true), Lit::new(2, false)],
3611 ];
3612 let flips = renaming_to_horn(3, &cl).expect("this formula is renamable to Horn");
3613 let renamed = apply_renaming(&cl, &flips);
3614 for c in &renamed {
3615 assert!(
3616 c.iter().filter(|l| l.is_positive()).count() <= 1,
3617 "after the flip-renaming every clause is Horn: {c:?}"
3618 );
3619 }
3620
3621 fn sm(s: &mut u64) -> u64 {
3624 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
3625 let mut z = *s;
3626 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3627 z ^ (z >> 31)
3628 }
3629 let mut state = 0x8077_0001u64;
3630 let mut renamable_seen = 0;
3631 for _ in 0..80 {
3632 let nv = 3 + (sm(&mut state) % 4) as usize;
3633 let m = 2 + (sm(&mut state) % 6) as usize;
3634 let mut clauses: Vec<Vec<Lit>> = Vec::new();
3635 for _ in 0..m {
3636 let mut c = Vec::new();
3637 for var in 0..nv {
3638 if sm(&mut state) % 2 == 0 {
3639 c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
3640 }
3641 }
3642 if !c.is_empty() {
3643 clauses.push(c);
3644 }
3645 }
3646 if clauses.is_empty() {
3647 continue;
3648 }
3649 let sat = |cs: &[Vec<Lit>]| {
3650 (0u64..(1u64 << nv)).any(|x| {
3651 cs.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
3652 })
3653 };
3654 if let Some(flips) = renaming_to_horn(nv, &clauses) {
3655 renamable_seen += 1;
3656 let renamed = apply_renaming(&clauses, &flips);
3657 assert!(
3658 renamed.iter().all(|c| c.iter().filter(|l| l.is_positive()).count() <= 1),
3659 "a found renaming must yield a Horn formula"
3660 );
3661 assert_eq!(sat(&clauses), sat(&renamed), "the flip-renaming preserves satisfiability");
3662 }
3663 }
3664 assert!(renamable_seen > 0, "the fuzz should hit renamable-Horn instances");
3665 }
3666
3667 #[test]
3672 fn symmetry_generates_the_solution_orbit_from_one_model() {
3673 let cnf = crate::families::clique_coloring(3, 3).0;
3674 let nv = cnf.num_vars;
3675 let satisfies = |m: &[bool]| {
3676 cnf.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
3677 };
3678 let model: Vec<bool> = (0u64..(1 << nv))
3680 .find_map(|x| {
3681 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
3682 satisfies(&m).then_some(m)
3683 })
3684 .expect("K₃ is 3-colourable");
3685
3686 let generators = crate::symmetry_detect::find_generators(nv, &cnf.clauses);
3688 let orbit = model_orbit(&model, &generators);
3689 assert!(orbit.len() > 1, "symmetry must generate more than one solution, got {}", orbit.len());
3690 for m in &orbit {
3691 assert!(satisfies(m), "every symmetric image of a model is a model: {m:?}");
3692 }
3693 let all_models = (0u64..(1 << nv))
3695 .filter(|&x| satisfies(&(0..nv).map(|v| (x >> v) & 1 != 0).collect::<Vec<_>>()))
3696 .count();
3697 assert!(orbit.len() <= all_models, "the orbit cannot exceed the model count");
3698 }
3699
3700 #[test]
3706 #[ignore = "timing benchmark"]
3707 fn symmetry_compression_flattens_the_time_to_constant() {
3708 use std::fmt::Write;
3709 use std::time::Instant;
3710 let mut chart = String::from("pigeons symbolic cert\n");
3711 chart.push_str("------------------- -------------\n");
3712 for &n in &[4u128, 64, 10_000, 1_000_000_000, (1u128 << 63), u128::MAX] {
3713 let reps = 2_000_000u32;
3714 let t = Instant::now();
3715 let mut last = None;
3716 for _ in 0..reps {
3717 last = crate::pigeonhole::certify_pigeonhole_unsat(std::hint::black_box(n), n - 1);
3718 }
3719 let ns = t.elapsed().as_secs_f64() * 1e9 / reps as f64;
3720 assert!(last.is_some(), "PHP({n}) is refuted by the symbolic cert");
3721 let _ = writeln!(chart, "{n:<19} {ns:>8.3} ns");
3722 }
3723 println!("\n{chart}");
3724 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
3725 if std::fs::create_dir_all(&dir).is_ok() {
3726 let _ = std::fs::write(
3727 dir.join("symmetry_compression_flat_time.txt"),
3728 format!("SYMMETRY-COMPRESSION FLAT TIME — pigeonhole on its orbit-type quotient is two rule-types\nand a count, decided in O(1). Constant nanoseconds from n=4 to n=2^128, where the CNF could\nnever be built. The clause-level cut is linear in the input; the quotient cut is flat.\n\n{chart}\n"),
3729 );
3730 }
3731 }
3732
3733 #[test]
3738 #[ignore = "measurement"]
3739 fn the_asymmetry_is_the_hardness_knob() {
3740 use std::fmt::Write;
3741 fn sm(s: &mut u64) -> u64 {
3742 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
3743 let mut z = *s;
3744 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3745 z ^ (z >> 31)
3746 }
3747 let (php, _) = crate::families::php(5);
3748 let nv = php.num_vars;
3749 let mut state = 0x4D55_0001u64;
3750 let mut chart = String::from("asymmetry(k) verdict nodes punches\n");
3751 chart.push_str("------------ ----------- ----- -------\n");
3752 for &k in &[0usize, 1, 2, 3, 4] {
3753 let mut clauses = php.clauses.clone();
3754 for _ in 0..k {
3755 let mut c: Vec<Lit> = Vec::new();
3756 while c.len() < 3 {
3757 let v = (sm(&mut state) % nv as u64) as u32;
3758 if !c.iter().any(|l| l.var() == v) {
3759 c.push(Lit::new(v, sm(&mut state) % 2 == 0));
3760 }
3761 }
3762 clauses.push(c);
3763 }
3764 let (verdict, stats) = autocarve_measured(nv, &clauses, 500_000);
3765 let _ = writeln!(chart, "{k:>12} {:<11} {:>5} {:>7}", format!("{verdict:?}"), stats.nodes, stats.punches);
3766 }
3767 println!("\n{chart}");
3768 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
3769 if std::fs::create_dir_all(&dir).is_ok() {
3770 let _ = std::fs::write(
3771 dir.join("asymmetry_is_the_knob.txt"),
3772 format!("THE ASYMMETRY IS THE HARDNESS KNOB — PHP(5) + k asymmetric clauses. Autocarve crushes the\nsymmetric core but branches the perturbation, so cost grows with k (distance from symmetric),\nnot with n. Near-symmetric is near-easy.\n\n{chart}\n"),
3773 );
3774 }
3775 }
3776
3777 #[test]
3780 #[ignore = "timing benchmark"]
3781 fn autocarve_timings_and_punches() {
3782 use std::fmt::Write;
3783 use std::time::Instant;
3784 let mut chart = String::from("instance vars verdict punches nodes time\n");
3785 chart.push_str("-------------------- ---- ------- ------- ------ ---------\n");
3786 let mut row = |name: String, nv: usize, cl: &[Vec<Lit>]| {
3787 let t = Instant::now();
3789 let mut last = (None, CarveStats::default());
3790 let reps = 200;
3791 for _ in 0..reps {
3792 last = autocarve_measured(nv, cl, 2_000_000);
3793 }
3794 let us = t.elapsed().as_secs_f64() * 1e6 / reps as f64;
3795 let (verdict, stats) = last;
3796 let _ = writeln!(
3797 chart,
3798 "{name:<20} {nv:>4} {:<7} {:>7} {:>6} {:>7.2}µs",
3799 format!("{verdict:?}"),
3800 stats.punches,
3801 stats.nodes,
3802 us
3803 );
3804 };
3805 for n in 4..=8 {
3806 let (cnf, _) = crate::families::php(n);
3807 row(format!("pigeonhole({n})"), cnf.num_vars, &cnf.clauses);
3808 }
3809 for m in [4usize, 6, 8] {
3810 let (nv, cl) = mutilated_chessboard(m);
3811 row(format!("mutilated({m}x{m})"), nv, &cl);
3812 }
3813 let (sel_nv, sel_cl) = selected_pigeonholes(4, 5);
3814 row("masked-php(4,5)".to_string(), sel_nv, &sel_cl);
3815 let (_, t, _) = crate::families::tseitin_expander(10, 0x9);
3816 row("tseitin(10)".to_string(), t.num_vars, &t.clauses);
3817
3818 println!("\n{chart}");
3819 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
3820 if std::fs::create_dir_all(&dir).is_ok() {
3821 let _ = std::fs::write(
3822 dir.join("autocarve_timings.txt"),
3823 format!("AUTOCARVE TIMINGS — recursive carve→decompose→cut→branch, per instance.\nPunches = certified cuts that fired; the cut is polynomial so time stays flat as n grows.\n\n{chart}\n"),
3824 );
3825 }
3826 }
3827
3828 #[test]
3834 fn autocarving_lets_the_rules_fall_out() {
3835 let (sel_nv, sel_cl) = selected_pigeonholes(4, 5);
3837 assert_eq!(autocarve(sel_nv, &sel_cl, 200_000), Some(false), "masked pigeonhole falls out under autocarve");
3838
3839 let (a_nv, a_cl) = selected_pigeonholes(4, 4);
3842 let s2 = a_nv as u32; let mut nested: Vec<Vec<Lit>> = Vec::new();
3844 for c in &a_cl {
3845 let mut c2 = c.clone();
3846 c2.push(Lit::new(s2, false)); nested.push(c2);
3848 }
3849 let (b, _) = crate::families::php(3);
3851 let off = s2 + 1;
3852 for c in &b.clauses {
3853 let mut c2: Vec<Lit> = c.iter().map(|l| Lit::new(l.var() + off, l.is_positive())).collect();
3854 c2.push(Lit::new(s2, true)); nested.push(c2);
3856 }
3857 let nested_nv = (off + b.num_vars as u32) as usize;
3858 assert_eq!(autocarve(nested_nv, &nested, 500_000), Some(false), "nested masked pigeonholes fall out");
3859
3860 fn sm(s: &mut u64) -> u64 {
3862 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
3863 let mut z = *s;
3864 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3865 z ^ (z >> 31)
3866 }
3867 let mut state = 0xFA11_0007u64;
3868 for _ in 0..60 {
3869 let nv = 4 + (sm(&mut state) % 4) as usize;
3870 let m = 3 + (sm(&mut state) % 8) as usize;
3871 let mut cl: Vec<Vec<Lit>> = Vec::new();
3872 for _ in 0..m {
3873 let mut c = Vec::new();
3874 for var in 0..nv {
3875 if sm(&mut state) % 3 == 0 {
3876 c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
3877 }
3878 }
3879 if !c.is_empty() {
3880 cl.push(c);
3881 }
3882 }
3883 if cl.is_empty() {
3884 continue;
3885 }
3886 let brute = (0u64..(1u64 << nv)).any(|x| {
3887 cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
3888 });
3889 if let Some(sat) = autocarve(nv, &cl, 1_000_000) {
3890 assert_eq!(sat, brute, "autocarve must match brute force: {cl:?}");
3891 }
3892 }
3893 }
3894
3895 #[test]
3900 fn the_unified_crush_pipeline_composes_every_lever() {
3901 let (php, _) = crate::families::php(4);
3902 let mut clauses = php.clauses.clone();
3903 let v = php.num_vars as u32;
3904 clauses.push(vec![Lit::new(v, true), Lit::new(0, true)]); clauses.push(vec![Lit::new(v + 1, true), Lit::new(v + 2, false)]); clauses.push(vec![Lit::new(v + 1, false), Lit::new(v + 2, true)]);
3907 let num_vars = (v + 3) as usize;
3908 assert_eq!(crush(num_vars, &clauses, 200_000), Some(false), "the pipeline crushes the composite");
3909
3910 fn sm(s: &mut u64) -> u64 {
3912 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
3913 let mut z = *s;
3914 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3915 z ^ (z >> 31)
3916 }
3917 let mut state = 0xC0DE_9999u64;
3918 for _ in 0..60 {
3919 let nv = 4 + (sm(&mut state) % 4) as usize;
3920 let m = 3 + (sm(&mut state) % 8) as usize;
3921 let mut cl: Vec<Vec<Lit>> = Vec::new();
3922 for _ in 0..m {
3923 let mut c = Vec::new();
3924 for var in 0..nv {
3925 if sm(&mut state) % 3 == 0 {
3926 c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
3927 }
3928 }
3929 if !c.is_empty() {
3930 cl.push(c);
3931 }
3932 }
3933 if cl.is_empty() {
3934 continue;
3935 }
3936 let brute = (0u64..(1u64 << nv)).any(|x| {
3937 cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
3938 });
3939 if let Some(sat) = crush(nv, &cl, 1_000_000) {
3940 assert_eq!(sat, brute, "crush must match brute force: {cl:?}");
3941 }
3942 }
3943 }
3944
3945 #[test]
3949 fn variable_elimination_carves_a_dimension_soundly() {
3950 let cl = vec![
3952 vec![Lit::new(0, true), Lit::new(1, true)],
3953 vec![Lit::new(0, false), Lit::new(2, true)],
3954 ];
3955 let projected = eliminate_variable(0, &cl);
3956 assert!(
3957 projected.iter().all(|c| c.iter().all(|l| l.var() != 0)),
3958 "the a-axis is carved away: {projected:?}"
3959 );
3960 assert!(
3961 projected.iter().any(|c| {
3962 let s: std::collections::BTreeSet<u32> = c.iter().map(|l| l.var()).collect();
3963 s == [1u32, 2].into_iter().collect()
3964 }),
3965 "the resolvent (b ∨ c) survives the projection"
3966 );
3967
3968 fn sm(s: &mut u64) -> u64 {
3970 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
3971 let mut z = *s;
3972 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3973 z ^ (z >> 31)
3974 }
3975 let mut state = 0xD1AE_0001u64;
3976 for _ in 0..60 {
3977 let nv = 4 + (sm(&mut state) % 4) as usize;
3978 let m = 3 + (sm(&mut state) % 8) as usize;
3979 let mut cl: Vec<Vec<Lit>> = Vec::new();
3980 for _ in 0..m {
3981 let mut c = Vec::new();
3982 for var in 0..nv {
3983 if sm(&mut state) % 3 == 0 {
3984 c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
3985 }
3986 }
3987 if !c.is_empty() {
3988 cl.push(c);
3989 }
3990 }
3991 if cl.is_empty() {
3992 continue;
3993 }
3994 let sat = |clauses: &[Vec<Lit>]| {
3995 (0u64..(1u64 << nv)).any(|x| {
3996 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
3997 })
3998 };
3999 let brute = sat(&cl);
4000 assert_eq!(brute, sat(&eliminate_variable(0, &cl)), "single elimination preserves SAT: {cl:?}");
4001 assert_eq!(brute, sat(&bounded_variable_elimination(nv, &cl)), "bounded VE preserves SAT: {cl:?}");
4002 }
4003 }
4004
4005 #[test]
4010 fn carve_peels_the_hypercube_to_a_verdict_or_the_core() {
4011 let unsat = vec![
4013 vec![Lit::new(0, true)],
4014 vec![Lit::new(0, false), Lit::new(1, true)],
4015 vec![Lit::new(1, false)],
4016 ];
4017 assert_eq!(carve(2, &unsat), CarveOutcome::Unsat);
4018
4019 let (php, _) = crate::families::php(4);
4021 assert!(
4022 matches!(carve(php.num_vars, &php.clauses), CarveOutcome::Core { .. }),
4023 "pigeonhole carves to its irreducible core"
4024 );
4025
4026 fn sm(s: &mut u64) -> u64 {
4028 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
4029 let mut z = *s;
4030 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4031 z ^ (z >> 31)
4032 }
4033 let mut state = 0xCA47_E000u64;
4034 for _ in 0..60 {
4035 let nv = 4 + (sm(&mut state) % 4) as usize;
4036 let m = 3 + (sm(&mut state) % 8) as usize;
4037 let mut cl: Vec<Vec<Lit>> = Vec::new();
4038 for _ in 0..m {
4039 let mut c = Vec::new();
4040 for var in 0..nv {
4041 if sm(&mut state) % 3 == 0 {
4042 c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
4043 }
4044 }
4045 if !c.is_empty() {
4046 cl.push(c);
4047 }
4048 }
4049 if cl.is_empty() {
4050 continue;
4051 }
4052 let sat = |clauses: &[Vec<Lit>]| {
4053 (0u64..(1u64 << nv)).any(|x| {
4054 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
4055 })
4056 };
4057 let brute = sat(&cl);
4058 match carve(nv, &cl) {
4059 CarveOutcome::Sat => assert!(brute, "carve says SAT: {cl:?}"),
4060 CarveOutcome::Unsat => assert!(!brute, "carve says UNSAT: {cl:?}"),
4061 CarveOutcome::Core { clauses, .. } => {
4062 assert_eq!(brute, sat(&clauses), "the carved core must preserve SAT: {cl:?}")
4063 }
4064 }
4065 }
4066 }
4067
4068 #[test]
4073 fn pure_literal_autarky_cuts_sections_and_keeps_the_core() {
4074 let (php, _) = crate::families::php(4);
4076 let (core, assigned) = pure_literal_reduce(php.num_vars, &php.clauses);
4077 assert!(assigned.is_empty(), "pigeonhole has no pure literals");
4078 assert_eq!(core.len(), php.clauses.len(), "the hard core survives untouched");
4079
4080 let easy = vec![
4082 vec![Lit::new(0, true), Lit::new(1, true)],
4083 vec![Lit::new(1, true), Lit::new(2, true)],
4084 ];
4085 let (core_easy, _) = pure_literal_reduce(3, &easy);
4086 assert!(core_easy.is_empty(), "an all-positive formula reduces to empty — SAT, no section left");
4087
4088 let mut wrapped = php.clauses.clone();
4090 let shell = php.num_vars as u32;
4091 wrapped.push(vec![Lit::new(shell, true), Lit::new(0, true)]); let (core_w, assigned_w) = pure_literal_reduce(php.num_vars + 1, &wrapped);
4093 assert!(!assigned_w.is_empty(), "the shell's pure literal is cut away");
4094 let e = clauses_to_expr(&core_w).expect("non-empty core");
4095 assert!(crate::pigeonhole::decide_pigeonhole_unsat(&e), "the surviving pigeonhole core is crushed");
4096
4097 fn sm(s: &mut u64) -> u64 {
4099 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
4100 let mut z = *s;
4101 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4102 z ^ (z >> 31)
4103 }
4104 let mut state = 0xA17A_4321u64;
4105 for _ in 0..60 {
4106 let nv = 4 + (sm(&mut state) % 4) as usize;
4107 let m = 3 + (sm(&mut state) % 7) as usize;
4108 let mut cl: Vec<Vec<Lit>> = Vec::new();
4109 for _ in 0..m {
4110 let mut c = Vec::new();
4111 for v in 0..nv {
4112 if sm(&mut state) % 3 == 0 {
4113 c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
4114 }
4115 }
4116 if !c.is_empty() {
4117 cl.push(c);
4118 }
4119 }
4120 if cl.is_empty() {
4121 continue;
4122 }
4123 let sat = |clauses: &[Vec<Lit>]| {
4124 (0u64..(1u64 << nv)).any(|x| {
4125 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
4126 })
4127 };
4128 let (reduced, _) = pure_literal_reduce(nv, &cl);
4129 assert_eq!(sat(&cl), sat(&reduced), "pure-literal reduction must preserve SAT: {cl:?}");
4130 }
4131 }
4132
4133 #[test]
4138 fn component_decomposition_unlocks_a_buried_cut() {
4139 let (php, _) = crate::families::php(4);
4140 let php_vars = php.num_vars as u32;
4141 let mut clauses: Vec<Vec<Lit>> = php.clauses.clone();
4142 let rnd = crate::families::random_3sat(10, 18, 0xD00D);
4145 for c in &rnd.clauses {
4146 clauses.push(c.iter().map(|l| Lit::new(l.var() + php_vars, l.is_positive())).collect());
4147 }
4148 let num_vars = php_vars as usize + rnd.num_vars;
4149
4150 let e = clauses_to_expr(&clauses).expect("non-empty");
4152 assert!(
4153 !crate::pigeonhole::decide_pigeonhole_unsat(&e)
4154 && !crate::xorsat::refute_via_parity(&e)
4155 && !crate::pseudo_boolean::refute_clausal(&e),
4156 "the monolithic mixed formula is recognized by no cut"
4157 );
4158 assert!(decompose_and_crush(num_vars, &clauses), "decomposition refutes the union");
4160 assert_eq!(components(num_vars, &clauses).len(), 2, "pigeonhole ⊔ random = two components");
4162 }
4163
4164 #[test]
4168 fn the_antipodal_map_is_the_center_inversion() {
4169 let n = 5;
4170 let antipode = CubeSym { perm: (0..n).collect(), flip: vec![true; n] };
4171 assert_eq!(antipode.map_fractional(&vec![0.5; n]), vec![0.5; n], "center-inversion fixes ½");
4172 let edges = [(0u32, 1u32), (1, 2), (2, 3), (3, 0)];
4174 let mut c4 = Vec::new();
4175 for (u, v) in edges {
4176 c4.push(vec![Lit::new(u, true), Lit::new(v, true)]);
4177 c4.push(vec![Lit::new(u, false), Lit::new(v, false)]);
4178 }
4179 assert!(is_antipodally_symmetric(&c4), "2-colouring an even cycle is self-complementary");
4180 assert!(!is_antipodally_symmetric(&crate::families::php(4).0.clauses), "pigeonhole is not");
4181 }
4182
4183 #[test]
4188 fn recursive_antipodal_breaking_is_sound_and_collapses_blocks() {
4189 let blocks = |k: usize| {
4192 let mut cl = Vec::new();
4193 for i in 0..k {
4194 let (a, b) = (2 * i as u32, 2 * i as u32 + 1);
4195 cl.push(vec![Lit::new(a, true), Lit::new(b, true)]);
4196 cl.push(vec![Lit::new(a, false), Lit::new(b, false)]);
4197 }
4198 (2 * k, cl)
4199 };
4200 for k in 2..=6 {
4201 let (nv, cl) = blocks(k);
4202 let anti = search_cost_antipodal(nv, &cl, 1_000_000);
4203 let plain = search_cost(nv, &cl, false, 1_000_000);
4204 assert!(matches!(anti, SearchCost::Decided { sat: true, .. }), "blocks are SAT: {anti:?}");
4205 let (an, pn) = (
4206 match anti { SearchCost::Decided { nodes, .. } => nodes, _ => usize::MAX },
4207 match plain { SearchCost::Decided { nodes, .. } => nodes, _ => usize::MAX },
4208 );
4209 assert!(an <= pn, "k={k}: antipodal {an} ≤ plain {pn} nodes");
4210 }
4211
4212 fn sm(s: &mut u64) -> u64 {
4214 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
4215 let mut z = *s;
4216 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4217 z ^ (z >> 31)
4218 }
4219 let mut state = 0x5EED_1234u64;
4220 for _ in 0..50 {
4221 let nv = 4 + (sm(&mut state) % 4) as usize;
4222 let m = 3 + (sm(&mut state) % 8) as usize;
4223 let mut cl: Vec<Vec<Lit>> = Vec::new();
4224 for _ in 0..m {
4225 let mut c = Vec::new();
4226 for v in 0..nv {
4227 if sm(&mut state) % 3 == 0 {
4228 c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
4229 }
4230 }
4231 if !c.is_empty() {
4232 cl.push(c);
4233 }
4234 }
4235 if cl.is_empty() {
4236 continue;
4237 }
4238 let brute = (0u64..(1u64 << nv)).any(|x| {
4239 cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
4240 });
4241 let anti = search_cost_antipodal(nv, &cl, 1_000_000);
4242 assert!(
4243 matches!(anti, SearchCost::Decided { sat, .. } if sat == brute),
4244 "antipodal search must match brute force: {anti:?} vs {brute}"
4245 );
4246 }
4247 }
4248
4249 #[test]
4255 fn the_exponential_gap_measured_and_banked() {
4256 use std::fmt::Write;
4257 let budget = 400_000usize;
4258 let cost = |c: SearchCost| match c {
4259 SearchCost::Decided { nodes, .. } => nodes,
4260 SearchCost::Exceeded { budget } => budget,
4261 };
4262 let mut chart = String::from(" n vars cut nodes no-cut nodes (resolution)\n");
4263 chart.push_str("-- ----- --------- -------------------------\n");
4264 let mut nocut_curve = Vec::new();
4265 for n in 2..=8 {
4266 let (cnf, _) = crate::families::php(n);
4267 let cut = search_cost(cnf.num_vars, &cnf.clauses, true, budget);
4268 let nocut = search_cost(cnf.num_vars, &cnf.clauses, false, budget);
4269 assert!(
4270 matches!(cut, SearchCost::Decided { nodes, .. } if nodes <= 2),
4271 "PHP({n}): cut must close in O(1) nodes, got {cut:?}"
4272 );
4273 let nc = cost(nocut);
4274 nocut_curve.push(nc);
4275 let nocut_str = if matches!(nocut, SearchCost::Exceeded { .. }) {
4276 format!("≥{budget} (exploded)")
4277 } else {
4278 format!("{nc}")
4279 };
4280 let _ = writeln!(chart, "{n:>2} {:>5} {:>9} {nocut_str}", cnf.num_vars, cost(cut));
4281 }
4282
4283 assert!(nocut_curve.windows(2).all(|w| w[1] >= w[0]), "raw search grows monotonically: {nocut_curve:?}");
4285 assert!(*nocut_curve.last().unwrap() >= 100_000, "PHP(8) raw search is vast vs the cut's 1 node: {nocut_curve:?}");
4286 assert!(nocut_curve[4] >= 1000, "the gap to the cut's single node is already vast by PHP(6): {nocut_curve:?}");
4287
4288 println!("\n{chart}");
4289 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
4290 if std::fs::create_dir_all(&dir).is_ok() {
4291 let _ = std::fs::write(
4292 dir.join("exponential_gap.txt"),
4293 format!("EXPONENTIAL GAP — same branch engine, certified cut ON vs OFF (raw resolution).\nThe counting cut closes pigeonhole at the root in ONE node at every scale; raw resolution\ngrows exponentially and explodes past {budget} nodes.\n\n{chart}\n"),
4294 );
4295 }
4296 }
4297
4298 #[test]
4302 fn auto_cut_classifies_and_crushes_every_family() {
4303 use CoverVerdict::Total;
4304 let php = Cover::of_cnf(&crate::families::php(5).0);
4305 assert_eq!(php.auto_certify(), Total { cut: Some(Shadow::Counting) });
4306
4307 let cc = Cover::of_cnf(&crate::families::clique_coloring(4, 3).0);
4308 assert_eq!(cc.auto_certify(), Total { cut: Some(Shadow::Counting) });
4309
4310 let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
4311 assert_eq!(Cover::of_cnf(&t).auto_certify(), Total { cut: Some(Shadow::Parity) });
4312
4313 let sat = DimacsCnf { num_vars: 3, clauses: vec![vec![Lit::new(0, true), Lit::new(1, true)]] };
4315 assert_eq!(Cover::of_cnf(&sat).auto_certify(), CoverVerdict::Escapes);
4316 }
4317
4318 #[test]
4326 fn measuring_randomness_the_quotient_climbs_as_structure_decays() {
4327 use std::fmt::Write;
4328 fn sm(s: &mut u64) -> u64 {
4329 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
4330 let mut z = *s;
4331 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4332 z ^ (z >> 31)
4333 }
4334 let (php, _) = crate::families::php(5);
4335 let nv = php.num_vars;
4336 let mut state = 0x4A22_0001u64;
4337 let mut chart = String::from("injected clauses quotient ratio\n");
4338 chart.push_str("-------- ------- -------- -----\n");
4339 let mut ratios = Vec::new();
4340 for &k in &[0usize, 4, 8, 16, 32] {
4341 let mut clauses = php.clauses.clone();
4342 for _ in 0..k {
4343 let mut c: Vec<Lit> = Vec::new();
4344 while c.len() < 3 {
4345 let v = (sm(&mut state) % nv as u64) as u32;
4346 if !c.iter().any(|l| l.var() == v) {
4347 c.push(Lit::new(v, sm(&mut state) % 2 == 0));
4348 }
4349 }
4350 clauses.push(c);
4351 }
4352 let generators = crate::symmetry_detect::find_generators(nv, &clauses);
4353 let quotient = clause_orbits(&clauses, &generators).len();
4354 let ratio = quotient as f64 / clauses.len() as f64;
4355 ratios.push(ratio);
4356 let _ = writeln!(chart, "{k:>8} {:>7} {quotient:>8} {ratio:.3}", clauses.len());
4357 }
4358 assert!(ratios[0] < 0.15, "pristine pigeonhole is highly compressible: {}", ratios[0]);
4361 assert!(ratios[1] > 0.9, "just four random clauses annihilate the symmetry (cliff): {ratios:?}");
4362
4363 println!("\n{chart}");
4364 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
4365 if std::fs::create_dir_all(&dir).is_ok() {
4366 let _ = std::fs::write(
4367 dir.join("randomness_measure.txt"),
4368 format!("MEASURING RANDOMNESS — a symmetry is a compression, so quotient-size (orbit-types ÷\nclauses) measures incompressibility (computable shadow of Kolmogorov complexity). Pigeonhole\nis maximally compressible; injecting random clauses erodes the symmetry and the quotient climbs\ntoward 1 — ordered → random as a continuous gradient.\n\n{chart}\n"),
4369 );
4370 }
4371 }
4372
4373 #[test]
4380 fn asymmetry_not_randomness_annihilates_the_structure() {
4381 use crate::symmetry_detect::{clause_key, find_generators};
4382 let php = crate::families::php(3).0;
4383 let nv = php.num_vars;
4384 let quotient = |cl: &[Vec<Lit>]| clause_orbits(cl, &find_generators(nv, cl)).len();
4385 let base = quotient(&php.clauses); let seed = vec![Lit::new(0, false), Lit::new(3, false)]; let mut broken = php.clauses.clone();
4390 broken.push(seed.clone());
4391 assert!(quotient(&broken) > base, "one asymmetric clause breaks the symmetry");
4392
4393 let generators = php_perm_symmetries(3);
4395 let mut seen: BTreeSet<Vec<u32>> = [clause_key(&seed)].into_iter().collect();
4396 let mut orbit = vec![seed.clone()];
4397 let mut stack = vec![seed.clone()];
4398 while let Some(c) = stack.pop() {
4399 for g in &generators {
4400 let img = g.apply_clause(&c);
4401 if seen.insert(clause_key(&img)) {
4402 orbit.push(img.clone());
4403 stack.push(img);
4404 }
4405 }
4406 }
4407 let mut symmetrized = php.clauses.clone();
4408 symmetrized.extend(orbit.iter().cloned());
4409 assert!(
4411 quotient(&symmetrized) <= base + 1,
4412 "the SAME clause, symmetrized ({} added), preserves the structure",
4413 orbit.len()
4414 );
4415 }
4416
4417 #[test]
4426 fn pseudorandom_is_kolmogorov_simple_but_symmetry_blind() {
4427 let a = crate::families::random_3sat(14, 50, 0x00AB_CDEF);
4429 let b = crate::families::random_3sat(14, 50, 0x00AB_CDEF);
4430 assert_eq!(a.clauses, b.clauses, "same seed ⟹ byte-identical: Kolmogorov complexity ≤ the seed");
4431
4432 let generators = crate::symmetry_detect::find_generators(a.num_vars, &a.clauses);
4434 let quotient = clause_orbits(&a.clauses, &generators).len();
4435 assert!(
4436 quotient * 2 > a.clauses.len(),
4437 "symmetry-blind: quotient {quotient} near the {} clauses, despite being seed-simple",
4438 a.clauses.len()
4439 );
4440
4441 let c = crate::families::random_3sat(14, 50, 0x00AB_CDF0);
4443 assert_ne!(a.clauses, c.clauses, "a different seed is a different object — the seed is the structure");
4444 }
4445
4446 #[test]
4454 fn breaking_the_symmetry_recovers_the_re_checkable_witness() {
4455 let php = crate::families::php(5).0;
4457 let e = clauses_to_expr(&php.clauses).expect("non-empty");
4458 let cert = crate::pigeonhole::counting_certificate(&e).expect("the counting break fires");
4459 assert!(crate::pigeonhole::check_counting_cert(&cert), "the recovered refutation witness re-checks: {cert:?}");
4460 let hall = crate::pigeonhole::hall_refutation(&e).expect("the Hall break fires");
4461 assert!(!hall.items.is_empty(), "the Hall break names the violating subset (a witness)");
4462
4463 let cc = crate::families::clique_coloring(3, 3).0;
4465 let nv = cc.num_vars;
4466 let satisfies = |m: &[bool]| {
4467 cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
4468 };
4469 let one_model: Vec<bool> = (0u64..(1 << nv))
4470 .find_map(|x| {
4471 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
4472 satisfies(&m).then_some(m)
4473 })
4474 .expect("clique_coloring(3,3) is SAT");
4475 let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
4476 let witnesses = model_orbit(&one_model, &generators);
4477 assert!(witnesses.len() > 1, "the symmetry recovers many witnesses from one");
4478 for w in &witnesses {
4479 assert!(satisfies(w), "every recovered witness is a genuine model");
4480 }
4481 }
4482
4483 #[test]
4489 fn symmetry_break_the_witness_to_its_canonical_representative() {
4490 let cc = crate::families::clique_coloring(3, 3).0;
4491 let nv = cc.num_vars;
4492 let satisfies =
4493 |m: &[bool]| cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
4494 let models: Vec<Vec<bool>> = (0u64..(1 << nv))
4495 .filter_map(|x| {
4496 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
4497 satisfies(&m).then_some(m)
4498 })
4499 .collect();
4500 let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
4501
4502 for m in &models {
4504 let canon = canonical_model(m, &generators);
4505 for sib in model_orbit(m, &generators) {
4506 assert_eq!(canonical_model(&sib, &generators), canon, "orbit-mates share a canonical witness");
4507 }
4508 assert!(satisfies(&canon), "the canonical witness is itself a genuine model");
4509 assert!(canon <= *m, "the canonical witness is the lex-least of its orbit");
4510 }
4511
4512 let canonicals: BTreeSet<Vec<bool>> =
4514 models.iter().map(|m| canonical_model(m, &generators)).collect();
4515 let orbits = partition_into_orbits(&models, &generators);
4516 assert_eq!(canonicals.len(), orbits.len(), "one canonical witness per orbit");
4517 assert!(canonicals.len() < models.len(), "the symmetry genuinely compressed the witness set");
4518 }
4519
4520 #[test]
4528 fn symmetry_break_across_the_witnesss_perspective_of_other_witnesses() {
4529 let cc = crate::families::clique_coloring(3, 3).0;
4530 let nv = cc.num_vars;
4531 let satisfies =
4532 |m: &[bool]| cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
4533 let models: Vec<Vec<bool>> = (0u64..(1 << nv))
4534 .filter_map(|x| {
4535 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
4536 satisfies(&m).then_some(m)
4537 })
4538 .collect();
4539 let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
4540 let group = perm_group_closure(&generators, nv);
4541 assert!(group.len() > 1, "clique_coloring(3,3) has a nontrivial symmetry group");
4542
4543 let mut saw_redundant_perspective = false;
4544 for m in &models {
4545 let persp = witness_perspective(m, &generators, nv);
4546 let orbit = model_orbit(m, &generators);
4547 let stab = stabilizer(m, &group);
4548
4549 assert_eq!(group.len(), orbit.len() * stab.len(), "orbit-stabilizer holds from m's frame");
4551 assert_eq!(persp.len(), orbit.len(), "one representative transformation per distinct witness");
4552 if stab.len() > 1 {
4553 saw_redundant_perspective = true; }
4555
4556 assert_eq!(persp[0].0, *m, "the witness sees itself first");
4558 assert!(persp[0].1.is_identity(), "it sees itself through the identity");
4559
4560 let mut destinations = BTreeSet::new();
4562 for (dest, sigma) in &persp {
4563 assert_eq!(apply_perm_to_model(sigma, m), *dest, "σ·m is the witness it claims");
4564 assert!(satisfies(dest), "every witness in the perspective is a genuine model");
4565 assert!(destinations.insert(dest.clone()), "no witness is named twice — redundancy is gone");
4566 }
4567 assert_eq!(destinations, orbit.iter().cloned().collect(), "the perspective covers the whole orbit");
4568 }
4569 assert!(saw_redundant_perspective, "at least one witness had a nontrivial stabilizer to break");
4570 }
4571
4572 #[test]
4579 fn burnside_counts_the_essentially_distinct_witnesses() {
4580 let check = |nv: usize, clauses: &[Vec<Lit>]| {
4581 let satisfies =
4582 |m: &[bool]| clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
4583 let models: Vec<Vec<bool>> = (0u64..(1u64 << nv))
4584 .filter_map(|x| {
4585 let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
4586 satisfies(&m).then_some(m)
4587 })
4588 .collect();
4589 let generators = crate::symmetry_detect::find_generators(nv, clauses);
4590 let group = perm_group_closure(&generators, nv);
4591
4592 let total_fixed: usize = group
4594 .iter()
4595 .map(|g| models.iter().filter(|m| apply_perm_to_model(g, m.as_slice()) == **m).count())
4596 .sum();
4597 assert_eq!(total_fixed % group.len(), 0, "Burnside sum divisible by |G|");
4598
4599 let direct = partition_into_orbits(&models, &generators).len();
4600 let burnside = burnside_orbit_count(&models, &group);
4601 let canonicals: BTreeSet<Vec<bool>> =
4602 models.iter().map(|m| canonical_model(m, &generators)).collect();
4603 assert_eq!(direct, burnside, "Burnside average == direct orbit partition");
4604 assert_eq!(burnside, canonicals.len(), "Burnside count == #distinct canonical witnesses");
4605 (models.len(), burnside, group.len())
4606 };
4607
4608 let cc = crate::families::clique_coloring(3, 3).0;
4610 let (raw, essential, gsize) = check(cc.num_vars, &cc.clauses);
4611 assert!(gsize > 1, "clique_coloring(3,3) has a nontrivial group");
4612 assert!(essential < raw, "symmetry genuinely compressed the witness count");
4613
4614 for seed in 0u64..40 {
4617 let cnf = crate::families::random_3sat(6, 18, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
4618 check(6, &cnf.clauses);
4619 }
4620 }
4621
4622 fn decorrelated_seed(tag: u64, i: u64) -> u64 {
4627 let mut z = tag.wrapping_mul(0xD1B5_4A32_D192_ED03).wrapping_add(i).wrapping_add(0x9E3779B97F4A7C15);
4628 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
4629 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
4630 z ^ (z >> 31)
4631 }
4632
4633 #[test]
4642 fn satisfiable_random_3sat_is_the_typical_case_below_the_threshold() {
4643 let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
4644 (0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
4645 };
4646 let n = 14usize;
4647 let trials = 60u64;
4648 let sat_count = |m: usize| {
4649 (0..trials)
4650 .filter(|&s| {
4651 let cnf = crate::families::random_3sat(n, m, decorrelated_seed(m as u64, s));
4652 is_sat(n, &cnf.clauses)
4653 })
4654 .count()
4655 };
4656 let low = sat_count(2 * n); let high = sat_count(6 * n); assert!(low > 0, "satisfiable random 3-SATs exist — the premise 'they can't be SAT' is false");
4659 assert!(2 * low > trials as usize, "below threshold, random 3-SAT is satisfiable in the MAJORITY: {low}/{trials}");
4660 assert!(high < low, "the satisfiability rate collapses across the density threshold — the phase transition");
4661 assert!(5 * high < trials as usize, "above threshold, random 3-SAT is overwhelmingly UNSAT: {high}/{trials}");
4662 }
4663
4664 #[test]
4672 fn the_satisfiability_threshold_climbs_from_3sat_to_4sat() {
4673 let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
4674 (0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
4675 };
4676 let n = 14usize;
4677 let trials = 60usize;
4678 let sat_rate = |k: usize, m: usize| {
4679 let tag = (k as u64) << 32 ^ m as u64;
4680 (0..trials as u64)
4681 .filter(|&s| is_sat(n, &crate::families::random_ksat(k, n, m, decorrelated_seed(tag, s)).clauses))
4682 .count()
4683 };
4684
4685 let three_at_6 = sat_rate(3, 6 * n);
4687 let four_at_6 = sat_rate(4, 6 * n);
4688 assert!(4 * three_at_6 < trials, "3-SAT at α=6 is above its 4.27 threshold → overwhelmingly UNSAT: {three_at_6}/{trials}");
4689 assert!(4 * four_at_6 > 3 * trials, "4-SAT at α=6 is below its 9.93 threshold → overwhelmingly SAT: {four_at_6}/{trials}");
4690 assert!(four_at_6 > three_at_6, "the threshold climbed: 4-SAT tolerates a density that already broke 3-SAT");
4691
4692 let four_at_14 = sat_rate(4, 14 * n);
4694 assert!(four_at_14 < four_at_6, "4-SAT's own phase transition: SAT-rate collapses from α=6 to α=14");
4695
4696 assert!(2 * sat_rate(3, 4 * n) > trials, "3-SAT at α=4 (below 4.27) is SAT-majority");
4698 assert!(2 * sat_rate(3, 6 * n) < trials, "3-SAT at α=6 (above 4.27) is UNSAT-majority");
4699 }
4700
4701 #[test]
4712 fn the_proof_complexity_ladder_separates_and_localizes_the_wall() {
4713 let e_of = |cl: &[Vec<Lit>]| clauses_to_expr(cl).unwrap();
4714
4715 let php = crate::families::php(4).0;
4717 assert_eq!(
4718 weakest_crushing_rung(php.num_vars, &php.clauses, php.num_vars),
4719 ProofRung::Counting,
4720 "PHP is a counting refutation"
4721 );
4722 assert!(!crate::xorsat::refute_via_parity(&e_of(&php.clauses)), "pigeonhole is invisible to GF(2) parity");
4723
4724 let (_, par) = crate::families::parity_unsat(8, 12, 0xA5A5);
4726 assert_eq!(
4727 weakest_crushing_rung(par.num_vars, &par.clauses, par.num_vars),
4728 ProofRung::Parity,
4729 "an XOR contradiction is a parity refutation"
4730 );
4731 let pe = e_of(&par.clauses);
4732 assert!(
4733 crate::pigeonhole::counting_certificate(&pe).is_none() && crate::pigeonhole::hall_refutation(&pe).is_none(),
4734 "a parity contradiction is invisible to counting / Hall"
4735 );
4736
4737 let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
4739 (0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
4740 };
4741 let residue = (0u64..600)
4742 .find_map(|seed| {
4743 let c = crate::families::random_3sat(5, 26, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
4744 (!is_sat(5, &c.clauses) && automorphism_group_size(5, &c.clauses) == 1).then_some(c)
4745 })
4746 .expect("a rigid UNSAT random 3-SAT exists — the finite hard residue is real");
4747 match weakest_crushing_rung(5, &residue.clauses, 5) {
4748 ProofRung::Nullstellensatz { min_degree } => {
4749 assert!(min_degree >= 3, "the residue needs genuine algebraic degree, got {min_degree}")
4750 }
4751 other => panic!("the rigid residue should land on the NS rung, got {other:?}"),
4752 }
4753 let re = e_of(&residue.clauses);
4754 assert!(
4755 crate::pigeonhole::counting_certificate(&re).is_none() && !crate::xorsat::refute_via_parity(&re),
4756 "the rigid residue is invisible to every narrow cut — that silence IS the wall"
4757 );
4758
4759 let cc = crate::families::clique_coloring(3, 3).0; assert!(is_sat(cc.num_vars, &cc.clauses), "clique_coloring(3,3) is SAT");
4765 assert_eq!(
4766 weakest_crushing_rung(cc.num_vars, &cc.clauses, 2),
4767 ProofRung::BeyondBudget,
4768 "a satisfiable instance fires no cut"
4769 );
4770 assert_eq!(
4771 weakest_crushing_rung(5, &residue.clauses, 2),
4772 ProofRung::BeyondBudget,
4773 "below its degree, hard-UNSAT looks identical to SAT — the detectors cannot tell them apart"
4774 );
4775 }
4776
4777 #[test]
4785 fn the_finite_hard_residue_exists_even_though_unbounded_random_cannot() {
4786 let sat = |nv: usize, cl: &[Vec<Lit>]| {
4787 (0u64..(1u64 << nv)).any(|x| {
4788 cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
4789 })
4790 };
4791 let mut found = None;
4793 for seed in 0u64..600 {
4794 let c = crate::families::random_3sat(5, 26, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
4795 if !sat(5, &c.clauses) && automorphism_group_size(5, &c.clauses) == 1 {
4796 found = Some(c);
4797 break;
4798 }
4799 }
4800 let cnf = found.expect("a rigid UNSAT random 3-SAT exists — the finite hard residue is real");
4801
4802 let d = diagnose(5, &cnf.clauses);
4804 assert_eq!(d.symmetry_bits, 0.0, "rigid — |Aut| = 1, no symmetry");
4805 assert_eq!(d.cut, None, "no counting/parity/cardinality cut applies");
4806
4807 let min_degree = (1..=5).find(|°| crate::polycalc::nullstellensatz_refutes(5, &cnf.clauses, deg));
4809 assert!(min_degree.is_some(), "decided by Nullstellensatz within the dimension cap (≤ n)");
4810 assert!(min_degree.unwrap() >= 3, "it needed real algebra (degree ≥ clause width), not a cheap cut");
4811 }
4812
4813 #[test]
4819 fn we_break_on_whatever_symmetry_exists_never_assuming() {
4820 let mut found = None;
4822 for seed in 0u64..400 {
4823 let cnf = crate::families::random_3sat(8, 11, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
4824 if automorphism_group_size(cnf.num_vars, &cnf.clauses) > 1 {
4825 found = Some(cnf);
4826 break;
4827 }
4828 }
4829 let cnf = found.expect("some random instance carries accidental symmetry — we don't assume");
4830 let bits = symmetry_entropy_bits(cnf.num_vars, &cnf.clauses);
4831 assert!(bits > 0.0, "we FOUND accidental symmetry in the random instance: {bits} bits");
4832
4833 let (sym_sat, pruned) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
4836 let (plain_sat, plain) = decide_laddered_nocut(cnf.num_vars, &cnf.clauses);
4837 assert_eq!(sym_sat, plain_sat, "breaking on the accidental symmetry preserves the verdict");
4838 assert!(
4839 pruned.nodes <= plain.nodes,
4840 "we broke on the accidental symmetry, cutting branches: {} ≤ {}",
4841 pruned.nodes,
4842 plain.nodes
4843 );
4844 }
4845
4846 #[test]
4852 fn the_structure_census() {
4853 use std::fmt::Write;
4854 let mut chart = String::from("family bits cut residue\n");
4855 chart.push_str("-------------------- ----- ------------- -------------------\n");
4856 let mut row = |name: &str, nv: usize, cl: &[Vec<Lit>]| -> Option<Vec<Vec<Lit>>> {
4857 let bits = symmetry_entropy_bits(nv, cl);
4858 let cut = clauses_to_expr(cl).and_then(|e| {
4859 if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
4860 Some(Shadow::Counting)
4861 } else if crate::xorsat::refute_via_parity(&e) {
4862 Some(Shadow::Parity)
4863 } else if crate::pseudo_boolean::refute_clausal(&e) {
4864 Some(Shadow::CuttingPlanes)
4865 } else {
4866 None
4867 }
4868 });
4869 let core = find_random_core(nv, cl, 100);
4870 let residue = match &core {
4871 None => "— (all structure)".to_string(),
4872 Some(c) => format!("{} clauses (RANDOM)", c.len()),
4873 };
4874 let _ = writeln!(chart, "{name:<20} {bits:>5.1} {:<13} {residue}", format!("{cut:?}"));
4875 core
4876 };
4877
4878 let php = crate::families::php(5).0;
4880 assert_eq!(row("pigeonhole(5)", php.num_vars, &php.clauses), None, "pigeonhole: no randomness");
4881 let cc = crate::families::clique_coloring(4, 3).0;
4882 assert_eq!(row("clique_coloring(4,3)", cc.num_vars, &cc.clauses), None, "clique: no randomness");
4883 let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
4884 assert_eq!(row("tseitin(8)", t.num_vars, &t.clauses), None, "tseitin: no randomness");
4885
4886 let rnd = crate::families::random_3sat(14, 58, 0xC0FFEE);
4888 let residue = row("random_3sat(14,58)", rnd.num_vars, &rnd.clauses);
4889 assert!(residue.is_some(), "random is the only family with an irreducible random residue");
4890
4891 println!("\n{chart}");
4892 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
4893 if std::fs::create_dir_all(&dir).is_ok() {
4894 let _ = std::fs::write(
4895 dir.join("structure_census.txt"),
4896 format!("STRUCTURE CENSUS — every structured family is fully crushed by structure (no random\nresidue); random is the ONLY family that leaves an irreducible core. Structure is always\nexploitable; randomness is the sole irreducible thing.\n\n{chart}\n"),
4897 );
4898 }
4899 }
4900
4901 #[test]
4906 fn finding_the_randomness_isolates_the_structureless_core() {
4907 let php = crate::families::php(4).0;
4909 assert_eq!(find_random_core(php.num_vars, &php.clauses, 50), None, "pigeonhole has no random core");
4910
4911 let rnd = crate::families::random_3sat(10, 26, 0xBEEF);
4914 let mut padded = rnd.clauses.clone();
4915 let shell = rnd.num_vars as u32;
4916 padded.push(vec![Lit::new(shell, true), Lit::new(0, true)]); let nv = rnd.num_vars + 1;
4918
4919 if let Some(core) = find_random_core(nv, &padded, 50) {
4920 assert!(core.iter().all(|c| c.iter().all(|l| l.var() != shell)), "the structural shell is stripped");
4922 let d = diagnose(nv, &core);
4926 assert!(d.cut.is_none(), "the isolated core has no exploitable cut — it's the randomness: {d:?}");
4927 assert_eq!(
4928 find_random_core(nv, &core, 50),
4929 Some(core.clone()),
4930 "the core is a reduction fixpoint — nothing strips it further"
4931 );
4932 }
4933 }
4934
4935 #[test]
4941 fn the_whole_portfolio_agrees_with_brute_force() {
4942 fn sm(s: &mut u64) -> u64 {
4943 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
4944 let mut z = *s;
4945 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4946 z ^ (z >> 31)
4947 }
4948 let mut state = 0x6005_0001u64;
4949 for _ in 0..100 {
4950 let nv = 4 + (sm(&mut state) % 5) as usize; let m = 3 + (sm(&mut state) % 12) as usize;
4952 let mut cl: Vec<Vec<Lit>> = Vec::new();
4953 for _ in 0..m {
4954 let mut c = Vec::new();
4955 for v in 0..nv {
4956 if sm(&mut state) % 3 == 0 {
4957 c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
4958 }
4959 }
4960 if !c.is_empty() {
4961 cl.push(c);
4962 }
4963 }
4964 if cl.is_empty() {
4965 continue;
4966 }
4967 let brute = (0u64..(1u64 << nv)).any(|x| {
4968 cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
4969 });
4970
4971 if let Some(sat) = crush(nv, &cl, 1_000_000) {
4973 assert_eq!(sat, brute, "crush disagrees: {cl:?}");
4974 }
4975 if let Some(sat) = autocarve(nv, &cl, 1_000_000) {
4976 assert_eq!(sat, brute, "autocarve disagrees: {cl:?}");
4977 }
4978 let (sym, _) = decide_laddered_sym(nv, &cl, true);
4979 assert_eq!(sym, brute, "symmetry-pruned ladder disagrees: {cl:?}");
4980 let (plain, _) = decide_laddered(nv, &cl);
4981 assert_eq!(plain, brute, "plain ladder disagrees: {cl:?}");
4982 let (nocut, _) = decide_laddered_nocut(nv, &cl);
4983 assert_eq!(nocut, brute, "cut-free baseline disagrees: {cl:?}");
4984 }
4985 }
4986
4987 #[test]
4993 fn auto_advance_drives_structure_to_its_fixpoint() {
4994 let php = crate::families::php(4).0;
4996 let (status, trace) = auto_advance(php.num_vars, &php.clauses, 50);
4997 assert_eq!(status, AdvanceStatus::Decided(false), "pigeonhole decided: {trace:?}");
4998 assert!(trace.last().unwrap().lever.contains("cut"), "by the cut: {trace:?}");
4999
5000 let mut layered = php.clauses.clone();
5002 layered.push(vec![Lit::new(php.num_vars as u32, true), Lit::new(0, true)]);
5003 let (st, tr) = auto_advance(php.num_vars + 1, &layered, 50);
5004 assert_eq!(st, AdvanceStatus::Decided(false), "layered decided: {tr:?}");
5005 assert!(tr.len() >= 2, "carve then cut — multiple advance steps: {tr:?}");
5006
5007 let rnd = crate::families::random_3sat(14, 58, 0xC0FFEE);
5009 let (sr, rtr) = auto_advance(rnd.num_vars, &rnd.clauses, 50);
5010 assert!(
5011 matches!(sr, AdvanceStatus::StructurelessResidue { .. }),
5012 "random advances to the irreducible residue: {sr:?}"
5013 );
5014 assert!(!rtr.iter().any(|s| s.lever.contains("cut")), "no certified cut on the residue: {rtr:?}");
5015 }
5016
5017 #[test]
5023 fn diagnose_auto_discovers_the_applicable_levers() {
5024 let php = crate::families::php(4).0;
5026 let dp = diagnose(php.num_vars, &php.clauses);
5027 assert_eq!(dp.cut, Some(Shadow::Counting), "pigeonhole offers the counting cut: {dp:?}");
5028 assert!(dp.symmetry_bits > 0.0, "and rich symmetry: {dp:?}");
5029 let lp = applicable_levers(&dp);
5030 assert!(lp.iter().any(|s| s.contains("counting")), "menu lists the counting cut: {lp:?}");
5031
5032 let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
5034 assert_eq!(diagnose(t.num_vars, &t.clauses).cut, Some(Shadow::Parity), "Tseitin offers parity");
5035
5036 let rnd = crate::families::random_3sat(14, 55, 0xC0FFEE);
5038 let dr = diagnose(rnd.num_vars, &rnd.clauses);
5039 assert_eq!(dr.cut, None, "random offers no cut: {dr:?}");
5040 let lr = applicable_levers(&dr);
5041 assert!(
5042 lr.iter().any(|s| s.contains("backdoor") || s.contains("carving") || s.contains("residue")),
5043 "the menu honestly falls back to backdoor/branch: {lr:?}"
5044 );
5045 }
5046
5047 #[test]
5053 fn the_complexity_spectrum_is_quotient_size() {
5054 use std::fmt::Write;
5055 let mut chart = String::from("family clauses quotient cut core\n");
5056 chart.push_str("-------------------- ------- -------- ------------- ----\n");
5057 let mut row = |name: String, nv: usize, cl: &[Vec<Lit>]| -> StructuralProfile {
5058 let p = structural_profile(nv, cl);
5059 let _ = writeln!(
5060 chart,
5061 "{name:<20} {:>7} {:>8} {:<13} {:>4}",
5062 p.clauses, p.quotient, format!("{:?}", p.cut), p.core_clauses
5063 );
5064 p
5065 };
5066
5067 let php = crate::families::php(5).0;
5068 let php_p = row("pigeonhole(5)".into(), php.num_vars, &php.clauses);
5069 let tsei = crate::families::tseitin_expander(8, 0x51).1;
5070 let tsei_p = row("tseitin(8)".into(), tsei.num_vars, &tsei.clauses);
5071 let cc = crate::families::clique_coloring(4, 3).0;
5072 let cc_p = row("clique_coloring(4,3)".into(), cc.num_vars, &cc.clauses);
5073 let rnd = crate::families::random_3sat(14, 50, 0xC0FFEE);
5074 let rnd_p = row("random_3sat(14,50)".into(), rnd.num_vars, &rnd.clauses);
5075
5076 assert!(php_p.quotient <= 3 && php_p.cut.is_some(), "pigeonhole: tiny quotient, a cut: {php_p:?}");
5078 assert!(tsei_p.quotient <= 5 && tsei_p.cut == Some(Shadow::Parity), "tseitin: small quotient, parity: {tsei_p:?}");
5079 assert!(cc_p.quotient <= 3 && cc_p.cut.is_some(), "clique: tiny quotient, a cut: {cc_p:?}");
5080 assert!(
5081 rnd_p.cut.is_none() && rnd_p.quotient * 2 > rnd_p.clauses,
5082 "random: no cut, quotient near the full clause count — the interesting residue: {rnd_p:?}"
5083 );
5084
5085 println!("\n{chart}");
5086 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
5087 if std::fs::create_dir_all(&dir).is_ok() {
5088 let _ = std::fs::write(
5089 dir.join("complexity_spectrum.txt"),
5090 format!("THE COMPLEXITY SPECTRUM IS QUOTIENT SIZE — how far symmetry collapses the cube predicts\neverything: a tiny orbit-type quotient comes with a single certified cut; a full quotient comes\nwith no cut and an irreducible core. Difficulty is quotient size.\n\n{chart}\n"),
5091 );
5092 }
5093 }
5094
5095 #[test]
5100 fn the_auto_collapse_spreads_across_families() {
5101 use std::fmt::Write;
5102 let mut table = String::from("family vars clauses rule_types shadow\n");
5103 let mut record = |name: &str, sig: &FamilySignature| {
5104 let _ = writeln!(
5105 table,
5106 "{name:<20} {:>4} {:>7} {:>10} {:?}",
5107 sig.num_vars, sig.clauses, sig.rule_types, sig.shadow
5108 );
5109 };
5110
5111 let php = crate::families::php(5).0;
5113 let php_sig = abstract_signature(php.num_vars, &php.clauses);
5114 record("pigeonhole(5)", &php_sig);
5115 assert_eq!(php_sig.shadow, Some(Shadow::Counting), "pigeonhole is a counting cover");
5116 assert!(php_sig.rule_types <= 4, "pigeonhole collapses to a few rule-types");
5117
5118 let cc = crate::families::clique_coloring(4, 3).0;
5120 let cc_sig = abstract_signature(cc.num_vars, &cc.clauses);
5121 record("clique_coloring(4,3)", &cc_sig);
5122 assert!(cc_sig.shadow.is_some(), "clique-coloring is refuted by a shadow");
5123
5124 let (_, tcnf, _) = crate::families::tseitin_expander(8, 0x51);
5126 let t_sig = abstract_signature(tcnf.num_vars, &tcnf.clauses);
5127 record("tseitin(8)", &t_sig);
5128 assert_eq!(t_sig.shadow, Some(Shadow::Parity), "Tseitin is a parity cover");
5129
5130 let rnd = crate::families::random_3sat(14, 40, 0xC0FFEE);
5132 let r_sig = abstract_signature(rnd.num_vars, &rnd.clauses);
5133 record("random_3sat(14,40)", &r_sig);
5134 assert_eq!(r_sig.shadow, None, "random hardness is not a recognized shadow class");
5135 assert!(r_sig.rule_types > 2, "random rules spread across many types — no global collapse");
5136
5137 println!("\n{table}");
5138 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
5139 if std::fs::create_dir_all(&dir).is_ok() {
5140 let _ = std::fs::write(
5141 dir.join("family_taxonomy.txt"),
5142 format!("ABSTRACT FAMILY TAXONOMY — symmetry-break the rules, probe the shadows\n\n{table}\n"),
5143 );
5144 }
5145 }
5146
5147 #[test]
5153 fn the_abstract_certificate_is_scale_invariant_where_the_closure_explodes() {
5154 for n in [4usize, 8, 16, 32] {
5155 let cert = pigeonhole_abstract_refutation(n).expect("pigeonhole refuted at every scale");
5156 assert_eq!(cert.rule_types, 2, "always exactly two rule-types — the abstraction is scale-invariant");
5157 assert_eq!(cert.witness.pigeons, n as u128);
5158 assert_eq!(cert.witness.holes, (n - 1) as u128);
5159 assert!(cert.witness.pigeons > cert.witness.holes, "the counting invariant refutes");
5160 assert!(crate::pigeonhole::check_counting_cert(&cert.witness), "the O(1) witness re-checks");
5161 let e = clauses_to_expr(&crate::families::php(n).0.clauses).unwrap();
5163 assert_eq!(crate::sat::prove_unsat(&e), crate::sat::UnsatOutcome::Refuted);
5164 }
5165 }
5166
5167 #[test]
5175 fn symmetric_resolution_refutes_pigeonhole_through_a_bounded_orbit_pattern() {
5176 let cover = php_cover(3);
5177 let gens = php_symmetries(3);
5178 let empty = Subcube { n: cover.n, care: 0, value: 0 };
5179
5180 let growth = symmetric_resolution_growth(&cover, &gens, 8);
5183 let last = *growth.last().unwrap();
5184 assert_eq!(last, growth[growth.len() - 2], "the resolution closure reaches a fixpoint");
5185 let (raw_fix, orbit_fix) = last;
5186 assert!(
5187 orbit_fix * 4 < raw_fix,
5188 "orbit-types {orbit_fix} ≪ raw {raw_fix}: symmetry collapses the derived rules"
5189 );
5190
5191 let mut raw: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
5193 let mut refuted = false;
5194 for _ in 0..8 {
5195 let current: Vec<Subcube> = raw.iter().copied().collect();
5196 for i in 0..current.len() {
5197 for j in (i + 1)..current.len() {
5198 if let Some((_, r)) = current[i].resolve(¤t[j]) {
5199 raw.insert(r);
5200 }
5201 }
5202 }
5203 if raw.contains(&empty) {
5204 refuted = true;
5205 break;
5206 }
5207 }
5208 assert!(refuted, "resolution closes PHP(3) to the empty clause — a refutation on the cube");
5209 }
5210
5211 #[test]
5214 fn symmetric_resolution_growth_is_monotone_and_orbit_bounded() {
5215 let cover = php_cover(3);
5216 let gens = php_symmetries(3);
5217 let growth = symmetric_resolution_growth(&cover, &gens, 6);
5218 for (raw, orbits) in &growth {
5219 assert!(orbits <= raw, "orbit-types never exceed raw rules");
5220 }
5221 for w in growth.windows(2) {
5222 assert!(w[1].0 >= w[0].0, "the raw closure only grows (monotone)");
5223 assert!(w[1].1 >= w[0].1, "and so does the orbit-type set");
5224 }
5225 }
5226
5227 #[test]
5231 fn resolution_nets_a_new_rule_covering_both_neighbors() {
5232 let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)], 4);
5233 let d = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, false)], 4);
5234 let (pivot, resolvent) = c.resolve(&d).expect("neighbors across x2 must resolve");
5235 assert_eq!(pivot, 2);
5236 assert_eq!(resolvent.clause_literals(), vec![(0, true), (1, true)], "resolvent is (x0 ∨ x1)");
5237 let union: BTreeSet<Corner> = c.footprint().into_iter().chain(d.footprint()).collect();
5238 let merged: BTreeSet<Corner> = resolvent.footprint().into_iter().collect();
5239 assert_eq!(merged, union, "the derived rule covers both neighbors and nothing more");
5240 assert_eq!(resolvent.dimension(), c.dimension() + 1, "one pivot freed ⟹ one dimension larger");
5241 }
5242
5243 #[test]
5246 fn resolution_matches_clause_resolution() {
5247 let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, false), Lit::new(2, true)], 5);
5248 let d = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false), Lit::new(3, true)], 5);
5249 let (pivot, r) = c.resolve(&d).expect("resolve on x0");
5250 assert_eq!(pivot, 0);
5251 let got: BTreeSet<(usize, bool)> = r.clause_literals().into_iter().collect();
5252 let want: BTreeSet<(usize, bool)> = [(1, false), (2, true), (3, true)].into_iter().collect();
5253 assert_eq!(got, want);
5254
5255 let e = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true)], 5);
5256 let f = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false)], 5);
5257 assert_eq!(e.resolve(&f), None, "a second clash blocks resolution (tautology)");
5258
5259 let g = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true)], 5);
5260 let h = Subcube::blocker(&[Lit::new(0, true), Lit::new(2, true)], 5);
5261 assert_eq!(g.resolve(&h), None, "no opposite literal ⟹ no resolution");
5262 }
5263
5264 #[test]
5268 fn resolution_commutes_with_symmetry() {
5269 let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, false), Lit::new(2, true)], 4);
5270 let d = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false), Lit::new(3, true)], 4);
5271 let sigma = CubeSym { perm: vec![3, 1, 0, 2], flip: vec![false, true, false, true] };
5272
5273 let (pivot, resolvent) = c.resolve(&d).unwrap();
5274 let (pivot_img, resolvent_img) =
5275 sigma.map_subcube(&c).resolve(&sigma.map_subcube(&d)).expect("the images still resolve");
5276 assert_eq!(pivot_img, sigma.perm[pivot], "the pivot moves with the symmetry");
5277 assert_eq!(
5278 resolvent_img,
5279 sigma.map_subcube(&resolvent),
5280 "resolution and symmetry commute ⟹ derived rules respect the orbits"
5281 );
5282 }
5283
5284 #[test]
5286 fn referencing_one_rule_nets_its_neighbors() {
5287 let cnf = DimacsCnf {
5288 num_vars: 3,
5289 clauses: vec![
5290 vec![Lit::new(0, true), Lit::new(1, true)],
5291 vec![Lit::new(0, false), Lit::new(2, true)],
5292 vec![Lit::new(1, true), Lit::new(2, true)],
5293 ],
5294 };
5295 let cover = Cover::of_cnf(&cnf);
5296 let neighbors = cover.neighbors(0);
5297 assert_eq!(neighbors.len(), 1, "only clause 1 is a resolution neighbor of clause 0");
5298 let (j, pivot, resolvent) = &neighbors[0];
5299 assert_eq!(*j, 1);
5300 assert_eq!(*pivot, 0);
5301 let lits: BTreeSet<(usize, bool)> = resolvent.clause_literals().into_iter().collect();
5302 assert_eq!(lits, [(1, true), (2, true)].into_iter().collect(), "the netted rule is (x1 ∨ x2)");
5303 }
5304
5305 #[test]
5311 fn there_is_no_random_only_unfound_structure() {
5312 let cnf = crate::families::random_3sat(11, 20, 0xBEEF);
5313 let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
5314
5315 assert!(
5318 backdoor.len() < cnf.num_vars,
5319 "backdoor {} must be smaller than {} variables",
5320 backdoor.len(),
5321 cnf.num_vars
5322 );
5323 assert!(
5324 is_strong_backdoor_to_2sat(&cnf.clauses, cnf.num_vars, &backdoor),
5325 "every fixing of the backdoor must leave a 2-SAT residual"
5326 );
5327
5328 let via_backdoor = decide_sat_via_2sat_backdoor(&cnf.clauses, cnf.num_vars, &backdoor);
5331 let e = clauses_to_expr(&cnf.clauses).expect("non-empty random instance");
5332 match crate::sat::prove_unsat(&e) {
5333 crate::sat::UnsatOutcome::Refuted => assert!(!via_backdoor, "prover says UNSAT"),
5334 crate::sat::UnsatOutcome::Sat(_) => assert!(via_backdoor, "prover says SAT"),
5335 crate::sat::UnsatOutcome::Unsupported => panic!("prover should decide this instance"),
5336 }
5337 }
5338
5339 #[test]
5344 fn a_random_instances_symmetry_is_definite_not_absent() {
5345 let cnf = crate::families::random_3sat(11, 20, 0xBEEF);
5346 let cover = Cover::of_cnf(&cnf);
5347 let sig = cover.discovered_rule_symmetry();
5348 assert!(sig.rule_orbits * 2 > sig.blockers, "global symmetry is small but definite: {sig:?}");
5351 let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
5352 assert!(!backdoor.is_empty(), "the structure is there — it is local (a backdoor)");
5353 }
5354
5355 #[test]
5359 fn symmetric_backdoor_branches_collapse_for_speed() {
5360 let n = 4; let (cnf, _) = crate::families::php(n);
5362 let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
5363 assert!(is_strong_backdoor_to_2sat(&cnf.clauses, cnf.num_vars, &backdoor));
5364
5365 let sat = decide_sat_via_2sat_backdoor(&cnf.clauses, cnf.num_vars, &backdoor);
5368 assert!(!sat, "PHP(4) is UNSAT: no backdoor branch leaves a satisfiable 2-SAT residual");
5369
5370 let branches = 1u64 << backdoor.len();
5373 let orbits = backdoor_branch_orbit_count(&backdoor, &php_perm_symmetries(n));
5374 assert!(orbits < branches, "symmetry collapses {branches} branches to {orbits} — speed");
5375 }
5376
5377 #[test]
5382 fn geometric_and_scalable_rule_orbits_are_the_same_quotient() {
5383 for n in 2..=7 {
5384 let cover = php_cover(n); let geometric = cover.blocker_orbits(&php_symmetries(n)).unwrap().len();
5386 let (cnf, _) = crate::families::php(n);
5387 let scalable = clause_orbits(&cnf.clauses, &php_perm_symmetries(n)).len();
5388 assert_eq!(geometric, scalable, "PHP({n}): geometric={geometric} scalable={scalable}");
5389 assert_eq!(geometric, 2, "and both see the two essential pigeonhole rules");
5390 }
5391 }
5392
5393 #[test]
5397 #[ignore = "scale-walk; banks the rule-symmetry complexity-limit chart"]
5398 fn rule_symmetry_complexity_limit_chart() {
5399 let mut chart = String::from(" n vars corners blockers gens rule_orbits\n");
5400 chart.push_str("--- ----- ------------ --------- ----- -----------\n");
5401 for n in 2..=24 {
5402 let sig = pigeonhole_rule_symmetry(n);
5403 let vars = n * (n - 1);
5404 chart.push_str(&format!(
5405 "{:>3} {:>5} 2^{:<10} {:>9} {:>5} {}\n",
5406 n, vars, vars, sig.blockers, sig.generators, sig.rule_orbits
5407 ));
5408 assert_eq!(sig.rule_orbits, 2, "rule symmetry must stay at 2 at every scale (n = {n})");
5409 }
5410 println!("\n{chart}");
5411 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
5412 if std::fs::create_dir_all(&dir).is_ok() {
5413 let _ = std::fs::write(
5414 dir.join("rule_symmetry_limits.txt"),
5415 format!(
5416 "RULE-SYMMETRY COMPLEXITY LIMIT — pigeonhole rules collapse to 2 orbits at every scale,\n\
5417 computed in milliseconds over the polynomial blocker set while the cube itself grows\n\
5418 to 2^{{n(n-1)}} corners. Two essential rules describe the entire infinite family.\n\n{chart}\n"
5419 ),
5420 );
5421 }
5422 }
5423}