Skip to main content

levenberg_marquardt_sparse/
lm.rs

1use crate::utils::enorm;
2use crate::{LeastSquaresProblem, SparseJacobian};
3use nalgebra::{DefaultAllocator, Dim, OVector, RealField, Vector, allocator::Allocator, convert};
4use num_traits::Float;
5#[cfg(feature = "tracing")]
6use tracing::debug;
7
8#[cfg(test)]
9#[allow(
10    clippy::float_cmp,
11    clippy::excessive_precision,
12    clippy::redundant_clone
13)]
14pub(crate) mod test_examples;
15#[cfg(test)]
16mod test_helpers;
17#[cfg(test)]
18mod test_init_step;
19
20#[derive(PartialEq, Eq, Debug)]
21/// Reasons for terminating the minimization.
22pub enum TerminationReason {
23    /// The residual or Jacobian computation was not successful, it returned `None`.
24    User(&'static str),
25    /// Encountered `NaN` or `$\pm\infty$`.
26    Numerical(&'static str),
27    /// The residuals are literally zero.
28    ResidualsZero,
29    /// The residuals vector and the Jacobian columns are almost orthogonal.
30    ///
31    /// This is the `gtol` termination criterion.
32    Orthogonal,
33    /// The `ftol` or `xtol` criterion was fulfilled.
34    Converged { ftol: bool, xtol: bool },
35    /// The bound for `ftol`, `xtol` or `gtol` was set so low that the
36    /// test passed with the machine epsilon but not with the actual
37    /// bound. This means you must increase the bound.
38    NoImprovementPossible(&'static str),
39    /// Maximum number of function evaluations was hit.
40    LostPatience,
41    /// The number of parameters `$n$` is zero.
42    NoParameters,
43    /// The number of residuals `$m$` is zero.
44    NoResiduals,
45    /// The shape of the computed residuals or Jacobian is not correct.
46    WrongDimensions(&'static str),
47}
48
49impl TerminationReason {
50    /// Compute whether the outcome is considered successful.
51    ///
52    /// This does not necessarily mean we have a minimizer.
53    /// Some termination criteria are approximations for necessary
54    /// optimality conditions or check limitations due to
55    /// floating point arithmetic.
56    pub fn was_successful(&self) -> bool {
57        matches!(
58            self,
59            TerminationReason::ResidualsZero
60                | TerminationReason::Orthogonal
61                | TerminationReason::Converged { .. }
62        )
63    }
64
65    /// A fundamental assumptions was not met.
66    ///
67    /// For example if the number of residuals changed.
68    pub fn was_usage_issue(&self) -> bool {
69        matches!(
70            self,
71            TerminationReason::NoParameters
72                | TerminationReason::NoResiduals
73                | TerminationReason::NoImprovementPossible(_)
74                | TerminationReason::WrongDimensions(_)
75        )
76    }
77}
78
79#[derive(Debug)]
80/// Information about the minimization.
81///
82/// Use this to inspect the minimization process. Most importantly
83/// you may want to check if there was a failure.
84pub struct MinimizationReport<F: RealField> {
85    pub termination: TerminationReason,
86    /// Number of residuals which were computed.
87    pub number_of_evaluations: usize,
88    /// Contains the value of `$f(\vec{x})$`.
89    pub objective_function: F,
90}
91
92/// Levenberg-Marquardt optimization algorithm.
93///
94/// See the [module documentation](index.html) for a usage example.
95///
96/// The runtime and termination behavior can be controlled by various hyperparameters.
97#[derive(Copy, Clone, Debug, PartialEq, Eq)]
98pub struct LevenbergMarquardt<F> {
99    ftol: F,
100    xtol: F,
101    gtol: F,
102    stepbound: F,
103    patience: usize,
104    scale_diag: bool,
105}
106
107impl<F: RealField + Float> Default for LevenbergMarquardt<F> {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl<F: RealField + Float> LevenbergMarquardt<F> {
114    pub fn new() -> Self {
115        let user_tol = F::default_epsilon() * convert(30.0);
116        Self {
117            ftol: user_tol,
118            xtol: user_tol,
119            gtol: user_tol,
120            stepbound: convert(100.0),
121            patience: 100,
122            scale_diag: true,
123        }
124    }
125
126    /// Set the relative error desired in the objective function `$f$`.
127    ///
128    /// Termination occurs when both the actual and
129    /// predicted relative reductions for `$f$` are at most `ftol`.
130    ///
131    /// # Panics
132    ///
133    /// Panics if `$\mathtt{ftol} < 0$`.
134    #[must_use]
135    pub fn with_ftol(self, ftol: F) -> Self {
136        assert!(!ftol.is_negative(), "ftol must be >= 0");
137        Self { ftol, ..self }
138    }
139
140    /// Set relative error between last two approximations.
141    ///
142    /// Termination occurs when the relative error between
143    /// two consecutive iterates is at most `xtol`.
144    ///
145    /// # Panics
146    ///
147    /// Panics if `$\mathtt{xtol} < 0$`.
148    #[must_use]
149    pub fn with_xtol(self, xtol: F) -> Self {
150        assert!(!xtol.is_negative(), "xtol must be >= 0");
151        Self { xtol, ..self }
152    }
153
154    /// Set orthogonality desired between the residual vector and its derivative.
155    ///
156    /// Termination occurs when the cosine of the angle
157    /// between the residual vector `$\vec{r}$` and any column of the Jacobian `$\mathbf{J}$` is at
158    /// most `gtol` in absolute value.
159    ///
160    /// With other words, the algorithm will terminate if
161    /// ```math
162    ///   \cos\bigl(\sphericalangle (\mathbf{J}\vec{e}_i, \vec{r})\bigr) =
163    ///   \frac{|(\mathbf{J}^\top \vec{r})_i|}{\|\mathbf{J}\vec{e}_i\|\|\vec{r}\|} \leq \texttt{gtol}
164    ///   \quad\text{for all }i=1,\ldots,n.
165    /// ```
166    ///
167    /// This is based on the fact that those vectors are orthogonal near the optimum (gradient is zero).
168    /// The angle check is scale invariant, whereas checking that
169    /// `$\nabla f(\vec{x})\approx \vec{0}$` is not.
170    ///
171    /// # Panics
172    ///
173    /// Panics if `$\mathtt{gtol} < 0$`.
174    #[must_use]
175    pub fn with_gtol(self, gtol: F) -> Self {
176        assert!(!gtol.is_negative(), "gtol must be >= 0");
177        Self { gtol, ..self }
178    }
179
180    /// Shortcut to set `tol` as in MINPACK `LMDER1`.
181    ///
182    /// Sets `ftol = xtol = tol` and `gtol = 0`.
183    ///
184    /// # Panics
185    ///
186    /// Panics if `$\mathtt{tol} \leq 0$`.
187    #[must_use]
188    pub fn with_tol(self, tol: F) -> Self {
189        assert!(tol.is_positive(), "tol must > 0");
190        Self {
191            ftol: tol,
192            xtol: tol,
193            gtol: F::zero(),
194            ..self
195        }
196    }
197
198    /// Set factor for the initial step bound.
199    ///
200    /// This bound is set to `$\mathtt{stepbound}\cdot\|\mathbf{D}\vec{x}\|$`
201    /// if nonzero, or else to `stepbound` itself. In most cases `stepbound` should lie
202    /// in the interval `$[0.1,100]$`.
203    ///
204    /// # Panics
205    ///
206    /// Panics if `$\mathtt{stepbound} \leq 0$`.
207    #[must_use]
208    pub fn with_stepbound(self, stepbound: F) -> Self {
209        assert!(stepbound.is_positive(), "stepbound must be > 0");
210        Self { stepbound, ..self }
211    }
212
213    /// Set factor for the maximal number of function evaluations.
214    ///
215    /// The maximal number of function evaluations is set to
216    /// `$\texttt{patience}\cdot(n + 1)$`.
217    ///
218    /// # Panics
219    ///
220    /// Panics if `$\mathtt{patience} \leq 0$`.
221    #[must_use]
222    pub fn with_patience(self, patience: usize) -> Self {
223        assert!(patience > 0, "patience must be > 0");
224        Self { patience, ..self }
225    }
226
227    /// Enable or disable whether the variables will be rescaled internally.
228    #[must_use]
229    pub fn with_scale_diag(self, scale_diag: bool) -> Self {
230        Self { scale_diag, ..self }
231    }
232
233    /// Try to solve the given least squares problem.
234    ///
235    /// The parameters of the problem which are set when this function is called
236    /// are used as the initial guess for `$\vec{x}$`.
237    pub fn minimize<N, M, O>(&self, target: O) -> (O, MinimizationReport<F>)
238    where
239        N: Dim,
240        M: Dim,
241        O: LeastSquaresProblem<F, M, N>,
242        DefaultAllocator: Allocator<N>,
243    {
244        let (mut lm, mut residuals) = match LM::new(self, target) {
245            Err(report) => return report,
246            Ok(res) => res,
247        };
248        let n = lm.x.nrows();
249        let mut lambda = Float::max(F::default_epsilon(), convert(1.0e-6f64));
250        let mut exhausted_outer_loops = 0usize;
251        let exhausted_outer_limit = 8usize;
252        let mut stagnation_count = 0usize;
253        let stagnation_limit = 12usize;
254
255        loop {
256            let jacobian = match lm.jacobian() {
257                Err(reason) => return lm.into_report(reason),
258                Ok(jacobian) => jacobian,
259            };
260            if jacobian.cols != n || jacobian.rows != lm.m {
261                return lm.into_report(TerminationReason::WrongDimensions("jacobian"));
262            }
263
264            let col_norms = sparse_column_norms::<F, N>(&jacobian, n);
265            let jt_r = sparse_jt_mul::<F, N>(&jacobian, residuals.as_slice(), n);
266
267            if lm.first_update {
268                lm.xnorm = if lm.config.scale_diag {
269                    for (d, col_norm) in lm.diag.iter_mut().zip(col_norms.iter()) {
270                        *d = if col_norm.is_zero() {
271                            F::one()
272                        } else {
273                            *col_norm
274                        };
275                    }
276                    lm.tmp.cmpy(F::one(), &lm.diag, &lm.x, F::zero());
277                    enorm(&lm.tmp)
278                } else {
279                    enorm(&lm.x)
280                };
281
282                lm.delta = if lm.xnorm.is_zero() {
283                    lm.config.stepbound
284                } else {
285                    lm.config.stepbound * lm.xnorm
286                };
287                lm.first_update = false;
288            } else if lm.config.scale_diag {
289                for (d, norm) in lm.diag.iter_mut().zip(col_norms.iter()) {
290                    *d = Float::max(*norm, *d);
291                }
292            }
293
294            lm.gnorm = max_scaled_gradient(&jt_r, &col_norms, lm.residuals_norm);
295            if lm.gnorm <= lm.config.gtol {
296                return lm.into_report(TerminationReason::Orthogonal);
297            }
298
299            #[cfg(feature = "tracing")]
300            debug!(
301                evals = lm.report.number_of_evaluations,
302                obj = format_args!("{:?}", lm.report.objective_function),
303                gnorm = format_args!("{:?}", lm.gnorm),
304                lambda = format_args!("{:?}", lambda),
305                "sparse outer iteration start"
306            );
307
308            let mut accepted = false;
309            let mut accepted_residuals = None;
310            let max_inner = 12usize;
311            #[cfg(feature = "tracing")]
312            let mut inner_tries = 0usize;
313
314            for _ in 0..max_inner {
315                #[cfg(feature = "tracing")]
316                {
317                    inner_tries += 1;
318                }
319                let step = solve_damped_normal_equations::<F, N>(
320                    &jacobian, &jt_r, &lm.diag, &col_norms, lambda, n,
321                );
322
323                let pnorm = scaled_norm(&step, &lm.diag);
324                if !pnorm.is_finite() {
325                    return lm.into_report(TerminationReason::Numerical("subproblem ||Dp||"));
326                }
327
328                lm.tmp.copy_from(&lm.x);
329                lm.tmp.axpy(-F::one(), &step, F::one());
330
331                lm.target.set_params(&lm.tmp);
332                lm.report.number_of_evaluations += 1;
333
334                let new_objective_function;
335                let (new_residuals, new_residuals_norm) = if let Some(res) = lm.target.residuals() {
336                    if res.nrows() != lm.m {
337                        return lm.into_report(TerminationReason::WrongDimensions("residuals"));
338                    }
339                    let norm = enorm(&res);
340                    new_objective_function = norm * norm * convert(0.5);
341                    (res, norm)
342                } else {
343                    return lm.into_report(TerminationReason::User("residuals"));
344                };
345
346                let actual_reduction = if new_residuals_norm * convert(0.1f64) < lm.residuals_norm {
347                    F::one() - Float::powi(new_residuals_norm / lm.residuals_norm, 2)
348                } else {
349                    -F::one()
350                };
351
352                let j_step = sparse_j_mul::<F, N>(&jacobian, &step);
353                // Stable predicted reduction: (2 r·Jp - ||Jp||²) / ||r||²
354                // Avoids catastrophic cancellation in 1 - ||r - Jp||² / ||r||²
355                let predicted_reduction = {
356                    let rn_sq = lm.residuals_norm * lm.residuals_norm;
357                    if rn_sq.is_zero() {
358                        F::zero()
359                    } else {
360                        let r_dot_jp = residuals
361                            .as_slice()
362                            .iter()
363                            .zip(j_step.iter())
364                            .fold(F::zero(), |acc, (&a, &b)| acc + a * b);
365                        let jp_sq = j_step.iter().fold(F::zero(), |acc, &v| acc + v * v);
366                        (convert::<f64, F>(2.0) * r_dot_jp - jp_sq) / rn_sq
367                    }
368                };
369                let ratio = if predicted_reduction <= F::zero() {
370                    F::zero()
371                } else {
372                    actual_reduction / predicted_reduction
373                };
374
375                if ratio > convert(0.0001f64) {
376                    accepted = true;
377                    accepted_residuals = Some((
378                        new_residuals,
379                        new_residuals_norm,
380                        new_objective_function,
381                        step,
382                        pnorm,
383                        actual_reduction,
384                        predicted_reduction,
385                        ratio,
386                    ));
387                    #[cfg(feature = "tracing")]
388                    debug!(
389                        evals = lm.report.number_of_evaluations,
390                        inner_tries,
391                        obj = format_args!("{:?}", lm.report.objective_function),
392                        "sparse step accepted"
393                    );
394                    lambda = Float::max(lambda * convert(0.333333333333f64), F::default_epsilon());
395                    break;
396                }
397
398                #[cfg(feature = "tracing")]
399                debug!(
400                    evals = lm.report.number_of_evaluations,
401                    inner_try = inner_tries,
402                    ratio = format_args!("{:?}", ratio),
403                    "sparse step rejected, increasing lambda"
404                );
405                lambda *= convert(2.0f64);
406                lm.target.set_params(&lm.x);
407            }
408
409            if !accepted {
410                // All inner retries failed; lambda is now very large.
411                // Continue to the next outer iteration — the recomputed Jacobian
412                // at the same point with the elevated lambda will produce a tiny,
413                // conservative step that is almost certainly acceptable.
414                exhausted_outer_loops += 1;
415                #[cfg(feature = "tracing")]
416                debug!(
417                    evals = lm.report.number_of_evaluations,
418                    exhausted_outer_loops,
419                    max_inner,
420                    "all inner tries exhausted, retrying outer iteration with larger lambda"
421                );
422                if lm.report.number_of_evaluations >= lm.max_fev {
423                    return lm.into_report(TerminationReason::LostPatience);
424                }
425                if exhausted_outer_loops >= exhausted_outer_limit {
426                    // We repeatedly failed to find an acceptable step while staying
427                    // at the same parameters/objective value. Treat this as practical
428                    // convergence (stagnation) instead of burning evaluations.
429                    return lm.into_report(TerminationReason::Converged {
430                        ftol: true,
431                        xtol: true,
432                    });
433                }
434                continue;
435            }
436
437            exhausted_outer_loops = 0;
438
439            let prev_objective = lm.report.objective_function;
440
441            let (
442                new_residuals,
443                new_residuals_norm,
444                new_objective_function,
445                _step,
446                pnorm,
447                actual_reduction,
448                predicted_reduction,
449                ratio,
450            ) = accepted_residuals.expect("accepted step must exist");
451
452            core::mem::swap(&mut lm.x, &mut lm.tmp);
453            lm.residuals_norm = new_residuals_norm;
454            lm.report.objective_function = new_objective_function;
455            residuals = new_residuals;
456
457            let objective_scale = Float::max(prev_objective, F::one());
458            let rel_obj_change =
459                Float::abs(prev_objective - new_objective_function) / objective_scale;
460            let stagnation_tol = Float::max(
461                lm.config.ftol,
462                Float::sqrt(Float::max(F::default_epsilon(), convert(1.0e-16f64))),
463            );
464            if rel_obj_change <= stagnation_tol {
465                stagnation_count += 1;
466            } else {
467                stagnation_count = 0;
468            }
469            if stagnation_count >= stagnation_limit {
470                return lm.into_report(TerminationReason::Converged {
471                    ftol: true,
472                    xtol: false,
473                });
474            }
475
476            if lm.config.scale_diag {
477                lm.tmp.cmpy(F::one(), &lm.diag, &lm.x, F::zero());
478                lm.xnorm = enorm(&lm.tmp);
479            } else {
480                lm.xnorm = enorm(&lm.x);
481            }
482
483            if lm.residuals_norm <= F::min_positive_value() {
484                return lm.into_report(TerminationReason::ResidualsZero);
485            }
486
487            let xtol_check = lm.delta <= lm.config.xtol * lm.xnorm
488                || pnorm <= lm.config.xtol * (lm.xnorm + lm.config.xtol);
489            // MINPACK-style relative reduction test for ftol.
490            // This lets us declare convergence even when residuals are non-zero
491            // (e.g., noisy data) once further progress becomes negligible.
492            let ftol_check = Float::abs(actual_reduction) <= lm.config.ftol
493                && predicted_reduction <= lm.config.ftol
494                && convert::<f64, F>(0.5) * ratio <= F::one();
495            if ftol_check || xtol_check {
496                return lm.into_report(TerminationReason::Converged {
497                    ftol: ftol_check,
498                    xtol: xtol_check,
499                });
500            }
501
502            if lm.report.number_of_evaluations >= lm.max_fev {
503                return lm.into_report(TerminationReason::LostPatience);
504            }
505        }
506    }
507}
508
509fn sparse_column_norms<F, N>(jacobian: &SparseJacobian<F>, n: usize) -> OVector<F, N>
510where
511    F: RealField + Float + Copy,
512    N: Dim,
513    DefaultAllocator: Allocator<N>,
514{
515    let mut out = OVector::<F, N>::zeros_generic(Dim::from_usize(n), Dim::from_usize(1));
516    for &(_, j, v) in jacobian.entries.iter() {
517        if j < n {
518            out[j] += v * v;
519        }
520    }
521    out.apply(|x| *x = Float::sqrt(*x));
522    out
523}
524
525fn sparse_jt_mul<F, N>(jacobian: &SparseJacobian<F>, y: &[F], n: usize) -> OVector<F, N>
526where
527    F: RealField + Float + Copy,
528    N: Dim,
529    DefaultAllocator: Allocator<N>,
530{
531    let mut out = OVector::<F, N>::zeros_generic(Dim::from_usize(n), Dim::from_usize(1));
532    for &(i, j, v) in jacobian.entries.iter() {
533        if i < y.len() && j < n {
534            out[j] += v * y[i];
535        }
536    }
537    out
538}
539
540fn sparse_j_mul<F, N>(jacobian: &SparseJacobian<F>, x: &OVector<F, N>) -> alloc::vec::Vec<F>
541where
542    F: RealField + Float + Copy,
543    N: Dim,
544    DefaultAllocator: Allocator<N>,
545{
546    let mut out = alloc::vec![F::zero(); jacobian.rows];
547    for &(i, j, v) in jacobian.entries.iter() {
548        if i < out.len() && j < x.nrows() {
549            out[i] += v * x[j];
550        }
551    }
552    out
553}
554
555fn max_scaled_gradient<F, N>(jt_r: &OVector<F, N>, col_norms: &OVector<F, N>, rnorm: F) -> F
556where
557    F: RealField + Float + Copy,
558    N: Dim,
559    DefaultAllocator: Allocator<N>,
560{
561    let mut out = F::zero();
562    if rnorm.is_zero() {
563        return out;
564    }
565    for i in 0..jt_r.nrows() {
566        let denom = col_norms[i] * rnorm;
567        if denom.is_positive() {
568            out = Float::max(out, Float::abs(jt_r[i] / denom));
569        }
570    }
571    out
572}
573
574fn scaled_norm<F, N>(x: &OVector<F, N>, diag: &OVector<F, N>) -> F
575where
576    F: RealField + Float + Copy,
577    N: Dim,
578    DefaultAllocator: Allocator<N>,
579{
580    let mut s = F::zero();
581    for i in 0..x.nrows() {
582        let v = x[i] * diag[i];
583        s += v * v;
584    }
585    Float::sqrt(s)
586}
587
588fn solve_damped_normal_equations<F, N>(
589    jacobian: &SparseJacobian<F>,
590    jt_r: &OVector<F, N>,
591    diag: &OVector<F, N>,
592    col_norms: &OVector<F, N>,
593    lambda: F,
594    n: usize,
595) -> OVector<F, N>
596where
597    F: RealField + Float + Copy,
598    N: Dim,
599    DefaultAllocator: Allocator<N>,
600{
601    // Jacobi preconditioner: M_inv[i] = 1 / (col_norms[i]^2 + lambda * diag[i]^2)
602    // col_norms[i]^2 = diag(J^T J)[i], so M approximates the diagonal of the system matrix.
603    let mut m_inv = OVector::<F, N>::zeros_generic(Dim::from_usize(n), Dim::from_usize(1));
604    for i in 0..n {
605        let d = col_norms[i] * col_norms[i] + lambda * diag[i] * diag[i];
606        m_inv[i] = if d > F::default_epsilon() {
607            F::one() / d
608        } else {
609            F::one()
610        };
611    }
612
613    let mut x = OVector::<F, N>::zeros_generic(Dim::from_usize(n), Dim::from_usize(1));
614    let mut r = jt_r.clone_owned();
615
616    // z = M^{-1} r
617    let mut z = OVector::<F, N>::zeros_generic(Dim::from_usize(n), Dim::from_usize(1));
618    for i in 0..n {
619        z[i] = r[i] * m_inv[i];
620    }
621
622    let mut p = z.clone_owned();
623    let bz0 = Float::max(jt_r.dot(&z), F::default_epsilon()); // ||b||^2 in M^{-1} norm
624    let tol_sq = Float::powi(
625        Float::max(convert(1.0e-10f64), F::default_epsilon() * convert(10.0f64)),
626        2,
627    );
628    let mut rz_old = r.dot(&z); // r^T M^{-1} r
629
630    let max_iter = 2 * n + 20;
631    for _ in 0..max_iter {
632        if rz_old <= tol_sq * bz0 {
633            break;
634        }
635
636        let ap = sparse_normal_op_mul(jacobian, &p, diag, lambda, n);
637        let denom = p.dot(&ap);
638        if !denom.is_finite() || Float::abs(denom) <= F::default_epsilon() {
639            break;
640        }
641        let alpha = rz_old / denom;
642
643        x.axpy(alpha, &p, F::one());
644        r.axpy(-alpha, &ap, F::one());
645        for i in 0..n {
646            z[i] = r[i] * m_inv[i];
647        }
648
649        let rz_new = r.dot(&z);
650        if rz_new <= tol_sq * bz0 {
651            break;
652        }
653        let beta = rz_new / rz_old;
654        p *= beta;
655        p += &z;
656        rz_old = rz_new;
657    }
658
659    x
660}
661
662fn sparse_normal_op_mul<F, N>(
663    jacobian: &SparseJacobian<F>,
664    x: &OVector<F, N>,
665    diag: &OVector<F, N>,
666    lambda: F,
667    n: usize,
668) -> OVector<F, N>
669where
670    F: RealField + Float + Copy,
671    N: Dim,
672    DefaultAllocator: Allocator<N>,
673{
674    let jx = sparse_j_mul(jacobian, x);
675    let mut out = sparse_jt_mul(jacobian, jx.as_slice(), n);
676    if !lambda.is_zero() {
677        for i in 0..out.nrows() {
678            out[i] += lambda * diag[i] * diag[i] * x[i];
679        }
680    }
681    out
682}
683
684/// Struct which holds the state of the LM algorithm and which implements its individual steps.
685struct LM<'a, F, N, M, O>
686where
687    F: RealField + Copy,
688    N: Dim,
689    M: Dim,
690    O: LeastSquaresProblem<F, M, N>,
691    DefaultAllocator: Allocator<N>,
692{
693    config: &'a LevenbergMarquardt<F>,
694    /// Current parameters `$\vec{x}$`
695    x: Vector<F, N, O::ParameterStorage>,
696    tmp: Vector<F, N, O::ParameterStorage>,
697    /// The implementation of `LeastSquaresProblem`
698    target: O,
699    /// Statistics and termination reasons, used for return value
700    report: MinimizationReport<F>,
701    /// The delta from the trust-region algorithm
702    delta: F,
703    /// `$\|\mathbf{D}\vec{x}\|`
704    xnorm: F,
705    gnorm: F,
706    residuals_norm: F,
707    /// The diagonal of `$\mathbf{D}$`
708    diag: OVector<F, N>,
709    /// Flag to check if it is the first diagonal update
710    first_update: bool,
711    max_fev: usize,
712    m: usize,
713}
714
715impl<'a, F, N, M, O> LM<'a, F, N, M, O>
716where
717    F: RealField + Float + Copy,
718    N: Dim,
719    M: Dim,
720    O: LeastSquaresProblem<F, M, N>,
721    DefaultAllocator: Allocator<N>,
722{
723    #[allow(clippy::type_complexity)]
724    fn new(
725        config: &'a LevenbergMarquardt<F>,
726        target: O,
727    ) -> Result<(Self, Vector<F, M, O::ResidualStorage>), (O, MinimizationReport<F>)> {
728        let mut report = MinimizationReport {
729            termination: TerminationReason::ResidualsZero,
730            number_of_evaluations: 1,
731            objective_function: <F as Float>::nan(),
732        };
733
734        // Evaluate at start point
735        let x = target.params();
736        let (residuals, residuals_norm) = if let Some(residuals) = target.residuals() {
737            let norm = enorm(&residuals);
738            report.objective_function = norm * norm * convert(0.5);
739            (residuals, norm)
740        } else {
741            return Err((
742                target,
743                MinimizationReport {
744                    termination: TerminationReason::User("residuals"),
745                    ..report
746                },
747            ));
748        };
749
750        // Initialize diagonal
751        let n = x.shape_generic().0;
752        let diag = OVector::<F, N>::from_element_generic(n, Dim::from_usize(1), F::one());
753        // Check n > 0
754        if diag.nrows() == 0 {
755            return Err((
756                target,
757                MinimizationReport {
758                    termination: TerminationReason::NoParameters,
759                    ..report
760                },
761            ));
762        }
763
764        let m = residuals.nrows();
765        if m == 0 {
766            return Err((
767                target,
768                MinimizationReport {
769                    termination: TerminationReason::NoResiduals,
770                    ..report
771                },
772            ));
773        }
774
775        if !residuals_norm.is_finite() {
776            return Err((
777                target,
778                MinimizationReport {
779                    termination: TerminationReason::Numerical("residuals norm"),
780                    ..report
781                },
782            ));
783        }
784
785        if residuals_norm <= Float::min_positive_value() {
786            // Already zero, nothing to do
787            return Err((target, report));
788        }
789
790        Ok((
791            Self {
792                config,
793                target,
794                report,
795                tmp: x.clone(),
796                x,
797                diag,
798                delta: F::zero(),
799                xnorm: F::zero(),
800                gnorm: F::zero(),
801                residuals_norm,
802                first_update: true,
803                max_fev: config.patience * (n.value() + 1),
804                m,
805            },
806            residuals,
807        ))
808    }
809
810    fn into_report(self, termination: TerminationReason) -> (O, MinimizationReport<F>) {
811        (
812            self.target,
813            MinimizationReport {
814                termination,
815                ..self.report
816            },
817        )
818    }
819
820    fn jacobian(&self) -> Result<SparseJacobian<F>, TerminationReason> {
821        match self.target.jacobian() {
822            Some(jacobian) => Ok(jacobian),
823            None => Err(TerminationReason::User("jacobian")),
824        }
825    }
826}