use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::Number;
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;
const N: usize = 1000;
#[derive(Default)]
struct SeparableQuartic;
impl TNLP for SeparableQuartic {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: N as i32,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: N as i32,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-2.0e19; N]);
b.x_u.copy_from_slice(&[2.0e19; N]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[2.0; N]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(
x.iter()
.enumerate()
.map(|(i, xi)| (xi - i as Number).powi(4))
.sum(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
for (i, (gi, xi)) in g.iter_mut().zip(x).enumerate() {
*gi = 4.0 * (xi - i as Number).powi(3);
}
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_mode: SparsityRequest<'_>,
) -> bool {
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 i in 0..N {
irow[i] = i as i32;
jcol[i] = i as i32;
}
}
SparsityRequest::Values { values } => {
let x = x.expect("eval_h(Values) without x");
for (i, v) in values.iter_mut().enumerate() {
*v = obj_factor * 12.0 * (x[i] - i as Number).powi(2);
}
}
}
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
fn solve(opts: &[(&str, Number)]) -> (ApplicationReturnStatus, Number, Number, usize) {
solve_capped(opts, None)
}
fn solve_capped(
opts: &[(&str, Number)],
max_iter: Option<i32>,
) -> (ApplicationReturnStatus, Number, Number, usize) {
let mut app = IpoptApplication::new();
for (k, v) in opts {
app.options_mut()
.set_numeric_value(k, *v, true, false)
.unwrap();
}
if let Some(m) = max_iter {
app.options_mut()
.set_integer_value("max_iter", m, true, false)
.unwrap();
}
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(SeparableQuartic));
let status = app.optimize_tnlp(tnlp);
let s = app.statistics();
(
status,
s.final_objective,
s.final_unscaled_kkt_error,
s.iteration_count as usize,
)
}
#[test]
fn masked_certificate_is_refused_and_the_run_reaches_the_true_minimum() {
let (off_status, off_obj, off_err, off_iter) =
solve(&[("obj_scale_certificate_threshold", 0.0)]);
let (on_status, on_obj, on_err, on_iter) = solve(&[]);
eprintln!(
"veto off: {off_status:?} obj={off_obj:.6e} unscaled_err={off_err:.3e} iters={off_iter}\n\
veto on : {on_status:?} obj={on_obj:.6e} unscaled_err={on_err:.3e} iters={on_iter}"
);
assert!(
matches!(off_status, ApplicationReturnStatus::SolveSucceeded),
"premise: opt-out should reproduce the false certificate, got {off_status:?}"
);
assert!(
off_obj > 1.0,
"premise: opt-out should stop far from the minimum, got obj {off_obj:.6e}"
);
assert!(
off_err > 1e-3,
"premise: the refused point should be grossly non-stationary unscaled, got {off_err:.3e}"
);
assert!(
matches!(on_status, ApplicationReturnStatus::SolveSucceeded),
"veto run should still end in a strict certificate, got {on_status:?}"
);
assert!(
on_obj < 1e-6,
"veto run should reach the true minimum, got obj {on_obj:.6e}"
);
assert!(
on_obj < off_obj,
"veto run must not be worse than the opt-out run"
);
assert!(on_iter > off_iter, "expected extra iterations to be spent");
}
#[test]
fn threshold_zero_restores_the_upstream_stop() {
let (status, obj, _, _) = solve(&[("obj_scale_certificate_threshold", 0.0)]);
assert!(matches!(status, ApplicationReturnStatus::SolveSucceeded));
assert!(
obj > 1.0,
"opt-out should keep the early stop, got {obj:.6e}"
);
}
#[test]
fn a_veto_that_does_not_pan_out_restores_the_refused_certificate() {
let (off_status, off_obj, _, off_iter) = solve(&[("obj_scale_certificate_threshold", 0.0)]);
assert!(matches!(
off_status,
ApplicationReturnStatus::SolveSucceeded
));
let cap = (off_iter + 2) as i32;
let (status, obj, _, _) = solve_capped(&[], Some(cap));
eprintln!("capped at {cap} iters: {status:?} obj={obj:.6e} (refused point was {off_obj:.6e})");
assert!(
!matches!(status, ApplicationReturnStatus::MaximumIterationsExceeded),
"a stalled veto must fall back to the refused point, not surface a bare failure"
);
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"unexpected status after a stalled veto: {status:?}"
);
assert!(
obj <= off_obj * (1.0 + 1e-9),
"fallback point {obj:.6e} is worse than the refused point {off_obj:.6e}"
);
}