use pounce_common::tolerance::is_significant;
use pounce_common::types::Number;
use pounce_nlp::tnlp::{BoundsInfo, StartingPoint, TNLP};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct FeasibleWitness {
pub x: Vec<Number>,
pub max_violation: Number,
}
pub fn starting_point_refutes_infeasibility(
tnlp: &Rc<RefCell<dyn TNLP>>,
lower_bound_inf: Number,
upper_bound_inf: Number,
tol: Number,
) -> Option<FeasibleWitness> {
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 {
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 mut x = vec![0.0; n];
let mut z_l = vec![0.0; n];
let mut z_u = vec![0.0; n];
let mut lambda = vec![0.0; m];
let have_x0 = tnlp.borrow_mut().get_starting_point(StartingPoint {
init_x: true,
x: &mut x,
init_z: false,
z_l: &mut z_l,
z_u: &mut z_u,
init_lambda: false,
lambda: &mut lambda,
});
if !have_x0 || x.iter().any(|v| !v.is_finite()) {
return None;
}
for j in 0..n {
let lo_present = x_l[j].is_finite() && x_l[j] > lower_bound_inf;
let hi_present = x_u[j].is_finite() && x_u[j] < upper_bound_inf;
if lo_present && hi_present && x_l[j] > x_u[j] {
return None;
}
if lo_present && x[j] < x_l[j] {
x[j] = x_l[j];
}
if hi_present && x[j] > x_u[j] {
x[j] = x_u[j];
}
}
let mut g = vec![0.0; m];
if m > 0 && !tnlp.borrow_mut().eval_g(&x, true, &mut g) {
return None;
}
let mut max_violation: Number = 0.0;
for i in 0..m {
let v = g[i];
if !v.is_finite() {
return None;
}
let finite_mag = |b: Number, is_lower: bool| -> Number {
let absent = if is_lower {
b <= lower_bound_inf
} else {
b >= upper_bound_inf
};
if b.is_finite() && !absent {
b.abs()
} else {
0.0
}
};
let scale = v
.abs()
.max(finite_mag(g_l[i], true))
.max(finite_mag(g_u[i], false));
let lo_viol = if g_l[i].is_finite() && g_l[i] > lower_bound_inf {
g_l[i] - v
} else {
0.0
};
let hi_viol = if g_u[i].is_finite() && g_u[i] < upper_bound_inf {
v - g_u[i]
} else {
0.0
};
let viol = lo_viol.max(hi_viol).max(0.0);
if is_significant(viol, scale, tol) {
return None;
}
max_violation = max_violation.max(viol);
}
Some(FeasibleWitness { x, max_violation })
}
#[cfg(test)]
mod tests {
use super::*;
use pounce_nlp::tnlp::{IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest};
const LO_INF: Number = -1e19;
const UP_INF: Number = 1e19;
struct OneRow {
x0: Vec<Number>,
lo: Vec<Number>,
hi: Vec<Number>,
a: Vec<Number>,
g_l: Number,
g_u: Number,
eval_g_ok: bool,
have_x0: bool,
}
impl OneRow {
fn new(
x0: Vec<Number>,
lo: Vec<Number>,
hi: Vec<Number>,
a: Vec<Number>,
g_l: Number,
g_u: Number,
) -> Self {
Self {
x0,
lo,
hi,
a,
g_l,
g_u,
eval_g_ok: true,
have_x0: true,
}
}
}
impl TNLP for OneRow {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.x0.len() as i32,
m: 1,
nnz_jac_g: self.a.len() as i32,
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[0] = self.g_l;
b.g_u[0] = self.g_u;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
if !self.have_x0 {
return false;
}
sp.x.copy_from_slice(&self.x0);
true
}
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 {
if !self.eval_g_ok {
return false;
}
g[0] = self.a.iter().zip(x).map(|(a, v)| a * v).sum();
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 refute(t: OneRow) -> Option<FeasibleWitness> {
let rc: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(t));
starting_point_refutes_infeasibility(&rc, LO_INF, UP_INF, 1e-8)
}
#[test]
fn extreme_scale_starting_point_refutes() {
let w = refute(OneRow::new(
vec![5e5, 5e5],
vec![0.0, 0.0],
vec![1e6, 1e6],
vec![-1e30, 1e30],
-1e-6,
UP_INF,
))
.expect("x0 satisfies the row exactly — the verdict must be withdrawn");
assert_eq!(w.x, vec![5e5, 5e5]);
assert_eq!(w.max_violation, 0.0);
}
#[test]
fn genuinely_infeasible_model_is_not_refuted() {
assert!(
refute(OneRow::new(
vec![0.3],
vec![0.0],
vec![0.6],
vec![1.0],
0.7,
UP_INF
))
.is_none()
);
}
#[test]
fn a_down_scaled_infeasible_row_is_never_refuted() {
for k in -12..=12 {
let s = 10f64.powi(k);
assert!(
refute(OneRow::new(
vec![0.5],
vec![0.0],
vec![1.0],
vec![s],
2.0 * s,
UP_INF
))
.is_none(),
"`x >= 2` over `x ∈ [0, 1]` is empty at row scaling 10^{k} too"
);
}
}
#[test]
fn a_scaled_feasible_row_is_refuted_at_every_scale() {
for k in -12..=12 {
let s = 10f64.powi(k);
assert!(
refute(OneRow::new(
vec![0.5],
vec![0.0],
vec![1.0],
vec![s],
0.25 * s,
UP_INF
))
.is_some(),
"`x >= 0.25` at `x = 0.5` holds at row scaling 10^{k} too"
);
}
}
#[test]
fn starting_point_is_clamped_into_the_box() {
let w = refute(OneRow::new(
vec![5.0],
vec![0.0],
vec![1.0],
vec![1.0],
0.0,
2.0,
))
.expect("clamped to x = 1, which satisfies 0 <= x <= 2");
assert_eq!(w.x, vec![1.0]);
}
#[test]
fn clamping_does_not_manufacture_a_witness() {
assert!(
refute(OneRow::new(
vec![5.0],
vec![0.0],
vec![1.0],
vec![1.0],
3.0,
UP_INF
))
.is_none(),
"clamped to x = 1, which violates x >= 3"
);
}
#[test]
fn unevaluable_model_declines_to_refute() {
let mut t = OneRow::new(vec![0.5], vec![0.0], vec![1.0], vec![1.0], 0.0, 2.0);
t.eval_g_ok = false;
assert!(refute(t).is_none());
let mut t = OneRow::new(vec![0.5], vec![0.0], vec![1.0], vec![1.0], 0.0, 2.0);
t.have_x0 = false;
assert!(refute(t).is_none());
let t = OneRow::new(vec![Number::NAN], vec![0.0], vec![1.0], vec![1.0], 0.0, 2.0);
assert!(refute(t).is_none());
}
#[test]
fn infinite_bound_sentinel_does_not_inflate_the_scale() {
assert!(
refute(OneRow::new(
vec![1.0],
vec![0.0],
vec![2.0],
vec![1.0],
LO_INF,
0.5
))
.is_none()
);
}
#[test]
fn crossed_box_declines_to_refute() {
assert!(
refute(OneRow::new(
vec![0.5],
vec![1.0],
vec![0.0],
vec![1.0],
LO_INF,
UP_INF
))
.is_none()
);
}
#[test]
fn an_upper_bound_past_the_lower_sentinel_is_not_a_crossed_box() {
let w = refute(OneRow::new(
vec![0.0],
vec![LO_INF],
vec![-5.0e20],
vec![1.0],
LO_INF,
-1.0e20,
))
.expect("x0 clamps to -5e20, which satisfies x <= -1e20");
assert_eq!(w.x, vec![-5.0e20]);
}
}