power-grid-model-ds 1.2.6__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- power_grid_model_ds/_core/model/graphs/container.py +48 -28
- power_grid_model_ds/_core/model/graphs/models/base.py +38 -15
- power_grid_model_ds/_core/model/graphs/models/rustworkx.py +12 -5
- power_grid_model_ds/_core/model/grids/base.py +5 -5
- power_grid_model_ds/_core/model/grids/helpers.py +3 -2
- power_grid_model_ds/_core/visualizer/__init__.py +0 -0
- power_grid_model_ds/_core/visualizer/app.py +90 -0
- power_grid_model_ds/_core/visualizer/callbacks/__init__.py +0 -0
- power_grid_model_ds/_core/visualizer/callbacks/element_scaling.py +37 -0
- power_grid_model_ds/_core/visualizer/callbacks/element_selection.py +33 -0
- power_grid_model_ds/_core/visualizer/callbacks/layout_dropdown.py +11 -0
- power_grid_model_ds/_core/visualizer/callbacks/search_form.py +60 -0
- power_grid_model_ds/_core/visualizer/layout/__init__.py +0 -0
- power_grid_model_ds/_core/visualizer/layout/colors.py +17 -0
- power_grid_model_ds/_core/visualizer/layout/cytoscape_config.py +54 -0
- power_grid_model_ds/_core/visualizer/layout/cytoscape_html.py +30 -0
- power_grid_model_ds/_core/visualizer/layout/cytoscape_styling.py +115 -0
- power_grid_model_ds/_core/visualizer/layout/header.py +31 -0
- power_grid_model_ds/_core/visualizer/layout/legenda.py +41 -0
- power_grid_model_ds/_core/visualizer/layout/search_form.py +64 -0
- power_grid_model_ds/_core/visualizer/layout/selection_output.py +14 -0
- power_grid_model_ds/_core/visualizer/parsers.py +58 -0
- power_grid_model_ds/visualizer.py +12 -0
- {power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/METADATA +8 -1
- {power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/RECORD +28 -10
- {power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/WHEEL +0 -0
- {power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@
|
|
5
5
|
"""Stores the GraphContainer class"""
|
6
6
|
|
7
7
|
import dataclasses
|
8
|
+
import warnings
|
8
9
|
from dataclasses import dataclass
|
9
10
|
from typing import TYPE_CHECKING, Generator
|
10
11
|
|
@@ -16,7 +17,7 @@ from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExis
|
|
16
17
|
from power_grid_model_ds._core.model.graphs.models import RustworkxGraphModel
|
17
18
|
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
|
18
19
|
|
19
|
-
if TYPE_CHECKING:
|
20
|
+
if TYPE_CHECKING:
|
20
21
|
from power_grid_model_ds._core.model.grids.base import Grid
|
21
22
|
|
22
23
|
|
@@ -56,47 +57,68 @@ class GraphContainer:
|
|
56
57
|
complete_graph=graph_model(active_only=False),
|
57
58
|
)
|
58
59
|
|
60
|
+
def add_node_array(self, node_array: NodeArray) -> None:
|
61
|
+
"""Add a node to all graphs"""
|
62
|
+
for field in dataclasses.fields(self):
|
63
|
+
graph = getattr(self, field.name)
|
64
|
+
graph.add_node_array(node_array=node_array, raise_on_fail=False)
|
65
|
+
|
66
|
+
def add_node(self, node: NodeArray) -> None:
|
67
|
+
"""Add a node to all graphs"""
|
68
|
+
warnings.warn(
|
69
|
+
"add_node is deprecated and will be removed in a future release, use add_node_array instead",
|
70
|
+
category=DeprecationWarning,
|
71
|
+
stacklevel=2,
|
72
|
+
)
|
73
|
+
self.add_node_array(node_array=node)
|
74
|
+
|
75
|
+
def add_branch_array(self, branch_array: BranchArray) -> None:
|
76
|
+
"""Add a branch to all graphs"""
|
77
|
+
for field in self.graph_attributes:
|
78
|
+
graph = getattr(self, field.name)
|
79
|
+
graph.add_branch_array(branch_array=branch_array)
|
80
|
+
|
59
81
|
def add_branch(self, branch: BranchArray) -> None:
|
82
|
+
"""Add a branch to all graphs"""
|
83
|
+
warnings.warn(
|
84
|
+
"add_branch is deprecated and will be removed in a future release, use add_branch_array instead",
|
85
|
+
category=DeprecationWarning,
|
86
|
+
stacklevel=2,
|
87
|
+
)
|
88
|
+
self.add_branch_array(branch_array=branch)
|
89
|
+
|
90
|
+
def add_branch3_array(self, branch3_array: Branch3Array) -> None:
|
60
91
|
"""Add a branch to all graphs"""
|
61
92
|
for field in self.graph_attributes:
|
62
93
|
graph = getattr(self, field.name)
|
63
|
-
graph.
|
64
|
-
setattr(self, field.name, graph)
|
94
|
+
graph.add_branch3_array(branch3_array=branch3_array)
|
65
95
|
|
66
96
|
def add_branch3(self, branch: Branch3Array) -> None:
|
67
97
|
"""Add a branch to all graphs"""
|
68
|
-
|
98
|
+
warnings.warn(
|
99
|
+
"add_branch3 is deprecated and will be removed in a future release, use add_branch3_array instead",
|
100
|
+
category=DeprecationWarning,
|
101
|
+
stacklevel=2,
|
102
|
+
)
|
103
|
+
self.add_branch3_array(branch3_array=branch)
|
104
|
+
|
105
|
+
def delete_node(self, node: NodeArray) -> None:
|
106
|
+
"""Remove a node from all graphs"""
|
107
|
+
for field in dataclasses.fields(self):
|
69
108
|
graph = getattr(self, field.name)
|
70
|
-
graph.
|
71
|
-
setattr(self, field.name, graph)
|
109
|
+
graph.delete_node_array(node_array=node)
|
72
110
|
|
73
111
|
def delete_branch(self, branch: BranchArray) -> None:
|
74
112
|
"""Remove a branch from all graphs"""
|
75
113
|
for field in self.graph_attributes:
|
76
114
|
graph = getattr(self, field.name)
|
77
115
|
graph.delete_branch_array(branch_array=branch)
|
78
|
-
setattr(self, field.name, graph)
|
79
116
|
|
80
117
|
def delete_branch3(self, branch: Branch3Array) -> None:
|
81
118
|
"""Remove a branch from all graphs"""
|
82
119
|
for field in self.graph_attributes:
|
83
120
|
graph = getattr(self, field.name)
|
84
121
|
graph.delete_branch3_array(branch3_array=branch)
|
85
|
-
setattr(self, field.name, graph)
|
86
|
-
|
87
|
-
def add_node(self, node: NodeArray) -> None:
|
88
|
-
"""Add a node to all graphs"""
|
89
|
-
for field in dataclasses.fields(self):
|
90
|
-
graph = getattr(self, field.name)
|
91
|
-
graph.add_node_array(node_array=node, raise_on_fail=False)
|
92
|
-
setattr(self, field.name, graph)
|
93
|
-
|
94
|
-
def delete_node(self, node: NodeArray) -> None:
|
95
|
-
"""Remove a node from all graphs"""
|
96
|
-
for field in dataclasses.fields(self):
|
97
|
-
graph = getattr(self, field.name)
|
98
|
-
graph.delete_node_array(node_array=node)
|
99
|
-
setattr(self, field.name, graph)
|
100
122
|
|
101
123
|
def make_active(self, branch: BranchArray) -> None:
|
102
124
|
"""Add branch to all active_only graphs"""
|
@@ -107,7 +129,6 @@ class GraphContainer:
|
|
107
129
|
graph = getattr(self, field.name)
|
108
130
|
if graph.active_only:
|
109
131
|
graph.add_branch(from_ext_node_id=from_node, to_ext_node_id=to_node)
|
110
|
-
setattr(self, field.name, graph)
|
111
132
|
|
112
133
|
def make_inactive(self, branch: BranchArray) -> None:
|
113
134
|
"""Remove a branch from all active_only graphs"""
|
@@ -118,7 +139,6 @@ class GraphContainer:
|
|
118
139
|
graph = getattr(self, field.name)
|
119
140
|
if graph.active_only:
|
120
141
|
graph.delete_branch(from_ext_node_id=from_node, to_ext_node_id=to_node)
|
121
|
-
setattr(self, field.name, graph)
|
122
142
|
|
123
143
|
@classmethod
|
124
144
|
def from_arrays(cls, arrays: "Grid") -> "GraphContainer":
|
@@ -142,9 +162,9 @@ class GraphContainer:
|
|
142
162
|
raise RecordDoesNotExist(f"Found invalid .to_node values in {array.__class__.__name__}")
|
143
163
|
|
144
164
|
def _append(self, array: FancyArray) -> None:
|
165
|
+
if isinstance(array, NodeArray):
|
166
|
+
self.add_node_array(array)
|
145
167
|
if isinstance(array, BranchArray):
|
146
|
-
self.
|
168
|
+
self.add_branch_array(array)
|
147
169
|
if isinstance(array, Branch3Array):
|
148
|
-
self.
|
149
|
-
if isinstance(array, NodeArray):
|
150
|
-
self.add_node(array)
|
170
|
+
self.add_branch3_array(array)
|
@@ -2,11 +2,11 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: MPL-2.0
|
4
4
|
|
5
|
+
import warnings
|
5
6
|
from abc import ABC, abstractmethod
|
6
7
|
from contextlib import contextmanager
|
7
8
|
from typing import TYPE_CHECKING, Generator
|
8
9
|
|
9
|
-
import numpy as np
|
10
10
|
from numpy._typing import NDArray
|
11
11
|
|
12
12
|
from power_grid_model_ds._core.model.arrays.pgm_arrays import Branch3Array, BranchArray, NodeArray
|
@@ -17,7 +17,7 @@ from power_grid_model_ds._core.model.graphs.errors import (
|
|
17
17
|
NoPathBetweenNodes,
|
18
18
|
)
|
19
19
|
|
20
|
-
if TYPE_CHECKING:
|
20
|
+
if TYPE_CHECKING:
|
21
21
|
from power_grid_model_ds._core.model.grids.base import Grid
|
22
22
|
|
23
23
|
|
@@ -113,8 +113,9 @@ class BaseGraphModel(ABC):
|
|
113
113
|
|
114
114
|
def add_node_array(self, node_array: NodeArray, raise_on_fail: bool = True) -> None:
|
115
115
|
"""Add all nodes in the node array to the graph."""
|
116
|
-
for
|
117
|
-
|
116
|
+
if raise_on_fail and any(self.has_node(x) for x in node_array["id"]):
|
117
|
+
raise GraphError("At least one node id already exists in the Graph.")
|
118
|
+
self._add_nodes(node_array["id"].tolist())
|
118
119
|
|
119
120
|
def delete_node_array(self, node_array: NodeArray, raise_on_fail: bool = True) -> None:
|
120
121
|
"""Delete all nodes in node_array from the graph"""
|
@@ -162,9 +163,14 @@ class BaseGraphModel(ABC):
|
|
162
163
|
|
163
164
|
def add_branch_array(self, branch_array: BranchArray) -> None:
|
164
165
|
"""Add all branches in the branch array to the graph."""
|
165
|
-
|
166
|
-
|
167
|
-
|
166
|
+
if self.active_only:
|
167
|
+
branch_array = branch_array[branch_array.is_active]
|
168
|
+
if not branch_array.size:
|
169
|
+
return
|
170
|
+
|
171
|
+
from_node_ids = self._externals_to_internals(branch_array["from_node"].tolist())
|
172
|
+
to_node_ids = self._externals_to_internals(branch_array["to_node"].tolist())
|
173
|
+
self._add_branches(from_node_ids, to_node_ids)
|
168
174
|
|
169
175
|
def add_branch3_array(self, branch3_array: Branch3Array) -> None:
|
170
176
|
"""Add all branch3s in the branch3 array to the graph."""
|
@@ -245,14 +251,25 @@ class BaseGraphModel(ABC):
|
|
245
251
|
target=self.external_to_internal(ext_end_node_id),
|
246
252
|
)
|
247
253
|
|
248
|
-
if internal_paths == []:
|
249
|
-
raise NoPathBetweenNodes(f"No path between nodes {ext_start_node_id} and {ext_end_node_id}")
|
250
|
-
|
251
254
|
return [self._internals_to_externals(path) for path in internal_paths]
|
252
255
|
|
253
|
-
def get_components(self, substation_nodes:
|
256
|
+
def get_components(self, substation_nodes: list[int] | None = None) -> list[list[int]]:
|
254
257
|
"""Returns all separate components when the substation_nodes are removed of the graph as lists"""
|
255
|
-
|
258
|
+
if substation_nodes:
|
259
|
+
warnings.warn(
|
260
|
+
message="""
|
261
|
+
get_components: substation_nodes argument is deprecated and will be removed in a future release.
|
262
|
+
The functionality is still available with the use of the `tmp_remove_nodes` context manager.
|
263
|
+
|
264
|
+
Example:
|
265
|
+
>>> with graph.tmp_remove_nodes(substation_nodes):
|
266
|
+
>>> components = graph.get_components()
|
267
|
+
""",
|
268
|
+
category=DeprecationWarning,
|
269
|
+
stacklevel=2,
|
270
|
+
)
|
271
|
+
with self.tmp_remove_nodes(substation_nodes or []):
|
272
|
+
internal_components = self._get_components()
|
256
273
|
return [self._internals_to_externals(component) for component in internal_components]
|
257
274
|
|
258
275
|
def get_connected(
|
@@ -333,7 +350,7 @@ class BaseGraphModel(ABC):
|
|
333
350
|
"""Build from arrays"""
|
334
351
|
new_graph = cls(active_only=active_only)
|
335
352
|
|
336
|
-
new_graph.add_node_array(node_array=arrays.node)
|
353
|
+
new_graph.add_node_array(node_array=arrays.node, raise_on_fail=False)
|
337
354
|
new_graph.add_branch_array(arrays.branches)
|
338
355
|
new_graph.add_branch3_array(arrays.three_winding_transformer)
|
339
356
|
|
@@ -371,11 +388,17 @@ class BaseGraphModel(ABC):
|
|
371
388
|
@abstractmethod
|
372
389
|
def _add_node(self, ext_node_id: int) -> None: ...
|
373
390
|
|
391
|
+
@abstractmethod
|
392
|
+
def _add_nodes(self, ext_node_ids: list[int]) -> None: ...
|
393
|
+
|
374
394
|
@abstractmethod
|
375
395
|
def _delete_node(self, node_id: int): ...
|
376
396
|
|
377
397
|
@abstractmethod
|
378
|
-
def _add_branch(self, from_node_id, to_node_id) -> None: ...
|
398
|
+
def _add_branch(self, from_node_id: int, to_node_id: int) -> None: ...
|
399
|
+
|
400
|
+
@abstractmethod
|
401
|
+
def _add_branches(self, from_node_ids: list[int], to_node_ids: list[int]) -> None: ...
|
379
402
|
|
380
403
|
@abstractmethod
|
381
404
|
def _delete_branch(self, from_node_id, to_node_id) -> None:
|
@@ -391,7 +414,7 @@ class BaseGraphModel(ABC):
|
|
391
414
|
def _get_all_paths(self, source, target) -> list[list[int]]: ...
|
392
415
|
|
393
416
|
@abstractmethod
|
394
|
-
def _get_components(self
|
417
|
+
def _get_components(self) -> list[list[int]]: ...
|
395
418
|
|
396
419
|
@abstractmethod
|
397
420
|
def _find_fundamental_cycles(self) -> list[list[int]]: ...
|
@@ -52,6 +52,12 @@ class RustworkxGraphModel(BaseGraphModel):
|
|
52
52
|
self._external_to_internal[ext_node_id] = graph_node_id
|
53
53
|
self._internal_to_external[graph_node_id] = ext_node_id
|
54
54
|
|
55
|
+
def _add_nodes(self, ext_node_ids: list[int]) -> None:
|
56
|
+
graph_node_ids = self._graph.add_nodes_from(ext_node_ids)
|
57
|
+
for ext_node_id, graph_node_id in zip(ext_node_ids, graph_node_ids):
|
58
|
+
self._external_to_internal[ext_node_id] = graph_node_id
|
59
|
+
self._internal_to_external[graph_node_id] = ext_node_id
|
60
|
+
|
55
61
|
def _delete_node(self, node_id: int):
|
56
62
|
self._graph.remove_node(node_id)
|
57
63
|
external_node_id = self._internal_to_external.pop(node_id)
|
@@ -66,6 +72,10 @@ class RustworkxGraphModel(BaseGraphModel):
|
|
66
72
|
def _add_branch(self, from_node_id: int, to_node_id: int):
|
67
73
|
self._graph.add_edge(from_node_id, to_node_id, None)
|
68
74
|
|
75
|
+
def _add_branches(self, from_node_ids: list[int], to_node_ids: list[int]):
|
76
|
+
edge_list = [(from_node_id, to_node_id, None) for from_node_id, to_node_id in zip(from_node_ids, to_node_ids)]
|
77
|
+
self._graph.add_edges_from(edge_list)
|
78
|
+
|
69
79
|
def _delete_branch(self, from_node_id: int, to_node_id: int) -> None:
|
70
80
|
try:
|
71
81
|
self._graph.remove_edge(from_node_id, to_node_id)
|
@@ -84,11 +94,8 @@ class RustworkxGraphModel(BaseGraphModel):
|
|
84
94
|
def _get_all_paths(self, source: int, target: int) -> list[list[int]]:
|
85
95
|
return list(rx.all_simple_paths(self._graph, source, target))
|
86
96
|
|
87
|
-
def _get_components(self
|
88
|
-
|
89
|
-
for os_node in substation_nodes:
|
90
|
-
no_os_graph.remove_node(os_node)
|
91
|
-
components = rx.connected_components(no_os_graph)
|
97
|
+
def _get_components(self) -> list[list[int]]:
|
98
|
+
components = rx.connected_components(self._graph)
|
92
99
|
return [list(component) for component in components]
|
93
100
|
|
94
101
|
def _get_connected(self, node_id: int, nodes_to_ignore: list[int], inclusive: bool = False) -> list[int]:
|
@@ -200,9 +200,9 @@ class Grid(FancyArrayContainer):
|
|
200
200
|
check_max_id (bool, optional): Whether to check if the array id is the maximum id. Defaults to True.
|
201
201
|
"""
|
202
202
|
self._append(array, check_max_id=check_max_id) # noqa
|
203
|
-
|
204
|
-
|
205
|
-
|
203
|
+
|
204
|
+
# pylint: disable=protected-access
|
205
|
+
self.graphs._append(array)
|
206
206
|
|
207
207
|
def add_branch(self, branch: BranchArray) -> None:
|
208
208
|
"""Add a branch to the grid
|
@@ -211,7 +211,7 @@ class Grid(FancyArrayContainer):
|
|
211
211
|
branch (BranchArray): The branch to add
|
212
212
|
"""
|
213
213
|
self._append(array=branch)
|
214
|
-
self.graphs.
|
214
|
+
self.graphs.add_branch_array(branch_array=branch)
|
215
215
|
|
216
216
|
logging.debug(f"added branch {branch.id} from {branch.from_node} to {branch.to_node}")
|
217
217
|
|
@@ -243,7 +243,7 @@ class Grid(FancyArrayContainer):
|
|
243
243
|
node (NodeArray): The node to add
|
244
244
|
"""
|
245
245
|
self._append(array=node)
|
246
|
-
self.graphs.
|
246
|
+
self.graphs.add_node_array(node_array=node)
|
247
247
|
logging.debug(f"added rail {node.id}")
|
248
248
|
|
249
249
|
def delete_node(self, node: NodeArray) -> None:
|
@@ -45,8 +45,9 @@ def set_feeder_ids(grid: "Grid"):
|
|
45
45
|
601 | 101 | 204
|
46
46
|
"""
|
47
47
|
_reset_feeder_ids(grid)
|
48
|
-
feeder_node_ids = grid.node.filter(node_type=NodeType.SUBSTATION_NODE)
|
49
|
-
|
48
|
+
feeder_node_ids = grid.node.filter(node_type=NodeType.SUBSTATION_NODE)["id"]
|
49
|
+
with grid.graphs.active_graph.tmp_remove_nodes(feeder_node_ids):
|
50
|
+
components = grid.graphs.active_graph.get_components()
|
50
51
|
for component_node_ids in components:
|
51
52
|
component_branches = _get_active_component_branches(grid, component_node_ids)
|
52
53
|
|
File without changes
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
import dash_bootstrap_components as dbc
|
6
|
+
from dash import Dash, dcc, html
|
7
|
+
from dash_bootstrap_components.icons import FONT_AWESOME
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
10
|
+
from power_grid_model_ds._core.visualizer.callbacks import ( # noqa: F401 # pylint: disable=unused-import
|
11
|
+
element_scaling,
|
12
|
+
element_selection,
|
13
|
+
layout_dropdown,
|
14
|
+
search_form,
|
15
|
+
)
|
16
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_html import get_cytoscape_html
|
17
|
+
from power_grid_model_ds._core.visualizer.layout.header import HEADER_HTML
|
18
|
+
from power_grid_model_ds._core.visualizer.layout.selection_output import SELECTION_OUTPUT_HTML
|
19
|
+
from power_grid_model_ds._core.visualizer.parsers import parse_branches, parse_node_array
|
20
|
+
from power_grid_model_ds.arrays import NodeArray
|
21
|
+
|
22
|
+
GOOGLE_FONTS = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
23
|
+
MDBOOTSTRAP = "https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/8.2.0/mdb.min.css"
|
24
|
+
|
25
|
+
|
26
|
+
def visualize(grid: Grid, debug: bool = False, port: int = 8050) -> None:
|
27
|
+
"""Visualize the Grid.
|
28
|
+
|
29
|
+
grid: Grid
|
30
|
+
The grid to visualize.
|
31
|
+
|
32
|
+
layout: str
|
33
|
+
The layout to use.
|
34
|
+
|
35
|
+
If 'layout' is not provided (""):
|
36
|
+
And grid.node contains "x" and "y" columns:
|
37
|
+
The layout will be set to "preset" which uses the x and y coordinates to place the nodes.
|
38
|
+
Otherwise:
|
39
|
+
The layout will be set to "breadthfirst", which is a hierarchical breadth-first-search (BFS) layout.
|
40
|
+
Other options:
|
41
|
+
- "random": A layout that places the nodes randomly.
|
42
|
+
- "circle": A layout that places the nodes in a circle.
|
43
|
+
- "concentric": A layout that places the nodes in concentric circles.
|
44
|
+
- "grid": A layout that places the nodes in a grid matrix.
|
45
|
+
- "cose": A layout that uses the CompoundSpring Embedder algorithm (force-directed layout)
|
46
|
+
"""
|
47
|
+
|
48
|
+
app = Dash(
|
49
|
+
external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP, MDBOOTSTRAP, FONT_AWESOME, GOOGLE_FONTS]
|
50
|
+
)
|
51
|
+
app.layout = get_app_layout(grid)
|
52
|
+
app.run(debug=debug, port=port)
|
53
|
+
|
54
|
+
|
55
|
+
def _get_columns_store(grid: Grid) -> dcc.Store:
|
56
|
+
return dcc.Store(
|
57
|
+
id="columns-store",
|
58
|
+
data={
|
59
|
+
"node": grid.node.columns,
|
60
|
+
"line": grid.line.columns,
|
61
|
+
"link": grid.link.columns,
|
62
|
+
"transformer": grid.transformer.columns,
|
63
|
+
"branch": grid.branches.columns,
|
64
|
+
},
|
65
|
+
)
|
66
|
+
|
67
|
+
|
68
|
+
def get_app_layout(grid: Grid) -> html.Div:
|
69
|
+
"""Get the app layout."""
|
70
|
+
columns_store = _get_columns_store(grid)
|
71
|
+
graph_layout = _get_graph_layout(grid.node)
|
72
|
+
elements = parse_node_array(grid.node) + parse_branches(grid)
|
73
|
+
cytoscape_html = get_cytoscape_html(graph_layout, elements)
|
74
|
+
|
75
|
+
return html.Div(
|
76
|
+
[
|
77
|
+
columns_store,
|
78
|
+
HEADER_HTML,
|
79
|
+
html.Hr(style={"border-color": "white", "margin": "0"}),
|
80
|
+
cytoscape_html,
|
81
|
+
SELECTION_OUTPUT_HTML,
|
82
|
+
],
|
83
|
+
)
|
84
|
+
|
85
|
+
|
86
|
+
def _get_graph_layout(nodes: NodeArray) -> str:
|
87
|
+
"""Determine the graph layout"""
|
88
|
+
if "x" in nodes.columns and "y" in nodes.columns:
|
89
|
+
return "preset"
|
90
|
+
return "breadthfirst"
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from copy import deepcopy
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from dash import Input, Output, callback
|
9
|
+
|
10
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_styling import BRANCH_WIDTH, DEFAULT_STYLESHEET, NODE_SIZE
|
11
|
+
|
12
|
+
|
13
|
+
@callback(
|
14
|
+
Output("cytoscape-graph", "stylesheet", allow_duplicate=True),
|
15
|
+
Input("node-scale-input", "value"),
|
16
|
+
Input("edge-scale-input", "value"),
|
17
|
+
prevent_initial_call=True,
|
18
|
+
)
|
19
|
+
def scale_elements(node_scale: float, edge_scale: float) -> list[dict[str, Any]]:
|
20
|
+
"""Callback to scale the elements of the graph."""
|
21
|
+
new_stylesheet = deepcopy(DEFAULT_STYLESHEET)
|
22
|
+
edge_style = {
|
23
|
+
"selector": "edge",
|
24
|
+
"style": {
|
25
|
+
"width": BRANCH_WIDTH * edge_scale,
|
26
|
+
},
|
27
|
+
}
|
28
|
+
new_stylesheet.append(edge_style)
|
29
|
+
node_style = {
|
30
|
+
"selector": "node",
|
31
|
+
"style": {
|
32
|
+
"height": NODE_SIZE * node_scale,
|
33
|
+
"width": NODE_SIZE * node_scale,
|
34
|
+
},
|
35
|
+
}
|
36
|
+
new_stylesheet.append(node_style)
|
37
|
+
return new_stylesheet
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from dash import Input, Output, callback, dash_table
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.visualizer.layout.selection_output import (
|
10
|
+
SELECTION_OUTPUT_HTML,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
@callback(
|
15
|
+
Output("selection-output", "children"),
|
16
|
+
Input("cytoscape-graph", "selectedNodeData"),
|
17
|
+
Input("cytoscape-graph", "selectedEdgeData"),
|
18
|
+
)
|
19
|
+
def display_selected_element(node_data, edge_data):
|
20
|
+
"""Display the tapped edge data."""
|
21
|
+
if node_data:
|
22
|
+
return _to_data_table(node_data.pop())
|
23
|
+
if edge_data:
|
24
|
+
return _to_data_table(edge_data.pop())
|
25
|
+
return SELECTION_OUTPUT_HTML
|
26
|
+
|
27
|
+
|
28
|
+
def _to_data_table(data: dict[str, Any]):
|
29
|
+
columns = data.keys()
|
30
|
+
data_table = dash_table.DataTable(
|
31
|
+
data=[data], columns=[{"name": key, "id": key} for key in columns], editable=False
|
32
|
+
)
|
33
|
+
return data_table
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from dash import Input, Output, callback
|
6
|
+
|
7
|
+
|
8
|
+
@callback(Output("cytoscape-graph", "layout"), Input("dropdown-update-layout", "value"), prevent_initial_call=True)
|
9
|
+
def update_layout(layout):
|
10
|
+
"""Callback to update the layout of the graph."""
|
11
|
+
return {"name": layout, "animate": True}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from dash import Input, Output, callback
|
7
|
+
|
8
|
+
from power_grid_model_ds._core.visualizer.layout.colors import CYTO_COLORS
|
9
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_styling import DEFAULT_STYLESHEET
|
10
|
+
|
11
|
+
|
12
|
+
@callback(
|
13
|
+
Output("cytoscape-graph", "stylesheet"),
|
14
|
+
Input("search-form-group-input", "value"),
|
15
|
+
Input("search-form-column-input", "value"),
|
16
|
+
Input("search-form-operator-input", "value"),
|
17
|
+
Input("search-form-value-input", "value"),
|
18
|
+
)
|
19
|
+
def search_element(group: str, column: str, operator: str, value: str) -> list[dict[str, Any]]:
|
20
|
+
"""Color the specified element red based on the input values."""
|
21
|
+
if not group or not column or not value:
|
22
|
+
return DEFAULT_STYLESHEET
|
23
|
+
|
24
|
+
# Determine if we're working with a node or an edge type
|
25
|
+
if group == "node":
|
26
|
+
style = {
|
27
|
+
"background-color": CYTO_COLORS["highlighted"],
|
28
|
+
"text-background-color": CYTO_COLORS["highlighted"],
|
29
|
+
}
|
30
|
+
else:
|
31
|
+
style = {"line-color": CYTO_COLORS["highlighted"], "target-arrow-color": CYTO_COLORS["highlighted"]}
|
32
|
+
|
33
|
+
if column == "id":
|
34
|
+
selector = f'[{column} {operator} "{value}"]'
|
35
|
+
else:
|
36
|
+
selector = f"[{column} {operator} {value}]"
|
37
|
+
|
38
|
+
new_style = {
|
39
|
+
"selector": selector,
|
40
|
+
"style": style,
|
41
|
+
}
|
42
|
+
return DEFAULT_STYLESHEET + [new_style]
|
43
|
+
|
44
|
+
|
45
|
+
@callback(
|
46
|
+
Output("search-form-column-input", "options"),
|
47
|
+
Output("search-form-column-input", "value"),
|
48
|
+
Input("search-form-group-input", "value"),
|
49
|
+
Input("columns-store", "data"),
|
50
|
+
)
|
51
|
+
def update_column_options(selected_group, store_data):
|
52
|
+
"""Update the column dropdown options based on the selected group."""
|
53
|
+
if not selected_group or not store_data:
|
54
|
+
return [], None
|
55
|
+
|
56
|
+
# Get columns for the selected group (node, line, link, or transformer)
|
57
|
+
columns = store_data.get(selected_group, [])
|
58
|
+
default_value = columns[0] if columns else "id"
|
59
|
+
|
60
|
+
return columns, default_value
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
YELLOW = "#facc37"
|
6
|
+
CYTO_COLORS = {
|
7
|
+
"line": YELLOW,
|
8
|
+
"link": "green",
|
9
|
+
"transformer": "#4290f5",
|
10
|
+
"node": YELLOW,
|
11
|
+
"selected": "#e28743",
|
12
|
+
"selected_transformer": "#0349a3",
|
13
|
+
"substation_node": "purple",
|
14
|
+
"open_branch": "#c9c9c9",
|
15
|
+
"highlighted": "#a10000",
|
16
|
+
}
|
17
|
+
BACKGROUND_COLOR = "#555555"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from dash import dcc, html
|
6
|
+
|
7
|
+
from power_grid_model_ds._core.visualizer.layout.colors import CYTO_COLORS
|
8
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_html import LAYOUT_OPTIONS
|
9
|
+
|
10
|
+
NODE_SCALE_HTML = [
|
11
|
+
html.I(className="fas fa-circle", style={"color": CYTO_COLORS["node"], "margin-right": "10px"}),
|
12
|
+
dcc.Input(
|
13
|
+
id="node-scale-input",
|
14
|
+
type="number",
|
15
|
+
value=1,
|
16
|
+
min=0.1,
|
17
|
+
step=0.1,
|
18
|
+
style={"width": "75px"},
|
19
|
+
),
|
20
|
+
html.Span(style={"margin-right": "10px"}),
|
21
|
+
]
|
22
|
+
|
23
|
+
EDGE_SCALE_HTML = [
|
24
|
+
html.I(className="fas fa-arrow-right-long", style={"color": CYTO_COLORS["line"], "margin-right": "10px"}),
|
25
|
+
dcc.Input(
|
26
|
+
id="edge-scale-input",
|
27
|
+
type="number",
|
28
|
+
value=1,
|
29
|
+
min=0.1,
|
30
|
+
step=0.1,
|
31
|
+
style={"width": "75px"},
|
32
|
+
),
|
33
|
+
]
|
34
|
+
|
35
|
+
SCALE_INPUTS = [
|
36
|
+
html.Div(
|
37
|
+
NODE_SCALE_HTML + EDGE_SCALE_HTML,
|
38
|
+
style={"margin": "0 20px 0 10px"},
|
39
|
+
),
|
40
|
+
]
|
41
|
+
|
42
|
+
LAYOUT_DROPDOWN_HTML = [
|
43
|
+
html.Div(
|
44
|
+
dcc.Dropdown(
|
45
|
+
id="dropdown-update-layout",
|
46
|
+
placeholder="Select layout",
|
47
|
+
value="",
|
48
|
+
clearable=False,
|
49
|
+
options=[{"label": name.capitalize(), "value": name} for name in LAYOUT_OPTIONS],
|
50
|
+
style={"width": "200px"},
|
51
|
+
),
|
52
|
+
style={"margin": "0 20px 0 10px"},
|
53
|
+
)
|
54
|
+
]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
import dash_cytoscape as cyto
|
8
|
+
from dash import html
|
9
|
+
|
10
|
+
from power_grid_model_ds._core.visualizer.layout.colors import BACKGROUND_COLOR
|
11
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_styling import DEFAULT_STYLESHEET
|
12
|
+
|
13
|
+
LAYOUT_OPTIONS = ["random", "circle", "concentric", "grid", "cose", "breadthfirst"]
|
14
|
+
|
15
|
+
_CYTO_INNER_STYLE = {"width": "100%", "height": "100%", "background-color": BACKGROUND_COLOR}
|
16
|
+
_CYTO_OUTER_STYLE = {"height": "80vh"}
|
17
|
+
|
18
|
+
|
19
|
+
def get_cytoscape_html(layout: str, elements: list[dict[str, Any]]) -> html.Div:
|
20
|
+
"""Get the Cytoscape HTML element"""
|
21
|
+
return html.Div(
|
22
|
+
cyto.Cytoscape(
|
23
|
+
id="cytoscape-graph",
|
24
|
+
layout={"name": layout},
|
25
|
+
style=_CYTO_INNER_STYLE,
|
26
|
+
elements=elements,
|
27
|
+
stylesheet=DEFAULT_STYLESHEET,
|
28
|
+
),
|
29
|
+
style=_CYTO_OUTER_STYLE,
|
30
|
+
)
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Contains selectors for the Cytoscape stylesheet."""
|
6
|
+
|
7
|
+
from power_grid_model_ds._core.visualizer.layout.colors import CYTO_COLORS
|
8
|
+
|
9
|
+
NODE_SIZE = 100
|
10
|
+
BRANCH_WIDTH = 10
|
11
|
+
|
12
|
+
_BRANCH_STYLE = {
|
13
|
+
"selector": "edge",
|
14
|
+
"style": {
|
15
|
+
"line-color": CYTO_COLORS["line"],
|
16
|
+
"target-arrow-color": CYTO_COLORS["line"],
|
17
|
+
"curve-style": "bezier",
|
18
|
+
"target-arrow-shape": "triangle",
|
19
|
+
"width": BRANCH_WIDTH,
|
20
|
+
},
|
21
|
+
}
|
22
|
+
_NODE_STYLE = {
|
23
|
+
"selector": "node",
|
24
|
+
"style": {
|
25
|
+
"label": "data(id)",
|
26
|
+
"border-width": 5,
|
27
|
+
"border-color": "black",
|
28
|
+
"font-size": 25,
|
29
|
+
"text-halign": "center",
|
30
|
+
"text-valign": "center",
|
31
|
+
"background-color": CYTO_COLORS["node"],
|
32
|
+
"text-background-color": CYTO_COLORS["node"],
|
33
|
+
"text-background-opacity": 1,
|
34
|
+
"text-background-shape": "round-rectangle",
|
35
|
+
"width": 75,
|
36
|
+
"height": 75,
|
37
|
+
},
|
38
|
+
}
|
39
|
+
_NODE_LARGE_ID_STYLE = {
|
40
|
+
"selector": "node[id > 10000000]",
|
41
|
+
"style": {"font-size": 15},
|
42
|
+
}
|
43
|
+
_SELECTED_NODE_STYLE = {
|
44
|
+
"selector": "node:selected, node:active",
|
45
|
+
"style": {"border-width": 5, "border-color": CYTO_COLORS["selected"]},
|
46
|
+
}
|
47
|
+
|
48
|
+
_SELECTED_BRANCH_STYLE = {
|
49
|
+
"selector": "edge:selected, edge:active",
|
50
|
+
"style": {"line-color": CYTO_COLORS["selected"], "target-arrow-color": CYTO_COLORS["selected"], "width": 10},
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
_SUBSTATION_NODE_STYLE = {
|
55
|
+
"selector": "node[node_type = 1]",
|
56
|
+
"style": {
|
57
|
+
"label": "data(id)",
|
58
|
+
"shape": "diamond",
|
59
|
+
"background-color": CYTO_COLORS["substation_node"],
|
60
|
+
"text-background-color": CYTO_COLORS["substation_node"],
|
61
|
+
"width": NODE_SIZE * 1.2,
|
62
|
+
"height": NODE_SIZE * 1.2,
|
63
|
+
"color": "white",
|
64
|
+
},
|
65
|
+
}
|
66
|
+
_TRANSFORMER_STYLE = {
|
67
|
+
"selector": "edge[group = 'transformer']",
|
68
|
+
"style": {"line-color": CYTO_COLORS["transformer"], "target-arrow-color": CYTO_COLORS["transformer"]},
|
69
|
+
}
|
70
|
+
_SELECTED_TRANSFORMER_STYLE = {
|
71
|
+
"selector": "edge[group = 'transformer']:selected, edge[group = 'transformer']:active",
|
72
|
+
"style": {
|
73
|
+
"line-color": CYTO_COLORS["selected_transformer"],
|
74
|
+
"target-arrow-color": CYTO_COLORS["selected_transformer"],
|
75
|
+
},
|
76
|
+
}
|
77
|
+
|
78
|
+
_OPEN_BRANCH_STYLE = {
|
79
|
+
"selector": "edge[from_status = 0], edge[to_status = 0]",
|
80
|
+
"style": {
|
81
|
+
"line-style": "dashed",
|
82
|
+
"line-color": CYTO_COLORS["open_branch"],
|
83
|
+
"target-arrow-color": CYTO_COLORS["open_branch"],
|
84
|
+
"source-arrow-color": CYTO_COLORS["open_branch"],
|
85
|
+
},
|
86
|
+
}
|
87
|
+
_OPEN_FROM_SIDE_BRANCH_STYLE = {
|
88
|
+
"selector": "edge[from_status = 0]",
|
89
|
+
"style": {
|
90
|
+
"source-arrow-shape": "diamond",
|
91
|
+
"source-arrow-fill": "hollow",
|
92
|
+
},
|
93
|
+
}
|
94
|
+
_OPEN_TO_SIDE_BRANCH_STYLE = {
|
95
|
+
"selector": "edge[to_status = 0]",
|
96
|
+
"style": {
|
97
|
+
"target-arrow-shape": "diamond",
|
98
|
+
"target-arrow-fill": "hollow",
|
99
|
+
},
|
100
|
+
}
|
101
|
+
|
102
|
+
|
103
|
+
DEFAULT_STYLESHEET = [
|
104
|
+
_NODE_STYLE,
|
105
|
+
_NODE_LARGE_ID_STYLE,
|
106
|
+
_SUBSTATION_NODE_STYLE,
|
107
|
+
_BRANCH_STYLE,
|
108
|
+
_TRANSFORMER_STYLE,
|
109
|
+
_SELECTED_NODE_STYLE,
|
110
|
+
_SELECTED_BRANCH_STYLE,
|
111
|
+
_SELECTED_TRANSFORMER_STYLE,
|
112
|
+
_OPEN_BRANCH_STYLE,
|
113
|
+
_OPEN_FROM_SIDE_BRANCH_STYLE,
|
114
|
+
_OPEN_TO_SIDE_BRANCH_STYLE,
|
115
|
+
]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
import dash_bootstrap_components as dbc
|
6
|
+
|
7
|
+
from power_grid_model_ds._core.visualizer.layout.colors import BACKGROUND_COLOR
|
8
|
+
from power_grid_model_ds._core.visualizer.layout.cytoscape_config import LAYOUT_DROPDOWN_HTML, SCALE_INPUTS
|
9
|
+
from power_grid_model_ds._core.visualizer.layout.legenda import LEGENDA_HTML
|
10
|
+
from power_grid_model_ds._core.visualizer.layout.search_form import SEARCH_FORM_HTML
|
11
|
+
|
12
|
+
_SEARCH_FORM_CARD_STYLE = {
|
13
|
+
"background-color": "#555555",
|
14
|
+
"color": "white",
|
15
|
+
"border-left": "1px solid white",
|
16
|
+
"border-right": "1px solid white",
|
17
|
+
"border-radius": 0,
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
HEADER_HTML = dbc.Row(
|
22
|
+
[
|
23
|
+
dbc.Col(LEGENDA_HTML, className="d-flex align-items-center"),
|
24
|
+
dbc.Col(
|
25
|
+
dbc.Card(SEARCH_FORM_HTML, style=_SEARCH_FORM_CARD_STYLE),
|
26
|
+
className="d-flex justify-content-center align-items-center",
|
27
|
+
),
|
28
|
+
dbc.Col(SCALE_INPUTS + LAYOUT_DROPDOWN_HTML, className="d-flex justify-content-end align-items-center"),
|
29
|
+
],
|
30
|
+
style={"background-color": BACKGROUND_COLOR},
|
31
|
+
)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
import dash_bootstrap_components as dbc
|
6
|
+
from dash import html
|
7
|
+
|
8
|
+
from power_grid_model_ds._core.visualizer.layout.colors import CYTO_COLORS
|
9
|
+
|
10
|
+
_MARGIN = "0 10px"
|
11
|
+
_FONT_SIZE = "2.5em"
|
12
|
+
|
13
|
+
NODE_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["node"]}
|
14
|
+
_SUBSTATION_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["substation_node"]}
|
15
|
+
_LINE_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["line"]}
|
16
|
+
_LINK_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["link"]}
|
17
|
+
_TRANSFORMER_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["transformer"]}
|
18
|
+
_OPEN_BRANCH_ICON_STYLE = {"font-size": _FONT_SIZE, "margin": _MARGIN, "color": CYTO_COLORS["open_branch"]}
|
19
|
+
LEGENDA_HTML = html.Div(
|
20
|
+
[
|
21
|
+
html.I(className="fas fa-circle", id="node-icon", style=NODE_ICON_STYLE),
|
22
|
+
dbc.Tooltip("Node", target="node-icon", placement="bottom"),
|
23
|
+
html.I(className="fas fa-diamond", id="substation-icon", style=_SUBSTATION_ICON_STYLE),
|
24
|
+
dbc.Tooltip("Substation", target="substation-icon", placement="bottom"),
|
25
|
+
html.I(className="fas fa-arrow-right-long", id="line-icon", style=_LINE_ICON_STYLE),
|
26
|
+
dbc.Tooltip("Line", target="line-icon", placement="bottom"),
|
27
|
+
html.I(className="fas fa-arrow-right-long", id="transformer-icon", style=_TRANSFORMER_ICON_STYLE),
|
28
|
+
dbc.Tooltip("Transformer", target="transformer-icon", placement="bottom"),
|
29
|
+
html.I(className="fas fa-arrow-right-long", id="link-icon", style=_LINK_ICON_STYLE),
|
30
|
+
dbc.Tooltip("Link", target="link-icon", placement="bottom"),
|
31
|
+
html.I(className="fas fa-ellipsis", id="open-branch-icon", style=_OPEN_BRANCH_ICON_STYLE),
|
32
|
+
dbc.Tooltip("Open Branch", target="open-branch-icon", placement="bottom"),
|
33
|
+
],
|
34
|
+
style={
|
35
|
+
"display": "flex",
|
36
|
+
"align-items": "center",
|
37
|
+
"margin": _MARGIN,
|
38
|
+
"width": "100%",
|
39
|
+
"text-shadow": "0 0 5px #000",
|
40
|
+
},
|
41
|
+
)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
import dash_bootstrap_components as dbc
|
6
|
+
from dash import html
|
7
|
+
|
8
|
+
SPAN_TEXT_STYLE = {"color": "white", "margin-right": "8px", "font-weight": "bold", "text-shadow": "0 0 5px #000"}
|
9
|
+
_INPUT_STYLE = {"width": "150px", "display": "inline-block"}
|
10
|
+
# Create your form components
|
11
|
+
GROUP_INPUT = dbc.Select(
|
12
|
+
id="search-form-group-input",
|
13
|
+
options=[
|
14
|
+
{"label": "node", "value": "node"},
|
15
|
+
{"label": "line", "value": "line"},
|
16
|
+
{"label": "link", "value": "link"},
|
17
|
+
{"label": "transformer", "value": "transformer"},
|
18
|
+
{"label": "branch", "value": "branch"},
|
19
|
+
],
|
20
|
+
value="node", # Default value
|
21
|
+
style=_INPUT_STYLE,
|
22
|
+
)
|
23
|
+
|
24
|
+
COLUMN_INPUT = dbc.Select(
|
25
|
+
id="search-form-column-input",
|
26
|
+
options=[{"label": "id", "value": "id"}],
|
27
|
+
value="id", # Default value
|
28
|
+
style=_INPUT_STYLE,
|
29
|
+
)
|
30
|
+
|
31
|
+
VALUE_INPUT = dbc.Input(id="search-form-value-input", placeholder="Enter value", type="text", style=_INPUT_STYLE)
|
32
|
+
|
33
|
+
OPERATOR_INPUT = dbc.Select(
|
34
|
+
id="search-form-operator-input",
|
35
|
+
options=[
|
36
|
+
{"label": "=", "value": "="},
|
37
|
+
{"label": "<", "value": "<"},
|
38
|
+
{"label": ">", "value": ">"},
|
39
|
+
{"label": "!=", "value": "!="},
|
40
|
+
],
|
41
|
+
value="=", # Default value
|
42
|
+
style={"width": "60px", "display": "inline-block", "margin": "0 8px"},
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
# Arrange as a sentence
|
47
|
+
SEARCH_FORM_HTML = html.Div(
|
48
|
+
[
|
49
|
+
html.Span("Search ", style=SPAN_TEXT_STYLE),
|
50
|
+
GROUP_INPUT,
|
51
|
+
html.Span(" with ", className="mx-2", style=SPAN_TEXT_STYLE),
|
52
|
+
COLUMN_INPUT,
|
53
|
+
OPERATOR_INPUT,
|
54
|
+
VALUE_INPUT,
|
55
|
+
],
|
56
|
+
style={
|
57
|
+
"display": "flex",
|
58
|
+
"align-items": "center",
|
59
|
+
"justify-content": "center", # Centers items horizontally
|
60
|
+
"padding": "10px",
|
61
|
+
"margin": "0 auto", # Centers the container itself
|
62
|
+
"width": "100%", # Ensures the container takes full width
|
63
|
+
},
|
64
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from dash import dcc, html
|
6
|
+
|
7
|
+
SELECTION_OUTPUT_HEADER_STYLE = {"margin": "20px 0 10px 0"}
|
8
|
+
_SELECTION_OUTPUT_STYLE = {"overflowX": "scroll", "textAlign": "center", "margin": "10px"}
|
9
|
+
|
10
|
+
SELECTION_OUTPUT_HTML = html.Div(
|
11
|
+
dcc.Markdown("Click on a **node** or **edge** to display its attributes.", style=SELECTION_OUTPUT_HEADER_STYLE),
|
12
|
+
id="selection-output",
|
13
|
+
style=_SELECTION_OUTPUT_STYLE,
|
14
|
+
)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
from typing import Any, Literal
|
6
|
+
|
7
|
+
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
|
8
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
9
|
+
from power_grid_model_ds.arrays import BranchArray, NodeArray
|
10
|
+
|
11
|
+
|
12
|
+
def parse_node_array(nodes: NodeArray) -> list[dict[str, Any]]:
|
13
|
+
"""Parse the nodes."""
|
14
|
+
parsed_nodes = []
|
15
|
+
|
16
|
+
with_coords = "x" in nodes.columns and "y" in nodes.columns
|
17
|
+
|
18
|
+
columns = nodes.columns
|
19
|
+
for node in nodes:
|
20
|
+
cyto_elements = {"data": _array_to_dict(node, columns)}
|
21
|
+
cyto_elements["data"]["id"] = str(node.id.item())
|
22
|
+
cyto_elements["data"]["group"] = "node"
|
23
|
+
if with_coords:
|
24
|
+
cyto_elements["position"] = {"x": node.x.item(), "y": -node.y.item()} # invert y-axis for visualization
|
25
|
+
parsed_nodes.append(cyto_elements)
|
26
|
+
return parsed_nodes
|
27
|
+
|
28
|
+
|
29
|
+
def parse_branches(grid: Grid) -> list[dict[str, Any]]:
|
30
|
+
"""Parse the branches."""
|
31
|
+
parsed_branches = []
|
32
|
+
parsed_branches.extend(parse_branch_array(grid.line, "line"))
|
33
|
+
parsed_branches.extend(parse_branch_array(grid.link, "link"))
|
34
|
+
parsed_branches.extend(parse_branch_array(grid.transformer, "transformer"))
|
35
|
+
return parsed_branches
|
36
|
+
|
37
|
+
|
38
|
+
def parse_branch_array(branches: BranchArray, group: Literal["line", "link", "transformer"]) -> list[dict[str, Any]]:
|
39
|
+
"""Parse the branch array."""
|
40
|
+
parsed_branches = []
|
41
|
+
columns = branches.columns
|
42
|
+
for branch in branches:
|
43
|
+
cyto_elements = {"data": _array_to_dict(branch, columns)}
|
44
|
+
cyto_elements["data"].update(
|
45
|
+
{
|
46
|
+
"id": str(branch.id.item()),
|
47
|
+
"source": str(branch.from_node.item()),
|
48
|
+
"target": str(branch.to_node.item()),
|
49
|
+
"group": group,
|
50
|
+
}
|
51
|
+
)
|
52
|
+
parsed_branches.append(cyto_elements)
|
53
|
+
return parsed_branches
|
54
|
+
|
55
|
+
|
56
|
+
def _array_to_dict(array_record: FancyArray, columns: list[str]) -> dict[str, Any]:
|
57
|
+
"""Stringify the record (required by Dash)."""
|
58
|
+
return dict(zip(columns, array_record.tolist().pop()))
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
try:
|
6
|
+
from power_grid_model_ds._core.visualizer.app import visualize
|
7
|
+
except ImportError as error:
|
8
|
+
raise ImportError(
|
9
|
+
"Missing dependencies for visualizer: install with 'pip install power-grid-model-ds[visualizer]'"
|
10
|
+
) from error
|
11
|
+
|
12
|
+
__all__ = ["visualize"]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: power-grid-model-ds
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0
|
4
4
|
Summary: Power Grid Model extension which provides a grid data structure for simulation and analysis
|
5
5
|
Author-email: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
6
6
|
License: MPL-2.0
|
@@ -35,6 +35,13 @@ Requires-Dist: ruff>=0.3.5; extra == "dev"
|
|
35
35
|
Requires-Dist: isort>=5.13.2; extra == "dev"
|
36
36
|
Requires-Dist: mypy>=1.9.0; extra == "dev"
|
37
37
|
Requires-Dist: pre-commit>=4; extra == "dev"
|
38
|
+
Requires-Dist: dash>=3.0.0; extra == "dev"
|
39
|
+
Requires-Dist: dash-bootstrap-components>=2.0.0; extra == "dev"
|
40
|
+
Requires-Dist: dash-cytoscape>=1.0.2; extra == "dev"
|
41
|
+
Provides-Extra: visualizer
|
42
|
+
Requires-Dist: dash>=3.0.0; extra == "visualizer"
|
43
|
+
Requires-Dist: dash-bootstrap-components>=2.0.0; extra == "visualizer"
|
44
|
+
Requires-Dist: dash-cytoscape>=1.0.2; extra == "visualizer"
|
38
45
|
Provides-Extra: doc
|
39
46
|
Requires-Dist: sphinx; extra == "doc"
|
40
47
|
Requires-Dist: myst-nb; extra == "doc"
|
@@ -6,6 +6,7 @@ power_grid_model_ds/errors.py,sha256=J07OFpLjh2Mg7aBnSTV47R5gCeqjNMkpL3hgNCy0Tx0
|
|
6
6
|
power_grid_model_ds/fancypy.py,sha256=-ZRnfiBWwrTMHEJKaHyQ8MkQ0LmLZVytfphCuVW7CPk,474
|
7
7
|
power_grid_model_ds/generators.py,sha256=suQCHIzLOXjJj3tLn1-vuzR9m6V-bhFZQWEukTp5tgI,709
|
8
8
|
power_grid_model_ds/graph_models.py,sha256=so5niaXJqtuL0hUmtSPxV6Bven5c8DfAlF3MM4I1d28,348
|
9
|
+
power_grid_model_ds/visualizer.py,sha256=W9GfWuM-CrDTnniCj1y-lxSt6IQkLJViW5aPC4syauw,411
|
9
10
|
power_grid_model_ds/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
11
|
power_grid_model_ds/_core/fancypy.py,sha256=MVnt6gRe2civTQBY17Dtp_wyqARpy4jP1NCpOoNW3nc,2672
|
11
12
|
power_grid_model_ds/_core/load_flow.py,sha256=2XKWHwhwvDa6hRgmlrv9XAKIZmTac4BGfpu9cZ1f_to,5832
|
@@ -42,22 +43,39 @@ power_grid_model_ds/_core/model/dtypes/sensors.py,sha256=U8HqvyH3fV348m4QjWy8IEY
|
|
42
43
|
power_grid_model_ds/_core/model/enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
44
|
power_grid_model_ds/_core/model/enums/nodes.py,sha256=UuJsWFasKREhVGbsUwGg5A-LvHnGTc4j636A_B4N9A4,356
|
44
45
|
power_grid_model_ds/_core/model/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
-
power_grid_model_ds/_core/model/graphs/container.py,sha256=
|
46
|
+
power_grid_model_ds/_core/model/graphs/container.py,sha256=ZsE6Wr7DvVcYJYjvBszqnHI0QF9cFlHFwTfTAQnWYOA,6602
|
46
47
|
power_grid_model_ds/_core/model/graphs/errors.py,sha256=-9fAsCqpKM0-lCM-tO2fVot9hqZBJa8WCxxqWEH_z6k,479
|
47
48
|
power_grid_model_ds/_core/model/graphs/models/__init__.py,sha256=xwhMajQKIAfL1R3cq2sMNXMWtqfI5WGBi6-P4clyTes,262
|
48
49
|
power_grid_model_ds/_core/model/graphs/models/_rustworkx_search.py,sha256=TqRR22TPx8XjRLukQxK8R8FjZvJ60CttDFctbISSbOM,2979
|
49
|
-
power_grid_model_ds/_core/model/graphs/models/base.py,sha256=
|
50
|
-
power_grid_model_ds/_core/model/graphs/models/rustworkx.py,sha256=
|
50
|
+
power_grid_model_ds/_core/model/graphs/models/base.py,sha256=KiPLA80DNzIALmaxxcDgaigUAUAe-W3p8nEYlxETAG0,17560
|
51
|
+
power_grid_model_ds/_core/model/graphs/models/rustworkx.py,sha256=jccM7MUhNqH2215mk_X8ihQ-Q8IkjjRhUk4jSruaxy8,6158
|
51
52
|
power_grid_model_ds/_core/model/grids/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
53
|
power_grid_model_ds/_core/model/grids/_text_sources.py,sha256=3CWn0zjaUkFhrnSEIqAas52xeW0EX3EWXIXyo8XSQgw,4229
|
53
|
-
power_grid_model_ds/_core/model/grids/base.py,sha256=
|
54
|
-
power_grid_model_ds/_core/model/grids/helpers.py,sha256=
|
54
|
+
power_grid_model_ds/_core/model/grids/base.py,sha256=8AQdqPuVLBjOKE2JQvlrQErq8NcCowot3SUlJvcmYvA,16592
|
55
|
+
power_grid_model_ds/_core/model/grids/helpers.py,sha256=g3xZ-asHqTnEbnzbla4UiXw47k56ke3u9bD1IQRbiRc,4235
|
55
56
|
power_grid_model_ds/_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
57
|
power_grid_model_ds/_core/utils/misc.py,sha256=aAdlXjyKN6_T4b_CQZ879TbkbDZW4TwBUsxTpkwvymU,1228
|
57
58
|
power_grid_model_ds/_core/utils/pickle.py,sha256=LGeTc7nu9RY1noOzLJzYaSHSWIgqzHy2xhmueKGuipc,1445
|
58
59
|
power_grid_model_ds/_core/utils/zip.py,sha256=9RtJYhjlgNZOtxr4iI-CpsjT1axw5kCCqprfTjaIsiI,2197
|
59
|
-
power_grid_model_ds
|
60
|
-
power_grid_model_ds
|
61
|
-
power_grid_model_ds
|
62
|
-
power_grid_model_ds
|
63
|
-
power_grid_model_ds
|
60
|
+
power_grid_model_ds/_core/visualizer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
61
|
+
power_grid_model_ds/_core/visualizer/app.py,sha256=uTtbHCRTQgLnfmJgwRmRSA_2kBHO2PZHD7fn2osGJSE,3340
|
62
|
+
power_grid_model_ds/_core/visualizer/parsers.py,sha256=WPotNkHkDNOia6J7YE4F4rWXgKW2TDiBaU3TbolEjLg,2165
|
63
|
+
power_grid_model_ds/_core/visualizer/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
|
+
power_grid_model_ds/_core/visualizer/callbacks/element_scaling.py,sha256=l8PPSqglytqBQgZ4yyNVx3wMYLF8Fcx8ku4skbGNZLM,1136
|
65
|
+
power_grid_model_ds/_core/visualizer/callbacks/element_selection.py,sha256=Teaeq0atd2rD_edaW0u_N-YCnaf6MQyC8WsgnEhQczk,963
|
66
|
+
power_grid_model_ds/_core/visualizer/callbacks/layout_dropdown.py,sha256=ekO2imgbMrV_R8HpCicbOSjFO2m1tjsj-zF4RHih5RA,424
|
67
|
+
power_grid_model_ds/_core/visualizer/callbacks/search_form.py,sha256=fi6azBaKIgV63vLCp0wrIB5mZ3zSWn2Jhc5VdR-Om44,2098
|
68
|
+
power_grid_model_ds/_core/visualizer/layout/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
69
|
+
power_grid_model_ds/_core/visualizer/layout/colors.py,sha256=tt-2LvkClYvQ5ozL2UCX5q63AbXaXXo7kK2yLAzeCVw,455
|
70
|
+
power_grid_model_ds/_core/visualizer/layout/cytoscape_config.py,sha256=_vnncYWIRERIgwqJY8aBXUYoXYh3l6Vd6G4voN3kTVQ,1471
|
71
|
+
power_grid_model_ds/_core/visualizer/layout/cytoscape_html.py,sha256=apiU1oOSrT30c2bYADUNNOGcAkG4rkqV5UtSLVWFMfU,1009
|
72
|
+
power_grid_model_ds/_core/visualizer/layout/cytoscape_styling.py,sha256=o5Rh8uNCyt9YxODkNH76sx_wKs8aCBrCMVGCFBTtegQ,3288
|
73
|
+
power_grid_model_ds/_core/visualizer/layout/header.py,sha256=el-5SlWIPeEbZZLbi3vYz9CjanTlhuChVfqkktLCHjw,1165
|
74
|
+
power_grid_model_ds/_core/visualizer/layout/legenda.py,sha256=8JTrMRgaYRaSIXAEoM0b3PgX7AYvW0_fHuYg8nY4y0g,2161
|
75
|
+
power_grid_model_ds/_core/visualizer/layout/search_form.py,sha256=4LPU6MjD3QEIeBEOOWBvV5-GyLMqNYWyKcUa_oWScCs,2002
|
76
|
+
power_grid_model_ds/_core/visualizer/layout/selection_output.py,sha256=fuPAdG5XmTj2E0oQ8w_tp9PGbB8bJ5fguteLtnM1EDg,534
|
77
|
+
power_grid_model_ds-1.3.0.dist-info/licenses/LICENSE,sha256=GpbnG1pNl-ORtSP6dmHeiZvwy36UFli6MEZy1XlmBqo,14906
|
78
|
+
power_grid_model_ds-1.3.0.dist-info/METADATA,sha256=zSFNY1sl-VQnnEIsIDsUsyPYdF6IXdQlyPShpU5TF8c,9212
|
79
|
+
power_grid_model_ds-1.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
80
|
+
power_grid_model_ds-1.3.0.dist-info/top_level.txt,sha256=nJa103Eqvm5TESYEKPFVImfLg_ugGOBznikrM-rZQZg,20
|
81
|
+
power_grid_model_ds-1.3.0.dist-info/RECORD,,
|
File without changes
|
{power_grid_model_ds-1.2.6.dist-info → power_grid_model_ds-1.3.0.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|