use std::cell::RefCell;
use std::rc::Rc;
use pounce_common::types::{Index, Number};
use crate::tnlp::{
BoundsInfo, IterStats, Linearity, MetaData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
StartingPoint, TNLP,
};
use crate::{IpoptCq, IpoptData};
pub const DEFAULT_BOUND_INF: Number = 1e19;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AdamConfig {
pub iters: usize,
pub lr: Number,
pub rho: Number,
pub beta1: Number,
pub beta2: Number,
pub eps: Number,
}
impl Default for AdamConfig {
fn default() -> Self {
Self {
iters: 200,
lr: 5e-2,
rho: 10.0,
beta1: 0.9,
beta2: 0.999,
eps: 1e-8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StartConditioner {
Jitter { seed: u64, scale: Number },
Adam(AdamConfig),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConditionerReport {
pub sanitised: Vec<usize>,
pub max_shift: Number,
pub iters: usize,
pub merit_initial: Number,
pub merit_final: Number,
pub stopped_early: bool,
}
pub struct ConditionedStartTnlp {
inner: Rc<RefCell<dyn TNLP>>,
conditioner: StartConditioner,
lower_inf: Number,
upper_inf: Number,
report: RefCell<Option<ConditionerReport>>,
cache: RefCell<Option<(Vec<Number>, Vec<Number>, ConditionerReport)>>,
}
impl ConditionedStartTnlp {
pub fn new(inner: Rc<RefCell<dyn TNLP>>, conditioner: StartConditioner) -> Self {
Self {
inner,
conditioner,
lower_inf: -DEFAULT_BOUND_INF,
upper_inf: DEFAULT_BOUND_INF,
report: RefCell::new(None),
cache: RefCell::new(None),
}
}
pub fn with_bound_inf(mut self, lower: Number, upper: Number) -> Self {
self.lower_inf = lower;
self.upper_inf = upper;
self
}
pub fn report(&self) -> Option<ConditionerReport> {
self.report.borrow().clone()
}
fn bounds(
&self,
n: usize,
m: usize,
) -> Option<(Vec<Number>, Vec<Number>, Vec<Number>, Vec<Number>)> {
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];
let ok = self.inner.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,
});
ok.then_some((x_l, x_u, g_l, g_u))
}
fn clip(&self, v: Number, lo: Number, hi: Number) -> Number {
let mut v = v;
if lo > self.lower_inf && v < lo {
v = lo;
}
if hi < self.upper_inf && v > hi {
v = hi;
}
v
}
fn sanitised_value(&self, lo: Number, hi: Number) -> Number {
let lo_present = lo > self.lower_inf;
let hi_present = hi < self.upper_inf;
match (lo_present, hi_present) {
(true, true) => 0.5 * (lo + hi),
(true, false) => lo + 1.0,
(false, true) => hi - 1.0,
(false, false) => 0.0,
}
}
}
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
}
pub fn violation(g: &[Number], g_l: &[Number], g_u: &[Number], out: &mut [Number]) {
for i in 0..g.len() {
let lo = g_l.get(i).copied().unwrap_or(Number::NEG_INFINITY);
let hi = g_u.get(i).copied().unwrap_or(Number::INFINITY);
out[i] = if g[i] < lo {
g[i] - lo
} else if g[i] > hi {
g[i] - hi
} else {
0.0
};
}
}
impl ConditionedStartTnlp {
fn apply_jitter(
&self,
x: &mut [Number],
x_l: &[Number],
x_u: &[Number],
seed: u64,
scale: Number,
) -> ConditionerReport {
let mut report = ConditionerReport::default();
let mut state = seed;
for i in 0..x.len() {
let lo = x_l.get(i).copied().unwrap_or(Number::NEG_INFINITY);
let hi = x_u.get(i).copied().unwrap_or(Number::INFINITY);
let base = if x[i].is_finite() {
x[i]
} else {
report.sanitised.push(i);
self.sanitised_value(lo, hi)
};
let step = scale * (1.0 + base.abs()) * unit_symmetric(&mut state);
let moved = self.clip(base + step, lo, hi);
report.max_shift = report.max_shift.max((moved - base).abs());
x[i] = moved;
}
report
}
fn apply_adam(
&self,
x: &mut [Number],
info: &NlpInfo,
x_l: &[Number],
x_u: &[Number],
g_l: &[Number],
g_u: &[Number],
cfg: &AdamConfig,
) -> ConditionerReport {
let n = x.len();
let m = info.m.max(0) as usize;
let nnz = info.nnz_jac_g.max(0) as usize;
let mut report = ConditionerReport::default();
for i in 0..n {
if !x[i].is_finite() {
report.sanitised.push(i);
let lo = x_l.get(i).copied().unwrap_or(Number::NEG_INFINITY);
let hi = x_u.get(i).copied().unwrap_or(Number::INFINITY);
x[i] = self.sanitised_value(lo, hi);
}
}
let start = x.to_vec();
let offset = match info.index_style {
crate::tnlp::IndexStyle::C => 0usize,
crate::tnlp::IndexStyle::Fortran => 1usize,
};
let (mut jrow, mut jcol) = (vec![0 as Index; nnz], vec![0 as Index; nnz]);
if m > 0
&& !self.inner.borrow_mut().eval_jac_g(
None,
true,
SparsityRequest::Structure {
irow: &mut jrow,
jcol: &mut jcol,
},
)
{
report.stopped_early = true;
return report;
}
let mut grad = vec![0.0; n];
let mut gval = vec![0.0; m];
let mut resid = vec![0.0; m];
let mut jval = vec![0.0; nnz];
let mut mom = vec![0.0; n];
let mut vel = vec![0.0; n];
let merit = |slf: &Self, x: &[Number]| -> Number {
let f = slf
.inner
.borrow_mut()
.eval_f(x, true)
.unwrap_or(Number::NAN);
if m == 0 {
return f;
}
let mut gv = vec![0.0; m];
if !slf.inner.borrow_mut().eval_g(x, false, &mut gv) {
return Number::NAN;
}
let mut r = vec![0.0; m];
violation(&gv, g_l, g_u, &mut r);
f + cfg.rho * r.iter().map(|v| v * v).sum::<Number>()
};
report.merit_initial = merit(self, x);
let mut ran = 0usize;
for t in 1..=cfg.iters {
if !self.inner.borrow_mut().eval_grad_f(x, true, &mut grad) {
report.stopped_early = true;
break;
}
if m > 0 {
if !self.inner.borrow_mut().eval_g(x, false, &mut gval)
|| !self.inner.borrow_mut().eval_jac_g(
Some(x),
false,
SparsityRequest::Values { values: &mut jval },
)
{
report.stopped_early = true;
break;
}
violation(&gval, g_l, g_u, &mut resid);
for k in 0..nnz {
let r = jrow[k].max(0) as usize;
let c = jcol[k].max(0) as usize;
let (Some(r), Some(c)) = (r.checked_sub(offset), c.checked_sub(offset)) else {
continue;
};
if r < m && c < n {
grad[c] += 2.0 * cfg.rho * jval[k] * resid[r];
}
}
}
if !grad.iter().all(|v| v.is_finite()) {
report.stopped_early = true;
break;
}
let bc1 = 1.0 - cfg.beta1.powi(t as i32);
let bc2 = 1.0 - cfg.beta2.powi(t as i32);
let mut moved_non_finite = false;
for i in 0..n {
mom[i] = cfg.beta1 * mom[i] + (1.0 - cfg.beta1) * grad[i];
vel[i] = cfg.beta2 * vel[i] + (1.0 - cfg.beta2) * grad[i] * grad[i];
let step = cfg.lr * (mom[i] / bc1) / ((vel[i] / bc2).sqrt() + cfg.eps);
let lo = x_l.get(i).copied().unwrap_or(Number::NEG_INFINITY);
let hi = x_u.get(i).copied().unwrap_or(Number::INFINITY);
let next = self.clip(x[i] - step, lo, hi);
if !next.is_finite() {
moved_non_finite = true;
break;
}
x[i] = next;
}
if moved_non_finite {
report.stopped_early = true;
break;
}
ran = t;
}
report.iters = ran;
report.merit_final = merit(self, x);
if !(report.merit_final < report.merit_initial) && report.merit_initial.is_finite() {
x.copy_from_slice(&start);
report.merit_final = report.merit_initial;
report.iters = 0;
}
report.max_shift = x
.iter()
.zip(start.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, Number::max);
report
}
}
impl TNLP for ConditionedStartTnlp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
self.inner.borrow_mut().get_nlp_info()
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
self.inner.borrow_mut().get_bounds_info(b)
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
let Some(info) = self.inner.borrow_mut().get_nlp_info() else {
return false;
};
let n = info.n.max(0) as usize;
let m = info.m.max(0) as usize;
let init_x = sp.init_x;
let StartingPoint {
x,
z_l,
z_u,
lambda,
init_z,
init_lambda,
..
} = sp;
if !self.inner.borrow_mut().get_starting_point(StartingPoint {
init_x,
x,
init_z,
z_l,
z_u,
init_lambda,
lambda,
}) {
return false;
}
if !init_x || n == 0 {
return true;
}
let Some((x_l, x_u, g_l, g_u)) = self.bounds(n, m) else {
return true;
};
if let Some((raw, conditioned, report)) = self.cache.borrow().as_ref()
&& raw.len() == n
&& raw
.iter()
.zip(x[..n].iter())
.all(|(a, b)| a.to_bits() == b.to_bits())
{
x[..n].copy_from_slice(conditioned);
*self.report.borrow_mut() = Some(report.clone());
return true;
}
let raw = x[..n].to_vec();
let report = match self.conditioner {
StartConditioner::Jitter { seed, scale } => {
self.apply_jitter(&mut x[..n], &x_l, &x_u, seed, scale)
}
StartConditioner::Adam(cfg) => {
self.apply_adam(&mut x[..n], &info, &x_l, &x_u, &g_l, &g_u, &cfg)
}
};
*self.cache.borrow_mut() = Some((raw, x[..n].to_vec(), report.clone()));
*self.report.borrow_mut() = Some(report);
true
}
fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
self.inner.borrow_mut().eval_f(x, new_x)
}
fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
self.inner.borrow_mut().eval_grad_f(x, new_x, grad_f)
}
fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
self.inner.borrow_mut().eval_g(x, new_x, g)
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
self.inner.borrow_mut().eval_jac_g(x, new_x, mode)
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
new_x: bool,
obj_factor: Number,
lambda: Option<&[Number]>,
new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
self.inner
.borrow_mut()
.eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
}
fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq) {
self.inner
.borrow_mut()
.finalize_solution(sol, ip_data, ip_cq)
}
fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
self.inner.borrow_mut().get_var_con_metadata(var, con)
}
fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
self.inner.borrow_mut().get_scaling_parameters(req)
}
fn get_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
self.inner.borrow_mut().get_variables_linearity(types)
}
fn get_objective_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
self.inner
.borrow_mut()
.get_objective_variables_linearity(types)
}
fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
self.inner.borrow_mut().get_constraints_linearity(types)
}
fn get_number_of_nonlinear_variables(&mut self) -> Index {
self.inner.borrow_mut().get_number_of_nonlinear_variables()
}
fn get_list_of_nonlinear_variables(&mut self, pos: &mut [Index]) -> bool {
self.inner.borrow_mut().get_list_of_nonlinear_variables(pos)
}
fn derivative_proofs(&mut self) -> crate::constant_derivatives::DerivativeProofs {
self.inner.borrow_mut().derivative_proofs()
}
fn intermediate_callback(
&mut self,
stats: IterStats,
ip_data: &IpoptData,
ip_cq: &IpoptCq,
) -> bool {
self.inner
.borrow_mut()
.intermediate_callback(stats, ip_data, ip_cq)
}
fn finalize_metadata(&mut self, var: &MetaData, con: &MetaData) {
self.inner.borrow_mut().finalize_metadata(var, con)
}
fn is_presolve_wrapper(&self) -> bool {
self.inner.borrow().is_presolve_wrapper()
}
fn scaling_factors(&self) -> Option<Vec<Number>> {
self.inner.borrow().scaling_factors()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tnlp::IndexStyle;
struct Toy {
n: usize,
start: Vec<Number>,
x_l: Vec<Number>,
x_u: Vec<Number>,
target: Vec<Number>,
g_l: Number,
g_u: Number,
with_constraint: bool,
init_x: bool,
f_calls: usize,
}
impl Toy {
fn new(n: usize) -> Self {
Self {
n,
start: vec![0.0; n],
x_l: vec![-DEFAULT_BOUND_INF; n],
x_u: vec![DEFAULT_BOUND_INF; n],
target: vec![0.0; n],
g_l: 0.0,
g_u: 0.0,
with_constraint: false,
init_x: true,
f_calls: 0,
}
}
fn m(&self) -> usize {
usize::from(self.with_constraint)
}
}
impl TNLP for Toy {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: self.n as Index,
m: self.m() as Index,
nnz_jac_g: (self.m() * self.n) as Index,
nnz_h_lag: self.n as Index,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&self.x_l);
b.x_u.copy_from_slice(&self.x_u);
if self.with_constraint {
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.init_x {
sp.x.copy_from_slice(&self.start);
}
true
}
fn eval_f(&mut self, x: &[Number], _n: bool) -> Option<Number> {
self.f_calls += 1;
Some(
0.5 * x
.iter()
.zip(&self.target)
.map(|(a, t)| (a - t) * (a - t))
.sum::<Number>(),
)
}
fn eval_grad_f(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
for i in 0..self.n {
g[i] = x[i] - self.target[i];
}
true
}
fn eval_g(&mut self, x: &[Number], _n: bool, g: &mut [Number]) -> bool {
if self.with_constraint {
g[0] = x.iter().sum();
}
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_n: bool,
mode: SparsityRequest<'_>,
) -> bool {
if !self.with_constraint {
return true;
}
match mode {
SparsityRequest::Structure { irow, jcol } => {
for j in 0..self.n {
irow[j] = 0;
jcol[j] = j as Index;
}
}
SparsityRequest::Values { values } => values.fill(1.0),
}
true
}
fn finalize_solution(&mut self, _s: Solution<'_>, _d: &IpoptData, _c: &IpoptCq) {}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_n: bool,
_o: Number,
_l: Option<&[Number]>,
_nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
for j in 0..self.n {
irow[j] = j as Index;
jcol[j] = j as Index;
}
}
SparsityRequest::Values { values } => values.fill(1.0),
}
true
}
}
fn conditioned(toy: Toy, c: StartConditioner) -> (Vec<Number>, ConditionerReport) {
let n = toy.n;
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(toy));
let mut wrapped = ConditionedStartTnlp::new(inner, c);
let (mut x, mut z_l, mut z_u, mut lam) =
(vec![0.0; n], vec![0.0; n], vec![0.0; n], vec![0.0; 1]);
assert!(wrapped.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 lam,
}));
(x, wrapped.report().unwrap_or_default())
}
fn jitter(seed: u64) -> StartConditioner {
StartConditioner::Jitter { seed, scale: 1e-2 }
}
#[test]
fn the_same_seed_produces_the_same_point() {
let (a, _) = conditioned(Toy::new(5), jitter(7));
let (b, _) = conditioned(Toy::new(5), jitter(7));
assert_eq!(a, b, "a retry rung has to be reproducible");
}
#[test]
fn a_different_seed_produces_a_different_point() {
let (a, _) = conditioned(Toy::new(5), jitter(7));
let (b, _) = conditioned(Toy::new(5), jitter(8));
assert_ne!(a, b);
}
#[test]
fn a_start_at_the_origin_actually_moves() {
let (x, report) = conditioned(Toy::new(4), jitter(1));
assert!(x.iter().all(|v| *v != 0.0), "no coordinate moved: {x:?}");
assert!(report.max_shift > 0.0);
assert!(
report.max_shift <= 1e-2,
"shift {} out of scale",
report.max_shift
);
}
#[test]
fn a_non_finite_start_is_sanitised_before_it_is_displaced() {
let mut toy = Toy::new(4);
toy.start = vec![Number::NAN, 1.0, Number::INFINITY, -2.0];
let (x, report) = conditioned(toy, jitter(3));
assert!(x.iter().all(|v| v.is_finite()), "{x:?}");
assert_eq!(report.sanitised, vec![0, 2]);
}
#[test]
fn a_sanitised_variable_lands_inside_its_bounds() {
let mut toy = Toy::new(3);
toy.start = vec![Number::NAN; 3];
toy.x_l = vec![2.0, 5.0, -DEFAULT_BOUND_INF];
toy.x_u = vec![4.0, DEFAULT_BOUND_INF, -1.0];
let (x, _) = conditioned(toy, jitter(11));
assert!((2.0..=4.0).contains(&x[0]), "{x:?}"); assert!(x[1] >= 5.0, "{x:?}"); assert!(x[2] <= -1.0, "{x:?}"); }
#[test]
fn the_displacement_never_leaves_the_box() {
let mut toy = Toy::new(3);
toy.start = vec![1.0, 1.0, 1.0];
toy.x_l = vec![1.0, 1.0, 1.0];
toy.x_u = vec![1.0, 1.0, 1.0]; let (x, report) = conditioned(
toy,
StartConditioner::Jitter {
seed: 5,
scale: 10.0,
},
);
assert_eq!(x, vec![1.0, 1.0, 1.0]);
assert_eq!(report.max_shift, 0.0);
}
#[test]
fn an_absent_bound_is_not_a_clip_target() {
let mut toy = Toy::new(2);
toy.start = vec![1e30, -1e30];
let (x, _) = conditioned(toy, jitter(2));
assert!(x[0] > 1e29 && x[1] < -1e29, "{x:?}");
}
#[test]
fn a_model_that_declines_to_set_a_start_is_left_alone() {
let mut toy = Toy::new(3);
toy.init_x = false;
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(toy));
let mut wrapped = ConditionedStartTnlp::new(inner, jitter(4));
let (mut x, mut z_l, mut z_u, mut lam) =
(vec![9.0; 3], vec![0.0; 3], vec![0.0; 3], vec![0.0; 1]);
assert!(wrapped.get_starting_point(StartingPoint {
init_x: false,
x: &mut x,
init_z: false,
z_l: &mut z_l,
z_u: &mut z_u,
init_lambda: false,
lambda: &mut lam,
}));
assert_eq!(
x,
vec![9.0; 3],
"no init_x means there is no start to condition"
);
assert!(wrapped.report().is_none());
}
#[test]
fn violation_is_zero_inside_the_band_and_signed_outside() {
let g = [1.0, -3.0, 7.0, 4.0];
let g_l = [0.0, 0.0, 0.0, 4.0];
let g_u = [2.0, 2.0, 2.0, 4.0];
let mut out = [0.0; 4];
violation(&g, &g_l, &g_u, &mut out);
assert_eq!(out, [0.0, -3.0, 5.0, 0.0]);
}
#[test]
fn violation_reduces_to_the_equality_residual() {
let g = [3.0, -1.0];
let b = [1.0, 1.0];
let mut out = [0.0; 2];
violation(&g, &b, &b, &mut out);
assert_eq!(out, [2.0, -2.0]);
}
fn adam(iters: usize) -> StartConditioner {
StartConditioner::Adam(AdamConfig {
iters,
..Default::default()
})
}
#[test]
fn the_warm_up_does_not_run_twice_for_the_same_start() {
let toy = Rc::new(RefCell::new(Toy::new(3)));
toy.borrow_mut().target = vec![1.0, -2.0, 0.5];
toy.borrow_mut().start = vec![5.0, 5.0, 5.0];
let inner: Rc<RefCell<dyn TNLP>> = toy.clone();
let mut wrapped = ConditionedStartTnlp::new(inner, adam(200));
let mut ask = |w: &mut ConditionedStartTnlp| {
let (mut x, mut z_l, mut z_u, mut lam) =
(vec![0.0; 3], vec![0.0; 3], vec![0.0; 3], vec![0.0; 1]);
assert!(w.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 lam,
}));
x
};
let first = ask(&mut wrapped);
let after_first = toy.borrow().f_calls;
assert!(after_first > 0, "the warm-up should have evaluated f");
let second = ask(&mut wrapped);
assert_eq!(
toy.borrow().f_calls,
after_first,
"the second call re-ran the warm-up",
);
assert_eq!(
first, second,
"and it must still answer with the same point"
);
assert!(
wrapped.report().is_some(),
"the report survives the cache hit"
);
toy.borrow_mut().start = vec![-4.0, 0.0, 7.0];
let third = ask(&mut wrapped);
assert!(
toy.borrow().f_calls > after_first,
"a changed start must re-run the warm-up",
);
assert_ne!(third, first);
}
#[test]
fn the_warm_up_walks_downhill_on_an_unconstrained_bowl() {
let mut toy = Toy::new(3);
toy.target = vec![1.0, -2.0, 0.5];
toy.start = vec![5.0, 5.0, 5.0];
let (x, report) = conditioned(toy, adam(200));
assert!(report.merit_final < report.merit_initial, "{report:?}");
assert_eq!(report.iters, 200);
let targets: [Number; 3] = [1.0, -2.0, 0.5];
for (i, (got, want)) in x.iter().zip(targets).enumerate() {
let before = (5.0 - want).abs();
let after = (got - want).abs();
assert!(
after < 0.15 * before,
"x[{i}] went {before} -> {after} ({x:?})"
);
}
}
#[test]
fn the_penalty_pulls_a_violated_constraint_back_toward_feasibility() {
let mut toy = Toy::new(2);
toy.with_constraint = true;
toy.g_l = 1.0;
toy.g_u = 1.0; toy.target = vec![10.0, 10.0]; toy.start = vec![10.0, 10.0];
let (x, report) = conditioned(toy, adam(200));
let before = 20.0 - 1.0;
let after = (x[0] + x[1] - 1.0).abs();
assert!(after < before, "sum went {before} -> {after} ({x:?})");
assert!(report.merit_final < report.merit_initial);
}
#[test]
fn a_warm_up_that_does_not_help_returns_the_original_point() {
let mut toy = Toy::new(3);
toy.target = vec![1.0, 2.0, 3.0];
toy.start = vec![1.0, 2.0, 3.0];
let (x, report) = conditioned(toy, adam(50));
assert_eq!(x, vec![1.0, 2.0, 3.0]);
assert_eq!(report.iters, 0);
assert_eq!(report.merit_final, report.merit_initial);
assert_eq!(report.max_shift, 0.0);
}
#[test]
fn a_sanitised_start_survives_a_warm_up_that_does_not_help() {
let mut toy = Toy::new(3);
toy.target = vec![0.0, 2.0, 3.0];
toy.start = vec![Number::NAN, 2.0, 3.0];
let (x, report) = conditioned(toy, adam(50));
assert_eq!(report.sanitised, vec![0]);
assert!(
x.iter().all(|v| v.is_finite()),
"the solver must not receive the un-sanitised start back: {x:?}",
);
assert_eq!(&x[1..], &[2.0, 3.0]);
assert!(report.max_shift.is_finite(), "{}", report.max_shift);
}
#[test]
fn a_zero_iteration_budget_is_a_no_op() {
let mut toy = Toy::new(3);
toy.start = vec![4.0, 5.0, 6.0];
let (x, report) = conditioned(toy, adam(0));
assert_eq!(x, vec![4.0, 5.0, 6.0]);
assert_eq!(report.iters, 0);
}
#[test]
fn the_warm_up_sanitises_a_non_finite_start_too() {
let mut toy = Toy::new(3);
toy.target = vec![1.0, 1.0, 1.0];
toy.start = vec![Number::NAN, 5.0, 5.0];
let (x, report) = conditioned(toy, adam(100));
assert_eq!(report.sanitised, vec![0]);
assert!(x.iter().all(|v| v.is_finite()), "{x:?}");
}
#[test]
fn the_warm_up_respects_variable_bounds() {
let mut toy = Toy::new(2);
toy.target = vec![-100.0, 100.0];
toy.start = vec![0.0, 0.0];
toy.x_l = vec![-1.0, -1.0];
toy.x_u = vec![1.0, 1.0];
let (x, _) = conditioned(toy, adam(200));
assert!(x.iter().all(|v| (-1.0..=1.0).contains(v)), "{x:?}");
}
#[test]
fn every_other_callback_is_forwarded_untouched() {
let mut toy = Toy::new(2);
toy.with_constraint = true;
toy.target = vec![1.0, 1.0];
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(toy));
let mut wrapped = ConditionedStartTnlp::new(Rc::clone(&inner), jitter(1));
let info = wrapped.get_nlp_info().unwrap();
assert_eq!((info.n, info.m, info.nnz_jac_g), (2, 1, 2));
let x = [3.0, 4.0];
assert_eq!(wrapped.eval_f(&x, true), Some(0.5 * (4.0 + 9.0)));
let mut grad = [0.0; 2];
assert!(wrapped.eval_grad_f(&x, true, &mut grad));
assert_eq!(grad, [2.0, 3.0]);
let mut g = [0.0; 1];
assert!(wrapped.eval_g(&x, true, &mut g));
assert_eq!(g, [7.0]);
let mut vals = [0.0; 2];
assert!(wrapped.eval_jac_g(
Some(&x),
true,
SparsityRequest::Values { values: &mut vals }
));
assert_eq!(vals, [1.0, 1.0]);
let mut b = ([0.0; 2], [0.0; 2], [0.0; 1], [0.0; 1]);
assert!(wrapped.get_bounds_info(BoundsInfo {
x_l: &mut b.0,
x_u: &mut b.1,
g_l: &mut b.2,
g_u: &mut b.3,
}));
assert_eq!(b.0, [-DEFAULT_BOUND_INF; 2]);
}
}