use pounce_common::types::Number;
use pounce_nlp::tnlp::{BoundsInfo, TNLP};
use std::cell::RefCell;
use std::rc::Rc;
pub const SADDLE_ESCAPE_RUNG_LABEL: &str = "restart_from_infeasibility_saddle";
const STEPS: [Number; 3] = [1e-2, 1e-3, 1e-4];
const DIRECTIONS: usize = 8;
const EXTRAPOLATIONS: usize = 10;
const MARGIN: Number = 1e-9;
const SEED: u64 = 0x5AD0_1E5C_A9E0_0001;
#[derive(Debug, Clone)]
pub struct SaddleEscape {
pub x: Vec<Number>,
pub theta_at_verdict: Number,
pub theta_escaped: Number,
pub step: Number,
pub multiple: Number,
}
pub fn infeasibility_saddle_escape(
tnlp: &Rc<RefCell<dyn TNLP>>,
x_verdict: &[Number],
lower_bound_inf: Number,
upper_bound_inf: Number,
) -> Option<SaddleEscape> {
let info = tnlp.borrow_mut().get_nlp_info()?;
let n = info.n.max(0) as usize;
let m = info.m.max(0) as usize;
if n == 0 || m == 0 || x_verdict.len() != n {
return None;
}
let mut x_l = vec![0.0; n];
let mut x_u = vec![0.0; n];
let mut g_l = vec![0.0; m];
let mut g_u = vec![0.0; m];
if !tnlp.borrow_mut().get_bounds_info(BoundsInfo {
x_l: &mut x_l,
x_u: &mut x_u,
g_l: &mut g_l,
g_u: &mut g_u,
}) {
return None;
}
let lo_present = |b: Number| b.is_finite() && b > lower_bound_inf;
let hi_present = |b: Number| b.is_finite() && b < upper_bound_inf;
for j in 0..n {
if lo_present(x_l[j]) && hi_present(x_u[j]) && x_l[j] > x_u[j] {
return None;
}
}
let clip = |j: usize, v: Number| -> Number {
let mut v = v;
if lo_present(x_l[j]) && v < x_l[j] {
v = x_l[j];
}
if hi_present(x_u[j]) && v > x_u[j] {
v = x_u[j];
}
v
};
let mut g = vec![0.0; m];
let mut theta = |x: &[Number]| -> Option<Number> {
if !tnlp.borrow_mut().eval_g(x, true, &mut g) {
return None;
}
let mut worst: Number = 0.0;
for i in 0..m {
let v = g[i];
if !v.is_finite() {
return None;
}
let below = if lo_present(g_l[i]) { g_l[i] - v } else { 0.0 };
let above = if hi_present(g_u[i]) { v - g_u[i] } else { 0.0 };
worst = worst.max(below).max(above);
}
Some(worst)
};
if x_verdict.iter().any(|v| !v.is_finite()) {
return None;
}
let base: Vec<Number> = (0..n).map(|j| clip(j, x_verdict[j])).collect();
let theta0 = theta(&base)?;
if !(theta0 > 0.0) {
return None;
}
let threshold = theta0 * (1.0 - MARGIN);
let mut state = SEED;
let mut u = vec![0.0; n];
let mut trial = vec![0.0; n];
for &step in &STEPS {
let mut best: Option<(Number, Vec<Number>)> = None;
for _ in 0..DIRECTIONS {
for (j, uj) in u.iter_mut().enumerate() {
let draw = unit_symmetric(&mut state);
let h = step * base[j].abs().max(1.0);
let near_lo = lo_present(x_l[j]) && base[j] - x_l[j] < h;
let near_hi = hi_present(x_u[j]) && x_u[j] - base[j] < h;
*uj = if near_lo || near_hi { 0.0 } else { draw };
}
let mut pair: [Option<(Number, Vec<Number>)>; 2] = [None, None];
for (k, sign) in [1.0, -1.0].into_iter().enumerate() {
for j in 0..n {
let h = step * base[j].abs().max(1.0);
trial[j] = clip(j, base[j] + sign * h * u[j]);
}
match theta(&trial) {
Some(t) if t < threshold => pair[k] = Some((t, trial.clone())),
_ => break,
}
}
let [Some(plus), Some(minus)] = pair else {
continue;
};
let lower = if plus.0 <= minus.0 { plus } else { minus };
if best.as_ref().is_none_or(|(b, _)| lower.0 < *b) {
best = Some(lower);
}
}
if let Some((mut t, mut x)) = best {
let dir: Vec<Number> = (0..n).map(|j| x[j] - base[j]).collect();
let mut multiple = 1.0;
for _ in 0..EXTRAPOLATIONS {
let next = 2.0 * multiple;
for j in 0..n {
trial[j] = clip(j, base[j] + next * dir[j]);
}
match theta(&trial) {
Some(tt) if tt < t => {
t = tt;
x.copy_from_slice(&trial);
multiple = next;
}
_ => break,
}
}
return Some(SaddleEscape {
x,
theta_at_verdict: theta0,
theta_escaped: t,
step,
multiple,
});
}
}
None
}
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn unit_symmetric(state: &mut u64) -> Number {
let bits = splitmix64(state) >> 11;
(bits as Number) / ((1u64 << 53) as Number) * 2.0 - 1.0
}
#[cfg(test)]
mod tests {
use super::*;
use pounce_nlp::tnlp::{
IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
};
const LO_INF: Number = -1e19;
const UP_INF: Number = 1e19;
struct Rows {
lo: Vec<Number>,
hi: Vec<Number>,
g_l: Vec<Number>,
g_u: Vec<Number>,
g: fn(&[Number], &mut [Number]),
scale: Number,
}
impl Rows {
fn scaled(mut self, s: Number) -> Self {
self.scale = s;
self
}
}
impl TNLP for Rows {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.lo.len() as i32,
m: self.g_l.len() as i32,
nnz_jac_g: 0,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&self.lo);
b.x_u.copy_from_slice(&self.hi);
b.g_l.copy_from_slice(&self.g_l);
b.g_u.copy_from_slice(&self.g_u);
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
false
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
grad_f.fill(0.0);
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
(self.g)(x, g);
for v in g.iter_mut() {
*v *= self.scale;
}
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_mode: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _c: &IpoptCq) {}
}
fn probe(t: Rows, x: &[Number]) -> Option<SaddleEscape> {
let rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(t));
infeasibility_saddle_escape(&rc, x, LO_INF, UP_INF)
}
#[test]
fn coincident_tangent_discs_are_a_saddle_of_the_violation() {
let rows = Rows {
lo: vec![LO_INF, LO_INF],
hi: vec![UP_INF, UP_INF],
g_l: vec![4.0],
g_u: vec![4.0],
scale: 1.0,
g: |x, g| g[0] = (x[0] - x[1]).powi(2),
};
let e = probe(rows, &[0.0, 0.0]).expect("separating the discs lowers the violation");
assert_eq!(e.theta_at_verdict, 4.0);
assert!(e.theta_escaped < 4.0);
assert_eq!(e.step, STEPS[0]);
}
#[test]
fn a_satisfied_row_broken_at_first_order_does_not_hide_the_saddle() {
let rows = Rows {
lo: vec![LO_INF; 3],
hi: vec![UP_INF; 3],
g_l: vec![4.0, 4.0],
g_u: vec![4.0, 4.0],
scale: 1.0,
g: |x, g| {
g[0] = (x[0] - x[1]).powi(2);
g[1] = (x[0] - x[2]).powi(2);
},
};
assert!(probe(rows, &[0.0, 0.0, 2.0]).is_some());
}
#[test]
fn a_strict_minimizer_of_the_violation_produces_no_point() {
let rows = Rows {
lo: vec![LO_INF],
hi: vec![UP_INF],
g_l: vec![0.0],
g_u: vec![0.0],
scale: 1.0,
g: |x, g| g[0] = x[0] * x[0] + 1.0,
};
assert!(probe(rows, &[0.0]).is_none());
}
#[test]
fn descent_that_only_exists_outside_the_box_is_not_counted() {
let rows = Rows {
lo: vec![0.0],
hi: vec![0.6],
g_l: vec![0.7],
g_u: vec![UP_INF],
scale: 1.0,
g: |x, g| g[0] = x[0],
};
assert!(probe(rows, &[0.6]).is_none());
}
#[test]
fn variables_on_a_bound_stay_put_so_the_saddle_still_shows() {
let rows = Rows {
lo: vec![LO_INF, LO_INF, 1.0, 1.0],
hi: vec![UP_INF; 4],
g_l: vec![0.0],
g_u: vec![0.0],
scale: 1.0,
g: |x, g| g[0] = (x[0] - x[1]).powi(2) - (x[2] + x[3]).powi(2),
};
let e = probe(rows, &[0.0, 0.0, 1.0, 1.0]).expect("the centres can separate");
assert_eq!(&e.x[2..], &[1.0, 1.0], "the radii stay on their bound");
}
#[test]
fn a_first_order_decrease_on_one_side_only_is_not_a_saddle() {
let rows = Rows {
lo: vec![LO_INF],
hi: vec![UP_INF],
g_l: vec![0.0, 2.0],
g_u: vec![0.0, 2.0],
scale: 1.0,
g: |x, g| {
g[0] = x[0];
g[1] = 2.0 * x[0];
},
};
assert!(probe(rows, &[1.0]).is_none());
}
#[test]
fn a_feasible_point_produces_no_point() {
let rows = Rows {
lo: vec![LO_INF, LO_INF],
hi: vec![UP_INF, UP_INF],
g_l: vec![4.0],
g_u: vec![4.0],
scale: 1.0,
g: |x, g| g[0] = (x[0] - x[1]).powi(2),
};
assert!(probe(rows, &[0.0, 2.0]).is_none());
}
#[test]
fn the_probe_is_deterministic() {
let mk = || Rows {
lo: vec![LO_INF, LO_INF],
hi: vec![UP_INF, UP_INF],
g_l: vec![4.0],
g_u: vec![4.0],
scale: 1.0,
g: |x, g| g[0] = (x[0] - x[1]).powi(2),
};
let a = probe(mk(), &[0.0, 0.0]).unwrap();
let b = probe(mk(), &[0.0, 0.0]).unwrap();
assert_eq!(a.x, b.x);
}
#[test]
fn the_verdict_does_not_depend_on_row_scaling() {
for k in -8..=8 {
let s = 10f64.powi(k);
let saddle = Rows {
lo: vec![LO_INF, LO_INF],
hi: vec![UP_INF, UP_INF],
g_l: vec![4.0 * s],
g_u: vec![4.0 * s],
scale: 1.0,
g: |x, g| g[0] = (x[0] - x[1]).powi(2),
}
.scaled(s);
assert!(
probe(saddle, &[0.0, 0.0]).is_some(),
"saddle at scale 10^{k}"
);
let minimizer = Rows {
lo: vec![LO_INF],
hi: vec![UP_INF],
g_l: vec![0.0],
g_u: vec![0.0],
scale: 1.0,
g: |x, g| g[0] = x[0] * x[0] + 1.0,
}
.scaled(s);
assert!(
probe(minimizer, &[0.0]).is_none(),
"minimizer at scale 10^{k}"
);
}
}
}