scirs2_optimize/unconstrained/
mod.rs1use crate::error::OptimizeError;
6use ndarray::{Array1, ArrayView1};
7use std::fmt;
8
9pub mod bfgs;
11pub mod conjugate_gradient;
12pub mod lbfgs;
13pub mod line_search;
14pub mod nelder_mead;
15pub mod newton;
16pub mod powell;
17pub mod result;
18pub mod trust_region;
19pub mod utils;
20
21pub use result::OptimizeResult;
23
24pub use bfgs::minimize_bfgs;
26pub use conjugate_gradient::minimize_conjugate_gradient;
27pub use lbfgs::{minimize_lbfgs, minimize_lbfgsb};
28pub use nelder_mead::minimize_nelder_mead;
29pub use newton::minimize_newton_cg;
30pub use powell::minimize_powell;
31pub use trust_region::{minimize_trust_exact, minimize_trust_krylov, minimize_trust_ncg};
32
33#[derive(Debug, Clone, Copy)]
35pub enum Method {
36 NelderMead,
38 Powell,
40 CG,
42 BFGS,
44 LBFGS,
46 LBFGSB,
48 NewtonCG,
50 TrustNCG,
52 TrustKrylov,
54 TrustExact,
56}
57
58#[derive(Debug, Clone)]
60pub struct Bounds {
61 pub lower: Vec<Option<f64>>,
63 pub upper: Vec<Option<f64>>,
65}
66
67#[derive(Debug, Clone)]
69pub struct Options {
70 pub max_iter: usize,
72 pub max_fev: Option<usize>,
74 pub ftol: f64,
76 pub xtol: f64,
78 pub gtol: f64,
80 pub initial_step: Option<f64>,
82 pub maxstep: Option<f64>,
84 pub finite_diff: bool,
86 pub eps: f64,
88 pub trust_radius: Option<f64>,
90 pub max_trust_radius: Option<f64>,
92 pub min_trust_radius: Option<f64>,
94 pub trust_tol: Option<f64>,
96 pub trust_max_iter: Option<usize>,
98 pub trust_eta: Option<f64>,
100 pub bounds: Option<Bounds>,
102}
103
104impl fmt::Display for Method {
106 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107 match self {
108 Method::NelderMead => write!(f, "Nelder-Mead"),
109 Method::Powell => write!(f, "Powell"),
110 Method::CG => write!(f, "Conjugate Gradient"),
111 Method::BFGS => write!(f, "BFGS"),
112 Method::LBFGS => write!(f, "L-BFGS"),
113 Method::LBFGSB => write!(f, "L-BFGS-B"),
114 Method::NewtonCG => write!(f, "Newton-CG"),
115 Method::TrustNCG => write!(f, "Trust-NCG"),
116 Method::TrustKrylov => write!(f, "Trust-Krylov"),
117 Method::TrustExact => write!(f, "Trust-Exact"),
118 }
119 }
120}
121
122impl Default for Options {
124 fn default() -> Self {
125 Options {
126 max_iter: 1000,
127 max_fev: None,
128 ftol: 1e-8,
129 xtol: 1e-8,
130 gtol: 1e-5,
131 initial_step: None,
132 maxstep: None,
133 finite_diff: false,
134 eps: 1.4901161193847656e-8,
135 trust_radius: Some(1.0),
136 max_trust_radius: Some(100.0),
137 min_trust_radius: Some(1e-10),
138 trust_tol: Some(1e-8),
139 trust_max_iter: Some(100),
140 trust_eta: Some(0.1),
141 bounds: None,
142 }
143 }
144}
145
146impl Bounds {
148 pub fn new(bounds: &[(Option<f64>, Option<f64>)]) -> Self {
150 let (lower, upper): (Vec<_>, Vec<_>) = bounds.iter().cloned().unzip();
151 Self { lower, upper }
152 }
153
154 pub fn from_vecs(lb: Vec<Option<f64>>, ub: Vec<Option<f64>>) -> Result<Self, OptimizeError> {
156 if lb.len() != ub.len() {
157 return Err(OptimizeError::ValueError(
158 "Lower and upper bounds must have the same length".to_string(),
159 ));
160 }
161
162 for (l, u) in lb.iter().zip(ub.iter()) {
163 if let (Some(l_val), Some(u_val)) = (l, u) {
164 if l_val > u_val {
165 return Err(OptimizeError::ValueError(
166 "Lower bound must be less than or equal to upper bound".to_string(),
167 ));
168 }
169 }
170 }
171
172 Ok(Self {
173 lower: lb,
174 upper: ub,
175 })
176 }
177
178 pub fn is_feasible(&self, x: &[f64]) -> bool {
180 if x.len() != self.lower.len() {
181 return false;
182 }
183
184 for (&xi, (&lb, &ub)) in x.iter().zip(self.lower.iter().zip(self.upper.iter())) {
185 if let Some(l) = lb {
186 if xi < l {
187 return false;
188 }
189 }
190 if let Some(u) = ub {
191 if xi > u {
192 return false;
193 }
194 }
195 }
196 true
197 }
198
199 pub fn project(&self, x: &mut [f64]) {
201 for (xi, (&lb, &ub)) in x.iter_mut().zip(self.lower.iter().zip(self.upper.iter())) {
202 if let Some(l) = lb {
203 if *xi < l {
204 *xi = l;
205 }
206 }
207 if let Some(u) = ub {
208 if *xi > u {
209 *xi = u;
210 }
211 }
212 }
213 }
214
215 pub fn has_bounds(&self) -> bool {
217 self.lower.iter().any(|b| b.is_some()) || self.upper.iter().any(|b| b.is_some())
218 }
219}
220
221pub fn minimize<F, S>(
223 fun: F,
224 x0: &[f64],
225 method: Method,
226 options: Option<Options>,
227) -> Result<OptimizeResult<S>, OptimizeError>
228where
229 F: FnMut(&ArrayView1<f64>) -> S + Clone,
230 S: Into<f64> + Clone,
231{
232 let options = &options.unwrap_or_default();
233 let x0 = Array1::from_vec(x0.to_vec());
234
235 if let Some(ref bounds) = options.bounds {
237 if !bounds.is_feasible(x0.as_slice().unwrap()) {
238 return Err(OptimizeError::ValueError(
239 "Initial point is not feasible".to_string(),
240 ));
241 }
242 }
243
244 match method {
245 Method::NelderMead => nelder_mead::minimize_nelder_mead(fun, x0, options),
246 Method::Powell => powell::minimize_powell(fun, x0, options),
247 Method::CG => conjugate_gradient::minimize_conjugate_gradient(fun, x0, options),
248 Method::BFGS => bfgs::minimize_bfgs(fun, x0, options),
249 Method::LBFGS => lbfgs::minimize_lbfgs(fun, x0, options),
250 Method::LBFGSB => lbfgs::minimize_lbfgsb(fun, x0, options),
251 Method::NewtonCG => newton::minimize_newton_cg(fun, x0, options),
252 Method::TrustNCG => trust_region::minimize_trust_ncg(fun, x0, options),
253 Method::TrustKrylov => trust_region::minimize_trust_krylov(fun, x0, options),
254 Method::TrustExact => trust_region::minimize_trust_exact(fun, x0, options),
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use approx::assert_abs_diff_eq;
262
263 #[test]
264 fn test_simple_quadratic() {
265 let quadratic = |x: &ArrayView1<f64>| -> f64 { x[0] * x[0] + x[1] * x[1] };
266
267 let x0 = vec![1.0, 1.0];
268 let result = minimize(quadratic, &x0, Method::BFGS, None);
269 assert!(result.is_ok());
270
271 let result = result.unwrap();
272 assert!(result.success);
273 assert_abs_diff_eq!(result.x[0], 0.0, epsilon = 1e-6);
274 assert_abs_diff_eq!(result.x[1], 0.0, epsilon = 1e-6);
275 }
276}