power-grid-model-ds 1.2.5__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.
Files changed (28) hide show
  1. power_grid_model_ds/_core/model/graphs/container.py +48 -28
  2. power_grid_model_ds/_core/model/graphs/models/base.py +38 -15
  3. power_grid_model_ds/_core/model/graphs/models/rustworkx.py +12 -5
  4. power_grid_model_ds/_core/model/grids/base.py +5 -5
  5. power_grid_model_ds/_core/model/grids/helpers.py +3 -2
  6. power_grid_model_ds/_core/visualizer/__init__.py +0 -0
  7. power_grid_model_ds/_core/visualizer/app.py +90 -0
  8. power_grid_model_ds/_core/visualizer/callbacks/__init__.py +0 -0
  9. power_grid_model_ds/_core/visualizer/callbacks/element_scaling.py +37 -0
  10. power_grid_model_ds/_core/visualizer/callbacks/element_selection.py +33 -0
  11. power_grid_model_ds/_core/visualizer/callbacks/layout_dropdown.py +11 -0
  12. power_grid_model_ds/_core/visualizer/callbacks/search_form.py +60 -0
  13. power_grid_model_ds/_core/visualizer/layout/__init__.py +0 -0
  14. power_grid_model_ds/_core/visualizer/layout/colors.py +17 -0
  15. power_grid_model_ds/_core/visualizer/layout/cytoscape_config.py +54 -0
  16. power_grid_model_ds/_core/visualizer/layout/cytoscape_html.py +30 -0
  17. power_grid_model_ds/_core/visualizer/layout/cytoscape_styling.py +115 -0
  18. power_grid_model_ds/_core/visualizer/layout/header.py +31 -0
  19. power_grid_model_ds/_core/visualizer/layout/legenda.py +41 -0
  20. power_grid_model_ds/_core/visualizer/layout/search_form.py +64 -0
  21. power_grid_model_ds/_core/visualizer/layout/selection_output.py +14 -0
  22. power_grid_model_ds/_core/visualizer/parsers.py +58 -0
  23. power_grid_model_ds/visualizer.py +12 -0
  24. {power_grid_model_ds-1.2.5.dist-info → power_grid_model_ds-1.3.0.dist-info}/METADATA +8 -1
  25. {power_grid_model_ds-1.2.5.dist-info → power_grid_model_ds-1.3.0.dist-info}/RECORD +28 -10
  26. {power_grid_model_ds-1.2.5.dist-info → power_grid_model_ds-1.3.0.dist-info}/WHEEL +0 -0
  27. {power_grid_model_ds-1.2.5.dist-info → power_grid_model_ds-1.3.0.dist-info}/licenses/LICENSE +0 -0
  28. {power_grid_model_ds-1.2.5.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: # pragma: no cover
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.add_branch_array(branch_array=branch)
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
- for field in self.graph_attributes:
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.add_branch3_array(branch3_array=branch)
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.add_branch(array)
168
+ self.add_branch_array(array)
147
169
  if isinstance(array, Branch3Array):
148
- self.add_branch3(array)
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: # pragma: no cover
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 node in node_array:
117
- self.add_node(ext_node_id=node.id.item(), raise_on_fail=raise_on_fail)
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
- for branch in branch_array:
166
- if self._branch_is_relevant(branch):
167
- self.add_branch(branch.from_node.item(), branch.to_node.item())
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: NDArray[np.int32]) -> list[list[int]]:
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
- internal_components = self._get_components(substation_nodes=self._externals_to_internals(substation_nodes))
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, substation_nodes: list[int]) -> list[list[int]]: ...
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, substation_nodes: list[int]) -> list[list[int]]:
88
- no_os_graph = self._graph.copy()
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
- for row in array:
204
- # pylint: disable=protected-access
205
- self.graphs._append(row)
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.add_branch(branch=branch)
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.add_node(node=node)
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).id
49
- components = grid.graphs.active_graph.get_components(feeder_node_ids)
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"
@@ -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
@@ -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.2.5
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=9hQLkrUMYnCIFj1gb2aLNXt0XhjGn_tlRv83RmJ9nxE,5830
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=GJ1ZExVBhpcezzWZqYAOeDSn9ePWvhniGFjR-qL5aBY,16730
50
- power_grid_model_ds/_core/model/graphs/models/rustworkx.py,sha256=a7PrDgge2KzMbeTQhQTE-KRPdKNj5TJvfYkzYNT8FtM,5730
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=knOYFl5snBFjccpbDPgWMrgCaFYN6JS74L9O15_snAI,16599
54
- power_grid_model_ds/_core/model/grids/helpers.py,sha256=VmIQ5a8oBMWW-VT5HOJk4iRRfEyO_OKc9LqKG0eVpSs,4174
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-1.2.5.dist-info/licenses/LICENSE,sha256=GpbnG1pNl-ORtSP6dmHeiZvwy36UFli6MEZy1XlmBqo,14906
60
- power_grid_model_ds-1.2.5.dist-info/METADATA,sha256=09WYlZJolaW81n19FZX8MulkW94lJLq9IuWmONLu5yk,8844
61
- power_grid_model_ds-1.2.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
62
- power_grid_model_ds-1.2.5.dist-info/top_level.txt,sha256=nJa103Eqvm5TESYEKPFVImfLg_ugGOBznikrM-rZQZg,20
63
- power_grid_model_ds-1.2.5.dist-info/RECORD,,
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,,