use super::errors::{LeastSquaresError, LeastSquaresStage};
use super::problem::LeastSquaresProblem;
use super::trust_region::{determine_lambda_and_parameter_update_with_count, LMParameter};
use super::utils::{enorm, epsmch};
use crate::numerical::Nonlinear_systems::engine::{SolveStatistics, StatisticsAvailability};
use crate::somelinalg::least_squares_qr::{LinearLeastSquaresDiagonalProblem, PivotedQR};
use nalgebra::{DMatrix, DVector};
use num_traits::Float;
use std::time::Instant;
pub const MINPACK_COMPAT: bool = false;
#[derive(PartialEq, Eq, Debug)]
pub enum TerminationReason {
User(&'static str),
Numerical(&'static str),
ResidualsZero,
Orthogonal,
Converged { ftol: bool, xtol: bool },
NoImprovementPossible(&'static str),
LostPatience,
MaxIterationsReached { limit: usize },
NoParameters,
NoResiduals,
WrongDimensions(&'static str),
DomainViolation,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum LeastSquaresTelemetryMode {
#[default]
Off,
Counters,
Detailed,
}
impl LeastSquaresTelemetryMode {
#[inline]
fn collects_counters(self) -> bool {
!matches!(self, Self::Off)
}
#[inline]
fn collects_timings(self) -> bool {
matches!(self, Self::Detailed)
}
}
impl TerminationReason {
pub fn was_successful(&self) -> bool {
matches!(
self,
TerminationReason::ResidualsZero
| TerminationReason::Orthogonal
| TerminationReason::Converged { .. }
)
}
pub fn was_usage_issue(&self) -> bool {
matches!(
self,
TerminationReason::NoParameters
| TerminationReason::NoResiduals
| TerminationReason::NoImprovementPossible(_)
| TerminationReason::WrongDimensions(_)
)
}
}
#[derive(Debug)]
pub struct MinimizationReport {
pub termination: TerminationReason,
pub number_of_evaluations: usize,
pub objective_function: f64,
pub rejected_domain_trials: usize,
pub statistics: SolveStatistics,
pub error: Option<LeastSquaresError>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LevenbergMarquardt {
ftol: f64,
xtol: f64,
gtol: f64,
stepbound: f64,
patience: usize,
max_iterations: Option<usize>,
scale_diag: bool,
telemetry_mode: LeastSquaresTelemetryMode,
}
impl Default for LevenbergMarquardt {
fn default() -> Self {
Self::new()
}
}
impl LevenbergMarquardt {
pub fn new() -> Self {
if MINPACK_COMPAT {
let user_tol = 1.49012e-08;
Self {
ftol: user_tol,
xtol: user_tol,
gtol: 0.0,
stepbound: 100.0,
patience: 100,
max_iterations: None,
scale_diag: true,
telemetry_mode: LeastSquaresTelemetryMode::Off,
}
} else {
let user_tol = f64::EPSILON * 30.0;
Self {
ftol: user_tol,
xtol: user_tol,
gtol: user_tol,
stepbound: 100.0,
patience: 100,
max_iterations: None,
scale_diag: true,
telemetry_mode: LeastSquaresTelemetryMode::Off,
}
}
}
#[must_use]
pub fn with_ftol(self, ftol: f64) -> Self {
self.try_with_ftol(ftol)
.expect("ftol must be finite and >= 0")
}
pub fn try_with_ftol(self, ftol: f64) -> Result<Self, LeastSquaresError> {
if !ftol.is_finite() || ftol < 0.0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "ftol",
value: ftol,
});
}
Ok(Self { ftol, ..self })
}
#[must_use]
pub fn with_xtol(self, xtol: f64) -> Self {
self.try_with_xtol(xtol)
.expect("xtol must be finite and >= 0")
}
pub fn try_with_xtol(self, xtol: f64) -> Result<Self, LeastSquaresError> {
if !xtol.is_finite() || xtol < 0.0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "xtol",
value: xtol,
});
}
Ok(Self { xtol, ..self })
}
#[must_use]
pub fn with_gtol(self, gtol: f64) -> Self {
self.try_with_gtol(gtol)
.expect("gtol must be finite and >= 0")
}
pub fn try_with_gtol(self, gtol: f64) -> Result<Self, LeastSquaresError> {
if !gtol.is_finite() || gtol < 0.0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "gtol",
value: gtol,
});
}
Ok(Self { gtol, ..self })
}
#[must_use]
pub fn with_tol(self, tol: f64) -> Self {
self.try_with_tol(tol).expect("tol must be finite and > 0")
}
pub fn try_with_tol(self, tol: f64) -> Result<Self, LeastSquaresError> {
if !tol.is_finite() || tol <= 0.0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "tol",
value: tol,
});
}
Ok(Self {
ftol: tol,
xtol: tol,
gtol: 0.0,
..self
})
}
#[must_use]
pub fn with_stepbound(self, stepbound: f64) -> Self {
self.try_with_stepbound(stepbound)
.expect("stepbound must be finite and > 0")
}
pub fn try_with_stepbound(self, stepbound: f64) -> Result<Self, LeastSquaresError> {
if !stepbound.is_finite() || stepbound <= 0.0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "stepbound",
value: stepbound,
});
}
Ok(Self { stepbound, ..self })
}
#[must_use]
pub fn with_patience(self, patience: usize) -> Self {
self.try_with_patience(patience)
.expect("patience must be > 0")
}
#[must_use]
pub fn with_max_iterations(self, max_iterations: usize) -> Self {
self.try_with_max_iterations(max_iterations)
.expect("max_iterations must be > 0")
}
pub fn try_with_max_iterations(self, max_iterations: usize) -> Result<Self, LeastSquaresError> {
if max_iterations == 0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "max_iterations",
value: 0.0,
});
}
Ok(Self {
max_iterations: Some(max_iterations),
..self
})
}
pub fn try_with_patience(self, patience: usize) -> Result<Self, LeastSquaresError> {
if patience == 0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "patience",
value: 0.0,
});
}
Ok(Self { patience, ..self })
}
#[must_use]
pub fn with_scale_diag(self, scale_diag: bool) -> Self {
Self { scale_diag, ..self }
}
#[must_use]
pub fn with_telemetry(self, mode: LeastSquaresTelemetryMode) -> Self {
Self {
telemetry_mode: mode,
..self
}
}
pub fn try_minimize<O>(&self, target: O) -> Result<(O, MinimizationReport), LeastSquaresError>
where
O: LeastSquaresProblem,
{
for (field, value, valid) in [
("ftol", self.ftol, self.ftol.is_finite() && self.ftol >= 0.0),
("xtol", self.xtol, self.xtol.is_finite() && self.xtol >= 0.0),
("gtol", self.gtol, self.gtol.is_finite() && self.gtol >= 0.0),
(
"stepbound",
self.stepbound,
self.stepbound.is_finite() && self.stepbound > 0.0,
),
] {
if !valid {
return Err(LeastSquaresError::InvalidConfiguration { field, value });
}
}
if self.patience == 0 {
return Err(LeastSquaresError::InvalidConfiguration {
field: "patience",
value: 0.0,
});
}
if self.max_iterations == Some(0) {
return Err(LeastSquaresError::InvalidConfiguration {
field: "max_iterations",
value: 0.0,
});
}
let (target, report) = self.minimize(target);
if let Some(error) = report.error.clone() {
return Err(error);
}
match &report.termination {
TerminationReason::User("residuals") => Err(LeastSquaresError::CallbackFailed {
stage: LeastSquaresStage::Residual,
}),
TerminationReason::User("jacobian") => Err(LeastSquaresError::CallbackFailed {
stage: LeastSquaresStage::Jacobian,
}),
TerminationReason::Numerical("initial parameters") => {
Err(LeastSquaresError::NonFiniteValue {
stage: LeastSquaresStage::Parameters,
index: 0,
})
}
TerminationReason::Numerical(stage) => Err(LeastSquaresError::NumericalBreakdown {
stage: if stage.contains("residual") {
LeastSquaresStage::Residual
} else if *stage == "jacobian" {
LeastSquaresStage::Jacobian
} else if stage.contains("parameter") || stage.contains(" x") {
LeastSquaresStage::Parameters
} else {
LeastSquaresStage::TrustRegion
},
}),
TerminationReason::WrongDimensions(_) => Err(LeastSquaresError::InvalidProblemShape {
stage: LeastSquaresStage::TrustRegion,
}),
TerminationReason::NoParameters => Err(LeastSquaresError::EmptyProblem {
stage: LeastSquaresStage::Parameters,
}),
TerminationReason::NoResiduals => Err(LeastSquaresError::EmptyProblem {
stage: LeastSquaresStage::Residual,
}),
TerminationReason::DomainViolation => Err(LeastSquaresError::DomainViolation),
TerminationReason::LostPatience => Err(LeastSquaresError::MaxEvaluationsReached {
limit: report.number_of_evaluations,
}),
TerminationReason::MaxIterationsReached { limit } => {
Err(LeastSquaresError::MaxIterationsReached { limit: *limit })
}
_ => Ok((target, report)),
}
}
pub fn minimize<O>(&self, target: O) -> (O, MinimizationReport)
where
O: LeastSquaresProblem,
{
let (mut lm, mut residuals) = match LM::new(self, target) {
Err(report) => return report,
Ok(res) => res,
};
let n = lm.x.nrows();
let mut iteration = 0usize;
loop {
if let Some(limit) = lm.config.max_iterations {
if iteration >= limit {
return lm.into_report(TerminationReason::MaxIterationsReached { limit });
}
}
lm.statistics.iterations = iteration + 1;
let mut lls = {
let jacobian = match lm.jacobian() {
Err(reason) => return lm.into_report(reason),
Ok(jacobian) => jacobian,
};
if jacobian.ncols() != n || jacobian.nrows() != lm.m {
let (expected, actual) = if jacobian.nrows() != lm.m {
(lm.m, jacobian.nrows())
} else {
(n, jacobian.ncols())
};
lm.failure_error = Some(LeastSquaresError::DimensionMismatch {
stage: LeastSquaresStage::Jacobian,
expected,
actual,
});
return lm.into_report(TerminationReason::WrongDimensions("jacobian"));
}
let qr_started = lm.solve_started.map(|_| Instant::now());
let qr = PivotedQR::new(jacobian);
if lm.config.telemetry_mode.collects_counters() {
lm.statistics.linear_factorizations += 1;
}
if let Some(started) = qr_started {
let elapsed = started.elapsed();
lm.statistics.linear_factorization_duration += elapsed;
lm.statistics.linear_solve_duration += elapsed;
}
qr.into_least_squares_diagonal_problem(residuals)
};
if let Err(reason) = lm.update_diag(&mut lls) {
return lm.into_report(reason);
};
residuals = loop {
if lm.config.telemetry_mode.collects_counters() {
lm.statistics.trust_region_trials += 1;
}
let solve_started = lm.solve_started.map(|_| Instant::now());
let param = determine_lambda_and_parameter_update_with_count(
&mut lls,
&lm.diag,
lm.delta,
lm.lambda,
lm.config.telemetry_mode.collects_counters(),
);
if let Some(started) = solve_started {
let elapsed = started.elapsed();
lm.statistics.linear_solve_duration += elapsed;
lm.statistics.trust_region_subproblem_duration += elapsed;
}
if lm.config.telemetry_mode.collects_counters() {
lm.statistics.linear_solves += param.linear_solve_count;
}
let tr_iteration = lm.trust_region_iteration(&mut lls, param);
match tr_iteration {
Ok(Some(residuals)) => break residuals,
Err(reason) => return lm.into_report(reason),
Ok(None) => (),
}
};
iteration += 1;
}
}
}
struct LM<'a, O>
where
O: LeastSquaresProblem,
{
config: &'a LevenbergMarquardt,
x: DVector<f64>,
tmp: DVector<f64>,
target: O,
report: MinimizationReport,
statistics: SolveStatistics,
solve_started: Option<Instant>,
delta: f64,
lambda: f64,
xnorm: f64,
gnorm: f64,
residuals_norm: f64,
diag: DVector<f64>,
first_trust_region_iteration: bool,
first_update: bool,
max_fev: usize,
m: usize,
failure_error: Option<LeastSquaresError>,
}
impl<'a, O> LM<'a, O>
where
O: LeastSquaresProblem,
{
fn new(
config: &'a LevenbergMarquardt,
target: O,
) -> Result<(Self, DVector<f64>), (O, MinimizationReport)> {
let mut statistics = SolveStatistics::default();
statistics.availability = if config.telemetry_mode.collects_counters() {
StatisticsAvailability::Collected
} else {
StatisticsAvailability::NotCollected
};
statistics.timings_collected = config.telemetry_mode.collects_timings();
let solve_started = config.telemetry_mode.collects_timings().then(Instant::now);
let mut report = MinimizationReport {
termination: TerminationReason::ResidualsZero,
number_of_evaluations: 1,
objective_function: f64::NAN,
rejected_domain_trials: 0,
statistics: statistics.clone(),
error: None,
};
let x = target.params();
if x.iter().any(|value| !value.is_finite()) {
let index = x.iter().position(|value| !value.is_finite()).unwrap_or(0);
return Err((
target,
MinimizationReport {
termination: TerminationReason::Numerical("initial parameters"),
error: Some(LeastSquaresError::NonFiniteValue {
stage: LeastSquaresStage::Parameters,
index,
}),
..report
},
));
}
let residual_started = solve_started.map(|_| Instant::now());
let (residuals, residuals_norm) = match target.try_residuals() {
Ok(residuals) => {
if config.telemetry_mode.collects_counters() {
statistics.residual_evaluations += 1;
statistics.state_residual_evaluations += 1;
}
if let Some(started) = residual_started {
statistics.residual_duration += started.elapsed();
}
let norm = enorm(&residuals);
report.objective_function = norm * norm * 0.5;
(residuals, norm)
}
Err(error) => {
if config.telemetry_mode.collects_counters() {
statistics.residual_evaluations += 1;
statistics.state_residual_evaluations += 1;
}
if let Some(started) = residual_started {
statistics.residual_duration += started.elapsed();
}
return Err((
target,
MinimizationReport {
termination: TerminationReason::User("residuals"),
statistics,
error: Some(error),
..report
},
));
}
};
report.statistics = statistics.clone();
let n = x.nrows();
let diag = DVector::from_element(n, 1.0);
if diag.nrows() == 0 {
return Err((
target,
MinimizationReport {
termination: TerminationReason::NoParameters,
..report
},
));
}
let m: usize = residuals.nrows();
if m == 0 {
return Err((
target,
MinimizationReport {
termination: TerminationReason::NoResiduals,
..report
},
));
}
if !residuals_norm.is_finite() && !MINPACK_COMPAT {
return Err((
target,
MinimizationReport {
termination: TerminationReason::Numerical("residuals norm"),
error: Some(LeastSquaresError::NumericalBreakdown {
stage: LeastSquaresStage::Residual,
}),
..report
},
));
}
if residuals_norm <= f64::MIN_POSITIVE && !MINPACK_COMPAT {
return Err((target, report));
}
Ok((
Self {
config,
target,
report,
statistics,
solve_started,
tmp: x.clone(),
x,
diag,
delta: 0.0,
lambda: 0.0,
xnorm: 0.0,
gnorm: 0.0,
residuals_norm,
first_trust_region_iteration: true,
first_update: true,
max_fev: config.patience * (n + 1),
m,
failure_error: None,
},
residuals,
))
}
fn into_report(self, termination: TerminationReason) -> (O, MinimizationReport) {
let mut statistics = self.statistics;
if let Some(started) = self.solve_started {
statistics.total_duration = started.elapsed();
}
(
self.target,
MinimizationReport {
termination,
rejected_domain_trials: self.report.rejected_domain_trials,
statistics,
error: self.failure_error.or(self.report.error),
..self.report
},
)
}
fn jacobian(&mut self) -> Result<DMatrix<f64>, TerminationReason> {
let started = self.solve_started.map(|_| Instant::now());
let jacobian = self.target.try_jacobian();
if self.config.telemetry_mode.collects_counters() {
self.statistics.jacobian_evaluations += 1;
self.statistics.state_jacobian_evaluations += 1;
}
if let Some(started) = started {
self.statistics.jacobian_duration += started.elapsed();
}
match jacobian {
Ok(jacobian) if jacobian.iter().all(|value| value.is_finite()) => Ok(jacobian),
Ok(jacobian) => {
let index = jacobian
.iter()
.position(|value| !value.is_finite())
.unwrap_or(0);
self.failure_error = Some(LeastSquaresError::NonFiniteValue {
stage: LeastSquaresStage::Jacobian,
index,
});
Err(TerminationReason::Numerical("jacobian"))
}
Err(error) => {
self.failure_error = Some(error);
Err(TerminationReason::User("jacobian"))
}
}
}
fn trial_residuals(&mut self) -> Option<DVector<f64>> {
let started = self.solve_started.map(|_| Instant::now());
let residuals = self.target.try_residuals();
if self.config.telemetry_mode.collects_counters() {
self.statistics.residual_evaluations += 1;
self.statistics.trial_residual_evaluations += 1;
}
if let Some(started) = started {
self.statistics.residual_duration += started.elapsed();
}
match residuals {
Ok(values) => Some(values),
Err(error) => {
self.failure_error = Some(error);
None
}
}
}
fn update_diag(
&mut self,
lls: &mut LinearLeastSquaresDiagonalProblem,
) -> Result<(), TerminationReason> {
self.gnorm = match lls.max_a_t_b_scaled(self.residuals_norm) {
Some(max_at_b) => max_at_b,
None if !MINPACK_COMPAT => return Err(TerminationReason::Numerical("jacobian")),
None => 0.0,
};
if self.gnorm <= self.config.gtol {
return Err(TerminationReason::Orthogonal);
}
if self.first_update {
self.xnorm = if self.config.scale_diag {
for (d, col_norm) in self.diag.iter_mut().zip(lls.column_norms.iter()) {
*d = if *col_norm == 0.0 { 1.0 } else { *col_norm };
}
self.tmp.copy_from(&self.x);
self.tmp.component_mul_assign(&self.diag);
enorm(&self.tmp)
} else {
enorm(&self.x)
};
if !self.xnorm.is_finite() && !MINPACK_COMPAT {
return Err(TerminationReason::Numerical("subproblem x"));
}
self.delta = if self.xnorm == 0.0 {
self.config.stepbound
} else {
self.config.stepbound * self.xnorm
};
self.first_update = false;
} else if self.config.scale_diag {
for (d, norm) in self.diag.iter_mut().zip(lls.column_norms.iter()) {
*d = Float::max(*norm, *d);
}
}
Ok(())
}
fn trust_region_iteration(
&mut self,
lls: &mut LinearLeastSquaresDiagonalProblem,
param: LMParameter,
) -> Result<Option<DVector<f64>>, TerminationReason> {
const P1: f64 = 0.1;
const P0001: f64 = 1.0e-4;
self.lambda = param.lambda;
let pnorm = param.dp_norm;
if !pnorm.is_finite() && !MINPACK_COMPAT {
return Err(TerminationReason::Numerical("subproblem ||Dp||"));
}
let predicted_reduction;
let dir_der;
{
let temp1 = (lls.a_x_norm(¶m.step) / self.residuals_norm).powi(2);
if !temp1.is_finite() && !MINPACK_COMPAT {
return Err(TerminationReason::Numerical("trust-region reduction"));
}
let temp2 = ((self.lambda.sqrt() * pnorm) / self.residuals_norm).powi(2);
if !temp2.is_finite() && !MINPACK_COMPAT {
return Err(TerminationReason::Numerical("trust-region reduction"));
}
predicted_reduction = temp1 + temp2 / 0.5;
dir_der = -(temp1 + temp2);
}
if self.first_trust_region_iteration && pnorm < self.delta {
self.delta = pnorm;
}
self.first_trust_region_iteration = false;
self.tmp.copy_from(&self.x);
self.tmp -= ¶m.step;
if !self.target.validate_trial(&self.tmp) {
self.target.set_params(&self.x);
self.report.rejected_domain_trials += 1;
if self.config.telemetry_mode.collects_counters() {
self.statistics.domain_rejections += 1;
self.statistics.rejected_steps += 1;
}
let minimum_delta = epsmch::<f64>() * f64::max(self.xnorm, 1.0);
if self.delta <= minimum_delta
|| self.report.number_of_evaluations + self.report.rejected_domain_trials
>= self.max_fev
{
return Err(TerminationReason::DomainViolation);
}
self.delta *= 0.1;
self.lambda = if self.lambda == 0.0 {
1.0
} else {
self.lambda * 10.0
};
return Ok(None);
}
self.target.set_params(&self.tmp);
self.report.number_of_evaluations += 1;
let new_objective_function;
let (residuals, new_residuals_norm) = if let Some(residuals) = self.trial_residuals() {
if residuals.nrows() != self.m {
self.failure_error = Some(LeastSquaresError::DimensionMismatch {
stage: LeastSquaresStage::Residual,
expected: self.m,
actual: residuals.nrows(),
});
return Err(TerminationReason::WrongDimensions("residuals"));
}
let norm = enorm(&residuals);
if !norm.is_finite() {
self.target.set_params(&self.x);
self.report.rejected_domain_trials += 1;
if self.config.telemetry_mode.collects_counters() {
self.statistics.domain_rejections += 1;
self.statistics.rejected_steps += 1;
}
let minimum_delta = epsmch::<f64>() * f64::max(self.xnorm, 1.0);
if self.delta <= minimum_delta
|| self.report.number_of_evaluations + self.report.rejected_domain_trials
>= self.max_fev
{
return Err(TerminationReason::DomainViolation);
}
self.delta *= 0.1;
self.lambda = if self.lambda == 0.0 {
1.0
} else {
self.lambda * 10.0
};
return Ok(None);
}
new_objective_function = norm * norm * 0.5;
(residuals, norm)
} else {
return Err(TerminationReason::User("residuals"));
};
let actual_reduction = if new_residuals_norm * P1 < self.residuals_norm {
1.0 - (new_residuals_norm / self.residuals_norm).powi(2)
} else {
-1.0
};
let ratio = if predicted_reduction == 0.0 {
0.0
} else {
actual_reduction / predicted_reduction
};
let half = 0.5;
if ratio <= 0.25 {
let mut temp = if !actual_reduction.is_sign_negative() {
half
} else {
half * dir_der / (dir_der + half * actual_reduction)
};
if new_residuals_norm * P1 >= self.residuals_norm || temp < P1 {
temp = P1;
};
self.delta = temp * f64::min(self.delta, pnorm * 10.0);
self.lambda /= temp;
} else if self.lambda == 0.0 || ratio >= 0.75 {
self.delta = pnorm / 0.5;
self.lambda *= half;
}
let update_considered_good = ratio >= P0001;
if self.config.telemetry_mode.collects_counters() {
if update_considered_good {
self.statistics.accepted_steps += 1;
} else {
self.statistics.rejected_steps += 1;
}
}
if update_considered_good {
core::mem::swap(&mut self.x, &mut self.tmp);
self.xnorm = if self.config.scale_diag {
self.tmp.copy_from(&self.x);
self.tmp.component_mul_assign(&self.diag);
enorm(&self.tmp)
} else {
enorm(&self.x)
};
if !self.xnorm.is_finite() && !MINPACK_COMPAT {
return Err(TerminationReason::Numerical("new x"));
}
self.residuals_norm = new_residuals_norm;
self.report.objective_function = new_objective_function;
}
if !MINPACK_COMPAT && self.residuals_norm <= f64::MIN_POSITIVE {
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::ResidualsZero);
}
let ftol_check = f64::abs(actual_reduction) <= self.config.ftol
&& predicted_reduction <= self.config.ftol
&& ratio * 0.5 <= 1.0;
let xtol_check = self.delta <= self.config.xtol * self.xnorm;
if ftol_check || xtol_check {
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::Converged {
ftol: ftol_check,
xtol: xtol_check,
});
}
if self.report.number_of_evaluations >= self.max_fev {
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::LostPatience);
}
if f64::abs(actual_reduction) <= epsmch()
&& predicted_reduction <= epsmch()
&& ratio * 0.5 <= 1.0
{
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::NoImprovementPossible("ftol"));
}
if self.delta <= epsmch::<f64>() * self.xnorm {
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::NoImprovementPossible("xtol"));
}
if self.gnorm <= epsmch() {
self.reset_params_if(!update_considered_good);
return Err(TerminationReason::NoImprovementPossible("gtol"));
}
if update_considered_good {
Ok(Some(residuals))
} else {
Ok(None)
}
}
#[inline]
fn reset_params_if(&mut self, reset: bool) {
if reset {
self.target.set_params(&self.x);
}
}
}