sat_solver/sat/cdcl.rs
1//! Implements the core Conflict-Driven Clause Learning (CDCL) SAT solver.
2//!
3//! The `Cdcl` struct orchestrates various components like variable selection,
4//! propagation, conflict analysis, and clause learning to determine the
5//! satisfiability of a given Conjunctive Normal Form (CNF) formula.
6//! It follows the typical CDCL algorithm structure, including decision-making,
7//! boolean constraint propagation (BCP), conflict analysis, clause learning,
8//! non-chronological backtracking, and restarts.
9
10use crate::sat::assignment::Assignment;
11use crate::sat::clause::Clause;
12use crate::sat::clause_management::ClauseManagement;
13use crate::sat::clause_storage::LiteralStorage;
14use crate::sat::cnf::Cnf;
15use crate::sat::conflict_analysis::{Analyser, Conflict};
16use crate::sat::literal::Literal;
17use crate::sat::propagation::Propagator;
18use crate::sat::restarter::Restarter;
19use crate::sat::solver::{DefaultConfig, SolutionStats, Solutions, Solver, SolverConfig};
20use crate::sat::trail::Reason;
21use crate::sat::trail::Trail;
22use crate::sat::variable_selection::VariableSelection;
23use crate::sat::{cnf, variable_selection};
24use std::fmt::Debug;
25
26/// Represents a CDCL SAT solver instance.
27///
28/// The solver iteratively makes decisions (assigns a truth value to a variable),
29/// propagates their logical consequences, and if a conflict (a clause that becomes
30/// false under the current assignment) arises, it analyses the conflict to learn
31/// a new clause. This learned clause helps prune the search space and guide the
32/// solver towards a solution or a proof of unsatisfiability.
33///
34/// The solver's behavior can be customised via the `Config` generic parameter,
35/// which defines the specific implementations for various components like
36/// literal representation, assignment tracking, propagation engine, variable
37/// selection heuristic, restart strategies, and clause database management.
38#[derive(Debug, Clone)]
39pub struct Cdcl<Config: SolverConfig = DefaultConfig> {
40 /// Tracks the current assignment of truth values (True, False, Undefined) to variables.
41 assignment: Config::Assignment,
42
43 /// Handles unit propagation, which is the process of
44 /// deducing further variable assignments implied by the current partial assignment
45 /// and unit clauses.
46 propagator: Config::Propagator,
47
48 /// The Conjunctive Normal Form (CNF) formula being solved.
49 /// This stores both the original clauses provided by the user and clauses
50 /// learned by the solver during conflict analysis.
51 pub cnf: Cnf<Config::Literal, Config::LiteralStorage>,
52
53 /// Implements the variable selection heuristic (e.g. VSIDS) responsible for
54 /// choosing the next unassigned variable to make a decision on.
55 selector: Config::VariableSelector,
56
57 /// The current decision level in the search tree.
58 /// Level 0 is for initial propagations from unit clauses in the input.
59 /// Each new decision increments this level.
60 decision_level: cnf::DecisionLevel,
61
62 /// The trail records the sequence of assigned literals, their decision levels,
63 /// and the reasons for their assignment (either a decision or an implication
64 /// from a clause). It is crucial for backtracking and conflict analysis.
65 trail: Trail<Config::Literal, Config::LiteralStorage>,
66
67 /// Manages the restart strategy. Restarts involve clearing the current
68 /// assignment (except for level 0 assignments) and restarting the search,
69 /// potentially using different heuristics or learned information.
70 restarter: Config::Restarter,
71
72 /// Analyses conflicts encountered during propagation to produce learned clauses
73 /// (lemmas) and determine the appropriate decision level to backtrack to.
74 conflict_analysis: Analyser<Config::Literal, Config::LiteralStorage>,
75
76 /// Manages the database of learned clauses, including strategies for periodically
77 /// removing less useful learned clauses to save memory and keep propagation efficient.
78 manager: Config::ClauseManager,
79}
80
81impl<Config: SolverConfig> Cdcl<Config> {
82 /// Checks if all variables in the CNF have been assigned a truth value.
83 ///
84 /// The number of variables is determined by `self.cnf.num_vars`, which typically
85 /// corresponds to the highest variable index used in the problem + 1.
86 /// The trail length (`self.trail.len()`) reflects the number of currently
87 /// assigned literals.
88 ///
89 /// # Returns
90 ///
91 /// `true` if the number of assigned literals in the trail equals the total
92 /// number of variables considered by the solver, `false` otherwise.
93 pub const fn all_assigned(&self) -> bool {
94 self.trail.len() == self.cnf.num_vars
95 }
96
97 /// Determines if the solver should perform a restart.
98 ///
99 /// A restart is advised under two conditions:
100 /// 1. The `Restarter` component itself signals that a restart is due (e.g. based on a Luby sequence or fixed conflict count).
101 /// 2. The "conflict rate" (number of restarts divided by trail length) exceeds a threshold (0.1).
102 /// The trail length serves as a rough proxy for the amount of search effort. A high rate
103 /// might indicate thrashing or a poor search path.
104 ///
105 /// Division by zero (if trail length is 0) results in `f64::INFINITY` for the rate if
106 /// `num_restarts > 0`, correctly triggering a restart. If `num_restarts` is also 0,
107 /// the rate is `f64::NAN`, which does not trigger the rate-based restart.
108 ///
109 /// # Returns
110 ///
111 /// `true` if a restart is advised, `false` otherwise.
112 fn should_restart(&mut self) -> bool {
113 #[allow(clippy::cast_precision_loss)]
114 let conflict_rate = if !self.trail.is_empty() {
115 self.restarter.num_restarts() as f64 / self.trail.len() as f64
116 } else if self.restarter.num_restarts() > 0 {
117 f64::INFINITY
118 } else {
119 0.0
120 };
121 self.restarter.should_restart() || conflict_rate > 0.1
122 }
123
124 /// Adds a propagated literal to the trail along with its reason and decision level.
125 ///
126 /// This method is called when a literal is implied by a clause (unit propagation).
127 /// The literal, current decision level, and the index of the implying clause (reason)
128 /// are recorded on the trail. This information is vital for conflict analysis and backtracking.
129 ///
130 /// # Arguments
131 ///
132 /// * `lit`: The literal that was propagated (e.g. variable `x` is True).
133 /// * `c_ref`: The index (reference) of the clause that became unit and caused this propagation.
134 fn add_propagation(&mut self, lit: Config::Literal, c_ref: usize) {
135 self.trail
136 .push(lit, self.decision_level, Reason::Clause(c_ref));
137 }
138
139 /// Adds a new clause to the solver's CNF database.
140 ///
141 /// Clauses that are empty (and thus trivially unsatisfiable if encountered this way)
142 /// or tautological (e.g. `(x OR -x OR y)`) are typically ignored as they provide
143 /// no useful constraints or can be simplified.
144 /// The new clause is added to the `Cnf` store and then registered with the `Propagator`
145 /// (e.g. for setting up watched literals).
146 ///
147 /// # Arguments
148 ///
149 /// * `clause`: The clause to add.
150 pub fn add_clause(&mut self, clause: Clause<Config::Literal, Config::LiteralStorage>) {
151 if clause.is_empty() || clause.is_tautology() {
152 return;
153 }
154
155 let c_ref = self.cnf.len();
156
157 self.cnf.add_clause(clause);
158 self.propagator.add_clause(&self.cnf[c_ref], c_ref);
159 }
160}
161
162impl<Config: SolverConfig> Solver<Config> for Cdcl<Config> {
163 /// Creates a new `Cdcl` solver instance for the given CNF formula.
164 ///
165 /// Initialises all internal components of the solver:
166 /// - `Assignment`: To store variable assignments (initialised to Undefined).
167 /// - `Trail`: To track decisions and propagations.
168 /// - `Propagator`: Set up with the initial clauses for watched literal schemes.
169 /// - `Cnf`: Stores the clauses of the problem.
170 /// - `VariableSelector`: Initialised with variable information (e.g. for VSIDS).
171 /// - `Restarter`: Initialised with default restart strategy parameters.
172 /// - `Analyser`: For conflict analysis.
173 /// - `ClauseManager`: For managing learned clauses.
174 /// The decision level is initialised to 0.
175 ///
176 /// # Arguments
177 ///
178 /// * `cnf`: The Conjunctive Normal Form (CNF) formula to be solved.
179 fn new<L: Literal, S: LiteralStorage<L>>(cnf: Cnf<L, S>) -> Self {
180 let cnf = cnf.convert();
181
182 let propagator = Propagator::new(&cnf);
183 let vars = &cnf.lits;
184 let selector = Config::VariableSelector::new(cnf.num_vars, vars, &cnf.clauses);
185 let restarter = Restarter::new();
186 let conflict_analysis = Analyser::new(cnf.num_vars);
187 let manager = Config::ClauseManager::new(&cnf.clauses);
188
189 Self {
190 assignment: Assignment::new(cnf.num_vars),
191 trail: Trail::new(&cnf.clauses, cnf.num_vars),
192 propagator,
193 cnf,
194 selector,
195 restarter,
196 decision_level: 0,
197 conflict_analysis,
198 manager,
199 }
200 }
201
202 fn from_parts<L: Literal, S: LiteralStorage<L>>(
203 cnf: Cnf<L, S>,
204 assignment: Config::Assignment,
205 manager: Config::ClauseManager,
206 propagator: Config::Propagator,
207 restarter: Config::Restarter,
208 selector: Config::VariableSelector,
209 ) -> Self {
210 let cnf = cnf.convert();
211 let conflict_analysis = Analyser::new(cnf.num_vars);
212
213 Self {
214 trail: Trail::new(&cnf.clauses, cnf.num_vars),
215 assignment,
216 propagator,
217 cnf,
218 selector,
219 conflict_analysis,
220 restarter,
221 manager,
222 decision_level: 0,
223 }
224 }
225
226 /// Attempts to solve the SAT problem for the loaded CNF formula using the CDCL algorithm.
227 ///
228 /// The main loop proceeds as follows:
229 /// 1. **Initial Check & Propagation**:
230 /// - If the CNF is empty, it's trivially SAT.
231 /// - Perform initial unit propagation (BCP) at decision level 0. If a conflict occurs, the formula is UNSAT.
232 /// - If any clause becomes empty (after initial setup or propagation), formula is UNSAT.
233 /// 2. **Main Loop**:
234 /// a. **Propagation**: Propagate assignments based on the current partial assignment.
235 /// - If propagation leads to a conflict (a clause becomes falsified):
236 /// i. Notify `ClauseManager` about the conflict (e.g. to adjust clause activities).
237 /// ii. Perform `ConflictAnalysis`:
238 /// - Generate a learned clause (lemma).
239 /// - Determine variables involved for activity bumping (e.g. VSIDS).
240 ///
241 /// iii. Handle the conflict result:
242 /// - `Conflict::Ground`: Conflict at level 0 implies the formula is UNSAT.
243 /// - `Conflict::Restart` or `Conflict::Unit`: These might also indicate UNSAT or special handling.
244 /// - `Conflict::Learned(assert_idx, learned_clause)`:
245 /// - The learned clause is processed (e.g. LBD calculation, activity bump).
246 /// - Determine backtrack level (`bt_level`) from the learned clause.
247 /// - Bump activities of clauses involved in the conflict.
248 /// - Backtrack: Undo assignments on the trail up to `bt_level`. Update `decision_level`.
249 /// - Add the learned clause to the database.
250 /// - Enqueue the asserting literal from the learned clause for propagation.
251 /// - Optionally, clean the clause database (`manager.clean_clause_db`).
252 ///
253 /// iv. Update variable selection heuristics (`selector.bumps`, `selector.decay`).
254 /// v. Check for restart (`should_restart`). If so, reset trail, assignments, and decision level.
255 ///
256 /// b. **Decision (No Conflict)**:
257 /// i. Increment `decision_level`.
258 /// ii. Check if all variables are assigned (`all_assigned`). If yes, a model is found; return SAT.
259 /// iii. Select an unassigned variable using `selector.pick()`.
260 /// - If a literal is picked, push it onto the trail as a decision.
261 /// - If no literal can be picked (e.g. all assigned but `all_assigned` was false, or selector gives up),
262 /// it might imply a solution is found (though `all_assigned` should be the primary check).
263 /// Return current solutions.
264 ///
265 /// The loop continues until a satisfying assignment is found (returns `Some(Solutions)`)
266 /// or the formula is proven unsatisfiable (returns `None`).
267 ///
268 /// # Returns
269 ///
270 /// * `Some(Solutions)`: If a satisfying assignment is found. Contains the model.
271 /// * `None`: If the formula is unsatisfiable.
272 fn solve(&mut self) -> Option<Solutions> {
273 if self.cnf.is_empty() {
274 return Some(Solutions::default());
275 }
276
277 if self
278 .propagator
279 .propagate(&mut self.trail, &mut self.assignment, &mut self.cnf)
280 .is_some()
281 {
282 return None;
283 }
284
285 if self.cnf.iter().any(Clause::is_empty) {
286 return None;
287 }
288
289 loop {
290 if let Some(c_ref) = // c_ref is the index of the conflict clause
291 self.propagator.propagate(
292 &mut self.trail,
293 &mut self.assignment,
294 &mut self.cnf,
295 )
296 {
297 self.manager.on_conflict(&mut self.cnf);
298
299 let (conflict, to_bump) = self.conflict_analysis.analyse_conflict(
300 &self.cnf,
301 &self.trail,
302 &self.assignment,
303 c_ref,
304 );
305
306 match conflict {
307 Conflict::Unit(_) | Conflict::Restart(_) | Conflict::Ground => {
308 return None;
309 }
310 Conflict::Learned(assert_idx, mut clause) => {
311 clause.swap(0, assert_idx);
312
313 let asserting_lit = clause[0];
314
315 let bt_level = clause
316 .iter()
317 .skip(1)
318 .map(|lit| self.trail.level(lit.variable()))
319 .max()
320 .unwrap_or(0);
321
322 clause.calculate_lbd(&self.trail);
323 clause.is_learnt = true;
324
325 clause.bump_activity(1.0);
326
327 self.manager
328 .bump_involved_clause_activities(&mut self.cnf, c_ref);
329
330 self.trail.backstep_to(&mut self.assignment, bt_level);
331 self.decision_level = bt_level;
332
333 let new_clause_idx = self.cnf.len();
334 self.add_clause(clause);
335
336 self.add_propagation(asserting_lit, new_clause_idx);
337
338 if self.manager.should_clean_db() {
339 self.manager.clean_clause_db(
340 &mut self.cnf,
341 &mut self.trail,
342 &mut self.propagator,
343 &mut self.assignment,
344 );
345 }
346 }
347 }
348
349 self.selector.bumps(to_bump);
350 self.selector.decay(variable_selection::VSIDS_DECAY_FACTOR);
351
352 if self.should_restart() {
353 // would be better to not reset the initial unit clauses, but no way round now
354 self.trail.reset();
355 self.assignment.reset();
356 self.decision_level = 0;
357 }
358 } else {
359 self.decision_level = self.decision_level.wrapping_add(1);
360
361 if self.all_assigned() {
362 return Some(self.solutions());
363 }
364
365 let lit_option = self.selector.pick(&self.assignment);
366
367 if let Some(lit) = lit_option {
368 self.trail.push(lit, self.decision_level, Reason::Decision);
369 } else {
370 return Some(self.solutions());
371 }
372 }
373 }
374 }
375
376 /// Retrieves the current satisfying assignment if one has been found.
377 ///
378 /// This method should typically be called after `solve()` returns `Some(_)`.
379 /// If called before solving, or if the formula is unsatisfiable, the returned
380 /// assignment might be incomplete, empty, or reflect some intermediate state.
381 /// The `Solutions` object usually maps variable indices to their truth values.
382 ///
383 /// # Returns
384 ///
385 /// A `Solutions` object representing the variable assignments in a model.
386 fn solutions(&self) -> Solutions {
387 self.assignment.get_solutions()
388 }
389
390 /// Gathers statistics about the solving process.
391 ///
392 /// These statistics can be useful for analysing solver performance and heuristics.
393 ///
394 /// # Returns
395 ///
396 /// A `SolutionStats` struct containing:
397 /// - `conflicts`: Total number of conflicts encountered.
398 /// - `decisions`: Total number of decisions made.
399 /// - `propagations`: Total number of propagations performed.
400 /// - `restarts`: Total number of restarts performed.
401 /// - `learnt_clauses`: Number of clauses currently in the learnt clause database.
402 /// - `removed_clauses`: Number of learnt clauses removed during database cleaning.
403 fn stats(&self) -> SolutionStats {
404 SolutionStats {
405 conflicts: self.conflict_analysis.count,
406 decisions: self.selector.decisions(),
407 propagations: self.propagator.num_propagations(),
408 restarts: self.restarter.num_restarts(),
409 learnt_clauses: self.cnf.len() - self.cnf.non_learnt_idx,
410 removed_clauses: self.manager.num_removed(),
411 }
412 }
413
414 /// Placeholder for a debugging function.
415 ///
416 /// This function is intended for future use, possibly to print internal solver state
417 /// or enable more verbose logging for debugging purposes. Currently, it is unimplemented.
418 fn debug(&mut self) {
419 todo!()
420 }
421}