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 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
114/// A [`Solver`] that can diagnose why a model is infeasible by computing an
115/// irreducible infeasible subsystem ([`Iis`]).
116///
117/// Implemented only by backends whose underlying solver exposes a native IIS/
118/// conflict-refiner options. Solve the model and if infeasible call
119/// [`compute_iis`](InfeasibilityDiagnosis::compute_iis) to get
120/// the minimal conflicting set.
121pub trait InfeasibilityDiagnosis: Solver {
122    /// Compute an irreducible infeasible subsystem for `model`.
123    ///
124    /// The backend solves `model` (with `opts`) and, if it is infeasible, returns the
125    /// minimal set of constraints and variable bounds responsible.
126    ///
127    /// # Errors
128    ///
129    /// Returns a [`SolverError`] if the solve fails, the backend cannot represent the
130    /// model, or the model is not actually infeasible.
131    fn compute_iis(&mut self, model: &Model, opts: &Self::Options) -> Result<Iis, SolverError>;
132}
133
134/// Helper for backends, whether a [`SolverResult`] indicates the model is infeasible.
135/// Treats the ambiguous `InfeasibleOrUnbounded` as infeasible.
136#[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}