use crate::nl_reader::NlProblem;
use pounce_common::types::{lower_bound_present, upper_bound_present};
use pounce_convex::{ConeSpec, QpProblem, QpResiduals, QpSolution, Triplet};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundRelax {
pub factor: f64,
pub cap: f64,
}
impl BoundRelax {
pub const NONE: Self = Self {
factor: 0.0,
cap: 0.0,
};
pub fn active(self) -> bool {
self.factor > 0.0 && self.cap > 0.0
}
fn var_delta(self, b: f64) -> f64 {
if !self.active() {
return 0.0;
}
(self.factor.abs() * b.abs().max(1.0)).min(self.cap)
}
fn for_row(self, lo: f64, hi: f64) -> Self {
if lower_bound_present(lo) && upper_bound_present(hi) && lo > hi {
Self::NONE
} else {
self
}
}
fn row_delta(self, b: f64) -> f64 {
if !self.active() {
return 0.0;
}
let scale = if b == 0.0 { 1.0 } else { b.abs() };
self.factor.abs().min(self.cap) * scale
}
}
pub fn extract_qp(prob: &NlProblem, relax: BoundRelax) -> Option<QpProblem> {
Some(extract_qp_with_map(prob, relax)?.0) }
#[derive(Debug, Clone)]
pub enum ConRowMap {
Eq { a_row: usize },
Ineq {
upper: Option<usize>,
lower: Option<usize>,
},
}
pub fn declared_residuals_qp(
prob: &NlProblem,
sol: &QpSolution,
relax: BoundRelax,
) -> Option<QpResiduals> {
if !relax.active() {
return None;
}
let (declared, _, _) = extract_qp_with_map(prob, BoundRelax::NONE)?;
debug_assert_eq!(declared.n, sol.x.len(), "declared re-extraction changed n");
Some(sol.kkt_residuals(&declared))
}
pub fn declared_residuals_socp(
prob: &NlProblem,
sol: &QpSolution,
relax: BoundRelax,
) -> Option<QpResiduals> {
if !relax.active() {
return None;
}
let (declared, _, _, cones) = extract_socp_with_map(prob, BoundRelax::NONE)?;
debug_assert_eq!(declared.n, sol.x.len(), "declared re-extraction changed n");
Some(sol.kkt_residuals_conic(&declared, &cones))
}
pub fn extract_qp_with_map(
prob: &NlProblem,
relax: BoundRelax,
) -> Option<(QpProblem, Vec<ConRowMap>, f64)> {
let n = prob.n;
let sign = if prob.minimize { 1.0 } else { -1.0 };
let (hess, obj_nl_linear, obj_nl_constant) = prob.obj_nonlinear.analyze_quadratic_full()?;
let mut p_lower: Vec<Triplet> = Vec::with_capacity(hess.len());
for ((i, j), v) in &hess {
let (row, col) = if i >= j { (*i, *j) } else { (*j, *i) };
p_lower.push(Triplet::new(row, col, sign * v));
}
let mut c = vec![0.0; n];
for (var, coef) in &prob.obj_linear {
c[*var] += sign * coef;
}
for (var, coef) in &obj_nl_linear {
c[*var] += sign * coef;
}
let mut a: Vec<Triplet> = Vec::new();
let mut b: Vec<f64> = Vec::new();
let mut g: Vec<Triplet> = Vec::new();
let mut h: Vec<f64> = Vec::new();
let mut con_map: Vec<ConRowMap> = Vec::with_capacity(prob.con_linear.len());
for (row, lin) in prob.con_linear.iter().enumerate() {
let lo = prob.g_l[row];
let hi = prob.g_u[row];
let (nl_lin, const_shift) = prob.con_nonlinear[row]
.analyze_quadratic_full()
.map(|(_, l, k)| (l, k))
.unwrap_or_default();
let mut coef = vec![0.0; n];
for (var, v) in lin {
coef[*var] += *v;
}
for (var, v) in &nl_lin {
coef[*var] += *v;
}
let nonzeros = || coef.iter().enumerate().filter(|(_, v)| **v != 0.0);
if lo == hi && lower_bound_present(lo) && upper_bound_present(hi) {
let eq_row = next_row(&b);
for (var, v) in nonzeros() {
a.push(Triplet::new(eq_row, var, *v));
}
b.push(lo - const_shift);
con_map.push(ConRowMap::Eq { a_row: eq_row });
} else {
let relax = relax.for_row(lo, hi);
let upper = if upper_bound_present(hi) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, *v));
}
h.push(hi + relax.row_delta(hi) - const_shift);
Some(gr)
} else {
None
};
let lower = if lower_bound_present(lo) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, -*v));
}
h.push(-(lo - relax.row_delta(lo) - const_shift));
Some(gr)
} else {
None
};
con_map.push(ConRowMap::Ineq { upper, lower });
}
}
let (lb, ub) = extract_box(prob, relax);
Some((
QpProblem {
n,
p_lower,
c,
a,
b,
g,
h,
lb,
ub,
},
con_map,
obj_nl_constant,
))
}
fn extract_box(prob: &NlProblem, relax: BoundRelax) -> (Vec<f64>, Vec<f64>) {
let as_declared = |i: usize| {
let (l, u) = (prob.x_l[i], prob.x_u[i]);
lower_bound_present(l) && upper_bound_present(u) && l >= u
};
let lb = (0..prob.n)
.map(|i| {
let v = prob.x_l[i];
if !lower_bound_present(v) {
f64::NEG_INFINITY
} else if as_declared(i) {
v
} else {
v - relax.var_delta(v)
}
})
.collect();
let ub = (0..prob.n)
.map(|i| {
let v = prob.x_u[i];
if !upper_bound_present(v) {
f64::INFINITY
} else if as_declared(i) {
v
} else {
v + relax.var_delta(v)
}
})
.collect();
(lb, ub)
}
pub fn recover_duals(prob: &NlProblem, con_map: &[ConRowMap], y: &[f64], z: &[f64]) -> Vec<f64> {
let sign = if prob.minimize { 1.0 } else { -1.0 };
con_map
.iter()
.map(|m| match m {
ConRowMap::Eq { a_row } => sign * y[*a_row],
ConRowMap::Ineq { upper, lower } => {
let zu = upper.map(|r| z[r]).unwrap_or(0.0);
let zl = lower.map(|r| z[r]).unwrap_or(0.0);
sign * (zu - zl)
}
})
.collect()
}
fn next_row(rhs: &[f64]) -> usize {
rhs.len()
}
pub fn recover_bound_mults(prob: &NlProblem, sol: &QpSolution) -> (Vec<f64>, Vec<f64>) {
let read = |v: &[f64]| -> Vec<f64> {
(0..prob.n)
.map(|i| v.get(i).copied().unwrap_or(0.0))
.collect()
};
(read(&sol.z_lb), read(&sol.z_ub))
}
#[derive(Debug, Clone)]
pub enum ConSocpMap {
Eq { a_row: usize },
Ineq {
upper: Option<usize>,
lower: Option<usize>,
},
Quad { z_row0: usize, z_row1: usize },
}
struct SocBlock {
con_idx: usize,
a: Vec<(usize, f64)>,
b_eff: f64,
f_rows: Vec<Vec<(usize, f64)>>,
}
pub fn extract_socp_with_map(
prob: &NlProblem,
relax: BoundRelax,
) -> Option<(QpProblem, Vec<ConSocpMap>, f64, Vec<ConeSpec>)> {
let n = prob.n;
let sign = if prob.minimize { 1.0 } else { -1.0 };
let (hess, obj_nl_linear, obj_nl_constant) = prob.obj_nonlinear.analyze_quadratic_full()?;
let mut p_lower: Vec<Triplet> = Vec::with_capacity(hess.len());
for ((i, j), v) in &hess {
let (row, col) = if i >= j { (*i, *j) } else { (*j, *i) };
p_lower.push(Triplet::new(row, col, sign * v));
}
let mut c = vec![0.0; n];
for (var, coef) in &prob.obj_linear {
c[*var] += sign * coef;
}
for (var, coef) in &obj_nl_linear {
c[*var] += sign * coef;
}
let mut a: Vec<Triplet> = Vec::new();
let mut b: Vec<f64> = Vec::new();
let mut g: Vec<Triplet> = Vec::new();
let mut h: Vec<f64> = Vec::new();
let mut con_map: Vec<ConSocpMap> = Vec::with_capacity(prob.m);
let mut soc_blocks: Vec<SocBlock> = Vec::new();
for (row, lin) in prob.con_linear.iter().enumerate() {
let lo = prob.g_l[row];
let hi = prob.g_u[row];
let nl = &prob.con_nonlinear[row];
let quad = nl.analyze_quadratic_full();
let is_quadratic = matches!(&quad, Some((hmap, _, _)) if !hmap.is_empty());
if is_quadratic {
let (hmap, nl_lin, nl_const) = quad.expect("checked above");
let mut a_map: std::collections::BTreeMap<usize, f64> =
std::collections::BTreeMap::new();
for (var, coef) in lin {
*a_map.entry(*var).or_insert(0.0) += *coef;
}
for (var, coef) in &nl_lin {
*a_map.entry(*var).or_insert(0.0) += *coef;
}
let a_vec: Vec<(usize, f64)> = a_map.into_iter().filter(|&(_, c)| c != 0.0).collect();
let f_rows = socp_factor_rows(&hmap);
let con_idx = con_map.len();
con_map.push(ConSocpMap::Quad {
z_row0: 0,
z_row1: 0,
}); soc_blocks.push(SocBlock {
con_idx,
a: a_vec,
b_eff: nl_const - (hi + relax.row_delta(hi)),
f_rows,
});
continue;
}
let (nl_lin, const_shift) = quad.map(|(_, l, k)| (l, k)).unwrap_or_default();
let mut coef = vec![0.0; n];
for (var, v) in lin {
coef[*var] += *v;
}
for (var, v) in &nl_lin {
coef[*var] += *v;
}
let nonzeros = || coef.iter().enumerate().filter(|(_, v)| **v != 0.0);
if lo == hi && lower_bound_present(lo) && upper_bound_present(hi) {
let eq_row = next_row(&b);
for (var, v) in nonzeros() {
a.push(Triplet::new(eq_row, var, *v));
}
b.push(lo - const_shift);
con_map.push(ConSocpMap::Eq { a_row: eq_row });
} else {
let relax = relax.for_row(lo, hi);
let upper = if upper_bound_present(hi) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, *v));
}
h.push(hi + relax.row_delta(hi) - const_shift);
Some(gr)
} else {
None
};
let lower = if lower_bound_present(lo) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, -*v));
}
h.push(-(lo - relax.row_delta(lo) - const_shift));
Some(gr)
} else {
None
};
con_map.push(ConSocpMap::Ineq { upper, lower });
}
}
let (lb, ub) = extract_box(prob, relax);
let num_nonneg = h.len();
let mut cones: Vec<ConeSpec> = Vec::with_capacity(1 + soc_blocks.len());
if num_nonneg > 0 {
cones.push(ConeSpec::Nonneg(num_nonneg));
}
for blk in soc_blocks {
let r = blk.f_rows.len();
let dim = r + 2;
let row0 = next_row(&h);
for &(var, coef) in &blk.a {
g.push(Triplet::new(row0, var, coef));
}
h.push(1.0 - blk.b_eff);
let row1 = next_row(&h);
for &(var, coef) in &blk.a {
g.push(Triplet::new(row1, var, coef));
}
h.push(-(1.0 + blk.b_eff));
let sqrt2 = std::f64::consts::SQRT_2;
for f in &blk.f_rows {
let gr = next_row(&h);
for &(var, fv) in f {
g.push(Triplet::new(gr, var, -sqrt2 * fv));
}
h.push(0.0);
}
cones.push(ConeSpec::SecondOrder(dim));
con_map[blk.con_idx] = ConSocpMap::Quad {
z_row0: row0,
z_row1: row1,
};
}
Some((
QpProblem {
n,
p_lower,
c,
a,
b,
g,
h,
lb,
ub,
},
con_map,
obj_nl_constant,
cones,
))
}
pub fn recover_socp_duals(
prob: &NlProblem,
con_map: &[ConSocpMap],
y: &[f64],
z: &[f64],
) -> Vec<f64> {
let sign = if prob.minimize { 1.0 } else { -1.0 };
con_map
.iter()
.map(|m| match m {
ConSocpMap::Eq { a_row } => sign * y[*a_row],
ConSocpMap::Ineq { upper, lower } => {
let zu = upper.map(|r| z[r]).unwrap_or(0.0);
let zl = lower.map(|r| z[r]).unwrap_or(0.0);
sign * (zu - zl)
}
ConSocpMap::Quad { z_row0, z_row1 } => sign * (z[*z_row0] + z[*z_row1]),
})
.collect()
}
fn socp_factor_rows(
hmap: &std::collections::BTreeMap<(usize, usize), f64>,
) -> Vec<Vec<(usize, f64)>> {
let support = quad_support(hmap);
let k = support.len();
if !hmap.keys().any(|&(i, j)| i != j) {
let mut diag: Vec<(usize, f64)> = hmap
.iter()
.filter(|&(_, &v)| v > 0.0)
.map(|(&(i, _), &v)| (i, v))
.collect();
diag.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("PSD diagonal is finite"));
return diag
.into_iter()
.map(|(i, v)| vec![(i, v / v.sqrt())])
.collect();
}
let dense = dense_symmetric_on_support(hmap, &support);
psd_outer_factor(dense, k)
.into_iter()
.map(|f| {
f.into_iter()
.enumerate()
.filter(|&(_, fv)| fv != 0.0)
.map(|(loc, fv)| (support[loc], fv))
.collect()
})
.collect()
}
fn quad_support(hmap: &std::collections::BTreeMap<(usize, usize), f64>) -> Vec<usize> {
let mut s: Vec<usize> = hmap.keys().flat_map(|&(i, j)| [i, j]).collect();
s.sort_unstable();
s.dedup();
s
}
fn dense_symmetric_on_support(
hmap: &std::collections::BTreeMap<(usize, usize), f64>,
support: &[usize],
) -> Vec<f64> {
let k = support.len();
let loc = |v: usize| support.binary_search(&v).expect("key came from support");
let mut dense = vec![0.0; k * k];
for (&(i, j), &v) in hmap {
let (li, lj) = (loc(i), loc(j));
dense[li * k + lj] = v;
dense[lj * k + li] = v;
}
dense
}
fn psd_outer_factor(mut a: Vec<f64>, n: usize) -> Vec<Vec<f64>> {
let mut rows: Vec<Vec<f64>> = Vec::new();
let d0: Vec<f64> = (0..n).map(|i| a[i * n + i].max(0.0)).collect();
let mut settled = vec![false; n];
for _ in 0..n {
let mut p = usize::MAX;
let mut best = f64::NEG_INFINITY;
for i in 0..n {
if settled[i] {
continue;
}
let d = a[i * n + i];
if d > best {
best = d;
p = i;
}
}
if p == usize::MAX {
break;
}
if best <= 1e-12 * d0[p] || best <= 0.0 {
settled[p] = true;
continue;
}
settled[p] = true;
let d = best.sqrt();
let mut f = vec![0.0; n];
for i in 0..n {
f[i] = a[i * n + p] / d;
}
for i in 0..n {
let fi = f[i];
if fi == 0.0 {
continue;
}
for j in 0..n {
a[i * n + j] -= fi * f[j];
}
}
rows.push(f);
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nl_reader::NlBody;
use crate::nl_reader::{BinOp, Expr};
use pounce_convex::{QpOptions, QpStatus, solve_qp_ipm, solve_socp_ipm};
use pounce_feral::FeralSolverInterface;
use pounce_linsol::SparseSymLinearSolverInterface;
fn backend() -> Box<dyn SparseSymLinearSolverInterface> {
Box::new(FeralSolverInterface::new())
}
fn pow2(var: usize) -> Expr {
Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(var)),
Box::new(Expr::Const(2.0)),
)
}
#[test]
fn extract_and_solve_socp_ball() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, -1.0), (1, -1.0)],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(Expr::Binary(
BinOp::Add,
Box::new(pow2(0)),
Box::new(pow2(1)),
))],
con_linear: vec![vec![]],
x_l: vec![-2e19, -2e19],
x_u: vec![2e19, 2e19],
g_l: vec![-2e19],
g_u: vec![1.0],
x0: vec![0.0, 0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const, cones) =
extract_socp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(obj_const, 0.0);
assert_eq!(cones, vec![ConeSpec::SecondOrder(4)]);
assert_eq!(qp.m_ineq(), 4);
let sol = solve_socp_ipm(&qp, &cones, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
assert!((sol.x[0] - inv_sqrt2).abs() < 1e-5, "x0={}", sol.x[0]);
assert!((sol.x[1] - inv_sqrt2).abs() < 1e-5, "x1={}", sol.x[1]);
assert!(
(sol.obj - (-2.0_f64.sqrt())).abs() < 1e-5,
"obj={}",
sol.obj
);
let lambda = recover_socp_duals(&prob, &con_map, &sol.y, &sol.z);
assert_eq!(lambda.len(), 1);
assert!(
(lambda[0] - 0.5 * 2.0_f64.sqrt()).abs() < 1e-3,
"ball constraint dual={}",
lambda[0]
);
}
#[test]
fn extract_and_solve_socp_folds_constraint_constant() {
let con = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
)),
Box::new(Expr::Const(2.0)),
);
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(con)],
con_linear: vec![vec![]],
x_l: vec![-2e19],
x_u: vec![2e19],
g_l: vec![-2e19],
g_u: vec![1.0],
x0: vec![0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, _con_map, obj_const, cones) =
extract_socp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(obj_const, 0.0);
assert_eq!(cones, vec![ConeSpec::SecondOrder(3)]);
let sol = solve_socp_ipm(&qp, &cones, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 2.0).abs() < 1e-5, "x0={}", sol.x[0]);
}
fn wide_ball(n: usize, i: usize, j: usize) -> NlProblem {
let sq = |v: usize| {
Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(v)),
Box::new(Expr::Const(2.0)),
)
};
NlProblem {
src: None,
cse_bodies: Vec::new(),
n,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(i, -1.0), (j, -1.0)],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(Expr::Binary(
BinOp::Add,
Box::new(sq(i)),
Box::new(sq(j)),
))],
con_linear: vec![vec![]],
x_l: vec![-10.0; n],
x_u: vec![10.0; n],
g_l: vec![-2e19],
g_u: vec![1.0],
x0: vec![0.0; n],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
}
}
#[test]
fn socp_factor_columns_scatter_back_to_original_variables() {
let prob = wide_ball(40, 11, 37);
let (qp, _con_map, _obj_const, cones) =
extract_socp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(cones, vec![ConeSpec::SecondOrder(4)]);
let factor_cols: std::collections::BTreeSet<usize> =
qp.g.iter().filter(|t| t.row >= 2).map(|t| t.col).collect();
assert_eq!(
factor_cols,
[11usize, 37].into_iter().collect(),
"factor rows must reference the row's own variables, got {factor_cols:?}"
);
}
#[test]
fn socp_extraction_is_sized_by_support_not_problem_width() {
let n = 50_000;
let prob = wide_ball(n, 7, n - 3);
let (qp, _con_map, _obj_const, cones) =
extract_socp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(cones, vec![ConeSpec::SecondOrder(4)]);
assert_eq!(qp.g.iter().filter(|t| t.row >= 2).count(), 2);
}
#[test]
fn socp_dense_factor_columns_scatter_back_to_original_variables() {
let con = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Add,
Box::new(Expr::Var(11)),
Box::new(Expr::Var(37)),
)),
Box::new(Expr::Const(2.0)),
);
let mut prob = wide_ball(40, 11, 37);
prob.con_nonlinear = vec![NlBody::Tree(con)];
let (qp, _con_map, _obj_const, cones) =
extract_socp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(cones, vec![ConeSpec::SecondOrder(3)], "rank 1 + 2");
let factor_cols: std::collections::BTreeSet<usize> =
qp.g.iter().filter(|t| t.row >= 2).map(|t| t.col).collect();
assert_eq!(factor_cols, [11usize, 37].into_iter().collect());
}
#[test]
fn diagonal_hessian_factors_in_linear_space() {
let mut h = std::collections::BTreeMap::new();
for i in 0..1000usize {
h.insert((i, i), 4.0);
}
let rows = socp_factor_rows(&h);
assert_eq!(rows.len(), 1000);
assert!(
rows.iter().all(|r| r.len() == 1),
"a diagonal Q must give one nonzero per factor row"
);
for (k, r) in rows.iter().enumerate() {
assert_eq!(r[0].0, k);
assert!((r[0].1 - 2.0).abs() < 1e-12, "√4 = 2, got {}", r[0].1);
}
}
#[test]
fn both_factor_paths_reconstruct_q_and_agree_on_rank() {
let recon = |rows: &[Vec<(usize, f64)>]| {
let mut q: std::collections::BTreeMap<(usize, usize), f64> = Default::default();
for r in rows {
for &(i, fi) in r {
for &(j, fj) in r {
*q.entry((i, j)).or_insert(0.0) += fi * fj;
}
}
}
q.retain(|_, v| v.abs() > 1e-12);
q
};
let diag: std::collections::BTreeMap<(usize, usize), f64> =
[((3, 3), 2.0), ((8, 8), 9.0), ((9, 9), 1e-20)]
.into_iter()
.collect();
let drows = socp_factor_rows(&diag);
assert_eq!(
drows.len(),
3,
"1e-20 on its own row is a small eigenvalue, not a missing one"
);
let support = quad_support(&diag);
let general = psd_outer_factor(dense_symmetric_on_support(&diag, &support), support.len());
let general_vals: Vec<f64> = general
.iter()
.map(|f| f.iter().copied().find(|v| *v != 0.0).expect("one nonzero"))
.collect();
let short_vals: Vec<f64> = drows.iter().map(|r| r[0].1).collect();
assert_eq!(
short_vals.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
general_vals.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"diagonal shortcut must reproduce psd_outer_factor bit for bit \
(got {short_vals:?} vs {general_vals:?})"
);
assert_eq!(drows[0][0].0, 8, "largest diagonal pivots first");
assert_eq!(drows[1][0].0, 3);
assert_eq!(drows[2][0].0, 9, "then the smallest, still emitted");
assert!(
(drows[2][0].1 - 1e-10).abs() < 1e-22,
"√1e-20 = 1e-10, got {}",
drows[2][0].1
);
let dq = recon(&drows);
assert_eq!(dq.len(), 2);
assert!((dq[&(3, 3)] - 2.0).abs() < 1e-12);
assert!((dq[&(8, 8)] - 9.0).abs() < 1e-12);
let mut coupled = diag.clone();
coupled.insert((3, 8), 1.0);
let grows = socp_factor_rows(&coupled);
assert_eq!(grows.len(), 3, "2 from the coupled block, 1 from `1e-20`");
let gq = recon(&grows);
assert!((gq[&(3, 3)] - 2.0).abs() < 1e-10);
assert!((gq[&(8, 8)] - 9.0).abs() < 1e-10);
assert!((gq[&(3, 8)] - 1.0).abs() < 1e-10);
assert!((gq[&(8, 3)] - 1.0).abs() < 1e-10);
assert!(
!gq.contains_key(&(9, 9)),
"1e-20 is below what the reconstruction resolves"
);
}
#[test]
fn psd_outer_factor_is_rank_revealing() {
let q = vec![1.0, 2.0, 2.0, 4.0];
let rows = psd_outer_factor(q.clone(), 2);
assert_eq!(rows.len(), 1, "rank-1 Q must give one factor row");
let mut recon = vec![0.0; 4];
for f in &rows {
for i in 0..2 {
for j in 0..2 {
recon[i * 2 + j] += f[i] * f[j];
}
}
}
for k in 0..4 {
assert!((recon[k] - q[k]).abs() < 1e-9, "recon[{k}]={}", recon[k]);
}
}
#[test]
fn rank_does_not_depend_on_the_units_the_columns_are_measured_in() {
let vs = [
[1.0, 2.0, 0.0, -1.0],
[0.0, 1.0, 3.0, 1.0],
[2.0, 0.0, 1.0, 4.0],
];
let n = 4;
let mut q = vec![0.0; n * n];
for v in &vs {
for i in 0..n {
for j in 0..n {
q[i * n + j] += v[i] * v[j];
}
}
}
assert_eq!(psd_outer_factor(q.clone(), n).len(), 3, "unscaled rank");
for spread in [1i32, 2, 3, 4, 6] {
for sign in [1i32, -1] {
let c: Vec<f64> = (0..n)
.map(|i| 10f64.powi(sign * spread * (i as i32 - 1)))
.collect();
let mut scaled = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
scaled[i * n + j] = c[i] * q[i * n + j] * c[j];
}
}
let rank = psd_outer_factor(scaled, n).len();
assert_eq!(
rank, 3,
"C Q C with C = diag(10^({sign}·{spread}·(i−1))) has the \
same rank as Q; got {rank}"
);
}
}
}
#[test]
fn a_spanned_direction_is_dropped_at_every_column_scaling() {
let v = [1.0, 2.0, -3.0, 0.5];
let n = 4;
let mut q = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
q[i * n + j] = v[i] * v[j];
}
}
for e in [-8i32, -4, 0, 4, 8] {
let c: Vec<f64> = (0..n).map(|i| 10f64.powi(e * (i as i32 - 1))).collect();
let mut scaled = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
scaled[i * n + j] = c[i] * q[i * n + j] * c[j];
}
}
assert_eq!(
psd_outer_factor(scaled, n).len(),
1,
"rank-1 Q stays rank 1 under diag(10^({e}·(i−1)))"
);
}
}
#[test]
fn a_live_pivot_below_a_spent_ones_roundoff_is_still_found() {
let n = 2;
let rows = psd_outer_factor(vec![2.0, 0.0, 0.0, 1e-20], n);
assert_eq!(
rows.len(),
2,
"both diagonal entries are eigenvalues; got {rows:?}"
);
assert!(
(rows[1][1] - 1e-10).abs() < 1e-22,
"the surviving row is √1e-20, got {}",
rows[1][1]
);
}
#[test]
fn extract_and_solve_equality_qp() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Binary(
BinOp::Add,
Box::new(pow2(0)),
Box::new(pow2(1)),
)),
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(Expr::Const(0.0))],
con_linear: vec![vec![(0, 1.0), (1, 1.0)]],
x_l: vec![-2e19, -2e19],
x_u: vec![2e19, 2e19],
g_l: vec![2.0],
g_u: vec![2.0],
x0: vec![0.0, 0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const) =
extract_qp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(obj_const, 0.0);
assert_eq!(qp.p_lower.len(), 2);
assert_eq!(qp.m_eq(), 1);
assert_eq!(qp.m_ineq(), 0);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-6, "x0={}", sol.x[0]);
assert!((sol.x[1] - 1.0).abs() < 1e-6, "x1={}", sol.x[1]);
assert!((sol.obj - 2.0).abs() < 1e-6, "obj={}", sol.obj);
let lambda = recover_duals(&prob, &con_map, &sol.y, &sol.z);
assert_eq!(lambda.len(), 1);
assert!(
(lambda[0] - (-2.0)).abs() < 1e-5,
"equality dual={}",
lambda[0]
);
}
#[test]
fn extract_keeps_linear_term_from_nonlinear_tree() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
)),
Box::new(Expr::Const(2.0)),
);
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(obj),
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![-2e19],
x_u: vec![2e19],
g_l: vec![],
g_u: vec![],
x0: vec![0.0],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.c.len(), 1);
assert!(
(qp.c[0] - (-6.0)).abs() < 1e-12,
"c[0]={} — linear term from the nonlinear tree was dropped",
qp.c[0]
);
assert_eq!(qp.p_lower.len(), 1);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!(
(sol.x[0] - 3.0).abs() < 1e-6,
"x0={} (expected 3)",
sol.x[0]
);
}
#[test]
fn inequality_dual_recovered() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(pow2(0)),
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(Expr::Const(0.0))],
con_linear: vec![vec![(0, 1.0)]], x_l: vec![-2e19],
x_u: vec![2e19],
g_l: vec![1.0], g_u: vec![2e19],
x0: vec![0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const) =
extract_qp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(obj_const, 0.0);
assert_eq!(qp.m_ineq(), 1);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-6, "x0={}", sol.x[0]);
let lambda = recover_duals(&prob, &con_map, &sol.y, &sol.z);
assert!((lambda[0] - (-2.0)).abs() < 1e-5, "ineq dual={}", lambda[0]);
}
#[test]
fn constraint_linear_terms_folded_in_tree_are_recovered() {
let con = Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
);
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(con)],
con_linear: vec![vec![]], x_l: vec![-2e19],
x_u: vec![2e19],
g_l: vec![0.0], g_u: vec![2e19],
x0: vec![0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, _obj_const) =
extract_qp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.m_ineq(), 1);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 3.0).abs() < 1e-5, "x0={}", sol.x[0]);
let lambda = recover_duals(&prob, &con_map, &sol.y, &sol.z);
assert_eq!(lambda.len(), 1);
assert!(lambda[0].is_finite(), "dual={}", lambda[0]);
}
#[test]
fn tree_embedded_objective_constant_is_recovered() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
)),
Box::new(Expr::Const(2.0)),
);
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(obj),
obj_linear: vec![],
obj_constant: 0.0, con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![0.0],
x_u: vec![1.0],
g_l: vec![],
g_u: vec![],
x0: vec![0.0],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, _con_map, obj_const) =
extract_qp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert!((obj_const - 9.0).abs() < 1e-12, "tree constant={obj_const}");
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-6, "x0={}", sol.x[0]);
let reported = sol.obj + obj_const;
assert!((reported - 4.0).abs() < 1e-5, "reported obj={reported}");
}
#[test]
fn extract_and_solve_bounded_qp() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(pow2(0)),
obj_linear: vec![(0, -6.0)],
obj_constant: 9.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![0.0],
x_u: vec![1.0],
g_l: vec![],
g_u: vec![],
x0: vec![0.0],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.m_ineq(), 0);
assert_eq!((qp.lb[0], qp.ub[0]), (0.0, 1.0));
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-6, "x0={}", sol.x[0]);
}
#[test]
fn extract_and_solve_lp() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, -1.0), (1, -1.0)],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![0.0, 0.0],
x_u: vec![1.0, 1.0],
g_l: vec![],
g_u: vec![],
x0: vec![0.0, 0.0],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert!(qp.p_lower.is_empty(), "LP has no Hessian");
assert_eq!(qp.m_ineq(), 0, "bounds are the box, not `G` rows");
assert_eq!(qp.lb, vec![0.0, 0.0]);
assert_eq!(qp.ub, vec![1.0, 1.0]);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 1.0).abs() < 1e-6);
assert!((sol.x[1] - 1.0).abs() < 1e-6);
}
#[test]
fn extract_maximize_negates() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 0,
num_obj: 1,
minimize: false,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![0.0],
x_u: vec![5.0],
g_l: vec![],
g_u: vec![],
x0: vec![0.0],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.c[0], -1.0);
let sol = solve_qp_ipm(&qp, &QpOptions::default(), backend);
assert_eq!(sol.status, QpStatus::Optimal);
assert!((sol.x[0] - 5.0).abs() < 1e-6, "x0={}", sol.x[0]);
}
#[test]
fn variable_bound_past_the_opposite_sentinel_is_kept() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 1,
m: 0,
num_obj: 1,
minimize: false, obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![-1e21],
x_u: vec![-5e20],
g_l: vec![],
g_u: vec![],
x0: vec![-7e20],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(
qp.ub[0], -5e20,
"`x0 <= -5e20` is a real bound and must reach the box; the \
symmetric |v| < 1e19 test dropped it, leaving an \
unbounded-above box the model does not declare"
);
}
#[test]
fn a_row_with_equal_bounds_past_the_sentinel_does_not_vanish() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![NlBody::Tree(Expr::Const(0.0))],
con_linear: vec![vec![(0, 1.0), (1, 1.0)]],
x_l: vec![-2e19, -2e19],
x_u: vec![2e19, 2e19],
g_l: vec![-5e20],
g_u: vec![-5e20],
x0: vec![0.0, 0.0],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(
qp.m_eq(),
0,
"the lower bound is absent, so this is no equality"
);
assert_eq!(
qp.m_ineq(),
1,
"`x0 + x1 <= -5e20` is a real constraint and must reach G; it used \
to disappear from the problem entirely"
);
assert_eq!(qp.h[0], -5e20);
}
#[test]
fn the_box_is_built_with_the_directional_bound_test() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0), (1, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![-2e19, 0.0],
x_u: vec![-5e20, 1.0],
g_l: vec![],
g_u: vec![],
x0: vec![-6e20, 0.5],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.m_ineq(), 0, "bounds are the box, not `G` rows");
assert_eq!(qp.lb[0], f64::NEG_INFINITY);
assert_eq!(qp.ub[0], -5e20);
assert_eq!(qp.lb[1], 0.0);
assert_eq!(qp.ub[1], 1.0);
}
#[test]
fn bound_multipliers_come_back_per_variable() {
let prob = NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 2,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0), (1, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![],
con_linear: vec![],
x_l: vec![0.0, 0.0],
x_u: vec![1.0, 1.0],
g_l: vec![],
g_u: vec![],
x0: vec![0.5, 0.5],
lambda0: vec![],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
};
let sol = QpSolution {
status: pounce_convex::QpStatus::Optimal,
x: vec![0.0, 1.0],
y: vec![],
z: vec![],
z_lb: vec![7.0, 0.0],
z_ub: vec![0.0, 9.0],
obj: 0.0,
iters: 0,
iterates: Vec::new(),
};
let (z_lb, z_ub) = recover_bound_mults(&prob, &sol);
assert_eq!(z_lb, vec![7.0, 0.0]);
assert_eq!(z_ub, vec![0.0, 9.0]);
let empty = QpSolution {
z_lb: Vec::new(),
z_ub: Vec::new(),
..sol
};
let (z_lb, z_ub) = recover_bound_mults(&prob, &empty);
assert_eq!(z_lb, vec![0.0, 0.0]);
assert_eq!(z_ub, vec![0.0, 0.0]);
}
fn relax_fixture() -> NlProblem {
NlProblem {
src: None,
cse_bodies: Vec::new(),
n: 3,
m: 3,
num_obj: 1,
minimize: true,
obj_nonlinear: NlBody::Tree(Expr::Const(0.0)),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![
NlBody::Tree(Expr::Const(0.0)),
NlBody::Tree(Expr::Const(0.0)),
NlBody::Tree(Expr::Const(0.0)),
],
con_linear: vec![
vec![(0, 1.0), (1, 1.0)],
vec![(1, 1.0), (2, 1.0)],
vec![(0, 1.0), (2, 1.0)],
],
x_l: vec![-4.0, 5.0, -2e19],
x_u: vec![8.0, 5.0, 2e19],
g_l: vec![2.0, -3.0, 7.0],
g_u: vec![2e19, 6.0, 7.0],
x0: vec![0.0, 0.0, 0.0],
lambda0: vec![0.0, 0.0, 0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
ampl_options: Vec::new(),
nl_counts: None,
var_names: Vec::new(),
con_names: Vec::new(),
}
}
#[test]
fn bound_relax_none_leaves_the_declared_model_alone() {
let prob = relax_fixture();
let (qp, _, _) = extract_qp_with_map(&prob, BoundRelax::NONE).expect("extract");
assert_eq!(qp.lb, vec![-4.0, 5.0, f64::NEG_INFINITY]);
assert_eq!(qp.ub, vec![8.0, 5.0, f64::INFINITY]);
assert_eq!(qp.b, vec![7.0]);
assert_eq!(qp.h, vec![-2.0, 6.0, 3.0]);
}
#[test]
fn bound_relax_widens_inequality_rows_and_the_free_box_only() {
let prob = relax_fixture();
let relax = BoundRelax {
factor: 1e-8,
cap: 1e-4,
};
let (qp, _, _) = extract_qp_with_map(&prob, relax).expect("extract");
assert!((qp.lb[0] - (-4.0 - 4e-8)).abs() < 1e-18);
assert!((qp.ub[0] - (8.0 + 8e-8)).abs() < 1e-18);
assert_eq!(qp.lb[1], 5.0);
assert_eq!(qp.ub[1], 5.0);
assert_eq!(qp.lb[2], f64::NEG_INFINITY);
assert_eq!(qp.ub[2], f64::INFINITY);
assert_eq!(qp.b, vec![7.0]);
assert!((qp.h[0] - -(2.0 - 2e-8)).abs() < 1e-18);
assert!((qp.h[1] - (6.0 + 6e-8)).abs() < 1e-18);
assert!((qp.h[2] - (3.0 + 3e-8)).abs() < 1e-18);
}
#[test]
fn bound_relax_caps_the_widening_and_floors_a_zero_row_bound() {
let mut prob = relax_fixture();
prob.g_l[0] = 0.0; let relax = BoundRelax {
factor: 1e-2,
cap: 1e-4,
};
let (qp, _, _) = extract_qp_with_map(&prob, relax).expect("extract");
assert!((qp.h[0] - 1e-4).abs() < 1e-18, "{}", qp.h[0]);
assert!((qp.lb[0] - (-4.0 - 1e-4)).abs() < 1e-18);
}
#[test]
fn bound_relax_does_not_close_a_crossed_box_or_a_crossed_row() {
let mut prob = relax_fixture();
prob.x_l[0] = 0.0;
prob.x_u[0] = -1e-8;
prob.g_l[1] = 1e-8;
prob.g_u[1] = 0.0;
let relax = BoundRelax {
factor: 1e-8,
cap: 1e-4,
};
let (qp, _, _) = extract_qp_with_map(&prob, relax).expect("extract");
assert_eq!(qp.lb[0], 0.0);
assert_eq!(qp.ub[0], -1e-8);
assert_eq!(qp.h[1], 0.0);
assert_eq!(qp.h[2], -1e-8);
assert!((qp.h[0] - -(2.0 - 2e-8)).abs() < 1e-18);
}
}