1use crate::calibration::{run_secom_calibration, run_secom_dsa_calibration, CalibrationGrid};
2use crate::config::PipelineConfig;
3use crate::dataset::phm2018;
4use crate::dataset::secom;
5use crate::error::Result;
6use crate::non_intrusive::materialize_non_intrusive_artifacts;
7use crate::output_paths::{default_data_root, default_output_root};
8use crate::phm2018_loader::run_phm2018_benchmark;
9use crate::pipeline::{run_secom_benchmark, PaperLockMetrics};
10use crate::unified_value_figure::{render_unified_value_figure, resolve_latest_completed_run};
11use clap::{Args, Parser, Subcommand};
12use std::path::PathBuf;
13
14#[derive(Debug, Parser)]
15#[command(name = "dsfb-semiconductor")]
16#[command(about = "DSFB semiconductor benchmark companion")]
17pub struct Cli {
18 #[command(subcommand)]
19 command: Command,
20}
21
22#[derive(Debug, Subcommand)]
23enum Command {
24 FetchSecom(DataArgs),
25 RunSecom(RunSecomArgs),
26 CalibrateSecom(CalibrateSecomArgs),
27 CalibrateSecomDsa(CalibrateSecomDsaArgs),
28 ProbePhm2018(ProbePhm2018Args),
29 RunPhm2018(RunPhm2018Args),
30 RenderNonIntrusiveArtifacts(RenderNonIntrusiveArtifactsArgs),
31 RenderUnifiedValueFigure(RenderUnifiedValueFigureArgs),
32 SbirDemo(SbirDemoArgs),
33 PaperLock(PaperLockArgs),
44}
45
46#[derive(Debug, Args)]
47struct DataArgs {
48 #[arg(long)]
49 data_root: Option<PathBuf>,
50}
51
52#[derive(Debug, Args)]
53struct RunSecomArgs {
54 #[command(flatten)]
55 data: DataArgs,
56 #[arg(long)]
57 output_root: Option<PathBuf>,
58 #[arg(long, default_value_t = false)]
59 fetch_if_missing: bool,
60 #[arg(long, default_value_t = 100)]
61 healthy_pass_runs: usize,
62 #[arg(long, default_value_t = 5)]
63 drift_window: usize,
64 #[arg(long, default_value_t = 3.0)]
65 envelope_sigma: f64,
66 #[arg(long, default_value_t = 0.5)]
67 boundary_fraction_of_rho: f64,
68 #[arg(long, default_value_t = 2)]
69 state_confirmation_steps: usize,
70 #[arg(long, default_value_t = 2)]
71 persistent_state_steps: usize,
72 #[arg(long, default_value_t = 10)]
73 density_window: usize,
74 #[arg(long, default_value_t = 0.2)]
75 ewma_alpha: f64,
76 #[arg(long, default_value_t = 3.0)]
77 ewma_sigma_multiplier: f64,
78 #[arg(long, default_value_t = 0.5)]
79 cusum_kappa_sigma_multiplier: f64,
80 #[arg(long, default_value_t = 5.0)]
81 cusum_alarm_sigma_multiplier: f64,
82 #[arg(long, default_value_t = 3.0)]
83 run_energy_sigma_multiplier: f64,
84 #[arg(long, default_value_t = 0.95)]
85 pca_variance_explained: f64,
86 #[arg(long, default_value_t = 3.0)]
87 pca_t2_sigma_multiplier: f64,
88 #[arg(long, default_value_t = 3.0)]
89 pca_spe_sigma_multiplier: f64,
90 #[arg(long, default_value_t = 3.0)]
91 drift_sigma_multiplier: f64,
92 #[arg(long, default_value_t = 3.0)]
93 slew_sigma_multiplier: f64,
94 #[arg(long, default_value_t = 10)]
95 grazing_window: usize,
96 #[arg(long, default_value_t = 3)]
97 grazing_min_hits: usize,
98 #[arg(long, default_value_t = 20)]
99 pre_failure_lookback_runs: usize,
100 #[arg(long, default_value_t = 5)]
101 dsa_window: usize,
102 #[arg(long, default_value_t = 2)]
103 dsa_persistence_runs: usize,
104 #[arg(long, default_value_t = 2.0)]
105 dsa_alert_tau: f64,
106 #[arg(long, default_value_t = 2)]
107 dsa_corroborating_feature_count_min: usize,
108}
109
110#[derive(Debug, Args)]
111struct CalibrateSecomArgs {
112 #[command(flatten)]
113 data: DataArgs,
114 #[arg(long)]
115 output_root: Option<PathBuf>,
116 #[arg(long, default_value_t = false)]
117 fetch_if_missing: bool,
118 #[arg(long, value_delimiter = ',', default_value = "100")]
119 healthy_pass_runs_grid: Vec<usize>,
120 #[arg(long, value_delimiter = ',', default_value = "5")]
121 drift_window_grid: Vec<usize>,
122 #[arg(long, value_delimiter = ',', default_value = "3.0")]
123 envelope_sigma_grid: Vec<f64>,
124 #[arg(long, value_delimiter = ',', default_value = "0.5")]
125 boundary_fraction_of_rho_grid: Vec<f64>,
126 #[arg(long, value_delimiter = ',', default_value = "2")]
127 state_confirmation_steps_grid: Vec<usize>,
128 #[arg(long, value_delimiter = ',', default_value = "2")]
129 persistent_state_steps_grid: Vec<usize>,
130 #[arg(long, value_delimiter = ',', default_value = "10")]
131 density_window_grid: Vec<usize>,
132 #[arg(long, value_delimiter = ',', default_value = "0.2")]
133 ewma_alpha_grid: Vec<f64>,
134 #[arg(long, value_delimiter = ',', default_value = "3.0")]
135 ewma_sigma_multiplier_grid: Vec<f64>,
136 #[arg(long, value_delimiter = ',', default_value = "0.5")]
137 cusum_kappa_sigma_multiplier_grid: Vec<f64>,
138 #[arg(long, value_delimiter = ',', default_value = "5.0")]
139 cusum_alarm_sigma_multiplier_grid: Vec<f64>,
140 #[arg(long, value_delimiter = ',', default_value = "3.0")]
141 run_energy_sigma_multiplier_grid: Vec<f64>,
142 #[arg(long, value_delimiter = ',', default_value = "0.95")]
143 pca_variance_explained_grid: Vec<f64>,
144 #[arg(long, value_delimiter = ',', default_value = "3.0")]
145 pca_t2_sigma_multiplier_grid: Vec<f64>,
146 #[arg(long, value_delimiter = ',', default_value = "3.0")]
147 pca_spe_sigma_multiplier_grid: Vec<f64>,
148 #[arg(long, value_delimiter = ',', default_value = "3.0")]
149 drift_sigma_multiplier_grid: Vec<f64>,
150 #[arg(long, value_delimiter = ',', default_value = "3.0")]
151 slew_sigma_multiplier_grid: Vec<f64>,
152 #[arg(long, value_delimiter = ',', default_value = "10")]
153 grazing_window_grid: Vec<usize>,
154 #[arg(long, value_delimiter = ',', default_value = "3")]
155 grazing_min_hits_grid: Vec<usize>,
156 #[arg(long, value_delimiter = ',', default_value = "20")]
157 pre_failure_lookback_runs_grid: Vec<usize>,
158}
159
160#[derive(Debug, Args)]
161struct CalibrateSecomDsaArgs {
162 #[command(flatten)]
163 data: DataArgs,
164 #[arg(long)]
165 output_root: Option<PathBuf>,
166 #[arg(long, default_value_t = false)]
167 fetch_if_missing: bool,
168 #[arg(long, default_value_t = 100)]
169 healthy_pass_runs: usize,
170 #[arg(long, default_value_t = 5)]
171 drift_window: usize,
172 #[arg(long, default_value_t = 3.0)]
173 envelope_sigma: f64,
174 #[arg(long, default_value_t = 0.5)]
175 boundary_fraction_of_rho: f64,
176 #[arg(long, default_value_t = 2)]
177 state_confirmation_steps: usize,
178 #[arg(long, default_value_t = 2)]
179 persistent_state_steps: usize,
180 #[arg(long, default_value_t = 10)]
181 density_window: usize,
182 #[arg(long, default_value_t = 0.2)]
183 ewma_alpha: f64,
184 #[arg(long, default_value_t = 3.0)]
185 ewma_sigma_multiplier: f64,
186 #[arg(long, default_value_t = 0.5)]
187 cusum_kappa_sigma_multiplier: f64,
188 #[arg(long, default_value_t = 5.0)]
189 cusum_alarm_sigma_multiplier: f64,
190 #[arg(long, default_value_t = 3.0)]
191 run_energy_sigma_multiplier: f64,
192 #[arg(long, default_value_t = 0.95)]
193 pca_variance_explained: f64,
194 #[arg(long, default_value_t = 3.0)]
195 pca_t2_sigma_multiplier: f64,
196 #[arg(long, default_value_t = 3.0)]
197 pca_spe_sigma_multiplier: f64,
198 #[arg(long, default_value_t = 3.0)]
199 drift_sigma_multiplier: f64,
200 #[arg(long, default_value_t = 3.0)]
201 slew_sigma_multiplier: f64,
202 #[arg(long, default_value_t = 10)]
203 grazing_window: usize,
204 #[arg(long, default_value_t = 3)]
205 grazing_min_hits: usize,
206 #[arg(long, default_value_t = 20)]
207 pre_failure_lookback_runs: usize,
208 #[arg(long, value_delimiter = ',', default_value = "5,10,15")]
209 dsa_window_grid: Vec<usize>,
210 #[arg(long, value_delimiter = ',', default_value = "2,3,4")]
211 dsa_persistence_runs_grid: Vec<usize>,
212 #[arg(long, value_delimiter = ',', default_value = "2.0,2.5,3.0")]
213 dsa_alert_tau_grid: Vec<f64>,
214 #[arg(long, value_delimiter = ',', default_value = "2,3,5")]
215 dsa_corroborating_feature_count_min_grid: Vec<usize>,
216}
217
218#[derive(Debug, Args)]
219struct ProbePhm2018Args {
220 #[arg(long)]
221 archive: Option<PathBuf>,
222 #[arg(long)]
223 data_root: Option<PathBuf>,
224}
225
226#[derive(Debug, Args)]
227struct RunPhm2018Args {
228 #[arg(long)]
229 data_root: Option<PathBuf>,
230 #[arg(long)]
231 output_root: Option<PathBuf>,
232 #[arg(long)]
233 secom_run_dir: Option<PathBuf>,
234}
235
236#[derive(Debug, Args)]
237struct SbirDemoArgs {
238 #[arg(long)]
240 data_root: Option<PathBuf>,
241 #[arg(long)]
243 output_root: Option<PathBuf>,
244 #[arg(long, default_value_t = true)]
246 fetch_if_missing: bool,
247}
248
249#[derive(Debug, Args)]
250struct PaperLockArgs {
251 #[arg(long)]
253 data_root: Option<PathBuf>,
254 #[arg(long)]
256 output_root: Option<PathBuf>,
257 #[arg(long, default_value_t = true)]
259 fetch_if_missing: bool,
260}
261
262#[derive(Debug, Args)]
263struct RenderNonIntrusiveArtifactsArgs {
264 #[arg(long)]
265 run_dir: PathBuf,
266}
267
268#[derive(Debug, Args)]
269struct RenderUnifiedValueFigureArgs {
270 #[arg(long)]
271 secom_run_dir: Option<PathBuf>,
272 #[arg(long)]
273 phm_run_dir: Option<PathBuf>,
274 #[arg(long)]
275 output_root: Option<PathBuf>,
276 #[arg(long)]
277 paper_tex: Option<PathBuf>,
278}
279
280pub fn run() -> Result<()> {
281 let cli = Cli::parse();
282 match cli.command {
283 Command::FetchSecom(args) => {
284 let data_root = args.data_root.unwrap_or_else(default_data_root);
285 let paths = secom::fetch_if_missing(&data_root)?;
286 println!("SECOM dataset ready at {}", paths.root.display());
287 println!("Archive: {}", paths.archive.display());
288 Ok(())
289 }
290 Command::RunSecom(args) => {
291 let data_root = args.data.data_root.unwrap_or_else(default_data_root);
292 let output_root = args.output_root.unwrap_or_else(default_output_root);
293 let config = PipelineConfig {
294 healthy_pass_runs: args.healthy_pass_runs,
295 drift_window: args.drift_window,
296 envelope_sigma: args.envelope_sigma,
297 boundary_fraction_of_rho: args.boundary_fraction_of_rho,
298 state_confirmation_steps: args.state_confirmation_steps,
299 persistent_state_steps: args.persistent_state_steps,
300 density_window: args.density_window,
301 ewma_alpha: args.ewma_alpha,
302 ewma_sigma_multiplier: args.ewma_sigma_multiplier,
303 cusum_kappa_sigma_multiplier: args.cusum_kappa_sigma_multiplier,
304 cusum_alarm_sigma_multiplier: args.cusum_alarm_sigma_multiplier,
305 run_energy_sigma_multiplier: args.run_energy_sigma_multiplier,
306 pca_variance_explained: args.pca_variance_explained,
307 pca_t2_sigma_multiplier: args.pca_t2_sigma_multiplier,
308 pca_spe_sigma_multiplier: args.pca_spe_sigma_multiplier,
309 drift_sigma_multiplier: args.drift_sigma_multiplier,
310 slew_sigma_multiplier: args.slew_sigma_multiplier,
311 grazing_window: args.grazing_window,
312 grazing_min_hits: args.grazing_min_hits,
313 pre_failure_lookback_runs: args.pre_failure_lookback_runs,
314 dsa: crate::precursor::DsaConfig {
315 window: args.dsa_window,
316 persistence_runs: args.dsa_persistence_runs,
317 alert_tau: args.dsa_alert_tau,
318 corroborating_feature_count_min: args.dsa_corroborating_feature_count_min,
319 },
320 ..PipelineConfig::default()
321 };
322 let artifacts = run_secom_benchmark(
323 &data_root,
324 Some(&output_root),
325 config,
326 args.fetch_if_missing,
327 )?;
328 println!("Run directory: {}", artifacts.run_dir.display());
329 println!("Metrics: {}", artifacts.metrics_path.display());
330 if let Some(pdf) = artifacts.report.pdf_path {
331 println!("PDF report: {}", pdf.display());
332 } else if let Some(error) = artifacts.report.pdf_error {
333 println!(
334 "PDF report failed: {}",
335 error.lines().next().unwrap_or("unknown error")
336 );
337 }
338 println!("ZIP bundle: {}", artifacts.zip_path.display());
339 Ok(())
340 }
341 Command::CalibrateSecom(args) => {
342 let data_root = args.data.data_root.unwrap_or_else(default_data_root);
343 let output_root = args.output_root.unwrap_or_else(default_output_root);
344 let grid = CalibrationGrid {
345 healthy_pass_runs: args.healthy_pass_runs_grid,
346 drift_window: args.drift_window_grid,
347 envelope_sigma: args.envelope_sigma_grid,
348 boundary_fraction_of_rho: args.boundary_fraction_of_rho_grid,
349 state_confirmation_steps: args.state_confirmation_steps_grid,
350 persistent_state_steps: args.persistent_state_steps_grid,
351 density_window: args.density_window_grid,
352 ewma_alpha: args.ewma_alpha_grid,
353 ewma_sigma_multiplier: args.ewma_sigma_multiplier_grid,
354 cusum_kappa_sigma_multiplier: args.cusum_kappa_sigma_multiplier_grid,
355 cusum_alarm_sigma_multiplier: args.cusum_alarm_sigma_multiplier_grid,
356 run_energy_sigma_multiplier: args.run_energy_sigma_multiplier_grid,
357 pca_variance_explained: args.pca_variance_explained_grid,
358 pca_t2_sigma_multiplier: args.pca_t2_sigma_multiplier_grid,
359 pca_spe_sigma_multiplier: args.pca_spe_sigma_multiplier_grid,
360 drift_sigma_multiplier: args.drift_sigma_multiplier_grid,
361 slew_sigma_multiplier: args.slew_sigma_multiplier_grid,
362 grazing_window: args.grazing_window_grid,
363 grazing_min_hits: args.grazing_min_hits_grid,
364 pre_failure_lookback_runs: args.pre_failure_lookback_runs_grid,
365 };
366 let artifacts =
367 run_secom_calibration(&data_root, Some(&output_root), grid, args.fetch_if_missing)?;
368 println!("Calibration run directory: {}", artifacts.run_dir.display());
369 println!(
370 "Calibration grid results: {}",
371 artifacts.grid_results_csv.display()
372 );
373 println!("Calibration summary: {}", artifacts.summary_json.display());
374 println!(
375 "Calibration report: {}",
376 artifacts.report_markdown.display()
377 );
378 Ok(())
379 }
380 Command::CalibrateSecomDsa(args) => {
381 let data_root = args.data.data_root.unwrap_or_else(default_data_root);
382 let output_root = args.output_root.unwrap_or_else(default_output_root);
383 let config = PipelineConfig {
384 healthy_pass_runs: args.healthy_pass_runs,
385 drift_window: args.drift_window,
386 envelope_sigma: args.envelope_sigma,
387 boundary_fraction_of_rho: args.boundary_fraction_of_rho,
388 state_confirmation_steps: args.state_confirmation_steps,
389 persistent_state_steps: args.persistent_state_steps,
390 density_window: args.density_window,
391 ewma_alpha: args.ewma_alpha,
392 ewma_sigma_multiplier: args.ewma_sigma_multiplier,
393 cusum_kappa_sigma_multiplier: args.cusum_kappa_sigma_multiplier,
394 cusum_alarm_sigma_multiplier: args.cusum_alarm_sigma_multiplier,
395 run_energy_sigma_multiplier: args.run_energy_sigma_multiplier,
396 pca_variance_explained: args.pca_variance_explained,
397 pca_t2_sigma_multiplier: args.pca_t2_sigma_multiplier,
398 pca_spe_sigma_multiplier: args.pca_spe_sigma_multiplier,
399 drift_sigma_multiplier: args.drift_sigma_multiplier,
400 slew_sigma_multiplier: args.slew_sigma_multiplier,
401 grazing_window: args.grazing_window,
402 grazing_min_hits: args.grazing_min_hits,
403 pre_failure_lookback_runs: args.pre_failure_lookback_runs,
404 ..PipelineConfig::default()
405 };
406 let artifacts = run_secom_dsa_calibration(
407 &data_root,
408 Some(&output_root),
409 config,
410 crate::precursor::DsaCalibrationGrid {
411 window: args.dsa_window_grid,
412 persistence_runs: args.dsa_persistence_runs_grid,
413 alert_tau: args.dsa_alert_tau_grid,
414 corroborating_feature_count_min: args.dsa_corroborating_feature_count_min_grid,
415 },
416 args.fetch_if_missing,
417 )?;
418 println!(
419 "DSA calibration run directory: {}",
420 artifacts.run_dir.display()
421 );
422 println!(
423 "DSA calibration grid: {}",
424 artifacts.grid_results_csv.display()
425 );
426 println!(
427 "DSA calibration summary: {}",
428 artifacts.summary_json.display()
429 );
430 println!(
431 "DSA calibration report: {}",
432 artifacts.report_markdown.display()
433 );
434 Ok(())
435 }
436 Command::ProbePhm2018(args) => {
437 let data_root = args.data_root.unwrap_or_else(default_data_root);
438 let status = phm2018::support_status(&data_root);
439 println!(
440 "PHM 2018 manual archive path: {}",
441 status.manual_placement_path.display()
442 );
443 println!("Official page: {}", status.official_page);
444 println!("Official link: {}", status.official_download_link);
445 println!(
446 "Archive summary support implemented: {}",
447 status.archive_summary_supported
448 );
449 println!("Implemented now: {}", status.fully_implemented);
450 println!("Blocker: {}", status.blocker);
451 let archive = args.archive.or_else(|| {
452 status
453 .manual_placement_path
454 .exists()
455 .then_some(status.manual_placement_path.clone())
456 });
457 if let Some(archive) = archive {
458 println!("Inspecting archive: {}", archive.display());
459 let manifest = phm2018::inspect_archive(&archive)?;
460 println!("{}", serde_json::to_string_pretty(&manifest)?);
461 }
462 Ok(())
463 }
464 Command::RunPhm2018(args) => {
465 let data_root = args.data_root.unwrap_or_else(default_data_root);
466 let output_root = args.output_root.unwrap_or_else(default_output_root);
467 let artifacts =
468 run_phm2018_benchmark(&data_root, &output_root, args.secom_run_dir.as_deref())?;
469 println!("Run directory: {}", artifacts.run_dir.display());
470 println!(
471 "PHM lead-time metrics: {}",
472 artifacts.lead_time_metrics_path.display()
473 );
474 println!(
475 "PHM early-warning stats: {}",
476 artifacts.early_warning_stats_path.display()
477 );
478 println!(
479 "Claim alignment report: {}",
480 artifacts.claim_alignment_report_path.display()
481 );
482 println!(
483 "Engineering report (tex): {}",
484 artifacts.tex_report_path.display()
485 );
486 if let Some(pdf) = &artifacts.pdf_path {
487 println!("Engineering report (pdf): {}", pdf.display());
488 }
489 println!("ZIP bundle: {}", artifacts.zip_path.display());
490 Ok(())
491 }
492 Command::RenderNonIntrusiveArtifacts(args) => {
493 let artifacts = materialize_non_intrusive_artifacts(&args.run_dir)?;
494 println!(
495 "Non-intrusive interface spec: {}",
496 artifacts.interface_spec_path.display()
497 );
498 println!(
499 "Non-intrusive architecture PNG: {}",
500 artifacts.architecture_png_path.display()
501 );
502 println!(
503 "Non-intrusive architecture SVG: {}",
504 artifacts.architecture_svg_path.display()
505 );
506 Ok(())
507 }
508 Command::RenderUnifiedValueFigure(args) => {
509 let output_root = args.output_root.unwrap_or_else(default_output_root);
510 let secom_root_candidates = [
511 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("output-dsfb-semiconductor"),
512 output_root.clone(),
513 ];
514 let secom_run_dir = match args.secom_run_dir {
515 Some(path) => path,
516 None => secom_root_candidates
517 .iter()
518 .find_map(|root| {
519 resolve_latest_completed_run(
520 root,
521 "_secom",
522 "dsa_operator_delta_targets.json",
523 )
524 })
525 .ok_or_else(|| {
526 crate::error::DsfbSemiconductorError::DatasetFormat(
527 "could not resolve a completed SECOM run directory".into(),
528 )
529 })?,
530 };
531 let phm_run_dir = match args.phm_run_dir {
532 Some(path) => Some(path),
533 None => {
534 let phm_root_candidates = [
535 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("output-dsfb-semiconductor"),
536 output_root.clone(),
537 ];
538 phm_root_candidates.iter().find_map(|root| {
539 resolve_latest_completed_run(
540 root,
541 "_phm2018",
542 "phm2018_early_warning_stats.json",
543 )
544 })
545 }
546 };
547 let paper_tex = args.paper_tex.or_else(|| {
548 Some(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("paper/semiconductor.tex"))
549 });
550 let artifacts = render_unified_value_figure(
551 &secom_run_dir,
552 phm_run_dir.as_deref(),
553 paper_tex.as_deref(),
554 )?;
555 println!("SECOM run: {}", artifacts.secom_run_dir.display());
556 if let Some(phm) = &artifacts.phm_run_dir {
557 println!("PHM run: {}", phm.display());
558 } else {
559 println!("PHM run: unavailable; Panel C rendered as placeholder");
560 }
561 println!("Unified figure: {}", artifacts.figure_path.display());
562 println!("Companion CSV: {}", artifacts.csv_path.display());
563 println!("PHM panel available: {}", artifacts.phm_panel_available);
564 println!("Paper updated: {}", artifacts.paper_updated);
565 Ok(())
566 }
567 Command::SbirDemo(args) => {
568 let data_root = args.data_root.unwrap_or_else(default_data_root);
569 let output_root = args.output_root.unwrap_or_else(default_output_root);
570
571 println!("[sbir-demo] Fetching SECOM dataset...");
573 let secom_paths = secom::fetch_if_missing(&data_root)?;
574 println!("[sbir-demo] SECOM ready: {}", secom_paths.root.display());
575
576 println!("[sbir-demo] Running SECOM benchmark...");
578 let secom_artifacts = run_secom_benchmark(
579 &data_root,
580 Some(&output_root),
581 PipelineConfig::default(),
582 args.fetch_if_missing,
583 )?;
584 println!(
585 "[sbir-demo] SECOM run dir: {}",
586 secom_artifacts.run_dir.display()
587 );
588 if let Some(pdf) = &secom_artifacts.report.pdf_path {
589 println!("[sbir-demo] SECOM PDF: {}", pdf.display());
590 }
591 println!(
592 "[sbir-demo] SECOM ZIP: {}",
593 secom_artifacts.zip_path.display()
594 );
595
596 println!("[sbir-demo] Running SECOM calibration...");
598 let cal_grid = CalibrationGrid {
599 healthy_pass_runs: vec![100],
600 drift_window: vec![5],
601 envelope_sigma: vec![3.0],
602 boundary_fraction_of_rho: vec![0.5],
603 state_confirmation_steps: vec![2],
604 persistent_state_steps: vec![2],
605 density_window: vec![10],
606 ewma_alpha: vec![0.2],
607 ewma_sigma_multiplier: vec![3.0],
608 cusum_kappa_sigma_multiplier: vec![0.5],
609 cusum_alarm_sigma_multiplier: vec![5.0],
610 run_energy_sigma_multiplier: vec![3.0],
611 pca_variance_explained: vec![0.95],
612 pca_t2_sigma_multiplier: vec![3.0],
613 pca_spe_sigma_multiplier: vec![3.0],
614 drift_sigma_multiplier: vec![3.0],
615 slew_sigma_multiplier: vec![3.0],
616 grazing_window: vec![10],
617 grazing_min_hits: vec![3],
618 pre_failure_lookback_runs: vec![20],
619 };
620 let cal_artifacts = run_secom_calibration(
621 &data_root,
622 Some(&output_root),
623 cal_grid,
624 args.fetch_if_missing,
625 )?;
626 println!(
627 "[sbir-demo] Calibration dir: {}",
628 cal_artifacts.run_dir.display()
629 );
630 if let Some(pdf) = &cal_artifacts.pdf_path {
631 println!("[sbir-demo] Calibration PDF: {}", pdf.display());
632 }
633 println!(
634 "[sbir-demo] Calibration ZIP: {}",
635 cal_artifacts.zip_path.display()
636 );
637
638 println!("[sbir-demo] Running DSA calibration...");
640 let dsa_artifacts = run_secom_dsa_calibration(
641 &data_root,
642 Some(&output_root),
643 PipelineConfig::default(),
644 crate::precursor::DsaCalibrationGrid {
645 window: vec![5],
646 persistence_runs: vec![2],
647 alert_tau: vec![2.0],
648 corroborating_feature_count_min: vec![2],
649 },
650 args.fetch_if_missing,
651 )?;
652 println!(
653 "[sbir-demo] DSA cal dir: {}",
654 dsa_artifacts.run_dir.display()
655 );
656 if let Some(pdf) = &dsa_artifacts.pdf_path {
657 println!("[sbir-demo] DSA cal PDF: {}", pdf.display());
658 }
659 println!(
660 "[sbir-demo] DSA cal ZIP: {}",
661 dsa_artifacts.zip_path.display()
662 );
663
664 let phm_status = phm2018::support_status(&data_root);
666 if phm_status.fully_implemented || phm_status.manual_placement_path.exists() {
667 println!("[sbir-demo] Running PHM 2018 benchmark...");
668 match run_phm2018_benchmark(
669 &data_root,
670 &output_root,
671 Some(&secom_artifacts.run_dir),
672 ) {
673 Ok(phm_artifacts) => {
674 println!(
675 "[sbir-demo] PHM run dir: {}",
676 phm_artifacts.run_dir.display()
677 );
678 println!(
679 "[sbir-demo] PHM tex report: {}",
680 phm_artifacts.tex_report_path.display()
681 );
682 if let Some(pdf) = &phm_artifacts.pdf_path {
683 println!("[sbir-demo] PHM PDF: {}", pdf.display());
684 }
685 println!(
686 "[sbir-demo] PHM ZIP: {}",
687 phm_artifacts.zip_path.display()
688 );
689 }
690 Err(e) => {
691 eprintln!("[sbir-demo] PHM 2018 run failed (skipping): {e}");
692 }
693 }
694 } else {
695 println!(
696 "[sbir-demo] PHM 2018 dataset not found. Checked for:"
697 );
698 println!(
699 "[sbir-demo] extracted dir: {}",
700 phm_status.extracted_dataset_path.display()
701 );
702 println!(
703 "[sbir-demo] archive: {}",
704 phm_status.manual_placement_path.display()
705 );
706 println!("[sbir-demo] Place either and re-run sbir-demo.");
707 }
708
709 println!("[sbir-demo] All artifacts generated.");
710 Ok(())
711 }
712 Command::PaperLock(args) => {
713 let data_root = args.data_root.unwrap_or_else(default_data_root);
714 let output_root = args.output_root.unwrap_or_else(default_output_root);
715
716 println!("[paper-lock] Running SECOM benchmark with fixed paper-lock config...");
717 let artifacts = run_secom_benchmark(
718 &data_root,
719 Some(&output_root),
720 PipelineConfig::default(),
721 args.fetch_if_missing,
722 )?;
723
724 let PaperLockMetrics {
725 episode_count,
726 precision,
727 detected_failures,
728 total_failures,
729 } = artifacts.paper_lock_metrics;
730
731 const EXPECTED_EPISODES: usize = 71;
732 const EXPECTED_MIN_PRECISION: f64 = 0.80;
733 const EXPECTED_RECALL: usize = 104;
734
735 let episode_ok = episode_count == EXPECTED_EPISODES;
736 let precision_ok = precision >= EXPECTED_MIN_PRECISION;
737 let recall_ok = detected_failures == EXPECTED_RECALL
738 && total_failures == EXPECTED_RECALL;
739
740 println!("[paper-lock] episode count : {episode_count:>4} (expected {EXPECTED_EPISODES}) {}",
741 if episode_ok { "OK" } else { "FAIL" });
742 println!("[paper-lock] precision : {:>7.1}% (expected >= {:.0}%) {}",
743 precision * 100.0,
744 EXPECTED_MIN_PRECISION * 100.0,
745 if precision_ok { "OK" } else { "FAIL" });
746 println!("[paper-lock] recall : {detected_failures}/{total_failures} (expected {EXPECTED_RECALL}/{EXPECTED_RECALL}) {}",
747 if recall_ok { "OK" } else { "FAIL" });
748 println!("[paper-lock] run dir : {}", artifacts.run_dir.display());
749
750 if episode_ok && precision_ok && recall_ok {
751 println!("[paper-lock] PASS — headline numbers reproduced.");
752 Ok(())
753 } else {
754 eprintln!("[paper-lock] FAIL — one or more headline numbers did not match.");
755 std::process::exit(1);
756 }
757 }
758 }
759}