1use crate::sign::SignTuple;
22use crate::grammar::GrammarState;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum MotifClass {
31 PreFailureSlowDrift,
34 TransientExcursion,
36 RecurrentBoundaryApproach,
38 AbruptOnset,
41 SpectralMaskApproach,
43 PhaseNoiseExcursion,
46 FreqHopTransition,
49 Unknown,
52 LnaGainInstability,
58 LoInstabilityPrecursor,
64}
65
66#[derive(Debug, Clone, Copy)]
69pub struct SyntaxThresholds {
70 pub drift_threshold: f32,
72 pub abrupt_slew_threshold: f32,
74 pub mask_approach_frac: f32,
76 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, }
88 }
89}
90
91pub 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#[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}