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