Skip to content

Commit 3d23371

Browse files
committed
Add n:m test for two batteries with different exclusion bounds
Both connected to the same inverter. Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent e609f7a commit 3d23371

File tree

2 files changed

+188
-12
lines changed

2 files changed

+188
-12
lines changed

src/frequenz/sdk/actor/power_distributing/power_distributing.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,19 +225,21 @@ def _get_bounds(
225225
)
226226
for battery, inverters in pairs_data
227227
),
228-
exclusion_lower=sum(
229-
min(
230-
battery.power_exclusion_lower_bound,
231-
inverter.active_power_exclusion_lower_bound,
232-
)
233-
for battery, inverter in pairs_data
228+
exclusion_lower=min(
229+
sum(battery.power_exclusion_lower_bound for battery, _ in pairs_data),
230+
sum(
231+
inverter.active_power_exclusion_lower_bound
232+
for _, inverters in pairs_data
233+
for inverter in inverters
234+
),
234235
),
235-
exclusion_upper=sum(
236-
max(
237-
battery.power_exclusion_upper_bound,
238-
inverter.active_power_exclusion_upper_bound,
239-
)
240-
for battery, inverter in pairs_data
236+
exclusion_upper=max(
237+
sum(battery.power_exclusion_upper_bound for battery, _ in pairs_data),
238+
sum(
239+
inverter.active_power_exclusion_upper_bound
240+
for _, inverters in pairs_data
241+
for inverter in inverters
242+
),
241243
),
242244
)
243245

tests/actor/power_distributing/test_power_distributing.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,180 @@ async def test_two_batteries_three_inverters(self, mocker: MockerFixture) -> Non
549549
assert result.excess_power.isclose(Power.from_watts(200.0))
550550
assert result.request == request
551551

552+
async def test_two_batteries_one_inverter_different_exclusion_bounds_2(
553+
self, mocker: MockerFixture
554+
) -> None:
555+
"""Test power distribution with two batteries having different exclusion bounds.
556+
557+
Test if power distribution works with two batteries connected to one
558+
inverter, each having different exclusion bounds.
559+
"""
560+
gen = GraphGenerator()
561+
batteries = gen.components(*[ComponentCategory.BATTERY] * 2)
562+
inverter = gen.component(ComponentCategory.INVERTER)
563+
564+
graph = gen.to_graph(
565+
(
566+
ComponentCategory.METER,
567+
(ComponentCategory.METER, [(inverter, [*batteries])]),
568+
)
569+
)
570+
571+
mockgrid = MockMicrogrid(graph=graph)
572+
await mockgrid.start(mocker)
573+
await self.init_component_data(mockgrid)
574+
await mockgrid.mock_client.send(
575+
inverter_msg(
576+
inverter.component_id,
577+
power=PowerBounds(-1000, -500, 500, 1000),
578+
)
579+
)
580+
await mockgrid.mock_client.send(
581+
battery_msg(
582+
batteries[0].component_id,
583+
soc=Metric(40, Bound(20, 80)),
584+
capacity=Metric(10_000),
585+
power=PowerBounds(-1000, -200, 200, 1000),
586+
)
587+
)
588+
await mockgrid.mock_client.send(
589+
battery_msg(
590+
batteries[1].component_id,
591+
soc=Metric(40, Bound(20, 80)),
592+
capacity=Metric(10_000),
593+
power=PowerBounds(-1000, -100, 100, 1000),
594+
)
595+
)
596+
597+
channel = Broadcast[Request]("power_distributor")
598+
channel_registry = ChannelRegistry(name="power_distributor")
599+
600+
request = Request(
601+
namespace=self._namespace,
602+
power=Power.from_watts(300.0),
603+
batteries={batteries[0].component_id, batteries[1].component_id},
604+
request_timeout=SAFETY_TIMEOUT,
605+
)
606+
607+
attrs = {"get_working_batteries.return_value": request.batteries}
608+
609+
mocker.patch(
610+
"frequenz.sdk.actor.power_distributing.power_distributing.BatteryPoolStatus",
611+
return_value=MagicMock(spec=BatteryPoolStatus, **attrs),
612+
)
613+
614+
mocker.patch("asyncio.sleep", new_callable=AsyncMock)
615+
battery_status_channel = Broadcast[BatteryStatus]("battery_status")
616+
617+
async with PowerDistributingActor(
618+
requests_receiver=channel.new_receiver(),
619+
channel_registry=channel_registry,
620+
battery_status_sender=battery_status_channel.new_sender(),
621+
):
622+
await channel.new_sender().send(request)
623+
result_rx = channel_registry.new_receiver(self._namespace)
624+
625+
done, pending = await asyncio.wait(
626+
[asyncio.create_task(result_rx.receive())],
627+
timeout=SAFETY_TIMEOUT.total_seconds(),
628+
)
629+
630+
assert len(pending) == 0
631+
assert len(done) == 1
632+
633+
result: Result = done.pop().result()
634+
assert isinstance(result, OutOfBounds)
635+
assert result.request == request
636+
assert result.bounds == PowerBounds(-1000, -500, 500, 1000)
637+
638+
async def test_two_batteries_one_inverter_different_exclusion_bounds(
639+
self, mocker: MockerFixture
640+
) -> None:
641+
"""Test power distribution with two batteries having different exclusion bounds.
642+
643+
Test if power distribution works with two batteries connected to one
644+
inverter, each having different exclusion bounds.
645+
"""
646+
gen = GraphGenerator()
647+
batteries = gen.components(*[ComponentCategory.BATTERY] * 2)
648+
649+
graph = gen.to_graph(
650+
(
651+
ComponentCategory.METER,
652+
(
653+
ComponentCategory.METER,
654+
[
655+
(
656+
ComponentCategory.INVERTER,
657+
[*batteries],
658+
),
659+
],
660+
),
661+
)
662+
)
663+
664+
mockgrid = MockMicrogrid(graph=graph)
665+
await mockgrid.start(mocker)
666+
await self.init_component_data(mockgrid)
667+
await mockgrid.mock_client.send(
668+
battery_msg(
669+
batteries[0].component_id,
670+
soc=Metric(40, Bound(20, 80)),
671+
capacity=Metric(10_000),
672+
power=PowerBounds(-1000, -200, 200, 1000),
673+
)
674+
)
675+
await mockgrid.mock_client.send(
676+
battery_msg(
677+
batteries[1].component_id,
678+
soc=Metric(40, Bound(20, 80)),
679+
capacity=Metric(10_000),
680+
power=PowerBounds(-1000, -100, 100, 1000),
681+
)
682+
)
683+
684+
channel = Broadcast[Request]("power_distributor")
685+
channel_registry = ChannelRegistry(name="power_distributor")
686+
687+
request = Request(
688+
namespace=self._namespace,
689+
power=Power.from_watts(300.0),
690+
batteries={batteries[0].component_id, batteries[1].component_id},
691+
request_timeout=SAFETY_TIMEOUT,
692+
)
693+
694+
attrs = {"get_working_batteries.return_value": request.batteries}
695+
696+
mocker.patch(
697+
"frequenz.sdk.actor.power_distributing.power_distributing.BatteryPoolStatus",
698+
return_value=MagicMock(spec=BatteryPoolStatus, **attrs),
699+
)
700+
701+
mocker.patch("asyncio.sleep", new_callable=AsyncMock)
702+
battery_status_channel = Broadcast[BatteryStatus]("battery_status")
703+
704+
async with PowerDistributingActor(
705+
requests_receiver=channel.new_receiver(),
706+
channel_registry=channel_registry,
707+
battery_status_sender=battery_status_channel.new_sender(),
708+
):
709+
await channel.new_sender().send(request)
710+
result_rx = channel_registry.new_receiver(self._namespace)
711+
712+
done, pending = await asyncio.wait(
713+
[asyncio.create_task(result_rx.receive())],
714+
timeout=SAFETY_TIMEOUT.total_seconds(),
715+
)
716+
717+
assert len(pending) == 0
718+
assert len(done) == 1
719+
720+
result: Result = done.pop().result()
721+
assert isinstance(result, OutOfBounds)
722+
assert result.request == request
723+
# each inverter is bounded at 500
724+
assert result.bounds == PowerBounds(-500, -400, 400, 500)
725+
552726
async def test_connected_but_not_requested_batteries(
553727
self, mocker: MockerFixture
554728
) -> None:

0 commit comments

Comments
 (0)