Skip to main content

logicaffeine_proof/
pr.rs

1//! The **PR** (propagation-redundancy) checker — the trust tier for *model-removing* clause
2//! additions, and the keystone that makes certified symmetry breaking possible.
3//!
4//! RUP ([`crate::rup`]) certifies only clauses *implied* by the formula. A symmetry-breaking
5//! predicate is different in kind: it deletes satisfying assignments (keeping at least one per
6//! symmetry orbit), so it is satisfiability-preserving but **not** implied — RUP must and does
7//! reject it. Propagation redundancy (Heule, Kiesl & Biere, CADE 2017) closes exactly this
8//! gap: a clause `C` is redundant w.r.t. `F` with witness `ω` iff `ω` satisfies `C` and
9//! `F|α ⊢₁ F|ω`, where `α = ¬C` is the assignment falsifying `C` and `⊢₁` is implication by
10//! unit propagation. Adding such a `C` preserves satisfiability, with an independently
11//! checkable certificate.
12//!
13//! Two witness forms are checked, each with its own well-specified criterion:
14//!
15//! - [`Witness::Assignment`] `ω` — the classic PR criterion `ω ⊨ C` and `F|α ⊢₁ F|ω`. This is
16//!   the general redundancy tier (vivification, BVE, …).
17//! - [`Witness::Substitution`] `σ` — the **substitution-redundancy** criterion for a symmetry.
18//!   Repair a model `τ` of `F ∧ ¬C` by composing with `σ`: the result `τ∘σ` satisfies `F`
19//!   automatically when `σ(F) = F`, and satisfies `C` exactly when `τ ⊨ σ(C)`. So `C` is
20//!   redundant iff **`σ` is an automorphism of the database AND `F ∧ ¬C ⊢₁ σ(C)`** (Heule &
21//!   Biere, substitution redundancy). The automorphism is checked against the *current*
22//!   database, so an earlier predicate that breaks a later generator's symmetry simply makes
23//!   that check fail — fail-closed, no generator-ordering hazard.
24//!
25//! The checker reuses the very same tiny unit-propagation core as [`crate::rup`]: its
26//! simplicity IS the trust, and it is wrapped by a brute-force equisatisfiability oracle over
27//! BOTH witness forms in the tests so it can never bless a clause that turns a satisfiable
28//! formula unsatisfiable.
29
30use crate::cdcl::Lit;
31use crate::proof::{ProofStep, Witness};
32use crate::rup;
33
34/// Verify a refutation made of [`ProofStep`]s over `original`. Each step's added clause must
35/// check (RUP or PR) against the database built so far; then the empty clause must be RUP.
36/// Returns `false` if any step fails to check — a bogus or unsound proof is rejected.
37pub fn check_pr_refutation(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> bool {
38    let mut db: Vec<Vec<Lit>> = original.to_vec();
39    for step in steps {
40        match step {
41            ProofStep::Rup(c) => {
42                if !rup::is_rup(num_vars, &db, c) {
43                    return false;
44                }
45                db.push(c.clone());
46            }
47            ProofStep::Pr { clause, witness } => {
48                if !is_pr(num_vars, &db, clause, witness) {
49                    return false;
50                }
51                db.push(clause.clone());
52            }
53            ProofStep::Delete(clause) => {
54                // A deletion is unchecked (always sound); remove the first matching clause.
55                let key = canon_clause(clause);
56                if let Some(pos) = db.iter().position(|d| canon_clause(d) == key) {
57                    db.swap_remove(pos);
58                }
59            }
60        }
61    }
62    rup::is_rup(num_vars, &db, &[])
63}
64
65/// A clause's canonical form (sorted, deduped literal codes) for set-equality comparison.
66fn canon_clause(c: &[Lit]) -> Vec<u32> {
67    let mut k: Vec<u32> = c
68        .iter()
69        .map(|l| l.var() * 2 + u32::from(!l.is_positive()))
70        .collect();
71    k.sort_unstable();
72    k.dedup();
73    k
74}
75
76/// Is `clause` propagation-redundant w.r.t. `db` under `witness`?
77pub fn is_pr(num_vars: usize, db: &[Vec<Lit>], clause: &[Lit], witness: &Witness) -> bool {
78    match witness {
79        Witness::Assignment(omega) => assignment_pr(num_vars, db, clause, omega),
80        Witness::Substitution(sigma) => substitution_sr(num_vars, db, clause, sigma),
81    }
82}
83
84/// [`is_pr`] with the automorphism re-check served by a pre-built incremental
85/// `AutomorphismIndex` (which must hold exactly the clauses of `db`). Identical verdict to
86/// [`is_pr`] — the index is an acceleration structure — but the substitution path no longer
87/// rebuilds the membership set per call, the difference between an `O(n⁴)` and an `O(n³)`
88/// certified refutation.
89pub fn is_pr_indexed(
90    num_vars: usize,
91    db: &[Vec<Lit>],
92    index: &mut crate::symmetry_detect::AutomorphismIndex,
93    clause: &[Lit],
94    witness: &Witness,
95) -> bool {
96    match witness {
97        Witness::Assignment(omega) => assignment_pr(num_vars, db, clause, omega),
98        Witness::Substitution(sigma) => {
99            if !index.is_automorphism(sigma) {
100                return false;
101            }
102            let sigma_c = sigma.apply_clause(clause);
103            let mut assume: Vec<Lit> = clause.iter().map(|l| l.negated()).collect();
104            assume.extend(sigma_c.iter().map(|l| l.negated()));
105            // Occurrence-driven propagation over the index, not a full-database scan.
106            let _ = db;
107            index.propagate_to_conflict(num_vars, &assume)
108        }
109    }
110}
111
112/// [`check_pr_refutation`] accelerated by an incrementally-maintained `AutomorphismIndex` — the
113/// same verdict, but each substitution step's automorphism re-check costs `O(support)` rather than
114/// `O(|db|)`. Falls back to the stateless checker if the proof deletes clauses (the index is
115/// append-only, and certified symmetry refutations do not delete).
116pub fn check_pr_refutation_fast(
117    num_vars: usize,
118    original: &[Vec<Lit>],
119    steps: &[ProofStep],
120) -> bool {
121    if steps.iter().any(|s| matches!(s, ProofStep::Delete(_))) {
122        return check_pr_refutation(num_vars, original, steps);
123    }
124    let mut db: Vec<Vec<Lit>> = original.to_vec();
125    let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(num_vars, original);
126    for step in steps {
127        match step {
128            ProofStep::Rup(c) => {
129                if !rup::is_rup(num_vars, &db, c) {
130                    return false;
131                }
132                db.push(c.clone());
133                index.insert(c.clone());
134            }
135            ProofStep::Pr { clause, witness } => {
136                if !is_pr_indexed(num_vars, &db, &mut index, clause, witness) {
137                    return false;
138                }
139                db.push(clause.clone());
140                index.insert(clause.clone());
141            }
142            ProofStep::Delete(_) => unreachable!("deletes handled by the fallback above"),
143        }
144    }
145    rup::is_rup(num_vars, &db, &[])
146}
147
148/// The substitution-redundancy criterion for a symmetry witness `σ`: `σ` must be an
149/// automorphism of `db` (so the σ-repair of any model stays a model), and `db ∧ ¬C ⊢₁ σ(C)`
150/// (so that repair also satisfies `C`). Both conditions are independently checkable, and the
151/// automorphism is re-verified against the *current* `db` — fail-closed against generator
152/// ordering.
153fn substitution_sr(
154    num_vars: usize,
155    db: &[Vec<Lit>],
156    clause: &[Lit],
157    sigma: &crate::proof::Perm,
158) -> bool {
159    if !crate::symmetry_detect::perm_is_automorphism(db, sigma) {
160        return false;
161    }
162    // `db ∧ ¬C ⊢₁ σ(C)`: assume α = ¬C together with ¬σ(C) and propagate db; require a conflict.
163    let sigma_c = sigma.apply_clause(clause);
164    let mut assume: Vec<Lit> = clause.iter().map(|l| l.negated()).collect();
165    assume.extend(sigma_c.iter().map(|l| l.negated()));
166    up_conflict(num_vars, db, &assume)
167}
168
169/// The propagation-redundancy criterion with an explicit assignment witness `ω` (given as the
170/// set of literals it sets true): `ω` satisfies `C`, and for every clause `D ∈ db`,
171/// `F|α ⊢₁ D|ω` where `α = ¬C`.
172fn assignment_pr(num_vars: usize, db: &[Vec<Lit>], clause: &[Lit], omega_true: &[Lit]) -> bool {
173    // Materialise ω as an assignment; a self-contradictory witness is no witness.
174    let mut omega: Vec<Option<bool>> = vec![None; num_vars];
175    for &l in omega_true {
176        if !rup::set_true(&mut omega, l) {
177            return false;
178        }
179    }
180    // Precondition: ω must satisfy the clause being added.
181    if !clause
182        .iter()
183        .any(|&l| rup::lit_val(&omega, l) == Some(true))
184    {
185        return false;
186    }
187    // α = ¬C, as the set of literals it sets true.
188    let alpha: Vec<Lit> = clause.iter().map(|&l| l.negated()).collect();
189
190    for d in db {
191        // Clauses ω satisfies carry no obligation (D|ω is a tautology).
192        if d.iter().any(|&m| rup::lit_val(&omega, m) == Some(true)) {
193            continue;
194        }
195        // D|ω keeps the ω-unassigned literals (ω-false ones are dropped). The obligation
196        // `F|α ⊢₁ D|ω` is checked by assuming α together with ¬(D|ω) and propagating db: it
197        // must conflict.
198        let mut assume = alpha.clone();
199        for &m in d {
200            if rup::lit_val(&omega, m).is_none() {
201                assume.push(m.negated());
202            }
203        }
204        if !up_conflict(num_vars, db, &assume) {
205            return false;
206        }
207    }
208    true
209}
210
211/// Assume every literal of `assume` and unit-propagate `db`; `true` iff a conflict results
212/// (an immediate clash among the assumptions counts).
213fn up_conflict(num_vars: usize, db: &[Vec<Lit>], assume: &[Lit]) -> bool {
214    let mut assign: Vec<Option<bool>> = vec![None; num_vars];
215    for &l in assume {
216        if !rup::set_true(&mut assign, l) {
217            return true;
218        }
219    }
220    rup::propagate(db, &mut assign)
221}
222
223#[cfg(test)]
224#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/pr/tests.rs"]
225mod tests;