use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::{Index, Number};
use pounce_nlp::solve_statistics::SolveStatistics;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, IterStats, NlpInfo, Solution, SparsityRequest,
StartingPoint, TNLP,
};
use std::cell::RefCell;
use std::rc::Rc;
const N: usize = 4;
#[derive(Default)]
struct Recording {
payloads: Vec<(Vec<Number>, Number)>,
trace: Vec<IterStats>,
}
impl TNLP for Recording {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: N as Index,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: N as Index,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for i in 0..N {
b.x_l[i] = -50.0;
b.x_u[i] = 50.0;
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
for i in 0..N {
sp.x[i] = 20.0 + i as Number;
}
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
let mut f = 0.0;
for i in 0..N {
let s = 10f64.powi(3 * i as i32 - 3);
f += s * (x[i] - 1.0).powi(4) + s * 0.5 * (x[i] - 1.0).powi(2);
}
Some(f)
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
for i in 0..N {
let s = 10f64.powi(3 * i as i32 - 3);
g[i] = s * 4.0 * (x[i] - 1.0).powi(3) + s * (x[i] - 1.0);
}
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 Index;
jcol[i] = i as Index;
}
}
SparsityRequest::Values { values } => {
let x = match x {
Some(x) => x,
None => return false,
};
for i in 0..N {
let s = 10f64.powi(3 * i as i32 - 3);
values[i] = obj_factor * (s * 12.0 * (x[i] - 1.0).powi(2) + s);
}
}
}
true
}
fn intermediate_callback(&mut self, s: IterStats, _d: &IpoptData, _q: &IpoptCq) -> bool {
self.trace.push(s);
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.payloads.push((sol.x.to_vec(), sol.obj_value));
}
}
struct Run {
payloads: Vec<(Vec<Number>, Number)>,
trace: Vec<IterStats>,
stats: SolveStatistics,
}
fn run_full(max_iter: i32, fallback: Option<bool>) -> Run {
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", max_iter, true, false)
.unwrap();
if let Some(v) = fallback {
app.options_mut()
.set_string_value(
"mu_strategy_fallback",
if v { "yes" } else { "no" },
true,
false,
)
.unwrap();
}
app.initialize().unwrap();
let concrete = Rc::new(RefCell::new(Recording::default()));
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::clone(&concrete) as _;
let _ = app.optimize_tnlp(tnlp);
let out = concrete.borrow().payloads.clone();
assert!(!out.is_empty(), "finalize_solution never ran");
Run {
payloads: out,
trace: concrete.borrow().trace.clone(),
stats: app.statistics(),
}
}
fn run(max_iter: i32, fallback: Option<bool>) -> Vec<(Vec<Number>, Number)> {
run_full(max_iter, fallback).payloads
}
#[test]
fn the_retry_really_displaces_the_first_attempts_payload() {
let p = run(4, None);
assert!(
p.len() >= 2,
"expected at least one retry payload; got {} finalize call(s), so the \
mu_strategy_fallback retry did not fire and this file proves nothing",
p.len()
);
let differ = p[0]
.0
.iter()
.zip(&p[1].0)
.any(|(a, b)| (a - b).abs() > 1e-9);
assert!(
differ,
"attempt 1 and the retry produced the same point, so this fixture \
cannot detect a missing floor: {:?} vs {:?}",
p[0].0, p[1].0
);
}
#[test]
fn the_last_finalize_payload_is_the_first_attempts() {
let p = run(4, None);
let (first_x, first_obj) = &p[0];
let (last_x, last_obj) = p.last().unwrap();
assert!(
first_x
.iter()
.zip(last_x)
.all(|(a, b)| (a - b).abs() <= 1e-12),
"the losing retry's point was left in the user's TNLP.\n attempt 1: \
{first_x:?}\n left behind: {last_x:?}\nThe status is floored to \
attempt 1's, so the point must be too (pounce#870)."
);
assert!(
(first_obj - last_obj).abs() <= 1e-12 * first_obj.abs().max(1.0),
"objective left behind ({last_obj:e}) is not attempt 1's ({first_obj:e})"
);
}
#[test]
fn the_floored_answer_matches_a_run_with_no_retry_at_all() {
let floored = run(4, None);
let no_retry = run(4, Some(false));
assert_eq!(
no_retry.len(),
1,
"mu_strategy_fallback=no must not retry at all"
);
let (a, _) = floored.last().unwrap();
let (b, _) = &no_retry[0];
assert!(
a.iter().zip(b).all(|(p, q)| (p - q).abs() <= 1e-12),
"floored answer {a:?} differs from the no-retry answer {b:?}"
);
}
#[test]
fn the_trace_ends_on_the_iterate_the_statistics_describe() {
let r = run_full(4, None);
let last = r.trace.last().expect("the callback fired at least once");
assert!(
r.stats.final_constr_viol > 1e-4 || r.stats.final_dual_inf > 1e-4,
"the solve must be cut short for this to discriminate; got \
inf_pr={:e} inf_du={:e}",
r.stats.final_constr_viol,
r.stats.final_dual_inf
);
assert!(
(last.inf_pr - r.stats.final_constr_viol).abs() < 1e-12
&& (last.inf_du - r.stats.final_dual_inf).abs() < 1e-12,
"the trace ends on a different iterate than the statistics describe.\n \
trace[-1]: inf_pr={:e} inf_du={:e}\n statistics: inf_pr={:e} inf_du={:e}\n\
A losing retry left its own last row at the end of the trace while the \
certificate was floored back to the winning attempt (pounce#870).",
last.inf_pr,
last.inf_du,
r.stats.final_constr_viol,
r.stats.final_dual_inf
);
}