Skip to content

Commit bc9dd63

Browse files
Generate formulas automatically from the component graph (#103)
2 parents 6ab8228 + 3f403e7 commit bc9dd63

File tree

16 files changed

+816
-178
lines changed

16 files changed

+816
-178
lines changed

src/frequenz/sdk/microgrid/client/_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
InverterData,
2424
MeterData,
2525
)
26-
from ..component._component import _component_category_from_protobuf
26+
from ..component._component import (
27+
_component_category_from_protobuf,
28+
_component_type_from_protobuf,
29+
)
2730
from ._connection import Connection
2831
from ._retry import LinearBackoff, RetryStrategy
2932

@@ -229,7 +232,11 @@ async def components(self) -> Iterable[Component]:
229232
component_list.components,
230233
)
231234
result: Iterable[Component] = map(
232-
lambda c: Component(c.id, _component_category_from_protobuf(c.category)),
235+
lambda c: Component(
236+
c.id,
237+
_component_category_from_protobuf(c.category),
238+
_component_type_from_protobuf(c.category, c.inverter),
239+
),
233240
components_only,
234241
)
235242

src/frequenz/sdk/microgrid/component/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
This package provides classes to operate con microgrid components.
77
"""
88

9-
from ._component import Component, ComponentCategory, ComponentMetricId
9+
from ._component import Component, ComponentCategory, ComponentMetricId, InverterType
1010
from ._component_data import (
1111
BatteryData,
1212
ComponentData,
@@ -25,5 +25,6 @@
2525
"EVChargerCableState",
2626
"EVChargerData",
2727
"InverterData",
28+
"InverterType",
2829
"MeterData",
2930
]

src/frequenz/sdk/microgrid/component/_component.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,52 @@
77

88
from dataclasses import dataclass
99
from enum import Enum
10+
from typing import Optional
1011

12+
import frequenz.api.microgrid.inverter_pb2 as inverter_pb
1113
import frequenz.api.microgrid.microgrid_pb2 as microgrid_pb
1214

1315

16+
class ComponentType(Enum):
17+
"""A base class from which individual component types are derived."""
18+
19+
20+
class InverterType(ComponentType):
21+
"""Enum representing inverter types."""
22+
23+
NONE = inverter_pb.Type.TYPE_UNSPECIFIED
24+
BATTERY = inverter_pb.Type.TYPE_BATTERY
25+
SOLAR = inverter_pb.Type.TYPE_SOLAR
26+
HYBRID = inverter_pb.Type.TYPE_HYBRID
27+
28+
29+
def _component_type_from_protobuf(
30+
component_category: microgrid_pb.ComponentCategory.ValueType,
31+
component_type: inverter_pb.Type.ValueType,
32+
) -> Optional[ComponentType]:
33+
"""Convert a protobuf InverterType message to Component enum.
34+
35+
For internal-only use by the `microgrid` package.
36+
37+
Args:
38+
component_category: category the type belongs to.
39+
component_type: protobuf enum to convert.
40+
41+
Returns:
42+
Enum value corresponding to the protobuf message.
43+
"""
44+
# ComponentType values in the protobuf definition are not unique across categories
45+
# as of v0.11.0, so we need to check the component category first, before doing any
46+
# component type checks.
47+
if component_category == microgrid_pb.ComponentCategory.COMPONENT_CATEGORY_INVERTER:
48+
if not any(t.value == component_type for t in InverterType):
49+
return None
50+
51+
return InverterType(component_type)
52+
53+
return None
54+
55+
1456
class ComponentCategory(Enum):
1557
"""Possible types of microgrid component."""
1658

@@ -62,6 +104,7 @@ class Component:
62104

63105
component_id: int
64106
category: ComponentCategory
107+
type: Optional[ComponentType] = None
65108

66109
def is_valid(self) -> bool:
67110
"""Check if this instance contains valid data.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 (
8+
ComponentNotFound,
9+
FormulaGenerationError,
10+
FormulaGenerator,
11+
)
12+
from ._grid_power_formula import GridPowerFormula
13+
from ._pv_power_formula import PVPowerFormula
14+
15+
__all__ = [
16+
#
17+
# Base class
18+
#
19+
"FormulaGenerator",
20+
#
21+
# Formula generators
22+
#
23+
"GridPowerFormula",
24+
"BatteryPowerFormula",
25+
"PVPowerFormula",
26+
#
27+
# Exceptions
28+
#
29+
"ComponentNotFound",
30+
"FormulaGenerationError",
31+
]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 ComponentNotFound, 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+
ComponentNotFound: if there are no batteries in the component graph, or if
31+
they don't have an inverter as a predecessor.
32+
FormulaGenerationError: If a battery has a non-inverter predecessor
33+
in the component graph.
34+
"""
35+
builder = self._get_builder(ComponentMetricId.ACTIVE_POWER)
36+
component_graph = microgrid.get().component_graph
37+
battery_inverters = list(
38+
comp
39+
for comp in component_graph.components()
40+
if comp.category == ComponentCategory.INVERTER
41+
and comp.type == InverterType.BATTERY
42+
)
43+
44+
if not battery_inverters:
45+
raise ComponentNotFound(
46+
"Unable to find any battery inverters in the component graph."
47+
)
48+
49+
for idx, comp in enumerate(battery_inverters):
50+
if idx > 0:
51+
builder.push_oper("+")
52+
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
53+
54+
return builder.build()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 FormulaGenerationError(Exception):
17+
"""An error encountered during formula generation from the component graph."""
18+
19+
20+
class ComponentNotFound(FormulaGenerationError):
21+
"""Indicates that a component required for generating a formula is not found."""
22+
23+
24+
class FormulaGenerator(ABC):
25+
"""A class for generating formulas from the component graph."""
26+
27+
def __init__(
28+
self,
29+
namespace: str,
30+
channel_registry: ChannelRegistry,
31+
resampler_subscription_sender: Sender[ComponentMetricRequest],
32+
) -> None:
33+
"""Create a `FormulaGenerator` instance.
34+
35+
Args:
36+
namespace: A namespace to use with the data-pipeline.
37+
channel_registry: A channel registry instance shared with the resampling
38+
actor.
39+
resampler_subscription_sender: A sender for sending metric requests to the
40+
resampling actor.
41+
"""
42+
self._channel_registry = channel_registry
43+
self._resampler_subscription_sender = resampler_subscription_sender
44+
self._namespace = namespace
45+
46+
def _get_builder(
47+
self, component_metric_id: ComponentMetricId
48+
) -> ResampledFormulaBuilder:
49+
builder = ResampledFormulaBuilder(
50+
self._namespace,
51+
self._channel_registry,
52+
self._resampler_subscription_sender,
53+
component_metric_id,
54+
)
55+
return builder
56+
57+
@abstractmethod
58+
async def generate(self) -> FormulaEngine:
59+
"""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 ComponentNotFound, 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+
ComponentNotFound: 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 ComponentNotFound(
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()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator for PV Power, from the component graph."""
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 ComponentNotFound, FormulaGenerator
10+
11+
12+
class PVPowerFormula(FormulaGenerator):
13+
"""Creates a formula engine for calculating the PV power production."""
14+
15+
async def generate(self) -> FormulaEngine:
16+
"""Make a formula for the PV power production of a microgrid.
17+
18+
Returns:
19+
A formula engine that will calculate PV power production values.
20+
21+
Raises:
22+
ComponentNotFound: if there are no PV inverters in the component graph.
23+
"""
24+
builder = self._get_builder(ComponentMetricId.ACTIVE_POWER)
25+
26+
component_graph = microgrid.get().component_graph
27+
pv_inverters = list(
28+
comp
29+
for comp in component_graph.components()
30+
if comp.category == ComponentCategory.INVERTER
31+
and comp.type == InverterType.SOLAR
32+
)
33+
34+
if not pv_inverters:
35+
raise ComponentNotFound(
36+
"Unable to find any PV inverters in the component graph."
37+
)
38+
39+
for idx, comp in enumerate(pv_inverters):
40+
if idx > 0:
41+
builder.push_oper("+")
42+
43+
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
44+
45+
return builder.build()

0 commit comments

Comments
 (0)