1use oximo_core::{
2 ConstraintId, IndicatorConstraintId, Model, SocConstraintId, SosConstraintId, VarId,
3};
4
5use crate::result::SolverResult;
6use crate::solver::Solver;
7use crate::status::SolverError;
8
9#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11pub enum VarBoundKind {
12 Lower,
14 Upper,
16}
17
18#[derive(Clone, Debug, Default)]
28pub struct Iis {
29 pub constraints: Vec<ConstraintId>,
31 pub soc_constraints: Vec<SocConstraintId>,
33 pub sos_constraints: Vec<SosConstraintId>,
34 pub indicator_constraints: Vec<IndicatorConstraintId>,
35 pub var_bounds: Vec<(VarId, VarBoundKind)>,
37}
38
39impl Iis {
40 #[must_use]
42 pub fn is_empty(&self) -> bool {
43 self.constraints.is_empty()
44 && self.soc_constraints.is_empty()
45 && self.sos_constraints.is_empty()
46 && self.indicator_constraints.is_empty()
47 && self.var_bounds.is_empty()
48 }
49
50 #[must_use]
53 pub fn len(&self) -> usize {
54 self.constraints.len()
55 + self.soc_constraints.len()
56 + self.sos_constraints.len()
57 + self.indicator_constraints.len()
58 + self.var_bounds.len()
59 }
60
61 #[must_use]
66 pub fn report<'a>(&'a self, model: &'a Model) -> IisReport<'a> {
67 IisReport { iis: self, model }
68 }
69}
70
71#[derive(Debug)]
73pub struct IisReport<'a> {
74 iis: &'a Iis,
75 model: &'a Model,
76}
77
78impl std::fmt::Display for IisReport<'_> {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 let iis = self.iis;
81 let m = self.model;
82
83 writeln!(f, "irreducible infeasible subsystem ({} members)", iis.len())?;
84
85 if !iis.constraints.is_empty() {
86 let model_constraints = m.constraints();
87 let cons = model_constraints.algebraic();
88 writeln!(f, "\nconstraints ({})", iis.constraints.len())?;
89 for id in &iis.constraints {
90 match cons.get(id.index()) {
91 Some(c) => writeln!(f, " {}", c.name)?,
92 None => writeln!(f, " <constraint #{}>", id.index())?,
93 }
94 }
95 }
96
97 if !iis.soc_constraints.is_empty() {
98 let socs = m.soc_constraints();
99 writeln!(f, "\nsoc constraints ({})", iis.soc_constraints.len())?;
100 for id in &iis.soc_constraints {
101 match socs.get(id.index()) {
102 Some(s) => writeln!(f, " {}", s.name)?,
103 None => writeln!(f, " <soc #{}>", id.index())?,
104 }
105 }
106 }
107
108 if !iis.sos_constraints.is_empty() {
109 let sos = m.sos_constraints();
110 writeln!(f, "\nsos constraints ({})", iis.sos_constraints.len())?;
111 for id in &iis.sos_constraints {
112 match sos.get(id.index()) {
113 Some(s) => writeln!(f, " {}", s.name)?,
114 None => writeln!(f, " <sos #{}>", id.index())?,
115 }
116 }
117 }
118
119 if !iis.indicator_constraints.is_empty() {
120 let indicators = m.indicator_constraints();
121 writeln!(f, "\nindicator constraints ({})", iis.indicator_constraints.len())?;
122 for id in &iis.indicator_constraints {
123 match indicators.get(id.index()) {
124 Some(c) => writeln!(f, " {}", c.name)?,
125 None => writeln!(f, " <indicator #{}>", id.index())?,
126 }
127 }
128 }
129
130 if !iis.var_bounds.is_empty() {
131 let vars = m.variables();
132 writeln!(f, "\nvariable bounds ({})", iis.var_bounds.len())?;
133 for (id, kind) in &iis.var_bounds {
134 let side = match kind {
135 VarBoundKind::Lower => "lower",
136 VarBoundKind::Upper => "upper",
137 };
138 match vars.get(id.index()) {
139 Some(v) => writeln!(f, " {} ({side} bound)", v.name)?,
140 None => writeln!(f, " <var #{}> ({side} bound)", id.index())?,
141 }
142 }
143 }
144
145 Ok(())
146 }
147}
148
149pub trait InfeasibilityDiagnosis: Solver {
157 fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
167}
168
169#[must_use]
172pub fn is_infeasible(result: &SolverResult) -> bool {
173 result.termination.is_infeasible()
174}
175
176#[cfg(test)]
177mod tests {
178 use oximo_core::{SosType, constraint, variable};
179
180 use super::*;
181
182 #[test]
183 fn report_names_members() {
184 let m = Model::new("infeas");
185 variable!(m, x >= 0.0);
186 let lo = constraint!(m, floor, x >= 2.0);
187 let hi = constraint!(m, ceil, x <= 1.0);
188 let sos = {
189 variable!(m, y);
190 m.add_sos_constraint("choice", SosType::Sos1, [(y, 1.0)]).id()
191 };
192
193 let iis = Iis {
194 constraints: vec![lo.id(), hi.id()],
195 soc_constraints: Vec::new(),
196 sos_constraints: vec![sos],
197 indicator_constraints: Vec::new(),
198 var_bounds: vec![(x.var_id().unwrap(), VarBoundKind::Lower)],
199 };
200
201 assert_eq!(iis.len(), 4);
202 assert!(!iis.is_empty());
203
204 let out = iis.report(&m).to_string();
205 assert!(out.contains("irreducible infeasible subsystem (4 members)"), "{out}");
206 assert!(out.contains("floor"), "{out}");
207 assert!(out.contains("ceil"), "{out}");
208 assert!(out.contains("sos constraints (1)"), "{out}");
209 assert!(out.contains("choice"), "{out}");
210 assert!(out.contains("x (lower bound)"), "{out}");
211 }
212
213 #[test]
214 fn report_handles_unknown_sos_ids() {
215 let m = Model::new("unknown");
216 let iis = Iis { sos_constraints: vec![SosConstraintId(9)], ..Iis::default() };
217 let out = iis.report(&m).to_string();
218 assert!(out.contains("<sos #9>"), "{out}");
219 }
220
221 #[test]
222 fn empty_iis_reports_zero() {
223 let m = Model::new("ok");
224 let iis = Iis::default();
225 assert!(iis.is_empty());
226 assert_eq!(iis.len(), 0);
227 let out = iis.report(&m).to_string();
228 assert!(out.contains("(0 members)"), "{out}");
229 }
230}