use crate::nl_reader::{BinOp, Expr, NlProblem, UnaryOp};
use std::collections::BTreeMap;
const PSD_TOL: f64 = 1e-9;
const SOCP_SIZE_BUDGET: u64 = 100_000_000;
const QCQP_SOCP_COUPLED_VARS: usize = 256;
const NL_INF: f64 = 1e19;
#[inline]
fn is_finite_bound(v: f64) -> bool {
v.abs() < NL_INF
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProblemClass {
Lp,
ConvexQp,
ConvexQcqp,
NonconvexQp,
Nlp,
}
impl ProblemClass {
pub fn name(self) -> &'static str {
match self {
ProblemClass::Lp => "LP",
ProblemClass::ConvexQp => "convex QP",
ProblemClass::ConvexQcqp => "convex QCQP",
ProblemClass::NonconvexQp => "nonconvex QP",
ProblemClass::Nlp => "NLP",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverChoice {
Nlp,
LpIpm,
QpIpm,
SocpIpm,
QpActiveSet,
}
impl SolverChoice {
pub fn describe(self) -> &'static str {
match self {
SolverChoice::Nlp => "NLP filter line-search interior-point (pounce-nlp)",
SolverChoice::LpIpm => "LP interior-point (pounce-convex)",
SolverChoice::QpIpm => "convex QP interior-point (pounce-convex)",
SolverChoice::SocpIpm => "convex QCQP conic interior-point (pounce-convex)",
SolverChoice::QpActiveSet => "active-set QP (pounce-qp)",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverSelection {
Auto,
Nlp,
LpIpm,
QpIpm,
Socp,
QpActiveSet,
}
impl SolverSelection {
pub fn parse(s: &str) -> Option<Self> {
match s {
"auto" => Some(SolverSelection::Auto),
"nlp" => Some(SolverSelection::Nlp),
"lp-ipm" => Some(SolverSelection::LpIpm),
"qp-ipm" => Some(SolverSelection::QpIpm),
"socp" => Some(SolverSelection::Socp),
"qp-active-set" => Some(SolverSelection::QpActiveSet),
_ => None,
}
}
pub const VALUES: &'static [&'static str] =
&["auto", "nlp", "lp-ipm", "qp-ipm", "socp", "qp-active-set"];
}
pub fn classify_problem(prob: &NlProblem) -> ProblemClass {
let obj_nl = !is_trivially_zero(&prob.obj_nonlinear);
let cons_nl = prob.con_nonlinear.iter().any(|e| !is_trivially_zero(e));
if !obj_nl && !cons_nl {
return ProblemClass::Lp;
}
let obj_quad = match analyze_quadratic(&prob.obj_nonlinear, prob.n) {
Some(q) => q,
None => return ProblemClass::Nlp,
};
let mut any_quadratic_constraint = false;
for c in &prob.con_nonlinear {
if is_trivially_zero(c) {
continue;
}
match analyze_quadratic(c, prob.n) {
Some(q) if q.is_empty() => {} Some(_) => any_quadratic_constraint = true,
None => return ProblemClass::Nlp,
}
}
if !obj_quad.is_empty() {
let effective: QuadHessian = if prob.minimize {
obj_quad.clone()
} else {
obj_quad.iter().map(|(k, v)| (*k, -v)).collect()
};
if !hessian_is_psd(&effective, prob.n) {
return ProblemClass::NonconvexQp;
}
}
if any_quadratic_constraint {
for (row, c) in prob.con_nonlinear.iter().enumerate() {
if is_trivially_zero(c) {
continue;
}
match analyze_quadratic(c, prob.n) {
Some(q) if q.is_empty() => {} Some(q) => {
let lo = prob.g_l[row];
let hi = prob.g_u[row];
let vacuous = !is_finite_bound(lo) && !is_finite_bound(hi);
let upper_only = is_finite_bound(hi) && !is_finite_bound(lo);
if vacuous {
continue;
}
if !upper_only
|| !hessian_is_psd(&q, prob.n)
|| qcqp_constraint_too_costly_for_socp(&q)
{
return ProblemClass::Nlp;
}
}
None => return ProblemClass::Nlp,
}
}
if (prob.n as u64).saturating_mul(prob.m as u64) > SOCP_SIZE_BUDGET {
return ProblemClass::Nlp;
}
return ProblemClass::ConvexQcqp;
}
if obj_quad.is_empty() {
ProblemClass::Lp
} else {
ProblemClass::ConvexQp
}
}
pub fn resolve_solver(
class: ProblemClass,
selection: SolverSelection,
) -> Result<SolverChoice, String> {
use ProblemClass as P;
use SolverSelection as S;
let is_lp = class == P::Lp;
let is_convex_qp = matches!(class, P::Lp | P::ConvexQp);
let is_conic = matches!(class, P::Lp | P::ConvexQp | P::ConvexQcqp);
match selection {
S::Auto => match class {
P::Lp | P::ConvexQp => Ok(SolverChoice::QpIpm),
P::ConvexQcqp => Ok(SolverChoice::SocpIpm),
_ => Ok(SolverChoice::Nlp),
},
S::Nlp => Ok(SolverChoice::Nlp),
S::LpIpm => {
if is_lp {
Ok(SolverChoice::LpIpm)
} else {
Err(mismatch_msg(class, "lp-ipm", "an LP"))
}
}
S::QpIpm => {
if is_convex_qp {
Ok(SolverChoice::QpIpm)
} else {
Err(mismatch_msg(class, "qp-ipm", "an LP or convex QP"))
}
}
S::Socp => {
if is_conic {
Ok(SolverChoice::SocpIpm)
} else {
Err(mismatch_msg(class, "socp", "a convex LP, QP, or QCQP"))
}
}
S::QpActiveSet => {
if is_convex_qp {
Ok(SolverChoice::QpActiveSet)
} else {
Err(mismatch_msg(class, "qp-active-set", "an LP or convex QP"))
}
}
}
}
fn mismatch_msg(class: ProblemClass, forced: &str, expected: &str) -> String {
format!(
"problem class {} does not match forced solver {} (expected {})",
class.name(),
forced,
expected
)
}
pub(crate) type QuadHessian = BTreeMap<(usize, usize), f64>;
pub(crate) type QuadForm = (QuadHessian, Vec<(usize, f64)>, f64);
pub(crate) fn analyze_quadratic(e: &Expr, n: usize) -> Option<QuadHessian> {
analyze_quadratic_full(e, n).map(|(h, _, _)| h)
}
pub(crate) fn analyze_quadratic_full(e: &Expr, _n: usize) -> Option<QuadForm> {
let poly = to_poly(e)?;
if poly.max_degree() > 2 {
return None;
}
let mut h: QuadHessian = BTreeMap::new();
let mut lin: Vec<(usize, f64)> = Vec::new();
let mut constant = 0.0;
for (vars, coef) in &poly.terms {
match vars.as_slice() {
[] => constant += *coef,
[i] => lin.push((*i, *coef)),
[i, j] => {
let (i, j) = (*i.min(j), *i.max(j));
let contrib = if i == j { 2.0 * coef } else { *coef };
*h.entry((i, j)).or_insert(0.0) += contrib;
}
_ => return None,
}
}
h.retain(|_, v| v.abs() > 0.0);
Some((h, lin, constant))
}
#[derive(Debug, Clone, Default)]
struct Poly {
terms: BTreeMap<Vec<usize>, f64>,
}
impl Poly {
fn constant(c: f64) -> Self {
let mut terms = BTreeMap::new();
if c != 0.0 {
terms.insert(Vec::new(), c);
}
Poly { terms }
}
fn var(i: usize) -> Self {
let mut terms = BTreeMap::new();
terms.insert(vec![i], 1.0);
Poly { terms }
}
fn max_degree(&self) -> usize {
self.terms.keys().map(|m| m.len()).max().unwrap_or(0)
}
fn as_constant(&self) -> Option<f64> {
match self.terms.len() {
0 => Some(0.0),
1 => self.terms.get(&Vec::new()).copied(),
_ => None,
}
}
fn add(mut self, other: &Poly) -> Poly {
for (m, c) in &other.terms {
*self.terms.entry(m.clone()).or_insert(0.0) += c;
}
self.prune();
self
}
fn neg(mut self) -> Poly {
for c in self.terms.values_mut() {
*c = -*c;
}
self
}
fn scale(mut self, s: f64) -> Poly {
if s == 0.0 {
return Poly::default();
}
for c in self.terms.values_mut() {
*c *= s;
}
self
}
fn mul(&self, other: &Poly) -> Option<Poly> {
let mut out = Poly::default();
for (ma, ca) in &self.terms {
for (mb, cb) in &other.terms {
if ma.len() + mb.len() > 2 {
return None;
}
let mut m = ma.clone();
m.extend_from_slice(mb);
m.sort_unstable();
*out.terms.entry(m).or_insert(0.0) += ca * cb;
}
}
out.prune();
Some(out)
}
fn prune(&mut self) {
self.terms.retain(|_, c| c.abs() > 0.0);
}
}
fn to_poly(e: &Expr) -> Option<Poly> {
match e {
Expr::Const(c) => Some(Poly::constant(*c)),
Expr::Var(i) => Some(Poly::var(*i)),
Expr::Cse(body) => to_poly(body),
Expr::Sum(items) => {
let mut acc = Poly::default();
for it in items {
let p = to_poly(it)?;
for (m, c) in &p.terms {
*acc.terms.entry(m.clone()).or_insert(0.0) += c;
}
}
acc.prune();
Some(acc)
}
Expr::Unary(op, a) => match op {
UnaryOp::Neg => Some(to_poly(a)?.neg()),
_ => None,
},
Expr::Binary(op, a, b) => {
let pa = to_poly(a)?;
let pb = to_poly(b)?;
match op {
BinOp::Add => Some(pa.add(&pb)),
BinOp::Sub => Some(pa.add(&pb.neg())),
BinOp::Mul => pa.mul(&pb),
BinOp::Div => {
let d = pb.as_constant()?;
if d == 0.0 {
None
} else {
Some(pa.scale(1.0 / d))
}
}
BinOp::Pow => {
let exp = pb.as_constant()?;
if exp == 0.0 {
Some(Poly::constant(1.0))
} else if exp == 1.0 {
Some(pa)
} else if exp == 2.0 {
pa.mul(&pa)
} else {
None
}
}
_ => None,
}
}
Expr::Funcall { .. } => None,
_ => None,
}
}
fn is_trivially_zero(e: &Expr) -> bool {
matches!(e, Expr::Const(c) if *c == 0.0)
}
fn hessian_active_vars(h: &QuadHessian) -> usize {
let mut active: Vec<usize> = Vec::with_capacity(2 * h.len());
for (i, j) in h.keys() {
active.push(*i);
active.push(*j);
}
active.sort_unstable();
active.dedup();
active.len()
}
fn qcqp_constraint_too_costly_for_socp(h: &QuadHessian) -> bool {
let has_offdiag = h.keys().any(|(i, j)| i != j);
has_offdiag && hessian_active_vars(h) > QCQP_SOCP_COUPLED_VARS
}
fn hessian_is_psd(h: &QuadHessian, _n: usize) -> bool {
if h.is_empty() {
return true; }
if h.keys().all(|(i, j)| i == j) {
return h.values().all(|v| *v >= -PSD_TOL);
}
coupled_hessian_is_psd(h)
}
fn coupled_hessian_is_psd(h: &QuadHessian) -> bool {
use feral::{CscMatrix, FactorStatus, Solver};
let mut active: Vec<usize> = Vec::with_capacity(2 * h.len());
for (i, j) in h.keys() {
active.push(*i);
active.push(*j);
}
active.sort_unstable();
active.dedup();
let k = active.len();
let idx = |v: usize| active.binary_search(&v).unwrap();
let mut rows: Vec<usize> = Vec::with_capacity(h.len() + k);
let mut cols: Vec<usize> = Vec::with_capacity(h.len() + k);
let mut vals: Vec<f64> = Vec::with_capacity(h.len() + k);
for ((i, j), v) in h {
let (ri, rj) = (idx(*i), idx(*j));
rows.push(rj);
cols.push(ri);
vals.push(*v);
}
for d in 0..k {
rows.push(d);
cols.push(d);
vals.push(PSD_TOL);
}
let mat = match CscMatrix::from_triplets(k, &rows, &cols, &vals) {
Ok(m) => m,
Err(_) => return false, };
let mut solver = Solver::new();
match solver.factor(&mat, None) {
FactorStatus::Success => {
solver.inertia().map(|i| i.negative == 0).unwrap_or(false)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nl_reader::parse_nl_text;
#[test]
fn parse_selection_values() {
assert_eq!(SolverSelection::parse("auto"), Some(SolverSelection::Auto));
assert_eq!(SolverSelection::parse("nlp"), Some(SolverSelection::Nlp));
assert_eq!(
SolverSelection::parse("lp-ipm"),
Some(SolverSelection::LpIpm)
);
assert_eq!(
SolverSelection::parse("qp-ipm"),
Some(SolverSelection::QpIpm)
);
assert_eq!(
SolverSelection::parse("qp-active-set"),
Some(SolverSelection::QpActiveSet)
);
assert_eq!(SolverSelection::parse("lp-simplex"), None);
assert_eq!(SolverSelection::parse("bogus"), None);
}
#[test]
fn auto_routes_convex_qp_family_to_qp_ipm() {
assert_eq!(
resolve_solver(ProblemClass::Lp, SolverSelection::Auto),
Ok(SolverChoice::QpIpm),
"auto should route LP to the convex IPM (P=0)"
);
assert_eq!(
resolve_solver(ProblemClass::ConvexQp, SolverSelection::Auto),
Ok(SolverChoice::QpIpm),
"auto should route convex QP to the convex IPM"
);
}
#[test]
fn auto_routes_convex_qcqp_to_socp() {
assert_eq!(
resolve_solver(ProblemClass::ConvexQcqp, SolverSelection::Auto),
Ok(SolverChoice::SocpIpm),
"auto should route convex QCQP to the conic IPM"
);
}
#[test]
fn auto_routes_nonconvex_to_nlp() {
for class in [ProblemClass::NonconvexQp, ProblemClass::Nlp] {
assert_eq!(
resolve_solver(class, SolverSelection::Auto),
Ok(SolverChoice::Nlp),
"auto must resolve to Nlp for {:?}",
class
);
}
}
#[test]
fn forced_socp_accepts_convex_cone_family_only() {
for class in [
ProblemClass::Lp,
ProblemClass::ConvexQp,
ProblemClass::ConvexQcqp,
] {
assert_eq!(
resolve_solver(class, SolverSelection::Socp),
Ok(SolverChoice::SocpIpm),
"socp should accept {:?}",
class
);
}
assert!(resolve_solver(ProblemClass::NonconvexQp, SolverSelection::Socp).is_err());
assert!(resolve_solver(ProblemClass::Nlp, SolverSelection::Socp).is_err());
}
#[test]
fn forced_nlp_always_ok() {
assert_eq!(
resolve_solver(ProblemClass::ConvexQp, SolverSelection::Nlp),
Ok(SolverChoice::Nlp)
);
}
#[test]
fn forced_lp_on_nlp_errors() {
let err = resolve_solver(ProblemClass::Nlp, SolverSelection::LpIpm).unwrap_err();
assert!(err.contains("NLP"), "msg should name detected class: {err}");
assert!(
err.contains("lp-ipm"),
"msg should name forced solver: {err}"
);
}
#[test]
fn forced_lp_on_lp_ok() {
assert_eq!(
resolve_solver(ProblemClass::Lp, SolverSelection::LpIpm),
Ok(SolverChoice::LpIpm)
);
}
#[test]
fn forced_qp_accepts_lp_and_convex_qp_only() {
assert_eq!(
resolve_solver(ProblemClass::Lp, SolverSelection::QpIpm),
Ok(SolverChoice::QpIpm)
);
assert_eq!(
resolve_solver(ProblemClass::ConvexQp, SolverSelection::QpIpm),
Ok(SolverChoice::QpIpm)
);
assert!(resolve_solver(ProblemClass::NonconvexQp, SolverSelection::QpIpm).is_err());
assert!(resolve_solver(ProblemClass::Nlp, SolverSelection::QpIpm).is_err());
}
#[test]
fn poly_of_quadratic_diagonal() {
let e = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(1.0)),
)),
Box::new(Expr::Const(2.0)),
);
let h = analyze_quadratic(&e, 1).expect("degree-2 polynomial");
assert_eq!(h.get(&(0, 0)), Some(&2.0));
}
#[test]
fn poly_rejects_transcendental() {
let e = Expr::Unary(UnaryOp::Sin, Box::new(Expr::Var(0)));
assert!(analyze_quadratic(&e, 1).is_none());
}
#[test]
fn poly_rejects_cubic() {
let e = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
);
assert!(analyze_quadratic(&e, 1).is_none());
}
#[test]
fn cross_term_hessian() {
let e = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
let h = analyze_quadratic(&e, 2).expect("degree-2");
assert_eq!(h.get(&(0, 1)), Some(&1.0));
}
#[test]
fn large_quadratic_sum_lowers_without_quadratic_blowup() {
const N: usize = 5000;
let terms: Vec<Expr> = (0..N)
.map(|i| Expr::Binary(BinOp::Mul, Box::new(Expr::Var(i)), Box::new(Expr::Var(i))))
.collect();
let e = Expr::Sum(terms);
let h = analyze_quadratic(&e, N).expect("degree-2 sum of squares is a QP");
assert_eq!(h.len(), N, "every xᵢ² contributes one diagonal entry");
assert_eq!(h.get(&(0, 0)), Some(&2.0));
assert_eq!(h.get(&(N - 1, N - 1)), Some(&2.0));
}
#[test]
fn psd_accepts_convex_separable() {
let mut h = QuadHessian::new();
h.insert((0, 0), 2.0);
h.insert((1, 1), 4.0);
assert!(hessian_is_psd(&h, 2));
}
#[test]
fn psd_rejects_indefinite() {
let mut h = QuadHessian::new();
h.insert((0, 1), 1.0);
assert!(!hessian_is_psd(&h, 2));
}
#[test]
fn psd_accepts_psd_with_zero_eigenvalue() {
let mut h = QuadHessian::new();
h.insert((0, 0), 1.0);
h.insert((0, 1), 1.0);
h.insert((1, 1), 1.0);
assert!(hessian_is_psd(&h, 2));
}
#[test]
fn psd_rejects_small_but_real_negative_curvature() {
let mut h = QuadHessian::new();
h.insert((0, 0), 2.0);
h.insert((1, 1), -1e-3);
assert!(
!hessian_is_psd(&h, 2),
"a −1e-3 eigenvalue must read indefinite, not be rounded to PSD"
);
}
#[test]
fn psd_threshold_is_psd_tol() {
let mut just_inside = QuadHessian::new();
just_inside.insert((0, 0), -1e-10); assert!(
hessian_is_psd(&just_inside, 1),
"−1e-10 is within tolerance and must round to PSD"
);
let mut just_outside = QuadHessian::new();
just_outside.insert((0, 0), -1e-7); assert!(
!hessian_is_psd(&just_outside, 1),
"−1e-7 is beyond tolerance and must read indefinite"
);
}
#[test]
fn large_diagonal_hessian_is_cheap_and_psd() {
let n = 50_000;
let mut h = QuadHessian::new();
for i in 0..n {
h.insert((i, i), 2.0);
}
assert!(
hessian_is_psd(&h, n),
"diag(2,…,2) is PSD and must be settled by the O(nnz) sign path"
);
}
#[test]
fn large_coupled_convex_hessian_is_certified_psd() {
let k = 1_000;
let mut h = QuadHessian::new();
for i in 0..k {
h.insert((i, i), 2.0);
}
for i in 0..(k - 1) {
h.insert((i, i + 1), 0.1);
}
assert!(
hessian_is_psd(&h, k),
"a diagonally-dominant coupled Hessian over {k} vars must be \
certified PSD by the sparse factorization (CVXQP regression)"
);
}
#[test]
fn large_coupled_indefinite_hessian_is_rejected() {
let k = 1_000;
let mut h = QuadHessian::new();
for i in 0..k {
h.insert((i, i), 2.0);
}
for i in 0..(k - 1) {
h.insert((i, i + 1), 0.1);
}
h.insert((0, 0), -5.0);
assert!(
!hessian_is_psd(&h, k),
"a coupled Hessian with a strong negative-curvature direction \
must be rejected regardless of size"
);
}
#[test]
fn small_coupled_hessian_is_certified_psd() {
let mut h = QuadHessian::new();
h.insert((0, 0), 2.0);
h.insert((0, 1), 1.0);
h.insert((1, 1), 2.0);
assert!(hessian_is_psd(&h, 2));
}
#[test]
fn classify_pure_lp() {
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::Const(0.0)],
con_linear: vec![vec![(0, 1.0), (1, 1.0)]],
x_l: vec![0.0, 0.0],
x_u: vec![f64::INFINITY, f64::INFINITY],
g_l: vec![f64::NEG_INFINITY],
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(),
};
assert_eq!(classify_problem(&prob), ProblemClass::Lp);
}
#[test]
fn classify_convex_qp() {
let obj = Expr::Binary(
BinOp::Add,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)),
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(1)),
Box::new(Expr::Const(2.0)),
)),
);
let prob = qp_stub(obj, vec![Expr::Const(0.0)]);
assert_eq!(classify_problem(&prob), ProblemClass::ConvexQp);
}
#[test]
fn classify_nonconvex_qp() {
let obj = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
let prob = qp_stub(obj, vec![Expr::Const(0.0)]);
assert_eq!(classify_problem(&prob), ProblemClass::NonconvexQp);
}
#[test]
fn classify_nlp_from_transcendental_objective() {
let obj = Expr::Unary(UnaryOp::Exp, Box::new(Expr::Var(0)));
let prob = qp_stub(obj, vec![Expr::Const(0.0)]);
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
#[test]
fn classify_maximize_psd_objective_is_nonconvex() {
let obj = Expr::Binary(
BinOp::Add,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)),
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(1)),
Box::new(Expr::Const(2.0)),
)),
);
let mut prob = qp_stub(obj, vec![Expr::Const(0.0)]);
prob.minimize = false;
assert_eq!(classify_problem(&prob), ProblemClass::NonconvexQp);
}
#[test]
fn classify_maximize_concave_objective_is_convex() {
let neg_sq = |v: usize| {
Expr::Unary(
UnaryOp::Neg,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(v)),
Box::new(Expr::Const(2.0)),
)),
)
};
let obj = Expr::Binary(BinOp::Add, Box::new(neg_sq(0)), Box::new(neg_sq(1)));
let mut prob = qp_stub(obj, vec![Expr::Const(0.0)]);
prob.minimize = false;
assert_eq!(classify_problem(&prob), ProblemClass::ConvexQp);
}
#[test]
fn classify_convex_qcqp() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let con = Expr::Binary(
BinOp::Add,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)),
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(1)),
Box::new(Expr::Const(2.0)),
)),
);
let prob = qp_stub(obj, vec![con]);
assert_eq!(classify_problem(&prob), ProblemClass::ConvexQcqp);
}
fn convex_qcqp_at_size(n: usize, m: usize) -> NlProblem {
let mut con_nonlinear = vec![Expr::Const(0.0); m];
con_nonlinear[0] = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let g_l = vec![f64::NEG_INFINITY; m];
let mut g_u = vec![f64::INFINITY; m];
g_u[0] = 1.0; NlProblem {
n,
m,
num_obj: 1,
minimize: true,
obj_nonlinear: Expr::Const(0.0),
obj_linear: vec![(0, 1.0)],
obj_constant: 0.0,
con_nonlinear,
con_linear: vec![vec![]; m],
x_l: vec![f64::NEG_INFINITY; n],
x_u: vec![f64::INFINITY; n],
g_l,
g_u,
x0: vec![0.0; n],
lambda0: vec![0.0; m],
suffixes: Default::default(),
imported_funcs: Vec::new(),
var_names: Vec::new(),
con_names: Vec::new(),
}
}
#[test]
fn small_convex_qcqp_routes_to_conic() {
let prob = convex_qcqp_at_size(100, 100); assert_eq!(classify_problem(&prob), ProblemClass::ConvexQcqp);
}
#[test]
fn oversized_convex_qcqp_falls_back_to_nlp() {
let prob = convex_qcqp_at_size(10_001, 10_001);
assert!((prob.n as u64) * (prob.m as u64) > SOCP_SIZE_BUDGET);
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
fn coupled_convex_qcqp_with_k_vars(k: usize) -> NlProblem {
let mut sum = Expr::Var(0);
for i in 1..k {
sum = Expr::Binary(BinOp::Add, Box::new(sum), Box::new(Expr::Var(i)));
}
let con = Expr::Binary(BinOp::Pow, Box::new(sum), Box::new(Expr::Const(2.0)));
NlProblem {
n: k,
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![f64::NEG_INFINITY; k],
x_u: vec![f64::INFINITY; k],
g_l: vec![f64::NEG_INFINITY],
g_u: vec![1.0],
x0: vec![0.0; k],
lambda0: vec![0.0],
suffixes: Default::default(),
imported_funcs: Vec::new(),
var_names: Vec::new(),
con_names: Vec::new(),
}
}
#[test]
fn heavily_coupled_convex_qcqp_falls_back_to_nlp() {
let k = QCQP_SOCP_COUPLED_VARS + 44; let prob = coupled_convex_qcqp_with_k_vars(k);
assert!((prob.n as u64) * (prob.m as u64) <= SOCP_SIZE_BUDGET);
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
#[test]
fn lightly_coupled_convex_qcqp_keeps_conic() {
let k = QCQP_SOCP_COUPLED_VARS - 6; let prob = coupled_convex_qcqp_with_k_vars(k);
assert_eq!(classify_problem(&prob), ProblemClass::ConvexQcqp);
}
#[test]
fn classify_concave_minimize_is_nonconvex() {
let obj = Expr::Unary(
UnaryOp::Neg,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)),
);
let prob = qp_stub(obj, vec![Expr::Const(0.0)]);
assert_eq!(classify_problem(&prob), ProblemClass::NonconvexQp);
}
#[test]
fn classify_qcqp_with_indefinite_constraint_falls_back_to_nlp() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let con = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
let prob = qp_stub(obj, vec![con]);
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
#[test]
fn classify_psd_quadratic_with_lower_bound_is_nonconvex() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let con = Expr::Binary(
BinOp::Add,
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)),
Box::new(Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(1)),
Box::new(Expr::Const(2.0)),
)),
);
let mut prob = qp_stub(obj, vec![con]);
prob.g_l = vec![1.0];
prob.g_u = vec![f64::INFINITY];
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
#[test]
fn classify_quadratic_equality_is_nonconvex() {
let obj = Expr::Const(0.0);
let con = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let mut prob = qp_stub(obj, vec![con]);
prob.g_l = vec![1.0];
prob.g_u = vec![1.0]; assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
#[test]
fn classify_cancelling_quadratic_objective_is_lp() {
let sq = || {
Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
)
};
let obj = Expr::Binary(BinOp::Sub, Box::new(sq()), Box::new(sq()));
let prob = qp_stub(obj, vec![Expr::Const(0.0)]);
assert_eq!(classify_problem(&prob), ProblemClass::Lp);
}
#[test]
fn classify_nlp_from_transcendental_constraint() {
let obj = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(2.0)),
);
let con = Expr::Unary(UnaryOp::Log, Box::new(Expr::Var(1)));
let prob = qp_stub(obj, vec![con]);
assert_eq!(classify_problem(&prob), ProblemClass::Nlp);
}
fn qp_stub(obj_nonlinear: Expr, con_nonlinear: Vec<Expr>) -> NlProblem {
let m = con_nonlinear.len();
NlProblem {
n: 2,
m,
num_obj: 1,
minimize: true,
obj_nonlinear,
obj_linear: vec![],
obj_constant: 0.0,
con_nonlinear,
con_linear: vec![vec![]; m],
x_l: vec![f64::NEG_INFINITY; 2],
x_u: vec![f64::INFINITY; 2],
g_l: vec![f64::NEG_INFINITY; m],
g_u: vec![0.0; m],
x0: vec![0.0; 2],
lambda0: vec![0.0; m],
suffixes: Default::default(),
imported_funcs: Vec::new(),
var_names: Vec::new(),
con_names: Vec::new(),
}
}
#[allow(dead_code)]
fn _parse(txt: &str) -> NlProblem {
parse_nl_text(txt).expect("valid .nl")
}
}