Skip to content

Commit 1a8a336

Browse files
Add fallback formula feature for BatteryPowerFormula
Signed-off-by: Elzbieta Kotulska <[email protected]>
1 parent fd6c03a commit 1a8a336

File tree

3 files changed

+95
-22
lines changed

3 files changed

+95
-22
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
- Fallback components are used in generated formulas. If primary components is unavailable, formula will generate metric from fallback components. Fallback formulas are implemented for:
66
- PVPowerFormula
7-
- ProducerPowerFormula
7+
- ProducerPowerFormula
8+
- BatteryPowerFormula

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def power(self) -> FormulaEngine[Power]:
245245
BatteryPowerFormula,
246246
FormulaGeneratorConfig(
247247
component_ids=self._pool_ref_store._batteries,
248+
allow_fallback=True,
248249
),
249250
)
250251
assert isinstance(engine, FormulaEngine)

src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33

44
"""Formula generator from component graph for Grid Power."""
55

6+
import itertools
67
import logging
78

8-
from frequenz.client.microgrid import ComponentMetricId
9+
from frequenz.client.microgrid import Component, ComponentCategory, ComponentMetricId
910

1011
from ....microgrid import connection_manager
1112
from ..._quantities import Power
1213
from ...formula_engine import FormulaEngine
14+
from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher
1315
from ._formula_generator import (
1416
NON_EXISTING_COMPONENT_ID,
1517
ComponentNotFound,
1618
FormulaGenerationError,
1719
FormulaGenerator,
20+
FormulaGeneratorConfig,
1821
)
1922

2023
_logger = logging.getLogger(__name__)
@@ -48,6 +51,7 @@ def generate(
4851
builder = self._get_builder(
4952
"battery-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
5053
)
54+
5155
component_ids = self._config.component_ids
5256
if not component_ids:
5357
_logger.warning(
@@ -65,39 +69,106 @@ def generate(
6569

6670
component_graph = connection_manager.get().component_graph
6771

68-
battery_inverters = frozenset(
69-
frozenset(
72+
# all_connected_batteries has to be set because
73+
# multiple inverters can be connected to the same battery
74+
inv_bat_mapping: dict[Component, set[Component]] = {}
75+
all_connected_batteries: set[Component] = set()
76+
for bat_id in component_ids:
77+
inverters = set(
7078
filter(
7179
component_graph.is_battery_inverter,
7280
component_graph.predecessors(bat_id),
7381
)
7482
)
75-
for bat_id in component_ids
76-
)
77-
78-
if not all(battery_inverters):
79-
raise ComponentNotFound(
80-
"All batteries must have at least one inverter as a predecessor."
81-
)
83+
if len(inverters) == 0:
84+
raise ComponentNotFound(
85+
"All batteries must have at least one inverter as a predecessor.",
86+
"Battery ID %s has no inverter as a predecessor.",
87+
bat_id,
88+
)
8289

83-
all_connected_batteries = set()
84-
for inverters in battery_inverters:
8590
for inverter in inverters:
86-
all_connected_batteries.update(
87-
component_graph.successors(inverter.component_id)
91+
inv_bat_mapping[inverter] = component_graph.successors(
92+
inverter.component_id
8893
)
94+
all_connected_batteries.update(inv_bat_mapping[inverter])
8995

9096
if len(all_connected_batteries) != len(component_ids):
9197
raise FormulaGenerationError(
9298
"All batteries behind a set of inverters must be requested."
9399
)
94100

95-
# Iterate over the flattened list of inverters
96-
for idx, comp in enumerate(
97-
inverter for inverters in battery_inverters for inverter in inverters
98-
):
99-
if idx > 0:
100-
builder.push_oper("+")
101-
builder.push_component_metric(comp.component_id, nones_are_zeros=True)
101+
if self._config.allow_fallback:
102+
fallbacks = self._get_fallback_formulas(inv_bat_mapping)
103+
104+
for idx, (primary_component, fallback_formula) in enumerate(
105+
fallbacks.items()
106+
):
107+
if idx > 0:
108+
builder.push_oper("+")
109+
110+
builder.push_component_metric(
111+
primary_component.component_id,
112+
nones_are_zeros=(
113+
primary_component.category != ComponentCategory.METER
114+
),
115+
fallback=fallback_formula,
116+
)
117+
else:
118+
for idx, comp in enumerate(inv_bat_mapping.keys()):
119+
if idx > 0:
120+
builder.push_oper("+")
121+
builder.push_component_metric(comp.component_id, nones_are_zeros=True)
102122

103123
return builder.build()
124+
125+
def _get_fallback_formulas(
126+
self, inv_bat_mapping: dict[Component, set[Component]]
127+
) -> dict[Component, FallbackFormulaMetricFetcher[Power] | None]:
128+
"""Find primary and fallback components and create fallback formulas.
129+
130+
The primary component is the one that will be used to calculate the battery power.
131+
But if it is not available, the fallback formula will be used instead.
132+
Fallback formulas calculates the battery power using the fallback components.
133+
Fallback formulas are wrapped in `FallbackFormulaMetricFetcher`.
134+
135+
Args:
136+
inv_bat_mapping: A mapping from inverter to connected batteries.
137+
138+
Returns:
139+
A dictionary mapping primary components to their FallbackFormulaMetricFetcher.
140+
"""
141+
fallbacks = self._get_metric_fallback_components(set(inv_bat_mapping.keys()))
142+
143+
fallback_formulas: dict[
144+
Component, FallbackFormulaMetricFetcher[Power] | None
145+
] = {}
146+
for primary_component, fallback_components in fallbacks.items():
147+
if len(fallback_components) == 0:
148+
fallback_formulas[primary_component] = None
149+
continue
150+
151+
battery_ids = set(
152+
map(
153+
lambda battery: battery.component_id,
154+
itertools.chain.from_iterable(
155+
inv_bat_mapping[inv] for inv in fallback_components
156+
),
157+
)
158+
)
159+
160+
generator = BatteryPowerFormula(
161+
f"{self._namespace}_fallback_{battery_ids}",
162+
self._channel_registry,
163+
self._resampler_subscription_sender,
164+
FormulaGeneratorConfig(
165+
component_ids=battery_ids,
166+
allow_fallback=False,
167+
),
168+
)
169+
170+
fallback_formulas[primary_component] = FallbackFormulaMetricFetcher(
171+
generator
172+
)
173+
174+
return fallback_formulas

0 commit comments

Comments
 (0)