Skip to main content

dsfb_srd/
export.rs

1use std::error::Error;
2use std::fs::{self, File};
3use std::io::{BufWriter, Write};
4use std::path::{Path, PathBuf};
5
6use chrono::{Duration, Utc};
7
8use crate::event::StructuralEvent;
9
10pub type DynError = Box<dyn Error>;
11
12#[derive(Clone, Debug)]
13pub struct RunManifestRow {
14    pub run_id: String,
15    pub timestamp: String,
16    pub config_hash: String,
17    pub crate_name: String,
18    pub crate_version: String,
19    pub n_events: usize,
20    pub n_channels: usize,
21    pub causal_window: usize,
22    pub tau_steps: usize,
23    pub shock_start: usize,
24    pub shock_end: usize,
25    pub beta: f64,
26    pub envelope_decay: f64,
27}
28
29#[derive(Clone, Debug)]
30pub struct ThresholdSweepRow {
31    pub run_id: String,
32    pub n_events: usize,
33    pub tau_threshold: f64,
34    pub reachable_count: usize,
35    pub reachable_fraction: f64,
36    pub edge_count: usize,
37    pub mean_out_degree: f64,
38    pub largest_component_fraction: f64,
39    pub component_entropy: f64,
40}
41
42#[derive(Clone, Debug)]
43pub struct TransitionSharpnessRow {
44    pub run_id: String,
45    pub n_events: usize,
46    pub tau_midpoint: f64,
47    pub drho_dtau: f64,
48    pub abs_drho_dtau: f64,
49}
50
51#[derive(Clone, Debug)]
52pub struct TimeLocalMetricsRow {
53    pub run_id: String,
54    pub tau_threshold: f64,
55    pub window_start: usize,
56    pub window_end: usize,
57    pub anchor_event: usize,
58    pub reachable_fraction: f64,
59    pub active_edge_count: usize,
60    pub mean_out_degree: f64,
61    pub regime_label: String,
62}
63
64#[derive(Clone, Debug)]
65pub struct GraphSnapshotRow {
66    pub run_id: String,
67    pub tau_threshold: f64,
68    pub src: usize,
69    pub dst: usize,
70    pub src_trust: f64,
71    pub dst_trust: f64,
72    pub compatible: bool,
73}
74
75#[derive(Clone, Debug)]
76pub struct ExportBundle {
77    pub manifest: RunManifestRow,
78    pub events: Vec<StructuralEvent>,
79    pub threshold_sweep: Vec<ThresholdSweepRow>,
80    pub transition_sharpness: Vec<TransitionSharpnessRow>,
81    pub time_local_metrics: Vec<TimeLocalMetricsRow>,
82    pub graph_snapshot_low: Vec<GraphSnapshotRow>,
83    pub graph_snapshot_critical: Vec<GraphSnapshotRow>,
84    pub graph_snapshot_high: Vec<GraphSnapshotRow>,
85    pub graph_snapshot_tau_020: Vec<GraphSnapshotRow>,
86    pub graph_snapshot_tau_030: Vec<GraphSnapshotRow>,
87    pub graph_snapshot_tau_040: Vec<GraphSnapshotRow>,
88}
89
90#[derive(Clone, Debug)]
91pub struct ExportOutcome {
92    pub timestamp: String,
93    pub run_dir: PathBuf,
94}
95
96pub fn prepare_output_dir(repo_root: &Path) -> Result<ExportOutcome, DynError> {
97    let output_root = repo_root.join("output-dsfb-srd");
98    fs::create_dir_all(&output_root)?;
99
100    let base = Utc::now();
101    for seconds in 0..120 {
102        let timestamp = (base + Duration::seconds(seconds))
103            .format("%Y%m%d-%H%M%S")
104            .to_string();
105        let run_dir = output_root.join(&timestamp);
106        if !run_dir.exists() {
107            fs::create_dir_all(&run_dir)?;
108            return Ok(ExportOutcome { timestamp, run_dir });
109        }
110    }
111
112    Err("unable to allocate a unique timestamped output directory".into())
113}
114
115pub fn write_bundle(bundle: &ExportBundle, run_dir: &Path) -> Result<(), DynError> {
116    write_run_manifest(&run_dir.join("run_manifest.csv"), &bundle.manifest)?;
117    write_events(
118        &run_dir.join("events.csv"),
119        &bundle.manifest.run_id,
120        &bundle.events,
121    )?;
122    write_threshold_sweep(
123        &run_dir.join("threshold_sweep.csv"),
124        &bundle.threshold_sweep,
125    )?;
126    write_transition_sharpness(
127        &run_dir.join("transition_sharpness.csv"),
128        &bundle.transition_sharpness,
129    )?;
130    write_time_local_metrics(
131        &run_dir.join("time_local_metrics.csv"),
132        &bundle.time_local_metrics,
133    )?;
134    write_graph_snapshot(
135        &run_dir.join("graph_snapshot_low.csv"),
136        &bundle.graph_snapshot_low,
137    )?;
138    write_graph_snapshot(
139        &run_dir.join("graph_snapshot_critical.csv"),
140        &bundle.graph_snapshot_critical,
141    )?;
142    write_graph_snapshot(
143        &run_dir.join("graph_snapshot_high.csv"),
144        &bundle.graph_snapshot_high,
145    )?;
146    write_graph_snapshot(
147        &run_dir.join("graph_snapshot_tau_020.csv"),
148        &bundle.graph_snapshot_tau_020,
149    )?;
150    write_graph_snapshot(
151        &run_dir.join("graph_snapshot_tau_030.csv"),
152        &bundle.graph_snapshot_tau_030,
153    )?;
154    write_graph_snapshot(
155        &run_dir.join("graph_snapshot_tau_040.csv"),
156        &bundle.graph_snapshot_tau_040,
157    )?;
158
159    Ok(())
160}
161
162fn write_run_manifest(path: &Path, row: &RunManifestRow) -> Result<(), DynError> {
163    let line = format!(
164        "{},{},{},{},{},{},{},{},{},{},{},{},{}",
165        row.run_id,
166        row.timestamp,
167        row.config_hash,
168        row.crate_name,
169        row.crate_version,
170        row.n_events,
171        row.n_channels,
172        row.causal_window,
173        row.tau_steps,
174        row.shock_start,
175        row.shock_end,
176        fmt_f64(row.beta),
177        fmt_f64(row.envelope_decay),
178    );
179
180    write_lines(
181        path,
182        "run_id,timestamp,config_hash,crate_name,crate_version,n_events,n_channels,causal_window,tau_steps,shock_start,shock_end,beta,envelope_decay",
183        std::iter::once(line),
184    )
185}
186
187fn write_events(path: &Path, run_id: &str, events: &[StructuralEvent]) -> Result<(), DynError> {
188    let lines = events.iter().map(|event| {
189        format!(
190            "{},{},{},{},{},{},{},{},{},{},{}",
191            run_id,
192            event.event_id,
193            event.time_index,
194            event.channel_id,
195            fmt_f64(event.latent_state),
196            fmt_f64(event.predicted_value),
197            fmt_f64(event.observed_value),
198            fmt_f64(event.residual),
199            fmt_f64(event.envelope),
200            fmt_f64(event.trust),
201            event.regime_label.as_str(),
202        )
203    });
204
205    write_lines(
206        path,
207        "run_id,event_id,time_index,channel_id,latent_state,predicted_value,observed_value,residual,envelope,trust,regime_label",
208        lines,
209    )
210}
211
212fn write_threshold_sweep(path: &Path, rows: &[ThresholdSweepRow]) -> Result<(), DynError> {
213    let lines = rows.iter().map(|row| {
214        format!(
215            "{},{},{},{},{},{},{},{},{}",
216            row.run_id,
217            row.n_events,
218            fmt_f64(row.tau_threshold),
219            row.reachable_count,
220            fmt_f64(row.reachable_fraction),
221            row.edge_count,
222            fmt_f64(row.mean_out_degree),
223            fmt_f64(row.largest_component_fraction),
224            fmt_f64(row.component_entropy),
225        )
226    });
227
228    write_lines(
229        path,
230        "run_id,n_events,tau_threshold,reachable_count,reachable_fraction,edge_count,mean_out_degree,largest_component_fraction,component_entropy",
231        lines,
232    )
233}
234
235fn write_transition_sharpness(
236    path: &Path,
237    rows: &[TransitionSharpnessRow],
238) -> Result<(), DynError> {
239    let lines = rows.iter().map(|row| {
240        format!(
241            "{},{},{},{},{}",
242            row.run_id,
243            row.n_events,
244            fmt_f64(row.tau_midpoint),
245            fmt_f64(row.drho_dtau),
246            fmt_f64(row.abs_drho_dtau),
247        )
248    });
249
250    write_lines(
251        path,
252        "run_id,n_events,tau_midpoint,drho_dtau,abs_drho_dtau",
253        lines,
254    )
255}
256
257fn write_time_local_metrics(path: &Path, rows: &[TimeLocalMetricsRow]) -> Result<(), DynError> {
258    let lines = rows.iter().map(|row| {
259        format!(
260            "{},{},{},{},{},{},{},{},{}",
261            row.run_id,
262            fmt_f64(row.tau_threshold),
263            row.window_start,
264            row.window_end,
265            row.anchor_event,
266            fmt_f64(row.reachable_fraction),
267            row.active_edge_count,
268            fmt_f64(row.mean_out_degree),
269            row.regime_label,
270        )
271    });
272
273    write_lines(
274        path,
275        "run_id,tau_threshold,window_start,window_end,anchor_event,reachable_fraction,active_edge_count,mean_out_degree,regime_label",
276        lines,
277    )
278}
279
280fn write_graph_snapshot(path: &Path, rows: &[GraphSnapshotRow]) -> Result<(), DynError> {
281    let lines = rows.iter().map(|row| {
282        format!(
283            "{},{},{},{},{},{},{}",
284            row.run_id,
285            fmt_f64(row.tau_threshold),
286            row.src,
287            row.dst,
288            fmt_f64(row.src_trust),
289            fmt_f64(row.dst_trust),
290            row.compatible,
291        )
292    });
293
294    write_lines(
295        path,
296        "run_id,tau_threshold,src,dst,src_trust,dst_trust,compatible",
297        lines,
298    )
299}
300
301fn write_lines<I>(path: &Path, header: &str, lines: I) -> Result<(), DynError>
302where
303    I: IntoIterator<Item = String>,
304{
305    let file = File::create(path)?;
306    let mut writer = BufWriter::new(file);
307    writeln!(writer, "{header}")?;
308    for line in lines {
309        writeln!(writer, "{line}")?;
310    }
311    writer.flush()?;
312    Ok(())
313}
314
315fn fmt_f64(value: f64) -> String {
316    format!("{value:.8}")
317}