sat_solver/sat/
conflict_analysis.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2//! Defines functions and structures related to conflict analysis in a SAT solver.
3//!
4//! Conflict analysis is a cornerstone of modern CDCL (Conflict-Driven Clause Learning)
5//! SAT solvers. When a conflict (a clause where all literals are false under the
6//! current assignment) is detected, the solver analyses the chain of implications
7//! (the "implication graph") that led to the conflict. This analysis aims to:
8//! 1. Identify a "learnt clause" that explains the conflict and can prevent similar
9//!    conflicts in the future. This clause is typically asserting, meaning it will
10//!    become unit after backtracking.
11//! 2. Determine a backtrack level: the decision level to which the solver should
12//!    backtrack to resolve the conflict and continue the search.
13//!
14//! This module provides:
15//! - The `Conflict` enum to represent different outcomes of conflict analysis.
16//! - The `Analyser` struct, which encapsulates the state and logic for performing
17//!   conflict analysis (using the 1UIP - First Unique Implication Point scheme).
18
19use crate::sat::assignment::Assignment;
20use crate::sat::clause::Clause;
21use crate::sat::clause_storage::LiteralStorage;
22use crate::sat::cnf::Cnf;
23use crate::sat::literal::{Literal, Variable};
24use crate::sat::trail::{Reason, Trail};
25use bit_vec::BitVec;
26use smallvec::SmallVec;
27
28/// Represents the outcome of a conflict analysis.
29///
30/// This enum categorises the result of analysing a conflict, which guides the solver's
31/// next actions (e.g. learning a clause, backtracking, restarting).
32///
33/// # Type Parameters
34///
35/// * `T`: The type of `Literal` used in clauses.
36/// * `S`: The `LiteralStorage` type used within clauses.
37#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
38pub enum Conflict<T: Literal, S: LiteralStorage<T>> {
39    /// A conflict at decision level 0 (ground level) was found.
40    /// This means the original formula is unsatisfiable. No clause is learnt.
41    #[default]
42    Ground,
43    /// A unit clause was learnt from the conflict.
44    /// This clause will propagate a literal immediately after backtracking.
45    Unit(Clause<T, S>),
46    /// A non-unit clause was learnt.
47    /// The `usize` is the index of the asserting literal within the learnt clause.
48    /// The `Learned` variant indicates the clause is asserting at some backtrack level.
49    Learned(usize, Clause<T, S>),
50    /// The conflict analysis suggests a restart is beneficial.
51    Restart(Clause<T, S>),
52}
53
54/// Encapsulates the state and methods for performing conflict analysis.
55///
56/// Using a struct allows for reusing allocations (like `seen` and `to_bump`)
57/// across multiple conflict analyses, which can improve performance.
58///
59/// # Type Parameters
60///
61/// * `T`: The type of `Literal`.
62/// * `S`: The `LiteralStorage` type for clauses.
63/// * `N`: A compile-time constant for the inline capacity of `to_bump` `SmallVec`.
64///   Defaults to 12, a common small size for literals involved in resolution steps.
65#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
66pub struct Analyser<T: Literal, S: LiteralStorage<T>, const N: usize = 12> {
67    /// A bit vector to mark variables that have been "seen" (processed)
68    /// during the current conflict analysis. Size is `num_vars`.
69    seen: BitVec,
70    /// Counts the number of literals in the current resolvent (conflict clause being built)
71    /// that belong to the current decision level. This helps find the 1UIP.
72    path_c: usize,
73    /// A small vector to store literals from clauses involved in the conflict.
74    /// These literals might be used to bump variable activities (VSIDS heuristic).
75    to_bump: SmallVec<[T; N]>,
76    /// Phantom data to associate the generic types `T` and `S` with the struct.
77    data: std::marker::PhantomData<*const (T, S)>,
78
79    /// A counter for the number of conflicts analysed for statistics.
80    pub count: usize,
81}
82
83impl<T: Literal, S: LiteralStorage<T>, const N: usize> Analyser<T, S, N> {
84    /// Initialises a new `Analyser`.
85    ///
86    /// # Arguments
87    ///
88    /// * `num_vars`: The total number of variables in the formula, used to size the `seen` vector.
89    pub(crate) fn new(num_vars: usize) -> Self {
90        Self {
91            seen: BitVec::from_elem(num_vars, false),
92            path_c: 0,
93            to_bump: SmallVec::with_capacity(N),
94            data: std::marker::PhantomData,
95            count: 0,
96        }
97    }
98
99    /// Checks if a variable (identified by its `Variable` ID) has been marked as seen.
100    ///
101    /// # Arguments
102    ///
103    /// * `var_id`: The `Variable` ID to check.
104    ///
105    /// # Safety
106    ///
107    /// Uses `get_unchecked` assuming `var_id` (cast to `usize`) is a valid index into `seen`.
108    /// This is generally safe if `num_vars` was correctly provided at construction and
109    /// `var_id` is always less than `num_vars`.
110    #[inline]
111    fn is_seen(&self, var_id: Variable) -> bool {
112        unsafe { self.seen.get_unchecked(var_id as usize) }
113    }
114
115    /// Marks a variable as seen.
116    ///
117    /// # Arguments
118    ///
119    /// * `var_id`: The `Variable` ID to mark.
120    ///
121    /// # Safety
122    ///
123    /// Uses `get_unchecked_mut` with similar safety assumptions as `is_seen`.
124    #[inline]
125    fn set_seen(&mut self, var_id: Variable) {
126        unsafe {
127            *self.seen.get_unchecked_mut(var_id as usize) = true;
128        }
129    }
130
131    /// Marks a variable as not seen (resets its seen status).
132    ///
133    /// # Arguments
134    ///
135    /// * `var_id`: The `Variable` ID to unmark.
136    ///
137    /// # Safety
138    ///
139    /// Uses `get_unchecked_mut` with similar safety assumptions as `is_seen`.
140    #[inline]
141    fn unset_seen(&mut self, var_id: Variable) {
142        unsafe {
143            *self.seen.get_unchecked_mut(var_id as usize) = false;
144        }
145    }
146
147    /// Performs a resolution step in the conflict analysis.
148    ///
149    /// Given a current conflict clause `c` (resolvent) and another clause `o` (the antecedent
150    /// of a literal `pivot_lit` in `c`), this function resolves `c` with `o` on the variable `pivot_var`.
151    /// The literal corresponding to `pivot_var` in `c` is removed, and literals from `o`
152    /// (excluding `pivot_lit.negated()`) are added to `c` if they are not already effectively present
153    /// (i.e. their variable is not seen, and they are false under current assignment at a lower level).
154    ///
155    /// # Arguments
156    ///
157    /// * `c`: Mutable reference to the current resolvent clause.
158    /// * `o`: Reference to the antecedent clause.
159    /// * `assignment`: The current variable assignment, to check literal values.
160    /// * `pivot_var`: The variable of the literal being resolved out of `c`.
161    /// * `c_idx_of_pivot_lit`: The index of the literal (related to `pivot_var`) in `c` to be removed.
162    /// * `trail`: The assignment trail, to check decision levels.
163    fn resolve(
164        &mut self,
165        c: &mut Clause<T, S>,
166        o: &Clause<T, S>,
167        assignment: &impl Assignment,
168        pivot_var: Variable,
169        c_idx_of_pivot_lit: usize,
170        trail: &Trail<T, S>,
171    ) {
172        c.remove_literal(c_idx_of_pivot_lit);
173        self.path_c = self.path_c.saturating_sub(1);
174        self.unset_seen(pivot_var);
175
176        for &lit_from_o in o.iter() {
177            let var_from_o = lit_from_o.variable();
178            if !self.is_seen(var_from_o) && assignment.literal_value(lit_from_o) == Some(false) {
179                self.set_seen(var_from_o);
180                self.to_bump.push(lit_from_o);
181                c.push(lit_from_o);
182
183                if trail.level(var_from_o) >= trail.decision_level() {
184                    self.path_c = self.path_c.wrapping_add(1);
185                }
186            }
187        }
188    }
189
190    /// Chooses the next literal from the trail to resolve with.
191    ///
192    /// Iterates backwards on the trail from current position `i`.
193    /// Finds the most recently assigned literal on the trail whose variable is currently "seen"
194    /// (i.e. present in the resolvent `c`). This literal will be the next pivot for resolution.
195    ///
196    /// # Arguments
197    ///
198    /// * `c`: The current resolvent clause.
199    /// * `trail`: The assignment trail.
200    /// * `i`: Mutable reference to the current index on the trail (updated by this function).
201    ///
202    /// # Returns
203    ///
204    /// `Some(k)` where `k` is the index of the chosen pivot literal in `c`.
205    /// `None` if no suitable literal is found (e.g. trail exhausted before 1UIP condition met).
206    fn choose_literal(
207        &self,
208        c: &Clause<T, S>,
209        trail: &Trail<T, S>,
210        i: &mut usize,
211    ) -> Option<usize> {
212        while *i > 0 {
213            *i -= 1;
214            let current_trail_entry = &trail[*i];
215            let var_on_trail = current_trail_entry.lit.variable();
216
217            if self.is_seen(var_on_trail) {
218                for k in 0..c.len() {
219                    if var_on_trail == c[k].variable() {
220                        return Some(k);
221                    }
222                }
223            }
224        }
225        None
226    }
227
228    /// Performs conflict analysis starting from a conflicting clause.
229    ///
230    /// This implements a 1UIP (First Unique Implication Point) learning scheme.
231    /// It iteratively resolves the conflicting clause with antecedents of literals
232    /// on the trail until a learnt clause is derived.
233    ///
234    /// # Arguments
235    ///
236    /// * `cnf`: The CNF formula, to get antecedent clauses.
237    /// * `trail`: The assignment trail.
238    /// * `assignment`: The current variable assignments.
239    /// * `conflicting_clause_ref`: Index of the initially conflicting clause in `cnf`.
240    ///
241    /// # Returns
242    ///
243    /// A tuple `(Conflict<T, S>, SmallVec<[T; N]>)`:
244    /// - The `Conflict` outcome (e.g. learnt clause, ground, restart).
245    /// - A `SmallVec` of literals involved in the conflict, for activity bumping.
246    pub(crate) fn analyse_conflict(
247        &mut self,
248        cnf: &Cnf<T, S>,
249        trail: &Trail<T, S>,
250        assignment: &impl Assignment,
251        conflicting_clause_ref: usize,
252    ) -> (Conflict<T, S>, SmallVec<[T; N]>) {
253        self.count = self.count.wrapping_add(1);
254        self.reset_for_analysis(cnf.num_vars);
255
256        let current_decision_level = trail.decision_level();
257
258        let mut resolvent_clause = cnf[conflicting_clause_ref].clone();
259        self.path_c = 0;
260
261        for &lit in resolvent_clause.iter() {
262            let var = lit.variable();
263            self.set_seen(var);
264            self.to_bump.push(lit);
265            if trail.level(var) >= current_decision_level {
266                self.path_c = self.path_c.wrapping_add(1);
267            }
268        }
269
270        let mut trail_idx = trail.len();
271
272        while self.path_c > usize::from(current_decision_level != 0) {
273            let Some(idx_in_resolvent_of_pivot) =
274                self.choose_literal(&resolvent_clause, trail, &mut trail_idx)
275            else {
276                break;
277            };
278
279            let antecedent_reason = trail[trail_idx].reason;
280            let antecedent_clause = match antecedent_reason {
281                Reason::Clause(ante_idx) | Reason::Unit(ante_idx) => cnf[ante_idx].clone(),
282                Reason::Decision => break,
283            };
284
285            let pivot_var = trail[trail_idx].lit.variable();
286
287            self.resolve(
288                &mut resolvent_clause,
289                &antecedent_clause,
290                assignment,
291                pivot_var,
292                idx_in_resolvent_of_pivot,
293                trail,
294            );
295        }
296
297        // was having problems with teh constructed clause being not all false
298        debug_assert!(
299            resolvent_clause
300                .iter()
301                .all(|&lit| assignment.literal_value(lit) == Some(false)),
302            "Learnt clause not entirely false under current assignment!"
303        );
304
305        let literals_to_bump = self.to_bump.clone();
306
307        if resolvent_clause.is_empty() {
308            (Conflict::Ground, literals_to_bump)
309        } else if resolvent_clause.is_unit() {
310            (Conflict::Unit(resolvent_clause), literals_to_bump)
311        } else {
312            if current_decision_level > 0 && self.path_c != 1 {
313                return (Conflict::Restart(resolvent_clause), literals_to_bump);
314            }
315
316            let mut asserting_lit_idx_in_clause: usize = 0;
317            let mut max_trail_pos_at_dl = 0;
318
319            for k in 0..resolvent_clause.len() {
320                let var = resolvent_clause[k].variable();
321                if trail.level(var) == current_decision_level {
322                    let pos_on_trail = trail.lit_to_pos[var as usize];
323                    if pos_on_trail > max_trail_pos_at_dl {
324                        max_trail_pos_at_dl = pos_on_trail;
325                        asserting_lit_idx_in_clause = k;
326                    }
327                }
328            }
329            (
330                Conflict::Learned(asserting_lit_idx_in_clause, resolvent_clause),
331                literals_to_bump,
332            )
333        }
334    }
335
336    /// Resets the analyser's state for a new conflict analysis.
337    /// Clears `seen` bits, `path_c`, and `to_bump` list.
338    /// This should be called before `analyse_conflict`.
339    fn reset_for_analysis(&mut self, num_vars: usize) {
340        if self.seen.len() == num_vars {
341            self.seen.clear();
342        } else {
343            self.seen = BitVec::from_elem(num_vars, false);
344        }
345        self.path_c = 0;
346        self.to_bump.clear();
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    // use super::*;
353    // use crate::sat::assignment::{Assignment, VecAssignment};
354    // use crate::sat::clause::Clause;
355    // use crate::sat::cnf::Cnf;
356    // use crate::sat::literal::PackedLiteral;
357    // use crate::sat::trail::{Reason, Trail};
358    // use smallvec::SmallVec;
359    //
360    // type TestLiteral = PackedLiteral;
361    // type TestClauseStorage = SmallVec<[TestLiteral; 8]>;
362    // type TestAnalyser = Analyser<TestLiteral, TestClauseStorage, 12>;
363    //
364    // #[test]
365    // fn test_analyse_ground_conflict() {
366    //     let mut cnf = Cnf::<TestLiteral, TestClauseStorage>::default();
367    //     cnf.clauses.push(Clause::from(vec![1]));
368    //     cnf.clauses.push(Clause::from(vec![-1]));
369    //     cnf.num_vars = 2;
370    //     cnf.non_learnt_idx = 2;
371    //
372    //     let mut assignment = VecAssignment::default();
373    //     assignment.set(1, true);
374    //
375    //     let mut trail = Trail::new(cnf.clauses.as_ref(), cnf.num_vars);
376    //     trail.push(TestLiteral::new(1, true), 0, Reason::Decision);
377    //     trail.push(TestLiteral::new(1, false), 0, Reason::Clause(0));
378    //
379    //     let mut analyser = TestAnalyser::new(cnf.num_vars);
380    //     let (conflict_type, _to_bump) = analyser.analyse_conflict(&cnf, &trail, &assignment, 1);
381    //
382    //     match conflict_type {
383    //         Conflict::Unit(learnt_clause) => {
384    //             assert_eq!(learnt_clause.len(), 1);
385    //             assert_eq!(learnt_clause[0], TestLiteral::from_i32(-1));
386    //         }
387    //         Conflict::Ground => {}
388    //         other => panic!("Expected Ground or Unit conflict, got {other:?}"),
389    //     }
390    //     assert_eq!(analyser.count, 1);
391    // }
392    //
393    // #[test]
394    // fn test_analyse_simple_1uip_conflict() {
395    //     let mut cnf = Cnf::<TestLiteral, TestClauseStorage>::default();
396    //     cnf.clauses.push([-1, 2].into_iter().collect::<Clause<_>>());
397    //     cnf.clauses.push([-1, 3].into_iter().collect::<Clause<_>>());
398    //     cnf.clauses
399    //         .push([-2, -3].into_iter().collect::<Clause<_>>());
400    //     cnf.num_vars = 4;
401    //     cnf.non_learnt_idx = 3;
402    //
403    //     let mut assignment = VecAssignment::default();
404    //     assignment.set(1, true);
405    //     assignment.set(2, true);
406    //     assignment.set(3, true);
407    //
408    //     let mut trail = Trail::new(cnf.clauses.as_ref(), cnf.num_vars);
409    //     trail.push(TestLiteral::new(1, true), 0, Reason::Decision);
410    //     trail.push(TestLiteral::new(2, true), 0, Reason::Decision);
411    //     trail.push(TestLiteral::new(3, true), 0, Reason::Clause(1));
412    //
413    //     let mut analyser = TestAnalyser::new(cnf.num_vars);
414    //     let (conflict_type, to_bump) = analyser.analyse_conflict(&cnf, &trail, &assignment, 2);
415    //
416    //     match conflict_type {
417    //         Conflict::Unit(learnt_clause) => {
418    //             assert_eq!(learnt_clause.len(), 1);
419    //             assert_eq!(learnt_clause[0], TestLiteral::from_i32(-1));
420    //         }
421    //         other => panic!("Expected Unit conflict, got {other:?}"),
422    //     }
423    //
424    //     assert!(to_bump.contains(&TestLiteral::from_i32(-1)));
425    //     assert!(to_bump.contains(&TestLiteral::from_i32(-2)));
426    //     assert!(to_bump.contains(&TestLiteral::from_i32(-3)));
427    //     assert_eq!(to_bump.len(), 3);
428    //     assert_eq!(analyser.count, 1);
429    // }
430}