use super::*;
use crate::options::SolverOptions;
use crate::sparse::CscMatrix;
#[allow(clippy::too_many_arguments)]
fn make_qp(
q_rows: &[usize], q_cols: &[usize], q_vals: &[f64], n: usize,
c: Vec<f64>,
a_rows: &[usize], a_cols: &[usize], a_vals: &[f64], m: usize,
b: Vec<f64>,
bounds: Vec<(f64, f64)>,
) -> QpProblem {
let q = if q_rows.is_empty() {
CscMatrix::new(n, n)
} else {
CscMatrix::from_triplets(q_rows, q_cols, q_vals, n, n).unwrap()
};
let a = if a_rows.is_empty() {
CscMatrix::new(m, n)
} else {
CscMatrix::from_triplets(a_rows, a_cols, a_vals, m, n).unwrap()
};
QpProblem::new_all_le(q, c, a, b, bounds).unwrap()
}
#[test]
fn test_fixed_var_removal() {
let prob = make_qp(
&[0, 1], &[0, 1], &[2.0, 2.0], 2,
vec![0.0, 0.0],
&[0, 0], &[0, 1], &[1.0, 1.0], 1,
vec![3.0],
vec![(0.0, 2.0), (1.0, 1.0)], );
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert_eq!(result.reduced.num_vars, 1, "y=1 fixed → 1 var remaining");
assert!((result.obj_offset - 1.0).abs() < 1e-10, "obj_offset=1.0");
assert!(result.was_reduced, "should be reduced");
}
#[test]
fn test_empty_row_removal() {
let prob = make_qp(
&[0], &[0], &[2.0], 1,
vec![0.0],
&[0], &[0], &[1.0], 2,
vec![5.0, 3.0], vec![(f64::NEG_INFINITY, f64::INFINITY)], );
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert!(result.reduced.num_constraints <= 1, "empty row should be removed");
assert_eq!(result.reduced.num_vars, 1, "x remains");
}
#[test]
fn test_no_reduction() {
let prob = make_qp(
&[0, 1], &[0, 1], &[2.0, 2.0], 2,
vec![-2.0, -4.0],
&[], &[], &[], 0,
vec![],
vec![(f64::NEG_INFINITY, f64::INFINITY); 2],
);
let opts = SolverOptions { use_ruiz_scaling: false, ..SolverOptions::default() };
let result = run_qp_presolve_phase1(&prob, &opts);
assert_eq!(result.reduced.num_vars, 2, "no reduction expected");
assert!(!result.was_reduced, "was_reduced = false");
}
#[test]
fn test_ge_constraint_redundant_removal() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![-1.0];
let bounds = vec![(0.0, 10.0)];
let prob = QpProblem::new(
q, c, a, b, bounds,
vec![crate::problem::ConstraintType::Ge],
).unwrap();
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert_eq!(result.reduced.num_constraints, 0,
"Ge x>=-1 は strict slack (row_lb=0 > b=-1) → 削除");
}
#[test]
fn test_ge_constraint_singleton_absorbed_by_step9() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![0.0];
let bounds = vec![(0.0, 10.0)];
let prob = QpProblem::new(
q, c, a, b, bounds,
vec![crate::problem::ConstraintType::Ge],
).unwrap();
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert!(!matches!(result.presolve_status, QpPresolveStatus::Infeasible), "feasible");
assert_eq!(result.reduced.num_constraints, 0,
"step9: singleton Ge x>=0 → bound 吸収 (no-op bound update でも行を除去)");
}
#[test]
fn test_ge_constraint_infeasible_detection() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![5.0]; let bounds = vec![(0.0, 3.0)]; let prob = QpProblem::new(
q, c, a, b, bounds,
vec![crate::problem::ConstraintType::Ge],
).unwrap();
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert!(
matches!(result.presolve_status, QpPresolveStatus::Infeasible),
"Ge制約 x>=5, x<=3 → Infeasible"
);
}
#[test]
fn test_ge_constraint_absorbed_bound_tightened() {
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let c = vec![0.0];
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let b = vec![2.0]; let bounds = vec![(0.0, 10.0)];
let prob = QpProblem::new(
q, c, a, b, bounds,
vec![crate::problem::ConstraintType::Ge],
).unwrap();
let result = run_qp_presolve_phase1(&prob, &SolverOptions::default());
assert!(!matches!(result.presolve_status, QpPresolveStatus::Infeasible), "feasible");
assert_eq!(result.reduced.num_constraints, 0,
"step9: singleton Ge x>=2 (lb=0) → lb tightened to 2, row absorbed");
assert!(result.reduced.bounds[0].0 >= 2.0 - 1e-12,
"reduced lb should be ≥ 2, got {}", result.reduced.bounds[0].0);
}
#[test]
fn test_kahan_add_eliminates_sequential_accumulation_error() {
use twofloat::TwoFloat;
let n = 227;
let mut vs: Vec<f64> = Vec::with_capacity(n);
let mut state: u64 = 0x9E3779B97F4A7C15;
for _ in 0..n {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let raw = (state as f64) / (u64::MAX as f64);
vs.push((raw * 200.0) - 100.0); }
let mut sum_dd = TwoFloat::from(1234.5);
for &v in &vs {
sum_dd += TwoFloat::from(v);
}
let truth = f64::from(sum_dd);
let mut s_naive = 1234.5_f64;
for &v in &vs {
s_naive += v;
}
let mut s_kahan = 1234.5_f64;
let mut comp = 0.0_f64;
for &v in &vs {
super::helpers::kahan_add(&mut s_kahan, &mut comp, v);
}
s_kahan += comp;
let err_naive = (s_naive - truth).abs();
let err_kahan = (s_kahan - truth).abs();
assert!(err_naive >= 1e-15, "naive should have measurable error, got {:.3e}", err_naive);
assert!(err_kahan <= err_naive,
"kahan should be ≤ naive: kahan={:.3e} naive={:.3e}", err_kahan, err_naive);
}
#[test]
fn test_apply_fixed_variable_kahan_accumulation_matches_dd() {
use twofloat::TwoFloat;
let n = 50usize;
let q = CscMatrix::new(n, n);
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
for j in 0..n {
rows.push(0);
cols.push(j);
vals.push(1.0 + j as f64);
}
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, n).unwrap();
let b = vec![1000.0_f64];
let bounds: Vec<(f64, f64)> = (0..n).map(|j| {
let v = 0.5 + (j as f64) * 0.01;
(v, v) }).collect();
let prob = QpProblem::new_all_le(q, vec![0.0; n], a, b.clone(), bounds.clone()).unwrap();
let opts = SolverOptions::default();
let result = run_qp_presolve_phase1(&prob, &opts);
let mut b_true_dd = TwoFloat::from(1000.0);
for j in 0..n {
b_true_dd -= TwoFloat::new_mul(1.0 + j as f64, 0.5 + (j as f64) * 0.01);
}
let b_true = f64::from(b_true_dd);
let mut b_kahan = 1000.0_f64;
let mut comp = 0.0_f64;
for j in 0..n {
super::helpers::kahan_add(&mut b_kahan, &mut comp, -((1.0 + j as f64) * (0.5 + (j as f64) * 0.01)));
}
b_kahan += comp;
let kahan_diff = (b_kahan - b_true).abs();
assert!(kahan_diff < 1e-14,
"kahan_add accumulation should match DD: diff={:.3e} (b_true={:.3e})", kahan_diff, b_true);
let _ = result;
}
use crate::qp::QpProblem;