Skip to content

Commit 201f3d3

Browse files
committed
Accept Bounds object in set_power, instead of separate values
This allows users to specify `None` when they want to set bounds only in one side, for example, if they only want a minimum charger power of 10kW, and don't care about the maximum. This commit also makes the `Bounds` type generic without restrictions, so that it can be specialized for more complex types like `Bounds[Power | None]`. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 6fc4ca3 commit 201f3d3

File tree

6 files changed

+41
-30
lines changed

6 files changed

+41
-30
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class Proposal:
7979

8080
source_id: str
8181
preferred_power: Power | None
82-
bounds: tuple[Power, Power] | None
82+
bounds: timeseries.Bounds[Power | None]
8383
battery_ids: frozenset[int]
8484
priority: int
8585
request_timeout: datetime.timedelta = datetime.timedelta(seconds=5.0)

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ def _calc_target_power(
6969
target_power = lower_bound
7070
else:
7171
target_power = next_proposal.preferred_power
72-
if next_proposal.bounds:
73-
lower_bound = max(lower_bound, next_proposal.bounds[0])
74-
upper_bound = min(upper_bound, next_proposal.bounds[1])
72+
low, high = next_proposal.bounds.lower, next_proposal.bounds.upper
73+
if low is not None:
74+
lower_bound = max(lower_bound, low)
75+
if high is not None:
76+
upper_bound = min(upper_bound, high)
7577

7678
return target_power
7779

@@ -176,15 +178,18 @@ def get_status(
176178
for next_proposal in reversed(self._battery_buckets.get(battery_ids, [])):
177179
if next_proposal.priority <= priority:
178180
break
179-
if next_proposal.bounds:
180-
calc_lower_bound = max(lower_bound, next_proposal.bounds[0])
181-
calc_upper_bound = min(upper_bound, next_proposal.bounds[1])
182-
if calc_lower_bound <= calc_upper_bound:
183-
lower_bound = calc_lower_bound
184-
upper_bound = calc_upper_bound
185-
else:
186-
break
187-
181+
low, high = next_proposal.bounds.lower, next_proposal.bounds.upper
182+
calc_lower_bound = lower_bound
183+
calc_upper_bound = upper_bound
184+
if low is not None:
185+
calc_lower_bound = max(calc_lower_bound, low)
186+
if high is not None:
187+
calc_upper_bound = min(calc_upper_bound, high)
188+
if calc_lower_bound <= calc_upper_bound:
189+
lower_bound = calc_lower_bound
190+
upper_bound = calc_upper_bound
191+
else:
192+
break
188193
return Report(
189194
target_power,
190195
inclusion_bounds=timeseries.Bounds[Power](

src/frequenz/sdk/timeseries/_base_types.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Timeseries basic types."""
55

66
import functools
7+
import typing
78
from collections.abc import Callable, Iterator
89
from dataclasses import dataclass
910
from datetime import datetime, timezone
@@ -137,12 +138,15 @@ def map(
137138
)
138139

139140

141+
_T = typing.TypeVar("_T")
142+
143+
140144
@dataclass(frozen=True)
141-
class Bounds(Generic[QuantityT]):
145+
class Bounds(Generic[_T]):
142146
"""Lower and upper bound values."""
143147

144-
lower: QuantityT
148+
lower: _T
145149
"""Lower bound."""
146150

147-
upper: QuantityT
151+
upper: _T
148152
"""Upper bound."""

src/frequenz/sdk/timeseries/battery_pool/_battery_pool_wrapper.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections import abc
1414
from datetime import timedelta
1515

16+
from ... import timeseries
1617
from ..._internal._channels import ReceiverFetcher
1718
from ...actor import _power_managing
1819
from ...timeseries import Energy, Percentage, Power, Sample, Temperature
@@ -62,7 +63,7 @@ async def set_power(
6263
*,
6364
request_timeout: timedelta = timedelta(seconds=5.0),
6465
include_broken_batteries: bool = False,
65-
_bounds: tuple[Power, Power] | None = None,
66+
_bounds: timeseries.Bounds[Power | None] = timeseries.Bounds(None, None),
6667
) -> None:
6768
"""Set the given power for the batteries in the pool.
6869
@@ -136,7 +137,7 @@ async def charge(
136137
_power_managing.Proposal(
137138
source_id=self._source_id,
138139
preferred_power=power,
139-
bounds=None,
140+
bounds=timeseries.Bounds(None, None),
140141
battery_ids=self._battery_pool._batteries,
141142
priority=0,
142143
request_timeout=request_timeout,
@@ -179,7 +180,7 @@ async def discharge(
179180
_power_managing.Proposal(
180181
source_id=self._source_id,
181182
preferred_power=power,
182-
bounds=None,
183+
bounds=timeseries.Bounds(None, None),
183184
battery_ids=self._battery_pool._batteries,
184185
priority=0,
185186
request_timeout=request_timeout,

tests/actor/_power_managing/test_matryoshka.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def test_matryoshka_algorithm() -> None: # pylint: disable=too-many-state
2929
def test_tgt_power(
3030
priority: int,
3131
power: float | None,
32-
bounds: tuple[float, float] | None,
32+
bounds: tuple[float | None, float | None],
3333
expected: float | None,
3434
) -> None:
3535
nonlocal call_count
@@ -40,9 +40,10 @@ def test_tgt_power(
4040
battery_ids=batteries,
4141
source_id=f"actor-{priority}",
4242
preferred_power=None if power is None else Power.from_watts(power),
43-
bounds=(Power.from_watts(bounds[0]), Power.from_watts(bounds[1]))
44-
if bounds
45-
else None,
43+
bounds=timeseries.Bounds(
44+
None if bounds[0] is None else Power.from_watts(bounds[0]),
45+
None if bounds[1] is None else Power.from_watts(bounds[1]),
46+
),
4647
priority=priority,
4748
),
4849
system_bounds,
@@ -91,18 +92,18 @@ def test_bounds(
9192
test_bounds(priority=2, expected_power=30.0, expected_bounds=(10.0, 50.0))
9293
test_bounds(priority=1, expected_power=30.0, expected_bounds=(25.0, 50.0))
9394

94-
test_tgt_power(priority=2, power=40.0, bounds=(40.0, 50.0), expected=40.0)
95+
test_tgt_power(priority=2, power=40.0, bounds=(40.0, None), expected=40.0)
9596
test_bounds(priority=3, expected_power=40.0, expected_bounds=(-200.0, 200.0))
9697
test_bounds(priority=2, expected_power=40.0, expected_bounds=(10.0, 50.0))
9798
test_bounds(priority=1, expected_power=40.0, expected_bounds=(40.0, 50.0))
9899

99-
test_tgt_power(priority=2, power=0.0, bounds=(-200.0, 200.0), expected=30.0)
100+
test_tgt_power(priority=2, power=0.0, bounds=(None, None), expected=30.0)
100101
test_bounds(priority=4, expected_power=30.0, expected_bounds=(-200.0, 200.0))
101102
test_bounds(priority=3, expected_power=30.0, expected_bounds=(-200.0, 200.0))
102103
test_bounds(priority=2, expected_power=30.0, expected_bounds=(10.0, 50.0))
103104
test_bounds(priority=1, expected_power=30.0, expected_bounds=(10.0, 50.0))
104105

105-
test_tgt_power(priority=4, power=-50.0, bounds=(-200.0, -50.0), expected=-50.0)
106+
test_tgt_power(priority=4, power=-50.0, bounds=(None, -50.0), expected=-50.0)
106107
test_bounds(priority=4, expected_power=-50.0, expected_bounds=(-200.0, 200.0))
107108
test_bounds(priority=3, expected_power=-50.0, expected_bounds=(-200.0, -50.0))
108109
test_bounds(priority=2, expected_power=-50.0, expected_bounds=(-200.0, -50.0))
@@ -118,7 +119,7 @@ def test_bounds(
118119
test_tgt_power(priority=4, power=-180.0, bounds=(-200.0, -50.0), expected=None)
119120
test_bounds(priority=1, expected_power=-150.0, expected_bounds=(-200.0, -50.0))
120121

121-
test_tgt_power(priority=4, power=50.0, bounds=(50.0, 200.0), expected=50.0)
122+
test_tgt_power(priority=4, power=50.0, bounds=(50.0, None), expected=50.0)
122123
test_bounds(priority=4, expected_power=50.0, expected_bounds=(-200.0, 200.0))
123124
test_bounds(priority=3, expected_power=50.0, expected_bounds=(50.0, 200.0))
124125
test_bounds(priority=2, expected_power=50.0, expected_bounds=(50.0, 200.0))

tests/timeseries/_battery_pool/test_battery_pool_control_methods.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from frequenz.channels import Sender
1414
from pytest_mock import MockerFixture
1515

16-
from frequenz.sdk import microgrid
16+
from frequenz.sdk import microgrid, timeseries
1717
from frequenz.sdk.actor import ResamplerConfig, _power_managing, power_distributing
1818
from frequenz.sdk.actor.power_distributing import BatteryStatus
1919
from frequenz.sdk.actor.power_distributing._battery_pool_status import BatteryPoolStatus
@@ -337,7 +337,7 @@ async def test_case_3(self, mocks: Mocks, mocker: MockerFixture) -> None:
337337
)
338338
await battery_pool_1.set_power(
339339
Power.from_watts(-1000.0),
340-
_bounds=(Power.from_watts(-1000.0), Power.from_watts(0.0)),
340+
_bounds=timeseries.Bounds(Power.from_watts(-1000.0), Power.from_watts(0.0)),
341341
)
342342
self._assert_report(
343343
await bounds_1_rx.receive(), power=-1000.0, lower=-4000.0, upper=4000.0
@@ -355,7 +355,7 @@ async def test_case_3(self, mocks: Mocks, mocker: MockerFixture) -> None:
355355

356356
await battery_pool_2.set_power(
357357
Power.from_watts(0.0),
358-
_bounds=(Power.from_watts(0.0), Power.from_watts(1000.0)),
358+
_bounds=timeseries.Bounds(Power.from_watts(0.0), Power.from_watts(1000.0)),
359359
)
360360
self._assert_report(
361361
await bounds_1_rx.receive(), power=0.0, lower=-4000.0, upper=4000.0

0 commit comments

Comments
 (0)