Skip to content

Commit 958d0d0

Browse files
authored
Add producer power formula (#444)
2 parents 56a9cc5 + aa99ad7 commit 958d0d0

File tree

5 files changed

+160
-0
lines changed

5 files changed

+160
-0
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
## New Features
1212

13+
- The logical meter has a new method that returns producer power, that is the sum of all energy producers.
14+
1315
<!-- Here goes the main new features and examples or instructions on how to use them -->
1416

1517
## Bug Fixes

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from ._grid_current_formula import GridCurrentFormula
1919
from ._grid_power_formula import GridPowerFormula
20+
from ._producer_power_formula import ProducerPowerFormula
2021
from ._pv_power_formula import PVPowerFormula
2122

2223
__all__ = [
@@ -35,6 +36,7 @@
3536
"BatteryPowerFormula",
3637
"EVChargerPowerFormula",
3738
"PVPowerFormula",
39+
"ProducerPowerFormula",
3840
#
3941
# Current formula generators
4042
#
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for Producer Power."""
5+
6+
from __future__ import annotations
7+
8+
from ....microgrid import connection_manager
9+
from ....microgrid.component import ComponentCategory, ComponentMetricId
10+
from .._formula_engine import FormulaEngine
11+
from ._formula_generator import (
12+
NON_EXISTING_COMPONENT_ID,
13+
ComponentNotFound,
14+
FormulaGenerator,
15+
)
16+
17+
18+
class ProducerPowerFormula(FormulaGenerator):
19+
"""Formula generator from component graph for calculating the Producer Power.
20+
21+
The producer power is calculated by summing up the power of all power producers,
22+
which are CHP and PV.
23+
"""
24+
25+
def generate(self) -> FormulaEngine:
26+
"""Generate formula for calculating producer power from the component graph.
27+
28+
Returns:
29+
A formula engine that will calculate the producer power.
30+
31+
Raises:
32+
ComponentNotFound: If the component graph does not contain a producer power
33+
component.
34+
RuntimeError: If the grid component has a single successor that is not a
35+
meter.
36+
"""
37+
builder = self._get_builder("producer_power", ComponentMetricId.ACTIVE_POWER)
38+
component_graph = connection_manager.get().component_graph
39+
grid_component = next(
40+
iter(
41+
component_graph.components(component_category={ComponentCategory.GRID})
42+
),
43+
None,
44+
)
45+
46+
if grid_component is None:
47+
raise ComponentNotFound("Grid component not found in the component graph.")
48+
49+
grid_successors = component_graph.successors(grid_component.component_id)
50+
if not grid_successors:
51+
raise ComponentNotFound("No components found in the component graph.")
52+
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+
grid_successors = component_graph.successors(grid_meter.component_id)
60+
61+
first_iteration = True
62+
for successor in iter(grid_successors):
63+
# if in the future we support additional producers, we need to add them here
64+
if component_graph.is_chp_chain(successor) or component_graph.is_pv_chain(
65+
successor
66+
):
67+
if not first_iteration:
68+
builder.push_oper("+")
69+
70+
first_iteration = False
71+
72+
builder.push_component_metric(
73+
successor.component_id,
74+
nones_are_zeros=successor.category != ComponentCategory.METER,
75+
)
76+
77+
if first_iteration:
78+
builder.push_component_metric(
79+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
80+
)
81+
82+
return builder.build()

src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
FormulaType,
2020
GridCurrentFormula,
2121
GridPowerFormula,
22+
ProducerPowerFormula,
2223
PVPowerFormula,
2324
)
2425

@@ -261,6 +262,31 @@ def consumer_power(self) -> FormulaEngine:
261262
assert isinstance(engine, FormulaEngine)
262263
return engine
263264

265+
@property
266+
def producer_power(self) -> FormulaEngine:
267+
"""Fetch the producer power for the microgrid.
268+
269+
Under normal circumstances this is expected to correspond to the production
270+
of the sites active parts excluding ev chargers and batteries.
271+
272+
This formula produces values that are in the Passive Sign Convention (PSC).
273+
274+
If a formula engine to calculate producer power is not already running, it will
275+
be started.
276+
277+
A receiver from the formula engine can be created using the `new_receiver`
278+
method.
279+
280+
Returns:
281+
A FormulaEngine that will calculate and stream producer power.
282+
"""
283+
engine = self._formula_pool.from_generator(
284+
"producer_power",
285+
ProducerPowerFormula,
286+
)
287+
assert isinstance(engine, FormulaEngine)
288+
return engine
289+
264290
@property
265291
def pv_power(self) -> FormulaEngine:
266292
"""Fetch the PV power in the microgrid.

tests/timeseries/test_logical_meter.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,51 @@ async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None
185185

186186
await mockgrid.mock_data.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0])
187187
assert (await consumer_power_receiver.receive()).value == 20.0
188+
189+
async def test_producer_power(self, mocker: MockerFixture) -> None:
190+
"""Test the producer power formula."""
191+
mockgrid = MockMicrogrid(grid_side_meter=False)
192+
mockgrid.add_solar_inverters(2)
193+
mockgrid.add_chps(2)
194+
await mockgrid.start_mock_datapipeline(mocker)
195+
196+
logical_meter = microgrid.logical_meter()
197+
producer_power_receiver = logical_meter.producer_power.new_receiver()
198+
199+
await mockgrid.mock_data.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0])
200+
assert (await producer_power_receiver.receive()).value == 14.0
201+
202+
async def test_producer_power_no_chp(self, mocker: MockerFixture) -> None:
203+
"""Test the producer power formula without a chp."""
204+
mockgrid = MockMicrogrid(grid_side_meter=False)
205+
mockgrid.add_solar_inverters(2)
206+
await mockgrid.start_mock_datapipeline(mocker)
207+
208+
logical_meter = microgrid.logical_meter()
209+
producer_power_receiver = logical_meter.producer_power.new_receiver()
210+
211+
await mockgrid.mock_data.send_meter_power([20.0, 2.0, 3.0])
212+
assert (await producer_power_receiver.receive()).value == 5.0
213+
214+
async def test_producer_power_no_pv(self, mocker: MockerFixture) -> None:
215+
"""Test the producer power formula without pv."""
216+
mockgrid = MockMicrogrid(grid_side_meter=False)
217+
mockgrid.add_chps(1)
218+
await mockgrid.start_mock_datapipeline(mocker)
219+
220+
logical_meter = microgrid.logical_meter()
221+
producer_power_receiver = logical_meter.producer_power.new_receiver()
222+
223+
await mockgrid.mock_data.send_meter_power([20.0, 2.0])
224+
assert (await producer_power_receiver.receive()).value == 2.0
225+
226+
async def test_no_producer_power(self, mocker: MockerFixture) -> None:
227+
"""Test the producer power formula without producers."""
228+
mockgrid = MockMicrogrid(grid_side_meter=False)
229+
await mockgrid.start_mock_datapipeline(mocker)
230+
231+
logical_meter = microgrid.logical_meter()
232+
producer_power_receiver = logical_meter.producer_power.new_receiver()
233+
234+
await mockgrid.mock_data.send_non_existing_component_value()
235+
assert (await producer_power_receiver.receive()).value == 0.0

0 commit comments

Comments
 (0)