use crate::core::constraint::evaluation::BoxProblem;
use crate::core::math::Scalar;
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::PointState;
use crate::core::termination::Termination;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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,
}
}
}
impl<F: Scalar> GoldenSection<F> {
pub fn with_absolute_position_tolerance(mut self, value: F) -> Self {
assert!(
value.is_finite() && value > F::zero(),
"position tolerance must be finite and positive"
);
self.tol_abs = value;
self
}
pub fn with_relative_position_tolerance(mut self, value: F) -> Self {
assert!(
value.is_finite() && value > F::zero(),
"position tolerance must be finite and positive"
);
self.tol_rel = value;
self
}
}
impl<P, F> Solver<P, PointState<F, F>> for GoldenSection<F>
where
F: Scalar,
P: CostFunction<Param = F, Output = F> + BoxProblem<Param = F>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<F, F>,
) -> Result<PointState<F, F>, Self::Error> {
crate::core::constraint::evaluation::validate_scalar_box_problem(
problem.inner(),
)
.map_err(crate::core::constraint::evaluation::invalid)?;
state.reset();
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.replace(c1, f1);
} else {
state.replace(c2, f2);
}
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<F, F>,
) -> Result<crate::SolverStep<PointState<F, F>>, 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.replace(u, fu);
Ok(crate::SolverStep::from((state, None)))
}
fn terminate(&self, _state: &PointState<F, F>) -> Option<Termination<F>> {
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(Termination::custom(
"golden_section.bracket",
"b-a <= 2 * (relative_tolerance * |best_interior_point| + absolute_tolerance).",
vec![
crate::Measurement {
name: "a".into(),
value: s.a,
},
crate::Measurement {
name: "b".into(),
value: s.b,
},
crate::Measurement {
name: "best_interior_point".into(),
value: x,
},
crate::Measurement {
name: "relative_tolerance".into(),
value: self.tol_rel,
},
crate::Measurement {
name: "absolute_tolerance".into(),
value: self.tol_abs,
},
],
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::executor::Executor;
use crate::core::state::State;
struct Quadratic {
lo: f64,
hi: f64,
}
impl crate::Constraints for Quadratic {
type Set = crate::ConstraintSet<crate::BoxBounds<f64>>;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new().with_bounds(crate::BoxBounds::new(
Some(&self.lo),
Some(&self.hi),
))
}
}
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))
}
}
#[test]
fn quadratic_finds_interior_min() {
let r = Executor::new(
Quadratic { lo: 0.0, hi: 5.0 },
GoldenSection::new(),
PointState::new(2.5),
)
.max_iter(100)
.run()
.unwrap();
assert_eq!(r.report.code(), crate::TerminationCode::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(),
PointState::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 crate::Constraints for Cubic {
type Set = crate::ConstraintSet<crate::BoxBounds<f64>>;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new().with_bounds(crate::BoxBounds::new(
Some(&self.lo),
Some(&self.hi),
))
}
}
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)
}
}
#[test]
fn cubic_unimodal_on_interval() {
let r = Executor::new(
Cubic { lo: 0.0, hi: 2.0 },
GoldenSection::new(),
PointState::new(0.5),
)
.max_iter(100)
.run()
.unwrap();
assert_eq!(r.report.code(), crate::TerminationCode::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(),
PointState::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() {
let r = Executor::new(
Cubic { lo: 0.0, hi: 2.0 },
(GoldenSection::new()).with_absolute_cost_change_tolerance(1e-12),
PointState::new(0.5),
)
.max_iter(200)
.run()
.unwrap();
assert!(
(r.best_param() - 1.0).abs() < 1e-3,
"best_x = {}, reason = {:?}",
r.best_param(),
r.report.code()
);
assert!(
(r.best_cost() + 2.0).abs() < 1e-6,
"best_cost = {}, reason = {:?}",
r.best_cost(),
r.report.code()
);
assert!(r.best_iter() > 0, "best_iter = {}", r.best_iter());
assert!(
r.best_cost_evals() > 0,
"best_cost_evals = {}",
r.best_cost_evals()
);
}
}