Skip to content

Commit f8d0334

Browse files
committed
Add formula generators for 3-phase grid and ev charger current
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent b4ffa2a commit f8d0334

File tree

4 files changed

+199
-4
lines changed

4 files changed

+199
-4
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
from ._battery_power_formula import BatteryPowerFormula
77
from ._battery_soc_formula import BatterySoCFormula
8+
from ._ev_charger_current_formula import EVChargerCurrentFormula
89
from ._formula_generator import (
910
ComponentNotFound,
1011
FormulaGenerationError,
1112
FormulaGenerator,
1213
)
14+
from ._grid_current_formula import GridCurrentFormula
1315
from ._grid_power_formula import GridPowerFormula
1416
from ._pv_power_formula import PVPowerFormula
1517

@@ -19,13 +21,18 @@
1921
#
2022
"FormulaGenerator",
2123
#
22-
# Formula generators
24+
# Power Formula generators
2325
#
2426
"GridPowerFormula",
2527
"BatteryPowerFormula",
2628
"BatterySoCFormula",
2729
"PVPowerFormula",
2830
#
31+
# Current formula generators
32+
#
33+
"GridCurrentFormula",
34+
"EVChargerCurrentFormula",
35+
#
2936
# Exceptions
3037
#
3138
"ComponentNotFound",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for 3-phase Grid Current."""
5+
6+
import logging
7+
from typing import List
8+
9+
from .....sdk import microgrid
10+
from ....microgrid.component import Component, ComponentCategory, ComponentMetricId
11+
from .._formula_engine import FormulaEngine, FormulaEngine3Phase
12+
from ._formula_generator import NON_EXISTING_COMPONENT_ID, FormulaGenerator
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class EVChargerCurrentFormula(FormulaGenerator):
18+
"""Create a formula engine from the component graph for calculating grid current."""
19+
20+
async def generate(self) -> FormulaEngine3Phase:
21+
"""Generate a formula for calculating total ev current from the component graph.
22+
23+
Returns:
24+
A formula engine that calculates total 3-phase ev charger current values.
25+
26+
Raises:
27+
ComponentNotFound: when the component graph doesn't have a `GRID` component.
28+
"""
29+
component_graph = microgrid.get().component_graph
30+
ev_chargers = [
31+
comp
32+
for comp in component_graph.components()
33+
if comp.category == ComponentCategory.EV_CHARGER
34+
]
35+
36+
if not ev_chargers:
37+
logger.warning(
38+
"Unable to find any EV Chargers in the component graph. "
39+
"Subscribing to the resampling actor with a non-existing "
40+
"component id, so that `0` values are sent from the formula."
41+
)
42+
# If there are no EV Chargers, we have to send 0 values as the same
43+
# frequency as the other streams. So we subscribe with a non-existing
44+
# component id, just to get a `None` message at the resampling interval.
45+
builder = self._get_builder("ev-current", ComponentMetricId.ACTIVE_POWER)
46+
await builder.push_component_metric(
47+
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
48+
)
49+
engine = builder.build()
50+
return FormulaEngine3Phase(
51+
"ev-current",
52+
(engine.new_receiver(), engine.new_receiver(), engine.new_receiver()),
53+
)
54+
55+
return FormulaEngine3Phase(
56+
"ev-current",
57+
(
58+
(
59+
await self._gen_phase_formula(
60+
ev_chargers, ComponentMetricId.CURRENT_PHASE_1
61+
)
62+
).new_receiver(),
63+
(
64+
await self._gen_phase_formula(
65+
ev_chargers, ComponentMetricId.CURRENT_PHASE_2
66+
)
67+
).new_receiver(),
68+
(
69+
await self._gen_phase_formula(
70+
ev_chargers, ComponentMetricId.CURRENT_PHASE_3
71+
)
72+
).new_receiver(),
73+
),
74+
)
75+
76+
async def _gen_phase_formula(
77+
self,
78+
ev_chargers: List[Component],
79+
metric_id: ComponentMetricId,
80+
) -> FormulaEngine:
81+
builder = self._get_builder("ev-current", metric_id)
82+
83+
# generate a formula that just adds values from all ev-chargers.
84+
for idx, comp in enumerate(ev_chargers):
85+
if idx > 0:
86+
builder.push_oper("+")
87+
88+
await builder.push_component_metric(comp.component_id, nones_are_zeros=True)
89+
90+
return builder.build()

src/frequenz/sdk/timeseries/logical_meter/_formula_generators/_formula_generator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66
import sys
77
from abc import ABC, abstractmethod
8+
from typing import Generic
89

910
from frequenz.channels import Sender
1011

1112
from ....actor import ChannelRegistry, ComponentMetricRequest
1213
from ....microgrid.component import ComponentMetricId
13-
from .._formula_engine import FormulaEngine
14+
from .._formula_engine import _GenericEngine
1415
from .._resampled_formula_builder import ResampledFormulaBuilder
1516

1617

@@ -25,7 +26,7 @@ class ComponentNotFound(FormulaGenerationError):
2526
NON_EXISTING_COMPONENT_ID = sys.maxsize
2627

2728

28-
class FormulaGenerator(ABC):
29+
class FormulaGenerator(ABC, Generic[_GenericEngine]):
2930
"""A class for generating formulas from the component graph."""
3031

3132
def __init__(
@@ -60,5 +61,5 @@ def _get_builder(
6061
return builder
6162

6263
@abstractmethod
63-
async def generate(self) -> FormulaEngine:
64+
async def generate(self) -> _GenericEngine:
6465
"""Generate a formula engine, based on the component graph."""
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Formula generator from component graph for 3-phase Grid Current."""
5+
6+
from typing import Set
7+
8+
from .....sdk import microgrid
9+
from ....microgrid.component import Component, ComponentCategory, ComponentMetricId
10+
from .._formula_engine import FormulaEngine, FormulaEngine3Phase
11+
from ._formula_generator import ComponentNotFound, FormulaGenerator
12+
13+
14+
class GridCurrentFormula(FormulaGenerator):
15+
"""Create a formula engine from the component graph for calculating grid current."""
16+
17+
async def generate(self) -> FormulaEngine3Phase:
18+
"""Generate a formula for calculating grid current from the component graph.
19+
20+
Returns:
21+
A formula engine that will calculate 3-phase grid current values.
22+
23+
Raises:
24+
ComponentNotFound: when the component graph doesn't have a `GRID` component.
25+
"""
26+
component_graph = microgrid.get().component_graph
27+
grid_component = next(
28+
(
29+
comp
30+
for comp in component_graph.components()
31+
if comp.category == ComponentCategory.GRID
32+
),
33+
None,
34+
)
35+
36+
if grid_component is None:
37+
raise ComponentNotFound(
38+
"Unable to find a GRID component from the component graph."
39+
)
40+
41+
grid_successors = component_graph.successors(grid_component.component_id)
42+
43+
return FormulaEngine3Phase(
44+
"grid-current",
45+
(
46+
(
47+
await self._gen_phase_formula(
48+
grid_successors, ComponentMetricId.CURRENT_PHASE_1
49+
)
50+
).new_receiver(),
51+
(
52+
await self._gen_phase_formula(
53+
grid_successors, ComponentMetricId.CURRENT_PHASE_2
54+
)
55+
).new_receiver(),
56+
(
57+
await self._gen_phase_formula(
58+
grid_successors, ComponentMetricId.CURRENT_PHASE_3
59+
)
60+
).new_receiver(),
61+
),
62+
)
63+
64+
async def _gen_phase_formula(
65+
self,
66+
grid_successors: Set[Component],
67+
metric_id: ComponentMetricId,
68+
) -> FormulaEngine:
69+
builder = self._get_builder("grid-current", metric_id)
70+
71+
# generate a formula that just adds values from all components that are
72+
# directly connected to the grid.
73+
for idx, comp in enumerate(grid_successors):
74+
if idx > 0:
75+
builder.push_oper("+")
76+
77+
# When inverters or ev chargers produce `None` samples, those
78+
# inverters are excluded from the calculation by treating their
79+
# `None` values as `0`s.
80+
#
81+
# This is not possible for Meters, so when they produce `None`
82+
# values, those values get propagated as the output.
83+
if comp.category in (
84+
ComponentCategory.INVERTER,
85+
ComponentCategory.EV_CHARGER,
86+
):
87+
nones_are_zeros = True
88+
elif comp.category == ComponentCategory.METER:
89+
nones_are_zeros = False
90+
else:
91+
continue
92+
93+
await builder.push_component_metric(
94+
comp.component_id, nones_are_zeros=nones_are_zeros
95+
)
96+
97+
return builder.build()

0 commit comments

Comments
 (0)