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}