use crate::grammar::GrammarState;
use crate::policy::PolicyDecision;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Episode {
pub index: usize,
pub residual_norm_sq: f64,
pub drift: f64,
pub grammar: &'static str,
pub decision: &'static str,
}
impl Episode {
#[inline]
#[must_use]
pub const fn empty() -> Self {
Self {
index: 0,
residual_norm_sq: 0.0,
drift: 0.0,
grammar: "Admissible",
decision: "Silent",
}
}
#[inline]
#[must_use]
pub const fn new(
index: usize,
residual_norm_sq: f64,
drift: f64,
grammar: GrammarState,
decision: PolicyDecision,
) -> Self {
Self {
index,
residual_norm_sq,
drift,
grammar: grammar.label(),
decision: decision.label(),
}
}
}
impl Default for Episode {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::{GrammarState, ReasonCode};
use crate::policy::PolicyDecision;
#[test]
fn empty_is_admissible_silent() {
let e = Episode::empty();
assert_eq!(e.grammar, "Admissible");
assert_eq!(e.decision, "Silent");
assert_eq!(e.index, 0);
assert_eq!(e.residual_norm_sq, 0.0);
assert_eq!(e.drift, 0.0);
}
#[test]
fn new_writes_expected_labels() {
let e = Episode::new(
42,
0.01,
0.001,
GrammarState::Boundary(ReasonCode::SustainedOutwardDrift),
PolicyDecision::Review,
);
assert_eq!(e.index, 42);
assert_eq!(e.grammar, "Boundary");
assert_eq!(e.decision, "Review");
}
#[test]
fn default_equals_empty() {
assert_eq!(Episode::default(), Episode::empty());
}
}