Skip to main content

oximo_core/
display.rs

1//! Human-readable rendering of models, constraints, objectives, and expressions.
2//!
3//! Everything here is a lazy adapter holding `&Model`. The model's `RefCell`s
4//! are borrowed at format time, so do not format one while holding a mutable
5//! borrow of the arena or registries.
6
7use std::fmt;
8
9use oximo_expr::{Expr, ExprId, render_expr};
10
11use crate::constraint::{ConstraintId, Sense};
12use crate::domain::Domain;
13use crate::indicator::IndicatorConstraintId;
14use crate::model::Model;
15use crate::soc::SocConstraintId;
16use crate::sos::SosConstraintId;
17use crate::var::{Variable, var_name};
18
19/// Compact `f64` rendering (shortest round-trip).
20fn fmt_num(v: f64) -> String {
21    if v == 0.0 { "0".to_string() } else { format!("{v}") }
22}
23
24/// Displays one expression as infix algebra, e.g. `3 x + 4 y - z`.
25/// Built by [`Model::display_expr`]/[`Model::display_expr_id`].
26#[derive(Debug)]
27pub struct ExprDisplay<'a> {
28    model: &'a Model,
29    id: ExprId,
30}
31
32impl fmt::Display for ExprDisplay<'_> {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        let arena = self.model.arena.borrow();
35        let vars = self.model.variables.borrow();
36        f.write_str(&render_expr(&arena, self.id, &|v| var_name(&vars, v)))
37    }
38}
39
40/// Displays one constraint as `name: lhs <= rhs` (or a range / equality).
41/// Built by [`Model::display_constraint`].
42#[derive(Debug)]
43pub struct ConstraintDisplay<'a> {
44    model: &'a Model,
45    id: ConstraintId,
46}
47
48impl fmt::Display for ConstraintDisplay<'_> {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        let arena = self.model.arena.borrow();
51        let vars = self.model.variables.borrow();
52        let constraints = self.model.constraints.borrow();
53        let c = &constraints[self.id.index()];
54        let resolve = |v| var_name(&vars, v);
55        let expr = render_expr(&arena, c.lhs, &resolve);
56        write!(f, "{}: ", c.name)?;
57        match c.as_single() {
58            Some((sense, rhs)) => write!(f, "{expr} {sense} {}", fmt_num(rhs))?,
59            None if c.is_range() => {
60                write!(
61                    f,
62                    "{} {} {expr} {} {}",
63                    fmt_num(c.lower),
64                    Sense::Le,
65                    Sense::Le,
66                    fmt_num(c.upper)
67                )?;
68            }
69            None => write!(f, "{expr} free")?,
70        }
71        if !c.active {
72            f.write_str(" (inactive)")?;
73        }
74        Ok(())
75    }
76}
77
78/// Displays the objective as `minimize <expr>`/`maximize <expr>`/`feasibility`.
79/// Built by [`Model::display_objective`].
80#[derive(Debug)]
81pub struct ObjectiveDisplay<'a> {
82    model: &'a Model,
83}
84
85impl fmt::Display for ObjectiveDisplay<'_> {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        if self.model.is_feasibility() {
88            return f.write_str("feasibility");
89        }
90        // Copy `(sense, expr)` out so the objective borrow ends before the
91        // expression rendering takes its own borrows.
92        let obj = self.model.objective.borrow().as_ref().map(|o| (o.sense, o.expr));
93        let Some((sense, expr)) = obj else {
94            return f.write_str("(no objective)");
95        };
96        write!(f, "{sense} {}", ExprDisplay { model: self.model, id: expr })
97    }
98}
99
100/// Displays one second-order cone constraint as `name: ||t1, t2|| <= bound`.
101/// Built by [`Model::display_soc`].
102#[derive(Debug)]
103pub struct SocDisplay<'a> {
104    model: &'a Model,
105    id: SocConstraintId,
106}
107
108#[derive(Debug)]
109pub struct SosDisplay<'a> {
110    model: &'a Model,
111    id: SosConstraintId,
112}
113
114#[derive(Debug)]
115pub struct IndicatorDisplay<'a> {
116    model: &'a Model,
117    id: IndicatorConstraintId,
118}
119
120impl fmt::Display for IndicatorDisplay<'_> {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        let arena = self.model.arena.borrow();
123        let vars = self.model.variables.borrow();
124        let indicators = self.model.indicator_constraints.borrow();
125        let c = &indicators[self.id.index()];
126        let resolve = |v| var_name(&vars, v);
127        let expr = render_expr(&arena, c.lhs, &resolve);
128        write!(f, "{}: {} = {} -> ", c.name, resolve(c.trigger), u8::from(c.active_value))?;
129        match c.as_single() {
130            Some((sense, rhs)) => write!(f, "{expr} {sense} {}", fmt_num(rhs))?,
131            None if c.is_range() => {
132                write!(f, "{} <= {expr} <= {}", fmt_num(c.lower), fmt_num(c.upper))?;
133            }
134            None => write!(f, "{expr} free")?,
135        }
136        if !c.active {
137            f.write_str(" (inactive)")?;
138        }
139        Ok(())
140    }
141}
142
143impl fmt::Display for SosDisplay<'_> {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        let vars = self.model.variables.borrow();
146        let sos = self.model.sos_constraints.borrow();
147        let c = &sos[self.id.index()];
148        write!(f, "{}: {} [", c.name, c.sos_type)?;
149        for (i, member) in c.members.iter().enumerate() {
150            if i > 0 {
151                f.write_str(", ")?;
152            }
153            write!(f, "{}: {}", var_name(&vars, member.variable), fmt_num(member.weight))?;
154        }
155        f.write_str("]")?;
156        if !c.active {
157            f.write_str(" (inactive)")?;
158        }
159        Ok(())
160    }
161}
162
163impl fmt::Display for SocDisplay<'_> {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let arena = self.model.arena.borrow();
166        let vars = self.model.variables.borrow();
167        let socs = self.model.soc_constraints.borrow();
168        let c = &socs[self.id.index()];
169        let resolve = |v| var_name(&vars, v);
170        write!(f, "{}: ||", c.name)?;
171        for (i, t) in c.terms.iter().enumerate() {
172            if i > 0 {
173                f.write_str(", ")?;
174            }
175            f.write_str(&render_expr(&arena, *t, &resolve))?;
176        }
177        write!(f, "|| {} {}", Sense::Le, render_expr(&arena, c.bound, &resolve))?;
178        if !c.active {
179            f.write_str(" (inactive)")?;
180        }
181        Ok(())
182    }
183}
184
185/// One `vars` section line: bounds relation plus a domain suffix,
186/// e.g. `0 <= y <= 1, binary`.
187fn write_var_line(f: &mut fmt::Formatter<'_>, v: &Variable) -> fmt::Result {
188    match (v.lb.is_finite(), v.ub.is_finite()) {
189        (true, true) if v.lb.total_cmp(&v.ub).is_eq() => {
190            write!(f, "{} = {}", v.name, fmt_num(v.lb))?;
191        }
192        (true, true) => write!(f, "{} <= {} <= {}", fmt_num(v.lb), v.name, fmt_num(v.ub))?,
193        (true, false) => write!(f, "{} >= {}", v.name, fmt_num(v.lb))?,
194        (false, true) => write!(f, "{} <= {}", v.name, fmt_num(v.ub))?,
195        (false, false) => write!(f, "{} free", v.name)?,
196    }
197    if v.domain == Domain::Real { Ok(()) } else { write!(f, ", {}", v.domain) }
198}
199
200impl Model {
201    /// Display adapter for an expression handle, resolving variable names
202    /// against this model.
203    #[must_use]
204    pub fn display_expr(&self, e: Expr<'_>) -> ExprDisplay<'_> {
205        self.display_expr_id(e.id)
206    }
207
208    /// Display adapter for a raw [`ExprId`] (as stored in constraints and the
209    /// objective).
210    #[must_use]
211    pub fn display_expr_id(&self, id: ExprId) -> ExprDisplay<'_> {
212        ExprDisplay { model: self, id }
213    }
214
215    /// Display adapter for one algebraic constraint.
216    #[must_use]
217    pub fn display_constraint(&self, id: ConstraintId) -> ConstraintDisplay<'_> {
218        ConstraintDisplay { model: self, id }
219    }
220
221    /// Display adapter for the objective (`minimize <expr>`/`maximize <expr>`/
222    /// `feasibility`/`(no objective)`).
223    #[must_use]
224    pub fn display_objective(&self) -> ObjectiveDisplay<'_> {
225        ObjectiveDisplay { model: self }
226    }
227
228    /// Display adapter for one second-order cone constraint.
229    #[must_use]
230    pub fn display_soc(&self, id: SocConstraintId) -> SocDisplay<'_> {
231        SocDisplay { model: self, id }
232    }
233
234    #[must_use]
235    pub fn display_sos(&self, id: impl Into<SosConstraintId>) -> SosDisplay<'_> {
236        SosDisplay { model: self, id: id.into() }
237    }
238
239    #[must_use]
240    pub fn display_indicator(&self, id: impl Into<IndicatorConstraintId>) -> IndicatorDisplay<'_> {
241        IndicatorDisplay { model: self, id: id.into() }
242    }
243}
244
245/// Pretty-print the whole model as readable algebra:
246///
247/// ```text
248/// Model 'diet' (LP)
249/// minimize 3 x + 4 y
250/// s.t.
251///   c1: x + 2 y <= 14
252///   c2: 3 x - y >= 0
253/// vars
254///   x >= 0
255///   y >= 0
256/// ```
257///
258/// Sections (`s.t.`, `vars`, `params`) are omitted when empty. The objective
259/// line is omitted when no objective was declared.
260impl fmt::Display for Model {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        writeln!(f, "Model '{}' ({})", self.name, self.kind())?;
263        if self.is_feasibility() || self.objective.borrow().is_some() {
264            writeln!(f, "{}", self.display_objective())?;
265        }
266        let n_constraints = self.constraints.borrow().len();
267        let n_socs = self.soc_constraints.borrow().len();
268        let n_sos = self.sos_constraints.borrow().len();
269        let n_indicators = self.indicator_constraints.borrow().len();
270        if n_constraints + n_socs + n_sos + n_indicators > 0 {
271            let n_constraints = u32::try_from(n_constraints).expect("constraint count fits u32");
272            let n_socs = u32::try_from(n_socs).expect("soc count fits u32");
273            let n_sos = u32::try_from(n_sos).expect("sos count fits u32");
274            let n_indicators = u32::try_from(n_indicators).expect("indicator count fits u32");
275            writeln!(f, "s.t.")?;
276            for i in 0..n_constraints {
277                writeln!(f, "  {}", self.display_constraint(ConstraintId(i)))?;
278            }
279            for i in 0..n_socs {
280                writeln!(f, "  {}", self.display_soc(SocConstraintId(i)))?;
281            }
282            for i in 0..n_sos {
283                writeln!(f, "  {}", self.display_sos(SosConstraintId(i)))?;
284            }
285            for i in 0..n_indicators {
286                writeln!(f, "  {}", self.display_indicator(IndicatorConstraintId(i)))?;
287            }
288        }
289        {
290            let vars = self.variables.borrow();
291            if !vars.is_empty() {
292                writeln!(f, "vars")?;
293                for v in vars.iter() {
294                    f.write_str("  ")?;
295                    write_var_line(f, v)?;
296                    writeln!(f)?;
297                }
298            }
299        }
300        let params = self.parameters.borrow();
301        if !params.is_empty() {
302            let arena = self.arena.borrow();
303            writeln!(f, "params")?;
304            for p in params.iter() {
305                writeln!(f, "  {} = {}", p.name, fmt_num(arena.param_value(p.id)))?;
306            }
307        }
308        Ok(())
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::constraint::Relate;
316    use crate::sos::SosType;
317
318    #[test]
319    fn full_model_snapshot() {
320        let m = Model::new("diet");
321        let x = m.__var("x").lb(0.0).build();
322        let y = m.__var("y").lb(0.0).build();
323        m.__add_constraint("c1", (x + 2.0 * y).le(14.0));
324        m.__add_constraint("c2", (3.0 * x - y).ge(0.0));
325        m.__minimize(3.0 * x + 4.0 * y);
326
327        let expected = "Model 'diet' (LP)\n\
328                        minimize 3 x + 4 y\n\
329                        s.t.\n\
330                        \x20 c1: x + 2 y <= 14\n\
331                        \x20 c2: 3 x - y >= 0\n\
332                        vars\n\
333                        \x20 x >= 0\n\
334                        \x20 y >= 0\n";
335        assert_eq!(format!("{m}"), expected);
336    }
337
338    #[test]
339    fn constraint_variants_render() {
340        let m = Model::new("t");
341        let x = m.__var("x").build();
342        let eq = m.__add_constraint("e", x.eq(5.0));
343        assert_eq!(m.display_constraint(eq.id()).to_string(), "e: x = 5");
344
345        m.__add_range("r", x, 2.0, 10.0);
346        let r = m.constraint_id("r").unwrap();
347        assert_eq!(m.display_constraint(r).to_string(), "r: 2 <= x <= 10");
348
349        let ge = m.__add_constraint("g", x.ge(1.0));
350        m.constraints.borrow_mut()[ge.index()].active = false;
351        assert_eq!(m.display_constraint(ge.id()).to_string(), "g: x >= 1 (inactive)");
352    }
353
354    #[test]
355    fn objective_and_feasibility() {
356        let m = Model::new("t");
357        let x = m.__var("x").build();
358        assert_eq!(m.display_objective().to_string(), "(no objective)");
359        m.__maximize(x);
360        assert_eq!(m.display_objective().to_string(), "maximize x");
361
362        let f = Model::new("f");
363        f.__feasibility();
364        assert_eq!(f.display_objective().to_string(), "feasibility");
365        assert!(format!("{f}").contains("feasibility\n"));
366    }
367
368    #[test]
369    fn var_lines_cover_domains_and_bounds() {
370        let m = Model::new("t");
371        m.__var("a").bounds(0.0, 5.0).build();
372        m.__var("b").binary().build();
373        m.__var("c").integer().build();
374        m.__var("d").build();
375        m.__var("e").ub(3.0).build();
376        m.__var("g").fix(2.0).build();
377        m.__var("h").lb(0.0).domain(crate::Domain::SemiContinuous { threshold: 2.0 }).build();
378        m.__var("i").domain(crate::Domain::SemiInteger { threshold: 3.0 }).build();
379
380        let out = format!("{m}");
381        assert!(out.contains("  0 <= a <= 5\n"), "{out}");
382        assert!(out.contains("  0 <= b <= 1, binary\n"), "{out}");
383        assert!(out.contains("  c free, integer\n"), "{out}");
384        assert!(out.contains("  d free\n"), "{out}");
385        assert!(out.contains("  e <= 3\n"), "{out}");
386        assert!(out.contains("  g = 2\n"), "{out}");
387        assert!(out.contains("  h >= 0, semi-continuous(2)\n"), "{out}");
388        assert!(out.contains("  i free, semi-integer(3)\n"), "{out}");
389    }
390
391    #[test]
392    fn params_section_shows_current_values() {
393        let m = Model::new("t");
394        let x = m.__var("x").build();
395        let price = m.__param("price", 4.0);
396        m.__minimize(price * x);
397        let out = format!("{m}");
398        assert!(out.contains("minimize 4 x\n"), "{out}");
399        assert!(out.contains("params\n  price = 4\n"), "{out}");
400
401        price.set_param_value(7.5);
402        let out = format!("{m}");
403        assert!(out.contains("minimize 7.5 x\n"), "{out}");
404        assert!(out.contains("params\n  price = 7.5\n"), "{out}");
405    }
406
407    #[test]
408    fn soc_row_renders_in_model_display() {
409        let m = Model::new("t");
410        let x = m.__var("x").build();
411        let y = m.__var("y").build();
412        let t = m.__var("t").lb(0.0).build();
413        let id = m.add_soc_constraint("q1", [x, y], t);
414        assert_eq!(m.display_soc(id.id()).to_string(), "q1: ||x, y|| <= t");
415        assert!(format!("{m}").contains("  q1: ||x, y|| <= t\n"));
416    }
417
418    #[test]
419    fn sos_row_renders_after_soc_in_model_display() {
420        let m = Model::new("t");
421        let x = m.__var("x").build();
422        let y = m.__var("y").build();
423        let t = m.__var("t").lb(0.0).build();
424        m.add_soc_constraint("q1", [x, y], t);
425        let id = m.add_sos_constraint("choice", SosType::Sos1, [(x, 1.0), (y, 2.5)]);
426        m.__minimize(x + y);
427
428        assert_eq!(m.display_sos(id).to_string(), "choice: SOS1 [x: 1, y: 2.5]");
429        let out = format!("{m}");
430        let soc_pos = out.find("  q1: ||x, y|| <= t\n").expect("SOC block");
431        let sos_pos = out.find("  choice: SOS1 [x: 1, y: 2.5]\n").expect("SOS block");
432        assert!(soc_pos < sos_pos, "SOC must render before SOS:\n{out}");
433    }
434
435    #[test]
436    fn display_expr_renders_nonlinear() {
437        let m = Model::new("t");
438        let x = m.__var("x").build();
439        let y = m.__var("y").build();
440        let e = x * y - y;
441        assert_eq!(m.display_expr(e).to_string(), "-y + x * y");
442    }
443}