Skip to content

Commit 0c61eb9

Browse files
Send 0 values from power formulas for non-existant component types (#151)
Earlier, the automatically generated formulas for `pv_power` and `battery_power` were raising exception in cases where pv or battery inverters were not found in the component graph, preventing us from having a single high level formula that can be reused in all locations. This is solved by subscribing to a non-existing component id, so that we can still send out 0 values at the same rate as the resampler.
2 parents 68f556d + 94d6c5a commit 0c61eb9

File tree

4 files changed

+99
-7
lines changed

4 files changed

+99
-7
lines changed

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_battery_power_formula.py

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

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

6+
import logging
7+
68
from .....sdk import microgrid
79
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
810
from .._formula_engine import FormulaEngine
9-
from ._formula_generator import ComponentNotFound, FormulaGenerator
11+
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
12+
13+
logger = logging.getLogger(__name__)
1014

1115

1216
class BatteryPowerFormula(FormulaGenerator):
@@ -42,9 +46,18 @@ async def generate(
4246
)
4347

4448
if not battery_inverters:
45-
raise ComponentNotFound(
46-
"Unable to find any battery inverters in the component graph."
49+
logging.warning(
50+
"Unable to find any battery inverters in the component graph. "
51+
"Subscribing to the resampling actor with a non-existing "
52+
"component id, so that `0` values are sent from the formula."
53+
)
54+
# If there are no battery inverters, we have to send 0 values as the same
55+
# frequency as the other streams. So we subscribe with a non-existing
56+
# component id, just to get a `None` message at the resampling interval.
57+
await builder.push_component_metric(
58+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
4759
)
60+
return builder.build()
4861

4962
for idx, comp in enumerate(battery_inverters):
5063
if idx > 0:

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_formula_generator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""Base class for formula generators that use the component graphs."""
55

6+
import sys
67
from abc import ABC, abstractmethod
78

89
from frequenz.channels import Sender
@@ -21,6 +22,9 @@ class ComponentNotFound(FormulaGenerationError):
2122
"""Indicates that a component required for generating a formula is not found."""
2223

2324

25+
NON_EXISTING_COMPONENT_ID = sys.maxsize
26+
27+
2428
class FormulaGenerator(ABC):
2529
"""A class for generating formulas from the component graph."""
2630

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

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

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

6+
import logging
7+
68
from .....sdk import microgrid
79
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
810
from .._formula_engine import FormulaEngine
9-
from ._formula_generator import ComponentNotFound, FormulaGenerator
11+
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
12+
13+
logger = logging.getLogger(__name__)
1014

1115

1216
class PVPowerFormula(FormulaGenerator):
@@ -32,9 +36,18 @@ async def generate(self) -> FormulaEngine:
3236
)
3337

3438
if not pv_inverters:
35-
raise ComponentNotFound(
36-
"Unable to find any PV inverters in the component graph."
39+
logging.warning(
40+
"Unable to find any PV inverters in the component graph. "
41+
"Subscribing to the resampling actor with a non-existing "
42+
"component id, so that `0` values are sent from the formula."
43+
)
44+
# If there are no PV inverters, we have to send 0 values as the same
45+
# frequency as the other streams. So we subscribe with a non-existing
46+
# component id, just to get a `None` message at the resampling interval.
47+
await builder.push_component_metric(
48+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
3749
)
50+
return builder.build()
3851

3952
for idx, comp in enumerate(pv_inverters):
4053
if idx > 0:

tests/timeseries/test_logical_meter.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ async def test_formula_composition( # pylint: disable=too-many-locals
253253
self,
254254
mocker: MockerFixture,
255255
) -> None:
256-
"""Test the battery power and pv power formulas."""
256+
"""Test the composition of formulas."""
257257
mockgrid = await MockMicrogrid.new(mocker, grid_side_meter=False)
258258
mockgrid.add_batteries(3)
259259
mockgrid.add_solar_inverters(2)
@@ -300,3 +300,65 @@ async def test_formula_composition( # pylint: disable=too-many-locals
300300
await engine._stop() # pylint: disable=protected-access
301301

302302
assert count == 10
303+
304+
async def test_formula_composition_missing_pv(self, mocker: MockerFixture) -> None:
305+
"""Test the composition of formulas with missing PV power data."""
306+
mockgrid = await MockMicrogrid.new(mocker, grid_side_meter=False)
307+
mockgrid.add_batteries(3)
308+
request_sender, channel_registry = await mockgrid.start()
309+
logical_meter = LogicalMeter(
310+
channel_registry,
311+
request_sender,
312+
microgrid.get().component_graph,
313+
)
314+
315+
battery_power_recv = await logical_meter.battery_power()
316+
pv_power_recv = await logical_meter.pv_power()
317+
engine = (pv_power_recv.clone() + battery_power_recv.clone()).build("inv_power")
318+
inv_calc_recv = engine.new_receiver()
319+
320+
count = 0
321+
for _ in range(10):
322+
bat_pow = await battery_power_recv.receive()
323+
pv_pow = await pv_power_recv.receive()
324+
inv_pow = await inv_calc_recv.receive()
325+
326+
assert inv_pow == bat_pow
327+
assert pv_pow.timestamp == inv_pow.timestamp and pv_pow.value == 0.0
328+
count += 1
329+
330+
await mockgrid.cleanup()
331+
await engine._stop() # pylint: disable=protected-access
332+
333+
assert count == 10
334+
335+
async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> None:
336+
"""Test the composition of formulas with missing battery power data."""
337+
mockgrid = await MockMicrogrid.new(mocker, grid_side_meter=False)
338+
mockgrid.add_solar_inverters(2)
339+
request_sender, channel_registry = await mockgrid.start()
340+
logical_meter = LogicalMeter(
341+
channel_registry,
342+
request_sender,
343+
microgrid.get().component_graph,
344+
)
345+
346+
battery_power_recv = await logical_meter.battery_power()
347+
pv_power_recv = await logical_meter.pv_power()
348+
engine = (pv_power_recv.clone() + battery_power_recv.clone()).build("inv_power")
349+
inv_calc_recv = engine.new_receiver()
350+
351+
count = 0
352+
for _ in range(10):
353+
bat_pow = await battery_power_recv.receive()
354+
pv_pow = await pv_power_recv.receive()
355+
inv_pow = await inv_calc_recv.receive()
356+
357+
assert inv_pow == pv_pow
358+
assert bat_pow.timestamp == inv_pow.timestamp and bat_pow.value == 0.0
359+
count += 1
360+
361+
await mockgrid.cleanup()
362+
await engine._stop() # pylint: disable=protected-access
363+
364+
assert count == 10

0 commit comments

Comments
 (0)