#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NonFiniteKind {
Nan,
PositiveInfinity,
NegativeInfinity,
}
impl NonFiniteKind {
#[must_use]
pub fn classify(value: f32) -> Option<Self> {
if value.is_nan() {
Some(Self::Nan)
} else if value.is_infinite() {
if value.is_sign_positive() {
Some(Self::PositiveInfinity)
} else {
Some(Self::NegativeInfinity)
}
} else {
None
}
}
}
impl std::fmt::Display for NonFiniteKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Nan => f.write_str("NaN"),
Self::PositiveInfinity => f.write_str("+inf"),
Self::NegativeInfinity => f.write_str("-inf"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CriticField {
Objective,
Volatility,
Surprise,
Stress,
TdError,
EmaReward,
}
impl std::fmt::Display for CriticField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Objective => "objective",
Self::Volatility => "volatility",
Self::Surprise => "surprise",
Self::Stress => "stress",
Self::TdError => "TD error",
Self::EmaReward => "EMA reward",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CriticError {
NonFinite {
field: CriticField,
kind: NonFiniteKind,
},
}
impl CriticError {
#[must_use]
pub fn from_non_finite(field: CriticField, value: f32) -> Option<Self> {
NonFiniteKind::classify(value).map(|kind| Self::NonFinite { field, kind })
}
#[must_use]
pub const fn field(&self) -> CriticField {
match *self {
Self::NonFinite { field, .. } => field,
}
}
#[must_use]
pub const fn kind(&self) -> NonFiniteKind {
match *self {
Self::NonFinite { kind, .. } => kind,
}
}
}
impl std::fmt::Display for CriticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::NonFinite { field, kind } => {
write!(f, "non-finite {field} ({kind})")
}
}
}
}
impl std::error::Error for CriticError {}
pub(crate) fn require_finite(value: f32, field: CriticField) -> Result<f32, CriticError> {
match CriticError::from_non_finite(field, value) {
Some(err) => Err(err),
None => Ok(value),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_finite_and_nonfinite() {
assert_eq!(NonFiniteKind::classify(0.0), None);
assert_eq!(NonFiniteKind::classify(-0.0), None);
assert_eq!(NonFiniteKind::classify(f32::MIN_POSITIVE), None);
assert_eq!(NonFiniteKind::classify(f32::MAX), None);
assert_eq!(NonFiniteKind::classify(f32::NAN), Some(NonFiniteKind::Nan));
assert_eq!(
NonFiniteKind::classify(f32::INFINITY),
Some(NonFiniteKind::PositiveInfinity)
);
assert_eq!(
NonFiniteKind::classify(f32::NEG_INFINITY),
Some(NonFiniteKind::NegativeInfinity)
);
}
#[test]
fn display_and_error_trait() {
let err = CriticError::NonFinite {
field: CriticField::Objective,
kind: NonFiniteKind::Nan,
};
assert_eq!(err.to_string(), "non-finite objective (NaN)");
assert_eq!(
CriticError::NonFinite {
field: CriticField::Volatility,
kind: NonFiniteKind::PositiveInfinity,
}
.to_string(),
"non-finite volatility (+inf)"
);
assert_eq!(
CriticError::NonFinite {
field: CriticField::Stress,
kind: NonFiniteKind::NegativeInfinity,
}
.to_string(),
"non-finite stress (-inf)"
);
let as_error: &dyn std::error::Error = &err;
assert!(as_error.source().is_none());
assert_eq!(err.field(), CriticField::Objective);
assert_eq!(err.kind(), NonFiniteKind::Nan);
assert_eq!(CriticField::Surprise.to_string(), "surprise");
assert_eq!(CriticField::TdError.to_string(), "TD error");
assert_eq!(CriticField::EmaReward.to_string(), "EMA reward");
}
#[test]
fn from_non_finite_skips_finite() {
assert!(CriticError::from_non_finite(CriticField::Objective, 1.0).is_none());
assert_eq!(require_finite(1.5, CriticField::Surprise).unwrap(), 1.5);
assert!(require_finite(f32::NAN, CriticField::Surprise).is_err());
}
}