Skip to main content

oximo_solver/
infeasibility.rs

1use oximo_core::{ConstraintId, Model, SocConstraintId, VarId};
2
3use crate::result::SolverResult;
4use crate::solver::Solver;
5use crate::status::SolverError;
6
7/// Which side of a variable's bound participates in an infeasibility.
8#[derive(Copy, Clone, Debug, PartialEq, Eq)]
9pub enum VarBoundKind {
10    /// The variable's lower bound.
11    Lower,
12    /// The variable's upper bound.
13    Upper,
14}
15
16/// An irreducible infeasible subsystem (IIS).
17///
18/// A minimal set of constraints and variable bounds that together make the model
19/// infeasible. Removing any single member makes the remaining subsystem feasible.
20///
21/// Backends that can diagnose infeasibility return one via
22/// [`InfeasibilityDiagnosis::compute_iis`]. The members are keyed by the same ids the
23/// model assigns ([`ConstraintId`], [`SocConstraintId`], [`VarId`]), so
24/// [`Iis::report`] can name them against the [`Model`].
25#[derive(Clone, Debug, Default)]
26pub struct Iis {
27    /// Algebraic constraints in the IIS.
28    pub constraints: Vec<ConstraintId>,
29    /// Second-order-cone constraints in the IIS.
30    pub soc_constraints: Vec<SocConstraintId>,
31    /// Variable bounds in the IIS, each `(variable, which bound)`.
32    pub var_bounds: Vec<(VarId, VarBoundKind)>,
33}
34
35impl Iis {
36    /// Whether the IIS carries no members.
37    #[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    /// The total number of members (constraints, SOC constraints, and variable
43    /// bounds) in the IIS.
44    #[must_use]
45    pub fn len(&self) -> usize {
46        self.constraints.len() + self.soc_constraints.len() + self.var_bounds.len()
47    }
48
49    /// A human-readable, model-aware listing of this IIS.
50    ///
51    /// It names every constraint and variable bound in the subsystem using the
52    /// model's own names. Render with [`ToString::to_string`] or by printing.
53    #[must_use]
54    pub fn report<'a>(&'a self, model: &'a Model) -> IisReport<'a> {
55        IisReport { iis: self, model }
56    }
57}
58
59/// A printable, model-aware listing of an [`Iis`]. Created by [`Iis::report`].
60#[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
115/// A [`Solver`] that can diagnose why a model is infeasible by computing an
116/// irreducible infeasible subsystem ([`Iis`]).
117///
118/// Implemented only by backends whose underlying solver exposes a native IIS/
119/// conflict-refiner options. Solve the model and if infeasible call
120/// [`compute_iis`](InfeasibilityDiagnosis::compute_iis) to get
121/// the minimal conflicting set.
122pub trait InfeasibilityDiagnosis: Solver {
123    /// Compute an irreducible infeasible subsystem for `model`.
124    ///
125    /// The backend solves `model` (with `opts`) and, if it is infeasible, returns the
126    /// minimal set of constraints and variable bounds responsible.
127    ///
128    /// # Errors
129    ///
130    /// Returns a [`SolverError`] if the solve fails, the backend cannot represent the
131    /// model, or the model is not actually infeasible.
132    fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
133}
134
135/// Helper for backends, whether a [`SolverResult`] indicates the model is infeasible.
136/// Treats the ambiguous `InfeasibleOrUnbounded` as infeasible.
137#[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}