Skip to main content

dsfb_rf/
envelope.rs

1//! Admissibility envelope E(k) = {r : ‖r‖ ≤ ρ(k)}.
2//!
3//! ## Mathematical Definition (paper §B.3, §V-D)
4//!
5//! E(k) = {r ∈ ℂⁿ : ‖r‖ ≤ ρ(k)}
6//! ρ = μ_healthy + 3σ_healthy   (from calibration window)
7//!
8//! The admissibility_multiplier() from PlatformContext scales ρ to +∞
9//! during waveform transitions and calibration periods, making envelope
10//! violations structurally impossible during suppressed windows.
11//!
12//! ## Envelope Sources (paper §V-D)
13//!
14//! 1. Receiver noise floor statistics: 3σ of healthy-window residual norm
15//! 2. Regulatory emission masks (ITU-R SM.1048-5 §4.3, MIL-STD-461G RE102)
16//! 3. Link budget margins
17//! 4. PLL hold-in range
18//! 5. 3GPP TS 36.141 §6.3 ACLR limits
19
20/// Admissibility envelope parameterized by radius ρ.
21///
22/// Constructed from the healthy calibration window statistics.
23/// The radius is stored as a fixed scalar; regime-dependent scaling
24/// is applied via `effective_rho()` using the platform multiplier.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct AdmissibilityEnvelope {
27    /// Base envelope radius ρ = μ_healthy + 3σ_healthy.
28    pub rho: f32,
29    /// Boundary fraction: Boundary state triggered when ‖r‖ > boundary_frac * ρ.
30    /// Paper default: 0.5 (50% of ρ).
31    pub boundary_frac: f32,
32    /// Slew threshold δ_s for AbruptSlewViolation detection.
33    pub delta_s: f32,
34}
35
36impl AdmissibilityEnvelope {
37    /// Construct envelope from calibrated radius ρ.
38    pub const fn new(rho: f32) -> Self {
39        Self {
40            rho,
41            boundary_frac: 0.5,
42            delta_s: 0.05,
43        }
44    }
45
46    /// Construct with custom boundary fraction and slew threshold.
47    pub const fn with_params(rho: f32, boundary_frac: f32, delta_s: f32) -> Self {
48        Self { rho, boundary_frac, delta_s }
49    }
50
51    /// Effective radius after applying platform multiplier.
52    ///
53    /// During waveform transitions: multiplier = +∞ → no violation possible.
54    #[inline]
55    pub fn effective_rho(&self, platform_multiplier: f32) -> f32 {
56        self.rho * platform_multiplier
57    }
58
59    /// Returns true if ‖r‖ > ρ_eff (Violation condition).
60    #[inline]
61    pub fn is_violation(&self, norm: f32, platform_multiplier: f32) -> bool {
62        let rho_eff = self.effective_rho(platform_multiplier);
63        norm > rho_eff
64    }
65
66    /// Returns true if ‖r‖ > boundary_frac * ρ_eff (Boundary approach condition).
67    #[inline]
68    pub fn is_boundary_approach(&self, norm: f32, platform_multiplier: f32) -> bool {
69        let rho_eff = self.effective_rho(platform_multiplier);
70        norm > self.boundary_frac * rho_eff
71    }
72
73    /// Calibrate envelope from a healthy-window residual norm slice.
74    ///
75    /// Computes μ + 3σ over the provided norms array.
76    /// This is the Stage III calibration protocol (paper §F.4).
77    pub fn calibrate_from_window(healthy_norms: &[f32]) -> Option<Self> {
78        if healthy_norms.is_empty() {
79            return None;
80        }
81        let n = healthy_norms.len() as f32;
82        let mean = healthy_norms.iter().sum::<f32>() / n;
83        let variance = healthy_norms.iter()
84            .map(|&x| (x - mean) * (x - mean))
85            .sum::<f32>() / n;
86        let std_dev = crate::math::sqrt_f32(variance);
87        let rho = mean + 3.0 * std_dev;
88        Some(Self::new(rho))
89    }
90}
91
92// ---------------------------------------------------------------
93// Tests
94// ---------------------------------------------------------------
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn calibration_from_uniform_window() {
101        // 100 samples all = 0.05: mean=0.05, std=0, rho=0.05
102        let norms = [0.05_f32; 100];
103        let env = AdmissibilityEnvelope::calibrate_from_window(&norms).unwrap();
104        assert!((env.rho - 0.05).abs() < 1e-3, "rho={} (expected ~0.05)", env.rho);
105    }
106
107    #[test]
108    fn calibration_3sigma_rule() {
109        // mean=0.0, std=0.1, rho should be ~0.3
110        let norms: [f32; 6] = [-0.1, -0.1, 0.0, 0.0, 0.1, 0.1];
111        let env = AdmissibilityEnvelope::calibrate_from_window(&norms).unwrap();
112        assert!(env.rho > 0.0, "rho should be positive");
113    }
114
115    #[test]
116    fn violation_detection() {
117        let env = AdmissibilityEnvelope::new(0.1);
118        assert!(!env.is_violation(0.05, 1.0));
119        assert!(!env.is_violation(0.1, 1.0));   // boundary, not violation
120        assert!(env.is_violation(0.11, 1.0));
121    }
122
123    #[test]
124    fn transition_suppresses_violation() {
125        let env = AdmissibilityEnvelope::new(0.1);
126        // Even norm=1000 should not be a violation when multiplier=+inf
127        assert!(!env.is_violation(1000.0, f32::INFINITY));
128    }
129
130    #[test]
131    fn boundary_approach_detection() {
132        let env = AdmissibilityEnvelope::new(0.1);
133        // boundary_frac=0.5, so boundary at 0.05
134        assert!(!env.is_boundary_approach(0.04, 1.0));
135        assert!(env.is_boundary_approach(0.06, 1.0));
136        assert!(env.is_boundary_approach(0.09, 1.0));
137    }
138
139    #[test]
140    fn calibrate_returns_none_for_empty() {
141        assert!(AdmissibilityEnvelope::calibrate_from_window(&[]).is_none());
142    }
143}