Skip to content

Commit b4b04ca

Browse files
authored
Pass through 0W requests to the microgrid API independent of exclusion bounds (#801)
The PowerManager and PowerDistributor were adjusting 0 power requests to exclusion bounds, which it shouldn't do. It now forwards 0 power requests directly to the microgrid API, which can decide how to adjust for exclusion bounds, if necessary. Closes #794
2 parents 471adc6 + c801aa7 commit b4b04ca

File tree

7 files changed

+142
-7
lines changed

7 files changed

+142
-7
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@
5252

5353
## Bug Fixes
5454

55-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
55+
- 0W power requests are now not adjusted to exclusion bounds by the `PowerManager` and `PowerDistributor`, and are sent over to the microgrid API directly.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def clamp_to_bounds( # pylint: disable=too-many-return-statements
138138

139139
# If the given value is within the exclusion bounds and the exclusion bounds are
140140
# within the given bounds, clamp the given value to the closest exclusion bound.
141-
if exclusion_bounds is not None:
141+
if exclusion_bounds is not None and not value.isclose(Power.zero()):
142142
if exclusion_bounds.lower < value < exclusion_bounds.upper:
143143
return exclusion_bounds.lower, exclusion_bounds.upper
144144

src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,16 @@ def distribute_power(
685685
Returns:
686686
Distribution result
687687
"""
688-
if power >= 0.0:
688+
if is_close_to_zero(power):
689+
return DistributionResult(
690+
distribution={
691+
inverter.component_id: 0.0
692+
for _, inverters in components
693+
for inverter in inverters
694+
},
695+
remaining_power=0.0,
696+
)
697+
if power > 0.0:
689698
return self._distribute_consume_power(power, components)
690699
return self._distribute_supply_power(power, components)
691700

tests/actor/_power_managing/test_matryoshka.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ async def test_matryoshka_with_excl_3() -> None:
313313
)
314314

315315
tester = StatefulTester(batteries, system_bounds)
316+
tester.tgt_power(priority=2, power=10.0, bounds=(None, None), expected=30.0)
317+
tester.tgt_power(priority=2, power=-10.0, bounds=(None, None), expected=-30.0)
318+
tester.tgt_power(priority=2, power=0.0, bounds=(None, None), expected=0.0)
319+
tester.tgt_power(priority=3, power=20.0, bounds=(None, None), expected=None)
320+
tester.tgt_power(priority=1, power=-20.0, bounds=(None, None), expected=-30.0)
321+
tester.tgt_power(priority=3, power=None, bounds=(None, None), expected=None)
322+
tester.tgt_power(priority=1, power=None, bounds=(None, None), expected=0.0)
316323

317324
tester.tgt_power(priority=2, power=25.0, bounds=(25.0, 50.0), expected=30.0)
318325
tester.bounds(priority=2, expected_power=30.0, expected_bounds=(-200.0, 200.0))

tests/actor/_power_managing/test_report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_adjust_to_bounds() -> None:
5858
inclusion_bounds=(-200.0, 200.0),
5959
exclusion_bounds=(-30.0, 30.0),
6060
)
61-
tester.case(0.0, -30.0, 30.0)
61+
tester.case(0.0, 0.0, 0.0)
6262
tester.case(-210.0, -200.0, None)
6363
tester.case(220.0, None, 200.0)
6464
tester.case(-20.0, -30.0, 30.0)

tests/actor/power_distributing/test_battery_distribution_algorithm.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,11 @@ def test_scenario_1(self) -> None:
10391039

10401040
algorithm = BatteryDistributionAlgorithm()
10411041

1042+
self.assert_result(
1043+
algorithm.distribute_power(0, components),
1044+
DistributionResult({1: 0, 3: 0, 5: 0}, remaining_power=0.0),
1045+
)
1046+
10421047
self.assert_result(
10431048
algorithm.distribute_power(-300, components),
10441049
DistributionResult({1: -100, 3: -100, 5: -100}, remaining_power=0.0),

tests/timeseries/_battery_pool/test_battery_pool_control_methods.py

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ async def _patch_battery_pool_status(
110110
)
111111
)
112112

113-
async def _init_data_for_batteries(self, mocks: Mocks) -> None:
113+
async def _init_data_for_batteries(
114+
self, mocks: Mocks, *, exclusion_bounds: tuple[float, float] | None = None
115+
) -> None:
116+
excl_lower = exclusion_bounds[0] if exclusion_bounds else 0.0
117+
excl_upper = exclusion_bounds[1] if exclusion_bounds else 0.0
114118
now = datetime.now(tz=timezone.utc)
115119
for battery_id in mocks.microgrid.battery_ids:
116120
mocks.streamer.start_streaming(
@@ -120,8 +124,8 @@ async def _init_data_for_batteries(self, mocks: Mocks) -> None:
120124
soc=50.0,
121125
soc_lower_bound=10.0,
122126
soc_upper_bound=90.0,
123-
power_exclusion_lower_bound=0.0,
124-
power_exclusion_upper_bound=0.0,
127+
power_exclusion_lower_bound=excl_lower,
128+
power_exclusion_upper_bound=excl_upper,
125129
power_inclusion_lower_bound=-1000.0,
126130
power_inclusion_upper_bound=1000.0,
127131
capacity=2000.0,
@@ -369,3 +373,113 @@ async def test_case_3(self, mocks: Mocks, mocker: MockerFixture) -> None:
369373
assert sorted(set_power.call_args_list) == [
370374
mocker.call(inv_id, 0.0) for inv_id in mocks.microgrid.battery_inverter_ids
371375
]
376+
377+
async def test_case_4(self, mocks: Mocks, mocker: MockerFixture) -> None:
378+
"""Test case 4.
379+
380+
- single battery pool with all batteries.
381+
- all batteries are working, but have exclusion bounds.
382+
"""
383+
set_power = typing.cast(
384+
AsyncMock, microgrid.connection_manager.get().api_client.set_power
385+
)
386+
await self._patch_battery_pool_status(mocks, mocker)
387+
await self._init_data_for_batteries(mocks, exclusion_bounds=(-100.0, 100.0))
388+
await self._init_data_for_inverters(mocks)
389+
390+
battery_pool = microgrid.battery_pool()
391+
bounds_rx = battery_pool.power_status.new_receiver()
392+
393+
self._assert_report(
394+
await bounds_rx.receive(), power=None, lower=-4000.0, upper=4000.0
395+
)
396+
397+
await battery_pool.propose_power(Power.from_watts(1000.0))
398+
399+
self._assert_report(
400+
await bounds_rx.receive(), power=1000.0, lower=-4000.0, upper=4000.0
401+
)
402+
assert set_power.call_count == 4
403+
assert sorted(set_power.call_args_list) == [
404+
mocker.call(inv_id, 250.0)
405+
for inv_id in mocks.microgrid.battery_inverter_ids
406+
]
407+
self._assert_report(
408+
await bounds_rx.receive(),
409+
power=1000.0,
410+
lower=-4000.0,
411+
upper=4000.0,
412+
expected_result_pred=lambda result: isinstance(
413+
result, power_distributing.Success
414+
),
415+
)
416+
417+
set_power.reset_mock()
418+
419+
# Non-zero power but within the exclusion bounds should get adjusted to nearest
420+
# available power.
421+
await battery_pool.propose_power(Power.from_watts(50.0))
422+
423+
self._assert_report(
424+
await bounds_rx.receive(), power=400.0, lower=-4000.0, upper=4000.0
425+
)
426+
assert set_power.call_count == 4
427+
assert sorted(set_power.call_args_list) == [
428+
mocker.call(inv_id, 100.0)
429+
for inv_id in mocks.microgrid.battery_inverter_ids
430+
]
431+
self._assert_report(
432+
await bounds_rx.receive(),
433+
power=400.0,
434+
lower=-4000.0,
435+
upper=4000.0,
436+
expected_result_pred=lambda result: isinstance(
437+
result, power_distributing.Success
438+
),
439+
)
440+
441+
set_power.reset_mock()
442+
443+
# Zero power should be allowed, even if there are exclusion bounds.
444+
await battery_pool.propose_power(Power.from_watts(0.0))
445+
446+
self._assert_report(
447+
await bounds_rx.receive(), power=0.0, lower=-4000.0, upper=4000.0
448+
)
449+
assert set_power.call_count == 4
450+
assert sorted(set_power.call_args_list) == [
451+
mocker.call(inv_id, 0.0) for inv_id in mocks.microgrid.battery_inverter_ids
452+
]
453+
self._assert_report(
454+
await bounds_rx.receive(),
455+
power=0.0,
456+
lower=-4000.0,
457+
upper=4000.0,
458+
expected_result_pred=lambda result: isinstance(
459+
result, power_distributing.Success
460+
),
461+
)
462+
463+
set_power.reset_mock()
464+
465+
# Non-zero power but within the exclusion bounds should get adjusted to nearest
466+
# available power.
467+
await battery_pool.propose_power(Power.from_watts(-150.0))
468+
469+
self._assert_report(
470+
await bounds_rx.receive(), power=-400.0, lower=-4000.0, upper=4000.0
471+
)
472+
assert set_power.call_count == 4
473+
assert sorted(set_power.call_args_list) == [
474+
mocker.call(inv_id, -100.0)
475+
for inv_id in mocks.microgrid.battery_inverter_ids
476+
]
477+
self._assert_report(
478+
await bounds_rx.receive(),
479+
power=-400.0,
480+
lower=-4000.0,
481+
upper=4000.0,
482+
expected_result_pred=lambda result: isinstance(
483+
result, power_distributing.Success
484+
),
485+
)

0 commit comments

Comments
 (0)