Skip to main content

dsfb_semiconductor/
metrics.rs

1use crate::baselines::BaselineSet;
2use crate::config::PipelineConfig;
3use crate::grammar::{GrammarReason, GrammarSet, GrammarState};
4use crate::nominal::NominalModel;
5use crate::precursor::DsaSignalSummary;
6use crate::preprocessing::{DatasetSummary, PreparedDataset};
7use crate::residual::ResidualSet;
8use crate::signs::SignSet;
9use serde::Serialize;
10use std::collections::BTreeSet;
11
12#[derive(Debug, Clone, Serialize)]
13pub struct FeatureMetrics {
14    pub feature_index: usize,
15    pub feature_name: String,
16    pub analyzable: bool,
17    pub healthy_mean: f64,
18    pub healthy_std: f64,
19    pub rho: f64,
20    pub ewma_healthy_mean: f64,
21    pub ewma_healthy_std: f64,
22    pub ewma_threshold: f64,
23    pub cusum_healthy_mean: f64,
24    pub cusum_healthy_std: f64,
25    pub cusum_kappa: f64,
26    pub cusum_alarm_threshold: f64,
27    pub drift_threshold: f64,
28    pub slew_threshold: f64,
29    pub missing_fraction: f64,
30    pub ewma_alarm_points: usize,
31    pub cusum_alarm_points: usize,
32    pub dsfb_raw_boundary_points: usize,
33    pub dsfb_persistent_boundary_points: usize,
34    pub dsfb_raw_violation_points: usize,
35    pub dsfb_persistent_violation_points: usize,
36    pub threshold_alarm_points: usize,
37    pub motif_point_hits: usize,
38    pub motif_run_hits: usize,
39    pub pre_failure_motif_run_hits: usize,
40    pub pre_failure_run_hits: usize,
41    pub motif_precision_proxy: Option<f64>,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct BenchmarkSummary {
46    pub dataset_summary: DatasetSummary,
47    pub analyzable_feature_count: usize,
48    pub grammar_imputation_suppression_points: usize,
49    pub threshold_alarm_points: usize,
50    pub ewma_alarm_points: usize,
51    pub cusum_alarm_points: usize,
52    pub run_energy_alarm_points: usize,
53    pub pca_fdc_alarm_points: usize,
54    pub dsfb_raw_boundary_points: usize,
55    pub dsfb_persistent_boundary_points: usize,
56    pub dsfb_raw_violation_points: usize,
57    pub dsfb_persistent_violation_points: usize,
58    pub failure_runs: usize,
59    pub failure_runs_with_preceding_dsfb_raw_signal: usize,
60    pub failure_runs_with_preceding_dsfb_persistent_signal: usize,
61    pub failure_runs_with_preceding_dsfb_raw_boundary_signal: usize,
62    pub failure_runs_with_preceding_dsfb_persistent_boundary_signal: usize,
63    pub failure_runs_with_preceding_dsfb_raw_violation_signal: usize,
64    pub failure_runs_with_preceding_dsfb_persistent_violation_signal: usize,
65    pub failure_runs_with_preceding_ewma_signal: usize,
66    pub failure_runs_with_preceding_cusum_signal: usize,
67    pub failure_runs_with_preceding_run_energy_signal: usize,
68    pub failure_runs_with_preceding_pca_fdc_signal: usize,
69    pub failure_runs_with_preceding_threshold_signal: usize,
70    pub pass_runs: usize,
71    pub pass_runs_with_dsfb_raw_boundary_signal: usize,
72    pub pass_runs_with_dsfb_persistent_boundary_signal: usize,
73    pub pass_runs_with_dsfb_raw_violation_signal: usize,
74    pub pass_runs_with_dsfb_persistent_violation_signal: usize,
75    pub pass_runs_with_ewma_signal: usize,
76    pub pass_runs_with_cusum_signal: usize,
77    pub pass_runs_with_run_energy_signal: usize,
78    pub pass_runs_with_pca_fdc_signal: usize,
79    pub pass_runs_with_threshold_signal: usize,
80    pub pass_run_dsfb_raw_boundary_nuisance_rate: f64,
81    pub pass_run_dsfb_persistent_boundary_nuisance_rate: f64,
82    pub pass_run_dsfb_raw_violation_nuisance_rate: f64,
83    pub pass_run_dsfb_persistent_violation_nuisance_rate: f64,
84    pub pass_run_ewma_nuisance_rate: f64,
85    pub pass_run_cusum_nuisance_rate: f64,
86    pub pass_run_run_energy_nuisance_rate: f64,
87    pub pass_run_pca_fdc_nuisance_rate: f64,
88    pub pass_run_threshold_nuisance_rate: f64,
89}
90
91#[derive(Debug, Clone, Serialize)]
92pub struct LeadTimeSummary {
93    pub failure_runs_with_raw_boundary_lead: usize,
94    pub failure_runs_with_persistent_boundary_lead: usize,
95    pub failure_runs_with_raw_violation_lead: usize,
96    pub failure_runs_with_persistent_violation_lead: usize,
97    pub failure_runs_with_threshold_lead: usize,
98    pub failure_runs_with_ewma_lead: usize,
99    pub failure_runs_with_cusum_lead: usize,
100    pub failure_runs_with_run_energy_lead: usize,
101    pub failure_runs_with_pca_fdc_lead: usize,
102    pub mean_raw_boundary_lead_runs: Option<f64>,
103    pub mean_persistent_boundary_lead_runs: Option<f64>,
104    pub mean_raw_violation_lead_runs: Option<f64>,
105    pub mean_persistent_violation_lead_runs: Option<f64>,
106    pub mean_threshold_lead_runs: Option<f64>,
107    pub mean_ewma_lead_runs: Option<f64>,
108    pub mean_cusum_lead_runs: Option<f64>,
109    pub mean_run_energy_lead_runs: Option<f64>,
110    pub mean_pca_fdc_lead_runs: Option<f64>,
111    pub mean_raw_boundary_minus_cusum_delta_runs: Option<f64>,
112    pub mean_raw_boundary_minus_run_energy_delta_runs: Option<f64>,
113    pub mean_raw_boundary_minus_pca_fdc_delta_runs: Option<f64>,
114    pub mean_raw_boundary_minus_threshold_delta_runs: Option<f64>,
115    pub mean_raw_boundary_minus_ewma_delta_runs: Option<f64>,
116    pub mean_persistent_boundary_minus_cusum_delta_runs: Option<f64>,
117    pub mean_persistent_boundary_minus_run_energy_delta_runs: Option<f64>,
118    pub mean_persistent_boundary_minus_pca_fdc_delta_runs: Option<f64>,
119    pub mean_persistent_boundary_minus_threshold_delta_runs: Option<f64>,
120    pub mean_persistent_boundary_minus_ewma_delta_runs: Option<f64>,
121    pub mean_raw_violation_minus_cusum_delta_runs: Option<f64>,
122    pub mean_raw_violation_minus_run_energy_delta_runs: Option<f64>,
123    pub mean_raw_violation_minus_pca_fdc_delta_runs: Option<f64>,
124    pub mean_raw_violation_minus_threshold_delta_runs: Option<f64>,
125    pub mean_raw_violation_minus_ewma_delta_runs: Option<f64>,
126    pub mean_persistent_violation_minus_cusum_delta_runs: Option<f64>,
127    pub mean_persistent_violation_minus_run_energy_delta_runs: Option<f64>,
128    pub mean_persistent_violation_minus_pca_fdc_delta_runs: Option<f64>,
129    pub mean_persistent_violation_minus_threshold_delta_runs: Option<f64>,
130    pub mean_persistent_violation_minus_ewma_delta_runs: Option<f64>,
131}
132
133#[derive(Debug, Clone, Serialize)]
134pub struct BoundaryEpisodeSummary {
135    pub raw_episode_count: usize,
136    pub persistent_episode_count: usize,
137    pub mean_raw_episode_length: Option<f64>,
138    pub mean_persistent_episode_length: Option<f64>,
139    pub max_raw_episode_length: usize,
140    pub max_persistent_episode_length: usize,
141    pub raw_non_escalating_episode_fraction: Option<f64>,
142    pub persistent_non_escalating_episode_fraction: Option<f64>,
143}
144
145#[derive(Debug, Clone, Serialize)]
146pub struct MotifMetric {
147    pub motif_name: String,
148    pub point_hits: usize,
149    pub run_hits: usize,
150    pub pre_failure_window_run_hits: usize,
151    pub pre_failure_window_precision_proxy: Option<f64>,
152}
153
154#[derive(Debug, Clone, Serialize)]
155pub struct PerFailureRunSignal {
156    pub failure_run_index: usize,
157    pub failure_timestamp: String,
158    pub earliest_dsfb_raw_boundary_run: Option<usize>,
159    pub earliest_dsfb_persistent_boundary_run: Option<usize>,
160    pub earliest_dsfb_raw_violation_run: Option<usize>,
161    pub earliest_dsfb_persistent_violation_run: Option<usize>,
162    pub earliest_threshold_run: Option<usize>,
163    pub earliest_ewma_run: Option<usize>,
164    pub earliest_cusum_run: Option<usize>,
165    pub earliest_run_energy_run: Option<usize>,
166    pub earliest_pca_fdc_run: Option<usize>,
167    pub dsfb_raw_boundary_lead_runs: Option<usize>,
168    pub dsfb_persistent_boundary_lead_runs: Option<usize>,
169    pub dsfb_raw_violation_lead_runs: Option<usize>,
170    pub dsfb_persistent_violation_lead_runs: Option<usize>,
171    pub threshold_lead_runs: Option<usize>,
172    pub ewma_lead_runs: Option<usize>,
173    pub cusum_lead_runs: Option<usize>,
174    pub run_energy_lead_runs: Option<usize>,
175    pub pca_fdc_lead_runs: Option<usize>,
176    pub dsfb_raw_boundary_minus_cusum_delta_runs: Option<i64>,
177    pub dsfb_raw_boundary_minus_run_energy_delta_runs: Option<i64>,
178    pub dsfb_raw_boundary_minus_pca_fdc_delta_runs: Option<i64>,
179    pub dsfb_raw_boundary_minus_threshold_delta_runs: Option<i64>,
180    pub dsfb_raw_boundary_minus_ewma_delta_runs: Option<i64>,
181    pub dsfb_persistent_boundary_minus_cusum_delta_runs: Option<i64>,
182    pub dsfb_persistent_boundary_minus_run_energy_delta_runs: Option<i64>,
183    pub dsfb_persistent_boundary_minus_pca_fdc_delta_runs: Option<i64>,
184    pub dsfb_persistent_boundary_minus_threshold_delta_runs: Option<i64>,
185    pub dsfb_persistent_boundary_minus_ewma_delta_runs: Option<i64>,
186    pub dsfb_raw_violation_minus_cusum_delta_runs: Option<i64>,
187    pub dsfb_raw_violation_minus_run_energy_delta_runs: Option<i64>,
188    pub dsfb_raw_violation_minus_pca_fdc_delta_runs: Option<i64>,
189    pub dsfb_raw_violation_minus_threshold_delta_runs: Option<i64>,
190    pub dsfb_raw_violation_minus_ewma_delta_runs: Option<i64>,
191    pub dsfb_persistent_violation_minus_cusum_delta_runs: Option<i64>,
192    pub dsfb_persistent_violation_minus_run_energy_delta_runs: Option<i64>,
193    pub dsfb_persistent_violation_minus_pca_fdc_delta_runs: Option<i64>,
194    pub dsfb_persistent_violation_minus_threshold_delta_runs: Option<i64>,
195    pub dsfb_persistent_violation_minus_ewma_delta_runs: Option<i64>,
196}
197
198#[derive(Debug, Clone, Serialize)]
199pub struct DensityMetricRecord {
200    pub run_index: usize,
201    pub timestamp: String,
202    pub label: i8,
203    pub in_pre_failure_window: bool,
204    pub raw_boundary_density: f64,
205    pub persistent_boundary_density: f64,
206    pub raw_violation_density: f64,
207    pub persistent_violation_density: f64,
208    pub threshold_density: f64,
209    pub ewma_density: f64,
210    pub cusum_density: f64,
211}
212
213#[derive(Debug, Clone, Serialize)]
214pub struct DensitySummary {
215    pub density_window: usize,
216    pub mean_raw_boundary_density_failure: f64,
217    pub mean_raw_boundary_density_pass: f64,
218    pub mean_persistent_boundary_density_failure: f64,
219    pub mean_persistent_boundary_density_pass: f64,
220    pub mean_raw_violation_density_failure: f64,
221    pub mean_raw_violation_density_pass: f64,
222    pub mean_persistent_violation_density_failure: f64,
223    pub mean_persistent_violation_density_pass: f64,
224    pub mean_threshold_density_failure: f64,
225    pub mean_threshold_density_pass: f64,
226    pub mean_ewma_density_failure: f64,
227    pub mean_ewma_density_pass: f64,
228    pub mean_cusum_density_failure: f64,
229    pub mean_cusum_density_pass: f64,
230}
231
232#[derive(Debug, Clone, Serialize)]
233pub struct BenchmarkMetrics {
234    pub summary: BenchmarkSummary,
235    pub lead_time_summary: LeadTimeSummary,
236    pub density_summary: DensitySummary,
237    pub boundary_episode_summary: BoundaryEpisodeSummary,
238    pub dsa_summary: Option<DsaSignalSummary>,
239    pub motif_metrics: Vec<MotifMetric>,
240    pub per_failure_run_signals: Vec<PerFailureRunSignal>,
241    pub density_metrics: Vec<DensityMetricRecord>,
242    pub feature_metrics: Vec<FeatureMetrics>,
243    pub top_feature_indices: Vec<usize>,
244}
245
246pub fn compute_metrics(
247    dataset: &PreparedDataset,
248    nominal: &NominalModel,
249    residuals: &ResidualSet,
250    signs: &SignSet,
251    baselines: &BaselineSet,
252    grammar: &GrammarSet,
253    config: &PipelineConfig,
254) -> BenchmarkMetrics {
255    let mut feature_metrics = Vec::new();
256    let mut threshold_alarm_points = 0usize;
257    let mut ewma_alarm_points = 0usize;
258    let mut cusum_alarm_points = 0usize;
259    let run_energy_alarm_points = baselines
260        .run_energy
261        .alarm
262        .iter()
263        .filter(|flag| **flag)
264        .count();
265    let pca_fdc_alarm_points = baselines.pca_fdc.alarm.iter().filter(|flag| **flag).count();
266    let mut dsfb_raw_boundary_points = 0usize;
267    let mut dsfb_persistent_boundary_points = 0usize;
268    let mut dsfb_raw_violation_points = 0usize;
269    let mut dsfb_persistent_violation_points = 0usize;
270    let mut grammar_imputation_suppression_points = 0usize;
271
272    for (((((feature, residual_trace), sign_trace), ewma_trace), cusum_trace), grammar_trace) in
273        nominal
274            .features
275            .iter()
276            .zip(&residuals.traces)
277            .zip(&signs.traces)
278            .zip(&baselines.ewma)
279            .zip(&baselines.cusum)
280            .zip(&grammar.traces)
281    {
282        let threshold_points = residual_trace
283            .threshold_alarm
284            .iter()
285            .filter(|flag| **flag)
286            .count();
287        let ewma_points = ewma_trace.alarm.iter().filter(|flag| **flag).count();
288        let cusum_points = cusum_trace.alarm.iter().filter(|flag| **flag).count();
289        let raw_boundary_points = grammar_trace
290            .raw_states
291            .iter()
292            .filter(|state| **state == GrammarState::Boundary)
293            .count();
294        let persistent_boundary_points = grammar_trace
295            .persistent_boundary
296            .iter()
297            .filter(|flag| **flag)
298            .count();
299        let raw_violation_points = grammar_trace
300            .raw_states
301            .iter()
302            .filter(|state| **state == GrammarState::Violation)
303            .count();
304        let persistent_violation_points = grammar_trace
305            .persistent_violation
306            .iter()
307            .filter(|flag| **flag)
308            .count();
309
310        threshold_alarm_points += threshold_points;
311        ewma_alarm_points += ewma_points;
312        cusum_alarm_points += cusum_points;
313        dsfb_raw_boundary_points += raw_boundary_points;
314        dsfb_persistent_boundary_points += persistent_boundary_points;
315        dsfb_raw_violation_points += raw_violation_points;
316        dsfb_persistent_violation_points += persistent_violation_points;
317        grammar_imputation_suppression_points += grammar_trace
318            .suppressed_by_imputation
319            .iter()
320            .filter(|flag| **flag)
321            .count();
322
323        feature_metrics.push(FeatureMetrics {
324            feature_index: feature.feature_index,
325            feature_name: feature.feature_name.clone(),
326            analyzable: feature.analyzable,
327            healthy_mean: feature.healthy_mean,
328            healthy_std: feature.healthy_std,
329            rho: feature.rho,
330            ewma_healthy_mean: ewma_trace.healthy_mean,
331            ewma_healthy_std: ewma_trace.healthy_std,
332            ewma_threshold: ewma_trace.threshold,
333            cusum_healthy_mean: cusum_trace.healthy_mean,
334            cusum_healthy_std: cusum_trace.healthy_std,
335            cusum_kappa: cusum_trace.kappa,
336            cusum_alarm_threshold: cusum_trace.alarm_threshold,
337            drift_threshold: sign_trace.drift_threshold,
338            slew_threshold: sign_trace.slew_threshold,
339            missing_fraction: dataset.per_feature_missing_fraction[feature.feature_index],
340            ewma_alarm_points: ewma_points,
341            cusum_alarm_points: cusum_points,
342            dsfb_raw_boundary_points: raw_boundary_points,
343            dsfb_persistent_boundary_points: persistent_boundary_points,
344            dsfb_raw_violation_points: raw_violation_points,
345            dsfb_persistent_violation_points: persistent_violation_points,
346            threshold_alarm_points: threshold_points,
347            motif_point_hits: 0,
348            motif_run_hits: 0,
349            pre_failure_motif_run_hits: 0,
350            pre_failure_run_hits: 0,
351            motif_precision_proxy: None,
352        });
353    }
354
355    let failure_indices = dataset
356        .labels
357        .iter()
358        .enumerate()
359        .filter_map(|(index, label)| (*label == 1).then_some(index))
360        .collect::<Vec<_>>();
361    let pass_indices = dataset
362        .labels
363        .iter()
364        .enumerate()
365        .filter_map(|(index, label)| (*label == -1).then_some(index))
366        .collect::<Vec<_>>();
367
368    let failure_window_mask = failure_window_mask(
369        dataset.labels.len(),
370        &failure_indices,
371        config.pre_failure_lookback_runs,
372    );
373    let motif_metrics = compute_motif_metrics(grammar, &failure_window_mask);
374    for (feature_metric, grammar_trace) in feature_metrics.iter_mut().zip(&grammar.traces) {
375        let (motif_point_hits, motif_run_hits, pre_failure_motif_run_hits) =
376            feature_motif_counts(grammar_trace, &failure_window_mask);
377        feature_metric.motif_point_hits = motif_point_hits;
378        feature_metric.motif_run_hits = motif_run_hits;
379        feature_metric.pre_failure_motif_run_hits = pre_failure_motif_run_hits;
380        feature_metric.pre_failure_run_hits = feature_pre_failure_run_hits(
381            grammar_trace,
382            &failure_indices,
383            config.pre_failure_lookback_runs,
384        );
385        feature_metric.motif_precision_proxy = (motif_run_hits > 0)
386            .then_some(pre_failure_motif_run_hits as f64 / motif_run_hits as f64);
387    }
388    let boundary_episode_summary = compute_boundary_episode_summary(grammar);
389    let per_failure_run_signals = compute_per_failure_run_signals(
390        dataset,
391        residuals,
392        baselines,
393        grammar,
394        config.pre_failure_lookback_runs,
395        &failure_indices,
396    );
397    let lead_time_summary = summarize_lead_times(&per_failure_run_signals);
398    let density_metrics = compute_density_metrics(
399        dataset,
400        nominal,
401        residuals,
402        baselines,
403        grammar,
404        config.density_window,
405        &failure_window_mask,
406    );
407    let density_summary = summarize_densities(&density_metrics, config.density_window);
408
409    let mut failure_runs_with_preceding_dsfb_raw_signal = 0usize;
410    let mut failure_runs_with_preceding_dsfb_persistent_signal = 0usize;
411    let mut failure_runs_with_preceding_dsfb_raw_boundary_signal = 0usize;
412    let mut failure_runs_with_preceding_dsfb_persistent_boundary_signal = 0usize;
413    let mut failure_runs_with_preceding_dsfb_raw_violation_signal = 0usize;
414    let mut failure_runs_with_preceding_dsfb_persistent_violation_signal = 0usize;
415    let mut failure_runs_with_preceding_ewma_signal = 0usize;
416    let mut failure_runs_with_preceding_cusum_signal = 0usize;
417    let mut failure_runs_with_preceding_run_energy_signal = 0usize;
418    let mut failure_runs_with_preceding_pca_fdc_signal = 0usize;
419    let mut failure_runs_with_preceding_threshold_signal = 0usize;
420    for record in &per_failure_run_signals {
421        if record.earliest_dsfb_raw_boundary_run.is_some()
422            || record.earliest_dsfb_raw_violation_run.is_some()
423        {
424            failure_runs_with_preceding_dsfb_raw_signal += 1;
425        }
426        if record.earliest_dsfb_persistent_boundary_run.is_some()
427            || record.earliest_dsfb_persistent_violation_run.is_some()
428        {
429            failure_runs_with_preceding_dsfb_persistent_signal += 1;
430        }
431        if record.earliest_dsfb_raw_boundary_run.is_some() {
432            failure_runs_with_preceding_dsfb_raw_boundary_signal += 1;
433        }
434        if record.earliest_dsfb_persistent_boundary_run.is_some() {
435            failure_runs_with_preceding_dsfb_persistent_boundary_signal += 1;
436        }
437        if record.earliest_dsfb_raw_violation_run.is_some() {
438            failure_runs_with_preceding_dsfb_raw_violation_signal += 1;
439        }
440        if record.earliest_dsfb_persistent_violation_run.is_some() {
441            failure_runs_with_preceding_dsfb_persistent_violation_signal += 1;
442        }
443        if record.earliest_ewma_run.is_some() {
444            failure_runs_with_preceding_ewma_signal += 1;
445        }
446        if record.earliest_cusum_run.is_some() {
447            failure_runs_with_preceding_cusum_signal += 1;
448        }
449        if record.earliest_run_energy_run.is_some() {
450            failure_runs_with_preceding_run_energy_signal += 1;
451        }
452        if record.earliest_pca_fdc_run.is_some() {
453            failure_runs_with_preceding_pca_fdc_signal += 1;
454        }
455        if record.earliest_threshold_run.is_some() {
456            failure_runs_with_preceding_threshold_signal += 1;
457        }
458    }
459
460    let pass_runs_with_dsfb_raw_boundary_signal =
461        count_runs_with_signal(&pass_indices, |run_index| {
462            any_trace_raw_state(grammar, run_index, GrammarState::Boundary)
463        });
464    let pass_runs_with_dsfb_persistent_boundary_signal =
465        count_runs_with_signal(&pass_indices, |run_index| {
466            any_trace_persistent(grammar, run_index, GrammarState::Boundary)
467        });
468    let pass_runs_with_dsfb_raw_violation_signal =
469        count_runs_with_signal(&pass_indices, |run_index| {
470            any_trace_raw_state(grammar, run_index, GrammarState::Violation)
471        });
472    let pass_runs_with_dsfb_persistent_violation_signal =
473        count_runs_with_signal(&pass_indices, |run_index| {
474            any_trace_persistent(grammar, run_index, GrammarState::Violation)
475        });
476    let pass_runs_with_ewma_signal = count_runs_with_signal(&pass_indices, |run_index| {
477        baselines.ewma.iter().any(|trace| trace.alarm[run_index])
478    });
479    let pass_runs_with_cusum_signal = count_runs_with_signal(&pass_indices, |run_index| {
480        baselines.cusum.iter().any(|trace| trace.alarm[run_index])
481    });
482    let pass_runs_with_run_energy_signal = count_runs_with_signal(&pass_indices, |run_index| {
483        baselines.run_energy.alarm[run_index]
484    });
485    let pass_runs_with_pca_fdc_signal = count_runs_with_signal(&pass_indices, |run_index| {
486        baselines.pca_fdc.alarm[run_index]
487    });
488    let pass_runs_with_threshold_signal = count_runs_with_signal(&pass_indices, |run_index| {
489        residuals
490            .traces
491            .iter()
492            .any(|trace| trace.threshold_alarm[run_index])
493    });
494
495    let mut top_feature_indices = feature_metrics
496        .iter()
497        .filter(|feature| nominal.features[feature.feature_index].analyzable)
498        .collect::<Vec<_>>();
499    top_feature_indices.sort_by(|left, right| {
500        right
501            .dsfb_persistent_boundary_points
502            .cmp(&left.dsfb_persistent_boundary_points)
503            .then_with(|| {
504                right
505                    .dsfb_raw_boundary_points
506                    .cmp(&left.dsfb_raw_boundary_points)
507            })
508            .then_with(|| right.ewma_alarm_points.cmp(&left.ewma_alarm_points))
509            .then_with(|| right.cusum_alarm_points.cmp(&left.cusum_alarm_points))
510            .then_with(|| {
511                right
512                    .threshold_alarm_points
513                    .cmp(&left.threshold_alarm_points)
514            })
515            .then_with(|| left.feature_index.cmp(&right.feature_index))
516    });
517    let top_feature_indices = top_feature_indices
518        .into_iter()
519        .take(6)
520        .map(|feature| feature.feature_index)
521        .collect::<Vec<_>>();
522
523    let pass_runs = pass_indices.len();
524
525    BenchmarkMetrics {
526        summary: BenchmarkSummary {
527            dataset_summary: dataset.summary.clone(),
528            analyzable_feature_count: nominal
529                .features
530                .iter()
531                .filter(|feature| feature.analyzable)
532                .count(),
533            grammar_imputation_suppression_points,
534            threshold_alarm_points,
535            ewma_alarm_points,
536            cusum_alarm_points,
537            run_energy_alarm_points,
538            pca_fdc_alarm_points,
539            dsfb_raw_boundary_points,
540            dsfb_persistent_boundary_points,
541            dsfb_raw_violation_points,
542            dsfb_persistent_violation_points,
543            failure_runs: failure_indices.len(),
544            failure_runs_with_preceding_dsfb_raw_signal,
545            failure_runs_with_preceding_dsfb_persistent_signal,
546            failure_runs_with_preceding_dsfb_raw_boundary_signal,
547            failure_runs_with_preceding_dsfb_persistent_boundary_signal,
548            failure_runs_with_preceding_dsfb_raw_violation_signal,
549            failure_runs_with_preceding_dsfb_persistent_violation_signal,
550            failure_runs_with_preceding_ewma_signal,
551            failure_runs_with_preceding_cusum_signal,
552            failure_runs_with_preceding_run_energy_signal,
553            failure_runs_with_preceding_pca_fdc_signal,
554            failure_runs_with_preceding_threshold_signal,
555            pass_runs,
556            pass_runs_with_dsfb_raw_boundary_signal,
557            pass_runs_with_dsfb_persistent_boundary_signal,
558            pass_runs_with_dsfb_raw_violation_signal,
559            pass_runs_with_dsfb_persistent_violation_signal,
560            pass_runs_with_ewma_signal,
561            pass_runs_with_cusum_signal,
562            pass_runs_with_run_energy_signal,
563            pass_runs_with_pca_fdc_signal,
564            pass_runs_with_threshold_signal,
565            pass_run_dsfb_raw_boundary_nuisance_rate: rate(
566                pass_runs_with_dsfb_raw_boundary_signal,
567                pass_runs,
568            ),
569            pass_run_dsfb_persistent_boundary_nuisance_rate: rate(
570                pass_runs_with_dsfb_persistent_boundary_signal,
571                pass_runs,
572            ),
573            pass_run_dsfb_raw_violation_nuisance_rate: rate(
574                pass_runs_with_dsfb_raw_violation_signal,
575                pass_runs,
576            ),
577            pass_run_dsfb_persistent_violation_nuisance_rate: rate(
578                pass_runs_with_dsfb_persistent_violation_signal,
579                pass_runs,
580            ),
581            pass_run_ewma_nuisance_rate: rate(pass_runs_with_ewma_signal, pass_runs),
582            pass_run_cusum_nuisance_rate: rate(pass_runs_with_cusum_signal, pass_runs),
583            pass_run_run_energy_nuisance_rate: rate(pass_runs_with_run_energy_signal, pass_runs),
584            pass_run_pca_fdc_nuisance_rate: rate(pass_runs_with_pca_fdc_signal, pass_runs),
585            pass_run_threshold_nuisance_rate: rate(pass_runs_with_threshold_signal, pass_runs),
586        },
587        lead_time_summary,
588        density_summary,
589        boundary_episode_summary,
590        dsa_summary: None,
591        motif_metrics,
592        per_failure_run_signals,
593        density_metrics,
594        feature_metrics,
595        top_feature_indices,
596    }
597}
598
599fn count_runs_with_signal<F>(run_indices: &[usize], predicate: F) -> usize
600where
601    F: Fn(usize) -> bool,
602{
603    run_indices
604        .iter()
605        .filter(|&&run_index| predicate(run_index))
606        .count()
607}
608
609fn any_trace_raw_state(grammar: &GrammarSet, run_index: usize, target: GrammarState) -> bool {
610    grammar
611        .traces
612        .iter()
613        .any(|trace| trace.raw_states[run_index] == target)
614}
615
616fn any_trace_persistent(grammar: &GrammarSet, run_index: usize, target: GrammarState) -> bool {
617    grammar.traces.iter().any(|trace| match target {
618        GrammarState::Boundary => trace.persistent_boundary[run_index],
619        GrammarState::Violation => trace.persistent_violation[run_index],
620        GrammarState::Admissible => false,
621    })
622}
623
624fn failure_window_mask(
625    run_count: usize,
626    failure_indices: &[usize],
627    pre_failure_lookback_runs: usize,
628) -> Vec<bool> {
629    let mut mask = vec![false; run_count];
630    for &failure_index in failure_indices {
631        let start = failure_index.saturating_sub(pre_failure_lookback_runs);
632        for slot in &mut mask[start..failure_index] {
633            *slot = true;
634        }
635    }
636    mask
637}
638
639fn compute_per_failure_run_signals(
640    dataset: &PreparedDataset,
641    residuals: &ResidualSet,
642    baselines: &BaselineSet,
643    grammar: &GrammarSet,
644    pre_failure_lookback_runs: usize,
645    failure_indices: &[usize],
646) -> Vec<PerFailureRunSignal> {
647    failure_indices
648        .iter()
649        .map(|&failure_index| {
650            let window_start = failure_index.saturating_sub(pre_failure_lookback_runs);
651            let earliest_dsfb_raw_boundary_run =
652                earliest_signal_in_window(window_start, failure_index, |run_index| {
653                    any_trace_raw_state(grammar, run_index, GrammarState::Boundary)
654                });
655            let earliest_dsfb_persistent_boundary_run =
656                earliest_signal_in_window(window_start, failure_index, |run_index| {
657                    any_trace_persistent(grammar, run_index, GrammarState::Boundary)
658                });
659            let earliest_dsfb_raw_violation_run =
660                earliest_signal_in_window(window_start, failure_index, |run_index| {
661                    any_trace_raw_state(grammar, run_index, GrammarState::Violation)
662                });
663            let earliest_dsfb_persistent_violation_run =
664                earliest_signal_in_window(window_start, failure_index, |run_index| {
665                    any_trace_persistent(grammar, run_index, GrammarState::Violation)
666                });
667            let earliest_threshold_run =
668                earliest_signal_in_window(window_start, failure_index, |run_index| {
669                    residuals
670                        .traces
671                        .iter()
672                        .any(|trace| trace.threshold_alarm[run_index])
673                });
674            let earliest_ewma_run =
675                earliest_signal_in_window(window_start, failure_index, |run_index| {
676                    baselines.ewma.iter().any(|trace| trace.alarm[run_index])
677                });
678            let earliest_cusum_run =
679                earliest_signal_in_window(window_start, failure_index, |run_index| {
680                    baselines.cusum.iter().any(|trace| trace.alarm[run_index])
681                });
682            let earliest_run_energy_run =
683                earliest_signal_in_window(window_start, failure_index, |run_index| {
684                    baselines.run_energy.alarm[run_index]
685                });
686            let earliest_pca_fdc_run =
687                earliest_signal_in_window(window_start, failure_index, |run_index| {
688                    baselines.pca_fdc.alarm[run_index]
689                });
690
691            let dsfb_raw_boundary_lead_runs =
692                earliest_dsfb_raw_boundary_run.map(|index| failure_index - index);
693            let dsfb_persistent_boundary_lead_runs =
694                earliest_dsfb_persistent_boundary_run.map(|index| failure_index - index);
695            let dsfb_raw_violation_lead_runs =
696                earliest_dsfb_raw_violation_run.map(|index| failure_index - index);
697            let dsfb_persistent_violation_lead_runs =
698                earliest_dsfb_persistent_violation_run.map(|index| failure_index - index);
699            let threshold_lead_runs = earliest_threshold_run.map(|index| failure_index - index);
700            let ewma_lead_runs = earliest_ewma_run.map(|index| failure_index - index);
701            let cusum_lead_runs = earliest_cusum_run.map(|index| failure_index - index);
702            let run_energy_lead_runs = earliest_run_energy_run.map(|index| failure_index - index);
703            let pca_fdc_lead_runs = earliest_pca_fdc_run.map(|index| failure_index - index);
704
705            PerFailureRunSignal {
706                failure_run_index: failure_index,
707                failure_timestamp: dataset.timestamps[failure_index]
708                    .format("%Y-%m-%d %H:%M:%S")
709                    .to_string(),
710                earliest_dsfb_raw_boundary_run,
711                earliest_dsfb_persistent_boundary_run,
712                earliest_dsfb_raw_violation_run,
713                earliest_dsfb_persistent_violation_run,
714                earliest_threshold_run,
715                earliest_ewma_run,
716                earliest_cusum_run,
717                earliest_run_energy_run,
718                earliest_pca_fdc_run,
719                dsfb_raw_boundary_lead_runs,
720                dsfb_persistent_boundary_lead_runs,
721                dsfb_raw_violation_lead_runs,
722                dsfb_persistent_violation_lead_runs,
723                threshold_lead_runs,
724                ewma_lead_runs,
725                cusum_lead_runs,
726                run_energy_lead_runs,
727                pca_fdc_lead_runs,
728                dsfb_raw_boundary_minus_cusum_delta_runs: paired_delta(
729                    dsfb_raw_boundary_lead_runs,
730                    cusum_lead_runs,
731                ),
732                dsfb_raw_boundary_minus_run_energy_delta_runs: paired_delta(
733                    dsfb_raw_boundary_lead_runs,
734                    run_energy_lead_runs,
735                ),
736                dsfb_raw_boundary_minus_pca_fdc_delta_runs: paired_delta(
737                    dsfb_raw_boundary_lead_runs,
738                    pca_fdc_lead_runs,
739                ),
740                dsfb_raw_boundary_minus_threshold_delta_runs: paired_delta(
741                    dsfb_raw_boundary_lead_runs,
742                    threshold_lead_runs,
743                ),
744                dsfb_raw_boundary_minus_ewma_delta_runs: paired_delta(
745                    dsfb_raw_boundary_lead_runs,
746                    ewma_lead_runs,
747                ),
748                dsfb_persistent_boundary_minus_threshold_delta_runs: paired_delta(
749                    dsfb_persistent_boundary_lead_runs,
750                    threshold_lead_runs,
751                ),
752                dsfb_persistent_boundary_minus_ewma_delta_runs: paired_delta(
753                    dsfb_persistent_boundary_lead_runs,
754                    ewma_lead_runs,
755                ),
756                dsfb_persistent_boundary_minus_cusum_delta_runs: paired_delta(
757                    dsfb_persistent_boundary_lead_runs,
758                    cusum_lead_runs,
759                ),
760                dsfb_persistent_boundary_minus_run_energy_delta_runs: paired_delta(
761                    dsfb_persistent_boundary_lead_runs,
762                    run_energy_lead_runs,
763                ),
764                dsfb_persistent_boundary_minus_pca_fdc_delta_runs: paired_delta(
765                    dsfb_persistent_boundary_lead_runs,
766                    pca_fdc_lead_runs,
767                ),
768                dsfb_raw_violation_minus_cusum_delta_runs: paired_delta(
769                    dsfb_raw_violation_lead_runs,
770                    cusum_lead_runs,
771                ),
772                dsfb_raw_violation_minus_run_energy_delta_runs: paired_delta(
773                    dsfb_raw_violation_lead_runs,
774                    run_energy_lead_runs,
775                ),
776                dsfb_raw_violation_minus_pca_fdc_delta_runs: paired_delta(
777                    dsfb_raw_violation_lead_runs,
778                    pca_fdc_lead_runs,
779                ),
780                dsfb_raw_violation_minus_threshold_delta_runs: paired_delta(
781                    dsfb_raw_violation_lead_runs,
782                    threshold_lead_runs,
783                ),
784                dsfb_raw_violation_minus_ewma_delta_runs: paired_delta(
785                    dsfb_raw_violation_lead_runs,
786                    ewma_lead_runs,
787                ),
788                dsfb_persistent_violation_minus_threshold_delta_runs: paired_delta(
789                    dsfb_persistent_violation_lead_runs,
790                    threshold_lead_runs,
791                ),
792                dsfb_persistent_violation_minus_ewma_delta_runs: paired_delta(
793                    dsfb_persistent_violation_lead_runs,
794                    ewma_lead_runs,
795                ),
796                dsfb_persistent_violation_minus_cusum_delta_runs: paired_delta(
797                    dsfb_persistent_violation_lead_runs,
798                    cusum_lead_runs,
799                ),
800                dsfb_persistent_violation_minus_run_energy_delta_runs: paired_delta(
801                    dsfb_persistent_violation_lead_runs,
802                    run_energy_lead_runs,
803                ),
804                dsfb_persistent_violation_minus_pca_fdc_delta_runs: paired_delta(
805                    dsfb_persistent_violation_lead_runs,
806                    pca_fdc_lead_runs,
807                ),
808            }
809        })
810        .collect()
811}
812
813fn earliest_signal_in_window<F>(start: usize, end: usize, predicate: F) -> Option<usize>
814where
815    F: Fn(usize) -> bool,
816{
817    (start..end).find(|&index| predicate(index))
818}
819
820fn paired_delta(left: Option<usize>, right: Option<usize>) -> Option<i64> {
821    Some(left? as i64 - right? as i64)
822}
823
824fn summarize_lead_times(records: &[PerFailureRunSignal]) -> LeadTimeSummary {
825    LeadTimeSummary {
826        failure_runs_with_raw_boundary_lead: count_present(
827            records
828                .iter()
829                .map(|record| record.dsfb_raw_boundary_lead_runs),
830        ),
831        failure_runs_with_persistent_boundary_lead: count_present(
832            records
833                .iter()
834                .map(|record| record.dsfb_persistent_boundary_lead_runs),
835        ),
836        failure_runs_with_raw_violation_lead: count_present(
837            records
838                .iter()
839                .map(|record| record.dsfb_raw_violation_lead_runs),
840        ),
841        failure_runs_with_persistent_violation_lead: count_present(
842            records
843                .iter()
844                .map(|record| record.dsfb_persistent_violation_lead_runs),
845        ),
846        failure_runs_with_threshold_lead: count_present(
847            records.iter().map(|record| record.threshold_lead_runs),
848        ),
849        failure_runs_with_ewma_lead: count_present(
850            records.iter().map(|record| record.ewma_lead_runs),
851        ),
852        failure_runs_with_cusum_lead: count_present(
853            records.iter().map(|record| record.cusum_lead_runs),
854        ),
855        failure_runs_with_run_energy_lead: count_present(
856            records.iter().map(|record| record.run_energy_lead_runs),
857        ),
858        failure_runs_with_pca_fdc_lead: count_present(
859            records.iter().map(|record| record.pca_fdc_lead_runs),
860        ),
861        mean_raw_boundary_lead_runs: mean_option_usize(
862            &records
863                .iter()
864                .map(|record| record.dsfb_raw_boundary_lead_runs)
865                .collect::<Vec<_>>(),
866        ),
867        mean_persistent_boundary_lead_runs: mean_option_usize(
868            &records
869                .iter()
870                .map(|record| record.dsfb_persistent_boundary_lead_runs)
871                .collect::<Vec<_>>(),
872        ),
873        mean_raw_violation_lead_runs: mean_option_usize(
874            &records
875                .iter()
876                .map(|record| record.dsfb_raw_violation_lead_runs)
877                .collect::<Vec<_>>(),
878        ),
879        mean_persistent_violation_lead_runs: mean_option_usize(
880            &records
881                .iter()
882                .map(|record| record.dsfb_persistent_violation_lead_runs)
883                .collect::<Vec<_>>(),
884        ),
885        mean_threshold_lead_runs: mean_option_usize(
886            &records
887                .iter()
888                .map(|record| record.threshold_lead_runs)
889                .collect::<Vec<_>>(),
890        ),
891        mean_ewma_lead_runs: mean_option_usize(
892            &records
893                .iter()
894                .map(|record| record.ewma_lead_runs)
895                .collect::<Vec<_>>(),
896        ),
897        mean_cusum_lead_runs: mean_option_usize(
898            &records
899                .iter()
900                .map(|record| record.cusum_lead_runs)
901                .collect::<Vec<_>>(),
902        ),
903        mean_run_energy_lead_runs: mean_option_usize(
904            &records
905                .iter()
906                .map(|record| record.run_energy_lead_runs)
907                .collect::<Vec<_>>(),
908        ),
909        mean_pca_fdc_lead_runs: mean_option_usize(
910            &records
911                .iter()
912                .map(|record| record.pca_fdc_lead_runs)
913                .collect::<Vec<_>>(),
914        ),
915        mean_raw_boundary_minus_cusum_delta_runs: mean_option_i64(
916            &records
917                .iter()
918                .map(|record| record.dsfb_raw_boundary_minus_cusum_delta_runs)
919                .collect::<Vec<_>>(),
920        ),
921        mean_raw_boundary_minus_run_energy_delta_runs: mean_option_i64(
922            &records
923                .iter()
924                .map(|record| record.dsfb_raw_boundary_minus_run_energy_delta_runs)
925                .collect::<Vec<_>>(),
926        ),
927        mean_raw_boundary_minus_pca_fdc_delta_runs: mean_option_i64(
928            &records
929                .iter()
930                .map(|record| record.dsfb_raw_boundary_minus_pca_fdc_delta_runs)
931                .collect::<Vec<_>>(),
932        ),
933        mean_raw_boundary_minus_threshold_delta_runs: mean_option_i64(
934            &records
935                .iter()
936                .map(|record| record.dsfb_raw_boundary_minus_threshold_delta_runs)
937                .collect::<Vec<_>>(),
938        ),
939        mean_raw_boundary_minus_ewma_delta_runs: mean_option_i64(
940            &records
941                .iter()
942                .map(|record| record.dsfb_raw_boundary_minus_ewma_delta_runs)
943                .collect::<Vec<_>>(),
944        ),
945        mean_persistent_boundary_minus_threshold_delta_runs: mean_option_i64(
946            &records
947                .iter()
948                .map(|record| record.dsfb_persistent_boundary_minus_threshold_delta_runs)
949                .collect::<Vec<_>>(),
950        ),
951        mean_persistent_boundary_minus_ewma_delta_runs: mean_option_i64(
952            &records
953                .iter()
954                .map(|record| record.dsfb_persistent_boundary_minus_ewma_delta_runs)
955                .collect::<Vec<_>>(),
956        ),
957        mean_persistent_boundary_minus_cusum_delta_runs: mean_option_i64(
958            &records
959                .iter()
960                .map(|record| record.dsfb_persistent_boundary_minus_cusum_delta_runs)
961                .collect::<Vec<_>>(),
962        ),
963        mean_persistent_boundary_minus_run_energy_delta_runs: mean_option_i64(
964            &records
965                .iter()
966                .map(|record| record.dsfb_persistent_boundary_minus_run_energy_delta_runs)
967                .collect::<Vec<_>>(),
968        ),
969        mean_persistent_boundary_minus_pca_fdc_delta_runs: mean_option_i64(
970            &records
971                .iter()
972                .map(|record| record.dsfb_persistent_boundary_minus_pca_fdc_delta_runs)
973                .collect::<Vec<_>>(),
974        ),
975        mean_raw_violation_minus_cusum_delta_runs: mean_option_i64(
976            &records
977                .iter()
978                .map(|record| record.dsfb_raw_violation_minus_cusum_delta_runs)
979                .collect::<Vec<_>>(),
980        ),
981        mean_raw_violation_minus_run_energy_delta_runs: mean_option_i64(
982            &records
983                .iter()
984                .map(|record| record.dsfb_raw_violation_minus_run_energy_delta_runs)
985                .collect::<Vec<_>>(),
986        ),
987        mean_raw_violation_minus_pca_fdc_delta_runs: mean_option_i64(
988            &records
989                .iter()
990                .map(|record| record.dsfb_raw_violation_minus_pca_fdc_delta_runs)
991                .collect::<Vec<_>>(),
992        ),
993        mean_raw_violation_minus_threshold_delta_runs: mean_option_i64(
994            &records
995                .iter()
996                .map(|record| record.dsfb_raw_violation_minus_threshold_delta_runs)
997                .collect::<Vec<_>>(),
998        ),
999        mean_raw_violation_minus_ewma_delta_runs: mean_option_i64(
1000            &records
1001                .iter()
1002                .map(|record| record.dsfb_raw_violation_minus_ewma_delta_runs)
1003                .collect::<Vec<_>>(),
1004        ),
1005        mean_persistent_violation_minus_threshold_delta_runs: mean_option_i64(
1006            &records
1007                .iter()
1008                .map(|record| record.dsfb_persistent_violation_minus_threshold_delta_runs)
1009                .collect::<Vec<_>>(),
1010        ),
1011        mean_persistent_violation_minus_ewma_delta_runs: mean_option_i64(
1012            &records
1013                .iter()
1014                .map(|record| record.dsfb_persistent_violation_minus_ewma_delta_runs)
1015                .collect::<Vec<_>>(),
1016        ),
1017        mean_persistent_violation_minus_cusum_delta_runs: mean_option_i64(
1018            &records
1019                .iter()
1020                .map(|record| record.dsfb_persistent_violation_minus_cusum_delta_runs)
1021                .collect::<Vec<_>>(),
1022        ),
1023        mean_persistent_violation_minus_run_energy_delta_runs: mean_option_i64(
1024            &records
1025                .iter()
1026                .map(|record| record.dsfb_persistent_violation_minus_run_energy_delta_runs)
1027                .collect::<Vec<_>>(),
1028        ),
1029        mean_persistent_violation_minus_pca_fdc_delta_runs: mean_option_i64(
1030            &records
1031                .iter()
1032                .map(|record| record.dsfb_persistent_violation_minus_pca_fdc_delta_runs)
1033                .collect::<Vec<_>>(),
1034        ),
1035    }
1036}
1037
1038fn compute_density_metrics(
1039    dataset: &PreparedDataset,
1040    nominal: &NominalModel,
1041    residuals: &ResidualSet,
1042    baselines: &BaselineSet,
1043    grammar: &GrammarSet,
1044    density_window: usize,
1045    failure_window_mask: &[bool],
1046) -> Vec<DensityMetricRecord> {
1047    let analyzable_feature_indices = nominal
1048        .features
1049        .iter()
1050        .filter(|feature| feature.analyzable)
1051        .map(|feature| feature.feature_index)
1052        .collect::<Vec<_>>();
1053    let feature_denominator = analyzable_feature_indices.len().max(1);
1054
1055    (0..dataset.labels.len())
1056        .map(|run_index| {
1057            let start = run_index.saturating_sub(density_window.saturating_sub(1));
1058            let window_len = run_index - start + 1;
1059            let denominator = (window_len * feature_denominator) as f64;
1060            let mut raw_boundary_hits = 0usize;
1061            let mut persistent_boundary_hits = 0usize;
1062            let mut raw_violation_hits = 0usize;
1063            let mut persistent_violation_hits = 0usize;
1064            let mut threshold_hits = 0usize;
1065            let mut ewma_hits = 0usize;
1066            let mut cusum_hits = 0usize;
1067
1068            for &feature_index in &analyzable_feature_indices {
1069                let grammar_trace = &grammar.traces[feature_index];
1070                let residual_trace = &residuals.traces[feature_index];
1071                let ewma_trace = &baselines.ewma[feature_index];
1072                let cusum_trace = &baselines.cusum[feature_index];
1073                for offset in start..=run_index {
1074                    if grammar_trace.raw_states[offset] == GrammarState::Boundary {
1075                        raw_boundary_hits += 1;
1076                    }
1077                    if grammar_trace.persistent_boundary[offset] {
1078                        persistent_boundary_hits += 1;
1079                    }
1080                    if grammar_trace.raw_states[offset] == GrammarState::Violation {
1081                        raw_violation_hits += 1;
1082                    }
1083                    if grammar_trace.persistent_violation[offset] {
1084                        persistent_violation_hits += 1;
1085                    }
1086                    if residual_trace.threshold_alarm[offset] {
1087                        threshold_hits += 1;
1088                    }
1089                    if ewma_trace.alarm[offset] {
1090                        ewma_hits += 1;
1091                    }
1092                    if cusum_trace.alarm[offset] {
1093                        cusum_hits += 1;
1094                    }
1095                }
1096            }
1097
1098            DensityMetricRecord {
1099                run_index,
1100                timestamp: dataset.timestamps[run_index]
1101                    .format("%Y-%m-%d %H:%M:%S")
1102                    .to_string(),
1103                label: dataset.labels[run_index],
1104                in_pre_failure_window: failure_window_mask[run_index],
1105                raw_boundary_density: raw_boundary_hits as f64 / denominator,
1106                persistent_boundary_density: persistent_boundary_hits as f64 / denominator,
1107                raw_violation_density: raw_violation_hits as f64 / denominator,
1108                persistent_violation_density: persistent_violation_hits as f64 / denominator,
1109                threshold_density: threshold_hits as f64 / denominator,
1110                ewma_density: ewma_hits as f64 / denominator,
1111                cusum_density: cusum_hits as f64 / denominator,
1112            }
1113        })
1114        .collect()
1115}
1116
1117fn summarize_densities(records: &[DensityMetricRecord], density_window: usize) -> DensitySummary {
1118    let failure_records = records
1119        .iter()
1120        .filter(|record| record.label == 1)
1121        .collect::<Vec<_>>();
1122    let pass_records = records
1123        .iter()
1124        .filter(|record| record.label == -1)
1125        .collect::<Vec<_>>();
1126
1127    DensitySummary {
1128        density_window,
1129        mean_raw_boundary_density_failure: mean_record_field(&failure_records, |record| {
1130            record.raw_boundary_density
1131        }),
1132        mean_raw_boundary_density_pass: mean_record_field(&pass_records, |record| {
1133            record.raw_boundary_density
1134        }),
1135        mean_persistent_boundary_density_failure: mean_record_field(&failure_records, |record| {
1136            record.persistent_boundary_density
1137        }),
1138        mean_persistent_boundary_density_pass: mean_record_field(&pass_records, |record| {
1139            record.persistent_boundary_density
1140        }),
1141        mean_raw_violation_density_failure: mean_record_field(&failure_records, |record| {
1142            record.raw_violation_density
1143        }),
1144        mean_raw_violation_density_pass: mean_record_field(&pass_records, |record| {
1145            record.raw_violation_density
1146        }),
1147        mean_persistent_violation_density_failure: mean_record_field(&failure_records, |record| {
1148            record.persistent_violation_density
1149        }),
1150        mean_persistent_violation_density_pass: mean_record_field(&pass_records, |record| {
1151            record.persistent_violation_density
1152        }),
1153        mean_threshold_density_failure: mean_record_field(&failure_records, |record| {
1154            record.threshold_density
1155        }),
1156        mean_threshold_density_pass: mean_record_field(&pass_records, |record| {
1157            record.threshold_density
1158        }),
1159        mean_ewma_density_failure: mean_record_field(&failure_records, |record| {
1160            record.ewma_density
1161        }),
1162        mean_ewma_density_pass: mean_record_field(&pass_records, |record| record.ewma_density),
1163        mean_cusum_density_failure: mean_record_field(&failure_records, |record| {
1164            record.cusum_density
1165        }),
1166        mean_cusum_density_pass: mean_record_field(&pass_records, |record| record.cusum_density),
1167    }
1168}
1169
1170fn mean_record_field<F>(records: &[&DensityMetricRecord], selector: F) -> f64
1171where
1172    F: Fn(&DensityMetricRecord) -> f64,
1173{
1174    if records.is_empty() {
1175        0.0
1176    } else {
1177        records.iter().map(|record| selector(record)).sum::<f64>() / records.len() as f64
1178    }
1179}
1180
1181fn compute_boundary_episode_summary(grammar: &GrammarSet) -> BoundaryEpisodeSummary {
1182    let raw = episode_stats(grammar, false);
1183    let persistent = episode_stats(grammar, true);
1184
1185    BoundaryEpisodeSummary {
1186        raw_episode_count: raw.episode_count,
1187        persistent_episode_count: persistent.episode_count,
1188        mean_raw_episode_length: raw.mean_episode_length,
1189        mean_persistent_episode_length: persistent.mean_episode_length,
1190        max_raw_episode_length: raw.max_episode_length,
1191        max_persistent_episode_length: persistent.max_episode_length,
1192        raw_non_escalating_episode_fraction: raw.non_escalating_episode_fraction,
1193        persistent_non_escalating_episode_fraction: persistent.non_escalating_episode_fraction,
1194    }
1195}
1196
1197struct EpisodeStats {
1198    episode_count: usize,
1199    mean_episode_length: Option<f64>,
1200    max_episode_length: usize,
1201    non_escalating_episode_fraction: Option<f64>,
1202}
1203
1204fn episode_stats(grammar: &GrammarSet, persistent: bool) -> EpisodeStats {
1205    let mut episode_count = 0usize;
1206    let mut total_length = 0usize;
1207    let mut max_length = 0usize;
1208    let mut non_escalating_episode_count = 0usize;
1209
1210    for trace in &grammar.traces {
1211        let mut index = 0usize;
1212        let len = trace.states.len();
1213        while index < len {
1214            let in_boundary = if persistent {
1215                trace.persistent_boundary[index]
1216            } else {
1217                trace.raw_states[index] == GrammarState::Boundary
1218            };
1219            if !in_boundary {
1220                index += 1;
1221                continue;
1222            }
1223
1224            let start = index;
1225            while index < len
1226                && if persistent {
1227                    trace.persistent_boundary[index]
1228                } else {
1229                    trace.raw_states[index] == GrammarState::Boundary
1230                }
1231            {
1232                index += 1;
1233            }
1234            let length = index - start;
1235            episode_count += 1;
1236            total_length += length;
1237            max_length = max_length.max(length);
1238
1239            let escalates = if persistent {
1240                index < len && trace.persistent_violation[index]
1241            } else {
1242                index < len && trace.raw_states[index] == GrammarState::Violation
1243            };
1244            if !escalates {
1245                non_escalating_episode_count += 1;
1246            }
1247        }
1248    }
1249
1250    EpisodeStats {
1251        episode_count,
1252        mean_episode_length: (episode_count > 0)
1253            .then_some(total_length as f64 / episode_count as f64),
1254        max_episode_length: max_length,
1255        non_escalating_episode_fraction: (episode_count > 0)
1256            .then_some(non_escalating_episode_count as f64 / episode_count as f64),
1257    }
1258}
1259
1260fn compute_motif_metrics(grammar: &GrammarSet, failure_window_mask: &[bool]) -> Vec<MotifMetric> {
1261    let motif_specs = [
1262        (
1263            "pre_failure_slow_drift",
1264            GrammarReason::SustainedOutwardDrift,
1265        ),
1266        ("transient_excursion", GrammarReason::AbruptSlewViolation),
1267        (
1268            "recurrent_boundary_approach",
1269            GrammarReason::RecurrentBoundaryGrazing,
1270        ),
1271    ];
1272
1273    motif_specs
1274        .iter()
1275        .map(|(motif_name, reason)| {
1276            let mut point_hits = 0usize;
1277            let mut run_hits = BTreeSet::new();
1278            let mut pre_failure_window_run_hits = BTreeSet::new();
1279
1280            for trace in &grammar.traces {
1281                for (run_index, trace_reason) in trace.raw_reasons.iter().enumerate() {
1282                    if trace_reason == reason {
1283                        point_hits += 1;
1284                        run_hits.insert(run_index);
1285                        if failure_window_mask[run_index] {
1286                            pre_failure_window_run_hits.insert(run_index);
1287                        }
1288                    }
1289                }
1290            }
1291
1292            let run_hit_count = run_hits.len();
1293            let pre_failure_run_hit_count = pre_failure_window_run_hits.len();
1294
1295            MotifMetric {
1296                motif_name: (*motif_name).into(),
1297                point_hits,
1298                run_hits: run_hit_count,
1299                pre_failure_window_run_hits: pre_failure_run_hit_count,
1300                pre_failure_window_precision_proxy: (run_hit_count > 0)
1301                    .then_some(pre_failure_run_hit_count as f64 / run_hit_count as f64),
1302            }
1303        })
1304        .collect()
1305}
1306
1307fn feature_motif_counts(
1308    grammar_trace: &crate::grammar::FeatureGrammarTrace,
1309    failure_window_mask: &[bool],
1310) -> (usize, usize, usize) {
1311    let mut point_hits = 0usize;
1312    let mut run_hits = 0usize;
1313    let mut pre_failure_run_hits = 0usize;
1314
1315    for (run_index, reason) in grammar_trace.raw_reasons.iter().enumerate() {
1316        if matches!(
1317            reason,
1318            GrammarReason::SustainedOutwardDrift
1319                | GrammarReason::AbruptSlewViolation
1320                | GrammarReason::RecurrentBoundaryGrazing
1321        ) {
1322            point_hits += 1;
1323            run_hits += 1;
1324            if failure_window_mask[run_index] {
1325                pre_failure_run_hits += 1;
1326            }
1327        }
1328    }
1329
1330    (point_hits, run_hits, pre_failure_run_hits)
1331}
1332
1333fn feature_pre_failure_run_hits(
1334    grammar_trace: &crate::grammar::FeatureGrammarTrace,
1335    failure_indices: &[usize],
1336    pre_failure_lookback_runs: usize,
1337) -> usize {
1338    failure_indices
1339        .iter()
1340        .filter(|&&failure_index| {
1341            let start = failure_index.saturating_sub(pre_failure_lookback_runs);
1342            (start..failure_index).any(|run_index| {
1343                matches!(
1344                    grammar_trace.raw_reasons[run_index],
1345                    GrammarReason::SustainedOutwardDrift
1346                        | GrammarReason::AbruptSlewViolation
1347                        | GrammarReason::RecurrentBoundaryGrazing
1348                ) || matches!(
1349                    grammar_trace.raw_states[run_index],
1350                    GrammarState::Boundary | GrammarState::Violation
1351                )
1352            })
1353        })
1354        .count()
1355}
1356
1357fn count_present<I, T>(iter: I) -> usize
1358where
1359    I: Iterator<Item = Option<T>>,
1360{
1361    iter.filter(|value| value.is_some()).count()
1362}
1363
1364fn rate(count: usize, total: usize) -> f64 {
1365    if total == 0 {
1366        0.0
1367    } else {
1368        count as f64 / total as f64
1369    }
1370}
1371
1372fn mean_option_usize(values: &[Option<usize>]) -> Option<f64> {
1373    let present = values.iter().flatten().copied().collect::<Vec<_>>();
1374    (!present.is_empty()).then_some(present.iter().sum::<usize>() as f64 / present.len() as f64)
1375}
1376
1377fn mean_option_i64(values: &[Option<i64>]) -> Option<f64> {
1378    let present = values.iter().flatten().copied().collect::<Vec<_>>();
1379    (!present.is_empty()).then_some(present.iter().sum::<i64>() as f64 / present.len() as f64)
1380}