use std::fmt;
use crate::errors::QlResult;
use crate::require;
use crate::types::{Real, Size};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EndCriteriaType {
None,
MaxIterations,
StationaryPoint,
StationaryFunctionValue,
StationaryFunctionAccuracy,
ZeroGradientNorm,
FunctionEpsilonTooSmall,
Unknown,
}
impl EndCriteriaType {
pub fn succeeded(self) -> bool {
matches!(
self,
EndCriteriaType::StationaryPoint
| EndCriteriaType::StationaryFunctionValue
| EndCriteriaType::StationaryFunctionAccuracy
)
}
}
impl fmt::Display for EndCriteriaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
EndCriteriaType::None => "None",
EndCriteriaType::MaxIterations => "MaxIterations",
EndCriteriaType::StationaryPoint => "StationaryPoint",
EndCriteriaType::StationaryFunctionValue => "StationaryFunctionValue",
EndCriteriaType::StationaryFunctionAccuracy => "StationaryFunctionAccuracy",
EndCriteriaType::ZeroGradientNorm => "ZeroGradientNorm",
EndCriteriaType::FunctionEpsilonTooSmall => "FunctionEpsilonTooSmall",
EndCriteriaType::Unknown => "Unknown",
})
}
}
#[derive(Clone, Copy, Debug)]
pub struct EndCriteria {
max_iterations: Size,
max_stationary_state_iterations: Size,
root_epsilon: Real,
function_epsilon: Real,
gradient_norm_epsilon: Real,
}
impl EndCriteria {
pub fn new(
max_iterations: Size,
max_stationary_state_iterations: Option<Size>,
root_epsilon: Real,
function_epsilon: Real,
gradient_norm_epsilon: Option<Real>,
) -> QlResult<Self> {
let max_stationary_state_iterations =
max_stationary_state_iterations.unwrap_or_else(|| (max_iterations / 2).min(100));
require!(
max_stationary_state_iterations > 1,
"maxStationaryStateIterations ({max_stationary_state_iterations}) must be greater than one"
);
require!(
max_stationary_state_iterations < max_iterations,
"maxStationaryStateIterations ({max_stationary_state_iterations}) must be less than maxIterations ({max_iterations})"
);
for (name, epsilon) in [
("rootEpsilon", Some(root_epsilon)),
("functionEpsilon", Some(function_epsilon)),
("gradientNormEpsilon", gradient_norm_epsilon),
] {
if let Some(epsilon) = epsilon
&& (!epsilon.is_finite() || epsilon < 0.0)
{
crate::fail!("{name} ({epsilon}) must be finite and non-negative");
}
}
Ok(EndCriteria {
max_iterations,
max_stationary_state_iterations,
root_epsilon,
function_epsilon,
gradient_norm_epsilon: gradient_norm_epsilon.unwrap_or(function_epsilon),
})
}
#[allow(clippy::too_many_arguments)]
pub fn check(
&self,
iteration: Size,
stat_state_iterations: &mut Size,
positive_optimization: bool,
fold: Real,
_normgold: Real,
fnew: Real,
normgnew: Real,
ec_type: &mut EndCriteriaType,
) -> bool {
self.check_max_iterations(iteration, ec_type)
|| self.check_stationary_function_value(fold, fnew, stat_state_iterations, ec_type)
|| self.check_stationary_function_accuracy(fnew, positive_optimization, ec_type)
|| self.check_zero_gradient_norm(normgnew, ec_type)
}
pub fn check_max_iterations(&self, iteration: Size, ec_type: &mut EndCriteriaType) -> bool {
if iteration < self.max_iterations {
return false;
}
*ec_type = EndCriteriaType::MaxIterations;
true
}
pub fn check_stationary_point(
&self,
x_old: Real,
x_new: Real,
stat_state_iterations: &mut Size,
ec_type: &mut EndCriteriaType,
) -> bool {
let diff = (x_new - x_old).abs();
if !diff.is_finite() || diff >= self.root_epsilon {
*stat_state_iterations = 0;
return false;
}
*stat_state_iterations += 1;
if *stat_state_iterations <= self.max_stationary_state_iterations {
return false;
}
*ec_type = EndCriteriaType::StationaryPoint;
true
}
pub fn check_stationary_function_value(
&self,
fx_old: Real,
fx_new: Real,
stat_state_iterations: &mut Size,
ec_type: &mut EndCriteriaType,
) -> bool {
let diff = (fx_new - fx_old).abs();
if !diff.is_finite() || diff >= self.function_epsilon {
*stat_state_iterations = 0;
return false;
}
*stat_state_iterations += 1;
if *stat_state_iterations <= self.max_stationary_state_iterations {
return false;
}
*ec_type = EndCriteriaType::StationaryFunctionValue;
true
}
pub fn check_stationary_function_accuracy(
&self,
f: Real,
positive_optimization: bool,
ec_type: &mut EndCriteriaType,
) -> bool {
if !positive_optimization || !f.is_finite() || f >= self.function_epsilon {
return false;
}
*ec_type = EndCriteriaType::StationaryFunctionAccuracy;
true
}
pub fn check_zero_gradient_norm(
&self,
gradient_norm: Real,
ec_type: &mut EndCriteriaType,
) -> bool {
if !gradient_norm.is_finite() || gradient_norm >= self.gradient_norm_epsilon {
return false;
}
*ec_type = EndCriteriaType::ZeroGradientNorm;
true
}
pub fn max_iterations(&self) -> Size {
self.max_iterations
}
pub fn max_stationary_state_iterations(&self) -> Size {
self.max_stationary_state_iterations
}
pub fn root_epsilon(&self) -> Real {
self.root_epsilon
}
pub fn function_epsilon(&self) -> Real {
self.function_epsilon
}
pub fn gradient_norm_epsilon(&self) -> Real {
self.gradient_norm_epsilon
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construction_defaults_and_validation() {
let ec = EndCriteria::new(1000, None, 1e-8, 1e-8, None).unwrap();
assert_eq!(ec.max_stationary_state_iterations(), 100);
assert_eq!(ec.gradient_norm_epsilon(), 1e-8);
let ec = EndCriteria::new(50, None, 1e-8, 1e-9, Some(1e-10)).unwrap();
assert_eq!(ec.max_stationary_state_iterations(), 25);
assert_eq!(ec.gradient_norm_epsilon(), 1e-10);
assert!(EndCriteria::new(1000, Some(1), 1e-8, 1e-8, None).is_err());
assert!(EndCriteria::new(100, Some(100), 1e-8, 1e-8, None).is_err());
}
#[test]
fn rejects_invalid_tolerances() {
assert!(EndCriteria::new(100, Some(10), Real::NAN, 1e-8, None).is_err());
assert!(EndCriteria::new(100, Some(10), 1e-8, -1e-8, None).is_err());
assert!(EndCriteria::new(100, Some(10), 1e-8, Real::INFINITY, None).is_err());
assert!(EndCriteria::new(100, Some(10), 1e-8, 1e-8, Some(Real::NAN)).is_err());
assert!(EndCriteria::new(100, Some(10), 0.0, 0.0, Some(0.0)).is_ok());
}
#[test]
fn max_iterations_check() {
let ec = EndCriteria::new(10, Some(5), 1e-8, 1e-8, None).unwrap();
let mut ec_type = EndCriteriaType::None;
assert!(!ec.check_max_iterations(9, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::None);
assert!(ec.check_max_iterations(10, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::MaxIterations);
}
#[test]
fn stationary_point_requires_consecutive_hits() {
let ec = EndCriteria::new(100, Some(2), 1e-8, 1e-8, None).unwrap();
let mut ec_type = EndCriteriaType::None;
let mut stat = 0;
assert!(!ec.check_stationary_point(0.0, 1e-9, &mut stat, &mut ec_type));
assert!(!ec.check_stationary_point(0.0, 1e-9, &mut stat, &mut ec_type));
assert!(!ec.check_stationary_point(0.0, 1.0, &mut stat, &mut ec_type));
assert_eq!(stat, 0);
assert!(!ec.check_stationary_point(0.0, 1e-9, &mut stat, &mut ec_type));
assert!(!ec.check_stationary_point(0.0, 1e-9, &mut stat, &mut ec_type));
assert!(ec.check_stationary_point(0.0, 1e-9, &mut stat, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::StationaryPoint);
}
#[test]
fn stationary_function_value_counts_like_stationary_point() {
let ec = EndCriteria::new(100, Some(2), 1e-8, 1e-8, None).unwrap();
let mut ec_type = EndCriteriaType::None;
let mut stat = 0;
for _ in 0..2 {
assert!(!ec.check_stationary_function_value(1.0, 1.0, &mut stat, &mut ec_type));
}
assert!(ec.check_stationary_function_value(1.0, 1.0, &mut stat, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::StationaryFunctionValue);
}
#[test]
fn function_accuracy_and_gradient_norm_checks() {
let ec = EndCriteria::new(100, Some(2), 1e-8, 1e-8, None).unwrap();
let mut ec_type = EndCriteriaType::None;
assert!(!ec.check_stationary_function_accuracy(1e-9, false, &mut ec_type));
assert!(!ec.check_stationary_function_accuracy(1.0, true, &mut ec_type));
assert!(ec.check_stationary_function_accuracy(1e-9, true, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::StationaryFunctionAccuracy);
assert!(!ec.check_zero_gradient_norm(1.0, &mut ec_type));
assert!(ec.check_zero_gradient_norm(1e-9, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::ZeroGradientNorm);
}
#[test]
fn non_finite_inputs_never_report_convergence() {
let ec = EndCriteria::new(100, Some(2), 1e-8, 1e-8, None).unwrap();
let mut ec_type = EndCriteriaType::None;
let mut stat = 0;
for _ in 0..5 {
assert!(!ec.check_stationary_point(0.0, Real::NAN, &mut stat, &mut ec_type));
assert_eq!(stat, 0);
}
for _ in 0..5 {
assert!(!ec.check_stationary_function_value(
Real::NAN,
Real::NAN,
&mut stat,
&mut ec_type
));
assert_eq!(stat, 0);
}
assert!(!ec.check_stationary_function_accuracy(Real::NAN, true, &mut ec_type));
assert!(!ec.check_zero_gradient_norm(Real::NAN, &mut ec_type));
assert_eq!(ec_type, EndCriteriaType::None);
}
#[test]
fn succeeded_and_display() {
assert!(EndCriteriaType::StationaryPoint.succeeded());
assert!(EndCriteriaType::StationaryFunctionValue.succeeded());
assert!(EndCriteriaType::StationaryFunctionAccuracy.succeeded());
assert!(!EndCriteriaType::None.succeeded());
assert!(!EndCriteriaType::MaxIterations.succeeded());
assert!(!EndCriteriaType::ZeroGradientNorm.succeeded());
assert_eq!(EndCriteriaType::MaxIterations.to_string(), "MaxIterations");
}
}