Skip to main content

dsfb_semiconductor/
grammar.rs

1pub mod layer;
2
3use crate::config::PipelineConfig;
4use crate::nominal::NominalModel;
5use crate::residual::ResidualSet;
6use crate::signs::SignSet;
7use serde::Serialize;
8#[cfg(not(feature = "std"))]
9use alloc::{string::String, vec::Vec};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12pub enum GrammarState {
13    Admissible,
14    Boundary,
15    Violation,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19pub enum GrammarReason {
20    Admissible,
21    SustainedOutwardDrift,
22    AbruptSlewViolation,
23    RecurrentBoundaryGrazing,
24    EnvelopeViolation,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct FeatureGrammarTrace {
29    pub feature_index: usize,
30    pub feature_name: String,
31    pub suppressed_by_imputation: Vec<bool>,
32    pub raw_states: Vec<GrammarState>,
33    pub raw_reasons: Vec<GrammarReason>,
34    pub states: Vec<GrammarState>,
35    pub reasons: Vec<GrammarReason>,
36    pub persistent_boundary: Vec<bool>,
37    pub persistent_violation: Vec<bool>,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct GrammarSet {
42    pub traces: Vec<FeatureGrammarTrace>,
43}
44
45pub fn evaluate_grammar(
46    residuals: &ResidualSet,
47    signs: &SignSet,
48    nominal: &NominalModel,
49    config: &PipelineConfig,
50) -> GrammarSet {
51    let mut traces = Vec::with_capacity(residuals.traces.len());
52
53    for (residual_trace, sign_trace) in residuals.traces.iter().zip(&signs.traces) {
54        let feature = &nominal.features[residual_trace.feature_index];
55        let (raw_states, raw_reasons, suppressed_by_imputation) =
56            evaluate_raw_trace(residual_trace, sign_trace, feature, config);
57        let (states, reasons) =
58            apply_hysteresis(&raw_states, &raw_reasons, config.state_confirmation_steps);
59        let persistent_boundary = persistent_mask(
60            &states,
61            GrammarState::Boundary,
62            config.persistent_state_steps,
63        );
64        let persistent_violation = persistent_mask(
65            &states,
66            GrammarState::Violation,
67            config.persistent_state_steps,
68        );
69
70        traces.push(FeatureGrammarTrace {
71            feature_index: residual_trace.feature_index,
72            feature_name: residual_trace.feature_name.clone(),
73            suppressed_by_imputation,
74            raw_states,
75            raw_reasons,
76            states,
77            reasons,
78            persistent_boundary,
79            persistent_violation,
80        });
81    }
82
83    GrammarSet { traces }
84}
85
86fn evaluate_raw_trace(
87    residual_trace: &crate::residual::ResidualFeatureTrace,
88    sign_trace: &crate::signs::FeatureSigns,
89    feature: &crate::nominal::NominalFeature,
90    config: &PipelineConfig,
91) -> (Vec<GrammarState>, Vec<GrammarReason>, Vec<bool>) {
92    let mut states = Vec::with_capacity(residual_trace.norms.len());
93    let mut reasons = Vec::with_capacity(residual_trace.norms.len());
94    let mut suppressed_by_imputation = Vec::with_capacity(residual_trace.norms.len());
95
96    for index in 0..residual_trace.norms.len() {
97        let zone_start = index.saturating_sub(config.grazing_window.saturating_sub(1));
98        let zone_hits = residual_trace.norms[zone_start..=index]
99            .iter()
100            .filter(|value| **value > config.boundary_fraction_of_rho * feature.rho)
101            .count();
102
103        let norm = residual_trace.norms[index];
104        let drift = sign_trace.drift[index];
105        let slew = sign_trace.slew[index].abs();
106        let state_suppressed_by_imputation = feature.analyzable && residual_trace.is_imputed[index];
107
108        let (state, reason) = if !feature.analyzable || state_suppressed_by_imputation {
109            (GrammarState::Admissible, GrammarReason::Admissible)
110        } else if norm > feature.rho {
111            (GrammarState::Violation, GrammarReason::EnvelopeViolation)
112        } else if norm > config.boundary_fraction_of_rho * feature.rho
113            && drift >= sign_trace.drift_threshold
114            && slew >= sign_trace.slew_threshold
115        {
116            (GrammarState::Boundary, GrammarReason::AbruptSlewViolation)
117        } else if norm > config.boundary_fraction_of_rho * feature.rho
118            && drift >= sign_trace.drift_threshold
119        {
120            (GrammarState::Boundary, GrammarReason::SustainedOutwardDrift)
121        } else if norm > config.boundary_fraction_of_rho * feature.rho
122            && zone_hits >= config.grazing_min_hits
123        {
124            (
125                GrammarState::Boundary,
126                GrammarReason::RecurrentBoundaryGrazing,
127            )
128        } else {
129            (GrammarState::Admissible, GrammarReason::Admissible)
130        };
131
132        states.push(state);
133        reasons.push(reason);
134        suppressed_by_imputation.push(state_suppressed_by_imputation);
135    }
136
137    (states, reasons, suppressed_by_imputation)
138}
139
140fn apply_hysteresis(
141    raw_states: &[GrammarState],
142    raw_reasons: &[GrammarReason],
143    confirmation_steps: usize,
144) -> (Vec<GrammarState>, Vec<GrammarReason>) {
145    if raw_states.is_empty() {
146        return (Vec::new(), Vec::new());
147    }
148
149    let mut states = Vec::with_capacity(raw_states.len());
150    let mut reasons = Vec::with_capacity(raw_states.len());
151    let mut current_state = raw_states[0];
152    let mut current_reason = raw_reasons[0];
153    let mut candidate_state: Option<GrammarState> = None;
154    let mut candidate_count = 0usize;
155
156    for (&raw_state, &raw_reason) in raw_states.iter().zip(raw_reasons) {
157        if raw_state == current_state {
158            candidate_state = None;
159            candidate_count = 0;
160            if raw_state != GrammarState::Admissible {
161                current_reason = raw_reason;
162            } else {
163                current_reason = GrammarReason::Admissible;
164            }
165        } else if candidate_state == Some(raw_state) {
166            candidate_count += 1;
167        } else {
168            candidate_state = Some(raw_state);
169            candidate_count = 1;
170        }
171
172        if let Some(next_state) = candidate_state {
173            if candidate_count >= confirmation_steps {
174                current_state = next_state;
175                current_reason = if current_state == GrammarState::Admissible {
176                    GrammarReason::Admissible
177                } else {
178                    raw_reason
179                };
180                candidate_state = None;
181                candidate_count = 0;
182            }
183        }
184
185        states.push(current_state);
186        reasons.push(if current_state == GrammarState::Admissible {
187            GrammarReason::Admissible
188        } else {
189            current_reason
190        });
191    }
192
193    (states, reasons)
194}
195
196fn persistent_mask(
197    states: &[GrammarState],
198    target: GrammarState,
199    minimum_steps: usize,
200) -> Vec<bool> {
201    let mut out = Vec::with_capacity(states.len());
202    let mut consecutive = 0usize;
203
204    for &state in states {
205        if state == target {
206            consecutive += 1;
207            out.push(consecutive >= minimum_steps);
208        } else {
209            consecutive = 0;
210            out.push(false);
211        }
212    }
213
214    out
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::nominal::{NominalFeature, NominalModel};
221    use crate::residual::{ResidualFeatureTrace, ResidualSet};
222    use crate::signs::{FeatureSigns, SignSet};
223
224    fn test_config() -> PipelineConfig {
225        PipelineConfig {
226            state_confirmation_steps: 1,
227            persistent_state_steps: 2,
228            ..PipelineConfig::default()
229        }
230    }
231
232    #[test]
233    fn violation_state_wins_over_boundary() {
234        let residuals = ResidualSet {
235            traces: vec![ResidualFeatureTrace {
236                feature_index: 0,
237                feature_name: "S001".into(),
238                imputed_values: vec![0.0, 2.0],
239                residuals: vec![0.0, 2.0],
240                norms: vec![0.0, 2.0],
241                threshold_alarm: vec![false, true],
242                is_imputed: vec![false, false],
243            }],
244        };
245        let signs = SignSet {
246            traces: vec![FeatureSigns {
247                feature_index: 0,
248                feature_name: "S001".into(),
249                drift: vec![0.0, 1.0],
250                slew: vec![0.0, 1.0],
251                drift_threshold: 0.1,
252                slew_threshold: 0.1,
253            }],
254        };
255        let nominal = NominalModel {
256            features: vec![NominalFeature {
257                feature_index: 0,
258                feature_name: "S001".into(),
259                healthy_mean: 0.0,
260                healthy_std: 0.5,
261                rho: 1.5,
262                healthy_observations: 10,
263                analyzable: true,
264            }],
265        };
266        let grammar = evaluate_grammar(&residuals, &signs, &nominal, &test_config());
267        assert_eq!(grammar.traces[0].raw_states[1], GrammarState::Violation);
268        assert_eq!(grammar.traces[0].states[1], GrammarState::Violation);
269        assert_eq!(
270            grammar.traces[0].raw_reasons[1],
271            GrammarReason::EnvelopeViolation
272        );
273    }
274
275    #[test]
276    fn hysteresis_requires_confirmation_before_state_change() {
277        let raw_states = vec![
278            GrammarState::Admissible,
279            GrammarState::Boundary,
280            GrammarState::Admissible,
281            GrammarState::Boundary,
282            GrammarState::Boundary,
283        ];
284        let raw_reasons = vec![
285            GrammarReason::Admissible,
286            GrammarReason::SustainedOutwardDrift,
287            GrammarReason::Admissible,
288            GrammarReason::SustainedOutwardDrift,
289            GrammarReason::SustainedOutwardDrift,
290        ];
291        let (states, _) = apply_hysteresis(&raw_states, &raw_reasons, 2);
292        assert_eq!(
293            states,
294            vec![
295                GrammarState::Admissible,
296                GrammarState::Admissible,
297                GrammarState::Admissible,
298                GrammarState::Admissible,
299                GrammarState::Boundary,
300            ]
301        );
302    }
303
304    #[test]
305    fn persistence_mask_starts_after_minimum_consecutive_steps() {
306        let states = vec![
307            GrammarState::Admissible,
308            GrammarState::Boundary,
309            GrammarState::Boundary,
310            GrammarState::Boundary,
311            GrammarState::Admissible,
312        ];
313        let mask = persistent_mask(&states, GrammarState::Boundary, 2);
314        assert_eq!(mask, vec![false, false, true, true, false]);
315    }
316
317    #[test]
318    fn imputed_runs_are_suppressed_to_admissible() {
319        let residuals = ResidualSet {
320            traces: vec![ResidualFeatureTrace {
321                feature_index: 0,
322                feature_name: "S001".into(),
323                imputed_values: vec![0.0, 1.2],
324                residuals: vec![0.0, 1.2],
325                norms: vec![0.0, 1.2],
326                threshold_alarm: vec![false, false],
327                is_imputed: vec![false, true],
328            }],
329        };
330        let signs = SignSet {
331            traces: vec![FeatureSigns {
332                feature_index: 0,
333                feature_name: "S001".into(),
334                drift: vec![0.0, 1.0],
335                slew: vec![0.0, 1.0],
336                drift_threshold: 0.1,
337                slew_threshold: 0.1,
338            }],
339        };
340        let nominal = NominalModel {
341            features: vec![NominalFeature {
342                feature_index: 0,
343                feature_name: "S001".into(),
344                healthy_mean: 0.0,
345                healthy_std: 0.5,
346                rho: 1.5,
347                healthy_observations: 10,
348                analyzable: true,
349            }],
350        };
351
352        let grammar = evaluate_grammar(&residuals, &signs, &nominal, &test_config());
353        assert_eq!(grammar.traces[0].raw_states[1], GrammarState::Admissible);
354        assert_eq!(grammar.traces[0].raw_reasons[1], GrammarReason::Admissible);
355        assert!(grammar.traces[0].suppressed_by_imputation[1]);
356    }
357}