33
44"""Formula generator from component graph for Grid Power."""
55
6+ import itertools
67import logging
78
8- from frequenz .client .microgrid import ComponentMetricId
9+ from frequenz .client .microgrid import Component , ComponentCategory , ComponentMetricId
910
1011from ....microgrid import connection_manager
1112from ..._quantities import Power
1213from ...formula_engine import FormulaEngine
14+ from ._fallback_formula_metric_fetcher import FallbackFormulaMetricFetcher
1315from ._formula_generator import (
1416 NON_EXISTING_COMPONENT_ID ,
1517 ComponentNotFound ,
1618 FormulaGenerationError ,
1719 FormulaGenerator ,
20+ FormulaGeneratorConfig ,
1821)
1922
2023_logger = logging .getLogger (__name__ )
@@ -48,8 +51,8 @@ def generate(
4851 builder = self ._get_builder (
4952 "battery-power" , ComponentMetricId .ACTIVE_POWER , Power .from_watts
5053 )
51- component_ids = self . _config . component_ids
52- if not component_ids :
54+
55+ if not self . _config . component_ids :
5356 _logger .warning (
5457 "No Battery component IDs specified. "
5558 "Subscribing to the resampling actor with a non-existing "
@@ -63,43 +66,109 @@ def generate(
6366 )
6467 return builder .build ()
6568
69+ component_ids = set (self ._config .component_ids )
6670 component_graph = connection_manager .get ().component_graph
71+ inv_bat_mapping : dict [Component , set [Component ]] = {}
6772
68- battery_inverters = frozenset (
69- frozenset (
73+ for bat_id in component_ids :
74+ inverters = set (
7075 filter (
7176 component_graph .is_battery_inverter ,
7277 component_graph .predecessors (bat_id ),
7378 )
7479 )
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- )
80+ if len (inverters ) == 0 :
81+ raise ComponentNotFound (
82+ "All batteries must have at least one inverter as a predecessor."
83+ f"Battery ID { bat_id } has no inverter as a predecessor." ,
84+ )
8285
83- all_connected_batteries = set ()
84- for inverters in battery_inverters :
8586 for inverter in inverters :
86- all_connected_batteries . update (
87- component_graph . successors ( inverter .component_id )
87+ all_connected_batteries = component_graph . successors (
88+ inverter .component_id
8889 )
90+ battery_ids = set (
91+ map (lambda battery : battery .component_id , all_connected_batteries )
92+ )
93+ if not battery_ids .issubset (component_ids ):
94+ raise FormulaGenerationError (
95+ f"Not all batteries behind inverter { inverter .component_id } "
96+ f"are requested. Missing: { battery_ids - component_ids } "
97+ )
98+
99+ inv_bat_mapping [inverter ] = all_connected_batteries
100+
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 )
122+
123+ 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.
89129
90- if len (all_connected_batteries ) != len (component_ids ):
91- raise FormulaGenerationError (
92- "All batteries behind a set of inverters must be requested."
130+ The primary component is the one that will be used to calculate the battery power.
131+ If it is not available, the fallback formula will be used instead.
132+ Fallback formulas calculate 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+ )
93158 )
94159
95- builder . push_oper ( "(" )
96- builder . push_oper ( "(" )
97- # Iterate over the flattened list of inverters
98- for idx , comp in enumerate (
99- inverter for inverters in battery_inverters for inverter in inverters
100- ):
101- if idx > 0 :
102- builder . push_oper ( "+" )
103- builder . push_component_metric ( comp . component_id , nones_are_zeros = True )
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+ )
104169
105- return builder .build ()
170+ fallback_formulas [primary_component ] = FallbackFormulaMetricFetcher (
171+ generator
172+ )
173+
174+ return fallback_formulas
0 commit comments