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}