pounce_qp/error.rs
1//! Error and status types for the QP solver.
2
3use std::fmt;
4
5/// Terminal status of a QP solve.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum QpStatus {
8 /// KKT residual and feasibility within tolerance.
9 Optimal,
10 /// Phase-1 elastic mode certified the QP as infeasible
11 /// (residual elastic slacks are nonzero at the elastic
12 /// solution).
13 Infeasible,
14 /// Descent direction of unbounded length found (only possible
15 /// when the reduced Hessian is indefinite or negative semi-
16 /// definite along a feasible ray).
17 Unbounded,
18 /// Iteration limit reached before convergence.
19 MaxIter,
20 /// Solver detected numerical breakdown (e.g., factor failure
21 /// not recoverable by inertia correction).
22 NumericalError,
23}
24
25impl fmt::Display for QpStatus {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 QpStatus::Optimal => write!(f, "optimal"),
29 QpStatus::Infeasible => write!(f, "infeasible"),
30 QpStatus::Unbounded => write!(f, "unbounded"),
31 QpStatus::MaxIter => write!(f, "max-iter"),
32 QpStatus::NumericalError => write!(f, "numerical-error"),
33 }
34 }
35}
36
37/// Hard errors โ problems the solver cannot return any meaningful
38/// solution for. Soft outcomes (max-iter, infeasible, unbounded) are
39/// reported via [`QpStatus`] inside a successful
40/// [`crate::QpSolution`].
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum QpError {
43 /// Problem-data dimensions disagree (e.g., `g.len() != n`).
44 DimensionMismatch(String),
45 /// A bound vector contains `bl > bu` for some index.
46 InvertedBounds(String),
47 /// Warm-start working set has the wrong length for the problem
48 /// dimensions.
49 WarmStartDimensionMismatch(String),
50 /// Linear-solver backend reported a hard failure that cannot be
51 /// recovered by the inertia / refactor logic.
52 LinearSolverFailure(String),
53 /// Feature required by this QP is not yet implemented in the
54 /// current crate phase (e.g., one-sided inequality constraints
55 /// before the working-set machinery lands).
56 UnsupportedFeature(String),
57}
58
59impl QpError {
60 /// True when this is a linear-solver failure that the ยง4.5
61 /// inertia-control loop may recover from by shifting the Hessian
62 /// diagonal โ i.e. a singular factor or a wrong-inertia report.
63 ///
64 /// Centralizes the recoverability decision so the retry loops in
65 /// `solver.rs` and `schur.rs` don't each re-implement a fragile
66 /// substring test. The match is **case-insensitive**: some failure
67 /// messages embed the backend's `Debug`-formatted `ESymSolverStatus`
68 /// (`Singular` / `WrongInertia`, capitalized โ produced by
69 /// `LinearSolver::resolve`'s catch-all `"resolve backend status:
70 /// {status:?}"`), which a bare lowercase `contains("singular")` /
71 /// `contains("inertia")` would silently miss, so those failures would
72 /// propagate as unrecoverable instead of triggering a shift retry
73 /// (L14).
74 pub fn is_recoverable_factorization_failure(&self) -> bool {
75 match self {
76 QpError::LinearSolverFailure(msg) => {
77 let m = msg.to_ascii_lowercase();
78 m.contains("inertia") || m.contains("singular")
79 }
80 _ => false,
81 }
82 }
83}
84
85impl fmt::Display for QpError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 QpError::DimensionMismatch(s) => write!(f, "dimension mismatch: {s}"),
89 QpError::InvertedBounds(s) => write!(f, "inverted bounds: {s}"),
90 QpError::WarmStartDimensionMismatch(s) => {
91 write!(f, "warm-start dimension mismatch: {s}")
92 }
93 QpError::LinearSolverFailure(s) => write!(f, "linear solver failure: {s}"),
94 QpError::UnsupportedFeature(s) => write!(f, "unsupported feature: {s}"),
95 }
96 }
97}
98
99impl std::error::Error for QpError {}