Skip to main content

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