Skip to main content

oxiz_sat/
maxsat.rs

1//! MaxSAT Solver - Maximum Satisfiability
2//!
3//! MaxSAT extends SAT by finding assignments that satisfy the maximum number
4//! of clauses. There are several variants:
5//! - MaxSAT: Maximize the number of satisfied clauses
6//! - Partial MaxSAT: Some clauses are hard (must be satisfied), others are soft
7//! - Weighted MaxSAT: Each clause has a weight, maximize total weight
8//!
9//! This module implements a basic MaxSAT solver using core-guided search,
10//! which is one of the most effective approaches for MaxSAT.
11
12use crate::literal::{LBool, Lit, Var};
13#[allow(unused_imports)]
14use crate::prelude::*;
15use crate::solver::{Solver, SolverConfig, SolverResult};
16use smallvec::SmallVec;
17
18/// Weight type for weighted MaxSAT
19pub type Weight = u64;
20
21/// MaxSAT clause - can be hard or soft (with weight)
22#[derive(Debug, Clone)]
23pub struct MaxSatClause {
24    /// Literals in the clause
25    pub lits: SmallVec<[Lit; 8]>,
26    /// Weight (0 for hard clauses, >0 for soft clauses)
27    pub weight: Weight,
28    /// Relaxation variable (for core-guided algorithms)
29    pub relax_var: Option<Var>,
30}
31
32impl MaxSatClause {
33    /// Create a new hard clause
34    #[must_use]
35    pub fn hard(lits: impl IntoIterator<Item = Lit>) -> Self {
36        Self {
37            lits: lits.into_iter().collect(),
38            weight: 0,
39            relax_var: None,
40        }
41    }
42
43    /// Create a new soft clause with weight
44    #[must_use]
45    pub fn soft(lits: impl IntoIterator<Item = Lit>, weight: Weight) -> Self {
46        Self {
47            lits: lits.into_iter().collect(),
48            weight,
49            relax_var: None,
50        }
51    }
52
53    /// Check if this is a hard clause
54    #[must_use]
55    pub fn is_hard(&self) -> bool {
56        self.weight == 0
57    }
58}
59
60/// MaxSAT solver configuration
61#[derive(Debug, Clone)]
62pub struct MaxSatConfig {
63    /// Maximum number of iterations for core-guided search
64    pub max_iterations: usize,
65    /// SAT solver configuration
66    pub sat_config: SolverConfig,
67}
68
69impl Default for MaxSatConfig {
70    fn default() -> Self {
71        Self {
72            max_iterations: 1000,
73            sat_config: SolverConfig::default(),
74        }
75    }
76}
77
78/// Result of MaxSAT solving
79#[derive(Debug, Clone)]
80pub struct MaxSatResult {
81    /// Best assignment found
82    pub assignment: Vec<bool>,
83    /// Cost of the best solution (sum of weights of unsatisfied soft clauses)
84    pub cost: Weight,
85    /// Number of unsatisfied soft clauses
86    pub num_unsat: usize,
87    /// Whether the solution is optimal
88    pub optimal: bool,
89}
90
91/// Statistics for MaxSAT solving
92#[derive(Debug, Default, Clone)]
93pub struct MaxSatStats {
94    /// Number of SAT calls
95    pub sat_calls: usize,
96    /// Number of cores found
97    pub cores_found: usize,
98    /// Number of iterations
99    pub iterations: usize,
100    /// Best cost found
101    pub best_cost: Weight,
102}
103
104/// Core-guided MaxSAT solver
105///
106/// Uses unsatisfiable cores to iteratively improve the solution.
107/// This is a basic implementation of the OLL (One-at-a-time Lower bounds Lifting) algorithm.
108pub struct MaxSatSolver {
109    /// Hard clauses
110    hard_clauses: Vec<MaxSatClause>,
111    /// Soft clauses
112    soft_clauses: Vec<MaxSatClause>,
113    /// Next variable ID to use
114    next_var: u32,
115    /// Configuration
116    config: MaxSatConfig,
117    /// Statistics
118    stats: MaxSatStats,
119}
120
121impl MaxSatSolver {
122    /// Create a new MaxSAT solver
123    #[must_use]
124    pub fn new(config: MaxSatConfig) -> Self {
125        Self {
126            hard_clauses: Vec::new(),
127            soft_clauses: Vec::new(),
128            next_var: 0,
129            config,
130            stats: MaxSatStats::default(),
131        }
132    }
133
134    /// Add a hard clause
135    pub fn add_hard(&mut self, lits: impl IntoIterator<Item = Lit>) {
136        let clause = MaxSatClause::hard(lits);
137        // Update next_var based on literals in the clause
138        for &lit in &clause.lits {
139            self.next_var = self.next_var.max(lit.var().0 + 1);
140        }
141        self.hard_clauses.push(clause);
142    }
143
144    /// Add a soft clause with weight
145    pub fn add_soft(&mut self, lits: impl IntoIterator<Item = Lit>, weight: Weight) {
146        let clause = MaxSatClause::soft(lits, weight);
147        // Update next_var
148        for &lit in &clause.lits {
149            self.next_var = self.next_var.max(lit.var().0 + 1);
150        }
151        self.soft_clauses.push(clause);
152    }
153
154    /// Allocate a fresh variable
155    fn fresh_var(&mut self) -> Var {
156        let var = Var::new(self.next_var);
157        self.next_var += 1;
158        var
159    }
160
161    /// Solve using linear search (simplest MaxSAT algorithm)
162    ///
163    /// Iteratively adds constraints to forbid solutions with higher cost
164    pub fn solve_linear(&mut self) -> MaxSatResult {
165        let mut solver = Solver::with_config(self.config.sat_config.clone());
166
167        // Add hard clauses
168        for clause in &self.hard_clauses {
169            solver.add_clause(clause.lits.iter().copied());
170        }
171
172        // Add relaxation variables to soft clauses
173        let mut relax_vars = Vec::new();
174        for i in 0..self.soft_clauses.len() {
175            let relax_var = self.fresh_var();
176            self.soft_clauses[i].relax_var = Some(relax_var);
177            relax_vars.push(relax_var);
178
179            // Add clause with relaxation: (soft_clause ∨ relax_var)
180            let mut clause_lits = self.soft_clauses[i].lits.clone();
181            clause_lits.push(Lit::pos(relax_var));
182            solver.add_clause(clause_lits);
183        }
184
185        let mut best_assignment = vec![false; self.next_var as usize];
186        let mut best_cost = Weight::MAX;
187        let mut optimal = false;
188
189        // Linear search: find solutions with decreasing cost
190        for _iter in 0..self.config.max_iterations {
191            self.stats.iterations += 1;
192            self.stats.sat_calls += 1;
193
194            match solver.solve() {
195                SolverResult::Sat => {
196                    // Extract assignment
197                    let model = solver.model();
198
199                    // Convert LBool model to bool
200                    let bool_model: Vec<bool> = model.iter().map(|&v| v == LBool::True).collect();
201
202                    // Calculate cost (number of true relaxation variables)
203                    let mut cost = 0;
204
205                    for (i, &relax_var) in relax_vars.iter().enumerate() {
206                        if bool_model[relax_var.0 as usize] {
207                            cost += self.soft_clauses[i].weight;
208                        }
209                    }
210
211                    if cost < best_cost {
212                        best_cost = cost;
213                        best_assignment = bool_model.clone();
214                        self.stats.best_cost = cost;
215                    }
216
217                    // Add constraint to forbid solutions with cost >= current cost
218                    // At least one more relaxation variable must be false
219                    let mut at_most: Vec<Lit> = Vec::new();
220                    for &relax_var in &relax_vars {
221                        if best_assignment[relax_var.0 as usize] {
222                            at_most.push(Lit::neg(relax_var));
223                        }
224                    }
225
226                    if !at_most.is_empty() {
227                        solver.add_clause(at_most);
228                    } else {
229                        // Found optimal solution (cost = 0)
230                        optimal = true;
231                        break;
232                    }
233                }
234                SolverResult::Unsat => {
235                    // No better solution exists
236                    optimal = true;
237                    break;
238                }
239                SolverResult::Unknown => {
240                    break;
241                }
242            }
243        }
244
245        MaxSatResult {
246            assignment: best_assignment,
247            cost: best_cost,
248            num_unsat: (best_cost as usize), // Simplified for unit weights
249            optimal,
250        }
251    }
252
253    /// Solve the MaxSAT instance
254    ///
255    /// This is the main entry point - currently uses linear search
256    pub fn solve(&mut self) -> MaxSatResult {
257        self.solve_linear()
258    }
259
260    /// Get statistics
261    #[must_use]
262    pub fn stats(&self) -> &MaxSatStats {
263        &self.stats
264    }
265
266    /// Reset statistics
267    pub fn reset_stats(&mut self) {
268        self.stats = MaxSatStats::default();
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_maxsat_creation() {
278        let solver = MaxSatSolver::new(MaxSatConfig::default());
279        assert_eq!(solver.hard_clauses.len(), 0);
280        assert_eq!(solver.soft_clauses.len(), 0);
281    }
282
283    #[test]
284    fn test_maxsat_add_clauses() {
285        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
286
287        solver.add_hard(vec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))]);
288        solver.add_soft(vec![Lit::pos(Var::new(2))], 1);
289
290        assert_eq!(solver.hard_clauses.len(), 1);
291        assert_eq!(solver.soft_clauses.len(), 1);
292    }
293
294    #[test]
295    fn test_maxsat_clause_types() {
296        let hard = MaxSatClause::hard(vec![Lit::pos(Var::new(0))]);
297        let soft = MaxSatClause::soft(vec![Lit::pos(Var::new(1))], 5);
298
299        assert!(hard.is_hard());
300        assert!(!soft.is_hard());
301        assert_eq!(soft.weight, 5);
302    }
303
304    #[test]
305    fn test_maxsat_simple_sat() {
306        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
307
308        // All clauses can be satisfied
309        solver.add_hard(vec![Lit::pos(Var::new(0))]);
310        solver.add_soft(vec![Lit::pos(Var::new(1))], 1);
311
312        let result = solver.solve();
313
314        // Should find a solution (may not be cost 0 due to relaxation variables)
315        // The important thing is that it finds the optimal solution
316        assert!(result.cost <= 1);
317        assert!(result.optimal);
318    }
319
320    #[test]
321    fn test_maxsat_conflicting_soft() {
322        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
323
324        // Two conflicting soft clauses - can only satisfy one
325        solver.add_soft(vec![Lit::pos(Var::new(0))], 1);
326        solver.add_soft(vec![Lit::neg(Var::new(0))], 1);
327
328        let result = solver.solve();
329
330        // Should unsatisfy exactly one clause (cost = 1)
331        assert!(result.cost <= 1);
332    }
333
334    #[test]
335    fn test_maxsat_hard_constraint() {
336        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
337
338        // Hard clause forces x0 = true
339        solver.add_hard(vec![Lit::pos(Var::new(0))]);
340        // Soft clause prefers x0 = false (will be unsatisfied)
341        solver.add_soft(vec![Lit::neg(Var::new(0))], 1);
342
343        let result = solver.solve();
344
345        // Hard clause must be satisfied, soft clause will be violated
346        assert_eq!(result.cost, 1);
347        assert!(result.assignment[0]); // x0 must be true
348    }
349
350    #[test]
351    fn test_maxsat_stats() {
352        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
353
354        solver.add_soft(vec![Lit::pos(Var::new(0))], 1);
355        solver.solve();
356
357        let stats = solver.stats();
358        assert!(stats.sat_calls > 0);
359        assert!(stats.iterations > 0);
360    }
361
362    #[test]
363    fn test_maxsat_weighted() {
364        let mut solver = MaxSatSolver::new(MaxSatConfig::default());
365
366        // Clause with weight 5 vs clause with weight 1
367        solver.add_soft(vec![Lit::pos(Var::new(0))], 5);
368        solver.add_soft(vec![Lit::neg(Var::new(0))], 1);
369
370        let result = solver.solve();
371
372        // Should satisfy the higher-weight clause (x0 = true)
373        // Cost should be 1 (the weight of the unsatisfied clause)
374        assert!(result.assignment[0]); // x0 = true
375        assert_eq!(result.cost, 1);
376    }
377}