1use crate::baselines::BaselineSet;
2use crate::cohort::OptimizationExecution;
3use crate::error::{DsfbSemiconductorError, Result};
4use crate::failure_driven::FailureDrivenArtifacts;
5use crate::grammar::{GrammarSet, GrammarState};
6use crate::metrics::BenchmarkMetrics;
7use crate::precursor::{DsaEvaluation, DsaPolicyState};
8use crate::preprocessing::PreparedDataset;
9use crate::residual::ResidualSet;
10use crate::semiotics::{DsfbMotifClass, MotifSet, SemanticLayer};
11use plotters::prelude::*;
12use serde::Serialize;
13use std::collections::{BTreeMap, BTreeSet};
14use std::path::Path;
15
16const TRADEOFF_PLOT_WIDTH: u32 = 1400;
17const TRADEOFF_PLOT_HEIGHT: u32 = 800;
18
19#[derive(Debug, Clone, Serialize)]
20pub struct RecurrentBoundaryStats {
21 pub total_boundary_points: usize,
22 pub total_run_hits: usize,
23 pub total_pre_failure_hits: usize,
24 pub pass_run_hits: usize,
25 pub precision_pre_failure: f64,
26 pub precision_pass: f64,
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct RecurrentBoundaryTradeoffRow {
31 pub suppression_level: f64,
32 pub suppression_label: String,
33 pub investigation_points: usize,
34 pub pass_run_nuisance_proxy: f64,
35 pub delta_nuisance_vs_selected_dsa: f64,
36 pub delta_nuisance_vs_ewma: f64,
37 pub failure_recall: usize,
38 pub failure_runs: usize,
39 pub recall_rate: f64,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct MetricRegroundingRow {
44 pub metric: String,
45 pub baseline: String,
46 pub dsfb_value: f64,
47 pub baseline_value: f64,
48 pub delta_percent: f64,
49}
50
51#[derive(Debug, Clone, Serialize)]
52pub struct TargetDRegressionAnalysis {
53 pub contributing_features: Vec<String>,
54 pub contributing_motifs: Vec<String>,
55 pub contributing_heuristics: Vec<String>,
56 pub contributing_policy_rules: Vec<String>,
57 pub causal_chain: Vec<String>,
58 pub why_regression_occurred: String,
59 pub action_taken: String,
60 pub tradeoff_justification: String,
61}
62
63#[derive(Debug, Clone, Serialize)]
64pub struct MissedFailureFeatureActivity {
65 pub feature_name: String,
66 pub behavior_classification: String,
67 pub max_dsa_score: f64,
68 pub initial_motif_hypothesis: String,
69 pub dominant_dsfb_motif: String,
70 pub dominant_grammar_state: String,
71 pub failure_explanation: String,
72}
73
74#[derive(Debug, Clone, Serialize)]
75pub struct MissedFailureRootCause {
76 pub failure_id: usize,
77 pub feature_activity: Vec<MissedFailureFeatureActivity>,
78 pub residual_trajectory: Vec<f64>,
79 pub drift_trajectory: Vec<f64>,
80 pub slew_trajectory: Vec<f64>,
81 pub motif_presence: Vec<String>,
82 pub grammar_state: Vec<String>,
83 pub reason_for_miss: String,
84 pub classification: String,
85 pub recovered_after_fix: bool,
86 pub recovery_feature: Option<String>,
87 pub recovery_note: String,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct LeadTimeComparisonRow {
92 pub failure_id: usize,
93 pub dsfb_lead_runs: Option<usize>,
94 pub threshold_lead_runs: Option<usize>,
95 pub earliest_semantic_match_lead_runs: Option<usize>,
96 pub threshold_minus_dsfb_runs: Option<i64>,
97 pub threshold_minus_semantic_match_runs: Option<i64>,
98}
99
100#[derive(Debug, Clone, Serialize)]
101pub struct LeadTimeExplanation {
102 pub mean_dsfb_lead_runs: Option<f64>,
103 pub mean_threshold_lead_runs: Option<f64>,
104 pub mean_semantic_match_lead_runs: Option<f64>,
105 pub threshold_earlier_failure_count: usize,
106 pub dsfb_earlier_failure_count: usize,
107 pub semantic_match_precedes_threshold_count: usize,
108 pub motif_emergence_precedes_threshold_count: usize,
109 pub explanation: String,
110 pub validation_note: String,
111}
112
113#[derive(Debug, Clone, Serialize)]
114pub struct EpisodePrecisionMetrics {
115 pub dsfb_episode_count: usize,
116 pub dsfb_pre_failure_episode_count: usize,
117 pub dsfb_precision: f64,
118 pub raw_alarm_count: usize,
119 pub raw_alarm_precision: f64,
120 pub precision_gain_factor: f64,
121}
122
123#[derive(Debug, Clone, Serialize)]
124pub struct SecomAddendumArtifacts {
125 pub recurrent_boundary_stats: RecurrentBoundaryStats,
126 pub recurrent_boundary_tradeoff_curve: Vec<RecurrentBoundaryTradeoffRow>,
127 pub required_tradeoff_statement: String,
128 pub required_tradeoff_statement_supported: bool,
129 pub metric_regrounding: Vec<MetricRegroundingRow>,
130 pub target_d_regression_analysis: TargetDRegressionAnalysis,
131 pub missed_failure_root_cause: MissedFailureRootCause,
132 pub lead_time_comparison: Vec<LeadTimeComparisonRow>,
133 pub lead_time_explanation: LeadTimeExplanation,
134 pub episode_precision_metrics: EpisodePrecisionMetrics,
135 pub executive_summary_text: String,
136 pub paper_abstract_artifact: String,
137}
138
139pub fn build_secom_addendum_artifacts(
140 dataset: &PreparedDataset,
141 residuals: &ResidualSet,
142 baselines: &BaselineSet,
143 grammar: &GrammarSet,
144 motifs: &MotifSet,
145 semantic_layer: &SemanticLayer,
146 metrics: &BenchmarkMetrics,
147 optimization: &OptimizationExecution,
148 failure_driven: &FailureDrivenArtifacts,
149 baseline_dsa: &DsaEvaluation,
150 optimized_dsa: &DsaEvaluation,
151 pre_failure_lookback_runs: usize,
152) -> SecomAddendumArtifacts {
153 let recurrent_boundary_stats =
154 build_recurrent_boundary_stats(dataset, motifs, pre_failure_lookback_runs);
155 let recurrent_boundary_tradeoff_curve = build_recurrent_boundary_tradeoff_curve(
156 dataset,
157 grammar,
158 motifs,
159 metrics,
160 optimized_dsa,
161 pre_failure_lookback_runs,
162 );
163 let required_tradeoff_statement = "Suppressing recurrent_boundary_approach sufficiently to achieve ≥40% nuisance reduction on SECOM necessarily reduces recall due to shared structural origin.".to_string();
164 let required_tradeoff_statement_supported =
165 recurrent_boundary_tradeoff_statement_supported(&recurrent_boundary_tradeoff_curve);
166 let metric_regrounding = build_metric_regrounding(
167 dataset,
168 residuals,
169 baselines,
170 metrics,
171 optimized_dsa,
172 pre_failure_lookback_runs,
173 );
174 let target_d_regression_analysis = build_target_d_regression_analysis(
175 dataset,
176 grammar,
177 motifs,
178 failure_driven,
179 baseline_dsa,
180 optimized_dsa,
181 &recurrent_boundary_tradeoff_curve,
182 );
183 let missed_failure_root_cause = build_missed_failure_root_cause(failure_driven);
184 let lead_time_comparison = build_lead_time_comparison(
185 semantic_layer,
186 optimized_dsa,
187 metrics,
188 pre_failure_lookback_runs,
189 );
190 let lead_time_explanation = build_lead_time_explanation(&lead_time_comparison);
191 let episode_precision_metrics = build_episode_precision_metrics(metrics, optimized_dsa);
192 let executive_summary_text = format!(
193 "Primary operator result: DSA episode precision is {:.1}% ({}/{} episodes preceding labeled failures) versus a raw-boundary precision proxy of {:.2}%, a {:.1}x gain. Investigation-worthy burden falls by {:.1}% versus numeric-only DSA ({} -> {} points), raw boundary episodes collapse by {:.1}% ({} -> {}), and recall reaches {}/{} while pass-run nuisance reduction versus EWMA remains bounded at {:.1}%.",
194 episode_precision_metrics.dsfb_precision * 100.0,
195 episode_precision_metrics.dsfb_pre_failure_episode_count,
196 episode_precision_metrics.dsfb_episode_count,
197 episode_precision_metrics.raw_alarm_precision * 100.0,
198 episode_precision_metrics.precision_gain_factor,
199 optimization.operator_delta_targets.delta_investigation_load * 100.0,
200 optimization.operator_delta_targets.baseline_investigation_points,
201 optimization.operator_delta_targets.optimized_review_escalate_points,
202 optimization.operator_delta_targets.delta_episode_count * 100.0,
203 optimization.operator_delta_targets.baseline_episode_count,
204 optimization.operator_delta_targets.optimized_episode_count,
205 optimization.operator_delta_targets.selected_configuration.failure_recall,
206 optimization.operator_delta_targets.selected_configuration.failure_runs,
207 optimization.operator_delta_targets.delta_nuisance_vs_ewma * 100.0,
208 );
209 let paper_abstract_artifact = format!(
210 "On SECOM, the policy-governed DSFB layer compresses raw structural activity from {} boundary episodes to {} DSA episodes, reduces investigation-worthy feature points from {} numeric-only DSA points to {}, and raises episode precision to {:.1}% from a raw-boundary precision proxy of {:.2}%. This supports a bounded structural-compression claim, not a blanket early-warning or nuisance-superiority claim.",
211 optimization.operator_delta_targets.baseline_episode_count,
212 optimization.operator_delta_targets.optimized_episode_count,
213 optimization.operator_delta_targets.baseline_investigation_points,
214 optimization.operator_delta_targets.optimized_review_escalate_points,
215 episode_precision_metrics.dsfb_precision * 100.0,
216 episode_precision_metrics.raw_alarm_precision * 100.0,
217 );
218
219 SecomAddendumArtifacts {
220 recurrent_boundary_stats,
221 recurrent_boundary_tradeoff_curve,
222 required_tradeoff_statement,
223 required_tradeoff_statement_supported,
224 metric_regrounding,
225 target_d_regression_analysis,
226 missed_failure_root_cause,
227 lead_time_comparison,
228 lead_time_explanation,
229 episode_precision_metrics,
230 executive_summary_text,
231 paper_abstract_artifact,
232 }
233}
234
235pub fn draw_recurrent_boundary_tradeoff_plot(
236 path: &Path,
237 rows: &[RecurrentBoundaryTradeoffRow],
238) -> Result<()> {
239 let root =
240 BitMapBackend::new(path, (TRADEOFF_PLOT_WIDTH, TRADEOFF_PLOT_HEIGHT)).into_drawing_area();
241 root.fill(&WHITE).map_err(plot_error)?;
242
243 let mut chart = ChartBuilder::on(&root)
244 .caption(
245 "SECOM recurrent_boundary_approach suppression tradeoff",
246 ("sans-serif", 28),
247 )
248 .margin(20)
249 .x_label_area_size(60)
250 .y_label_area_size(70)
251 .build_cartesian_2d(0usize..rows.len(), 0.0f64..1.05f64)
252 .map_err(plot_error)?;
253
254 chart
255 .configure_mesh()
256 .disable_mesh()
257 .x_labels(rows.len())
258 .x_label_formatter(&|index| {
259 rows.get(*index)
260 .map(|row| row.suppression_label.clone())
261 .unwrap_or_default()
262 })
263 .y_desc("Recall rate / nuisance reduction")
264 .draw()
265 .map_err(plot_error)?;
266
267 let recall_series = rows
268 .iter()
269 .enumerate()
270 .map(|(index, row)| (index, row.recall_rate))
271 .collect::<Vec<_>>();
272 let nuisance_series = rows
273 .iter()
274 .enumerate()
275 .map(|(index, row)| (index, row.delta_nuisance_vs_ewma))
276 .collect::<Vec<_>>();
277
278 chart
279 .draw_series(LineSeries::new(recall_series.clone(), &BLACK))
280 .map_err(plot_error)?;
281 chart
282 .draw_series(
283 recall_series
284 .into_iter()
285 .map(|point| Circle::new(point, 5, BLACK.filled())),
286 )
287 .map_err(plot_error)?;
288
289 chart
290 .draw_series(LineSeries::new(
291 nuisance_series.clone(),
292 &RGBColor(100, 100, 100),
293 ))
294 .map_err(plot_error)?;
295 chart
296 .draw_series(
297 nuisance_series
298 .into_iter()
299 .map(|point| TriangleMarker::new(point, 6, RGBColor(100, 100, 100).filled())),
300 )
301 .map_err(plot_error)?;
302
303 chart
304 .draw_series(std::iter::once(PathElement::new(
305 vec![(0usize, 0.40f64), (rows.len().saturating_sub(1), 0.40f64)],
306 ShapeStyle::from(&RGBColor(170, 170, 170)).stroke_width(1),
307 )))
308 .map_err(plot_error)?;
309
310 root.present().map_err(plot_error)?;
311 Ok(())
312}
313
314fn build_recurrent_boundary_stats(
315 dataset: &PreparedDataset,
316 motifs: &MotifSet,
317 pre_failure_lookback_runs: usize,
318) -> RecurrentBoundaryStats {
319 let failure_mask = failure_window_mask(
320 dataset.labels.len(),
321 &dataset.labels,
322 pre_failure_lookback_runs,
323 );
324 let total_boundary_points = motifs
325 .traces
326 .iter()
327 .flat_map(|trace| trace.labels.iter())
328 .filter(|label| **label == DsfbMotifClass::RecurrentBoundaryApproach)
329 .count();
330 let mut run_hits = BTreeSet::new();
331 let mut pre_failure_hits = BTreeSet::new();
332 let mut pass_run_hits = BTreeSet::new();
333
334 for trace in &motifs.traces {
335 for (run_index, label) in trace.labels.iter().enumerate() {
336 if *label != DsfbMotifClass::RecurrentBoundaryApproach {
337 continue;
338 }
339 run_hits.insert(run_index);
340 if failure_mask[run_index] {
341 pre_failure_hits.insert(run_index);
342 }
343 if dataset.labels[run_index] == -1 {
344 pass_run_hits.insert(run_index);
345 }
346 }
347 }
348
349 let total_run_hits = run_hits.len();
350 let total_pre_failure_hits = pre_failure_hits.len();
351 let pass_run_hits_count = pass_run_hits.len();
352
353 RecurrentBoundaryStats {
354 total_boundary_points,
355 total_run_hits,
356 total_pre_failure_hits,
357 pass_run_hits: pass_run_hits_count,
358 precision_pre_failure: ratio(total_pre_failure_hits, total_run_hits),
359 precision_pass: ratio(pass_run_hits_count, total_run_hits),
360 }
361}
362
363fn build_recurrent_boundary_tradeoff_curve(
364 dataset: &PreparedDataset,
365 grammar: &GrammarSet,
366 motifs: &MotifSet,
367 metrics: &BenchmarkMetrics,
368 optimized_dsa: &DsaEvaluation,
369 pre_failure_lookback_runs: usize,
370) -> Vec<RecurrentBoundaryTradeoffRow> {
371 let motif_by_feature = motifs
372 .traces
373 .iter()
374 .map(|trace| (trace.feature_index, trace))
375 .collect::<BTreeMap<_, _>>();
376 let grammar_by_feature = grammar
377 .traces
378 .iter()
379 .map(|trace| (trace.feature_index, trace))
380 .collect::<BTreeMap<_, _>>();
381 let pass_indices = dataset
382 .labels
383 .iter()
384 .enumerate()
385 .filter_map(|(index, label)| (*label == -1).then_some(index))
386 .collect::<Vec<_>>();
387 let failure_indices = dataset
388 .labels
389 .iter()
390 .enumerate()
391 .filter_map(|(index, label)| (*label == 1).then_some(index))
392 .collect::<Vec<_>>();
393
394 let suppression_levels = [
395 (0.0, "baseline"),
396 (0.25, "mild_gate"),
397 (0.50, "watch_cap"),
398 (0.75, "strong_cap"),
399 (1.00, "silent_cap"),
400 ];
401
402 suppression_levels
403 .into_iter()
404 .map(|(level, label)| {
405 let mut feature_alerts =
406 vec![vec![false; dataset.labels.len()]; optimized_dsa.traces.len()];
407 let mut investigation_points = 0usize;
408
409 for (trace_slot, feature_trace) in optimized_dsa.traces.iter().enumerate() {
410 let motif_trace = motif_by_feature
411 .get(&feature_trace.feature_index)
412 .unwrap_or_else(|| {
413 panic!("missing motif trace {}", feature_trace.feature_index)
414 });
415 let grammar_trace = grammar_by_feature
416 .get(&feature_trace.feature_index)
417 .unwrap_or_else(|| {
418 panic!("missing grammar trace {}", feature_trace.feature_index)
419 });
420 for run_index in 0..feature_trace.policy_state.len() {
421 let remapped = remap_recurrent_boundary_state(
422 level,
423 feature_trace.policy_state[run_index],
424 motif_trace.labels[run_index],
425 grammar_trace.raw_states[run_index],
426 grammar_trace.persistent_violation[run_index],
427 feature_trace.boundary_density_w[run_index],
428 feature_trace.motif_recurrence_w[run_index],
429 );
430 if is_review_or_escalate(remapped) {
431 feature_alerts[trace_slot][run_index] = true;
432 investigation_points += 1;
433 }
434 }
435 }
436
437 let corroborating_m = optimized_dsa.run_signals.corroborating_feature_count_min;
438 let run_alert = (0..dataset.labels.len())
439 .map(|run_index| {
440 feature_alerts
441 .iter()
442 .filter(|trace| trace[run_index])
443 .count()
444 >= corroborating_m
445 })
446 .collect::<Vec<_>>();
447
448 let failure_recall = failure_indices
449 .iter()
450 .filter(|&&failure_index| {
451 let start = failure_index.saturating_sub(pre_failure_lookback_runs);
452 run_alert[start..failure_index].iter().any(|flag| *flag)
453 })
454 .count();
455 let pass_alert_runs = pass_indices
456 .iter()
457 .filter(|&&run_index| run_alert[run_index])
458 .count();
459
460 RecurrentBoundaryTradeoffRow {
461 suppression_level: level,
462 suppression_label: label.into(),
463 investigation_points,
464 pass_run_nuisance_proxy: ratio(pass_alert_runs, pass_indices.len()),
465 delta_nuisance_vs_selected_dsa: relative_reduction(
466 optimized_dsa.summary.pass_run_nuisance_proxy,
467 ratio(pass_alert_runs, pass_indices.len()),
468 ),
469 delta_nuisance_vs_ewma: relative_reduction(
470 metrics.summary.pass_run_ewma_nuisance_rate,
471 ratio(pass_alert_runs, pass_indices.len()),
472 ),
473 failure_recall,
474 failure_runs: failure_indices.len(),
475 recall_rate: ratio(failure_recall, failure_indices.len()),
476 }
477 })
478 .collect()
479}
480
481fn recurrent_boundary_tradeoff_statement_supported(rows: &[RecurrentBoundaryTradeoffRow]) -> bool {
482 rows.iter().any(|row| row.delta_nuisance_vs_ewma >= 0.40)
483 && rows
484 .iter()
485 .filter(|row| row.delta_nuisance_vs_ewma >= 0.40)
486 .all(|row| row.failure_recall < row.failure_runs)
487}
488
489fn remap_recurrent_boundary_state(
490 suppression_level: f64,
491 state: DsaPolicyState,
492 motif: DsfbMotifClass,
493 grammar_state: GrammarState,
494 persistent_violation: bool,
495 boundary_density: f64,
496 motif_recurrence: f64,
497) -> DsaPolicyState {
498 if motif != DsfbMotifClass::RecurrentBoundaryApproach {
499 return state;
500 }
501
502 if suppression_level >= 1.0 {
503 return DsaPolicyState::Silent;
504 }
505 if suppression_level >= 0.75 {
506 if persistent_violation || grammar_state == GrammarState::Violation {
507 return DsaPolicyState::Watch;
508 }
509 return DsaPolicyState::Silent;
510 }
511 if suppression_level >= 0.50 {
512 if is_review_or_escalate(state) {
513 return DsaPolicyState::Watch;
514 }
515 return state;
516 }
517 if suppression_level >= 0.25
518 && is_review_or_escalate(state)
519 && (boundary_density < 0.75 || motif_recurrence < 0.75)
520 {
521 return DsaPolicyState::Watch;
522 }
523
524 state
525}
526
527fn build_metric_regrounding(
528 dataset: &PreparedDataset,
529 residuals: &ResidualSet,
530 baselines: &BaselineSet,
531 metrics: &BenchmarkMetrics,
532 optimized_dsa: &DsaEvaluation,
533 pre_failure_lookback_runs: usize,
534) -> Vec<MetricRegroundingRow> {
535 let threshold_signal = (0..dataset.labels.len())
536 .map(|run_index| {
537 residuals
538 .traces
539 .iter()
540 .any(|trace| trace.threshold_alarm[run_index])
541 })
542 .collect::<Vec<_>>();
543 let ewma_signal = (0..dataset.labels.len())
544 .map(|run_index| baselines.ewma.iter().any(|trace| trace.alarm[run_index]))
545 .collect::<Vec<_>>();
546
547 let threshold_episode_count = count_episodes(&threshold_signal);
548 let threshold_precision = episode_precision(
549 &threshold_signal,
550 &failure_window_mask(
551 dataset.labels.len(),
552 &dataset.labels,
553 pre_failure_lookback_runs,
554 ),
555 );
556
557 let ewma_episode_count = count_episodes(&ewma_signal);
558 let ewma_precision = episode_precision(
559 &ewma_signal,
560 &failure_window_mask(
561 dataset.labels.len(),
562 &dataset.labels,
563 pre_failure_lookback_runs,
564 ),
565 );
566
567 let failure_mask = failure_window_mask(
568 dataset.labels.len(),
569 &dataset.labels,
570 pre_failure_lookback_runs,
571 );
572 let numeric_dsa_episode_count =
573 count_episodes(&optimized_dsa.run_signals.numeric_primary_run_alert);
574 let dsfb_precision = optimized_dsa
575 .episode_summary
576 .precursor_quality
577 .unwrap_or_default();
578 let numeric_precision = episode_precision(
579 &optimized_dsa.run_signals.numeric_primary_run_alert,
580 &failure_mask,
581 );
582
583 vec![
584 MetricRegroundingRow {
585 metric: "Investigation load".into(),
586 baseline: "Numeric-only DSA".into(),
587 dsfb_value: investigation_points(optimized_dsa) as f64,
588 baseline_value: optimized_dsa.summary.numeric_alert_point_count as f64,
589 delta_percent: relative_reduction(
590 optimized_dsa.summary.numeric_alert_point_count as f64,
591 investigation_points(optimized_dsa) as f64,
592 ),
593 },
594 MetricRegroundingRow {
595 metric: "Investigation load".into(),
596 baseline: "Threshold".into(),
597 dsfb_value: investigation_points(optimized_dsa) as f64,
598 baseline_value: metrics.summary.threshold_alarm_points as f64,
599 delta_percent: relative_reduction(
600 metrics.summary.threshold_alarm_points as f64,
601 investigation_points(optimized_dsa) as f64,
602 ),
603 },
604 MetricRegroundingRow {
605 metric: "Investigation load".into(),
606 baseline: "EWMA".into(),
607 dsfb_value: investigation_points(optimized_dsa) as f64,
608 baseline_value: metrics.summary.ewma_alarm_points as f64,
609 delta_percent: relative_reduction(
610 metrics.summary.ewma_alarm_points as f64,
611 investigation_points(optimized_dsa) as f64,
612 ),
613 },
614 MetricRegroundingRow {
615 metric: "Episode count".into(),
616 baseline: "Numeric-only DSA".into(),
617 dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
618 baseline_value: numeric_dsa_episode_count as f64,
619 delta_percent: relative_reduction(
620 numeric_dsa_episode_count as f64,
621 optimized_dsa.episode_summary.dsa_episode_count as f64,
622 ),
623 },
624 MetricRegroundingRow {
625 metric: "Episode count".into(),
626 baseline: "Threshold".into(),
627 dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
628 baseline_value: threshold_episode_count as f64,
629 delta_percent: relative_reduction(
630 threshold_episode_count as f64,
631 optimized_dsa.episode_summary.dsa_episode_count as f64,
632 ),
633 },
634 MetricRegroundingRow {
635 metric: "Episode count".into(),
636 baseline: "EWMA".into(),
637 dsfb_value: optimized_dsa.episode_summary.dsa_episode_count as f64,
638 baseline_value: ewma_episode_count as f64,
639 delta_percent: relative_reduction(
640 ewma_episode_count as f64,
641 optimized_dsa.episode_summary.dsa_episode_count as f64,
642 ),
643 },
644 MetricRegroundingRow {
645 metric: "Nuisance reduction".into(),
646 baseline: "Numeric-only DSA".into(),
647 dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
648 baseline_value: optimized_dsa
649 .summary
650 .numeric_primary_pass_run_nuisance_proxy,
651 delta_percent: relative_reduction(
652 optimized_dsa
653 .summary
654 .numeric_primary_pass_run_nuisance_proxy,
655 optimized_dsa.summary.pass_run_nuisance_proxy,
656 ),
657 },
658 MetricRegroundingRow {
659 metric: "Nuisance reduction".into(),
660 baseline: "Threshold".into(),
661 dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
662 baseline_value: metrics.summary.pass_run_threshold_nuisance_rate,
663 delta_percent: relative_reduction(
664 metrics.summary.pass_run_threshold_nuisance_rate,
665 optimized_dsa.summary.pass_run_nuisance_proxy,
666 ),
667 },
668 MetricRegroundingRow {
669 metric: "Nuisance reduction".into(),
670 baseline: "EWMA".into(),
671 dsfb_value: optimized_dsa.summary.pass_run_nuisance_proxy,
672 baseline_value: metrics.summary.pass_run_ewma_nuisance_rate,
673 delta_percent: relative_reduction(
674 metrics.summary.pass_run_ewma_nuisance_rate,
675 optimized_dsa.summary.pass_run_nuisance_proxy,
676 ),
677 },
678 MetricRegroundingRow {
679 metric: "Recall".into(),
680 baseline: "Numeric-only DSA".into(),
681 dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
682 baseline_value: optimized_dsa.summary.numeric_primary_failure_run_recall as f64,
683 delta_percent: relative_gain(
684 optimized_dsa.summary.numeric_primary_failure_run_recall as f64,
685 optimized_dsa.summary.failure_run_recall as f64,
686 ),
687 },
688 MetricRegroundingRow {
689 metric: "Recall".into(),
690 baseline: "Threshold".into(),
691 dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
692 baseline_value: metrics.summary.failure_runs_with_preceding_threshold_signal as f64,
693 delta_percent: relative_gain(
694 metrics.summary.failure_runs_with_preceding_threshold_signal as f64,
695 optimized_dsa.summary.failure_run_recall as f64,
696 ),
697 },
698 MetricRegroundingRow {
699 metric: "Recall".into(),
700 baseline: "EWMA".into(),
701 dsfb_value: optimized_dsa.summary.failure_run_recall as f64,
702 baseline_value: metrics.summary.failure_runs_with_preceding_ewma_signal as f64,
703 delta_percent: relative_gain(
704 metrics.summary.failure_runs_with_preceding_ewma_signal as f64,
705 optimized_dsa.summary.failure_run_recall as f64,
706 ),
707 },
708 MetricRegroundingRow {
709 metric: "Lead time".into(),
710 baseline: "Numeric-only DSA".into(),
711 dsfb_value: optimized_dsa
712 .summary
713 .mean_lead_time_runs
714 .unwrap_or_default(),
715 baseline_value: optimized_dsa
716 .comparison_summary
717 .numeric_dsa
718 .mean_lead_time_runs
719 .unwrap_or_default(),
720 delta_percent: relative_gain(
721 optimized_dsa
722 .comparison_summary
723 .numeric_dsa
724 .mean_lead_time_runs
725 .unwrap_or_default(),
726 optimized_dsa
727 .summary
728 .mean_lead_time_runs
729 .unwrap_or_default(),
730 ),
731 },
732 MetricRegroundingRow {
733 metric: "Lead time".into(),
734 baseline: "Threshold".into(),
735 dsfb_value: optimized_dsa
736 .summary
737 .mean_lead_time_runs
738 .unwrap_or_default(),
739 baseline_value: metrics
740 .lead_time_summary
741 .mean_threshold_lead_runs
742 .unwrap_or_default(),
743 delta_percent: relative_gain(
744 metrics
745 .lead_time_summary
746 .mean_threshold_lead_runs
747 .unwrap_or_default(),
748 optimized_dsa
749 .summary
750 .mean_lead_time_runs
751 .unwrap_or_default(),
752 ),
753 },
754 MetricRegroundingRow {
755 metric: "Lead time".into(),
756 baseline: "EWMA".into(),
757 dsfb_value: optimized_dsa
758 .summary
759 .mean_lead_time_runs
760 .unwrap_or_default(),
761 baseline_value: metrics
762 .lead_time_summary
763 .mean_ewma_lead_runs
764 .unwrap_or_default(),
765 delta_percent: relative_gain(
766 metrics
767 .lead_time_summary
768 .mean_ewma_lead_runs
769 .unwrap_or_default(),
770 optimized_dsa
771 .summary
772 .mean_lead_time_runs
773 .unwrap_or_default(),
774 ),
775 },
776 MetricRegroundingRow {
777 metric: "Episode precision".into(),
778 baseline: "Numeric-only DSA".into(),
779 dsfb_value: dsfb_precision,
780 baseline_value: numeric_precision,
781 delta_percent: relative_gain(numeric_precision, dsfb_precision),
782 },
783 MetricRegroundingRow {
784 metric: "Episode precision".into(),
785 baseline: "Threshold".into(),
786 dsfb_value: dsfb_precision,
787 baseline_value: threshold_precision,
788 delta_percent: relative_gain(threshold_precision, dsfb_precision),
789 },
790 MetricRegroundingRow {
791 metric: "Episode precision".into(),
792 baseline: "EWMA".into(),
793 dsfb_value: dsfb_precision,
794 baseline_value: ewma_precision,
795 delta_percent: relative_gain(ewma_precision, dsfb_precision),
796 },
797 ]
798}
799
800fn build_target_d_regression_analysis(
801 dataset: &PreparedDataset,
802 grammar: &GrammarSet,
803 motifs: &MotifSet,
804 failure_driven: &FailureDrivenArtifacts,
805 baseline_dsa: &DsaEvaluation,
806 optimized_dsa: &DsaEvaluation,
807 tradeoff_curve: &[RecurrentBoundaryTradeoffRow],
808) -> TargetDRegressionAnalysis {
809 let pass_indices = dataset
810 .labels
811 .iter()
812 .enumerate()
813 .filter_map(|(index, label)| (*label == -1).then_some(index))
814 .collect::<Vec<_>>();
815 let motif_by_feature = motifs
816 .traces
817 .iter()
818 .map(|trace| (trace.feature_index, trace))
819 .collect::<BTreeMap<_, _>>();
820 let grammar_by_feature = grammar
821 .traces
822 .iter()
823 .map(|trace| (trace.feature_index, trace))
824 .collect::<BTreeMap<_, _>>();
825
826 let mut added_pass_points_by_feature = BTreeMap::<String, usize>::new();
827 let mut motif_counts = BTreeMap::<String, usize>::new();
828 let mut grammar_counts = BTreeMap::<String, usize>::new();
829
830 for (baseline_trace, optimized_trace) in baseline_dsa.traces.iter().zip(&optimized_dsa.traces) {
831 let motif_trace = match motif_by_feature.get(&optimized_trace.feature_index) {
832 Some(trace) => *trace,
833 None => continue,
834 };
835 let grammar_trace = match grammar_by_feature.get(&optimized_trace.feature_index) {
836 Some(trace) => *trace,
837 None => continue,
838 };
839 let mut added = 0usize;
840 for &run_index in &pass_indices {
841 let baseline_flag = is_review_or_escalate(baseline_trace.policy_state[run_index]);
842 let optimized_flag = is_review_or_escalate(optimized_trace.policy_state[run_index]);
843 if optimized_flag && !baseline_flag {
844 added += 1;
845 *motif_counts
846 .entry(motif_trace.labels[run_index].as_lowercase().to_string())
847 .or_default() += 1;
848 *grammar_counts
849 .entry(grammar_label(grammar_trace.raw_states[run_index]).to_string())
850 .or_default() += 1;
851 }
852 }
853 if added > 0 {
854 added_pass_points_by_feature.insert(optimized_trace.feature_name.clone(), added);
855 }
856 }
857
858 let mut contributing_features_counts =
859 added_pass_points_by_feature.into_iter().collect::<Vec<_>>();
860 contributing_features_counts
861 .sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
862 let contributing_features = contributing_features_counts
863 .into_iter()
864 .take(5)
865 .map(|(feature, _)| feature)
866 .collect::<Vec<_>>();
867
868 let mut contributing_motifs_counts = motif_counts.into_iter().collect::<Vec<_>>();
869 contributing_motifs_counts
870 .sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
871 let contributing_motifs = contributing_motifs_counts
872 .into_iter()
873 .take(3)
874 .map(|(motif, _)| motif)
875 .collect::<Vec<_>>();
876
877 let contributing_features_text = if contributing_features.is_empty() {
878 "no single dominant feature".into()
879 } else {
880 contributing_features.join(", ")
881 };
882 let contributing_motifs_text = if contributing_motifs.is_empty() {
883 "none".into()
884 } else {
885 contributing_motifs.join(", ")
886 };
887 let grammar_counts_text = if grammar_counts.is_empty() {
888 "none".into()
889 } else {
890 grammar_counts
891 .clone()
892 .into_iter()
893 .take(3)
894 .map(|(name, _)| name)
895 .collect::<Vec<_>>()
896 .join(", ")
897 };
898
899 let mut contributing_heuristics = failure_driven
900 .heuristic_provenance
901 .iter()
902 .filter(|row| {
903 row.intended_effect == "recover_failure"
904 && contributing_features
905 .iter()
906 .any(|feature| row.uses_features.contains(feature))
907 })
908 .map(|row| row.heuristic_id.clone())
909 .collect::<Vec<_>>();
910 contributing_heuristics.sort();
911 contributing_heuristics.dedup();
912
913 let mut contributing_policy_rules = Vec::new();
914 if contributing_features
915 .iter()
916 .any(|feature| matches!(feature.as_str(), "S092" | "S134" | "S275"))
917 {
918 contributing_policy_rules.push(
919 "bounded recall-rescue override: allow_review_without_escalate=true with persistence and corroboration relaxed on feature-local recovery paths"
920 .into(),
921 );
922 }
923 if optimized_dsa
924 .parameter_manifest
925 .feature_policy_override_summary
926 .iter()
927 .any(|summary| summary.contains("suppress_if_isolated=true"))
928 {
929 contributing_policy_rules.push(
930 "isolated-pass suppression rule: allow_watch_only=true plus suppress_if_isolated=true on nuisance-only pass features"
931 .into(),
932 );
933 }
934 if contributing_motifs
935 .iter()
936 .any(|motif| motif == "recurrent_boundary_approach")
937 {
938 contributing_policy_rules.push(
939 "recurrent_boundary_approach remains persistence-gated and globally capped below strong suppression because stronger nuisance cuts give back recall"
940 .into(),
941 );
942 }
943
944 let strong_suppression_rows = tradeoff_curve
945 .iter()
946 .filter(|row| row.delta_nuisance_vs_ewma >= 0.40)
947 .collect::<Vec<_>>();
948 let tradeoff_justification = if strong_suppression_rows.is_empty() {
949 "No recurrent_boundary_approach suppression row reached a 40% nuisance reduction versus EWMA, so the current regression is carried as a bounded operator tradeoff rather than falsely claimed as solved.".into()
950 } else {
951 let recall_rows = strong_suppression_rows
952 .iter()
953 .map(|row| {
954 format!(
955 "{} => recall {}/{}",
956 row.suppression_label, row.failure_recall, row.failure_runs
957 )
958 })
959 .collect::<Vec<_>>()
960 .join("; ");
961 format!(
962 "Further suppression was rejected in this pass. Every recurrent_boundary_approach suppression row that reaches at least a 40% nuisance reduction versus EWMA drops recall below full coverage: {}.",
963 recall_rows
964 )
965 };
966
967 TargetDRegressionAnalysis {
968 contributing_features,
969 contributing_motifs,
970 contributing_heuristics,
971 contributing_policy_rules,
972 causal_chain: vec![
973 "Failure-local rescue overrides for the former misses promote grammar-qualified Watch states to Review on specific low-burden features.".into(),
974 "Those promotions create a small number of additional pass-only run segments, which increases review episodes per pass run even while total investigation points still fall materially.".into(),
975 "The accepted isolated-pass suppressions reduce point burden, but they do not fully remove the extra run segmentation introduced by recall recovery.".into(),
976 "The recurrent_boundary_approach suppression sweep shows that stronger motif-wide suppression would cut nuisance further only by giving back recall.".into(),
977 ],
978 why_regression_occurred: format!(
979 "The regression is driven by recall-recovery promotions on {} under motif classes {} and grammar states {}. The specific policy change is bounded Watch->Review rescue without escalation on these features, while the compensating isolated-pass suppression rules only partially offset the extra pass-only segmentation.",
980 contributing_features_text,
981 contributing_motifs_text,
982 grammar_counts_text,
983 ),
984 action_taken: "formal_tradeoff_justification".into(),
985 tradeoff_justification,
986 }
987}
988
989fn build_missed_failure_root_cause(
990 failure_driven: &FailureDrivenArtifacts,
991) -> MissedFailureRootCause {
992 let case = failure_driven
993 .failure_cases
994 .iter()
995 .find(|case| case.failure_id == 2)
996 .or_else(|| {
997 failure_driven
998 .failure_cases
999 .iter()
1000 .find(|case| !case.baseline_detected_by_dsa && case.optimized_detected_by_dsa)
1001 })
1002 .or_else(|| failure_driven.failure_cases.first())
1003 .unwrap_or_else(|| panic!("failure-driven artifacts missing baseline failure cases"));
1004 let top_feature = case
1005 .top_contributing_features
1006 .first()
1007 .unwrap_or_else(|| panic!("failure case {} missing top feature", case.failure_id));
1008 let reason_for_miss = missed_failure_reason(case.exact_miss_rule.as_str()).to_string();
1009 let classification = if case.optimized_detected_by_dsa {
1010 "recoverable (and fix)"
1011 } else if top_feature.max_dsa_score <= 0.0 {
1012 "data limitation"
1013 } else {
1014 "structurally unrecoverable"
1015 };
1016
1017 MissedFailureRootCause {
1018 failure_id: case.failure_id,
1019 feature_activity: case
1020 .top_contributing_features
1021 .iter()
1022 .map(|feature| MissedFailureFeatureActivity {
1023 feature_name: feature.feature_name.clone(),
1024 behavior_classification: feature.behavior_classification.clone(),
1025 max_dsa_score: feature.max_dsa_score,
1026 initial_motif_hypothesis: feature.initial_motif_hypothesis.clone(),
1027 dominant_dsfb_motif: feature.dominant_dsfb_motif.clone(),
1028 dominant_grammar_state: feature.dominant_grammar_state.clone(),
1029 failure_explanation: feature.failure_explanation.clone(),
1030 })
1031 .collect(),
1032 residual_trajectory: top_feature.residual_trajectory.clone(),
1033 drift_trajectory: top_feature.drift_trajectory.clone(),
1034 slew_trajectory: top_feature.slew_trajectory.clone(),
1035 motif_presence: top_feature.motif_timeline.clone(),
1036 grammar_state: top_feature.grammar_state_timeline.clone(),
1037 reason_for_miss,
1038 classification: classification.into(),
1039 recovered_after_fix: case.optimized_detected_by_dsa,
1040 recovery_feature: case
1041 .top_contributing_features
1042 .iter()
1043 .find(|feature| feature.feature_name == "S092")
1044 .map(|feature| feature.feature_name.clone())
1045 .or_else(|| {
1046 case.top_contributing_features
1047 .first()
1048 .map(|feature| feature.feature_name.clone())
1049 }),
1050 recovery_note: format!(
1051 "The former 103/104 limiting case was failure run {}. It remained a Watch-class near-miss under {} until the bounded {} structural-support rescue was accepted, after which the selected row reached 104/104.",
1052 case.failure_id,
1053 case.exact_miss_rule,
1054 case
1055 .top_contributing_features
1056 .iter()
1057 .find(|feature| feature.feature_name == "S092")
1058 .map(|feature| feature.feature_name.as_str())
1059 .unwrap_or(top_feature.feature_name.as_str()),
1060 ),
1061 }
1062}
1063
1064fn build_lead_time_comparison(
1065 semantic_layer: &SemanticLayer,
1066 optimized_dsa: &DsaEvaluation,
1067 metrics: &BenchmarkMetrics,
1068 pre_failure_lookback_runs: usize,
1069) -> Vec<LeadTimeComparisonRow> {
1070 let dsa_by_failure = optimized_dsa
1071 .per_failure_run_signals
1072 .iter()
1073 .map(|row| (row.failure_run_index, row))
1074 .collect::<BTreeMap<_, _>>();
1075
1076 metrics
1077 .per_failure_run_signals
1078 .iter()
1079 .map(|metric_row| {
1080 let dsa_row = dsa_by_failure
1081 .get(&metric_row.failure_run_index)
1082 .copied()
1083 .unwrap_or_else(|| {
1084 panic!("missing DSA failure row {}", metric_row.failure_run_index)
1085 });
1086 let earliest_semantic_match_run = earliest_semantic_match_run(
1087 semantic_layer,
1088 metric_row.failure_run_index,
1089 pre_failure_lookback_runs,
1090 );
1091 let earliest_semantic_match_lead_runs = earliest_semantic_match_run
1092 .map(|run_index| metric_row.failure_run_index - run_index);
1093 LeadTimeComparisonRow {
1094 failure_id: metric_row.failure_run_index,
1095 dsfb_lead_runs: dsa_row.dsa_lead_runs,
1096 threshold_lead_runs: metric_row.threshold_lead_runs,
1097 earliest_semantic_match_lead_runs,
1098 threshold_minus_dsfb_runs: diff_optional(
1099 metric_row.threshold_lead_runs,
1100 dsa_row.dsa_lead_runs,
1101 ),
1102 threshold_minus_semantic_match_runs: diff_optional(
1103 metric_row.threshold_lead_runs,
1104 earliest_semantic_match_lead_runs,
1105 ),
1106 }
1107 })
1108 .collect()
1109}
1110
1111fn build_lead_time_explanation(rows: &[LeadTimeComparisonRow]) -> LeadTimeExplanation {
1112 let mean_dsfb = mean_option(
1113 rows.iter()
1114 .filter_map(|row| row.dsfb_lead_runs.map(|value| value as f64)),
1115 );
1116 let mean_threshold = mean_option(
1117 rows.iter()
1118 .filter_map(|row| row.threshold_lead_runs.map(|value| value as f64)),
1119 );
1120 let mean_semantic = mean_option(rows.iter().filter_map(|row| {
1121 row.earliest_semantic_match_lead_runs
1122 .map(|value| value as f64)
1123 }));
1124 let threshold_earlier_failure_count = rows
1125 .iter()
1126 .filter(|row| {
1127 matches!(
1128 (row.threshold_lead_runs, row.dsfb_lead_runs),
1129 (Some(threshold), Some(dsfb)) if threshold > dsfb
1130 )
1131 })
1132 .count();
1133 let dsfb_earlier_failure_count = rows
1134 .iter()
1135 .filter(|row| {
1136 matches!(
1137 (row.threshold_lead_runs, row.dsfb_lead_runs),
1138 (Some(threshold), Some(dsfb)) if dsfb > threshold
1139 )
1140 })
1141 .count();
1142 let semantic_match_precedes_threshold_count = rows
1143 .iter()
1144 .filter(|row| {
1145 matches!(
1146 (row.earliest_semantic_match_lead_runs, row.threshold_lead_runs),
1147 (Some(semantic), Some(threshold)) if semantic > threshold
1148 )
1149 })
1150 .count();
1151
1152 LeadTimeExplanation {
1153 mean_dsfb_lead_runs: mean_dsfb,
1154 mean_threshold_lead_runs: mean_threshold,
1155 mean_semantic_match_lead_runs: mean_semantic,
1156 threshold_earlier_failure_count,
1157 dsfb_earlier_failure_count,
1158 semantic_match_precedes_threshold_count,
1159 motif_emergence_precedes_threshold_count: semantic_match_precedes_threshold_count,
1160 explanation: "Threshold fires on any sufficiently large residual deviation, while DSFB waits for grammar-qualified motif structure and then applies persistence-constrained policy promotion. That layered requirement makes DSFB more selective but also later on SECOM.".into(),
1161 validation_note: format!(
1162 "Mean threshold lead is {} runs versus mean DSA lead {} runs; earliest grammar-qualified motif emergence averages {} runs. This gap is consistent with DSFB waiting for structured motifs rather than raw deviation alone.",
1163 format_option(mean_threshold),
1164 format_option(mean_dsfb),
1165 format_option(mean_semantic),
1166 ),
1167 }
1168}
1169
1170fn missed_failure_reason(exact_miss_rule: &str) -> &'static str {
1171 match exact_miss_rule {
1172 "watch_class_near_miss_below_numeric_gate" | "policy_state_never_reached_review" => {
1173 "policy suppression"
1174 }
1175 "directional_consistency_gate" | "feature_override_fragmentation_ceiling" => {
1176 "grammar rejection"
1177 }
1178 "numeric_score_below_tau" => "no precursor",
1179 _ => "feature absence",
1180 }
1181}
1182
1183fn build_episode_precision_metrics(
1184 metrics: &BenchmarkMetrics,
1185 optimized_dsa: &DsaEvaluation,
1186) -> EpisodePrecisionMetrics {
1187 let dsfb_episode_count = optimized_dsa.episode_summary.dsa_episode_count;
1188 let dsfb_pre_failure_episode_count =
1189 optimized_dsa.episode_summary.dsa_episodes_preceding_failure;
1190 let dsfb_precision = optimized_dsa
1191 .episode_summary
1192 .precursor_quality
1193 .unwrap_or_default();
1194 let raw_alarm_count = optimized_dsa.episode_summary.raw_boundary_episode_count;
1195 let raw_alarm_precision = ratio(metrics.summary.failure_runs, raw_alarm_count);
1196 let precision_gain_factor = if raw_alarm_precision > 0.0 {
1197 dsfb_precision / raw_alarm_precision
1198 } else {
1199 0.0
1200 };
1201
1202 EpisodePrecisionMetrics {
1203 dsfb_episode_count,
1204 dsfb_pre_failure_episode_count,
1205 dsfb_precision,
1206 raw_alarm_count,
1207 raw_alarm_precision,
1208 precision_gain_factor,
1209 }
1210}
1211
1212fn earliest_semantic_match_run(
1213 semantic_layer: &SemanticLayer,
1214 failure_index: usize,
1215 pre_failure_lookback_runs: usize,
1216) -> Option<usize> {
1217 let start = failure_index.saturating_sub(pre_failure_lookback_runs);
1218 semantic_layer
1219 .semantic_matches
1220 .iter()
1221 .filter(|row| row.run_index >= start && row.run_index < failure_index)
1222 .map(|row| row.run_index)
1223 .min()
1224}
1225
1226fn investigation_points(dsa: &DsaEvaluation) -> usize {
1227 dsa.summary.review_point_count + dsa.summary.escalate_point_count
1228}
1229
1230fn count_episodes(signal: &[bool]) -> usize {
1231 let mut count = 0usize;
1232 let mut in_episode = false;
1233 for flag in signal {
1234 if *flag && !in_episode {
1235 count += 1;
1236 in_episode = true;
1237 } else if !*flag {
1238 in_episode = false;
1239 }
1240 }
1241 count
1242}
1243
1244fn episode_precision(signal: &[bool], failure_window_mask: &[bool]) -> f64 {
1245 let episodes = episode_ranges(signal);
1246 if episodes.is_empty() {
1247 return 0.0;
1248 }
1249 let preceding = episodes
1250 .iter()
1251 .filter(|(start, end)| (*start..=*end).any(|index| failure_window_mask[index]))
1252 .count();
1253 preceding as f64 / episodes.len() as f64
1254}
1255
1256fn episode_ranges(signal: &[bool]) -> Vec<(usize, usize)> {
1257 let mut episodes = Vec::new();
1258 let mut start = None;
1259 for (index, flag) in signal.iter().copied().enumerate() {
1260 match (start, flag) {
1261 (None, true) => start = Some(index),
1262 (Some(episode_start), false) => {
1263 episodes.push((episode_start, index - 1));
1264 start = None;
1265 }
1266 _ => {}
1267 }
1268 }
1269 if let Some(episode_start) = start {
1270 episodes.push((episode_start, signal.len().saturating_sub(1)));
1271 }
1272 episodes
1273}
1274
1275fn failure_window_mask(run_count: usize, labels: &[i8], lookback: usize) -> Vec<bool> {
1276 let mut mask = vec![false; run_count];
1277 for (failure_index, label) in labels.iter().enumerate() {
1278 if *label != 1 {
1279 continue;
1280 }
1281 let start = failure_index.saturating_sub(lookback);
1282 for slot in &mut mask[start..failure_index] {
1283 *slot = true;
1284 }
1285 }
1286 mask
1287}
1288
1289fn grammar_label(state: GrammarState) -> &'static str {
1290 match state {
1291 GrammarState::Admissible => "Admissible",
1292 GrammarState::Boundary => "BoundaryGrazing",
1293 GrammarState::Violation => "PersistentViolation",
1294 }
1295}
1296
1297fn is_review_or_escalate(state: DsaPolicyState) -> bool {
1298 matches!(state, DsaPolicyState::Review | DsaPolicyState::Escalate)
1299}
1300
1301fn relative_reduction(baseline: f64, value: f64) -> f64 {
1302 if baseline.abs() <= f64::EPSILON {
1303 0.0
1304 } else {
1305 (baseline - value) / baseline
1306 }
1307}
1308
1309fn relative_gain(baseline: f64, value: f64) -> f64 {
1310 if baseline.abs() <= f64::EPSILON {
1311 0.0
1312 } else {
1313 (value - baseline) / baseline
1314 }
1315}
1316
1317fn ratio(numerator: usize, denominator: usize) -> f64 {
1318 if denominator == 0 {
1319 0.0
1320 } else {
1321 numerator as f64 / denominator as f64
1322 }
1323}
1324
1325fn diff_optional(left: Option<usize>, right: Option<usize>) -> Option<i64> {
1326 match (left, right) {
1327 (Some(lhs), Some(rhs)) => Some(lhs as i64 - rhs as i64),
1328 _ => None,
1329 }
1330}
1331
1332fn mean_option(values: impl Iterator<Item = f64>) -> Option<f64> {
1333 let values = values.collect::<Vec<_>>();
1334 (!values.is_empty()).then_some(values.iter().sum::<f64>() / values.len() as f64)
1335}
1336
1337fn format_option(value: Option<f64>) -> String {
1338 value
1339 .map(|value| format!("{value:.4}"))
1340 .unwrap_or_else(|| "n/a".into())
1341}
1342
1343fn plot_error<E: std::fmt::Display>(error: E) -> DsfbSemiconductorError {
1344 DsfbSemiconductorError::DatasetFormat(format!("plot generation failed: {error}"))
1345}