Skip to content

Commit d033941

Browse files
Improve consumer power formula (#609)
Use dfs on the component graph to find consumer or non consumer components, in order to enhance the consumer power formula.
2 parents 3551cd3 + c26bb5e commit d033941

File tree

2 files changed

+93
-51
lines changed

2 files changed

+93
-51
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ This release replaces the `@actor` decorator with a new `Actor` class.
8484

8585
## New Features
8686

87-
- DFS for compentent graph
87+
- Added `DFS` to the component graph
8888

8989
- `BackgroundService`: This new abstract base class can be used to write other classes that runs one or more tasks in the background. It provides a consistent API to start and stop these services and also takes care of the handling of the background tasks. It can also work as an `async` context manager, giving the service a deterministic lifetime and guaranteed cleanup.
9090

@@ -106,6 +106,6 @@ This release replaces the `@actor` decorator with a new `Actor` class.
106106

107107
- Fix `pv_power` not working in setups with 2 grid meters by using a new reliable function to search for components in the components graph
108108

109-
- Fix `consumer_power` similar to `pv_power`
109+
- Fix `consumer_power` and `producer_power` similar to `pv_power`
110110

111111
- Zero value requests received by the `PowerDistributingActor` will now always be accepted, even when there are non-zero exclusion bounds.

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

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

6-
from __future__ import annotations
7-
86
import logging
9-
from collections import abc
107

118
from ....microgrid import connection_manager
129
from ....microgrid.component import Component, ComponentCategory, ComponentMetricId
@@ -50,95 +47,140 @@ def generate(self) -> FormulaEngine[Power]:
5047
if not grid_successors:
5148
raise ComponentNotFound("No components found in the component graph.")
5249

53-
if len(grid_successors) == 1:
54-
grid_meter = next(iter(grid_successors))
55-
if grid_meter.category != ComponentCategory.METER:
56-
raise RuntimeError(
57-
"Only grid successor in the component graph is not a meter."
58-
)
59-
return self._gen_with_grid_meter(builder, grid_meter)
60-
return self._gen_without_grid_meter(builder, grid_successors)
50+
component_graph = connection_manager.get().component_graph
51+
if all(
52+
successor.category == ComponentCategory.METER
53+
and not component_graph.is_battery_chain(successor)
54+
and not component_graph.is_chp_chain(successor)
55+
and not component_graph.is_pv_chain(successor)
56+
and not component_graph.is_ev_charger_chain(successor)
57+
for successor in grid_successors
58+
):
59+
return self._gen_with_grid_meter(builder, grid_successors)
60+
61+
return self._gen_without_grid_meter(builder, self._get_grid_component())
6162

6263
def _gen_with_grid_meter(
6364
self,
6465
builder: ResampledFormulaBuilder[Power],
65-
grid_meter: Component,
66+
grid_meters: set[Component],
6667
) -> FormulaEngine[Power]:
6768
"""Generate formula for calculating consumer power with grid meter.
6869
6970
Args:
7071
builder: The formula engine builder.
71-
grid_meter: The grid meter component.
72+
grid_meters: The grid meter component.
7273
7374
Returns:
7475
A formula engine that will calculate the consumer power.
7576
"""
77+
assert grid_meters
7678
component_graph = connection_manager.get().component_graph
77-
successors = component_graph.successors(grid_meter.component_id)
7879

79-
builder.push_component_metric(grid_meter.component_id, nones_are_zeros=False)
80+
def non_consumer_component(component: Component) -> bool:
81+
"""
82+
Check if a component is not a consumer component.
83+
84+
Args:
85+
component: The component to check.
8086
81-
for successor in successors:
87+
Returns:
88+
True if the component is not a consumer component, False otherwise.
89+
"""
8290
# If the component graph supports additional types of grid successors in the
8391
# future, additional checks need to be added here.
84-
if (
85-
component_graph.is_battery_chain(successor)
86-
or component_graph.is_chp_chain(successor)
87-
or component_graph.is_pv_chain(successor)
88-
or component_graph.is_ev_charger_chain(successor)
89-
):
92+
return (
93+
component_graph.is_battery_chain(component)
94+
or component_graph.is_chp_chain(component)
95+
or component_graph.is_pv_chain(component)
96+
or component_graph.is_ev_charger_chain(component)
97+
)
98+
99+
# join all non consumer components reachable from the different grid meters
100+
non_consumer_components: set[Component] = set()
101+
for grid_meter in grid_meters:
102+
non_consumer_components = non_consumer_components.union(
103+
component_graph.dfs(grid_meter, set(), non_consumer_component)
104+
)
105+
106+
# push all grid meters
107+
for idx, grid_meter in enumerate(grid_meters):
108+
if idx > 0:
90109
builder.push_oper("-")
91-
nones_are_zeros = True
92-
if successor.category == ComponentCategory.METER:
93-
nones_are_zeros = False
94-
builder.push_component_metric(
95-
successor.component_id, nones_are_zeros=nones_are_zeros
96-
)
110+
builder.push_component_metric(
111+
grid_meter.component_id, nones_are_zeros=False
112+
)
113+
114+
# push all non consumer components and subtract them from the grid meters
115+
for component in non_consumer_components:
116+
builder.push_oper("-")
117+
builder.push_component_metric(
118+
component.component_id,
119+
nones_are_zeros=component.category != ComponentCategory.METER,
120+
)
97121

98122
return builder.build()
99123

100124
def _gen_without_grid_meter(
101125
self,
102126
builder: ResampledFormulaBuilder[Power],
103-
grid_successors: abc.Iterable[Component],
127+
grid: Component,
104128
) -> FormulaEngine[Power]:
105129
"""Generate formula for calculating consumer power without a grid meter.
106130
107131
Args:
108132
builder: The formula engine builder.
109-
grid_successors: The grid successors.
133+
grid: The grid component.
110134
111135
Returns:
112136
A formula engine that will calculate the consumer power.
113137
"""
114-
component_graph = connection_manager.get().component_graph
115-
is_first = True
116-
for successor in grid_successors:
138+
139+
def consumer_component(component: Component) -> bool:
140+
"""
141+
Check if a component is a consumer component.
142+
143+
Args:
144+
component: The component to check.
145+
146+
Returns:
147+
True if the component is a consumer component, False otherwise.
148+
"""
117149
# If the component graph supports additional types of grid successors in the
118150
# future, additional checks need to be added here.
119-
if (
120-
component_graph.is_battery_chain(successor)
121-
or component_graph.is_chp_chain(successor)
122-
or component_graph.is_pv_chain(successor)
123-
or component_graph.is_ev_charger_chain(successor)
124-
):
125-
continue
126-
if not is_first:
127-
builder.push_oper("+")
128-
is_first = False
129-
builder.push_component_metric(successor.component_id, nones_are_zeros=False)
151+
return (
152+
component.category
153+
in {ComponentCategory.METER, ComponentCategory.INVERTER}
154+
and not component_graph.is_battery_chain(component)
155+
and not component_graph.is_chp_chain(component)
156+
and not component_graph.is_pv_chain(component)
157+
and not component_graph.is_ev_charger_chain(component)
158+
)
159+
160+
component_graph = connection_manager.get().component_graph
161+
consumer_components = component_graph.dfs(grid, set(), consumer_component)
130162

131-
if len(builder.finalize()[0]) == 0:
163+
if not consumer_components:
164+
_logger.warning(
165+
"Unable to find any consumers in the component graph. "
166+
"Subscribing to the resampling actor with a non-existing "
167+
"component id, so that `0` values are sent from the formula."
168+
)
132169
# If there are no consumer components, we have to send 0 values at the same
133170
# frequency as the other streams. So we subscribe with a non-existing
134171
# component id, just to get a `None` message at the resampling interval.
135172
builder.push_component_metric(
136173
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
137174
)
138-
_logger.warning(
139-
"Unable to find any consumers in the component graph. "
140-
"Subscribing to the resampling actor with a non-existing "
141-
"component id, so that `0` values are sent from the formula."
175+
return builder.build()
176+
177+
for idx, component in enumerate(consumer_components):
178+
if idx > 0:
179+
builder.push_oper("+")
180+
181+
builder.push_component_metric(
182+
component.component_id,
183+
nones_are_zeros=component.category != ComponentCategory.METER,
142184
)
143185

144186
return builder.build()

0 commit comments

Comments
 (0)