#![allow(clippy::needless_range_loop)]
use crate::core::math::Scalar;
use super::driver::Transition;
use super::geometry::{mesh_and_poll_size, poll_directions};
use super::halton::halton_seed;
type PbEval<'a, F, E> = dyn FnMut(&[F]) -> Result<(F, F), E> + 'a;
pub(crate) struct PbOutcome<F> {
pub(crate) transition: Transition,
pub(crate) incumbent: (Vec<F>, F, F),
}
pub(crate) struct PbWork<F> {
n: usize,
ell: i32,
t0: usize,
ell_max: i32,
t_max: usize,
scale: F,
poll_size_min: F,
h_max: F,
feasible: Option<(Vec<F>, F)>,
infeasible: Vec<(Vec<F>, F, F)>,
}
impl<F: Scalar> PbWork<F> {
pub(crate) fn try_init<E>(
x0: Vec<F>,
poll_size_init: F,
poll_size_min: F,
eval: &mut PbEval<F, E>,
) -> Result<(Self, Vec<F>, F, F), E> {
let n = x0.len();
assert!(n >= 1, "Mads requires a non-empty start point");
let (f0, h0) = eval(&x0)?;
let mut work = Self {
n,
ell: 0,
t0: halton_seed(n),
ell_max: 0,
t_max: 0,
scale: poll_size_init,
poll_size_min,
h_max: F::infinity(),
feasible: None,
infeasible: Vec::new(),
};
work.record(x0, f0, h0);
let (xs, fs, hs) = work.reported_incumbent();
Ok((work, xs, fs, hs))
}
pub(crate) fn poll_size(&self) -> F {
let (_, poll) = mesh_and_poll_size(self.ell);
self.scale * F::from_f64(poll).expect("poll size representable")
}
pub(crate) fn mesh_index(&self) -> i32 {
self.ell
}
fn record(&mut self, x: Vec<F>, f: F, h: F) {
if h <= F::zero() {
match &self.feasible {
Some((_, best)) if *best <= f => {}
_ => self.feasible = Some((x, f)),
}
} else {
self.infeasible.push((x, f, h));
}
}
fn select_x_inf(&self) -> Option<usize> {
let mut best: Option<usize> = None;
for (i, (_, f, h)) in self.infeasible.iter().enumerate() {
if *h > self.h_max {
continue;
}
match best {
Some(j) => {
let (_, fj, hj) = &self.infeasible[j];
if f < fj || (f == fj && h < hj) {
best = Some(i);
}
}
None => best = Some(i),
}
}
best
}
fn reported_incumbent(&self) -> (Vec<F>, F, F) {
if let Some((x, f)) = &self.feasible {
(x.clone(), *f, F::zero())
} else {
let i = self
.select_x_inf()
.expect("a recorded point always yields an incumbent");
let (x, f, h) = &self.infeasible[i];
(x.clone(), *f, *h)
}
}
pub(crate) fn step<E>(&mut self, eval: &mut PbEval<F, E>) -> Result<PbOutcome<F>, E> {
let feas_pre = self.feasible.clone();
let inf_pre = self.select_x_inf().map(|i| self.infeasible[i].clone());
let h_inf_pre = inf_pre.as_ref().map(|t| t.2);
let t = if self.ell >= self.ell_max {
self.ell_max = self.ell;
(self.ell + self.t0 as i32) as usize
} else {
self.t_max + 1
};
self.t_max = self.t_max.max(t);
let (mesh_f64, _) = mesh_and_poll_size(self.ell);
let mesh = self.scale * F::from_f64(mesh_f64).expect("mesh size representable");
let dirs = poll_directions(t, self.ell, self.n);
let centers: Vec<Vec<F>> = [
feas_pre.as_ref().map(|(x, _)| x.clone()),
inf_pre.as_ref().map(|(x, _, _)| x.clone()),
]
.into_iter()
.flatten()
.collect();
let mut dominating = false;
'poll: for center in ¢ers {
for d in &dirs {
let mut trial = center.clone();
for i in 0..self.n {
let di = F::from_i64(d[i]).expect("integer direction representable");
trial[i] = trial[i] + mesh * di;
}
let (f, h) = eval(&trial)?;
let feasible = h <= F::zero();
self.record(trial, f, h);
if feasible {
if feas_pre.as_ref().is_none_or(|(_, ff)| f < *ff) {
dominating = true;
break 'poll;
}
} else if let Some((_, fi, hi)) = &inf_pre {
if f <= *fi && h <= *hi && (f < *fi || h < *hi) {
dominating = true;
break 'poll;
}
}
}
}
if dominating {
self.ell -= 1;
if let Some(h) = h_inf_pre {
self.h_max = h;
}
} else if let Some(hp) = h_inf_pre {
let below: Vec<F> = self
.infeasible
.iter()
.map(|(_, _, h)| *h)
.filter(|h| *h < hp)
.collect();
if below.is_empty() {
self.ell += 1;
self.h_max = hp;
} else {
let new_h = below.into_iter().fold(F::neg_infinity(), F::max);
self.h_max = new_h;
}
} else {
self.ell += 1;
}
let incumbent = self.reported_incumbent();
let transition = if self.poll_size() <= self.poll_size_min {
Transition::Converged
} else {
Transition::Continue
};
Ok(PbOutcome {
transition,
incumbent,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::Infallible;
fn run_steps<O>(x0: Vec<f64>, mut oracle: O, steps: usize) -> PbWork<f64>
where
O: FnMut(&[f64]) -> (f64, f64),
{
let mut eval = |x: &[f64]| -> Result<(f64, f64), Infallible> { Ok(oracle(x)) };
let (mut work, _, _, _) =
PbWork::try_init(x0, 1.0, 1e-9, &mut eval).expect("init infallible");
for _ in 0..steps {
work.step(&mut eval).expect("step infallible");
}
work
}
#[test]
fn record_routes_by_feasibility() {
let mut w: PbWork<f64> = PbWork {
n: 1,
ell: 0,
t0: 2,
ell_max: 0,
t_max: 0,
scale: 1.0,
poll_size_min: 1e-9,
h_max: f64::INFINITY,
feasible: None,
infeasible: Vec::new(),
};
w.record(vec![0.0], 1.0, 0.0); w.record(vec![1.0], 0.5, 0.25); assert_eq!(w.feasible, Some((vec![0.0], 1.0)));
assert_eq!(w.infeasible.len(), 1);
}
#[test]
fn select_x_inf_picks_least_f_within_threshold() {
let mut w: PbWork<f64> = PbWork {
n: 1,
ell: 0,
t0: 2,
ell_max: 0,
t_max: 0,
scale: 1.0,
poll_size_min: 1e-9,
h_max: 2.0,
feasible: None,
infeasible: vec![
(vec![0.0], 5.0, 0.5),
(vec![1.0], 1.0, 1.5),
(vec![2.0], 0.0, 3.0), ],
};
assert_eq!(w.select_x_inf(), Some(1));
w.h_max = 0.6; assert_eq!(w.select_x_inf(), Some(0));
w.h_max = 0.1; assert_eq!(w.select_x_inf(), None);
}
#[test]
fn drives_threshold_to_feasibility() {
let oracle = |x: &[f64]| {
let xi = x[0];
let c = -xi; let viol = c.max(0.0);
(xi, viol * viol)
};
let work = run_steps(vec![-1.0], oracle, 60);
assert!(work.h_max <= 1e-6, "h_max = {}", work.h_max);
let (_, f, h) = work.reported_incumbent();
assert!(h <= 1e-9, "reported violation {h}");
assert!(f.abs() < 1e-2, "reported f {f}");
}
}