use crate::problem::SolverResult;
use crate::qp::QpProblem;
use super::qp_transforms::{QpPostsolveStep, QpPresolveResult};
pub fn postsolve_qp(presolve_result: &QpPresolveResult, reduced_sol: &SolverResult) -> SolverResult {
let n = presolve_result.orig_num_vars;
let m = presolve_result.orig_num_constraints;
let mut solution = vec![0.0f64; n];
for (j, &maybe_jj) in presolve_result.col_map.iter().enumerate() {
if let Some(jj) = maybe_jj {
if jj < reduced_sol.solution.len() {
solution[j] = reduced_sol.solution[jj];
}
}
}
let mut reduced_dual = reduced_sol.dual_solution.clone();
for step in presolve_result.postsolve_stack.steps.iter().rev() {
match step {
QpPostsolveStep::FixedVar { idx, val, .. } => {
solution[*idx] = *val;
}
QpPostsolveStep::SingletonRow { col, val, .. } => {
solution[*col] = *val;
}
QpPostsolveStep::SingletonIneqToBound { .. } => {
}
QpPostsolveStep::EmptyCol { idx, val } => {
solution[*idx] = *val;
}
QpPostsolveStep::LargeCoeffRowScale { row_scales } => {
for (i, y) in reduced_dual.iter_mut().enumerate() {
if i < row_scales.len() {
*y *= row_scales[i];
}
}
}
}
}
let mut dual_solution = vec![0.0f64; m];
for (i, &maybe_ii) in presolve_result.row_map.iter().enumerate() {
if let Some(ii) = maybe_ii {
if ii < reduced_dual.len() {
dual_solution[i] = reduced_dual[ii];
}
}
}
let objective = reduced_sol.objective + presolve_result.obj_offset;
let reduced_costs = if !reduced_sol.reduced_costs.is_empty() {
let mut rc = vec![0.0f64; n];
for (j, &maybe_jj) in presolve_result.col_map.iter().enumerate() {
if let Some(jj) = maybe_jj {
if jj < reduced_sol.reduced_costs.len() {
rc[j] = reduced_sol.reduced_costs[jj];
}
}
}
rc
} else if presolve_result.reduced.num_vars == 0 && n > 0 {
vec![0.0f64; n]
} else {
vec![]
};
SolverResult {
status: reduced_sol.status.clone(),
objective,
solution,
dual_solution,
bound_duals: reduced_sol.bound_duals.clone(),
iterations: reduced_sol.iterations,
final_residuals: reduced_sol.final_residuals,
pfeas: reduced_sol.pfeas,
dfeas: reduced_sol.dfeas,
gap: reduced_sol.gap,
duality_gap_rel: reduced_sol.duality_gap_rel,
reduced_costs,
slack: reduced_sol.slack.clone(),
warm_start_basis: reduced_sol.warm_start_basis.clone(),
timing_breakdown: reduced_sol.timing_breakdown,
postsolve_dfeas: None,
stats: reduced_sol.stats.clone(),
bound_gap_cert: None,
}
}
pub fn postsolve_qp_with_dual_recovery(
presolve_result: &QpPresolveResult,
reduced_sol: &SolverResult,
orig_problem: &QpProblem,
) -> SolverResult {
let mut sol = postsolve_qp(presolve_result, reduced_sol);
if sol.solution.len() != orig_problem.num_vars {
return sol;
}
let n = orig_problem.num_vars;
let _ = n;
const RECOVER_PASSES: usize = 1;
let trace = std::env::var("POSTSOLVE_TRACE").ok().as_deref() == Some("1");
if trace {
let kkt = simple_kkt_inf(&orig_problem.q, &orig_problem.a, &orig_problem.c, &orig_problem.bounds, &sol);
eprintln!("POSTSOLVE [after dim expand, before recovery] kkt_inf={:.3e}", kkt);
}
for pass in 0..RECOVER_PASSES {
for step in presolve_result.postsolve_stack.steps.iter().rev() {
match step {
QpPostsolveStep::SingletonRow { row, col, .. } => {
recover_y_for_singleton_row(*row, *col, orig_problem, &mut sol);
}
QpPostsolveStep::SingletonIneqToBound { row, col, ct, .. } => {
recover_y_for_singleton_row(*row, *col, orig_problem, &mut sol);
let y = sol.dual_solution[*row];
sol.dual_solution[*row] = match ct {
crate::problem::ConstraintType::Le => y.max(0.0),
crate::problem::ConstraintType::Ge => y.min(0.0),
_ => y,
};
}
_ => {}
}
}
if trace {
let kkt = simple_kkt_inf(&orig_problem.q, &orig_problem.a, &orig_problem.c, &orig_problem.bounds, &sol);
eprintln!("POSTSOLVE [after recovery pass {}] kkt_inf={:.3e}", pass, kkt);
}
}
sol
}
fn simple_kkt_inf(
q: &crate::sparse::CscMatrix,
a: &crate::sparse::CscMatrix,
c: &[f64],
bounds: &[(f64, f64)],
sol: &SolverResult,
) -> f64 {
use twofloat::TwoFloat;
let n = bounds.len();
if sol.solution.len() != n { return f64::NAN; }
let zero_dd = TwoFloat::from(0.0);
let mut qx_dd: Vec<TwoFloat> = vec![zero_dd; n];
for col in 0..n {
let xv = sol.solution[col];
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
qx_dd[q.row_ind[k]] = qx_dd[q.row_ind[k]] + TwoFloat::new_mul(q.values[k], xv);
}
}
let mut aty_dd: Vec<TwoFloat> = vec![zero_dd; n];
if a.nrows > 0 && !sol.dual_solution.is_empty() {
for col in 0..n {
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
aty_dd[col] = aty_dd[col] + TwoFloat::new_mul(a.values[k], sol.dual_solution[a.row_ind[k]]);
}
}
}
let mut max_r = 0.0_f64;
for j in 0..n {
let (lb, ub) = bounds[j];
if lb.is_finite() && ub.is_finite() && (lb - ub).abs() < 1e-12 { continue; }
let r = qx_dd[j] + TwoFloat::from(c[j]) + aty_dd[j];
max_r = max_r.max(f64::from(r).abs());
}
max_r
}
pub(crate) fn recover_y_for_singleton_row(
row: usize,
col: usize,
orig: &QpProblem,
sol: &mut SolverResult,
) {
recover_y_for_singleton_row_with_bound(row, col, orig, sol, 0.0);
}
pub(crate) fn recover_y_for_singleton_row_with_bound(
row: usize,
col: usize,
orig: &QpProblem,
sol: &mut SolverResult,
bound_contrib_col: f64,
) {
if row >= orig.num_constraints || col >= orig.num_vars {
return;
}
if sol.dual_solution.len() != orig.num_constraints {
return;
}
let mut a_row_col = 0.0_f64;
let s = orig.a.col_ptr[col];
let e = orig.a.col_ptr[col + 1];
for k in s..e {
if orig.a.row_ind[k] == row {
a_row_col = orig.a.values[k];
break;
}
}
if a_row_col.abs() < 1e-30 {
return;
}
use twofloat::TwoFloat;
let qx_col = compute_qx_at(&orig.q, &sol.solution, col);
let mut aty_others_dd = TwoFloat::from(0.0);
for k in s..e {
let r = orig.a.row_ind[k];
if r == row { continue; }
aty_others_dd = aty_others_dd + TwoFloat::new_mul(orig.a.values[k], sol.dual_solution[r]);
}
let aty_col_others = f64::from(aty_others_dd);
let target = -(qx_col + orig.c[col] + aty_col_others + bound_contrib_col);
let y_new = target / a_row_col;
if y_new.is_finite() {
sol.dual_solution[row] = y_new;
}
}
pub(crate) fn bound_contrib_at_var(
bounds: &[(f64, f64)],
bound_duals: &[f64],
var: usize,
) -> f64 {
if bound_duals.is_empty() {
return 0.0;
}
let n_lb_total = bounds.iter().filter(|&&(lb, _)| lb.is_finite()).count();
let mut contrib = 0.0_f64;
let mut lb_idx = 0_usize;
let mut ub_idx = n_lb_total;
for (j, &(lb, ub)) in bounds.iter().enumerate() {
if lb.is_finite() {
if j == var && lb_idx < bound_duals.len() {
contrib -= bound_duals[lb_idx];
}
lb_idx += 1;
}
if ub.is_finite() {
if j == var && ub_idx < bound_duals.len() {
contrib += bound_duals[ub_idx];
}
ub_idx += 1;
}
}
contrib
}
fn compute_qx_at(q: &crate::sparse::CscMatrix, x: &[f64], col: usize) -> f64 {
use twofloat::TwoFloat;
let mut sum = TwoFloat::from(0.0);
let s = q.col_ptr[col];
let e = q.col_ptr[col + 1];
for ptr in s..e {
let k = q.row_ind[ptr];
sum = sum + TwoFloat::new_mul(q.values[ptr], x[k]);
}
f64::from(sum)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::presolve::qp_transforms::run_qp_presolve_phase1;
use crate::options::SolverOptions;
use crate::problem::SolveStatus;
use crate::qp::{solve_qp_with, QpProblem};
use crate::sparse::CscMatrix;
#[test]
fn test_postsolve_fixed_var() {
let n = 2usize;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], n, n).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::new(0, n);
let b = vec![];
let bounds = vec![(0.0_f64, 1.0_f64), (0.5_f64, 0.5_f64)]; let prob = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions::default();
let presolve_result = run_qp_presolve_phase1(&prob, &opts);
assert_eq!(presolve_result.reduced.num_vars, 1, "y fixed → 1 var");
let reduced_sol = solve_qp_with(&presolve_result.reduced, &opts);
assert_eq!(reduced_sol.status, SolveStatus::Optimal, "reduced sol optimal");
let final_sol = postsolve_qp(&presolve_result, &reduced_sol);
assert_eq!(final_sol.solution.len(), 2, "restored to 2 vars");
assert!((final_sol.solution[1] - 0.5).abs() < 1e-8, "y=0.5 restored");
assert_eq!(final_sol.status, SolveStatus::Optimal);
}
#[test]
fn test_postsolve_dual_row_map() {
let n = 2usize;
let m = 2usize;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], n, n).unwrap();
let c = vec![0.0f64; n];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], m, n).unwrap();
let b = vec![3.0f64, 5.0f64];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); n];
let prob = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions::default();
let presolve_result = run_qp_presolve_phase1(&prob, &opts);
assert_eq!(presolve_result.orig_num_constraints, 2, "orig m=2");
let m_reduced = presolve_result.reduced.num_constraints;
assert_eq!(m_reduced, 1, "empty row removed → m_reduced=1");
let final_sol = solve_qp_with(&prob, &opts);
assert_eq!(final_sol.status, SolveStatus::Optimal);
assert_eq!(final_sol.dual_solution.len(), 2,
"dual_solution must have orig_num_constraints length after postsolve");
}
#[test]
fn test_postsolve_dual_value_correctness() {
let n = 2usize;
let m = 1usize;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], n, n).unwrap();
let c = vec![-2.0f64, -2.0f64]; let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], m, n).unwrap();
let b = vec![2.0f64];
let bounds = vec![(0.0_f64, f64::INFINITY); n];
let prob = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions::default();
let result = solve_qp_with(&prob, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.solution[0] - 1.0).abs() < 1e-6, "x=1");
assert!((result.solution[1] - 1.0).abs() < 1e-6, "y=1");
assert_eq!(result.dual_solution.len(), 1, "dual len=1");
assert!(result.dual_solution[0] >= -1e-6, "dual >= 0");
assert!((result.dual_solution[0] - 1.0).abs() < 1e-4,
"dual ≈ 1.0, got {}", result.dual_solution[0]);
}
#[test]
fn test_postsolve_singleton_row_dual_recovery() {
let n = 3usize;
let m = 2usize;
let q = CscMatrix::from_triplets(&[1, 2], &[1, 2], &[1.0, 1.0], n, n).unwrap();
let c = vec![0.0, -2.0, -2.0];
let a = CscMatrix::from_triplets(
&[0, 1, 1, 1],
&[0, 0, 1, 2],
&[3.0, 1.0, 1.0, 1.0],
m, n,
).unwrap();
let b = vec![6.0, 5.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); n]; let constraint_types = vec![
crate::problem::ConstraintType::Eq,
crate::problem::ConstraintType::Eq,
];
let prob = QpProblem::new(q, c, a, b, bounds, constraint_types).unwrap();
let opts = SolverOptions::default();
let result = solve_qp_with(&prob, &opts);
assert_eq!(result.status, SolveStatus::Optimal, "should converge");
assert!((result.solution[0] - 2.0).abs() < 1e-5, "x≈2 (fixed by singleton), got {}", result.solution[0]);
assert!((result.solution[1] - 1.5).abs() < 1e-5, "y≈1.5, got {}", result.solution[1]);
assert!((result.solution[2] - 1.5).abs() < 1e-5, "z≈1.5, got {}", result.solution[2]);
let qx = prob.q.mat_vec_mul(&result.solution).unwrap();
let aty = prob.a.transpose().mat_vec_mul(&result.dual_solution).unwrap();
let mut max_rd = 0.0_f64;
for j in 0..n {
let r = (qx[j] + prob.c[j] + aty[j]).abs();
max_rd = max_rd.max(r);
}
assert!(
max_rd < 1e-6,
"max KKT residual should be ≈ 0, got {} (bug: postsolve dropped singleton-row dual)",
max_rd
);
}
#[test]
fn test_postsolve_dual_recovery_ill_scaled() {
let n = 4usize;
let m = 3usize;
let q = CscMatrix::from_triplets(&[1, 2, 3], &[1, 2, 3], &[1.0, 1.0, 1.0], n, n).unwrap();
let c = vec![0.0, -2.0, -2.0, -2.0];
let a = CscMatrix::from_triplets(
&[0, 1, 1, 2, 2],
&[0, 0, 1, 2, 3],
&[1e-3, 1e6, 1.0, 1.0, 1.0],
m, n,
).unwrap();
let b = vec![1e-3, 1e6 + 1.0, 2.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); n];
let constraint_types = vec![crate::problem::ConstraintType::Eq; 3];
let prob = QpProblem::new(q, c, a, b, bounds, constraint_types).unwrap();
let opts = SolverOptions::default();
let result = solve_qp_with(&prob, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.solution[0] - 1.0).abs() < 1e-5);
assert!((result.solution[1] - 1.0).abs() < 1e-5);
assert!((result.solution[2] - 1.0).abs() < 1e-5);
assert!((result.solution[3] - 1.0).abs() < 1e-5);
let qx = prob.q.mat_vec_mul(&result.solution).unwrap();
let aty = prob.a.transpose().mat_vec_mul(&result.dual_solution).unwrap();
let mut max_rd = 0.0_f64;
let mut max_aty = 0.0_f64;
for j in 0..n {
let r = (qx[j] + prob.c[j] + aty[j]).abs();
max_rd = max_rd.max(r);
max_aty = max_aty.max(aty[j].abs());
}
let denom = (1.0_f64).max(max_aty);
let rel = max_rd / denom;
assert!(
rel < 1e-5,
"ill-scaled でも relative KKT 残差は 1e-5 以下を維持すべき (got {})",
rel
);
}
#[test]
fn test_postsolve_preserves_residuals() {
let n = 2usize;
let m = 1usize;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], n, n).unwrap();
let c = vec![-2.0f64, -2.0f64];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], m, n).unwrap();
let b = vec![2.0f64];
let bounds = vec![(0.0_f64, f64::INFINITY); n];
let prob = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions::default();
let presolve_result = run_qp_presolve_phase1(&prob, &opts);
let reduced_sol = solve_qp_with(&presolve_result.reduced, &opts);
assert_eq!(reduced_sol.status, SolveStatus::Optimal);
let final_sol = postsolve_qp(&presolve_result, &reduced_sol);
if let Some((pf, df, g)) = final_sol.final_residuals {
assert!(final_sol.pfeas.is_some(), "pfeas must be preserved after postsolve");
assert!(final_sol.dfeas.is_some(), "dfeas must be preserved after postsolve");
assert!(final_sol.gap.is_some(), "gap must be preserved after postsolve");
assert_eq!(final_sol.pfeas.unwrap(), pf, "pfeas must match final_residuals");
assert_eq!(final_sol.dfeas.unwrap(), df, "dfeas must match final_residuals");
assert_eq!(final_sol.gap.unwrap(), g, "gap must match final_residuals");
}
}
#[test]
fn test_recover_y_for_singleton_row_zeroes_stationarity() {
use crate::problem::ConstraintType;
let n = 1usize;
let m = 2usize;
let q = CscMatrix::new(n, n);
let c = vec![5.0_f64];
let a = CscMatrix::from_triplets(&[0, 1], &[0, 0], &[2.0_f64, 1.0], m, n).unwrap();
let b = vec![4.0_f64, 10.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY)];
let cts = vec![ConstraintType::Eq, ConstraintType::Le];
let prob = QpProblem::new(q, c, a, b, bounds, cts).unwrap();
let mut sol = SolverResult {
status: SolveStatus::Optimal,
solution: vec![2.0],
dual_solution: vec![0.0, 0.0],
bound_duals: vec![],
..SolverResult::default()
};
recover_y_for_singleton_row(0, 0, &prob, &mut sol);
assert!((sol.dual_solution[0] - (-2.5)).abs() < 1e-12,
"y[0] should be -2.5, got {}", sol.dual_solution[0]);
let aty0 = 2.0 * sol.dual_solution[0] + 1.0 * sol.dual_solution[1];
let stat = 0.0 + 5.0 + aty0;
assert!(stat.abs() < 1e-12, "stationarity zeroed after recovery, got {}", stat);
}
#[test]
fn test_compute_qx_at_symmetric_q() {
let q = CscMatrix::from_triplets(
&[0, 1, 0, 1], &[0, 0, 1, 1], &[2.0_f64, 3.0, 3.0, 4.0], 2, 2,
).unwrap();
let x = vec![1.0_f64, 1.0];
assert!((compute_qx_at(&q, &x, 0) - 5.0).abs() < 1e-12);
assert!((compute_qx_at(&q, &x, 1) - 7.0).abs() < 1e-12);
}
}