use core::marker::PhantomData;
pub mod diagnostics;
use diagnostics::{Trace, Trial, TrialKind};
use crate::core::constraint::evaluation::BoxProblem;
use crate::core::convergence::{Check, SimplexChecks, optional_tolerance};
use crate::core::math::{
ClampInPlace, Scalar, ScaleInPlace, ScaledAdd, VectorIndex, VectorLen,
};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::SimplexProgress;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NelderMead<V, F: Scalar = f64, Mode = Unbounded> {
scratch: Vec<V>,
#[cfg_attr(feature = "serde", serde(skip, default = "Trace::default"))]
trace: Trace<V, F>,
config: ParamConfig<F>,
convergence: SimplexChecks<F>,
params: Option<Params<F>>,
_mode: PhantomData<fn() -> Mode>,
}
pub struct Unbounded;
pub struct Projected;
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct Params<F> {
alpha: F,
beta: F,
gamma: F,
delta: F,
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum ParamConfig<F> {
Standard,
Adaptive,
Fixed(Params<F>),
}
impl<V, F: Scalar> Default for NelderMead<V, F, Unbounded> {
fn default() -> Self {
Self::new()
}
}
impl<V, F: Scalar> NelderMead<V, F, Unbounded> {
pub fn new() -> Self {
Self {
config: ParamConfig::Standard,
convergence: SimplexChecks::nelder_mead_defaults(),
params: None,
scratch: Vec::new(),
trace: Trace {
enabled: false,
sequence: 0,
trials: Vec::new(),
},
_mode: PhantomData,
}
}
pub fn adaptive() -> Self {
Self {
config: ParamConfig::Adaptive,
convergence: SimplexChecks::nelder_mead_defaults(),
params: None,
scratch: Vec::new(),
trace: Trace {
enabled: false,
sequence: 0,
trials: Vec::new(),
},
_mode: PhantomData,
}
}
pub fn with_params(alpha: F, beta: F, gamma: F, delta: F) -> Self {
assert!(alpha > F::zero(), "α must be > 0");
assert!(beta > F::one(), "β must be > 1");
assert!(gamma > F::zero() && gamma < F::one(), "γ must be in (0, 1)");
assert!(delta > F::zero() && delta < F::one(), "δ must be in (0, 1)");
Self {
config: ParamConfig::Fixed(Params {
alpha,
beta,
gamma,
delta,
}),
convergence: SimplexChecks::nelder_mead_defaults(),
params: None,
scratch: Vec::new(),
trace: Trace {
enabled: false,
sequence: 0,
trials: Vec::new(),
},
_mode: PhantomData,
}
}
pub fn projected(self) -> NelderMead<V, F, Projected> {
NelderMead {
config: self.config,
convergence: self.convergence,
params: self.params,
scratch: self.scratch,
trace: self.trace,
_mode: PhantomData,
}
}
}
impl<V, Mode, F: Scalar> NelderMead<V, F, Mode> {
pub fn with_absolute_simplex_size_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.convergence.size = optional_tolerance(value);
self
}
pub fn with_relative_simplex_size_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.convergence.relative_size = optional_tolerance(value);
self
}
pub fn with_absolute_simplex_cost_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.convergence.cost = optional_tolerance(value);
self
}
pub fn with_relative_simplex_cost_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.convergence.relative_cost = optional_tolerance(value);
self
}
pub fn with_absolute_simplex_cost_std_dev_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.convergence.cost_std_dev = optional_tolerance(value);
self
}
pub fn with_trial_diagnostics(mut self, enabled: bool) -> Self {
self.trace.enabled = enabled;
self.trace.trials.clear();
self
}
pub fn trial_observations(&self) -> &[Trial<V, F>] {
&self.trace.trials
}
fn resolve(config: ParamConfig<F>, n: usize) -> Params<F> {
assert!(n >= 1, "NelderMead requires at least a 1-D problem");
match config {
ParamConfig::Standard => {
let half = F::from_f64(0.5).unwrap();
Params {
alpha: F::one(),
beta: F::from_f64(2.0).unwrap(),
gamma: half,
delta: half,
}
}
ParamConfig::Adaptive => {
let n = F::from_usize(n).unwrap();
let two = F::from_f64(2.0).unwrap();
Params {
alpha: F::one(),
beta: F::one() + two / n,
gamma: F::from_f64(0.75).unwrap() - F::one() / (two * n),
delta: F::one() - F::one() / n,
}
}
ParamConfig::Fixed(p) => p,
}
}
}
fn affine_into<V, F>(out: &mut V, a: &V, b: &V, t: F)
where
V: Clone + ScaleInPlace<F> + ScaledAdd<F>,
F: Scalar,
{
out.scale_in_place(F::zero());
out.scaled_add(F::one() - t, a);
out.scaled_add(t, b);
}
fn centroid_into<V, F>(out: &mut V, vertices: &[V])
where
V: Clone + ScaleInPlace<F> + ScaledAdd<F>,
F: Scalar,
{
let inv = F::from_usize(vertices.len()).unwrap().recip();
out.scale_in_place(F::zero());
for v in vertices {
out.scaled_add(inv, v);
}
}
fn init_costs_and_sort<P, V, F>(
problem: &mut Problem<P>,
state: &mut SimplexProgress<V, F>,
) -> Result<(), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
{
for v in &state.vertices {
state.costs.push(problem.cost(v)?);
}
state.sort();
Ok(())
}
#[allow(clippy::type_complexity)]
fn next_iter_inner<P, V, F, Proj>(
problem: &mut Problem<P>,
mut state: SimplexProgress<V, F>,
p: Params<F>,
scratch: &mut [V],
trace: &mut Trace<V, F>,
project: &Proj,
) -> Result<crate::SolverStep<SimplexProgress<V, F>>, P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone + ScaleInPlace<F> + ScaledAdd<F>,
Proj: Fn(&P, &mut V),
{
trace.trials.clear();
let m = state.vertices.len();
let n = m - 1;
let worst = m - 1;
let f1 = state.costs[0];
let fn_ = state.costs[n - 1];
let fnp1 = state.costs[worst];
let (xbar_slice, rest) = scratch.split_at_mut(1);
let (xr_slice, alt_slice) = rest.split_at_mut(1);
let x_bar = &mut xbar_slice[0];
let x_r = &mut xr_slice[0];
let x_alt = &mut alt_slice[0];
centroid_into(x_bar, &state.vertices[..n]);
affine_into(x_r, x_bar, &state.vertices[worst], -p.alpha);
project(problem.inner(), x_r);
let fr = trace.evaluate(problem, x_r, TrialKind::Reflection)?;
if f1 <= fr && fr < fn_ {
trace.accept(0, true);
std::mem::swap(&mut state.vertices[worst], x_r);
state.costs[worst] = fr;
} else if fr < f1 {
affine_into(x_alt, x_bar, x_r, p.beta);
project(problem.inner(), x_alt);
let fe = trace.evaluate(problem, x_alt, TrialKind::Expansion)?;
let expanded = fe < fr;
trace.accept(0, !expanded);
trace.accept(1, expanded);
if fe < fr {
std::mem::swap(&mut state.vertices[worst], x_alt);
state.costs[worst] = fe;
} else {
std::mem::swap(&mut state.vertices[worst], x_r);
state.costs[worst] = fr;
}
} else if fr < fnp1 {
affine_into(x_alt, x_bar, x_r, p.gamma);
project(problem.inner(), x_alt);
trace.accept(0, false);
let foc =
trace.evaluate(problem, x_alt, TrialKind::OutsideContraction)?;
trace.accept(1, foc <= fr);
if foc <= fr {
std::mem::swap(&mut state.vertices[worst], x_alt);
state.costs[worst] = foc;
} else {
shrink_inner(problem, &mut state, p.delta, project, trace)?;
}
} else {
affine_into(x_alt, x_bar, &state.vertices[worst], p.gamma);
project(problem.inner(), x_alt);
trace.accept(0, false);
let fic =
trace.evaluate(problem, x_alt, TrialKind::InsideContraction)?;
trace.accept(1, fic < fnp1);
if fic < fnp1 {
std::mem::swap(&mut state.vertices[worst], x_alt);
state.costs[worst] = fic;
} else {
shrink_inner(problem, &mut state, p.delta, project, trace)?;
}
}
state.sort();
Ok(crate::SolverStep::from((state, None)))
}
fn shrink_inner<P, V, F, Proj>(
problem: &mut Problem<P>,
state: &mut SimplexProgress<V, F>,
delta: F,
project: &Proj,
trace: &mut Trace<V, F>,
) -> Result<(), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone + ScaleInPlace<F> + ScaledAdd<F>,
Proj: Fn(&P, &mut V),
{
let one = F::one();
let (best_slice, rest) = state.vertices.split_at_mut(1);
let best = &best_slice[0];
for (i, (v, c)) in rest.iter_mut().zip(&mut state.costs[1..]).enumerate() {
v.scale_in_place(delta);
v.scaled_add(one - delta, best);
project(problem.inner(), v);
*c = trace.evaluate(problem, v, TrialKind::Shrink(i + 1))?;
trace.accept(trace.trials.len().saturating_sub(1), true);
}
Ok(())
}
impl<P, V, F> Solver<P, SimplexProgress<V, F>> for NelderMead<V, F, Unbounded>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone + VectorLen + VectorIndex<F> + ScaleInPlace<F> + ScaledAdd<F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: SimplexProgress<V, F>,
) -> Result<SimplexProgress<V, F>, Self::Error> {
state.reset();
self.convergence.reset();
self.trace.trials.clear();
self.trace.sequence = 0;
let n = state.vertices.len() - 1;
self.params = Some(Self::resolve(self.config, n));
self.scratch = vec![state.vertices[0].clone(); 3];
init_costs_and_sort(problem, &mut state)?;
self.convergence.capture_initial_cost_spread(state.costs());
Ok(state)
}
fn reset_convergence(&mut self) {
self.convergence.reset();
}
fn check_convergence(
&mut self,
problem: &Problem<P>,
state: &SimplexProgress<V, F>,
) -> Option<crate::Termination<F>> {
self.convergence.check(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: SimplexProgress<V, F>,
) -> Result<crate::SolverStep<SimplexProgress<V, F>>, Self::Error> {
let p = self
.params
.expect("NelderMead::init must run before next_iter");
next_iter_inner(
problem,
state,
p,
&mut self.scratch,
&mut self.trace,
&|_: &P, _: &mut V| {},
)
}
}
impl<P, V, F> Solver<P, SimplexProgress<V, F>> for NelderMead<V, F, Projected>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + BoxProblem<Param = V>,
V: Clone
+ VectorLen
+ VectorIndex<F>
+ ScaleInPlace<F>
+ ScaledAdd<F>
+ ClampInPlace,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: SimplexProgress<V, F>,
) -> Result<SimplexProgress<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_coordinate_box_problem(
problem.inner(),
state.vertices[0].vec_len(),
false,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
state.reset();
self.convergence.reset();
self.trace.trials.clear();
self.trace.sequence = 0;
let n = state.vertices.len() - 1;
self.params = Some(Self::resolve(self.config, n));
for v in state.vertices.iter_mut() {
v.clamp_in_place(problem.inner().lower(), problem.inner().upper());
}
self.scratch = vec![state.vertices[0].clone(); 3];
init_costs_and_sort(problem, &mut state)?;
self.convergence.capture_initial_cost_spread(state.costs());
Ok(state)
}
fn reset_convergence(&mut self) {
self.convergence.reset();
}
fn check_convergence(
&mut self,
problem: &Problem<P>,
state: &SimplexProgress<V, F>,
) -> Option<crate::Termination<F>> {
self.convergence.check(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: SimplexProgress<V, F>,
) -> Result<crate::SolverStep<SimplexProgress<V, F>>, Self::Error> {
let p = self
.params
.expect("NelderMead::init must run before next_iter");
next_iter_inner(
problem,
state,
p,
&mut self.scratch,
&mut self.trace,
&|problem: &P, v: &mut V| {
v.clamp_in_place(problem.lower(), problem.upper())
},
)
.map_err(Into::into)
}
}