use crate::envelope::AdmissibilityEnvelope;
use crate::episode::Episode;
use crate::grammar::GrammarEvaluator;
use crate::platform::RobotContext;
use crate::policy::PolicyDecision;
use crate::sign::SignWindow;
pub struct DsfbRoboticsEngine<const W: usize, const K: usize> {
envelope: AdmissibilityEnvelope,
sign_window: SignWindow<W>,
grammar: GrammarEvaluator<K>,
}
impl<const W: usize, const K: usize> DsfbRoboticsEngine<W, K> {
#[must_use]
pub const fn new(rho: f64) -> Self {
Self {
envelope: AdmissibilityEnvelope::new(rho),
sign_window: SignWindow::<W>::new(),
grammar: GrammarEvaluator::<K>::new(),
}
}
#[must_use]
pub const fn from_envelope(envelope: AdmissibilityEnvelope) -> Self {
Self {
envelope,
sign_window: SignWindow::<W>::new(),
grammar: GrammarEvaluator::<K>::new(),
}
}
pub fn set_envelope(&mut self, envelope: AdmissibilityEnvelope) {
self.envelope = envelope;
}
#[inline]
#[must_use]
pub fn envelope(&self) -> AdmissibilityEnvelope {
self.envelope
}
pub fn observe_one(
&mut self,
norm: f64,
below_floor: bool,
context: RobotContext,
index: usize,
) -> Episode {
let sign = self.sign_window.push(norm, below_floor);
let state = self.grammar.evaluate(&sign, &self.envelope, context);
let decision = PolicyDecision::from_grammar(state);
Episode::new(index, norm * norm, sign.drift, state, decision)
}
pub fn observe(
&mut self,
residuals: &[f64],
out: &mut [Episode],
context: RobotContext,
) -> usize {
debug_assert!(residuals.len() <= usize::MAX / 2, "residuals slice unreasonable");
debug_assert!(out.len() <= usize::MAX / 2, "output buffer unreasonable");
let mut written = 0_usize;
let n = residuals.len().min(out.len());
let mut i = 0_usize;
while i < n {
let r = residuals[i];
let below_floor = !r.is_finite();
let norm = if r.is_finite() { crate::math::abs_f64(r) } else { 0.0 };
out[written] = self.observe_one(norm, below_floor, context, i);
written += 1;
i += 1;
}
written
}
pub fn reset(&mut self) {
self.sign_window.reset();
self.grammar.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn streaming_stays_admissible_for_quiet_input() {
let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
let residuals = [0.01_f64; 32];
let mut out = [Episode::empty(); 32];
let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
assert_eq!(n, 32);
for e in &out[..n] {
assert_eq!(e.grammar, "Admissible");
assert_eq!(e.decision, "Silent");
}
}
#[test]
fn persistent_violation_produces_escalate() {
let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
let residuals = [0.5_f64; 32];
let mut out = [Episode::empty(); 32];
let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
assert_eq!(n, 32);
let escalated = out[..n].iter().filter(|e| e.decision == "Escalate").count();
assert!(escalated >= 30, "expected ≥30 Escalate episodes, got {}", escalated);
}
#[test]
fn commissioning_suppresses_everything() {
let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
let residuals = [1_000.0_f64; 32];
let mut out = [Episode::empty(); 32];
let n = eng.observe(&residuals, &mut out, RobotContext::ArmCommissioning);
assert_eq!(n, 32);
for e in &out[..n] {
assert_eq!(e.grammar, "Admissible");
assert_eq!(e.decision, "Silent");
}
}
#[test]
fn observe_respects_output_capacity() {
let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
let residuals = [0.02_f64; 32];
let mut small_out = [Episode::empty(); 4];
let n = eng.observe(&residuals, &mut small_out, RobotContext::ArmOperating);
assert_eq!(n, 4, "must never write past output capacity");
}
#[test]
fn observe_one_preserves_sample_index() {
let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
for i in 0..10 {
let e = eng.observe_one(0.02, false, RobotContext::ArmOperating, i);
assert_eq!(e.index, i);
}
}
#[test]
fn nonfinite_residual_treated_as_below_floor() {
let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
let residuals = [0.02_f64, f64::NAN, 0.02, f64::INFINITY, 0.02];
let mut out = [Episode::empty(); 5];
let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
assert_eq!(n, 5);
for e in &out[..n] {
assert_eq!(e.grammar, "Admissible");
assert_eq!(e.decision, "Silent");
}
}
#[test]
fn reset_clears_streaming_state_but_keeps_envelope() {
let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
let before = eng.envelope().rho;
for _ in 0..10 {
eng.observe_one(0.5, false, RobotContext::ArmOperating, 0);
}
eng.reset();
let after = eng.envelope().rho;
assert_eq!(before, after);
let e = eng.observe_one(0.01, false, RobotContext::ArmOperating, 0);
assert_eq!(e.grammar, "Admissible");
}
}