use crate::grammar::ReasonCode;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HorizonPoint {
pub snr_db: f32,
pub drift_rate: f32,
pub detected: bool,
pub detection_latency: u32,
}
pub struct SemioticHorizon<const N: usize> {
points: [HorizonPoint; N],
count: usize,
}
impl<const N: usize> SemioticHorizon<N> {
pub const fn new() -> Self {
Self {
points: [HorizonPoint {
snr_db: 0.0,
drift_rate: 0.0,
detected: false,
detection_latency: 0,
}; N],
count: 0,
}
}
pub fn record(&mut self, snr_db: f32, drift_rate: f32, detected: bool, latency: u32) -> bool {
if self.count >= N { return false; }
self.points[self.count] = HorizonPoint {
snr_db,
drift_rate,
detected,
detection_latency: latency,
};
self.count += 1;
true
}
pub fn len(&self) -> usize { self.count }
pub fn is_empty(&self) -> bool { self.count == 0 }
pub fn points(&self) -> &[HorizonPoint] {
&self.points[..self.count]
}
pub fn detection_rate(&self) -> f32 {
if self.count == 0 { return 0.0; }
let detected = self.points[..self.count].iter().filter(|p| p.detected).count();
detected as f32 / self.count as f32
}
pub fn mean_detection_latency(&self) -> f32 {
let detected: &[HorizonPoint] = &self.points[..self.count];
let (sum, count) = detected.iter()
.filter(|p| p.detected && p.detection_latency > 0)
.fold((0u64, 0u32), |(s, c), p| (s + p.detection_latency as u64, c + 1));
if count == 0 { return 0.0; }
sum as f32 / count as f32
}
}
impl<const N: usize> Default for SemioticHorizon<N> {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysicalMechanism {
PaThermalDrift,
LoAging,
PimOnset,
PhaseNoiseDegradation,
IntentionalJamming,
AdjacentChannelInterference,
FhssTransition,
AntennaCouplingTransient,
Unknown,
}
pub fn candidate_mechanisms(reason: ReasonCode) -> &'static [PhysicalMechanism] {
match reason {
ReasonCode::SustainedOutwardDrift => &[
PhysicalMechanism::PaThermalDrift,
PhysicalMechanism::LoAging,
PhysicalMechanism::AdjacentChannelInterference,
],
ReasonCode::AbruptSlewViolation => &[
PhysicalMechanism::IntentionalJamming,
PhysicalMechanism::PimOnset,
PhysicalMechanism::AntennaCouplingTransient,
],
ReasonCode::RecurrentBoundaryGrazing => &[
PhysicalMechanism::FhssTransition,
PhysicalMechanism::AdjacentChannelInterference,
],
ReasonCode::EnvelopeViolation => &[
PhysicalMechanism::IntentionalJamming,
PhysicalMechanism::PaThermalDrift,
],
}
}
pub fn model_reference(mechanism: PhysicalMechanism) -> &'static str {
match mechanism {
PhysicalMechanism::PaThermalDrift => "Arrhenius thermal acceleration model",
PhysicalMechanism::LoAging => "Allan variance / frequency stability model",
PhysicalMechanism::PimOnset => "Passive intermodulation model (3rd/5th order)",
PhysicalMechanism::PhaseNoiseDegradation => "Leeson's phase noise model",
PhysicalMechanism::IntentionalJamming => "J/S ratio and effective radiated power model",
PhysicalMechanism::AdjacentChannelInterference => "3GPP TS 36.141 §6.3 ACLR model",
PhysicalMechanism::FhssTransition => "Hop rate and dwell time analysis",
PhysicalMechanism::AntennaCouplingTransient => "Coupling coefficient and VSWR model",
PhysicalMechanism::Unknown => "Endoductive regime — no prior model",
}
}
pub trait PhysicsModel {
fn predict_drift_rate(&self, param: f32) -> f32;
fn label(&self) -> &'static str;
fn reference(&self) -> &'static str;
fn maps_to_reason(&self) -> ReasonCode;
}
#[derive(Debug, Clone, Copy)]
pub struct ArrheniusModel {
pub alpha_0: f32,
pub e_a_ev: f32,
pub label_str: &'static str,
}
impl ArrheniusModel {
pub const GAAS_PHEMT: Self = Self {
alpha_0: 1.0,
e_a_ev: 1.6,
label_str: "GaAs_pHEMT_Ea=1.6eV",
};
pub const GAN_HEMT: Self = Self {
alpha_0: 1.0,
e_a_ev: 2.1,
label_str: "GaN_HEMT_Ea=2.1eV",
};
}
impl PhysicsModel for ArrheniusModel {
fn predict_drift_rate(&self, temperature_celsius: f32) -> f32 {
let t_k = temperature_celsius + 273.15_f32;
let kb = 8.617_333e-5_f32;
self.alpha_0 * exp_approx(-self.e_a_ev / (kb * t_k))
}
fn label(&self) -> &'static str { self.label_str }
fn reference(&self) -> &'static str {
"Kayali 1999 JPL-96-25 Arrhenius thermal acceleration model"
}
fn maps_to_reason(&self) -> ReasonCode { ReasonCode::SustainedOutwardDrift }
}
#[derive(Debug, Clone, Copy)]
pub struct AllanVarianceModel {
pub h_white: f32,
pub h_flicker: f32,
pub h_rw: f32,
pub label_str: &'static str,
}
impl AllanVarianceModel {
pub const OCXO_CLASS_A: Self = Self {
h_white: 1e-9,
h_flicker: 1e-11,
h_rw: 1e-17,
label_str: "OCXO_Class_A",
};
pub const TCXO_GRADE_B: Self = Self {
h_white: 1e-7,
h_flicker: 1e-9,
h_rw: 1e-15,
label_str: "TCXO_Grade_B",
};
}
impl PhysicsModel for AllanVarianceModel {
fn predict_drift_rate(&self, tau: f32) -> f32 {
if tau <= 0.0 { return 0.0; }
let avar = self.h_white / (2.0 * tau)
+ self.h_flicker * 2.0 * 0.693_147_f32 + self.h_rw * (2.0 * 9.869_604_f32 / 3.0) * tau; crate::math::sqrt_f32(avar.max(0.0))
}
fn label(&self) -> &'static str { self.label_str }
fn reference(&self) -> &'static str {
"Allan 1966 Proc. IEEE 54(2):221-230; IEEE Std 1193-2003"
}
fn maps_to_reason(&self) -> ReasonCode { ReasonCode::SustainedOutwardDrift }
}
#[derive(Debug, Clone, Copy)]
pub struct PhysicsConsistencyResult {
pub predicted_drift: f32,
pub observed_drift: f32,
pub deviation_ratio: f32,
pub is_consistent: bool,
pub reason: ReasonCode,
}
pub fn evaluate_physics_consistency(
model: &dyn PhysicsModel,
observed_drift: f32,
platform_param: f32,
tolerance: f32,
) -> PhysicsConsistencyResult {
let predicted = model.predict_drift_rate(platform_param);
let deviation_ratio = if predicted > 1e-38 {
(observed_drift - predicted).abs() / predicted
} else {
f32::MAX
};
let is_consistent = deviation_ratio <= tolerance.abs();
PhysicsConsistencyResult {
predicted_drift: predicted,
observed_drift,
deviation_ratio,
is_consistent,
reason: model.maps_to_reason(),
}
}
fn exp_approx(x: f32) -> f32 {
let y = x * 1.442_695_f32;
let y = if y > 120.0 { 120.0 } else if y < -120.0 { -120.0 } else { y };
let n = if y >= 0.0 { y as i32 } else { y as i32 - 1 };
let frac = y - n as f32;
let ln2 = 0.693_147_f32;
let mantissa = 1.0 + frac * (ln2 + frac * (0.240_226_f32 + frac * 0.055_504_f32));
if n >= 0 {
let mut acc = 1.0_f32;
for _ in 0..n { acc *= 2.0; }
acc * mantissa
} else {
let mut acc = 1.0_f32;
for _ in 0..(-n) { acc *= 0.5; }
acc * mantissa
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semiotic_horizon_record_and_query() {
let mut horizon = SemioticHorizon::<16>::new();
horizon.record(10.0, 0.005, true, 15);
horizon.record(5.0, 0.005, true, 25);
horizon.record(-5.0, 0.005, false, 0);
horizon.record(-15.0, 0.001, false, 0);
assert_eq!(horizon.len(), 4);
assert!((horizon.detection_rate() - 0.5).abs() < 1e-4);
}
#[test]
fn mean_latency_only_counts_detected() {
let mut horizon = SemioticHorizon::<8>::new();
horizon.record(10.0, 0.01, true, 10);
horizon.record(5.0, 0.01, true, 20);
horizon.record(-10.0, 0.01, false, 0);
let lat = horizon.mean_detection_latency();
assert!((lat - 15.0).abs() < 1e-4, "mean latency of detected: {}", lat);
}
#[test]
fn candidate_mechanisms_for_drift() {
let mechs = candidate_mechanisms(ReasonCode::SustainedOutwardDrift);
assert!(mechs.contains(&PhysicalMechanism::PaThermalDrift));
assert!(mechs.contains(&PhysicalMechanism::LoAging));
}
#[test]
fn candidate_mechanisms_for_jamming() {
let mechs = candidate_mechanisms(ReasonCode::AbruptSlewViolation);
assert!(mechs.contains(&PhysicalMechanism::IntentionalJamming));
}
#[test]
fn model_reference_non_empty() {
let ref_str = model_reference(PhysicalMechanism::PaThermalDrift);
assert!(ref_str.contains("Arrhenius"));
let leeson = model_reference(PhysicalMechanism::PhaseNoiseDegradation);
assert!(leeson.contains("Leeson"));
}
#[test]
fn horizon_capacity_enforced() {
let mut h = SemioticHorizon::<2>::new();
assert!(h.record(0.0, 0.0, true, 1));
assert!(h.record(0.0, 0.0, false, 0));
assert!(!h.record(0.0, 0.0, true, 1), "must reject when full");
}
#[test]
fn arrhenius_drift_increases_with_temperature() {
let model = ArrheniusModel::GAAS_PHEMT;
let drift_25 = model.predict_drift_rate(25.0);
let drift_125 = model.predict_drift_rate(125.0);
assert!(drift_125 > drift_25,
"Arrhenius: higher T → higher drift: {}→{}", drift_25, drift_125);
}
#[test]
fn arrhenius_gan_slower_than_gaas_at_same_temp() {
let gaas = ArrheniusModel::GAAS_PHEMT.predict_drift_rate(125.0);
let gan = ArrheniusModel::GAN_HEMT.predict_drift_rate(125.0);
assert!(gan < gaas,
"GaN (E_a=2.1) must have lower drift than GaAs (E_a=1.6): {} vs {}", gan, gaas);
}
#[test]
fn allan_variance_ocxo_better_than_tcxo() {
let ocxo = AllanVarianceModel::OCXO_CLASS_A.predict_drift_rate(1.0);
let tcxo = AllanVarianceModel::TCXO_GRADE_B.predict_drift_rate(1.0);
assert!(ocxo < tcxo,
"OCXO must be more stable than TCXO: {} vs {}", ocxo, tcxo);
}
#[test]
fn allan_variance_returns_zero_for_zero_tau() {
let m = AllanVarianceModel::OCXO_CLASS_A;
let s = m.predict_drift_rate(0.0);
assert_eq!(s, 0.0, "AVAR at τ=0 must return 0");
}
#[test]
fn physics_consistency_within_tolerance() {
let model = ArrheniusModel::GAAS_PHEMT;
let predicted = model.predict_drift_rate(85.0);
let result = evaluate_physics_consistency(&model, predicted * 1.1, 85.0, 0.20);
assert!(result.is_consistent,
"10% deviation within 20% tolerance: ratio={}", result.deviation_ratio);
}
#[test]
fn physics_consistency_outside_tolerance() {
let model = ArrheniusModel::GAAS_PHEMT;
let predicted = model.predict_drift_rate(85.0);
let result = evaluate_physics_consistency(&model, predicted * 3.0, 85.0, 0.50);
assert!(!result.is_consistent,
"200% deviation outside 50% tolerance: ratio={}", result.deviation_ratio);
}
#[test]
fn physics_model_reason_codes() {
assert_eq!(ArrheniusModel::GAAS_PHEMT.maps_to_reason(), ReasonCode::SustainedOutwardDrift);
assert_eq!(AllanVarianceModel::TCXO_GRADE_B.maps_to_reason(), ReasonCode::SustainedOutwardDrift);
}
#[test]
fn exp_approx_reasonable_accuracy() {
let e0 = exp_approx(0.0);
let e1 = exp_approx(1.0);
let em1 = exp_approx(-1.0);
assert!((e0 - 1.0).abs() < 0.01, "exp(0) ≈ 1: {}", e0);
assert!((e1 - 2.718).abs() < 0.05, "exp(1) ≈ 2.718: {}", e1);
assert!((em1 - 0.368).abs() < 0.01, "exp(-1) ≈ 0.368: {}", em1);
}
}