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}