1use std::fmt;
8
9use oximo_expr::{Expr, ExprId, render_expr};
10
11use crate::constraint::{ConstraintId, Sense};
12use crate::domain::Domain;
13use crate::model::Model;
14use crate::objective::ObjectiveSense;
15use crate::soc::SocConstraintId;
16use crate::var::{Variable, var_name};
17
18fn fmt_num(v: f64) -> String {
20 if v == 0.0 { "0".to_string() } else { format!("{v}") }
21}
22
23#[derive(Debug)]
26pub struct ExprDisplay<'a> {
27 model: &'a Model,
28 id: ExprId,
29}
30
31impl fmt::Display for ExprDisplay<'_> {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 let arena = self.model.arena.borrow();
34 let vars = self.model.variables.borrow();
35 f.write_str(&render_expr(&arena, self.id, &|v| var_name(&vars, v)))
36 }
37}
38
39#[derive(Debug)]
42pub struct ConstraintDisplay<'a> {
43 model: &'a Model,
44 id: ConstraintId,
45}
46
47impl fmt::Display for ConstraintDisplay<'_> {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 let arena = self.model.arena.borrow();
50 let vars = self.model.variables.borrow();
51 let constraints = self.model.constraints.borrow();
52 let c = &constraints[self.id.index()];
53 let resolve = |v| var_name(&vars, v);
54 let expr = render_expr(&arena, c.lhs, &resolve);
55 write!(f, "{}: ", c.name)?;
56 match c.as_single() {
57 Some((Sense::Le, rhs)) => write!(f, "{expr} <= {}", fmt_num(rhs))?,
58 Some((Sense::Ge, rhs)) => write!(f, "{expr} >= {}", fmt_num(rhs))?,
59 Some((Sense::Eq, rhs)) => write!(f, "{expr} = {}", fmt_num(rhs))?,
60 None if c.is_range() => {
61 write!(f, "{} <= {expr} <= {}", fmt_num(c.lower), fmt_num(c.upper))?;
62 }
63 None => write!(f, "{expr} free")?,
64 }
65 if !c.active {
66 f.write_str(" (inactive)")?;
67 }
68 Ok(())
69 }
70}
71
72#[derive(Debug)]
75pub struct ObjectiveDisplay<'a> {
76 model: &'a Model,
77}
78
79impl fmt::Display for ObjectiveDisplay<'_> {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 if self.model.is_feasibility() {
82 return f.write_str("feasibility");
83 }
84 let obj = self.model.objective.borrow().as_ref().map(|o| (o.sense, o.expr));
87 let Some((sense, expr)) = obj else {
88 return f.write_str("(no objective)");
89 };
90 let word = match sense {
91 ObjectiveSense::Minimize => "min",
92 ObjectiveSense::Maximize => "max",
93 };
94 write!(f, "{word} {}", ExprDisplay { model: self.model, id: expr })
95 }
96}
97
98#[derive(Debug)]
101pub struct SocDisplay<'a> {
102 model: &'a Model,
103 id: SocConstraintId,
104}
105
106impl fmt::Display for SocDisplay<'_> {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 let arena = self.model.arena.borrow();
109 let vars = self.model.variables.borrow();
110 let socs = self.model.soc_constraints.borrow();
111 let c = &socs[self.id.index()];
112 let resolve = |v| var_name(&vars, v);
113 write!(f, "{}: ||", c.name)?;
114 for (i, t) in c.terms.iter().enumerate() {
115 if i > 0 {
116 f.write_str(", ")?;
117 }
118 f.write_str(&render_expr(&arena, *t, &resolve))?;
119 }
120 write!(f, "|| <= {}", render_expr(&arena, c.bound, &resolve))?;
121 if !c.active {
122 f.write_str(" (inactive)")?;
123 }
124 Ok(())
125 }
126}
127
128fn write_var_line(f: &mut fmt::Formatter<'_>, v: &Variable) -> fmt::Result {
131 match (v.lb.is_finite(), v.ub.is_finite()) {
132 (true, true) if v.lb.total_cmp(&v.ub).is_eq() => {
133 write!(f, "{} = {}", v.name, fmt_num(v.lb))?;
134 }
135 (true, true) => write!(f, "{} <= {} <= {}", fmt_num(v.lb), v.name, fmt_num(v.ub))?,
136 (true, false) => write!(f, "{} >= {}", v.name, fmt_num(v.lb))?,
137 (false, true) => write!(f, "{} <= {}", v.name, fmt_num(v.ub))?,
138 (false, false) => write!(f, "{} free", v.name)?,
139 }
140 match v.domain {
141 Domain::Real => Ok(()),
142 Domain::Integer => f.write_str(", integer"),
143 Domain::Binary => f.write_str(", binary"),
144 Domain::SemiContinuous { threshold } => {
145 write!(f, ", semicontinuous(threshold={})", fmt_num(threshold))
146 }
147 Domain::SemiInteger { threshold } => {
148 write!(f, ", semiinteger(threshold={})", fmt_num(threshold))
149 }
150 }
151}
152
153impl Model {
154 #[must_use]
157 pub fn display_expr(&self, e: Expr<'_>) -> ExprDisplay<'_> {
158 self.display_expr_id(e.id)
159 }
160
161 #[must_use]
164 pub fn display_expr_id(&self, id: ExprId) -> ExprDisplay<'_> {
165 ExprDisplay { model: self, id }
166 }
167
168 #[must_use]
170 pub fn display_constraint(&self, id: ConstraintId) -> ConstraintDisplay<'_> {
171 ConstraintDisplay { model: self, id }
172 }
173
174 #[must_use]
177 pub fn display_objective(&self) -> ObjectiveDisplay<'_> {
178 ObjectiveDisplay { model: self }
179 }
180
181 #[must_use]
183 pub fn display_soc(&self, id: SocConstraintId) -> SocDisplay<'_> {
184 SocDisplay { model: self, id }
185 }
186}
187
188impl fmt::Display for Model {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 writeln!(f, "Model '{}' ({:?})", self.name, self.kind())?;
206 if self.is_feasibility() || self.objective.borrow().is_some() {
207 writeln!(f, "{}", self.display_objective())?;
208 }
209 let n_constraints = self.constraints.borrow().len();
210 let n_socs = self.soc_constraints.borrow().len();
211 if n_constraints + n_socs > 0 {
212 let n_constraints = u32::try_from(n_constraints).expect("constraint count fits u32");
213 let n_socs = u32::try_from(n_socs).expect("soc count fits u32");
214 writeln!(f, "s.t.")?;
215 for i in 0..n_constraints {
216 writeln!(f, " {}", self.display_constraint(ConstraintId(i)))?;
217 }
218 for i in 0..n_socs {
219 writeln!(f, " {}", self.display_soc(SocConstraintId(i)))?;
220 }
221 }
222 {
223 let vars = self.variables.borrow();
224 if !vars.is_empty() {
225 writeln!(f, "vars")?;
226 for v in vars.iter() {
227 f.write_str(" ")?;
228 write_var_line(f, v)?;
229 writeln!(f)?;
230 }
231 }
232 }
233 let params = self.parameters.borrow();
234 if !params.is_empty() {
235 let arena = self.arena.borrow();
236 writeln!(f, "params")?;
237 for p in params.iter() {
238 writeln!(f, " {} = {}", p.name, fmt_num(arena.param_value(p.id)))?;
239 }
240 }
241 Ok(())
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::constraint::Relate;
249
250 #[test]
251 fn full_model_snapshot() {
252 let m = Model::new("diet");
253 let x = m.__var("x").lb(0.0).build();
254 let y = m.__var("y").lb(0.0).build();
255 m.__add_constraint("c1", (x + 2.0 * y).le(14.0));
256 m.__add_constraint("c2", (3.0 * x - y).ge(0.0));
257 m.__minimize(3.0 * x + 4.0 * y);
258
259 let expected = "Model 'diet' (LP)\n\
260 min 3 x + 4 y\n\
261 s.t.\n\
262 \x20 c1: x + 2 y <= 14\n\
263 \x20 c2: 3 x - y >= 0\n\
264 vars\n\
265 \x20 x >= 0\n\
266 \x20 y >= 0\n";
267 assert_eq!(format!("{m}"), expected);
268 }
269
270 #[test]
271 fn constraint_variants_render() {
272 let m = Model::new("t");
273 let x = m.__var("x").build();
274 let eq = m.__add_constraint("e", x.eq(5.0));
275 assert_eq!(m.display_constraint(eq).to_string(), "e: x = 5");
276
277 m.__add_range("r", x, 2.0, 10.0);
278 let r = m.constraint_id("r").unwrap();
279 assert_eq!(m.display_constraint(r).to_string(), "r: 2 <= x <= 10");
280
281 let ge = m.__add_constraint("g", x.ge(1.0));
282 m.constraints.borrow_mut()[ge.index()].active = false;
283 assert_eq!(m.display_constraint(ge).to_string(), "g: x >= 1 (inactive)");
284 }
285
286 #[test]
287 fn objective_and_feasibility() {
288 let m = Model::new("t");
289 let x = m.__var("x").build();
290 assert_eq!(m.display_objective().to_string(), "(no objective)");
291 m.__maximize(x);
292 assert_eq!(m.display_objective().to_string(), "max x");
293
294 let f = Model::new("f");
295 f.__feasibility();
296 assert_eq!(f.display_objective().to_string(), "feasibility");
297 assert!(format!("{f}").contains("feasibility\n"));
298 }
299
300 #[test]
301 fn var_lines_cover_domains_and_bounds() {
302 let m = Model::new("t");
303 m.__var("a").bounds(0.0, 5.0).build();
304 m.__var("b").binary().build();
305 m.__var("c").integer().build();
306 m.__var("d").build();
307 m.__var("e").ub(3.0).build();
308 m.__var("g").fix(2.0).build();
309 m.__var("h").lb(0.0).domain(crate::Domain::SemiContinuous { threshold: 2.0 }).build();
310
311 let out = format!("{m}");
312 assert!(out.contains(" 0 <= a <= 5\n"), "{out}");
313 assert!(out.contains(" 0 <= b <= 1, binary\n"), "{out}");
314 assert!(out.contains(" c free, integer\n"), "{out}");
315 assert!(out.contains(" d free\n"), "{out}");
316 assert!(out.contains(" e <= 3\n"), "{out}");
317 assert!(out.contains(" g = 2\n"), "{out}");
318 assert!(out.contains(" h >= 0, semicontinuous(threshold=2)\n"), "{out}");
319 }
320
321 #[test]
322 fn params_section_shows_current_values() {
323 let m = Model::new("t");
324 let x = m.__var("x").build();
325 let price = m.__param("price", 4.0);
326 m.__minimize(price * x);
327 let out = format!("{m}");
328 assert!(out.contains("min 4 x\n"), "{out}");
329 assert!(out.contains("params\n price = 4\n"), "{out}");
330
331 price.set_param_value(7.5);
332 let out = format!("{m}");
333 assert!(out.contains("min 7.5 x\n"), "{out}");
334 assert!(out.contains("params\n price = 7.5\n"), "{out}");
335 }
336
337 #[test]
338 fn soc_row_renders_in_model_display() {
339 let m = Model::new("t");
340 let x = m.__var("x").build();
341 let y = m.__var("y").build();
342 let t = m.__var("t").lb(0.0).build();
343 let id = m.add_soc_constraint("q1", [x, y], t);
344 assert_eq!(m.display_soc(id).to_string(), "q1: ||x, y|| <= t");
345 assert!(format!("{m}").contains(" q1: ||x, y|| <= t\n"));
346 }
347
348 #[test]
349 fn display_expr_renders_nonlinear() {
350 let m = Model::new("t");
351 let x = m.__var("x").build();
352 let y = m.__var("y").build();
353 let e = x * y - y;
354 assert_eq!(m.display_expr(e).to_string(), "-y + x * y");
355 }
356}