vpp_sim/devices/
ev_charger.rs1use crate::devices::types::{Device, DeviceContext};
2use rand::{RngExt, SeedableRng, rngs::StdRng};
3
4#[derive(Debug, Clone)]
5struct EvSession {
6 arrival_step: usize,
7 deadline_step: usize,
8 remaining_kwh: f32,
9}
10
11#[derive(Debug)]
21pub struct EvCharger {
22 pub max_charge_kw: f32,
24
25 pub steps_per_day: usize,
27
28 pub demand_kwh_min: f32,
30
31 pub demand_kwh_max: f32,
33
34 pub dwell_steps_min: usize,
36
37 pub dwell_steps_max: usize,
39
40 sampled_day: Option<usize>,
41 session: Option<EvSession>,
42 rng: StdRng,
43}
44
45impl EvCharger {
46 pub fn new(
47 max_charge_kw: f32,
48 steps_per_day: usize,
49 demand_kwh_min: f32,
50 demand_kwh_max: f32,
51 dwell_steps_min: usize,
52 dwell_steps_max: usize,
53 seed: u64,
54 ) -> Self {
55 assert!(max_charge_kw > 0.0);
56 assert!(steps_per_day > 0);
57 assert!(demand_kwh_min >= 0.0);
58 assert!(demand_kwh_max >= demand_kwh_min);
59 assert!(dwell_steps_min > 0);
60 assert!(dwell_steps_max >= dwell_steps_min);
61
62 Self {
63 max_charge_kw,
64 steps_per_day,
65 demand_kwh_min,
66 demand_kwh_max,
67 dwell_steps_min,
68 dwell_steps_max,
69 sampled_day: None,
70 session: None,
71 rng: StdRng::seed_from_u64(seed),
72 }
73 }
74
75 fn dt_hours(&self) -> f32 {
76 24.0 / self.steps_per_day as f32
77 }
78
79 fn sample_session_for_day(&mut self, day: usize) {
80 let dwell_max = self.dwell_steps_max.min(self.steps_per_day);
81 let dwell_min = self.dwell_steps_min.min(dwell_max);
82 let dwell = self.rng.random_range(dwell_min..=dwell_max);
83
84 let latest_arrival = self.steps_per_day - dwell;
85 let arrival = self.rng.random_range(0..=latest_arrival);
86 let deadline = arrival + dwell;
87
88 let max_deliverable_kwh = self.max_charge_kw * self.dt_hours() * dwell as f32;
89 let raw_demand = self
90 .rng
91 .random_range(self.demand_kwh_min..=self.demand_kwh_max);
92 let demand_kwh = raw_demand.min(max_deliverable_kwh).max(0.0);
93
94 self.sampled_day = Some(day);
95 self.session = Some(EvSession {
96 arrival_step: arrival,
97 deadline_step: deadline,
98 remaining_kwh: demand_kwh,
99 });
100 }
101
102 pub fn requested_power_kw(&mut self, context: &DeviceContext) -> f32 {
104 let day = context.timestep / self.steps_per_day;
105 let day_t = context.timestep % self.steps_per_day;
106 let dt_hours = self.dt_hours();
107
108 if self.sampled_day != Some(day) {
109 self.sample_session_for_day(day);
110 }
111
112 let Some(session) = &self.session else {
113 return 0.0;
114 };
115
116 if day_t < session.arrival_step || day_t >= session.deadline_step {
117 return 0.0;
118 }
119
120 if session.remaining_kwh <= 0.0 {
121 return 0.0;
122 }
123
124 let remaining_steps = session.deadline_step - day_t;
125 if remaining_steps == 0 {
126 return 0.0;
127 }
128
129 (session.remaining_kwh / (remaining_steps as f32 * dt_hours)).max(0.0)
130 }
131}
132
133impl Device for EvCharger {
134 fn power_kw(&mut self, context: &DeviceContext) -> f32 {
135 let requested_kw = self.requested_power_kw(context);
136 let dt_hours = self.dt_hours();
137
138 if requested_kw <= 0.0 {
139 return 0.0;
140 }
141
142 let cap_kw = context.setpoint_kw.unwrap_or(self.max_charge_kw).max(0.0);
143 let charge_kw = requested_kw.min(cap_kw).min(self.max_charge_kw).max(0.0);
144
145 let Some(session) = &mut self.session else {
146 return 0.0;
147 };
148
149 let delivered_kwh = charge_kw * dt_hours;
150 session.remaining_kwh = (session.remaining_kwh - delivered_kwh).max(0.0);
151
152 charge_kw
153 }
154
155 fn device_type(&self) -> &'static str {
156 "EvCharger"
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn ctx(t: usize) -> DeviceContext {
165 DeviceContext::new(t)
166 }
167
168 #[test]
169 fn deterministic_for_same_seed() {
170 let mut ev1 = EvCharger::new(7.2, 24, 6.0, 12.0, 4, 10, 42);
171 let mut ev2 = EvCharger::new(7.2, 24, 6.0, 12.0, 4, 10, 42);
172
173 for t in 0..48 {
174 assert_eq!(ev1.power_kw(&ctx(t)), ev2.power_kw(&ctx(t)));
175 }
176 }
177
178 #[test]
179 fn no_charging_outside_session_window() {
180 let mut ev = EvCharger::new(7.2, 24, 0.0, 0.0, 4, 4, 7);
181
182 let mut non_zero_steps = 0;
183 for t in 0..24 {
184 if ev.power_kw(&ctx(t)) > 0.0 {
185 non_zero_steps += 1;
186 }
187 }
188
189 assert_eq!(non_zero_steps, 0);
190 }
191
192 #[test]
193 fn feasible_session_finishes_by_deadline() {
194 let mut ev = EvCharger::new(7.2, 24, 10.0, 10.0, 6, 6, 99);
195
196 let mut total_kwh = 0.0;
197 let dt_hours = 24.0 / 24.0;
198 for t in 0..24 {
199 total_kwh += ev.power_kw(&ctx(t)) * dt_hours;
200 }
201
202 assert!((total_kwh - 10.0).abs() < 1e-4);
203 }
204}