#![allow(clippy::print_stdout, clippy::print_stderr)]
use super::iter::solve_ippmm_inner;
use super::state::{warm_bound_margin, WARM_BOUND_REL_MARGIN};
use super::warm_start::apply_qp_warm_start;
use crate::options::{QpWarmStart, SolverOptions};
use crate::problem::{ConstraintType, SolveStatus};
use crate::qp::ipm_core::kkt::build_extended_constraints;
use crate::qp::problem::QpProblem;
use crate::sparse::CscMatrix;
const EPS: f64 = 1e-4;
fn close(a: f64, b: f64, name: &str) {
assert!(
(a - b).abs() < EPS,
"{}: expected {:.8}, got {:.8} (diff={:.2e})",
name,
b,
a,
(a - b).abs()
);
}
fn default_opts() -> SolverOptions {
SolverOptions {
timeout_secs: Some(10.0),
use_ruiz_scaling: false,
..Default::default()
}
}
#[test]
fn test_ippmm_basic_2d() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = solve_ippmm_inner(&problem, &default_opts(), default_opts().ipm_eps());
assert_eq!(result.status, SolveStatus::Optimal, "IPPMM-T1: status");
close(result.solution[0], 0.5, "IPPMM-T1: x[0]");
close(result.solution[1], 0.5, "IPPMM-T1: x[1]");
close(result.objective, 0.5, "IPPMM-T1: objective");
}
#[test]
fn test_ippmm_unconstrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![-6.0, -8.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = solve_ippmm_inner(&problem, &default_opts(), default_opts().ipm_eps());
assert_eq!(result.status, SolveStatus::Optimal, "IPPMM-T2: status");
close(result.solution[0], 3.0, "IPPMM-T2: x[0]");
close(result.solution[1], 4.0, "IPPMM-T2: x[1]");
close(result.objective, -25.0, "IPPMM-T2: objective");
}
#[test]
fn test_ippmm_equality_constraint() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0, 1.0, -1.0, -1.0], 2, 2)
.unwrap();
let b = vec![1.0, -1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = solve_ippmm_inner(&problem, &default_opts(), default_opts().ipm_eps());
assert_eq!(result.status, SolveStatus::Optimal, "IPPMM-T3: status");
close(result.solution[0], 0.5, "IPPMM-T3: x[0]");
close(result.solution[1], 0.5, "IPPMM-T3: x[1]");
close(result.objective, 0.5, "IPPMM-T3: objective");
}
#[test]
fn test_ippmm_box_constrained() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![-4.0, -4.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(0.0_f64, 1.0_f64); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let result = solve_ippmm_inner(&problem, &default_opts(), default_opts().ipm_eps());
assert_eq!(result.status, SolveStatus::Optimal, "IPPMM-T4: status");
close(result.solution[0], 1.0, "IPPMM-T4: x[0]");
close(result.solution[1], 1.0, "IPPMM-T4: x[1]");
close(result.objective, -6.0, "IPPMM-T4: objective");
}
#[test]
fn test_ippmm_timeout() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
let opts = SolverOptions {
timeout_secs: Some(0.0001),
use_ruiz_scaling: false,
..Default::default()
};
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert!(
result.status == SolveStatus::Timeout || result.status == SolveStatus::Optimal,
"IPPMM-T5: expected Timeout or Optimal, got {:?}",
result.status
);
}
#[test]
fn test_ippmm_eq_convergence_check() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let b = vec![1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(q, c, a, b, bounds, vec![ConstraintType::Eq]).unwrap();
let opts = SolverOptions {
timeout_secs: Some(5.0),
use_ruiz_scaling: false,
..Default::default()
};
let start = std::time::Instant::now();
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert!(
start.elapsed().as_secs_f64() < 6.0,
"Test exceeded 6 second wall-clock limit"
);
assert_eq!(result.status, SolveStatus::Optimal, "conv-eq: status");
close(result.solution[0], 0.5, "conv-eq: x[0]");
close(result.solution[1], 0.5, "conv-eq: x[1]");
}
#[test]
fn test_ippmm_le_convergence_check() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(q, c, a, b, bounds, vec![ConstraintType::Le]).unwrap();
let opts = SolverOptions {
timeout_secs: Some(5.0),
use_ruiz_scaling: false,
..Default::default()
};
let start = std::time::Instant::now();
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert!(
start.elapsed().as_secs_f64() < 6.0,
"Test exceeded 6 second wall-clock limit"
);
assert_eq!(result.status, SolveStatus::Optimal, "conv-le: status");
close(result.solution[0], 0.5, "conv-le: x[0]");
close(result.solution[1], 0.5, "conv-le: x[1]");
}
#[test]
fn test_ippmm_ge_defensive() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let b = vec![1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(q, c, a, b, bounds, vec![ConstraintType::Ge]).unwrap();
let opts = SolverOptions {
timeout_secs: Some(5.0),
use_ruiz_scaling: false,
..Default::default()
};
let start = std::time::Instant::now();
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert!(
start.elapsed().as_secs_f64() < 6.0,
"Test exceeded 6 second wall-clock limit"
);
assert_eq!(result.status, SolveStatus::Optimal, "ge-defensive: status");
close(result.solution[0], 0.5, "ge-defensive: x[0]");
close(result.solution[1], 0.5, "ge-defensive: x[1]");
}
#[test]
fn test_ippmm_empty_constraints() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], 2, 2).unwrap();
let c = vec![-1.0, -1.0];
let a = CscMatrix::new(0, 2);
let b: Vec<f64> = vec![];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new(q, c, a, b, bounds, vec![]).unwrap();
let opts = SolverOptions {
timeout_secs: Some(5.0),
use_ruiz_scaling: false,
..Default::default()
};
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert_eq!(
result.status,
SolveStatus::Optimal,
"empty-constraints: status"
);
close(result.solution[0], 1.0, "empty-constraints: x[0]");
close(result.solution[1], 1.0, "empty-constraints: x[1]");
}
#[test]
fn test_ippmm_multiple_equality_constraints() {
let q = CscMatrix::from_triplets(&[0, 1, 2], &[0, 1, 2], &[2.0, 2.0, 2.0], 3, 3).unwrap();
let c = vec![0.0, 0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 1, 2], &[1.0, 1.0, 1.0, 1.0], 2, 3)
.unwrap();
let b = vec![1.0, 1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 3];
let problem = QpProblem::new(
q,
c,
a,
b,
bounds,
vec![ConstraintType::Eq, ConstraintType::Eq],
)
.unwrap();
let opts = SolverOptions {
timeout_secs: Some(5.0),
use_ruiz_scaling: false,
..Default::default()
};
let result = solve_ippmm_inner(&problem, &opts, opts.ipm_eps());
assert_eq!(result.status, SolveStatus::Optimal, "multi-eq: status");
close(result.solution[0], 1.0 / 3.0, "multi-eq: x[0]");
close(result.solution[1], 2.0 / 3.0, "multi-eq: x[1]");
close(result.solution[2], 1.0 / 3.0, "multi-eq: x[2]");
}
#[test]
fn test_warm_bound_margin_scale_tracking() {
let cases = [
(0.0_f64, WARM_BOUND_REL_MARGIN),
(0.25_f64, WARM_BOUND_REL_MARGIN),
(-0.5_f64, WARM_BOUND_REL_MARGIN),
(1.0_f64, WARM_BOUND_REL_MARGIN),
(-1.0_f64, WARM_BOUND_REL_MARGIN),
(10.0_f64, WARM_BOUND_REL_MARGIN * 10.0),
(1e3_f64, WARM_BOUND_REL_MARGIN * 1e3),
(-1e6_f64, WARM_BOUND_REL_MARGIN * 1e6),
(1e8_f64, WARM_BOUND_REL_MARGIN * 1e8),
(1e11_f64, WARM_BOUND_REL_MARGIN * 1e11),
];
for (b, expected) in cases {
let got = warm_bound_margin(b);
let denom = expected.abs().max(WARM_BOUND_REL_MARGIN);
assert!(
((got - expected) / denom).abs() < 1e-12,
"warm_bound_margin({}) = {:.6e}, expected {:.6e} (旧 ABS=1.0 fixed retention 疑い)",
b,
got,
expected,
);
}
assert!(
warm_bound_margin(1.0) < 1e-3,
"|b|=1 で margin={:.3e} は old absolute 1.0 を残している",
warm_bound_margin(1.0),
);
assert!(
warm_bound_margin(1e8) > 10.0,
"|b|=1e8 で margin={:.3e} は old absolute 1.0 を残している",
warm_bound_margin(1e8),
);
}
#[test]
fn test_warm_start_half_finite_bound_scale_tracking() {
let cases: [(f64, f64, f64, &str); 5] = [
(1.0, 1.5, 1.5, "|lb|=1 well-interior"),
(0.0, 0.1, 0.1, "|lb|=0 small warm"),
(10.0, 10.1, 10.1, "|lb|=10 close warm"),
(1e8, 1e8 + 50.0, 1e8 + 100.0, "|lb|=1e8 needs scaled push"),
(
1e6,
0.999e6,
1e6 + 1.0,
"|lb|=1e6 below bound floors at lb+margin",
),
];
for (lb, xj, expected, name) in cases {
let problem = build_single_var_lb_problem(lb);
let x_out = apply_warm_and_extract_x(&problem, xj);
assert!(
(x_out - expected).abs() < 1e-9 * expected.abs().max(1.0),
"{}: x={:.6e}, expected {:.6e} (lb={:.1e}, xj_warm={:.6e})",
name,
x_out,
expected,
lb,
xj,
);
}
}
#[test]
fn test_warm_start_both_finite_bound_scale_tracking() {
{
let problem = build_single_var_box_problem(-1e10, 1e10);
let x_out = apply_warm_and_extract_x(&problem, 1e10 - 1.0);
let new_margin_lower_bound = 1e3; assert!(
x_out <= 1e10 - new_margin_lower_bound,
"large-range box: x={:.6e} は ub-1.0 に貼り付き (旧 ABS=1.0 cap retention 疑い)",
x_out,
);
}
{
let problem = build_single_var_box_problem(0.0, 1e6);
let x_out = apply_warm_and_extract_x(&problem, 5e5);
assert!(
(x_out - 5e5).abs() < 1e-6,
"mid-range box well-interior 維持"
);
}
{
let problem = build_single_var_box_problem(0.0, 1.0);
let x_out = apply_warm_and_extract_x(&problem, 0.5);
assert!((x_out - 0.5).abs() < 1e-9, "small box well-interior 維持");
}
{
let problem = build_single_var_box_problem(3.0, 3.0);
let x_out = apply_warm_and_extract_x(&problem, 100.0);
assert!(
(x_out - 3.0).abs() < 1e-12,
"collapsed box (lb=ub): midpoint=lb=ub"
);
}
}
fn build_single_var_lb_problem(lb: f64) -> QpProblem {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::new(0, 1);
let b: Vec<f64> = vec![];
let bounds = vec![(lb, f64::INFINITY)];
QpProblem::new(q, c, a, b, bounds, vec![]).unwrap()
}
fn build_single_var_box_problem(lb: f64, ub: f64) -> QpProblem {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::new(0, 1);
let b: Vec<f64> = vec![];
let bounds = vec![(lb, ub)];
QpProblem::new(q, c, a, b, bounds, vec![]).unwrap()
}
#[test]
fn test_box_only_nondiag_q_stall_reproducer() {
use crate::qp::solve_qp_with;
let q = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[2.0, 1.0, 1.0, 2.0], 2, 2)
.unwrap();
let c = vec![-6.0, -6.0];
let a = CscMatrix::new(0, 2);
let b = vec![];
let bounds = vec![(0.0_f64, 4.0_f64); 2];
let problem = QpProblem::new_all_le(q, c, a, b, bounds).unwrap();
for use_ruiz in [false, true] {
let opts = SolverOptions {
use_ruiz_scaling: use_ruiz,
timeout_secs: Some(10.0),
..Default::default()
};
let result = solve_qp_with(&problem, &opts);
eprintln!(
"ruiz={} STATUS={:?} x={:?} obj={:.6} iters={}",
use_ruiz, result.status, result.solution, result.objective, result.iterations
);
if let Some((pf, df, mu)) = result.final_residuals {
eprintln!("RESID pf={:.3e} df={:.3e} mu={:.3e}", pf, df, mu);
}
assert_eq!(
result.status,
SolveStatus::Optimal,
"ruiz={} expected Optimal, got {:?}",
use_ruiz,
result.status
);
assert!(
(result.solution[0] - 2.0).abs() < 0.1,
"ruiz={} x[0]={:.4}",
use_ruiz,
result.solution[0]
);
assert!(
(result.solution[1] - 2.0).abs() < 0.1,
"ruiz={} x[1]={:.4}",
use_ruiz,
result.solution[1]
);
assert!(
(result.objective - (-12.0)).abs() < 0.5,
"ruiz={} obj={:.4}",
use_ruiz,
result.objective
);
}
let opts_inner = default_opts();
let q2 = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[2.0, 1.0, 1.0, 2.0], 2, 2)
.unwrap();
let problem2 = QpProblem::new_all_le(
q2,
vec![-6.0, -6.0],
CscMatrix::new(0, 2),
vec![],
vec![(0.0_f64, 4.0_f64); 2],
)
.unwrap();
let r2 = solve_ippmm_inner(&problem2, &opts_inner, opts_inner.ipm_eps());
eprintln!(
"inner STATUS={:?} x={:?} obj={:.6}",
r2.status, r2.solution, r2.objective
);
assert_eq!(
r2.status,
SolveStatus::Optimal,
"inner: expected Optimal, got {:?}",
r2.status
);
}
#[test]
#[allow(clippy::type_complexity)]
fn test_peu_and_gate_box_only_nondiag_stall_sentinel() {
use crate::qp::solve_qp_with;
let cases: &[(
Vec<(usize, usize, f64)>,
Vec<f64>,
Vec<(f64, f64)>,
Vec<f64>,
f64,
&str,
)] = &[
(
vec![(0, 0, 2.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 2.0)],
vec![-6.0, -6.0],
vec![(0.0, 4.0), (0.0, 4.0)],
vec![2.0, 2.0],
-12.0,
"min x²+xy+y²-6x-6y [0,4]²",
),
(
vec![(0, 0, 2.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 2.0)],
vec![-8.0, -6.0],
vec![(0.0, 6.0), (0.0, 6.0)],
vec![10.0 / 3.0, 4.0 / 3.0],
-52.0 / 3.0,
"min x²+xy+y²-8x-6y [0,6]²",
),
(
vec![(0, 0, 4.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 4.0)],
vec![-4.0, -4.0],
vec![(0.0, 3.0), (0.0, 3.0)],
vec![4.0 / 5.0, 4.0 / 5.0],
-3.2,
"min 2x²+xy+2y²-4x-4y [0,3]²",
),
];
for (q_trips, c, bounds, x_star, obj_star, name) in cases {
let rows: Vec<usize> = q_trips.iter().map(|&(r, _, _)| r).collect();
let cols: Vec<usize> = q_trips.iter().map(|&(_, c, _)| c).collect();
let vals: Vec<f64> = q_trips.iter().map(|&(_, _, v)| v).collect();
let n = bounds.len();
let q = CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap();
let a = CscMatrix::new(0, n);
let problem = QpProblem::new_all_le(q, c.clone(), a, vec![], bounds.clone()).unwrap();
for use_ruiz in [false, true] {
let opts = SolverOptions {
use_ruiz_scaling: use_ruiz,
timeout_secs: Some(10.0),
..Default::default()
};
let result = solve_qp_with(&problem, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"{} ruiz={}: expected Optimal, got {:?}",
name,
use_ruiz,
result.status
);
for (i, &xi_star) in x_star.iter().enumerate() {
assert!(
(result.solution[i] - xi_star).abs() < 0.1,
"{} ruiz={}: x[{}]={:.4} expected {:.4}",
name,
use_ruiz,
i,
result.solution[i],
xi_star
);
}
assert!(
(result.objective - obj_star).abs() < 0.5,
"{} ruiz={}: obj={:.4} expected {:.4}",
name,
use_ruiz,
result.objective,
obj_star
);
}
}
}
#[test]
#[allow(clippy::type_complexity)]
fn test_box_only_nondiag_q_multi_pattern() {
use crate::qp::solve_qp_with;
let cases: &[(
&[usize],
&[usize],
&[f64],
&[f64],
&[(f64, f64)],
&[f64],
f64,
&str,
)] = &[
(
&[0, 1],
&[0, 1],
&[2.0, 2.0],
&[-4.0, -4.0],
&[(0.0, 5.0), (0.0, 5.0)],
&[2.0, 2.0],
-8.0,
"diag Q baseline",
),
(
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[2.0, 1.0, 1.0, 2.0],
&[-6.0, -6.0],
&[(0.0, 4.0), (0.0, 4.0)],
&[2.0, 2.0],
-12.0,
"nondiag Q [0,4]²",
),
(
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[2.0, 1.0, 1.0, 2.0],
&[-8.0, -6.0],
&[(0.0, 6.0), (0.0, 6.0)],
&[10.0 / 3.0, 4.0 / 3.0],
-52.0 / 3.0,
"nondiag Q asymm-c",
),
(
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[2.0, 1.0, 1.0, 2.0],
&[-6.0, -6.0],
&[(0.0, 1.0), (0.0, 1.0)],
&[1.0, 1.0],
-9.0,
"nondiag Q tight box [0,1]²",
),
];
for &(rows, cols, vals, c, bounds, x_star, obj_star, name) in cases {
let n = bounds.len();
let q = CscMatrix::from_triplets(rows, cols, vals, n, n).unwrap();
let a = CscMatrix::new(0, n);
let problem = QpProblem::new_all_le(q, c.to_vec(), a, vec![], bounds.to_vec()).unwrap();
for use_ruiz in [false, true] {
let opts = SolverOptions {
use_ruiz_scaling: use_ruiz,
timeout_secs: Some(10.0),
..Default::default()
};
let result = solve_qp_with(&problem, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"{} ruiz={}: expected Optimal, got {:?}",
name,
use_ruiz,
result.status
);
for (i, &xi) in x_star.iter().enumerate() {
assert!(
(result.solution[i] - xi).abs() < 0.1,
"{} ruiz={}: x[{}]={:.4} expected {:.4}",
name,
use_ruiz,
i,
result.solution[i],
xi
);
}
assert!(
(result.objective - obj_star).abs() < 0.5,
"{} ruiz={}: obj={:.4} expected {:.4}",
name,
use_ruiz,
result.objective,
obj_star
);
}
}
}
fn apply_warm_and_extract_x(problem: &QpProblem, xj_warm: f64) -> f64 {
let (a_ext, b_ext, m_ext, m_orig, _, is_eq_ext) = build_extended_constraints(problem);
let ws = QpWarmStart {
x: vec![xj_warm],
y: vec![0.0_f64; m_orig],
mu: 1e-3,
};
let mut x = vec![0.0_f64; 1];
let mut y = vec![0.0_f64; m_ext];
let mut s = vec![0.0_f64; m_ext];
let mu = apply_qp_warm_start(
&ws, problem, &a_ext, &b_ext, &is_eq_ext, m_orig, m_ext, &mut x, &mut y, &mut s,
);
assert!(mu.is_some(), "apply_qp_warm_start dropped (dim mismatch?)");
x[0]
}