Skip to content

Commit

Permalink
Support exclusion bounds in power distributor (#562)
Browse files Browse the repository at this point in the history
To be merged after #537

Closes #152
  • Loading branch information
llucax authored Aug 9, 2023
2 parents fd18523 + be9d239 commit 6258c76
Show file tree
Hide file tree
Showing 6 changed files with 691 additions and 283 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

- The `ConfigManagingActor` constructor now can accept a `pathlib.Path` as `config_path` too (before it accepted only a `str`).

- The `PowerDistributingActor` now considers exclusion bounds, when finding an optimal distribution for power between batteries.

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
93 changes: 73 additions & 20 deletions src/frequenz/sdk/actor/power_distributing/power_distributing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from ...power import DistributionAlgorithm, DistributionResult, InvBatPair
from ._battery_pool_status import BatteryPoolStatus, BatteryStatus
from .request import Request
from .result import Error, OutOfBound, PartialFailure, Result, Success
from .result import Error, OutOfBound, PartialFailure, PowerBounds, Result, Success

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -226,6 +226,49 @@ def __init__(
bat_id: None for bat_id, _ in self._bat_inv_map.items()
}

def _get_bounds(
self,
pairs_data: list[InvBatPair],
) -> PowerBounds:
"""Get power bounds for given batteries.
Args:
pairs_data: list of battery and adjacent inverter data pairs.
Returns:
Power bounds for given batteries.
"""
return PowerBounds(
inclusion_lower=sum(
max(
battery.power_inclusion_lower_bound,
inverter.active_power_inclusion_lower_bound,
)
for battery, inverter in pairs_data
),
inclusion_upper=sum(
min(
battery.power_inclusion_upper_bound,
inverter.active_power_inclusion_upper_bound,
)
for battery, inverter in pairs_data
),
exclusion_lower=sum(
min(
battery.power_exclusion_lower_bound,
inverter.active_power_exclusion_lower_bound,
)
for battery, inverter in pairs_data
),
exclusion_upper=sum(
max(
battery.power_exclusion_upper_bound,
inverter.active_power_exclusion_upper_bound,
)
for battery, inverter in pairs_data
),
)

def _get_upper_bound(self, batteries: abc.Set[int], include_broken: bool) -> float:
"""Get total upper bound of power to be set for given batteries.
Expand Down Expand Up @@ -307,11 +350,6 @@ async def run(self) -> None:
await asyncio.sleep(self._wait_for_data_sec)

async for request in self._requests_receiver:
error = self._check_request(request)
if error:
await self._send_result(request.namespace, error)
continue

try:
pairs_data: List[InvBatPair] = self._get_components_data(
request.batteries, request.include_broken_batteries
Expand All @@ -329,6 +367,11 @@ async def run(self) -> None:
)
continue

error = self._check_request(request, pairs_data)
if error:
await self._send_result(request.namespace, error)
continue

try:
distribution = self._get_power_distribution(request, pairs_data)
except ValueError as err:
Expand Down Expand Up @@ -452,11 +495,16 @@ def _get_power_distribution(

return result

def _check_request(self, request: Request) -> Optional[Result]:
def _check_request(
self,
request: Request,
pairs_data: List[InvBatPair],
) -> Optional[Result]:
"""Check whether the given request if correct.
Args:
request: request to check
pairs_data: list of battery and adjacent inverter data pairs.
Returns:
Result for the user if the request is wrong, None otherwise.
Expand All @@ -472,19 +520,24 @@ def _check_request(self, request: Request) -> Optional[Result]:
)
return Error(request=request, msg=msg)

if not request.adjust_power:
if request.power < 0:
bound = self._get_lower_bound(
request.batteries, request.include_broken_batteries
)
if request.power < bound:
return OutOfBound(request=request, bound=bound)
else:
bound = self._get_upper_bound(
request.batteries, request.include_broken_batteries
)
if request.power > bound:
return OutOfBound(request=request, bound=bound)
bounds = self._get_bounds(pairs_data)
if request.adjust_power:
# Automatic power adjustments can only bring down the requested power down
# to the inclusion bounds.
#
# If the requested power is in the exclusion bounds, it is NOT possible to
# increase it so that it is outside the exclusion bounds.
if bounds.exclusion_lower < request.power < bounds.exclusion_upper:
return OutOfBound(request=request, bound=bounds)
else:
in_lower_range = (
bounds.inclusion_lower <= request.power <= bounds.exclusion_lower
)
in_upper_range = (
bounds.exclusion_upper <= request.power <= bounds.inclusion_upper
)
if not (in_lower_range or in_upper_range):
return OutOfBound(request=request, bound=bounds)

return None

Expand Down
14 changes: 12 additions & 2 deletions src/frequenz/sdk/actor/power_distributing/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ class Error(Result):
"""The error message explaining why error happened."""


@dataclasses.dataclass
class PowerBounds:
"""Inclusion and exclusion power bounds for requested batteries."""

inclusion_lower: float
exclusion_lower: float
exclusion_upper: float
inclusion_upper: float


@dataclasses.dataclass
class OutOfBound(Result):
"""Result returned when the power was not set because it was out of bounds.
Expand All @@ -85,8 +95,8 @@ class OutOfBound(Result):
`adjust_power = False` and the requested power is not within the batteries bounds.
"""

bound: float
"""The total power bound for the requested batteries.
bound: PowerBounds
"""The power bounds for the requested batteries.
If the requested power negative, then this value is the lower bound.
Otherwise it is upper bound.
Expand Down
Loading

0 comments on commit 6258c76

Please sign in to comment.