oximo_solver/
infeasibility.rs1use oximo_core::{ConstraintId, Model, SocConstraintId, VarId};
2
3use crate::result::SolverResult;
4use crate::solver::Solver;
5use crate::status::SolverError;
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq)]
9pub enum VarBoundKind {
10 Lower,
12 Upper,
14}
15
16#[derive(Clone, Debug, Default)]
26pub struct Iis {
27 pub constraints: Vec<ConstraintId>,
29 pub soc_constraints: Vec<SocConstraintId>,
31 pub var_bounds: Vec<(VarId, VarBoundKind)>,
33}
34
35impl Iis {
36 #[must_use]
38 pub fn is_empty(&self) -> bool {
39 self.constraints.is_empty() && self.soc_constraints.is_empty() && self.var_bounds.is_empty()
40 }
41
42 #[must_use]
45 pub fn len(&self) -> usize {
46 self.constraints.len() + self.soc_constraints.len() + self.var_bounds.len()
47 }
48
49 #[must_use]
54 pub fn report<'a>(&'a self, model: &'a Model) -> IisReport<'a> {
55 IisReport { iis: self, model }
56 }
57}
58
59#[derive(Debug)]
61pub struct IisReport<'a> {
62 iis: &'a Iis,
63 model: &'a Model,
64}
65
66impl std::fmt::Display for IisReport<'_> {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let iis = self.iis;
69 let m = self.model;
70
71 writeln!(f, "irreducible infeasible subsystem ({} members)", iis.len())?;
72
73 if !iis.constraints.is_empty() {
74 let model_constraints = m.constraints();
75 let cons = model_constraints.algebraic();
76 writeln!(f, "\nconstraints ({})", iis.constraints.len())?;
77 for id in &iis.constraints {
78 match cons.get(id.index()) {
79 Some(c) => writeln!(f, " {}", c.name)?,
80 None => writeln!(f, " <constraint #{}>", id.index())?,
81 }
82 }
83 }
84
85 if !iis.soc_constraints.is_empty() {
86 let socs = m.soc_constraints();
87 writeln!(f, "\nsoc constraints ({})", iis.soc_constraints.len())?;
88 for id in &iis.soc_constraints {
89 match socs.get(id.index()) {
90 Some(s) => writeln!(f, " {}", s.name)?,
91 None => writeln!(f, " <soc #{}>", id.index())?,
92 }
93 }
94 }
95
96 if !iis.var_bounds.is_empty() {
97 let vars = m.variables();
98 writeln!(f, "\nvariable bounds ({})", iis.var_bounds.len())?;
99 for (id, kind) in &iis.var_bounds {
100 let side = match kind {
101 VarBoundKind::Lower => "lower",
102 VarBoundKind::Upper => "upper",
103 };
104 match vars.get(id.index()) {
105 Some(v) => writeln!(f, " {} ({side} bound)", v.name)?,
106 None => writeln!(f, " <var #{}> ({side} bound)", id.index())?,
107 }
108 }
109 }
110
111 Ok(())
112 }
113}
114
115pub trait InfeasibilityDiagnosis: Solver {
123 fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
133}
134
135#[must_use]
138pub fn is_infeasible(result: &SolverResult) -> bool {
139 result.termination.is_infeasible()
140}
141
142#[cfg(test)]
143mod tests {
144 use oximo_core::{constraint, variable};
145
146 use super::*;
147
148 #[test]
149 fn report_names_members() {
150 let m = Model::new("infeas");
151 variable!(m, x >= 0.0);
152 let lo = constraint!(m, floor, x >= 2.0);
153 let hi = constraint!(m, ceil, x <= 1.0);
154
155 let iis = Iis {
156 constraints: vec![lo, hi],
157 soc_constraints: Vec::new(),
158 var_bounds: vec![(x.var_id().unwrap(), VarBoundKind::Lower)],
159 };
160
161 assert_eq!(iis.len(), 3);
162 assert!(!iis.is_empty());
163
164 let out = iis.report(&m).to_string();
165 assert!(out.contains("irreducible infeasible subsystem (3 members)"), "{out}");
166 assert!(out.contains("floor"), "{out}");
167 assert!(out.contains("ceil"), "{out}");
168 assert!(out.contains("x (lower bound)"), "{out}");
169 }
170
171 #[test]
172 fn empty_iis_reports_zero() {
173 let m = Model::new("ok");
174 let iis = Iis::default();
175 assert!(iis.is_empty());
176 assert_eq!(iis.len(), 0);
177 let out = iis.report(&m).to_string();
178 assert!(out.contains("(0 members)"), "{out}");
179 }
180}