use crate::EvaluationKind;
use crate::core::math::Scalar;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TerminationStage {
Boundary,
Step {
completed: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct TerminationReport<F: Scalar = f64> {
pub termination: Termination<F>,
pub stage: TerminationStage,
pub iteration: u64,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Termination<F: Scalar = f64> {
Converged(Convergence<F>),
Limit(Vec<ExecutionLimit>),
Target {
cost: F,
target: F,
},
Stalled(Stall<F>),
Failed(NumericalFailure<F>),
Cancelled,
Application(ApplicationStop),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(deserialize = "F: serde::Deserialize<'de>"))
)]
pub struct Convergence<F: Scalar = f64> {
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "deserialize_criteria")
)]
criteria: Vec<ConvergenceCriterion<F>>,
}
#[cfg(feature = "serde")]
fn deserialize_criteria<'de, F, D>(
deserializer: D,
) -> Result<Vec<ConvergenceCriterion<F>>, D::Error>
where
F: Scalar + serde::Deserialize<'de>,
D: serde::Deserializer<'de>,
{
let criteria =
<Vec<ConvergenceCriterion<F>> as serde::Deserialize>::deserialize(
deserializer,
)?;
if criteria.is_empty() {
return Err(serde::de::Error::custom(
"convergence requires at least one satisfied predicate",
));
}
Ok(criteria)
}
impl<F: Scalar> Convergence<F> {
pub fn new(criterion: ConvergenceCriterion<F>) -> Self {
Self {
criteria: vec![criterion],
}
}
pub fn criteria(&self) -> &[ConvergenceCriterion<F>] {
&self.criteria
}
pub fn push(&mut self, criterion: ConvergenceCriterion<F>) {
self.criteria.push(criterion);
}
pub(crate) fn append(&mut self, other: Self) {
self.criteria.extend(other.criteria);
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ConvergenceCriterion<F: Scalar = f64> {
pub test: ConvergenceTest,
pub evidence: ConvergenceEvidence<F>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ConvergenceTest {
AbsoluteGradientSquared,
RelativeGradientSquared,
AbsoluteGradientInfinity,
ProjectedGradient,
BoundClippedGradient,
AbsoluteStep,
RelativeStep,
AbsoluteCostChange,
RelativeCostChange,
RelativeCostReduction,
Simplex,
GradientOrthogonality,
RobustGradientOrthogonality,
RelativeModelReduction,
RelativeTrialStep,
RelativeTrustRadius,
AbsoluteScaledGradient,
NoFreeParameters,
DistributionSize,
Radius,
Mesh,
Custom {
key: String,
definition: String,
},
AbsoluteSimplexCostStdDev,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ConvergenceEvidence<F: Scalar = f64> {
UpperBound {
value: F,
bound: F,
tolerance: F,
reference: Option<F>,
},
FactoredUpperBound {
value: BinaryValue<F>,
bound: BinaryValue<F>,
tolerance: F,
reference: Option<BinaryValue<F>>,
},
ModelReduction {
actual: F,
predicted: F,
reference_cost: F,
gain_ratio: F,
tolerance: F,
},
Simplex {
size: Option<Threshold<F>>,
cost_spread: Option<Threshold<F>>,
},
NoFreeParameters,
Measurements(Vec<Measurement<F>>),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BinaryValue<F: Scalar = f64> {
pub significand: F,
pub exponent: i32,
}
impl<F: Scalar> BinaryValue<F> {
pub(crate) fn product(factors: &[F]) -> Self {
if factors.contains(&F::zero()) {
return Self {
significand: F::zero(),
exponent: 0,
};
}
let mut significand = F::one();
let mut exponent = 0_i32;
for factor in factors {
let (mantissa, power, _) = factor.integer_decode();
let shift = 63 - mantissa.leading_zeros();
significand = significand
* (F::from_u64(mantissa).unwrap()
/ F::from_u64(1_u64 << shift).unwrap());
exponent += i32::from(power) + shift as i32;
if significand >= F::from_f64(2.).unwrap() {
significand = significand * F::from_f64(0.5).unwrap();
exponent += 1;
}
}
Self {
significand,
exponent,
}
}
pub(crate) fn product_le(left: &[F], right: &[F]) -> bool {
if left.contains(&F::zero()) {
return true;
}
if right.contains(&F::zero()) {
return false;
}
let left = Self::product(left);
let right = Self::product(right);
(left.exponent, left.significand) <= (right.exponent, right.significand)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Threshold<F: Scalar = f64> {
pub value: F,
pub tolerance: F,
pub absolute_tolerance: Option<F>,
pub relative_tolerance: Option<F>,
pub reference: Option<F>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Measurement<F: Scalar = f64> {
pub name: String,
pub value: F,
}
impl<F: Scalar> ConvergenceCriterion<F> {
pub fn new(
test: ConvergenceTest,
evidence: ConvergenceEvidence<F>,
) -> Self {
Self { test, evidence }
}
pub fn upper_bound(
test: ConvergenceTest,
value: F,
bound: F,
tolerance: F,
reference: Option<F>,
) -> Self {
Self::new(
test,
ConvergenceEvidence::UpperBound {
value,
bound,
tolerance,
reference,
},
)
}
pub fn custom(
key: impl Into<String>,
definition: impl Into<String>,
measurements: Vec<Measurement<F>>,
) -> Self {
Self::new(
ConvergenceTest::Custom {
key: key.into(),
definition: definition.into(),
},
ConvergenceEvidence::Measurements(measurements),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ExecutionLimit {
Iterations {
observed: u64,
limit: u64,
},
CostEvaluations {
observed: u64,
limit: u64,
},
GradientEvaluations {
observed: u64,
limit: u64,
},
Evaluations {
kind: EvaluationKind,
observed: u64,
limit: u64,
},
Time {
elapsed: Duration,
limit: Duration,
},
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Stall<F: Scalar = f64> {
NoImprovement {
iterations: u64,
patience: u64,
tolerance: F,
reference_cost: F,
best_cost: F,
},
NoAcceptedMove {
iterations: u64,
patience: u64,
},
Numerical {
message: String,
measurements: Vec<Measurement<F>>,
},
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum NumericalFailure<F: Scalar = f64> {
Solver {
message: String,
measurements: Vec<Measurement<F>>,
},
Inner {
report: Box<TerminationReport<F>>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ApplicationStop {
pub code: String,
}
impl ApplicationStop {
pub fn new(code: impl Into<String>) -> Self {
Self { code: code.into() }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PartialResultPolicy {
Consume,
RequireConvergence,
}
impl<F: Scalar> Termination<F> {
pub fn native_convergence_tests(
&self,
) -> Vec<crate::NativeConvergenceTest> {
use crate::NativeConvergenceTest as N;
use ConvergenceTest as C;
let Self::Converged(c) = self else {
return vec![];
};
c.criteria()
.iter()
.filter_map(|criterion| {
Some(match criterion.test {
C::AbsoluteGradientInfinity => N::AbsoluteGradient,
C::GradientOrthogonality => N::GradientOrthogonality,
C::RobustGradientOrthogonality => {
N::RobustGradientOrthogonality
}
C::RelativeModelReduction => N::RelativeModelReduction,
C::RelativeTrialStep => N::RelativeTrialStep,
C::RelativeTrustRadius => N::RelativeTrustRadius,
C::AbsoluteScaledGradient => N::AbsoluteScaledGradient,
C::NoFreeParameters => N::NoFreeParameters,
_ => return None,
})
})
.collect()
}
pub fn converged(criterion: ConvergenceCriterion<F>) -> Self {
Self::Converged(Convergence::new(criterion))
}
pub fn is_converged(&self) -> bool {
matches!(self, Self::Converged(_))
}
pub fn is_failure(&self) -> bool {
matches!(self, Self::Failed(_))
}
pub fn can_continue_as_inner(&self, policy: PartialResultPolicy) -> bool {
self.is_converged()
|| (policy == PartialResultPolicy::Consume
&& matches!(
self,
Self::Limit(_) | Self::Target { .. } | Self::Stalled(_)
))
}
pub fn numerical_failure(message: impl Into<String>) -> Self {
Self::Failed(NumericalFailure::Solver {
message: message.into(),
measurements: vec![],
})
}
pub fn numerical_stall(message: impl Into<String>) -> Self {
Self::Stalled(Stall::Numerical {
message: message.into(),
measurements: vec![],
})
}
pub(crate) fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::Converged(mut a), Self::Converged(b)) => {
a.append(b);
Self::Converged(a)
}
(Self::Converged(_), b) => b,
(_, b) if b.is_failure() => b,
(a, _) => a,
}
}
}
impl<F: Scalar> TerminationReport<F> {
pub fn new(
termination: Termination<F>,
stage: TerminationStage,
iteration: u64,
) -> Self {
Self {
termination,
stage,
iteration,
}
}
pub fn code(&self) -> TerminationCode {
self.termination.code()
}
pub fn into_outer_termination(
self,
policy: PartialResultPolicy,
) -> Option<Termination<F>> {
match self.termination {
Termination::Converged(_) => None,
Termination::Cancelled => Some(Termination::Cancelled),
Termination::Application(stop) => {
Some(Termination::Application(stop))
}
Termination::Limit(_)
| Termination::Target { .. }
| Termination::Stalled(_)
if policy == PartialResultPolicy::Consume =>
{
None
}
_ => Some(Termination::Failed(NumericalFailure::Inner {
report: Box::new(self),
})),
}
}
}
impl<F: Scalar> Termination<F> {
pub fn upper_bound(
test: ConvergenceTest,
value: F,
bound: F,
tolerance: F,
reference: Option<F>,
) -> Self {
Self::converged(ConvergenceCriterion::upper_bound(
test, value, bound, tolerance, reference,
))
}
pub fn custom(
key: impl Into<String>,
definition: impl Into<String>,
measurements: Vec<Measurement<F>>,
) -> Self {
Self::converged(ConvergenceCriterion::custom(
key,
definition,
measurements,
))
}
pub fn code(&self) -> TerminationCode {
use ConvergenceTest as C;
use TerminationCode as R;
match self {
Self::Converged(c) => match &c.criteria()[0].test {
C::AbsoluteGradientSquared => R::GradientTolerance,
C::RelativeGradientSquared => R::RelativeGradientTolerance,
C::ProjectedGradient | C::BoundClippedGradient => {
R::ProjectedGradientTolerance
}
C::AbsoluteStep => R::ParamTolerance,
C::RelativeStep => R::RelativeParamTolerance,
C::AbsoluteCostChange => R::CostTolerance,
C::RelativeCostChange | C::RelativeCostReduction => {
R::RelativeCostTolerance
}
C::Simplex | C::AbsoluteSimplexCostStdDev => {
R::SimplexTolerance
}
C::DistributionSize => R::CmaEsTolerance,
C::Radius => R::RhoTolerance,
C::Mesh => R::MeshTolerance,
_ => R::SolverConverged,
},
Self::Limit(limits) => match limits.first() {
Some(ExecutionLimit::Iterations { .. }) => R::MaxIter,
Some(ExecutionLimit::CostEvaluations { .. }) => R::MaxCostEvals,
Some(ExecutionLimit::GradientEvaluations { .. }) => {
R::MaxGradientEvals
}
Some(ExecutionLimit::Time { .. }) => R::MaxTime,
_ => R::MaxEvaluations,
},
Self::Target { .. } => R::TargetCost,
Self::Stalled(Stall::NoImprovement { .. }) => R::NoImprovement,
Self::Stalled(Stall::NoAcceptedMove { .. }) => R::NoAcceptedMove,
Self::Stalled(Stall::Numerical { .. }) => R::NumericalNoProgress,
Self::Failed(_) => R::SolverFailed,
Self::Cancelled => R::Cancelled,
Self::Application(_) => R::UserRequested,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum TerminationCode {
MaxIter,
MaxCostEvals,
MaxGradientEvals,
GradientTolerance,
RelativeGradientTolerance,
ProjectedGradientTolerance,
ParamTolerance,
RelativeParamTolerance,
CostTolerance,
RelativeCostTolerance,
TargetCost,
NoImprovement,
NoAcceptedMove,
SimplexTolerance,
CmaEsTolerance,
RhoTolerance,
MeshTolerance,
MaxTime,
Cancelled,
UserRequested,
SolverConverged,
SolverFailed,
NumericalNoProgress,
MaxEvaluations,
}
impl TerminationCode {
pub fn is_failure(&self) -> bool {
matches!(self, Self::SolverFailed)
}
}
impl TerminationCode {
pub const fn as_u8(self) -> u8 {
match self {
Self::MaxIter => 0,
Self::MaxCostEvals => 1,
Self::MaxGradientEvals => 2,
Self::GradientTolerance => 3,
Self::RelativeGradientTolerance => 4,
Self::ProjectedGradientTolerance => 5,
Self::ParamTolerance => 6,
Self::RelativeParamTolerance => 7,
Self::CostTolerance => 8,
Self::RelativeCostTolerance => 9,
Self::TargetCost => 10,
Self::NoImprovement => 11,
Self::NoAcceptedMove => 12,
Self::SimplexTolerance => 13,
Self::CmaEsTolerance => 14,
Self::RhoTolerance => 15,
Self::MeshTolerance => 16,
Self::MaxTime => 17,
Self::Cancelled => 18,
Self::UserRequested => 19,
Self::SolverConverged => 20,
Self::SolverFailed => 21,
Self::NumericalNoProgress => 22,
Self::MaxEvaluations => 23,
}
}
}