Skip to main content

oximo_solver/
infeasibility.rs

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/// Which side of a variable's bound participates in an infeasibility.
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11pub enum VarBoundKind {
12    /// The variable's lower bound.
13    Lower,
14    /// The variable's upper bound.
15    Upper,
16}
17
18/// An irreducible infeasible subsystem (IIS).
19///
20/// A minimal set of constraints and variable bounds that together make the model
21/// infeasible. Removing any single member makes the remaining subsystem feasible.
22///
23/// Backends that can diagnose infeasibility return one via
24/// [`InfeasibilityDiagnosis::compute_iis`]. The members are keyed by the same ids the
25/// model assigns ([`ConstraintId`], [`SocConstraintId`], [`VarId`]), so
26/// [`Iis::report`] can name them against the [`Model`].
27#[derive(Clone, Debug, Default)]
28pub struct Iis {
29    /// Algebraic constraints in the IIS.
30    pub constraints: Vec<ConstraintId>,
31    /// Second-order-cone constraints in the IIS.
32    pub soc_constraints: Vec<SocConstraintId>,
33    pub sos_constraints: Vec<SosConstraintId>,
34    pub indicator_constraints: Vec<IndicatorConstraintId>,
35    /// Variable bounds in the IIS, each `(variable, which bound)`.
36    pub var_bounds: Vec<(VarId, VarBoundKind)>,
37}
38
39impl Iis {
40    /// Whether the IIS carries no members.
41    #[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    /// The total number of members (constraints, SOC constraints, and variable
51    /// bounds) in the IIS.
52    #[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    /// A human-readable, model-aware listing of this IIS.
62    ///
63    /// It names every constraint and variable bound in the subsystem using the
64    /// model's own names. Render with [`ToString::to_string`] or by printing.
65    #[must_use]
66    pub fn report<'a>(&'a self, model: &'a Model) -> IisReport<'a> {
67        IisReport { iis: self, model }
68    }
69}
70
71/// A printable, model-aware listing of an [`Iis`]. Created by [`Iis::report`].
72#[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
149/// A [`Solver`] that can diagnose why a model is infeasible by computing an
150/// irreducible infeasible subsystem ([`Iis`]).
151///
152/// Implemented only by backends whose underlying solver exposes a native IIS/
153/// conflict-refiner options. Solve the model and if infeasible call
154/// [`compute_iis`](InfeasibilityDiagnosis::compute_iis) to get
155/// the minimal conflicting set.
156pub trait InfeasibilityDiagnosis: Solver {
157    /// Compute an irreducible infeasible subsystem for `model`.
158    ///
159    /// The backend solves `model` (with `opts`) and, if it is infeasible, returns the
160    /// minimal set of constraints and variable bounds responsible.
161    ///
162    /// # Errors
163    ///
164    /// Returns a [`SolverError`] if the solve fails, the backend cannot represent the
165    /// model, or the model is not actually infeasible.
166    fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
167}
168
169/// Helper for backends, whether a [`SolverResult`] indicates the model is infeasible.
170/// Treats the ambiguous `InfeasibleOrUnbounded` as infeasible.
171#[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}