Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -609,8 +609,12 @@ def _distribute_multi_inverter_pairs(
else:
remaining_power = power.power

# Inverters are sorted by largest excl bound first
for inverter_id in inverter_ids:
# Sort inverters to have the largest exclusion bounds first
sorted_inverter_ids = sorted(
inverter_ids, key=lambda inv_id: excl_bounds[inv_id], reverse=True
)

for inverter_id in sorted_inverter_ids:
if (
not is_close_to_zero(remaining_power)
and excl_bounds[inverter_id] <= remaining_power
Expand Down
3 changes: 2 additions & 1 deletion src/frequenz/sdk/microgrid/_power_managing/_matryoshka.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ def _validate_component_ids(

for bucket in self._component_buckets:
if any(component_id in bucket for component_id in component_ids):
comp_ids = ", ".join(map(str, sorted(component_ids)))
raise NotImplementedError(
f"PowerManagingActor: component IDs {component_ids} are already"
f"PowerManagingActor: component IDs {comp_ids} are already"
" part of another bucket. Overlapping buckets are not"
" yet supported."
)
Expand Down
10 changes: 5 additions & 5 deletions src/frequenz/sdk/microgrid/component_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,14 +915,14 @@ def _validate_graph(self) -> None:
# This check doesn't seem to have much sense, it only search for nodes without
# data associated with them. We leave it here for now, but we should consider
# removing it in the future.
undefined: list[int] = [
if undefined := [
node[0] for node in self._graph.nodes(data=True) if len(node[1]) == 0
]

if len(undefined) > 0:
]:
undefined_str = ", ".join(map(str, sorted(undefined)))
raise InvalidGraphError(
f"Missing definition for graph components: {undefined}"
f"Missing definition for graph components: {undefined_str}"
)

# should be true as a consequence of checks above
if sum(1 for _ in self.components()) <= 0:
raise InvalidGraphError("Graph must have a least one component!")
Expand Down
8 changes: 4 additions & 4 deletions tests/actor/_power_managing/test_matryoshka.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,8 @@ async def test_matryoshka_drop_old_proposals() -> None:
with pytest.raises(
NotImplementedError,
match=re.escape(
"PowerManagingActor: component IDs frozenset({8, 5}) are already "
+ "part of another bucket. Overlapping buckets are not yet supported."
"PowerManagingActor: component IDs 5, 8 are already part of "
"another bucket. Overlapping buckets are not yet supported."
),
):
tester.tgt_power(
Expand Down Expand Up @@ -506,8 +506,8 @@ def ensure_overlapping_bucket_request_fails() -> None:
with pytest.raises(
NotImplementedError,
match=re.escape(
"PowerManagingActor: component IDs frozenset({8, 5}) are already "
+ "part of another bucket. Overlapping buckets are not yet supported."
"PowerManagingActor: component IDs 5, 8 are already part of "
"another bucket. Overlapping buckets are not yet supported."
),
):
tester.tgt_power(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# pylint: disable=too-many-lines
"""Tests for distribution algorithm."""
import math
from collections.abc import Sequence, Set
from dataclasses import dataclass
from datetime import datetime, timezone

Expand Down Expand Up @@ -1023,6 +1024,20 @@ def assert_result(result: DistributionResult, expected: DistributionResult) -> N
assert result.distribution == approx(expected.distribution, abs=0.01)
assert result.remaining_power == approx(expected.remaining_power, abs=0.01)

@staticmethod
def assert_any_result(
result: DistributionResult,
expected_distribution_ids: Set[int],
expected_distribution_powers: Sequence[float],
expected_remaining_power: float,
) -> None:
"""Assert the result is as expected, disregarding which power goes to which component."""
assert result.remaining_power == expected_remaining_power
assert {*result.distribution.keys()} == expected_distribution_ids
assert list(sorted(result.distribution.values())) == list(
sorted(expected_distribution_powers)
)

def test_scenario_1(self) -> None:
"""Test scenario 1.

Expand Down Expand Up @@ -1349,19 +1364,27 @@ def test_scenario_4(self) -> None:

algorithm = BatteryDistributionAlgorithm()

self.assert_result(
# The assignment of power to each inverter can be swapped, as they both have the
# same bounds, none will be preferred
self.assert_any_result(
algorithm.distribute_power(-300, components),
DistributionResult({2: -300, 3: 0}, remaining_power=0.0),
expected_distribution_ids={2, 3},
expected_distribution_powers=[-300, 0],
expected_remaining_power=0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(300, components),
DistributionResult({2: 300, 3: 0}, remaining_power=0.0),
expected_distribution_ids={2, 3},
expected_distribution_powers=[300, 0],
expected_remaining_power=0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(-1800, components),
DistributionResult({2: -1000, 3: -500}, remaining_power=-300.0),
expected_distribution_ids={2, 3},
expected_distribution_powers=[-1000, -500],
expected_remaining_power=-300,
)

def test_scenario_5(self) -> None:
Expand Down Expand Up @@ -1431,33 +1454,37 @@ def test_scenario_5(self) -> None:

algorithm = BatteryDistributionAlgorithm()

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(-300, components),
DistributionResult({10: -200, 11: 0, 20: -100, 21: 0}, remaining_power=0.0),
expected_distribution_ids={10, 11, 20, 21},
expected_distribution_powers=[-200, 0, -100, 0],
expected_remaining_power=0.0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(300, components),
DistributionResult({10: 200, 11: 0, 20: 100, 21: 0}, remaining_power=0.0),
expected_distribution_ids={10, 11, 20, 21},
expected_distribution_powers=[200, 0, 100, 0],
expected_remaining_power=0.0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(-1800, components),
DistributionResult(
{10: -300, 11: 0, 20: -1000, 21: -500}, remaining_power=0.0
),
expected_distribution_ids={10, 11, 20, 21},
expected_distribution_powers=[-300, 0, -1000, -500],
expected_remaining_power=0.0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(3000, components),
DistributionResult(
{10: 1000, 11: 500, 20: 1000, 21: 500}, remaining_power=0.0
),
expected_distribution_ids={10, 11, 20, 21},
expected_distribution_powers=[1000, 500, 1000, 500],
expected_remaining_power=0.0,
)

self.assert_result(
self.assert_any_result(
algorithm.distribute_power(3500, components),
DistributionResult(
{10: 1000, 11: 500, 20: 1000, 21: 500}, remaining_power=500.0
),
expected_distribution_ids={10, 11, 20, 21},
expected_distribution_powers=[1000, 500, 1000, 500],
expected_remaining_power=500.0,
)
20 changes: 15 additions & 5 deletions tests/timeseries/_battery_pool/test_battery_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,21 @@ async def setup_batteries_pool(mocker: MockerFixture) -> AsyncIterator[SetupArgs
# the scope of this tests. This tests should cover BatteryPool only.
# We use our own battery status channel, where we easily control set of working
# batteries.
all_batteries = list(get_components(mock_microgrid, ComponentCategory.BATTERY))
all_batteries = get_components(mock_microgrid, ComponentCategory.BATTERY)

battery_pool = microgrid.new_battery_pool(
priority=5, component_ids=set(all_batteries[:2])
)
# This is a hack because these tests used to rely on the order in which components
# are returned from the component graph using `all_batteries[:2]` to get the first 2
# batteries. But the component graph returns a set, which doesn't have any ordering
# guarantees, so if the Python implementation changes, the hashing can change and
# this resulting order could be different, breaking the tests. This is why we are
# now specifying IDs to use explicitly (these are the IDs that were used when the
# tests were written). This can also change in the future if generator of mock
# components changes, as it might produce different IDs, so this solution is also
# not bullet-proof.
assert 8 in all_batteries
assert 11 in all_batteries

battery_pool = microgrid.new_battery_pool(priority=5, component_ids=set([8, 11]))

dp = microgrid._data_pipeline._DATA_PIPELINE
assert dp is not None
Expand Down Expand Up @@ -1125,7 +1135,7 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals
)
compare_messages(msg, expected)

batteries_in_pool = list(battery_pool.component_ids)
batteries_in_pool = list(sorted(battery_pool.component_ids))
scenarios: list[Scenario[SystemBounds]] = [
Scenario(
next(iter(bat_invs_map[batteries_in_pool[0]])),
Expand Down
Loading