1use crate::devices::{BaseLoad, Battery, Device, DeviceContext, EvCharger, SolarPv};
2use crate::forecast::NaiveForecast;
3use crate::scenario::ScenarioConfig;
4use crate::sim::clock::Clock;
5use crate::sim::controller::NaiveRtController;
6use crate::sim::event::DemandResponseEvent;
7use crate::sim::feeder::Feeder;
8use crate::sim::schedule::DayAheadSchedule;
9use crate::telemetry::TelemetryRow;
10
11pub struct SimulationKpis {
12 pub rmse_tracking_kw: f32,
13 pub curtailment_pct: f32,
14 pub feeder_peak_load_kw: f32,
15}
16
17pub struct SimulationResult {
18 pub telemetry: Vec<TelemetryRow>,
19 pub kpis: SimulationKpis,
20}
21
22pub fn run_scenario(config: &ScenarioConfig, print_readable_log: bool) -> SimulationResult {
23 let houses = config.houses as f32;
24 let steps_per_day = config.steps_per_day;
25 let dt_hr = 24.0 / steps_per_day as f32;
26 let mut clock = Clock::new(steps_per_day); let mut load = BaseLoad::new(
29 0.8 * houses, 0.7 * houses, 1.2, 0.05, steps_per_day, config.seed, );
36
37 let baseload_device = load.device_type();
38 let mut baseline_load = BaseLoad::new(
39 0.8 * houses,
40 0.7 * houses,
41 1.2,
42 0.05,
43 steps_per_day,
44 config.seed,
45 );
46 let mut baseline = Vec::with_capacity(steps_per_day);
47 for t in 0..steps_per_day {
48 baseline.push(baseline_load.power_kw(&DeviceContext::new(t)));
49 }
50 let forecaster = NaiveForecast;
51 let load_forecast = forecaster.forecast(&baseline, steps_per_day);
52 let target_schedule = DayAheadSchedule::flat_target(&load_forecast);
53
54 let mut pv = SolarPv::new(
55 config.solar_kw_peak_per_house * houses, steps_per_day, 6, 18, 0.05, config.seed.wrapping_add(1), );
62
63 let solar_device = pv.device_type();
64
65 let mut battery = Battery::new(
66 10.0 * houses, 0.5, 5.0 * houses, 5.0 * houses, 0.95, 0.95, steps_per_day, );
74
75 let battery_device = battery.device_type();
76 let mut ev = EvCharger::new(
77 7.2 * houses, steps_per_day, 4.0 * houses, 14.0 * houses, 3, 10, config.seed.wrapping_add(2), );
85 let ev_device = ev.device_type();
86
87 let mut feeder = Feeder::with_limits(
88 "MainFeeder",
89 config.feeder_kw, config.feeder_kw * 0.8, );
92
93 let dr_event = DemandResponseEvent::new(
94 config.dr_start_step,
95 config.dr_end_step,
96 config.dr_reduction_kw_per_house * houses,
97 );
98
99 let controller = NaiveRtController;
100
101 let mut telemetry = Vec::with_capacity(steps_per_day);
102 let mut tracking_error_sq_sum = 0.0_f32;
103 let mut tracking_error_count = 0_usize;
104 let mut requested_curtailment_sum_kw = 0.0_f32;
105 let mut achieved_curtailment_sum_kw = 0.0_f32;
106 let mut feeder_peak_load_kw = 0.0_f32;
107
108 clock.run(|t| {
109 let context = DeviceContext::new(t);
110
111 let base_demand_kw_raw = load.power_kw(&context);
112 let forecast_kw = load_forecast[context.timestep];
113 let target_kw = target_schedule[context.timestep];
114 let solar_kw = pv.power_kw(&context);
115 let ev_requested_kw = ev.requested_power_kw(&context);
116
117 let dr_requested_kw = dr_event.requested_reduction_at_kw(t);
118 let (base_demand_kw, ev_after_dr_kw, dr_achieved_kw) = controller.apply_demand_response_kw(
119 base_demand_kw_raw,
120 ev_requested_kw,
121 dr_requested_kw,
122 );
123
124 let net_fixed_kw = base_demand_kw - solar_kw;
125 let ev_capped_kw = controller.capped_flexible_load_kw(
126 net_fixed_kw,
127 ev_after_dr_kw,
128 feeder.max_import_kw(),
129 battery.max_discharge_kw,
130 );
131 let ev_context = DeviceContext::with_setpoint(context.timestep, ev_capped_kw);
132 let ev_kw = ev.power_kw(&ev_context);
133
134 let net_without_battery = net_fixed_kw + ev_kw;
135 let battery_setpoint_kw = controller.constrained_battery_setpoint_kw(
136 net_without_battery,
137 target_kw,
138 feeder.max_import_kw(),
139 feeder.max_export_kw(),
140 battery.max_charge_kw,
141 battery.max_discharge_kw,
142 );
143 let battery_context = DeviceContext::with_setpoint(context.timestep, battery_setpoint_kw);
144
145 let battery_kw = battery.power_kw(&battery_context);
146 feeder.reset();
147 feeder.add_net_kw(base_demand_kw);
148 feeder.add_net_kw(ev_kw);
149 feeder.add_net_kw(-solar_kw);
150 feeder.add_net_kw(-battery_kw);
151 let feeder_kw = feeder.net_kw();
152 let tracking_error_kw = feeder_kw - target_kw;
153 let feeder_name = feeder.name();
154
155 tracking_error_sq_sum += tracking_error_kw * tracking_error_kw;
156 tracking_error_count += 1;
157 requested_curtailment_sum_kw += dr_requested_kw;
158 achieved_curtailment_sum_kw += dr_achieved_kw;
159 feeder_peak_load_kw = feeder_peak_load_kw.max(feeder_kw);
160
161 let row = TelemetryRow {
162 timestep: t,
163 time_hr: t as f32 * dt_hr,
164 target_kw,
165 feeder_kw,
166 tracking_error_kw,
167 baseload_kw: base_demand_kw,
168 solar_kw,
169 ev_requested_kw,
170 ev_dispatched_kw: ev_kw,
171 battery_kw,
172 battery_soc: battery.soc,
173 dr_requested_kw,
174 dr_achieved_kw,
175 limit_ok: feeder.within_limits(),
176 };
177 telemetry.push(row);
178
179 let soc = battery.soc * 100.0;
180 if print_readable_log {
181 println!(
182 "Time (Hr) {t}: {baseload_device}={base_demand_kw:.2} kW, \
183 RawBase={base_demand_kw_raw:.2} kW, \
184 Forecast={forecast_kw:.2} kW, \
185 Target={target_kw:.2} kW, \
186 {solar_device}={solar_kw:.2} kW, \
187 {ev_device}={ev_kw:.2} kW (Req={ev_requested_kw:.2}, DR={ev_after_dr_kw:.2}, Cap={ev_capped_kw:.2}), \
188 {battery_device}={battery_kw:.2} kW (SoC={soc:.1}%), \
189 {feeder_name}={feeder_kw:.2} kW, \
190 Error={tracking_error_kw:.2} kW, \
191 DR(req={dr_requested_kw:.2}, done={dr_achieved_kw:.2}), \
192 LimitOK={}",
193 feeder.within_limits()
194 );
195 }
196 });
197
198 let rmse_tracking_kw = if tracking_error_count > 0 {
199 (tracking_error_sq_sum / tracking_error_count as f32).sqrt()
200 } else {
201 0.0
202 };
203
204 let curtailment_pct = if requested_curtailment_sum_kw > 0.0 {
205 100.0 * achieved_curtailment_sum_kw / requested_curtailment_sum_kw
206 } else {
207 0.0
208 };
209
210 SimulationResult {
211 telemetry,
212 kpis: SimulationKpis {
213 rmse_tracking_kw,
214 curtailment_pct,
215 feeder_peak_load_kw,
216 },
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::run_scenario;
223 use crate::scenario::ScenarioConfig;
224 use crate::telemetry::write_telemetry_csv;
225
226 #[test]
227 fn same_scenario_and_seed_is_deterministic() {
228 let scenario = ScenarioConfig {
229 houses: 3,
230 feeder_kw: 40.0,
231 seed: 777,
232 steps_per_day: 24,
233 ..ScenarioConfig::default()
234 };
235
236 let run_a = run_scenario(&scenario, false);
237 let run_b = run_scenario(&scenario, false);
238
239 let mut out_a = Vec::new();
240 write_telemetry_csv(&mut out_a, &run_a.telemetry).expect("first export should succeed");
241
242 let mut out_b = Vec::new();
243 write_telemetry_csv(&mut out_b, &run_b.telemetry).expect("second export should succeed");
244
245 assert_eq!(out_a, out_b);
246 }
247}