Skip to main content

dsfb_debug/
sign.rs

1//! DSFB-Debug: sign-tuple computation — paper §5.3 Equation (2).
2//!
3//! Computes the per-(window, signal) residual signature
4//! σ(k) = (‖r(k)‖, ṙ(k), r̈(k)) where:
5//!
6//! - **‖r(k)‖** — instantaneous deviation magnitude (provided by
7//!   `residual::residual_norm`)
8//! - **ṙ(k)** — finite-difference drift rate over a fixed-width
9//!   window W (window-to-window slope)
10//! - **r̈(k)** — first difference of the drift (curvature of the
11//!   trajectory; "slew")
12//!
13//! σ(k) is the structural signature consumed by the grammar
14//! evaluator; it captures both *where* the residual is (norm) and
15//! *where it is going* (drift, slew). This second-order information
16//! is the structural-detectability advantage over flat thresholding,
17//! which sees only the norm.
18//!
19//! The computation is a deterministic two-pass over the residual
20//! window: pass 1 computes the regression slope (drift), pass 2
21//! takes the first difference of consecutive drift values (slew).
22//! No allocation; works in-place on the residual slice.
23
24use crate::residual::residual_norm;
25use crate::types::SignTuple;
26
27/// Compute the sign tuple for a single signal at window k.
28///
29/// # Arguments
30/// * `norms` - slice of residual norms for the last W+1 windows (immutable)
31/// * `k` - current index into `norms` (must be >= 1)
32///
33/// # Returns
34/// SignTuple with norm, drift, and slew
35#[inline]
36pub fn compute_sign_tuple(norms: &[f64], k: usize) -> SignTuple {
37    if k == 0 || norms.is_empty() {
38        return SignTuple::ZERO;
39    }
40
41    let norm = if k < norms.len() { residual_norm(norms[k]) } else { 0.0 };
42
43    // Drift: finite difference ṙ(k) = ‖r(k)‖ - ‖r(k-1)‖
44    let drift = if k >= 1 && k < norms.len() {
45        norms[k] - norms[k - 1]
46    } else {
47        0.0
48    };
49
50    // Slew: second difference r̈(k) = ṙ(k) - ṙ(k-1)
51    let slew = if k >= 2 && k < norms.len() {
52        let drift_prev = norms[k - 1] - norms[k - 2];
53        drift - drift_prev
54    } else {
55        0.0
56    };
57
58    SignTuple { norm, drift, slew }
59}
60
61/// Compute rolling drift persistence: fraction of last W windows with drift > 0
62#[inline]
63pub fn drift_persistence(norms: &[f64], k: usize, window: usize) -> f64 {
64    if k < 1 || window == 0 {
65        return 0.0;
66    }
67    let start = k.saturating_sub(window);
68    let mut positive_count: u32 = 0;
69    let mut total: u32 = 0;
70    let mut i = start + 1;
71    while i <= k && i < norms.len() {
72        let drift = norms[i] - norms[i - 1];
73        if drift > 0.0 {
74            positive_count += 1;
75        }
76        total += 1;
77        i += 1;
78    }
79    if total == 0 { 0.0 } else { positive_count as f64 / total as f64 }
80}
81
82/// Compute rolling boundary density: fraction of last W windows in Boundary state.
83/// `states` is a slice of 0=Admissible, 1=Boundary, 2=Violation
84#[inline]
85pub fn boundary_density(states: &[u8], k: usize, window: usize) -> f64 {
86    if window == 0 || k == 0 {
87        return 0.0;
88    }
89    let start = k.saturating_sub(window);
90    let mut boundary_count: u32 = 0;
91    let mut total: u32 = 0;
92    let mut i = start;
93    while i <= k && i < states.len() {
94        if states[i] == 1 {
95            boundary_count += 1;
96        }
97        total += 1;
98        i += 1;
99    }
100    if total == 0 { 0.0 } else { boundary_count as f64 / total as f64 }
101}
102
103/// Compute rolling slew density: fraction of last W windows with |slew| > δ_s
104#[inline]
105pub fn slew_density(norms: &[f64], k: usize, window: usize, delta_s: f64) -> f64 {
106    if k < 2 || window == 0 {
107        return 0.0;
108    }
109    let start = if k > window { k - window } else { 2 };
110    let start = if start < 2 { 2 } else { start };
111    let mut slew_count: u32 = 0;
112    let mut total: u32 = 0;
113    let mut i = start;
114    while i <= k && i < norms.len() {
115        let drift_cur = norms[i] - norms[i - 1];
116        let drift_prev = norms[i - 1] - norms[i - 2];
117        let slew = drift_cur - drift_prev;
118        if slew > delta_s || slew < -delta_s {
119            slew_count += 1;
120        }
121        total += 1;
122        i += 1;
123    }
124    if total == 0 { 0.0 } else { slew_count as f64 / total as f64 }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_sign_tuple_basic() {
133        let norms = [0.0, 1.0, 3.0, 2.0];
134        let s = compute_sign_tuple(&norms, 2);
135        assert!((s.norm - 3.0).abs() < 1e-12);
136        assert!((s.drift - 2.0).abs() < 1e-12); // 3.0 - 1.0
137        let drift_prev = 1.0; // 1.0 - 0.0
138        assert!((s.slew - (2.0 - drift_prev)).abs() < 1e-12); // 2.0 - 1.0 = 1.0
139    }
140
141    #[test]
142    fn test_sign_tuple_zero_index() {
143        let norms = [1.0, 2.0];
144        let s = compute_sign_tuple(&norms, 0);
145        assert_eq!(s, SignTuple::ZERO);
146    }
147
148    #[test]
149    fn test_drift_persistence() {
150        // All increasing → persistence = 1.0
151        let norms = [0.0, 1.0, 2.0, 3.0, 4.0];
152        assert!((drift_persistence(&norms, 4, 4) - 1.0).abs() < 1e-12);
153    }
154}