use crate::core::constraint::BoxConstraints;
use crate::core::math::Scalar;
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::ScalarState;
use crate::core::termination::TerminationReason;
pub struct GoldenSection<F = f64> {
tol_rel: F,
tol_abs: F,
inner: Option<Inner<F>>,
}
fn golden_r<F: Scalar>() -> F {
F::from_f64(0.618_033_988_749_894_9).unwrap()
}
#[derive(Clone, Copy)]
struct Inner<F> {
a: F,
b: F,
c1: F,
c2: F,
f1: F,
f2: F,
}
impl<F: Scalar> Default for GoldenSection<F> {
fn default() -> Self {
Self::new()
}
}
impl<F: Scalar> GoldenSection<F> {
pub fn new() -> Self {
Self {
tol_rel: F::epsilon().sqrt(),
tol_abs: F::from_f64(1e-12).unwrap(),
inner: None,
}
}
pub fn with_tol(tol_rel: F, tol_abs: F) -> Self {
assert!(tol_rel > F::zero(), "tol_rel must be > 0");
assert!(tol_abs > F::zero(), "tol_abs must be > 0");
Self {
tol_rel,
tol_abs,
inner: None,
}
}
}
impl<P, F> Solver<P, ScalarState<F>> for GoldenSection<F>
where
F: Scalar,
P: CostFunction<Param = F, Output = F> + BoxConstraints,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: ScalarState<F>,
) -> Result<ScalarState<F>, Self::Error> {
let a = *problem.inner().lower();
let b = *problem.inner().upper();
assert!(
a.is_finite() && b.is_finite() && a < b,
"GoldenSection requires a finite, ordered bracket: lower < upper"
);
let r = golden_r::<F>();
let span = b - a;
let c1 = a + (F::one() - r) * span;
let c2 = a + r * span;
let f1 = problem.cost(&c1)?;
let f2 = problem.cost(&c2)?;
self.inner = Some(Inner {
a,
b,
c1,
c2,
f1,
f2,
});
if f1 <= f2 {
state.param = c1;
state.cost = Some(f1);
} else {
state.param = c2;
state.cost = Some(f2);
}
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: ScalarState<F>,
) -> Result<(ScalarState<F>, Option<TerminationReason>), Self::Error> {
let s = self
.inner
.as_mut()
.expect("GoldenSection::init must run first");
let r = golden_r::<F>();
let (u, fu);
if s.f1 <= s.f2 {
s.b = s.c2;
s.c2 = s.c1;
s.f2 = s.f1;
s.c1 = s.a + (F::one() - r) * (s.b - s.a);
s.f1 = problem.cost(&s.c1)?;
u = s.c1;
fu = s.f1;
} else {
s.a = s.c1;
s.c1 = s.c2;
s.f1 = s.f2;
s.c2 = s.a + r * (s.b - s.a);
s.f2 = problem.cost(&s.c2)?;
u = s.c2;
fu = s.f2;
}
state.param = u;
state.cost = Some(fu);
Ok((state, None))
}
fn terminate(&self, _state: &ScalarState<F>) -> Option<TerminationReason> {
let s = self.inner.as_ref()?;
let two = F::from_f64(2.0).unwrap();
let x = if s.f1 <= s.f2 { s.c1 } else { s.c2 };
let tol = self.tol_rel * x.abs() + self.tol_abs;
if s.b - s.a <= two * tol {
Some(TerminationReason::SolverConverged)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::executor::Executor;
use crate::core::state::State;
use crate::core::termination::TerminationReason;
struct Quadratic {
lo: f64,
hi: f64,
}
impl CostFunction for Quadratic {
type Param = f64;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &f64) -> Result<f64, Self::Error> {
Ok((x - 2.0).powi(2))
}
}
impl BoxConstraints for Quadratic {
fn lower(&self) -> &f64 {
&self.lo
}
fn upper(&self) -> &f64 {
&self.hi
}
}
#[test]
fn quadratic_finds_interior_min() {
let r = Executor::new(
Quadratic { lo: 0.0, hi: 5.0 },
GoldenSection::new(),
ScalarState::new(2.5),
)
.max_iter(100)
.run()
.unwrap();
assert_eq!(r.reason, TerminationReason::SolverConverged);
assert!((r.param() - 2.0).abs() < 1e-6, "x = {}", r.param());
assert!(*r.param() >= 0.0 && *r.param() <= 5.0);
}
#[test]
fn monotonic_function_converges_to_boundary() {
let r = Executor::new(
Quadratic { lo: 3.0, hi: 5.0 },
GoldenSection::new(),
ScalarState::new(4.0),
)
.max_iter(200)
.run()
.unwrap();
assert!((r.param() - 3.0).abs() < 1e-5, "x = {}", r.param());
}
struct Cubic {
lo: f64,
hi: f64,
}
impl CostFunction for Cubic {
type Param = f64;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &f64) -> Result<f64, Self::Error> {
Ok(x.powi(3) - 3.0 * x)
}
}
impl BoxConstraints for Cubic {
fn lower(&self) -> &f64 {
&self.lo
}
fn upper(&self) -> &f64 {
&self.hi
}
}
#[test]
fn cubic_unimodal_on_interval() {
let r = Executor::new(
Cubic { lo: 0.0, hi: 2.0 },
GoldenSection::new(),
ScalarState::new(0.5),
)
.max_iter(100)
.run()
.unwrap();
assert_eq!(r.reason, TerminationReason::SolverConverged);
assert!(
(r.best_param() - 1.0).abs() < 1e-6,
"x = {}",
r.best_param()
);
assert!((r.best_cost() + 2.0).abs() < 1e-10, "f = {}", r.best_cost());
assert!(
r.state.cost_evals() < 80,
"evals = {}",
r.state.cost_evals()
);
}
#[test]
fn seed_is_ignored() {
let run = |seed: f64| {
Executor::new(
Quadratic { lo: 0.0, hi: 5.0 },
GoldenSection::new(),
ScalarState::new(seed),
)
.max_iter(100)
.run()
.unwrap()
};
let a = run(0.01);
let b = run(4.99);
assert!(
(a.param() - b.param()).abs() < 1e-9,
"{} vs {}",
a.param(),
b.param()
);
}
#[test]
fn cost_tolerance_does_not_fire_on_non_improving_probe() {
use crate::core::termination::CostTolerance;
let r = Executor::new(
Cubic { lo: 0.0, hi: 2.0 },
GoldenSection::new(),
ScalarState::new(0.5),
)
.max_iter(200)
.terminate_on(CostTolerance::new(1e-12))
.run()
.unwrap();
assert!(
(r.best_param() - 1.0).abs() < 1e-3,
"best_x = {}, reason = {:?}",
r.best_param(),
r.reason
);
assert!(
(r.best_cost() + 2.0).abs() < 1e-6,
"best_cost = {}, reason = {:?}",
r.best_cost(),
r.reason
);
assert!(r.best_iter() > 0, "best_iter = {}", r.best_iter());
assert!(
r.best_cost_evals() > 0,
"best_cost_evals = {}",
r.best_cost_evals()
);
}
}