use crate::dispatch::analyze_quadratic_full;
use crate::nl_reader::NlProblem;
use pounce_convex::{ConeSpec, QpProblem, Triplet};
const NL_INF: f64 = 1e19;
fn is_finite_bound(v: f64) -> bool {
v.abs() < NL_INF
}
pub fn extract_qp(prob: &NlProblem) -> Option<QpProblem> {
Some(extract_qp_with_map(prob)?.0) }
#[derive(Debug, Clone)]
pub enum ConRowMap {
Eq { a_row: usize },
Ineq {
upper: Option<usize>,
lower: Option<usize>,
},
}
pub fn extract_qp_with_map(prob: &NlProblem) -> 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) = analyze_quadratic_full(&prob.obj_nonlinear, n)?;
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) = analyze_quadratic_full(&prob.con_nonlinear[row], n)
.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 && is_finite_bound(lo) {
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 upper = if is_finite_bound(hi) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, *v));
}
h.push(hi - const_shift);
Some(gr)
} else {
None
};
let lower = if is_finite_bound(lo) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, -*v));
}
h.push(-(lo - const_shift));
Some(gr)
} else {
None
};
con_map.push(ConRowMap::Ineq { upper, lower });
}
}
for i in 0..n {
let xl = prob.x_l[i];
let xu = prob.x_u[i];
if is_finite_bound(xu) {
let gr = next_row(&h);
g.push(Triplet::new(gr, i, 1.0)); h.push(xu);
}
if is_finite_bound(xl) {
let gr = next_row(&h);
g.push(Triplet::new(gr, i, -1.0)); h.push(-xl);
}
}
Some((
QpProblem {
n,
p_lower,
c,
a,
b,
g,
h,
lb: Vec::new(),
ub: Vec::new(),
},
con_map,
obj_nl_constant,
))
}
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()
}
#[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<f64>,
b_eff: f64,
f_rows: Vec<Vec<f64>>,
}
pub fn extract_socp_with_map(
prob: &NlProblem,
) -> 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) = analyze_quadratic_full(&prob.obj_nonlinear, n)?;
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 = analyze_quadratic_full(nl, n);
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_vec = vec![0.0; n];
for (var, coef) in lin {
a_vec[*var] += *coef;
}
for (var, coef) in &nl_lin {
a_vec[*var] += *coef;
}
let dense = dense_symmetric(&hmap, n);
let f_rows = psd_outer_factor(dense, n);
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,
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 && is_finite_bound(lo) {
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 upper = if is_finite_bound(hi) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, *v));
}
h.push(hi - const_shift);
Some(gr)
} else {
None
};
let lower = if is_finite_bound(lo) {
let gr = next_row(&h);
for (var, v) in nonzeros() {
g.push(Triplet::new(gr, var, -*v));
}
h.push(-(lo - const_shift));
Some(gr)
} else {
None
};
con_map.push(ConSocpMap::Ineq { upper, lower });
}
}
for i in 0..n {
let xl = prob.x_l[i];
let xu = prob.x_u[i];
if is_finite_bound(xu) {
let gr = next_row(&h);
g.push(Triplet::new(gr, i, 1.0));
h.push(xu);
}
if is_finite_bound(xl) {
let gr = next_row(&h);
g.push(Triplet::new(gr, i, -1.0));
h.push(-xl);
}
}
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.iter().enumerate() {
if coef != 0.0 {
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.iter().enumerate() {
if coef != 0.0 {
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.iter().enumerate() {
if fv != 0.0 {
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: Vec::new(),
ub: Vec::new(),
},
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 dense_symmetric(hmap: &std::collections::BTreeMap<(usize, usize), f64>, n: usize) -> Vec<f64> {
let mut dense = vec![0.0; n * n];
for (&(i, j), &v) in hmap {
dense[i * n + j] = v;
dense[j * n + i] = v;
}
dense
}
fn psd_outer_factor(mut a: Vec<f64>, n: usize) -> Vec<Vec<f64>> {
let mut rows: Vec<Vec<f64>> = Vec::new();
let max_diag = (0..n).map(|i| a[i * n + i]).fold(0.0_f64, f64::max);
let tol = 1e-12 * max_diag.max(1.0);
for _ in 0..n {
let mut p = 0usize;
let mut best = f64::NEG_INFINITY;
for i in 0..n {
let d = a[i * n + i];
if d > best {
best = d;
p = i;
}
}
if best <= tol {
break;
}
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::{BinOp, Expr};
use pounce_convex::{solve_qp_ipm, solve_socp_ipm, QpOptions, QpStatus};
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 {
n: 2,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: Expr::Const(0.0),
obj_linear: vec![(0, -1.0), (1, -1.0)],
obj_constant: 0.0,
con_nonlinear: vec![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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const, cones) = extract_socp_with_map(&prob).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 {
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: Expr::Const(0.0),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, _con_map, obj_const, cones) = extract_socp_with_map(&prob).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]);
}
#[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 extract_and_solve_equality_qp() {
let prob = NlProblem {
n: 2,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: Expr::Binary(BinOp::Add, Box::new(pow2(0)), Box::new(pow2(1))),
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear: vec![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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const) = extract_qp_with_map(&prob).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 {
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: 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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob).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 {
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: pow2(0),
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear: vec![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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, obj_const) = extract_qp_with_map(&prob).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 {
n: 1,
m: 1,
num_obj: 1,
minimize: true,
obj_nonlinear: Expr::Const(0.0),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear: vec![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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, con_map, _obj_const) = extract_qp_with_map(&prob).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 {
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: 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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let (qp, _con_map, obj_const) = extract_qp_with_map(&prob).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 {
n: 1,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: 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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob).expect("extract");
assert_eq!(qp.m_ineq(), 2);
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 {
n: 2,
m: 0,
num_obj: 1,
minimize: true,
obj_nonlinear: 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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob).expect("extract");
assert!(qp.p_lower.is_empty(), "LP has no Hessian");
assert_eq!(qp.m_ineq(), 4); 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 {
n: 1,
m: 0,
num_obj: 1,
minimize: false,
obj_nonlinear: 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(),
var_names: Vec::new(),
con_names: Vec::new(),
};
let qp = extract_qp(&prob).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]);
}
}