#![allow(clippy::needless_range_loop)]
pub(crate) mod driver;
pub(crate) mod geometry;
pub(crate) mod init;
pub(crate) mod rescue;
pub(crate) mod trsbox;
#[cfg(test)]
mod parity;
use std::marker::PhantomData;
use crate::core::constraint::BoxConstraints;
use crate::core::math::{Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::BobyqaState;
use crate::core::termination::TerminationReason;
use driver::{BobyqaWork, Transition};
pub struct Bounded;
pub struct Bobyqa<Mode = Bounded, F = f64> {
rho_beg: F,
rho_end: F,
npt: Option<usize>,
work: Option<BobyqaWork<F>>,
_mode: PhantomData<fn() -> Mode>,
}
impl<F: Scalar> Bobyqa<Bounded, F> {
pub fn new() -> Self {
Self {
rho_beg: F::from_f64(1.0).expect("1.0 representable"),
rho_end: F::from_f64(1e-6).expect("1e-6 representable"),
npt: None,
work: None,
_mode: PhantomData,
}
}
pub fn with_rho_beg(mut self, rho_beg: F) -> Self {
self.rho_beg = rho_beg;
self
}
pub fn with_rho_end(mut self, rho_end: F) -> Self {
self.rho_end = rho_end;
self
}
pub fn with_npt(mut self, npt: usize) -> Self {
self.npt = Some(npt);
self
}
}
impl<F: Scalar> Default for Bobyqa<Bounded, F> {
fn default() -> Self {
Self::new()
}
}
fn fill_from<V, F>(template: &V, slice: &[F]) -> V
where
V: Clone + std::ops::IndexMut<usize, Output = F>,
F: Copy,
{
let mut v = template.clone();
for (i, &x) in slice.iter().enumerate() {
v[i] = x;
}
v
}
fn to_vec<V, F>(v: &V, n: usize) -> Vec<F>
where
V: std::ops::Index<usize, Output = F>,
F: Copy,
{
(0..n).map(|i| v[i]).collect()
}
impl<P, V, F> Solver<P, BobyqaState<V, F>> for Bobyqa<Bounded, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + BoxConstraints,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: BobyqaState<V, F>,
) -> Result<BobyqaState<V, F>, Self::Error> {
let n = state.param.vec_len();
assert!(n >= 1, "Bobyqa requires a non-empty start point");
let npt = self.npt.unwrap_or(2 * n + 1);
let x0 = to_vec(&state.param, n);
let lower = to_vec(problem.inner().lower(), n);
let upper = to_vec(problem.inner().upper(), n);
let template = state.param.clone();
let (work, best_x, best_f) = {
let mut eval =
|slice: &[F]| -> Result<F, P::Error> { problem.cost(&fill_from(&template, slice)) };
BobyqaWork::try_init(
x0,
&lower,
&upper,
self.rho_beg,
self.rho_end,
npt,
&mut eval,
)?
};
state.param = fill_from(&template, &best_x);
state.cost = Some(best_f);
state.rho = work.rho();
self.work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: BobyqaState<V, F>,
) -> Result<(BobyqaState<V, F>, Option<TerminationReason>), Self::Error> {
let template = state.param.clone();
let work = self
.work
.as_mut()
.expect("Bobyqa::init must run before next_iter");
let out = {
let mut eval =
|slice: &[F]| -> Result<F, P::Error> { problem.cost(&fill_from(&template, slice)) };
work.step(&mut eval)?
};
state.rho = work.rho();
let mut best_f = state.cost.expect("Bobyqa::init seeds the cost");
for (xabs, f_new) in &out.evaluated {
if *f_new < best_f {
best_f = *f_new;
state.param = fill_from(&template, xabs);
state.cost = Some(best_f);
}
}
let reason = match out.transition {
Transition::Converged => Some(TerminationReason::SolverConverged),
Transition::Continue | Transition::RhoReduced => None,
};
Ok((state, reason))
}
}
#[cfg(test)]
mod tests {
use crate::core::constraint::BoxConstraints;
use crate::core::problem::CostFunction;
use crate::{Bobyqa, BobyqaState, Executor, MaxCostEvals};
struct Quad {
lower: Vec<f64>,
upper: Vec<f64>,
}
impl CostFunction for Quad {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok((x[0] - 1.0).powi(2) + 2.0 * (x[1] + 2.0).powi(2))
}
}
impl BoxConstraints for Quad {
fn lower(&self) -> &Vec<f64> {
&self.lower
}
fn upper(&self) -> &Vec<f64> {
&self.upper
}
}
#[test]
fn converges_interior_minimum() {
let problem = Quad {
lower: vec![-5.0, -5.0],
upper: vec![5.0, 5.0],
};
let result = Executor::new(
problem,
Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
BobyqaState::new(vec![0.0, 0.0]),
)
.terminate_on(MaxCostEvals(500))
.run()
.unwrap();
let x = result.best_param();
assert!((x[0] - 1.0).abs() < 1e-5, "x0 = {}", x[0]);
assert!((x[1] + 2.0).abs() < 1e-5, "x1 = {}", x[1]);
assert!(result.best_cost() < 1e-9, "cost = {}", result.best_cost());
}
#[test]
fn narrow_box_revises_rho_beg() {
let problem = Quad {
lower: vec![0.9, -2.2],
upper: vec![1.1, -1.8],
};
let result = Executor::new(problem, Bobyqa::new(), BobyqaState::new(vec![1.0, -2.0]))
.terminate_on(MaxCostEvals(500))
.run()
.unwrap();
let x = result.best_param();
assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
assert!((x[1] + 2.0).abs() < 1e-3, "x1 = {}", x[1]);
}
#[test]
fn infeasible_start_converges() {
let problem = Quad {
lower: vec![-3.0, -3.0],
upper: vec![3.0, 3.0],
};
let result = Executor::new(
problem,
Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
BobyqaState::new(vec![100.0, -100.0]),
)
.terminate_on(MaxCostEvals(500))
.run()
.unwrap();
let x = result.best_param();
assert!((x[0] - 1.0).abs() < 1e-5, "x0 = {}", x[0]);
assert!((x[1] + 2.0).abs() < 1e-5, "x1 = {}", x[1]);
}
#[test]
fn converges_to_active_bound() {
struct Bowl {
lower: Vec<f64>,
upper: Vec<f64>,
}
impl CostFunction for Bowl {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok((x[0] - 5.0).powi(2) + (x[1] - 5.0).powi(2))
}
}
impl BoxConstraints for Bowl {
fn lower(&self) -> &Vec<f64> {
&self.lower
}
fn upper(&self) -> &Vec<f64> {
&self.upper
}
}
let problem = Bowl {
lower: vec![-2.0, -2.0],
upper: vec![2.0, 2.0],
};
let result = Executor::new(
problem,
Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
BobyqaState::new(vec![0.0, 0.0]),
)
.terminate_on(MaxCostEvals(500))
.run()
.unwrap();
let x = result.best_param();
assert!((x[0] - 2.0).abs() < 1e-5, "x0 = {}", x[0]);
assert!((x[1] - 2.0).abs() < 1e-5, "x1 = {}", x[1]);
}
#[test]
fn converges_rosenbrock_box() {
struct Rosen {
lower: Vec<f64>,
upper: Vec<f64>,
}
impl CostFunction for Rosen {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok(100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2))
}
}
impl BoxConstraints for Rosen {
fn lower(&self) -> &Vec<f64> {
&self.lower
}
fn upper(&self) -> &Vec<f64> {
&self.upper
}
}
let problem = Rosen {
lower: vec![-5.0, -5.0],
upper: vec![5.0, 5.0],
};
let result = Executor::new(
problem,
Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-6),
BobyqaState::new(vec![-1.2, 1.0]),
)
.terminate_on(MaxCostEvals(2000))
.run()
.unwrap();
let x = result.best_param();
assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-3, "x1 = {}", x[1]);
assert!(result.best_cost() < 1e-6, "cost = {}", result.best_cost());
}
}