use std::{cmp::PartialOrd, time::Instant};
use ndarray::{Array, Array1, Array2, s};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::numerical_engines::{VectorFunctionWrapper, VectorNumericalMinimiser, NumericalOptimisationResult};
type Simplex = Vec<(f64, Array1<f64>)>;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct NelderMead {
pub max_iterations: u64,
pub max_fun_calls: u64,
pub adaptive: bool,
pub eps: f64,
pub x_tolerance: f64,
pub f_tolerance: f64,
}
impl NelderMead {
pub fn new(max_iterations: Option<u64>, max_fun_calls: Option<u64>, adaptive: Option<bool>, eps: Option<f64>, x_tolerance: Option<f64>, f_tolerance: Option<f64>) -> Self {
Self {
max_iterations: max_iterations.unwrap_or(1_000),
max_fun_calls: max_fun_calls.unwrap_or(1_000),
adaptive: adaptive.unwrap_or(false),
eps: eps.unwrap_or(0.05),
x_tolerance: x_tolerance.unwrap_or(1e-4f64),
f_tolerance: f_tolerance.unwrap_or(1e-4f64),
}
}
pub fn max_iterations(mut self, max_iterations: Option<u64>) -> Self {
self.max_iterations = max_iterations.unwrap_or(1_000);
self
}
pub fn max_fun_calls(mut self, max_fun_calls: Option<u64>) -> Self {
self.max_fun_calls = max_fun_calls.unwrap_or(1_000);
self
}
pub fn adaptive(mut self, adaptive: Option<bool>) -> Self {
self.adaptive = adaptive.unwrap_or(false);
self
}
pub fn minimise_simplex<F: FnMut(&Array1<f64>) -> Result<f64, DigiFiError>>(&self, func: &mut VectorFunctionWrapper<F>, init_simplex: Array2<f64>) -> Result<Array1<f64>, DigiFiError> {
let mut simplex: Simplex = init_simplex.outer_iter().map(|xi| (func.call(&xi.to_owned()).unwrap(), xi.to_owned())).collect::<Simplex>();
self.order_simplex(&mut simplex);
let mut centroid: Array1<f64> = self.centroid(&simplex);
let n: usize = simplex.len();
let (alpha, beta, gamma, delta) = self.initialize_parameters(n);
let mut iterations: u64 = 1;
while !self.finished(&simplex, iterations, self.max_iterations, func.num, self.max_fun_calls) {
let f_n1: f64 = simplex[n-1].0;
let f_n: f64 = simplex[n-2].0;
let f_0: f64 = simplex[0].0;
let reflected: Array1<f64> = ¢roid + &(alpha * &(¢roid - &simplex[n-1].1));
let f_reflected: f64 = func.call(&reflected)?;
if f_reflected < f_n && f_reflected > f_0 {
self.lean_update(&mut simplex, &mut centroid, reflected, f_reflected);
} else if f_reflected < f_0 {
let expanded: Array1<f64> = ¢roid + &(beta * &(&reflected - ¢roid));
let f_expanded: f64 = func.call(&expanded)?;
if f_expanded < f_reflected {
self.lean_update(&mut simplex, &mut centroid, expanded, f_expanded);
} else {
self.lean_update(&mut simplex, &mut centroid, reflected, f_reflected);
}
} else if f_reflected < f_n1 && f_reflected >= f_n {
let contracted: Array1<f64> = ¢roid + &(gamma * (¢roid - &simplex[n-1].1));
let f_contracted: f64 = func.call(&contracted)?;
if f_contracted < f_reflected {
self.lean_update(&mut simplex, &mut centroid, contracted, f_contracted);
} else {
self.shrink(&mut simplex, func, delta, &mut centroid)?;
}
} else {
let contracted: Array1<f64> = ¢roid - &(gamma * (¢roid - &simplex[n-1].1));
let f_contracted: f64 = func.call(&contracted)?;
if f_contracted < f_reflected {
self.lean_update(&mut simplex, &mut centroid, contracted, f_contracted);
} else {
self.shrink(&mut simplex, func, delta, &mut centroid)?;
}
}
iterations += 1;
}
Ok(simplex.remove(0).1)
}
#[inline]
fn initialize_parameters(&self, n: usize) -> (f64, f64, f64, f64) {
if self.adaptive {
let dim: f64 = n as f64;
(1.0, 1.0 + 2.0 / dim, 0.75 - 1.0 / (2.0 * dim), 1.0 - 1.0 / dim)
} else {
(1.0, 2.0, 0.5, 0.5)
}
}
#[inline]
fn finished(&self, simplex: &Simplex, iterations: u64, max_iterations: u64, nfeval: u64, max_fun_calls: u64) -> bool {
let n: usize = simplex.len();
iterations >= max_iterations
|| nfeval >= max_fun_calls
|| ( simplex[n-1].0 - simplex[0].0 < self.f_tolerance
&& (&simplex[n-1].1 - &simplex[0].1).mapv(f64::abs).sum() < n as f64 * self.x_tolerance )
}
#[inline]
fn lean_update(&self, simplex: &mut Simplex, centroid: &mut Array1<f64>, xnew: Array1<f64>, fnew: f64) -> () {
let n_minus_one: usize = simplex.len() - 1;
*centroid += &(&xnew / (n_minus_one as f64));
simplex[n_minus_one] = (fnew, xnew);
self.order_simplex(simplex);
*centroid -= &(&simplex[n_minus_one].1 / (n_minus_one as f64));
}
#[inline]
fn shrink<F: FnMut(&Array1<f64>) -> Result<f64, DigiFiError>>(&self, simplex: &mut Simplex, f: &mut VectorFunctionWrapper<F>, sigma: f64, centroid: &mut Array1<f64>) -> Result<(), DigiFiError> {
{
let mut iter = simplex.iter_mut();
let (_, x_0) = iter.next()
.ok_or(DigiFiError::Other {
title: Self::error_title(),
details: "Could not grab next element from the simplex.".to_owned(),
})?;
for (fi, xi) in iter {
*xi *= sigma;
*xi += &((1.0 - sigma) * &x_0.view());
*fi = f.call(xi)?;
}
}
let n_minus_two: usize = simplex.len() - 2;
let old_worst: Array1<f64> = simplex[n_minus_two].1.to_owned();
*centroid *= sigma;
*centroid += &((1.0 - sigma) * &simplex[0].1);
self.order_simplex(simplex);
*centroid += &((&old_worst - &simplex[n_minus_two].1) / (n_minus_two as f64));
Ok(())
}
#[inline]
fn centroid(&self, simplex: &Simplex) -> Array1<f64> {
let n_minus_one: usize = simplex.len() - 1;
let mut centroid: Array1<f64> = Array1::zeros(simplex[0].1.len());
for (_, xi) in simplex.iter().take(n_minus_one) {
centroid += xi;
}
centroid / (n_minus_one as f64)
}
#[inline]
fn order_simplex(&self, simplex: &mut Simplex) -> () {
simplex.sort_by(|&(fa, _), &(fb, _)| fa.partial_cmp(&fb).unwrap());
}
}
impl Default for NelderMead {
fn default() -> Self {
Self {
max_iterations: 1_000,
max_fun_calls: 1_000,
adaptive: false,
eps: 0.05,
x_tolerance: 1e-4f64,
f_tolerance: 1e-4f64,
}
}
}
impl<F: FnMut(&Array1<f64>) -> Result<f64, DigiFiError>> VectorNumericalMinimiser<F> for NelderMead {
fn minimise(&self, mut func: VectorFunctionWrapper<F>, x_0: Array1<f64>) -> Result<NumericalOptimisationResult, DigiFiError> {
let start: Instant = <NelderMead as VectorNumericalMinimiser<F>>::time_start(&self);
let n: usize = x_0.len();
let mut init_simplex: Array2<f64> = Array2::default((n+1, n));
init_simplex.slice_mut(s![0, ..]).assign(&x_0);
init_simplex.slice_mut(s![1.., ..]).assign(&(Array::eye(n) * self.eps + &x_0 * (1.0 - self.eps)));
let argmin: Array1<f64> = self.minimise_simplex(&mut func, init_simplex)?;
let min_value: f64 = func.quick_call(&argmin)?;
let runtime: f64 = <NelderMead as VectorNumericalMinimiser<F>>::time_end(&self, start);
Ok(self.minimisation_result(func, argmin, min_value, runtime))
}
}
impl ErrorTitle for NelderMead {
fn error_title() -> String {
String::from("Nelder-Mead Numerical Engine")
}
}
#[cfg(test)]
mod tests {
use ndarray::Array1;
use crate::error::DigiFiError;
use crate::utilities::{TEST_ACCURACY, numerical_engines::{VectorNumericalMinimiser, NumericalOptimisationResult, NelderMead}};
#[test]
fn unit_test_nelder_mead_minimise() {
let func = |x: &Array1<f64>| -> Result<f64, DigiFiError> {
Ok((1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2))
};
let x_0: Array1<f64> = Array1::from_vec(vec![3.0, -8.3]);
let result: NumericalOptimisationResult = NelderMead::default().minimise(func.into(), x_0).unwrap();
assert!((result.argmin[0] - 1.0).abs() < 100_000.0 * TEST_ACCURACY);
assert!((result.argmin[1] - 1.0).abs() < 100_000.0 * TEST_ACCURACY);
}
}