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(×tamp);
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}