use crate::cdcl::{BudgetedResult, Lit, SolveResult, Solver, Var};
use crate::cnf::Cnf;
use crate::rup;
use crate::ProofExpr;
use std::collections::HashMap;
use std::collections::BTreeSet;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModelOutcome {
Sat(Vec<(String, bool)>),
Unsat,
Unsupported,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum UnsatOutcome {
Refuted,
Sat(Vec<(String, bool)>),
Unsupported,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EquivOutcome {
Equivalent,
Differ(Vec<(String, bool)>),
Unsupported,
}
pub fn find_model(e: &ProofExpr) -> ModelOutcome {
let mut cnf = Cnf::new();
if cnf.assert(e).is_none() {
return ModelOutcome::Unsupported;
}
let (mut solver, atom_of) = cnf.into_solver_with_atoms();
match solver.solve() {
SolveResult::Unsat => ModelOutcome::Unsat,
SolveResult::Sat(model) => ModelOutcome::Sat(decode_model_from(&atom_of, &model, &[e])),
}
}
pub fn prove_equivalence(a: &ProofExpr, b: &ProofExpr) -> EquivOutcome {
if a == b {
return EquivOutcome::Equivalent;
}
let neg_iff = ProofExpr::Not(Box::new(ProofExpr::Iff(
Box::new(a.clone()),
Box::new(b.clone()),
)));
match prove_unsat(&neg_iff) {
UnsatOutcome::Refuted => EquivOutcome::Equivalent,
UnsatOutcome::Sat(model) => EquivOutcome::Differ(model),
UnsatOutcome::Unsupported => EquivOutcome::Unsupported,
}
}
fn refutes_modular(e: &ProofExpr) -> bool {
let mut cnf = Cnf::new();
if cnf.assert(e).is_none() {
return false;
}
let Some(rec) = crate::modp::recover_from_cnf(cnf.num_vars(), cnf.clauses()) else {
return false;
};
if crate::modp::is_prime(rec.modulus) {
match crate::modp::solve(&rec.equations, rec.num_vars, rec.modulus) {
crate::modp::ModpOutcome::Unsat(combo) => {
crate::modp::is_refutation(&rec.equations, rec.num_vars, rec.modulus, &combo)
}
_ => false,
}
} else {
match crate::modm::solve(&rec.equations, rec.num_vars, rec.modulus) {
Some(crate::modm::ModmOutcome::Unsat { modulus, combo }) => {
crate::modm::is_refutation(&rec.equations, rec.num_vars, modulus, &combo)
}
_ => false,
}
}
}
fn refutes_by_collapse(e: &ProofExpr) -> bool {
let mut cnf = Cnf::new();
if cnf.assert(e).is_none() {
return false;
}
!matches!(
crate::lyapunov::auto_collapse(cnf.num_vars(), cnf.clauses()),
crate::lyapunov::AutoCollapse::None
)
}
fn gated_algebraic_degree(n: usize) -> usize {
const MONO_BUDGET: u128 = 4000;
let mut dmax = 0usize;
let mut monos: u128 = 1; let mut binom: u128 = 1; for d in 1..=n {
binom = binom * (n - d + 1) as u128 / d as u128;
if monos + binom > MONO_BUDGET {
break;
}
monos += binom;
dmax = d;
}
dmax
}
fn refutes_by_nullstellensatz(e: &ProofExpr) -> bool {
let mut cnf = Cnf::new();
if cnf.assert(e).is_none() {
return false;
}
let n = cnf.num_vars();
let d = gated_algebraic_degree(n);
if d == 0 {
return false;
}
crate::polycalc::nullstellensatz_refutes(n, cnf.clauses(), d)
}
fn refutes_by_polynomial_calculus(e: &ProofExpr) -> bool {
let mut cnf = Cnf::new();
if cnf.assert(e).is_none() {
return false;
}
let n = cnf.num_vars();
let d = gated_algebraic_degree(n);
if d == 0 {
return false;
}
crate::polycalc::polynomial_calculus_refutes(n, cnf.clauses(), d)
}
const CERTIFIED_SYMMETRY_VAR_CAP: usize = 64;
const EASY_SEARCH_CONFLICTS: u64 = 2_000;
pub fn prove_unsat_certified(e: &ProofExpr) -> Option<Vec<crate::proof::ProofStep>> {
let mut cnf = Cnf::new();
cnf.assert(e)?;
let r = crate::sym_certify::certified_unsat_auto(cnf.num_vars(), cnf.clauses());
r.refuted.then_some(r.steps)
}
pub fn prove_unsat(e: &ProofExpr) -> UnsatOutcome {
if crate::pigeonhole::decide_pigeonhole_unsat(e) {
return UnsatOutcome::Refuted;
}
if crate::ordering::refutes_ordering_principle(e) {
return UnsatOutcome::Refuted;
}
if crate::pseudo_boolean::refute_clausal(e) {
return UnsatOutcome::Refuted;
}
if crate::xorsat::refute_via_parity(e) {
return UnsatOutcome::Refuted;
}
if refutes_modular(e) {
return UnsatOutcome::Refuted;
}
let augmented = crate::symmetry::break_symmetries(e);
let mut cnf = Cnf::new();
if cnf.assert(&augmented).is_none() {
return UnsatOutcome::Unsupported;
}
let num_vars = cnf.num_vars();
let (mut solver, atom_of) = cnf.into_solver_with_atoms();
let first = match solver.solve_budgeted(EASY_SEARCH_CONFLICTS) {
BudgetedResult::Sat(model) => SolveResult::Sat(model),
BudgetedResult::Unsat => SolveResult::Unsat,
BudgetedResult::Budget => {
if refutes_by_collapse(e) {
return UnsatOutcome::Refuted;
}
if refutes_by_nullstellensatz(e) {
return UnsatOutcome::Refuted;
}
if refutes_by_polynomial_calculus(e) {
return UnsatOutcome::Refuted;
}
let mut base = Cnf::new();
if base.assert(e).is_some()
&& base.num_vars() <= CERTIFIED_SYMMETRY_VAR_CAP
&& crate::sym_certify::certified_unsat_auto(base.num_vars(), base.clauses()).refuted
{
return UnsatOutcome::Refuted;
}
let originals: Vec<Vec<Lit>> = solver.original_clauses().to_vec();
solver = Solver::new(num_vars);
for c in originals {
solver.add_clause(c);
}
solver.solve()
}
};
match first {
SolveResult::Sat(model) => UnsatOutcome::Sat(decode_model_from(&atom_of, &model, &[e])),
SolveResult::Unsat => {
let learned: Vec<Vec<Lit>> = solver.learned().iter().map(|c| c.lits.clone()).collect();
if rup::check_refutation(num_vars, solver.original_clauses(), &learned) {
UnsatOutcome::Refuted
} else {
UnsatOutcome::Unsupported
}
}
}
}
pub fn decode_model(cnf: &Cnf, model: &[bool], exprs: &[&ProofExpr]) -> Vec<(String, bool)> {
let mut atoms = BTreeSet::new();
for e in exprs {
collect_atoms(e, &mut atoms);
}
atoms
.into_iter()
.filter_map(|name| {
cnf.atom_value(&ProofExpr::Atom(name.clone()), model)
.map(|v| (name, v))
})
.collect()
}
pub fn decode_model_from(
atom_of: &HashMap<String, Var>,
model: &[bool],
exprs: &[&ProofExpr],
) -> Vec<(String, bool)> {
let mut atoms = BTreeSet::new();
for e in exprs {
collect_atoms(e, &mut atoms);
}
atoms
.into_iter()
.filter_map(|name| {
atom_of
.get(&format!("atom:{name}"))
.and_then(|&v| model.get(v as usize).copied())
.map(|val| (name, val))
})
.collect()
}
fn collect_atoms(e: &ProofExpr, out: &mut BTreeSet<String>) {
match e {
ProofExpr::Atom(name) => {
out.insert(name.clone());
}
ProofExpr::Not(p) => collect_atoms(p, out),
ProofExpr::And(p, q)
| ProofExpr::Or(p, q)
| ProofExpr::Implies(p, q)
| ProofExpr::Iff(p, q) => {
collect_atoms(p, out);
collect_atoms(q, out);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn atom(s: &str) -> ProofExpr {
ProofExpr::Atom(s.to_string())
}
fn not(e: ProofExpr) -> ProofExpr {
ProofExpr::Not(Box::new(e))
}
fn and(a: ProofExpr, b: ProofExpr) -> ProofExpr {
ProofExpr::And(Box::new(a), Box::new(b))
}
fn or(a: ProofExpr, b: ProofExpr) -> ProofExpr {
ProofExpr::Or(Box::new(a), Box::new(b))
}
fn implies(a: ProofExpr, b: ProofExpr) -> ProofExpr {
ProofExpr::Implies(Box::new(a), Box::new(b))
}
fn eval(e: &ProofExpr, env: &[(String, bool)]) -> bool {
match e {
ProofExpr::Atom(n) => env.iter().find(|(k, _)| k == n).map(|(_, v)| *v).unwrap_or(false),
ProofExpr::Not(p) => !eval(p, env),
ProofExpr::And(p, q) => eval(p, env) && eval(q, env),
ProofExpr::Or(p, q) => eval(p, env) || eval(q, env),
ProofExpr::Implies(p, q) => !eval(p, env) || eval(q, env),
ProofExpr::Iff(p, q) => eval(p, env) == eval(q, env),
_ => panic!("non-boolean node in test eval"),
}
}
#[test]
fn reflexive_equivalence_is_certified() {
let f = implies(atom("req@0"), atom("ack@0"));
assert_eq!(prove_equivalence(&f, &f), EquivOutcome::Equivalent);
}
#[test]
fn de_morgan_is_equivalent() {
let lhs = not(and(atom("a@0"), atom("b@0")));
let rhs = or(not(atom("a@0")), not(atom("b@0")));
assert_eq!(prove_equivalence(&lhs, &rhs), EquivOutcome::Equivalent);
}
#[test]
fn distributivity_is_equivalent() {
let lhs = and(atom("a@0"), or(atom("b@0"), atom("c@0")));
let rhs = or(and(atom("a@0"), atom("b@0")), and(atom("a@0"), atom("c@0")));
assert_eq!(prove_equivalence(&lhs, &rhs), EquivOutcome::Equivalent);
}
#[test]
fn distinct_tautologies_are_equivalent() {
let lhs = or(atom("p@0"), not(atom("p@0")));
let rhs = or(atom("q@0"), not(atom("q@0")));
assert_eq!(prove_equivalence(&lhs, &rhs), EquivOutcome::Equivalent);
}
#[test]
fn implication_is_not_its_consequent() {
let f = implies(atom("req@0"), atom("ack@0"));
let s = atom("ack@0");
match prove_equivalence(&f, &s) {
EquivOutcome::Differ(model) => {
assert_ne!(
eval(&f, &model),
eval(&s, &model),
"counterexample {:?} must distinguish the two formulas",
model
);
}
other => panic!("expected Differ, got {:?}", other),
}
}
#[test]
fn implication_is_not_its_converse() {
let f = implies(atom("req@0"), atom("ack@0"));
let s = implies(atom("ack@0"), atom("req@0"));
match prove_equivalence(&f, &s) {
EquivOutcome::Differ(model) => {
assert_ne!(eval(&f, &model), eval(&s, &model));
}
other => panic!("expected Differ, got {:?}", other),
}
}
#[test]
fn find_model_of_contradiction_is_unsat() {
assert_eq!(find_model(&and(atom("a@0"), not(atom("a@0")))), ModelOutcome::Unsat);
}
#[test]
fn find_model_of_satisfiable_returns_witness() {
let e = and(atom("a@0"), implies(atom("a@0"), atom("b@0")));
match find_model(&e) {
ModelOutcome::Sat(model) => {
assert!(eval(&e, &model), "returned model must actually satisfy the formula");
assert!(model.iter().any(|(k, v)| k == "a@0" && *v));
assert!(model.iter().any(|(k, v)| k == "b@0" && *v));
}
other => panic!("expected Sat, got {:?}", other),
}
}
}