use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::Number;
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::solve_statistics::SolveStatistics;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
TNLP,
};
use std::cell::RefCell;
use std::rc::Rc;
const SCALE: Number = 1e-3;
const F_STAR: Number = -3e-3;
struct BoundActive {
scale: Number,
ub: Number,
panic_in_retry: bool,
finalized: usize,
x: Vec<Number>,
obj: Number,
}
impl Default for BoundActive {
fn default() -> Self {
Self {
scale: SCALE,
ub: 1.0,
panic_in_retry: false,
finalized: 0,
x: Vec::new(),
obj: Number::NAN,
}
}
}
impl TNLP for BoundActive {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 4,
m: 1,
nnz_jac_g: 4,
nnz_h_lag: 1,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-10.0, 0.0, 0.0, 0.0]);
b.x_u.copy_from_slice(&[10.0, self.ub, self.ub, self.ub]);
b.g_l[0] = 1.5;
b.g_u[0] = 2e19;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
let h = 0.5 * self.ub;
sp.x.copy_from_slice(&[0.0, h, h, h]);
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
if self.panic_in_retry && self.finalized >= 1 {
panic!("injected: a panic inside the scale audit's re-solve");
}
Some(self.scale * ((x[0] - 1.0).powi(2) - x[1] - x[2] - x[3]))
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
let s = self.scale;
g.copy_from_slice(&[s * 2.0 * (x[0] - 1.0), -s, -s, -s]);
true
}
fn eval_g(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
g[0] = x[0].powi(3) + x[1] + x[2] + x[3];
true
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, _n: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for k in 0..4 {
irow[k] = 0;
jcol[k] = k as i32;
}
}
SparsityRequest::Values { values: v } => {
let x = x.unwrap();
v.copy_from_slice(&[3.0 * x[0] * x[0], 1.0, 1.0, 1.0]);
}
}
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
lambda: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow[0] = 0;
jcol[0] = 0;
}
SparsityRequest::Values { values: v } => {
let x = x.unwrap();
let l = lambda.unwrap();
v[0] = obj_factor * self.scale * 2.0 + l[0] * 6.0 * x[0];
}
}
true
}
fn finalize_solution(&mut self, s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.finalized += 1;
self.x = s.x.to_vec();
self.obj = s.obj_value;
}
}
fn new_app(opts: &[(&str, Number)]) -> IpoptApplication {
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
for &(k, v) in opts {
app.options_mut()
.set_numeric_value(k, v, true, false)
.unwrap();
}
app.initialize().unwrap();
app
}
fn solve_on(
app: &mut IpoptApplication,
model: BoundActive,
) -> (ApplicationReturnStatus, SolveStatistics, BoundActive) {
let tnlp = Rc::new(RefCell::new(model));
let status = app.optimize_tnlp(tnlp.clone());
let st = app.statistics();
let model = std::mem::take(&mut *tnlp.borrow_mut());
(status, st, model)
}
fn solve(opts: &[(&str, Number)]) -> (ApplicationReturnStatus, SolveStatistics, BoundActive) {
solve_on(&mut new_app(opts), BoundActive::default())
}
fn assert_inside(st: &SolveStatistics, m: &BoundActive) {
assert_eq!(m.x.len(), 4);
assert!((-10.0..=10.0).contains(&m.x[0]), "y = {}", m.x[0]);
for (i, &xi) in m.x[1..].iter().enumerate() {
assert!(
(0.0..=m.ub).contains(&xi),
"finalize_solution received x{} = {xi:.17e}, outside [0, {}]",
i + 1,
m.ub
);
}
let f_star = m.scale * (-3.0 * m.ub);
assert!(
m.obj >= f_star,
"objective {:.12e} below f* {f_star:e}",
m.obj
);
assert_eq!(st.final_declared_box_viol, 0.0);
assert_eq!(st.final_objective, m.obj);
}
#[test]
fn the_promoted_re_solve_hands_finalize_solution_a_point_inside_the_box() {
let (status, st, m) = solve(&[]);
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
assert!(
m.obj - F_STAR < 1e-10,
"expected the promoted re-solve's objective, got {:.12e}",
m.obj
);
assert!(st.warnings.is_empty(), "{:?}", st.warnings);
assert_inside(&st, &m);
}
#[test]
fn a_named_widening_keeps_the_base_attempt_and_its_box_measurement() {
let (status, st, m) = solve(&[("bound_relax_factor", 1e-8)]);
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
assert!(
st.warnings
.iter()
.any(|w| w.contains("outside the declared variable bounds")),
"{:?}",
st.warnings
);
assert_inside(&st, &m);
}
#[test]
fn large_bounds_promote_inside_the_box_and_decline_only_on_genuine_excess() {
for ub in [1e4, 1e6] {
let model = || BoundActive {
ub,
..BoundActive::default()
};
let (status, st, m) = solve_on(&mut new_app(&[]), model());
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded, "ub {ub}");
assert!(st.warnings.is_empty(), "ub {ub}: {:?}", st.warnings);
assert_inside(&st, &m);
let (_, st, m) = solve_on(&mut new_app(&[("bound_relax_factor", 1e-8)]), model());
assert!(
st.warnings
.iter()
.any(|w| w.contains("outside the declared variable bounds")),
"ub {ub}: {:?}",
st.warnings
);
assert_eq!(st.final_objective, m.obj, "ub {ub}");
}
}
fn assert_next_solve_is_clean(app: &mut IpoptApplication, after: &str) {
let o = app.options();
assert_eq!(
o.get_integer_value("max_iter", "").unwrap(),
(500, true),
"{after}: max_iter"
);
assert_eq!(
o.get_numeric_value("max_cpu_time", "").unwrap(),
(1000.0, true),
"{after}: max_cpu_time"
);
assert!(
!o.get_numeric_value("obj_scaling_factor", "").unwrap().1,
"{after}: obj_scaling_factor left caller-set"
);
assert!(
!o.get_numeric_value("max_wall_time", "").unwrap().1,
"{after}: max_wall_time left caller-set"
);
let probe = BoundActive {
scale: 1.0,
..BoundActive::default()
};
let (status, st, _) = solve_on(app, probe);
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded, "{after}");
assert!(
st.final_declared_box_viol > 1e-9,
"{after}: the next solve ran with the variable box un-widened \
(declared-box violation {:e})",
st.final_declared_box_viol
);
}
#[test]
fn the_next_solve_after_a_promoted_or_declined_re_solve_is_unaffected() {
for (after, relax) in [("promotion", None), ("decline", Some(1e-8))] {
let mut opts = vec![("max_cpu_time", 1000.0)];
if let Some(r) = relax {
opts.push(("bound_relax_factor", r));
}
let mut app = new_app(&opts);
app.options_mut()
.set_integer_value("max_iter", 500, true, false)
.unwrap();
let (status, st, _) = solve_on(&mut app, BoundActive::default());
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded, "{after}");
assert_eq!(
st.warnings.is_empty(),
relax.is_none(),
"{after}: {:?}",
st.warnings
);
assert_next_solve_is_clean(&mut app, after);
}
}
#[test]
fn a_panic_inside_the_re_solve_does_not_leak_its_configuration() {
let mut app = new_app(&[("max_cpu_time", 1000.0)]);
app.options_mut()
.set_integer_value("max_iter", 500, true, false)
.unwrap();
let model = BoundActive {
panic_in_retry: true,
..BoundActive::default()
};
let tnlp = Rc::new(RefCell::new(model));
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
app.optimize_tnlp(tnlp.clone())
}));
assert!(caught.is_err(), "the injected panic did not fire");
assert_eq!(
tnlp.borrow().finalized,
1,
"the panic must land inside the re-solve, after the base attempt"
);
assert_next_solve_is_clean(&mut app, "panic");
}