Skip to content

Commit e48110d

Browse files
authored
Forward zero power requests always to the microgrid API (#591)
Only non-zero requests should be rejected if they fall within the exclusion bounds. This PR also: - moves distribution algorithm into the power distributing actor's dir. - remove unused methods - rename `OutOfBound` -> `OutOfBounds` - Improve power distribution algorithm docstrings.
2 parents 10a763b + fa66566 commit e48110d

File tree

10 files changed

+164
-150
lines changed

10 files changed

+164
-150
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
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 `frequenz.sdk.power` package contained the power distribution algorithm, which is for internal use in the sdk, and is no longer part of the public API.
10+
11+
- `PowerDistributingActor`'s result type `OutOfBound` has been renamed to `OutOfBounds`, and its member variable `bound` has been renamed to `bounds`.
1012

1113
## New Features
1214

@@ -25,3 +27,4 @@
2527
- Fix `pv_power` not working in setups with 2 grid meters by using a new
2628
reliable function to search for components in the components graph
2729
- Fix `consumer_power` similar to `pv_power`
30+
- Zero value requests received by the `PowerDistributingActor` will now always be accepted, even when there are non-zero exclusion bounds.

benchmarks/power_distribution/power_distributor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from frequenz.sdk.actor.power_distributing import (
1818
BatteryStatus,
1919
Error,
20-
OutOfBound,
20+
OutOfBounds,
2121
PartialFailure,
2222
PowerDistributingActor,
2323
Request,
@@ -75,7 +75,7 @@ def parse_result(result: List[List[Result]]) -> Dict[str, float]:
7575
Error: 0,
7676
Success: 0,
7777
PartialFailure: 0,
78-
OutOfBound: 0,
78+
OutOfBounds: 0,
7979
}
8080

8181
for result_list in result:
@@ -86,7 +86,7 @@ def parse_result(result: List[List[Result]]) -> Dict[str, float]:
8686
"success_num": result_counts[Success],
8787
"failed_num": result_counts[PartialFailure],
8888
"error_num": result_counts[Error],
89-
"out_of_bound": result_counts[OutOfBound],
89+
"out_of_bounds": result_counts[OutOfBounds],
9090
}
9191

9292

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
from ._battery_pool_status import BatteryStatus
1414
from .power_distributing import PowerDistributingActor
1515
from .request import Request
16-
from .result import Error, OutOfBound, PartialFailure, Result, Success
16+
from .result import Error, OutOfBounds, PartialFailure, Result, Success
1717

1818
__all__ = [
1919
"PowerDistributingActor",
2020
"Request",
2121
"Result",
2222
"Error",
2323
"Success",
24-
"OutOfBound",
24+
"OutOfBounds",
2525
"PartialFailure",
2626
"BatteryStatus",
2727
]

src/frequenz/sdk/power/_distribution_algorithm.py renamed to src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_distribution_algorithm.py

Lines changed: 26 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from frequenz.sdk._internal._math import is_close_to_zero
1212

13-
from ..microgrid.component import BatteryData, InverterData
13+
from ....microgrid.component import BatteryData, InverterData
1414

1515
_logger = logging.getLogger(__name__)
1616

@@ -147,39 +147,16 @@ def __init__(self, distributor_exponent: float = 1) -> None:
147147
* `Bat1.available_soc = 10`, `Bat2.available_soc = 30`
148148
* `Bat1.available_soc / Bat2.available_soc = 3`
149149
150-
We need to distribute 8000W.
150+
A request power of 8000W will be distributed as follows, for different
151+
values of `distribution_exponent`:
151152
152-
If `distribution_exponent` is:
153+
| distribution_exponent | Bat1 | Bat2 |
154+
|-----------------------|------|------|
155+
| 0 | 4000 | 4000 |
156+
| 1 | 2000 | 6000 |
157+
| 2 | 800 | 7200 |
158+
| 3 | 285 | 7715 |
153159
154-
* `0`: distribution for each battery will be the equal.
155-
```python
156-
BAT1_DISTRIBUTION = 4000
157-
BAT2_DISTRIBUTION = 4000
158-
```
159-
160-
* `1`: then `Bat2` will have 3x more power assigned then `Bat1`.
161-
```python
162-
# 10 * x + 30 * x = 8000
163-
X = 200
164-
BAT1_DISTRIBUTION = 2000
165-
BAT2_DISTRIBUTION = 6000
166-
```
167-
168-
* `2`: then `Bat2` will have 9x more power assigned then `Bat1`.
169-
```python
170-
# 10^2 * x + 30^2 * x = 8000
171-
X = 80
172-
BAT1_DISTRIBUTION = 800
173-
BAT2_DISTRIBUTION = 7200
174-
```
175-
176-
* `3`: then `Bat2` will have 27x more power assigned then `Bat1`.
177-
```python
178-
# 10^3 * x + 30^3 * x = 8000
179-
X = 0.285714286
180-
BAT1_DISTRIBUTION = 285
181-
BAT2_DISTRIBUTION = 7715
182-
```
183160
184161
# Example 2
185162
@@ -189,39 +166,15 @@ def __init__(self, distributor_exponent: float = 1) -> None:
189166
* `Bat1.available_soc = 30`, `Bat2.available_soc = 60`
190167
* `Bat1.available_soc / Bat2.available_soc = 2`
191168
192-
We need to distribute 900W.
193-
194-
If `distribution_exponent` is:
169+
A request power of 900W will be distributed as follows, for different
170+
values of `distribution_exponent`.
195171
196-
* `0`: distribution for each battery will be the same.
197-
```python
198-
BAT1_DISTRIBUTION = 4500
199-
BAT2_DISTRIBUTION = 450
200-
```
201-
202-
* `1`: then `Bat2` will have 2x more power assigned then `Bat1`.
203-
```python
204-
# 30 * x + 60 * x = 900
205-
X = 100
206-
BAT1_DISTRIBUTION = 300
207-
BAT2_DISTRIBUTION = 600
208-
```
209-
210-
* `2`: then `Bat2` will have 4x more power assigned then `Bat1`.
211-
```python
212-
# 30^2 * x + 60^2 * x = 900
213-
X = 0.2
214-
BAT1_DISTRIBUTION = 180
215-
BAT2_DISTRIBUTION = 720
216-
```
217-
218-
* `3`: then `Bat2` will have 8x more power assigned then `Bat1`.
219-
```python
220-
# 30^3 * x + 60^3 * x = 900
221-
X = 0.003703704
222-
BAT1_DISTRIBUTION = 100
223-
BAT2_DISTRIBUTION = 800
224-
```
172+
| distribution_exponent | Bat1 | Bat2 |
173+
|-----------------------|------|------|
174+
| 0 | 450 | 450 |
175+
| 1 | 300 | 600 |
176+
| 2 | 180 | 720 |
177+
| 3 | 100 | 800 |
225178
226179
# Example 3
227180
@@ -230,26 +183,19 @@ def __init__(self, distributor_exponent: float = 1) -> None:
230183
* `Bat1.soc = 44` and `Bat2.soc = 64`.
231184
* `Bat1.available_soc = 36 (80 - 44)`, `Bat2.available_soc = 16 (80 - 64)`
232185
233-
We need to distribute 900W.
186+
A request power of 900W will be distributed as follows, for these values of
187+
`distribution_exponent`:
234188
235189
If `distribution_exponent` is:
236190
237-
* `0`: distribution for each battery will be the equal.
238-
```python
239-
BAT1_DISTRIBUTION = 450
240-
BAT2_DISTRIBUTION = 450
241-
```
242-
243-
* `0.5`: then `Bat2` will have 6/4x more power assigned then `Bat1`.
244-
```python
245-
# sqrt(36) * x + sqrt(16) * x = 900
246-
X = 100
247-
BAT1_DISTRIBUTION = 600
248-
BAT2_DISTRIBUTION = 400
249-
```
191+
| distribution_exponent | Bat1 | Bat2 |
192+
|-----------------------|------|------|
193+
| 0 | 450 | 450 |
194+
| 0.5 | 600 | 400 |
250195
251196
Raises:
252197
ValueError: If distributor_exponent < 0
198+
253199
"""
254200
super().__init__()
255201

@@ -401,6 +347,8 @@ def _distribute_power( # pylint: disable=too-many-arguments
401347

402348
for inverter_id, deficit in deficits.items():
403349
while not is_close_to_zero(deficit) and deficit < 0.0:
350+
if not excess_reserved:
351+
break
404352
take_from = max(excess_reserved.items(), key=lambda item: item[1])
405353
if is_close_to_zero(take_from[1]) or take_from[1] < 0.0:
406354
break

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

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import grpc
2727
from frequenz.channels import Peekable, Receiver, Sender
2828

29+
from ..._internal._math import is_close_to_zero
2930
from ...actor import ChannelRegistry
3031
from ...actor._decorator import actor
3132
from ...microgrid import ComponentGraph, connection_manager
@@ -36,10 +37,14 @@
3637
ComponentCategory,
3738
InverterData,
3839
)
39-
from ...power import DistributionAlgorithm, DistributionResult, InvBatPair
4040
from ._battery_pool_status import BatteryPoolStatus, BatteryStatus
41+
from ._distribution_algorithm import (
42+
DistributionAlgorithm,
43+
DistributionResult,
44+
InvBatPair,
45+
)
4146
from .request import Request
42-
from .result import Error, OutOfBound, PartialFailure, PowerBounds, Result, Success
47+
from .result import Error, OutOfBounds, PartialFailure, PowerBounds, Result, Success
4348

4449
_logger = logging.getLogger(__name__)
4550

@@ -269,56 +274,6 @@ def _get_bounds(
269274
),
270275
)
271276

272-
def _get_upper_bound(self, batteries: abc.Set[int], include_broken: bool) -> float:
273-
"""Get total upper bound of power to be set for given batteries.
274-
275-
Note, output of that function doesn't guarantee that this bound will be
276-
the same when the request is processed.
277-
278-
Args:
279-
batteries: List of batteries
280-
include_broken: whether all batteries in the batteries set in the
281-
request must be used regardless the status.
282-
283-
Returns:
284-
Upper bound for `set_power` operation.
285-
"""
286-
pairs_data: List[InvBatPair] = self._get_components_data(
287-
batteries, include_broken
288-
)
289-
return sum(
290-
min(
291-
battery.power_inclusion_upper_bound,
292-
inverter.active_power_inclusion_upper_bound,
293-
)
294-
for battery, inverter in pairs_data
295-
)
296-
297-
def _get_lower_bound(self, batteries: abc.Set[int], include_broken: bool) -> float:
298-
"""Get total lower bound of power to be set for given batteries.
299-
300-
Note, output of that function doesn't guarantee that this bound will be
301-
the same when the request is processed.
302-
303-
Args:
304-
batteries: List of batteries
305-
include_broken: whether all batteries in the batteries set in the
306-
request must be used regardless the status.
307-
308-
Returns:
309-
Lower bound for `set_power` operation.
310-
"""
311-
pairs_data: List[InvBatPair] = self._get_components_data(
312-
batteries, include_broken
313-
)
314-
return sum(
315-
max(
316-
battery.power_inclusion_lower_bound,
317-
inverter.active_power_inclusion_lower_bound,
318-
)
319-
for battery, inverter in pairs_data
320-
)
321-
322277
async def _send_result(self, namespace: str, result: Result) -> None:
323278
"""Send result to the user.
324279
@@ -375,6 +330,7 @@ async def run(self) -> None:
375330
try:
376331
distribution = self._get_power_distribution(request, pairs_data)
377332
except ValueError as err:
333+
_logger.exception("Couldn't distribute power")
378334
error_msg = f"Couldn't distribute power, error: {str(err)}"
379335
await self._send_result(
380336
request.namespace, Error(request=request, msg=str(error_msg))
@@ -521,14 +477,20 @@ def _check_request(
521477
return Error(request=request, msg=msg)
522478

523479
bounds = self._get_bounds(pairs_data)
480+
481+
# Zero power requests are always forwarded to the microgrid API, even if they
482+
# are outside the exclusion bounds.
483+
if is_close_to_zero(request.power):
484+
return None
485+
524486
if request.adjust_power:
525487
# Automatic power adjustments can only bring down the requested power down
526488
# to the inclusion bounds.
527489
#
528490
# If the requested power is in the exclusion bounds, it is NOT possible to
529491
# increase it so that it is outside the exclusion bounds.
530492
if bounds.exclusion_lower < request.power < bounds.exclusion_upper:
531-
return OutOfBound(request=request, bound=bounds)
493+
return OutOfBounds(request=request, bounds=bounds)
532494
else:
533495
in_lower_range = (
534496
bounds.inclusion_lower <= request.power <= bounds.exclusion_lower
@@ -537,7 +499,7 @@ def _check_request(
537499
bounds.exclusion_upper <= request.power <= bounds.inclusion_upper
538500
)
539501
if not (in_lower_range or in_upper_range):
540-
return OutOfBound(request=request, bound=bounds)
502+
return OutOfBounds(request=request, bounds=bounds)
541503

542504
return None
543505

src/frequenz/sdk/actor/power_distributing/result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ class PowerBounds:
8888

8989

9090
@dataclasses.dataclass
91-
class OutOfBound(Result):
91+
class OutOfBounds(Result):
9292
"""Result returned when the power was not set because it was out of bounds.
9393
9494
This result happens when the originating request was done with
9595
`adjust_power = False` and the requested power is not within the batteries bounds.
9696
"""
9797

98-
bound: PowerBounds
98+
bounds: PowerBounds
9999
"""The power bounds for the requested batteries.
100100
101101
If the requested power negative, then this value is the lower bound.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# License: MIT
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

4-
"""Test power distribution module."""
4+
"""Tests for the power distributing actor and algorithm."""

tests/power/test_distribution_algorithm.py renamed to tests/actor/power_distributing/test_distribution_algorithm.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010

1111
from pytest import approx, raises
1212

13+
from frequenz.sdk.actor.power_distributing._distribution_algorithm import (
14+
DistributionAlgorithm,
15+
DistributionResult,
16+
InvBatPair,
17+
)
1318
from frequenz.sdk.actor.power_distributing.result import PowerBounds
1419
from frequenz.sdk.microgrid.component import BatteryData, InverterData
15-
from frequenz.sdk.power import DistributionAlgorithm, DistributionResult, InvBatPair
1620

17-
from ..utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper
21+
from ...utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper
1822

1923

2024
@dataclass

0 commit comments

Comments
 (0)