Skip to content

Commit 0b1c5ae

Browse files
committed
Add FormulaGenerator class and grid_power/battery_power formulas
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent a2b4475 commit 0b1c5ae

File tree

5 files changed

+238
-2
lines changed

5 files changed

+238
-2
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Generators for formulas from component graphs."""
5+
6+
from ._battery_power_formula import BatteryPowerFormula
7+
from ._formula_generator import FormulaGenerator
8+
from ._grid_power_formula import GridPowerFormula
9+
10+
__all__ = [
11+
"GridPowerFormula",
12+
"BatteryPowerFormula",
13+
"FormulaGenerator",
14+
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for Grid Power."""
5+
6+
from .....sdk import microgrid
7+
from ....microgrid.component import ComponentCategory, ComponentMetricId, InverterType
8+
from .._formula_engine import FormulaEngine
9+
from ._formula_generator import FormulaGenerator
10+
11+
12+
class BatteryPowerFormula(FormulaGenerator):
13+
"""Creates a formula engine from the component graph for calculating grid power."""
14+
15+
async def generate(
16+
self,
17+
) -> FormulaEngine:
18+
"""Make a formula for the cumulative AC battery power of a microgrid.
19+
20+
The calculation is performed by adding the Active Powers of all the inverters
21+
that are attached to batteries.
22+
23+
If there's no data coming from an inverter, that inverter's power will be
24+
treated as 0.
25+
26+
Returns:
27+
A formula engine that will calculate cumulative battery power values.
28+
29+
Raises:
30+
RuntimeError: if there are no batteries in the component graph, or if they
31+
don't have an inverter as a predecessor.
32+
"""
33+
builder = self._get_builder(ComponentMetricId.ACTIVE_POWER)
34+
component_graph = microgrid.get().component_graph
35+
battery_inverters = list(
36+
comp
37+
for comp in component_graph.components()
38+
if comp.category == ComponentCategory.INVERTER
39+
and comp.type == InverterType.BATTERY
40+
)
41+
42+
if not battery_inverters:
43+
raise RuntimeError(
44+
"Unable to find any battery inverters in the component graph."
45+
)
46+
47+
for idx, comp in enumerate(battery_inverters):
48+
if idx > 0:
49+
builder.push_oper("+")
50+
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
51+
52+
return builder.build()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Base class for formula generators that use the component graphs."""
5+
6+
from abc import ABC, abstractmethod
7+
8+
from frequenz.channels import Sender
9+
10+
from ....actor import ChannelRegistry, ComponentMetricRequest
11+
from ....microgrid.component import ComponentMetricId
12+
from .._formula_engine import FormulaEngine
13+
from .._resampled_formula_builder import ResampledFormulaBuilder
14+
15+
16+
class FormulaGenerator(ABC):
17+
"""A class for generating formulas from the component graph."""
18+
19+
def __init__(
20+
self,
21+
namespace: str,
22+
channel_registry: ChannelRegistry,
23+
resampler_subscription_sender: Sender[ComponentMetricRequest],
24+
) -> None:
25+
"""Create a `FormulaGenerator` instance.
26+
27+
Args:
28+
namespace: A namespace to use with the data-pipeline.
29+
channel_registry: A channel registry instance shared with the resampling
30+
actor.
31+
resampler_subscription_sender: A sender for sending metric requests to the
32+
resampling actor.
33+
"""
34+
self._channel_registry = channel_registry
35+
self._resampler_subscription_sender = resampler_subscription_sender
36+
self._namespace = namespace
37+
38+
def _get_builder(
39+
self, component_metric_id: ComponentMetricId
40+
) -> ResampledFormulaBuilder:
41+
builder = ResampledFormulaBuilder(
42+
self._namespace,
43+
self._channel_registry,
44+
self._resampler_subscription_sender,
45+
component_metric_id,
46+
)
47+
return builder
48+
49+
@abstractmethod
50+
async def generate(self) -> FormulaEngine:
51+
"""Generate a formula engine, based on the component graph."""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for Grid Power."""
5+
6+
from .....sdk import microgrid
7+
from ....microgrid.component import ComponentCategory, ComponentMetricId
8+
from .._formula_engine import FormulaEngine
9+
from ._formula_generator import FormulaGenerator
10+
11+
12+
class GridPowerFormula(FormulaGenerator):
13+
"""Creates a formula engine from the component graph for calculating grid power."""
14+
15+
async def generate(
16+
self,
17+
) -> FormulaEngine:
18+
"""Generate a formula for calculating grid power from the component graph.
19+
20+
Returns:
21+
A formula engine that will calculate grid power values.
22+
23+
Raises:
24+
RuntimeError: when the component graph doesn't have a `GRID` component.
25+
"""
26+
builder = self._get_builder(ComponentMetricId.ACTIVE_POWER)
27+
component_graph = microgrid.get().component_graph
28+
grid_component = next(
29+
(
30+
comp
31+
for comp in component_graph.components()
32+
if comp.category == ComponentCategory.GRID
33+
),
34+
None,
35+
)
36+
37+
if grid_component is None:
38+
raise RuntimeError(
39+
"Unable to find a GRID component from the component graph."
40+
)
41+
42+
grid_successors = component_graph.successors(grid_component.component_id)
43+
44+
# generate a formula that just adds values from all commponents that are
45+
# directly connected to the grid.
46+
for idx, comp in enumerate(grid_successors):
47+
if idx > 0:
48+
builder.push_oper("+")
49+
50+
# Ensure the device has an `ACTIVE_POWER` metric. When inverters
51+
# produce `None` samples, those inverters are excluded from the
52+
# calculation by treating their `None` values as `0`s.
53+
#
54+
# This is not possible for Meters, so when they produce `None`
55+
# values, those values get propagated as the output.
56+
if comp.category == ComponentCategory.INVERTER:
57+
nones_are_zeros = True
58+
elif comp.category == ComponentCategory.METER:
59+
nones_are_zeros = False
60+
else:
61+
continue
62+
63+
await builder.push_component_metric(
64+
comp.component_id, nones_are_zeros=nones_are_zeros
65+
)
66+
67+
return builder.build()

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

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33

44
"""A logical meter for calculating high level metrics for a microgrid."""
55

6+
from __future__ import annotations
7+
68
import asyncio
79
import logging
810
import uuid
9-
from typing import Dict, List
11+
from typing import Dict, List, Type
1012

1113
from frequenz.channels import Broadcast, Receiver, Sender
1214

1315
from ...actor import ChannelRegistry, ComponentMetricRequest
16+
from ...microgrid import ComponentGraph
1417
from ...microgrid.component import ComponentMetricId
1518
from .. import Sample
1619
from ._formula_engine import FormulaEngine
20+
from ._formula_generators import BatteryPowerFormula, FormulaGenerator, GridPowerFormula
1721
from ._resampled_formula_builder import ResampledFormulaBuilder
1822

1923
logger = logging.Logger(__name__)
@@ -35,6 +39,7 @@ def __init__(
3539
self,
3640
channel_registry: ChannelRegistry,
3741
resampler_subscription_sender: Sender[ComponentMetricRequest],
42+
component_graph: ComponentGraph,
3843
) -> None:
3944
"""Create a `LogicalMeter instance`.
4045
@@ -43,14 +48,15 @@ def __init__(
4348
actor.
4449
resampler_subscription_sender: A sender for sending metric requests to the
4550
resampling actor.
51+
component_graph: The component graph representing the microgrid.
4652
"""
4753
self._channel_registry = channel_registry
4854
self._resampler_subscription_sender = resampler_subscription_sender
4955

5056
# Use a randomly generated uuid to create a unique namespace name for the local
5157
# meter to use when communicating with the resampling actor.
5258
self._namespace = f"logical-meter-{uuid.uuid4()}"
53-
59+
self._component_graph = component_graph
5460
self._output_channels: Dict[str, Broadcast[Sample]] = {}
5561
self._tasks: List[asyncio.Task[None]] = []
5662

@@ -118,3 +124,49 @@ async def start_formula(
118124
)
119125
)
120126
return out_chan.new_receiver()
127+
128+
async def _get_formula_stream(
129+
self,
130+
channel_key: str,
131+
generator: Type[FormulaGenerator],
132+
) -> Receiver[Sample]:
133+
if channel_key in self._output_channels:
134+
return self._output_channels[channel_key].new_receiver()
135+
136+
formula_engine = await generator(
137+
self._namespace, self._channel_registry, self._resampler_subscription_sender
138+
).generate()
139+
out_chan = Broadcast[Sample](channel_key)
140+
self._output_channels[channel_key] = out_chan
141+
self._tasks.append(
142+
asyncio.create_task(
143+
self._run_formula(formula_engine, out_chan.new_sender())
144+
)
145+
)
146+
return out_chan.new_receiver()
147+
148+
async def grid_power(self) -> Receiver[Sample]:
149+
"""Fetch the grid power for the microgrid.
150+
151+
If a formula engine to calculate grid power is not already running, it
152+
will be started. Else, we'll just get a new receiver to the already
153+
existing data stream.
154+
155+
Returns:
156+
A *new* receiver that will stream grid_power values.
157+
158+
"""
159+
return await self._get_formula_stream("grid_power", GridPowerFormula)
160+
161+
async def battery_power(self) -> Receiver[Sample]:
162+
"""Fetch the cumulative battery power in the microgrid.
163+
164+
If a formula engine to calculate cumulative battery power is not
165+
already running, it will be started. Else, we'll just get a new
166+
receiver to the already existing data stream.
167+
168+
Returns:
169+
A *new* receiver that will stream battery_power values.
170+
171+
"""
172+
return await self._get_formula_stream("battery_power", BatteryPowerFormula)

0 commit comments

Comments
 (0)