Skip to content

Commit 6fb3a17

Browse files
committed
Fix exclusion-bound target direction
Signed-off-by: Mathias L. Baumann <mathias.baumann@frequenz.com>
1 parent 3332503 commit 6fb3a17

3 files changed

Lines changed: 53 additions & 4 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ This release updates the microgrid component graph library to v0.6.0, which stop
1111
- Producer power used to clamp each producer at zero. A producer that draws power now adds a positive value instead of zero, so the total can be positive. For example, PV producing 10 kW and a CHP drawing 2 kW used to give -10 kW and now give -8 kW. There is one exception: when the two share a meter below the grid meter, that meter sends data, and `disable_fallback_components` is off, the meter measures them together, so they gave -8 kW before too. `producer.power.min(Power.zero()).build("producer_power")` clamps the total at zero, but it still differs from the old result when one producer draws power while another produces.
1212
- With `ComponentGraphConfig(include_phantom_loads_in_consumer_formula=True)`, consumer power is unchanged: it still clamps each of its terms at zero.
1313

14+
## Bug Fixes
15+
16+
* Make the PowerManager fall back to zero instead of changing the requested power direction when a target is snapped outside asymmetric exclusion bounds.

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ def _get_nearest_possible_power(
4040
case (None, p) | (p, None) if p:
4141
return p
4242
case (low, high) if low and high:
43-
if high - power < power - low:
44-
return high
45-
return low
43+
# The exclusion bounds are a region where the system cannot operate.
44+
nearest = high if high - power < power - low else low
45+
# Never turn a request into power in the opposite direction.
46+
if (power > Power.zero() and nearest < Power.zero()) or (
47+
power < Power.zero() and nearest > Power.zero()
48+
):
49+
return Power.zero()
50+
return nearest
4651
case _:
4752
return Power.zero()
4853

@@ -197,9 +202,10 @@ def _calc_targets( # pylint: disable=too-many-branches,too-many-statements
197202

198203
if allocations:
199204
_logger.info(
200-
"PowerManager allocations for component IDs: %s: %s",
205+
"PowerManager allocations for component IDs: %s: %s, target power: %s",
201206
sorted(component_ids),
202207
allocations,
208+
target_power,
203209
)
204210

205211
return target_power, Bounds[Power](lower=lower_bound, upper=upper_bound)

‎tests/actor/_power_managing/test_shifting_matryoshka.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# pylint: disable=duplicate-code
77

88
import asyncio
9+
import logging
910
import re
1011
from datetime import datetime, timedelta, timezone
1112

@@ -397,6 +398,45 @@ async def test_matryoshka_with_excl_3() -> None:
397398
tester.bounds(priority=0, expected_power=-50.0, expected_bounds=(-60.0, 5.0))
398399

399400

401+
@pytest.mark.parametrize(
402+
("requested_power", "exclusion_bounds", "expected_power"),
403+
[
404+
(-3.0, (-35.0, 7.0), 0.0),
405+
(3.0, (-7.0, 35.0), 0.0),
406+
(-30.0, (-35.0, 7.0), -35.0),
407+
],
408+
)
409+
async def test_matryoshka_avoids_opposite_direction_outside_exclusion_bounds(
410+
caplog: pytest.LogCaptureFixture,
411+
requested_power: float,
412+
exclusion_bounds: tuple[float, float],
413+
expected_power: float,
414+
) -> None:
415+
"""Avoid targets that would snap to the opposite direction."""
416+
batteries = frozenset({ComponentId(2), ComponentId(5)})
417+
system_bounds = _base_types.SystemBounds(
418+
timestamp=datetime.now(tz=timezone.utc),
419+
inclusion_bounds=timeseries.Bounds(
420+
lower=Power.from_watts(-200.0), upper=Power.from_watts(200.0)
421+
),
422+
exclusion_bounds=timeseries.Bounds(
423+
lower=Power.from_watts(exclusion_bounds[0]),
424+
upper=Power.from_watts(exclusion_bounds[1]),
425+
),
426+
)
427+
tester = StatefulTester(batteries, system_bounds)
428+
429+
with caplog.at_level(logging.INFO):
430+
tester.tgt_power(
431+
priority=1,
432+
power=requested_power,
433+
bounds=(-200.0, 200.0),
434+
expected=expected_power,
435+
)
436+
437+
assert f"target power: {expected_power:g} W" in caplog.messages[-1]
438+
439+
400440
async def test_matryoshka_drop_old_proposals() -> None:
401441
"""Tests for the power managing actor.
402442

0 commit comments

Comments
 (0)