use crate::envelope::AdmissibilityEnvelope;
use crate::platform::RobotContext;
use crate::sign::SignTuple;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ReasonCode {
SustainedOutwardDrift,
AbruptSlewViolation,
RecurrentBoundaryGrazing,
EnvelopeViolation,
}
impl ReasonCode {
#[inline]
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::SustainedOutwardDrift => "SustainedOutwardDrift",
Self::AbruptSlewViolation => "AbruptSlewViolation",
Self::RecurrentBoundaryGrazing => "RecurrentBoundaryGrazing",
Self::EnvelopeViolation => "EnvelopeViolation",
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GrammarState {
#[default]
Admissible,
Boundary(ReasonCode),
Violation,
}
impl GrammarState {
#[inline]
#[must_use]
pub const fn requires_attention(&self) -> bool {
!matches!(self, Self::Admissible)
}
#[inline]
#[must_use]
pub const fn is_violation(&self) -> bool {
matches!(self, Self::Violation)
}
#[inline]
#[must_use]
pub const fn is_boundary(&self) -> bool {
matches!(self, Self::Boundary(_))
}
#[inline]
#[must_use]
pub const fn severity(&self) -> u8 {
match self {
Self::Admissible => 0,
Self::Boundary(_) => 1,
Self::Violation => 2,
}
}
#[inline]
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Admissible => "Admissible",
Self::Boundary(_) => "Boundary",
Self::Violation => "Violation",
}
}
}
pub struct GrammarEvaluator<const K: usize> {
pending: GrammarState,
confirmations: u8,
committed: GrammarState,
boundary_hits: [bool; K],
hit_head: usize,
hit_count: usize,
}
impl<const K: usize> GrammarEvaluator<K> {
#[must_use]
pub const fn new() -> Self {
Self {
pending: GrammarState::Admissible,
confirmations: 0,
committed: GrammarState::Admissible,
boundary_hits: [false; K],
hit_head: 0,
hit_count: 0,
}
}
#[inline]
#[must_use]
pub fn state(&self) -> GrammarState {
self.committed
}
pub fn evaluate(
&mut self,
sign: &SignTuple,
envelope: &AdmissibilityEnvelope,
context: RobotContext,
) -> GrammarState {
debug_assert!(envelope.rho >= 0.0, "envelope radius must be non-negative");
debug_assert!((0.0..=1.0).contains(&envelope.boundary_frac), "boundary_frac out of [0,1]");
if context.is_suppressed() {
self.committed = GrammarState::Admissible;
self.pending = GrammarState::Admissible;
self.confirmations = 0;
self.boundary_hits = [false; K];
self.hit_head = 0;
self.hit_count = 0;
return GrammarState::Admissible;
}
let multiplier = context.admissibility_multiplier();
debug_assert!(multiplier >= 0.0, "admissibility multiplier must be non-negative");
let raw = self.compute_raw_state(sign, envelope, multiplier);
if K > 0 {
let is_approach = envelope.is_boundary_approach(sign.norm, multiplier)
&& !envelope.is_violation(sign.norm, multiplier);
self.boundary_hits[self.hit_head] = is_approach;
self.hit_head = (self.hit_head + 1) % K;
if self.hit_count < K {
self.hit_count += 1;
}
}
if raw == self.pending {
if self.confirmations < 2 {
self.confirmations += 1;
}
if self.confirmations >= 2 {
self.committed = raw;
}
} else {
self.pending = raw;
self.confirmations = 1;
}
self.committed
}
fn compute_raw_state(
&self,
sign: &SignTuple,
envelope: &AdmissibilityEnvelope,
multiplier: f64,
) -> GrammarState {
debug_assert!(envelope.rho >= 0.0);
debug_assert!(multiplier >= 0.0);
debug_assert!(self.hit_count <= K, "hit_count must never exceed K");
if envelope.is_violation(sign.norm, multiplier) {
return GrammarState::Violation;
}
if envelope.is_boundary_approach(sign.norm, multiplier) {
if sign.is_outward_drift() {
return GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
}
if sign.is_abrupt_slew(envelope.delta_s) {
return GrammarState::Boundary(ReasonCode::AbruptSlewViolation);
}
}
if K > 0 && self.hit_count >= K {
let grazing_hits = self.boundary_hits.iter().filter(|&&h| h).count();
debug_assert!(grazing_hits <= K, "grazing_hits bounded by buffer length");
if grazing_hits >= K {
return GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing);
}
}
GrammarState::Admissible
}
pub fn reset(&mut self) {
*self = Self::new();
}
}
impl<const K: usize> Default for GrammarEvaluator<K> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::AdmissibilityEnvelope;
use crate::platform::RobotContext;
use crate::sign::SignTuple;
fn env() -> AdmissibilityEnvelope {
AdmissibilityEnvelope::new(0.1)
}
#[test]
fn clean_signal_is_admissible() {
let mut e = GrammarEvaluator::<4>::new();
for _ in 0..5 {
let s = SignTuple::new(0.02, 0.0, 0.0);
assert_eq!(e.evaluate(&s, &env(), RobotContext::ArmOperating), GrammarState::Admissible);
}
}
#[test]
fn violation_committed_after_hysteresis() {
let mut e = GrammarEvaluator::<4>::new();
let big = SignTuple::new(0.15, 0.0, 0.0);
e.evaluate(&big, &env(), RobotContext::ArmOperating);
let s = e.evaluate(&big, &env(), RobotContext::ArmOperating);
assert_eq!(s, GrammarState::Violation);
}
#[test]
fn single_transient_dismissed_by_hysteresis() {
let mut e = GrammarEvaluator::<4>::new();
let big = SignTuple::new(0.15, 0.0, 0.0);
let small = SignTuple::new(0.02, 0.0, 0.0);
e.evaluate(&big, &env(), RobotContext::ArmOperating);
let s = e.evaluate(&small, &env(), RobotContext::ArmOperating);
assert_eq!(s, GrammarState::Admissible, "single transient must be dismissed");
}
#[test]
fn commissioning_suppresses_violations() {
let mut e = GrammarEvaluator::<4>::new();
let huge = SignTuple::new(1_000.0, 50.0, 5.0);
for _ in 0..5 {
assert_eq!(e.evaluate(&huge, &env(), RobotContext::ArmCommissioning), GrammarState::Admissible);
}
}
#[test]
fn sustained_outward_drift_is_boundary() {
let mut e = GrammarEvaluator::<4>::new();
let drift = SignTuple::new(0.07, 0.005, 0.0);
e.evaluate(&drift, &env(), RobotContext::ArmOperating);
let s = e.evaluate(&drift, &env(), RobotContext::ArmOperating);
assert_eq!(s, GrammarState::Boundary(ReasonCode::SustainedOutwardDrift));
}
#[test]
fn abrupt_slew_is_boundary_when_in_approach_band() {
let mut e = GrammarEvaluator::<4>::new();
let s_in = SignTuple::new(0.08, 0.0, 0.2);
e.evaluate(&s_in, &env(), RobotContext::ArmOperating);
let s = e.evaluate(&s_in, &env(), RobotContext::ArmOperating);
assert_eq!(s, GrammarState::Boundary(ReasonCode::AbruptSlewViolation));
}
#[test]
fn recurrent_grazing_detected_after_k_hits() {
let mut e = GrammarEvaluator::<3>::new();
let graze = SignTuple::new(0.07, 0.0, 0.0);
for _ in 0..5 {
e.evaluate(&graze, &env(), RobotContext::ArmOperating);
}
assert_eq!(e.state(), GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing));
}
#[test]
fn severity_monotone_with_state() {
assert!(GrammarState::Violation.severity() > GrammarState::Boundary(ReasonCode::EnvelopeViolation).severity());
assert!(GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).severity() > GrammarState::Admissible.severity());
}
#[test]
fn labels_are_stable() {
assert_eq!(GrammarState::Admissible.label(), "Admissible");
assert_eq!(GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).label(), "Boundary");
assert_eq!(GrammarState::Violation.label(), "Violation");
}
}