use basin::problems::BoothBoxed;
use basin::{
CostFunction, Executor, Gradient, LbfgsState, MaxIter, ProjectedGradientTolerance, LBFGSB,
};
#[test]
fn unbounded_rosenbrock_2d_converges() {
struct Rosen {
l: Vec<f64>,
u: Vec<f64>,
}
impl CostFunction for Rosen {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
(1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2)
}
}
impl Gradient for Rosen {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
let dfdx0 = -2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0] * x[0]);
let dfdx1 = 200.0 * (x[1] - x[0] * x[0]);
vec![dfdx0, dfdx1]
}
}
impl basin::BoxConstrained for Rosen {
fn lower(&self) -> &Vec<f64> {
&self.l
}
fn upper(&self) -> &Vec<f64> {
&self.u
}
}
let problem = Rosen {
l: vec![f64::NEG_INFINITY; 2],
u: vec![f64::INFINITY; 2],
};
let lower = problem.l.clone();
let upper = problem.u.clone();
let state = LbfgsState::new(vec![-1.2, 1.0], 5);
let result = Executor::new(problem, LBFGSB::new(), state)
.terminate_on(MaxIter(200))
.terminate_on(ProjectedGradientTolerance::new(lower, upper, 1e-8))
.run();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
assert!(
(result.param()[0] - 1.0).abs() < 1e-4,
"x[0] = {}",
result.param()[0]
);
assert!(
(result.param()[1] - 1.0).abs() < 1e-4,
"x[1] = {}",
result.param()[1]
);
}
#[test]
fn booth_at_corner_converges() {
let problem = BoothBoxed::<Vec<f64>>::new(vec![-1.0, -1.0], vec![1.0, 1.0]);
let lower = vec![-1.0, -1.0];
let upper = vec![1.0, 1.0];
let state = LbfgsState::new(vec![0.0, 0.0], 5);
let result = Executor::new(problem, LBFGSB::new(), state)
.terminate_on(MaxIter(100))
.terminate_on(ProjectedGradientTolerance::new(
lower.clone(),
upper.clone(),
1e-8,
))
.run();
let expected_x = [1.0, 1.0];
assert!(
(result.param()[0] - expected_x[0]).abs() < 1e-5,
"x[0] = {}",
result.param()[0]
);
assert!(
(result.param()[1] - expected_x[1]).abs() < 1e-5,
"x[1] = {}",
result.param()[1]
);
assert!(
(result.cost() - 20.0).abs() < 1e-8,
"cost = {}, expected 20",
result.cost()
);
}
#[test]
fn booth_slack_bounds_recover_unconstrained_minimum() {
let problem = BoothBoxed::<Vec<f64>>::new(vec![-5.0, -5.0], vec![5.0, 5.0]);
let lower = vec![-5.0, -5.0];
let upper = vec![5.0, 5.0];
let state = LbfgsState::new(vec![0.0, 0.0], 5);
let result = Executor::new(problem, LBFGSB::new(), state)
.terminate_on(MaxIter(100))
.terminate_on(ProjectedGradientTolerance::new(lower, upper, 1e-10))
.run();
assert!(
(result.param()[0] - 1.0).abs() < 1e-4,
"x[0] = {}",
result.param()[0]
);
assert!(
(result.param()[1] - 3.0).abs() < 1e-4,
"x[1] = {}",
result.param()[1]
);
assert!(result.cost() < 1e-8, "cost = {}", result.cost());
}
#[test]
fn quadratic_5d_diagonal_converges_quickly() {
struct Quadratic {
diag: Vec<f64>,
l: Vec<f64>,
u: Vec<f64>,
}
impl CostFunction for Quadratic {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
let mut c = 0.0;
for (i, xi) in x.iter().enumerate() {
c += 0.5 * self.diag[i] * xi * xi - xi;
}
c
}
}
impl Gradient for Quadratic {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
let mut g = vec![0.0; x.len()];
for (i, xi) in x.iter().enumerate() {
g[i] = self.diag[i] * xi - 1.0;
}
g
}
}
impl basin::BoxConstrained for Quadratic {
fn lower(&self) -> &Vec<f64> {
&self.l
}
fn upper(&self) -> &Vec<f64> {
&self.u
}
}
let problem = Quadratic {
diag: vec![1.0, 2.0, 3.0, 4.0, 5.0],
l: vec![-2.0; 5],
u: vec![2.0; 5],
};
let lower = problem.l.clone();
let upper = problem.u.clone();
let initial = vec![0.0; 5];
let state = LbfgsState::new(initial, 5);
let result = Executor::new(problem, LBFGSB::new(), state)
.terminate_on(MaxIter(50))
.terminate_on(ProjectedGradientTolerance::new(lower, upper, 1e-10))
.run();
let expected_cost = -0.5 * (1.0 + 0.5 + 1.0 / 3.0 + 0.25 + 0.2);
assert!(
(result.cost() - expected_cost).abs() < 1e-8,
"cost = {}, expected {}",
result.cost(),
expected_cost
);
for (i, x_i) in result.param().iter().enumerate() {
let expected = 1.0 / (i + 1) as f64;
assert!(
(x_i - expected).abs() < 1e-5,
"x[{i}] = {x_i}, expected {expected}"
);
}
}