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;
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn unit(&mut self) -> Number {
(self.next_u64() >> 11) as Number / (1u64 << 53) as Number
}
fn pick<T: Copy>(&mut self, xs: &[T]) -> T {
xs[(self.next_u64() % xs.len() as u64) as usize]
}
}
#[derive(Clone)]
struct Spec {
n: usize,
p: i32,
a: Vec<Number>,
c: Vec<Number>,
w: Vec<Number>,
x0: Number,
arows: Vec<Vec<Number>>,
brhs: Vec<Number>,
eq: bool,
}
struct Problem(Spec, Rc<RefCell<Vec<Number>>>);
impl TNLP for Problem {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.0.n as i32,
m: self.0.arows.len() as i32,
nnz_jac_g: (self.0.arows.len() * self.0.n) as i32,
nnz_h_lag: self.0.n as i32,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for v in b.x_l.iter_mut() {
*v = -2.0e19;
}
for v in b.x_u.iter_mut() {
*v = 2.0e19;
}
let s = &self.0;
for (k, rhs) in s.brhs.iter().enumerate() {
b.g_u[k] = *rhs;
b.g_l[k] = if s.eq { *rhs } else { -2.0e19 };
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
for v in sp.x.iter_mut() {
*v = self.0.x0;
}
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
let s = &self.0;
Some(
(0..s.n)
.map(|i| s.c[i] * (x[i] - s.a[i]).powi(s.p) - s.w[i] * x[i] * x[i])
.sum(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
let s = &self.0;
for i in 0..s.n {
g[i] = s.c[i] * s.p as Number * (x[i] - s.a[i]).powi(s.p - 1) - 2.0 * s.w[i] * x[i];
}
true
}
fn eval_g(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
for (k, row) in self.0.arows.iter().enumerate() {
g[k] = row.iter().zip(x).map(|(a, xi)| a * xi).sum();
}
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, mode: SparsityRequest<'_>) -> bool {
let s = &self.0;
match mode {
SparsityRequest::Structure { irow, jcol } => {
let mut t = 0;
for k in 0..s.arows.len() {
for j in 0..s.n {
irow[t] = k as i32;
jcol[t] = j as i32;
t += 1;
}
}
}
SparsityRequest::Values { values } => {
let mut t = 0;
for row in &s.arows {
for a in row {
values[t] = *a;
t += 1;
}
}
}
}
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
let s = &self.0;
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..s.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 in 0..s.n {
let pp = s.p as Number;
values[i] = obj_factor
* (s.c[i] * pp * (pp - 1.0) * (x[i] - s.a[i]).powi(s.p - 2) - 2.0 * s.w[i]);
}
}
}
true
}
fn finalize_solution(&mut self, s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
*self.1.borrow_mut() = s.x.to_vec();
}
}
struct Outcome {
status: ApplicationReturnStatus,
obj: Number,
iters: usize,
}
fn run(spec: &Spec, threshold: Option<Number>, max_cpu: Option<Number>) -> Outcome {
let mut app = IpoptApplication::new();
if let Some(t) = threshold {
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", t, true, false)
.unwrap();
}
if let Some(t) = max_cpu {
app.options_mut()
.set_numeric_value("max_cpu_time", t, true, false)
.unwrap();
}
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec.clone(),
Rc::new(RefCell::new(Vec::new())),
)));
let status = app.optimize_tnlp(tnlp);
let s = app.statistics();
Outcome {
status,
obj: s.final_objective,
iters: s.iteration_count as usize,
}
}
fn run_capped(spec: &Spec, threshold: Option<Number>, max_iter: i32) -> Outcome {
let mut app = IpoptApplication::new();
if let Some(t) = threshold {
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", t, true, false)
.unwrap();
}
app.options_mut()
.set_integer_value("max_iter", max_iter, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec.clone(),
Rc::new(RefCell::new(Vec::new())),
)));
let status = app.optimize_tnlp(tnlp);
let s = app.statistics();
Outcome {
status,
obj: s.final_objective,
iters: s.iteration_count as usize,
}
}
fn succeeded(s: ApplicationReturnStatus) -> bool {
matches!(
s,
ApplicationReturnStatus::SolveSucceeded | ApplicationReturnStatus::SolvedToAcceptableLevel
)
}
fn gen_spec(rng: &mut Rng) -> Spec {
let n = rng.pick(&[2usize, 5, 20, 200]);
let p = rng.pick(&[2i32, 4, 6, 8]);
let amag = rng.pick(&[1.0, 10.0, 1e3, 1e5]);
let cspread = rng.pick(&[1.0, 1e3, 1e6]);
let wmag = rng.pick(&[0.0, 0.0, 1.0, 100.0]);
let x0 = rng.pick(&[0.0, 2.0, -50.0]);
let m = rng.pick(&[0usize, 0, 1, 3]).min(n.saturating_sub(1));
let eq = rng.pick(&[true, false]);
let a: Vec<Number> = (0..n).map(|_| (rng.unit() * 2.0 - 1.0) * amag).collect();
let arows: Vec<Vec<Number>> = (0..m)
.map(|_| (0..n).map(|_| rng.unit() * 2.0 - 1.0).collect())
.collect();
let brhs = arows
.iter()
.map(|row| {
let at_min: Number = row.iter().zip(&a).map(|(r, ai)| r * ai).sum();
at_min + (rng.unit() * 2.0 - 1.0) * amag.sqrt()
})
.collect();
Spec {
n,
p,
a,
c: (0..n).map(|_| 1.0 + rng.unit() * (cspread - 1.0)).collect(),
w: (0..n).map(|_| rng.unit() * wmag).collect(),
x0,
arows,
brhs,
eq,
}
}
#[test]
fn veto_never_degrades_status_or_objective() {
let mut rng = Rng(0x5EED_2000);
let (mut cases, mut improved, mut vetoed_paths) = (0, 0, 0);
for case in 0..240 {
let spec = gen_spec(&mut rng);
let base = run(&spec, Some(0.0), None);
let veto = run(&spec, None, None);
cases += 1;
if succeeded(base.status) {
assert!(
succeeded(veto.status),
"case {case} (n={} p={} x0={}): baseline succeeded but veto gave {:?}",
spec.n,
spec.p,
spec.x0,
veto.status
);
let slack = 1e-9 * base.obj.abs().max(1.0);
assert!(
veto.obj <= base.obj + slack,
"case {case} (n={} p={} amag~{:.0e} w={}): veto objective {:.12e} is WORSE than \
baseline {:.12e}",
spec.n,
spec.p,
spec.a.iter().fold(0.0_f64, |m, v| m.max(v.abs())),
spec.w.iter().fold(0.0_f64, |m, v| m.max(*v)),
veto.obj,
base.obj
);
if veto.obj < base.obj - slack {
improved += 1;
}
}
if veto.iters > base.iters {
vetoed_paths += 1;
}
}
assert!(
vetoed_paths >= 10,
"only {vetoed_paths}/{cases} cases engaged the veto — the fuzz is not exercising it"
);
eprintln!("fuzz: {cases} cases, veto engaged on {vetoed_paths}, improved {improved}");
}
#[test]
fn an_exit_forced_before_the_veto_finishes_still_yields_the_refused_certificate() {
let mut rng = Rng(0xC0DE_2000);
let (mut checked, mut forced) = (0, 0);
for case in 0..80 {
let spec = gen_spec(&mut rng);
let base = run(&spec, Some(0.0), None);
if !succeeded(base.status) || base.iters == 0 {
continue;
}
let veto_free = run(&spec, None, None);
if veto_free.iters <= base.iters {
continue;
}
forced += 1;
let capped = run_capped(&spec, None, base.iters as i32);
checked += 1;
assert!(
!matches!(
capped.status,
ApplicationReturnStatus::MaximumIterationsExceeded
),
"case {case}: a veto cut short at {} iters surfaced MaximumIterationsExceeded \
where the baseline succeeded",
base.iters
);
let slack = 1e-9 * base.obj.abs().max(1.0);
assert!(
capped.obj <= base.obj + slack,
"case {case}: cut-short veto objective {:.12e} is worse than the refused \
certificate {:.12e}",
capped.obj,
base.obj
);
}
assert!(
forced >= 10 && checked >= 10,
"only {checked} cases exercised a forced exit — the fuzz is not reaching this path"
);
eprintln!("forced-exit fuzz: {checked} cases checked");
}
#[test]
fn opt_out_is_inert_and_the_solver_stays_deterministic() {
let mut rng = Rng(0xDEAD_2000);
for case in 0..40 {
let spec = gen_spec(&mut rng);
let a = run(&spec, Some(0.0), None);
let b = run(&spec, Some(0.0), None);
assert_eq!(
format!("{:?}", a.status),
format!("{:?}", b.status),
"case {case}: opt-out is non-deterministic"
);
assert!(
(a.obj - b.obj).abs() <= 0.0 || a.obj.to_bits() == b.obj.to_bits(),
"case {case}: opt-out objective differs between runs: {} vs {}",
a.obj,
b.obj
);
let c = run(&spec, None, None);
let d = run(&spec, None, None);
assert_eq!(
format!("{:?}", c.status),
format!("{:?}", d.status),
"case {case}: veto run is non-deterministic"
);
assert!(
c.obj.to_bits() == d.obj.to_bits(),
"case {case}: veto objective differs between runs: {} vs {}",
c.obj,
d.obj
);
}
}
fn eval_obj(spec: &Spec, x: &[Number]) -> Number {
(0..spec.n)
.map(|i| spec.c[i] * (x[i] - spec.a[i]).powi(spec.p) - spec.w[i] * x[i] * x[i])
.sum()
}
#[test]
fn the_returned_point_matches_the_reported_objective() {
let mut rng = Rng(0xF00D_2000);
let mut checked = 0;
for case in 0..120 {
let spec = gen_spec(&mut rng);
let seen = Rc::new(RefCell::new(Vec::new()));
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> =
Rc::new(RefCell::new(Problem(spec.clone(), Rc::clone(&seen))));
let status = app.optimize_tnlp(tnlp);
let reported = app.statistics().final_objective;
let x = seen.borrow().clone();
if x.len() != spec.n || !reported.is_finite() {
continue;
}
checked += 1;
let direct = eval_obj(&spec, &x);
let scale = reported.abs().max(direct.abs()).max(1.0);
assert!(
(direct - reported).abs() <= 1e-6 * scale,
"case {case} ({status:?}): returned x evaluates to {direct:.12e} but the reported \
objective is {reported:.12e}"
);
}
assert!(
checked >= 60,
"only {checked} cases produced a usable solution vector"
);
eprintln!("solution-consistency fuzz: {checked} cases checked");
}
#[test]
fn veto_state_does_not_leak_across_solves_on_a_reused_application() {
let mut rng = Rng(0xBEEF_2000);
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
for case in 0..40 {
let spec = gen_spec(&mut rng);
let fresh = run(&spec, None, None);
let seen = Rc::new(RefCell::new(Vec::new()));
let tnlp: Rc<RefCell<dyn TNLP>> =
Rc::new(RefCell::new(Problem(spec.clone(), Rc::clone(&seen))));
let status = app.optimize_tnlp(tnlp);
let obj = app.statistics().final_objective;
assert_eq!(
format!("{status:?}"),
format!("{:?}", fresh.status),
"case {case}: reused application gave a different status than a fresh one"
);
let scale = obj.abs().max(fresh.obj.abs()).max(1.0);
assert!(
(obj - fresh.obj).abs() <= 1e-9 * scale,
"case {case}: reused application gave {obj:.12e}, fresh gave {:.12e} — state leaked",
fresh.obj
);
}
}
struct InconsistentPair {
a: Number,
}
impl TNLP for InconsistentPair {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 2,
nnz_jac_g: 4,
nnz_h_lag: 2,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-2.0e19; 2]);
b.x_u.copy_from_slice(&[2.0e19; 2]);
b.g_l.copy_from_slice(&[0.0, 0.0]);
b.g_u.copy_from_slice(&[0.0, 0.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[3.0, 3.0]);
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some((x[0] - self.a).powi(4) + (x[1] - self.a).powi(4))
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
g[0] = 4.0 * (x[0] - self.a).powi(3);
g[1] = 4.0 * (x[1] - self.a).powi(3);
true
}
fn eval_g(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
let r = x[0] * x[0] + x[1] * x[1];
g[0] = r - 1.0;
g[1] = r - 4.0;
true
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, _n: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 0, 1, 1]);
jcol.copy_from_slice(&[0, 1, 0, 1]);
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
values[0] = 2.0 * x[0];
values[1] = 2.0 * x[1];
values[2] = 2.0 * x[0];
values[3] = 2.0 * x[1];
}
}
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.copy_from_slice(&[0, 1]);
jcol.copy_from_slice(&[0, 1]);
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
let lam = lambda.map(|l| l[0] + l[1]).unwrap_or(0.0);
values[0] = obj_factor * 12.0 * (x[0] - self.a).powi(2) + 2.0 * lam;
values[1] = obj_factor * 12.0 * (x[1] - self.a).powi(2) + 2.0 * lam;
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn the_veto_survives_the_restoration_phase() {
let mut entered = 0;
for a in [1e3, 1e4, 1e5] {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(InconsistentPair { a }));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.restoration_calls)
};
let (bs, bo, br) = solve(0.0);
let (vs, vo, vr) = solve(1e-4);
eprintln!(
"a={a:e}: baseline {bs:?} f={bo:.6e} resto={br} | veto {vs:?} f={vo:.6e} resto={vr}"
);
let restoration_involved = |st: ApplicationReturnStatus| {
matches!(
st,
ApplicationReturnStatus::RestorationFailed
| ApplicationReturnStatus::InfeasibleProblemDetected
)
};
if br > 0 || vr > 0 || restoration_involved(bs) || restoration_involved(vs) {
entered += 1;
}
if !succeeded(bs) {
assert!(
!succeeded(vs),
"a={a:e}: baseline correctly reported {bs:?} on an infeasible problem but the \
veto reported {vs:?}"
);
}
assert!(
vo.is_finite(),
"a={a:e}: restoration + veto produced a non-finite objective"
);
}
assert!(
entered >= 3,
"only {entered}/3 configurations reached restoration — the trigger no longer works"
);
eprintln!("restoration: {entered}/3 configurations entered restoration");
}
struct AcceptableOnly {
a: Number,
amp: Number,
k: Number,
}
impl TNLP for AcceptableOnly {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 2,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-2.0e19; 2]);
b.x_u.copy_from_slice(&[2.0e19; 2]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[0.0, 1.0]);
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some(self.amp * (x[0] - self.a).powi(4) - self.k * (1.0 + x[1] * x[1]).sqrt())
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
g[0] = 4.0 * self.amp * (x[0] - self.a).powi(3);
g[1] = -self.k * x[1] / (1.0 + x[1] * x[1]).sqrt();
true
}
fn eval_g(&mut self, _x: &[Number], _n: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, _m: SparsityRequest<'_>) -> bool {
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 1]);
jcol.copy_from_slice(&[0, 1]);
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
let d = (1.0 + x[1] * x[1]).sqrt();
values[0] = obj_factor * 12.0 * self.amp * (x[0] - self.a).powi(2);
values[1] = obj_factor * (-self.k / (d * d * d));
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn a_blocked_acceptable_certificate_is_not_turned_into_a_failure() {
for (a, amp, k) in [
(1e5, 1.0, 10.0),
(1e5, 1.0, 50.0),
(1e3, 1.0, 10.0),
(1e4, 1.0, 3.0),
] {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(AcceptableOnly { a, amp, k }));
let st = app.optimize_tnlp(t);
(
st,
app.statistics().final_objective,
app.statistics().iteration_count,
)
};
let (bs, bo, bi) = solve(0.0);
let (vs, vo, vi) = solve(1e-4);
eprintln!(
"a={a:e} k={k}: baseline {bs:?} f={bo:.6e} it={bi} | veto {vs:?} f={vo:.6e} it={vi}"
);
if !succeeded(bs) {
continue;
}
assert!(
succeeded(vs),
"a={a:e} k={k}: baseline ended {bs:?} but the veto surfaced {vs:?}"
);
}
}
struct ConcaveQuartic {
a: Vec<Number>,
}
impl TNLP for ConcaveQuartic {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.a.len() as i32,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: self.a.len() as i32,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for v in b.x_l.iter_mut() {
*v = -2.0e19;
}
for v in b.x_u.iter_mut() {
*v = 2.0e19;
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
for v in sp.x.iter_mut() {
*v = 2.0;
}
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some(
-x.iter()
.zip(&self.a)
.map(|(xi, ai)| (xi - ai).powi(4))
.sum::<Number>(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
for (i, gi) in g.iter_mut().enumerate() {
*gi = -4.0 * (x[i] - self.a[i]).powi(3);
}
true
}
fn eval_g(&mut self, _x: &[Number], _n: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, _m: SparsityRequest<'_>) -> bool {
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..self.a.len() {
irow[i] = i as i32;
jcol[i] = i as i32;
}
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
for (i, v) in values.iter_mut().enumerate() {
*v = obj_factor * (-12.0) * (x[i] - self.a[i]).powi(2);
}
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn the_veto_is_not_disabled_by_a_negative_objective_scaling_factor() {
let a: Vec<Number> = (0..50).map(|i| 1e3 + i as Number).collect();
let solve_min = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let spec = Spec {
n: 50,
p: 4,
a: a.clone(),
c: vec![1.0; 50],
w: vec![0.0; 50],
x0: 2.0,
arows: Vec::new(),
brhs: Vec::new(),
eq: true,
};
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec,
Rc::new(RefCell::new(Vec::new())),
)));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.final_unscaled_kkt_error)
};
let solve_max = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scaling_factor", -1.0, true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(ConcaveQuartic { a: a.clone() }));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.final_unscaled_kkt_error)
};
let (min_off, min_off_obj, _) = solve_min(0.0);
let (min_on, min_on_obj, _) = solve_min(1e-4);
let (max_off, max_off_obj, max_err) = solve_max(0.0);
let (max_on, max_on_obj, _) = solve_max(1e-4);
eprintln!(
"min f -> 0: off {min_off:?} {min_off_obj:.6e} | on {min_on:?} {min_on_obj:.6e}\n\
max g -> 0: off {max_off:?} {max_off_obj:.6e} | on {max_on:?} {max_on_obj:.6e} \
unscaled_err(off)={max_err:.3e}"
);
assert!(
max_err >= 0.0,
"unscaled KKT error came back NEGATIVE ({max_err:.3e}) under a negative objective \
scaling factor — a max-norm cannot be negative, and the pounce#173 unscaled gate is \
defeated by it, independently of this veto"
);
let min_gain = min_off_obj.abs() - min_on_obj.abs();
let max_gain = max_off_obj.abs() - max_on_obj.abs();
assert!(
min_gain > 0.0,
"premise: the veto should improve the minimization ({min_off_obj:.6e} -> {min_on_obj:.6e})"
);
assert!(
max_gain > 0.5 * min_gain,
"the veto moved the minimization {min_gain:.6e} closer to the optimum but the identical \
maximization only {max_gain:.6e} — the mechanism is sign-dependent"
);
}
#[test]
fn sqp_path_behaviour_on_a_masked_objective_is_pinned() {
let a: Vec<Number> = (0..50).map(|i| 1e3 + i as Number).collect();
let spec = Spec {
n: 50,
p: 4,
a,
c: vec![1.0; 50],
w: vec![0.0; 50],
x0: 2.0,
arows: Vec::new(),
brhs: Vec::new(),
eq: true,
};
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("algorithm", "active-set-sqp", true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec,
Rc::new(RefCell::new(Vec::new())),
)));
let status = app.optimize_tnlp(t);
let obj = app.statistics().final_objective;
eprintln!("SQP on the masked quartic: {status:?} obj={obj:.6e} (true minimum 0)");
if succeeded(status) {
assert!(
obj < 1e-3,
"SQP reported {status:?} at objective {obj:.6e} on a masked problem whose minimum \
is 0 — it shares the gh #200 false-certificate bug and needs its own remedy"
);
}
}
#[test]
fn the_reported_barrier_parameter_belongs_to_the_returned_point() {
for (a, k) in [(1e5, 10.0), (1e3, 10.0)] {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(AcceptableOnly { a, amp: 1.0, k }));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.final_mu)
};
let (bs, bo, bmu) = solve(0.0);
let (vs, vo, vmu) = solve(1e-4);
eprintln!(
"a={a:e} k={k}: baseline {bs:?} f={bo:.6e} mu={bmu:.3e} | veto {vs:?} f={vo:.6e} mu={vmu:.3e}"
);
assert!(
(vo - bo).abs() <= 1e-9 * bo.abs().max(1.0),
"premise: the fallback should return the baseline point"
);
assert!(
(vmu - bmu).abs() <= 1e-6 * bmu.abs().max(1e-300),
"a={a:e}: returned the baseline point (f={vo:.6e}) but reported mu={vmu:.3e} \
where the point's own barrier parameter is {bmu:.3e} — an (x, mu) pair that \
never existed"
);
}
}
struct WellConditioned;
impl TNLP for WellConditioned {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 10,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 10,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[5.0; 10]);
b.x_u.copy_from_slice(&[2.0e19; 10]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[8.0; 10]);
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some(
x.iter()
.enumerate()
.map(|(i, v)| (v - i as Number).powi(2))
.sum(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
for (i, gi) in g.iter_mut().enumerate() {
*gi = 2.0 * (x[i] - i as Number);
}
true
}
fn eval_g(&mut self, _x: &[Number], _n: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, _m: SparsityRequest<'_>) -> bool {
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..10 {
irow[i] = i as i32;
jcol[i] = i as i32;
}
}
SparsityRequest::Values { values } => {
for v in values.iter_mut() {
*v = obj_factor * 2.0;
}
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn user_scaled_well_conditioned_problems_do_not_pay_a_veto_tax() {
for df in [1e-5, 1e-6, 1e-8] {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scaling_factor", df, true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(WellConditioned));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(
st,
s.final_objective,
s.iteration_count,
s.final_unscaled_kkt_error,
)
};
let (bs, bo, bi, berr) = solve(0.0);
let (vs, vo, vi, _) = solve(1e-4);
eprintln!(
"obj_scaling_factor={df:e}: baseline {bs:?} f={bo:.6e} it={bi} unscaled_err={berr:.2e} | veto {vs:?} f={vo:.6e} it={vi}"
);
assert!(
bi > 3 && berr > 1e-6,
"premise: df={df:e} did not reach the veto regime (it={bi}, unscaled_err={berr:.2e}) \
— this test would pass vacuously"
);
assert_eq!(
format!("{bs:?}"),
format!("{vs:?}"),
"df={df:e}: user scaling changed the status"
);
assert!(
vi <= bi + 2,
"df={df:e}: a deliberately user-scaled, well-conditioned solve took {vi} iterations \
with the veto vs {bi} without — a per-solve tax on a legitimate configuration"
);
}
}
#[test]
fn the_veto_does_not_suppress_a_fallback_retry() {
for (flag, a, k) in [
("mu_strategy_fallback", 1e5, 10.0),
("mu_strategy_fallback", 1e3, 10.0),
("l1_fallback_on_restoration_failure", 1e5, 10.0),
("l1_fallback_on_restoration_failure", 1e3, 10.0),
] {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value(flag, "yes", true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(AcceptableOnly { a, amp: 1.0, k }));
let st = app.optimize_tnlp(t);
(st, app.statistics().final_objective)
};
let (bs, bo) = solve(0.0);
let (vs, vo) = solve(1e-4);
eprintln!("{flag} a={a:e}: baseline {bs:?} f={bo:.6e} | veto {vs:?} f={vo:.6e}");
assert!(
!(succeeded(bs) && !succeeded(vs)),
"{flag} a={a:e}: baseline ended {bs:?} but the veto gave {vs:?} — a retry was \
suppressed or a status lost"
);
assert!(
vo <= bo + 1e-9 * bo.abs().max(1.0),
"{flag} a={a:e}: veto objective {vo:.6e} worse than baseline {bo:.6e}"
);
}
}
#[test]
fn the_guarantee_holds_under_limited_memory_hessians() {
let mut rng = Rng(0x11BF_6500);
let (mut cases, mut engaged) = (0, 0);
for case in 0..80 {
let spec = gen_spec(&mut rng);
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("hessian_approximation", "limited-memory", true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec.clone(),
Rc::new(RefCell::new(Vec::new())),
)));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.iteration_count)
};
let (bs, bo, bi) = solve(0.0);
let (vs, vo, vi) = solve(1e-4);
cases += 1;
if vi != bi {
engaged += 1;
}
if !succeeded(bs) {
continue;
}
assert!(
succeeded(vs),
"case {case} (n={} p={}): under L-BFGS the baseline ended {bs:?} but the veto gave {vs:?}",
spec.n,
spec.p
);
assert!(
vo <= bo + 1e-9 * bo.abs().max(1.0),
"case {case} (n={} p={}): under L-BFGS the veto returned {vo:.12e}, worse than \
baseline {bo:.12e}",
spec.n,
spec.p
);
}
assert!(
engaged >= 5,
"only {engaged}/{cases} L-BFGS cases changed trajectory — not exercising the veto"
);
eprintln!("L-BFGS: {cases} cases, veto changed the run on {engaged}");
}
fn assert_invariant_under(
label: &str,
seed: u64,
n_cases: usize,
opts: &[(&str, &str)],
int_opts: &[(&str, i32)],
) {
let mut rng = Rng(seed);
let (mut cases, mut engaged) = (0, 0);
for case in 0..n_cases {
let spec = gen_spec(&mut rng);
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
for (k, v) in opts {
app.options_mut()
.set_string_value(k, v, true, false)
.unwrap();
}
for (k, v) in int_opts {
app.options_mut()
.set_integer_value(k, *v, true, false)
.unwrap();
}
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec.clone(),
Rc::new(RefCell::new(Vec::new())),
)));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.iteration_count)
};
let (bs, bo, bi) = solve(0.0);
let (vs, vo, vi) = solve(1e-4);
cases += 1;
if vi != bi {
engaged += 1;
}
if !succeeded(bs) {
continue;
}
assert!(
succeeded(vs),
"{label} case {case} (n={} p={}): baseline ended {bs:?} but the veto gave {vs:?}",
spec.n,
spec.p
);
assert!(
vo <= bo + 1e-9 * bo.abs().max(1.0),
"{label} case {case} (n={} p={}): veto objective {vo:.12e} worse than baseline {bo:.12e}",
spec.n,
spec.p
);
}
assert!(
engaged >= 3,
"{label}: only {engaged}/{cases} cases changed trajectory — not exercising the veto"
);
eprintln!("{label}: {cases} cases, veto changed the run on {engaged}");
}
#[test]
fn the_guarantee_holds_with_acceptable_termination_disabled() {
assert_invariant_under(
"acceptable_iter=0",
0xACCE_9700,
80,
&[],
&[("acceptable_iter", 0)],
);
}
#[test]
fn the_guarantee_holds_under_the_adaptive_mu_strategy() {
assert_invariant_under(
"mu=adaptive",
0xADAF_9700,
80,
&[("mu_strategy", "adaptive")],
&[],
);
}
#[test]
fn the_guarantee_holds_alongside_the_kkt_fidelity_gate() {
let mut rng = Rng(0xF1DE_9700);
let (mut cases, mut engaged) = (0, 0);
for case in 0..80 {
let spec = gen_spec(&mut rng);
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("kkt_fidelity_tol", 1e-4, true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Problem(
spec.clone(),
Rc::new(RefCell::new(Vec::new())),
)));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(st, s.final_objective, s.iteration_count)
};
let (bs, bo, bi) = solve(0.0);
let (vs, vo, vi) = solve(1e-4);
cases += 1;
if vi != bi {
engaged += 1;
}
if !succeeded(bs) {
continue;
}
assert!(
succeeded(vs),
"kkt_fidelity case {case}: baseline {bs:?} but veto {vs:?} — the gate and the veto \
disagree on the same point"
);
assert!(
vo <= bo + 1e-9 * bo.abs().max(1.0),
"kkt_fidelity case {case}: {vo:.12e} > {bo:.12e}"
);
}
assert!(
engaged >= 3,
"kkt_fidelity: only {engaged}/{cases} engaged the veto"
);
eprintln!("kkt_fidelity_tol: {cases} cases, veto changed the run on {engaged}");
}
struct WarmSeeded {
spec: Spec,
seed: Option<(Vec<Number>, Vec<Number>, Vec<Number>)>,
captured: Rc<RefCell<Option<(Vec<Number>, Vec<Number>, Vec<Number>)>>>,
}
impl TNLP for WarmSeeded {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.spec.n as i32,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: self.spec.n as i32,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for v in b.x_l.iter_mut() {
*v = -1.0e6;
}
for v in b.x_u.iter_mut() {
*v = 1.0e6;
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
match &self.seed {
Some((x, zl, zu)) => {
sp.x.copy_from_slice(x);
if sp.init_z {
sp.z_l.copy_from_slice(zl);
sp.z_u.copy_from_slice(zu);
}
true
}
None => {
for v in sp.x.iter_mut() {
*v = self.spec.x0;
}
true
}
}
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
let s = &self.spec;
Some(
(0..s.n)
.map(|i| s.c[i] * (x[i] - s.a[i]).powi(s.p) - s.w[i] * x[i] * x[i])
.sum(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
let s = &self.spec;
for i in 0..s.n {
g[i] = s.c[i] * s.p as Number * (x[i] - s.a[i]).powi(s.p - 1) - 2.0 * s.w[i] * x[i];
}
true
}
fn eval_g(&mut self, _x: &[Number], _n: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, _m: SparsityRequest<'_>) -> bool {
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
o: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
let s = &self.spec;
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..s.n {
irow[i] = i as i32;
jcol[i] = i as i32;
}
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
for i in 0..s.n {
let pp = s.p as Number;
values[i] = o
* (s.c[i] * pp * (pp - 1.0) * (x[i] - s.a[i]).powi(s.p - 2) - 2.0 * s.w[i]);
}
}
}
true
}
fn finalize_solution(&mut self, s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
*self.captured.borrow_mut() = Some((s.x.to_vec(), s.z_l.to_vec(), s.z_u.to_vec()));
}
}
#[test]
fn a_warm_start_chain_is_unaffected_by_the_veto() {
let mut rng = Rng(0x3A57_9700);
let (mut cases, mut engaged) = (0, 0);
for case in 0..60 {
let spec = gen_spec(&mut rng);
let chain = |threshold: Number| {
let run = |seed: Option<(Vec<Number>, Vec<Number>, Vec<Number>)>| {
let cap = Rc::new(RefCell::new(None));
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
if seed.is_some() {
app.options_mut()
.set_string_value("warm_start_init_point", "yes", true, false)
.unwrap();
}
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(WarmSeeded {
spec: spec.clone(),
seed,
captured: Rc::clone(&cap),
}));
let st = app.optimize_tnlp(t);
let s = app.statistics();
(
st,
s.final_objective,
s.iteration_count,
cap.borrow().clone(),
)
};
let (s1, o1, i1, c1) = run(None);
let (s2, o2, i2, _) = run(c1);
(s1, o1, i1, s2, o2, i2)
};
let (bs1, bo1, bi1, bs2, bo2, _) = chain(0.0);
let (vs1, vo1, vi1, vs2, vo2, _) = chain(1e-4);
cases += 1;
if vi1 != bi1 {
engaged += 1;
}
if succeeded(bs1) {
assert!(succeeded(vs1), "case {case} leg1: {bs1:?} -> {vs1:?}");
assert!(
vo1 <= bo1 + 1e-9 * bo1.abs().max(1.0),
"case {case} leg1 objective"
);
}
if succeeded(bs2) {
assert!(
succeeded(vs2),
"case {case} leg2 (warm-started): baseline chain ended {bs2:?} but the veto \
chain gave {vs2:?} — inconsistent state carried across the restore"
);
assert!(
vo2 <= bo2 + 1e-9 * bo2.abs().max(1.0),
"case {case} leg2: warm-started veto chain reached {vo2:.12e}, worse than the \
baseline chain's {bo2:.12e}"
);
}
}
assert!(
engaged >= 3,
"warm-start: only {engaged}/{cases} engaged the veto"
);
eprintln!("warm-start chain: {cases} cases, veto changed leg 1 on {engaged}");
}
struct ChronologyCase;
const CHRONO_A: [Number; 3] = [-0.6562098709158892, -6.6421858042642175, -2.598669849860882];
const CHRONO_W: [Number; 3] = [9682.539592993013, 5145.503368047705, 6220.696295926191];
const CHRONO_B: Number = 12.825582160408128;
impl TNLP for ChronologyCase {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 3,
m: 3,
nnz_jac_g: 9,
nnz_h_lag: 3,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for v in b.x_l.iter_mut() {
*v = -2.0e19;
}
for v in b.x_u.iter_mut() {
*v = 2.0e19;
}
for v in b.g_l.iter_mut() {
*v = 0.0;
}
for v in b.g_u.iter_mut() {
*v = 0.0;
}
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
for v in sp.x.iter_mut() {
*v = -50.0;
}
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some(
(0..3)
.map(|i| (x[i] - CHRONO_A[i]).powi(6) - CHRONO_W[i] * x[i] * x[i])
.sum::<Number>(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
for i in 0..3 {
g[i] = 6.0 * (x[i] - CHRONO_A[i]).powi(5) - 2.0 * CHRONO_W[i] * x[i];
}
true
}
fn eval_g(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
let v: Number = x.iter().map(|v| v * v).sum::<Number>() - CHRONO_B;
g[0] = v;
g[1] = v;
g[2] = (x[0] - CHRONO_A[0]) * (x[0] - CHRONO_A[0]);
true
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, _n: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..3 {
for j in 0..3 {
irow[i * 3 + j] = i as i32;
jcol[i * 3 + j] = j as i32;
}
}
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
for j in 0..3 {
values[j] = 2.0 * x[j];
values[3 + j] = 2.0 * x[j];
values[6 + j] = 0.0;
}
values[6] = 2.0 * (x[0] - CHRONO_A[0]);
}
}
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for i in 0..3 {
irow[i] = i as i32;
jcol[i] = i as i32;
}
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
let lam = l.expect("no lambda");
for i in 0..3 {
values[i] = obj_factor
* (30.0 * (x[i] - CHRONO_A[i]).powi(4) - 2.0 * CHRONO_W[i])
+ 2.0 * (lam[0] + lam[1]);
}
values[0] += 2.0 * lam[2];
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn the_earliest_refusal_is_the_one_restored_not_the_strictest() {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_numeric_value("kkt_fidelity_tol", 1e-4, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 250, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(ChronologyCase));
let status = app.optimize_tnlp(tnlp);
let s = app.statistics();
(status, s.final_objective, s.iteration_count)
};
let (base_status, base_obj, base_iters) = solve(0.0);
let (veto_status, veto_obj, veto_iters) = solve(1e-4);
assert_ne!(
base_iters, veto_iters,
"premise broken: the veto did not engage, so this test proves nothing \
(base {base_iters} iters, veto {veto_iters})"
);
assert_eq!(
base_status,
ApplicationReturnStatus::SolvedToAcceptableLevel,
"premise broken: the baseline is supposed to stop at the acceptable \
level here; got {base_status:?}"
);
assert_eq!(
veto_status, base_status,
"status regressed: base {base_status:?} -> veto {veto_status:?}"
);
assert!(
veto_obj <= base_obj + 1e-9 * base_obj.abs().max(1.0),
"equal status but the veto returned a worse objective: base {base_obj:.15e} \
-> veto {veto_obj:.15e} (worse by {:.3e}) — the fallback restored the \
later strict refusal instead of the earlier acceptable one",
veto_obj - base_obj
);
}
struct MonotoneToBound;
impl TNLP for MonotoneToBound {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 1,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 1,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[1e-12]);
b.x_u.copy_from_slice(&[10.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[1e-12]);
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
Some(1.0 / x[0])
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
g[0] = -1.0 / (x[0] * x[0]);
true
}
fn eval_g(&mut self, _x: &[Number], _n: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(&mut self, _x: Option<&[Number]>, _n: bool, _m: SparsityRequest<'_>) -> bool {
true
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
_n: bool,
obj_factor: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0]);
jcol.copy_from_slice(&[0]);
}
SparsityRequest::Values { values } => {
let x = x.expect("no x");
values[0] = obj_factor * 2.0 / (x[0] * x[0] * x[0]);
}
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn a_masked_monotone_bound_optimum_is_reached_not_a_false_interior_success() {
let solve = |threshold: Number| {
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("hessian_approximation", "limited-memory", true, false)
.unwrap();
app.options_mut()
.set_numeric_value("obj_scale_certificate_threshold", threshold, true, false)
.unwrap();
app.options_mut()
.set_integer_value("max_iter", 300, true, false)
.unwrap();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(MonotoneToBound));
let status = app.optimize_tnlp(tnlp);
let s = app.statistics();
(status, s.final_objective, s.iteration_count)
};
let (base_status, base_obj, _base_iters) = solve(0.0);
assert!(
succeeded(base_status) && base_obj > 0.2,
"premise broken: the veto-off baseline is supposed to reproduce the gh #327 \
false interior success (f ≈ 0.35); got {base_status:?} f={base_obj:.6e}"
);
let (veto_status, veto_obj, _veto_iters) = solve(1e-4);
assert!(
veto_obj <= 0.1 + 1e-3,
"gh #327: the masked monotone problem must converge to the bound optimum \
f = 0.1, but the veto returned {veto_status:?} f={veto_obj:.6e} — the fallback \
rolled back to the non-stationary interior point instead of keeping the \
would-be certificate at the bound"
);
assert!(
succeeded(veto_status),
"gh #327: reached the bound optimum but reported {veto_status:?}"
);
}