vpp_sim/sim/
controller.rs

1/// Naive real-time controller.
2///
3/// Uses only the battery to track a target feeder net load.
4#[derive(Debug, Default, Clone, Copy)]
5pub struct NaiveRtController;
6
7impl NaiveRtController {
8    /// Compute the battery power setpoint required to track target feeder load.
9    ///
10    /// Feeder model: `feeder_kw = net_without_battery - battery_kw`
11    /// Therefore: `battery_kw = net_without_battery - target_kw`
12    #[cfg(test)]
13    pub fn battery_setpoint_kw(&self, net_without_battery: f32, target_kw: f32) -> f32 {
14        net_without_battery - target_kw
15    }
16
17    /// Cap a flexible load (e.g., EV/pump/refrigeration-like demand) so feeder import can be kept under limit
18    /// with available battery discharge.
19    pub fn capped_flexible_load_kw(
20        &self,
21        net_fixed_kw: f32,
22        requested_flexible_kw: f32,
23        max_import_kw: f32,
24        battery_max_discharge_kw: f32,
25    ) -> f32 {
26        let requested = requested_flexible_kw.max(0.0);
27        let overload_kw =
28            (net_fixed_kw + requested - battery_max_discharge_kw - max_import_kw).max(0.0);
29        (requested - overload_kw).max(0.0)
30    }
31
32    /// Compute battery setpoint while enforcing feeder import/export constraints.
33    pub fn constrained_battery_setpoint_kw(
34        &self,
35        net_without_battery_kw: f32,
36        target_kw: f32,
37        max_import_kw: f32,
38        max_export_kw: f32,
39        battery_max_charge_kw: f32,
40        battery_max_discharge_kw: f32,
41    ) -> f32 {
42        let min_feeder_kw = -max_export_kw;
43        let max_feeder_kw = max_import_kw;
44        let constrained_target_kw = target_kw.clamp(min_feeder_kw, max_feeder_kw);
45
46        // From feeder = net_without_battery - battery, derive feasible battery range
47        // that respects feeder limits and battery limits.
48        let low_kw = (-battery_max_charge_kw).max(net_without_battery_kw - max_feeder_kw);
49        let high_kw = battery_max_discharge_kw.min(net_without_battery_kw - min_feeder_kw);
50
51        let desired_kw = net_without_battery_kw - constrained_target_kw;
52        if low_kw <= high_kw {
53            desired_kw.clamp(low_kw, high_kw)
54        } else {
55            // No feasible point can satisfy both battery and feeder limits; return the
56            // battery-limited command closest to desired.
57            desired_kw.clamp(-battery_max_charge_kw, battery_max_discharge_kw)
58        }
59    }
60
61    /// Apply demand response by shedding flexible load first, then curtailable baseload.
62    ///
63    /// Returns `(baseload_after_kw, flexible_after_kw, achieved_reduction_kw)`.
64    pub fn apply_demand_response_kw(
65        &self,
66        baseload_kw: f32,
67        flexible_load_kw: f32,
68        requested_reduction_kw: f32,
69    ) -> (f32, f32, f32) {
70        let mut remaining_reduction = requested_reduction_kw.max(0.0);
71
72        let flexible = flexible_load_kw.max(0.0);
73        let flex_shed = flexible.min(remaining_reduction);
74        let flexible_after = flexible - flex_shed;
75        remaining_reduction -= flex_shed;
76
77        let base = baseload_kw.max(0.0);
78        let base_shed = base.min(remaining_reduction);
79        let baseload_after = base - base_shed;
80
81        let achieved = flex_shed + base_shed;
82        (baseload_after, flexible_after, achieved)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::NaiveRtController;
89
90    #[test]
91    fn discharges_when_load_is_above_target() {
92        let controller = NaiveRtController;
93        let setpoint = controller.battery_setpoint_kw(3.0, 1.0);
94        assert_eq!(setpoint, 2.0);
95    }
96
97    #[test]
98    fn charges_when_load_is_below_target() {
99        let controller = NaiveRtController;
100        let setpoint = controller.battery_setpoint_kw(1.0, 2.5);
101        assert_eq!(setpoint, -1.5);
102    }
103
104    #[test]
105    fn caps_flexible_load_when_import_cannot_be_met() {
106        let controller = NaiveRtController;
107        let capped = controller.capped_flexible_load_kw(6.0, 4.0, 5.0, 3.0);
108        assert_eq!(capped, 2.0);
109    }
110
111    #[test]
112    fn keeps_flexible_load_when_import_is_feasible() {
113        let controller = NaiveRtController;
114        let capped = controller.capped_flexible_load_kw(2.0, 2.5, 5.0, 3.0);
115        assert_eq!(capped, 2.5);
116    }
117
118    #[test]
119    fn constrained_battery_setpoint_respects_import_limit() {
120        let controller = NaiveRtController;
121        let battery_kw = controller.constrained_battery_setpoint_kw(6.0, 1.0, 5.0, 4.0, 4.0, 3.0);
122        let feeder_kw = 6.0 - battery_kw;
123        assert!(feeder_kw <= 5.0 + 1e-6);
124    }
125
126    #[test]
127    fn constrained_battery_setpoint_is_battery_limited_when_infeasible() {
128        let controller = NaiveRtController;
129        let battery_kw = controller.constrained_battery_setpoint_kw(10.0, 1.0, 5.0, 4.0, 4.0, 3.0);
130        let feeder_kw = 10.0 - battery_kw;
131        assert_eq!(battery_kw, 3.0);
132        assert_eq!(feeder_kw, 7.0);
133    }
134
135    #[test]
136    fn constrained_battery_setpoint_respects_export_limit() {
137        let controller = NaiveRtController;
138        let battery_kw = controller.constrained_battery_setpoint_kw(-6.0, -5.0, 5.0, 2.0, 4.0, 3.0);
139        let feeder_kw = -6.0 - battery_kw;
140        assert!(feeder_kw >= -2.0 - 1e-6);
141    }
142
143    #[test]
144    fn demand_response_sheds_flexible_then_baseload() {
145        let controller = NaiveRtController;
146        let (base_after, flex_after, achieved) = controller.apply_demand_response_kw(3.0, 2.0, 4.0);
147        assert_eq!(flex_after, 0.0);
148        assert_eq!(base_after, 1.0);
149        assert_eq!(achieved, 4.0);
150    }
151
152    #[test]
153    fn demand_response_limited_by_available_load() {
154        let controller = NaiveRtController;
155        let (base_after, flex_after, achieved) = controller.apply_demand_response_kw(1.0, 0.5, 3.0);
156        assert_eq!(flex_after, 0.0);
157        assert_eq!(base_after, 0.0);
158        assert_eq!(achieved, 1.5);
159    }
160}