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