Skip to content

Commit 0fb0e58

Browse files
authored
Document microgrid concepts from an SDK perspective (#722)
Closes #456
2 parents db4365a + 6add297 commit 0fb0e58

File tree

5 files changed

+163
-58
lines changed

5 files changed

+163
-58
lines changed

docs/_scripts/macros.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""This module defines macros for use in Markdown files."""
55

6+
import os
67
import pathlib
78
from typing import Any
89

@@ -45,19 +46,24 @@ def define_env(env: macros.MacrosPlugin) -> None:
4546
env.variables["code_annotation_marker"] = _CODE_ANNOTATION_MARKER
4647

4748
@env.macro # type: ignore[misc]
48-
def glossary(term: str) -> str:
49+
def glossary(term: str, text: str | None = None) -> str:
4950
"""Create a link to the glossary entry for the given term.
5051
5152
Args:
5253
term: The term to link to.
54+
text: The text to display for the link. Defaults to the term.
5355
5456
Returns:
5557
The Markdown link to the glossary entry for the given term.
5658
"""
5759
current_path = pathlib.Path(env.page.file.src_uri)
5860
glossary_path = pathlib.Path("user-guide/glossary.md")
59-
link_path = glossary_path.relative_to(current_path.parent)
60-
return f"[{term}]({link_path}#{_slugify(term)})"
61+
# This needs to use `os.path.relpath` instead of `pathlib.Path.relative_to`
62+
# because the latter expects one path to be a parent of the other, which is not
63+
# always the case, for example when referencing the glossary from the API
64+
# reference.
65+
link_path = os.path.relpath(glossary_path, current_path.parent)
66+
return f"[{text or term}]({link_path}#{_slugify(term)})"
6167

6268
# The code below is a temporary workaround to make `mkdocs-macros` work with
6369
# `mkdocstrings` until a proper `mkdocs-macros` *pluglet* is available. See

docs/user-guide/glossary.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ article](https://en.wikipedia.org/wiki/Electric_vehicle) for more details.
3434

3535
### PSC
3636

37-
Passive sign convention. See the [Wikipedia
37+
[Passive sign convention](#passive-sign-convention). See the [Wikipedia
3838
article](https://en.wikipedia.org/wiki/Passive_sign_convention) for more
3939
details.
4040

@@ -169,6 +169,9 @@ A convention for the direction of power flow in a circuit. When the electricity
169169
is flowing into a [component](#component) the value is positive, and when it is
170170
flowing out of a component the value is negative.
171171

172+
In microgrids that have a grid connection, power flowing away from the grid is
173+
positive, and power flowing towards the grid is negative.
174+
172175
## Component Data
173176

174177
### Metric
@@ -277,7 +280,7 @@ Same as [power](#power).
277280

278281
### Load
279282

280-
Typically refers to a device that [consume](#consumption) electricity, but also
283+
Typically refers to a device that [consumes](#consumption) electricity, but also
281284
to the amount of electricity [consumed](#consumption) by such a device. In
282285
a [microgrid](#microgrid) context, it is often used to refer to all the
283286
electrical devices that are doing active work, for example a light bulb,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Microgrid Concepts
2+
3+
::: frequenz.sdk.microgrid
4+
options:
5+
members: []
6+
show_bases: false
7+
show_root_heading: false
8+
show_root_toc_entry: false
9+
show_source: false

src/frequenz/sdk/microgrid/__init__.py

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,126 @@
11
# License: MIT
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

4-
"""Microgrid monitoring and control system.
4+
"""A {{glossary("microgrid")}} is a local electrical grid that connects a set of
5+
electrical components together. They are often built around a passive power consumer,
6+
to supplement the electricity consumed from the {{glossary("grid", "public grid")}} with
7+
on-site power generation or storage systems.
58
6-
This package provides a complete suite of data structures and functionality
7-
for monitoring and adjusting the state of a microgrid.
8-
"""
9+
Microgrids can also function in {{glossary("island", "island-mode")}}, without a grid
10+
connection, or without a local power consumer, but they have to have at least one of the
11+
two, to be meaningful.
12+
13+
## Frequenz SDK Microgrid Model
14+
15+
The SDK aims to provide an abstract model of the microgrid that enables high-level
16+
interactions with {{glossary("component", "microgrid components")}}, without having to
17+
worry about (or even be aware of) location-specific details such as:
18+
19+
- where the {{glossary("meter", "meters")}} are placed,
20+
- how many {{glossary("battery", "batteries")}},
21+
- whether there's a grid connection or a passive consumer,
22+
- what models the {{glossary("inverter", "inverters")}} are, etc.
23+
- whether components are having downtimes, because {{glossary("metric", "metrics")}} and
24+
limits get adjusted automatically when components are having downtimes.
25+
26+
Users of the SDK can develop applications around this interface once and deploy
27+
anywhere, and the SDK will take care of translating the requests and instructions to
28+
correspond to the specific microgrid configurations.
29+
30+
``` mermaid
31+
flowchart LR
32+
33+
subgraph Left[Measurements only]
34+
direction LR
35+
grid["Grid Connection"]
36+
consumer["Consumer"]
37+
pv["PV Arrays"]
38+
chp["CHP"]
39+
end
40+
41+
junction(( ))
42+
43+
subgraph Right[Measurements and control]
44+
direction LR
45+
bat["Batteries"]
46+
ev["EV Chargers"]
47+
end
48+
49+
grid --- junction
50+
consumer --- junction
51+
pv --- junction
52+
chp --- junction
53+
54+
junction --- bat
55+
junction --- ev
56+
```
57+
58+
## Grid
59+
60+
This refers to a microgrid's connection to the external Grid. The power flowing through
61+
this connection can be streamed through
62+
[`grid_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.grid_power].
63+
64+
In locations without a grid connection, this method remains accessible, and streams zero
65+
values.
66+
67+
## Consumer
68+
69+
This is the main power consumer at the site of a microgrid, and often the
70+
{{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]
73+
74+
In locations without a consumer, this method streams zero values.
75+
76+
## Producers: PV Arrays, CHP
77+
78+
The total {{glossary("pv", "PV")}} power production in a microgrid can be streamed
79+
through [`pv_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.pv_power] , and
80+
similarly the total CHP production in a site can be streamed through
81+
[`chp_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.chp_power]. And total
82+
producer power is available through
83+
[`producer_power`][frequenz.sdk.timeseries.logical_meter.LogicalMeter.producer_power].
84+
85+
As is the case with the other methods, if PV Arrays or CHPs are not available in a
86+
microgrid, the corresponding methods stream zero values.
87+
88+
## Batteries
89+
90+
The total Battery power is available through
91+
[`battery_pool`][frequenz.sdk.microgrid.battery_pool]'s
92+
[`power`][frequenz.sdk.timeseries.battery_pool.BatteryPool.power]. The battery pool by
93+
default uses all batteries available at a location, but battery pool instances can be
94+
created for subsets of batteries if necessary, by specifying the battery ids.
95+
96+
The `battery_pool` also provides
97+
[`soc`][frequenz.sdk.timeseries.battery_pool.BatteryPool.soc],
98+
[`capacity`][frequenz.sdk.timeseries.battery_pool.BatteryPool.capacity],
99+
[`temperature`][frequenz.sdk.timeseries.battery_pool.BatteryPool.temperature] and
100+
available power bounds through the
101+
[`power_status`][frequenz.sdk.timeseries.battery_pool.BatteryPool.power_status] method.
102+
103+
The `battery_pool` also provides control methods
104+
[`propose_power`][frequenz.sdk.timeseries.battery_pool.BatteryPool.propose_power] (which
105+
accepts values in the {{glossary("psc", "Passive Sign Convention")}} and supports both
106+
charging and discharging), or through
107+
[`propose_charge`][frequenz.sdk.timeseries.battery_pool.BatteryPool.propose_charge], or
108+
[`propose_discharge`][frequenz.sdk.timeseries.battery_pool.BatteryPool.propose_discharge].
109+
110+
## EV Chargers
111+
112+
The [`ev_charger_pool`][frequenz.sdk.microgrid.ev_charger_pool] offers a
113+
[`power`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.power] method that
114+
streams the total power measured for all the {{glossary("ev-charger", "EV Chargers")}}
115+
at a site.
116+
117+
It also offers a
118+
[`component_data`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.component_data]
119+
method for fetching the status of individual EV Chargers, including state changes like
120+
when an EV is connected or disconnected, and a
121+
[`set_bounds`][frequenz.sdk.timeseries.ev_charger_pool.EVChargerPool.set_bounds] method
122+
to limit the charge power of individual EV Chargers.
123+
""" # noqa: D205, D400
9124

10125
from ..actor import ResamplerConfig
11126
from . import _data_pipeline, client, component, connection_manager, fuse, grid

src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -33,63 +33,35 @@ class LogicalMeter:
3333
3434
Example:
3535
```python
36-
from frequenz.channels import Sender, Broadcast
37-
from frequenz.sdk.actor import DataSourcingActor, ComponentMetricsResamplingActor
38-
from frequenz.sdk.timeseries import ResamplerConfig
39-
from frequenz.sdk.microgrid import initialize
4036
from datetime import timedelta
4137
42-
channel_registry = ChannelRegistry(name="data-registry")
43-
44-
# Create a channels for sending/receiving subscription requests
45-
data_source_request_channel = Broadcast[ComponentMetricRequest]("data-source")
46-
data_source_request_sender = data_source_request_channel.new_sender()
47-
data_source_request_receiver = data_source_request_channel.new_receiver()
48-
49-
resampling_request_channel = Broadcast[ComponentMetricRequest]("resample")
50-
resampling_request_sender = resampling_request_channel.new_sender()
51-
resampling_request_receiver = resampling_request_channel.new_receiver()
38+
from frequenz.sdk import microgrid
39+
from frequenz.sdk.timeseries import ResamplerConfig
5240
53-
# Instantiate a data sourcing actor
54-
_data_sourcing_actor = DataSourcingActor(
55-
request_receiver=data_source_request_receiver, registry=channel_registry
41+
await microgrid.initialize(
42+
"127.0.0.1",
43+
50051,
44+
ResamplerConfig(resampling_period=timedelta(seconds=1))
5645
)
5746
58-
# Instantiate a resampling actor
59-
async with ComponentMetricsResamplingActor(
60-
channel_registry=channel_registry,
61-
data_sourcing_request_sender=data_source_request_sender,
62-
resampling_request_receiver=resampling_request_receiver,
63-
config=ResamplerConfig(resampling_period=timedelta(seconds=1)),
64-
):
65-
await initialize(
66-
"127.0.0.1",
67-
50051,
68-
ResamplerConfig(resampling_period=timedelta(seconds=1))
69-
)
47+
logical_meter = microgrid.logical_meter()
7048
71-
# Create a logical meter instance
72-
logical_meter = LogicalMeter(
73-
channel_registry,
74-
resampling_request_sender,
75-
)
49+
# Get a receiver for a builtin formula
50+
grid_power_recv = logical_meter.grid_power.new_receiver()
51+
async for grid_power_sample in grid_power_recv:
52+
print(grid_power_sample)
7653
77-
# Get a receiver for a builtin formula
78-
grid_power_recv = logical_meter.grid_power.new_receiver()
79-
for grid_power_sample in grid_power_recv:
80-
print(grid_power_sample)
81-
82-
# or compose formula receivers to create a new formula
83-
net_power_recv = (
84-
(
85-
logical_meter.grid_power
86-
- logical_meter.pv_power
87-
)
88-
.build("net_power")
89-
.new_receiver()
54+
# or compose formulas to create a new formula
55+
net_power_recv = (
56+
(
57+
logical_meter.grid_power
58+
- logical_meter.pv_power
9059
)
91-
for net_power_sample in net_power_recv:
92-
print(net_power_sample)
60+
.build("net_power")
61+
.new_receiver()
62+
)
63+
async for net_power_sample in net_power_recv:
64+
print(net_power_sample)
9365
```
9466
"""
9567

0 commit comments

Comments
 (0)