Skip to main content

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}