Skip to content

Commit f808f9d

Browse files
authored
Merge pull request #28 from cwasicki/formula
Support metric dependent formulas
2 parents 26cdbba + 85c10f4 commit f808f9d

File tree

3 files changed

+67
-38
lines changed

3 files changed

+67
-38
lines changed

RELEASE_NOTES.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
## Summary
44

5-
Renamed the `"load"` component type to `"consumption"` for clarity.
65

76
## Upgrading
87

9-
- If your code references `"load"` as a component type, update it to `"consumption"`.
10-
- Made the `MicrogridConfig` reader tolerant to missing `ctype` fields, allowing collection of incomplete microgrid configs.
8+
* Made the `MicrogridConfig` reader tolerant to missing `ctype` fields, allowing collection of incomplete microgrid configs.
9+
* Formula configs are now defined per metric to support different formulas for each metric in the same config file.
10+
This is a breaking change which requires updating the formula fields in the config file.
11+
* Default formulas are defined for AC active power and battery SoC metrics.
12+
The default SoC calculation uses simple averages and ignore different battery capacities.
13+
* The `cids` method is changed to support getting metric-specific CIDs which in this case are extracted from the formula.
1114

1215
## New Features
1316

src/frequenz/lib/notebooks/config.py

+59-20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""Configuration for microgrids."""
55

6+
import re
67
import tomllib
78
from dataclasses import dataclass
89
from typing import Any, Literal, cast, get_args
@@ -30,20 +31,60 @@ class ComponentTypeConfig:
3031
component: list[int] | None = None
3132
"""List of component IDs for this component."""
3233

33-
formula: str = ""
34+
formula: dict[str, str] | None = None
3435
"""Formula to calculate the power of this component."""
3536

3637
def __post_init__(self) -> None:
3738
"""Set the default formula if none is provided."""
38-
if not self.formula:
39-
self.formula = self._default_formula()
40-
41-
def cids(self) -> list[int]:
39+
self.formula = self.formula or {}
40+
if "AC_ACTIVE_POWER" not in self.formula:
41+
self.formula["AC_ACTIVE_POWER"] = "+".join(
42+
[f"#{cid}" for cid in self._default_cids()]
43+
)
44+
if self.component_type == "battery" and "BATTERY_SOC_PCT" not in self.formula:
45+
if self.component:
46+
cids = self.component
47+
form = "+".join([f"#{cid}" for cid in cids])
48+
form = f"({form})/({len(cids)})"
49+
self.formula["BATTERY_SOC_PCT"] = form
50+
51+
def cids(self, metric: str = "") -> list[int]:
4252
"""Get component IDs for this component.
4353
4454
By default, the meter IDs are returned if available, otherwise the inverter IDs.
4555
For components without meters or inverters, the component IDs are returned.
4656
57+
If a metric is provided, the component IDs are extracted from the formula.
58+
59+
Args:
60+
metric: Metric name of the formula.
61+
62+
Returns:
63+
List of component IDs for this component.
64+
65+
Raises:
66+
ValueError: If the metric is not supported or improperly set.
67+
"""
68+
if metric:
69+
if not isinstance(self.formula, dict):
70+
raise ValueError("Formula must be a dictionary.")
71+
formula = self.formula.get(metric)
72+
if not formula:
73+
raise ValueError(
74+
f"{metric} does not have a formula for {self.component_type}"
75+
)
76+
# Extract component IDs from the formula which are given as e.g. #123
77+
pattern = r"#(\d+)"
78+
return [int(e) for e in re.findall(pattern, self.formula[metric])]
79+
80+
return self._default_cids()
81+
82+
def _default_cids(self) -> list[int]:
83+
"""Get the default component IDs for this component.
84+
85+
If available, the meter IDs are returned, otherwise the inverter IDs.
86+
For components without meters or inverters, the component IDs are returned.
87+
4788
Returns:
4889
List of component IDs for this component.
4990
@@ -59,14 +100,6 @@ def cids(self) -> list[int]:
59100

60101
raise ValueError(f"No IDs available for {self.component_type}")
61102

62-
def _default_formula(self) -> str:
63-
"""Return the default formula for this component."""
64-
return "+".join([f"#{cid}" for cid in self.cids()])
65-
66-
def has_formula_for(self, metric: str) -> bool:
67-
"""Return whether this formula is valid for a metric."""
68-
return metric in ["AC_ACTIVE_POWER", "AC_REACTIVE_POWER"]
69-
70103
@classmethod
71104
def is_valid_type(cls, ctype: str) -> bool:
72105
"""Check if `ctype` is a valid enum value."""
@@ -191,7 +224,10 @@ def component_types(self) -> list[str]:
191224
return list(self._component_types_cfg.keys())
192225

193226
def component_type_ids(
194-
self, component_type: str, component_category: str | None = None
227+
self,
228+
component_type: str,
229+
component_category: str | None = None,
230+
metric: str = "",
195231
) -> list[int]:
196232
"""Get a list of all component IDs for a component type.
197233
@@ -200,6 +236,7 @@ def component_type_ids(
200236
component_category: Specific category of component IDs to retrieve
201237
(e.g., "meter", "inverter", or "component"). If not provided,
202238
the default logic is used.
239+
metric: Metric name of the formula if CIDs should be extracted from the formula.
203240
204241
Returns:
205242
List of component IDs for this component type.
@@ -222,7 +259,7 @@ def component_type_ids(
222259
category_ids = cast(list[int], getattr(cfg, component_category, []))
223260
return category_ids
224261

225-
return cfg.cids()
262+
return cfg.cids(metric)
226263

227264
def formula(self, component_type: str, metric: str) -> str:
228265
"""Get the formula for a component type.
@@ -235,16 +272,18 @@ def formula(self, component_type: str, metric: str) -> str:
235272
Formula to be used for this aggregated component as string.
236273
237274
Raises:
238-
ValueError: If the component type is unknown.
275+
ValueError: If the component type is unknown or formula is missing.
239276
"""
240277
cfg = self._component_types_cfg.get(component_type)
241278
if not cfg:
242279
raise ValueError(f"{component_type} not found in config.")
280+
if cfg.formula is None:
281+
raise ValueError(f"No formula set for {component_type}")
282+
formula = cfg.formula.get(metric)
283+
if not formula:
284+
raise ValueError(f"{component_type} is missing formula for {metric}")
243285

244-
if not cfg.has_formula_for(metric):
245-
raise ValueError(f"{metric} not supported for {component_type}")
246-
247-
return cfg.formula
286+
return formula
248287

249288
@staticmethod
250289
def load_configs(*paths: str) -> dict[str, "MicrogridConfig"]:

tests/test_config.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"1": {
1515
"meta": {"name": "Test Grid", "gid": 1},
1616
"ctype": {
17-
"pv": {"meter": [101, 102], "formula": "AC_ACTIVE_POWER"},
17+
"pv": {"meter": [101, 102], "formula": {"AC_ACTIVE_POWER": "#12+#23"}},
1818
"battery": {
1919
"inverter": [201, 202, 203],
2020
"component": [301, 302, 303, 304, 305, 306],
@@ -61,19 +61,6 @@ def test_component_type_config_cids() -> None:
6161
config.cids()
6262

6363

64-
def test_component_type_config_default_formula() -> None:
65-
"""Test that the default formula is generated correctly."""
66-
config = ComponentTypeConfig(component_type="pv", meter=[1, 2])
67-
assert config._default_formula() == "#1+#2" # pylint: disable=protected-access
68-
69-
70-
def test_component_type_config_has_formula_for() -> None:
71-
"""Test whether a component type has a valid formula for a metric."""
72-
config = ComponentTypeConfig(component_type="pv", formula="AC_ACTIVE_POWER")
73-
assert config.has_formula_for("AC_ACTIVE_POWER")
74-
assert not config.has_formula_for("INVALID_METRIC")
75-
76-
7764
def test_microgrid_config_init(valid_microgrid_config: MicrogridConfig) -> None:
7865
"""Test initialisation of MicrogridConfig with valid configuration data."""
7966
assert valid_microgrid_config.meta.name == "Test Grid"
@@ -117,7 +104,7 @@ def test_microgrid_config_component_type_ids(
117104

118105
def test_microgrid_config_formula(valid_microgrid_config: MicrogridConfig) -> None:
119106
"""Test retrieval of formula for a given component type and metric."""
120-
assert valid_microgrid_config.formula("pv", "AC_ACTIVE_POWER") == "AC_ACTIVE_POWER"
107+
assert valid_microgrid_config.formula("pv", "AC_ACTIVE_POWER") == "#12+#23"
121108

122109
with pytest.raises(ValueError):
123110
valid_microgrid_config.formula("pv", "INVALID_METRIC")

0 commit comments

Comments
 (0)