1use std::borrow::Cow;
2use std::time::Duration;
3
4use oximo_core::{
5 ConstraintId, Expr, ExprNode, IndexKey, IndexedVar, Model, ObjectiveSense, SocConstraintId,
6 VarId,
7};
8use rustc_hash::FxHashMap;
9
10use crate::status::{PrimalStatus, TerminationStatus};
11
12#[derive(Clone, Debug, Default)]
18pub struct SolutionPoint {
19 pub primal: FxHashMap<VarId, f64>,
20 pub objective: Option<f64>,
21}
22
23impl SolutionPoint {
24 pub fn value(&self, id: VarId) -> Option<f64> {
26 self.primal.get(&id).copied()
27 }
28
29 pub fn value_of(&self, expr: Expr<'_>) -> Option<f64> {
34 let arena = expr.arena.borrow();
35 match arena.get(expr.id) {
36 ExprNode::Var(v) => self.primal.get(v).copied(),
37 _ => None,
38 }
39 }
40
41 pub fn value_of_idx<V, K: Into<IndexKey>>(
46 &self,
47 var: &IndexedVar<'_, V>,
48 key: K,
49 ) -> Option<f64> {
50 var.get(key).and_then(|e| self.value_of(e))
51 }
52
53 pub fn values_of<'iv, 'a, V>(
58 &'iv self,
59 var: &'iv IndexedVar<'a, V>,
60 ) -> impl Iterator<Item = (&'iv IndexKey, f64)> + 'iv {
61 var.iter().filter_map(|(k, e)| self.value_of(*e).map(|v| (k, v)))
62 }
63}
64
65#[derive(Clone, Debug)]
75pub struct SolverResult {
76 pub termination: TerminationStatus,
77 pub primal_status: PrimalStatus,
78 pub solutions: Vec<SolutionPoint>,
79 pub dual: FxHashMap<ConstraintId, f64>,
80 pub soc_dual: FxHashMap<SocConstraintId, f64>,
81 pub reduced_costs: FxHashMap<VarId, f64>,
82 pub best_bound: Option<f64>,
83 pub gap: Option<f64>,
84 pub solve_time: Duration,
85 pub iterations: u64,
86 pub raw_log: Option<String>,
87 pub solver_name: Option<Cow<'static, str>>,
88}
89
90impl Default for SolverResult {
91 fn default() -> Self {
92 Self {
93 termination: TerminationStatus::NotSolved,
94 primal_status: PrimalStatus::NoSolution,
95 solutions: Vec::new(),
96 dual: FxHashMap::default(),
97 soc_dual: FxHashMap::default(),
98 reduced_costs: FxHashMap::default(),
99 best_bound: None,
100 gap: None,
101 solve_time: Duration::ZERO,
102 iterations: 0,
103 raw_log: None,
104 solver_name: None,
105 }
106 }
107}
108
109impl SolverResult {
110 pub fn result_count(&self) -> usize {
113 self.solutions.len()
114 }
115
116 pub fn solution(&self, i: usize) -> Option<&SolutionPoint> {
118 self.solutions.get(i)
119 }
120
121 pub fn best(&self) -> Option<&SolutionPoint> {
123 self.solutions.first()
124 }
125
126 pub fn has_solution(&self) -> bool {
130 self.primal_status.has_solution()
131 }
132
133 pub fn objective(&self) -> Option<f64> {
135 self.solutions.first().and_then(|s| s.objective)
136 }
137
138 pub fn primal(&self) -> Option<&FxHashMap<VarId, f64>> {
140 self.solutions.first().map(|s| &s.primal)
141 }
142
143 pub fn value(&self, id: VarId) -> Option<f64> {
145 self.solutions.first().and_then(|s| s.value(id))
146 }
147
148 pub fn value_of(&self, expr: Expr<'_>) -> Option<f64> {
152 self.solutions.first().and_then(|s| s.value_of(expr))
153 }
154
155 pub fn dual_of(&self, c: ConstraintId) -> Option<f64> {
156 self.dual.get(&c).copied()
157 }
158
159 pub fn soc_dual_of(&self, c: SocConstraintId) -> Option<f64> {
162 self.soc_dual.get(&c).copied()
163 }
164
165 pub fn value_of_idx<V, K: Into<IndexKey>>(
168 &self,
169 var: &IndexedVar<'_, V>,
170 key: K,
171 ) -> Option<f64> {
172 var.get(key).and_then(|e| self.value_of(e))
173 }
174
175 pub fn values_of<'iv, 'a, V>(
178 &'iv self,
179 var: &'iv IndexedVar<'a, V>,
180 ) -> impl Iterator<Item = (&'iv IndexKey, f64)> + 'iv {
181 var.iter().filter_map(|(k, e)| self.value_of(*e).map(|v| (k, v)))
182 }
183
184 pub fn report<'a>(&'a self, model: &'a Model) -> ModelReport<'a> {
191 ModelReport { result: self, model }
192 }
193}
194
195#[derive(Debug)]
198pub struct ModelReport<'a> {
199 result: &'a SolverResult,
200 model: &'a Model,
201}
202
203fn num(x: f64) -> String {
206 let s = format!("{x:.6}");
207 let trimmed = s.trim_end_matches('0').trim_end_matches('.');
208 if trimmed.is_empty() || trimmed == "-0" { "0".to_owned() } else { trimmed.to_owned() }
209}
210
211impl std::fmt::Display for ModelReport<'_> {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 let r = self.result;
214 let m = self.model;
215
216 let sense = {
217 let obj = m.objective();
218 match obj.as_ref().map(|o| o.sense) {
219 Some(ObjectiveSense::Minimize) => "minimize",
220 Some(ObjectiveSense::Maximize) => "maximize",
221 None => "no objective",
222 }
223 };
224
225 writeln!(f, "solution summary")?;
226 writeln!(f, " solver : {}", r.solver_name.as_deref().unwrap_or("(unknown)"))?;
227 writeln!(f, " model : {} ({:?}, {sense})", m.name, m.kind())?;
228 writeln!(f, " termination: {:?}", r.termination)?;
229 writeln!(f, " primal : {:?}", r.primal_status)?;
230 writeln!(f, " solutions : {}", r.result_count())?;
231 match r.objective() {
232 Some(v) => writeln!(f, " objective : {}", num(v))?,
233 None => writeln!(f, " objective : (none)")?,
234 }
235 if let Some(b) = r.best_bound {
236 writeln!(f, " best bound : {}", num(b))?;
237 }
238 if let Some(g) = r.gap {
239 writeln!(f, " gap : {}", num(g))?;
240 }
241 writeln!(f, " solve time : {:?}", r.solve_time)?;
242 writeln!(f, " iterations : {}", r.iterations)?;
243
244 let vars = m.variables();
246 writeln!(f, "\nvariables ({})", vars.len())?;
247 if let Some(best) = r.best() {
248 let width = vars.iter().map(|v| v.name.len()).max().unwrap_or(0);
249 let show_rc = !r.reduced_costs.is_empty();
250 for v in vars.iter() {
251 let val = best.value(v.id).map_or_else(|| "n/a".to_owned(), num);
252 match (show_rc, r.reduced_costs.get(&v.id)) {
253 (true, Some(rc)) => {
254 writeln!(f, " {:<width$} = {val} (reduced cost {})", v.name, num(*rc))?;
255 }
256 _ => writeln!(f, " {:<width$} = {val}", v.name)?,
257 }
258 }
259 } else {
260 writeln!(f, " (no primal solution)")?;
261 }
262
263 if !r.dual.is_empty() {
265 let cons = m.constraints();
266 writeln!(f, "\nconstraints ({})", cons.len())?;
267 let width = cons.iter().map(|c| c.name.len()).max().unwrap_or(0);
268 for (i, c) in cons.iter().enumerate() {
269 let id = ConstraintId(u32::try_from(i).expect("constraint index fits u32"));
270 let d = r.dual_of(id).map_or_else(|| "n/a".to_owned(), num);
271 writeln!(f, " {:<width$} dual = {d}", c.name)?;
272 }
273 }
274
275 if !r.soc_dual.is_empty() {
277 let socs = m.soc_constraints();
278 writeln!(f, "\nsoc constraints ({})", socs.len())?;
279 let width = socs.iter().map(|s| s.name.len()).max().unwrap_or(0);
280 for (i, s) in socs.iter().enumerate() {
281 let id = SocConstraintId(u32::try_from(i).expect("soc index fits u32"));
282 let d = r.soc_dual_of(id).map_or_else(|| "n/a".to_owned(), num);
283 writeln!(f, " {:<width$} dual = {d}", s.name)?;
284 }
285 }
286
287 Ok(())
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn empty_result_has_no_solution() {
297 let r = SolverResult::default();
298 assert_eq!(r.result_count(), 0);
299 assert!(r.best().is_none());
300 assert!(r.objective().is_none());
301 assert!(r.primal().is_none());
302 assert!(r.value(VarId(0)).is_none());
303 assert!(r.solution(0).is_none());
304 }
305
306 #[test]
307 fn best_is_solution_zero() {
308 let mut p0 = FxHashMap::default();
309 p0.insert(VarId(0), 1.5);
310 let mut p1 = FxHashMap::default();
311 p1.insert(VarId(0), 2.5);
312 let r = SolverResult {
313 termination: TerminationStatus::Optimal,
314 primal_status: PrimalStatus::OptimalPoint,
315 solutions: vec![
316 SolutionPoint { primal: p0, objective: Some(10.0) },
317 SolutionPoint { primal: p1, objective: Some(9.0) },
318 ],
319 ..Default::default()
320 };
321 assert_eq!(r.result_count(), 2);
322 assert_eq!(r.objective(), Some(10.0));
323 assert_eq!(r.value(VarId(0)), Some(1.5));
324 assert_eq!(r.solution(1).unwrap().value(VarId(0)), Some(2.5));
325 }
326
327 #[test]
328 fn report_renders_sections() {
329 use oximo_core::{constraint, objective, variable};
330
331 let m = Model::new("toy");
332 variable!(m, x >= 0.0);
333 let c = constraint!(m, c, x <= 5.0);
334 objective!(m, Max, x);
335
336 let mut primal = FxHashMap::default();
337 primal.insert(x.var_id().unwrap(), 5.0);
338 let mut dual = FxHashMap::default();
339 dual.insert(c, 1.0);
340
341 let r = SolverResult {
342 termination: TerminationStatus::Optimal,
343 primal_status: PrimalStatus::OptimalPoint,
344 solutions: vec![SolutionPoint { primal, objective: Some(5.0) }],
345 dual,
346 solver_name: Some("TestSolver".into()),
347 ..Default::default()
348 };
349
350 let out = r.report(&m).to_string();
351 assert!(out.contains("solver : TestSolver"), "{out}");
352 assert!(out.contains("termination: Optimal"), "{out}");
353 assert!(out.contains("primal : OptimalPoint"), "{out}");
354 assert!(out.contains("objective : 5"), "{out}");
355 assert!(out.contains("(LP, maximize)"), "{out}");
356 assert!(out.contains("x = 5"), "{out}");
357 assert!(out.contains("dual = 1"), "{out}");
358 }
359}