1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum NonFiniteKind {
17 Nan,
19 PositiveInfinity,
21 NegativeInfinity,
23}
24
25impl NonFiniteKind {
26 #[must_use]
30 pub fn classify(value: f32) -> Option<Self> {
31 if value.is_nan() {
32 Some(Self::Nan)
33 } else if value.is_infinite() {
34 if value.is_sign_positive() {
35 Some(Self::PositiveInfinity)
36 } else {
37 Some(Self::NegativeInfinity)
38 }
39 } else {
40 None
41 }
42 }
43}
44
45impl std::fmt::Display for NonFiniteKind {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Nan => f.write_str("NaN"),
49 Self::PositiveInfinity => f.write_str("+inf"),
50 Self::NegativeInfinity => f.write_str("-inf"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum CriticField {
64 Objective,
66 Volatility,
68 Surprise,
73 Stress,
75 TdError,
77 EmaReward,
79}
80
81impl std::fmt::Display for CriticField {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.write_str(match self {
84 Self::Objective => "objective",
85 Self::Volatility => "volatility",
86 Self::Surprise => "surprise",
87 Self::Stress => "stress",
88 Self::TdError => "TD error",
89 Self::EmaReward => "EMA reward",
90 })
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum CriticError {
126 NonFinite {
128 field: CriticField,
130 kind: NonFiniteKind,
132 },
133}
134
135impl CriticError {
136 #[must_use]
140 pub fn from_non_finite(field: CriticField, value: f32) -> Option<Self> {
141 NonFiniteKind::classify(value).map(|kind| Self::NonFinite { field, kind })
142 }
143
144 #[must_use]
146 pub const fn field(&self) -> CriticField {
147 match *self {
148 Self::NonFinite { field, .. } => field,
149 }
150 }
151
152 #[must_use]
154 pub const fn kind(&self) -> NonFiniteKind {
155 match *self {
156 Self::NonFinite { kind, .. } => kind,
157 }
158 }
159}
160
161impl std::fmt::Display for CriticError {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match *self {
164 Self::NonFinite { field, kind } => {
165 write!(f, "non-finite {field} ({kind})")
166 }
167 }
168 }
169}
170
171impl std::error::Error for CriticError {}
172
173pub(crate) fn require_finite(value: f32, field: CriticField) -> Result<f32, CriticError> {
175 match CriticError::from_non_finite(field, value) {
176 Some(err) => Err(err),
177 None => Ok(value),
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn classify_finite_and_nonfinite() {
187 assert_eq!(NonFiniteKind::classify(0.0), None);
188 assert_eq!(NonFiniteKind::classify(-0.0), None);
189 assert_eq!(NonFiniteKind::classify(f32::MIN_POSITIVE), None);
190 assert_eq!(NonFiniteKind::classify(f32::MAX), None);
191 assert_eq!(NonFiniteKind::classify(f32::NAN), Some(NonFiniteKind::Nan));
192 assert_eq!(
193 NonFiniteKind::classify(f32::INFINITY),
194 Some(NonFiniteKind::PositiveInfinity)
195 );
196 assert_eq!(
197 NonFiniteKind::classify(f32::NEG_INFINITY),
198 Some(NonFiniteKind::NegativeInfinity)
199 );
200 }
201
202 #[test]
203 fn display_and_error_trait() {
204 let err = CriticError::NonFinite {
205 field: CriticField::Objective,
206 kind: NonFiniteKind::Nan,
207 };
208 assert_eq!(err.to_string(), "non-finite objective (NaN)");
209 assert_eq!(
210 CriticError::NonFinite {
211 field: CriticField::Volatility,
212 kind: NonFiniteKind::PositiveInfinity,
213 }
214 .to_string(),
215 "non-finite volatility (+inf)"
216 );
217 assert_eq!(
218 CriticError::NonFinite {
219 field: CriticField::Stress,
220 kind: NonFiniteKind::NegativeInfinity,
221 }
222 .to_string(),
223 "non-finite stress (-inf)"
224 );
225 let as_error: &dyn std::error::Error = &err;
226 assert!(as_error.source().is_none());
227 assert_eq!(err.field(), CriticField::Objective);
228 assert_eq!(err.kind(), NonFiniteKind::Nan);
229 assert_eq!(CriticField::Surprise.to_string(), "surprise");
230 assert_eq!(CriticField::TdError.to_string(), "TD error");
231 assert_eq!(CriticField::EmaReward.to_string(), "EMA reward");
232 }
233
234 #[test]
235 fn from_non_finite_skips_finite() {
236 assert!(CriticError::from_non_finite(CriticField::Objective, 1.0).is_none());
237 assert_eq!(require_finite(1.5, CriticField::Surprise).unwrap(), 1.5);
238 assert!(require_finite(f32::NAN, CriticField::Surprise).is_err());
239 }
240}