1use lox_approx::approx_eq;
8use thiserror::Error;
9
10use crate::error::LoxError;
11use crate::math::callback::Callback;
12use crate::math::float::abs;
13
14#[derive(Debug, Error)]
16pub enum MinimizerError {
17 #[error(
19 "minimization did not converge after {iterations} iterations at x = {x}, f(x) = {value}"
20 )]
21 NotConverged {
22 iterations: u32,
24 x: f64,
26 value: f64,
28 },
29 #[error(transparent)]
31 Callback(#[from] LoxError),
32}
33
34pub trait FindBracketedMinimum<F>
36where
37 F: Callback,
38{
39 fn find_minimum_in_bracket(&self, f: F, bracket: (f64, f64)) -> Result<f64, MinimizerError>;
41}
42
43#[derive(Debug, Copy, Clone, PartialEq)]
47pub struct BrentMinimizer {
48 pub max_iter: u32,
50 pub abs_tol: f64,
52}
53
54impl Default for BrentMinimizer {
55 fn default() -> Self {
56 Self {
57 max_iter: 500,
58 abs_tol: 1e-10,
59 }
60 }
61}
62
63const GOLDEN: f64 = 0.381_966_011_250_105_1; impl<F> FindBracketedMinimum<F> for BrentMinimizer
67where
68 F: Callback,
69{
70 fn find_minimum_in_bracket(&self, f: F, bracket: (f64, f64)) -> Result<f64, MinimizerError> {
71 let (mut a, mut b) = bracket;
72 if a > b {
73 core::mem::swap(&mut a, &mut b);
74 }
75
76 let mut x = a + GOLDEN * (b - a);
80 let mut w = x;
81 let mut v = x;
82 let mut fx = f.call(x)?;
83 let mut fw = fx;
84 let mut fv = fx;
85
86 let mut e = 0.0_f64;
89 let mut d = 0.0_f64;
90
91 for _ in 0..self.max_iter {
92 let midpoint = 0.5 * (a + b);
93 let tol1 = self.abs_tol * abs(x) + 1e-10;
94 let tol2 = 2.0 * tol1;
95
96 if abs(x - midpoint) <= tol2 - 0.5 * (b - a) {
98 return Ok(x);
99 }
100
101 let mut use_golden = true;
103 if abs(e) > tol1 {
104 let r = (x - w) * (fx - fv);
106 let q = (x - v) * (fx - fw);
107 let p = (x - v) * q - (x - w) * r;
108 let q = 2.0 * (q - r);
109 let (p, q) = if q > 0.0 { (-p, q) } else { (p, -q) };
110
111 if abs(p) < abs(0.5 * q * e) && p > q * (a - x) && p < q * (b - x) {
113 e = d;
114 d = p / q;
115 let u = x + d;
116
117 if (u - a) < tol2 || (b - u) < tol2 {
119 d = if x < midpoint { tol1 } else { -tol1 };
120 }
121 use_golden = false;
122 }
123 }
124
125 if use_golden {
126 e = if x < midpoint { b - x } else { a - x };
128 d = GOLDEN * e;
129 }
130
131 let u = if abs(d) >= tol1 {
133 x + d
134 } else if d > 0.0 {
135 x + tol1
136 } else {
137 x - tol1
138 };
139
140 let fu = f.call(u)?;
141
142 if fu <= fx {
144 if u < x {
145 b = x;
146 } else {
147 a = x;
148 }
149 v = w;
150 fv = fw;
151 w = x;
152 fw = fx;
153 x = u;
154 fx = fu;
155 } else {
156 if u < x {
157 a = u;
158 } else {
159 b = u;
160 }
161 if fu <= fw || approx_eq!(w, x, atol <= 1e-15) {
162 v = w;
163 fv = fw;
164 w = u;
165 fw = fu;
166 } else if fu <= fv
167 || approx_eq!(v, x, atol <= 1e-15)
168 || approx_eq!(v, w, atol <= 1e-15)
169 {
170 v = u;
171 fv = fu;
172 }
173 }
174 }
175
176 Err(MinimizerError::NotConverged {
177 iterations: self.max_iter,
178 x,
179 value: fx,
180 })
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use core::f64::consts::PI;
187 use lox_approx::assert_approx_eq;
188
189 use super::*;
190 use crate::math::float::{cos, powi};
191
192 #[test]
193 fn test_brent_minimizer_quadratic() {
194 let minimizer = BrentMinimizer::default();
195 let x = minimizer
196 .find_minimum_in_bracket(|x: f64| powi(x - 3.0, 2), (0.0, 5.0))
197 .expect("should converge");
198 assert_approx_eq!(x, 3.0, atol <= 1e-8);
199 }
200
201 #[test]
202 fn test_brent_minimizer_cosine() {
203 let minimizer = BrentMinimizer::default();
205 let x = minimizer
206 .find_minimum_in_bracket(|x: f64| cos(x), (PI / 2.0, 3.0 * PI / 2.0))
207 .expect("should converge");
208 assert_approx_eq!(x, PI, atol <= 1e-8);
209 }
210
211 #[test]
212 fn test_brent_minimizer_reversed_bracket() {
213 let minimizer = BrentMinimizer::default();
214 let x = minimizer
215 .find_minimum_in_bracket(|x: f64| powi(x - 2.0, 2), (5.0, 0.0))
216 .expect("should converge");
217 assert_approx_eq!(x, 2.0, atol <= 1e-8);
218 }
219
220 #[test]
221 fn test_brent_minimizer_custom_tolerance() {
222 let minimizer = BrentMinimizer {
223 max_iter: 100,
224 abs_tol: 1e-4,
225 };
226 let x = minimizer
227 .find_minimum_in_bracket(|x: f64| powi(x - 1.0, 2), (-2.0, 5.0))
228 .expect("should converge");
229 assert_approx_eq!(x, 1.0, atol <= 1e-3);
230 }
231
232 #[test]
233 fn test_brent_minimizer_not_converged() {
234 let minimizer = BrentMinimizer {
235 max_iter: 0,
236 abs_tol: 1e-15,
237 };
238 let result = minimizer.find_minimum_in_bracket(|x: f64| powi(x - 1.0, 2), (0.0, 5.0));
239 assert!(result.is_err());
240 }
241}