use crate::grammar::GrammarState;
use crate::syntax::MotifClass;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Provenance {
FrameworkDesign,
PublicDataObserved,
FieldValidated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SemanticDisposition {
PreTransitionCluster,
CorroboratingDrift,
TransientNoise,
RecurrentPattern,
AbruptOnsetEvent,
MaskApproach,
PhaseNoiseDegradation,
Unknown,
LnaGainInstability,
LoInstabilityPrecursor,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MotifEntry {
pub motif_class: MotifClass,
pub min_severity: u8,
pub disposition: SemanticDisposition,
pub provenance: Provenance,
pub description: &'static str,
}
impl MotifEntry {
#[inline]
pub fn matches(&self, motif: MotifClass, grammar: GrammarState) -> bool {
self.motif_class == motif && grammar.severity() >= self.min_severity
}
}
pub struct HeuristicsBank<const M: usize> {
entries: [Option<MotifEntry>; M],
count: usize,
}
impl<const M: usize> HeuristicsBank<M> {
pub const fn empty() -> Self {
Self {
entries: [None; M],
count: 0,
}
}
pub fn default_rf() -> Self {
const MOTIFS: [MotifEntry; 9] = [
MotifEntry { motif_class: MotifClass::PreFailureSlowDrift, min_severity: 1, disposition: SemanticDisposition::PreTransitionCluster, provenance: Provenance::FrameworkDesign, description: "Persistent outward drift toward boundary" },
MotifEntry { motif_class: MotifClass::TransientExcursion, min_severity: 2, disposition: SemanticDisposition::TransientNoise, provenance: Provenance::FrameworkDesign, description: "Brief violation with rapid recovery" },
MotifEntry { motif_class: MotifClass::RecurrentBoundaryApproach, min_severity: 1, disposition: SemanticDisposition::RecurrentPattern, provenance: Provenance::FrameworkDesign, description: "Repeated near-boundary excursions" },
MotifEntry { motif_class: MotifClass::AbruptOnset, min_severity: 2, disposition: SemanticDisposition::AbruptOnsetEvent, provenance: Provenance::FrameworkDesign, description: "Abrupt large slew" },
MotifEntry { motif_class: MotifClass::SpectralMaskApproach, min_severity: 1, disposition: SemanticDisposition::MaskApproach, provenance: Provenance::FrameworkDesign, description: "Monotone outward drift toward mask edge" },
MotifEntry { motif_class: MotifClass::PhaseNoiseExcursion, min_severity: 1, disposition: SemanticDisposition::PhaseNoiseDegradation, provenance: Provenance::FrameworkDesign, description: "Oscillatory slew with growing amplitude" },
MotifEntry { motif_class: MotifClass::FreqHopTransition, min_severity: 1, disposition: SemanticDisposition::TransientNoise, provenance: Provenance::FrameworkDesign, description: "FHSS waveform transition (suppressible)" },
MotifEntry { motif_class: MotifClass::LnaGainInstability, min_severity: 1, disposition: SemanticDisposition::LnaGainInstability, provenance: Provenance::FrameworkDesign, description: "Linear gain ramp, near-zero second derivative" },
MotifEntry { motif_class: MotifClass::LoInstabilityPrecursor, min_severity: 1, disposition: SemanticDisposition::LoInstabilityPrecursor, provenance: Provenance::FrameworkDesign, description: "Recurrent boundary grazing with oscillatory slew" },
];
let mut bank = Self::empty();
for entry in MOTIFS {
bank.register(entry);
}
bank
}
pub fn register(&mut self, entry: MotifEntry) -> bool {
if self.count >= M {
return false;
}
self.entries[self.count] = Some(entry);
self.count += 1;
true
}
pub fn lookup(&self, motif: MotifClass, grammar: GrammarState) -> SemanticDisposition {
for i in 0..self.count {
if let Some(ref entry) = self.entries[i] {
if entry.matches(motif, grammar) {
return entry.disposition;
}
}
}
SemanticDisposition::Unknown
}
#[inline]
pub fn len(&self) -> usize {
self.count
}
#[inline]
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn entries(&self) -> impl Iterator<Item = &MotifEntry> {
self.entries[..self.count]
.iter()
.filter_map(|e| e.as_ref())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KnownClockClass {
TcxoSteadyState,
OcxoWarmup,
FreeRunXtal,
PllAcquisition,
LowNoiseOcxo,
Unknown,
}
impl KnownClockClass {
pub const fn label(self) -> &'static str {
match self {
KnownClockClass::TcxoSteadyState => "TcxoSteadyState",
KnownClockClass::OcxoWarmup => "OcxoWarmup",
KnownClockClass::FreeRunXtal => "FreeRunXtal",
KnownClockClass::PllAcquisition => "PllAcquisition",
KnownClockClass::LowNoiseOcxo => "LowNoiseOcxo",
KnownClockClass::Unknown => "Unknown",
}
}
pub const fn is_internal_cause(self) -> bool {
matches!(
self,
KnownClockClass::TcxoSteadyState
| KnownClockClass::OcxoWarmup
| KnownClockClass::FreeRunXtal
| KnownClockClass::PllAcquisition
| KnownClockClass::LowNoiseOcxo
)
}
}
pub fn classify_clock_instability(sigma_y: &[f32], taus: &[f32]) -> KnownClockClass {
if sigma_y.len() < 3 || taus.len() < 3 || sigma_y.len() != taus.len() {
return KnownClockClass::Unknown;
}
let (sum_x, sum_y, sum_xx, sum_xy, m) = accumulate_log_sums(sigma_y, taus);
if m < 3 {
return KnownClockClass::Unknown;
}
let mf = m as f32;
let denom = mf * sum_xx - sum_x * sum_x;
if denom.abs() < 1e-9 {
return KnownClockClass::Unknown;
}
let alpha = (mf * sum_xy - sum_x * sum_y) / denom;
classify_slope(alpha)
}
fn accumulate_log_sums(sigma_y: &[f32], taus: &[f32]) -> (f32, f32, f32, f32, u32) {
let log = |v: f32| -> f32 { crate::math::ln_f32(v.max(1e-38)) };
let n = sigma_y.len().min(taus.len());
let mut sum_x = 0.0_f32;
let mut sum_y = 0.0_f32;
let mut sum_xx = 0.0_f32;
let mut sum_xy = 0.0_f32;
let mut m = 0u32;
for i in 0..n {
if taus[i] > 0.0 && sigma_y[i] > 0.0 {
let lx = log(taus[i]);
let ly = log(sigma_y[i]);
sum_x += lx;
sum_y += ly;
sum_xx += lx * lx;
sum_xy += lx * ly;
m += 1;
}
}
(sum_x, sum_y, sum_xx, sum_xy, m)
}
fn classify_slope(alpha: f32) -> KnownClockClass {
if alpha < -1.2 {
KnownClockClass::LowNoiseOcxo
} else if alpha <= -0.7 {
KnownClockClass::TcxoSteadyState
} else if alpha < -0.1 {
KnownClockClass::OcxoWarmup
} else if alpha < 0.2 {
KnownClockClass::PllAcquisition
} else {
KnownClockClass::FreeRunXtal
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::GrammarState;
#[test]
fn default_bank_has_nine_entries() {
let bank = HeuristicsBank::<32>::default_rf();
assert_eq!(bank.len(), 9);
}
#[test]
fn slow_drift_lookup_returns_pre_transition() {
let bank = HeuristicsBank::<32>::default_rf();
let disp = bank.lookup(
MotifClass::PreFailureSlowDrift,
GrammarState::Boundary(crate::grammar::ReasonCode::SustainedOutwardDrift),
);
assert_eq!(disp, SemanticDisposition::PreTransitionCluster);
}
#[test]
fn unknown_motif_returns_unknown() {
let bank = HeuristicsBank::<32>::default_rf();
let disp = bank.lookup(MotifClass::Unknown, GrammarState::Admissible);
assert_eq!(disp, SemanticDisposition::Unknown);
}
#[test]
fn abrupt_onset_lookup() {
let bank = HeuristicsBank::<32>::default_rf();
let disp = bank.lookup(
MotifClass::AbruptOnset,
GrammarState::Violation,
);
assert_eq!(disp, SemanticDisposition::AbruptOnsetEvent);
}
#[test]
fn bank_register_beyond_capacity_returns_false() {
let mut bank = HeuristicsBank::<2>::empty();
let entry = MotifEntry {
motif_class: MotifClass::Unknown,
min_severity: 0,
disposition: SemanticDisposition::Unknown,
provenance: Provenance::FrameworkDesign,
description: "test",
};
assert!(bank.register(entry));
assert!(bank.register(entry));
assert!(!bank.register(entry), "should be full at M=2");
}
#[test]
fn transient_excursion_requires_violation_severity() {
let bank = HeuristicsBank::<32>::default_rf();
let disp = bank.lookup(
MotifClass::TransientExcursion,
GrammarState::Boundary(crate::grammar::ReasonCode::SustainedOutwardDrift),
);
assert_eq!(disp, SemanticDisposition::Unknown,
"TransientExcursion requires Violation severity");
}
#[test]
fn clock_labels_are_correct() {
assert_eq!(KnownClockClass::TcxoSteadyState.label(), "TcxoSteadyState");
assert_eq!(KnownClockClass::OcxoWarmup.label(), "OcxoWarmup");
assert_eq!(KnownClockClass::FreeRunXtal.label(), "FreeRunXtal");
assert_eq!(KnownClockClass::PllAcquisition.label(), "PllAcquisition");
assert_eq!(KnownClockClass::LowNoiseOcxo.label(), "LowNoiseOcxo");
}
#[test]
fn clock_all_are_internal() {
for &cls in &[
KnownClockClass::TcxoSteadyState,
KnownClockClass::OcxoWarmup,
KnownClockClass::FreeRunXtal,
KnownClockClass::PllAcquisition,
KnownClockClass::LowNoiseOcxo,
] {
assert!(cls.is_internal_cause(), "{:?} should be internal", cls);
}
assert!(!KnownClockClass::Unknown.is_internal_cause());
}
#[test]
fn classify_tcxo_steady_state_slope_minus_one() {
let taus: [f32; 5] = [1.0, 2.0, 4.0, 8.0, 16.0];
let sigma_y: [f32; 5] = [1e-11, 0.5e-11, 0.25e-11, 0.125e-11, 0.0625e-11];
let cls = classify_clock_instability(&sigma_y, &taus);
assert_eq!(cls, KnownClockClass::TcxoSteadyState, "α≈-1 slope: {:?}", cls);
}
#[test]
fn classify_freerun_xtal_slope_plus_half() {
let taus: [f32; 5] = [1.0, 4.0, 9.0, 16.0, 25.0];
let sigma_y: [f32; 5] = [1e-11, 2e-11, 3e-11, 4e-11, 5e-11];
let cls = classify_clock_instability(&sigma_y, &taus);
assert_eq!(cls, KnownClockClass::FreeRunXtal, "α≈+0.5 slope: {:?}", cls);
}
#[test]
fn classify_too_few_points_returns_unknown() {
let taus: [f32; 2] = [1.0, 2.0];
let sigma_y: [f32; 2] = [1e-11, 0.5e-11];
assert_eq!(classify_clock_instability(&sigma_y, &taus), KnownClockClass::Unknown);
}
}