sat_solver/sat/clause.rs
1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2
3//! Contains details of a clause, a fundamental component in SAT solvers.
4//!
5//! A clause is a disjunction of literals (e.g. `x1 OR !x2 OR x3`).
6//! This module defines the `Clause` struct, which stores literals and associated metadata
7//! relevant for Conflict-Driven Clause Learning (CDCL) SAT solvers, such as
8//! Literal Blocks Distance (LBD) and activity scores for clause deletion strategies.
9
10use crate::sat::clause_storage::LiteralStorage;
11use crate::sat::literal::{Literal, PackedLiteral};
12use crate::sat::trail::Trail;
13use bit_vec::BitVec;
14use core::ops::{Index, IndexMut};
15use itertools::Itertools;
16use ordered_float::OrderedFloat;
17use rustc_hash::{FxBuildHasher, FxHashSet};
18use smallvec::SmallVec;
19use std::hash::Hash;
20use std::marker::PhantomData;
21
22/// Represents a clause in a SAT formula.
23///
24/// A clause is a set of literals, typically interpreted as a disjunction.
25/// For example, the clause `{L1, L2, L3}` represents `L1 OR L2 OR L3`.
26///
27/// # Type Parameters
28///
29/// * `L`: The type of literal stored in the clause. Defaults to `PackedLiteral`.
30/// Must implement the `Literal` trait.
31/// * `S`: The storage type for the literals. Defaults to `SmallVec<[L; 8]>`.
32/// Must implement the `LiteralStorage<L>` trait, providing storage for literals.
33#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
34pub struct Clause<L: Literal = PackedLiteral, S: LiteralStorage<L> = SmallVec<[L; 8]>> {
35 /// The collection of literals forming the clause.
36 /// The specific storage (e.g. `Vec`, `SmallVec`) is determined by the `S` type parameter.
37 pub literals: S,
38 /// Literal Blocks Distance (LBD) of the clause.
39 /// LBD is a measure of the "locality" of a learnt clause in terms of decision levels.
40 /// Lower LBD often indicates higher quality learnt clauses. Calculated for learnt clauses.
41 pub lbd: u32,
42 /// Flag indicating if the clause has been marked for deletion.
43 /// Deleted clauses are removed by the solver during database cleaning.
44 pub deleted: bool,
45 /// Flag indicating if the clause was learnt during conflict analysis.
46 /// Original clauses (from the input problem) are not marked as learnt.
47 pub is_learnt: bool,
48 /// Activity score of the clause, used in clause deletion heuristics (e.g. VSIDS-like).
49 /// Higher activity suggests the clause has been more recently involved in conflicts or propagations.
50 pub activity: OrderedFloat<f64>,
51 /// `PhantomData` to ensure handling of the generic type `L`.
52 data: PhantomData<*const L>,
53}
54
55impl<L: Literal, S: LiteralStorage<L>> AsRef<[L]> for Clause<L, S> {
56 /// Returns a slice containing the literals in the clause.
57 /// This allows the clause to be treated as a slice for read-only operations.
58 fn as_ref(&self) -> &[L] {
59 self.literals.as_ref()
60 }
61}
62
63impl<L: Literal, S: LiteralStorage<L>> FromIterator<L> for Clause<L, S> {
64 /// Creates a new clause from an iterator of literals.
65 ///
66 /// The literals from the iterator are collected into the `literals` field.
67 ///
68 /// Literals are deduplicated during creation using `itertools::Itertools::unique`.
69 /// For example, `[L1, L1, L2]` becomes `{L1, L2}`.
70 ///
71 /// The clause is initialised as not learnt, not deleted, with LBD 0 and activity 0.0.
72 /// Other fields (`lbd`, `deleted`, `is_learnt`, `activity`) are initialised to default values.
73 fn from_iter<I: IntoIterator<Item = L>>(iter: I) -> Self {
74 Self {
75 literals: iter.into_iter().unique().collect(),
76 lbd: 0,
77 deleted: false,
78 is_learnt: false,
79 activity: OrderedFloat::from(0.0),
80 data: PhantomData,
81 }
82 }
83}
84
85impl<L: Literal, S: LiteralStorage<L>> Clause<L, S> {
86 /// Creates a new clause from a slice of literals.
87 ///
88 /// # Arguments
89 ///
90 /// * `literals_slice`: A slice of literals to form the clause.
91 #[must_use]
92 pub fn new(literals_slice: &[L]) -> Self {
93 literals_slice.iter().copied().collect()
94 }
95
96 /// Performs resolution between this clause and another clause on a pivot literal.
97 ///
98 /// The resolution rule is: `(A V x) AND (B V !x) => (A V B)`.
99 /// In this context, `self` represents `(A V x)` and `other` represents `(B V !x)`.
100 /// The `pivot` argument is the literal `x`.
101 ///
102 /// - If `pivot` (i.e. `x`) is not found in `self.literals`, or `pivot.negated()` (i.e. `!x`)
103 /// is not found in `other.literals`, resolution is not applicable with the given pivot.
104 /// In this case, a clone of `self` is returned.
105 /// - The resolvent clause is formed by taking the union of literals from `self` (excluding `pivot`)
106 /// and `other` (excluding `pivot.negated()`), with duplicates removed.
107 /// - If the resulting resolvent is a tautology (e.g. contains both `y` and `!y`),
108 /// a default (typically empty and non-learnt) clause is returned. A tautological resolvent
109 /// is logically true and provides no new constraints.
110 ///
111 /// # Arguments
112 ///
113 /// * `other`: The other clause to resolve with.
114 /// * `pivot`: The literal `x` to resolve upon. `self` should contain `pivot`, and
115 /// `other` should contain `pivot.negated()`.
116 ///
117 /// # Returns
118 ///
119 /// The resolved clause. This is a new `Clause` instance.
120 #[must_use]
121 pub fn resolve(&self, other: &Self, pivot: L) -> Self {
122 if !self.literals.iter().contains(&pivot)
123 || !other.literals.iter().contains(&pivot.negated())
124 {
125 return self.clone();
126 }
127
128 let mut resolved_literals: FxHashSet<L> = FxHashSet::default();
129
130 for &lit in self.literals.iter() {
131 if lit != pivot {
132 resolved_literals.insert(lit);
133 }
134 }
135 for &lit in other.literals.iter() {
136 if lit != pivot.negated() {
137 resolved_literals.insert(lit);
138 }
139 }
140
141 let resolved_clause = resolved_literals.into_iter().collect::<Self>();
142
143 if resolved_clause.is_tautology() {
144 Self::default()
145 } else {
146 resolved_clause
147 }
148 }
149
150 /// Performs binary resolution: resolves `self` with a binary clause `binary`.
151 ///
152 /// A binary clause contains exactly two literals. Let `binary` be `(L1 V L2)`.
153 /// - If `self` contains `!L1`, the result is `(self - {!L1}) V {L2}`.
154 /// - If `self` contains `!L2`, the result is `(self - {!L2}) V {L1}`.
155 /// The literals in the resulting clause are unique.
156 /// If the resulting clause is a tautology, `None` is returned.
157 ///
158 /// # Arguments
159 ///
160 /// * `binary`: A binary clause (must have exactly two literals).
161 ///
162 /// # Returns
163 ///
164 /// `Some(resolved_clause)` if resolution is possible and the result is not a tautology.
165 /// `None` if `binary` is not a binary clause, resolution is not applicable (no matching negated literal found),
166 /// or the result of resolution is a tautology.
167 ///
168 /// # Panics
169 /// The `unwrap_or_else` for `position` might lead to unexpected behavior if the literal to be removed
170 /// is not found (which shouldn't happen if `lit` was found in `self.literals` and `new_clause` is a clone).
171 /// If `position` returns `None`, `remove_literal` would attempt to remove the last element of `new_clause.literals`.
172 #[must_use]
173 pub fn resolve_binary(&self, binary: &Self) -> Option<Self> {
174 if binary.len() != 2 {
175 return None;
176 }
177
178 let bin_lit1 = binary.literals[0];
179 let bin_lit2 = binary.literals[1];
180
181 for &lit_in_self in self.literals.iter() {
182 if lit_in_self == bin_lit1.negated() {
183 let mut new_clause = self.clone();
184 let pos = new_clause
185 .literals
186 .iter()
187 .position(|&x| x == lit_in_self)
188 .unwrap_or_else(|| new_clause.literals.len().saturating_sub(1));
189 if !new_clause.literals.is_empty() {
190 new_clause.remove_literal(pos);
191 }
192 new_clause.push(bin_lit2);
193
194 return if new_clause.is_tautology() {
195 None
196 } else {
197 Some(new_clause)
198 };
199 } else if lit_in_self == bin_lit2.negated() {
200 let mut new_clause = self.clone();
201 let pos = new_clause
202 .literals
203 .iter()
204 .position(|&x| x == lit_in_self)
205 .unwrap_or_else(|| new_clause.literals.len().saturating_sub(1));
206 if !new_clause.literals.is_empty() {
207 new_clause.remove_literal(pos);
208 }
209 new_clause.push(bin_lit1);
210
211 return if new_clause.is_tautology() {
212 None
213 } else {
214 Some(new_clause)
215 };
216 }
217 }
218
219 None
220 }
221
222 /// Adds a literal to the clause, if it is not already present.
223 ///
224 /// # Arguments
225 ///
226 /// * `literal`: The literal to add.
227 pub fn push(&mut self, literal: L) {
228 if !self.literals.iter().contains(&literal) {
229 self.literals.push(literal);
230 }
231 }
232
233 /// Checks if the clause is a tautology.
234 ///
235 /// A clause is a tautology if it contains both a literal and its negation
236 /// (e.g. `x OR !x`). Such clauses are always true and not useful
237 ///
238 /// # Returns
239 ///
240 /// `true` if the clause is a tautology, `false` otherwise.
241 #[must_use]
242 pub fn is_tautology(&self) -> bool {
243 let mut set = FxHashSet::with_capacity_and_hasher(self.len(), FxBuildHasher);
244
245 for lit_ref in self.literals.iter() {
246 let lit = *lit_ref;
247 if set.contains(&lit.negated()) {
248 return true;
249 }
250 set.insert(lit);
251 }
252 false
253 }
254
255 /// Returns the number of literals in the clause.
256 #[must_use]
257 pub fn len(&self) -> usize {
258 self.literals.len()
259 }
260
261 /// Returns an iterator over the literals in the clause.
262 pub fn iter(&self) -> impl Iterator<Item = &L> {
263 self.literals.iter()
264 }
265
266 /// Returns a mutable iterator over the literals in the clause.
267 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut L> {
268 self.literals.iter_mut()
269 }
270
271 /// Swaps two literals in the clause by their indices.
272 ///
273 /// # Arguments
274 ///
275 /// * `i`: The index of the first literal.
276 /// * `j`: The index of the second literal.
277 ///
278 /// # Panics
279 ///
280 /// Panics if `i` or `j` are out of bounds for `self.literals`.
281 pub fn swap(&mut self, i: usize, j: usize) {
282 self.literals.swap(i, j);
283 }
284
285 /// Checks if the clause is a unit clause.
286 ///
287 /// A unit clause is a clause with exactly one literal.
288 ///
289 /// # Returns
290 ///
291 /// `true` if the clause has exactly one literal, `false` otherwise.
292 #[must_use]
293 pub fn is_unit(&self) -> bool {
294 self.len() == 1
295 }
296
297 /// Checks if the clause is empty.
298 ///
299 /// An empty clause (containing no literals) represents a contradiction (logical false).
300 ///
301 /// # Returns
302 ///
303 /// `true` if the clause has no literals, `false` otherwise.
304 #[must_use]
305 pub fn is_empty(&self) -> bool {
306 self.literals.is_empty()
307 }
308
309 /// Checks if the clause is marked as deleted.
310 #[must_use]
311 pub const fn is_deleted(&self) -> bool {
312 self.deleted
313 }
314
315 /// Marks the clause as deleted.
316 ///
317 /// This only sets the `deleted` flag to `true`. It does not remove the clause
318 pub const fn delete(&mut self) {
319 self.deleted = true;
320 }
321
322 /// Removes a literal from the clause at the given index.
323 ///
324 /// This method uses `swap_remove` for efficiency, which means the order of
325 /// the remaining literals in the clause may change (the last literal is swapped
326 /// into the `idx` position, and then the length is reduced).
327 ///
328 /// # Arguments
329 ///
330 /// * `idx`: The index of the literal to remove.
331 ///
332 /// # Panics
333 ///
334 /// Panics if `idx` is out of bounds for `self.literals`.
335 pub fn remove_literal(&mut self, idx: usize) {
336 self.literals.swap_remove(idx);
337 }
338
339 /// Calculates and updates the Literal Block Distance (LBD) of the clause.
340 ///
341 /// LBD is defined as the number of distinct decision levels (excluding level 0)
342 /// of the variables in the clause's literals. This metric is used
343 /// to estimate the "quality" or "usefulness" of learnt clauses
344 ///
345 /// Special LBD rules applied:
346 /// - If the clause is empty, LBD is 0.
347 /// - If all literals are at decision level 0 (or unassigned and treated as level 0),
348 /// and the clause is not empty, LBD is 1.
349 /// - For learnt, non-empty clauses, LBD is ensured to be at least 1.
350 ///
351 /// # Arguments
352 ///
353 /// * `trail`: The solver's assignment trail, used to find the decision level
354 /// of each literal's variable.
355 ///
356 /// # Panics
357 /// Behavior of `BitVec` indexing with `level` (a `u32`) depends on `usize` size.
358 pub fn calculate_lbd(&mut self, trail: &Trail<L, S>) {
359 if self.is_empty() {
360 self.lbd = 0;
361 return;
362 }
363
364 let max_level_in_clause = self
365 .literals
366 .iter()
367 .map(|lit| trail.level(lit.variable()))
368 .max()
369 .unwrap_or(0);
370
371 let mut levels_seen = BitVec::from_elem(max_level_in_clause.wrapping_add(1), false);
372 let mut count = 0u32;
373
374 for &lit in self.literals.iter() {
375 let level = trail.level(lit.variable());
376 if level > 0 {
377 let level_idx = level;
378 if !levels_seen.get(level_idx).unwrap_or(true) {
379 levels_seen.set(level_idx, true);
380 count = count.wrapping_add(1);
381 }
382 }
383 }
384
385 if count == 0 {
386 self.lbd = if !self.is_empty()
387 && self.literals.iter().any(|l| trail.level(l.variable()) == 0)
388 {
389 1
390 } else {
391 u32::from(!self.is_empty())
392 };
393 } else {
394 self.lbd = count;
395 }
396
397 if self.is_learnt && !self.is_empty() && self.lbd == 0 {
398 self.lbd = 1;
399 }
400 }
401
402 /// Converts this clause into a clause with different literal or storage types.
403 ///
404 /// This is useful for benchmarking primarily
405 /// The literals are converted one by one using `T::new(original_lit.variable(), original_lit.polarity())`.
406 /// Metadata like LBD, deleted status, learnt status, and activity are copied.
407 ///
408 /// # Type Parameters
409 ///
410 /// * `T`: The target `Literal` type for the new clause.
411 /// * `U`: The target `LiteralStorage<T>` type for the new clause.
412 ///
413 /// # Returns
414 ///
415 /// A new `Clause<T, U>` with equivalent literals and copied metadata.
416 pub fn convert<T: Literal, U: LiteralStorage<T>>(&self) -> Clause<T, U> {
417 let literals_as_t: Vec<T> = self
418 .literals
419 .iter()
420 .map(|lit| T::new(lit.variable(), lit.polarity()))
421 .collect_vec();
422
423 let mut new_clause = Clause::<T, U>::new(&literals_as_t);
424 new_clause.lbd = self.lbd;
425 new_clause.deleted = self.deleted;
426 new_clause.is_learnt = self.is_learnt;
427 new_clause.activity = self.activity;
428 new_clause
429 }
430
431 /// Increases the activity score of the clause by a specified increment.
432 ///
433 /// This is part of VSIDS-like (Variable State Independent Decaying Sum) heuristics,
434 /// where clauses involved in recent conflict analysis get their activity "bumped",
435 /// making them less likely to be deleted.
436 ///
437 /// # Arguments
438 ///
439 /// * `increment`: The positive amount to add to the clause's activity score.
440 pub fn bump_activity(&mut self, increment: f64) {
441 self.activity += increment;
442 }
443
444 /// Decays the activity score of the clause by a multiplicative factor.
445 ///
446 /// # Arguments
447 ///
448 /// * `factor`: The factor to multiply the activity score by (typically between 0 and 1).
449 pub fn decay_activity(&mut self, factor: f64) {
450 self.activity *= factor;
451 }
452
453 /// Returns the raw floating-point activity score of the clause.
454 #[must_use]
455 pub const fn activity(&self) -> f64 {
456 self.activity.0
457 }
458}
459
460impl<T: Literal, S: LiteralStorage<T>> Index<usize> for Clause<T, S> {
461 type Output = T;
462
463 /// Accesses the literal at the given `index` within the clause.
464 ///
465 /// # Panics
466 ///
467 /// Panics if `index` is out of bounds for `self.literals`.
468 ///
469 /// # Safety
470 ///
471 /// This implementation uses `get_unchecked` for performance
472 fn index(&self, index: usize) -> &Self::Output {
473 // Safety: Caller must ensure `index` is within bounds `[0, self.literals.len())`.
474 // This is fine is used within the correct context.
475 unsafe { self.literals.get_unchecked(index) }
476 }
477}
478
479impl<T: Literal, S: LiteralStorage<T>> IndexMut<usize> for Clause<T, S> {
480 /// Mutably accesses the literal at the given `index` within the clause.
481 ///
482 /// # Panics
483 ///
484 /// Panics if `index` is out of bounds for `self.literals`.
485 ///
486 /// # Safety
487 ///
488 /// This implementation uses `get_unchecked_mut` for performance.
489 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
490 // Safety: Caller must ensure `index` is within bounds `[0, self.literals.len())`.
491 // This is fine is used within the correct context.
492 unsafe { self.literals.get_unchecked_mut(index) }
493 }
494}
495
496impl<T: Literal, S: LiteralStorage<T>> From<&Clause<T, S>> for Vec<T> {
497 /// Converts a reference to a clause into a `Vec` of its literals.
498 ///
499 /// The literals are copied from the clause's internal storage into a new `Vec<T>`.
500 fn from(clause: &Clause<T, S>) -> Self {
501 clause.literals.iter().copied().collect()
502 }
503}
504
505impl<L: Literal, S: LiteralStorage<L>> From<Vec<i32>> for Clause<L, S> {
506 /// Creates a clause from a `Vec<i32>`, where integers represent literals
507 /// in DIMACS format (e.g. `1` for variable 1 true, `-2` for variable 2 false).
508 ///
509 /// This constructor uses `Clause::new`, so literals will be deduplicated.
510 /// Other fields (`lbd`, `deleted`, etc.) are initialised to defaults.
511 fn from(literals_dimacs: Vec<i32>) -> Self {
512 literals_dimacs.iter().copied().map(L::from_i32).collect()
513 }
514}
515
516impl<L: Literal, S: LiteralStorage<L>> From<Vec<L>> for Clause<L, S> {
517 /// Creates a clause directly from a `Vec<L>`.
518 fn from(literals_vec: Vec<L>) -> Self {
519 literals_vec.into_iter().collect()
520 }
521}
522
523impl<L: Literal, S: LiteralStorage<L>> From<&Vec<L>> for Clause<L, S> {
524 /// Creates a clause from a reference to a `Vec<L>`, cloning the literals.
525 ///
526 /// The literals from `literals_vec` are cloned to create the `literals` field.
527 fn from(literals_vec: &Vec<L>) -> Self {
528 literals_vec.iter().copied().collect()
529 }
530}
531
532impl<L: Literal, S: LiteralStorage<L>> FromIterator<i32> for Clause<L, S> {
533 /// Creates a clause from an iterator of `i32` (DIMACS literals).
534 ///
535 /// This is similar to `From<Vec<i32>>`. Literals are converted from DIMACS format.
536 /// See `From<Vec<i32>>` for more details on DIMACS conversion and variable indexing.
537 fn from_iter<I: IntoIterator<Item = i32>>(iter: I) -> Self {
538 iter.into_iter().map(L::from_i32).collect()
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_new_from_i32_vec_and_len() {
548 let clause: Clause = Clause::from(vec![1, 2, 3]);
549 assert_eq!(clause.len(), 3, "Clause length should be 3");
550
551 let expected_lit = PackedLiteral::new(1_u32, true);
552 assert!(
553 clause.literals.iter().any(|&l| l == expected_lit),
554 "Clause should contain literal for var 1 positive"
555 );
556 }
557
558 #[test]
559 fn test_swap_no_assert() {
560 let mut clause: Clause = Clause::from(vec![1, 2, 3]);
561
562 if clause.len() == 3 {
563 let lit0_before = clause.literals[0];
564 let lit2_before = clause.literals[2];
565 clause.swap(0, 2);
566 assert_eq!(
567 clause.literals[0], lit2_before,
568 "Literal at index 0 should be the original literal from index 2"
569 );
570 assert_eq!(
571 clause.literals[2], lit0_before,
572 "Literal at index 2 should be the original literal from index 0"
573 );
574 } else {
575 panic!("Clause length was not 3 after creation from vec![1,2,3]");
576 }
577 }
578
579 #[test]
580 fn test_is_tautology() {
581 let tautology_clause: Clause = Clause::from(vec![1, -1]);
582 assert!(
583 tautology_clause.is_tautology(),
584 "Clause should be tautology"
585 );
586
587 let non_tautology_clause: Clause = Clause::from(vec![1, 2]);
588 assert!(
589 !non_tautology_clause.is_tautology(),
590 "Clause should not be tautology"
591 );
592 }
593
594 #[test]
595 fn test_resolve() {
596 let clause1: Clause = Clause::from(vec![1, 2]);
597 let clause2: Clause = Clause::from(vec![-1, 3]);
598 let pivot = PackedLiteral::new(1_u32, true);
599
600 let resolved_clause = clause1.resolve(&clause2, pivot);
601 assert_eq!(
602 resolved_clause.literals.len(),
603 2,
604 "Resolved clause should have 2 literals"
605 );
606 assert!(
607 resolved_clause
608 .literals
609 .iter()
610 .any(|&l| l == PackedLiteral::new(2_u32, true))
611 );
612 assert!(
613 resolved_clause
614 .literals
615 .iter()
616 .any(|&l| l == PackedLiteral::new(3_u32, true))
617 );
618 }
619
620 #[test]
621 fn test_is_unit() {
622 let unit_clause: Clause = Clause::from(vec![1]);
623 assert!(unit_clause.is_unit(), "Clause should be unit");
624
625 let non_unit_clause: Clause = Clause::from(vec![1, 2]);
626 assert!(!non_unit_clause.is_unit(), "Clause should not be unit");
627 }
628
629 #[test]
630 fn test_is_empty() {
631 let empty_clause: Clause = Clause::default();
632 assert!(empty_clause.is_empty(), "Clause should be empty");
633
634 let non_empty_clause: Clause = Clause::from(vec![1]);
635 assert!(!non_empty_clause.is_empty(), "Clause should not be empty");
636 }
637}