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;