Skip to content

Commit 10a763b

Browse files
Improve formula generators (#599)
We use the recently introduced DFS in the component graph to update the pv power and the producer power formule in the logical meter.
2 parents 1186f6f + b08ed58 commit 10a763b

File tree

6 files changed

+73
-76
lines changed

6 files changed

+73
-76
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@
2222
- Fix `consumer_power()` not working certain configurations.
2323
In microgrids without consumers and no main meter, the formula
2424
would never return any values.
25+
- Fix `pv_power` not working in setups with 2 grid meters by using a new
26+
reliable function to search for components in the components graph
27+
- Fix `consumer_power` similar to `pv_power`

src/frequenz/sdk/microgrid/_graph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def dfs(
276276
"""
277277
Search for components that fulfill the condition in the Graph.
278278
279-
DFS is used for searching the graph. The graph travarsal is stopped
279+
DFS is used for searching the graph. The graph traversal is stopped
280280
once a component fulfills the condition.
281281
282282
Args:
@@ -285,7 +285,7 @@ def dfs(
285285
condition: The condition function to check for.
286286
287287
Returns:
288-
A set of component ids where the coresponding components fulfill
288+
A set of component ids where the corresponding components fulfill
289289
the condition function.
290290
"""
291291

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_producer_power_formula.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33

44
"""Formula generator from component graph for Producer Power."""
55

6-
from __future__ import annotations
6+
import logging
77

88
from ....microgrid import connection_manager
99
from ....microgrid.component import ComponentCategory, ComponentMetricId
1010
from ..._quantities import Power
1111
from .._formula_engine import FormulaEngine
1212
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
1313

14+
_logger = logging.getLogger(__name__)
15+
1416

1517
class ProducerPowerFormula(FormulaGenerator[Power]):
1618
"""Formula generator from component graph for calculating the Producer Power.
@@ -34,36 +36,37 @@ def generate(self) -> FormulaEngine[Power]:
3436
builder = self._get_builder(
3537
"producer_power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
3638
)
37-
component_graph = connection_manager.get().component_graph
38-
grid_successors = self._get_grid_component_successors()
39-
40-
if len(grid_successors) == 1:
41-
grid_meter = next(iter(grid_successors))
42-
if grid_meter.category != ComponentCategory.METER:
43-
raise RuntimeError(
44-
"Only grid successor in the component graph is not a meter."
45-
)
46-
grid_successors = component_graph.successors(grid_meter.component_id)
4739

48-
first_iteration = True
49-
for successor in iter(grid_successors):
50-
# if in the future we support additional producers, we need to add them here
51-
if component_graph.is_chp_chain(successor) or component_graph.is_pv_chain(
52-
successor
53-
):
54-
if not first_iteration:
55-
builder.push_oper("+")
40+
component_graph = connection_manager.get().component_graph
41+
# if in the future we support additional producers, we need to add them to the lambda
42+
producer_components = component_graph.dfs(
43+
self._get_grid_component(),
44+
set(),
45+
lambda component: component_graph.is_pv_chain(component)
46+
or component_graph.is_chp_chain(component),
47+
)
5648

57-
first_iteration = False
49+
if not producer_components:
50+
_logger.warning(
51+
"Unable to find any producer components in the component graph. "
52+
"Subscribing to the resampling actor with a non-existing "
53+
"component id, so that `0` values are sent from the formula."
54+
)
55+
# If there are no producer components, we have to send 0 values at the same
56+
# frequency as the other streams. So we subscribe with a non-existing
57+
# component id, just to get a `None` message at the resampling interval.
58+
builder.push_component_metric(
59+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
60+
)
61+
return builder.build()
5862

59-
builder.push_component_metric(
60-
successor.component_id,
61-
nones_are_zeros=successor.category != ComponentCategory.METER,
62-
)
63+
for idx, component in enumerate(producer_components):
64+
if idx > 0:
65+
builder.push_oper("+")
6366

64-
if first_iteration:
6567
builder.push_component_metric(
66-
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
68+
component.component_id,
69+
nones_are_zeros=component.category != ComponentCategory.METER,
6770
)
6871

6972
return builder.build()

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_pv_power_formula.py

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

44
"""Formula generator for PV Power, from the component graph."""
55

6-
from __future__ import annotations
7-
86
import logging
9-
from collections import abc
107

118
from ....microgrid import connection_manager
129
from ....microgrid.component import ComponentCategory, ComponentMetricId
@@ -33,14 +30,20 @@ def generate(self) -> FormulaEngine[Power]:
3330
"pv-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
3431
)
3532

36-
pv_components = self._get_pv_power_components()
33+
component_graph = connection_manager.get().component_graph
34+
pv_components = component_graph.dfs(
35+
self._get_grid_component(),
36+
set(),
37+
component_graph.is_pv_chain,
38+
)
39+
3740
if not pv_components:
3841
_logger.warning(
39-
"Unable to find any PV inverters in the component graph. "
42+
"Unable to find any PV components in the component graph. "
4043
"Subscribing to the resampling actor with a non-existing "
4144
"component id, so that `0` values are sent from the formula."
4245
)
43-
# If there are no PV inverters, we have to send 0 values at the same
46+
# If there are no PV components, we have to send 0 values at the same
4447
# frequency as the other streams. So we subscribe with a non-existing
4548
# component id, just to get a `None` message at the resampling interval.
4649
builder.push_component_metric(
@@ -50,11 +53,15 @@ def generate(self) -> FormulaEngine[Power]:
5053

5154
builder.push_oper("(")
5255
builder.push_oper("(")
53-
for idx, comp_id in enumerate(pv_components):
56+
for idx, component in enumerate(pv_components):
5457
if idx > 0:
5558
builder.push_oper("+")
5659

57-
builder.push_component_metric(comp_id, nones_are_zeros=True)
60+
# should only be the case if the component is not a meter
61+
builder.push_component_metric(
62+
component.component_id,
63+
nones_are_zeros=component.category != ComponentCategory.METER,
64+
)
5865
builder.push_oper(")")
5966
if self._config.formula_type == FormulaType.PRODUCTION:
6067
builder.push_oper("*")
@@ -65,31 +72,3 @@ def generate(self) -> FormulaEngine[Power]:
6572
builder.push_clipper(0.0, None)
6673

6774
return builder.build()
68-
69-
def _get_pv_power_components(self) -> abc.Set[int]:
70-
"""Get the component ids of the PV inverters or meters in the component graph.
71-
72-
Returns:
73-
A set of component ids of the PV inverters or meters in the component graph.
74-
75-
Raises:
76-
RuntimeError: if the grid component has no PV inverters or meters as successors.
77-
"""
78-
component_graph = connection_manager.get().component_graph
79-
grid_successors = self._get_grid_component_successors()
80-
81-
if len(grid_successors) == 1:
82-
successor = next(iter(grid_successors))
83-
if successor.category != ComponentCategory.METER:
84-
raise RuntimeError(
85-
"Only grid successor in the component graph is not a meter."
86-
)
87-
grid_successors = component_graph.successors(successor.component_id)
88-
89-
pv_meters_or_inverters: set[int] = set()
90-
91-
for successor in grid_successors:
92-
if component_graph.is_pv_chain(successor):
93-
pv_meters_or_inverters.add(successor.component_id)
94-
95-
return pv_meters_or_inverters

tests/timeseries/mock_microgrid.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def add_consumer_meters(self, count: int = 1) -> None:
249249
self._connections.add(Connection(self._connect_to, meter_id))
250250
self._start_meter_streaming(meter_id)
251251

252-
def add_chps(self, count: int) -> None:
252+
def add_chps(self, count: int, no_meters: bool = False) -> None:
253253
"""Add CHPs with connected meters to the mock microgrid.
254254
255255
Args:
@@ -263,12 +263,13 @@ def add_chps(self, count: int) -> None:
263263
self.meter_ids.append(meter_id)
264264
self.chp_ids.append(chp_id)
265265

266-
self._components.add(
267-
Component(
268-
meter_id,
269-
ComponentCategory.METER,
266+
if not no_meters:
267+
self._components.add(
268+
Component(
269+
meter_id,
270+
ComponentCategory.METER,
271+
)
270272
)
271-
)
272273
self._components.add(
273274
Component(
274275
chp_id,
@@ -277,8 +278,11 @@ def add_chps(self, count: int) -> None:
277278
)
278279

279280
self._start_meter_streaming(meter_id)
280-
self._connections.add(Connection(self._connect_to, meter_id))
281-
self._connections.add(Connection(meter_id, chp_id))
281+
if no_meters:
282+
self._connections.add(Connection(self._connect_to, chp_id))
283+
else:
284+
self._connections.add(Connection(self._connect_to, meter_id))
285+
self._connections.add(Connection(meter_id, chp_id))
282286

283287
def add_batteries(self, count: int, no_meter: bool = False) -> None:
284288
"""Add batteries with connected inverters and meters to the microgrid.

tests/timeseries/test_logical_meter.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,17 @@ async def test_pv_power_no_meter(self, mocker: MockerFixture) -> None:
240240
await pv_consumption_power_receiver.receive()
241241
).value == Power.from_watts(0.0)
242242

243+
async def test_pv_power_no_pv_components(self, mocker: MockerFixture) -> None:
244+
"""Test the pv power formula without having any pv components."""
245+
mockgrid = MockMicrogrid(grid_meter=True)
246+
await mockgrid.start(mocker)
247+
248+
logical_meter = microgrid.logical_meter()
249+
pv_power_receiver = logical_meter.pv_power.new_receiver()
250+
251+
await mockgrid.mock_resampler.send_non_existing_component_value()
252+
assert (await pv_power_receiver.receive()).value == Power.zero()
253+
243254
async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None:
244255
"""Test the consumer power formula with a grid meter."""
245256
mockgrid = MockMicrogrid(grid_meter=True)
@@ -313,15 +324,12 @@ async def test_producer_power_no_pv_no_consumer_meter(
313324
) -> None:
314325
"""Test the producer power formula without pv and without consumer meter."""
315326
mockgrid = MockMicrogrid(grid_meter=False)
316-
mockgrid.add_chps(1)
327+
mockgrid.add_chps(1, True)
317328
await mockgrid.start(mocker)
318329

319330
logical_meter = microgrid.logical_meter()
320331
producer_power_receiver = logical_meter.producer_power.new_receiver()
321332

322-
# As there is only one meter in the microgrid, the formula interprets it
323-
# as main meter instead of chp meter, so it reads the power from the
324-
# chp component directly.
325333
await mockgrid.mock_resampler.send_chp_power([2.0])
326334
assert (await producer_power_receiver.receive()).value == Power.from_watts(2.0)
327335

0 commit comments

Comments
 (0)