problemreductions/models/formula/
sat.rs

1//! Boolean Satisfiability (SAT) problem implementation.
2//!
3//! SAT is the problem of determining if there exists an assignment of
4//! Boolean variables that makes a given Boolean formula true. This is
5//! the decision version - for the optimization variant (MAX-SAT), see
6//! the separate MaxSatisfiability type (if available).
7
8use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::{Problem, SatisfactionProblem};
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "Satisfiability",
15        module_path: module_path!(),
16        description: "Find satisfying assignment for CNF formula",
17        fields: &[
18            FieldInfo { name: "num_vars", type_name: "usize", description: "Number of Boolean variables" },
19            FieldInfo { name: "clauses", type_name: "Vec<CNFClause>", description: "Clauses in conjunctive normal form" },
20        ],
21    }
22}
23
24/// A clause in conjunctive normal form (CNF).
25///
26/// A clause is a disjunction (OR) of literals.
27/// Literals are represented as signed integers:
28/// - Positive i means variable i
29/// - Negative -i means NOT variable i
30///
31/// Variables are 1-indexed in the external representation but
32/// 0-indexed internally.
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct CNFClause {
35    /// Literals in this clause (signed integers, 1-indexed).
36    pub literals: Vec<i32>,
37}
38
39impl CNFClause {
40    /// Create a new clause from literals.
41    ///
42    /// Literals are signed integers where positive means the variable
43    /// and negative means its negation. Variables are 1-indexed.
44    pub fn new(literals: Vec<i32>) -> Self {
45        Self { literals }
46    }
47
48    /// Check if the clause is satisfied by an assignment.
49    ///
50    /// # Arguments
51    /// * `assignment` - Boolean assignment, 0-indexed
52    pub fn is_satisfied(&self, assignment: &[bool]) -> bool {
53        self.literals.iter().any(|&lit| {
54            let var = lit.unsigned_abs() as usize - 1; // Convert to 0-indexed
55            let value = assignment.get(var).copied().unwrap_or(false);
56            if lit > 0 {
57                value
58            } else {
59                !value
60            }
61        })
62    }
63
64    /// Get the variables involved in this clause (0-indexed).
65    pub fn variables(&self) -> Vec<usize> {
66        self.literals
67            .iter()
68            .map(|&lit| lit.unsigned_abs() as usize - 1)
69            .collect()
70    }
71
72    /// Get the number of literals.
73    pub fn len(&self) -> usize {
74        self.literals.len()
75    }
76
77    /// Check if the clause is empty.
78    pub fn is_empty(&self) -> bool {
79        self.literals.is_empty()
80    }
81}
82
83/// Boolean Satisfiability (SAT) problem in CNF form.
84///
85/// Given a Boolean formula in conjunctive normal form (CNF),
86/// determine if there exists an assignment that satisfies all clauses.
87/// This is the decision version of the problem.
88///
89/// # Example
90///
91/// ```
92/// use problemreductions::models::formula::{Satisfiability, CNFClause};
93/// use problemreductions::{Problem, Solver, BruteForce};
94///
95/// // Formula: (x1 OR x2) AND (NOT x1 OR x3) AND (NOT x2 OR NOT x3)
96/// let problem = Satisfiability::new(
97///     3,
98///     vec![
99///         CNFClause::new(vec![1, 2]),      // x1 OR x2
100///         CNFClause::new(vec![-1, 3]),     // NOT x1 OR x3
101///         CNFClause::new(vec![-2, -3]),    // NOT x2 OR NOT x3
102///     ],
103/// );
104///
105/// let solver = BruteForce::new();
106/// let solutions = solver.find_all_satisfying(&problem);
107///
108/// // Verify solutions satisfy all clauses
109/// for sol in solutions {
110///     assert!(problem.evaluate(&sol));
111/// }
112/// ```
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Satisfiability {
115    /// Number of variables.
116    num_vars: usize,
117    /// Clauses in CNF.
118    clauses: Vec<CNFClause>,
119}
120
121impl Satisfiability {
122    /// Create a new SAT problem.
123    pub fn new(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
124        Self { num_vars, clauses }
125    }
126
127    /// Get the number of variables.
128    pub fn num_vars(&self) -> usize {
129        self.num_vars
130    }
131
132    /// Get the number of clauses.
133    pub fn num_clauses(&self) -> usize {
134        self.clauses.len()
135    }
136
137    /// Get the total number of literal occurrences across all clauses.
138    pub fn num_literals(&self) -> usize {
139        self.clauses.iter().map(|c| c.len()).sum()
140    }
141
142    /// Get the clauses.
143    pub fn clauses(&self) -> &[CNFClause] {
144        &self.clauses
145    }
146
147    /// Get a specific clause.
148    pub fn get_clause(&self, index: usize) -> Option<&CNFClause> {
149        self.clauses.get(index)
150    }
151
152    /// Count satisfied clauses for an assignment.
153    pub fn count_satisfied(&self, assignment: &[bool]) -> usize {
154        self.clauses
155            .iter()
156            .filter(|c| c.is_satisfied(assignment))
157            .count()
158    }
159
160    /// Check if an assignment satisfies all clauses.
161    pub fn is_satisfying(&self, assignment: &[bool]) -> bool {
162        self.clauses.iter().all(|c| c.is_satisfied(assignment))
163    }
164
165    /// Check if a solution (config) is valid.
166    ///
167    /// For SAT, a valid solution is one that satisfies all clauses.
168    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
169        self.evaluate(config)
170    }
171
172    /// Convert a usize config to boolean assignment.
173    fn config_to_assignment(config: &[usize]) -> Vec<bool> {
174        config.iter().map(|&v| v == 1).collect()
175    }
176}
177
178impl Problem for Satisfiability {
179    const NAME: &'static str = "Satisfiability";
180    type Metric = bool;
181
182    fn dims(&self) -> Vec<usize> {
183        vec![2; self.num_vars]
184    }
185
186    fn evaluate(&self, config: &[usize]) -> bool {
187        let assignment = Self::config_to_assignment(config);
188        self.is_satisfying(&assignment)
189    }
190
191    fn variant() -> Vec<(&'static str, &'static str)> {
192        crate::variant_params![]
193    }
194}
195
196impl SatisfactionProblem for Satisfiability {}
197
198crate::declare_variants! {
199    Satisfiability => "2^num_variables",
200}
201
202/// Check if an assignment satisfies a SAT formula.
203///
204/// # Arguments
205/// * `num_vars` - Number of variables
206/// * `clauses` - Clauses as vectors of literals (1-indexed, signed)
207/// * `assignment` - Boolean assignment (0-indexed)
208#[cfg(test)]
209pub(crate) fn is_satisfying_assignment(
210    _num_vars: usize,
211    clauses: &[Vec<i32>],
212    assignment: &[bool],
213) -> bool {
214    clauses.iter().all(|clause| {
215        clause.iter().any(|&lit| {
216            let var = lit.unsigned_abs() as usize - 1;
217            let value = assignment.get(var).copied().unwrap_or(false);
218            if lit > 0 {
219                value
220            } else {
221                !value
222            }
223        })
224    })
225}
226
227#[cfg(test)]
228#[path = "../../unit_tests/models/formula/sat.rs"]
229mod tests;