1use serde::Serialize;
2use std::fs::File;
3use std::io::{self, BufWriter, Write};
4use std::path::Path;
5
6pub const TELEMETRY_SCHEMA_V1_HEADER: &str = "timestep,time_hr,target_kw,feeder_kw,tracking_error_kw,baseload_kw,solar_kw,ev_requested_kw,ev_dispatched_kw,battery_kw,battery_soc,dr_requested_kw,dr_achieved_kw,limit_ok";
7
8#[derive(Clone, Debug, Serialize)]
9pub struct TelemetryRow {
10 pub timestep: usize,
11 pub time_hr: f32,
12 pub target_kw: f32,
13 pub feeder_kw: f32,
14 pub tracking_error_kw: f32,
15 pub baseload_kw: f32,
16 pub solar_kw: f32,
17 pub ev_requested_kw: f32,
18 pub ev_dispatched_kw: f32,
19 pub battery_kw: f32,
20 pub battery_soc: f32,
21 pub dr_requested_kw: f32,
22 pub dr_achieved_kw: f32,
23 pub limit_ok: bool,
24}
25
26pub fn write_telemetry_csv<W: Write>(writer: &mut W, rows: &[TelemetryRow]) -> io::Result<()> {
27 writeln!(writer, "{TELEMETRY_SCHEMA_V1_HEADER}")?;
28 for row in rows {
29 writeln!(
30 writer,
31 "{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{}",
32 row.timestep,
33 row.time_hr,
34 row.target_kw,
35 row.feeder_kw,
36 row.tracking_error_kw,
37 row.baseload_kw,
38 row.solar_kw,
39 row.ev_requested_kw,
40 row.ev_dispatched_kw,
41 row.battery_kw,
42 row.battery_soc,
43 row.dr_requested_kw,
44 row.dr_achieved_kw,
45 row.limit_ok
46 )?;
47 }
48 Ok(())
49}
50
51pub fn write_telemetry_to_path(path: &Path, rows: &[TelemetryRow]) -> io::Result<()> {
52 let file = File::create(path)?;
53 let mut writer = BufWriter::new(file);
54 write_telemetry_csv(&mut writer, rows)?;
55 writer.flush()
56}
57
58#[cfg(test)]
59mod tests {
60 use super::{TELEMETRY_SCHEMA_V1_HEADER, write_telemetry_csv};
61 use crate::runner::run_scenario;
62 use crate::scenario::ScenarioConfig;
63
64 #[test]
65 fn telemetry_csv_has_schema_v1_header_and_rows_per_timestep() {
66 let result = run_scenario(&ScenarioConfig::default(), false);
67 assert_eq!(result.telemetry.len(), 24);
68
69 let mut out = Vec::new();
70 write_telemetry_csv(&mut out, &result.telemetry).expect("csv export should succeed");
71
72 let csv = String::from_utf8(out).expect("csv output should be valid UTF-8");
73 let mut lines = csv.lines();
74 assert_eq!(lines.next(), Some(TELEMETRY_SCHEMA_V1_HEADER));
75 assert_eq!(lines.count(), 24);
76 }
77
78 #[test]
79 fn telemetry_export_is_deterministic_for_fixed_seed_and_config() {
80 let run_a = run_scenario(&ScenarioConfig::default(), false);
81 let run_b = run_scenario(&ScenarioConfig::default(), false);
82
83 let mut out_a = Vec::new();
84 write_telemetry_csv(&mut out_a, &run_a.telemetry).expect("first export should succeed");
85
86 let mut out_b = Vec::new();
87 write_telemetry_csv(&mut out_b, &run_b.telemetry).expect("second export should succeed");
88
89 assert_eq!(out_a, out_b);
90 }
91}