Skip to content

Commit ce78f35

Browse files
authored
Improve handling of out-of-bounds requests (#1296)
This PR fixes a bug in the power manager, that was causing proposals to be ignored when they were proposing bounds that were fully outside the available bounds, under some cases. Closes #1294
2 parents 2a57fa1 + 2181981 commit ce78f35

File tree

3 files changed

+62
-14
lines changed

3 files changed

+62
-14
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
## Bug Fixes
1616

1717
- The log level for when components are transitioning to a `WORKING` state is lowered to `INFO`, and the log message has been improved.
18+
19+
- This fixes a bug in the power manager, that was causing proposals to be ignored when they were proposing bounds that were fully outside the available bounds, under some cases.

src/frequenz/sdk/microgrid/_power_managing/_shifting_matryoshka.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(
6767
self._component_buckets: dict[frozenset[ComponentId], set[Proposal]] = {}
6868
self._target_power: dict[frozenset[ComponentId], Power] = {}
6969

70-
def _calc_targets(
70+
def _calc_targets( # pylint: disable=too-many-branches,too-many-statements
7171
self,
7272
component_ids: frozenset[ComponentId],
7373
system_bounds: SystemBounds,
@@ -122,8 +122,26 @@ def _calc_targets(
122122
if upper_bound < lower_bound:
123123
break
124124

125-
proposal_lower = next_proposal.bounds.lower or lower_bound
126-
proposal_upper = next_proposal.bounds.upper or upper_bound
125+
match (next_proposal.bounds.lower, next_proposal.bounds.upper):
126+
case (None, None):
127+
proposal_lower = lower_bound
128+
proposal_upper = upper_bound
129+
case (Power(), None):
130+
proposal_lower = next_proposal.bounds.lower
131+
if proposal_lower > upper_bound:
132+
proposal_upper = proposal_lower
133+
else:
134+
proposal_upper = upper_bound
135+
case (None, Power()):
136+
proposal_upper = next_proposal.bounds.upper
137+
if proposal_upper < lower_bound:
138+
proposal_lower = proposal_upper
139+
else:
140+
proposal_lower = lower_bound
141+
case (Power(), Power()):
142+
proposal_lower = next_proposal.bounds.lower
143+
proposal_upper = next_proposal.bounds.upper
144+
127145
proposal_power = next_proposal.preferred_power
128146

129147
# Make sure that if the proposal specified bounds, they make sense.

tests/actor/_power_managing/test_shifting_matryoshka.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,34 @@ def ensure_overlapping_bucket_request_fails() -> None:
559559
)
560560

561561

562+
async def test_out_of_bounds_proposals() -> None:
563+
"""Test out-of-bounds proposals for the shifting matryoshka algorithm."""
564+
batteries = frozenset({ComponentId(2), ComponentId(5)})
565+
566+
system_bounds = _base_types.SystemBounds(
567+
timestamp=datetime.now(tz=timezone.utc),
568+
inclusion_bounds=timeseries.Bounds(
569+
lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0)
570+
),
571+
exclusion_bounds=None,
572+
)
573+
tester = StatefulTester(batteries, system_bounds)
574+
575+
tester.bounds(priority=2, expected_power=None, expected_bounds=(-200.0, 200.0))
576+
577+
tester.tgt_power(priority=2, power=250.0, bounds=(250.0, None), expected=200.0)
578+
tester.bounds(priority=1, expected_power=200.0, expected_bounds=(0.0, 0.0))
579+
580+
tester.tgt_power(priority=2, power=250.0, bounds=(250.0, 300.0), expected=200.0)
581+
tester.bounds(priority=1, expected_power=200.0, expected_bounds=(0.0, 0.0))
582+
583+
tester.tgt_power(priority=2, power=-250.0, bounds=(-300.0, -250.0), expected=-200.0)
584+
tester.bounds(priority=1, expected_power=-200.0, expected_bounds=(0.0, 0.0))
585+
586+
tester.tgt_power(priority=2, power=-250.0, bounds=(None, -250.0), expected=-200.0)
587+
tester.bounds(priority=1, expected_power=-200.0, expected_bounds=(0.0, 0.0))
588+
589+
562590
async def test_matryoshka_shifting_limiting() -> None:
563591
"""Tests for the power managing actor.
564592
@@ -572,10 +600,10 @@ async def test_matryoshka_shifting_limiting() -> None:
572600
| 5 | -120 kW .. 70 kW | -100 kW .. 80 kW | 80 kW | 70 kW | 90 kW |
573601
| 4 | -170 kW .. 0 kW | None | -120 kW | -120 kW | -30 kW |
574602
| 3 | -50 kW .. 120 kW | None | 60 kW | 60 kW | 30 kW |
575-
| 2 | -110 kW .. 60 kW | -40 kW .. 30 kW | 20 kW | 20 kW | 50 kW |
576-
| 1 | -60 kW .. 10 kW | -50 kW .. 40 kW | 25 kW | 10 kW | 60 kW |
577-
| 0 | -60 kW .. 0 kW | None | 12 kW | 0 kW | 60 kW |
578-
| -1 | -60 kW .. 0 kW | -40 kW .. -10 kW | -10 kW | -10 kW | 50 kW |
603+
| 2 | -110 kW .. 60 kW | -40 kW .. 50 kW | 20 kW | 20 kW | 50 kW |
604+
| 1 | -60 kW .. 30 kW | -50 kW .. 40 kW | 25 kW | 25 kW | 75 kW |
605+
| 0 | -75 kW .. 5 kW | 50 kW .. None | 50 kW | 0 kW | 80 kW |
606+
| -1 | 0 kW .. 0 kW | -40 kW .. -10 kW | -10 kW | 0 kW | 80 kW |
579607
|-------|-------------------|------------------|---------|----------|-----------|
580608
| | | | | Power | |
581609
| | | | | Setpoint | 50 kW |
@@ -609,13 +637,13 @@ async def test_matryoshka_shifting_limiting() -> None:
609637
tester.tgt_power(priority=3, power=60.0, bounds=(None, None), expected=30.0)
610638
tester.bounds(priority=2, expected_power=30.0, expected_bounds=(-110.0, 60.0))
611639

612-
tester.tgt_power(priority=2, power=20.0, bounds=(-40.0, 30.0), expected=50.0)
613-
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(-60.0, 10.0))
640+
tester.tgt_power(priority=2, power=20.0, bounds=(-40.0, 50.0), expected=50.0)
641+
tester.bounds(priority=1, expected_power=50.0, expected_bounds=(-60.0, 30.0))
614642

615-
tester.tgt_power(priority=1, power=25.0, bounds=(-50.0, 40.0), expected=60.0)
616-
tester.bounds(priority=0, expected_power=60.0, expected_bounds=(-60.0, 0.0))
643+
tester.tgt_power(priority=1, power=25.0, bounds=(-50.0, 40.0), expected=75.0)
644+
tester.bounds(priority=0, expected_power=75.0, expected_bounds=(-75.0, 5.0))
617645

618-
tester.tgt_power(priority=0, power=12.0, bounds=(None, None), expected=60.0)
619-
tester.bounds(priority=-1, expected_power=60.0, expected_bounds=(-60.0, 0.0))
646+
tester.tgt_power(priority=0, power=50.0, bounds=(50.0, None), expected=80.0)
647+
tester.bounds(priority=-1, expected_power=80.0, expected_bounds=(0.0, 0.0))
620648

621-
tester.tgt_power(priority=-1, power=-10.0, bounds=(-40.0, -10.0), expected=50.0)
649+
tester.tgt_power(priority=-1, power=-10.0, bounds=(-40.0, -10.0), expected=80.0)

0 commit comments

Comments
 (0)