use crate::grammar::GrammarState;
use crate::residual::ResidualSource;
use crate::ReasonCode;
#[derive(Debug, Clone)]
pub struct Episode {
pub start_ts: u64,
pub end_ts: Option<u64>,
pub grammar_state: GrammarState,
pub reason_code: ReasonCode,
pub primary_source: ResidualSource,
pub max_drift: f64,
pub max_slew: f64,
pub max_residual: f64,
pub sample_count: u32,
}
impl Episode {
pub fn duration_ns(&self) -> Option<u64> {
self.end_ts.map(|end| end.saturating_sub(self.start_ts))
}
pub fn is_open(&self) -> bool {
self.end_ts.is_none()
}
pub fn is_anomalous(&self) -> bool {
matches!(
self.grammar_state,
GrammarState::Boundary | GrammarState::Violation
)
}
}
pub struct EpisodeBuilder {
current: Option<Episode>,
}
impl EpisodeBuilder {
pub fn new() -> Self {
Self { current: None }
}
pub fn open(
&mut self,
timestamp_ns: u64,
grammar_state: GrammarState,
reason_code: ReasonCode,
source: ResidualSource,
) {
self.current = Some(Episode {
start_ts: timestamp_ns,
end_ts: None,
grammar_state,
reason_code,
primary_source: source,
max_drift: 0.0,
max_slew: 0.0,
max_residual: 0.0,
sample_count: 0,
});
}
pub fn update(&mut self, residual: f64, drift: f64, slew: f64) {
if let Some(ref mut ep) = self.current {
ep.max_drift = ep.max_drift.max(drift.abs());
ep.max_slew = ep.max_slew.max(slew.abs());
ep.max_residual = ep.max_residual.max(residual.abs());
ep.sample_count += 1;
}
}
pub fn close(&mut self, timestamp_ns: u64) -> Option<Episode> {
if let Some(mut ep) = self.current.take() {
ep.end_ts = Some(timestamp_ns);
Some(ep)
} else {
None
}
}
pub fn is_open(&self) -> bool {
self.current.is_some()
}
pub fn current(&self) -> Option<&Episode> {
self.current.as_ref()
}
}
impl Default for EpisodeBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_episode_lifecycle() {
let mut builder = EpisodeBuilder::new();
assert!(!builder.is_open());
builder.open(
1000,
GrammarState::Boundary,
ReasonCode::SustainedLatencyDrift,
ResidualSource::Latency,
);
assert!(builder.is_open());
builder.update(5.0, 0.3, 0.01);
builder.update(6.0, 0.4, 0.02);
let ep = builder.close(3000).unwrap();
assert_eq!(ep.start_ts, 1000);
assert_eq!(ep.end_ts, Some(3000));
assert_eq!(ep.grammar_state, GrammarState::Boundary);
assert_eq!(ep.sample_count, 2);
assert!((ep.max_drift - 0.4).abs() < 1e-10);
assert!(ep.is_anomalous());
}
}