sat_solver/sat/
clause_management.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2#![allow(clippy::similar_names)]
3//! This module defines strategies for managing the clause database in a SAT solver.
4//!
5//! Clause management is crucial for the performance of modern SAT solvers, especially
6//! Conflict-Driven Clause Learning (CDCL) solvers. As solvers learn new clauses
7//! during conflict analysis, the clause database can grow very large. Effective
8//! management strategies aim to:
9//! - Periodically remove less useful learnt clauses to keep the database size manageable.
10//! - Prioritise keeping "high-quality" clauses (e.g. those with low LBD or high activity).
11//! - Decay clause activities to reflect their recent involvement in conflicts.
12//!
13//! This module provides a `ClauseManagement` trait that abstracts these strategies,
14//! along with concrete implementations like `LbdClauseManagement` (which uses
15//! Literal Blocks Distance and activity scores) and `NoClauseManagement` (a no-op strategy).
16
17use crate::sat::assignment::Assignment;
18use crate::sat::clause::Clause;
19use crate::sat::clause_management::ClauseManagementImpls::LbdActivityClauseManagement;
20use crate::sat::clause_storage::LiteralStorage;
21use crate::sat::cnf::Cnf;
22use crate::sat::literal::Literal;
23use crate::sat::propagation::Propagator;
24use crate::sat::trail::Trail;
25use clap::ValueEnum;
26use rustc_hash::{FxHashMap, FxHashSet};
27use std::fmt::{Debug, Display};
28
29/// A constant factor used to decay clause activities.
30const DECAY_FACTOR: f64 = 0.95;
31
32/// Trait defining the interface for clause database management strategies.
33///
34/// Implementors of this trait control when and how the clause database (specifically
35/// learnt clauses) is pruned or maintained.
36///
37/// # Type Parameters
38///
39/// * `L`: The type of `Literal` used in clauses.
40/// * `S`: The `LiteralStorage` type used within clauses.
41pub trait ClauseManagement: Clone + Debug + Default {
42    /// Creates a new instance of the clause management strategy.
43    ///
44    /// # Arguments
45    ///
46    /// * `clauses`: An initial slice of clauses. This might be used to initialise
47    ///   internal structures
48    fn new<L: Literal, S: LiteralStorage<L>>(clauses: &[Clause<L, S>]) -> Self;
49
50    /// Called by the solver after a conflict occurs and a new clause might have been learnt.
51    ///
52    /// This method allows the strategy to update its internal state, such as
53    /// incrementing conflict counters or decaying clause activities.
54    ///
55    /// # Arguments
56    ///
57    /// * `cnf`: A mutable reference to the `Cnf` formula, allowing modification of clause activities.
58    fn on_conflict<L: Literal, S: LiteralStorage<L>>(&mut self, cnf: &mut Cnf<L, S>);
59
60    /// Determines whether the clause database should be cleaned.
61    ///
62    /// This is typically based on a conflict counter reaching a certain interval.
63    ///
64    /// # Returns
65    ///
66    /// `true` if the database cleaning process should be triggered, `false` otherwise.
67    fn should_clean_db(&self) -> bool;
68
69    /// Performs the clause database cleaning operation.
70    ///
71    /// This method identifies and removes learnt clauses deemed less useful.
72    /// It needs to coordinate with other solver components like the `Trail` (to handle
73    /// reasons for assignments) and the `Propagator` (to update watched literals).
74    ///
75    /// # Arguments
76    ///
77    /// * `cnf`: A mutable reference to the `Cnf` formula, from which clauses will be removed.
78    /// * `trail`: A mutable reference to the `Trail`, to update clause indices used as reasons.
79    /// * `propagator`: A mutable reference to the `Propagator`, to update its internal clause references.
80    /// * `assignment`: A mutable reference to the `Assignment` (currently unused by `LbdClauseManagement`).
81    fn clean_clause_db<L: Literal, S: LiteralStorage<L>, P: Propagator<L, S, A>, A: Assignment>(
82        &mut self,
83        cnf: &mut Cnf<L, S>,
84        trail: &mut Trail<L, S>,
85        propagator: &mut P,
86        assignment: &mut A,
87    );
88
89    /// Bumps the activity of clauses involved in deriving a new learnt clause.
90    ///
91    /// Typically, the learnt clause itself and potentially clauses used in its resolution
92    /// have their activity increased.
93    ///
94    /// # Arguments
95    ///
96    /// * `cnf`: A mutable reference to the `Cnf` formula, to access and modify the clause.
97    /// * `c_ref`: The index of the clause whose activity should be bumped (often the newly learnt clause).
98    fn bump_involved_clause_activities<L: Literal, S: LiteralStorage<L>>(
99        &mut self,
100        cnf: &mut Cnf<L, S>,
101        c_ref: usize,
102    );
103
104    /// Returns the total number of clauses removed by this management strategy so far.
105    fn num_removed(&self) -> usize;
106}
107
108/// A clause management strategy based on Literal Blocks Distance (LBD) and activity.
109///
110/// This strategy periodically cleans the learnt clause database by:
111/// 1. Identifying candidate learnt clauses for removal (those not locked by the trail).
112/// 2. Sorting candidates: clauses with LBD <= 2 are prioritised to be kept.
113///    Among others, clauses with lower LBD are preferred, and then lower activity (as less active clauses are removed).
114/// 3. Removing roughly half of the worst-ranking candidates.
115/// 4. Clause activities are decayed after each conflict.
116///
117/// # Type Parameters
118///
119/// * `L`: The type of `Literal`.
120/// * `S`: The `LiteralStorage` type.
121/// * `N`: A compile-time constant defining the conflict interval at which database cleaning is considered.
122#[derive(Debug, Clone, Default, PartialEq)]
123pub struct LbdClauseManagement<const N: usize> {
124    /// The interval (number of conflicts) between database cleaning attempts.
125    interval: usize,
126    /// Counter for conflicts since the last database cleanup.
127    conflicts_since_last_cleanup: usize,
128    /// Total number of clauses removed by this strategy.
129    num_removed: usize,
130
131    /// Buffer for storing candidates for removal: (`original_index`, lbd, activity).
132    candidates: Vec<(usize, u32, f64)>,
133    /// Set of original indices of clauses selected for removal.
134    indices_to_remove: FxHashSet<usize>,
135    /// Buffer for storing learnt clauses that are kept after cleaning.
136    new_learnt_clauses: Vec<Vec<i32>>,
137    /// Map from old clause indices to new clause indices after compaction.
138    old_to_new_idx_map: FxHashMap<usize, usize>,
139}
140
141impl<const N: usize> ClauseManagement for LbdClauseManagement<N> {
142    /// Creates a new `LbdClauseManagement` instance.
143    ///
144    /// Initialises internal buffers and sets the cleaning interval based on `N`.
145    /// The initial `clauses` slice is used to estimate initial capacities but not directly stored.
146    fn new<L: Literal, S: LiteralStorage<L>>(clauses: &[Clause<L, S>]) -> Self {
147        let initial_capacity = clauses.len();
148        let candidates = Vec::with_capacity(initial_capacity);
149
150        let mut indices_to_remove = FxHashSet::default();
151        indices_to_remove.reserve(initial_capacity);
152
153        let new_learnt_clauses = Vec::with_capacity(initial_capacity);
154
155        let mut old_to_new_idx_map = FxHashMap::default();
156        old_to_new_idx_map.reserve(initial_capacity);
157
158        Self {
159            interval: N,
160            conflicts_since_last_cleanup: 0,
161            num_removed: 0,
162            candidates,
163            indices_to_remove,
164            new_learnt_clauses,
165            old_to_new_idx_map,
166        }
167    }
168
169    /// Increments the conflict counter and decays activities of all learnt clauses.
170    fn on_conflict<L: Literal, S: LiteralStorage<L>>(&mut self, cnf: &mut Cnf<L, S>) {
171        self.conflicts_since_last_cleanup = self.conflicts_since_last_cleanup.wrapping_add(1);
172        Self::decay_clause_activities(cnf);
173    }
174
175    /// Returns `true` if `conflicts_since_last_cleanup` has reached the `interval`.
176    fn should_clean_db(&self) -> bool {
177        self.conflicts_since_last_cleanup >= self.interval
178    }
179
180    /// Cleans the learnt clause database.
181    ///
182    /// - Selects learnt clauses not locked by the trail as removal candidates.
183    /// - Sorts candidates:
184    ///   - Clauses with LBD <= 2 are strongly preferred (kept).
185    ///   - Otherwise, lower LBD is better (kept).
186    ///   - For equal LBD, lower activity is worse (removed).
187    /// - Removes approximately half of the "worst" candidates.
188    /// - Rebuilds the learnt part of the `Cnf`'s clause list.
189    /// - Updates `Trail` and `Propagator` with remapped clause indices.
190    /// - Resets the conflict counter.
191    ///
192    /// The `assignment` parameter is currently unused by this implementation.
193    fn clean_clause_db<L: Literal, S: LiteralStorage<L>, P: Propagator<L, S, A>, A: Assignment>(
194        &mut self,
195        cnf: &mut Cnf<L, S>,
196        trail: &mut Trail<L, S>,
197        propagator: &mut P,
198        _assignment: &mut A,
199    ) {
200        let learnt_start_idx = cnf.non_learnt_idx;
201        if cnf.len() <= learnt_start_idx {
202            return;
203        }
204
205        self.candidates.clear();
206        let locked_clauses = trail.get_locked_clauses();
207
208        for idx in learnt_start_idx..cnf.len() {
209            if !locked_clauses.contains(&idx) {
210                let clause = &cnf[idx];
211                self.candidates.push((idx, clause.lbd, clause.activity()));
212            }
213        }
214
215        if self.candidates.is_empty() {
216            return;
217        }
218
219        let num_candidates = self.candidates.len();
220        let num_to_remove = num_candidates / 2;
221
222        if num_to_remove == 0 {
223            return;
224        }
225
226        let comparison = |a: &(usize, u32, f64), b: &(usize, u32, f64)| {
227            let (_, lbd_a, act_a) = a;
228            let (_, lbd_b, act_b) = b;
229
230            let keep_a_heuristic = *lbd_a <= 2;
231            let keep_b_heuristic = *lbd_b <= 2;
232
233            if keep_a_heuristic && !keep_b_heuristic {
234                return std::cmp::Ordering::Greater;
235            }
236            if !keep_a_heuristic && keep_b_heuristic {
237                return std::cmp::Ordering::Less;
238            }
239
240            match lbd_a.cmp(lbd_b) {
241                // lower LBD is better
242                std::cmp::Ordering::Equal => act_a
243                    .partial_cmp(act_b)
244                    .unwrap_or(std::cmp::Ordering::Equal),
245                other_lbd_cmp => other_lbd_cmp.reverse(),
246            }
247        };
248
249        self.candidates
250            .select_nth_unstable_by(num_to_remove, comparison);
251
252        self.indices_to_remove.clear();
253        for (idx, _, _) in self.candidates.iter().take(num_to_remove) {
254            self.indices_to_remove.insert(*idx);
255        }
256
257        let mut new_learnt_clauses = Vec::with_capacity(num_to_remove);
258        self.old_to_new_idx_map.clear();
259
260        let mut current_new_idx = learnt_start_idx;
261        for old_idx in learnt_start_idx..cnf.len() {
262            if !self.indices_to_remove.contains(&old_idx) {
263                new_learnt_clauses.push(cnf[old_idx].clone());
264                self.old_to_new_idx_map.insert(old_idx, current_new_idx);
265                current_new_idx = current_new_idx.wrapping_add(1);
266            }
267        }
268
269        let new_learnt_count = self.new_learnt_clauses.len();
270        let new_total_len = learnt_start_idx.wrapping_add(new_learnt_count);
271
272        // update CNF by replacing old learnt clauses with the filtered list
273        cnf.clauses.truncate(learnt_start_idx);
274        cnf.clauses.append(&mut new_learnt_clauses);
275
276        trail.remap_clause_indices(&self.old_to_new_idx_map);
277
278        propagator.cleanup_learnt(learnt_start_idx);
279        for new_idx in learnt_start_idx..new_total_len {
280            // add watches for kept learnt clauses
281            propagator.add_clause(&cnf[new_idx], new_idx);
282        }
283
284        self.conflicts_since_last_cleanup = 0;
285        self.num_removed = self.num_removed.wrapping_add(num_to_remove);
286    }
287
288    /// Bumps the activity of the specified clause.
289    ///
290    /// In this strategy, only the referenced clause itself has its activity bumped by a fixed amount (1.0).
291    fn bump_involved_clause_activities<L: Literal, S: LiteralStorage<L>>(
292        &mut self,
293        cnf: &mut Cnf<L, S>,
294        c_ref: usize,
295    ) {
296        let clause = &mut cnf[c_ref];
297        clause.bump_activity(1.0);
298    }
299
300    /// Returns the total number of clauses removed so far.
301    fn num_removed(&self) -> usize {
302        self.num_removed
303    }
304}
305
306impl<const N: usize> LbdClauseManagement<N> {
307    /// Decays the activity of all learnt clauses by a fixed factor.
308    /// This is called after each conflict.
309    fn decay_clause_activities<L: Literal, S: LiteralStorage<L>>(cnf: &mut Cnf<L, S>) {
310        for clause in &mut cnf.clauses[cnf.non_learnt_idx..] {
311            clause.decay_activity(DECAY_FACTOR);
312        }
313    }
314}
315
316/// A clause management strategy that performs no operations.
317///
318/// This strategy does not clean the clause database, bump activities, or decay them.
319///
320/// # Type Parameters
321///
322/// * `L`: The type of `Literal`.
323/// * `S`: The `LiteralStorage` type.
324#[derive(Debug, Clone, Default, PartialEq, Copy, Eq)]
325pub struct NoClauseManagement;
326
327impl ClauseManagement for NoClauseManagement {
328    /// Creates a new `NoClauseManagement` instance.
329    /// The `clauses` argument is ignored.
330    fn new<L: Literal, S: LiteralStorage<L>>(_clauses: &[Clause<L, S>]) -> Self {
331        Self
332    }
333
334    /// This is a no-op for `NoClauseManagement`.
335    fn on_conflict<L: Literal, S: LiteralStorage<L>>(&mut self, _cnf: &mut Cnf<L, S>) {}
336
337    /// Always returns `false` as this strategy never cleans the database.
338    fn should_clean_db(&self) -> bool {
339        false
340    }
341
342    /// This is a no-op for `NoClauseManagement`.
343    fn clean_clause_db<L: Literal, S: LiteralStorage<L>, P: Propagator<L, S, A>, A: Assignment>(
344        &mut self,
345        _cnf: &mut Cnf<L, S>,
346        _trail: &mut Trail<L, S>,
347        _propagator: &mut P,
348        _assignment: &mut A,
349    ) {
350    }
351
352    /// This is a no-op for `NoClauseManagement`.
353    fn bump_involved_clause_activities<L: Literal, S: LiteralStorage<L>>(
354        &mut self,
355        _cnf: &mut Cnf<L, S>,
356        _c_ref: usize,
357    ) {
358    }
359
360    /// Always returns `0` as no clauses are ever removed.
361    fn num_removed(&self) -> usize {
362        0
363    }
364}
365
366/// Possible clause management implementations
367#[derive(Debug, Clone, PartialEq)]
368pub enum ClauseManagementImpls<const N: usize> {
369    /// No clause management variant
370    NoClauseManagement(NoClauseManagement),
371    /// LBD and activity variant
372    LbdActivityClauseManagement(LbdClauseManagement<N>),
373}
374
375impl<const N: usize> Default for ClauseManagementImpls<N> {
376    fn default() -> Self {
377        Self::NoClauseManagement(NoClauseManagement)
378    }
379}
380
381impl<const N: usize> ClauseManagement for ClauseManagementImpls<N> {
382    fn new<L: Literal, S: LiteralStorage<L>>(clauses: &[Clause<L, S>]) -> Self {
383        LbdActivityClauseManagement(LbdClauseManagement::new(clauses))
384    }
385
386    fn on_conflict<L: Literal, S: LiteralStorage<L>>(&mut self, cnf: &mut Cnf<L, S>) {
387        match self {
388            LbdActivityClauseManagement(m) => m.on_conflict(cnf),
389            Self::NoClauseManagement(m) => m.on_conflict(cnf),
390        }
391    }
392
393    fn should_clean_db(&self) -> bool {
394        match self {
395            LbdActivityClauseManagement(l) => l.should_clean_db(),
396            Self::NoClauseManagement(m) => m.should_clean_db(),
397        }
398    }
399
400    fn clean_clause_db<L: Literal, S: LiteralStorage<L>, P: Propagator<L, S, A>, A: Assignment>(
401        &mut self,
402        cnf: &mut Cnf<L, S>,
403        trail: &mut Trail<L, S>,
404        propagator: &mut P,
405        assignment: &mut A,
406    ) {
407        match self {
408            LbdActivityClauseManagement(m) => m.clean_clause_db(cnf, trail, propagator, assignment),
409            Self::NoClauseManagement(m) => m.clean_clause_db(cnf, trail, propagator, assignment),
410        }
411    }
412
413    fn bump_involved_clause_activities<L: Literal, S: LiteralStorage<L>>(
414        &mut self,
415        cnf: &mut Cnf<L, S>,
416        c_ref: usize,
417    ) {
418        match self {
419            LbdActivityClauseManagement(m) => m.bump_involved_clause_activities(cnf, c_ref),
420            Self::NoClauseManagement(m) => m.bump_involved_clause_activities(cnf, c_ref),
421        }
422    }
423
424    fn num_removed(&self) -> usize {
425        match self {
426            LbdActivityClauseManagement(m) => m.num_removed(),
427            Self::NoClauseManagement(m) => m.num_removed(),
428        }
429    }
430}
431
432/// Enum representing the type of clause management strategy to use in a SAT solver.
433#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, Default, ValueEnum)]
434pub enum ClauseManagementType {
435    /// No clause management strategy
436    NoClauseManagement,
437    /// LBD and activity-based clause management strategy
438    #[default]
439    LbdActivityClauseManagement,
440}
441
442impl Display for ClauseManagementType {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        match self {
445            Self::NoClauseManagement => write!(f, "No Clause Management"),
446            Self::LbdActivityClauseManagement => write!(f, "LBD and Activity Clause Management"),
447        }
448    }
449}
450
451impl ClauseManagementType {
452    /// Converts the `ClauseManagementType` to a concrete `ClauseManagementImpls`.
453    #[allow(dead_code)]
454    pub fn to_impl<L: Literal, S: LiteralStorage<L>, const N: usize>(
455        self,
456        clauses: &[Clause<L, S>],
457    ) -> ClauseManagementImpls<N> {
458        match self {
459            Self::NoClauseManagement => {
460                ClauseManagementImpls::NoClauseManagement(NoClauseManagement::new(clauses))
461            }
462            Self::LbdActivityClauseManagement => {
463                LbdActivityClauseManagement(LbdClauseManagement::<N>::new(clauses))
464            }
465        }
466    }
467}