Skip to content

Commit 9fd4865

Browse files
authored
Warn if duplicate *pool instances are being created (#904)
This is because such usage could make the power manager take longer to converge on the desired power. This PR also improves the docs for the `microgrid.*pool()` functions. Closes #902
2 parents df875ee + 804899a commit 9fd4865

File tree

2 files changed

+111
-41
lines changed

2 files changed

+111
-41
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
## New Features
2222

23-
<!-- Here goes the main new features and examples or instructions on how to use them -->
23+
- Warning messages are logged when multiple instances of `*Pool`s are created for the same set of batteries, with the same priority values.
2424

2525
## Bug Fixes
2626

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 110 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from __future__ import annotations
1212

13+
import logging
1314
import sys
1415
import typing
1516
from collections import abc
@@ -44,6 +45,8 @@
4445
from ..timeseries.logical_meter import LogicalMeter
4546
from ..timeseries.producer import Producer
4647

48+
_logger = logging.getLogger(__name__)
49+
4750

4851
_REQUEST_RECV_BUFFER_SIZE = 500
4952
"""The maximum number of requests that can be queued in the request receiver.
@@ -103,11 +106,22 @@ def __init__(
103106
self._consumer: Consumer | None = None
104107
self._producer: Producer | None = None
105108
self._grid: Grid | None = None
106-
self._ev_charger_pools: dict[frozenset[int], EVChargerPoolReferenceStore] = {}
107-
self._battery_pools: dict[frozenset[int], BatteryPoolReferenceStore] = {}
109+
self._ev_charger_pool_reference_stores: dict[
110+
frozenset[int], EVChargerPoolReferenceStore
111+
] = {}
112+
self._battery_pool_reference_stores: dict[
113+
frozenset[int], BatteryPoolReferenceStore
114+
] = {}
108115
self._frequency_instance: GridFrequency | None = None
109116
self._voltage_instance: VoltageStreamer | None = None
110117

118+
self._known_pool_keys: set[str] = set()
119+
"""A set of keys for corresponding to created EVChargerPool instances.
120+
121+
This is used to warn the user if they try to create a new EVChargerPool instance
122+
for the same set of component IDs, and with the same priority.
123+
"""
124+
111125
def frequency(self) -> GridFrequency:
112126
"""Return the grid frequency measuring point."""
113127
if self._frequency_instance is None:
@@ -191,26 +205,45 @@ def ev_charger_pool(
191205
self._ev_power_wrapper.start()
192206

193207
# We use frozenset to make a hashable key from the input set.
194-
key: frozenset[int] = frozenset()
208+
ref_store_key: frozenset[int] = frozenset()
195209
if ev_charger_ids is not None:
196-
key = frozenset(ev_charger_ids)
197-
198-
if key not in self._ev_charger_pools:
199-
self._ev_charger_pools[key] = EVChargerPoolReferenceStore(
200-
channel_registry=self._channel_registry,
201-
resampler_subscription_sender=self._resampling_request_sender(),
202-
status_receiver=self._ev_power_wrapper.status_channel.new_receiver(
203-
limit=1
204-
),
205-
power_manager_requests_sender=(
206-
self._ev_power_wrapper.proposal_channel.new_sender()
207-
),
208-
power_manager_bounds_subs_sender=(
209-
self._ev_power_wrapper.bounds_subscription_channel.new_sender()
210-
),
211-
component_ids=ev_charger_ids,
210+
ref_store_key = frozenset(ev_charger_ids)
211+
212+
pool_key = f"{ref_store_key}-{priority}"
213+
if pool_key in self._known_pool_keys:
214+
_logger.warning(
215+
"An EVChargerPool instance was already created for ev_charger_ids=%s "
216+
"and priority=%s using `microgrid.ev_charger_pool(...)`."
217+
"\n Hint: If the multiple instances are created from the same actor, "
218+
"consider reusing the same instance."
219+
"\n Hint: If the instances are created from different actors, "
220+
"consider using different priorities to distinguish them.",
221+
ev_charger_ids,
222+
priority,
223+
)
224+
else:
225+
self._known_pool_keys.add(pool_key)
226+
227+
if ref_store_key not in self._ev_charger_pool_reference_stores:
228+
self._ev_charger_pool_reference_stores[ref_store_key] = (
229+
EVChargerPoolReferenceStore(
230+
channel_registry=self._channel_registry,
231+
resampler_subscription_sender=self._resampling_request_sender(),
232+
status_receiver=self._ev_power_wrapper.status_channel.new_receiver(
233+
limit=1
234+
),
235+
power_manager_requests_sender=(
236+
self._ev_power_wrapper.proposal_channel.new_sender()
237+
),
238+
power_manager_bounds_subs_sender=(
239+
self._ev_power_wrapper.bounds_subscription_channel.new_sender()
240+
),
241+
component_ids=ev_charger_ids,
242+
)
212243
)
213-
return EVChargerPool(self._ev_charger_pools[key], name, priority)
244+
return EVChargerPool(
245+
self._ev_charger_pool_reference_stores[ref_store_key], name, priority
246+
)
214247

215248
def grid(self) -> Grid:
216249
"""Return the grid measuring point."""
@@ -253,28 +286,47 @@ def battery_pool(
253286
self._battery_power_wrapper.start()
254287

255288
# We use frozenset to make a hashable key from the input set.
256-
key: frozenset[int] = frozenset()
289+
ref_store_key: frozenset[int] = frozenset()
257290
if battery_ids is not None:
258-
key = frozenset(battery_ids)
259-
260-
if key not in self._battery_pools:
261-
self._battery_pools[key] = BatteryPoolReferenceStore(
262-
channel_registry=self._channel_registry,
263-
resampler_subscription_sender=self._resampling_request_sender(),
264-
batteries_status_receiver=self._battery_power_wrapper.status_channel.new_receiver(
265-
limit=1
266-
),
267-
power_manager_requests_sender=(
268-
self._battery_power_wrapper.proposal_channel.new_sender()
269-
),
270-
power_manager_bounds_subscription_sender=(
271-
self._battery_power_wrapper.bounds_subscription_channel.new_sender()
272-
),
273-
min_update_interval=self._resampler_config.resampling_period,
274-
batteries_id=battery_ids,
291+
ref_store_key = frozenset(battery_ids)
292+
293+
pool_key = f"{ref_store_key}-{priority}"
294+
if pool_key in self._known_pool_keys:
295+
_logger.warning(
296+
"A BatteryPool instance was already created for battery_ids=%s and "
297+
"priority=%s using `microgrid.battery_pool(...)`."
298+
"\n Hint: If the multiple instances are created from the same actor, "
299+
"consider reusing the same instance."
300+
"\n Hint: If the instances are created from different actors, "
301+
"consider using different priorities to distinguish them.",
302+
battery_ids,
303+
priority,
304+
)
305+
else:
306+
self._known_pool_keys.add(pool_key)
307+
308+
if ref_store_key not in self._battery_pool_reference_stores:
309+
self._battery_pool_reference_stores[ref_store_key] = (
310+
BatteryPoolReferenceStore(
311+
channel_registry=self._channel_registry,
312+
resampler_subscription_sender=self._resampling_request_sender(),
313+
batteries_status_receiver=(
314+
self._battery_power_wrapper.status_channel.new_receiver(limit=1)
315+
),
316+
power_manager_requests_sender=(
317+
self._battery_power_wrapper.proposal_channel.new_sender()
318+
),
319+
power_manager_bounds_subscription_sender=(
320+
self._battery_power_wrapper.bounds_subscription_channel.new_sender()
321+
),
322+
min_update_interval=self._resampler_config.resampling_period,
323+
batteries_id=battery_ids,
324+
)
275325
)
276326

277-
return BatteryPool(self._battery_pools[key], name, priority)
327+
return BatteryPool(
328+
self._battery_pool_reference_stores[ref_store_key], name, priority
329+
)
278330

279331
def _data_sourcing_request_sender(self) -> Sender[ComponentMetricRequest]:
280332
"""Return a Sender for sending requests to the data sourcing actor.
@@ -331,7 +383,7 @@ async def _stop(self) -> None:
331383
if self._resampling_actor:
332384
await self._resampling_actor.actor.stop()
333385
await self._battery_power_wrapper.stop()
334-
for pool in self._battery_pools.values():
386+
for pool in self._battery_pool_reference_stores.values():
335387
await pool.stop()
336388

337389

@@ -393,6 +445,15 @@ def ev_charger_pool(
393445
When specifying priority, bigger values indicate higher priority. The default
394446
priority is the lowest possible value.
395447
448+
It is recommended to reuse the same instance of the `EVChargerPool` within the
449+
same actor, unless they are managing different sets of EV chargers.
450+
451+
In deployments with multiple actors managing the same set of EV chargers, it is
452+
recommended to use different priorities to distinguish between them. If not,
453+
a random prioritization will be imposed on them to resolve conflicts, which may
454+
lead to unexpected behavior like longer duration to converge on the desired
455+
power.
456+
396457
Args:
397458
ev_charger_ids: Optional set of IDs of EV Chargers to be managed by the
398459
EVChargerPool. If not specified, all EV Chargers available in the
@@ -421,6 +482,15 @@ def battery_pool(
421482
When specifying priority, bigger values indicate higher priority. The default
422483
priority is the lowest possible value.
423484
485+
It is recommended to reuse the same instance of the `BatteryPool` within the
486+
same actor, unless they are managing different sets of batteries.
487+
488+
In deployments with multiple actors managing the same set of batteries, it is
489+
recommended to use different priorities to distinguish between them. If not,
490+
a random prioritization will be imposed on them to resolve conflicts, which may
491+
lead to unexpected behavior like longer duration to converge on the desired
492+
power.
493+
424494
Args:
425495
battery_ids: Optional set of IDs of batteries to be managed by the `BatteryPool`.
426496
If not specified, all batteries available in the component graph are used.

0 commit comments

Comments
 (0)