Skip to content

Commit 162e4f6

Browse files
committed
Update _methods to use shared battery/inverter mappings function
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent a9a5ba6 commit 162e4f6

File tree

3 files changed

+162
-283
lines changed

3 files changed

+162
-283
lines changed

src/frequenz/sdk/timeseries/battery_pool/_methods.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414

1515
from ..._internal._asyncio import cancel_and_await
1616
from ..._internal._constants import RECEIVER_MAX_SIZE, WAIT_FOR_COMPONENT_DATA_SEC
17+
from ...actor.power_distributing.power_distributing import (
18+
_get_battery_inverter_mappings,
19+
)
1720
from ._component_metric_fetcher import (
1821
ComponentMetricFetcher,
1922
LatestBatteryMetricsFetcher,
2023
LatestInverterMetricsFetcher,
2124
)
2225
from ._component_metrics import ComponentMetricsData
23-
from ._metric_calculator import MetricCalculator, T, battery_inverter_mapping
26+
from ._metric_calculator import MetricCalculator, T
2427

2528
_logger = logging.getLogger(__name__)
2629

@@ -83,7 +86,13 @@ def __init__(
8386
min_update_interval: Minimum frequency for sending update about the change.
8487
"""
8588
self._metric_calculator: MetricCalculator[T] = metric_calculator
86-
self._bat_inv_map = battery_inverter_mapping(self._metric_calculator.batteries)
89+
self._bat_inv_map = _get_battery_inverter_mappings(
90+
self._metric_calculator.batteries,
91+
inv_bats=False,
92+
bat_bats=False,
93+
inv_invs=False,
94+
)["bat_invs"]
95+
8796
self._working_batteries: set[int] = working_batteries.intersection(
8897
metric_calculator.batteries
8998
)
@@ -137,10 +146,11 @@ def update_working_batteries(self, new_working_batteries: set[int]) -> None:
137146
new_set = new_working_batteries.intersection(self._metric_calculator.batteries)
138147

139148
stopped_working = self._working_batteries - new_set
140-
for bid in stopped_working:
149+
for battery_id in stopped_working:
141150
# Removed cached metrics for components that stopped working.
142-
self._cached_metrics.pop(bid, None)
143-
self._cached_metrics.pop(self._bat_inv_map[bid], None)
151+
self._cached_metrics.pop(battery_id, None)
152+
for inv_id in self._bat_inv_map[battery_id]:
153+
self._cached_metrics.pop(inv_id, None)
144154

145155
if new_set != self._working_batteries:
146156
self._working_batteries = new_set

src/frequenz/sdk/timeseries/battery_pool/_metric_calculator.py

Lines changed: 100 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66

77
import logging
88
from abc import ABC, abstractmethod
9-
from collections.abc import Iterable, Mapping, Set
9+
from collections.abc import Mapping, Set
1010
from datetime import datetime, timezone
1111
from typing import Generic, TypeVar
1212

1313
from ... import timeseries
14-
from ...microgrid import connection_manager
15-
from ...microgrid.component import ComponentCategory, ComponentMetricId, InverterType
14+
from ...actor.power_distributing._distribution_algorithm._distribution_algorithm import (
15+
_aggregate_battery_power_bounds,
16+
)
17+
from ...actor.power_distributing.power_distributing import (
18+
_get_battery_inverter_mappings,
19+
)
20+
from ...actor.power_distributing.result import PowerBounds
21+
from ...microgrid.component import ComponentMetricId
1622
from ...timeseries import Sample
1723
from .._quantities import Energy, Percentage, Power, Temperature
1824
from ._component_metrics import ComponentMetricsData
@@ -24,43 +30,6 @@
2430
"""Minimal timestamp that can be used in the formula."""
2531

2632

27-
def battery_inverter_mapping(batteries: Iterable[int]) -> dict[int, int]:
28-
"""Create mapping between battery and adjacent inverter.
29-
30-
Args:
31-
batteries: Set of batteries
32-
33-
Returns:
34-
Mapping between battery and adjacent inverter.
35-
"""
36-
graph = connection_manager.get().component_graph
37-
bat_inv_map: dict[int, int] = {}
38-
for battery_id in batteries:
39-
try:
40-
predecessors = graph.predecessors(battery_id)
41-
except KeyError as err:
42-
# If battery_id is not in the component graph, then print error and ignore
43-
# this id. Wrong component id might be bug in config file. We won't stop
44-
# everything because of bug in config file.
45-
_logger.error(str(err))
46-
continue
47-
48-
inverter_id = next(
49-
(
50-
comp.component_id
51-
for comp in predecessors
52-
if comp.category == ComponentCategory.INVERTER
53-
and comp.type == InverterType.BATTERY
54-
),
55-
None,
56-
)
57-
if inverter_id is None:
58-
_logger.info("Battery %d has no adjacent inverter.", battery_id)
59-
else:
60-
bat_inv_map[battery_id] = inverter_id
61-
return bat_inv_map
62-
63-
6433
# Formula output types class have no common interface
6534
# Print all possible types here.
6635
T = TypeVar("T", Sample[Percentage], Sample[Energy], PowerMetrics, Sample[Temperature])
@@ -462,7 +431,13 @@ def __init__(
462431
Args:
463432
batteries: What batteries should be used for calculation.
464433
"""
465-
self._bat_inv_map = battery_inverter_mapping(batteries)
434+
mappings: dict[str, dict[int, frozenset[int]]] = _get_battery_inverter_mappings(
435+
batteries, inv_bats=False, bat_bats=True, inv_invs=False
436+
)
437+
438+
self._bat_inv_map = mappings["bat_invs"]
439+
self._bat_bats_map = mappings["bat_bats"]
440+
466441
used_batteries = set(self._bat_inv_map.keys())
467442

468443
if len(self._bat_inv_map) == 0:
@@ -518,86 +493,13 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]:
518493
Returns:
519494
Map between inverter id and set of required metrics id.
520495
"""
521-
return {cid: self._inverter_metrics for cid in set(self._bat_inv_map.values())}
522-
523-
def _fetch_inclusion_bounds(
524-
self,
525-
battery_id: int,
526-
inverter_id: int,
527-
metrics_data: dict[int, ComponentMetricsData],
528-
) -> tuple[datetime, list[float], list[float]]:
529-
timestamp = _MIN_TIMESTAMP
530-
inclusion_lower_bounds: list[float] = []
531-
inclusion_upper_bounds: list[float] = []
532-
533-
# Inclusion upper and lower bounds are not related.
534-
# If one is missing, then we can still use the other.
535-
if battery_id in metrics_data:
536-
data = metrics_data[battery_id]
537-
value = data.get(ComponentMetricId.POWER_INCLUSION_UPPER_BOUND)
538-
if value is not None:
539-
timestamp = max(timestamp, data.timestamp)
540-
inclusion_upper_bounds.append(value)
541-
542-
value = data.get(ComponentMetricId.POWER_INCLUSION_LOWER_BOUND)
543-
if value is not None:
544-
timestamp = max(timestamp, data.timestamp)
545-
inclusion_lower_bounds.append(value)
546-
547-
if inverter_id in metrics_data:
548-
data = metrics_data[inverter_id]
549-
550-
value = data.get(ComponentMetricId.ACTIVE_POWER_INCLUSION_UPPER_BOUND)
551-
if value is not None:
552-
timestamp = max(data.timestamp, timestamp)
553-
inclusion_upper_bounds.append(value)
554-
555-
value = data.get(ComponentMetricId.ACTIVE_POWER_INCLUSION_LOWER_BOUND)
556-
if value is not None:
557-
timestamp = max(data.timestamp, timestamp)
558-
inclusion_lower_bounds.append(value)
559-
560-
return (timestamp, inclusion_lower_bounds, inclusion_upper_bounds)
561-
562-
def _fetch_exclusion_bounds(
563-
self,
564-
battery_id: int,
565-
inverter_id: int,
566-
metrics_data: dict[int, ComponentMetricsData],
567-
) -> tuple[datetime, list[float], list[float]]:
568-
timestamp = _MIN_TIMESTAMP
569-
exclusion_lower_bounds: list[float] = []
570-
exclusion_upper_bounds: list[float] = []
571-
572-
# Exclusion upper and lower bounds are not related.
573-
# If one is missing, then we can still use the other.
574-
if battery_id in metrics_data:
575-
data = metrics_data[battery_id]
576-
value = data.get(ComponentMetricId.POWER_EXCLUSION_UPPER_BOUND)
577-
if value is not None:
578-
timestamp = max(timestamp, data.timestamp)
579-
exclusion_upper_bounds.append(value)
580-
581-
value = data.get(ComponentMetricId.POWER_EXCLUSION_LOWER_BOUND)
582-
if value is not None:
583-
timestamp = max(timestamp, data.timestamp)
584-
exclusion_lower_bounds.append(value)
585-
586-
if inverter_id in metrics_data:
587-
data = metrics_data[inverter_id]
588-
589-
value = data.get(ComponentMetricId.ACTIVE_POWER_EXCLUSION_UPPER_BOUND)
590-
if value is not None:
591-
timestamp = max(data.timestamp, timestamp)
592-
exclusion_upper_bounds.append(value)
593-
594-
value = data.get(ComponentMetricId.ACTIVE_POWER_EXCLUSION_LOWER_BOUND)
595-
if value is not None:
596-
timestamp = max(data.timestamp, timestamp)
597-
exclusion_lower_bounds.append(value)
598-
599-
return (timestamp, exclusion_lower_bounds, exclusion_upper_bounds)
496+
return {
497+
inverter_id: self._inverter_metrics
498+
for inverters in set(self._bat_inv_map.values())
499+
for inverter_id in inverters
500+
}
600501

502+
# pylint: disable=too-many-locals
601503
def calculate(
602504
self,
603505
metrics_data: dict[int, ComponentMetricsData],
@@ -618,32 +520,89 @@ def calculate(
618520
Return None if there are no component metrics.
619521
"""
620522
timestamp = _MIN_TIMESTAMP
523+
loop_timestamp = _MIN_TIMESTAMP
621524
inclusion_bounds_lower = 0.0
622525
inclusion_bounds_upper = 0.0
623526
exclusion_bounds_lower = 0.0
624527
exclusion_bounds_upper = 0.0
625528

626-
for battery_id in working_batteries:
627-
inverter_id = self._bat_inv_map[battery_id]
628-
(
629-
_ts,
630-
inclusion_lower_bounds,
631-
inclusion_upper_bounds,
632-
) = self._fetch_inclusion_bounds(battery_id, inverter_id, metrics_data)
633-
timestamp = max(timestamp, _ts)
634-
(
635-
_ts,
636-
exclusion_lower_bounds,
637-
exclusion_upper_bounds,
638-
) = self._fetch_exclusion_bounds(battery_id, inverter_id, metrics_data)
639-
if len(inclusion_upper_bounds) > 0:
640-
inclusion_bounds_upper += min(inclusion_upper_bounds)
641-
if len(inclusion_lower_bounds) > 0:
642-
inclusion_bounds_lower += max(inclusion_lower_bounds)
643-
if len(exclusion_upper_bounds) > 0:
644-
exclusion_bounds_upper += max(exclusion_upper_bounds)
645-
if len(exclusion_lower_bounds) > 0:
646-
exclusion_bounds_lower += min(exclusion_lower_bounds)
529+
battery_sets = {
530+
self._bat_bats_map[battery_id] for battery_id in working_batteries
531+
}
532+
533+
def get_validated_bounds(
534+
comp_id: int, comp_metric_ids: list[ComponentMetricId]
535+
) -> PowerBounds | None:
536+
results: list[float] = []
537+
# Make timestamp accessible
538+
nonlocal loop_timestamp
539+
local_timestamp = loop_timestamp
540+
541+
if data := metrics_data.get(comp_id):
542+
for comp_metric_id in comp_metric_ids:
543+
val = data.get(comp_metric_id)
544+
if val is not None:
545+
local_timestamp = max(loop_timestamp, data.timestamp)
546+
results.append(val)
547+
548+
if len(results) != len(comp_metric_ids):
549+
return None
550+
551+
loop_timestamp = local_timestamp
552+
return PowerBounds(
553+
inclusion_lower=results[0],
554+
exclusion_lower=results[1],
555+
exclusion_upper=results[2],
556+
inclusion_upper=results[3],
557+
)
558+
559+
def get_bounds_list(
560+
comp_ids: frozenset[int], comp_metric_ids: list[ComponentMetricId]
561+
) -> list[PowerBounds]:
562+
return list(
563+
x
564+
for x in map(
565+
lambda comp_id: get_validated_bounds(comp_id, comp_metric_ids),
566+
comp_ids,
567+
)
568+
if x is not None
569+
)
570+
571+
for battery_ids in battery_sets:
572+
loop_timestamp = timestamp
573+
574+
inverter_ids = self._bat_inv_map[next(iter(battery_ids))]
575+
576+
battery_bounds = get_bounds_list(battery_ids, self._battery_metrics)
577+
578+
if len(battery_bounds) == 0:
579+
continue
580+
581+
aggregated_bat_bounds = _aggregate_battery_power_bounds(battery_bounds)
582+
583+
inverter_bounds = get_bounds_list(inverter_ids, self._inverter_metrics)
584+
585+
if len(inverter_bounds) == 0:
586+
continue
587+
588+
timestamp = max(timestamp, loop_timestamp)
589+
590+
inclusion_bounds_lower += max(
591+
aggregated_bat_bounds.inclusion_lower,
592+
sum(bound.inclusion_lower for bound in inverter_bounds),
593+
)
594+
inclusion_bounds_upper += min(
595+
aggregated_bat_bounds.inclusion_upper,
596+
sum(bound.inclusion_upper for bound in inverter_bounds),
597+
)
598+
exclusion_bounds_lower += min(
599+
aggregated_bat_bounds.exclusion_lower,
600+
sum(bound.exclusion_lower for bound in inverter_bounds),
601+
)
602+
exclusion_bounds_upper += max(
603+
aggregated_bat_bounds.exclusion_upper,
604+
sum(bound.exclusion_upper for bound in inverter_bounds),
605+
)
647606

648607
if timestamp == _MIN_TIMESTAMP:
649608
return PowerMetrics(

0 commit comments

Comments
 (0)