1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::graph::EventId;
5use anyhow::{ensure, Context, Result};
6use chrono::Local;
7use dsfb::DsfbParams;
8
9#[derive(Debug, Clone)]
10pub struct DscdSweepConfig {
11 pub num_events: usize,
12 pub tau_min: f64,
13 pub tau_max: f64,
14 pub tau_steps: usize,
15 pub max_depth: Option<usize>,
16 pub dsfb_params: DsfbParams,
17}
18
19impl Default for DscdSweepConfig {
20 fn default() -> Self {
21 Self {
22 num_events: 1_024,
23 tau_min: 0.0,
24 tau_max: 1.0,
25 tau_steps: 101,
26 max_depth: None,
27 dsfb_params: DsfbParams::default(),
28 }
29 }
30}
31
32impl DscdSweepConfig {
33 pub fn validate(&self) -> Result<()> {
34 ensure!(self.num_events > 0, "num_events must be greater than zero");
35 ensure!(self.tau_steps > 0, "tau_steps must be greater than zero");
36 ensure!(self.tau_min.is_finite(), "tau_min must be finite");
37 ensure!(self.tau_max.is_finite(), "tau_max must be finite");
38 ensure!(
39 self.tau_max >= self.tau_min,
40 "tau_max must be greater than or equal to tau_min"
41 );
42 Ok(())
43 }
44
45 pub fn tau_grid(&self) -> Vec<f64> {
46 if self.tau_steps == 1 {
47 return vec![self.tau_min];
48 }
49
50 let span = self.tau_max - self.tau_min;
51 let denom = (self.tau_steps - 1) as f64;
52 (0..self.tau_steps)
53 .map(|idx| self.tau_min + span * idx as f64 / denom)
54 .collect()
55 }
56}
57
58#[derive(Debug, Clone)]
65pub struct DscdScalingConfig {
66 pub event_counts: Vec<usize>,
67 pub tau_grid: Vec<f64>,
68 pub initial_event: EventId,
69 pub max_path_length: usize,
70 pub critical_fraction: f64,
71 pub dsfb_params: DsfbParams,
72}
73
74impl Default for DscdScalingConfig {
75 fn default() -> Self {
76 Self {
77 event_counts: vec![2_048, 4_096, 8_192, 16_384, 32_768],
78 tau_grid: (0..=200).map(|idx| idx as f64 / 200.0).collect(),
79 initial_event: EventId(0),
80 max_path_length: usize::MAX,
81 critical_fraction: 0.5,
82 dsfb_params: DsfbParams::default(),
83 }
84 }
85}
86
87impl DscdScalingConfig {
88 pub fn validate(&self) -> Result<()> {
89 ensure!(
90 !self.event_counts.is_empty(),
91 "event_counts must contain at least one N"
92 );
93 ensure!(
94 self.event_counts.iter().all(|&n| n > 0),
95 "event_counts values must be greater than zero"
96 );
97 ensure!(
98 !self.tau_grid.is_empty(),
99 "tau_grid must contain at least one threshold"
100 );
101 ensure!(
102 self.tau_grid.iter().all(|tau| tau.is_finite()),
103 "tau_grid must contain finite thresholds"
104 );
105 ensure!(
106 self.tau_grid.windows(2).all(|pair| pair[1] >= pair[0]),
107 "tau_grid must be sorted in nondecreasing order"
108 );
109 ensure!(
110 (0.0..=1.0).contains(&self.critical_fraction),
111 "critical_fraction must be in [0, 1]"
112 );
113 Ok(())
114 }
115}
116
117#[derive(Debug, Clone)]
118pub struct OutputPaths {
119 pub root: PathBuf,
120 pub run_dir: PathBuf,
121}
122
123pub fn workspace_root_dir() -> PathBuf {
124 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
125 manifest_dir
126 .parent()
127 .and_then(Path::parent)
128 .map(Path::to_path_buf)
129 .unwrap_or(manifest_dir)
130}
131
132pub fn create_timestamped_output_dir() -> Result<OutputPaths> {
133 let root = workspace_root_dir().join("output-dsfb-dscd");
134 create_timestamped_output_dir_in(&root)
135}
136
137pub fn create_timestamped_output_dir_in(root: &Path) -> Result<OutputPaths> {
138 fs::create_dir_all(root)
139 .with_context(|| format!("failed to create output root {}", root.display()))?;
140
141 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
142 let mut run_dir = root.join(×tamp);
143 let mut suffix = 1_u32;
144
145 while run_dir.exists() {
146 run_dir = root.join(format!("{timestamp}_{suffix:02}"));
147 suffix += 1;
148 }
149
150 fs::create_dir_all(&run_dir)
151 .with_context(|| format!("failed to create run directory {}", run_dir.display()))?;
152
153 Ok(OutputPaths {
154 root: root.to_path_buf(),
155 run_dir,
156 })
157}