Skip to main content

dsfb_tmtr/
output.rs

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(&timestamp);
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}