pounce_algorithm/sqp/result.rs
1//! `SqpResult` / `SqpStatus` / `SqpError` — return types for
2//! `SqpAlgorithm::optimize`.
3
4use pounce_common::Number;
5use pounce_qp::{QpError, WorkingSet};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SqpStatus {
10 /// KKT residuals all below their tolerances.
11 Optimal,
12 /// `max_iter` reached without convergence.
13 MaxIter,
14 /// QP subproblem returned an `Infeasible` status (elastic
15 /// mode certified the QP infeasible).
16 InfeasibleSubproblem,
17 /// Line search failed to find an acceptable step (Phase 5b
18 /// commit 5+; not produced by the c3 always-full-step loop).
19 LineSearchFailed,
20 /// The QP subproblem solver neither produced a usable step nor
21 /// certified infeasibility — it hit its own iteration limit or a
22 /// numerical breakdown (e.g. the extreme m/n ≫ 1 degenerate phase-1
23 /// of #282). This is an HONEST non-committal failure: the SQP could
24 /// not compute a search direction, but — unlike `InfeasibleSubproblem`
25 /// — it makes no infeasibility claim it cannot back with a
26 /// certificate. Maps to `Search_Direction_Becomes_Too_Small`.
27 QpStepFailed,
28}
29
30impl fmt::Display for SqpStatus {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 SqpStatus::Optimal => write!(f, "optimal"),
34 SqpStatus::MaxIter => write!(f, "max-iter"),
35 SqpStatus::InfeasibleSubproblem => write!(f, "infeasible-subproblem"),
36 SqpStatus::LineSearchFailed => write!(f, "line-search-failed"),
37 SqpStatus::QpStepFailed => write!(f, "qp-step-failed"),
38 }
39 }
40}
41
42#[derive(Debug)]
43pub enum SqpError {
44 /// Hard QP-solver failure (singular, dimension mismatch, etc.).
45 QpFailure(QpError),
46 /// Caller-supplied dimensions disagree.
47 DimensionMismatch(String),
48}
49
50impl fmt::Display for SqpError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 SqpError::QpFailure(e) => write!(f, "QP subproblem failure: {e}"),
54 SqpError::DimensionMismatch(s) => write!(f, "dimension mismatch: {s}"),
55 }
56 }
57}
58
59impl From<QpError> for SqpError {
60 fn from(e: QpError) -> Self {
61 SqpError::QpFailure(e)
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct SqpResult {
67 pub x: Vec<Number>,
68 pub lambda_g: Vec<Number>,
69 pub lambda_x: Vec<Number>,
70 pub obj: Number,
71 pub status: SqpStatus,
72 pub n_iter: u32,
73 pub n_qp_solves: u32,
74 /// Final stationarity residual (max-norm of `∇f + Jᵀ λ_g + λ_x`).
75 pub final_stationarity: Number,
76 /// Final constraint violation (max-norm of `c(x*)` for
77 /// equalities plus bound-violation slack).
78 pub final_constr_viol: Number,
79 /// Final QP working set, suitable for warm-starting the next
80 /// `optimize_with_warm_start` call (§6 design-note contract).
81 /// `None` only when no QP was solved (e.g. cold-start declared
82 /// the iterate optimal at the very first KKT check).
83 pub working_set: Option<WorkingSet>,
84}