vpp_sim/
telemetry.rs

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}