Skip to content

Commit 87b37b1

Browse files
authored
Make PowerManagingActor more modular and generic (#773)
This PR updates the `PowerManagingActor` to - take type of components we are controlling. Currently only batteries are supported. - Remove all other references to batteries from the `PowerManagingActor`, except in the interactions with the `PowerDistributingActor`. The `PowerDistributingActor` needs to be modularized as well, before the either of these actors can be used with other types of components. All changes in this PR are internal and don't affect the external interface of the `BatteryPool`.
2 parents cb84598 + 20b4570 commit 87b37b1

File tree

15 files changed

+396
-273
lines changed

15 files changed

+396
-273
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
- The `BatteryPool.power_status` method now streams objects of type `BatteryPoolReport`. They're identical to the previous `Report` objects, except for the name of the class.
1010

1111
## New Features
1212

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
"""A power manager implementation."""
55

6-
from ._base_classes import Algorithm, Proposal, Report, ReportRequest
6+
from ._base_classes import Algorithm, Proposal, ReportRequest, _Report
77
from ._power_managing_actor import PowerManagingActor
88

99
__all__ = [
1010
"Algorithm",
1111
"PowerManagingActor",
1212
"Proposal",
13-
"Report",
13+
"_Report",
1414
"ReportRequest",
1515
]

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

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from . import _bounds
1717

1818
if typing.TYPE_CHECKING:
19-
from ...timeseries.battery_pool import PowerMetrics
19+
from ...timeseries._base_types import SystemBounds
2020
from .. import power_distributing
2121

2222

@@ -27,8 +27,8 @@ class ReportRequest:
2727
source_id: str
2828
"""The source ID of the actor sending the request."""
2929

30-
battery_ids: frozenset[int]
31-
"""The battery IDs to report on."""
30+
component_ids: frozenset[int]
31+
"""The component IDs to report on."""
3232

3333
priority: int
3434
"""The priority of the actor ."""
@@ -40,15 +40,67 @@ def get_channel_name(self) -> str:
4040
The channel name to use to identify the corresponding report channel
4141
from the channel registry.
4242
"""
43-
return f"power_manager.report.{self.battery_ids=}.{self.priority=}"
43+
return f"power_manager.report.{self.component_ids=}.{self.priority=}"
44+
45+
46+
class Report(typing.Protocol):
47+
"""Current PowerManager report for a set of components.
48+
49+
This protocol can be specialized by different component pools to provide more
50+
specific details and documentation for the reports.
51+
"""
52+
53+
@property
54+
def bounds(self) -> timeseries.Bounds[Power] | None:
55+
"""The bounds for the components.
56+
57+
These bounds are adjusted to any restrictions placed by actors with higher
58+
priorities.
59+
60+
There might be exclusion zones within these bounds. If necessary, the
61+
`adjust_to_bounds` method may be used to check if a desired power value fits the
62+
bounds, or to get the closest possible power values that do fit the bounds.
63+
"""
64+
65+
@abc.abstractmethod
66+
def adjust_to_bounds(self, power: Power) -> tuple[Power | None, Power | None]:
67+
"""Adjust a power value to the bounds.
68+
69+
This method can be used to adjust a desired power value to the power bounds
70+
available to the actor.
71+
72+
If the given power value falls within the usable bounds, it will be returned
73+
unchanged.
74+
75+
If it falls outside the usable bounds, the closest possible value on the
76+
corresponding side will be returned. For example, if the given power is lower
77+
than the lowest usable power, only the lowest usable power will be returned, and
78+
similarly for the highest usable power.
79+
80+
If the given power falls within an exclusion zone that's contained within the
81+
usable bounds, the closest possible power values on both sides will be returned.
82+
83+
!!! note
84+
It is completely optional to use this method to adjust power values before
85+
proposing them, because the PowerManager will do this automatically. This
86+
method is provided for convenience, and for granular control when there are
87+
two possible power values, both of which fall within the available bounds.
88+
89+
Args:
90+
power: The power value to adjust.
91+
92+
Returns:
93+
A tuple of the closest power values to the desired power that fall within
94+
the available bounds for the actor.
95+
"""
4496

4597

4698
@dataclasses.dataclass(frozen=True, kw_only=True)
47-
class Report:
48-
"""Current PowerManager report for a set of batteries."""
99+
class _Report(Report):
100+
"""Current PowerManager report for a set of components."""
49101

50102
target_power: Power | None
51-
"""The currently set power for the batteries."""
103+
"""The currently set power for the components."""
52104

53105
distribution_result: power_distributing.Result | None
54106
"""The result of the last power distribution.
@@ -57,14 +109,14 @@ class Report:
57109
"""
58110

59111
_inclusion_bounds: timeseries.Bounds[Power] | None
60-
"""The available inclusion bounds for the batteries, for the actor's priority.
112+
"""The available inclusion bounds for the components, for the actor's priority.
61113
62114
These bounds are adjusted to any restrictions placed by actors with higher
63115
priorities.
64116
"""
65117

66118
_exclusion_bounds: timeseries.Bounds[Power] | None
67-
"""The exclusion bounds for the batteries.
119+
"""The exclusion bounds for the components.
68120
69121
The power manager doesn't manage exclusion bounds, so these are aggregations of
70122
values reported by the microgrid API.
@@ -75,15 +127,14 @@ class Report:
75127

76128
@property
77129
def bounds(self) -> timeseries.Bounds[Power] | None:
78-
"""The bounds for the batteries.
130+
"""The bounds for the components.
79131
80132
These bounds are adjusted to any restrictions placed by actors with higher
81133
priorities.
82134
83135
There might be exclusion zones within these bounds. If necessary, the
84-
[`adjust_to_bounds`][frequenz.sdk.timeseries.battery_pool.Report.adjust_to_bounds]
85-
method may be used to check if a desired power value fits the bounds, or to get
86-
the closest possible power values that do fit the bounds.
136+
`adjust_to_bounds` method may be used to check if a desired power value fits the
137+
bounds, or to get the closest possible power values that do fit the bounds.
87138
"""
88139
return self._inclusion_bounds
89140

@@ -106,29 +157,9 @@ def adjust_to_bounds(self, power: Power) -> tuple[Power | None, Power | None]:
106157
107158
!!! note
108159
It is completely optional to use this method to adjust power values before
109-
proposing them through the battery pool, because the battery pool will do
110-
this automatically. This method is provided for convenience, and for
111-
granular control when there are two possible power values, both of which
112-
fall within the available bounds.
113-
114-
Example:
115-
```python
116-
from frequenz.sdk import microgrid
117-
118-
power_status_rx = microgrid.battery_pool().power_status.new_receiver()
119-
power_status = await power_status_rx.receive()
120-
desired_power = Power.from_watts(1000.0)
121-
122-
match power_status.adjust_to_bounds(desired_power):
123-
case (power, _) if power == desired_power:
124-
print("Desired power is available.")
125-
case (None, power) | (power, None) if power:
126-
print(f"Closest available power is {power}.")
127-
case (lower, upper) if lower and upper:
128-
print(f"Two options {lower}, {upper} to propose to battery pool.")
129-
case (None, None):
130-
print("No available power")
131-
```
160+
proposing them, because the PowerManager will do this automatically. This
161+
method is provided for convenience, and for granular control when there are
162+
two possible power values, both of which fall within the available bounds.
132163
133164
Args:
134165
power: The power value to adjust.
@@ -150,13 +181,13 @@ def adjust_to_bounds(self, power: Power) -> tuple[Power | None, Power | None]:
150181

151182
@dataclasses.dataclass(frozen=True, kw_only=True)
152183
class Proposal:
153-
"""A proposal for a battery to be charged or discharged."""
184+
"""A proposal for a set of components to be charged or discharged."""
154185

155186
source_id: str
156187
"""The source ID of the actor sending the request."""
157188

158189
preferred_power: Power | None
159-
"""The preferred power to be distributed to the batteries.
190+
"""The preferred power to be distributed to the components.
160191
161192
If `None`, the preferred power of higher priority actors will get precedence.
162193
"""
@@ -166,12 +197,12 @@ class Proposal:
166197
167198
These bounds will apply to actors with a lower priority, and can be overridden by
168199
bounds from actors with a higher priority. If None, the power bounds will be set to
169-
the maximum power of the batteries in the pool. This is currently an experimental
200+
the maximum power of the components in the pool. This is currently an experimental
170201
feature.
171202
"""
172203

173-
battery_ids: frozenset[int]
174-
"""The battery IDs to distribute the power to."""
204+
component_ids: frozenset[int]
205+
"""The component IDs to distribute the power to."""
175206

176207
priority: int
177208
"""The priority of the actor sending the proposal."""
@@ -207,23 +238,23 @@ class BaseAlgorithm(abc.ABC):
207238
@abc.abstractmethod
208239
def calculate_target_power(
209240
self,
210-
battery_ids: frozenset[int],
241+
component_ids: frozenset[int],
211242
proposal: Proposal | None,
212-
system_bounds: PowerMetrics,
243+
system_bounds: SystemBounds,
213244
must_return_power: bool = False,
214245
) -> Power | None:
215-
"""Calculate and return the target power for the given batteries.
246+
"""Calculate and return the target power for the given components.
216247
217248
Args:
218-
battery_ids: The battery IDs to calculate the target power for.
249+
component_ids: The component IDs to calculate the target power for.
219250
proposal: If given, the proposal to added to the bucket, before the target
220251
power is calculated.
221-
system_bounds: The system bounds for the batteries in the proposal.
252+
system_bounds: The system bounds for the components in the proposal.
222253
must_return_power: If `True`, the algorithm must return a target power,
223254
even if it hasn't changed since the last call.
224255
225256
Returns:
226-
The new target power for the batteries, or `None` if the target power
257+
The new target power for the components, or `None` if the target power
227258
didn't change.
228259
"""
229260

@@ -232,19 +263,19 @@ def calculate_target_power(
232263
@abc.abstractmethod
233264
def get_status(
234265
self,
235-
battery_ids: frozenset[int],
266+
component_ids: frozenset[int],
236267
priority: int,
237-
system_bounds: PowerMetrics,
268+
system_bounds: SystemBounds,
238269
distribution_result: power_distributing.Result | None,
239-
) -> Report:
240-
"""Get the bounds for a set of batteries, for the given priority.
270+
) -> _Report:
271+
"""Get the bounds for a set of components, for the given priority.
241272
242273
Args:
243-
battery_ids: The IDs of the batteries to get the bounds for.
274+
component_ids: The IDs of the components to get the bounds for.
244275
priority: The priority of the actor for which the bounds are requested.
245-
system_bounds: The system bounds for the batteries.
276+
system_bounds: The system bounds for the components.
246277
distribution_result: The result of the last power distribution.
247278
248279
Returns:
249-
The bounds for the batteries.
280+
The bounds for the components.
250281
"""

0 commit comments

Comments
 (0)