Skip to main content

dsfb_rf/
grammar.rs

1//! Grammar FSM: Admissible | Boundary[ReasonCode] | Violation
2//!
3//! ## Mathematical Definition (paper §B.4, §V-C)
4//!
5//! State assignment rules (per observation k):
6//! - Violation:  ‖r(k)‖ > ρ_eff
7//! - Boundary:   ‖r(k)‖ > 0.5ρ_eff  AND  (ṙ(k) > 0 OR |r̈(k)| > δ_s)
8//!   OR:         recurrent near-boundary hits ≥ K in window W
9//! - Admissible: otherwise
10//!
11//! Hysteresis: 2 consecutive confirmations required before a state
12//! change is committed. Sub-threshold observations forced to Admissible.
13//!
14//! ## Design Note
15//!
16//! This is a single canonical FSM implementation. The semiconductor crate
17//! had the dual-FSM defect (3-state batch vs. 6-state streaming).
18//! This crate has exactly one FSM: the 3-state typed grammar above.
19//! No alternative implementations exist in this module.
20
21use crate::envelope::AdmissibilityEnvelope;
22use crate::sign::SignTuple;
23use crate::platform::WaveformState;
24
25/// Reason code qualifying a Boundary grammar state.
26///
27/// Typed reason codes allow operators to distinguish classes of structural
28/// behavior without modulation classification (paper Table II).
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum ReasonCode {
32    /// Persistent positive ṙ over W consecutive observations.
33    /// RF contexts: PA thermal drift, LO aging, slow interference buildup.
34    SustainedOutwardDrift,
35    /// Abrupt |r̈| > δ_s event.
36    /// RF contexts: jamming onset, hardware fault, LO phase jump.
37    AbruptSlewViolation,
38    /// Recurrent near-boundary hits ≥ K in window W.
39    /// RF contexts: cyclic interference, periodic spectral sharing.
40    RecurrentBoundaryGrazing,
41    /// Confirmed ‖r(k)‖ > ρ_eff.
42    EnvelopeViolation,
43}
44
45/// The DSFB grammar state — the typed intermediate representation.
46///
47/// This is what operators see instead of a scalar alarm count.
48/// The typed state encodes both the severity and the structural character
49/// of the observed residual trajectory.
50#[derive(Debug, Clone, Copy, PartialEq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum GrammarState {
53    /// Residual within envelope, drift inward or bounded. Nominal operation.
54    Admissible,
55    /// Residual approaching envelope boundary with sustained outward drift
56    /// or recurrent grazing. Early-warning state.
57    Boundary(ReasonCode),
58    /// Residual has exited envelope. Structural fault state.
59    Violation,
60}
61
62impl GrammarState {
63    /// Returns true if this state warrants any operator attention.
64    #[inline]
65    pub fn requires_attention(&self) -> bool {
66        !matches!(self, GrammarState::Admissible)
67    }
68
69    /// Returns true if this is a Violation state.
70    #[inline]
71    pub fn is_violation(&self) -> bool {
72        matches!(self, GrammarState::Violation)
73    }
74
75    /// Returns true if this is a Boundary state.
76    #[inline]
77    pub fn is_boundary(&self) -> bool {
78        matches!(self, GrammarState::Boundary(_))
79    }
80
81    /// Severity level: 0=Admissible, 1=Boundary, 2=Violation.
82    #[inline]
83    pub fn severity(&self) -> u8 {
84        match self {
85            GrammarState::Admissible => 0,
86            GrammarState::Boundary(_) => 1,
87            GrammarState::Violation => 2,
88        }
89    }
90
91    /// Severity-based trust scalar T ∈ [0, 1].
92    ///
93    /// Returns a deterministic, bounded trust weight that downstream stages
94    /// can use to *down-weight* grammar evidence that is already at
95    /// boundary or violation.  This is the semiotics-engine `trust_scalar_for()`
96    /// severity dimension (de Beer 2026, §IV):
97    ///
98    /// - Admissible  → 1.0  (full trust: nominal region, no structural concern)
99    /// - Boundary    → 0.5  (half trust: approach region, evidence partial)
100    /// - Violation   → 0.0  (no trust: outside envelope, evidence suppressed)
101    ///
102    /// Use `geometry_trust()` for a continuous, geometry-aware version.
103    #[inline]
104    pub fn severity_trust(&self) -> f32 {
105        match self {
106            GrammarState::Admissible       => 1.0,
107            GrammarState::Boundary(_)      => 0.5,
108            GrammarState::Violation        => 0.0,
109        }
110    }
111
112    /// Geometry-based grammar trust scalar T ∈ [0, 1].
113    ///
114    /// Provides a *continuous* trust measure based on how far inside the
115    /// admissibility envelope the current residual norm lies, within the
116    /// boundary band.  Derived from semiotics-engine eq. (trust_scalar_for):
117    ///
118    /// ```text
119    /// margin        = (ρ − ‖r‖) / ρ              (normalised inward distance)
120    /// T             = clamp(margin / band_frac, 0, 1)
121    /// ```
122    ///
123    /// `band_frac` is the boundary band width as a fraction of ρ
124    /// (semiotics-engine default: 0.04 = 4 %).
125    ///
126    /// ## Semantics
127    ///
128    /// - T = 1.0: residual deep inside envelope — full confidence
129    /// - T ≈ 0.5: residual halfway through the boundary band
130    /// - T = 0.0: residual at or outside the envelope boundary — suppressed
131    ///
132    /// Independent of grammar state: can be used even when the FSM is in
133    /// Admissible but the norm is close to ρ.
134    #[inline]
135    pub fn geometry_trust(norm: f32, rho: f32, band_frac: f32) -> f32 {
136        if rho <= 1e-30 { return 0.0; }
137        let margin = (rho - norm) / rho;
138        if band_frac < 1e-12 {
139            return if margin >= 0.0 { 1.0 } else { 0.0 };
140        }
141        let t = margin / band_frac;
142        t.max(0.0).min(1.0)
143    }
144
145    /// Combined grammar trust scalar: minimum of severity_trust and geometry_trust.
146    ///
147    /// Takes the more conservative of the two trust dimensions.
148    /// This is the recommended scalar for downstream weighting (e.g., DSA
149    /// score blending, HRET combination).
150    #[inline]
151    pub fn combined_trust(&self, norm: f32, rho: f32, band_frac: f32) -> f32 {
152        let st = self.severity_trust();
153        let gt = GrammarState::geometry_trust(norm, rho, band_frac);
154        st.min(gt)
155    }
156}
157
158/// Grammar evaluator with hysteresis and boundary-grazing history.
159///
160/// Generic `W` = drift window, `K` = persistence threshold.
161/// All state is stack-allocated; no heap, no unsafe.
162pub struct GrammarEvaluator<const K: usize> {
163    /// Pending (unconfirmed) grammar state awaiting hysteresis confirmation.
164    pending: GrammarState,
165    /// Confirmation counter for current pending state (0..=2).
166    confirmations: u8,
167    /// Confirmed (committed) grammar state.
168    committed: GrammarState,
169    /// Circular buffer of recent boundary-approach flags for grazing detection.
170    boundary_hits: [bool; K],
171    /// Write head for boundary_hits buffer.
172    hit_head: usize,
173    /// Number of boundary hits inserted so far (saturates at K).
174    hit_count: usize,
175}
176
177impl<const K: usize> GrammarEvaluator<K> {
178    /// Create a new evaluator initialized to Admissible.
179    pub const fn new() -> Self {
180        Self {
181            pending: GrammarState::Admissible,
182            confirmations: 0,
183            committed: GrammarState::Admissible,
184            boundary_hits: [false; K],
185            hit_head: 0,
186            hit_count: 0,
187        }
188    }
189
190    /// Current committed grammar state (after hysteresis).
191    #[inline]
192    pub fn state(&self) -> GrammarState {
193        self.committed
194    }
195
196    /// Evaluate the grammar state for one observation.
197    ///
198    /// Returns the committed grammar state after applying hysteresis.
199    /// If the waveform state is suppressed, forces Admissible.
200    pub fn evaluate(
201        &mut self,
202        sign: &SignTuple,
203        envelope: &AdmissibilityEnvelope,
204        waveform_state: WaveformState,
205    ) -> GrammarState {
206        // Suppressed window: force Admissible (paper §XIV-C, §B.4)
207        if waveform_state.is_suppressed() {
208            self.committed = GrammarState::Admissible;
209            self.pending = GrammarState::Admissible;
210            self.confirmations = 0;
211            return GrammarState::Admissible;
212        }
213
214        let multiplier = waveform_state.admissibility_multiplier();
215        let raw_state = self.compute_raw_state(sign, envelope, multiplier);
216
217        // Update boundary-grazing history
218        let is_boundary_approach = envelope.is_boundary_approach(sign.norm, multiplier)
219            && !envelope.is_violation(sign.norm, multiplier);
220        self.boundary_hits[self.hit_head] = is_boundary_approach;
221        self.hit_head = (self.hit_head + 1) % K;
222        if self.hit_count < K { self.hit_count += 1; }
223
224        // Apply hysteresis: require 2 consecutive confirmations (paper §B.4)
225        if raw_state == self.pending {
226            if self.confirmations < 2 {
227                self.confirmations += 1;
228            }
229            if self.confirmations >= 2 {
230                self.committed = raw_state;
231            }
232        } else {
233            self.pending = raw_state;
234            self.confirmations = 1;
235        }
236
237        self.committed
238    }
239
240    /// Compute raw grammar state (before hysteresis).
241    fn compute_raw_state(
242        &self,
243        sign: &SignTuple,
244        envelope: &AdmissibilityEnvelope,
245        multiplier: f32,
246    ) -> GrammarState {
247        // Violation check first (hardest condition)
248        if envelope.is_violation(sign.norm, multiplier) {
249            return GrammarState::Violation;
250        }
251
252        // Boundary: outward drift
253        if envelope.is_boundary_approach(sign.norm, multiplier) {
254            if sign.is_outward_drift() {
255                return GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
256            }
257            if sign.is_abrupt_slew(envelope.delta_s) {
258                return GrammarState::Boundary(ReasonCode::AbruptSlewViolation);
259            }
260        }
261
262        // Boundary: recurrent grazing — K hits in the last K observations
263        let grazing_hits = self.boundary_hits.iter().filter(|&&h| h).count();
264        if self.hit_count >= K && grazing_hits >= K {
265            return GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing);
266        }
267
268        GrammarState::Admissible
269    }
270
271    /// Reset the evaluator (e.g., after a post-transition guard expires).
272    pub fn reset(&mut self) {
273        *self = Self::new();
274    }
275}
276
277impl<const K: usize> Default for GrammarEvaluator<K> {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283// ---------------------------------------------------------------
284// Tests
285// ---------------------------------------------------------------
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::envelope::AdmissibilityEnvelope;
290    use crate::sign::SignTuple;
291    use crate::platform::WaveformState;
292
293    fn make_envelope() -> AdmissibilityEnvelope {
294        AdmissibilityEnvelope::new(0.1)
295    }
296
297    #[test]
298    fn clean_signal_is_admissible() {
299        let mut eval = GrammarEvaluator::<4>::new();
300        let env = make_envelope();
301        for _ in 0..5 {
302            let sig = SignTuple::new(0.02, 0.0, 0.0);
303            let state = eval.evaluate(&sig, &env, WaveformState::Operational);
304            assert_eq!(state, GrammarState::Admissible);
305        }
306    }
307
308    #[test]
309    fn violation_detected_after_hysteresis() {
310        let mut eval = GrammarEvaluator::<4>::new();
311        let env = make_envelope();
312        // First observation: violation — pending but not yet confirmed
313        let sig = SignTuple::new(0.15, 0.02, 0.001);
314        let s1 = eval.evaluate(&sig, &env, WaveformState::Operational);
315        // After 2 confirmations: committed Violation
316        let s2 = eval.evaluate(&sig, &env, WaveformState::Operational);
317        assert_eq!(s2, GrammarState::Violation, "s1={:?} s2={:?}", s1, s2);
318    }
319
320    #[test]
321    fn transient_spike_dismissed_by_hysteresis() {
322        let mut eval = GrammarEvaluator::<4>::new();
323        let env = make_envelope();
324        // Single violation then immediate recovery
325        let above = SignTuple::new(0.15, 0.02, 0.0);
326        let below = SignTuple::new(0.02, 0.0, 0.0);
327        eval.evaluate(&above, &env, WaveformState::Operational);
328        // Recovery before 2nd confirmation: hysteresis resets
329        let state = eval.evaluate(&below, &env, WaveformState::Operational);
330        assert_eq!(state, GrammarState::Admissible,
331            "single transient should be dismissed by hysteresis");
332    }
333
334    #[test]
335    fn transition_suppresses_violation() {
336        let mut eval = GrammarEvaluator::<4>::new();
337        let env = make_envelope();
338        let huge = SignTuple::new(1000.0, 100.0, 10.0);
339        // Even a massive residual must produce Admissible during transition
340        for _ in 0..5 {
341            let state = eval.evaluate(&huge, &env, WaveformState::Transition);
342            assert_eq!(state, GrammarState::Admissible);
343        }
344    }
345
346    #[test]
347    fn sustained_outward_drift_detected() {
348        let mut eval = GrammarEvaluator::<4>::new();
349        let env = make_envelope();
350        // Norm in boundary zone (> 0.05), positive drift
351        let sig = SignTuple::new(0.07, 0.005, 0.0001);
352        eval.evaluate(&sig, &env, WaveformState::Operational);
353        let state = eval.evaluate(&sig, &env, WaveformState::Operational);
354        assert_eq!(state,
355            GrammarState::Boundary(ReasonCode::SustainedOutwardDrift));
356    }
357
358    #[test]
359    fn grammar_state_severity_ordering() {
360        assert!(GrammarState::Violation.severity() >
361                GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).severity());
362        assert!(GrammarState::Boundary(ReasonCode::EnvelopeViolation).severity() >
363                GrammarState::Admissible.severity());
364    }
365
366    #[test]
367    fn severity_trust_bounded_and_ordered() {
368        let t_adm = GrammarState::Admissible.severity_trust();
369        let t_bnd = GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).severity_trust();
370        let t_vio = GrammarState::Violation.severity_trust();
371        assert!((t_adm - 1.0).abs() < 1e-6);
372        assert!((t_bnd - 0.5).abs() < 1e-6);
373        assert!((t_vio - 0.0).abs() < 1e-6);
374        assert!(t_adm > t_bnd);
375        assert!(t_bnd > t_vio);
376    }
377
378    #[test]
379    fn geometry_trust_deep_inside() {
380        // norm = 0, rho = 0.10, band = 4% → margin = 1.0 → T = 1.0 / 0.04 → clamped 1.0
381        let t = GrammarState::geometry_trust(0.0, 0.10, 0.04);
382        assert!((t - 1.0).abs() < 1e-6, "deep inside → T=1.0, got {}", t);
383    }
384
385    #[test]
386    fn geometry_trust_at_boundary() {
387        // norm = rho → margin = 0 → T = 0
388        let t = GrammarState::geometry_trust(0.10, 0.10, 0.04);
389        assert!((t - 0.0).abs() < 1e-6, "at boundary → T=0.0, got {}", t);
390    }
391
392    #[test]
393    fn geometry_trust_interpolates() {
394        // norm = 0.098, rho = 0.10, band = 4%
395        // margin = (0.10 - 0.098) / 0.10 = 0.02
396        // T = 0.02 / 0.04 = 0.5
397        let t = GrammarState::geometry_trust(0.098, 0.10, 0.04);
398        assert!((t - 0.5).abs() < 1e-5, "midpoint of band → T=0.5, got {}", t);
399    }
400
401    #[test]
402    fn combined_trust_takes_minimum() {
403        // Outside envelope: geometry = 0, severity_trust(Violation) = 0 → combined = 0
404        let t = GrammarState::Violation.combined_trust(0.15, 0.10, 0.04);
405        assert!((t - 0.0).abs() < 1e-6);
406        // Deep inside, Admissible → combined = 1.0
407        let t2 = GrammarState::Admissible.combined_trust(0.01, 0.10, 0.04);
408        assert!((t2 - 1.0).abs() < 1e-6);
409    }
410}