use crate::options::QpOptions;
use crate::problem::{HessianInertia, QpProblem};
use crate::solver::{ParametricActiveSetSolver, QpSolver};
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
use pounce_feral::FeralSolverInterface;
use pounce_linalg::triplet::{GenTMatrix, GenTMatrixSpace, SymTMatrix, SymTMatrixSpace};
fn new_solver() -> ParametricActiveSetSolver {
ParametricActiveSetSolver::new(Box::new(FeralSolverInterface::new()))
}
fn identity_hessian(n: usize) -> SymTMatrix {
let irows: Vec<i32> = (1..=n as i32).collect();
let jcols = irows.clone();
let space = SymTMatrixSpace::new(n as i32, irows, jcols);
let mut h = SymTMatrix::new(space);
h.set_values(&vec![1.0; n]);
h
}
fn empty_gen(m: usize, n: usize) -> GenTMatrix {
GenTMatrix::new(GenTMatrixSpace::new(
m as i32,
n as i32,
Vec::new(),
Vec::new(),
))
}
#[test]
fn problem_1_unconstrained_identity_hessian() {
let n = 3;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [1.5, -2.0, 0.25];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF; 3];
let xu = [NLP_UPPER_BOUND_INF; 3];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
let expected = [-1.5, 2.0, -0.25];
for (i, (xi, ei)) in sol.x.iter().zip(expected.iter()).enumerate() {
assert!((xi - ei).abs() < 1e-12, "x[{i}] = {xi} but expected {ei}",);
}
let expected_obj = -0.5 * g.iter().map(|gi| gi * gi).sum::<f64>();
assert!(
(sol.obj - expected_obj).abs() < 1e-12,
"obj = {} but expected {}",
sol.obj,
expected_obj
);
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert_eq!(sol.stats.n_refactor, 1);
assert_eq!(sol.stats.n_schur_updates, 0);
assert_eq!(sol.stats.n_working_set_changes, 0);
}
#[test]
fn problem_2_equality_only_full_rank() {
let n = 3;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1, 1], vec![1, 2, 3]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0, 1.0]);
let g = [0.0; 3];
let bl = [3.0]; let bu = [3.0];
let xl = [NLP_LOWER_BOUND_INF; 3];
let xu = [NLP_UPPER_BOUND_INF; 3];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
for i in 0..n {
assert!(
(sol.x[i] - 1.0).abs() < 1e-12,
"x[{i}] = {} but expected 1.0",
sol.x[i]
);
}
assert!(
(sol.lambda_g[0] + 1.0).abs() < 1e-12,
"λ_g[0] = {} but expected −1.0",
sol.lambda_g[0]
);
assert!(
(sol.obj - 1.5).abs() < 1e-12,
"obj = {} but expected 1.5",
sol.obj
);
let ax: f64 = sol.x.iter().sum();
assert!((ax - 3.0).abs() < 1e-12, "Ax = {ax} but expected 3");
assert_eq!(sol.working.constraints[0], crate::ConsStatus::Equality);
assert_eq!(sol.status, crate::QpStatus::Optimal);
}
#[test]
fn problem_2b_equality_only_non_identity_hessian() {
let n = 2;
let m = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[2.0, 4.0]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [-2.0, -8.0];
let bl = [2.0];
let bu = [2.0];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
let expected_x = [1.0 / 3.0, 5.0 / 3.0];
for (i, (xi, ei)) in sol.x.iter().zip(expected_x.iter()).enumerate() {
assert!((xi - ei).abs() < 1e-12, "x[{i}] = {xi} but expected {ei}",);
}
assert!(
(sol.lambda_g[0] - 4.0 / 3.0).abs() < 1e-12,
"λ_g[0] = {} but expected 4/3",
sol.lambda_g[0]
);
let expected_obj = -25.0 / 3.0;
assert!(
(sol.obj - expected_obj).abs() < 1e-12,
"obj = {} but expected {}",
sol.obj,
expected_obj
);
assert_eq!(sol.status, crate::QpStatus::Optimal);
}
#[test]
fn problem_3_box_constrained_diagonal_hessian() {
let n = 3;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2, 3], vec![1, 2, 3]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[2.0, 3.0, 1.0]);
let a = empty_gen(0, n);
let g = [-8.0, 6.0, -3.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [-1.0, -1.0, -1.0];
let xu = [1.0, 1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
let expected_x = [1.0, -1.0, 1.0];
for (i, (xi, ei)) in sol.x.iter().zip(expected_x.iter()).enumerate() {
assert!((xi - ei).abs() < 1e-10, "x[{i}] = {xi} but expected {ei}",);
}
let expected_lx = [-6.0, 3.0, -2.0];
for (i, (lx, ei)) in sol.lambda_x.iter().zip(expected_lx.iter()).enumerate() {
assert!(
(lx - ei).abs() < 1e-10,
"lambda_x[{i}] = {lx} but expected {ei}",
);
}
assert!(
(sol.obj - (-14.0)).abs() < 1e-10,
"obj = {} but expected -14.0",
sol.obj,
);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::AtUpper);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::AtLower);
assert_eq!(sol.working.bounds[2], crate::BoundStatus::AtUpper);
assert_eq!(sol.stats.n_working_set_changes, 3);
}
#[test]
fn box_interior_optimum_activates_no_bounds() {
let n = 2;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[2.0, 2.0]);
let a = empty_gen(0, n);
let g = [-0.5, 0.25]; let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [-1.0, -1.0];
let xu = [1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.25).abs() < 1e-12);
assert!((sol.x[1] + 0.125).abs() < 1e-12);
assert_eq!(sol.lambda_x, vec![0.0, 0.0]);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::Inactive);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::Inactive);
assert_eq!(sol.stats.n_working_set_changes, 0);
}
#[test]
fn box_one_sided_lower_bound_inactive() {
let n = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1], vec![1]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0]);
let a = empty_gen(0, n);
let g = [-4.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [1.0];
let xu = [NLP_UPPER_BOUND_INF];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 4.0).abs() < 1e-12);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::Inactive);
}
#[test]
fn box_one_sided_lower_bound_active() {
let n = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1], vec![1]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0]);
let a = empty_gen(0, n);
let g = [4.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [1.0];
let xu = [NLP_UPPER_BOUND_INF];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-12);
assert!(
(sol.lambda_x[0] - 5.0).abs() < 1e-12,
"lambda_x[0] = {} but expected 5.0",
sol.lambda_x[0]
);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::AtLower);
}
#[test]
fn box_fixed_variable_solved_in_subspace() {
let n = 2;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0, 1.0]);
let a = empty_gen(0, n);
let g = [-3.0, -2.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF, 5.0];
let xu = [NLP_UPPER_BOUND_INF, 5.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 3.0).abs() < 1e-12);
assert!((sol.x[1] - 5.0).abs() < 1e-12);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::Inactive);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::Fixed);
}
#[test]
fn eq_plus_bounds_interior_optimum() {
let n = 3;
let m = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2, 3], vec![1, 2, 3]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0; 3]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1, 1], vec![1, 2, 3]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0, 1.0]);
let g = [0.0; 3];
let bl = [0.6];
let bu = [0.6];
let xl = [-1.0; 3];
let xu = [1.0; 3];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
for (i, &xi) in sol.x.iter().enumerate() {
assert!((xi - 0.2).abs() < 1e-10, "x[{i}] = {xi} but expected 0.2",);
}
assert!(
(sol.lambda_g[0] - (-0.2)).abs() < 1e-10,
"lambda_g[0] = {} but expected -0.2",
sol.lambda_g[0]
);
for (i, &lx) in sol.lambda_x.iter().enumerate() {
assert!(lx.abs() < 1e-10, "lambda_x[{i}] = {lx} but expected 0");
}
for (i, &b) in sol.working.bounds.iter().enumerate() {
assert_eq!(
b,
crate::BoundStatus::Inactive,
"bound {i} should be inactive"
);
}
assert_eq!(sol.working.constraints[0], crate::ConsStatus::Equality);
}
#[test]
fn eq_plus_bounds_bound_active_at_init_marginal_multiplier() {
let n = 2;
let m = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0, 1.0]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [0.0, -1.0];
let bl = [1.0];
let bu = [1.0];
let xl = [0.0, NLP_LOWER_BOUND_INF];
let xu = [1.0, NLP_UPPER_BOUND_INF];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.0).abs() < 1e-10, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] - 1.0).abs() < 1e-10, "x[1] = {}", sol.x[1]);
assert!(sol.lambda_g[0].abs() < 1e-10);
assert!(sol.lambda_x[0].abs() < 1e-10);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::AtLower);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::Inactive);
assert_eq!(sol.working.constraints[0], crate::ConsStatus::Equality);
}
#[test]
fn eq_plus_bounds_with_infeasible_relaxed_init_recovers_via_elastic() {
let n = 2;
let m = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0, 1.0]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[2.0, 1.0]);
let g = [0.0, 0.0];
let bl = [1.0];
let bu = [1.0];
let xl = [-1.0, 0.5];
let xu = [1.0, 1.0];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.25).abs() < 1e-7, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] - 0.5).abs() < 1e-7, "x[1] = {}", sol.x[1]);
}
#[test]
fn general_ineq_cold_eq_relaxed_at_lower_bound() {
let n = 2;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [1.0, 1.0];
let bl = [-2.0];
let bu = [5.0];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] + 1.0).abs() < 1e-10);
assert!((sol.x[1] + 1.0).abs() < 1e-10);
assert_eq!(sol.working.constraints[0], crate::ConsStatus::AtLower);
}
#[test]
fn warm_start_general_ineq_at_optimum_returns_in_one_iter() {
let n = 2;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [1.0, 1.0];
let bl = [-1.0];
let bu = [NLP_UPPER_BOUND_INF];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![-0.5, -0.5],
lambda_g: vec![-0.5],
lambda_x: vec![0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![crate::BoundStatus::Inactive; 2],
constraints: vec![crate::ConsStatus::AtLower],
},
};
let mut solver = new_solver();
let sol = solver.solve(&qp, Some(&ws), &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] + 0.5).abs() < 1e-10, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] + 0.5).abs() < 1e-10, "x[1] = {}", sol.x[1]);
assert!(
(sol.lambda_g[0] + 0.5).abs() < 1e-10,
"lambda_g[0] = {}",
sol.lambda_g[0]
);
assert_eq!(sol.working.constraints[0], crate::ConsStatus::AtLower);
assert_eq!(sol.stats.n_working_set_changes, 0);
}
#[test]
fn warm_start_with_wrong_bound_in_working_set_drops_it() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-0.5, 0.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [0.0, 0.0];
let xu = [1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![0.0, 0.0],
lambda_g: vec![],
lambda_x: vec![0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![crate::BoundStatus::AtLower, crate::BoundStatus::AtLower],
constraints: vec![],
},
};
let mut solver = new_solver();
let sol = solver.solve(&qp, Some(&ws), &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.5).abs() < 1e-10, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] - 0.0).abs() < 1e-10, "x[1] = {}", sol.x[1]);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::Inactive);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::AtLower);
assert_eq!(sol.stats.n_working_set_changes, 1);
}
#[test]
fn schur_path_matches_refactor_path_on_binding_ineq() {
let n = 2;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [1.0, 1.0];
let bl = [-1.0];
let bu = [NLP_UPPER_BOUND_INF];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![-0.5, -0.5],
lambda_g: vec![-0.5],
lambda_x: vec![0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![crate::BoundStatus::Inactive; 2],
constraints: vec![crate::ConsStatus::AtLower],
},
};
let mut solver = new_solver();
let sol_default = solver.solve(&qp, Some(&ws), &QpOptions::default()).unwrap();
assert_eq!(sol_default.status, crate::QpStatus::Optimal);
let mut opts_schur = QpOptions::default();
opts_schur.use_schur_updates = true;
let sol_schur = solver.solve(&qp, Some(&ws), &opts_schur).unwrap();
assert_eq!(sol_schur.status, crate::QpStatus::Optimal);
for (i, (&a, &b)) in sol_default.x.iter().zip(sol_schur.x.iter()).enumerate() {
assert!((a - b).abs() < 1e-9, "x[{i}] default={a} schur={b}",);
}
assert!((sol_default.lambda_g[0] - sol_schur.lambda_g[0]).abs() < 1e-9);
assert!((sol_default.obj - sol_schur.obj).abs() < 1e-9);
assert_eq!(sol_default.working, sol_schur.working);
}
#[test]
fn schur_path_matches_refactor_path_on_drop_test() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-0.5, 0.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [0.0, 0.0];
let xu = [1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![0.0, 0.0],
lambda_g: vec![],
lambda_x: vec![0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![crate::BoundStatus::AtLower, crate::BoundStatus::AtLower],
constraints: vec![],
},
};
let mut solver = new_solver();
let mut opts_schur = QpOptions::default();
opts_schur.use_schur_updates = true;
let sol = solver.solve(&qp, Some(&ws), &opts_schur).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.5).abs() < 1e-9, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] - 0.0).abs() < 1e-9, "x[1] = {}", sol.x[1]);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::Inactive);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::AtLower);
assert!(
sol.stats.n_schur_updates > 0,
"expected ≥1 Schur update, got {}",
sol.stats.n_schur_updates
);
}
#[test]
fn schur_multi_step_add_drop_add_matches_fresh_factor() {
let n = 3;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-0.25, -0.6, -0.9];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [0.0, 0.0, 0.0];
let xu = [1.0, 1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![0.0, 0.0, 0.0],
lambda_g: vec![],
lambda_x: vec![0.0, 0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![
crate::BoundStatus::AtLower,
crate::BoundStatus::AtLower,
crate::BoundStatus::AtLower,
],
constraints: vec![],
},
};
let mut solver = new_solver();
let mut opts_default = QpOptions::default();
opts_default.use_schur_updates = false;
let sol_default = solver.solve(&qp, Some(&ws), &opts_default).unwrap();
assert_eq!(sol_default.status, crate::QpStatus::Optimal);
let mut solver_b = new_solver();
let mut opts_schur = QpOptions::default();
opts_schur.use_schur_updates = true;
let sol_schur = solver_b.solve(&qp, Some(&ws), &opts_schur).unwrap();
assert_eq!(sol_schur.status, crate::QpStatus::Optimal);
for i in 0..n {
assert!(
(sol_default.x[i] - sol_schur.x[i]).abs() < 1e-9,
"x[{i}]: refactor = {}, schur = {}",
sol_default.x[i],
sol_schur.x[i],
);
}
assert!((sol_default.obj - sol_schur.obj).abs() < 1e-9);
assert!(
sol_schur.stats.n_schur_updates >= 2,
"expected ≥2 Schur updates, got {}",
sol_schur.stats.n_schur_updates
);
}
#[test]
fn solve_with_working_set_recovers_optimum_from_active_set_seed() {
let n = 2;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [-1.0, -2.0];
let bl = [1.0];
let bu = [1.0];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let working = crate::WorkingSet {
bounds: vec![crate::BoundStatus::Inactive; 2],
constraints: vec![crate::ConsStatus::Equality],
};
let mut solver = new_solver();
let sol = solver
.solve_with_working_set(&qp, &working, &QpOptions::default())
.unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.0).abs() < 1e-10);
assert!((sol.x[1] - 1.0).abs() < 1e-10);
assert!(
(sol.lambda_g[0] - 1.0).abs() < 1e-10,
"lambda_g[0] = {} (expected 1.0)",
sol.lambda_g[0]
);
}
#[test]
fn anti_cycling_expand_two_pass_converges_at_degenerate_vertex() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-1.0, -1.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF, NLP_LOWER_BOUND_INF];
let xu = [0.5, 0.5];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol_default = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol_default.status, crate::QpStatus::Optimal);
assert!((sol_default.x[0] - 0.5).abs() < 1e-10);
assert!((sol_default.x[1] - 0.5).abs() < 1e-10);
let opts_expand = crate::QpOptions {
anti_cycling: crate::AntiCyclingChoice::Expand,
..QpOptions::default()
};
let sol_expand = solver.solve(&qp, None, &opts_expand).unwrap();
assert_eq!(sol_expand.status, crate::QpStatus::Optimal);
assert!((sol_expand.x[0] - 0.5).abs() < 1e-10);
assert!((sol_expand.x[1] - 0.5).abs() < 1e-10);
}
#[test]
fn expand_tau_growth_does_not_break_correctness() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-1.0, -1.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF, NLP_LOWER_BOUND_INF];
let xu = [0.5, 0.5];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let opts = crate::QpOptions {
anti_cycling: crate::AntiCyclingChoice::Expand,
expand_tol_initial: 1e-12,
expand_tol_growth: 1e-8, expand_tol_max: 1e-7, ..crate::QpOptions::default()
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &opts).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.5).abs() < 1e-9);
assert!((sol.x[1] - 0.5).abs() < 1e-9);
}
#[test]
fn anti_cycling_expand_single_blocker_matches_default() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-2.0, -1.0]; let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [1.0, NLP_UPPER_BOUND_INF];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let opts_expand = crate::QpOptions {
anti_cycling: crate::AntiCyclingChoice::Expand,
..QpOptions::default()
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &opts_expand).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-10);
assert!((sol.x[1] - 1.0).abs() < 1e-10);
assert_eq!(sol.working.bounds[0], crate::BoundStatus::AtUpper);
assert_eq!(sol.working.bounds[1], crate::BoundStatus::Inactive);
}
#[test]
fn anti_cycling_bland_picks_lowest_indexed_violation() {
let n = 2;
let h = identity_hessian(n);
let a = empty_gen(0, n);
let g = [-0.25, -0.5];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [0.0, 0.0];
let xu = [1.0, 1.0];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let ws = crate::QpWarmStart {
x: vec![0.0, 0.0],
lambda_g: vec![],
lambda_x: vec![0.0, 0.0],
working: crate::WorkingSet {
bounds: vec![crate::BoundStatus::AtLower, crate::BoundStatus::AtLower],
constraints: vec![],
},
};
let mut solver = new_solver();
let sol_default = solver.solve(&qp, Some(&ws), &QpOptions::default()).unwrap();
assert_eq!(sol_default.status, crate::QpStatus::Optimal);
assert!((sol_default.x[0] - 0.25).abs() < 1e-10);
assert!((sol_default.x[1] - 0.5).abs() < 1e-10);
let opts_bland = crate::QpOptions {
anti_cycling: crate::AntiCyclingChoice::Bland,
..QpOptions::default()
};
let sol_bland = solver.solve(&qp, Some(&ws), &opts_bland).unwrap();
assert_eq!(sol_bland.status, crate::QpStatus::Optimal);
assert!((sol_bland.x[0] - 0.25).abs() < 1e-10);
assert!((sol_bland.x[1] - 0.5).abs() < 1e-10);
}
#[test]
fn general_ineq_solved_via_l1_elastic_when_cold_infeasible() {
let n = 2;
let m = 1;
let h = identity_hessian(n);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [0.0, 0.0];
let bl = [1.0];
let bu = [NLP_UPPER_BOUND_INF];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 0.5).abs() < 1e-6, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] - 0.5).abs() < 1e-6, "x[1] = {}", sol.x[1]);
assert!(sol.stats.used_phase1, "elastic mode should have been used");
assert_eq!(sol.working.constraints[0], crate::ConsStatus::AtLower);
}
#[test]
fn problem_6_indefinite_h_with_pd_reduced_hessian() {
let n = 2;
let m = 1;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[-1.0, 2.0]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 1], vec![1, 2]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [0.0, 0.0];
let bl = [1.0];
let bu = [1.0];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Indefinite,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[0] - 2.0).abs() < 1e-10, "x[0] = {}", sol.x[0]);
assert!((sol.x[1] + 1.0).abs() < 1e-10, "x[1] = {}", sol.x[1]);
assert!(
(sol.lambda_g[0] - 2.0).abs() < 1e-10,
"lambda_g[0] = {}",
sol.lambda_g[0]
);
assert!(
(sol.obj + 1.0).abs() < 1e-10,
"obj = {} but expected -1.0",
sol.obj
);
}
#[test]
fn inertia_control_shift_succeeds_on_psd_singular_hessian() {
let n = 2;
let h_space = SymTMatrixSpace::new(n as i32, vec![1, 2], vec![1, 2]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[0.0, 1.0]);
let a = empty_gen(0, n);
let g = [-1.0, -2.0];
let bl: [f64; 0] = [];
let bu: [f64; 0] = [];
let xl = [NLP_LOWER_BOUND_INF; 2];
let xu = [NLP_UPPER_BOUND_INF; 2];
let qp = QpProblem {
n,
m: 0,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Indefinite,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(sol.status, crate::QpStatus::Optimal);
assert!((sol.x[1] - 2.0).abs() < 1e-6, "x[1] = {}", sol.x[1]);
assert!(
sol.x[0] > 1.0,
"x[0] = {} should be large (≈ 1/δ_final after shift)",
sol.x[0]
);
}
#[test]
fn problem_5_infeasibility_certified_by_elastic_mode() {
let n = 1;
let m = 2;
let h_space = SymTMatrixSpace::new(n as i32, vec![1], vec![1]);
let mut h = SymTMatrix::new(h_space);
h.set_values(&[1.0]);
let a_space = GenTMatrixSpace::new(m as i32, n as i32, vec![1, 2], vec![1, 1]);
let mut a = GenTMatrix::new(a_space);
a.set_values(&[1.0, 1.0]);
let g = [0.0];
let bl = [5.0, NLP_LOWER_BOUND_INF];
let bu = [NLP_UPPER_BOUND_INF, 3.0];
let xl = [NLP_LOWER_BOUND_INF];
let xu = [NLP_UPPER_BOUND_INF];
let qp = QpProblem {
n,
m,
h: &h,
g: &g,
a: &a,
bl: &bl,
bu: &bu,
xl: &xl,
xu: &xu,
hessian_inertia: HessianInertia::Psd,
};
let mut solver = new_solver();
let sol = solver.solve(&qp, None, &QpOptions::default()).unwrap();
assert_eq!(
sol.status,
crate::QpStatus::Infeasible,
"expected Infeasible; got {:?}",
sol.status
);
assert!(
sol.stats.used_phase1,
"elastic mode should have run for an infeasible problem"
);
assert!(
(sol.x[0] - 3.0).abs() < 1e-6,
"x = {} but expected 3.0 (the minimum-violation point)",
sol.x[0]
);
}