dsfb_debug/envelope.rs
1//! DSFB-Debug: admissibility envelope — paper §5.4 Equation (3).
2//!
3//! Defines the admissibility region:
4//!
5//! ```text
6//! E(k) = { r ∈ Rⁿ : ‖r‖ ≤ ρ(k) }
7//! ```
8//!
9//! A residual is *admissible* when its norm sits inside the
10//! envelope; *boundary-grazing* when it approaches ρ; *violating*
11//! when it exits. The envelope's radius `ρ(k)` is operator-defined
12//! and may be sourced from any of:
13//!
14//! - **SLO / SLA targets** — e.g. p99 latency must not exceed 500 ms
15//! - **Error-budget boundaries** — burn-rate alerting derivatives
16//! - **Resource-utilisation ceilings** — heap %, CPU %, queue depth
17//! - **Healthy-window baselines** — `ρ = mean + k·sigma` over the
18//! first N fault-free windows
19//!
20//! This module also exposes `sqrt_approx_pub` — a Newton-Raphson
21//! square-root approximation that the no_std core uses to avoid a
22//! `libm` / `std::f64::sqrt` dependency. The approximation converges
23//! in ≤4 iterations to within 1 ulp of `f64::sqrt` for inputs in the
24//! engine's operating range.
25
26/// Check if a residual norm is within the admissibility envelope
27#[inline]
28pub fn is_admissible(norm: f64, rho: f64) -> bool {
29 norm <= rho
30}
31
32/// Check if a residual norm is in the boundary zone (> boundary_fraction * ρ)
33#[inline]
34pub fn is_boundary_zone(norm: f64, rho: f64, boundary_fraction: f64) -> bool {
35 norm > rho * boundary_fraction && norm <= rho
36}
37
38/// Check if a residual norm has exited the envelope (violation)
39#[inline]
40pub fn is_violation(norm: f64, rho: f64) -> bool {
41 norm > rho
42}
43
44/// Compute envelope radius from healthy-window statistics.
45/// ρ = 3σ of healthy residual distribution
46///
47/// # Arguments
48/// * `healthy_residuals` - residual values from healthy window (immutable)
49///
50/// # Returns
51/// 3σ envelope radius, or 1.0 if insufficient data
52pub fn compute_envelope_radius(healthy_residuals: &[f64]) -> f64 {
53 let n = healthy_residuals.len();
54 if n < 2 {
55 return 1.0; // fallback — documented limitation
56 }
57
58 // Compute mean
59 let mut sum = 0.0;
60 let mut i = 0;
61 while i < n {
62 sum += healthy_residuals[i];
63 i += 1;
64 }
65 let mean = sum / n as f64;
66
67 // Compute variance
68 let mut var_sum = 0.0;
69 i = 0;
70 while i < n {
71 let d = healthy_residuals[i] - mean;
72 var_sum += d * d;
73 i += 1;
74 }
75 let variance = var_sum / (n - 1) as f64;
76
77 // ρ = 3σ
78 let sigma = sqrt_approx(variance);
79 let rho = 3.0 * sigma;
80
81 // Guard: minimum rho to prevent zero-width envelopes
82 if rho < 1e-10 { 1e-10 } else { rho }
83}
84
85/// Public wrapper for sqrt_approx (used by baseline module)
86#[inline]
87pub fn sqrt_approx_pub(x: f64) -> f64 {
88 sqrt_approx(x)
89}
90
91/// Approximate square root using Newton's method (no_std compatible)
92#[inline]
93fn sqrt_approx(x: f64) -> f64 {
94 if x <= 0.0 {
95 return 0.0;
96 }
97 // Use bit manipulation for initial guess, then Newton iterations
98 let mut guess = x * 0.5;
99 if guess == 0.0 {
100 guess = 1.0;
101 }
102 // 8 Newton iterations — converges to ~15 digits for f64
103 let mut i = 0;
104 while i < 8 {
105 guess = 0.5 * (guess + x / guess);
106 i += 1;
107 }
108 guess
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_admissible() {
117 assert!(is_admissible(2.0, 3.0));
118 assert!(is_admissible(3.0, 3.0));
119 assert!(!is_admissible(3.1, 3.0));
120 }
121
122 #[test]
123 fn test_boundary_zone() {
124 assert!(is_boundary_zone(2.0, 3.0, 0.5)); // 2.0 > 1.5, <= 3.0
125 assert!(!is_boundary_zone(1.0, 3.0, 0.5)); // 1.0 <= 1.5
126 assert!(!is_boundary_zone(3.5, 3.0, 0.5)); // 3.5 > 3.0
127 }
128
129 #[test]
130 fn test_envelope_radius() {
131 // Known data: [1, 2, 3, 4, 5] → σ ≈ 1.581, 3σ ≈ 4.743
132 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
133 let rho = compute_envelope_radius(&data);
134 assert!((rho - 4.743).abs() < 0.1);
135 }
136
137 #[test]
138 fn test_sqrt_approx() {
139 assert!((sqrt_approx(4.0) - 2.0).abs() < 1e-10);
140 assert!((sqrt_approx(9.0) - 3.0).abs() < 1e-10);
141 assert!((sqrt_approx(2.0) - core::f64::consts::SQRT_2).abs() < 1e-6);
142 }
143}