Skip to main content

frequenz_microgrid_component_graph/
component_category.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! This module defines the `ComponentCategory` enum, which represents the
5//! category of a component.
6
7use crate::ComponentGraphConfig;
8use crate::graph_traits::Node;
9use std::fmt::Display;
10
11/// Represents the type of an inverter.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub enum InverterType {
14    Unspecified,
15    Pv,
16    Battery,
17    Hybrid,
18}
19
20impl Display for InverterType {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            InverterType::Unspecified => write!(f, "Unspecified"),
24            InverterType::Pv => write!(f, "Pv"),
25            InverterType::Battery => write!(f, "Battery"),
26            InverterType::Hybrid => write!(f, "Hybrid"),
27        }
28    }
29}
30
31/// Represents the type of a battery.
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub enum BatteryType {
34    Unspecified,
35    LiIon,
36    NaIon,
37}
38
39impl Display for BatteryType {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            BatteryType::Unspecified => write!(f, "Unspecified"),
43            BatteryType::LiIon => write!(f, "LiIon"),
44            BatteryType::NaIon => write!(f, "NaIon"),
45        }
46    }
47}
48
49/// Represents the type of an EV charger.
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub enum EvChargerType {
52    Unspecified,
53    Ac,
54    Dc,
55    Hybrid,
56}
57
58impl Display for EvChargerType {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            EvChargerType::Unspecified => write!(f, "Unspecified"),
62            EvChargerType::Ac => write!(f, "AC"),
63            EvChargerType::Dc => write!(f, "DC"),
64            EvChargerType::Hybrid => write!(f, "Hybrid"),
65        }
66    }
67}
68
69/// Represents the category of a component.
70///
71/// Values of the underlying generated `ComponentCategory` and `ComponentType` types
72/// need to be converted to this type, so that they can be used in the
73/// `ComponentGraph`.
74#[derive(Clone, Copy, Debug, PartialEq)]
75pub enum ComponentCategory {
76    Unspecified,
77    GridConnectionPoint,
78    Meter,
79    Inverter(InverterType),
80    Converter,
81    Battery(BatteryType),
82    EvCharger(EvChargerType),
83    Breaker,
84    Precharger,
85    Chp,
86    Electrolyzer,
87    PowerTransformer,
88    Hvac,
89    Plc,
90    CryptoMiner,
91    StaticTransferSwitch,
92    UninterruptiblePowerSupply,
93    CapacitorBank,
94    WindTurbine,
95    SteamBoiler,
96}
97
98impl Display for ComponentCategory {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            ComponentCategory::Unspecified => write!(f, "Unspecified"),
102            ComponentCategory::GridConnectionPoint => write!(f, "GridConnectionPoint"),
103            ComponentCategory::Meter => write!(f, "Meter"),
104            ComponentCategory::Battery(battery_type) => write!(f, "Battery({battery_type})"),
105            ComponentCategory::Inverter(inverter_type) => write!(f, "{inverter_type}Inverter"),
106            ComponentCategory::EvCharger(ev_charger_type) => {
107                write!(f, "EVCharger({ev_charger_type})")
108            }
109            ComponentCategory::Converter => write!(f, "Converter"),
110            ComponentCategory::CryptoMiner => write!(f, "CryptoMiner"),
111            ComponentCategory::Electrolyzer => write!(f, "Electrolyzer"),
112            ComponentCategory::Chp => write!(f, "CHP"),
113            ComponentCategory::Precharger => write!(f, "Precharger"),
114            ComponentCategory::Hvac => write!(f, "HVAC"),
115            ComponentCategory::Breaker => write!(f, "Breaker"),
116            ComponentCategory::PowerTransformer => write!(f, "PowerTransformer"),
117            ComponentCategory::Plc => write!(f, "PLC"),
118            ComponentCategory::StaticTransferSwitch => write!(f, "StaticTransferSwitch"),
119            ComponentCategory::UninterruptiblePowerSupply => {
120                write!(f, "UninterruptiblePowerSupply")
121            }
122            ComponentCategory::CapacitorBank => write!(f, "CapacitorBank"),
123            ComponentCategory::WindTurbine => write!(f, "WindTurbine"),
124            ComponentCategory::SteamBoiler => write!(f, "SteamBoiler"),
125        }
126    }
127}
128
129impl ComponentCategory {
130    /// Returns `true` if this category is a *pass-through*: a component
131    /// that has no specific handling in the graph and should be treated
132    /// as transparent by validators and formula generators.
133    ///
134    /// Pass-through nodes can sit anywhere in the chain between handled
135    /// categories. They don't generate or consume their own measurement,
136    /// and neighbor relationships across them are evaluated by walking
137    /// past them as if they weren't present.
138    ///
139    /// `Unspecified` is intentionally not a pass-through: it's rejected
140    /// at graph-construction time before validators or formula
141    /// generators ever see it.
142    pub(crate) fn is_passthrough(self) -> bool {
143        use ComponentCategory as C;
144        matches!(
145            self,
146            C::Converter
147                | C::Breaker
148                | C::Precharger
149                | C::Electrolyzer
150                | C::PowerTransformer
151                | C::Hvac
152                | C::Plc
153                | C::CryptoMiner
154                | C::StaticTransferSwitch
155                | C::UninterruptiblePowerSupply
156                | C::CapacitorBank
157        )
158    }
159}
160
161/// Predicates for checking the component category of a `Node`.
162pub(crate) trait CategoryPredicates: Node {
163    fn is_unspecified(&self) -> bool {
164        self.category() == ComponentCategory::Unspecified
165    }
166
167    fn is_grid(&self) -> bool {
168        self.category() == ComponentCategory::GridConnectionPoint
169    }
170
171    fn is_meter(&self) -> bool {
172        self.category() == ComponentCategory::Meter
173    }
174
175    fn is_battery(&self) -> bool {
176        matches!(self.category(), ComponentCategory::Battery(_))
177    }
178
179    fn is_inverter(&self) -> bool {
180        matches!(self.category(), ComponentCategory::Inverter(_))
181    }
182
183    fn is_battery_inverter(&self, config: &ComponentGraphConfig) -> bool {
184        match self.category() {
185            ComponentCategory::Inverter(InverterType::Battery) => true,
186            ComponentCategory::Inverter(InverterType::Unspecified) => {
187                config.allow_unspecified_inverters
188            }
189            _ => false,
190        }
191    }
192
193    fn is_pv_inverter(&self) -> bool {
194        self.category() == ComponentCategory::Inverter(InverterType::Pv)
195    }
196
197    fn is_hybrid_inverter(&self) -> bool {
198        self.category() == ComponentCategory::Inverter(InverterType::Hybrid)
199    }
200
201    fn is_unspecified_inverter(&self, config: &ComponentGraphConfig) -> bool {
202        match self.category() {
203            ComponentCategory::Inverter(InverterType::Unspecified) => {
204                !config.allow_unspecified_inverters
205            }
206            _ => false,
207        }
208    }
209
210    fn is_ev_charger(&self) -> bool {
211        matches!(self.category(), ComponentCategory::EvCharger(_))
212    }
213
214    fn is_chp(&self) -> bool {
215        self.category() == ComponentCategory::Chp
216    }
217
218    fn is_wind_turbine(&self) -> bool {
219        self.category() == ComponentCategory::WindTurbine
220    }
221
222    fn is_steam_boiler(&self) -> bool {
223        self.category() == ComponentCategory::SteamBoiler
224    }
225}
226
227/// Implement the `CategoryPredicates` trait for all types that implement the
228/// `Node` trait.
229impl<T: Node> CategoryPredicates for T {}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    /// Locks the pass-through classification. If a category is added or
236    /// re-classified, this test forces an explicit decision rather than
237    /// silently inheriting the default.
238    #[test]
239    fn test_is_passthrough_classification() {
240        // Pass-through categories: no specific validation rule, no formula
241        // contribution; transparent to traversal.
242        for c in [
243            ComponentCategory::Converter,
244            ComponentCategory::Breaker,
245            ComponentCategory::Precharger,
246            ComponentCategory::Electrolyzer,
247            ComponentCategory::PowerTransformer,
248            ComponentCategory::Hvac,
249            ComponentCategory::Plc,
250            ComponentCategory::CryptoMiner,
251            ComponentCategory::StaticTransferSwitch,
252            ComponentCategory::UninterruptiblePowerSupply,
253            ComponentCategory::CapacitorBank,
254        ] {
255            assert!(c.is_passthrough(), "{c} should be a pass-through");
256        }
257
258        // Handled or special-cased categories.
259        for c in [
260            ComponentCategory::Unspecified,
261            ComponentCategory::GridConnectionPoint,
262            ComponentCategory::Meter,
263            ComponentCategory::Inverter(InverterType::Battery),
264            ComponentCategory::Inverter(InverterType::Pv),
265            ComponentCategory::Inverter(InverterType::Hybrid),
266            ComponentCategory::Inverter(InverterType::Unspecified),
267            ComponentCategory::Battery(BatteryType::LiIon),
268            ComponentCategory::Battery(BatteryType::NaIon),
269            ComponentCategory::Battery(BatteryType::Unspecified),
270            ComponentCategory::EvCharger(EvChargerType::Ac),
271            ComponentCategory::EvCharger(EvChargerType::Dc),
272            ComponentCategory::EvCharger(EvChargerType::Hybrid),
273            ComponentCategory::EvCharger(EvChargerType::Unspecified),
274            ComponentCategory::Chp,
275            ComponentCategory::WindTurbine,
276        ] {
277            assert!(!c.is_passthrough(), "{c} should not be a pass-through");
278        }
279    }
280}