Skip to main content

dsfb_rf/
syntax.rs

1//! Syntax layer: classify sign tuples into named temporal motifs.
2//!
3//! The syntax layer sits between the sign tuple and the grammar state.
4//! It maps σ(k) = (‖r‖, ṙ, r̈) onto a named `MotifClass`, which
5//! the heuristics bank then looks up to retrieve operator-facing
6//! semantic dispositions and provenance.
7//!
8//! ## RF Motif Classes (paper §V-F)
9//!
10//! | Class | Signature | RF Context |
11//! |---|---|---|
12//! | PreFailureSlowDrift | norm in [0.3ρ,ρ], ṙ>0 sustained | PA thermal drift, LO aging |
13//! | TransientExcursion | norm > ρ for 1–2 obs, rapid recovery | Single-sample noise spike |
14//! | RecurrentBoundaryApproach | periodic boundary entries | Cyclic interference |
15//! | AbruptOnset | r̈ >> 0 with large norm jump | Jamming onset, HW fault |
16//! | SpectralMaskApproach | norm drifting toward 1.0 (normalized mask) | TX power creep |
17//! | PhaseNoiseExcursion | oscillatory ṙ with growing |ṙ| | Oscillator aging |
18//! | FreqHopTransition | abrupt slew + recovery to new baseline | FHSS waveform boundary |
19//! | Unknown | no pattern matches | Endoductive: return σ(k) for operator |
20
21use crate::sign::SignTuple;
22use crate::grammar::GrammarState;
23
24/// Named temporal motif class.
25///
26/// The syntax layer maps (sign_tuple, grammar_state) → MotifClass.
27/// This is the input to the heuristics bank lookup.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum MotifClass {
31    /// Persistent positive ṙ while norm approaches ρ.
32    /// Primary pre-transition precursor motif.
33    PreFailureSlowDrift,
34    /// Brief norm spike above ρ with rapid recovery (< 2 observations).
35    TransientExcursion,
36    /// Repeated near-boundary excursions in a rolling window.
37    RecurrentBoundaryApproach,
38    /// Abrupt large slew: |r̈| > δ_abrupt.
39    /// Consistent with jamming onset or hardware fault.
40    AbruptOnset,
41    /// Monotone outward drift toward normalized mask boundary (norm → 1.0).
42    SpectralMaskApproach,
43    /// Oscillatory ṙ with growing amplitude.
44    /// Consistent with phase noise or oscillator aging.
45    PhaseNoiseExcursion,
46    /// Abrupt slew followed by rapid stabilization at a new norm baseline.
47    /// Consistent with FHSS waveform transition (should be suppressed by platform context).
48    FreqHopTransition,
49    /// No motif pattern matched. Endoductive regime.
50    /// Operator receives the full σ(k) trajectory; DSFB returns semantic Unknown.
51    Unknown,
52    /// Monotone linear norm increase with near-zero second derivative.
53    /// Signature: ṙ > threshold, |r̈| ≈ 0 (constant-rate gain ramp).
54    /// RF context: LNA thermal runaway, progressive gain collapse.
55    /// Structurally distinct from `PreFailureSlowDrift`: the gain ramp is
56    /// linear (no acceleration) and starts below 30% ρ.
57    LnaGainInstability,
58    /// Recurrent boundary grazing with oscillatory slew pattern.
59    /// Signature: `RecurrentBoundaryGrazing` reason code AND |r̈| > 0.
60    /// RF context: LO phase noise excursion, oscillator aging or vibration.
61    /// Carries an Allan-deviation instability character distinguishable from
62    /// `RecurrentBoundaryApproach` (which has no oscillatory slew).
63    LoInstabilityPrecursor,
64}
65
66/// Thresholds for syntax classification. All are dimensionless fractions
67/// of ρ or absolute rates, set from the paper's Stage III protocol.
68#[derive(Debug, Clone, Copy)]
69pub struct SyntaxThresholds {
70    /// Minimum drift rate to qualify as SlowDrift motif.
71    pub drift_threshold: f32,
72    /// Minimum |r̈| to qualify as AbruptOnset motif.
73    pub abrupt_slew_threshold: f32,
74    /// Norm fraction above which SpectralMaskApproach is considered.
75    pub mask_approach_frac: f32,
76    /// Maximum norm for TransientExcursion (above rho but recovers).
77    pub transient_max_overshoot: f32,
78}
79
80impl Default for SyntaxThresholds {
81    fn default() -> Self {
82        Self {
83            drift_threshold: 0.002,
84            abrupt_slew_threshold: 0.05,
85            mask_approach_frac: 0.80,
86            transient_max_overshoot: 2.0, // up to 2× ρ for transient
87        }
88    }
89}
90
91/// Classify a sign tuple and grammar state into a named motif.
92///
93/// This is a pure deterministic function: identical inputs always
94/// produce identical outputs (Theorem 9 of the paper).
95pub fn classify(
96    sign: &SignTuple,
97    grammar: GrammarState,
98    rho: f32,
99    thresholds: &SyntaxThresholds,
100) -> MotifClass {
101    try_violation_motif(sign, grammar, rho, thresholds)
102        .or_else(|| try_recurrent_grazing_motif(sign, grammar, thresholds))
103        .or_else(|| try_boundary_drift_motif(sign, grammar, rho, thresholds))
104        .or_else(|| try_boundary_slew_motif(sign, grammar, rho, thresholds))
105        .unwrap_or(MotifClass::Unknown)
106}
107
108fn try_violation_motif(
109    sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
110) -> Option<MotifClass> {
111    if grammar.is_violation() && sign.slew.abs() > thresholds.abrupt_slew_threshold {
112        return Some(MotifClass::AbruptOnset);
113    }
114    if grammar.is_violation()
115        && sign.norm < rho * thresholds.transient_max_overshoot
116        && sign.drift.abs() < thresholds.drift_threshold * 5.0
117    {
118        return Some(MotifClass::TransientExcursion);
119    }
120    None
121}
122
123fn try_recurrent_grazing_motif(
124    sign: &SignTuple, grammar: GrammarState, thresholds: &SyntaxThresholds,
125) -> Option<MotifClass> {
126    if let GrammarState::Boundary(crate::grammar::ReasonCode::RecurrentBoundaryGrazing) = grammar {
127        if sign.slew.abs() > thresholds.drift_threshold * 1.5 {
128            return Some(MotifClass::LoInstabilityPrecursor);
129        }
130        return Some(MotifClass::RecurrentBoundaryApproach);
131    }
132    None
133}
134
135fn try_boundary_drift_motif(
136    sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
137) -> Option<MotifClass> {
138    if !grammar.is_boundary() { return None; }
139    if sign.drift > thresholds.drift_threshold
140        && sign.norm < rho * 0.30
141        && sign.slew.abs() < thresholds.drift_threshold * 0.5
142    {
143        return Some(MotifClass::LnaGainInstability);
144    }
145    if sign.drift > thresholds.drift_threshold
146        && sign.norm > rho * 0.30
147        && sign.norm <= rho
148    {
149        return Some(MotifClass::PreFailureSlowDrift);
150    }
151    if sign.norm > rho * thresholds.mask_approach_frac && sign.drift > 0.0 {
152        return Some(MotifClass::SpectralMaskApproach);
153    }
154    None
155}
156
157fn try_boundary_slew_motif(
158    sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
159) -> Option<MotifClass> {
160    if !grammar.is_boundary() { return None; }
161    if sign.slew.abs() > thresholds.drift_threshold * 2.0 && sign.norm > rho * 0.3 {
162        return Some(MotifClass::PhaseNoiseExcursion);
163    }
164    if sign.slew.abs() > thresholds.abrupt_slew_threshold * 0.5 {
165        return Some(MotifClass::FreqHopTransition);
166    }
167    None
168}
169
170// ---------------------------------------------------------------
171// Tests
172// ---------------------------------------------------------------
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::grammar::{GrammarState, ReasonCode};
177
178    fn thresh() -> SyntaxThresholds { SyntaxThresholds::default() }
179
180    #[test]
181    fn slow_drift_classified() {
182        let sign = SignTuple::new(0.07, 0.005, 0.0001);
183        let grammar = GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
184        let motif = classify(&sign, grammar, 0.1, &thresh());
185        assert_eq!(motif, MotifClass::PreFailureSlowDrift);
186    }
187
188    #[test]
189    fn abrupt_onset_classified() {
190        let sign = SignTuple::new(0.15, 0.01, 0.1);
191        let grammar = GrammarState::Violation;
192        let motif = classify(&sign, grammar, 0.1, &thresh());
193        assert_eq!(motif, MotifClass::AbruptOnset);
194    }
195
196    #[test]
197    fn admissible_with_no_drift_is_unknown() {
198        let sign = SignTuple::new(0.02, 0.0, 0.0);
199        let grammar = GrammarState::Admissible;
200        let motif = classify(&sign, grammar, 0.1, &thresh());
201        assert_eq!(motif, MotifClass::Unknown);
202    }
203
204    #[test]
205    fn recurrent_grazing_classified() {
206        let sign = SignTuple::new(0.06, 0.001, 0.0);
207        let grammar = GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing);
208        let motif = classify(&sign, grammar, 0.1, &thresh());
209        assert_eq!(motif, MotifClass::RecurrentBoundaryApproach);
210    }
211}