1use std::collections::BTreeMap;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct PbConstraint {
25 terms: BTreeMap<usize, (i64, bool)>,
26 degree: i64,
27}
28
29impl PbConstraint {
30 pub fn at_least(lits: &[(usize, bool)], k: i64) -> PbConstraint {
33 let terms = lits.iter().map(|&(v, s)| (v, (1, s))).collect();
34 PbConstraint { terms, degree: k }
35 }
36
37 pub fn clause(lits: &[(usize, bool)]) -> PbConstraint {
40 Self::at_least(lits, 1)
41 }
42
43 pub fn at_most(lits: &[(usize, bool)], k: i64) -> PbConstraint {
45 let negated: Vec<(usize, bool)> = lits.iter().map(|&(v, s)| (v, !s)).collect();
46 Self::at_least(&negated, lits.len() as i64 - k)
47 }
48
49 pub fn degree(&self) -> i64 {
51 self.degree
52 }
53
54 pub fn len(&self) -> usize {
56 self.terms.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
60 self.terms.is_empty()
61 }
62
63 pub fn add(&self, other: &PbConstraint) -> PbConstraint {
68 let mut terms = self.terms.clone();
69 let mut degree = self.degree + other.degree;
70 for (&v, &(c2, s2)) in &other.terms {
71 match terms.get(&v).copied() {
72 None => {
73 terms.insert(v, (c2, s2));
74 }
75 Some((c1, s1)) if s1 == s2 => {
76 terms.insert(v, (c1 + c2, s1));
77 }
78 Some((c1, _s1_opposite)) => {
79 let cancel = c1.min(c2);
80 degree -= cancel;
81 match c1.cmp(&c2) {
82 std::cmp::Ordering::Equal => {
83 terms.remove(&v);
84 }
85 std::cmp::Ordering::Greater => {
86 terms.insert(v, (c1 - c2, self.terms[&v].1));
87 }
88 std::cmp::Ordering::Less => {
89 terms.insert(v, (c2 - c1, s2));
90 }
91 }
92 }
93 }
94 }
95 PbConstraint { terms, degree }
96 }
97
98 pub fn multiply(&self, c: i64) -> PbConstraint {
101 assert!(c > 0, "multiply by a positive integer");
102 PbConstraint {
103 terms: self.terms.iter().map(|(&v, &(coeff, s))| (v, (coeff * c, s))).collect(),
104 degree: self.degree * c,
105 }
106 }
107
108 pub fn divide_round(&self, d: i64) -> PbConstraint {
113 assert!(d > 0, "divide by a positive integer");
114 let ceil = |x: i64| x.div_euclid(d) + i64::from(x.rem_euclid(d) != 0);
115 PbConstraint {
116 terms: self.terms.iter().map(|(&v, &(c, s))| (v, (ceil(c), s))).collect(),
117 degree: ceil(self.degree),
118 }
119 }
120
121 pub fn saturate(&self) -> PbConstraint {
125 let d = self.degree;
126 PbConstraint {
127 terms: self.terms.iter().map(|(&v, &(c, s))| (v, (c.min(d), s))).collect(),
128 degree: d,
129 }
130 }
131
132 pub fn is_contradiction(&self) -> bool {
135 let max_lhs: i64 = self.terms.values().map(|&(c, _)| c).sum();
136 self.degree > max_lhs
137 }
138
139 pub fn is_satisfied(&self, assign: &dyn Fn(usize) -> bool) -> bool {
142 let lhs: i64 = self
143 .terms
144 .iter()
145 .map(|(&v, &(c, s))| if assign(v) == s { c } else { 0 })
146 .sum();
147 lhs >= self.degree
148 }
149
150 pub fn new_weighted(terms: &[(usize, i64, bool)], degree: i64) -> PbConstraint {
153 PbConstraint { terms: terms.iter().map(|&(v, c, s)| (v, (c, s))).collect(), degree }
154 }
155
156 pub fn term(&self, v: usize) -> Option<(i64, bool)> {
158 self.terms.get(&v).copied()
159 }
160
161 pub fn terms(&self) -> impl Iterator<Item = (usize, i64, bool)> + '_ {
164 self.terms.iter().map(|(&v, &(c, s))| (v, c, s))
165 }
166}
167
168pub fn is_pb_symmetry(constraints: &[PbConstraint], perm: &[usize]) -> bool {
171 constraints
172 .iter()
173 .all(|c| (0..perm.len()).all(|v| c.term(perm[v]) == c.term(v)))
174}
175
176pub fn coeff_symmetry_generators(num_vars: usize, constraints: &[PbConstraint]) -> Vec<Vec<usize>> {
183 let profile = |v: usize| -> Vec<Option<(i64, bool)>> {
184 constraints.iter().map(|c| c.term(v)).collect()
185 };
186 let mut classes: BTreeMap<Vec<Option<(i64, bool)>>, Vec<usize>> = BTreeMap::new();
187 for v in 0..num_vars {
188 let p = profile(v);
189 if p.iter().all(|t| t.is_none()) {
190 continue; }
192 classes.entry(p).or_default().push(v);
193 }
194 let mut gens = Vec::new();
195 for vars in classes.values() {
196 for w in vars.windows(2) {
197 let mut p: Vec<usize> = (0..num_vars).collect();
198 p.swap(w[0], w[1]);
199 gens.push(p);
200 }
201 }
202 gens
203}
204
205pub fn php_refutation(n: usize) -> PbConstraint {
211 let holes = n - 1;
212 let var = |i: usize, h: usize| i * holes + h;
213 let mut acc: Option<PbConstraint> = None;
214 let mut absorb = |c: PbConstraint| {
215 acc = Some(match acc.take() {
216 None => c,
217 Some(a) => a.add(&c),
218 });
219 };
220 for i in 0..n {
222 let lits: Vec<(usize, bool)> = (0..holes).map(|h| (var(i, h), true)).collect();
223 absorb(PbConstraint::clause(&lits));
224 }
225 for h in 0..holes {
227 let lits: Vec<(usize, bool)> = (0..n).map(|i| (var(i, h), true)).collect();
228 absorb(PbConstraint::at_most(&lits, 1));
229 }
230 acc.expect("PHP has at least one constraint")
231}
232
233use crate::ProofExpr;
238use std::collections::HashMap;
239
240pub fn refute_clausal(e: &ProofExpr) -> bool {
251 let Some((rows, exclusions, nvars)) = extract_clausal(e) else {
252 return false;
253 };
254 if rows.is_empty() {
255 return false;
256 }
257 let mut uf = UnionFind::new(nvars);
259 for &(a, b) in &exclusions {
260 uf.union(a, b);
261 }
262 let excl_set: std::collections::HashSet<(usize, usize)> =
263 exclusions.iter().map(|&(a, b)| (a.min(b), a.max(b))).collect();
264 let mut comps: HashMap<usize, Vec<usize>> = HashMap::new();
265 for v in 0..nvars {
266 comps.entry(uf.find(v)).or_default().push(v);
267 }
268 let mut sum: Option<PbConstraint> = None;
269 let mut absorb = |c: PbConstraint| {
270 sum = Some(match sum.take() {
271 None => c,
272 Some(s) => s.add(&c),
273 });
274 };
275 for row in &rows {
276 let lits: Vec<(usize, bool)> = row.iter().map(|&v| (v, true)).collect();
277 absorb(PbConstraint::at_least(&lits, 1));
278 }
279 for members in comps.values() {
280 if members.len() < 2 {
281 continue;
282 }
283 let is_clique = members.iter().enumerate().all(|(i, &a)| {
285 members[i + 1..].iter().all(|&b| excl_set.contains(&(a.min(b), a.max(b))))
286 });
287 if !is_clique {
288 return false; }
290 let lits: Vec<(usize, bool)> = members.iter().map(|&v| (v, true)).collect();
291 absorb(PbConstraint::at_most(&lits, 1));
292 }
293 match sum {
294 Some(c) => c.is_contradiction() || c.saturate().is_contradiction(),
295 None => false,
296 }
297}
298
299fn extract_clausal(e: &ProofExpr) -> Option<(Vec<Vec<usize>>, Vec<(usize, usize)>, usize)> {
303 let mut conjuncts = Vec::new();
304 flatten_and(e, &mut conjuncts);
305 let mut idx: HashMap<String, usize> = HashMap::new();
306 let mut var = |name: &str, idx: &mut HashMap<String, usize>| -> usize {
307 let n = idx.len();
308 *idx.entry(name.to_string()).or_insert(n)
309 };
310 let mut rows = Vec::new();
311 let mut excl = Vec::new();
312 for c in conjuncts {
313 if let Some(atoms) = positive_disjunction(c) {
314 rows.push(atoms.iter().map(|a| var(a, &mut idx)).collect());
315 } else if let Some((a, b)) = exclusion_pair(c) {
316 excl.push((var(&a, &mut idx), var(&b, &mut idx)));
317 } else {
318 return None;
319 }
320 }
321 let nvars = idx.len();
322 Some((rows, excl, nvars))
323}
324
325fn flatten_and<'a>(e: &'a ProofExpr, out: &mut Vec<&'a ProofExpr>) {
326 let mut stack = vec![e];
331 while let Some(node) = stack.pop() {
332 match node {
333 ProofExpr::And(l, r) => {
334 stack.push(r);
335 stack.push(l);
336 }
337 other => out.push(other),
338 }
339 }
340}
341
342fn positive_disjunction(e: &ProofExpr) -> Option<Vec<String>> {
343 fn walk(e: &ProofExpr, out: &mut Vec<String>) -> bool {
344 match e {
345 ProofExpr::Or(l, r) => walk(l, out) && walk(r, out),
346 ProofExpr::Atom(a) => {
347 out.push(a.clone());
348 true
349 }
350 _ => false,
351 }
352 }
353 let mut atoms = Vec::new();
354 walk(e, &mut atoms).then_some(atoms)
355}
356
357fn exclusion_pair(e: &ProofExpr) -> Option<(String, String)> {
358 match e {
359 ProofExpr::Not(inner) => match inner.as_ref() {
360 ProofExpr::And(a, b) => match (a.as_ref(), b.as_ref()) {
361 (ProofExpr::Atom(a), ProofExpr::Atom(b)) => Some((a.clone(), b.clone())),
362 _ => None,
363 },
364 _ => None,
365 },
366 ProofExpr::Or(l, r) => match (l.as_ref(), r.as_ref()) {
367 (ProofExpr::Not(a), ProofExpr::Not(b)) => match (a.as_ref(), b.as_ref()) {
368 (ProofExpr::Atom(a), ProofExpr::Atom(b)) => Some((a.clone(), b.clone())),
369 _ => None,
370 },
371 _ => None,
372 },
373 _ => None,
374 }
375}
376
377struct UnionFind {
378 parent: Vec<usize>,
379}
380impl UnionFind {
381 fn new(n: usize) -> Self {
382 UnionFind { parent: (0..n).collect() }
383 }
384 fn find(&mut self, x: usize) -> usize {
385 if self.parent[x] != x {
386 let r = self.find(self.parent[x]);
387 self.parent[x] = r;
388 }
389 self.parent[x]
390 }
391 fn union(&mut self, a: usize, b: usize) {
392 let (ra, rb) = (self.find(a), self.find(b));
393 if ra != rb {
394 self.parent[ra] = rb;
395 }
396 }
397}
398
399use crate::cdcl::Lit;
400
401pub struct CardinalityTheory {
424 num_vars: usize,
425 constraints: Vec<(Vec<(usize, bool)>, i64)>,
427}
428
429impl CardinalityTheory {
430 pub fn new(num_vars: usize, constraints: &[PbConstraint]) -> Self {
435 let constraints = constraints
436 .iter()
437 .map(|pb| {
438 let lits: Vec<(usize, bool)> = pb
439 .terms()
440 .map(|(v, c, s)| {
441 assert_eq!(c, 1, "CardinalityTheory requires unit coefficients (got {c} on var {v})");
442 (v, s)
443 })
444 .collect();
445 (lits, pb.degree())
446 })
447 .collect();
448 CardinalityTheory { num_vars, constraints }
449 }
450}
451
452impl crate::cdcl::Theory for CardinalityTheory {
453 fn propagate(&mut self, trail: &[Lit]) -> Vec<Vec<Lit>> {
454 let mut a: Vec<Option<bool>> = vec![None; self.num_vars];
455 for &l in trail {
456 a[l.var() as usize] = Some(l.is_positive());
457 }
458 let mut out: Vec<Vec<Lit>> = Vec::new();
459 for (lits, k) in &self.constraints {
460 let k = *k;
461 if k <= 0 {
462 continue; }
464 let n = lits.len() as i64;
465 if k > n {
466 out.push(Vec::new()); continue;
468 }
469 let mut false_lits: Vec<(usize, bool)> = Vec::new();
470 let mut unassigned: Vec<(usize, bool)> = Vec::new();
471 let mut true_count = 0i64;
472 for &(v, s) in lits {
473 match a[v] {
474 Some(val) if val == s => true_count += 1,
475 Some(_) => false_lits.push((v, s)),
476 None => unassigned.push((v, s)),
477 }
478 }
479 if true_count >= k {
480 continue; }
482 let max_false = n - k; let fc = false_lits.len() as i64;
484 if fc > max_false {
485 let take = (max_false + 1) as usize;
487 out.push(false_lits[..take].iter().map(|&(v, s)| Lit::new(v as u32, s)).collect());
488 } else if fc == max_false && !unassigned.is_empty() {
489 let base: Vec<Lit> = false_lits.iter().map(|&(v, s)| Lit::new(v as u32, s)).collect();
491 for &(v, s) in &unassigned {
492 let mut c = base.clone();
493 c.push(Lit::new(v as u32, s));
494 out.push(c);
495 }
496 }
497 }
498 out
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 fn for_all_assignments(nvars: usize, mut f: impl FnMut(&dyn Fn(usize) -> bool)) {
507 for mask in 0..(1u32 << nvars) {
508 let assign = move |v: usize| (mask >> v) & 1 == 1;
509 f(&assign);
510 }
511 }
512
513 fn assert_implied(premises: &[&PbConstraint], derived: &PbConstraint, nvars: usize) {
516 for_all_assignments(nvars, |a| {
517 if premises.iter().all(|c| c.is_satisfied(a)) {
518 assert!(
519 derived.is_satisfied(a),
520 "UNSOUND rule: a premise-satisfying assignment falsifies the conclusion"
521 );
522 }
523 });
524 }
525
526 #[test]
527 fn addition_is_sound() {
528 let c1 = PbConstraint::at_least(&[(0, true), (1, true), (2, true)], 2);
530 let c2 = PbConstraint::at_least(&[(0, false), (1, true), (3, true)], 2);
531 assert_implied(&[&c1, &c2], &c1.add(&c2), 4);
532
533 let a = PbConstraint::clause(&[(0, true), (1, true)]);
535 let b = PbConstraint::at_least(&[(0, false), (1, false)], 1);
536 let sum = a.add(&b);
537 assert_implied(&[&a, &b], &sum, 2);
538 assert!(sum.is_empty() && sum.degree() == 0, "x+¬x cancels to a trivially-true 0 ≥ 0");
539 }
540
541 #[test]
542 fn multiply_divide_saturate_are_sound() {
543 let c = PbConstraint::at_least(&[(0, true), (1, true), (2, false)], 2);
544 assert_implied(&[&c], &c.multiply(3), 3);
545 assert_implied(&[&c], &c.saturate(), 3);
546 let d = PbConstraint { terms: [(0, (5, true)), (1, (3, true)), (2, (2, true))].into(), degree: 6 };
548 assert_implied(&[&d], &d.divide_round(2), 3);
549 assert_implied(&[&d], &d.divide_round(3), 3);
550 }
551
552 #[test]
553 fn contradiction_detection() {
554 let unsat = PbConstraint { terms: BTreeMap::new(), degree: 1 };
555 assert!(unsat.is_contradiction(), "0 ≥ 1 is a contradiction");
556 let tight = PbConstraint::at_least(&[(0, true), (1, true)], 2);
557 assert!(!tight.is_contradiction(), "x0+x1 ≥ 2 is satisfiable (both true)");
558 let over = PbConstraint::at_least(&[(0, true)], 2);
559 assert!(over.is_contradiction(), "x0 ≥ 2 is impossible for a 0/1 variable");
560 }
561
562 #[test]
563 fn pigeonhole_is_refuted_in_linear_size() {
564 for n in 2..=30 {
567 let refutation = php_refutation(n);
568 assert!(
569 refutation.is_contradiction(),
570 "PHP({n}) must collapse to a contradiction under cutting planes"
571 );
572 assert!(refutation.is_empty(), "every x meets its ¬x and cancels — the LHS is empty");
573 assert_eq!(refutation.degree(), 1, "the terminal is exactly 0 ≥ 1");
574 }
575 }
576
577 fn php_expr(n: usize) -> ProofExpr {
578 let holes = n - 1;
579 let p = |i: usize, h: usize| ProofExpr::Atom(format!("p_{i}_{h}"));
580 let mut clauses = Vec::new();
581 for i in 0..n {
582 clauses.push(
583 (0..holes).map(|h| p(i, h)).reduce(|a, b| ProofExpr::Or(Box::new(a), Box::new(b))).unwrap(),
584 );
585 }
586 for h in 0..holes {
587 for i in 0..n {
588 for j in (i + 1)..n {
589 clauses.push(ProofExpr::Not(Box::new(ProofExpr::And(Box::new(p(i, h)), Box::new(p(j, h))))));
590 }
591 }
592 }
593 clauses.into_iter().reduce(|a, b| ProofExpr::And(Box::new(a), Box::new(b))).unwrap()
594 }
595
596 fn feasible_expr(n: usize) -> ProofExpr {
597 let q = |i: usize, h: usize| ProofExpr::Atom(format!("q_{i}_{h}"));
599 let mut clauses = Vec::new();
600 for i in 0..n {
601 clauses.push((0..n).map(|h| q(i, h)).reduce(|a, b| ProofExpr::Or(Box::new(a), Box::new(b))).unwrap());
602 }
603 for h in 0..n {
604 for i in 0..n {
605 for j in (i + 1)..n {
606 clauses.push(ProofExpr::Not(Box::new(ProofExpr::And(Box::new(q(i, h)), Box::new(q(j, h))))));
607 }
608 }
609 }
610 clauses.into_iter().reduce(|a, b| ProofExpr::And(Box::new(a), Box::new(b))).unwrap()
611 }
612
613 #[test]
614 fn cutting_planes_refutes_pairwise_pigeonhole_from_cnf() {
615 for n in 2..=10 {
618 assert!(refute_clausal(&php_expr(n)), "cutting planes must refute pairwise PHP({n}) from CNF");
619 }
620 }
621
622 #[test]
623 fn cutting_planes_refutation_is_sound() {
624 for n in 1..=8 {
627 assert!(!refute_clausal(&feasible_expr(n)), "feasible({n}) must NOT be refuted");
628 }
629 let lone = ProofExpr::Or(
630 Box::new(ProofExpr::Atom("a".into())),
631 Box::new(ProofExpr::Atom("b".into())),
632 );
633 assert!(!refute_clausal(&lone), "a lone satisfiable clause is not a cutting-plane contradiction");
634 }
635
636 #[test]
637 fn pigeonhole_refutation_is_genuinely_unsat_small() {
638 let n = 3usize;
641 let holes = n - 1;
642 let var = |i: usize, h: usize| i * holes + h;
643 let mut cs = Vec::new();
644 for i in 0..n {
645 cs.push(PbConstraint::clause(&(0..holes).map(|h| (var(i, h), true)).collect::<Vec<_>>()));
646 }
647 for h in 0..holes {
648 cs.push(PbConstraint::at_most(&(0..n).map(|i| (var(i, h), true)).collect::<Vec<_>>(), 1));
649 }
650 let mut any = false;
651 for_all_assignments(n * holes, |a| {
652 if cs.iter().all(|c| c.is_satisfied(a)) {
653 any = true;
654 }
655 });
656 assert!(!any, "PHP(3,2) is UNSAT — no assignment satisfies all constraints");
657 }
658
659 #[test]
660 fn coefficient_symmetry_is_detected_and_sound() {
661 let weighted = PbConstraint::new_weighted(&[(0, 2, true), (1, 2, true), (2, 3, true)], 4);
664 let gens = coeff_symmetry_generators(3, &[weighted.clone()]);
665 assert_eq!(gens.len(), 1, "one generator: the x0 ↔ x1 swap");
666 assert_eq!(gens[0], vec![1, 0, 2], "x0 ↔ x1");
667 assert!(gens.iter().all(|g| is_pb_symmetry(&[weighted.clone()], g)));
669 for_all_assignments(3, |a| {
671 let swapped = |v: usize| a(if v == 0 { 1 } else if v == 1 { 0 } else { v });
672 assert_eq!(
673 weighted.is_satisfied(a),
674 weighted.is_satisfied(&swapped),
675 "the x0↔x1 swap preserves satisfaction"
676 );
677 });
678
679 let distinct = PbConstraint::new_weighted(&[(0, 1, true), (1, 2, true), (2, 3, true)], 3);
681 assert!(coeff_symmetry_generators(3, &[distinct]).is_empty(), "all-distinct coefficients: no symmetry");
682
683 let c1 = PbConstraint::new_weighted(&[(0, 2, true), (1, 2, true), (2, 2, true)], 3);
686 let c2 = PbConstraint::new_weighted(&[(0, 1, true), (1, 1, true), (2, 5, true)], 2);
687 let sysg = coeff_symmetry_generators(3, &[c1.clone(), c2.clone()]);
688 assert_eq!(sysg, vec![vec![1, 0, 2]], "only x0 ↔ x1 survives both constraints");
689 assert!(sysg.iter().all(|g| is_pb_symmetry(&[c1.clone(), c2.clone()], g)));
690 }
691
692 #[test]
695 fn cardinality_theory_forces_then_conflicts_on_at_least_two() {
696 use crate::cdcl::{Lit, Theory};
697 let lits = [(0usize, true), (1, true), (2, true)];
698 let mut th = CardinalityTheory::new(3, &[PbConstraint::at_least(&lits, 2)]);
699 let forced = th.propagate(&[Lit::new(0, false)]);
701 assert_eq!(forced.len(), 2, "two literals forced; got {forced:?}");
702 for c in &forced {
703 let free: Vec<&Lit> = c.iter().filter(|l| l.var() != 0).collect();
704 assert_eq!(free.len(), 1, "each reason is unit under {{x0=false}}: {c:?}");
705 assert!(free[0].is_positive(), "the forced literal is x_i true: {c:?}");
706 }
707 let conf = th.propagate(&[Lit::new(0, false), Lit::new(1, false)]);
709 assert!(!conf.is_empty(), "must conflict; got {conf:?}");
710 assert!(
711 conf.iter().any(|c| !c.is_empty() && c.iter().all(|l| (l.var() == 0 || l.var() == 1) && l.is_positive())),
712 "a conflict clause of the two now-false literals; got {conf:?}"
713 );
714 }
715
716 #[test]
720 fn cardinality_theory_alone_refutes_an_infeasible_system() {
721 use crate::cdcl::{Solver, SolveResult, Theory};
722 let lits = [(0usize, true), (1, true), (2, true)];
723 let card = vec![PbConstraint::at_least(&lits, 3), PbConstraint::at_most(&lits, 1)];
724 let mut s = Solver::new(3);
725 let mut t: Vec<Box<dyn Theory>> = vec![Box::new(CardinalityTheory::new(3, &card))];
726 assert!(matches!(s.solve_with(&mut t), SolveResult::Unsat), "≥3 ∧ ≤1 of three is UNSAT");
727 }
728
729 #[test]
732 fn incxor_alone_refutes_an_inconsistent_system() {
733 use crate::cdcl::{Solver, SolveResult, Theory};
734 use crate::xor_engine::IncXor;
735 use crate::xorsat::XorEquation;
736 let xor = vec![XorEquation::new(vec![0, 1], false), XorEquation::new(vec![0, 1], true)];
737 let mut s = Solver::new(2);
738 let mut t: Vec<Box<dyn Theory>> = vec![Box::new(IncXor::new(2, &xor))];
739 assert!(matches!(s.solve_with(&mut t), SolveResult::Unsat), "x0⊕x1=0 ∧ x0⊕x1=1 is UNSAT");
740 }
741
742 #[test]
747 fn fused_parity_and_cardinality_is_unsat_though_neither_alone_is() {
748 use crate::cdcl::{Solver, SolveResult, Theory};
749 use crate::xor_engine::XorEngine;
750 use crate::xorsat::XorEquation;
751 let xor = vec![XorEquation::new(vec![0, 1, 2], true)];
756 let lits = [(0usize, true), (1, true), (2, true)];
757 let card = vec![PbConstraint::at_least(&lits, 2), PbConstraint::at_most(&lits, 2)];
758
759 let mut s1 = Solver::new(3);
760 let mut t1: Vec<Box<dyn Theory>> = vec![Box::new(XorEngine::new(3, &xor))];
761 assert!(matches!(s1.solve_with(&mut t1), SolveResult::Sat(_)), "parity alone is SAT");
762
763 let mut s2 = Solver::new(3);
764 let mut t2: Vec<Box<dyn Theory>> = vec![Box::new(CardinalityTheory::new(3, &card))];
765 assert!(matches!(s2.solve_with(&mut t2), SolveResult::Sat(_)), "exactly-two alone is SAT");
766
767 let mut s = Solver::new(3);
768 let mut fused: Vec<Box<dyn Theory>> =
769 vec![Box::new(XorEngine::new(3, &xor)), Box::new(CardinalityTheory::new(3, &card))];
770 assert!(matches!(s.solve_with(&mut fused), SolveResult::Unsat), "odd-parity ∧ exactly-two is UNSAT");
771 }
772
773 #[test]
776 fn fused_parity_and_cardinality_sat_model_is_valid() {
777 use crate::cdcl::{Solver, SolveResult, Theory};
778 use crate::xor_engine::XorEngine;
779 use crate::xorsat::XorEquation;
780 let xor = vec![XorEquation::new(vec![0, 1, 2], false)];
781 let lits = [(0usize, true), (1, true), (2, true)];
782 let card = vec![PbConstraint::at_least(&lits, 2), PbConstraint::at_most(&lits, 2)];
783 let mut s = Solver::new(3);
784 let mut fused: Vec<Box<dyn Theory>> =
785 vec![Box::new(XorEngine::new(3, &xor)), Box::new(CardinalityTheory::new(3, &card))];
786 match s.solve_with(&mut fused) {
787 SolveResult::Sat(m) => {
788 assert_eq!(m.iter().filter(|&&b| b).count(), 2, "exactly two true: {m:?}");
789 assert!(!(m[0] ^ m[1] ^ m[2]), "even parity: {m:?}");
790 }
791 SolveResult::Unsat => panic!("even-parity ∧ exactly-two is SAT"),
792 }
793 }
794
795 #[test]
800 fn fused_solve_matches_brute_force() {
801 use crate::cdcl::{Lit, Solver, SolveResult, Theory};
802 use crate::xor_engine::XorEngine;
803 use crate::xorsat::XorEquation;
804 let mut st = 0xCA11_AB1Eu64;
805 let mut rng = || {
806 st ^= st << 13;
807 st ^= st >> 7;
808 st ^= st << 17;
809 st
810 };
811 for _ in 0..300 {
812 let n = 3 + (rng() % 3) as usize; let mut xor_specs: Vec<(Vec<usize>, bool)> = Vec::new();
814 for _ in 0..(1 + rng() % 2) {
815 let vars: Vec<usize> = (0..n).filter(|_| rng() % 2 == 0).collect();
816 if vars.len() >= 2 {
817 xor_specs.push((vars, rng() % 2 == 0));
818 }
819 }
820 let mut pbs: Vec<PbConstraint> = Vec::new();
821 for _ in 0..(1 + rng() % 2) {
822 let lits: Vec<(usize, bool)> =
823 (0..n).filter_map(|v| (rng() % 2 == 0).then(|| (v, rng() % 2 == 0))).collect();
824 if lits.len() < 2 {
825 continue;
826 }
827 let k = (rng() % (lits.len() as u64 + 1)) as i64;
828 pbs.push(if rng() % 2 == 0 { PbConstraint::at_least(&lits, k) } else { PbConstraint::at_most(&lits, k) });
829 }
830 let mut clauses: Vec<Vec<Lit>> = Vec::new();
831 for _ in 0..(rng() % 3) {
832 let c: Vec<Lit> =
833 (0..n).filter_map(|v| (rng() % 2 == 0).then(|| Lit::new(v as u32, rng() % 2 == 0))).collect();
834 if !c.is_empty() {
835 clauses.push(c);
836 }
837 }
838
839 let xor_ok = |x: u64| xor_specs.iter().all(|(vars, rhs)| (vars.iter().filter(|&&v| (x >> v) & 1 == 1).count() % 2 == 1) == *rhs);
840 let pb_ok = |x: u64| {
841 pbs.iter().all(|pb| {
842 let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
843 sum >= pb.degree()
844 })
845 };
846 let cl_ok = |x: u64| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()));
847 let brute_sat = (0u64..(1u64 << n)).any(|x| xor_ok(x) && pb_ok(x) && cl_ok(x));
848
849 let xeqs: Vec<XorEquation> = xor_specs.iter().map(|(v, r)| XorEquation::new(v.clone(), *r)).collect();
850 let mut s = Solver::new(n);
851 for c in &clauses {
852 s.add_clause(c.clone());
853 }
854 let mut theories: Vec<Box<dyn Theory>> =
855 vec![Box::new(XorEngine::new(n, &xeqs)), Box::new(CardinalityTheory::new(n, &pbs))];
856 let got = s.solve_with(&mut theories);
857 assert_eq!(
858 matches!(got, SolveResult::Sat(_)),
859 brute_sat,
860 "fused verdict must match brute force (n={n}, xor={xor_specs:?}, pbs={pbs:?}, clauses={clauses:?})"
861 );
862 if let SolveResult::Sat(m) = got {
863 let x = (0..n).fold(0u64, |acc, v| acc | ((m[v] as u64) << v));
864 assert!(xor_ok(x) && pb_ok(x) && cl_ok(x), "the fused model must satisfy every constraint: {m:?}");
865 }
866 }
867 }
868}