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}