frequenz-gridpool 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {frequenz_gridpool-0.3.0/src/frequenz_gridpool.egg-info → frequenz_gridpool-0.3.1}/PKG-INFO +54 -6
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/README.md +44 -0
- frequenz_gridpool-0.3.1/RELEASE_NOTES.md +19 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/pyproject.toml +12 -6
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/cli/__main__.py +42 -0
- frequenz_gridpool-0.3.1/src/frequenz/gridpool/cli/_render_graph.py +295 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1/src/frequenz_gridpool.egg-info}/PKG-INFO +54 -6
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/SOURCES.txt +1 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/requires.txt +10 -5
- frequenz_gridpool-0.3.0/RELEASE_NOTES.md +0 -17
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/LICENSE +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/MANIFEST.in +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/setup.cfg +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/__init__.py +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/_graph_generator.py +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/_microgrid_config.py +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/cli/__init__.py +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/conftest.py +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/py.typed +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/dependency_links.txt +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/entry_points.txt +0 -0
- {frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-gridpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: High-level interface to grid pools for the Frequenz platform.
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License: MIT
|
|
@@ -25,6 +25,9 @@ Requires-Dist: asyncclick<9,>=8.3.0.4
|
|
|
25
25
|
Requires-Dist: typing-extensions<5,>=4.14.1
|
|
26
26
|
Requires-Dist: frequenz-microgrid-component-graph<0.4,>=0.3.4
|
|
27
27
|
Requires-Dist: frequenz-client-assets<0.3,>=0.2.0
|
|
28
|
+
Provides-Extra: render-graph
|
|
29
|
+
Requires-Dist: matplotlib<4,>=3.7.0; extra == "render-graph"
|
|
30
|
+
Requires-Dist: networkx<4,>=3.0; extra == "render-graph"
|
|
28
31
|
Provides-Extra: dev-flake8
|
|
29
32
|
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
30
33
|
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
@@ -41,12 +44,13 @@ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
|
|
|
41
44
|
Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
|
|
42
45
|
Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
|
|
43
46
|
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
44
|
-
Requires-Dist: mkdocs-material==9.7.
|
|
45
|
-
Requires-Dist: mkdocstrings[python]==1.0.
|
|
47
|
+
Requires-Dist: mkdocs-material==9.7.1; extra == "dev-mkdocs"
|
|
48
|
+
Requires-Dist: mkdocstrings[python]==1.0.1; extra == "dev-mkdocs"
|
|
46
49
|
Requires-Dist: mkdocstrings-python==1.19.0; extra == "dev-mkdocs"
|
|
47
50
|
Requires-Dist: frequenz-repo-config[lib]==0.13.7; extra == "dev-mkdocs"
|
|
48
51
|
Provides-Extra: dev-mypy
|
|
49
|
-
Requires-Dist: mypy==1.19.
|
|
52
|
+
Requires-Dist: mypy==1.19.1; extra == "dev-mypy"
|
|
53
|
+
Requires-Dist: types-networkx>=3.6.1.20251220; extra == "dev-mypy"
|
|
50
54
|
Requires-Dist: types-Markdown==3.10.0.20251106; extra == "dev-mypy"
|
|
51
55
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
52
56
|
Provides-Extra: dev-noxfile
|
|
@@ -55,14 +59,14 @@ Requires-Dist: frequenz-repo-config[lib]==0.13.7; extra == "dev-noxfile"
|
|
|
55
59
|
Provides-Extra: dev-pylint
|
|
56
60
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
57
61
|
Provides-Extra: dev-pytest
|
|
58
|
-
Requires-Dist: pytest==
|
|
62
|
+
Requires-Dist: pytest==9.0.2; extra == "dev-pytest"
|
|
59
63
|
Requires-Dist: pylint==4.0.4; extra == "dev-pytest"
|
|
60
64
|
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.7; extra == "dev-pytest"
|
|
61
65
|
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
62
66
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev-pytest"
|
|
63
67
|
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
64
68
|
Provides-Extra: dev
|
|
65
|
-
Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
69
|
+
Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]; extra == "dev"
|
|
66
70
|
Dynamic: license-file
|
|
67
71
|
|
|
68
72
|
# Frequenz Gridpool Library
|
|
@@ -85,6 +89,50 @@ The following platforms are officially supported (tested):
|
|
|
85
89
|
- **Operating System:** Ubuntu Linux 20.04
|
|
86
90
|
- **Architectures:** amd64, arm64
|
|
87
91
|
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
This package ships the `gridpool-cli` command with two subcommands.
|
|
95
|
+
|
|
96
|
+
### Setup
|
|
97
|
+
|
|
98
|
+
Set the Assets API credentials before running the CLI:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export ASSETS_API_URL="grpc://..."
|
|
102
|
+
export ASSETS_API_AUTH_KEY="..."
|
|
103
|
+
export ASSETS_API_SIGN_SECRET="..."
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Print component formulas
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gridpool-cli print-formulas <microgrid_id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Optional prefix formatting:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
gridpool-cli print-formulas <microgrid_id> --prefix "{microgrid_id}.{component}"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Render component graph
|
|
119
|
+
|
|
120
|
+
Rendering requires optional dependencies. Install with:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install frequenz-gridpool[render-graph]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gridpool-cli render-graph <microgrid_id>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To save without opening a window:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gridpool-cli render-graph <microgrid_id> --no-show --output component_graph.png
|
|
134
|
+
```
|
|
135
|
+
|
|
88
136
|
## Contributing
|
|
89
137
|
|
|
90
138
|
If you want to know how to build this project and contribute to it, please
|
|
@@ -18,6 +18,50 @@ The following platforms are officially supported (tested):
|
|
|
18
18
|
- **Operating System:** Ubuntu Linux 20.04
|
|
19
19
|
- **Architectures:** amd64, arm64
|
|
20
20
|
|
|
21
|
+
## CLI
|
|
22
|
+
|
|
23
|
+
This package ships the `gridpool-cli` command with two subcommands.
|
|
24
|
+
|
|
25
|
+
### Setup
|
|
26
|
+
|
|
27
|
+
Set the Assets API credentials before running the CLI:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export ASSETS_API_URL="grpc://..."
|
|
31
|
+
export ASSETS_API_AUTH_KEY="..."
|
|
32
|
+
export ASSETS_API_SIGN_SECRET="..."
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Print component formulas
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gridpool-cli print-formulas <microgrid_id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Optional prefix formatting:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
gridpool-cli print-formulas <microgrid_id> --prefix "{microgrid_id}.{component}"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Render component graph
|
|
48
|
+
|
|
49
|
+
Rendering requires optional dependencies. Install with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install frequenz-gridpool[render-graph]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gridpool-cli render-graph <microgrid_id>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To save without opening a window:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gridpool-cli render-graph <microgrid_id> --no-show --output component_graph.png
|
|
63
|
+
```
|
|
64
|
+
|
|
21
65
|
## Contributing
|
|
22
66
|
|
|
23
67
|
If you want to know how to build this project and contribute to it, please
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Frequenz Gridpool Library Release Notes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
<!-- Here goes a general summary of what this release is about -->
|
|
6
|
+
|
|
7
|
+
## Upgrading
|
|
8
|
+
|
|
9
|
+
* The minimum required version of `frequenz-microgrid-component-graph` is now `v0.3.4`.
|
|
10
|
+
|
|
11
|
+
## New Features
|
|
12
|
+
|
|
13
|
+
* Added `gridpool-cli render-graph` to visualize microgrid component graphs using the
|
|
14
|
+
Assets API credentials (`ASSETS_API_URL`, `ASSETS_API_AUTH_KEY`, and
|
|
15
|
+
`ASSETS_API_SIGN_SECRET`).
|
|
16
|
+
|
|
17
|
+
## Bug Fixes
|
|
18
|
+
|
|
19
|
+
- Fixed component graph rendering so children follow the vertical order of their parents, keeping upper-level branches above lower ones in the layered layout.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[build-system]
|
|
5
5
|
requires = [
|
|
6
|
-
"setuptools == 80.
|
|
6
|
+
"setuptools == 80.10.1",
|
|
7
7
|
"setuptools_scm[toml] == 9.2.2",
|
|
8
8
|
"frequenz-repo-config[lib] == 0.13.7",
|
|
9
9
|
]
|
|
@@ -42,6 +42,11 @@ name = "Frequenz Energy-as-a-Service GmbH"
|
|
|
42
42
|
email = "floss@frequenz.com"
|
|
43
43
|
|
|
44
44
|
[project.optional-dependencies]
|
|
45
|
+
render-graph = [
|
|
46
|
+
"matplotlib >= 3.7.0, < 4",
|
|
47
|
+
"networkx >= 3.0, < 4",
|
|
48
|
+
]
|
|
49
|
+
|
|
45
50
|
dev-flake8 = [
|
|
46
51
|
"flake8 == 7.3.0",
|
|
47
52
|
"flake8-docstrings == 1.7.0",
|
|
@@ -57,13 +62,14 @@ dev-mkdocs = [
|
|
|
57
62
|
"mkdocs-gen-files == 0.5.0",
|
|
58
63
|
"mkdocs-literate-nav == 0.6.2",
|
|
59
64
|
"mkdocs-macros-plugin == 1.5.0",
|
|
60
|
-
"mkdocs-material == 9.7.
|
|
61
|
-
"mkdocstrings[python] == 1.0.
|
|
65
|
+
"mkdocs-material == 9.7.1",
|
|
66
|
+
"mkdocstrings[python] == 1.0.1",
|
|
62
67
|
"mkdocstrings-python == 1.19.0",
|
|
63
68
|
"frequenz-repo-config[lib] == 0.13.7",
|
|
64
69
|
]
|
|
65
70
|
dev-mypy = [
|
|
66
|
-
"mypy == 1.19.
|
|
71
|
+
"mypy == 1.19.1",
|
|
72
|
+
"types-networkx>=3.6.1.20251220",
|
|
67
73
|
"types-Markdown == 3.10.0.20251106",
|
|
68
74
|
# For checking the noxfile, docs/ script, and tests
|
|
69
75
|
"frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
@@ -78,7 +84,7 @@ dev-pylint = [
|
|
|
78
84
|
"frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
79
85
|
]
|
|
80
86
|
dev-pytest = [
|
|
81
|
-
"pytest ==
|
|
87
|
+
"pytest == 9.0.2",
|
|
82
88
|
"pylint == 4.0.4", # We need this to check for the examples
|
|
83
89
|
"frequenz-repo-config[extra-lint-examples] == 0.13.7",
|
|
84
90
|
"pytest-mock == 3.15.1",
|
|
@@ -86,7 +92,7 @@ dev-pytest = [
|
|
|
86
92
|
"async-solipsism == 0.9",
|
|
87
93
|
]
|
|
88
94
|
dev = [
|
|
89
|
-
"frequenz-gridpool[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
|
|
95
|
+
"frequenz-gridpool[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]",
|
|
90
96
|
]
|
|
91
97
|
|
|
92
98
|
[project.urls]
|
|
@@ -10,6 +10,7 @@ from frequenz.client.assets import AssetsApiClient
|
|
|
10
10
|
from frequenz.client.common.microgrid import MicrogridId
|
|
11
11
|
|
|
12
12
|
from frequenz.gridpool import ComponentGraphGenerator
|
|
13
|
+
from frequenz.gridpool.cli._render_graph import ComponentGraphRenderer, RenderOptions
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@click.group()
|
|
@@ -63,6 +64,47 @@ async def print_formulas(
|
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
@cli.command("render-graph")
|
|
68
|
+
@click.argument("microgrid_id", type=int)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--output",
|
|
71
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
72
|
+
default="component_graph.png",
|
|
73
|
+
show_default=True,
|
|
74
|
+
help="Output image path.",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--show/--no-show",
|
|
78
|
+
default=True,
|
|
79
|
+
show_default=True,
|
|
80
|
+
help="Display the graph interactively.",
|
|
81
|
+
)
|
|
82
|
+
async def render_graph(microgrid_id: int, output: str, show: bool) -> None:
|
|
83
|
+
"""Render and save a component graph visualization for a microgrid."""
|
|
84
|
+
url = os.environ.get("ASSETS_API_URL")
|
|
85
|
+
key = os.environ.get("ASSETS_API_AUTH_KEY")
|
|
86
|
+
secret = os.environ.get("ASSETS_API_SIGN_SECRET")
|
|
87
|
+
if not url or not key or not secret:
|
|
88
|
+
raise click.ClickException(
|
|
89
|
+
"ASSETS_API_URL, ASSETS_API_AUTH_KEY, ASSETS_API_SIGN_SECRET must be set."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
async with AssetsApiClient(
|
|
94
|
+
url,
|
|
95
|
+
auth_key=key,
|
|
96
|
+
sign_secret=secret,
|
|
97
|
+
) as client:
|
|
98
|
+
renderer = ComponentGraphRenderer(client)
|
|
99
|
+
graph = await renderer.build_graph(MicrogridId(microgrid_id))
|
|
100
|
+
if not graph.nodes:
|
|
101
|
+
raise click.ClickException("No components found for this microgrid.")
|
|
102
|
+
pos = renderer.compute_layout(graph)
|
|
103
|
+
renderer.render(graph, pos, RenderOptions(output=output, show=show))
|
|
104
|
+
except RuntimeError as exc:
|
|
105
|
+
raise click.ClickException(str(exc)) from exc
|
|
106
|
+
|
|
107
|
+
|
|
66
108
|
def main() -> None:
|
|
67
109
|
"""Run the CLI tool."""
|
|
68
110
|
cli(_anyio_backend="asyncio")
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
# pylint: disable=import-error
|
|
4
|
+
|
|
5
|
+
"""Render component graphs via the Assets API."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from frequenz.client.assets import AssetsApiClient
|
|
13
|
+
from frequenz.client.common.microgrid import MicrogridId
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _format_category(category: Any) -> str:
|
|
17
|
+
"""Normalize a component category into a short display label."""
|
|
18
|
+
if category is None:
|
|
19
|
+
return "UNKNOWN"
|
|
20
|
+
if hasattr(category, "name"):
|
|
21
|
+
return str(category.name).replace("COMPONENT_CATEGORY_", "")
|
|
22
|
+
return str(category).replace("COMPONENT_CATEGORY_", "")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_networkx() -> Any:
|
|
26
|
+
try:
|
|
27
|
+
# pylint: disable=import-outside-toplevel
|
|
28
|
+
import networkx as nx
|
|
29
|
+
except ModuleNotFoundError as exc:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"Rendering requires optional dependencies. Install "
|
|
32
|
+
"`frequenz-gridpool[render-graph]`."
|
|
33
|
+
) from exc
|
|
34
|
+
return nx
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _require_matplotlib() -> Any:
|
|
38
|
+
try:
|
|
39
|
+
# pylint: disable=import-outside-toplevel
|
|
40
|
+
import matplotlib.pyplot as plt
|
|
41
|
+
except ModuleNotFoundError as exc:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"Rendering requires optional dependencies. Install "
|
|
44
|
+
"`frequenz-gridpool[render-graph]`."
|
|
45
|
+
) from exc
|
|
46
|
+
return plt
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class RenderOptions:
|
|
51
|
+
"""Rendering options for component graphs."""
|
|
52
|
+
|
|
53
|
+
output: str
|
|
54
|
+
show: bool = True
|
|
55
|
+
figsize: tuple[int, int] = (15, 10)
|
|
56
|
+
title: str = "Microgrid Component Graph"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ComponentGraphRenderer:
|
|
60
|
+
"""Build and render component graphs for a microgrid."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, client: AssetsApiClient) -> None:
|
|
63
|
+
"""Initialize the renderer.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
client: API client used to fetch microgrid components and their connections.
|
|
67
|
+
"""
|
|
68
|
+
self._client = client
|
|
69
|
+
|
|
70
|
+
async def build_graph(self, microgrid_id: MicrogridId) -> Any:
|
|
71
|
+
"""Build a directed graph representing the microgrid electrical topology.
|
|
72
|
+
|
|
73
|
+
Fetches electrical components and their connections for the given microgrid and
|
|
74
|
+
constructs a directed graph where nodes represent components and edges represent
|
|
75
|
+
connections from source to destination.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
microgrid_id: Identifier of the microgrid to fetch and model.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A directed graph containing component nodes and connection edges.
|
|
82
|
+
"""
|
|
83
|
+
components = await self._client.list_microgrid_electrical_components(
|
|
84
|
+
microgrid_id
|
|
85
|
+
)
|
|
86
|
+
connections = (
|
|
87
|
+
await self._client.list_microgrid_electrical_component_connections(
|
|
88
|
+
microgrid_id
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
nx = _require_networkx()
|
|
93
|
+
graph = nx.DiGraph()
|
|
94
|
+
|
|
95
|
+
for component in components:
|
|
96
|
+
graph.add_node(
|
|
97
|
+
component.id,
|
|
98
|
+
name=component.name or str(component.id),
|
|
99
|
+
category=_format_category(component.category),
|
|
100
|
+
orig_id=component.id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for connection in connections:
|
|
104
|
+
if connection is None:
|
|
105
|
+
continue
|
|
106
|
+
graph.add_edge(connection.source, connection.destination)
|
|
107
|
+
|
|
108
|
+
return graph
|
|
109
|
+
|
|
110
|
+
def compute_layout(self, graph: Any) -> dict[Any, tuple[float, float]]:
|
|
111
|
+
"""Compute a layered node layout with the root on the left.
|
|
112
|
+
|
|
113
|
+
Selects a root node, groups nodes by shortest-path distance from the root, and
|
|
114
|
+
assigns (x, y) coordinates such that layers are spaced along the x-axis.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
graph: A graph-like object containing nodes and edges.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
A mapping from node identifiers to (x, y) coordinates.
|
|
121
|
+
"""
|
|
122
|
+
if not graph.nodes:
|
|
123
|
+
return {}
|
|
124
|
+
|
|
125
|
+
root_node = self._select_root(graph)
|
|
126
|
+
layered_nodes = self._group_by_level(graph, root_node)
|
|
127
|
+
ordered_layers = self._order_layers(graph, layered_nodes)
|
|
128
|
+
return self._build_positions(ordered_layers)
|
|
129
|
+
|
|
130
|
+
def _select_root(self, graph: Any) -> Any:
|
|
131
|
+
"""Select a root node for layout.
|
|
132
|
+
|
|
133
|
+
Prefers a node with no incoming edges and at least one outgoing edge. If no such
|
|
134
|
+
node exists, falls back to an arbitrary node.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
graph: A graph-like object containing nodes and edges.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The selected root node identifier.
|
|
141
|
+
"""
|
|
142
|
+
roots = [
|
|
143
|
+
node
|
|
144
|
+
for node in graph.nodes
|
|
145
|
+
if graph.in_degree(node) == 0 and graph.out_degree(node) > 0
|
|
146
|
+
]
|
|
147
|
+
return roots[0] if roots else list(graph.nodes)[0]
|
|
148
|
+
|
|
149
|
+
def _group_by_level(self, graph: Any, root_node: Any) -> dict[int, list[Any]]:
|
|
150
|
+
"""Group nodes into layers by shortest-path distance from a root node.
|
|
151
|
+
|
|
152
|
+
Nodes reachable from the root are assigned to layers based on their shortest-path
|
|
153
|
+
distance. Nodes not reachable from the root are placed into a final "orphan"
|
|
154
|
+
layer after the deepest reachable layer.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
graph: A graph-like object containing nodes and edges.
|
|
158
|
+
root_node: The node to treat as the root for distance computation.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A mapping from layer index to the list of nodes in that layer.
|
|
162
|
+
"""
|
|
163
|
+
nx = _require_networkx()
|
|
164
|
+
levels = nx.single_source_shortest_path_length(graph, root_node)
|
|
165
|
+
layered_nodes: dict[int, list[Any]] = {}
|
|
166
|
+
for node, dist in levels.items():
|
|
167
|
+
layered_nodes.setdefault(dist, []).append(node)
|
|
168
|
+
|
|
169
|
+
orphans = [node for node in graph.nodes if node not in levels]
|
|
170
|
+
if orphans:
|
|
171
|
+
orphan_layer = max(layered_nodes.keys()) + 1 if layered_nodes else 0
|
|
172
|
+
layered_nodes[orphan_layer] = orphans
|
|
173
|
+
return layered_nodes
|
|
174
|
+
|
|
175
|
+
def _order_layers(
|
|
176
|
+
self, graph: Any, layered_nodes: dict[int, list[Any]]
|
|
177
|
+
) -> dict[int, list[Any]]:
|
|
178
|
+
"""Order nodes within each layer based on their parent positions.
|
|
179
|
+
|
|
180
|
+
Reorders the nodes in each layer to improve visual readability when rendering a
|
|
181
|
+
layered graph layout. The first layer is ordered deterministically (by string
|
|
182
|
+
representation). For subsequent layers, nodes are ordered primarily by the
|
|
183
|
+
lowest index of any already-ordered parent (predecessor) from the previous
|
|
184
|
+
layers, with a deterministic string-based tie-breaker.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
graph:
|
|
188
|
+
Graph-like object providing predecessor relationships via
|
|
189
|
+
``graph.predecessors(node)``.
|
|
190
|
+
layered_nodes:
|
|
191
|
+
Mapping from layer index to the list of nodes assigned to that layer.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A new mapping with the same layer keys as ``layered_nodes``, where each
|
|
195
|
+
layer's node list is ordered to align children beneath their parents.
|
|
196
|
+
"""
|
|
197
|
+
ordered_layers: dict[int, list[Any]] = {}
|
|
198
|
+
previous_order: dict[Any, int] = {}
|
|
199
|
+
|
|
200
|
+
for level in sorted(layered_nodes):
|
|
201
|
+
nodes = layered_nodes[level]
|
|
202
|
+
if level == 0:
|
|
203
|
+
ordered_nodes = sorted(nodes, key=str)
|
|
204
|
+
else:
|
|
205
|
+
|
|
206
|
+
def sort_key(node: Any) -> tuple[int, str]:
|
|
207
|
+
parents = [
|
|
208
|
+
parent
|
|
209
|
+
for parent in graph.predecessors(node)
|
|
210
|
+
if parent in previous_order
|
|
211
|
+
]
|
|
212
|
+
if parents:
|
|
213
|
+
parent_index = min(previous_order[parent] for parent in parents)
|
|
214
|
+
else:
|
|
215
|
+
parent_index = len(previous_order)
|
|
216
|
+
return (parent_index, str(node))
|
|
217
|
+
|
|
218
|
+
ordered_nodes = sorted(nodes, key=sort_key)
|
|
219
|
+
|
|
220
|
+
ordered_layers[level] = ordered_nodes
|
|
221
|
+
previous_order = {node: idx for idx, node in enumerate(ordered_nodes)}
|
|
222
|
+
|
|
223
|
+
return ordered_layers
|
|
224
|
+
|
|
225
|
+
def _build_positions(
|
|
226
|
+
self, layered_nodes: dict[int, list[Any]]
|
|
227
|
+
) -> dict[Any, tuple[float, float]]:
|
|
228
|
+
"""Compute node positions from pre-grouped layers.
|
|
229
|
+
|
|
230
|
+
Assigns x-coordinates based on layer index and y-coordinates by distributing
|
|
231
|
+
nodes evenly within each layer.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
layered_nodes: Mapping from layer index to the list of nodes in that layer.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A mapping from node identifiers to (x, y) coordinates.
|
|
238
|
+
"""
|
|
239
|
+
x_spacing, y_spacing = 2.5, 1.2
|
|
240
|
+
pos: dict[Any, tuple[float, float]] = {}
|
|
241
|
+
for level in sorted(layered_nodes):
|
|
242
|
+
nodes = layered_nodes[level]
|
|
243
|
+
x_pos = level * x_spacing
|
|
244
|
+
y_start = (len(nodes) - 1) * y_spacing / 2
|
|
245
|
+
for i, node in enumerate(nodes):
|
|
246
|
+
pos[node] = (x_pos, y_start - (i * y_spacing))
|
|
247
|
+
return pos
|
|
248
|
+
|
|
249
|
+
def render(
|
|
250
|
+
self, graph: Any, pos: dict[Any, tuple[float, float]], options: RenderOptions
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Render a component graph to an image file and optionally display it.
|
|
253
|
+
|
|
254
|
+
This method uses NetworkX and Matplotlib to draw the given graph using the
|
|
255
|
+
provided node positions, applies basic styling (labels, colors, arrows),
|
|
256
|
+
saves the resulting figure to the path specified in ``options.output``,
|
|
257
|
+
and, if configured, opens an interactive window to show the plot.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
graph: A NetworkX graph-like object (typically a ``DiGraph``) whose
|
|
261
|
+
nodes and edges represent the microgrid components and their
|
|
262
|
+
electrical connections.
|
|
263
|
+
pos: A mapping from node identifiers to ``(x, y)`` coordinates used
|
|
264
|
+
to place each node in the rendered figure, usually obtained from
|
|
265
|
+
:meth:`compute_layout`.
|
|
266
|
+
options: Rendering configuration, including output file path, whether
|
|
267
|
+
to show the figure interactively, figure size, and plot title.
|
|
268
|
+
"""
|
|
269
|
+
nx = _require_networkx()
|
|
270
|
+
plt = _require_matplotlib()
|
|
271
|
+
|
|
272
|
+
node_labels = {
|
|
273
|
+
node: (
|
|
274
|
+
f'{graph.nodes[node].get("name", str(node))}\n'
|
|
275
|
+
f'ID: {graph.nodes[node].get("orig_id", node)}'
|
|
276
|
+
)
|
|
277
|
+
for node in graph.nodes
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
plt.figure(figsize=options.figsize)
|
|
281
|
+
nx.draw(
|
|
282
|
+
graph,
|
|
283
|
+
pos,
|
|
284
|
+
with_labels=True,
|
|
285
|
+
labels=node_labels,
|
|
286
|
+
node_color="lightblue",
|
|
287
|
+
edge_color="gray",
|
|
288
|
+
node_size=800,
|
|
289
|
+
font_size=8,
|
|
290
|
+
)
|
|
291
|
+
plt.title(options.title)
|
|
292
|
+
plt.tight_layout()
|
|
293
|
+
plt.savefig(options.output, dpi=300)
|
|
294
|
+
if options.show:
|
|
295
|
+
plt.show()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frequenz-gridpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: High-level interface to grid pools for the Frequenz platform.
|
|
5
5
|
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
6
|
License: MIT
|
|
@@ -25,6 +25,9 @@ Requires-Dist: asyncclick<9,>=8.3.0.4
|
|
|
25
25
|
Requires-Dist: typing-extensions<5,>=4.14.1
|
|
26
26
|
Requires-Dist: frequenz-microgrid-component-graph<0.4,>=0.3.4
|
|
27
27
|
Requires-Dist: frequenz-client-assets<0.3,>=0.2.0
|
|
28
|
+
Provides-Extra: render-graph
|
|
29
|
+
Requires-Dist: matplotlib<4,>=3.7.0; extra == "render-graph"
|
|
30
|
+
Requires-Dist: networkx<4,>=3.0; extra == "render-graph"
|
|
28
31
|
Provides-Extra: dev-flake8
|
|
29
32
|
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
30
33
|
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
@@ -41,12 +44,13 @@ Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
|
|
|
41
44
|
Requires-Dist: mkdocs-gen-files==0.5.0; extra == "dev-mkdocs"
|
|
42
45
|
Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
|
|
43
46
|
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
44
|
-
Requires-Dist: mkdocs-material==9.7.
|
|
45
|
-
Requires-Dist: mkdocstrings[python]==1.0.
|
|
47
|
+
Requires-Dist: mkdocs-material==9.7.1; extra == "dev-mkdocs"
|
|
48
|
+
Requires-Dist: mkdocstrings[python]==1.0.1; extra == "dev-mkdocs"
|
|
46
49
|
Requires-Dist: mkdocstrings-python==1.19.0; extra == "dev-mkdocs"
|
|
47
50
|
Requires-Dist: frequenz-repo-config[lib]==0.13.7; extra == "dev-mkdocs"
|
|
48
51
|
Provides-Extra: dev-mypy
|
|
49
|
-
Requires-Dist: mypy==1.19.
|
|
52
|
+
Requires-Dist: mypy==1.19.1; extra == "dev-mypy"
|
|
53
|
+
Requires-Dist: types-networkx>=3.6.1.20251220; extra == "dev-mypy"
|
|
50
54
|
Requires-Dist: types-Markdown==3.10.0.20251106; extra == "dev-mypy"
|
|
51
55
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
52
56
|
Provides-Extra: dev-noxfile
|
|
@@ -55,14 +59,14 @@ Requires-Dist: frequenz-repo-config[lib]==0.13.7; extra == "dev-noxfile"
|
|
|
55
59
|
Provides-Extra: dev-pylint
|
|
56
60
|
Requires-Dist: frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
57
61
|
Provides-Extra: dev-pytest
|
|
58
|
-
Requires-Dist: pytest==
|
|
62
|
+
Requires-Dist: pytest==9.0.2; extra == "dev-pytest"
|
|
59
63
|
Requires-Dist: pylint==4.0.4; extra == "dev-pytest"
|
|
60
64
|
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.13.7; extra == "dev-pytest"
|
|
61
65
|
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
62
66
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev-pytest"
|
|
63
67
|
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
64
68
|
Provides-Extra: dev
|
|
65
|
-
Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
69
|
+
Requires-Dist: frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]; extra == "dev"
|
|
66
70
|
Dynamic: license-file
|
|
67
71
|
|
|
68
72
|
# Frequenz Gridpool Library
|
|
@@ -85,6 +89,50 @@ The following platforms are officially supported (tested):
|
|
|
85
89
|
- **Operating System:** Ubuntu Linux 20.04
|
|
86
90
|
- **Architectures:** amd64, arm64
|
|
87
91
|
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
This package ships the `gridpool-cli` command with two subcommands.
|
|
95
|
+
|
|
96
|
+
### Setup
|
|
97
|
+
|
|
98
|
+
Set the Assets API credentials before running the CLI:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export ASSETS_API_URL="grpc://..."
|
|
102
|
+
export ASSETS_API_AUTH_KEY="..."
|
|
103
|
+
export ASSETS_API_SIGN_SECRET="..."
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Print component formulas
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gridpool-cli print-formulas <microgrid_id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Optional prefix formatting:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
gridpool-cli print-formulas <microgrid_id> --prefix "{microgrid_id}.{component}"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Render component graph
|
|
119
|
+
|
|
120
|
+
Rendering requires optional dependencies. Install with:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install frequenz-gridpool[render-graph]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gridpool-cli render-graph <microgrid_id>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To save without opening a window:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gridpool-cli render-graph <microgrid_id> --no-show --output component_graph.png
|
|
134
|
+
```
|
|
135
|
+
|
|
88
136
|
## Contributing
|
|
89
137
|
|
|
90
138
|
If you want to know how to build this project and contribute to it, please
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/SOURCES.txt
RENAMED
|
@@ -10,6 +10,7 @@ src/frequenz/gridpool/conftest.py
|
|
|
10
10
|
src/frequenz/gridpool/py.typed
|
|
11
11
|
src/frequenz/gridpool/cli/__init__.py
|
|
12
12
|
src/frequenz/gridpool/cli/__main__.py
|
|
13
|
+
src/frequenz/gridpool/cli/_render_graph.py
|
|
13
14
|
src/frequenz_gridpool.egg-info/PKG-INFO
|
|
14
15
|
src/frequenz_gridpool.egg-info/SOURCES.txt
|
|
15
16
|
src/frequenz_gridpool.egg-info/dependency_links.txt
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/requires.txt
RENAMED
|
@@ -5,7 +5,7 @@ frequenz-microgrid-component-graph<0.4,>=0.3.4
|
|
|
5
5
|
frequenz-client-assets<0.3,>=0.2.0
|
|
6
6
|
|
|
7
7
|
[dev]
|
|
8
|
-
frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]
|
|
8
|
+
frequenz-gridpool[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]
|
|
9
9
|
|
|
10
10
|
[dev-flake8]
|
|
11
11
|
flake8==7.3.0
|
|
@@ -25,13 +25,14 @@ mike==2.1.3
|
|
|
25
25
|
mkdocs-gen-files==0.5.0
|
|
26
26
|
mkdocs-literate-nav==0.6.2
|
|
27
27
|
mkdocs-macros-plugin==1.5.0
|
|
28
|
-
mkdocs-material==9.7.
|
|
29
|
-
mkdocstrings[python]==1.0.
|
|
28
|
+
mkdocs-material==9.7.1
|
|
29
|
+
mkdocstrings[python]==1.0.1
|
|
30
30
|
mkdocstrings-python==1.19.0
|
|
31
31
|
frequenz-repo-config[lib]==0.13.7
|
|
32
32
|
|
|
33
33
|
[dev-mypy]
|
|
34
|
-
mypy==1.19.
|
|
34
|
+
mypy==1.19.1
|
|
35
|
+
types-networkx>=3.6.1.20251220
|
|
35
36
|
types-Markdown==3.10.0.20251106
|
|
36
37
|
frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
37
38
|
|
|
@@ -43,9 +44,13 @@ frequenz-repo-config[lib]==0.13.7
|
|
|
43
44
|
frequenz-gridpool[dev-mkdocs,dev-noxfile,dev-pytest]
|
|
44
45
|
|
|
45
46
|
[dev-pytest]
|
|
46
|
-
pytest==
|
|
47
|
+
pytest==9.0.2
|
|
47
48
|
pylint==4.0.4
|
|
48
49
|
frequenz-repo-config[extra-lint-examples]==0.13.7
|
|
49
50
|
pytest-mock==3.15.1
|
|
50
51
|
pytest-asyncio==1.3.0
|
|
51
52
|
async-solipsism==0.9
|
|
53
|
+
|
|
54
|
+
[render-graph]
|
|
55
|
+
matplotlib<4,>=3.7.0
|
|
56
|
+
networkx<4,>=3.0
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Frequenz Gridpool Library Release Notes
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
<!-- Here goes a general summary of what this release is about -->
|
|
6
|
-
|
|
7
|
-
## Upgrading
|
|
8
|
-
|
|
9
|
-
* The minimum required version of `frequenz-microgrid-component-graph` is now `v0.3.4`.
|
|
10
|
-
|
|
11
|
-
## New Features
|
|
12
|
-
|
|
13
|
-
<!-- Here goes the main new features and examples or instructions on how to use them -->
|
|
14
|
-
|
|
15
|
-
## Bug Fixes
|
|
16
|
-
|
|
17
|
-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/_graph_generator.py
RENAMED
|
File without changes
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz/gridpool/_microgrid_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{frequenz_gridpool-0.3.0 → frequenz_gridpool-0.3.1}/src/frequenz_gridpool.egg-info/top_level.txt
RENAMED
|
File without changes
|