Skip to content

Commit 1a3dce7

Browse files
authored
Bug fixes in the PowerManager and the BatteryPool (#898)
2 parents 364d594 + 76b7968 commit 1a3dce7

File tree

4 files changed

+48
-11
lines changed

4 files changed

+48
-11
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@
2323
## Bug Fixes
2424

2525
- A bug was fixed where the grid fuse was not created properly and would end up with a `max_current` with type `float` instead of `Current`.
26+
27+
- `BatteryPool.propose_discharge` now converts power values to the passive-sign convention. Earlier it was not doing this and that was causing it to charge instead of discharge.
28+
29+
- Fix a bug that was causing the power managing actor to crash and restart when cleaning up old proposals.

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,35 @@ def __lt__(self, other: Proposal) -> bool:
231231
self.priority == other.priority and self.source_id < other.source_id
232232
)
233233

234+
def __eq__(self, other: object) -> bool:
235+
"""Check if two proposals are equal.
236+
237+
Equality is determined by the priority and source ID of the proposals, so
238+
two proposals are equal if they have the same priority and source ID, even
239+
if they have different power values or creation times.
240+
241+
This is so that there is only one active proposal for each actor in the bucket,
242+
and older proposals are replaced by newer ones.
243+
244+
Args:
245+
other: The other proposal to compare to.
246+
247+
Returns:
248+
Whether the two proposals are equal.
249+
"""
250+
if not isinstance(other, Proposal):
251+
return NotImplemented
252+
253+
return self.priority == other.priority and self.source_id == other.source_id
254+
255+
def __hash__(self) -> int:
256+
"""Get the hash of the proposal.
257+
258+
Returns:
259+
The hash of the proposal.
260+
"""
261+
return hash((self.priority, self.source_id))
262+
234263

235264
class Algorithm(enum.Enum):
236265
"""The available algorithms for the power manager."""

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from ...timeseries import Power
3030
from . import _bounds
3131
from ._base_classes import BaseAlgorithm, Proposal, _Report
32-
from ._sorted_set import SortedSet
3332

3433
if typing.TYPE_CHECKING:
3534
from ...timeseries._base_types import SystemBounds
@@ -44,12 +43,12 @@ class Matryoshka(BaseAlgorithm):
4443
def __init__(self, max_proposal_age: timedelta) -> None:
4544
"""Create a new instance of the matryoshka algorithm."""
4645
self._max_proposal_age_sec = max_proposal_age.total_seconds()
47-
self._component_buckets: dict[frozenset[int], SortedSet[Proposal]] = {}
46+
self._component_buckets: dict[frozenset[int], set[Proposal]] = {}
4847
self._target_power: dict[frozenset[int], Power] = {}
4948

5049
def _calc_target_power(
5150
self,
52-
proposals: SortedSet[Proposal],
51+
proposals: set[Proposal],
5352
system_bounds: SystemBounds,
5453
) -> Power:
5554
"""Calculate the target power for the given components.
@@ -83,7 +82,7 @@ def _calc_target_power(
8382
exclusion_bounds = system_bounds.exclusion_bounds
8483

8584
target_power = Power.zero()
86-
for next_proposal in reversed(proposals):
85+
for next_proposal in sorted(proposals, reverse=True):
8786
if upper_bound < lower_bound:
8887
break
8988
if next_proposal.preferred_power:
@@ -183,9 +182,10 @@ def calculate_target_power(
183182
return None
184183

185184
if proposal is not None:
186-
self._component_buckets.setdefault(component_ids, SortedSet()).insert(
187-
proposal
188-
)
185+
bucket = self._component_buckets.setdefault(component_ids, set())
186+
if proposal in bucket:
187+
bucket.remove(proposal)
188+
bucket.add(proposal)
189189

190190
# If there has not been any proposal for the given components, don't calculate a
191191
# target power and just return `None`.
@@ -243,7 +243,9 @@ def get_status(
243243
):
244244
exclusion_bounds = system_bounds.exclusion_bounds
245245

246-
for next_proposal in reversed(self._component_buckets.get(component_ids, [])):
246+
for next_proposal in sorted(
247+
self._component_buckets.get(component_ids, []), reverse=True
248+
):
247249
if next_proposal.priority <= priority:
248250
break
249251
proposal_lower = next_proposal.bounds.lower or lower_bound
@@ -286,4 +288,4 @@ def drop_old_proposals(self, loop_time: float) -> None:
286288
if (loop_time - proposal.creation_time) > self._max_proposal_age_sec:
287289
to_delete.append(proposal)
288290
for proposal in to_delete:
289-
bucket.delete(proposal)
291+
bucket.remove(proposal)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,10 @@ async def propose_discharge(
215215
Raises:
216216
ValueError: If the given power is negative.
217217
"""
218-
if power and power < Power.zero():
219-
raise ValueError("Discharge power must be positive.")
218+
if power:
219+
if power < Power.zero():
220+
raise ValueError("Discharge power must be positive.")
221+
power = -power
220222
await self._battery_pool._power_manager_requests_sender.send(
221223
_power_managing.Proposal(
222224
source_id=self._source_id,

0 commit comments

Comments
 (0)