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 cons = m.constraints();
75 writeln!(f, "\nconstraints ({})", iis.constraints.len())?;
76 for id in &iis.constraints {
77 match cons.get(id.index()) {
78 Some(c) => writeln!(f, " {}", c.name)?,
79 None => writeln!(f, " <constraint #{}>", id.index())?,
80 }
81 }
82 }
83
84 if !iis.soc_constraints.is_empty() {
85 let socs = m.soc_constraints();
86 writeln!(f, "\nsoc constraints ({})", iis.soc_constraints.len())?;
87 for id in &iis.soc_constraints {
88 match socs.get(id.index()) {
89 Some(s) => writeln!(f, " {}", s.name)?,
90 None => writeln!(f, " <soc #{}>", id.index())?,
91 }
92 }
93 }
94
95 if !iis.var_bounds.is_empty() {
96 let vars = m.variables();
97 writeln!(f, "\nvariable bounds ({})", iis.var_bounds.len())?;
98 for (id, kind) in &iis.var_bounds {
99 let side = match kind {
100 VarBoundKind::Lower => "lower",
101 VarBoundKind::Upper => "upper",
102 };
103 match vars.get(id.index()) {
104 Some(v) => writeln!(f, " {} ({side} bound)", v.name)?,
105 None => writeln!(f, " <var #{}> ({side} bound)", id.index())?,
106 }
107 }
108 }
109
110 Ok(())
111 }
112}
113
114pub trait InfeasibilityDiagnosis: Solver {
122 fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
132}
133
134#[must_use]
137pub fn is_infeasible(result: &SolverResult) -> bool {
138 result.termination.is_infeasible()
139}
140
141#[cfg(test)]
142mod tests {
143 use oximo_core::{constraint, variable};
144
145 use super::*;
146
147 #[test]
148 fn report_names_members() {
149 let m = Model::new("infeas");
150 variable!(m, x >= 0.0);
151 let lo = constraint!(m, floor, x >= 2.0);
152 let hi = constraint!(m, ceil, x <= 1.0);
153
154 let iis = Iis {
155 constraints: vec![lo, hi],
156 soc_constraints: Vec::new(),
157 var_bounds: vec![(x.var_id().unwrap(), VarBoundKind::Lower)],
158 };
159
160 assert_eq!(iis.len(), 3);
161 assert!(!iis.is_empty());
162
163 let out = iis.report(&m).to_string();
164 assert!(out.contains("irreducible infeasible subsystem (3 members)"), "{out}");
165 assert!(out.contains("floor"), "{out}");
166 assert!(out.contains("ceil"), "{out}");
167 assert!(out.contains("x (lower bound)"), "{out}");
168 }
169
170 #[test]
171 fn empty_iis_reports_zero() {
172 let m = Model::new("ok");
173 let iis = Iis::default();
174 assert!(iis.is_empty());
175 assert_eq!(iis.len(), 0);
176 let out = iis.report(&m).to_string();
177 assert!(out.contains("(0 members)"), "{out}");
178 }
179}