1use std::fs::{self, File};
2use std::path::{Path, PathBuf};
3use std::thread;
4use std::time::Duration;
5
6use anyhow::{Context, Result};
7use chrono::Local;
8use csv::Writer;
9use serde::Serialize;
10use serde_json::json;
11
12use crate::simulation::SimulationRun;
13
14#[derive(Debug, Clone)]
15pub struct RunDirectory {
16 pub output_root: PathBuf,
17 pub timestamp: String,
18 pub run_dir: PathBuf,
19}
20
21pub fn create_run_directory(output_root: &Path) -> Result<RunDirectory> {
22 fs::create_dir_all(output_root)
23 .with_context(|| format!("failed to create output root {}", output_root.display()))?;
24 loop {
25 let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
26 let run_dir = output_root.join(×tamp);
27 if !run_dir.exists() {
28 fs::create_dir_all(&run_dir)
29 .with_context(|| format!("failed to create run directory {}", run_dir.display()))?;
30 return Ok(RunDirectory {
31 output_root: output_root.to_path_buf(),
32 timestamp,
33 run_dir,
34 });
35 }
36 thread::sleep(Duration::from_millis(1100));
37 }
38}
39
40pub fn write_run_outputs(run: &SimulationRun, run_dir: &RunDirectory) -> Result<()> {
41 write_json(run_dir.run_dir.join("config.json"), &run.config)?;
42 let scenario_names = run
43 .scenarios
44 .iter()
45 .map(|scenario| scenario.definition.name.clone())
46 .collect::<Vec<_>>();
47 write_json(
48 run_dir.run_dir.join("run_manifest.json"),
49 &json!({
50 "crate": "dsfb-tmtr",
51 "version": env!("CARGO_PKG_VERSION"),
52 "timestamp": run_dir.timestamp,
53 "output_root": run_dir.output_root,
54 "run_dir": run_dir.run_dir,
55 "config_hash": run.config_hash,
56 "scenario_count": run.scenarios.len(),
57 "scenarios": scenario_names,
58 "artifacts": [
59 "config.json",
60 "run_manifest.json",
61 "scenario_summary.csv",
62 "trajectories.csv",
63 "trust_timeseries.csv",
64 "residuals.csv",
65 "correction_events.csv",
66 "prediction_tubes.csv",
67 "causal_edges.csv",
68 "causal_metrics.csv",
69 "notebook_ready_summary.json"
70 ]
71 }),
72 )?;
73
74 write_scenario_summary_csv(run, &run_dir.run_dir.join("scenario_summary.csv"))?;
75 write_trajectories_csv(run, &run_dir.run_dir.join("trajectories.csv"))?;
76 write_trust_csv(run, &run_dir.run_dir.join("trust_timeseries.csv"))?;
77 write_residuals_csv(run, &run_dir.run_dir.join("residuals.csv"))?;
78 write_corrections_csv(run, &run_dir.run_dir.join("correction_events.csv"))?;
79 write_prediction_tubes_csv(run, &run_dir.run_dir.join("prediction_tubes.csv"))?;
80 write_causal_edges_csv(run, &run_dir.run_dir.join("causal_edges.csv"))?;
81 write_causal_metrics_csv(run, &run_dir.run_dir.join("causal_metrics.csv"))?;
82 write_json(
83 run_dir.run_dir.join("notebook_ready_summary.json"),
84 &run.notebook_summary,
85 )?;
86 Ok(())
87}
88
89fn write_json(path: PathBuf, value: &impl Serialize) -> Result<()> {
90 let file =
91 File::create(&path).with_context(|| format!("failed to create {}", path.display()))?;
92 serde_json::to_writer_pretty(file, value)
93 .with_context(|| format!("failed to write {}", path.display()))
94}
95
96fn csv_writer(path: &Path) -> Result<Writer<File>> {
97 let file =
98 File::create(path).with_context(|| format!("failed to create {}", path.display()))?;
99 Ok(Writer::from_writer(file))
100}
101
102fn write_scenario_summary_csv(run: &SimulationRun, path: &Path) -> Result<()> {
103 let mut writer = csv_writer(path)?;
104 for scenario in &run.scenarios {
105 writer.serialize(&scenario.summary)?;
106 }
107 writer.flush()?;
108 Ok(())
109}
110
111fn write_trajectories_csv(run: &SimulationRun, path: &Path) -> Result<()> {
112 #[derive(Serialize)]
113 struct Row<'a> {
114 scenario: &'a str,
115 mode: &'a str,
116 observer_level: usize,
117 observer_name: &'a str,
118 time_index: usize,
119 ground_truth: f64,
120 prediction: f64,
121 estimate: f64,
122 measurement: Option<f64>,
123 available: bool,
124 degraded_interval: bool,
125 refinement_interval: bool,
126 }
127 let mut writer = csv_writer(path)?;
128 for scenario in &run.scenarios {
129 for mode in [&scenario.baseline, &scenario.tmtr] {
130 for observer in &mode.observers {
131 for time_index in 0..observer.estimate.len() {
132 writer.serialize(Row {
133 scenario: &scenario.definition.name,
134 mode: &mode.mode,
135 observer_level: observer.level,
136 observer_name: &observer.name,
137 time_index,
138 ground_truth: scenario.truth[time_index],
139 prediction: observer.prediction[time_index],
140 estimate: observer.estimate[time_index],
141 measurement: observer.measurement[time_index],
142 available: observer.available[time_index],
143 degraded_interval: (scenario.definition.degraded_start
144 ..=scenario.definition.degraded_end)
145 .contains(&time_index),
146 refinement_interval: (scenario.definition.degraded_start
147 ..=scenario.definition.refinement_end)
148 .contains(&time_index),
149 })?;
150 }
151 }
152 }
153 }
154 writer.flush()?;
155 Ok(())
156}
157
158fn write_trust_csv(run: &SimulationRun, path: &Path) -> Result<()> {
159 #[derive(Serialize)]
160 struct Row<'a> {
161 scenario: &'a str,
162 mode: &'a str,
163 observer_level: usize,
164 observer_name: &'a str,
165 time_index: usize,
166 trust: f64,
167 envelope: f64,
168 }
169 let mut writer = csv_writer(path)?;
170 for scenario in &run.scenarios {
171 for mode in [&scenario.baseline, &scenario.tmtr] {
172 for observer in &mode.observers {
173 for time_index in 0..observer.trust.len() {
174 writer.serialize(Row {
175 scenario: &scenario.definition.name,
176 mode: &mode.mode,
177 observer_level: observer.level,
178 observer_name: &observer.name,
179 time_index,
180 trust: observer.trust[time_index],
181 envelope: observer.envelope[time_index],
182 })?;
183 }
184 }
185 }
186 }
187 writer.flush()?;
188 Ok(())
189}
190
191fn write_residuals_csv(run: &SimulationRun, path: &Path) -> Result<()> {
192 #[derive(Serialize)]
193 struct Row<'a> {
194 scenario: &'a str,
195 mode: &'a str,
196 observer_level: usize,
197 observer_name: &'a str,
198 time_index: usize,
199 residual: f64,
200 abs_residual: f64,
201 innovation: f64,
202 }
203 let mut writer = csv_writer(path)?;
204 for scenario in &run.scenarios {
205 for mode in [&scenario.baseline, &scenario.tmtr] {
206 for observer in &mode.observers {
207 for time_index in 0..observer.residual.len() {
208 writer.serialize(Row {
209 scenario: &scenario.definition.name,
210 mode: &mode.mode,
211 observer_level: observer.level,
212 observer_name: &observer.name,
213 time_index,
214 residual: observer.residual[time_index],
215 abs_residual: observer.residual[time_index].abs(),
216 innovation: observer.innovation[time_index],
217 })?;
218 }
219 }
220 }
221 }
222 writer.flush()?;
223 Ok(())
224}
225
226fn write_corrections_csv(run: &SimulationRun, path: &Path) -> Result<()> {
227 let mut writer = csv_writer(path)?;
228 for scenario in &run.scenarios {
229 for event in &scenario.tmtr.correction_events {
230 writer.serialize(event)?;
231 }
232 }
233 writer.flush()?;
234 Ok(())
235}
236
237fn write_prediction_tubes_csv(run: &SimulationRun, path: &Path) -> Result<()> {
238 let mut writer = csv_writer(path)?;
239 for scenario in &run.scenarios {
240 for tube in scenario
241 .baseline
242 .prediction_tubes
243 .iter()
244 .chain(scenario.tmtr.prediction_tubes.iter())
245 {
246 writer.serialize(tube)?;
247 }
248 }
249 writer.flush()?;
250 Ok(())
251}
252
253fn write_causal_edges_csv(run: &SimulationRun, path: &Path) -> Result<()> {
254 let mut writer = csv_writer(path)?;
255 for scenario in &run.scenarios {
256 for edge in scenario
257 .baseline
258 .causal_graph
259 .edges
260 .iter()
261 .chain(scenario.tmtr.causal_graph.edges.iter())
262 {
263 writer.serialize(edge)?;
264 }
265 }
266 writer.flush()?;
267 Ok(())
268}
269
270fn write_causal_metrics_csv(run: &SimulationRun, path: &Path) -> Result<()> {
271 #[derive(Serialize)]
272 struct Row<'a> {
273 scenario: &'a str,
274 mode: &'a str,
275 metric: &'a str,
276 value: f64,
277 }
278 let mut writer = csv_writer(path)?;
279 for scenario in &run.scenarios {
280 for (mode, metrics) in [
281 ("baseline", &scenario.baseline.causal_metrics),
282 ("tmtr", &scenario.tmtr.causal_metrics),
283 ] {
284 let rows = [
285 ("edge_count", metrics.edge_count as f64),
286 ("backward_edge_count", metrics.backward_edge_count as f64),
287 ("cycle_count", metrics.cycle_count as f64),
288 (
289 "reachable_nodes_from_anchor",
290 metrics.reachable_nodes_from_anchor as f64,
291 ),
292 (
293 "local_window_edge_density",
294 metrics.local_window_edge_density,
295 ),
296 ("max_in_degree", metrics.max_in_degree as f64),
297 ("max_out_degree", metrics.max_out_degree as f64),
298 ("max_path_length", metrics.max_path_length as f64),
299 ("mean_path_length", metrics.mean_path_length),
300 ];
301 for (metric, value) in rows {
302 writer.serialize(Row {
303 scenario: &scenario.definition.name,
304 mode,
305 metric,
306 value,
307 })?;
308 }
309 }
310 }
311 writer.flush()?;
312 Ok(())
313}