Skip to main content

dsfb_semiconductor/
calibration.rs

1use crate::baselines::compute_baselines;
2use crate::config::PipelineConfig;
3use crate::dataset::secom;
4use crate::error::{DsfbSemiconductorError, Result};
5use crate::grammar::evaluate_grammar;
6use crate::metrics::compute_metrics;
7use crate::nominal::build_nominal_model;
8use crate::output_paths::{compile_pdf, create_timestamped_run_dir, default_output_root, zip_directory};
9use crate::precursor::{
10    run_dsa_calibration_grid, summarize_dsa_grid, DsaCalibrationGrid, DsaCalibrationRow,
11    DsaGridSummary,
12};
13use crate::preprocessing::prepare_secom;
14use crate::residual::compute_residuals;
15use crate::signs::compute_signs;
16use serde::Serialize;
17use std::fs;
18use std::path::{Path, PathBuf};
19
20#[derive(Debug, Clone, Serialize)]
21pub struct CalibrationGrid {
22    pub healthy_pass_runs: Vec<usize>,
23    pub drift_window: Vec<usize>,
24    pub envelope_sigma: Vec<f64>,
25    pub boundary_fraction_of_rho: Vec<f64>,
26    pub state_confirmation_steps: Vec<usize>,
27    pub persistent_state_steps: Vec<usize>,
28    pub density_window: Vec<usize>,
29    pub ewma_alpha: Vec<f64>,
30    pub ewma_sigma_multiplier: Vec<f64>,
31    pub cusum_kappa_sigma_multiplier: Vec<f64>,
32    pub cusum_alarm_sigma_multiplier: Vec<f64>,
33    pub run_energy_sigma_multiplier: Vec<f64>,
34    pub pca_variance_explained: Vec<f64>,
35    pub pca_t2_sigma_multiplier: Vec<f64>,
36    pub pca_spe_sigma_multiplier: Vec<f64>,
37    pub drift_sigma_multiplier: Vec<f64>,
38    pub slew_sigma_multiplier: Vec<f64>,
39    pub grazing_window: Vec<usize>,
40    pub grazing_min_hits: Vec<usize>,
41    pub pre_failure_lookback_runs: Vec<usize>,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct CalibrationArtifacts {
46    pub run_dir: PathBuf,
47    pub grid_results_csv: PathBuf,
48    pub summary_json: PathBuf,
49    pub report_markdown: PathBuf,
50    pub tex_report_path: PathBuf,
51    pub pdf_path: Option<PathBuf>,
52    pub zip_path: PathBuf,
53}
54
55#[derive(Debug, Clone, Serialize)]
56pub struct DsaCalibrationArtifacts {
57    pub run_dir: PathBuf,
58    pub grid_results_csv: PathBuf,
59    pub summary_json: PathBuf,
60    pub report_markdown: PathBuf,
61    pub tex_report_path: PathBuf,
62    pub pdf_path: Option<PathBuf>,
63    pub zip_path: PathBuf,
64}
65
66#[derive(Debug, Clone, Serialize)]
67struct CalibrationRunConfiguration {
68    dataset: String,
69    data_root: String,
70    output_root: String,
71    fetch_if_missing: bool,
72    grid: CalibrationGrid,
73}
74
75#[derive(Debug, Clone, Serialize)]
76struct CalibrationSummary {
77    grid_point_count: usize,
78    top_by_persistent_boundary_recall: Option<CalibrationResultRow>,
79    top_by_persistent_boundary_mean_lead: Option<CalibrationResultRow>,
80    top_by_low_persistent_boundary_nuisance: Option<CalibrationResultRow>,
81    top_by_persistent_boundary_minus_threshold_delta: Option<CalibrationResultRow>,
82    top_by_persistent_boundary_minus_ewma_delta: Option<CalibrationResultRow>,
83}
84
85#[derive(Debug, Clone, Serialize)]
86pub struct CalibrationResultRow {
87    pub config_id: usize,
88    pub healthy_pass_runs: usize,
89    pub drift_window: usize,
90    pub envelope_sigma: f64,
91    pub boundary_fraction_of_rho: f64,
92    pub state_confirmation_steps: usize,
93    pub persistent_state_steps: usize,
94    pub density_window: usize,
95    pub ewma_alpha: f64,
96    pub ewma_sigma_multiplier: f64,
97    pub cusum_kappa_sigma_multiplier: f64,
98    pub cusum_alarm_sigma_multiplier: f64,
99    pub run_energy_sigma_multiplier: f64,
100    pub pca_variance_explained: f64,
101    pub pca_t2_sigma_multiplier: f64,
102    pub pca_spe_sigma_multiplier: f64,
103    pub drift_sigma_multiplier: f64,
104    pub slew_sigma_multiplier: f64,
105    pub grazing_window: usize,
106    pub grazing_min_hits: usize,
107    pub pre_failure_lookback_runs: usize,
108    pub analyzable_feature_count: usize,
109    pub failure_runs: usize,
110    pub dsfb_raw_recall: usize,
111    pub dsfb_persistent_recall: usize,
112    pub dsfb_raw_boundary_recall: usize,
113    pub dsfb_persistent_boundary_recall: usize,
114    pub dsfb_raw_violation_recall: usize,
115    pub dsfb_persistent_violation_recall: usize,
116    pub ewma_recall: usize,
117    pub cusum_recall: usize,
118    pub run_energy_recall: usize,
119    pub pca_fdc_recall: usize,
120    pub threshold_recall: usize,
121    pub mean_raw_boundary_lead_runs: Option<f64>,
122    pub mean_persistent_boundary_lead_runs: Option<f64>,
123    pub mean_raw_violation_lead_runs: Option<f64>,
124    pub mean_persistent_violation_lead_runs: Option<f64>,
125    pub mean_ewma_lead_runs: Option<f64>,
126    pub mean_cusum_lead_runs: Option<f64>,
127    pub mean_run_energy_lead_runs: Option<f64>,
128    pub mean_pca_fdc_lead_runs: Option<f64>,
129    pub mean_threshold_lead_runs: Option<f64>,
130    pub mean_persistent_boundary_minus_cusum_delta_runs: Option<f64>,
131    pub mean_persistent_boundary_minus_pca_fdc_delta_runs: Option<f64>,
132    pub mean_persistent_boundary_minus_ewma_delta_runs: Option<f64>,
133    pub mean_persistent_boundary_minus_threshold_delta_runs: Option<f64>,
134    pub pass_run_dsfb_persistent_boundary_nuisance_rate: f64,
135    pub pass_run_dsfb_persistent_violation_nuisance_rate: f64,
136    pub pass_run_ewma_nuisance_rate: f64,
137    pub pass_run_cusum_nuisance_rate: f64,
138    pub pass_run_run_energy_nuisance_rate: f64,
139    pub pass_run_pca_fdc_nuisance_rate: f64,
140    pub pass_run_threshold_nuisance_rate: f64,
141    pub persistent_boundary_episode_count: usize,
142    pub mean_persistent_boundary_episode_length: Option<f64>,
143    pub persistent_non_escalating_boundary_episode_fraction: Option<f64>,
144    pub mean_persistent_boundary_density_failure: f64,
145    pub mean_persistent_boundary_density_pass: f64,
146    pub mean_persistent_violation_density_failure: f64,
147    pub mean_persistent_violation_density_pass: f64,
148    pub mean_threshold_density_failure: f64,
149    pub mean_threshold_density_pass: f64,
150    pub mean_ewma_density_failure: f64,
151    pub mean_ewma_density_pass: f64,
152    pub pre_failure_slow_drift_precision_proxy: Option<f64>,
153    pub transient_excursion_precision_proxy: Option<f64>,
154    pub recurrent_boundary_approach_precision_proxy: Option<f64>,
155}
156
157impl CalibrationGrid {
158    pub fn validate(&self) -> Result<()> {
159        let grid_point_count = self.grid_point_count();
160        if grid_point_count == 0 {
161            return Err(DsfbSemiconductorError::DatasetFormat(
162                "calibration grid must contain at least one point".into(),
163            ));
164        }
165        if grid_point_count > 4096 {
166            return Err(DsfbSemiconductorError::DatasetFormat(format!(
167                "calibration grid is too large ({grid_point_count} points); reduce the grid before running"
168            )));
169        }
170        Ok(())
171    }
172
173    pub fn grid_point_count(&self) -> usize {
174        [
175            self.healthy_pass_runs.len(),
176            self.drift_window.len(),
177            self.envelope_sigma.len(),
178            self.boundary_fraction_of_rho.len(),
179            self.state_confirmation_steps.len(),
180            self.persistent_state_steps.len(),
181            self.density_window.len(),
182            self.ewma_alpha.len(),
183            self.ewma_sigma_multiplier.len(),
184            self.cusum_kappa_sigma_multiplier.len(),
185            self.cusum_alarm_sigma_multiplier.len(),
186            self.run_energy_sigma_multiplier.len(),
187            self.pca_variance_explained.len(),
188            self.pca_t2_sigma_multiplier.len(),
189            self.pca_spe_sigma_multiplier.len(),
190            self.drift_sigma_multiplier.len(),
191            self.slew_sigma_multiplier.len(),
192            self.grazing_window.len(),
193            self.grazing_min_hits.len(),
194            self.pre_failure_lookback_runs.len(),
195        ]
196        .into_iter()
197        .product()
198    }
199
200    pub fn expand(&self) -> Vec<PipelineConfig> {
201        let mut configs = Vec::new();
202        for &healthy_pass_runs in &self.healthy_pass_runs {
203            for &drift_window in &self.drift_window {
204                for &envelope_sigma in &self.envelope_sigma {
205                    for &boundary_fraction_of_rho in &self.boundary_fraction_of_rho {
206                        for &state_confirmation_steps in &self.state_confirmation_steps {
207                            for &persistent_state_steps in &self.persistent_state_steps {
208                                for &density_window in &self.density_window {
209                                    for &ewma_alpha in &self.ewma_alpha {
210                                        for &ewma_sigma_multiplier in &self.ewma_sigma_multiplier {
211                                            for &cusum_kappa_sigma_multiplier in
212                                                &self.cusum_kappa_sigma_multiplier
213                                            {
214                                                for &cusum_alarm_sigma_multiplier in
215                                                    &self.cusum_alarm_sigma_multiplier
216                                                {
217                                                    for &run_energy_sigma_multiplier in
218                                                        &self.run_energy_sigma_multiplier
219                                                    {
220                                                        for &pca_variance_explained in
221                                                            &self.pca_variance_explained
222                                                        {
223                                                            for &pca_t2_sigma_multiplier in
224                                                                &self.pca_t2_sigma_multiplier
225                                                            {
226                                                                for &pca_spe_sigma_multiplier in
227                                                                    &self.pca_spe_sigma_multiplier
228                                                                {
229                                                                    for &drift_sigma_multiplier in
230                                                                        &self.drift_sigma_multiplier
231                                                                    {
232                                                                        for &slew_sigma_multiplier in &self.slew_sigma_multiplier {
233                                                                            for &grazing_window in &self.grazing_window {
234                                                                                for &grazing_min_hits in &self.grazing_min_hits {
235                                                                                    for &pre_failure_lookback_runs in &self.pre_failure_lookback_runs {
236                                                                                        configs.push(PipelineConfig {
237                                                                                            healthy_pass_runs,
238                                                                                            drift_window,
239                                                                                            envelope_sigma,
240                                                                                            boundary_fraction_of_rho,
241                                                                                            state_confirmation_steps,
242                                                                                            persistent_state_steps,
243                                                                                            density_window,
244                                                                                            ewma_alpha,
245                                                                                            ewma_sigma_multiplier,
246                                                                                            cusum_kappa_sigma_multiplier,
247                                                                                            cusum_alarm_sigma_multiplier,
248                                                                                            run_energy_sigma_multiplier,
249                                                                                            pca_variance_explained,
250                                                                                            pca_t2_sigma_multiplier,
251                                                                                            pca_spe_sigma_multiplier,
252                                                                                            drift_sigma_multiplier,
253                                                                                            slew_sigma_multiplier,
254                                                                                            grazing_window,
255                                                                                            grazing_min_hits,
256                                                                                            pre_failure_lookback_runs,
257                                                                                            ..PipelineConfig::default()
258                                                                                        });
259                                                                                    }
260                                                                                }
261                                                                            }
262                                                                        }
263                                                                    }
264                                                                }
265                                                            }
266                                                        }
267                                                    }
268                                                }
269                                            }
270                                        }
271                                    }
272                                }
273                            }
274                        }
275                    }
276                }
277            }
278        }
279        configs
280    }
281}
282
283pub fn run_secom_calibration(
284    data_root: &Path,
285    output_root: Option<&Path>,
286    grid: CalibrationGrid,
287    fetch_if_missing: bool,
288) -> Result<CalibrationArtifacts> {
289    grid.validate()?;
290
291    let _paths = if fetch_if_missing {
292        secom::fetch_if_missing(data_root)?
293    } else {
294        secom::ensure_present(data_root)?
295    };
296    let dataset = secom::load_from_root(data_root)?;
297
298    let output_root = output_root
299        .map(Path::to_path_buf)
300        .unwrap_or_else(default_output_root);
301    fs::create_dir_all(&output_root)?;
302    let run_dir = create_timestamped_run_dir(&output_root, "secom_calibration")?;
303
304    let expanded = grid.expand();
305    let mut rows = Vec::with_capacity(expanded.len());
306    for (config_id, config) in expanded.iter().enumerate() {
307        config
308            .validate()
309            .map_err(DsfbSemiconductorError::DatasetFormat)?;
310        let prepared = prepare_secom(&dataset, config)?;
311        let nominal = build_nominal_model(&prepared, config);
312        let residuals = compute_residuals(&prepared, &nominal);
313        let signs = compute_signs(&prepared, &nominal, &residuals, config);
314        let baselines = compute_baselines(&prepared, &nominal, &residuals, config);
315        let grammar = evaluate_grammar(&residuals, &signs, &nominal, config);
316        let metrics = compute_metrics(
317            &prepared, &nominal, &residuals, &signs, &baselines, &grammar, config,
318        );
319
320        rows.push(CalibrationResultRow {
321            config_id,
322            healthy_pass_runs: config.healthy_pass_runs,
323            drift_window: config.drift_window,
324            envelope_sigma: config.envelope_sigma,
325            boundary_fraction_of_rho: config.boundary_fraction_of_rho,
326            state_confirmation_steps: config.state_confirmation_steps,
327            persistent_state_steps: config.persistent_state_steps,
328            density_window: config.density_window,
329            ewma_alpha: config.ewma_alpha,
330            ewma_sigma_multiplier: config.ewma_sigma_multiplier,
331            cusum_kappa_sigma_multiplier: config.cusum_kappa_sigma_multiplier,
332            cusum_alarm_sigma_multiplier: config.cusum_alarm_sigma_multiplier,
333            run_energy_sigma_multiplier: config.run_energy_sigma_multiplier,
334            pca_variance_explained: config.pca_variance_explained,
335            pca_t2_sigma_multiplier: config.pca_t2_sigma_multiplier,
336            pca_spe_sigma_multiplier: config.pca_spe_sigma_multiplier,
337            drift_sigma_multiplier: config.drift_sigma_multiplier,
338            slew_sigma_multiplier: config.slew_sigma_multiplier,
339            grazing_window: config.grazing_window,
340            grazing_min_hits: config.grazing_min_hits,
341            pre_failure_lookback_runs: config.pre_failure_lookback_runs,
342            analyzable_feature_count: metrics.summary.analyzable_feature_count,
343            failure_runs: metrics.summary.failure_runs,
344            dsfb_raw_recall: metrics.summary.failure_runs_with_preceding_dsfb_raw_signal,
345            dsfb_persistent_recall: metrics
346                .summary
347                .failure_runs_with_preceding_dsfb_persistent_signal,
348            dsfb_raw_boundary_recall: metrics
349                .summary
350                .failure_runs_with_preceding_dsfb_raw_boundary_signal,
351            dsfb_persistent_boundary_recall: metrics
352                .summary
353                .failure_runs_with_preceding_dsfb_persistent_boundary_signal,
354            dsfb_raw_violation_recall: metrics
355                .summary
356                .failure_runs_with_preceding_dsfb_raw_violation_signal,
357            dsfb_persistent_violation_recall: metrics
358                .summary
359                .failure_runs_with_preceding_dsfb_persistent_violation_signal,
360            ewma_recall: metrics.summary.failure_runs_with_preceding_ewma_signal,
361            cusum_recall: metrics.summary.failure_runs_with_preceding_cusum_signal,
362            run_energy_recall: metrics
363                .summary
364                .failure_runs_with_preceding_run_energy_signal,
365            pca_fdc_recall: metrics.summary.failure_runs_with_preceding_pca_fdc_signal,
366            threshold_recall: metrics.summary.failure_runs_with_preceding_threshold_signal,
367            mean_raw_boundary_lead_runs: metrics.lead_time_summary.mean_raw_boundary_lead_runs,
368            mean_persistent_boundary_lead_runs: metrics
369                .lead_time_summary
370                .mean_persistent_boundary_lead_runs,
371            mean_raw_violation_lead_runs: metrics.lead_time_summary.mean_raw_violation_lead_runs,
372            mean_persistent_violation_lead_runs: metrics
373                .lead_time_summary
374                .mean_persistent_violation_lead_runs,
375            mean_ewma_lead_runs: metrics.lead_time_summary.mean_ewma_lead_runs,
376            mean_cusum_lead_runs: metrics.lead_time_summary.mean_cusum_lead_runs,
377            mean_run_energy_lead_runs: metrics.lead_time_summary.mean_run_energy_lead_runs,
378            mean_pca_fdc_lead_runs: metrics.lead_time_summary.mean_pca_fdc_lead_runs,
379            mean_threshold_lead_runs: metrics.lead_time_summary.mean_threshold_lead_runs,
380            mean_persistent_boundary_minus_cusum_delta_runs: metrics
381                .lead_time_summary
382                .mean_persistent_boundary_minus_cusum_delta_runs,
383            mean_persistent_boundary_minus_pca_fdc_delta_runs: metrics
384                .lead_time_summary
385                .mean_persistent_boundary_minus_pca_fdc_delta_runs,
386            mean_persistent_boundary_minus_ewma_delta_runs: metrics
387                .lead_time_summary
388                .mean_persistent_boundary_minus_ewma_delta_runs,
389            mean_persistent_boundary_minus_threshold_delta_runs: metrics
390                .lead_time_summary
391                .mean_persistent_boundary_minus_threshold_delta_runs,
392            pass_run_dsfb_persistent_boundary_nuisance_rate: metrics
393                .summary
394                .pass_run_dsfb_persistent_boundary_nuisance_rate,
395            pass_run_dsfb_persistent_violation_nuisance_rate: metrics
396                .summary
397                .pass_run_dsfb_persistent_violation_nuisance_rate,
398            pass_run_ewma_nuisance_rate: metrics.summary.pass_run_ewma_nuisance_rate,
399            pass_run_cusum_nuisance_rate: metrics.summary.pass_run_cusum_nuisance_rate,
400            pass_run_run_energy_nuisance_rate: metrics.summary.pass_run_run_energy_nuisance_rate,
401            pass_run_pca_fdc_nuisance_rate: metrics.summary.pass_run_pca_fdc_nuisance_rate,
402            pass_run_threshold_nuisance_rate: metrics.summary.pass_run_threshold_nuisance_rate,
403            persistent_boundary_episode_count: metrics
404                .boundary_episode_summary
405                .persistent_episode_count,
406            mean_persistent_boundary_episode_length: metrics
407                .boundary_episode_summary
408                .mean_persistent_episode_length,
409            persistent_non_escalating_boundary_episode_fraction: metrics
410                .boundary_episode_summary
411                .persistent_non_escalating_episode_fraction,
412            mean_persistent_boundary_density_failure: metrics
413                .density_summary
414                .mean_persistent_boundary_density_failure,
415            mean_persistent_boundary_density_pass: metrics
416                .density_summary
417                .mean_persistent_boundary_density_pass,
418            mean_persistent_violation_density_failure: metrics
419                .density_summary
420                .mean_persistent_violation_density_failure,
421            mean_persistent_violation_density_pass: metrics
422                .density_summary
423                .mean_persistent_violation_density_pass,
424            mean_threshold_density_failure: metrics.density_summary.mean_threshold_density_failure,
425            mean_threshold_density_pass: metrics.density_summary.mean_threshold_density_pass,
426            mean_ewma_density_failure: metrics.density_summary.mean_ewma_density_failure,
427            mean_ewma_density_pass: metrics.density_summary.mean_ewma_density_pass,
428            pre_failure_slow_drift_precision_proxy: motif_precision(
429                &metrics,
430                "pre_failure_slow_drift",
431            ),
432            transient_excursion_precision_proxy: motif_precision(&metrics, "transient_excursion"),
433            recurrent_boundary_approach_precision_proxy: motif_precision(
434                &metrics,
435                "recurrent_boundary_approach",
436            ),
437        });
438    }
439
440    let grid_results_csv = run_dir.join("calibration_grid_results.csv");
441    let summary_json = run_dir.join("calibration_best_by_metric.json");
442    let report_markdown = run_dir.join("calibration_report.md");
443
444    write_grid_results(&grid_results_csv, &rows)?;
445    write_summary(
446        &summary_json,
447        &CalibrationSummary {
448            grid_point_count: rows.len(),
449            top_by_persistent_boundary_recall: best_by_persistent_boundary_recall(&rows),
450            top_by_persistent_boundary_mean_lead: best_by_persistent_boundary_mean_lead(&rows),
451            top_by_low_persistent_boundary_nuisance: best_by_low_persistent_boundary_nuisance(
452                &rows,
453            ),
454            top_by_persistent_boundary_minus_threshold_delta:
455                best_by_persistent_boundary_minus_threshold_delta(&rows),
456            top_by_persistent_boundary_minus_ewma_delta:
457                best_by_persistent_boundary_minus_ewma_delta(&rows),
458        },
459    )?;
460    fs::write(
461        run_dir.join("calibration_run_configuration.json"),
462        serde_json::to_string_pretty(&CalibrationRunConfiguration {
463            dataset: "SECOM".into(),
464            data_root: data_root.display().to_string(),
465            output_root: output_root.display().to_string(),
466            fetch_if_missing,
467            grid: grid.clone(),
468        })?,
469    )?;
470    fs::write(
471        run_dir.join("parameter_grid_manifest.json"),
472        serde_json::to_string_pretty(&grid)?,
473    )?;
474    fs::write(
475        &report_markdown,
476        calibration_report(
477            &rows,
478            best_by_persistent_boundary_recall(&rows).as_ref(),
479            best_by_persistent_boundary_minus_threshold_delta(&rows).as_ref(),
480            best_by_persistent_boundary_minus_ewma_delta(&rows).as_ref(),
481            best_by_low_persistent_boundary_nuisance(&rows).as_ref(),
482        ),
483    )?;
484
485    let tex_report_path = run_dir.join("calibration_engineering_report.tex");
486    fs::write(
487        &tex_report_path,
488        calibration_tex_report(
489            &rows,
490            best_by_persistent_boundary_recall(&rows).as_ref(),
491            best_by_persistent_boundary_minus_threshold_delta(&rows).as_ref(),
492            best_by_persistent_boundary_minus_ewma_delta(&rows).as_ref(),
493            best_by_low_persistent_boundary_nuisance(&rows).as_ref(),
494        ),
495    )?;
496    let (pdf_path, pdf_error) = compile_pdf(&tex_report_path, &run_dir);
497    if let Some(ref err) = pdf_error {
498        eprintln!(
499            "[calibrate-secom] PDF compile warning: {}",
500            err.lines().next().unwrap_or("unknown")
501        );
502    }
503    let zip_path = run_dir.join("calibration_bundle.zip");
504    zip_directory(&run_dir, &zip_path)?;
505
506    Ok(CalibrationArtifacts {
507        run_dir,
508        grid_results_csv,
509        summary_json,
510        report_markdown,
511        tex_report_path,
512        pdf_path,
513        zip_path,
514    })
515}
516
517pub fn run_secom_dsa_calibration(
518    data_root: &Path,
519    output_root: Option<&Path>,
520    config: PipelineConfig,
521    grid: DsaCalibrationGrid,
522    fetch_if_missing: bool,
523) -> Result<DsaCalibrationArtifacts> {
524    config
525        .validate()
526        .map_err(DsfbSemiconductorError::DatasetFormat)?;
527
528    let _paths = if fetch_if_missing {
529        secom::fetch_if_missing(data_root)?
530    } else {
531        secom::ensure_present(data_root)?
532    };
533    let dataset = secom::load_from_root(data_root)?;
534    let prepared = prepare_secom(&dataset, &config)?;
535    let nominal = build_nominal_model(&prepared, &config);
536    let residuals = compute_residuals(&prepared, &nominal);
537    let signs = compute_signs(&prepared, &nominal, &residuals, &config);
538    let baselines = compute_baselines(&prepared, &nominal, &residuals, &config);
539    let grammar = evaluate_grammar(&residuals, &signs, &nominal, &config);
540
541    let output_root = output_root
542        .map(Path::to_path_buf)
543        .unwrap_or_else(default_output_root);
544    fs::create_dir_all(&output_root)?;
545    let run_dir = create_timestamped_run_dir(&output_root, "secom_dsa_calibration")?;
546    let rows = run_dsa_calibration_grid(
547        &prepared,
548        &nominal,
549        &residuals,
550        &signs,
551        &baselines,
552        &grammar,
553        &grid,
554        config.pre_failure_lookback_runs,
555    )?;
556    let summary = summarize_dsa_grid(&rows);
557
558    let grid_results_csv = run_dir.join("dsa_grid_results.csv");
559    let summary_json = run_dir.join("dsa_grid_summary.json");
560    let report_markdown = run_dir.join("dsa_calibration_report.md");
561    write_dsa_grid_results(&grid_results_csv, &rows)?;
562    fs::write(&summary_json, serde_json::to_string_pretty(&summary)?)?;
563    fs::write(&report_markdown, dsa_calibration_report(&summary))?;
564
565    let tex_report_path = run_dir.join("dsa_calibration_engineering_report.tex");
566    fs::write(&tex_report_path, dsa_calibration_tex_report(&summary))?;
567    let (pdf_path, pdf_error) = compile_pdf(&tex_report_path, &run_dir);
568    if let Some(ref err) = pdf_error {
569        eprintln!(
570            "[calibrate-secom-dsa] PDF compile warning: {}",
571            err.lines().next().unwrap_or("unknown")
572        );
573    }
574    let zip_path = run_dir.join("dsa_calibration_bundle.zip");
575    zip_directory(&run_dir, &zip_path)?;
576    fs::write(
577        run_dir.join("dsa_calibration_run_configuration.json"),
578        serde_json::to_string_pretty(&CalibrationRunConfiguration {
579            dataset: "SECOM".into(),
580            data_root: data_root.display().to_string(),
581            output_root: output_root.display().to_string(),
582            fetch_if_missing,
583            grid: CalibrationGrid {
584                healthy_pass_runs: vec![config.healthy_pass_runs],
585                drift_window: vec![config.drift_window],
586                envelope_sigma: vec![config.envelope_sigma],
587                boundary_fraction_of_rho: vec![config.boundary_fraction_of_rho],
588                state_confirmation_steps: vec![config.state_confirmation_steps],
589                persistent_state_steps: vec![config.persistent_state_steps],
590                density_window: vec![config.density_window],
591                ewma_alpha: vec![config.ewma_alpha],
592                ewma_sigma_multiplier: vec![config.ewma_sigma_multiplier],
593                cusum_kappa_sigma_multiplier: vec![config.cusum_kappa_sigma_multiplier],
594                cusum_alarm_sigma_multiplier: vec![config.cusum_alarm_sigma_multiplier],
595                run_energy_sigma_multiplier: vec![config.run_energy_sigma_multiplier],
596                pca_variance_explained: vec![config.pca_variance_explained],
597                pca_t2_sigma_multiplier: vec![config.pca_t2_sigma_multiplier],
598                pca_spe_sigma_multiplier: vec![config.pca_spe_sigma_multiplier],
599                drift_sigma_multiplier: vec![config.drift_sigma_multiplier],
600                slew_sigma_multiplier: vec![config.slew_sigma_multiplier],
601                grazing_window: vec![config.grazing_window],
602                grazing_min_hits: vec![config.grazing_min_hits],
603                pre_failure_lookback_runs: vec![config.pre_failure_lookback_runs],
604            },
605        })?,
606    )?;
607    fs::write(
608        run_dir.join("dsa_parameter_grid_manifest.json"),
609        serde_json::to_string_pretty(&grid)?,
610    )?;
611
612    Ok(DsaCalibrationArtifacts {
613        run_dir,
614        grid_results_csv,
615        summary_json,
616        report_markdown,
617        tex_report_path,
618        pdf_path,
619        zip_path,
620    })
621}
622
623fn write_grid_results(path: &Path, rows: &[CalibrationResultRow]) -> Result<()> {
624    let mut writer = csv::Writer::from_path(path)?;
625    for row in rows {
626        writer.serialize(row)?;
627    }
628    writer.flush()?;
629    Ok(())
630}
631
632fn write_dsa_grid_results(path: &Path, rows: &[DsaCalibrationRow]) -> Result<()> {
633    let mut writer = csv::Writer::from_path(path)?;
634    for row in rows {
635        writer.serialize(row)?;
636    }
637    writer.flush()?;
638    Ok(())
639}
640
641fn dsa_calibration_report(summary: &DsaGridSummary) -> String {
642    let mut out = String::new();
643    out.push_str("# SECOM DSA calibration report\n\n");
644    out.push_str(&format!(
645        "- Grid points evaluated: {}\n- Primary success condition: {}\n- Success rows: {}\n- Cross-feature corroboration effect: {}\n- Limiting factor: {}\n\n",
646        summary.grid_point_count,
647        summary.primary_success_condition_definition,
648        summary.success_row_count,
649        summary.cross_feature_corroboration_effect,
650        summary.limiting_factor,
651    ));
652    if let Some(row) = &summary.closest_to_success {
653        out.push_str("## Closest to primary success\n\n");
654        out.push_str(&format!(
655            "- config_id: {}\n- W: {}\n- K: {}\n- tau: {:.2}\n- m: {}\n- recall: {}/{}\n- mean lead time: {}\n- nuisance: {:.4}\n- precursor quality: {}\n- compression ratio: {}\n\n",
656            row.config_id,
657            row.window,
658            row.persistence_runs,
659            row.alert_tau,
660            row.corroborating_feature_count_min,
661            row.failure_run_recall,
662            row.failure_runs,
663            format_option_f64(row.mean_lead_time_runs),
664            row.pass_run_nuisance_proxy,
665            format_option_f64(row.precursor_quality),
666            format_option_f64(row.compression_ratio),
667        ));
668    }
669    out
670}
671
672fn write_summary(path: &Path, summary: &CalibrationSummary) -> Result<()> {
673    fs::write(path, serde_json::to_string_pretty(summary)?)?;
674    Ok(())
675}
676
677fn best_by_persistent_boundary_recall(
678    rows: &[CalibrationResultRow],
679) -> Option<CalibrationResultRow> {
680    rows.iter().cloned().max_by(|left, right| {
681        left.dsfb_persistent_boundary_recall
682            .cmp(&right.dsfb_persistent_boundary_recall)
683            .then_with(|| {
684                cmp_option_f64(
685                    left.mean_persistent_boundary_lead_runs,
686                    right.mean_persistent_boundary_lead_runs,
687                )
688            })
689            .then_with(|| {
690                cmp_f64_ascending(
691                    left.pass_run_dsfb_persistent_boundary_nuisance_rate,
692                    right.pass_run_dsfb_persistent_boundary_nuisance_rate,
693                )
694            })
695    })
696}
697
698fn best_by_persistent_boundary_mean_lead(
699    rows: &[CalibrationResultRow],
700) -> Option<CalibrationResultRow> {
701    rows.iter().cloned().max_by(|left, right| {
702        cmp_option_f64(
703            left.mean_persistent_boundary_lead_runs,
704            right.mean_persistent_boundary_lead_runs,
705        )
706        .then_with(|| {
707            left.dsfb_persistent_boundary_recall
708                .cmp(&right.dsfb_persistent_boundary_recall)
709        })
710        .then_with(|| {
711            cmp_f64_ascending(
712                left.pass_run_dsfb_persistent_boundary_nuisance_rate,
713                right.pass_run_dsfb_persistent_boundary_nuisance_rate,
714            )
715        })
716    })
717}
718
719fn best_by_low_persistent_boundary_nuisance(
720    rows: &[CalibrationResultRow],
721) -> Option<CalibrationResultRow> {
722    rows.iter().cloned().max_by(|left, right| {
723        cmp_f64_ascending(
724            left.pass_run_dsfb_persistent_boundary_nuisance_rate,
725            right.pass_run_dsfb_persistent_boundary_nuisance_rate,
726        )
727        .then_with(|| {
728            left.dsfb_persistent_boundary_recall
729                .cmp(&right.dsfb_persistent_boundary_recall)
730        })
731        .then_with(|| {
732            cmp_option_f64(
733                left.mean_persistent_boundary_lead_runs,
734                right.mean_persistent_boundary_lead_runs,
735            )
736        })
737    })
738}
739
740fn best_by_persistent_boundary_minus_threshold_delta(
741    rows: &[CalibrationResultRow],
742) -> Option<CalibrationResultRow> {
743    rows.iter().cloned().max_by(|left, right| {
744        cmp_option_f64(
745            left.mean_persistent_boundary_minus_threshold_delta_runs,
746            right.mean_persistent_boundary_minus_threshold_delta_runs,
747        )
748        .then_with(|| {
749            left.dsfb_persistent_boundary_recall
750                .cmp(&right.dsfb_persistent_boundary_recall)
751        })
752        .then_with(|| {
753            cmp_f64_ascending(
754                left.pass_run_dsfb_persistent_boundary_nuisance_rate,
755                right.pass_run_dsfb_persistent_boundary_nuisance_rate,
756            )
757        })
758    })
759}
760
761fn best_by_persistent_boundary_minus_ewma_delta(
762    rows: &[CalibrationResultRow],
763) -> Option<CalibrationResultRow> {
764    rows.iter().cloned().max_by(|left, right| {
765        cmp_option_f64(
766            left.mean_persistent_boundary_minus_ewma_delta_runs,
767            right.mean_persistent_boundary_minus_ewma_delta_runs,
768        )
769        .then_with(|| {
770            left.dsfb_persistent_boundary_recall
771                .cmp(&right.dsfb_persistent_boundary_recall)
772        })
773        .then_with(|| {
774            cmp_f64_ascending(
775                left.pass_run_dsfb_persistent_boundary_nuisance_rate,
776                right.pass_run_dsfb_persistent_boundary_nuisance_rate,
777            )
778        })
779    })
780}
781
782fn motif_precision(metrics: &crate::metrics::BenchmarkMetrics, motif_name: &str) -> Option<f64> {
783    metrics
784        .motif_metrics
785        .iter()
786        .find(|metric| metric.motif_name == motif_name)
787        .and_then(|metric| metric.pre_failure_window_precision_proxy)
788}
789
790fn cmp_option_f64(left: Option<f64>, right: Option<f64>) -> std::cmp::Ordering {
791    match (left, right) {
792        (Some(left), Some(right)) => left
793            .partial_cmp(&right)
794            .unwrap_or(std::cmp::Ordering::Equal),
795        (Some(_), None) => std::cmp::Ordering::Greater,
796        (None, Some(_)) => std::cmp::Ordering::Less,
797        (None, None) => std::cmp::Ordering::Equal,
798    }
799}
800
801fn cmp_f64_ascending(left: f64, right: f64) -> std::cmp::Ordering {
802    right
803        .partial_cmp(&left)
804        .unwrap_or(std::cmp::Ordering::Equal)
805}
806
807fn calibration_report(
808    rows: &[CalibrationResultRow],
809    best_recall: Option<&CalibrationResultRow>,
810    best_threshold_delta: Option<&CalibrationResultRow>,
811    best_ewma_delta: Option<&CalibrationResultRow>,
812    best_low_nuisance: Option<&CalibrationResultRow>,
813) -> String {
814    let mut out = String::new();
815    out.push_str("# SECOM calibration report\n\n");
816    out.push_str(&format!("- Grid points evaluated: {}\n\n", rows.len()));
817    out.push_str(
818        "This report summarizes deterministic parameter-grid trade-offs over persistent DSFB boundary lead time, recall, and nuisance proxies. It is a calibration report, not a superiority claim.\n\n",
819    );
820
821    if let Some(row) = best_recall {
822        out.push_str("## Best persistent-boundary recall\n\n");
823        out.push_str(&format!(
824            "- config_id: {}\n- persistent boundary recall: {}\n- mean persistent boundary lead runs: {}\n- persistent boundary minus threshold delta runs: {}\n- pass-run persistent boundary nuisance rate: {:.4}\n\n",
825            row.config_id,
826            row.dsfb_persistent_boundary_recall,
827            format_option_f64(row.mean_persistent_boundary_lead_runs),
828            format_option_f64(row.mean_persistent_boundary_minus_threshold_delta_runs),
829            row.pass_run_dsfb_persistent_boundary_nuisance_rate,
830        ));
831    }
832
833    if let Some(row) = best_threshold_delta {
834        out.push_str("## Best persistent-boundary minus threshold delta\n\n");
835        out.push_str(&format!(
836            "- config_id: {}\n- mean persistent boundary minus threshold delta runs: {}\n- persistent boundary recall: {}\n- pass-run persistent boundary nuisance rate: {:.4}\n\n",
837            row.config_id,
838            format_option_f64(row.mean_persistent_boundary_minus_threshold_delta_runs),
839            row.dsfb_persistent_boundary_recall,
840            row.pass_run_dsfb_persistent_boundary_nuisance_rate,
841        ));
842    }
843
844    if let Some(row) = best_ewma_delta {
845        out.push_str("## Best persistent-boundary minus EWMA delta\n\n");
846        out.push_str(&format!(
847            "- config_id: {}\n- mean persistent boundary minus EWMA delta runs: {}\n- persistent boundary recall: {}\n- pass-run persistent boundary nuisance rate: {:.4}\n\n",
848            row.config_id,
849            format_option_f64(row.mean_persistent_boundary_minus_ewma_delta_runs),
850            row.dsfb_persistent_boundary_recall,
851            row.pass_run_dsfb_persistent_boundary_nuisance_rate,
852        ));
853    }
854
855    if let Some(row) = best_low_nuisance {
856        out.push_str("## Lowest persistent-boundary nuisance\n\n");
857        out.push_str(&format!(
858            "- config_id: {}\n- pass-run persistent boundary nuisance rate: {:.4}\n- persistent boundary recall: {}\n- mean persistent boundary lead runs: {}\n\n",
859            row.config_id,
860            row.pass_run_dsfb_persistent_boundary_nuisance_rate,
861            row.dsfb_persistent_boundary_recall,
862            format_option_f64(row.mean_persistent_boundary_lead_runs),
863        ));
864    }
865
866    out.push_str("## Interpretation\n\n");
867    out.push_str(
868        "A positive persistent-boundary lead delta is meaningful only if it is paired with acceptable nuisance and bounded calibration sensitivity. In the current crate this grid is intended to surface trade-offs explicitly, not to imply that a favorable configuration is already deployment-ready.\n",
869    );
870    out
871}
872
873fn calibration_tex_report(
874    rows: &[CalibrationResultRow],
875    best_recall: Option<&CalibrationResultRow>,
876    best_threshold_delta: Option<&CalibrationResultRow>,
877    best_ewma_delta: Option<&CalibrationResultRow>,
878    best_low_nuisance: Option<&CalibrationResultRow>,
879) -> String {
880    let fmt = format_option_f64;
881    let row_section = |label: &str, row: Option<&CalibrationResultRow>| -> String {
882        match row {
883            None => format!("\\subsection{{{label}}}\nNo row available.\n\n"),
884            Some(r) => format!(
885                "\\subsection{{{label}}}\n\
886                 \\begin{{itemize}}\n\
887                 \\item config\\_id: {}\n\
888                 \\item persistent boundary recall: {}\n\
889                 \\item mean persistent boundary lead runs: {}\n\
890                 \\item persistent boundary minus threshold delta: {}\n\
891                 \\item pass-run persistent boundary nuisance rate: {:.4}\n\
892                 \\end{{itemize}}\n\n",
893                r.config_id,
894                r.dsfb_persistent_boundary_recall,
895                fmt(r.mean_persistent_boundary_lead_runs),
896                fmt(r.mean_persistent_boundary_minus_threshold_delta_runs),
897                r.pass_run_dsfb_persistent_boundary_nuisance_rate,
898            ),
899        }
900    };
901
902    format!(
903        r#"\documentclass{{article}}
904\usepackage[utf8]{{inputenc}}
905\usepackage[margin=1in]{{geometry}}
906\usepackage{{booktabs}}
907\usepackage{{hyperref}}
908\usepackage{{parskip}}
909\title{{DSFB SECOM Calibration Engineering Report}}
910\author{{DSFB Semiconductor Companion Crate}}
911\date{{\today}}
912\begin{{document}}
913\maketitle
914
915\section{{Grid Summary}}
916\begin{{itemize}}
917  \item Dataset: SECOM (UCI Machine Learning Repository)
918  \item Grid points evaluated: {}
919  \item Purpose: surface parameter trade-offs; not a superiority claim
920\end{{itemize}}
921
922\section{{Best Configurations}}
923{}{}{}{}
924\section{{Interpretation}}
925A positive persistent-boundary lead delta is meaningful only if paired with
926acceptable nuisance and bounded calibration sensitivity.
927This grid surfaces trade-offs explicitly; a favorable configuration here does
928not imply deployment readiness.
929
930\end{{document}}
931"#,
932        rows.len(),
933        row_section("Best persistent-boundary recall", best_recall),
934        row_section(
935            "Best persistent-boundary minus threshold delta",
936            best_threshold_delta
937        ),
938        row_section(
939            "Best persistent-boundary minus EWMA delta",
940            best_ewma_delta
941        ),
942        row_section("Lowest persistent-boundary nuisance", best_low_nuisance),
943    )
944}
945
946fn dsa_calibration_tex_report(summary: &crate::precursor::DsaGridSummary) -> String {
947    let fmt = format_option_f64;
948    let closest_section = match &summary.closest_to_success {
949        None => "No row available.\n".into(),
950        Some(r) => format!(
951            "\\begin{{itemize}}\n\
952             \\item config\\_id: {}\n\
953             \\item W: {}, K: {}, $\\tau$: {:.2}, m: {}\n\
954             \\item recall: {}/{}\n\
955             \\item mean lead time: {}\n\
956             \\item nuisance proxy: {:.4}\n\
957             \\item precursor quality: {}\n\
958             \\item compression ratio: {}\n\
959             \\end{{itemize}}\n",
960            r.config_id,
961            r.window,
962            r.persistence_runs,
963            r.alert_tau,
964            r.corroborating_feature_count_min,
965            r.failure_run_recall,
966            r.failure_runs,
967            fmt(r.mean_lead_time_runs),
968            r.pass_run_nuisance_proxy,
969            fmt(r.precursor_quality),
970            fmt(r.compression_ratio),
971        ),
972    };
973
974    format!(
975        r#"\documentclass{{article}}
976\usepackage[utf8]{{inputenc}}
977\usepackage[margin=1in]{{geometry}}
978\usepackage{{booktabs}}
979\usepackage{{hyperref}}
980\usepackage{{parskip}}
981\title{{DSFB SECOM DSA Calibration Engineering Report}}
982\author{{DSFB Semiconductor Companion Crate}}
983\date{{\today}}
984\begin{{document}}
985\maketitle
986
987\section{{Grid Summary}}
988\begin{{itemize}}
989  \item Dataset: SECOM (UCI Machine Learning Repository)
990  \item Grid points evaluated: {}
991  \item Primary success condition: {}
992  \item Success rows: {}
993  \item Cross-feature corroboration effect: {}
994  \item Limiting factor: {}
995\end{{itemize}}
996
997\section{{Closest to Primary Success}}
998{closest_section}
999\section{{Interpretation}}
1000The DSA calibration grid sweeps $(W, K, \tau, m)$ over additive structural
1001accumulator parameters.
1002The primary success condition is a strict operational threshold.
1003Rows labelled as success satisfy the threshold; closeness indicates
1004how far the best configuration falls from guaranteed deployment quality.
1005
1006\end{{document}}
1007"#,
1008        summary.grid_point_count,
1009        summary.primary_success_condition_definition,
1010        summary.success_row_count,
1011        summary.cross_feature_corroboration_effect,
1012        summary.limiting_factor,
1013        closest_section = closest_section,
1014    )
1015}
1016
1017fn format_option_f64(value: Option<f64>) -> String {
1018    value
1019        .map(|value| format!("{value:.4}"))
1020        .unwrap_or_else(|| "n/a".into())
1021}