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}