use std::cmp::Ordering;
use crate::prelude::*;
pub const OPTIMIZE_TOL: f64 = 1.220_703_125e-4;
const BRENT_SQRT_EPS: f64 = 1.490_116_119_384_765_6e-8;
const BRENT_GOLDEN: f64 = 0.381_966_011_250_105_15;
const NELDER_MEAD_STEP: f64 = 0.05;
const NELDER_MEAD_ZERO_STEP: f64 = 0.000_25;
pub fn brentq<F>(
mut f: F,
lower: f64,
upper: f64,
xtol: f64,
rtol: f64,
max_iter: usize,
) -> Result<f64, EdgeErrors>
where
F: FnMut(f64) -> f64,
{
if lower.partial_cmp(&upper) != Some(Ordering::Less) {
return Err(EdgeErrors::InvalidArgument(format!(
"brentq needs lower < upper, got [{lower}, {upper}]"
)));
}
if xtol <= 0.0 {
return Err(EdgeErrors::MustBePositive("xtol".to_string()));
}
let (mut xpre, mut xcur) = (lower, upper);
let (mut fpre, mut fcur) = (f(xpre), f(xcur));
if fpre == 0.0 {
return Ok(xpre);
}
if fcur == 0.0 {
return Ok(xcur);
}
if fpre.signum() == fcur.signum() {
return Err(EdgeErrors::RootNotBracketed {
lower,
upper,
f_lower: fpre,
f_upper: fcur,
});
}
let (mut xblk, mut fblk) = (0.0, 0.0);
let (mut spre, mut scur) = (0.0, 0.0);
for _ in 0..max_iter {
if fpre != 0.0 && fcur != 0.0 && fpre.signum() != fcur.signum() {
xblk = xpre;
fblk = fpre;
spre = xcur - xpre;
scur = spre;
}
if fblk.abs() < fcur.abs() {
xpre = xcur;
xcur = xblk;
xblk = xpre;
fpre = fcur;
fcur = fblk;
fblk = fpre;
}
let delta = (xtol + rtol * xcur.abs()) / 2.0;
let sbis = (xblk - xcur) / 2.0;
if fcur == 0.0 || sbis.abs() < delta {
return Ok(xcur);
}
if spre.abs() > delta && fcur.abs() < fpre.abs() {
let stry = if xpre == xblk {
-fcur * (xcur - xpre) / (fcur - fpre)
} else {
let dpre = (fpre - fcur) / (xpre - xcur);
let dblk = (fblk - fcur) / (xblk - xcur);
-fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre))
};
if 2.0 * stry.abs() < (spre.abs()).min(3.0 * sbis.abs() - delta) {
spre = scur;
scur = stry;
} else {
spre = sbis;
scur = sbis;
}
} else {
spre = sbis;
scur = sbis;
}
xpre = xcur;
fpre = fcur;
if scur.abs() > delta {
xcur += scur;
} else {
xcur += if sbis > 0.0 { delta } else { -delta };
}
fcur = f(xcur);
}
Err(EdgeErrors::NoConvergence {
routine: "brentq",
iterations: max_iter,
last_delta: (xblk - xcur).abs(),
})
}
pub fn brent_fmin<F>(ax: f64, bx: f64, mut f: F, tol: f64) -> f64
where
F: FnMut(f64) -> f64,
{
let (mut a, mut b) = (ax, bx);
let mut v = a + BRENT_GOLDEN * (b - a);
let (mut w, mut x) = (v, v);
let mut d = 0.0_f64;
let mut e = 0.0_f64;
let mut fx = f(x);
let (mut fv, mut fw) = (fx, fx);
let tol3 = tol / 3.0;
loop {
let xm = 0.5 * (a + b);
let tol1 = BRENT_SQRT_EPS * x.abs() + tol3;
let t2 = 2.0 * tol1;
if (x - xm).abs() <= t2 - 0.5 * (b - a) {
return x;
}
let (mut p, mut q, mut r) = (0.0_f64, 0.0_f64, 0.0_f64);
if e.abs() > tol1 {
r = (x - w) * (fx - fv);
q = (x - v) * (fx - fw);
p = (x - v) * q - (x - w) * r;
q = 2.0 * (q - r);
if q > 0.0 {
p = -p;
} else {
q = -q;
}
r = e;
e = d;
}
if p.abs() >= (0.5 * q * r).abs() || p <= q * (a - x) || p >= q * (b - x) {
e = if x < xm { b - x } else { a - x };
d = BRENT_GOLDEN * e;
} else {
d = p / q;
let u = x + d;
if u - a < t2 || b - u < t2 {
d = if x >= xm { -tol1 } else { tol1 };
}
}
let u = if d.abs() >= tol1 {
x + d
} else if d > 0.0 {
x + tol1
} else {
x - tol1
};
let fu = f(u);
if fu <= fx {
if u < x {
b = x;
} else {
a = x;
}
v = w;
fv = fw;
w = x;
fw = fx;
x = u;
fx = fu;
} else {
if u < x {
a = u;
} else {
b = u;
}
if fu <= fw || w == x {
v = w;
fv = fw;
w = u;
fw = fu;
} else if fu <= fv || v == x || v == w {
v = u;
fv = fu;
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct NelderMeadParams {
pub xatol: f64,
pub fatol: f64,
pub max_iter: usize,
}
impl Default for NelderMeadParams {
fn default() -> Self {
Self {
xatol: 1e-4,
fatol: 1e-4,
max_iter: 1000,
}
}
}
#[derive(Clone, Debug)]
pub struct NelderMeadResult {
pub x: Vec<f64>,
pub f: f64,
pub iterations: usize,
pub converged: bool,
}
pub fn nelder_mead<F>(
mut f: F,
x0: &[f64],
params: Option<NelderMeadParams>,
) -> Result<NelderMeadResult, EdgeErrors>
where
F: FnMut(&[f64]) -> f64,
{
let params = params.unwrap_or_default();
let n = x0.len();
if n == 0 {
return Err(EdgeErrors::MustBePositive("x0.len()".to_string()));
}
let mut simplex: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
simplex.push(x0.to_vec());
for i in 0..n {
let mut vertex = x0.to_vec();
if vertex[i] != 0.0 {
vertex[i] *= 1.0 + NELDER_MEAD_STEP;
} else {
vertex[i] = NELDER_MEAD_ZERO_STEP;
}
simplex.push(vertex);
}
let mut values: Vec<f64> = simplex.iter().map(|v| f(v)).collect();
let mut order: Vec<usize> = (0..=n).collect();
let mut iterations = 0;
let mut converged = false;
for iter in 0..params.max_iter {
iterations = iter + 1;
order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
let best = order[0];
let worst = order[n];
let second_worst = order[n - 1];
let spread_x = order[1..]
.iter()
.map(|&i| {
simplex[i]
.iter()
.zip(simplex[best].iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max)
})
.fold(0.0_f64, f64::max);
let spread_f = order[1..]
.iter()
.map(|&i| (values[i] - values[best]).abs())
.fold(0.0_f64, f64::max);
if spread_x <= params.xatol && spread_f <= params.fatol {
converged = true;
break;
}
let mut centroid = vec![0.0; n];
for &i in &order[..n] {
for (c, v) in centroid.iter_mut().zip(simplex[i].iter()) {
*c += *v;
}
}
for c in centroid.iter_mut() {
*c /= n as f64;
}
let reflected: Vec<f64> = (0..n)
.map(|j| centroid[j] + (centroid[j] - simplex[worst][j]))
.collect();
let f_reflected = f(&reflected);
if f_reflected < values[best] {
let expanded: Vec<f64> = (0..n)
.map(|j| centroid[j] + 2.0 * (centroid[j] - simplex[worst][j]))
.collect();
let f_expanded = f(&expanded);
if f_expanded < f_reflected {
simplex[worst] = expanded;
values[worst] = f_expanded;
} else {
simplex[worst] = reflected;
values[worst] = f_reflected;
}
} else if f_reflected < values[second_worst] {
simplex[worst] = reflected;
values[worst] = f_reflected;
} else {
let inside = f_reflected >= values[worst];
let contracted: Vec<f64> = if inside {
(0..n)
.map(|j| centroid[j] - 0.5 * (centroid[j] - simplex[worst][j]))
.collect()
} else {
(0..n)
.map(|j| centroid[j] + 0.5 * (centroid[j] - simplex[worst][j]))
.collect()
};
let f_contracted = f(&contracted);
let accept = if inside {
f_contracted < values[worst]
} else {
f_contracted <= f_reflected
};
if accept {
simplex[worst] = contracted;
values[worst] = f_contracted;
} else {
let anchor = simplex[best].clone();
for &i in &order[1..] {
for j in 0..n {
simplex[i][j] = anchor[j] + 0.5 * (simplex[i][j] - anchor[j]);
}
values[i] = f(&simplex[i]);
}
}
}
}
order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
let best = order[0];
Ok(NelderMeadResult {
x: simplex[best].clone(),
f: values[best],
iterations,
converged,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_brentq_finds_a_polynomial_root() {
let root = brentq(
|x: f64| x.powi(3) - 2.0 * x - 5.0,
1.0,
3.0,
1e-14,
1e-15,
200,
)
.unwrap();
assert_relative_eq!(root, 2.094_551_481_542_326_5, max_relative = 1e-12);
}
#[test]
fn test_brentq_handles_a_root_at_an_endpoint() {
let root = brentq(|x: f64| x - 1.0, 1.0, 3.0, 1e-14, 1e-15, 200).unwrap();
assert_relative_eq!(root, 1.0, max_relative = 1e-14);
}
#[test]
fn test_brentq_rejects_an_unbracketed_interval() {
let err = brentq(|x: f64| x * x + 1.0, -1.0, 1.0, 1e-12, 1e-14, 100).unwrap_err();
assert!(matches!(err, EdgeErrors::RootNotBracketed { .. }));
}
#[test]
fn test_brentq_converges_on_a_steep_function() {
let root = brentq(|x: f64| x.exp() - 1e6, 0.0, 30.0, 1e-12, 1e-14, 200).unwrap();
assert_relative_eq!(root, 1e6_f64.ln(), max_relative = 1e-10);
}
#[test]
fn test_nelder_mead_minimises_rosenbrock() {
let res = nelder_mead(
|x: &[f64]| 100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2),
&[-1.2, 1.0],
Some(NelderMeadParams {
xatol: 1e-10,
fatol: 1e-12,
max_iter: 5000,
}),
)
.unwrap();
assert!(res.converged);
assert_relative_eq!(res.x[0], 1.0, max_relative = 1e-4);
assert_relative_eq!(res.x[1], 1.0, max_relative = 1e-4);
}
#[test]
fn test_nelder_mead_handles_a_zero_starting_coordinate() {
let res = nelder_mead(
|x: &[f64]| (x[0] - 3.0).powi(2) + (x[1] + 2.0).powi(2),
&[0.0, 0.0],
Some(NelderMeadParams {
xatol: 1e-10,
fatol: 1e-12,
max_iter: 5000,
}),
)
.unwrap();
assert_relative_eq!(res.x[0], 3.0, max_relative = 1e-5);
assert_relative_eq!(res.x[1], -2.0, max_relative = 1e-5);
}
#[test]
fn test_nelder_mead_rejects_empty_start() {
let err = nelder_mead(|_: &[f64]| 0.0, &[], None).unwrap_err();
assert!(matches!(err, EdgeErrors::MustBePositive(_)));
}
}