Skip to content

Commit b0cf5e2

Browse files
authored
More updates for n:m support - battery pool's power formula (#730)
This specifically focuses on the power forumla for the battery pool. * [x] test with no battery * [x] test with no inverter * [x] test with requests that don't specify all batteries behind the inverters refs #501
2 parents 23a141f + c26c518 commit b0cf5e2

File tree

5 files changed

+197
-44
lines changed

5 files changed

+197
-44
lines changed

RELEASE_NOTES.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- Allow multiplying and dividing any `Quantity` by a `float`. This just scales the `Quantity` value.
1414
- Allow dividing any `Quantity` by another quaintity of the same type. This just returns a ration between both quantities.
1515

16+
- The battery pool `power` method now supports scenarios where one or more inverters can have multiple batteries connected to it and one or more batteries can have multiple inverters connected to it.
17+
1618
## Bug Fixes
1719

1820
- Fix grid current formula generator to add the operator `+` to the engine only when the component category is handled.

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

+31-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._formula_generator import (
1313
NON_EXISTING_COMPONENT_ID,
1414
ComponentNotFound,
15+
FormulaGenerationError,
1516
FormulaGenerator,
1617
)
1718

@@ -40,6 +41,8 @@ def generate(
4041
they don't have an inverter as a predecessor.
4142
FormulaGenerationError: If a battery has a non-inverter predecessor
4243
in the component graph.
44+
FormulaGenerationError: If not all batteries behind a set of inverters
45+
have been requested.
4346
"""
4447
builder = self._get_builder(
4548
"battery-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
@@ -61,16 +64,39 @@ def generate(
6164

6265
component_graph = connection_manager.get().component_graph
6366

64-
battery_inverters = list(
65-
next(iter(component_graph.predecessors(bat_id))) for bat_id in component_ids
67+
battery_inverters = frozenset(
68+
frozenset(
69+
filter(
70+
component_graph.is_battery_inverter,
71+
component_graph.predecessors(bat_id),
72+
)
73+
)
74+
for bat_id in component_ids
6675
)
6776

68-
if len(component_ids) != len(battery_inverters):
77+
if not all(battery_inverters):
6978
raise ComponentNotFound(
70-
"Can't find inverters for all batteries from the component graph."
79+
"All batteries must have at least one inverter as a predecessor."
80+
)
81+
82+
all_connected_batteries = set()
83+
for inverters in battery_inverters:
84+
for inverter in inverters:
85+
all_connected_batteries.update(
86+
component_graph.successors(inverter.component_id)
87+
)
88+
89+
if len(all_connected_batteries) != len(component_ids):
90+
raise FormulaGenerationError(
91+
"All batteries behind a set of inverters must be requested."
7192
)
7293

73-
for idx, comp in enumerate(battery_inverters):
94+
builder.push_oper("(")
95+
builder.push_oper("(")
96+
# Iterate over the flattened list of inverters
97+
for idx, comp in enumerate(
98+
inverter for inverters in battery_inverters for inverter in inverters
99+
):
74100
if idx > 0:
75101
builder.push_oper("+")
76102
builder.push_component_metric(comp.component_id, nones_are_zeros=True)

tests/actor/power_distributing/test_power_distributing.py

+2-37
Original file line numberDiff line numberDiff line change
@@ -537,21 +537,7 @@ async def test_battery_two_inverters(self, mocker: MockerFixture) -> None:
537537
graph = gen.to_graph(
538538
(
539539
ComponentCategory.METER,
540-
[
541-
(
542-
ComponentCategory.METER,
543-
[
544-
(
545-
ComponentCategory.INVERTER,
546-
bat_component,
547-
),
548-
(
549-
ComponentCategory.INVERTER,
550-
bat_component,
551-
),
552-
],
553-
),
554-
],
540+
[gen.battery_with_inverter(bat_component, 2)],
555541
)
556542
)
557543

@@ -600,28 +586,7 @@ async def test_two_batteries_three_inverters(self, mocker: MockerFixture) -> Non
600586
batteries = gen.components(*[ComponentCategory.BATTERY] * 2)
601587

602588
graph = gen.to_graph(
603-
(
604-
ComponentCategory.METER,
605-
[
606-
(
607-
ComponentCategory.METER,
608-
[
609-
(
610-
ComponentCategory.INVERTER,
611-
[*batteries],
612-
),
613-
(
614-
ComponentCategory.INVERTER,
615-
[*batteries],
616-
),
617-
(
618-
ComponentCategory.INVERTER,
619-
[*batteries],
620-
),
621-
],
622-
),
623-
],
624-
)
589+
(ComponentCategory.METER, gen.batteries_with_inverter(batteries, 3))
625590
)
626591

627592
async with _mocks(mocker, graph=graph) as mocks:

tests/timeseries/_battery_pool/test_battery_pool.py

+105-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
)
4343
from frequenz.sdk.timeseries._base_types import SystemBounds
4444
from frequenz.sdk.timeseries.battery_pool import BatteryPool
45+
from frequenz.sdk.timeseries.formula_engine._formula_generators._formula_generator import (
46+
FormulaGenerationError,
47+
)
48+
from tests.utils.graph_generator import GraphGenerator
4549

4650
from ...timeseries.mock_microgrid import MockMicrogrid
4751
from ...utils.component_data_streamer import MockComponentDataStreamer
@@ -488,10 +492,110 @@ async def run_test_battery_status_channel( # pylint: disable=too-many-arguments
488492

489493

490494
async def test_battery_pool_power(mocker: MockerFixture) -> None:
491-
"""Test `BatteryPool.{,production,consumption}_power` methods."""
495+
"""Test `BatteryPool.power` method."""
492496
mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker)
493497
mockgrid.add_batteries(2)
498+
await mockgrid.start(mocker)
499+
await _test_battery_pool_power(mockgrid)
500+
501+
502+
async def test_battery_pool_power_two_inverters_per_battery(
503+
mocker: MockerFixture,
504+
) -> None:
505+
"""Test power method with two inverters per battery."""
506+
gen = GraphGenerator()
507+
bat = gen.component(ComponentCategory.BATTERY)
508+
mockgrid = MockMicrogrid(
509+
graph=gen.to_graph((ComponentCategory.METER, gen.battery_with_inverter(bat, 2)))
510+
)
511+
await mockgrid.start(mocker)
512+
await _test_battery_pool_power(mockgrid)
513+
514+
515+
async def test_batter_pool_power_two_batteries_per_inverter(
516+
mocker: MockerFixture,
517+
) -> None:
518+
"""Test power method with two batteries per inverter."""
519+
gen = GraphGenerator()
520+
mockgrid = MockMicrogrid(
521+
graph=gen.to_graph(
522+
[
523+
ComponentCategory.METER,
524+
(
525+
ComponentCategory.INVERTER,
526+
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
527+
),
528+
ComponentCategory.METER,
529+
(
530+
ComponentCategory.INVERTER,
531+
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
532+
),
533+
]
534+
)
535+
)
536+
await mockgrid.start(mocker)
537+
await _test_battery_pool_power(mockgrid)
538+
539+
540+
async def test_batter_pool_power_no_batteries(mocker: MockerFixture) -> None:
541+
"""Test power method with no batteries."""
542+
mockgrid = MockMicrogrid(
543+
graph=GraphGenerator().to_graph(
544+
(
545+
ComponentCategory.METER,
546+
[ComponentCategory.INVERTER, ComponentCategory.INVERTER],
547+
)
548+
)
549+
)
550+
await mockgrid.start(mocker)
551+
battery_pool = microgrid.battery_pool()
552+
power_receiver = battery_pool.power.new_receiver()
553+
554+
await mockgrid.mock_resampler.send_non_existing_component_value()
555+
assert (await power_receiver.receive()).value == Power.from_watts(0)
556+
557+
558+
async def test_battery_pool_power_with_no_inverters(mocker: MockerFixture) -> None:
559+
"""Test power method with no inverters."""
560+
mockgrid = MockMicrogrid(
561+
graph=GraphGenerator().to_graph(
562+
(ComponentCategory.METER, ComponentCategory.BATTERY)
563+
)
564+
)
565+
await mockgrid.start(mocker)
566+
567+
with pytest.raises(RuntimeError):
568+
microgrid.battery_pool()
569+
570+
571+
async def test_battery_pool_power_incomplete_bat_request(mocker: MockerFixture) -> None:
572+
"""Test power method when not all requested ids are behind the same inverter."""
573+
gen = GraphGenerator()
574+
bats = gen.components(
575+
ComponentCategory.BATTERY, ComponentCategory.BATTERY, ComponentCategory.BATTERY
576+
)
577+
578+
mockgrid = MockMicrogrid(
579+
graph=gen.to_graph(
580+
(
581+
ComponentCategory.METER,
582+
gen.batteries_with_inverter(bats, 2),
583+
)
584+
)
585+
)
586+
await mockgrid.start(mocker)
587+
588+
with pytest.raises(FormulaGenerationError):
589+
# Request only two of the three batteries behind the inverters
590+
battery_pool = microgrid.battery_pool(
591+
battery_ids=set([bats[1].component_id, bats[0].component_id])
592+
)
593+
power_receiver = battery_pool.power.new_receiver()
594+
await mockgrid.mock_resampler.send_bat_inverter_power([2.0])
595+
assert (await power_receiver.receive()).value == Power.from_watts(2.0)
596+
494597

598+
async def _test_battery_pool_power(mockgrid: MockMicrogrid) -> None:
495599
async with mockgrid:
496600
battery_pool = microgrid.battery_pool()
497601
power_receiver = battery_pool.power.new_receiver()

tests/utils/graph_generator.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,68 @@ def new_id(self) -> dict[ComponentCategory, int]:
5959
self._id_increment += 1
6060
return id_per_category
6161

62+
def battery_with_inverter(self, battery: Component, num_inverters: int) -> Any:
63+
"""Add a meter and inverters to the given battery.
64+
65+
Args:
66+
battery: the battery component.
67+
num_inverters: the number of inverters to create.
68+
69+
Returns:
70+
connected graph components for the given battery.
71+
"""
72+
assert battery.category == ComponentCategory.BATTERY
73+
return self._battery_with_inverter(battery, num_inverters)
74+
75+
def batteries_with_inverter(
76+
self, one_or_more_batteries: list[Component], num_inverters: int
77+
) -> Any:
78+
"""Add a meter and inverters to the given batteries.
79+
80+
Args:
81+
one_or_more_batteries: the battery components.
82+
num_inverters: the number of inverters to create.
83+
84+
Returns:
85+
connected graph components for the given batteries.
86+
"""
87+
assert all(
88+
b.category == ComponentCategory.BATTERY for b in one_or_more_batteries
89+
)
90+
return self._battery_with_inverter(one_or_more_batteries, num_inverters)
91+
92+
def _battery_with_inverter(
93+
self, one_or_more_batteries: Component | list[Component], num_inverters: int
94+
) -> Any:
95+
"""Add a meter and inverters to the given battery or batteries.
96+
97+
Args:
98+
one_or_more_batteries: the battery component or components.
99+
num_inverters: the number of inverters to create.
100+
101+
Returns:
102+
connected graph components for the given battery or batteries.
103+
"""
104+
return (
105+
ComponentCategory.METER,
106+
[
107+
(
108+
self.component(ComponentCategory.INVERTER, InverterType.BATTERY),
109+
one_or_more_batteries,
110+
)
111+
for _ in range(num_inverters)
112+
],
113+
)
114+
62115
@overload
63-
def component(self, other: Component) -> Component:
116+
def component(
117+
self, other: Component, comp_type: ComponentType | None = None
118+
) -> Component:
64119
"""Just return the given component.
65120
66121
Args:
67122
other: the component to return.
123+
comp_type: the component type to set, ignored
68124
69125
Returns:
70126
the given component.

0 commit comments

Comments
 (0)