Skip to content

Commit 7161472

Browse files
Move consumer and producer formulas out from logical meter (#799)
Move consumer and producer power formulas from logical meter to their own logical components Consumer and Producer, respectively. Related to #782
2 parents b425905 + 0918a8a commit 7161472

File tree

11 files changed

+437
-242
lines changed

11 files changed

+437
-242
lines changed

RELEASE_NOTES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@
4444
grid_current_recv = grid.current.new_receiver()
4545
```
4646

47+
- Consumer and producer power formulas were moved from `microgrid.logical_meter()` to `microgrid.consumer()` and `microgrid.producer()`, respectively.
48+
49+
Previously,
50+
51+
```python
52+
logical_meter = microgrid.logical_meter()
53+
consumer_power_recv = logical_meter.consumer_power.new_receiver()
54+
producer_power_recv = logical_meter.producer_power.new_receiver()
55+
```
56+
57+
Now,
58+
59+
```python
60+
consumer_power_recv = microgrid.consumer().power.new_receiver()
61+
producer_power_recv = microgrid.producer().power.new_receiver()
62+
```
63+
4764
- The `ComponentGraph.components()` parameters `component_id` and `component_category` were renamed to `component_ids` and `component_categories`, respectively.
4865

4966
- The `GridFrequency.component` property was renamed to `GridFrequency.source`

docs/user-guide/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ the component graph are:
167167

168168
- figure out how to calculate high level metrics like
169169
[`grid_power`][frequenz.sdk.timeseries.grid.Grid.power],
170-
[`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power],
170+
[`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power],
171171
etc. for a microgrid, using the available components.
172172
- identify the available {{glossary("battery", "batteries")}} or
173173
{{glossary("EV charger", "EV chargers")}} at a site that can be controlled.

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@
6868
6969
This is the main power consumer at the site of a microgrid, and often the
7070
{{glossary("load")}} the microgrid is built to support. The power drawn by the consumer
71-
is available through
72-
[`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power]
71+
is available through [`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power]
7372
7473
In locations without a consumer, this method streams zero values.
7574
@@ -80,7 +79,7 @@
8079
similarly the total CHP production in a site can be streamed through
8180
[`chp_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.chp_power]. And total
8281
producer power is available through
83-
[`producer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.producer_power].
82+
[`producer_power`][frequenz.sdk.timeseries.producer.Producer.power].
8483
8584
As is the case with the other methods, if PV Arrays or CHPs are not available in a
8685
microgrid, the corresponding methods stream zero values.
@@ -126,10 +125,12 @@
126125
from . import _data_pipeline, client, component, connection_manager, metadata
127126
from ._data_pipeline import (
128127
battery_pool,
128+
consumer,
129129
ev_charger_pool,
130130
frequency,
131131
grid,
132132
logical_meter,
133+
producer,
133134
voltage,
134135
)
135136

@@ -150,11 +151,13 @@ async def initialize(host: str, port: int, resampler_config: ResamplerConfig) ->
150151
"initialize",
151152
"client",
152153
"component",
154+
"consumer",
153155
"battery_pool",
154156
"ev_charger_pool",
155157
"grid",
156158
"frequency",
157159
"logical_meter",
158160
"metadata",
161+
"producer",
159162
"voltage",
160163
]

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
from ..timeseries.battery_pool._battery_pool_reference_store import (
4747
BatteryPoolReferenceStore,
4848
)
49+
from ..timeseries.consumer import Consumer
4950
from ..timeseries.ev_charger_pool import EVChargerPool
5051
from ..timeseries.logical_meter import LogicalMeter
52+
from ..timeseries.producer import Producer
5153

5254

5355
_REQUEST_RECV_BUFFER_SIZE = 500
@@ -118,18 +120,16 @@ def __init__(
118120
self._power_managing_actor: _power_managing.PowerManagingActor | None = None
119121

120122
self._logical_meter: LogicalMeter | None = None
123+
self._consumer: Consumer | None = None
124+
self._producer: Producer | None = None
121125
self._grid: Grid | None = None
122126
self._ev_charger_pools: dict[frozenset[int], EVChargerPool] = {}
123127
self._battery_pools: dict[frozenset[int], BatteryPoolReferenceStore] = {}
124128
self._frequency_instance: GridFrequency | None = None
125129
self._voltage_instance: VoltageStreamer | None = None
126130

127131
def frequency(self) -> GridFrequency:
128-
"""Fetch the grid frequency for the microgrid.
129-
130-
Returns:
131-
The GridFrequency instance.
132-
"""
132+
"""Return the grid frequency measuring point."""
133133
if self._frequency_instance is None:
134134
self._frequency_instance = GridFrequency(
135135
self._data_sourcing_request_sender(),
@@ -139,11 +139,7 @@ def frequency(self) -> GridFrequency:
139139
return self._frequency_instance
140140

141141
def voltage(self) -> VoltageStreamer:
142-
"""Fetch the 3-phase voltage for the microgrid.
143-
144-
Returns:
145-
The VoltageStreamer instance.
146-
"""
142+
"""Return the 3-phase voltage measuring point."""
147143
if not self._voltage_instance:
148144
self._voltage_instance = VoltageStreamer(
149145
self._resampling_request_sender(),
@@ -153,13 +149,7 @@ def voltage(self) -> VoltageStreamer:
153149
return self._voltage_instance
154150

155151
def logical_meter(self) -> LogicalMeter:
156-
"""Return the logical meter instance.
157-
158-
If a LogicalMeter instance doesn't exist, a new one is created and returned.
159-
160-
Returns:
161-
A logical meter instance.
162-
"""
152+
"""Return the logical meter of the microgrid."""
163153
from ..timeseries.logical_meter import LogicalMeter
164154

165155
if self._logical_meter is None:
@@ -169,6 +159,28 @@ def logical_meter(self) -> LogicalMeter:
169159
)
170160
return self._logical_meter
171161

162+
def consumer(self) -> Consumer:
163+
"""Return the consumption measuring point of the microgrid."""
164+
from ..timeseries.consumer import Consumer
165+
166+
if self._consumer is None:
167+
self._consumer = Consumer(
168+
channel_registry=self._channel_registry,
169+
resampler_subscription_sender=self._resampling_request_sender(),
170+
)
171+
return self._consumer
172+
173+
def producer(self) -> Producer:
174+
"""Return the production measuring point of the microgrid."""
175+
from ..timeseries.producer import Producer
176+
177+
if self._producer is None:
178+
self._producer = Producer(
179+
channel_registry=self._channel_registry,
180+
resampler_subscription_sender=self._resampling_request_sender(),
181+
)
182+
return self._producer
183+
172184
def ev_charger_pool(
173185
self,
174186
ev_charger_ids: set[int] | None = None,
@@ -201,13 +213,7 @@ def ev_charger_pool(
201213
return self._ev_charger_pools[key]
202214

203215
def grid(self) -> Grid:
204-
"""Return the grid instance.
205-
206-
If a Grid instance doesn't exist, a new one is created and returned.
207-
208-
Returns:
209-
A Grid instance.
210-
"""
216+
"""Return the grid measuring point."""
211217
if self._grid is None:
212218
initialize_grid(
213219
channel_registry=self._channel_registry,
@@ -419,32 +425,28 @@ async def initialize(resampler_config: ResamplerConfig) -> None:
419425

420426

421427
def frequency() -> GridFrequency:
422-
"""Return the grid frequency.
423-
424-
Returns:
425-
The grid frequency.
426-
"""
428+
"""Return the grid frequency measuring point."""
427429
return _get().frequency()
428430

429431

430432
def voltage() -> VoltageStreamer:
431-
"""Return the 3-phase voltage for the microgrid.
432-
433-
Returns:
434-
The 3-phase voltage.
435-
"""
433+
"""Return the 3-phase voltage measuring point."""
436434
return _get().voltage()
437435

438436

439437
def logical_meter() -> LogicalMeter:
440-
"""Return the logical meter instance.
438+
"""Return the logical meter of the microgrid."""
439+
return _get().logical_meter()
441440

442-
If a LogicalMeter instance doesn't exist, a new one is created and returned.
443441

444-
Returns:
445-
A logical meter instance.
446-
"""
447-
return _get().logical_meter()
442+
def consumer() -> Consumer:
443+
"""Return the [`Consumption`][frequenz.sdk.timeseries.consumer.Consumer] measuring point."""
444+
return _get().consumer()
445+
446+
447+
def producer() -> Producer:
448+
"""Return the [`Production`][frequenz.sdk.timeseries.producer.Producer] measuring point."""
449+
return _get().producer()
448450

449451

450452
def ev_charger_pool(ev_charger_ids: set[int] | None = None) -> EVChargerPool:
@@ -492,11 +494,7 @@ def battery_pool(
492494

493495

494496
def grid() -> Grid:
495-
"""Return the grid instance.
496-
497-
Returns:
498-
The Grid instance.
499-
"""
497+
"""Return the grid measuring point."""
500498
return _get().grid()
501499

502500

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""The logical component for calculating high level consumer metrics for a microgrid."""
5+
6+
import uuid
7+
8+
from frequenz.channels import Sender
9+
10+
from ..actor import ChannelRegistry, ComponentMetricRequest
11+
from ._quantities import Power
12+
from .formula_engine import FormulaEngine
13+
from .formula_engine._formula_engine_pool import FormulaEnginePool
14+
from .formula_engine._formula_generators import ConsumerPowerFormula
15+
16+
17+
class Consumer:
18+
"""Calculate high level consumer metrics in a microgrid.
19+
20+
Under normal circumstances this is expected to correspond to the gross
21+
consumption of the site excluding active parts and battery.
22+
23+
Consumer provides methods for fetching power values from different points
24+
in the microgrid. These methods return `FormulaReceiver` objects, which can
25+
be used like normal `Receiver`s, but can also be composed to form
26+
higher-order formula streams.
27+
28+
!!! note
29+
`Consumer` instances are not meant to be created directly by users.
30+
Use the [`microgrid.consumer`][frequenz.sdk.microgrid.consumer] method
31+
for creating `Consumer` instances.
32+
33+
Example:
34+
```python
35+
from datetime import timedelta
36+
37+
from frequenz.sdk import microgrid
38+
from frequenz.sdk.timeseries import ResamplerConfig
39+
40+
await microgrid.initialize(
41+
"127.0.0.1",
42+
50051,
43+
ResamplerConfig(resampling_period=timedelta(seconds=1.0))
44+
)
45+
46+
consumer = microgrid.consumer()
47+
48+
# Get a receiver for a builtin formula
49+
consumer_power_recv = consumer.power.new_receiver()
50+
async for consumer_power_sample in consumer_power_recv:
51+
print(consumer_power_sample)
52+
```
53+
"""
54+
55+
_formula_pool: FormulaEnginePool
56+
"""The formula engine pool to generate consumer metrics."""
57+
58+
def __init__(
59+
self,
60+
channel_registry: ChannelRegistry,
61+
resampler_subscription_sender: Sender[ComponentMetricRequest],
62+
) -> None:
63+
"""Initialize the consumer formula generator.
64+
65+
Args:
66+
channel_registry: The channel registry to use for the consumer.
67+
resampler_subscription_sender: The sender to use for resampler subscriptions.
68+
"""
69+
namespace = f"consumer-{uuid.uuid4()}"
70+
self._formula_pool = FormulaEnginePool(
71+
namespace,
72+
channel_registry,
73+
resampler_subscription_sender,
74+
)
75+
76+
@property
77+
def power(self) -> FormulaEngine[Power]:
78+
"""Fetch the consumer power for the microgrid.
79+
80+
This formula produces values that are in the Passive Sign Convention (PSC).
81+
82+
It will start the formula engine to calculate consumer power if it is
83+
not already running.
84+
85+
A receiver from the formula engine can be created using the
86+
`new_receiver` method.
87+
88+
Returns:
89+
A FormulaEngine that will calculate and stream consumer power.
90+
"""
91+
engine = self._formula_pool.from_power_formula_generator(
92+
"consumer_power",
93+
ConsumerPowerFormula,
94+
)
95+
assert isinstance(engine, FormulaEngine)
96+
return engine
97+
98+
async def stop(self) -> None:
99+
"""Stop all formula engines."""
100+
await self._formula_pool.stop()

src/frequenz/sdk/timeseries/formula_engine/_formula_engine.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class FormulaEngine(
232232
233233
They are used in the SDK to calculate and stream metrics like
234234
[`grid_power`][frequenz.sdk.timeseries.grid.Grid.power],
235-
[`consumer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.consumer_power],
235+
[`consumer_power`][frequenz.sdk.timeseries.consumer.Consumer.power],
236236
etc., which are building blocks of the
237237
[Frequenz SDK Microgrid Model][frequenz.sdk.microgrid--frequenz-sdk-microgrid-model].
238238
@@ -332,10 +332,8 @@ def from_receiver(
332332
from frequenz.sdk.timeseries import Power
333333
334334
async def run() -> None:
335-
producer_power_engine = microgrid.logical_meter().producer_power
336-
consumer_power_recv = (
337-
microgrid.logical_meter().consumer_power.new_receiver()
338-
)
335+
producer_power_engine = microgrid.producer().power
336+
consumer_power_recv = microgrid.consumer().power.new_receiver()
339337
340338
excess_power_recv = (
341339
(

0 commit comments

Comments
 (0)