#![allow(clippy::needless_range_loop)]
pub(crate) mod driver;
pub(crate) mod geometry;
pub(crate) mod halton;
pub(crate) mod progressive_barrier;
use std::marker::PhantomData;
use crate::core::constraint::evaluation::{BoxProblem, invalid};
use crate::core::inner::InitialState;
use crate::core::math::{Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::{PointState, SelectedState, State};
use crate::core::termination::Termination;
use crate::solver::nelder_mead::Unbounded;
use crate::{ConstraintSolveError, NonlinearInequalityValues};
use crate::{ViolationMeasure, ViolationReport};
use driver::{MadsWork, Transition};
use progressive_barrier::PbWork;
pub struct Bounded;
pub struct Constrained;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "F: serde::Serialize",
deserialize = "F: serde::Deserialize<'de>"
))
)]
pub struct Mads<Mode = Unbounded, F: Scalar = f64> {
poll_tolerance: Option<F>,
poll_size_init: F,
poll_size_min: F,
work: Option<MadsWork<F>>,
pb_work: Option<PbWork<F>>,
constraint_count: Option<usize>,
_mode: PhantomData<fn() -> Mode>,
}
impl<Mode, F: Scalar> Mads<Mode, F> {
pub fn poll_size(&self) -> Option<F> {
self.work
.as_ref()
.map(MadsWork::poll_size)
.or_else(|| self.pb_work.as_ref().map(PbWork::poll_size))
}
pub fn mesh_index(&self) -> Option<i32> {
self.work
.as_ref()
.map(MadsWork::mesh_index)
.or_else(|| self.pb_work.as_ref().map(PbWork::mesh_index))
}
pub fn with_absolute_poll_size_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.poll_tolerance =
crate::core::convergence::optional_tolerance(value);
self
}
}
impl<F: Scalar> Mads<Unbounded, F> {
pub fn new() -> Self {
Self {
poll_size_init: F::from_f64(1.0).expect("1.0 representable"),
poll_tolerance: None,
poll_size_min: F::from_f64(1e-6).expect("1e-6 representable"),
work: None,
pb_work: None,
constraint_count: None,
_mode: PhantomData,
}
}
}
impl<Mode, F: Scalar> Mads<Mode, F> {
pub fn with_initial_poll_size(mut self, poll_size_init: F) -> Self {
self.poll_size_init = poll_size_init;
self
}
pub fn with_minimum_poll_size(mut self, poll_size_min: F) -> Self {
self.poll_size_min = poll_size_min;
self
}
}
impl<F: Scalar> Mads<Unbounded, F> {
pub fn bounded(self) -> Mads<Bounded, F> {
Mads {
poll_size_init: self.poll_size_init,
poll_tolerance: self.poll_tolerance,
poll_size_min: self.poll_size_min,
work: None,
pb_work: None,
constraint_count: None,
_mode: PhantomData,
}
}
pub fn constrained(self) -> Mads<Constrained, F> {
Mads {
poll_size_init: self.poll_size_init,
poll_tolerance: self.poll_tolerance,
poll_size_min: self.poll_size_min,
work: None,
pb_work: None,
constraint_count: None,
_mode: PhantomData,
}
}
}
impl<F: Scalar> Default for Mads<Unbounded, F> {
fn default() -> Self {
Self::new()
}
}
fn fill_from<V, F>(template: &V, slice: &[F]) -> V
where
V: Clone + std::ops::IndexMut<usize, Output = F>,
F: Copy,
{
let mut v = template.clone();
for (i, &x) in slice.iter().enumerate() {
v[i] = x;
}
v
}
fn to_vec<V, F>(v: &V, n: usize) -> Vec<F>
where
V: std::ops::Index<usize, Output = F>,
F: Copy,
{
(0..n).map(|i| v[i]).collect()
}
fn out_of_box<F: Scalar>(x: &[F], lower: &[F], upper: &[F]) -> bool {
x.iter()
.zip(lower)
.zip(upper)
.any(|((&xi, &lo), &up)| xi < lo || xi > up)
}
impl<V, F> InitialState<V> for Mads<Unbounded, F>
where
F: Scalar,
V: Clone,
{
type State = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
impl<V, F> InitialState<V> for Mads<Bounded, F>
where
F: Scalar,
V: Clone,
{
type State = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
impl<V, F> InitialState<V> for Mads<Constrained, F>
where
F: Scalar,
V: Clone,
{
type State = SelectedState<V, F>;
fn seed(&self, x: &V) -> Self::State {
SelectedState::new(x.clone())
}
}
impl<P, V, F> Solver<P, PointState<V, F>> for Mads<Unbounded, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
P: crate::ProblemConstraints<Constraints = crate::NoConstraints>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
state.reset();
self.work = None;
self.pb_work = None;
let n = state.param().vec_len();
assert!(n >= 1, "Mads requires a non-empty start point");
let x0: Vec<F> = (0..n).map(|i| state.param()[i]).collect();
let template = state.param().clone();
let (work, best_x, best_f) = {
let mut eval = |slice: &[F]| -> Result<F, P::Error> {
problem.cost(&fill_from(&template, slice))
};
MadsWork::try_init(
x0,
self.poll_size_init,
self.poll_size_min,
&mut eval,
)?
};
state.replace(fill_from(&template, &best_x), best_f);
self.work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, Self::Error> {
let template = state.param().clone();
let work = self
.work
.as_mut()
.expect("Mads::init must run before next_iter");
let out = {
let mut eval = |slice: &[F]| -> Result<F, P::Error> {
problem.cost(&fill_from(&template, slice))
};
work.step(&mut eval)?
};
let mut best_f = state.cost();
for (xabs, f_new) in &out.evaluated {
if *f_new < best_f {
best_f = *f_new;
state.replace(fill_from(&template, xabs), best_f);
}
}
let reason = match out.transition {
Transition::Converged => Some(Termination::custom(
"mads.final_poll",
"The poll schedule reached its configured minimum size.",
vec![crate::Measurement {
name: "minimum_poll_size".into(),
value: self.poll_size_min,
}],
)),
Transition::Continue => None,
};
Ok(crate::SolverStep::from((state, reason)))
}
fn terminate(&self, _state: &PointState<V, F>) -> Option<Termination<F>> {
let tolerance = self.poll_tolerance?;
let metric = self.poll_size()?;
(metric.is_finite() && metric <= tolerance).then(|| {
Termination::upper_bound(
crate::ConvergenceTest::Mesh,
metric,
tolerance,
tolerance,
None,
)
})
}
}
fn violation<V, F>(c: &V, m: usize) -> F
where
V: std::ops::Index<usize, Output = F>,
F: Scalar,
{
let mut h = F::zero();
for j in 0..m {
let cj = c[j];
if cj > F::zero() {
h = h + cj * cj;
}
}
h
}
impl<P, V, F> Solver<P, SelectedState<V, F>> for Mads<Constrained, F>
where
F: Scalar,
P: CostFunction<
Param = V,
Output = F,
Constraints = crate::Constraints<
crate::NoBounds,
crate::NoLinearConstraints,
crate::NonlinearBlocks<
crate::NoNonlinearBlock,
crate::NonlinearBlock,
>,
>,
> + NonlinearInequalityValues<Param = V, Error = <P as CostFunction>::Error>,
<P as NonlinearInequalityValues>::Values:
VectorLen + std::ops::Index<usize, Output = F>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
type Error = ConstraintSolveError<<P as CostFunction>::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: SelectedState<V, F>,
) -> Result<SelectedState<V, F>, Self::Error> {
state.reset();
self.work = None;
self.pb_work = None;
let n = state.param().vec_len();
assert!(n >= 1, "Mads requires a non-empty start point");
let m = crate::ProblemConstraints::constraint_set(problem.inner())
.nonlinear()
.inequalities()
.count();
self.constraint_count = Some(m);
let x0: Vec<F> = (0..n).map(|i| state.param()[i]).collect();
let template = state.param().clone();
let (work, best_x, best_f, best_h) = {
let mut eval = |slice: &[F]| -> Result<
(F, F),
ConstraintSolveError<<P as CostFunction>::Error>,
> {
let xv = fill_from(&template, slice);
let f = problem.cost(&xv)?;
let c = checked_inequalities(problem, &xv, m)?;
Ok((f, violation(&c, m)))
};
PbWork::try_init(
x0,
self.poll_size_init,
self.poll_size_min,
&mut eval,
)?
};
state.replace(
fill_from(&template, &best_x),
best_f,
ViolationReport::new(best_h, ViolationMeasure::SquaredL2),
);
state.select_current();
self.pb_work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: SelectedState<V, F>,
) -> Result<crate::SolverStep<SelectedState<V, F>>, Self::Error> {
let m = self
.constraint_count
.expect("MADS initialization declares the constraint count");
let template = state.param().clone();
let work = self
.pb_work
.as_mut()
.expect("Mads::init must run before next_iter");
let out = {
let mut eval = |slice: &[F]| -> Result<
(F, F),
ConstraintSolveError<<P as CostFunction>::Error>,
> {
let xv = fill_from(&template, slice);
let f = problem.cost(&xv)?;
let c = checked_inequalities(problem, &xv, m)?;
Ok((f, violation(&c, m)))
};
work.step(&mut eval)?
};
let (x_star, f_star, h_star) = out.incumbent;
let changed = state.current().is_none_or(|(x, f, h)| {
f != f_star
|| h.value != h_star
|| (0..x_star.len()).any(|i| x[i] != x_star[i])
});
state.replace(
fill_from(&template, &x_star),
f_star,
ViolationReport::new(h_star, ViolationMeasure::SquaredL2),
);
if changed {
state.select_current();
}
let reason = match out.transition {
Transition::Converged => Some(Termination::custom(
"mads.final_poll",
"The poll schedule reached its configured minimum size.",
vec![crate::Measurement {
name: "minimum_poll_size".into(),
value: self.poll_size_min,
}],
)),
Transition::Continue => None,
};
Ok(crate::SolverStep::from((state, reason)))
}
fn terminate(
&self,
_state: &SelectedState<V, F>,
) -> Option<Termination<F>> {
let tolerance = self.poll_tolerance?;
let metric = self.poll_size()?;
(metric.is_finite() && metric <= tolerance).then(|| {
Termination::upper_bound(
crate::ConvergenceTest::Mesh,
metric,
tolerance,
tolerance,
None,
)
})
}
}
impl<P, V, F> Solver<P, PointState<V, F>> for Mads<Bounded, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + BoxProblem<Param = V>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
V: crate::VectorLen + std::ops::Index<usize, Output = F>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_box_problem(
problem.inner(),
Some(state.param().vec_len()),
false,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
state.reset();
self.work = None;
self.pb_work = None;
let n = state.param().vec_len();
assert!(n >= 1, "Mads requires a non-empty start point");
let lower = to_vec(problem.inner().lower(), n);
let upper = to_vec(problem.inner().upper(), n);
let x0: Vec<F> = (0..n)
.map(|i| {
let xi = state.param()[i];
if xi < lower[i] {
lower[i]
} else if xi > upper[i] {
upper[i]
} else {
xi
}
})
.collect();
let template = state.param().clone();
let (work, best_x, best_f) = {
let mut eval = |slice: &[F]| -> Result<
F,
crate::ConstraintSolveError<P::Error>,
> {
if out_of_box(slice, &lower, &upper) {
Ok(F::infinity()) } else {
Ok(problem.cost(&fill_from(&template, slice))?)
}
};
MadsWork::try_init(
x0,
self.poll_size_init,
self.poll_size_min,
&mut eval,
)?
};
state.replace(fill_from(&template, &best_x), best_f);
self.work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, Self::Error> {
let n = state.param().vec_len();
let lower = to_vec(problem.inner().lower(), n);
let upper = to_vec(problem.inner().upper(), n);
let template = state.param().clone();
let work = self
.work
.as_mut()
.expect("Mads::init must run before next_iter");
let out = {
let mut eval = |slice: &[F]| -> Result<
F,
crate::ConstraintSolveError<P::Error>,
> {
if out_of_box(slice, &lower, &upper) {
Ok(F::infinity())
} else {
Ok(problem.cost(&fill_from(&template, slice))?)
}
};
work.step(&mut eval)?
};
let mut best_f = state.cost();
for (xabs, f_new) in &out.evaluated {
if *f_new < best_f {
best_f = *f_new;
state.replace(fill_from(&template, xabs), best_f);
}
}
let reason = match out.transition {
Transition::Converged => Some(Termination::custom(
"mads.final_poll",
"The poll schedule reached its configured minimum size.",
vec![crate::Measurement {
name: "minimum_poll_size".into(),
value: self.poll_size_min,
}],
)),
Transition::Continue => None,
};
Ok(crate::SolverStep::from((state, reason)))
}
fn terminate(&self, _state: &PointState<V, F>) -> Option<Termination<F>> {
let tolerance = self.poll_tolerance?;
let metric = self.poll_size()?;
(metric.is_finite() && metric <= tolerance).then(|| {
Termination::upper_bound(
crate::ConvergenceTest::Mesh,
metric,
tolerance,
tolerance,
None,
)
})
}
}
fn checked_inequalities<P, V, F>(
p: &mut Problem<P>,
x: &V,
m: usize,
) -> Result<Vec<F>, ConstraintSolveError<<P as CostFunction>::Error>>
where
P: CostFunction<
Param = V,
Output = F,
Constraints = crate::Constraints<
crate::NoBounds,
crate::NoLinearConstraints,
crate::NonlinearBlocks<
crate::NoNonlinearBlock,
crate::NonlinearBlock,
>,
>,
> + NonlinearInequalityValues<Param = V, Error = <P as CostFunction>::Error>,
<P as NonlinearInequalityValues>::Values:
VectorLen + std::ops::Index<usize, Output = F>,
F: Scalar,
{
if crate::ProblemConstraints::constraint_set(p.inner())
.nonlinear()
.inequalities()
.count()
!= m
{
return Err(invalid(crate::ConstraintError::LayoutChanged));
}
if m == 0 {
return Ok(vec![]);
}
p.counts_mut().residual_evals += 1;
let values =
<P as NonlinearInequalityValues>::nonlinear_inequalities(p.inner(), x)?;
crate::core::constraint::model::dimension(
crate::ConstraintBlock::NonlinearInequalities,
crate::ConstraintAxis::Length,
m,
values.vec_len(),
)
.map_err(invalid)?;
Ok((0..m).map(|i| values[i]).collect())
}