Skip to main content

dsfb_dscd/
config.rs

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/// Configuration for deterministic finite-size scaling of the DSCD trust
59/// threshold transition.
60///
61/// This configuration drives reproducible scaling sweeps over event counts and
62/// trust thresholds with no random sampling, supporting the finite-size
63/// threshold-sharpening analysis described in Theorem 4 of the DSCD paper.
64#[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(&timestamp);
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}