Skip to main content

otspot_core/qp/ipm_solver/
outcome.rs

1//! 内部 outcome: status mutation を API 境界 1 箇所に集約するための struct。
2
3use crate::sparse::CscMatrix;
4
5/// 残差ベース収束判定を `satisfies_eps` に集約。Infeasible / Unbounded のみ
6/// `infeasibility_status` で保持し、finalize で Timeout に丸めない。
7#[derive(Clone, Debug)]
8pub struct IpmOutcome {
9    pub solution: Vec<f64>,
10    pub dual_solution: Vec<f64>,
11    /// lb 有限の y_lb + ub 有限の y_ub。
12    pub bound_duals: Vec<f64>,
13    pub objective: f64,
14    pub iterations: usize,
15    /// 成分相対化 stationarity 残差 max_j |Qx+c+Aᵀy+z|_j / scale_j。
16    pub kkt_residual_rel: f64,
17    /// 成分正規化 primal violation max_i violation/(1+|a|+|b|)。
18    pub primal_residual_rel: f64,
19    /// max_j max(lb−x, x−ub)。
20    pub bound_violation: f64,
21    /// 成分相対化 complementarity 残差。stationarity のみでは「feasible だが optimal でない点」を見逃すため別立て。
22    pub complementarity_residual_rel: f64,
23    /// |p − d| / max(|p|,|d|,1)。rank-deficient Q の偽 Optimal を弾く。
24    pub duality_gap_rel: f64,
25    pub numerical_failure: bool,
26    /// 確定判定された Infeasible / Unbounded のみ保持 (他 status は残差から外部判定)。
27    pub infeasibility_status: Option<crate::problem::SolveStatus>,
28    /// 慣性修正付き IPM が走った場合、収束時に Optimal でなく LocallyOptimal を返す。
29    pub is_locally_optimal: bool,
30    /// postsolve の saddle-point Krylov IR が `kkt_already_pass` ゲートで省略されたか。
31    /// 既に user_eps を満たす収束解で重い拡大 KKT 因子化を回避した場合に true。
32    /// ゲートを外す (常時 refine) と false になる sentinel 用観測点。
33    pub postsolve_krylov_ir_skipped: bool,
34    /// IPM + postsolve stage 別計測。常時収集 (instrumentation only)。
35    pub timing: Option<crate::problem::TimingBreakdown>,
36}
37
38impl IpmOutcome {
39    pub fn empty() -> Self {
40        Self {
41            solution: Vec::new(),
42            dual_solution: Vec::new(),
43            bound_duals: Vec::new(),
44            objective: f64::INFINITY,
45            iterations: 0,
46            kkt_residual_rel: f64::INFINITY,
47            primal_residual_rel: f64::INFINITY,
48            bound_violation: f64::INFINITY,
49            complementarity_residual_rel: f64::INFINITY,
50            duality_gap_rel: f64::INFINITY,
51            numerical_failure: false,
52            infeasibility_status: None,
53            is_locally_optimal: false,
54            postsolve_krylov_ir_skipped: false,
55            timing: None,
56        }
57    }
58
59    /// 構造的判定 (Infeasible / Unbounded / NonConvex) を保持する outcome。
60    pub fn infeasibility(status: crate::problem::SolveStatus) -> Self {
61        debug_assert!(
62            matches!(
63                status,
64                crate::problem::SolveStatus::Infeasible
65                    | crate::problem::SolveStatus::Unbounded
66                    | crate::problem::SolveStatus::NonConvex(_)
67            ),
68            "infeasibility outcome must be Infeasible / Unbounded / NonConvex, got {:?}",
69            status
70        );
71        Self {
72            infeasibility_status: Some(status),
73            ..Self::empty()
74        }
75    }
76
77    /// Suboptimal→Optimal 昇格時の rel gap 上限 (scaling.rs::PROMOTION_GAP_TOL と整合)。
78    pub const PROMOTION_GAP_TOL: f64 = 1e-1;
79
80    pub fn satisfies_eps(&self, eps: f64) -> bool {
81        !self.solution.is_empty()
82            && !self.numerical_failure
83            && self.kkt_residual_rel <= eps
84            && self.primal_residual_rel <= eps
85            && self.bound_violation <= eps
86            && self.complementarity_residual_rel <= eps
87            && self.duality_gap_rel < Self::PROMOTION_GAP_TOL
88    }
89
90    /// satisfies_eps と整合する max-componentwise 残差 (小さいほど良い)。
91    pub fn quality_score(&self) -> f64 {
92        if self.solution.is_empty() || self.numerical_failure {
93            return f64::INFINITY;
94        }
95        self.kkt_residual_rel
96            .max(self.primal_residual_rel)
97            .max(self.bound_violation)
98            .max(self.complementarity_residual_rel)
99    }
100}
101
102/// KKT 計算に必要な要素だけを参照する軽量 view。
103///
104/// `eliminated_cols[j] == true` の col は presolve が物理削除した EmptyCol で、
105/// postsolve が `x[j]=val` を埋め戻したあとの original space stationarity 評価から除外する
106/// (bd=0 慣例で r=0 になる前提)。reduced space (IPM 内部) では `&[]` を渡す:
107/// 削除済み col は構造的に存在しないため。長さ != bounds.len() の slice は無視する。
108pub struct ProblemView<'a> {
109    pub q: &'a CscMatrix,
110    pub a: &'a CscMatrix,
111    pub c: &'a [f64],
112    pub b: &'a [f64],
113    pub bounds: &'a [(f64, f64)],
114    pub constraint_types: &'a [crate::problem::ConstraintType],
115    pub eliminated_cols: &'a [bool],
116}
117
118impl<'a> ProblemView<'a> {
119    /// presolve 情報なしで構築する (IPM internal / tests)。eliminated_cols = `&[]`。
120    pub fn from_problem(problem: &'a crate::qp::problem::QpProblem) -> Self {
121        Self {
122            q: &problem.q,
123            a: &problem.a,
124            c: &problem.c,
125            b: &problem.b,
126            bounds: &problem.bounds,
127            constraint_types: &problem.constraint_types,
128            eliminated_cols: &[],
129        }
130    }
131}