1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use clap::{Parser, ValueEnum};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
9#[serde(rename_all = "snake_case")]
10pub enum ScenarioSelection {
11 All,
12 DisturbanceRecovery,
13 ForwardPrediction,
14 HierarchyConsistency,
15 AerospaceNavigation,
16 RoboticsSensorOcclusion,
17 IndustrialFaultRefinement,
18 NeuralMultimodalDelay,
19}
20
21impl Default for ScenarioSelection {
22 fn default() -> Self {
23 Self::All
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
28#[serde(rename_all = "snake_case")]
29pub enum KernelKind {
30 Uniform,
31 Exponential,
32 ResonanceGated,
33}
34
35impl Default for KernelKind {
36 fn default() -> Self {
37 Self::ResonanceGated
38 }
39}
40
41#[derive(Debug, Clone, Parser)]
42pub struct Cli {
43 #[arg(long)]
44 pub config: Option<PathBuf>,
45 #[arg(long, value_enum)]
46 pub scenario: Option<ScenarioSelection>,
47 #[arg(long)]
48 pub output_root: Option<PathBuf>,
49 #[arg(long)]
50 pub n_steps: Option<usize>,
51 #[arg(long)]
52 pub delta: Option<usize>,
53 #[arg(long)]
54 pub prediction_horizon: Option<usize>,
55 #[arg(long)]
56 pub max_iterations: Option<usize>,
57 #[arg(long)]
58 pub max_recursion_depth: Option<usize>,
59 #[arg(long)]
60 pub convergence_tolerance: Option<f64>,
61 #[arg(long)]
62 pub trust_threshold: Option<f64>,
63 #[arg(long)]
64 pub min_trust_gap: Option<f64>,
65 #[arg(long, value_enum)]
66 pub kernel: Option<KernelKind>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct SimulationConfig {
71 pub scenario: ScenarioSelection,
72 pub output_root: String,
73 pub n_steps: usize,
74 pub delta: usize,
75 pub prediction_horizon: usize,
76 pub max_iterations: usize,
77 pub max_recursion_depth: usize,
78 pub convergence_tolerance: f64,
79 pub trust_threshold: f64,
80 pub min_trust_gap: f64,
81 pub kernel: KernelKind,
82}
83
84impl Default for SimulationConfig {
85 fn default() -> Self {
86 Self {
87 scenario: ScenarioSelection::All,
88 output_root: default_output_root().display().to_string(),
89 n_steps: 1000,
90 delta: 48,
91 prediction_horizon: 32,
92 max_iterations: 6,
93 max_recursion_depth: 2,
94 convergence_tolerance: 1e-3,
95 trust_threshold: 0.62,
96 min_trust_gap: 0.04,
97 kernel: KernelKind::ResonanceGated,
98 }
99 }
100}
101
102impl SimulationConfig {
103 pub fn from_cli(cli: Cli) -> Result<Self> {
104 let mut config = if let Some(path) = cli.config.as_ref() {
105 Self::from_json_file(path)?
106 } else {
107 Self::default()
108 };
109
110 if let Some(scenario) = cli.scenario {
111 config.scenario = scenario;
112 }
113 if let Some(path) = cli.output_root {
114 config.output_root = path.display().to_string();
115 }
116 if let Some(n_steps) = cli.n_steps {
117 config.n_steps = n_steps.max(64);
118 }
119 if let Some(delta) = cli.delta {
120 config.delta = delta.max(4);
121 }
122 if let Some(prediction_horizon) = cli.prediction_horizon {
123 config.prediction_horizon = prediction_horizon.max(4);
124 }
125 if let Some(max_iterations) = cli.max_iterations {
126 config.max_iterations = max_iterations.max(1);
127 }
128 if let Some(max_recursion_depth) = cli.max_recursion_depth {
129 config.max_recursion_depth = max_recursion_depth.max(1);
130 }
131 if let Some(convergence_tolerance) = cli.convergence_tolerance {
132 config.convergence_tolerance = convergence_tolerance.max(1e-9);
133 }
134 if let Some(trust_threshold) = cli.trust_threshold {
135 config.trust_threshold = trust_threshold.clamp(0.0, 1.0);
136 }
137 if let Some(min_trust_gap) = cli.min_trust_gap {
138 config.min_trust_gap = min_trust_gap.max(0.0);
139 }
140 if let Some(kernel) = cli.kernel {
141 config.kernel = kernel;
142 }
143 config.prediction_horizon = config
144 .prediction_horizon
145 .min(config.n_steps.saturating_sub(1));
146 config.delta = config.delta.min(config.n_steps.saturating_sub(2));
147 Ok(config)
148 }
149
150 pub fn from_json_file(path: &Path) -> Result<Self> {
151 let raw = fs::read_to_string(path)
152 .with_context(|| format!("failed to read config file {}", path.display()))?;
153 let mut config: Self = serde_json::from_str(&raw)
154 .with_context(|| format!("failed to parse config file {}", path.display()))?;
155 if config.output_root.is_empty() {
156 config.output_root = default_output_root().display().to_string();
157 }
158 Ok(config)
159 }
160
161 pub fn output_root_path(&self) -> PathBuf {
162 PathBuf::from(&self.output_root)
163 }
164
165 pub fn stable_hash(&self) -> Result<String> {
166 let json = serde_json::to_string(self).context("failed to serialize config for hashing")?;
167 Ok(stable_hash_bytes(json.as_bytes()))
168 }
169}
170
171pub fn default_output_root() -> PathBuf {
172 Path::new(env!("CARGO_MANIFEST_DIR"))
173 .join("..")
174 .join("..")
175 .join("output-dsfb-tmtr")
176}
177
178pub fn stable_hash_bytes(bytes: &[u8]) -> String {
179 let mut hash: u64 = 0xcbf29ce484222325;
180 for byte in bytes {
181 hash ^= u64::from(*byte);
182 hash = hash.wrapping_mul(0x100000001b3);
183 }
184 format!("{hash:016x}")
185}