use crate::core::errors::RustyQLibError;
pub mod bfgs;
pub mod conjugate_gradient;
pub mod differential_evolution;
pub mod levenberg_marquardt;
pub(crate) mod line_search;
pub mod nelder_mead;
pub(crate) mod numerics;
pub mod steepest_descent;
pub use bfgs::bfgs;
pub use conjugate_gradient::conjugate_gradient;
pub use differential_evolution::differential_evolution;
pub use levenberg_marquardt::levenberg_marquardt;
pub use nelder_mead::nelder_mead;
pub use steepest_descent::steepest_descent;
#[derive(Debug, Clone)]
pub struct OptimResult {
pub x: Vec<f64>,
pub value: f64,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct OptimConfig {
pub tol: f64,
pub max_iter: usize,
}
impl Default for OptimConfig {
fn default() -> Self {
Self { tol: 1e-8, max_iter: 500 }
}
}
impl OptimConfig {
pub fn new(tol: f64, max_iter: usize) -> Self {
Self { tol, max_iter }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
SteepestDescent,
ConjugateGradient,
Bfgs,
LevenbergMarquardt,
NelderMead,
DifferentialEvolution,
}
pub struct Problem<'a> {
pub f: Option<&'a dyn Fn(&[f64]) -> f64>,
pub residuals: Option<&'a dyn Fn(&[f64]) -> Vec<f64>>,
pub gradient: Option<&'a dyn Fn(&[f64]) -> Vec<f64>>,
pub jacobian: Option<&'a dyn Fn(&[f64]) -> Vec<Vec<f64>>>,
pub x0: Vec<f64>,
pub bounds: Option<Vec<(f64, f64)>>,
pub seed: u64,
}
impl<'a> Problem<'a> {
pub fn scalar(f: &'a dyn Fn(&[f64]) -> f64, x0: Vec<f64>) -> Self {
Self { f: Some(f), residuals: None, gradient: None, jacobian: None, x0, bounds: None, seed: 42 }
}
pub fn least_squares(residuals: &'a dyn Fn(&[f64]) -> Vec<f64>, x0: Vec<f64>) -> Self {
Self { f: None, residuals: Some(residuals), gradient: None, jacobian: None, x0, bounds: None, seed: 42 }
}
pub fn with_gradient(mut self, gradient: &'a dyn Fn(&[f64]) -> Vec<f64>) -> Self {
self.gradient = Some(gradient);
self
}
pub fn with_jacobian(mut self, jacobian: &'a dyn Fn(&[f64]) -> Vec<Vec<f64>>) -> Self {
self.jacobian = Some(jacobian);
self
}
pub fn with_bounds(mut self, bounds: Vec<(f64, f64)>) -> Self {
self.bounds = Some(bounds);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
}
pub fn minimize(
cfg: &OptimConfig,
method: Method,
problem: &Problem,
) -> Result<OptimResult, RustyQLibError> {
let sum_sq;
let f: &dyn Fn(&[f64]) -> f64 = match (problem.f, problem.residuals) {
(Some(f), _) => f,
(None, Some(r)) => {
sum_sq = move |x: &[f64]| r(x).iter().map(|e| e * e).sum::<f64>();
&sum_sq
}
(None, None) => return Err(RustyQLibError::invalid_input("optimization problem", "Problem has neither a scalar objective nor residuals")),
};
match method {
Method::SteepestDescent => Ok(steepest_descent(cfg, f, problem.gradient, &problem.x0)),
Method::ConjugateGradient => Ok(conjugate_gradient(cfg, f, problem.gradient, &problem.x0)),
Method::Bfgs => Ok(bfgs(cfg, f, problem.gradient, &problem.x0)),
Method::LevenbergMarquardt => {
let r = problem
.residuals
.ok_or(RustyQLibError::invalid_input("optimization problem", "Levenberg-Marquardt needs residuals: use Problem::least_squares"))?;
Ok(levenberg_marquardt(cfg, r, problem.jacobian, &problem.x0))
}
Method::NelderMead => Ok(nelder_mead(cfg, f, &problem.x0)),
Method::DifferentialEvolution => {
let bounds = problem
.bounds
.as_deref()
.ok_or(RustyQLibError::invalid_input("optimization problem", "differential evolution needs bounds: use Problem::with_bounds"))?;
Ok(differential_evolution(cfg, f, bounds, problem.seed))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sphere(x: &[f64]) -> f64 {
(x[0] - 1.0).powi(2) + (x[1] + 2.0).powi(2)
}
#[test]
fn every_scalar_method_is_pluggable_on_one_problem() {
let f = |x: &[f64]| sphere(x);
let cfg = OptimConfig::new(1e-10, 2000);
for method in [
Method::SteepestDescent,
Method::ConjugateGradient,
Method::Bfgs,
Method::NelderMead,
Method::DifferentialEvolution,
] {
let problem = Problem::scalar(&f, vec![4.0, 4.0])
.with_bounds(vec![(-10.0, 10.0), (-10.0, 10.0)]);
let r = minimize(&cfg, method, &problem).unwrap();
assert!(
(r.x[0] - 1.0).abs() < 1e-3 && (r.x[1] + 2.0).abs() < 1e-3,
"{method:?}: {:?}",
r.x
);
}
}
#[test]
fn least_squares_problems_feed_scalar_methods_too() {
let r = |x: &[f64]| vec![x[0] - 1.0, x[1] + 2.0];
let problem = Problem::least_squares(&r, vec![5.0, 5.0]);
let fit = minimize(&OptimConfig::default(), Method::Bfgs, &problem).unwrap();
assert!(fit.value < 1e-10, "{fit:?}");
}
#[test]
fn missing_requirements_error_clearly() {
let f = |x: &[f64]| sphere(x);
let no_bounds = Problem::scalar(&f, vec![0.0, 0.0]);
assert!(minimize(&OptimConfig::default(), Method::DifferentialEvolution, &no_bounds).is_err());
assert!(minimize(&OptimConfig::default(), Method::LevenbergMarquardt, &no_bounds).is_err());
}
}