use crate::sign::SignTuple;
use crate::grammar::GrammarState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MotifClass {
PreFailureSlowDrift,
TransientExcursion,
RecurrentBoundaryApproach,
AbruptOnset,
SpectralMaskApproach,
PhaseNoiseExcursion,
FreqHopTransition,
Unknown,
LnaGainInstability,
LoInstabilityPrecursor,
}
#[derive(Debug, Clone, Copy)]
pub struct SyntaxThresholds {
pub drift_threshold: f32,
pub abrupt_slew_threshold: f32,
pub mask_approach_frac: f32,
pub transient_max_overshoot: f32,
}
impl Default for SyntaxThresholds {
fn default() -> Self {
Self {
drift_threshold: 0.002,
abrupt_slew_threshold: 0.05,
mask_approach_frac: 0.80,
transient_max_overshoot: 2.0, }
}
}
pub fn classify(
sign: &SignTuple,
grammar: GrammarState,
rho: f32,
thresholds: &SyntaxThresholds,
) -> MotifClass {
try_violation_motif(sign, grammar, rho, thresholds)
.or_else(|| try_recurrent_grazing_motif(sign, grammar, thresholds))
.or_else(|| try_boundary_drift_motif(sign, grammar, rho, thresholds))
.or_else(|| try_boundary_slew_motif(sign, grammar, rho, thresholds))
.unwrap_or(MotifClass::Unknown)
}
fn try_violation_motif(
sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
) -> Option<MotifClass> {
if grammar.is_violation() && sign.slew.abs() > thresholds.abrupt_slew_threshold {
return Some(MotifClass::AbruptOnset);
}
if grammar.is_violation()
&& sign.norm < rho * thresholds.transient_max_overshoot
&& sign.drift.abs() < thresholds.drift_threshold * 5.0
{
return Some(MotifClass::TransientExcursion);
}
None
}
fn try_recurrent_grazing_motif(
sign: &SignTuple, grammar: GrammarState, thresholds: &SyntaxThresholds,
) -> Option<MotifClass> {
if let GrammarState::Boundary(crate::grammar::ReasonCode::RecurrentBoundaryGrazing) = grammar {
if sign.slew.abs() > thresholds.drift_threshold * 1.5 {
return Some(MotifClass::LoInstabilityPrecursor);
}
return Some(MotifClass::RecurrentBoundaryApproach);
}
None
}
fn try_boundary_drift_motif(
sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
) -> Option<MotifClass> {
if !grammar.is_boundary() { return None; }
if sign.drift > thresholds.drift_threshold
&& sign.norm < rho * 0.30
&& sign.slew.abs() < thresholds.drift_threshold * 0.5
{
return Some(MotifClass::LnaGainInstability);
}
if sign.drift > thresholds.drift_threshold
&& sign.norm > rho * 0.30
&& sign.norm <= rho
{
return Some(MotifClass::PreFailureSlowDrift);
}
if sign.norm > rho * thresholds.mask_approach_frac && sign.drift > 0.0 {
return Some(MotifClass::SpectralMaskApproach);
}
None
}
fn try_boundary_slew_motif(
sign: &SignTuple, grammar: GrammarState, rho: f32, thresholds: &SyntaxThresholds,
) -> Option<MotifClass> {
if !grammar.is_boundary() { return None; }
if sign.slew.abs() > thresholds.drift_threshold * 2.0 && sign.norm > rho * 0.3 {
return Some(MotifClass::PhaseNoiseExcursion);
}
if sign.slew.abs() > thresholds.abrupt_slew_threshold * 0.5 {
return Some(MotifClass::FreqHopTransition);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::{GrammarState, ReasonCode};
fn thresh() -> SyntaxThresholds { SyntaxThresholds::default() }
#[test]
fn slow_drift_classified() {
let sign = SignTuple::new(0.07, 0.005, 0.0001);
let grammar = GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
let motif = classify(&sign, grammar, 0.1, &thresh());
assert_eq!(motif, MotifClass::PreFailureSlowDrift);
}
#[test]
fn abrupt_onset_classified() {
let sign = SignTuple::new(0.15, 0.01, 0.1);
let grammar = GrammarState::Violation;
let motif = classify(&sign, grammar, 0.1, &thresh());
assert_eq!(motif, MotifClass::AbruptOnset);
}
#[test]
fn admissible_with_no_drift_is_unknown() {
let sign = SignTuple::new(0.02, 0.0, 0.0);
let grammar = GrammarState::Admissible;
let motif = classify(&sign, grammar, 0.1, &thresh());
assert_eq!(motif, MotifClass::Unknown);
}
#[test]
fn recurrent_grazing_classified() {
let sign = SignTuple::new(0.06, 0.001, 0.0);
let grammar = GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing);
let motif = classify(&sign, grammar, 0.1, &thresh());
assert_eq!(motif, MotifClass::RecurrentBoundaryApproach);
}
}