1use crate::envelope::AdmissibilityEnvelope;
30use crate::episode::Episode;
31use crate::grammar::GrammarEvaluator;
32use crate::platform::RobotContext;
33use crate::policy::PolicyDecision;
34use crate::sign::SignWindow;
35
36pub struct DsfbRoboticsEngine<const W: usize, const K: usize> {
40 envelope: AdmissibilityEnvelope,
41 sign_window: SignWindow<W>,
42 grammar: GrammarEvaluator<K>,
43}
44
45impl<const W: usize, const K: usize> DsfbRoboticsEngine<W, K> {
46 #[must_use]
49 pub const fn new(rho: f64) -> Self {
50 Self {
51 envelope: AdmissibilityEnvelope::new(rho),
52 sign_window: SignWindow::<W>::new(),
53 grammar: GrammarEvaluator::<K>::new(),
54 }
55 }
56
57 #[must_use]
59 pub const fn from_envelope(envelope: AdmissibilityEnvelope) -> Self {
60 Self {
61 envelope,
62 sign_window: SignWindow::<W>::new(),
63 grammar: GrammarEvaluator::<K>::new(),
64 }
65 }
66
67 pub fn set_envelope(&mut self, envelope: AdmissibilityEnvelope) {
70 self.envelope = envelope;
71 }
72
73 #[inline]
75 #[must_use]
76 pub fn envelope(&self) -> AdmissibilityEnvelope {
77 self.envelope
78 }
79
80 pub fn observe_one(
89 &mut self,
90 norm: f64,
91 below_floor: bool,
92 context: RobotContext,
93 index: usize,
94 ) -> Episode {
95 let sign = self.sign_window.push(norm, below_floor);
96 let state = self.grammar.evaluate(&sign, &self.envelope, context);
97 let decision = PolicyDecision::from_grammar(state);
98 Episode::new(index, norm * norm, sign.drift, state, decision)
99 }
100
101 pub fn observe(
112 &mut self,
113 residuals: &[f64],
114 out: &mut [Episode],
115 context: RobotContext,
116 ) -> usize {
117 debug_assert!(residuals.len() <= usize::MAX / 2, "residuals slice unreasonable");
118 debug_assert!(out.len() <= usize::MAX / 2, "output buffer unreasonable");
119
120 let mut written = 0_usize;
121 let n = residuals.len().min(out.len());
122 let mut i = 0_usize;
123 while i < n {
124 let r = residuals[i];
125 let below_floor = !r.is_finite();
128 let norm = if r.is_finite() { crate::math::abs_f64(r) } else { 0.0 };
129 out[written] = self.observe_one(norm, below_floor, context, i);
130 written += 1;
131 i += 1;
132 }
133 written
134 }
135
136 pub fn reset(&mut self) {
142 self.sign_window.reset();
143 self.grammar.reset();
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn streaming_stays_admissible_for_quiet_input() {
153 let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
154 let residuals = [0.01_f64; 32];
155 let mut out = [Episode::empty(); 32];
156 let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
157 assert_eq!(n, 32);
158 for e in &out[..n] {
159 assert_eq!(e.grammar, "Admissible");
160 assert_eq!(e.decision, "Silent");
161 }
162 }
163
164 #[test]
165 fn persistent_violation_produces_escalate() {
166 let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
167 let residuals = [0.5_f64; 32];
168 let mut out = [Episode::empty(); 32];
169 let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
170 assert_eq!(n, 32);
171 let escalated = out[..n].iter().filter(|e| e.decision == "Escalate").count();
173 assert!(escalated >= 30, "expected ≥30 Escalate episodes, got {}", escalated);
174 }
175
176 #[test]
177 fn commissioning_suppresses_everything() {
178 let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
179 let residuals = [1_000.0_f64; 32];
180 let mut out = [Episode::empty(); 32];
181 let n = eng.observe(&residuals, &mut out, RobotContext::ArmCommissioning);
182 assert_eq!(n, 32);
183 for e in &out[..n] {
184 assert_eq!(e.grammar, "Admissible");
185 assert_eq!(e.decision, "Silent");
186 }
187 }
188
189 #[test]
190 fn observe_respects_output_capacity() {
191 let mut eng = DsfbRoboticsEngine::<8, 4>::new(0.1);
192 let residuals = [0.02_f64; 32];
193 let mut small_out = [Episode::empty(); 4];
194 let n = eng.observe(&residuals, &mut small_out, RobotContext::ArmOperating);
195 assert_eq!(n, 4, "must never write past output capacity");
196 }
197
198 #[test]
199 fn observe_one_preserves_sample_index() {
200 let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
201 for i in 0..10 {
202 let e = eng.observe_one(0.02, false, RobotContext::ArmOperating, i);
203 assert_eq!(e.index, i);
204 }
205 }
206
207 #[test]
208 fn nonfinite_residual_treated_as_below_floor() {
209 let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
210 let residuals = [0.02_f64, f64::NAN, 0.02, f64::INFINITY, 0.02];
211 let mut out = [Episode::empty(); 5];
212 let n = eng.observe(&residuals, &mut out, RobotContext::ArmOperating);
213 assert_eq!(n, 5);
214 for e in &out[..n] {
215 assert_eq!(e.grammar, "Admissible");
216 assert_eq!(e.decision, "Silent");
217 }
218 }
219
220 #[test]
221 fn reset_clears_streaming_state_but_keeps_envelope() {
222 let mut eng = DsfbRoboticsEngine::<4, 3>::new(0.1);
223 let before = eng.envelope().rho;
224 for _ in 0..10 {
225 eng.observe_one(0.5, false, RobotContext::ArmOperating, 0);
226 }
227 eng.reset();
228 let after = eng.envelope().rho;
229 assert_eq!(before, after);
230 let e = eng.observe_one(0.01, false, RobotContext::ArmOperating, 0);
232 assert_eq!(e.grammar, "Admissible");
233 }
234}