1use 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
19fn fmt_num(v: f64) -> String {
21 if v == 0.0 { "0".to_string() } else { format!("{v}") }
22}
23
24#[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#[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#[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 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#[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
185fn 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 #[must_use]
204 pub fn display_expr(&self, e: Expr<'_>) -> ExprDisplay<'_> {
205 self.display_expr_id(e.id)
206 }
207
208 #[must_use]
211 pub fn display_expr_id(&self, id: ExprId) -> ExprDisplay<'_> {
212 ExprDisplay { model: self, id }
213 }
214
215 #[must_use]
217 pub fn display_constraint(&self, id: ConstraintId) -> ConstraintDisplay<'_> {
218 ConstraintDisplay { model: self, id }
219 }
220
221 #[must_use]
224 pub fn display_objective(&self) -> ObjectiveDisplay<'_> {
225 ObjectiveDisplay { model: self }
226 }
227
228 #[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
245impl 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}