use crate::envelope::EnvelopePosition;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GrammarState {
Admissible,
Boundary,
Violation,
}
impl GrammarState {
pub fn severity(&self) -> u8 {
match self {
Self::Admissible => 0,
Self::Boundary => 1,
Self::Violation => 2,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GrammarTransition {
pub from: GrammarState,
pub to: GrammarState,
pub timestamp_ns: u64,
pub confirmation_count: u32,
}
pub struct GrammarMachine {
current_state: GrammarState,
pending_position: Option<EnvelopePosition>,
consecutive_count: u32,
hysteresis_count: u32,
last_transition_ts: u64,
}
impl GrammarMachine {
pub fn new(hysteresis_count: u32) -> Self {
Self {
current_state: GrammarState::Admissible,
pending_position: None,
consecutive_count: 0,
hysteresis_count: hysteresis_count.max(1),
last_transition_ts: 0,
}
}
pub fn step(
&mut self,
position: EnvelopePosition,
timestamp_ns: u64,
) -> (GrammarState, Option<GrammarTransition>) {
let target_state = match position {
EnvelopePosition::Interior => GrammarState::Admissible,
EnvelopePosition::BoundaryZone => GrammarState::Boundary,
EnvelopePosition::Exterior => GrammarState::Violation,
};
if target_state == self.current_state {
self.pending_position = None;
self.consecutive_count = 0;
return (self.current_state, None);
}
match self.pending_position {
Some(pending) if position_to_state(pending) == target_state => {
self.consecutive_count += 1;
}
None
| Some(EnvelopePosition::Interior)
| Some(EnvelopePosition::BoundaryZone)
| Some(EnvelopePosition::Exterior) => {
self.pending_position = Some(position);
self.consecutive_count = 1;
}
}
if self.consecutive_count >= self.hysteresis_count {
let transition = GrammarTransition {
from: self.current_state,
to: target_state,
timestamp_ns,
confirmation_count: self.consecutive_count,
};
self.current_state = target_state;
self.pending_position = None;
self.consecutive_count = 0;
self.last_transition_ts = timestamp_ns;
(self.current_state, Some(transition))
} else {
(self.current_state, None)
}
}
pub fn state(&self) -> GrammarState {
self.current_state
}
pub fn reset(&mut self) {
self.current_state = GrammarState::Admissible;
self.pending_position = None;
self.consecutive_count = 0;
}
}
fn position_to_state(pos: EnvelopePosition) -> GrammarState {
match pos {
EnvelopePosition::Interior => GrammarState::Admissible,
EnvelopePosition::BoundaryZone => GrammarState::Boundary,
EnvelopePosition::Exterior => GrammarState::Violation,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hysteresis_prevents_premature_transition() {
let mut machine = GrammarMachine::new(3);
let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 1);
assert_eq!(state, GrammarState::Admissible);
assert!(trans.is_none());
let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 2);
assert_eq!(state, GrammarState::Admissible);
assert!(trans.is_none());
let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 3);
assert_eq!(state, GrammarState::Boundary);
assert!(trans.is_some());
let t = trans.unwrap();
assert_eq!(t.from, GrammarState::Admissible);
assert_eq!(t.to, GrammarState::Boundary);
}
#[test]
fn test_interrupted_hysteresis_resets() {
let mut machine = GrammarMachine::new(3);
machine.step(EnvelopePosition::BoundaryZone, 1);
machine.step(EnvelopePosition::BoundaryZone, 2);
machine.step(EnvelopePosition::Interior, 3);
machine.step(EnvelopePosition::BoundaryZone, 4);
machine.step(EnvelopePosition::BoundaryZone, 5);
let (state, _) = machine.step(EnvelopePosition::BoundaryZone, 6);
assert_eq!(state, GrammarState::Boundary);
}
}