Skip to content

Commit 4ece16b

Browse files
committed
Allow configuring the component graph from microgrid.initialize()
The new `component_graph_config` keyword argument takes a `ComponentGraphConfig` and is passed to the `ComponentGraph` constructor. When it is `None`, the library's default configuration is used. `ComponentGraphConfig` and `FormulaOverrides` are re-exported from `frequenz.sdk.microgrid`. The test helper class that had the same name is renamed to `ComponentGraphSpec`, to avoid confusion with the re-exported library class. Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
1 parent 1969671 commit 4ece16b

6 files changed

Lines changed: 135 additions & 22 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
## New Features
1010

11+
* `microgrid.initialize()` has a new keyword argument `component_graph_config`. It takes a `ComponentGraphConfig` and gives full control over how the component graph is built. For example, pass `ComponentGraphConfig(prefer_meters_in_component_formulas=True)` to make the per-category formulas read from the meter first, as in previous releases. `ComponentGraphConfig` and `FormulaOverrides` are re-exported from `frequenz.sdk.microgrid`.
1112
* The PV inverter manager now accounts for the power measured on unreachable PV inverters when distributing power, so the reachable inverters compensate for it (matching the battery manager's behavior).
1213

1314
## Bug Fixes

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@
370370

371371
from datetime import timedelta
372372

373+
from frequenz.microgrid_component_graph import ComponentGraphConfig, FormulaOverrides
374+
373375
from ..timeseries._resampling._config import ResamplerConfig
374376
from . import _data_pipeline, connection_manager
375377
from ._data_pipeline import (
@@ -393,6 +395,7 @@ async def initialize(
393395
api_power_request_timeout: timedelta = timedelta(seconds=5.0),
394396
# pylint: disable-next: line-too-long
395397
battery_power_manager_algorithm: PowerManagerAlgorithm = PowerManagerAlgorithm.SHIFTING_MATRYOSHKA, # noqa: E501
398+
component_graph_config: ComponentGraphConfig | None = None,
396399
) -> None:
397400
"""Initialize the microgrid connection manager and the data pipeline.
398401
@@ -409,8 +412,17 @@ async def initialize(
409412
will be unavailable from the corresponding component pools.
410413
battery_power_manager_algorithm: The power manager algorithm to use for
411414
batteries.
415+
component_graph_config: The configuration for building the component
416+
graph. Use it, for example, to make the per-category formulas
417+
(like the battery or PV formulas) read from the meter first
418+
(`prefer_meters_in_component_formulas`), or to set per-formula
419+
overrides through `FormulaOverrides`. When `None`, the library's
420+
default configuration is used.
412421
"""
413-
await connection_manager.initialize(server_url)
422+
await connection_manager.initialize(
423+
server_url,
424+
component_graph_config=component_graph_config,
425+
)
414426
await _data_pipeline.initialize(
415427
resampler_config,
416428
api_power_request_timeout=api_power_request_timeout,
@@ -419,6 +431,8 @@ async def initialize(
419431

420432

421433
__all__ = [
434+
"ComponentGraphConfig",
435+
"FormulaOverrides",
422436
"PowerManagerAlgorithm",
423437
"initialize",
424438
"consumer",

src/frequenz/sdk/microgrid/connection_manager.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
MicrogridApiClient,
1919
MicrogridInfo,
2020
)
21+
from frequenz.microgrid_component_graph import ComponentGraphConfig
2122

2223
from .component_graph import ComponentGraph
2324

@@ -27,7 +28,12 @@
2728
class ConnectionManager(ABC):
2829
"""Creates and stores core features."""
2930

30-
def __init__(self, server_url: str) -> None:
31+
def __init__(
32+
self,
33+
server_url: str,
34+
*,
35+
component_graph_config: ComponentGraphConfig | None = None,
36+
) -> None:
3137
"""Create object instance.
3238
3339
Args:
@@ -36,9 +42,12 @@ def __init__(self, server_url: str) -> None:
3642
where the port should be an int between `0` and `65535` (defaulting to
3743
`9090`) and ssl should be a boolean (defaulting to false). For example:
3844
`grpc://localhost:1090?ssl=true`.
45+
component_graph_config: The configuration for building the component
46+
graph. When `None`, the library's default configuration is used.
3947
"""
4048
super().__init__()
4149
self._server_url = server_url
50+
self._component_graph_config = component_graph_config
4251

4352
@property
4453
def server_url(self) -> str:
@@ -94,7 +103,12 @@ async def _initialize(self) -> None:
94103
class _InsecureConnectionManager(ConnectionManager):
95104
"""Microgrid Api with insecure channel implementation."""
96105

97-
def __init__(self, server_url: str) -> None:
106+
def __init__(
107+
self,
108+
server_url: str,
109+
*,
110+
component_graph_config: ComponentGraphConfig | None = None,
111+
) -> None:
98112
"""Create and stores core features.
99113
100114
Args:
@@ -103,8 +117,10 @@ def __init__(self, server_url: str) -> None:
103117
where the port should be an int between `0` and `65535` (defaulting to
104118
`9090`) and ssl should be a boolean (defaulting to false). For example:
105119
`grpc://localhost:1090?ssl=true`.
120+
component_graph_config: The configuration for building the component
121+
graph. When `None`, the library's default configuration is used.
106122
"""
107-
super().__init__(server_url)
123+
super().__init__(server_url, component_graph_config=component_graph_config)
108124
self._client = MicrogridApiClient(server_url)
109125
# To create graph from the API client we need await.
110126
# So create empty graph here, and update it in `run` method.
@@ -172,14 +188,23 @@ async def _initialize(self) -> None:
172188
self._client.list_components(),
173189
self._client.list_connections(),
174190
)
175-
self._graph = ComponentGraph(set(components), set(connections))
191+
if self._component_graph_config is None:
192+
self._graph = ComponentGraph(set(components), set(connections))
193+
else:
194+
self._graph = ComponentGraph(
195+
set(components), set(connections), self._component_graph_config
196+
)
176197

177198

178199
_CONNECTION_MANAGER: ConnectionManager | None = None
179200
"""The ConnectionManager singleton instance."""
180201

181202

182-
async def initialize(server_url: str) -> None:
203+
async def initialize(
204+
server_url: str,
205+
*,
206+
component_graph_config: ComponentGraphConfig | None = None,
207+
) -> None:
183208
"""Initialize the MicrogridApi. This function should be called only once.
184209
185210
Args:
@@ -188,6 +213,8 @@ async def initialize(server_url: str) -> None:
188213
where the port should be an int between `0` and `65535` (defaulting to
189214
`9090`) and ssl should be a boolean (defaulting to false). For example:
190215
`grpc://localhost:1090?ssl=true`.
216+
component_graph_config: The configuration for building the component
217+
graph. When `None`, the library's default configuration is used.
191218
"""
192219
# From Doc: pylint just try to discourage this usage.
193220
# That doesn't mean you cannot use it.
@@ -197,7 +224,10 @@ async def initialize(server_url: str) -> None:
197224

198225
_logger.info("Connecting to microgrid at %s", server_url)
199226

200-
connection_manager = _InsecureConnectionManager(server_url)
227+
connection_manager = _InsecureConnectionManager(
228+
server_url,
229+
component_graph_config=component_graph_config,
230+
)
201231
await connection_manager._initialize() # pylint: disable=protected-access
202232

203233
# Check again that _MICROGRID_API is None in case somebody had the great idea of

tests/microgrid/test_microgrid_api.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
Meter,
2929
)
3030

31-
from frequenz.sdk.microgrid import connection_manager
31+
from frequenz.sdk.microgrid import ComponentGraphConfig, connection_manager
3232

3333
_MICROGRID_ID = MicrogridId(1)
3434

@@ -135,6 +135,28 @@ def microgrid(self) -> MicrogridInfo:
135135
),
136136
)
137137

138+
@staticmethod
139+
def _mock_microgrid_client(
140+
components: list[list[Component]],
141+
connections: list[list[ComponentConnection]],
142+
microgrid: MicrogridInfo,
143+
) -> MagicMock:
144+
"""Create a mock microgrid API client.
145+
146+
Args:
147+
components: components to return, one list per call.
148+
connections: connections to return, one list per call.
149+
microgrid: the information about the microgrid.
150+
151+
Returns:
152+
the mock client.
153+
"""
154+
client = MagicMock()
155+
client.list_components = AsyncMock(side_effect=components)
156+
client.list_connections = AsyncMock(side_effect=connections)
157+
client.get_microgrid_info = AsyncMock(return_value=microgrid)
158+
return client
159+
138160
@mock.patch("grpc.aio.insecure_channel")
139161
async def test_connection_manager(
140162
self,
@@ -151,10 +173,9 @@ async def test_connection_manager(
151173
connections: connections
152174
microgrid: the information about the microgrid
153175
"""
154-
microgrid_client = MagicMock()
155-
microgrid_client.list_components = AsyncMock(side_effect=components)
156-
microgrid_client.list_connections = AsyncMock(side_effect=connections)
157-
microgrid_client.get_microgrid_info = AsyncMock(return_value=microgrid)
176+
microgrid_client = self._mock_microgrid_client(
177+
components, connections, microgrid
178+
)
158179

159180
with mock.patch(
160181
"frequenz.sdk.microgrid.connection_manager.MicrogridApiClient",
@@ -214,6 +235,57 @@ async def test_connection_manager(
214235
assert api.microgrid_id == microgrid.id
215236
assert api.location == microgrid.location
216237

238+
@mock.patch("grpc.aio.insecure_channel")
239+
async def test_component_graph_config(
240+
self,
241+
_insecure_channel_mock: MagicMock,
242+
components: list[list[Component]],
243+
connections: list[list[ComponentConnection]],
244+
microgrid: MicrogridInfo,
245+
) -> None:
246+
"""Test that the component graph config is used to build the graph.
247+
248+
Args:
249+
_insecure_channel_mock: insecure channel mock from `mock.patch`
250+
components: components
251+
connections: connections
252+
microgrid: the information about the microgrid
253+
"""
254+
microgrid_client = self._mock_microgrid_client(
255+
[components[0]] * 2, [connections[0]] * 2, microgrid
256+
)
257+
258+
with mock.patch(
259+
"frequenz.sdk.microgrid.connection_manager.MicrogridApiClient",
260+
return_value=microgrid_client,
261+
):
262+
# pylint: disable=protected-access
263+
default_manager = connection_manager._InsecureConnectionManager(
264+
"grpc://127.0.0.1:10001"
265+
)
266+
await default_manager._initialize()
267+
268+
meters_first_manager = connection_manager._InsecureConnectionManager(
269+
"grpc://127.0.0.1:10001",
270+
component_graph_config=ComponentGraphConfig(
271+
prefer_meters_in_component_formulas=True
272+
),
273+
)
274+
await meters_first_manager._initialize()
275+
276+
batteries = {ComponentId(9), ComponentId(12)}
277+
# By default the battery inverters are the primary source and the
278+
# meters are the fallback.
279+
assert (
280+
default_manager.component_graph.battery_formula(batteries)
281+
== "COALESCE(#8, #7, 0.0) + COALESCE(#11, #10, 0.0)"
282+
)
283+
# With `prefer_meters_in_component_formulas` the meters come first.
284+
assert (
285+
meters_first_manager.component_graph.battery_formula(batteries)
286+
== "COALESCE(#7, #8, 0.0) + COALESCE(#10, #11, 0.0)"
287+
)
288+
217289
@mock.patch("grpc.aio.insecure_channel")
218290
async def test_connection_manager_another_method(
219291
self,

tests/timeseries/_battery_pool/test_battery_pool.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from ...utils.component_data_streamer import MockComponentDataStreamer
4949
from ...utils.component_data_wrapper import BatteryDataWrapper, InverterDataWrapper
5050
from ...utils.component_graph_utils import (
51-
ComponentGraphConfig,
51+
ComponentGraphSpec,
5252
create_component_graph_structure,
5353
)
5454
from ...utils.mock_microgrid_client import MockMicrogridClient
@@ -113,7 +113,7 @@ class SetupArgs:
113113

114114

115115
def create_mock_microgrid(
116-
mocker: MockerFixture, config: ComponentGraphConfig
116+
mocker: MockerFixture, config: ComponentGraphSpec
117117
) -> MockMicrogridClient:
118118
"""Create mock microgrid and initialize it.
119119
@@ -143,9 +143,7 @@ async def setup_all_batteries(mocker: MockerFixture) -> AsyncIterator[SetupArgs]
143143
Yields:
144144
Arguments that are needed in test.
145145
"""
146-
mock_microgrid = create_mock_microgrid(
147-
mocker, ComponentGraphConfig(batteries_num=2)
148-
)
146+
mock_microgrid = create_mock_microgrid(mocker, ComponentGraphSpec(batteries_num=2))
149147
min_update_interval: float = 0.2
150148
# pylint: disable=protected-access
151149
microgrid._data_pipeline._DATA_PIPELINE = None
@@ -195,9 +193,7 @@ async def setup_batteries_pool(mocker: MockerFixture) -> AsyncIterator[SetupArgs
195193
Yields:
196194
Arguments that are needed in test.
197195
"""
198-
mock_microgrid = create_mock_microgrid(
199-
mocker, ComponentGraphConfig(batteries_num=4)
200-
)
196+
mock_microgrid = create_mock_microgrid(mocker, ComponentGraphSpec(batteries_num=4))
201197
streamer = MockComponentDataStreamer(mock_microgrid)
202198
min_update_interval: float = 0.2
203199
# pylint: disable=protected-access

tests/utils/component_graph_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
@dataclass
24-
class ComponentGraphConfig:
24+
class ComponentGraphSpec:
2525
"""Config with information how the component graph should be created."""
2626

2727
grid_side_meter: bool = True
@@ -44,7 +44,7 @@ class ComponentGraphConfig:
4444

4545

4646
def create_component_graph_structure(
47-
component_graph_config: ComponentGraphConfig,
47+
component_graph_config: ComponentGraphSpec,
4848
) -> tuple[set[Component], set[ComponentConnection]]:
4949
"""Create structure of components graph.
5050

0 commit comments

Comments
 (0)