dsfb_semiconductor/
signs.rs1#[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}