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}