rustyqlib/core/optimization/
mod.rs1use crate::core::errors::RustyQLibError;
39
40pub mod bfgs;
41pub mod conjugate_gradient;
42pub mod differential_evolution;
43pub mod levenberg_marquardt;
44pub(crate) mod line_search;
45pub mod nelder_mead;
46pub(crate) mod numerics;
47pub mod steepest_descent;
48
49pub use bfgs::bfgs;
50pub use conjugate_gradient::conjugate_gradient;
51pub use differential_evolution::differential_evolution;
52pub use levenberg_marquardt::levenberg_marquardt;
53pub use nelder_mead::nelder_mead;
54pub use steepest_descent::steepest_descent;
55
56#[derive(Debug, Clone)]
58pub struct OptimResult {
59 pub x: Vec<f64>,
61 pub value: f64,
64 pub iterations: usize,
66 pub converged: bool,
68}
69
70#[derive(Debug, Clone, Copy)]
77pub struct OptimConfig {
78 pub tol: f64,
79 pub max_iter: usize,
80}
81
82impl Default for OptimConfig {
83 fn default() -> Self {
84 Self { tol: 1e-8, max_iter: 500 }
85 }
86}
87
88impl OptimConfig {
89 pub fn new(tol: f64, max_iter: usize) -> Self {
90 Self { tol, max_iter }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Method {
97 SteepestDescent,
98 ConjugateGradient,
99 Bfgs,
100 LevenbergMarquardt,
101 NelderMead,
102 DifferentialEvolution,
103}
104
105pub struct Problem<'a> {
109 pub f: Option<&'a dyn Fn(&[f64]) -> f64>,
110 pub residuals: Option<&'a dyn Fn(&[f64]) -> Vec<f64>>,
111 pub gradient: Option<&'a dyn Fn(&[f64]) -> Vec<f64>>,
112 pub jacobian: Option<&'a dyn Fn(&[f64]) -> Vec<Vec<f64>>>,
113 pub x0: Vec<f64>,
114 pub bounds: Option<Vec<(f64, f64)>>,
116 pub seed: u64,
118}
119
120impl<'a> Problem<'a> {
121 pub fn scalar(f: &'a dyn Fn(&[f64]) -> f64, x0: Vec<f64>) -> Self {
123 Self { f: Some(f), residuals: None, gradient: None, jacobian: None, x0, bounds: None, seed: 42 }
124 }
125
126 pub fn least_squares(residuals: &'a dyn Fn(&[f64]) -> Vec<f64>, x0: Vec<f64>) -> Self {
128 Self { f: None, residuals: Some(residuals), gradient: None, jacobian: None, x0, bounds: None, seed: 42 }
129 }
130
131 pub fn with_gradient(mut self, gradient: &'a dyn Fn(&[f64]) -> Vec<f64>) -> Self {
132 self.gradient = Some(gradient);
133 self
134 }
135
136 pub fn with_jacobian(mut self, jacobian: &'a dyn Fn(&[f64]) -> Vec<Vec<f64>>) -> Self {
137 self.jacobian = Some(jacobian);
138 self
139 }
140
141 pub fn with_bounds(mut self, bounds: Vec<(f64, f64)>) -> Self {
142 self.bounds = Some(bounds);
143 self
144 }
145
146 pub fn with_seed(mut self, seed: u64) -> Self {
147 self.seed = seed;
148 self
149 }
150}
151
152pub fn minimize(
158 cfg: &OptimConfig,
159 method: Method,
160 problem: &Problem,
161) -> Result<OptimResult, RustyQLibError> {
162 let sum_sq;
164 let f: &dyn Fn(&[f64]) -> f64 = match (problem.f, problem.residuals) {
165 (Some(f), _) => f,
166 (None, Some(r)) => {
167 sum_sq = move |x: &[f64]| r(x).iter().map(|e| e * e).sum::<f64>();
168 &sum_sq
169 }
170 (None, None) => return Err(RustyQLibError::invalid_input("optimization problem", "Problem has neither a scalar objective nor residuals")),
171 };
172 match method {
173 Method::SteepestDescent => Ok(steepest_descent(cfg, f, problem.gradient, &problem.x0)),
174 Method::ConjugateGradient => Ok(conjugate_gradient(cfg, f, problem.gradient, &problem.x0)),
175 Method::Bfgs => Ok(bfgs(cfg, f, problem.gradient, &problem.x0)),
176 Method::LevenbergMarquardt => {
177 let r = problem
178 .residuals
179 .ok_or(RustyQLibError::invalid_input("optimization problem", "Levenberg-Marquardt needs residuals: use Problem::least_squares"))?;
180 Ok(levenberg_marquardt(cfg, r, problem.jacobian, &problem.x0))
181 }
182 Method::NelderMead => Ok(nelder_mead(cfg, f, &problem.x0)),
183 Method::DifferentialEvolution => {
184 let bounds = problem
185 .bounds
186 .as_deref()
187 .ok_or(RustyQLibError::invalid_input("optimization problem", "differential evolution needs bounds: use Problem::with_bounds"))?;
188 Ok(differential_evolution(cfg, f, bounds, problem.seed))
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 fn sphere(x: &[f64]) -> f64 {
198 (x[0] - 1.0).powi(2) + (x[1] + 2.0).powi(2)
199 }
200
201 #[test]
202 fn every_scalar_method_is_pluggable_on_one_problem() {
203 let f = |x: &[f64]| sphere(x);
204 let cfg = OptimConfig::new(1e-10, 2000);
205 for method in [
206 Method::SteepestDescent,
207 Method::ConjugateGradient,
208 Method::Bfgs,
209 Method::NelderMead,
210 Method::DifferentialEvolution,
211 ] {
212 let problem = Problem::scalar(&f, vec![4.0, 4.0])
213 .with_bounds(vec![(-10.0, 10.0), (-10.0, 10.0)]);
214 let r = minimize(&cfg, method, &problem).unwrap();
215 assert!(
216 (r.x[0] - 1.0).abs() < 1e-3 && (r.x[1] + 2.0).abs() < 1e-3,
217 "{method:?}: {:?}",
218 r.x
219 );
220 }
221 }
222
223 #[test]
224 fn least_squares_problems_feed_scalar_methods_too() {
225 let r = |x: &[f64]| vec![x[0] - 1.0, x[1] + 2.0];
227 let problem = Problem::least_squares(&r, vec![5.0, 5.0]);
228 let fit = minimize(&OptimConfig::default(), Method::Bfgs, &problem).unwrap();
229 assert!(fit.value < 1e-10, "{fit:?}");
230 }
231
232 #[test]
233 fn missing_requirements_error_clearly() {
234 let f = |x: &[f64]| sphere(x);
235 let no_bounds = Problem::scalar(&f, vec![0.0, 0.0]);
236 assert!(minimize(&OptimConfig::default(), Method::DifferentialEvolution, &no_bounds).is_err());
237 assert!(minimize(&OptimConfig::default(), Method::LevenbergMarquardt, &no_bounds).is_err());
238 }
239}