Skip to content

Commit 596f35e

Browse files
committed
Add support for steam boilers to power manager - WIP
- Extended PowerManagingActor - Extended PowerWrapper - Extended DataPipeline - Extended PowerDistributingActor Signed-off-by: Simon Völcker <simon.voelcker@frequenz.com>
1 parent 9982aa1 commit 596f35e

10 files changed

Lines changed: 577 additions & 7 deletions

File tree

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
new_battery_pool,
381381
new_ev_charger_pool,
382382
new_pv_pool,
383+
new_steam_boiler_pool,
383384
producer,
384385
voltage_per_phase,
385386
)
@@ -428,6 +429,7 @@ async def initialize(
428429
"new_battery_pool",
429430
"new_ev_charger_pool",
430431
"new_pv_pool",
432+
"new_steam_boiler_pool",
431433
"producer",
432434
"voltage_per_phase",
433435
]

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@
1818

1919
from frequenz.channels import Broadcast, Sender
2020
from frequenz.client.common.microgrid.components import ComponentId
21-
from frequenz.client.microgrid.component import Battery, EvCharger, SolarInverter
21+
from frequenz.client.microgrid.component import (
22+
Battery,
23+
EvCharger,
24+
SolarInverter,
25+
SteamBoiler,
26+
)
2227

2328
from .._internal._channels import ChannelRegistry
2429
from ..actor._actor import Actor
2530
from ..timeseries import ResamplerConfig
2631
from ..timeseries._voltage_streamer import VoltageStreamer
32+
from ..timeseries.steam_boiler_pool import SteamBoilerPool
33+
from ..timeseries.steam_boiler_pool._steam_boiler_pool_reference_store import (
34+
SteamBoilerPoolReferenceStore,
35+
)
2736
from ._data_sourcing import ComponentMetricRequest, DataSourcingActor
2837
from ._power_managing._base_classes import DefaultPower, PowerManagerAlgorithm
2938
from ._power_wrapper import PowerWrapper
@@ -127,6 +136,13 @@ def __init__(
127136
# https://github.com/frequenz-floss/frequenz-sdk-python/issues/1285
128137
component_class=SolarInverter,
129138
)
139+
self._steam_boiler_power_wrapper = PowerWrapper(
140+
self._channel_registry,
141+
api_power_request_timeout=api_power_request_timeout,
142+
power_manager_algorithm=PowerManagerAlgorithm.MATRYOSHKA,
143+
default_power=DefaultPower.MAX,
144+
component_class=SteamBoiler,
145+
)
130146

131147
self._logical_meter: LogicalMeter | None = None
132148
self._consumer: Consumer | None = None
@@ -141,6 +157,9 @@ def __init__(
141157
self._pv_pool_reference_stores: dict[
142158
frozenset[ComponentId], PVPoolReferenceStore
143159
] = {}
160+
self._steam_boiler_pool_reference_stores: dict[
161+
frozenset[ComponentId], SteamBoilerPoolReferenceStore
162+
] = {}
144163
self._frequency_instance: GridFrequency | None = None
145164
self._voltage_instance: VoltageStreamer | None = None
146165

@@ -447,6 +466,82 @@ def new_battery_pool(
447466
priority=priority,
448467
)
449468

469+
def new_steam_boiler_pool(
470+
self,
471+
*,
472+
priority: int,
473+
component_ids: abc.Set[ComponentId] | None = None,
474+
name: str | None = None,
475+
) -> SteamBoilerPool:
476+
"""Return the corresponding SteamBoilerPool instance for the given ids.
477+
478+
If a SteamBoilerPool instance for the given ids doesn't exist, a new one is
479+
created and returned.
480+
481+
Args:
482+
priority: The priority of the actor making the call.
483+
component_ids: Optional set of IDs of steam boilers to be managed by the
484+
SteamBoilerPool.
485+
name: An optional name used to identify this instance of the pool or a
486+
corresponding actor in the logs.
487+
488+
Returns:
489+
A SteamBoilerPool instance.
490+
"""
491+
from ..timeseries.steam_boiler_pool import SteamBoilerPool
492+
from ..timeseries.steam_boiler_pool._steam_boiler_pool_reference_store import (
493+
SteamBoilerPoolReferenceStore,
494+
)
495+
496+
if not self._steam_boiler_power_wrapper.started:
497+
self._steam_boiler_power_wrapper.start()
498+
499+
# We use frozenset to make a hashable key from the input set.
500+
ref_store_key: frozenset[ComponentId] = frozenset()
501+
if component_ids is not None:
502+
ref_store_key = frozenset(component_ids)
503+
504+
pool_key = f"{ref_store_key}-{priority}"
505+
if pool_key in self._known_pool_keys:
506+
_logger.warning(
507+
"A SteamBoilerPool instance was already created for steam_boiler_ids=%s "
508+
"and priority=%s using `microgrid.steam_boiler_pool(...)`."
509+
"\n Hint: If the multiple instances are created from the same actor, "
510+
"consider reusing the same instance."
511+
"\n Hint: If the instances are created from different actors, "
512+
"consider using different priorities to distinguish them.",
513+
component_ids,
514+
priority,
515+
)
516+
else:
517+
self._known_pool_keys.add(pool_key)
518+
519+
if ref_store_key not in self._steam_boiler_pool_reference_stores:
520+
self._steam_boiler_pool_reference_stores[ref_store_key] = (
521+
SteamBoilerPoolReferenceStore(
522+
channel_registry=self._channel_registry,
523+
resampler_subscription_sender=self._resampling_request_sender(),
524+
status_receiver=self._steam_boiler_power_wrapper.status_channel.new_receiver(
525+
limit=1
526+
),
527+
power_manager_requests_sender=(
528+
self._steam_boiler_power_wrapper.proposal_channel.new_sender()
529+
),
530+
power_manager_bounds_subs_sender=(
531+
self._steam_boiler_power_wrapper.bounds_subscription_channel.new_sender()
532+
),
533+
power_distribution_results_fetcher=(
534+
self._steam_boiler_power_wrapper.distribution_results_fetcher()
535+
),
536+
component_ids=component_ids,
537+
)
538+
)
539+
return SteamBoilerPool(
540+
pool_ref_store=self._steam_boiler_pool_reference_stores[ref_store_key],
541+
name=name,
542+
priority=priority,
543+
)
544+
450545
def _data_sourcing_request_sender(self) -> Sender[ComponentMetricRequest]:
451546
"""Return a Sender for sending requests to the data sourcing actor.
452547
@@ -503,12 +598,15 @@ async def _stop(self) -> None:
503598
await self._battery_power_wrapper.stop()
504599
await self._ev_power_wrapper.stop()
505600
await self._pv_power_wrapper.stop()
601+
await self._steam_boiler_power_wrapper.stop()
506602
for pool in self._battery_pool_reference_stores.values():
507603
await pool.stop()
508604
for evpool in self._ev_charger_pool_reference_stores.values():
509605
await evpool.stop()
510606
for pvpool in self._pv_pool_reference_stores.values():
511607
await pvpool.stop()
608+
for steam_boiler_pool in self._steam_boiler_pool_reference_stores.values():
609+
await steam_boiler_pool.stop()
512610

513611

514612
_DATA_PIPELINE: _DataPipeline | None = None
@@ -682,6 +780,45 @@ def new_pv_pool(
682780
return _get().new_pv_pool(priority=priority, component_ids=component_ids, name=name)
683781

684782

783+
def new_steam_boiler_pool(
784+
*,
785+
priority: int,
786+
component_ids: abc.Set[ComponentId] | None = None,
787+
name: str | None = None,
788+
) -> SteamBoilerPool:
789+
"""Return a new `SteamBoilerPool` instance for the given parameters.
790+
791+
The priority value is used to resolve conflicts when multiple actors are trying to
792+
propose different power values for the same set of steam boilers.
793+
794+
!!! note
795+
When specifying priority, bigger values indicate higher priority.
796+
797+
It is recommended to reuse the same instance of the `SteamBoilerPool` within the same
798+
actor, unless they are managing different sets of steam boilers.
799+
800+
In deployments with multiple actors managing the same set of steam boilers, it is
801+
recommended to use different priorities to distinguish between them. If not,
802+
a random prioritization will be imposed on them to resolve conflicts, which may
803+
lead to unexpected behavior like longer duration to converge on the desired
804+
power.
805+
806+
Args:
807+
priority: The priority of the actor making the call.
808+
component_ids: Optional set of IDs of steam boilers to be managed by the
809+
`SteamBoilerPool`. If not specified, all steam boilers available in the component
810+
graph are used.
811+
name: An optional name used to identify this instance of the pool or a
812+
corresponding actor in the logs.
813+
814+
Returns:
815+
A `SteamBoilerPool` instance.
816+
"""
817+
return _get().new_steam_boiler_pool(
818+
priority=priority, component_ids=component_ids, name=name
819+
)
820+
821+
685822
def grid() -> Grid:
686823
"""Return the grid measuring point."""
687824
return _get().grid()

src/frequenz/sdk/microgrid/_old_component_data.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,28 @@ def to_samples(self) -> ComponentDataSamples:
11011101
)
11021102

11031103

1104+
@dataclass(kw_only=True)
1105+
class SteamBoilerData(ChpData):
1106+
"""A wrapper class for holding steam boiler data."""
1107+
1108+
active_power: float = 0.0
1109+
"""The total active 3-phase AC power, in Watts (W).
1110+
1111+
Represented in the passive sign convention.
1112+
1113+
* Positive means consumption from the grid.
1114+
* Negative means supply into the grid.
1115+
"""
1116+
1117+
power_upper_bound: float = 0.0
1118+
"""Upper bound for steam boiler power draw in watts."""
1119+
1120+
power_lower_bound: float = 0.0
1121+
"""Lower bound for steam boiler power draw in watts."""
1122+
1123+
CATEGORY: ClassVar[ComponentCategory] = ComponentCategory.STEAM_BOILER
1124+
1125+
11041126
@dataclass(kw_only=True)
11051127
class EVChargerData(ComponentData): # pylint: disable=too-many-instance-attributes
11061128
"""A wrapper class for holding ev_charger data."""

src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_steam_boiler_manager/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)