Skip to main content

dsfb_semiconductor/
signs.rs

1#[cfg(feature = "std")]
2use crate::config::PipelineConfig;
3#[cfg(feature = "std")]
4use crate::nominal::NominalModel;
5#[cfg(feature = "std")]
6use crate::preprocessing::PreparedDataset;
7#[cfg(feature = "std")]
8use crate::residual::ResidualSet;
9use serde::Serialize;
10#[cfg(not(feature = "std"))]
11use alloc::{string::String, vec::Vec};
12
13#[derive(Debug, Clone, Serialize)]
14pub struct FeatureSigns {
15    pub feature_index: usize,
16    pub feature_name: String,
17    pub drift: Vec<f64>,
18    pub slew: Vec<f64>,
19    pub drift_threshold: f64,
20    pub slew_threshold: f64,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct SignSet {
25    pub traces: Vec<FeatureSigns>,
26}
27
28#[cfg(feature = "std")]
29pub fn compute_signs(
30    dataset: &PreparedDataset,
31    nominal: &NominalModel,
32    residuals: &ResidualSet,
33    config: &PipelineConfig,
34) -> SignSet {
35    let mut traces = Vec::with_capacity(residuals.traces.len());
36
37    for residual_trace in &residuals.traces {
38        let feature = &nominal.features[residual_trace.feature_index];
39        let drift = compute_drift(
40            &residual_trace.norms,
41            &residual_trace.is_imputed,
42            config.drift_window,
43        );
44        let slew = compute_slew(&drift, &residual_trace.is_imputed);
45
46        let healthy_drift = dataset
47            .healthy_pass_indices
48            .iter()
49            .filter_map(|&idx| drift.get(idx).copied())
50            .collect::<Vec<_>>();
51        let healthy_slew = dataset
52            .healthy_pass_indices
53            .iter()
54            .filter_map(|&idx| slew.get(idx).copied())
55            .collect::<Vec<_>>();
56        let drift_threshold = if feature.analyzable {
57            config.drift_sigma_multiplier
58                * sample_std(&healthy_drift)
59                    .unwrap_or(config.epsilon)
60                    .max(config.epsilon)
61        } else {
62            0.0
63        };
64        let slew_threshold = if feature.analyzable {
65            config.slew_sigma_multiplier
66                * sample_std(&healthy_slew)
67                    .unwrap_or(config.epsilon)
68                    .max(config.epsilon)
69        } else {
70            0.0
71        };
72
73        traces.push(FeatureSigns {
74            feature_index: residual_trace.feature_index,
75            feature_name: residual_trace.feature_name.clone(),
76            drift,
77            slew,
78            drift_threshold,
79            slew_threshold,
80        });
81    }
82
83    SignSet { traces }
84}
85
86pub fn compute_drift(values: &[f64], is_imputed: &[bool], window: usize) -> Vec<f64> {
87    let mut drift = vec![0.0; values.len()];
88    for index in window..values.len() {
89        if is_imputed[index] || is_imputed[index - window] {
90            drift[index] = 0.0;
91        } else {
92            drift[index] = (values[index] - values[index - window]) / window as f64;
93        }
94    }
95    drift
96}
97
98pub fn compute_slew(drift: &[f64], is_imputed: &[bool]) -> Vec<f64> {
99    let mut slew = vec![0.0; drift.len()];
100    for index in 1..drift.len() {
101        if is_imputed[index] || is_imputed[index - 1] {
102            slew[index] = 0.0;
103        } else {
104            slew[index] = drift[index] - drift[index - 1];
105        }
106    }
107    slew
108}
109
110#[cfg(feature = "std")]
111fn sample_std(values: &[f64]) -> Option<f64> {
112    if values.len() < 2 {
113        return None;
114    }
115    let mean = values.iter().sum::<f64>() / values.len() as f64;
116    let variance = values
117        .iter()
118        .map(|value| {
119            let centered = *value - mean;
120            centered * centered
121        })
122        .sum::<f64>()
123        / (values.len() as f64 - 1.0);
124    Some(variance.sqrt())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn drift_matches_window_difference() {
133        let drift = compute_drift(&[0.0, 1.0, 2.0, 3.0, 4.0], &[false; 5], 2);
134        assert_eq!(drift, vec![0.0, 0.0, 1.0, 1.0, 1.0]);
135    }
136
137    #[test]
138    fn slew_is_difference_of_drift() {
139        let slew = compute_slew(&[0.0, 0.5, 1.0, 1.0], &[false; 4]);
140        assert_eq!(slew, vec![0.0, 0.5, 0.5, 0.0]);
141    }
142
143    #[test]
144    fn imputed_endpoints_zero_drift_and_slew() {
145        let drift = compute_drift(
146            &[0.0, 1.0, 2.0, 3.0, 4.0],
147            &[false, false, true, false, false],
148            2,
149        );
150        assert_eq!(drift, vec![0.0, 0.0, 0.0, 1.0, 0.0]);
151
152        let slew = compute_slew(&drift, &[false, false, true, false, false]);
153        assert_eq!(slew, vec![0.0, 0.0, 0.0, 0.0, -1.0]);
154    }
155}