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 linear search with iterative tightening
260    ///
261    /// This uses linear search:
262    /// 1. Find an initial feasible solution
263    /// 2. Add a constraint to improve the objective
264    /// 3. Repeat until UNSAT (no better solution exists)
265    /// 4. Return the last satisfying value
266    fn optimize_int(
267        &mut self,
268        objective: &Objective,
269        term_manager: &mut TermManager,
270    ) -> OptimizationResult {
271        // Check initial satisfiability
272        let result = self.solver.check(term_manager);
273        if result != SolverResult::Sat {
274            return if result == SolverResult::Unsat {
275                OptimizationResult::Unsat
276            } else {
277                OptimizationResult::Unknown
278            };
279        }
280
281        // Get initial model
282        let mut best_model = match self.solver.model() {
283            Some(m) => m.clone(),
284            None => return OptimizationResult::Unknown,
285        };
286
287        // Evaluate the objective in the model to get initial value
288        let value_term = best_model.eval(objective.term, term_manager);
289
290        // Try to extract the integer value
291        let mut current_value = if let Some(t) = term_manager.get(value_term) {
292            if let TermKind::IntConst(n) = &t.kind {
293                n.clone()
294            } else {
295                // Can't extract value, return as-is
296                return OptimizationResult::Optimal {
297                    value: value_term,
298                    model: best_model,
299                };
300            }
301        } else {
302            return OptimizationResult::Unknown;
303        };
304
305        let mut best_value_term = value_term;
306
307        // Linear search: iteratively tighten the bound
308        let max_iterations = 1000; // Prevent infinite loops
309        for _ in 0..max_iterations {
310            // Push a new scope for the improvement constraint
311            self.solver.push();
312
313            // Add constraint to improve the objective
314            let bound_term = term_manager.mk_int(current_value.clone());
315            let improvement_constraint = match objective.kind {
316                ObjectiveKind::Minimize => {
317                    // For minimization: objective < current_value
318                    term_manager.mk_lt(objective.term, bound_term)
319                }
320                ObjectiveKind::Maximize => {
321                    // For maximization: objective > current_value
322                    term_manager.mk_gt(objective.term, bound_term)
323                }
324            };
325            self.solver.assert(improvement_constraint, term_manager);
326
327            // Check if there's a better solution
328            let result = self.solver.check(term_manager);
329            if result == SolverResult::Sat {
330                // Found a better solution
331                if let Some(model) = self.solver.model() {
332                    let new_value_term = model.eval(objective.term, term_manager);
333
334                    if let Some(t) = term_manager.get(new_value_term)
335                        && let TermKind::IntConst(n) = &t.kind
336                    {
337                        current_value = n.clone();
338                        best_value_term = new_value_term;
339                        best_model = model.clone();
340                    }
341                }
342                // Pop and continue searching
343                self.solver.pop();
344            } else {
345                // No better solution exists - current best is optimal
346                self.solver.pop();
347                break;
348            }
349        }
350
351        OptimizationResult::Optimal {
352            value: best_value_term,
353            model: best_model,
354        }
355    }
356
357    /// Optimize a real objective using linear search with iterative tightening
358    ///
359    /// Similar to integer optimization, but works with real (rational) values.
360    fn optimize_real(
361        &mut self,
362        objective: &Objective,
363        term_manager: &mut TermManager,
364    ) -> OptimizationResult {
365        // Check initial satisfiability
366        let result = self.solver.check(term_manager);
367        if result != SolverResult::Sat {
368            return if result == SolverResult::Unsat {
369                OptimizationResult::Unsat
370            } else {
371                OptimizationResult::Unknown
372            };
373        }
374
375        // Get initial model
376        let mut best_model = match self.solver.model() {
377            Some(m) => m.clone(),
378            None => return OptimizationResult::Unknown,
379        };
380
381        // Evaluate the objective in the model
382        let value_term = best_model.eval(objective.term, term_manager);
383
384        // Try to extract the value (real or int)
385        let mut current_value: Option<Rational64> = None;
386        if let Some(term) = term_manager.get(value_term) {
387            match &term.kind {
388                TermKind::RealConst(val) => {
389                    current_value = Some(*val);
390                }
391                TermKind::IntConst(val) => {
392                    // Convert BigInt to Rational64
393                    let int_val = if val.sign() == num_bigint::Sign::Minus {
394                        -val.to_string()
395                            .trim_start_matches('-')
396                            .parse::<i64>()
397                            .unwrap_or(0)
398                    } else {
399                        val.to_string().parse::<i64>().unwrap_or(0)
400                    };
401                    current_value = Some(Rational64::from_integer(int_val));
402                }
403                _ => {}
404            }
405        }
406
407        let Some(mut current_val) = current_value else {
408            // Can't extract value, return as-is
409            return OptimizationResult::Optimal {
410                value: value_term,
411                model: best_model,
412            };
413        };
414
415        let mut best_value = current_val;
416
417        // Linear search: iteratively tighten the bound
418        let max_iterations = 1000;
419        for _ in 0..max_iterations {
420            self.solver.push();
421
422            // Add constraint to improve the objective
423            let bound_term = term_manager.mk_real(current_val);
424            let improvement_constraint = match objective.kind {
425                ObjectiveKind::Minimize => term_manager.mk_lt(objective.term, bound_term),
426                ObjectiveKind::Maximize => term_manager.mk_gt(objective.term, bound_term),
427            };
428            self.solver.assert(improvement_constraint, term_manager);
429
430            let result = self.solver.check(term_manager);
431            if result == SolverResult::Sat {
432                if let Some(model) = self.solver.model() {
433                    let new_value_term = model.eval(objective.term, term_manager);
434
435                    if let Some(t) = term_manager.get(new_value_term) {
436                        let new_val = match &t.kind {
437                            TermKind::RealConst(v) => Some(*v),
438                            TermKind::IntConst(v) => {
439                                let int_val = if v.sign() == num_bigint::Sign::Minus {
440                                    -v.to_string()
441                                        .trim_start_matches('-')
442                                        .parse::<i64>()
443                                        .unwrap_or(0)
444                                } else {
445                                    v.to_string().parse::<i64>().unwrap_or(0)
446                                };
447                                Some(Rational64::from_integer(int_val))
448                            }
449                            _ => None,
450                        };
451
452                        if let Some(v) = new_val {
453                            current_val = v;
454                            best_value = v;
455                            best_model = model.clone();
456                        }
457                    }
458                }
459                self.solver.pop();
460            } else {
461                self.solver.pop();
462                break;
463            }
464        }
465
466        // Return best value as real term
467        let final_value_term = term_manager.mk_real(best_value);
468        OptimizationResult::Optimal {
469            value: final_value_term,
470            model: best_model,
471        }
472    }
473}
474
475impl Default for Optimizer {
476    fn default() -> Self {
477        Self::new()
478    }
479}
480
481/// A Pareto-optimal solution (for multi-objective optimization)
482#[derive(Debug, Clone)]
483pub struct ParetoPoint {
484    /// Objective values
485    pub values: Vec<TermId>,
486    /// Model achieving these values
487    pub model: crate::solver::Model,
488}
489
490impl Optimizer {
491    /// Find Pareto-optimal solutions for multi-objective optimization
492    ///
493    /// This implements a simple iterative approach:
494    /// 1. Find an initial solution
495    /// 2. Add constraints to exclude dominated solutions
496    /// 3. Repeat until no more solutions exist
497    ///
498    /// Note: This can be expensive for problems with many Pareto-optimal points
499    pub fn pareto_optimize(&mut self, term_manager: &mut TermManager) -> Vec<ParetoPoint> {
500        let mut pareto_front = Vec::new();
501
502        // Encode all assertions
503        for &assertion in &self.assertions.clone() {
504            self.solver.assert(assertion, term_manager);
505        }
506        self.assertions.clear();
507
508        if self.objectives.is_empty() {
509            return pareto_front;
510        }
511
512        // Find Pareto-optimal solutions iteratively
513        let max_points = 100; // Limit to avoid infinite loops
514        for _ in 0..max_points {
515            // Check if there's a solution
516            match self.solver.check(term_manager) {
517                SolverResult::Sat => {
518                    // Get the model and evaluate all objectives
519                    if let Some(model) = self.solver.model() {
520                        let mut values = Vec::new();
521                        for objective in &self.objectives {
522                            let value = model.eval(objective.term, term_manager);
523                            values.push(value);
524                        }
525
526                        // Add this point to the Pareto front
527                        pareto_front.push(ParetoPoint {
528                            values: values.clone(),
529                            model: model.clone(),
530                        });
531
532                        // Add constraints to exclude this and dominated solutions
533                        // For minimization: we want obj < current_value
534                        // For maximization: we want obj > current_value
535                        self.solver.push();
536                        let mut improvement_disjuncts = Vec::new();
537
538                        for (idx, objective) in self.objectives.iter().enumerate() {
539                            let current_value = values[idx];
540                            let improvement = match objective.kind {
541                                ObjectiveKind::Minimize => {
542                                    term_manager.mk_lt(objective.term, current_value)
543                                }
544                                ObjectiveKind::Maximize => {
545                                    term_manager.mk_gt(objective.term, current_value)
546                                }
547                            };
548                            improvement_disjuncts.push(improvement);
549                        }
550
551                        // At least one objective must improve
552                        if !improvement_disjuncts.is_empty() {
553                            let constraint = term_manager.mk_or(improvement_disjuncts);
554                            self.solver.assert(constraint, term_manager);
555                        } else {
556                            // No objectives to improve - done
557                            self.solver.pop();
558                            break;
559                        }
560                    } else {
561                        break;
562                    }
563                }
564                SolverResult::Unsat => {
565                    // No more Pareto-optimal solutions
566                    break;
567                }
568                SolverResult::Unknown => {
569                    // Unknown - stop searching
570                    break;
571                }
572            }
573        }
574
575        pareto_front
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use num_bigint::BigInt;
583
584    #[test]
585    fn test_solver_direct() {
586        // Test the solver directly without optimization
587        let mut solver = Solver::new();
588        let mut tm = TermManager::new();
589
590        solver.set_logic("QF_LIA");
591
592        let x = tm.mk_var("x", tm.sorts.int_sort);
593        let zero = tm.mk_int(BigInt::zero());
594        let ten = tm.mk_int(BigInt::from(10));
595
596        let c1 = tm.mk_ge(x, zero);
597        let c2 = tm.mk_le(x, ten);
598
599        solver.assert(c1, &mut tm);
600        solver.assert(c2, &mut tm);
601
602        let result = solver.check(&mut tm);
603        assert_eq!(result, SolverResult::Sat, "Solver should return SAT");
604    }
605
606    #[test]
607    fn test_optimizer_encoding() {
608        // Test that the optimizer properly encodes assertions
609        let mut optimizer = Optimizer::new();
610        let mut tm = TermManager::new();
611
612        optimizer.set_logic("QF_LIA");
613
614        let x = tm.mk_var("x", tm.sorts.int_sort);
615        let zero = tm.mk_int(BigInt::zero());
616        let ten = tm.mk_int(BigInt::from(10));
617
618        let c1 = tm.mk_ge(x, zero);
619        let c2 = tm.mk_le(x, ten);
620
621        optimizer.assert(c1);
622        optimizer.assert(c2);
623
624        // Now encode and check without optimization
625        for &assertion in &optimizer.assertions.clone() {
626            optimizer.solver.assert(assertion, &mut tm);
627        }
628        optimizer.assertions.clear();
629
630        let result = optimizer.solver.check(&mut tm);
631        assert_eq!(result, SolverResult::Sat, "Should be SAT after encoding");
632    }
633
634    #[test]
635    fn test_optimizer_basic() {
636        let mut optimizer = Optimizer::new();
637        let mut tm = TermManager::new();
638
639        optimizer.set_logic("QF_LIA");
640
641        // Create variable x
642        let x = tm.mk_var("x", tm.sorts.int_sort);
643
644        // Assert x >= 0
645        let zero = tm.mk_int(BigInt::zero());
646        let c1 = tm.mk_ge(x, zero);
647        optimizer.assert(c1);
648
649        // Assert x <= 10
650        let ten = tm.mk_int(BigInt::from(10));
651        let c2 = tm.mk_le(x, ten);
652        optimizer.assert(c2);
653
654        // Minimize x
655        optimizer.minimize(x);
656
657        let result = optimizer.optimize(&mut tm);
658        match result {
659            OptimizationResult::Optimal { value, .. } => {
660                // Should be 0
661                if let Some(t) = tm.get(value) {
662                    if let TermKind::IntConst(n) = &t.kind {
663                        assert_eq!(*n, BigInt::zero());
664                    } else {
665                        panic!("Expected integer constant");
666                    }
667                }
668            }
669            OptimizationResult::Unsat => panic!("Unexpected unsat result"),
670            OptimizationResult::Unbounded => panic!("Unexpected unbounded result"),
671            OptimizationResult::Unknown => panic!("Got unknown result"),
672        }
673    }
674
675    #[test]
676    fn test_optimizer_maximize() {
677        let mut optimizer = Optimizer::new();
678        let mut tm = TermManager::new();
679
680        optimizer.set_logic("QF_LIA");
681
682        let x = tm.mk_var("x", tm.sorts.int_sort);
683
684        // Assert x >= 0
685        let zero = tm.mk_int(BigInt::zero());
686        let c1 = tm.mk_ge(x, zero);
687        optimizer.assert(c1);
688
689        // Assert x <= 10
690        let ten = tm.mk_int(BigInt::from(10));
691        let c2 = tm.mk_le(x, ten);
692        optimizer.assert(c2);
693
694        // Maximize x
695        optimizer.maximize(x);
696
697        let result = optimizer.optimize(&mut tm);
698        match result {
699            OptimizationResult::Optimal { value, .. } => {
700                // Should be 10
701                if let Some(t) = tm.get(value) {
702                    if let TermKind::IntConst(n) = &t.kind {
703                        assert_eq!(*n, BigInt::from(10));
704                    } else {
705                        panic!("Expected integer constant");
706                    }
707                }
708            }
709            _ => panic!("Expected optimal result"),
710        }
711    }
712
713    #[test]
714    fn test_optimizer_unsat() {
715        let mut optimizer = Optimizer::new();
716        let mut tm = TermManager::new();
717
718        optimizer.set_logic("QF_LIA");
719
720        // Create unsatisfiable formula using explicit contradiction
721        let x = tm.mk_var("x", tm.sorts.int_sort);
722        let y = tm.mk_var("y", tm.sorts.int_sort);
723
724        // x = y and x != y (unsatisfiable)
725        let eq = tm.mk_eq(x, y);
726        let neq = tm.mk_not(eq);
727        optimizer.assert(eq);
728        optimizer.assert(neq);
729
730        optimizer.minimize(x);
731
732        let result = optimizer.optimize(&mut tm);
733        // TODO: Currently arithmetic theory solving is incomplete
734        // So this may not detect unsat. For now, just verify it doesn't crash
735        match result {
736            OptimizationResult::Unsat
737            | OptimizationResult::Unknown
738            | OptimizationResult::Optimal { .. } => {}
739            OptimizationResult::Unbounded => panic!("Unexpected unbounded result"),
740        }
741    }
742}