Skip to main content

dsfb_add/
output.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use chrono::Utc;
5use csv::Writer;
6
7use crate::{rlt::RltTrajectoryPoint, AddError, TcpPoint};
8
9#[derive(Debug, Clone)]
10pub struct PhaseBoundaryRow {
11    pub steps_per_run: usize,
12    pub mode: String,
13    pub is_perturbed: bool,
14    pub lambda_star: Option<f64>,
15    pub lambda_0_1: Option<f64>,
16    pub lambda_0_9: Option<f64>,
17    pub transition_width: Option<f64>,
18    pub max_derivative: Option<f64>,
19}
20
21#[derive(Debug, Clone)]
22pub struct StructuralLawSummaryRow {
23    pub steps_per_run: usize,
24    pub is_perturbed: bool,
25    pub pearson_r: f64,
26    pub spearman_rho: f64,
27    pub slope: f64,
28    pub intercept: f64,
29    pub r2: f64,
30    pub residual_variance: f64,
31    pub mse_resid: f64,
32    pub slope_ci_low: f64,
33    pub slope_ci_high: f64,
34    pub sample_count: usize,
35    pub ratio_mean: f64,
36    pub ratio_std: f64,
37}
38
39#[derive(Debug, Clone)]
40pub struct DiagnosticsSummaryRow {
41    pub steps_per_run: usize,
42    pub residual_mean: f64,
43    pub residual_std: f64,
44    pub residual_skew_approx: f64,
45    pub residual_kurtosis_approx: f64,
46    pub ratio_mean: f64,
47    pub ratio_std: f64,
48    pub ratio_min: f64,
49    pub ratio_max: f64,
50}
51
52#[derive(Debug, Clone)]
53pub struct CrossLayerThresholdRow {
54    pub steps_per_run: usize,
55    pub lambda_star: Option<f64>,
56    pub echo_slope_star: Option<f64>,
57    pub entropy_density_star: Option<f64>,
58}
59
60#[derive(Debug, Clone)]
61pub struct TcpPhaseAlignmentRow {
62    pub steps_per_run: usize,
63    pub lambda_star: Option<f64>,
64    pub lambda_tp_peak: Option<f64>,
65    pub lambda_b1_peak: Option<f64>,
66    pub delta_tp: Option<f64>,
67    pub delta_b1: Option<f64>,
68}
69
70#[derive(Debug, Clone)]
71pub struct RobustnessMetricRow {
72    pub metric: String,
73    pub steps_per_run: usize,
74    pub baseline: f64,
75    pub perturbed: f64,
76    pub delta: f64,
77}
78
79pub fn repo_root_dir() -> PathBuf {
80    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
81    manifest_dir
82        .parent()
83        .and_then(|path| path.parent())
84        .map(Path::to_path_buf)
85        .unwrap_or(manifest_dir)
86}
87
88pub fn create_timestamped_output_dir() -> Result<PathBuf, AddError> {
89    let output_root = repo_root_dir().join("output-dsfb-add");
90    fs::create_dir_all(&output_root)?;
91
92    let timestamp = Utc::now().format("%Y-%m-%dT%H-%M-%SZ").to_string();
93    let mut output_dir = output_root.join(&timestamp);
94    let mut counter = 1_u32;
95
96    while output_dir.exists() {
97        output_dir = output_root.join(format!("{timestamp}-{counter:02}"));
98        counter += 1;
99    }
100
101    fs::create_dir_all(&output_dir)?;
102    Ok(output_dir)
103}
104
105fn ensure_len(context: &'static str, expected: usize, actual: usize) -> Result<(), AddError> {
106    if expected == actual {
107        return Ok(());
108    }
109
110    Err(AddError::LengthMismatch {
111        context,
112        expected,
113        got: actual,
114    })
115}
116
117fn fmt_f64(value: f64) -> String {
118    format!("{value:.10}")
119}
120
121fn fmt_option_f64(value: Option<f64>) -> String {
122    value.map(fmt_f64).unwrap_or_default()
123}
124
125pub fn write_aet_csv(
126    path: &Path,
127    lambda_grid: &[f64],
128    echo_slope: &[f64],
129    avg_increment: &[f64],
130    steps_per_run: usize,
131    is_perturbed: bool,
132) -> Result<(), AddError> {
133    ensure_len("aet echo_slope", lambda_grid.len(), echo_slope.len())?;
134    ensure_len("aet avg_increment", lambda_grid.len(), avg_increment.len())?;
135
136    let mut writer = Writer::from_path(path)?;
137    writer.write_record([
138        "lambda",
139        "echo_slope",
140        "avg_increment",
141        "steps_per_run",
142        "is_perturbed",
143    ])?;
144
145    for idx in 0..lambda_grid.len() {
146        writer.write_record([
147            fmt_f64(lambda_grid[idx]),
148            fmt_f64(echo_slope[idx]),
149            fmt_f64(avg_increment[idx]),
150            steps_per_run.to_string(),
151            is_perturbed.to_string(),
152        ])?;
153    }
154
155    writer.flush()?;
156    Ok(())
157}
158
159pub fn write_tcp_csv(
160    path: &Path,
161    lambda_grid: &[f64],
162    betti0: &[usize],
163    betti1: &[usize],
164    l_tcp: &[f64],
165    avg_radius: &[f64],
166    max_radius: &[f64],
167    variance_radius: &[f64],
168    steps_per_run: usize,
169    is_perturbed: bool,
170) -> Result<(), AddError> {
171    ensure_len("tcp betti0", lambda_grid.len(), betti0.len())?;
172    ensure_len("tcp betti1", lambda_grid.len(), betti1.len())?;
173    ensure_len("tcp l_tcp", lambda_grid.len(), l_tcp.len())?;
174    ensure_len("tcp avg_radius", lambda_grid.len(), avg_radius.len())?;
175    ensure_len("tcp max_radius", lambda_grid.len(), max_radius.len())?;
176    ensure_len(
177        "tcp variance_radius",
178        lambda_grid.len(),
179        variance_radius.len(),
180    )?;
181
182    let mut writer = Writer::from_path(path)?;
183    writer.write_record([
184        "lambda",
185        "betti0",
186        "betti1",
187        "l_tcp",
188        "avg_radius",
189        "max_radius",
190        "variance_radius",
191        "steps_per_run",
192        "is_perturbed",
193    ])?;
194
195    for idx in 0..lambda_grid.len() {
196        writer.write_record([
197            fmt_f64(lambda_grid[idx]),
198            betti0[idx].to_string(),
199            betti1[idx].to_string(),
200            fmt_f64(l_tcp[idx]),
201            fmt_f64(avg_radius[idx]),
202            fmt_f64(max_radius[idx]),
203            fmt_f64(variance_radius[idx]),
204            steps_per_run.to_string(),
205            is_perturbed.to_string(),
206        ])?;
207    }
208
209    writer.flush()?;
210    Ok(())
211}
212
213pub fn write_rlt_csv(
214    path: &Path,
215    lambda_grid: &[f64],
216    escape_rate: &[f64],
217    expansion_ratio: &[f64],
218    steps_per_run: usize,
219    is_perturbed: bool,
220) -> Result<(), AddError> {
221    ensure_len("rlt escape_rate", lambda_grid.len(), escape_rate.len())?;
222    ensure_len(
223        "rlt expansion_ratio",
224        lambda_grid.len(),
225        expansion_ratio.len(),
226    )?;
227
228    let mut writer = Writer::from_path(path)?;
229    writer.write_record([
230        "lambda",
231        "escape_rate",
232        "expansion_ratio",
233        "steps_per_run",
234        "is_perturbed",
235    ])?;
236
237    for idx in 0..lambda_grid.len() {
238        writer.write_record([
239            fmt_f64(lambda_grid[idx]),
240            fmt_f64(escape_rate[idx]),
241            fmt_f64(expansion_ratio[idx]),
242            steps_per_run.to_string(),
243            is_perturbed.to_string(),
244        ])?;
245    }
246
247    writer.flush()?;
248    Ok(())
249}
250
251pub fn write_iwlt_csv(
252    path: &Path,
253    lambda_grid: &[f64],
254    entropy_density: &[f64],
255    avg_increment: &[f64],
256    steps_per_run: usize,
257    is_perturbed: bool,
258) -> Result<(), AddError> {
259    ensure_len(
260        "iwlt entropy_density",
261        lambda_grid.len(),
262        entropy_density.len(),
263    )?;
264    ensure_len("iwlt avg_increment", lambda_grid.len(), avg_increment.len())?;
265
266    let mut writer = Writer::from_path(path)?;
267    writer.write_record([
268        "lambda",
269        "entropy_density",
270        "avg_increment",
271        "steps_per_run",
272        "is_perturbed",
273    ])?;
274
275    for idx in 0..lambda_grid.len() {
276        writer.write_record([
277            fmt_f64(lambda_grid[idx]),
278            fmt_f64(entropy_density[idx]),
279            fmt_f64(avg_increment[idx]),
280            steps_per_run.to_string(),
281            is_perturbed.to_string(),
282        ])?;
283    }
284
285    writer.flush()?;
286    Ok(())
287}
288
289pub fn write_tcp_points_csv(path: &Path, points: &[TcpPoint]) -> Result<(), AddError> {
290    let mut writer = Writer::from_path(path)?;
291    writer.write_record(["t", "x", "y"])?;
292
293    for point in points {
294        writer.write_record([point.t.to_string(), fmt_f64(point.x), fmt_f64(point.y)])?;
295    }
296
297    writer.flush()?;
298    Ok(())
299}
300
301pub fn write_rlt_trajectory_csv(
302    path: &Path,
303    points: &[RltTrajectoryPoint],
304) -> Result<(), AddError> {
305    let mut writer = Writer::from_path(path)?;
306    writer.write_record([
307        "step",
308        "lambda",
309        "vertex_id",
310        "x",
311        "y",
312        "distance_from_start",
313    ])?;
314
315    for point in points {
316        writer.write_record([
317            point.step.to_string(),
318            fmt_f64(point.lambda),
319            point.vertex_id.to_string(),
320            point.x.to_string(),
321            point.y.to_string(),
322            point.distance_from_start.to_string(),
323        ])?;
324    }
325
326    writer.flush()?;
327    Ok(())
328}
329
330pub fn write_rlt_phase_boundary_csv(
331    path: &Path,
332    rows: &[PhaseBoundaryRow],
333) -> Result<(), AddError> {
334    let mut writer = Writer::from_path(path)?;
335    writer.write_record([
336        "steps_per_run",
337        "mode",
338        "is_perturbed",
339        "lambda_star",
340        "lambda_0_1",
341        "lambda_0_9",
342        "transition_width",
343        "max_derivative",
344    ])?;
345
346    for row in rows {
347        writer.write_record([
348            row.steps_per_run.to_string(),
349            row.mode.clone(),
350            row.is_perturbed.to_string(),
351            fmt_option_f64(row.lambda_star),
352            fmt_option_f64(row.lambda_0_1),
353            fmt_option_f64(row.lambda_0_9),
354            fmt_option_f64(row.transition_width),
355            fmt_option_f64(row.max_derivative),
356        ])?;
357    }
358
359    writer.flush()?;
360    Ok(())
361}
362
363pub fn write_structural_law_summary_csv(
364    path: &Path,
365    rows: &[StructuralLawSummaryRow],
366) -> Result<(), AddError> {
367    let mut writer = Writer::from_path(path)?;
368    writer.write_record([
369        "steps_per_run",
370        "is_perturbed",
371        "pearson_r",
372        "spearman_rho",
373        "slope",
374        "intercept",
375        "r2",
376        "residual_variance",
377        "mse_resid",
378        "slope_ci_low",
379        "slope_ci_high",
380        "sample_count",
381        "ratio_mean",
382        "ratio_std",
383    ])?;
384
385    for row in rows {
386        writer.write_record([
387            row.steps_per_run.to_string(),
388            row.is_perturbed.to_string(),
389            fmt_f64(row.pearson_r),
390            fmt_f64(row.spearman_rho),
391            fmt_f64(row.slope),
392            fmt_f64(row.intercept),
393            fmt_f64(row.r2),
394            fmt_f64(row.residual_variance),
395            fmt_f64(row.mse_resid),
396            fmt_f64(row.slope_ci_low),
397            fmt_f64(row.slope_ci_high),
398            row.sample_count.to_string(),
399            fmt_f64(row.ratio_mean),
400            fmt_f64(row.ratio_std),
401        ])?;
402    }
403
404    writer.flush()?;
405    Ok(())
406}
407
408pub fn write_diagnostics_summary_csv(
409    path: &Path,
410    rows: &[DiagnosticsSummaryRow],
411) -> Result<(), AddError> {
412    let mut writer = Writer::from_path(path)?;
413    writer.write_record([
414        "steps_per_run",
415        "residual_mean",
416        "residual_std",
417        "residual_skew_approx",
418        "residual_kurtosis_approx",
419        "ratio_mean",
420        "ratio_std",
421        "ratio_min",
422        "ratio_max",
423    ])?;
424
425    for row in rows {
426        writer.write_record([
427            row.steps_per_run.to_string(),
428            fmt_f64(row.residual_mean),
429            fmt_f64(row.residual_std),
430            fmt_f64(row.residual_skew_approx),
431            fmt_f64(row.residual_kurtosis_approx),
432            fmt_f64(row.ratio_mean),
433            fmt_f64(row.ratio_std),
434            fmt_f64(row.ratio_min),
435            fmt_f64(row.ratio_max),
436        ])?;
437    }
438
439    writer.flush()?;
440    Ok(())
441}
442
443pub fn write_cross_layer_thresholds_csv(
444    path: &Path,
445    rows: &[CrossLayerThresholdRow],
446) -> Result<(), AddError> {
447    let mut writer = Writer::from_path(path)?;
448    writer.write_record([
449        "steps_per_run",
450        "lambda_star",
451        "echo_slope_star",
452        "entropy_density_star",
453    ])?;
454
455    for row in rows {
456        writer.write_record([
457            row.steps_per_run.to_string(),
458            fmt_option_f64(row.lambda_star),
459            fmt_option_f64(row.echo_slope_star),
460            fmt_option_f64(row.entropy_density_star),
461        ])?;
462    }
463
464    writer.flush()?;
465    Ok(())
466}
467
468pub fn write_tcp_phase_alignment_csv(
469    path: &Path,
470    rows: &[TcpPhaseAlignmentRow],
471) -> Result<(), AddError> {
472    let mut writer = Writer::from_path(path)?;
473    writer.write_record([
474        "steps_per_run",
475        "lambda_star",
476        "lambda_tp_peak",
477        "lambda_b1_peak",
478        "delta_tp",
479        "delta_b1",
480    ])?;
481
482    for row in rows {
483        writer.write_record([
484            row.steps_per_run.to_string(),
485            fmt_option_f64(row.lambda_star),
486            fmt_option_f64(row.lambda_tp_peak),
487            fmt_option_f64(row.lambda_b1_peak),
488            fmt_option_f64(row.delta_tp),
489            fmt_option_f64(row.delta_b1),
490        ])?;
491    }
492
493    writer.flush()?;
494    Ok(())
495}
496
497pub fn write_robustness_metrics_csv(
498    path: &Path,
499    rows: &[RobustnessMetricRow],
500) -> Result<(), AddError> {
501    let mut writer = Writer::from_path(path)?;
502    writer.write_record(["metric", "steps_per_run", "baseline", "perturbed", "delta"])?;
503
504    for row in rows {
505        writer.write_record([
506            row.metric.clone(),
507            row.steps_per_run.to_string(),
508            fmt_f64(row.baseline),
509            fmt_f64(row.perturbed),
510            fmt_f64(row.delta),
511        ])?;
512    }
513
514    writer.flush()?;
515    Ok(())
516}