From d574ae342b73f03aff84d0fdbde6638bce5cdaaf Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 1 Aug 2023 16:24:57 +0200 Subject: [PATCH 01/12] Add a `_get_bounds` method in PowerDistributingActor This method will replace the `_get_{upper,lower}_bound` methods in subsequent commits. Signed-off-by: Sahas Subramanian --- .../power_distributing/power_distributing.py | 47 ++++++++++++++++++- .../sdk/actor/power_distributing/result.py | 10 ++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/frequenz/sdk/actor/power_distributing/power_distributing.py b/src/frequenz/sdk/actor/power_distributing/power_distributing.py index c7366cbb5..c23900a94 100644 --- a/src/frequenz/sdk/actor/power_distributing/power_distributing.py +++ b/src/frequenz/sdk/actor/power_distributing/power_distributing.py @@ -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__) @@ -226,6 +226,51 @@ def __init__( bat_id: None for bat_id, _ in self._bat_inv_map.items() } + def _get_bounds(self, batteries: abc.Set[int], include_broken: bool) -> PowerBounds: + """Get power bounds for given batteries. + + Args: + batteries: List of batteries + include_broken: whether all batteries in the batteries set in the + request must be used regardless the status. + + Returns: + Power bounds for given batteries. + """ + pairs_data: List[InvBatPair] = self._get_components_data( + batteries, include_broken + ) + 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. diff --git a/src/frequenz/sdk/actor/power_distributing/result.py b/src/frequenz/sdk/actor/power_distributing/result.py index ebc0b641d..b43e599dd 100644 --- a/src/frequenz/sdk/actor/power_distributing/result.py +++ b/src/frequenz/sdk/actor/power_distributing/result.py @@ -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. From d43034316af8f8ca0bb4eeef17106b6e430edee1 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 1 Aug 2023 19:10:10 +0200 Subject: [PATCH 02/12] Remove unused `to_protobuf` method in power distribution tests Signed-off-by: Sahas Subramanian --- tests/power/test_distribution_algorithm.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/power/test_distribution_algorithm.py b/tests/power/test_distribution_algorithm.py index 070cd86f8..a650220ca 100644 --- a/tests/power/test_distribution_algorithm.py +++ b/tests/power/test_distribution_algorithm.py @@ -8,7 +8,6 @@ from datetime import datetime, timezone from typing import Dict, List, Optional -from frequenz.api.common.metrics_pb2 import Bounds # pylint: disable=no-name-in-module from pytest import approx, raises from frequenz.sdk.microgrid.component import BatteryData, InverterData @@ -24,14 +23,6 @@ class Bound: lower: float upper: float - def to_protobuf(self) -> Bounds: - """Create protobuf Bounds message from that instance. - - Returns: - Protobuf Bounds message. - """ - return Bounds(lower=self.lower, upper=self.upper) - @dataclass class Metric: From db23e329aa0e5b465beb02a14b9db0127a1f6cca Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 1 Aug 2023 19:12:02 +0200 Subject: [PATCH 03/12] Support exclusion bounds in power distribution tests This just updates the tests to use data structures that support exclusion bounds, so that once support is implemented in the actor, it would be easy to add incremental tests. Signed-off-by: Sahas Subramanian --- tests/actor/test_power_distributing.py | 29 +-- tests/power/test_distribution_algorithm.py | 251 +++++++++++---------- 2 files changed, 143 insertions(+), 137 deletions(-) diff --git a/tests/actor/test_power_distributing.py b/tests/actor/test_power_distributing.py index 5b9b01be2..ef2625dff 100644 --- a/tests/actor/test_power_distributing.py +++ b/tests/actor/test_power_distributing.py @@ -26,6 +26,7 @@ from frequenz.sdk.actor.power_distributing.result import ( Error, OutOfBound, + PowerBounds, Result, Success, ) @@ -92,7 +93,7 @@ async def init_component_data(self, mockgrid: MockMicrogrid) -> None: battery.component_id, capacity=Metric(98000), soc=Metric(40, Bound(20, 80)), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ) ) @@ -101,7 +102,7 @@ async def init_component_data(self, mockgrid: MockMicrogrid) -> None: await mockgrid.mock_client.send( inverter_msg( inverter.component_id, - power=Bound(-500, 500), + power=PowerBounds(-500, 0, 0, 500), ) ) @@ -166,7 +167,7 @@ async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: 9, soc=Metric(math.nan, Bound(20, 80)), capacity=Metric(98000), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ) ) @@ -231,7 +232,7 @@ async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: 9, soc=Metric(40, Bound(20, 80)), capacity=Metric(math.nan), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ) ) @@ -288,7 +289,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: await mockgrid.mock_client.send( inverter_msg( 18, - power=Bound(math.nan, math.nan), + power=PowerBounds(math.nan, 0, 0, math.nan), ) ) @@ -296,7 +297,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: await mockgrid.mock_client.send( inverter_msg( 8, - power=Bound(-1000, math.nan), + power=PowerBounds(-1000, 0, 0, math.nan), ) ) @@ -305,7 +306,7 @@ async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: 9, soc=Metric(40, Bound(20, 80)), capacity=Metric(float(98000)), - power=Bound(math.nan, math.nan), + power=PowerBounds(math.nan, 0, 0, math.nan), ) ) @@ -450,7 +451,7 @@ async def test_power_distributor_one_user_adjust_power_consume( assert isinstance(result, OutOfBound) assert result is not None assert result.request == request - assert result.bound == 1000 + assert result.bound.inclusion_upper == 1000 async def test_power_distributor_one_user_adjust_power_supply( self, mocker: MockerFixture @@ -503,7 +504,7 @@ async def test_power_distributor_one_user_adjust_power_supply( assert isinstance(result, OutOfBound) assert result is not None assert result.request == request - assert result.bound == -1000 + assert result.bound.inclusion_lower == -1000 async def test_power_distributor_one_user_adjust_power_success( self, mocker: MockerFixture @@ -760,13 +761,13 @@ async def test_force_request_battery_nan_value_non_cached( 9, soc=Metric(math.nan, Bound(20, 80)), capacity=Metric(math.nan), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ), battery_msg( 19, soc=Metric(40, Bound(20, 80)), capacity=Metric(math.nan), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ), ) @@ -849,19 +850,19 @@ async def test_result() -> None: 9, soc=Metric(math.nan, Bound(20, 80)), capacity=Metric(98000), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ), battery_msg( 19, soc=Metric(40, Bound(20, 80)), capacity=Metric(math.nan), - power=Bound(-1000, 1000), + power=PowerBounds(-1000, 0, 0, 1000), ), battery_msg( 29, soc=Metric(40, Bound(20, 80)), capacity=Metric(float(98000)), - power=Bound(math.nan, math.nan), + power=PowerBounds(math.nan, 0, 0, math.nan), ), ) diff --git a/tests/power/test_distribution_algorithm.py b/tests/power/test_distribution_algorithm.py index a650220ca..48dfacca9 100644 --- a/tests/power/test_distribution_algorithm.py +++ b/tests/power/test_distribution_algorithm.py @@ -10,6 +10,7 @@ from pytest import approx, raises +from frequenz.sdk.actor.power_distributing.result import PowerBounds from frequenz.sdk.microgrid.component import BatteryData, InverterData from frequenz.sdk.power import DistributionAlgorithm, InvBatPair @@ -36,7 +37,7 @@ def battery_msg( # pylint: disable=too-many-arguments component_id: int, capacity: Metric, soc: Metric, - power: Bound, + power: PowerBounds, timestamp: datetime = datetime.now(timezone.utc), ) -> BatteryData: """Create protobuf battery components with given arguments. @@ -58,15 +59,17 @@ def battery_msg( # pylint: disable=too-many-arguments soc=soc.now if soc.now is not None else math.nan, soc_lower_bound=soc.bound.lower if soc.bound is not None else math.nan, soc_upper_bound=soc.bound.upper if soc.bound is not None else math.nan, - power_inclusion_lower_bound=power.lower, - power_inclusion_upper_bound=power.upper, + power_inclusion_lower_bound=power.inclusion_lower, + power_exclusion_lower_bound=power.exclusion_lower, + power_exclusion_upper_bound=power.exclusion_upper, + power_inclusion_upper_bound=power.inclusion_upper, timestamp=timestamp, ) def inverter_msg( component_id: int, - power: Bound, + power: PowerBounds, timestamp: datetime = datetime.now(timezone.utc), ) -> InverterData: """Create protobuf inverter components with given arguments. @@ -83,8 +86,10 @@ def inverter_msg( return InverterDataWrapper( component_id=component_id, timestamp=timestamp, - active_power_inclusion_lower_bound=power.lower, - active_power_inclusion_upper_bound=power.upper, + active_power_inclusion_lower_bound=power.inclusion_lower, + active_power_exclusion_lower_bound=power.exclusion_lower, + active_power_exclusion_upper_bound=power.exclusion_upper, + active_power_inclusion_upper_bound=power.inclusion_upper, ) @@ -259,7 +264,7 @@ def create_components( # pylint: disable=too-many-arguments num: int, capacity: List[Metric], soc: List[Metric], - power: List[Bound], + power: List[PowerBounds], ) -> List[InvBatPair]: """Create components with given arguments. @@ -295,12 +300,12 @@ def test_supply_three_batteries_1(self) -> None: # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(-900, 0), - Bound(-1000, 0), - Bound(-800, 0), - Bound(-700, 0), - Bound(-900, 0), - Bound(-900, 0), + PowerBounds(-900, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-800, 0, 0, 0), + PowerBounds(-700, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), ] components = self.create_components(3, capacity, soc, bounds) @@ -320,12 +325,12 @@ def test_supply_three_batteries_2(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(-900, 0), - Bound(-1000, 0), - Bound(-800, 0), - Bound(-700, 0), - Bound(-900, 0), - Bound(-900, 0), + PowerBounds(-900, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-800, 0, 0, 0), + PowerBounds(-700, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), ] components = self.create_components(3, capacity, soc, bounds) @@ -345,12 +350,12 @@ def test_supply_three_batteries_3(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-600, 0), - Bound(-1000, 0), - Bound(-600, 0), - Bound(-100, 0), - Bound(-800, 0), - Bound(-900, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-100, 0, 0, 0), + PowerBounds(-800, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), ] components = self.create_components(3, capacity, soc, supply_bounds) @@ -370,12 +375,12 @@ def test_supply_three_batteries_4(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(-600, 0), - Bound(-1000, 0), - Bound(-600, 0), - Bound(-100, 0), - Bound(-800, 0), - Bound(-900, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-100, 0, 0, 0), + PowerBounds(-800, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), ] components = self.create_components(3, capacity, soc, bounds) @@ -395,12 +400,12 @@ def test_supply_three_batteries_5(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-600, 0), - Bound(-1000, 0), - Bound(-600, 0), - Bound(-100, 0), - Bound(-800, 0), - Bound(-900, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-100, 0, 0, 0), + PowerBounds(-800, 0, 0, 0), + PowerBounds(-900, 0, 0, 0), ] components = self.create_components(3, capacity, soc, supply_bounds) @@ -420,10 +425,10 @@ def test_supply_two_batteries_1(self) -> None: # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-600, 0), - Bound(-1000, 0), - Bound(-600, 0), - Bound(-1000, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), ] components = self.create_components(2, capacity, soc, supply_bounds) @@ -442,10 +447,10 @@ def test_supply_two_batteries_2(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-600, 0), - Bound(-1000, 0), - Bound(-600, 0), - Bound(-1000, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), + PowerBounds(-600, 0, 0, 0), + PowerBounds(-1000, 0, 0, 0), ] components = self.create_components(2, capacity, soc, supply_bounds) @@ -466,12 +471,12 @@ def test_consumption_three_batteries_1(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 900), - Bound(0, 1000), - Bound(0, 800), - Bound(0, 700), - Bound(0, 900), - Bound(0, 900), + PowerBounds(0, 0, 0, 900), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 700), + PowerBounds(0, 0, 0, 900), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -491,12 +496,12 @@ def test_consumption_three_batteries_2(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 900), - Bound(0, 1000), - Bound(0, 800), - Bound(0, 700), - Bound(0, 900), - Bound(0, 900), + PowerBounds(0, 0, 0, 900), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 700), + PowerBounds(0, 0, 0, 900), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -516,12 +521,12 @@ def test_consumption_three_batteries_3(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 600), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 100), - Bound(0, 800), - Bound(0, 900), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 100), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -541,12 +546,12 @@ def test_consumption_three_batteries_4(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 600), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 100), - Bound(0, 800), - Bound(0, 900), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 100), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -566,12 +571,12 @@ def test_consumption_three_batteries_5(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 600), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 100), - Bound(0, 800), - Bound(0, 900), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 100), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -591,12 +596,12 @@ def test_consumption_three_batteries_6(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 600), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 100), - Bound(0, 800), - Bound(0, 900), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 100), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -616,12 +621,12 @@ def test_consumption_three_batteries_7(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 500), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 700), - Bound(0, 800), - Bound(0, 900), + PowerBounds(0, 0, 0, 500), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 700), + PowerBounds(0, 0, 0, 800), + PowerBounds(0, 0, 0, 900), ] components = self.create_components(3, capacity, soc, bounds) @@ -640,10 +645,10 @@ def test_consumption_two_batteries_1(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 600), - Bound(0, 1000), - Bound(0, 600), - Bound(0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), + PowerBounds(0, 0, 0, 600), + PowerBounds(0, 0, 0, 1000), ] components = self.create_components(2, capacity, soc, bounds) @@ -662,10 +667,10 @@ def test_consumption_two_batteries_distribution_exponent(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), ] components = self.create_components(2, capacity, soc, bounds) @@ -696,10 +701,10 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), ] components = self.create_components(2, capacity, soc, bounds) @@ -748,10 +753,10 @@ def test_supply_two_batteries_distribution_exponent(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), ] components = self.create_components(2, capacity, soc, bounds) @@ -782,10 +787,10 @@ def test_supply_two_batteries_distribution_exponent_1(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), ] components = self.create_components(2, capacity, soc, supply_bounds) @@ -817,12 +822,12 @@ def test_supply_three_batteries_distribution_exponent_2(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), ] components = self.create_components(3, capacity, soc, bounds) @@ -860,12 +865,12 @@ def test_supply_three_batteries_distribution_exponent_3(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm supply_bounds = [ - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), - Bound(-9000, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), + PowerBounds(-9000, 0, 0, 0), ] components = self.create_components(3, capacity, soc, supply_bounds) @@ -890,10 +895,10 @@ def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: ] # consume bounds == 0 makes sure they are not used in supply algorithm bounds = [ - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), - Bound(0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), + PowerBounds(0, 0, 0, 9000), ] components = self.create_components(2, capacity, soc, bounds) From 12749c1c2ac79caaac591cf50a9604a5b32f3edc Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 1 Aug 2023 17:54:38 +0200 Subject: [PATCH 04/12] Respect exclusion bounds in `PowerDistributingActor._check_request` Signed-off-by: Sahas Subramanian --- .../power_distributing/power_distributing.py | 34 ++++++++++++------- .../sdk/actor/power_distributing/result.py | 4 +-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/frequenz/sdk/actor/power_distributing/power_distributing.py b/src/frequenz/sdk/actor/power_distributing/power_distributing.py index c23900a94..0e14d4547 100644 --- a/src/frequenz/sdk/actor/power_distributing/power_distributing.py +++ b/src/frequenz/sdk/actor/power_distributing/power_distributing.py @@ -517,19 +517,27 @@ 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(request.batteries, request.include_broken_batteries) + 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 ( + request.power > bounds.exclusion_lower + and 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 diff --git a/src/frequenz/sdk/actor/power_distributing/result.py b/src/frequenz/sdk/actor/power_distributing/result.py index b43e599dd..188fad9fe 100644 --- a/src/frequenz/sdk/actor/power_distributing/result.py +++ b/src/frequenz/sdk/actor/power_distributing/result.py @@ -95,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. From de1331d6bbc6adbca8dafd1198534456e087c91d Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Wed, 2 Aug 2023 15:11:21 +0200 Subject: [PATCH 05/12] Refactor to reduce the amount of calls to the component data cache Signed-off-by: Sahas Subramanian --- .../power_distributing/power_distributing.py | 31 ++++++++++--------- tests/actor/test_power_distributing.py | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/frequenz/sdk/actor/power_distributing/power_distributing.py b/src/frequenz/sdk/actor/power_distributing/power_distributing.py index 0e14d4547..44f27a8d1 100644 --- a/src/frequenz/sdk/actor/power_distributing/power_distributing.py +++ b/src/frequenz/sdk/actor/power_distributing/power_distributing.py @@ -226,20 +226,18 @@ def __init__( bat_id: None for bat_id, _ in self._bat_inv_map.items() } - def _get_bounds(self, batteries: abc.Set[int], include_broken: bool) -> PowerBounds: + def _get_bounds( + self, + pairs_data: list[InvBatPair], + ) -> PowerBounds: """Get power bounds for given batteries. Args: - batteries: List of batteries - include_broken: whether all batteries in the batteries set in the - request must be used regardless the status. + pairs_data: list of battery and adjacent inverter data pairs. Returns: Power bounds for given batteries. """ - pairs_data: List[InvBatPair] = self._get_components_data( - batteries, include_broken - ) return PowerBounds( inclusion_lower=sum( max( @@ -352,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 @@ -374,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: @@ -497,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. @@ -517,7 +520,7 @@ def _check_request(self, request: Request) -> Optional[Result]: ) return Error(request=request, msg=msg) - bounds = self._get_bounds(request.batteries, request.include_broken_batteries) + 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. diff --git a/tests/actor/test_power_distributing.py b/tests/actor/test_power_distributing.py index ef2625dff..0dc4772f7 100644 --- a/tests/actor/test_power_distributing.py +++ b/tests/actor/test_power_distributing.py @@ -397,7 +397,7 @@ async def test_power_distributor_invalid_battery_id( result: Result = done.pop().result() assert isinstance(result, Error) assert result.request == request - err_msg = re.search(r"^No battery 100, available batteries:", result.msg) + err_msg = re.search(r"No battery 100, available batteries:", result.msg) assert err_msg is not None async def test_power_distributor_one_user_adjust_power_consume( From 3ee0f27165de04021ac47dca7cbb196cd6b686e2 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Thu, 3 Aug 2023 19:03:00 +0200 Subject: [PATCH 06/12] Update distribution algorithm to support exclusion bounds Signed-off-by: Sahas Subramanian --- .../sdk/power/_distribution_algorithm.py | 154 ++++++++++++------ tests/power/test_distribution_algorithm.py | 35 ++-- 2 files changed, 125 insertions(+), 64 deletions(-) diff --git a/src/frequenz/sdk/power/_distribution_algorithm.py b/src/frequenz/sdk/power/_distribution_algorithm.py index e6991c2d0..f0102398e 100644 --- a/src/frequenz/sdk/power/_distribution_algorithm.py +++ b/src/frequenz/sdk/power/_distribution_algorithm.py @@ -4,6 +4,7 @@ """Power distribution algorithm to distribute power between batteries.""" import logging +import math from dataclasses import dataclass from typing import Dict, List, NamedTuple, Tuple @@ -277,8 +278,11 @@ def _total_capacity(self, components: List[InvBatPair]) -> float: return total_capacity def _compute_battery_availability_ratio( - self, components: List[InvBatPair], available_soc: Dict[int, float] - ) -> Tuple[List[Tuple[InvBatPair, float]], float]: + self, + components: List[InvBatPair], + available_soc: Dict[int, float], + excl_bounds: Dict[int, float], + ) -> Tuple[List[Tuple[InvBatPair, float, float]], float]: r"""Compute battery ratio and the total sum of all of them. battery_availability_ratio = capacity_ratio[i] * available_soc[i] @@ -291,6 +295,7 @@ def _compute_battery_availability_ratio( available_soc: How much SoC remained to reach * SoC upper bound - if need to distribute consumption power * SoC lower bound - if need to distribute supply power + excl_bounds: Exclusion bounds for each inverter Returns: Tuple where first argument is battery availability ratio for each @@ -299,32 +304,37 @@ def _compute_battery_availability_ratio( of all battery ratios in the list. """ total_capacity = self._total_capacity(components) - battery_availability_ratio: List[Tuple[InvBatPair, float]] = [] + battery_availability_ratio: List[Tuple[InvBatPair, float, float]] = [] total_battery_availability_ratio: float = 0.0 for pair in components: - battery = pair[0] + battery, inverter = pair capacity_ratio = battery.capacity / total_capacity soc_factor = pow( available_soc[battery.component_id], self._distributor_exponent ) ratio = capacity_ratio * soc_factor - battery_availability_ratio.append((pair, ratio)) + battery_availability_ratio.append( + (pair, excl_bounds[inverter.component_id], ratio) + ) total_battery_availability_ratio += ratio - battery_availability_ratio.sort(key=lambda item: item[1], reverse=True) + battery_availability_ratio.sort( + key=lambda item: (item[1], item[2]), reverse=True + ) return battery_availability_ratio, total_battery_availability_ratio - def _distribute_power( + def _distribute_power( # pylint: disable=too-many-arguments self, components: List[InvBatPair], power_w: float, available_soc: Dict[int, float], - upper_bounds: Dict[int, float], + incl_bounds: Dict[int, float], + excl_bounds: Dict[int, float], ) -> DistributionResult: - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals,too-many-branches,too-many-statements """Distribute power between given components. After this method power should be distributed between batteries @@ -336,9 +346,8 @@ def _distribute_power( available_soc: how much SoC remained to reach: * SoC upper bound - if need to distribute consumption power * SoC lower bound - if need to distribute supply power - upper_bounds: Min between upper bound of each pair in the components list: - * supply upper bound - if need to distribute consumption power - * consumption lower bound - if need to distribute supply power + incl_bounds: Inclusion bounds for each inverter + excl_bounds: Exclusion bounds for each inverter Returns: Distribution result. @@ -346,20 +355,25 @@ def _distribute_power( ( battery_availability_ratio, sum_ratio, - ) = self._compute_battery_availability_ratio(components, available_soc) + ) = self._compute_battery_availability_ratio( + components, available_soc, excl_bounds + ) distribution: Dict[int, float] = {} - + print(f"{power_w=}") # sum_ratio == 0 means that all batteries are fully charged / discharged if is_close_to_zero(sum_ratio): distribution = {inverter.component_id: 0 for _, inverter in components} return DistributionResult(distribution, power_w) distributed_power: float = 0.0 + reserved_power: float = 0.0 power_to_distribute: float = power_w used_ratio: float = 0.0 ratio = sum_ratio - for pair, battery_ratio in battery_availability_ratio: + excess_reserved: dict[int, float] = {} + deficits: dict[int, float] = {} + for pair, excl_bound, battery_ratio in battery_availability_ratio: inverter = pair[1] # ratio = 0, means all remaining batteries reach max SoC lvl or have no # capacity @@ -367,26 +381,63 @@ def _distribute_power( distribution[inverter.component_id] = 0.0 continue - distribution[inverter.component_id] = ( - power_to_distribute * battery_ratio / ratio - ) - + power_to_distribute = power_w - reserved_power + calculated_power = power_to_distribute * battery_ratio / ratio + reserved_power += max(calculated_power, excl_bound) used_ratio += battery_ratio - + ratio = sum_ratio - used_ratio # If the power allocated for that inverter is out of bound, # then we need to distribute more power over all remaining batteries. - upper_bound = upper_bounds[inverter.component_id] - if distribution[inverter.component_id] > upper_bound: - distribution[inverter.component_id] = upper_bound - distributed_power += upper_bound - # Distribute only the remaining power. - power_to_distribute = power_w - distributed_power - # Distribute between remaining batteries - ratio = sum_ratio - used_ratio + incl_bound = incl_bounds[inverter.component_id] + if calculated_power > incl_bound: + excess_reserved[inverter.component_id] = incl_bound - excl_bound + # # Distribute between remaining batteries + elif calculated_power < excl_bound: + deficits[inverter.component_id] = calculated_power - excl_bound else: - distributed_power += distribution[inverter.component_id] + excess_reserved[inverter.component_id] = calculated_power - excl_bound + + distributed_power += excl_bound + distribution[inverter.component_id] = excl_bound + + for inverter_id, deficit in deficits.items(): + while not math.isclose(deficit, 0.0, abs_tol=1e-6) and deficit < 0.0: + take_from = max(excess_reserved.items(), key=lambda item: item[1]) + if math.isclose(take_from[1], 0.0, abs_tol=1e-6) or take_from[1] < 0.0: + break + if take_from[1] >= -deficit or math.isclose( + take_from[1], -deficit, abs_tol=1e-6 + ): + excess_reserved[take_from[0]] += deficit + deficits[inverter_id] = 0.0 + deficit = 0.0 + else: + deficit += excess_reserved[take_from[0]] + deficits[inverter_id] = deficit + excess_reserved[take_from[0]] = 0.0 + + for inverter_id, excess in excess_reserved.items(): + distribution[inverter_id] += excess + distributed_power += excess + + for inverter_id, deficit in deficits.items(): + if deficit < -0.1: + left_over = power_w - distributed_power + if left_over > -deficit: + distributed_power += deficit + deficit = 0.0 + deficits[inverter_id] = 0.0 + elif left_over > 0.0: + deficit += left_over + distributed_power += left_over + deficits[inverter_id] = deficit + + left_over = power_w - distributed_power + dist = DistributionResult(distribution, left_over) - return DistributionResult(distribution, power_w - distributed_power) + return self._greedy_distribute_remaining_power( + dist.distribution, incl_bounds, dist.remaining_power + ) def _greedy_distribute_remaining_power( self, @@ -487,19 +538,21 @@ def _distribute_consume_power( 0.0, battery.soc_upper_bound - battery.soc ) - bounds: Dict[int, float] = {} + incl_bounds: Dict[int, float] = {} + excl_bounds: Dict[int, float] = {} for battery, inverter in components: # We can supply/consume with int only - inverter_bound = inverter.active_power_inclusion_upper_bound - battery_bound = battery.power_inclusion_upper_bound - bounds[inverter.component_id] = min(inverter_bound, battery_bound) + incl_bounds[inverter.component_id] = min( + inverter.active_power_inclusion_upper_bound, + battery.power_inclusion_upper_bound, + ) + excl_bounds[inverter.component_id] = max( + inverter.active_power_exclusion_upper_bound, + battery.power_exclusion_upper_bound, + ) - result: DistributionResult = self._distribute_power( - components, power_w, available_soc, bounds - ) - - return self._greedy_distribute_remaining_power( - result.distribution, bounds, result.remaining_power + return self._distribute_power( + components, power_w, available_soc, incl_bounds, excl_bounds ) def _distribute_supply_power( @@ -525,19 +578,20 @@ def _distribute_supply_power( 0.0, battery.soc - battery.soc_lower_bound ) - bounds: Dict[int, float] = {} + incl_bounds: Dict[int, float] = {} + excl_bounds: Dict[int, float] = {} for battery, inverter in components: - # We can consume with int only - inverter_bound = inverter.active_power_inclusion_lower_bound - battery_bound = battery.power_inclusion_lower_bound - bounds[inverter.component_id] = -1 * max(inverter_bound, battery_bound) + incl_bounds[inverter.component_id] = -1 * max( + inverter.active_power_inclusion_lower_bound, + battery.power_inclusion_lower_bound, + ) + excl_bounds[inverter.component_id] = -1 * min( + inverter.active_power_exclusion_lower_bound, + battery.power_exclusion_lower_bound, + ) result: DistributionResult = self._distribute_power( - components, -1 * power_w, available_soc, bounds - ) - - result = self._greedy_distribute_remaining_power( - result.distribution, bounds, result.remaining_power + components, -1 * power_w, available_soc, incl_bounds, excl_bounds ) for inverter_id in result.distribution.keys(): diff --git a/tests/power/test_distribution_algorithm.py b/tests/power/test_distribution_algorithm.py index 48dfacca9..d72ff077f 100644 --- a/tests/power/test_distribution_algorithm.py +++ b/tests/power/test_distribution_algorithm.py @@ -140,11 +140,12 @@ def test_distribute_power_one_battery(self) -> None: components = self.create_components_with_capacity(1, capacity) available_soc: Dict[int, float] = {0: 40} - upper_bounds: Dict[int, float] = {1: 500} + incl_bounds: Dict[int, float] = {1: 500} + excl_bounds: Dict[int, float] = {1: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 650, available_soc, upper_bounds + components, 650, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 500}) @@ -160,11 +161,12 @@ def test_distribute_power_two_batteries_1(self) -> None: components = self.create_components_with_capacity(2, capacity) available_soc: Dict[int, float] = {0: 40, 2: 20} - upper_bounds: Dict[int, float] = {1: 500, 3: 500} + incl_bounds: Dict[int, float] = {1: 500, 3: 500} + excl_bounds: Dict[int, float] = {1: 0, 3: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 600, available_soc, upper_bounds + components, 600, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 400, 3: 200}) @@ -180,11 +182,12 @@ def test_distribute_power_two_batteries_2(self) -> None: components = self.create_components_with_capacity(2, capacity) available_soc: Dict[int, float] = {0: 20, 2: 20} - upper_bounds: Dict[int, float] = {1: 500, 3: 500} + incl_bounds: Dict[int, float] = {1: 500, 3: 500} + excl_bounds: Dict[int, float] = {1: 0, 3: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 600, available_soc, upper_bounds + components, 600, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 200, 3: 400}) @@ -201,11 +204,12 @@ def test_distribute_power_two_batteries_bounds(self) -> None: components = self.create_components_with_capacity(2, capacity) available_soc: Dict[int, float] = {0: 40, 2: 20} - upper_bounds: Dict[int, float] = {1: 250, 3: 330} + incl_bounds: Dict[int, float] = {1: 250, 3: 330} + excl_bounds: Dict[int, float] = {1: 0, 3: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 600, available_soc, upper_bounds + components, 600, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 250, 3: 330}) @@ -217,11 +221,12 @@ def test_distribute_power_three_batteries(self) -> None: components = self.create_components_with_capacity(3, capacity) available_soc: Dict[int, float] = {0: 40, 2: 20, 4: 20} - upper_bounds: Dict[int, float] = {1: 1000, 3: 3400, 5: 3550} + incl_bounds: Dict[int, float] = {1: 1000, 3: 3400, 5: 3550} + excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 1000, available_soc, upper_bounds + components, 1000, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 400, 3: 400, 5: 200}) @@ -233,11 +238,12 @@ def test_distribute_power_three_batteries_2(self) -> None: components = self.create_components_with_capacity(3, capacity) available_soc: Dict[int, float] = {0: 80, 2: 10, 4: 20} - upper_bounds: Dict[int, float] = {1: 400, 3: 3400, 5: 300} + incl_bounds: Dict[int, float] = {1: 400, 3: 3400, 5: 300} + excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 1000, available_soc, upper_bounds + components, 1000, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 400, 3: 300, 5: 300}) @@ -249,11 +255,12 @@ def test_distribute_power_three_batteries_3(self) -> None: components = self.create_components_with_capacity(3, capacity) available_soc: Dict[int, float] = {0: 80, 2: 10, 4: 20} - upper_bounds: Dict[int, float] = {1: 500, 3: 300, 5: 300} + incl_bounds: Dict[int, float] = {1: 500, 3: 300, 5: 300} + excl_bounds: Dict[int, float] = {1: 0, 3: 0, 5: 0} algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm._distribute_power( # pylint: disable=protected-access - components, 1000, available_soc, upper_bounds + components, 1000, available_soc, incl_bounds, excl_bounds ) assert result.distribution == approx({1: 0, 3: 300, 5: 0}) From ffb95bf4194c9ac309022d311a380915422b8ff2 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 4 Aug 2023 12:34:27 +0200 Subject: [PATCH 07/12] Extract reusable `create_components` method outside test class This allows us to not pack all tests in the same class, but group them separately in smaller units. Signed-off-by: Sahas Subramanian --- tests/power/test_distribution_algorithm.py | 99 +++++++++++----------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/tests/power/test_distribution_algorithm.py b/tests/power/test_distribution_algorithm.py index d72ff077f..475ace46e 100644 --- a/tests/power/test_distribution_algorithm.py +++ b/tests/power/test_distribution_algorithm.py @@ -93,6 +93,33 @@ def inverter_msg( ) +def create_components( + num: int, + capacity: List[Metric], + soc: List[Metric], + power: List[PowerBounds], +) -> List[InvBatPair]: + """Create components with given arguments. + + Args: + num: Number of components + capacity: Capacity for each battery + soc: SoC for each battery + soc_bounds: SoC bounds for each battery + supply_bounds: Supply bounds for each battery and inverter + consumption_bounds: Consumption bounds for each battery and inverter + + Returns: + List of the components + """ + components: List[InvBatPair] = [] + for i in range(0, num): + battery = battery_msg(2 * i, capacity[i], soc[i], power[2 * i]) + inverter = inverter_msg(2 * i + 1, power[2 * i + 1]) + components.append(InvBatPair(battery, inverter)) + return components + + class TestDistributionAlgorithm: # pylint: disable=too-many-public-methods """Test whether the algorithm works as expected.""" @@ -266,34 +293,6 @@ def test_distribute_power_three_batteries_3(self) -> None: assert result.distribution == approx({1: 0, 3: 300, 5: 0}) assert result.remaining_power == approx(700.0) - def create_components( # pylint: disable=too-many-arguments - self, - num: int, - capacity: List[Metric], - soc: List[Metric], - power: List[PowerBounds], - ) -> List[InvBatPair]: - """Create components with given arguments. - - Args: - num: Number of components - capacity: Capacity for each battery - soc: SoC for each battery - soc_bounds: SoC bounds for each battery - supply_bounds: Supply bounds for each battery and inverter - consumption_bounds: Consumption bounds for each battery and inverter - - Returns: - List of the components - """ - - components: List[InvBatPair] = [] - for i in range(0, num): - battery = battery_msg(2 * i, capacity[i], soc[i], power[2 * i]) - inverter = inverter_msg(2 * i + 1, power[2 * i + 1]) - components.append(InvBatPair(battery, inverter)) - return components - # Test distribute supply power def test_supply_three_batteries_1(self) -> None: """Test distribute supply power for batteries with different SoC.""" @@ -314,7 +313,7 @@ def test_supply_three_batteries_1(self) -> None: PowerBounds(-900, 0, 0, 0), PowerBounds(-900, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-1200, components) @@ -339,7 +338,7 @@ def test_supply_three_batteries_2(self) -> None: PowerBounds(-900, 0, 0, 0), PowerBounds(-900, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-1400, components) @@ -364,7 +363,7 @@ def test_supply_three_batteries_3(self) -> None: PowerBounds(-800, 0, 0, 0), PowerBounds(-900, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, supply_bounds) + components = create_components(3, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-1400, components) @@ -389,7 +388,7 @@ def test_supply_three_batteries_4(self) -> None: PowerBounds(-800, 0, 0, 0), PowerBounds(-900, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-1700, components) @@ -414,7 +413,7 @@ def test_supply_three_batteries_5(self) -> None: PowerBounds(-800, 0, 0, 0), PowerBounds(-900, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, supply_bounds) + components = create_components(3, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-1700, components) @@ -437,7 +436,7 @@ def test_supply_two_batteries_1(self) -> None: PowerBounds(-600, 0, 0, 0), PowerBounds(-1000, 0, 0, 0), ] - components = self.create_components(2, capacity, soc, supply_bounds) + components = create_components(2, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-600, components) @@ -459,7 +458,7 @@ def test_supply_two_batteries_2(self) -> None: PowerBounds(-600, 0, 0, 0), PowerBounds(-1000, 0, 0, 0), ] - components = self.create_components(2, capacity, soc, supply_bounds) + components = create_components(2, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-600, components) @@ -485,7 +484,7 @@ def test_consumption_three_batteries_1(self) -> None: PowerBounds(0, 0, 0, 900), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1200, components) @@ -510,7 +509,7 @@ def test_consumption_three_batteries_2(self) -> None: PowerBounds(0, 0, 0, 900), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1400, components) @@ -535,7 +534,7 @@ def test_consumption_three_batteries_3(self) -> None: PowerBounds(0, 0, 0, 800), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1400, components) @@ -560,7 +559,7 @@ def test_consumption_three_batteries_4(self) -> None: PowerBounds(0, 0, 0, 800), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1700, components) @@ -585,7 +584,7 @@ def test_consumption_three_batteries_5(self) -> None: PowerBounds(0, 0, 0, 800), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1700, components) @@ -610,7 +609,7 @@ def test_consumption_three_batteries_6(self) -> None: PowerBounds(0, 0, 0, 800), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(1700, components) @@ -635,7 +634,7 @@ def test_consumption_three_batteries_7(self) -> None: PowerBounds(0, 0, 0, 800), PowerBounds(0, 0, 0, 900), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(500, components) @@ -657,7 +656,7 @@ def test_consumption_two_batteries_1(self) -> None: PowerBounds(0, 0, 0, 600), PowerBounds(0, 0, 0, 1000), ] - components = self.create_components(2, capacity, soc, bounds) + components = create_components(2, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(600, components) @@ -679,7 +678,7 @@ def test_consumption_two_batteries_distribution_exponent(self) -> None: PowerBounds(0, 0, 0, 9000), PowerBounds(0, 0, 0, 9000), ] - components = self.create_components(2, capacity, soc, bounds) + components = create_components(2, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(8000, components) @@ -713,7 +712,7 @@ def test_consumption_two_batteries_distribution_exponent_1(self) -> None: PowerBounds(0, 0, 0, 9000), PowerBounds(0, 0, 0, 9000), ] - components = self.create_components(2, capacity, soc, bounds) + components = create_components(2, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(900, components) @@ -765,7 +764,7 @@ def test_supply_two_batteries_distribution_exponent(self) -> None: PowerBounds(-9000, 0, 0, 0), PowerBounds(-9000, 0, 0, 0), ] - components = self.create_components(2, capacity, soc, bounds) + components = create_components(2, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-8000, components) @@ -799,7 +798,7 @@ def test_supply_two_batteries_distribution_exponent_1(self) -> None: PowerBounds(-9000, 0, 0, 0), PowerBounds(-9000, 0, 0, 0), ] - components = self.create_components(2, capacity, soc, supply_bounds) + components = create_components(2, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-8000, components) @@ -836,7 +835,7 @@ def test_supply_three_batteries_distribution_exponent_2(self) -> None: PowerBounds(-9000, 0, 0, 0), PowerBounds(-9000, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, bounds) + components = create_components(3, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=1) result = algorithm.distribute_power(-8000, components) @@ -879,7 +878,7 @@ def test_supply_three_batteries_distribution_exponent_3(self) -> None: PowerBounds(-9000, 0, 0, 0), PowerBounds(-9000, 0, 0, 0), ] - components = self.create_components(3, capacity, soc, supply_bounds) + components = create_components(3, capacity, soc, supply_bounds) algorithm = DistributionAlgorithm(distributor_exponent=0.5) result = algorithm.distribute_power(-1300, components) @@ -907,7 +906,7 @@ def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: PowerBounds(0, 0, 0, 9000), PowerBounds(0, 0, 0, 9000), ] - components = self.create_components(2, capacity, soc, bounds) + components = create_components(2, capacity, soc, bounds) algorithm = DistributionAlgorithm(distributor_exponent=0.5) result = algorithm.distribute_power(1000, components) From 03a3cae7d15d615d8e19e52753e49de53807fe74 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Fri, 4 Aug 2023 19:19:21 +0200 Subject: [PATCH 08/12] Test exclusion bounds in power distribution algorithm Signed-off-by: Sahas Subramanian --- tests/power/test_distribution_algorithm.py | 289 ++++++++++++++++++++- 1 file changed, 288 insertions(+), 1 deletion(-) diff --git a/tests/power/test_distribution_algorithm.py b/tests/power/test_distribution_algorithm.py index 475ace46e..722b8c9f8 100644 --- a/tests/power/test_distribution_algorithm.py +++ b/tests/power/test_distribution_algorithm.py @@ -12,7 +12,7 @@ from frequenz.sdk.actor.power_distributing.result import PowerBounds from frequenz.sdk.microgrid.component import BatteryData, InverterData -from frequenz.sdk.power import DistributionAlgorithm, InvBatPair +from frequenz.sdk.power import DistributionAlgorithm, DistributionResult, InvBatPair from ..utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper @@ -919,3 +919,290 @@ def test_supply_two_batteries_distribution_exponent_less_then_1(self) -> None: assert result.distribution == approx({1: 500, 3: 500}) assert result.remaining_power == approx(0.0) + + +class TestDistWithExclBounds: + """Test the distribution algorithm with exclusive bounds.""" + + @staticmethod + def assert_result(result: DistributionResult, expected: DistributionResult) -> None: + """Assert the result is as expected.""" + assert result.distribution == approx(expected.distribution, abs=0.01) + assert result.remaining_power == approx(expected.remaining_power, abs=0.01) + + def test_scenario_1(self) -> None: + """Test scenario 1. + + Set params for 3 batteries: + capacities: 10000, 10000, 10000 + socs: 30, 50, 70 + + individual soc bounds: 10-90 + individual bounds: -1000, -100, 100, 1000 + + battery pool bounds: -3000, -300, 300, 1000 + + Expected result: + + | request | | excess | + | power | distribution | remaining | + |---------+------------------------+-----------| + | -300 | -100, -100, -100 | 0 | + | 300 | 100, 100, 100 | 0 | + | -600 | -100, -200, -300 | 0 | + | 900 | 466.66, 300, 133.33 | 0 | + | -900 | -133.33, -300, -466.66 | 0 | + | 2200 | 1000, 850, 350 | 0 | + | -2200 | -350, -850, -1000 | 0 | + | 2800 | 1000, 1000, 800 | 0 | + | -2800 | -800, -1000, -1000 | 0 | + | 3800 | 1000, 1000, 1000 | 800 | + | -3200 | -1000, -1000, -1000 | -200 | + + """ + capacities: List[Metric] = [Metric(10000), Metric(10000), Metric(10000)] + soc: List[Metric] = [ + Metric(30.0, Bound(10, 90)), + Metric(50.0, Bound(10, 90)), + Metric(70.0, Bound(10, 90)), + ] + bounds = [ + PowerBounds(-1000, -100, 20, 1000), + PowerBounds(-1000, -90, 100, 1000), + PowerBounds(-1000, -50, 100, 1000), + PowerBounds(-1000, -100, 90, 1000), + PowerBounds(-1000, -20, 100, 1000), + PowerBounds(-1000, -100, 80, 1000), + ] + components = create_components(3, capacities, soc, bounds) + + algorithm = DistributionAlgorithm() + + self.assert_result( + algorithm.distribute_power(-300, components), + DistributionResult({1: -100, 3: -100, 5: -100}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(300, components), + DistributionResult({1: 100, 3: 100, 5: 100}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-600, components), + DistributionResult({1: -100, 3: -200, 5: -300}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(900, components), + DistributionResult({1: 450, 3: 300, 5: 150}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-900, components), + DistributionResult({1: -150, 3: -300, 5: -450}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(2200, components), + DistributionResult({1: 1000, 3: 833.33, 5: 366.66}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-2200, components), + DistributionResult({1: -366.66, 3: -833.33, 5: -1000}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(2800, components), + DistributionResult({1: 1000, 3: 1000, 5: 800}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-2800, components), + DistributionResult({1: -800, 3: -1000, 5: -1000}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(3800, components), + DistributionResult({1: 1000, 3: 1000, 5: 1000}, remaining_power=800.0), + ) + self.assert_result( + algorithm.distribute_power(-3200, components), + DistributionResult({1: -1000, 3: -1000, 5: -1000}, remaining_power=-200.0), + ) + + def test_scenario_2(self) -> None: + """Test scenario 2. + + Set params for 3 batteries: + capacities: 10000, 10000, 10000 + socs: 50, 50, 70 + + individual soc bounds: 10-90 + individual bounds: -1000, -100, 100, 1000 + + battery pool bounds: -3000, -300, 300, 1000 + + Expected result: + + | request | | excess | + | power | distribution | remaining | + |---------+---------------------------+-----------| + | -300 | -100, -100, -100 | 0 | + | 300 | 100, 100, 100 | 0 | + | -530 | -151.42, -151.42, -227.14 | 0 | + | 530 | 212, 212, 106 | 0 | + | 2000 | 800, 800, 400 | 0 | + | -2000 | -571.42, -571.42, -857.14 | 0 | + | 2500 | 1000, 1000, 500 | 0 | + | -2500 | -785.71, -714.28, -1000.0 | 0 | + | 3000 | 1000, 1000, 1000 | 0 | + | -3000 | -1000, -1000, -1000 | 0 | + | 3500 | 1000, 1000, 1000 | 500 | + | -3500 | -1000, -1000, -1000 | -500 | + """ + capacities: List[Metric] = [Metric(10000), Metric(10000), Metric(10000)] + soc: List[Metric] = [ + Metric(50.0, Bound(10, 90)), + Metric(50.0, Bound(10, 90)), + Metric(70.0, Bound(10, 90)), + ] + bounds = [ + PowerBounds(-1000, -100, 20, 1000), + PowerBounds(-1000, -90, 100, 1000), + PowerBounds(-1000, -50, 100, 1000), + PowerBounds(-1000, -100, 90, 1000), + PowerBounds(-1000, -20, 100, 1000), + PowerBounds(-1000, -100, 80, 1000), + ] + components = create_components(3, capacities, soc, bounds) + + algorithm = DistributionAlgorithm() + + self.assert_result( + algorithm.distribute_power(-300, components), + DistributionResult({1: -100, 3: -100, 5: -100}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(300, components), + DistributionResult({1: 100, 3: 100, 5: 100}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-530, components), + DistributionResult( + {1: -151.42, 3: -151.42, 5: -227.14}, remaining_power=0.0 + ), + ) + self.assert_result( + algorithm.distribute_power(530, components), + DistributionResult({1: 212, 3: 212, 5: 106}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(2000, components), + DistributionResult({1: 800, 3: 800, 5: 400}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-2000, components), + DistributionResult( + {1: -571.42, 3: -571.42, 5: -857.14}, remaining_power=0.0 + ), + ) + self.assert_result( + algorithm.distribute_power(2500, components), + DistributionResult({1: 1000, 3: 1000, 5: 500}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-2500, components), + DistributionResult( + {1: -785.71, 3: -714.28, 5: -1000.0}, remaining_power=0.0 + ), + ) + self.assert_result( + algorithm.distribute_power(3000, components), + DistributionResult({1: 1000, 3: 1000, 5: 1000}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-3000, components), + DistributionResult({1: -1000, 3: -1000, 5: -1000}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(3500, components), + DistributionResult({1: 1000, 3: 1000, 5: 1000}, remaining_power=500.0), + ) + self.assert_result( + algorithm.distribute_power(-3500, components), + DistributionResult({1: -1000, 3: -1000, 5: -1000}, remaining_power=-500.0), + ) + + def test_scenario_3(self) -> None: + """Test scenario 3. + + Set params for 3 batteries: + capacities: 10000, 10000, 10000 + socs: 50, 50, 70 + + individual soc bounds: 10-90 + individual bounds 1: -1000, 0, 0, 1000 + individual bounds 2: -1000, -100, 100, 1000 + individual bounds 3: -1000, 0, 0, 1000 + + battery pool bounds: -3000, -100, 100, 1000 + + Expected result: + + | request | | excess | + | power | distribution | remaining | + |---------+---------------------------+-----------| + | -300 | -88, -108.57, -123.43 | 0 | + | 300 | 128, 128, 64 | 0 | + | -1800 | -514.28, -514.28, -771.42 | 0 | + | 1800 | 720, 720, 360 | 0 | + | -2800 | -800, -1000, -1000 | 0 | + | 2800 | 1000, 1000, 800 | 0 | + | -3500 | -1000, -1000, -1000 | -500 | + | 3500 | 1000, 1000, 1000 | 500 | + """ + capacities: List[Metric] = [Metric(10000), Metric(10000), Metric(10000)] + soc: List[Metric] = [ + Metric(50.0, Bound(10, 90)), + Metric(50.0, Bound(10, 90)), + Metric(70.0, Bound(10, 90)), + ] + bounds = [ + PowerBounds(-1000, 0, 0, 1000), + PowerBounds(-1000, 0, 0, 1000), + PowerBounds(-1000, -100, 100, 1000), + PowerBounds(-1000, -100, 100, 1000), + PowerBounds(-1000, 0, 0, 1000), + PowerBounds(-1000, 0, 0, 1000), + ] + components = create_components(3, capacities, soc, bounds) + + algorithm = DistributionAlgorithm() + + self.assert_result( + algorithm.distribute_power(-320, components), + DistributionResult({1: -88, 3: -108.57, 5: -123.43}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(320, components), + DistributionResult({1: 128, 3: 128, 5: 64}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-1800, components), + DistributionResult( + {1: -514.28, 3: -514.28, 5: -771.42}, remaining_power=0.0 + ), + ) + self.assert_result( + algorithm.distribute_power(1800, components), + DistributionResult({1: 720, 3: 720, 5: 360}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-2800, components), + DistributionResult({1: -800, 3: -1000, 5: -1000}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(2800, components), + DistributionResult({1: 1000, 3: 1000, 5: 800}, remaining_power=0.0), + ) + self.assert_result( + algorithm.distribute_power(-3500, components), + DistributionResult({1: -1000, 3: -1000, 5: -1000}, remaining_power=-500.0), + ) + self.assert_result( + algorithm.distribute_power(3500, components), + DistributionResult({1: 1000, 3: 1000, 5: 1000}, remaining_power=500.0), + ) From 390096cdb04995ffa3e899471f87f7275f056bbc Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 8 Aug 2023 12:26:38 +0200 Subject: [PATCH 09/12] Update RELEASE_NOTES.md Signed-off-by: Sahas Subramanian --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fd862f365..d7d0105a9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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 From c3418aa3ae5664b7f2d2ed74549e8376c9565767 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 9 Aug 2023 10:07:42 +0200 Subject: [PATCH 10/12] Remove leftover `print()` Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/power/_distribution_algorithm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frequenz/sdk/power/_distribution_algorithm.py b/src/frequenz/sdk/power/_distribution_algorithm.py index f0102398e..b270a0738 100644 --- a/src/frequenz/sdk/power/_distribution_algorithm.py +++ b/src/frequenz/sdk/power/_distribution_algorithm.py @@ -360,7 +360,6 @@ def _distribute_power( # pylint: disable=too-many-arguments ) distribution: Dict[int, float] = {} - print(f"{power_w=}") # sum_ratio == 0 means that all batteries are fully charged / discharged if is_close_to_zero(sum_ratio): distribution = {inverter.component_id: 0 for _, inverter in components} From f034cc4bccb030b8a9a29f154ef83db7cd94a8cf Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 9 Aug 2023 10:09:26 +0200 Subject: [PATCH 11/12] Use `is_close_to_zero()` instead of `math.isclose(..., abs_tol=...)` We already have an utility function to check if a `float` is close to zero, so we can use that instead. Signed-off-by: Leandro Lucarella --- src/frequenz/sdk/power/_distribution_algorithm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frequenz/sdk/power/_distribution_algorithm.py b/src/frequenz/sdk/power/_distribution_algorithm.py index b270a0738..8215737af 100644 --- a/src/frequenz/sdk/power/_distribution_algorithm.py +++ b/src/frequenz/sdk/power/_distribution_algorithm.py @@ -400,9 +400,9 @@ def _distribute_power( # pylint: disable=too-many-arguments distribution[inverter.component_id] = excl_bound for inverter_id, deficit in deficits.items(): - while not math.isclose(deficit, 0.0, abs_tol=1e-6) and deficit < 0.0: + while not is_close_to_zero(deficit) and deficit < 0.0: take_from = max(excess_reserved.items(), key=lambda item: item[1]) - if math.isclose(take_from[1], 0.0, abs_tol=1e-6) or take_from[1] < 0.0: + if is_close_to_zero(take_from[1]) or take_from[1] < 0.0: break if take_from[1] >= -deficit or math.isclose( take_from[1], -deficit, abs_tol=1e-6 From be9d2392765d60b6c3196a0ebf60c6d70ba1fedd Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 9 Aug 2023 10:50:41 +0200 Subject: [PATCH 12/12] Simplify `a < b < c` comparison Co-authored-by: daniel-zullo-frequenz <120166726+daniel-zullo-frequenz@users.noreply.github.com> Signed-off-by: Leandro Lucarella --- .../sdk/actor/power_distributing/power_distributing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/frequenz/sdk/actor/power_distributing/power_distributing.py b/src/frequenz/sdk/actor/power_distributing/power_distributing.py index 44f27a8d1..2ada4d05b 100644 --- a/src/frequenz/sdk/actor/power_distributing/power_distributing.py +++ b/src/frequenz/sdk/actor/power_distributing/power_distributing.py @@ -527,10 +527,7 @@ def _check_request( # # 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 ( - request.power > bounds.exclusion_lower - and request.power < bounds.exclusion_upper - ): + if bounds.exclusion_lower < request.power < bounds.exclusion_upper: return OutOfBound(request=request, bound=bounds) else: in_lower_range = (