use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::{Index, Number};
use pounce_convex::{solve_socp_ipm, ConeSpec, QpOptions, QpProblem, QpStatus, Triplet};
use pounce_feral::FeralSolverInterface;
use pounce_linsol::SparseSymLinearSolverInterface;
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
TNLP,
};
use std::cell::RefCell;
use std::rc::Rc;
fn backend() -> Box<dyn SparseSymLinearSolverInterface> {
Box::new(FeralSolverInterface::new())
}
fn opts() -> QpOptions {
QpOptions {
max_iter: 200,
..QpOptions::default()
}
}
struct ClosureNlp {
n: usize,
lb: Vec<f64>,
ub: Vec<f64>,
x0: Vec<f64>,
a_rows: Vec<Vec<(usize, f64)>>,
b: Vec<f64>,
f: Box<dyn Fn(&[f64]) -> f64>,
grad: Box<dyn Fn(&[f64], &mut [f64])>,
hess_pattern: Vec<(usize, usize)>,
hess: Box<dyn Fn(&[f64], f64, &mut [f64])>,
captured_obj: RefCell<Option<f64>>,
captured_x: RefCell<Option<Vec<f64>>>,
}
impl TNLP for ClosureNlp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
let nnz_jac: usize = self.a_rows.iter().map(|r| r.len()).sum();
Some(NlpInfo {
n: self.n as Index,
m: self.a_rows.len() as Index,
nnz_jac_g: nnz_jac as Index,
nnz_h_lag: self.hess_pattern.len() as Index,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&self.lb);
b.x_u.copy_from_slice(&self.ub);
for (i, &bi) in self.b.iter().enumerate() {
b.g_l[i] = bi;
b.g_u[i] = bi;
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&self.x0);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some((self.f)(x))
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
(self.grad)(x, grad);
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
for (r, row) in self.a_rows.iter().enumerate() {
g[r] = row.iter().map(|&(c, v)| v * x[c]).sum();
}
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
let mut k = 0;
for (r, row) in self.a_rows.iter().enumerate() {
for &(c, _) in row {
irow[k] = r as Index;
jcol[k] = c as Index;
k += 1;
}
}
}
SparsityRequest::Values { values } => {
let mut k = 0;
for row in &self.a_rows {
for &(_, v) in row {
values[k] = v;
k += 1;
}
}
}
}
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_new_x: bool,
obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for (k, &(r, c)) in self.hess_pattern.iter().enumerate() {
irow[k] = r as Index;
jcol[k] = c as Index;
}
}
SparsityRequest::Values { values } => {
(self.hess)(x.expect("eval_h needs x"), obj_factor, values);
}
}
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
*self.captured_obj.borrow_mut() = Some(sol.obj_value);
*self.captured_x.borrow_mut() = Some(sol.x.to_vec());
}
}
fn solve_nlp(label: &str, nlp: ClosureNlp) -> (f64, Vec<f64>) {
let mut app = IpoptApplication::new();
app.initialize().expect("init");
let _ = app.options_mut().read_from_str("print_level 0\n", true);
let rc = Rc::new(RefCell::new(nlp));
let tnlp: Rc<RefCell<dyn TNLP>> = rc.clone();
let t0 = std::time::Instant::now();
let status = app.optimize_tnlp(Rc::clone(&tnlp));
let dt = t0.elapsed();
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"NLP solve failed: {status:?}"
);
eprintln!(
" [{label}] NLP: iters={}, time={:.1}µs",
app.statistics().iteration_count,
dt.as_secs_f64() * 1e6
);
let obj = rc.borrow().captured_obj.borrow().expect("obj");
let x = rc.borrow().captured_x.borrow().clone().expect("x");
(obj, x)
}
fn timed_conic(label: &str, prob: &QpProblem, specs: &[ConeSpec]) -> pounce_convex::QpSolution {
let t0 = std::time::Instant::now();
let sol = solve_socp_ipm(prob, specs, &opts(), backend);
let dt = t0.elapsed();
eprintln!(
" [{label}] conic: iters={}, time={:.1}µs",
sol.iters,
dt.as_secs_f64() * 1e6
);
sol
}
#[test]
fn geometric_program_conic_matches_nlp() {
let prob = QpProblem {
n: 3, p_lower: vec![],
c: vec![0.0, 1.0, 1.0],
a: vec![],
b: vec![],
g: vec![
Triplet::new(0, 0, -1.0), Triplet::new(2, 1, -1.0), Triplet::new(3, 0, 1.0), Triplet::new(5, 2, -1.0), ],
h: vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0],
lb: vec![],
ub: vec![],
};
let conic = timed_conic("GP", &prob, &[ConeSpec::Exponential, ConeSpec::Exponential]);
assert!(
matches!(
conic.status,
QpStatus::Optimal | QpStatus::OptimalInaccurate
),
"conic: {:?}",
conic.status
);
let nlp = ClosureNlp {
n: 1,
lb: vec![-30.0],
ub: vec![30.0],
x0: vec![0.5],
a_rows: vec![],
b: vec![],
f: Box::new(|x| x[0].exp() + (-x[0]).exp()),
grad: Box::new(|x, g| g[0] = x[0].exp() - (-x[0]).exp()),
hess_pattern: vec![(0, 0)],
hess: Box::new(|x, of, v| v[0] = of * (x[0].exp() + (-x[0]).exp())),
captured_obj: RefCell::new(None),
captured_x: RefCell::new(None),
};
let (nlp_obj, _) = solve_nlp("GP", nlp);
assert!(
(conic.obj - nlp_obj).abs() < 1e-5,
"GP objectives disagree: conic={}, nlp={nlp_obj}",
conic.obj
);
assert!((conic.obj - 2.0).abs() < 1e-5, "GP obj {} vs 2", conic.obj);
eprintln!("GP: conic obj={:.8}, nlp obj={:.8}", conic.obj, nlp_obj);
}
#[test]
fn entropy_maximization_conic_matches_nlp() {
let n = 3usize;
let want_obj = -(n as f64).ln();
let mut g = Vec::new();
let mut h = Vec::new();
for i in 0..n {
let base = 3 * i;
g.push(Triplet::new(base, i, -1.0)); h.push(0.0);
g.push(Triplet::new(base + 1, n + i, -1.0)); h.push(0.0);
h.push(1.0); }
let a: Vec<Triplet> = (0..n).map(|i| Triplet::new(0, n + i, 1.0)).collect();
let mut c = vec![0.0; 2 * n];
for ci in c.iter_mut().take(n) {
*ci = -1.0; }
let prob = QpProblem {
n: 2 * n,
p_lower: vec![],
c,
a,
b: vec![1.0],
g,
h,
lb: vec![],
ub: vec![],
};
let specs = vec![ConeSpec::Exponential; n];
let conic = timed_conic("entropy", &prob, &specs);
assert!(
matches!(
conic.status,
QpStatus::Optimal | QpStatus::OptimalInaccurate
),
"conic: {:?}",
conic.status
);
let nlp = ClosureNlp {
n,
lb: vec![1e-9; n],
ub: vec![1e19; n],
x0: vec![1.0 / n as f64; n],
a_rows: vec![(0..n).map(|i| (i, 1.0)).collect()],
b: vec![1.0],
f: Box::new(|x| x.iter().map(|&xi| xi * xi.ln()).sum()),
grad: Box::new(|x, g| {
for (gi, &xi) in g.iter_mut().zip(x) {
*gi = xi.ln() + 1.0;
}
}),
hess_pattern: (0..n).map(|i| (i, i)).collect(),
hess: Box::new(|x, of, v| {
for (vi, &xi) in v.iter_mut().zip(x) {
*vi = of / xi; }
}),
captured_obj: RefCell::new(None),
captured_x: RefCell::new(None),
};
let (nlp_obj, nlp_x) = solve_nlp("entropy", nlp);
assert!(
(conic.obj - nlp_obj).abs() < 1e-5,
"entropy objectives disagree: conic={}, nlp={nlp_obj}",
conic.obj
);
assert!(
(conic.obj - want_obj).abs() < 1e-5,
"entropy obj {} vs −log {n} = {want_obj}",
conic.obj
);
for i in 0..n {
assert!(
(conic.x[n + i] - 1.0 / n as f64).abs() < 1e-4,
"conic x[{i}] = {} vs 1/{n}",
conic.x[n + i]
);
assert!((nlp_x[i] - 1.0 / n as f64).abs() < 1e-4, "nlp x[{i}]");
}
eprintln!(
"entropy(n={n}): conic obj={:.8}, nlp obj={:.8}, want={want_obj:.8}",
conic.obj, nlp_obj
);
}
#[test]
fn log_sum_exp_conic_matches_nlp() {
let prob = QpProblem {
n: 5, p_lower: vec![],
c: vec![1.0, 0.0, 0.0, 0.0, 0.0],
a: vec![Triplet::new(0, 1, 1.0), Triplet::new(0, 2, 1.0)], b: vec![0.0],
g: vec![
Triplet::new(0, 1, -1.0), Triplet::new(0, 0, 1.0), Triplet::new(2, 3, -1.0), Triplet::new(3, 2, -1.0), Triplet::new(3, 0, 1.0), Triplet::new(5, 4, -1.0), Triplet::new(6, 3, 1.0),
Triplet::new(6, 4, 1.0),
],
h: vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
lb: vec![],
ub: vec![],
};
let specs = [
ConeSpec::Exponential,
ConeSpec::Exponential,
ConeSpec::Nonneg(1),
];
let conic = timed_conic("lse", &prob, &specs);
assert!(
matches!(
conic.status,
QpStatus::Optimal | QpStatus::OptimalInaccurate
),
"conic: {:?}",
conic.status
);
let nlp = ClosureNlp {
n: 2,
lb: vec![-1e19; 2],
ub: vec![1e19; 2],
x0: vec![0.5, -0.5],
a_rows: vec![vec![(0, 1.0), (1, 1.0)]],
b: vec![0.0],
f: Box::new(|x| (x[0].exp() + x[1].exp()).ln()),
grad: Box::new(|x, g| {
let (e0, e1) = (x[0].exp(), x[1].exp());
let s = e0 + e1;
g[0] = e0 / s;
g[1] = e1 / s;
}),
hess_pattern: vec![(0, 0), (1, 0), (1, 1)],
hess: Box::new(|x, of, v| {
let (e0, e1) = (x[0].exp(), x[1].exp());
let s = e0 + e1;
let (p0, p1) = (e0 / s, e1 / s);
v[0] = of * p0 * (1.0 - p0);
v[1] = -of * p0 * p1;
v[2] = of * p1 * (1.0 - p1);
}),
captured_obj: RefCell::new(None),
captured_x: RefCell::new(None),
};
let (nlp_obj, _) = solve_nlp("lse", nlp);
let want = 2.0_f64.ln();
assert!(
(conic.obj - nlp_obj).abs() < 1e-5,
"lse objectives disagree: conic={}, nlp={nlp_obj}",
conic.obj
);
assert!(
(conic.obj - want).abs() < 1e-5,
"lse obj {} vs log2",
conic.obj
);
eprintln!("lse: conic obj={:.8}, nlp obj={:.8}", conic.obj, nlp_obj);
}
#[test]
fn power_cone_geometric_mean_matches_nlp() {
let prob = QpProblem {
n: 3, p_lower: vec![],
c: vec![-1.0, 0.0, 0.0], a: vec![
Triplet::new(0, 1, 1.0), Triplet::new(1, 2, 1.0), ],
b: vec![2.0, 8.0],
g: vec![
Triplet::new(0, 0, -1.0), Triplet::new(1, 1, -1.0), Triplet::new(2, 2, -1.0), ],
h: vec![0.0, 0.0, 0.0],
lb: vec![],
ub: vec![],
};
let conic = timed_conic("power-gm", &prob, &[ConeSpec::Power(0.5)]);
assert!(
matches!(
conic.status,
QpStatus::Optimal | QpStatus::OptimalInaccurate
),
"conic: {:?}",
conic.status
);
let nlp = ClosureNlp {
n: 1,
lb: vec![0.0],
ub: vec![10.0],
x0: vec![1.0],
a_rows: vec![],
b: vec![],
f: Box::new(|x| -x[0]),
grad: Box::new(|_x, g| g[0] = -1.0),
hess_pattern: vec![(0, 0)],
hess: Box::new(|_x, _of, v| v[0] = 0.0),
captured_obj: RefCell::new(None),
captured_x: RefCell::new(None),
};
let mut nlp = nlp;
nlp.ub = vec![(2.0_f64 * 8.0).sqrt()];
let (nlp_obj, _) = solve_nlp("power-gm", nlp);
assert!(
(-conic.obj - 4.0).abs() < 1e-5,
"conic x* = {} vs geometric mean 4",
-conic.obj
);
assert!(
(conic.obj - nlp_obj).abs() < 1e-5,
"power objectives disagree: conic={}, nlp={nlp_obj}",
conic.obj
);
assert!((conic.x[0] - 4.0).abs() < 1e-4, "x = {}", conic.x[0]);
assert!((conic.x[1] - 2.0).abs() < 1e-4, "y = {}", conic.x[1]);
assert!((conic.x[2] - 8.0).abs() < 1e-4, "z = {}", conic.x[2]);
eprintln!("power-gm: conic x*={:.8}", -conic.obj);
}
#[test]
fn entropy_maximization_larger_instance() {
let n = 16usize;
let want_obj = -(n as f64).ln();
let mut g = Vec::new();
let mut h = Vec::new();
for i in 0..n {
let base = 3 * i;
g.push(Triplet::new(base, i, -1.0)); h.push(0.0);
g.push(Triplet::new(base + 1, n + i, -1.0)); h.push(0.0);
h.push(1.0); }
let a: Vec<Triplet> = (0..n).map(|i| Triplet::new(0, n + i, 1.0)).collect();
let mut c = vec![0.0; 2 * n];
for ci in c.iter_mut().take(n) {
*ci = -1.0;
}
let prob = QpProblem {
n: 2 * n,
p_lower: vec![],
c,
a,
b: vec![1.0],
g,
h,
lb: vec![],
ub: vec![],
};
let specs = vec![ConeSpec::Exponential; n];
let conic = timed_conic("entropy16", &prob, &specs);
assert!(
matches!(
conic.status,
QpStatus::Optimal | QpStatus::OptimalInaccurate
),
"conic: {:?}",
conic.status
);
assert!(
(conic.obj - want_obj).abs() < 1e-4,
"entropy(n=16) obj {} vs −log 16 = {want_obj}",
conic.obj
);
for i in 0..n {
assert!(
(conic.x[n + i] - 1.0 / n as f64).abs() < 1e-3,
"x[{i}] = {} vs 1/16",
conic.x[n + i]
);
}
}
#[test]
fn near_boundary_gp_matches_nlp() {
let mut solved_any = false;
for &u in &[1.0_f64, 1.5, 2.0, 2.5, 3.0] {
let prob = QpProblem {
n: 3, p_lower: vec![],
c: vec![0.0, 1.0, 1.0],
a: vec![Triplet::new(0, 0, 1.0)], b: vec![u],
g: vec![
Triplet::new(0, 0, -1.0), Triplet::new(2, 1, -1.0), Triplet::new(3, 0, 1.0), Triplet::new(5, 2, -1.0), ],
h: vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0],
lb: vec![],
ub: vec![],
};
let conic = timed_conic(
"gp-boundary",
&prob,
&[ConeSpec::Exponential, ConeSpec::Exponential],
);
assert!(
matches!(
conic.status,
QpStatus::Optimal
| QpStatus::OptimalInaccurate
| QpStatus::NumericalFailure
| QpStatus::IterationLimit
),
"u={u}: unexpected status {:?}",
conic.status
);
if conic.status != QpStatus::Optimal {
eprintln!(
"gp-boundary: u={u} -> {:?} (documented near-boundary gap)",
conic.status
);
continue;
}
solved_any = true;
let want = u.exp() + (-u).exp();
let nlp = ClosureNlp {
n: 1,
lb: vec![u],
ub: vec![u],
x0: vec![u],
a_rows: vec![],
b: vec![],
f: Box::new(|x| x[0].exp() + (-x[0]).exp()),
grad: Box::new(|x, g| g[0] = x[0].exp() - (-x[0]).exp()),
hess_pattern: vec![(0, 0)],
hess: Box::new(|x, of, v| v[0] = of * (x[0].exp() + (-x[0]).exp())),
captured_obj: RefCell::new(None),
captured_x: RefCell::new(None),
};
let (nlp_obj, _) = solve_nlp("gp-boundary", nlp);
assert!(
(conic.obj - want).abs() < 1e-4,
"u={u}: near-boundary GP obj {} vs e^u+e^-u = {want}",
conic.obj
);
assert!(
(conic.obj - nlp_obj).abs() < 1e-4,
"u={u}: GP objectives disagree: conic={}, nlp={nlp_obj}",
conic.obj
);
eprintln!(
"gp-boundary: u={u} conic obj={:.8}, nlp obj={:.8}",
conic.obj, nlp_obj
);
}
assert!(
solved_any,
"exp-cone driver solved no near-boundary GP instance"
);
}