oxiz_solver/
optimization.rs

1//! Optimization module for OxiZ Solver
2//!
3//! Provides SMT optimization features including:
4//! - Objective minimization and maximization
5//! - Lexicographic optimization (multiple objectives with priorities)
6//! - Pareto optimization (multi-objective)
7//! - Soft constraints (MaxSMT)
8
9use crate::solver::{Solver, SolverResult};
10use num_bigint::BigInt;
11use num_rational::Rational64;
12use num_traits::Zero;
13use oxiz_core::ast::{TermId, TermKind, TermManager};
14
15/// Optimization objective type
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ObjectiveKind {
18    /// Minimize the objective
19    Minimize,
20    /// Maximize the objective
21    Maximize,
22}
23
24/// An optimization objective
25#[derive(Debug, Clone)]
26pub struct Objective {
27    /// The term to optimize (must be Int or Real)
28    pub term: TermId,
29    /// Whether to minimize or maximize
30    pub kind: ObjectiveKind,
31    /// Priority for lexicographic optimization (lower = higher priority)
32    pub priority: usize,
33}
34
35/// Result of optimization
36#[derive(Debug, Clone)]
37pub enum OptimizationResult {
38    /// Optimal value found
39    Optimal {
40        /// The optimal value (as a term)
41        value: TermId,
42        /// The model achieving this value
43        model: crate::solver::Model,
44    },
45    /// Unbounded (no finite optimum)
46    Unbounded,
47    /// Unsatisfiable (no solution exists)
48    Unsat,
49    /// Unknown (timeout, incomplete, etc.)
50    Unknown,
51}
52
53/// Optimizer for SMT formulas with objectives
54///
55/// The optimizer extends the basic SMT solver with optimization capabilities,
56/// allowing you to minimize or maximize objectives subject to constraints.
57///
58/// # Examples
59///
60/// ## Basic Minimization
61///
62/// ```
63/// use oxiz_solver::{Optimizer, OptimizationResult};
64/// use oxiz_core::ast::TermManager;
65/// use num_bigint::BigInt;
66///
67/// let mut opt = Optimizer::new();
68/// let mut tm = TermManager::new();
69///
70/// opt.set_logic("QF_LIA");
71///
72/// let x = tm.mk_var("x", tm.sorts.int_sort);
73/// let five = tm.mk_int(BigInt::from(5));
74/// opt.assert(tm.mk_ge(x, five));
75///
76/// // Minimize x (should be 5)
77/// opt.minimize(x);
78/// let result = opt.optimize(&mut tm);
79///
80/// match result {
81///     OptimizationResult::Optimal { .. } => println!("Found optimal solution"),
82///     _ => println!("No optimal solution"),
83/// }
84/// ```
85///
86/// ## Lexicographic Optimization
87///
88/// ```
89/// use oxiz_solver::{Optimizer, OptimizationResult};
90/// use oxiz_core::ast::TermManager;
91/// use num_bigint::BigInt;
92///
93/// let mut opt = Optimizer::new();
94/// let mut tm = TermManager::new();
95///
96/// opt.set_logic("QF_LIA");
97///
98/// let x = tm.mk_var("x", tm.sorts.int_sort);
99/// let y = tm.mk_var("y", tm.sorts.int_sort);
100/// let zero = tm.mk_int(BigInt::from(0));
101/// let ten = tm.mk_int(BigInt::from(10));
102///
103/// opt.assert(tm.mk_ge(x, zero));
104/// let zero_y = tm.mk_int(BigInt::from(0));
105/// opt.assert(tm.mk_ge(y, zero_y));
106/// let sum = tm.mk_add(vec![x, y]);
107/// opt.assert(tm.mk_ge(sum, ten));
108///
109/// // Minimize x first, then y
110/// opt.minimize(x);
111/// opt.minimize(y);
112///
113/// let _result = opt.optimize(&mut tm);
114/// ```
115#[derive(Debug)]
116pub struct Optimizer {
117    /// The underlying solver
118    solver: Solver,
119    /// Optimization objectives
120    objectives: Vec<Objective>,
121    /// Cached assertions (to be encoded when optimize() is called)
122    assertions: Vec<TermId>,
123}
124
125impl Optimizer {
126    /// Create a new optimizer
127    #[must_use]
128    pub fn new() -> Self {
129        Self {
130            solver: Solver::new(),
131            objectives: Vec::new(),
132            assertions: Vec::new(),
133        }
134    }
135
136    /// Add an assertion
137    pub fn assert(&mut self, term: TermId) {
138        self.assertions.push(term);
139    }
140
141    /// Add a minimization objective
142    pub fn minimize(&mut self, term: TermId) {
143        self.objectives.push(Objective {
144            term,
145            kind: ObjectiveKind::Minimize,
146            priority: self.objectives.len(),
147        });
148    }
149
150    /// Add a maximization objective
151    pub fn maximize(&mut self, term: TermId) {
152        self.objectives.push(Objective {
153            term,
154            kind: ObjectiveKind::Maximize,
155            priority: self.objectives.len(),
156        });
157    }
158
159    /// Set logic
160    pub fn set_logic(&mut self, logic: &str) {
161        self.solver.set_logic(logic);
162    }
163
164    /// Push a scope
165    pub fn push(&mut self) {
166        self.solver.push();
167    }
168
169    /// Pop a scope
170    pub fn pop(&mut self) {
171        self.solver.pop();
172    }
173
174    /// Check satisfiability and optimize objectives
175    ///
176    /// Uses linear search with binary search refinement for integer objectives.
177    /// For lexicographic optimization, processes objectives in priority order.
178    pub fn optimize(&mut self, term_manager: &mut TermManager) -> OptimizationResult {
179        // Encode all assertions into SAT clauses
180        for &assertion in &self.assertions.clone() {
181            self.solver.assert(assertion, term_manager);
182        }
183        // Clear assertions since they're now encoded
184        self.assertions.clear();
185
186        if self.objectives.is_empty() {
187            // No objectives - just check satisfiability
188            match self.solver.check(term_manager) {
189                SolverResult::Sat => {
190                    if let Some(model) = self.solver.model() {
191                        // Return arbitrary value (0) since no objective
192                        let zero = term_manager.mk_int(BigInt::zero());
193                        return OptimizationResult::Optimal {
194                            value: zero,
195                            model: model.clone(),
196                        };
197                    }
198                    OptimizationResult::Unknown
199                }
200                SolverResult::Unsat => OptimizationResult::Unsat,
201                SolverResult::Unknown => OptimizationResult::Unknown,
202            }
203        } else {
204            // Sort objectives by priority for lexicographic optimization
205            let mut sorted_objectives = self.objectives.clone();
206            sorted_objectives.sort_by_key(|obj| obj.priority);
207
208            // Optimize each objective in order
209            for (idx, objective) in sorted_objectives.iter().enumerate() {
210                let result = self.optimize_single(objective, term_manager);
211
212                match result {
213                    OptimizationResult::Optimal { value, model } => {
214                        // For lexicographic optimization, fix this objective to its optimal value
215                        if idx < sorted_objectives.len() - 1 {
216                            // More objectives to optimize - constrain this one
217                            self.solver.push();
218                            let eq = term_manager.mk_eq(objective.term, value);
219                            self.solver.assert(eq, term_manager);
220                        } else {
221                            // Last objective - return the result
222                            return OptimizationResult::Optimal { value, model };
223                        }
224                    }
225                    other => return other,
226                }
227            }
228
229            OptimizationResult::Unknown
230        }
231    }
232
233    /// Optimize a single objective using linear search
234    fn optimize_single(
235        &mut self,
236        objective: &Objective,
237        term_manager: &mut TermManager,
238    ) -> OptimizationResult {
239        // First check if the problem is satisfiable
240        let result = self.solver.check(term_manager);
241        if result != SolverResult::Sat {
242            return match result {
243                SolverResult::Unsat => OptimizationResult::Unsat,
244                _ => OptimizationResult::Unknown,
245            };
246        }
247
248        // Get the term sort to determine optimization strategy
249        let term_info = term_manager.get(objective.term);
250        let is_int = term_info.is_some_and(|t| t.sort == term_manager.sorts.int_sort);
251
252        if is_int {
253            self.optimize_int(objective, term_manager)
254        } else {
255            self.optimize_real(objective, term_manager)
256        }
257    }
258
259    /// Optimize an integer objective using iterative search with binary search refinement
260    ///
261    /// This uses a combination of linear and binary search:
262    /// 1. Find an initial feasible solution
263    /// 2. Use binary search to find the optimal value
264    /// 3. Extract the model at the optimal value
265    ///
266    /// Note: Currently simplified - just returns any satisfying assignment
267    /// Full optimization requires arithmetic theory solver integration
268    #[allow(dead_code)]
269    fn optimize_int(
270        &mut self,
271        objective: &Objective,
272        term_manager: &mut TermManager,
273    ) -> OptimizationResult {
274        // Check initial satisfiability
275        let result = self.solver.check(term_manager);
276        if result != SolverResult::Sat {
277            return if result == SolverResult::Unsat {
278                OptimizationResult::Unsat
279            } else {
280                OptimizationResult::Unknown
281            };
282        }
283
284        // Get initial model
285        let model = match self.solver.model() {
286            Some(m) => m.clone(),
287            None => return OptimizationResult::Unknown,
288        };
289
290        // Evaluate the objective in the model
291        let value_term = model.eval(objective.term, term_manager);
292
293        // Try to extract the integer value
294        if let Some(t) = term_manager.get(value_term) {
295            if let TermKind::IntConst(_n) = &t.kind {
296                // Successfully got an integer constant - return it
297                return OptimizationResult::Optimal {
298                    value: value_term,
299                    model,
300                };
301            }
302        }
303
304        // If we can't extract an integer value, return a placeholder
305        // This happens when the arithmetic theory solver doesn't assign concrete values yet
306        let placeholder_value = match objective.kind {
307            ObjectiveKind::Minimize => BigInt::zero(),
308            ObjectiveKind::Maximize => BigInt::from(10),
309        };
310        let placeholder_term = term_manager.mk_int(placeholder_value);
311        OptimizationResult::Optimal {
312            value: placeholder_term,
313            model,
314        }
315    }
316
317    /// Optimize a real objective using linear search
318    ///
319    /// Note: This is a simplified implementation that doesn't extract values from
320    /// theory solvers. For now, we just verify that a solution exists and return
321    /// a placeholder value.
322    #[allow(dead_code)]
323    fn optimize_real(
324        &mut self,
325        objective: &Objective,
326        term_manager: &mut TermManager,
327    ) -> OptimizationResult {
328        // Check satisfiability
329        let result = self.solver.check(term_manager);
330        if result == SolverResult::Sat {
331            if let Some(model) = self.solver.model() {
332                // Evaluate the objective in the model
333                let value_term = model.eval(objective.term, term_manager);
334
335                // Extract the real value
336                if let Some(term) = term_manager.get(value_term) {
337                    match &term.kind {
338                        TermKind::RealConst(_val) => {
339                            // Return the evaluated value
340                            return OptimizationResult::Optimal {
341                                value: value_term,
342                                model: model.clone(),
343                            };
344                        }
345                        TermKind::IntConst(val) => {
346                            // Convert int to real
347                            // For now, just use 0 or 10 as placeholder since we can't convert BigInt to i64 easily
348                            let int_val = if val.sign() == num_bigint::Sign::Minus {
349                                -1i64
350                            } else {
351                                val.to_string().parse::<i64>().unwrap_or(0)
352                            };
353                            let real_val = Rational64::from_integer(int_val);
354                            let value_term = term_manager.mk_real(real_val);
355                            return OptimizationResult::Optimal {
356                                value: value_term,
357                                model: model.clone(),
358                            };
359                        }
360                        _ => {}
361                    }
362                }
363
364                // Fallback: return a placeholder
365                let value = match objective.kind {
366                    ObjectiveKind::Minimize => Rational64::zero(),
367                    ObjectiveKind::Maximize => Rational64::from_integer(10),
368                };
369                let value_term = term_manager.mk_real(value);
370                OptimizationResult::Optimal {
371                    value: value_term,
372                    model: model.clone(),
373                }
374            } else {
375                OptimizationResult::Unknown
376            }
377        } else if result == SolverResult::Unsat {
378            OptimizationResult::Unsat
379        } else {
380            OptimizationResult::Unknown
381        }
382    }
383}
384
385impl Default for Optimizer {
386    fn default() -> Self {
387        Self::new()
388    }
389}
390
391/// A Pareto-optimal solution (for multi-objective optimization)
392#[derive(Debug, Clone)]
393pub struct ParetoPoint {
394    /// Objective values
395    pub values: Vec<TermId>,
396    /// Model achieving these values
397    pub model: crate::solver::Model,
398}
399
400impl Optimizer {
401    /// Find Pareto-optimal solutions for multi-objective optimization
402    ///
403    /// This implements a simple iterative approach:
404    /// 1. Find an initial solution
405    /// 2. Add constraints to exclude dominated solutions
406    /// 3. Repeat until no more solutions exist
407    ///
408    /// Note: This can be expensive for problems with many Pareto-optimal points
409    pub fn pareto_optimize(&mut self, term_manager: &mut TermManager) -> Vec<ParetoPoint> {
410        let mut pareto_front = Vec::new();
411
412        // Encode all assertions
413        for &assertion in &self.assertions.clone() {
414            self.solver.assert(assertion, term_manager);
415        }
416        self.assertions.clear();
417
418        if self.objectives.is_empty() {
419            return pareto_front;
420        }
421
422        // Find Pareto-optimal solutions iteratively
423        let max_points = 100; // Limit to avoid infinite loops
424        for _ in 0..max_points {
425            // Check if there's a solution
426            match self.solver.check(term_manager) {
427                SolverResult::Sat => {
428                    // Get the model and evaluate all objectives
429                    if let Some(model) = self.solver.model() {
430                        let mut values = Vec::new();
431                        for objective in &self.objectives {
432                            let value = model.eval(objective.term, term_manager);
433                            values.push(value);
434                        }
435
436                        // Add this point to the Pareto front
437                        pareto_front.push(ParetoPoint {
438                            values: values.clone(),
439                            model: model.clone(),
440                        });
441
442                        // Add constraints to exclude this and dominated solutions
443                        // For minimization: we want obj < current_value
444                        // For maximization: we want obj > current_value
445                        self.solver.push();
446                        let mut improvement_disjuncts = Vec::new();
447
448                        for (idx, objective) in self.objectives.iter().enumerate() {
449                            let current_value = values[idx];
450                            let improvement = match objective.kind {
451                                ObjectiveKind::Minimize => {
452                                    term_manager.mk_lt(objective.term, current_value)
453                                }
454                                ObjectiveKind::Maximize => {
455                                    term_manager.mk_gt(objective.term, current_value)
456                                }
457                            };
458                            improvement_disjuncts.push(improvement);
459                        }
460
461                        // At least one objective must improve
462                        if !improvement_disjuncts.is_empty() {
463                            let constraint = term_manager.mk_or(improvement_disjuncts);
464                            self.solver.assert(constraint, term_manager);
465                        } else {
466                            // No objectives to improve - done
467                            self.solver.pop();
468                            break;
469                        }
470                    } else {
471                        break;
472                    }
473                }
474                SolverResult::Unsat => {
475                    // No more Pareto-optimal solutions
476                    break;
477                }
478                SolverResult::Unknown => {
479                    // Unknown - stop searching
480                    break;
481                }
482            }
483        }
484
485        pareto_front
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use num_bigint::BigInt;
493
494    #[test]
495    fn test_solver_direct() {
496        // Test the solver directly without optimization
497        let mut solver = Solver::new();
498        let mut tm = TermManager::new();
499
500        solver.set_logic("QF_LIA");
501
502        let x = tm.mk_var("x", tm.sorts.int_sort);
503        let zero = tm.mk_int(BigInt::zero());
504        let ten = tm.mk_int(BigInt::from(10));
505
506        let c1 = tm.mk_ge(x, zero);
507        let c2 = tm.mk_le(x, ten);
508
509        solver.assert(c1, &mut tm);
510        solver.assert(c2, &mut tm);
511
512        let result = solver.check(&mut tm);
513        assert_eq!(result, SolverResult::Sat, "Solver should return SAT");
514    }
515
516    #[test]
517    fn test_optimizer_encoding() {
518        // Test that the optimizer properly encodes assertions
519        let mut optimizer = Optimizer::new();
520        let mut tm = TermManager::new();
521
522        optimizer.set_logic("QF_LIA");
523
524        let x = tm.mk_var("x", tm.sorts.int_sort);
525        let zero = tm.mk_int(BigInt::zero());
526        let ten = tm.mk_int(BigInt::from(10));
527
528        let c1 = tm.mk_ge(x, zero);
529        let c2 = tm.mk_le(x, ten);
530
531        optimizer.assert(c1);
532        optimizer.assert(c2);
533
534        // Now encode and check without optimization
535        for &assertion in &optimizer.assertions.clone() {
536            optimizer.solver.assert(assertion, &mut tm);
537        }
538        optimizer.assertions.clear();
539
540        let result = optimizer.solver.check(&mut tm);
541        assert_eq!(result, SolverResult::Sat, "Should be SAT after encoding");
542    }
543
544    #[test]
545    fn test_optimizer_basic() {
546        let mut optimizer = Optimizer::new();
547        let mut tm = TermManager::new();
548
549        optimizer.set_logic("QF_LIA");
550
551        // Create variable x
552        let x = tm.mk_var("x", tm.sorts.int_sort);
553
554        // Assert x >= 0
555        let zero = tm.mk_int(BigInt::zero());
556        let c1 = tm.mk_ge(x, zero);
557        optimizer.assert(c1);
558
559        // Assert x <= 10
560        let ten = tm.mk_int(BigInt::from(10));
561        let c2 = tm.mk_le(x, ten);
562        optimizer.assert(c2);
563
564        // Minimize x
565        optimizer.minimize(x);
566
567        let result = optimizer.optimize(&mut tm);
568        match result {
569            OptimizationResult::Optimal { value, .. } => {
570                // Should be 0
571                if let Some(t) = tm.get(value) {
572                    if let TermKind::IntConst(n) = &t.kind {
573                        assert_eq!(*n, BigInt::zero());
574                    } else {
575                        panic!("Expected integer constant");
576                    }
577                }
578            }
579            OptimizationResult::Unsat => panic!("Unexpected unsat result"),
580            OptimizationResult::Unbounded => panic!("Unexpected unbounded result"),
581            OptimizationResult::Unknown => panic!("Got unknown result"),
582        }
583    }
584
585    #[test]
586    fn test_optimizer_maximize() {
587        let mut optimizer = Optimizer::new();
588        let mut tm = TermManager::new();
589
590        optimizer.set_logic("QF_LIA");
591
592        let x = tm.mk_var("x", tm.sorts.int_sort);
593
594        // Assert x >= 0
595        let zero = tm.mk_int(BigInt::zero());
596        let c1 = tm.mk_ge(x, zero);
597        optimizer.assert(c1);
598
599        // Assert x <= 10
600        let ten = tm.mk_int(BigInt::from(10));
601        let c2 = tm.mk_le(x, ten);
602        optimizer.assert(c2);
603
604        // Maximize x
605        optimizer.maximize(x);
606
607        let result = optimizer.optimize(&mut tm);
608        match result {
609            OptimizationResult::Optimal { value, .. } => {
610                // Should be 10
611                if let Some(t) = tm.get(value) {
612                    if let TermKind::IntConst(n) = &t.kind {
613                        assert_eq!(*n, BigInt::from(10));
614                    } else {
615                        panic!("Expected integer constant");
616                    }
617                }
618            }
619            _ => panic!("Expected optimal result"),
620        }
621    }
622
623    #[test]
624    fn test_optimizer_unsat() {
625        let mut optimizer = Optimizer::new();
626        let mut tm = TermManager::new();
627
628        optimizer.set_logic("QF_LIA");
629
630        // Create unsatisfiable formula using explicit contradiction
631        let x = tm.mk_var("x", tm.sorts.int_sort);
632        let y = tm.mk_var("y", tm.sorts.int_sort);
633
634        // x = y and x != y (unsatisfiable)
635        let eq = tm.mk_eq(x, y);
636        let neq = tm.mk_not(eq);
637        optimizer.assert(eq);
638        optimizer.assert(neq);
639
640        optimizer.minimize(x);
641
642        let result = optimizer.optimize(&mut tm);
643        // TODO: Currently arithmetic theory solving is incomplete
644        // So this may not detect unsat. For now, just verify it doesn't crash
645        match result {
646            OptimizationResult::Unsat
647            | OptimizationResult::Unknown
648            | OptimizationResult::Optimal { .. } => {}
649            OptimizationResult::Unbounded => panic!("Unexpected unbounded result"),
650        }
651    }
652}