1use crate::error::{all_finite, finite, tolerance, NumericError, NumericResult, Status};
19use crate::linalg::{Matrix, Qr};
20
21#[derive(Debug, Clone, Copy, PartialEq)]
23#[must_use]
24pub struct ScalarMinimum {
25 pub x: f64,
27 pub value: f64,
29 pub lower: f64,
31 pub upper: f64,
34 pub iterations: usize,
36 pub evaluations: usize,
38 pub status: Status,
40}
41
42impl ScalarMinimum {
43 pub fn error_bound(&self) -> f64 {
46 self.upper - self.lower
47 }
48
49 pub fn converged(self) -> NumericResult<Self> {
55 if self.status == Status::Converged {
56 Ok(self)
57 } else {
58 Err(NumericError::NotConverged {
59 name: "minimize_scalar",
60 error_estimate: self.error_bound(),
61 })
62 }
63 }
64}
65
66pub fn minimize_scalar<F>(
80 mut f: F,
81 lower: f64,
82 upper: f64,
83 x_tolerance: f64,
84) -> NumericResult<ScalarMinimum>
85where
86 F: FnMut(f64) -> f64,
87{
88 finite(lower, "lower bound")?;
89 finite(upper, "upper bound")?;
90 tolerance(x_tolerance, "x tolerance")?;
91 if lower >= upper {
92 return Err(NumericError::InvalidArgument {
93 name: "interval",
94 reason: "must have lower < upper",
95 });
96 }
97 const MAX_ITERATIONS: usize = 1000;
98 let golden = 0.5 * (3.0 - 5.0f64.sqrt());
99 let sqrt_eps = f64::EPSILON.sqrt();
100 let mut evaluations = 0usize;
101 let mut eval = |x: f64| -> NumericResult<f64> {
102 evaluations += 1;
103 let v = f(x);
104 if v.is_finite() {
105 Ok(v)
106 } else {
107 Err(NumericError::NonFiniteEvaluation {
108 name: "function",
109 at: Some(x),
110 })
111 }
112 };
113
114 let (mut a, mut b) = (lower, upper);
115 let mut x = a + golden * (b - a);
116 let (mut w, mut v) = (x, x);
117 let mut fx = eval(x)?;
118 let (mut fw, mut fv) = (fx, fx);
119 let (mut d, mut e) = (0.0f64, 0.0f64);
120 let mut iterations = 0;
121 let status = loop {
122 let xm = 0.5 * (a + b);
123 let tol1 = sqrt_eps * x.abs() + x_tolerance / 3.0;
124 let tol2 = 2.0 * tol1;
125 if (x - xm).abs() <= tol2 - 0.5 * (b - a) {
126 break Status::Converged;
127 }
128 if iterations >= MAX_ITERATIONS {
129 break Status::BudgetExhausted;
130 }
131 iterations += 1;
132 let mut use_golden = true;
133 if e.abs() > tol1 {
134 let r = (x - w) * (fx - fv);
135 let mut q = (x - v) * (fx - fw);
136 let mut p = (x - v) * q - (x - w) * r;
137 q = 2.0 * (q - r);
138 if q > 0.0 {
139 p = -p;
140 }
141 q = q.abs();
142 let previous = e;
143 e = d;
144 if p.abs() < (0.5 * q * previous).abs() && p > q * (a - x) && p < q * (b - x) {
145 d = p / q;
146 let u = x + d;
147 if u - a < tol2 || b - u < tol2 {
148 d = tol1.copysign(xm - x);
149 }
150 use_golden = false;
151 }
152 }
153 if use_golden {
154 e = if x >= xm { a - x } else { b - x };
155 d = golden * e;
156 }
157 let u = if d.abs() >= tol1 {
158 x + d
159 } else {
160 x + tol1.copysign(d)
161 };
162 let fu = eval(u)?;
163 if fu <= fx {
164 if u >= x {
165 a = x;
166 } else {
167 b = x;
168 }
169 v = w;
170 fv = fw;
171 w = x;
172 fw = fx;
173 x = u;
174 fx = fu;
175 } else {
176 if u < x {
177 a = u;
178 } else {
179 b = u;
180 }
181 if fu <= fw || w == x {
182 v = w;
183 fv = fw;
184 w = u;
185 fw = fu;
186 } else if fu <= fv || v == x || v == w {
187 v = u;
188 fv = fu;
189 }
190 }
191 };
192 Ok(ScalarMinimum {
193 x,
194 value: fx,
195 lower: a,
196 upper: b,
197 iterations,
198 evaluations,
199 status,
200 })
201}
202
203pub trait Residuals {
206 fn residual_count(&self) -> usize;
208
209 fn residuals(&mut self, x: &[f64], out: &mut [f64]);
211
212 fn jacobian(&mut self, x: &[f64], out: &mut Matrix) {
219 let m = self.residual_count();
220 let mut base = vec![0.0; m];
221 self.residuals(x, &mut base);
222 let mut shifted = vec![0.0; m];
223 let mut probe = x.to_vec();
224 for j in 0..x.len() {
225 let h = f64::EPSILON.sqrt() * x[j].abs().max(1.0);
226 probe[j] = x[j] + h;
227 let actual = probe[j] - x[j];
228 self.residuals(&probe, &mut shifted);
229 for i in 0..m {
230 out[(i, j)] = (shifted[i] - base[i]) / actual;
231 }
232 probe[j] = x[j];
233 }
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq)]
239pub struct LevenbergMarquardtOptions {
240 pub gradient_tolerance: f64,
242 pub step_tolerance: f64,
244 pub cost_tolerance: f64,
247 pub max_iterations: usize,
249}
250
251impl LevenbergMarquardtOptions {
252 pub fn new(tolerance: f64) -> Self {
255 Self {
256 gradient_tolerance: tolerance,
257 step_tolerance: tolerance,
258 cost_tolerance: 0.0,
259 max_iterations: 500,
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum Termination {
267 Gradient,
269 Step,
271 Cost,
273 IterationLimit,
275 NoProgress,
277}
278
279#[derive(Debug, Clone, PartialEq)]
281#[must_use]
282pub struct LeastSquaresMinimum {
283 pub x: Vec<f64>,
285 pub cost: f64,
287 pub gradient_norm: f64,
289 pub error_estimate: Option<f64>,
296 pub jacobian_condition: f64,
298 pub iterations: usize,
300 pub evaluations: usize,
302 pub termination: Termination,
304 pub status: Status,
306}
307
308impl LeastSquaresMinimum {
309 pub fn converged(self) -> NumericResult<Self> {
315 if self.status == Status::Converged {
316 Ok(self)
317 } else {
318 Err(NumericError::NotConverged {
319 name: "levenberg_marquardt",
320 error_estimate: self.error_estimate.unwrap_or(f64::INFINITY),
321 })
322 }
323 }
324}
325
326fn norm(v: &[f64]) -> f64 {
327 v.iter().map(|t| t * t).sum::<f64>().sqrt()
328}
329
330pub fn levenberg_marquardt<R: Residuals>(
341 problem: &mut R,
342 x0: &[f64],
343 options: LevenbergMarquardtOptions,
344) -> NumericResult<LeastSquaresMinimum> {
345 all_finite(x0, "initial point")?;
346 let gtol = tolerance(options.gradient_tolerance, "gradient tolerance")?;
347 let xtol = tolerance(options.step_tolerance, "step tolerance")?;
348 let ftol = tolerance(options.cost_tolerance, "cost tolerance")?;
349 let n = x0.len();
350 let m = problem.residual_count();
351 if n == 0 {
352 return Err(NumericError::InvalidArgument {
353 name: "initial point",
354 reason: "must have at least one unknown",
355 });
356 }
357 if m == 0 {
358 return Err(NumericError::InvalidArgument {
359 name: "residual count",
360 reason: "must be at least one",
361 });
362 }
363
364 let mut evaluations = 0usize;
365 let mut evaluate = |problem: &mut R, x: &[f64]| -> Vec<f64> {
366 evaluations += 1;
367 let mut r = vec![0.0; m];
368 problem.residuals(x, &mut r);
369 r
370 };
371 let jacobian = |problem: &mut R, x: &[f64]| -> NumericResult<Matrix> {
372 let mut j = Matrix::zeros(m, n);
373 problem.jacobian(x, &mut j);
374 if (0..m).all(|i| j.row(i).iter().all(|v| v.is_finite())) {
375 Ok(j)
376 } else {
377 Err(NumericError::NonFiniteEvaluation {
378 name: "jacobian",
379 at: None,
380 })
381 }
382 };
383
384 let mut x = x0.to_vec();
385 let mut r = evaluate(problem, &x);
386 if r.iter().any(|v| !v.is_finite()) {
387 return Err(NumericError::NonFiniteEvaluation {
388 name: "residuals",
389 at: None,
390 });
391 }
392 let mut cost = 0.5 * r.iter().map(|t| t * t).sum::<f64>();
393 let mut jac = jacobian(problem, &x)?;
394 let gradient = |jac: &Matrix, r: &[f64]| -> Vec<f64> {
395 (0..n)
396 .map(|j| (0..m).map(|i| jac[(i, j)] * r[i]).sum())
397 .collect()
398 };
399 let mut g = gradient(&jac, &r);
400 let column_norm =
401 |jac: &Matrix, j: usize| (0..m).map(|i| jac[(i, j)].powi(2)).sum::<f64>().sqrt();
402 let mut scale: Vec<f64> = (0..n)
403 .map(|j| {
404 let c = column_norm(&jac, j);
405 if c > 0.0 {
406 c
407 } else {
408 1.0
409 }
410 })
411 .collect();
412 let mut mu = 1e-3f64;
413 let mut nu = 2.0f64;
414 let mut iterations = 0;
415
416 let termination = loop {
417 let g_inf = g.iter().fold(0.0f64, |s, v| s.max(v.abs()));
418 if g_inf <= gtol {
419 break Termination::Gradient;
420 }
421 if cost <= ftol {
422 break Termination::Cost;
423 }
424 if iterations >= options.max_iterations {
425 break Termination::IterationLimit;
426 }
427 iterations += 1;
428
429 let root_mu = mu.sqrt();
431 let augmented = Matrix::from_fn(m + n, n, |i, j| {
432 if i < m {
433 jac[(i, j)]
434 } else if i - m == j {
435 root_mu * scale[j]
436 } else {
437 0.0
438 }
439 });
440 let mut rhs: Vec<f64> = r.iter().map(|v| -v).collect();
441 rhs.extend(std::iter::repeat_n(0.0, n));
442 let step = Qr::new(&augmented).and_then(|qr| qr.solve_least_squares(&rhs));
443 let delta = match step {
444 Ok(s) => s.x,
445 Err(NumericError::RankDeficient { .. }) => {
446 mu *= nu;
447 nu *= 2.0;
448 if !mu.is_finite() || mu > 1e300 {
449 break Termination::NoProgress;
450 }
451 continue;
452 }
453 Err(other) => return Err(other),
454 };
455 let x_norm = norm(&x);
456 if norm(&delta) <= xtol * (x_norm + xtol) {
457 break Termination::Step;
458 }
459 let trial: Vec<f64> = x.iter().zip(&delta).map(|(a, b)| a + b).collect();
460 let r_trial = evaluate(problem, &trial);
461 let cost_trial = if r_trial.iter().all(|v| v.is_finite()) {
462 0.5 * r_trial.iter().map(|t| t * t).sum::<f64>()
463 } else {
464 f64::INFINITY
465 };
466 let jd = jac.mul_vec(&delta)?;
468 let model: f64 = r.iter().zip(&jd).map(|(a, b)| (a + b) * (a + b)).sum();
469 let predicted = cost - 0.5 * model;
470 let actual = cost - cost_trial;
471 if predicted > 0.0 && actual > 0.0 {
472 let rho = actual / predicted;
473 x = trial;
474 r = r_trial;
475 cost = cost_trial;
476 jac = jacobian(problem, &x)?;
477 g = gradient(&jac, &r);
478 for (j, s) in scale.iter_mut().enumerate() {
479 *s = s.max(column_norm(&jac, j));
480 }
481 mu *= (1.0f64 / 3.0).max(1.0 - (2.0 * rho - 1.0).powi(3));
482 nu = 2.0;
483 } else {
484 mu *= nu;
485 nu *= 2.0;
486 if !mu.is_finite() || mu > 1e300 {
487 break Termination::NoProgress;
488 }
489 }
490 };
491
492 let gradient_norm = g.iter().fold(0.0f64, |s, v| s.max(v.abs()));
493 let (error_estimate, jacobian_condition) = match Qr::new(&jac) {
494 Ok(qr) => {
495 let neg: Vec<f64> = r.iter().map(|v| -v).collect();
496 let estimate = qr.solve_least_squares(&neg).ok().map(|s| norm(&s.x));
497 let condition = if qr.rank() < n {
498 f64::INFINITY
499 } else {
500 qr.condition_estimate()
501 };
502 (estimate, condition)
503 }
504 Err(_) => (None, f64::INFINITY),
505 };
506 let status = match termination {
507 Termination::Gradient | Termination::Step | Termination::Cost => Status::Converged,
508 Termination::IterationLimit => Status::BudgetExhausted,
509 Termination::NoProgress => Status::RoundoffLimited,
510 };
511 Ok(LeastSquaresMinimum {
512 x,
513 cost,
514 gradient_norm,
515 error_estimate,
516 jacobian_condition,
517 iterations,
518 evaluations,
519 termination,
520 status,
521 })
522}