Skip to main content

dsfb_semiconductor/
nominal.rs

1#[cfg(feature = "std")]
2use crate::preprocessing::PreparedDataset;
3#[cfg(feature = "std")]
4use crate::config::PipelineConfig;
5use serde::Serialize;
6#[cfg(not(feature = "std"))]
7use alloc::{string::String, vec::Vec};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct NominalFeature {
11    pub feature_index: usize,
12    pub feature_name: String,
13    pub healthy_mean: f64,
14    pub healthy_std: f64,
15    pub rho: f64,
16    pub healthy_observations: usize,
17    pub analyzable: bool,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct NominalModel {
22    pub features: Vec<NominalFeature>,
23}
24
25#[cfg(feature = "std")]
26pub fn build_nominal_model(dataset: &PreparedDataset, config: &PipelineConfig) -> NominalModel {
27    let feature_count = dataset.feature_names.len();
28    let mut features = Vec::with_capacity(feature_count);
29
30    for feature_index in 0..feature_count {
31        let healthy_values = dataset
32            .healthy_pass_indices
33            .iter()
34            .filter_map(|&run_index| dataset.raw_values[run_index][feature_index])
35            .collect::<Vec<_>>();
36        let healthy_observations = healthy_values.len();
37        let healthy_mean = mean(&healthy_values).unwrap_or(0.0);
38        let healthy_std = sample_std(&healthy_values, healthy_mean).unwrap_or(0.0);
39        let analyzable = healthy_observations >= config.minimum_healthy_observations
40            && healthy_std > config.epsilon;
41        let rho = if analyzable {
42            config.envelope_sigma * healthy_std
43        } else {
44            0.0
45        };
46
47        features.push(NominalFeature {
48            feature_index,
49            feature_name: dataset.feature_names[feature_index].clone(),
50            healthy_mean,
51            healthy_std,
52            rho,
53            healthy_observations,
54            analyzable,
55        });
56    }
57
58    NominalModel { features }
59}
60
61#[cfg(feature = "std")]
62fn mean(values: &[f64]) -> Option<f64> {
63    (!values.is_empty()).then(|| values.iter().sum::<f64>() / values.len() as f64)
64}
65
66#[cfg(feature = "std")]
67fn sample_std(values: &[f64], mean: f64) -> Option<f64> {
68    if values.len() < 2 {
69        return None;
70    }
71    let variance = values
72        .iter()
73        .map(|value| {
74            let centered = *value - mean;
75            centered * centered
76        })
77        .sum::<f64>()
78        / (values.len() as f64 - 1.0);
79    Some(variance.sqrt())
80}
81
82#[cfg(all(test, feature = "std"))]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn sample_std_is_zero_for_constant_series() {
88        let std = sample_std(&[2.0, 2.0, 2.0], 2.0).unwrap();
89        assert_eq!(std, 0.0);
90    }
91}