Skip to content

Commit 8431b2f

Browse files
committed
Improve target_power calculation and handling of overlapping bounds
When actors with different priorities have not completely-contained, but overlapping bounds, use the intersection to find the target_power closest to the requirements of the lowest priority actor. This commit also adds some test cases and updates the test to run directly on a `Matryoshka()` instance, rather than through a `PowerManagingActor` instance. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent ff00d27 commit 8431b2f

File tree

2 files changed

+58
-69
lines changed

2 files changed

+58
-69
lines changed

src/frequenz/sdk/actor/_power_managing/_matryoshka.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,17 @@ def get_target_power(
9999

100100
target_power = Power.zero()
101101
for next_proposal in reversed(proposals):
102-
if (
103-
next_proposal.preferred_power > upper_bound
104-
or next_proposal.preferred_power < lower_bound
105-
):
106-
continue
107-
target_power = next_proposal.preferred_power
102+
if upper_bound < lower_bound:
103+
break
104+
if next_proposal.preferred_power > upper_bound:
105+
target_power = upper_bound
106+
elif next_proposal.preferred_power < lower_bound:
107+
target_power = lower_bound
108+
else:
109+
target_power = next_proposal.preferred_power
108110
if next_proposal.bounds:
109-
lower_bound = next_proposal.bounds[0]
110-
upper_bound = next_proposal.bounds[1]
111+
lower_bound = max(lower_bound, next_proposal.bounds[0])
112+
upper_bound = min(upper_bound, next_proposal.bounds[1])
111113

112114
if (
113115
battery_ids not in self._target_power
@@ -146,8 +148,8 @@ def get_status(
146148
if next_proposal.priority <= priority:
147149
break
148150
if next_proposal.bounds:
149-
lower_bound = next_proposal.bounds[0]
150-
upper_bound = next_proposal.bounds[1]
151+
lower_bound = max(lower_bound, next_proposal.bounds[0])
152+
upper_bound = min(upper_bound, next_proposal.bounds[1])
151153

152154
return Report(
153155
target_power=self._target_power.get(battery_ids, Power.zero()),
Lines changed: 46 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,72 @@
11
# License: MIT
22
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
33

4-
"""Tests for the power managing actor."""
4+
"""Tests for the Matryoshka power manager algorithm."""
55

66
from datetime import datetime, timezone
7-
from unittest.mock import AsyncMock
87

9-
from frequenz.channels import Broadcast
10-
from pytest_mock import MockerFixture
11-
12-
from frequenz.sdk.actor._channel_registry import ChannelRegistry
13-
from frequenz.sdk.actor._power_managing import Proposal, ReportRequest
14-
from frequenz.sdk.actor._power_managing._power_managing_actor import PowerManagingActor
15-
from frequenz.sdk.actor.power_distributing.request import Request
8+
from frequenz.sdk.actor._power_managing import Proposal
9+
from frequenz.sdk.actor._power_managing._matryoshka import Matryoshka
1610
from frequenz.sdk.timeseries import Power, battery_pool
1711

1812

19-
async def test_power_managing_actor_matryoshka(mocker: MockerFixture) -> None:
13+
async def test_matryoshka_algorithm() -> None:
2014
"""Tests for the power managing actor."""
21-
input_channel = Broadcast[Proposal]("power managing proposals")
22-
output_channel = Broadcast[Request]("Power managing outputs")
23-
power_bounds_sub_channel = Broadcast[ReportRequest]("power bounds subscription")
24-
channel_registry = ChannelRegistry(name="test_channel_registry")
25-
input_tx = input_channel.new_sender()
26-
output_rx = output_channel.new_receiver()
27-
2815
batteries = frozenset({2, 5})
2916

30-
mocker.patch(
31-
"frequenz.sdk.actor._power_managing._power_managing_actor"
32-
".PowerManagingActor._add_bounds_tracker",
33-
new_callable=AsyncMock,
17+
algorithm = Matryoshka()
18+
system_bounds = battery_pool.PowerMetrics(
19+
timestamp=datetime.now(tz=timezone.utc),
20+
inclusion_bounds=battery_pool.Bounds(
21+
lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0)
22+
),
23+
exclusion_bounds=battery_pool.Bounds(lower=Power.zero(), upper=Power.zero()),
3424
)
3525

36-
async def case(
37-
*,
26+
call_count = 0
27+
28+
def case(
3829
priority: int,
3930
power: float,
40-
bounds: tuple[float, float],
31+
bounds: tuple[float, float] | None,
4132
expected: float | None,
4233
) -> None:
43-
await input_tx.send(
34+
nonlocal call_count
35+
call_count += 1
36+
tgt_power = algorithm.get_target_power(
37+
batteries,
4438
Proposal(
4539
battery_ids=batteries,
4640
source_id=f"actor-{priority}",
4741
preferred_power=Power.from_watts(power),
48-
bounds=(Power.from_watts(bounds[0]), Power.from_watts(bounds[1])),
42+
bounds=(Power.from_watts(bounds[0]), Power.from_watts(bounds[1]))
43+
if bounds
44+
else None,
4945
priority=priority,
50-
)
51-
)
52-
if expected is not None:
53-
assert (await output_rx.receive()).power.as_watts() == expected
54-
55-
async with PowerManagingActor(
56-
input_channel.new_receiver(),
57-
power_bounds_sub_channel.new_receiver(),
58-
output_channel.new_sender(),
59-
channel_registry,
60-
) as powmgract:
61-
powmgract._system_bounds[ # pylint: disable=protected-access
62-
batteries
63-
] = battery_pool.PowerMetrics(
64-
timestamp=datetime.now(tz=timezone.utc),
65-
inclusion_bounds=battery_pool.Bounds(
66-
lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0)
67-
),
68-
exclusion_bounds=battery_pool.Bounds(
69-
lower=Power.zero(), upper=Power.zero()
7046
),
47+
system_bounds,
7148
)
49+
assert tgt_power == (
50+
Power.from_watts(expected) if expected is not None else None
51+
)
52+
53+
case(priority=2, power=25.0, bounds=(25.0, 50.0), expected=25.0)
54+
case(priority=1, power=20.0, bounds=(20.0, 50.0), expected=None)
55+
case(priority=3, power=10.0, bounds=(10.0, 15.0), expected=15.0)
56+
case(priority=3, power=10.0, bounds=(10.0, 22.0), expected=22.0)
57+
case(priority=1, power=30.0, bounds=(20.0, 50.0), expected=None)
58+
case(priority=3, power=10.0, bounds=(10.0, 50.0), expected=30.0)
59+
case(priority=2, power=40.0, bounds=(40.0, 50.0), expected=40.0)
60+
case(priority=2, power=0.0, bounds=(-200.0, 200.0), expected=30.0)
61+
case(priority=4, power=-50.0, bounds=(-200.0, -50.0), expected=-50.0)
62+
case(priority=3, power=-0.0, bounds=(-200.0, 200.0), expected=None)
63+
case(priority=1, power=-150.0, bounds=(-200.0, -150.0), expected=-150.0)
64+
case(priority=4, power=-180.0, bounds=(-200.0, -50.0), expected=None)
65+
case(priority=4, power=50.0, bounds=(50.0, 200.0), expected=50.0)
7266

73-
await case(priority=2, power=25.0, bounds=(25.0, 50.0), expected=25.0)
74-
await case(priority=1, power=20.0, bounds=(20.0, 50.0), expected=None)
75-
await case(priority=3, power=10.0, bounds=(10.0, 15.0), expected=10.0)
76-
await case(priority=3, power=10.0, bounds=(10.0, 22.0), expected=20.0)
77-
await case(priority=1, power=30.0, bounds=(20.0, 50.0), expected=10.0)
78-
await case(priority=3, power=10.0, bounds=(10.0, 50.0), expected=30.0)
79-
await case(priority=2, power=40.0, bounds=(40.0, 50.0), expected=40.0)
80-
await case(priority=2, power=0.0, bounds=(-200.0, 200.0), expected=30.0)
81-
await case(priority=4, power=-50.0, bounds=(-200.0, -50.0), expected=-50.0)
82-
await case(priority=3, power=-0.0, bounds=(-200.0, 200.0), expected=None)
83-
await case(priority=1, power=-150.0, bounds=(-200.0, -150.0), expected=-150.0)
84-
await case(priority=4, power=-180.0, bounds=(-200.0, -50.0), expected=None)
85-
await case(priority=4, power=50.0, bounds=(50.0, 200.0), expected=50.0)
67+
case(priority=4, power=0.0, bounds=(-200.0, 200.0), expected=-150.0)
68+
case(priority=3, power=0.0, bounds=(-200.0, 200.0), expected=None)
69+
case(priority=2, power=50.0, bounds=(-100, 100), expected=-100.0)
70+
case(priority=1, power=100.0, bounds=(100, 200), expected=100.0)
71+
case(priority=1, power=50.0, bounds=(50, 200), expected=50.0)
72+
case(priority=1, power=200.0, bounds=(50, 200), expected=100.0)

0 commit comments

Comments
 (0)