epyt-flow 0.10.0__py3-none-any.whl → 0.12.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.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/benchmarks/gecco_water_quality.py +2 -2
- epyt_flow/data/benchmarks/leakdb.py +40 -5
- epyt_flow/data/benchmarks/water_usage.py +4 -3
- epyt_flow/data/networks.py +27 -14
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +11 -13
- epyt_flow/rest_api/scenario/control_handlers.py +118 -0
- epyt_flow/rest_api/scenario/event_handlers.py +114 -1
- epyt_flow/rest_api/scenario/handlers.py +33 -0
- epyt_flow/rest_api/server.py +14 -2
- epyt_flow/serialization.py +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/backend/__init__.py +1 -0
- epyt_flow/simulation/backend/my_epyt.py +1056 -0
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/quality_events.py +3 -1
- epyt_flow/simulation/scada/scada_data.py +716 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +645 -119
- epyt_flow/simulation/sensor_config.py +18 -2
- epyt_flow/topology.py +24 -7
- epyt_flow/uncertainty/model_uncertainty.py +80 -62
- epyt_flow/uncertainty/sensor_noise.py +15 -4
- epyt_flow/uncertainty/uncertainties.py +71 -18
- epyt_flow/uncertainty/utils.py +40 -13
- epyt_flow/utils.py +45 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/visualization/scenario_visualizer.py +1240 -0
- epyt_flow/visualization/visualization_utils.py +738 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/METADATA +15 -4
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +35 -36
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/WHEEL +1 -1
- epyt_flow/gym/control_gyms.py +0 -47
- epyt_flow/metrics.py +0 -466
- epyt_flow/models/__init__.py +0 -2
- epyt_flow/models/event_detector.py +0 -31
- epyt_flow/models/sensor_interpolation_detector.py +0 -118
- epyt_flow/simulation/scada/advanced_control.py +0 -138
- epyt_flow/simulation/scenario_visualizer.py +0 -1307
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a class for visualizing scenarios.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Union, List, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from matplotlib.animation import FuncAnimation
|
|
10
|
+
import matplotlib as mpl
|
|
11
|
+
import networkx.drawing.nx_pylab as nxp
|
|
12
|
+
from svgpath2mpl import parse_path
|
|
13
|
+
|
|
14
|
+
from ..simulation.scenario_simulator import ScenarioSimulator
|
|
15
|
+
from ..simulation.scada.scada_data import ScadaData
|
|
16
|
+
from ..visualization import JunctionObject, EdgeObject, ColorScheme, \
|
|
17
|
+
epyt_flow_colors
|
|
18
|
+
|
|
19
|
+
PUMP_PATH = ('M 202.5 93 A 41.5 42 0 0 0 161 135 A 41.5 42 0 0 0 202.5 177 A '
|
|
20
|
+
'41.5 42 0 0 0 244 135 A 41.5 42 0 0 0 241.94922 122 L 278 122 '
|
|
21
|
+
'L 278 93 L 203 93 L 203 93.011719 A 41.5 42 0 0 0 202.5 93 z')
|
|
22
|
+
RESERVOIR_PATH = ('M 325 41 A 43 24.5 0 0 0 282.05664 65 L 282 65 L 282 65.5 '
|
|
23
|
+
'L 282 163 L 282 168 L 282 216 L 305 216 L 305 168 L 345 '
|
|
24
|
+
'168 L 345 216 L 368 216 L 368 168 L 368 163 L 368 65.5 L '
|
|
25
|
+
'368 65 L 367.98047 65 A 43 24.5 0 0 0 325 41 z')
|
|
26
|
+
TANK_PATH = ('M 325 41 A 43 24.5 0 0 0 282.05664 65 L 282 65 L 282 65.5 L 282 '
|
|
27
|
+
'185 L 368 185 L 368 65.5 L 368 65 L 367.98047 65 A 43 24.5 0 0'
|
|
28
|
+
' 0 325 41 z')
|
|
29
|
+
VALVE_PATH = ('M 9.9999064 9.9999064 L 9.9999064 110 L 69.999862 59.999955 L '
|
|
30
|
+
'9.9999064 9.9999064 z M 69.999862 59.999955 L 129.99982 110 L '
|
|
31
|
+
'129.99982 9.9999064 L 69.999862 59.999955 z')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Marker:
|
|
35
|
+
"""
|
|
36
|
+
The Marker class provides svg representations of hydraulic components
|
|
37
|
+
(pump, reservoir, tank and valve), which are loaded from their respective
|
|
38
|
+
svg paths and transformed into
|
|
39
|
+
`~matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
40
|
+
objects in order to be used with the matplotlib library.
|
|
41
|
+
|
|
42
|
+
Attributes
|
|
43
|
+
----------
|
|
44
|
+
pump : `matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
45
|
+
Marker for the pump, loaded from PUMP_PATH.
|
|
46
|
+
reservoir : `matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
47
|
+
Marker for the reservoir, loaded from RESERVOIR_PATH.
|
|
48
|
+
tank : `matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
49
|
+
Marker for the tank, loaded from TANK_PATH.
|
|
50
|
+
valve : `matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
51
|
+
Marker for the valve, loaded from VALVE_PATH.
|
|
52
|
+
|
|
53
|
+
Methods
|
|
54
|
+
-------
|
|
55
|
+
__marker_from_path(path, scale_p=1)
|
|
56
|
+
Loads and applies transformations to the marker shape from the given
|
|
57
|
+
path.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
"""
|
|
62
|
+
Initializes the Marker class and assigns
|
|
63
|
+
`matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
64
|
+
markers for pump, reservoir, tank, and valve components.
|
|
65
|
+
"""
|
|
66
|
+
self.pump = self.__marker_from_path(PUMP_PATH, 2)
|
|
67
|
+
self.reservoir = self.__marker_from_path(RESERVOIR_PATH)
|
|
68
|
+
self.tank = self.__marker_from_path(TANK_PATH)
|
|
69
|
+
self.valve = self.__marker_from_path(VALVE_PATH)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def __marker_from_path(path: str, scale_p: int = 1) -> mpl.path.Path:
|
|
73
|
+
"""
|
|
74
|
+
Loads the marker from the specified path and adjusts it representation
|
|
75
|
+
by aligning, rotating and scaling it.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
path : `str`
|
|
80
|
+
The svg path describing the marker shape.
|
|
81
|
+
scale_p : `float`, optional
|
|
82
|
+
Scaling factor for the marker (default is 1).
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
`matplotlib.path.Path <https://matplotlib.org/stable/api/path_api.html#matplotlib.path.Path>`_
|
|
87
|
+
The transformed marker object after loading and adjusting it.
|
|
88
|
+
"""
|
|
89
|
+
marker_tmp = parse_path(path)
|
|
90
|
+
marker_tmp.vertices -= marker_tmp.vertices.mean(axis=0)
|
|
91
|
+
marker_tmp = marker_tmp.transformed(
|
|
92
|
+
mpl.transforms.Affine2D().rotate_deg(180))
|
|
93
|
+
marker_tmp = marker_tmp.transformed(
|
|
94
|
+
mpl.transforms.Affine2D().scale(-scale_p, scale_p))
|
|
95
|
+
return marker_tmp
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ScenarioVisualizer:
|
|
99
|
+
"""
|
|
100
|
+
This class provides the necessary function to generate visualizations in
|
|
101
|
+
the form of plots or animations from water network data.
|
|
102
|
+
|
|
103
|
+
Given a :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
|
|
104
|
+
object, this class provides the necessary functions to plot the network
|
|
105
|
+
topology and to color hydraulic elements according to simulation data. The
|
|
106
|
+
resulting plot can then be displayed or saved.
|
|
107
|
+
|
|
108
|
+
Attributes
|
|
109
|
+
----------
|
|
110
|
+
__scenario : :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
|
|
111
|
+
ScenarioSimulator object containing the network topology and
|
|
112
|
+
configurations to obtain the simulation data which should be displayed.
|
|
113
|
+
fig : `matplotlib.pyplot.Figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure>`_ or None
|
|
114
|
+
Figure object used for plotting, created and customized by calling the
|
|
115
|
+
methods of this class, initialized as None.
|
|
116
|
+
ax : `~matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html#matplotlib-axes-axes>`_ or None
|
|
117
|
+
The axes for plotting, initialized as None.
|
|
118
|
+
scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or None
|
|
119
|
+
SCADA data created by the ScenarioSimulator object, initialized as
|
|
120
|
+
None.
|
|
121
|
+
topology : :class:`~epyt_flow.topology.NetworkTopology`
|
|
122
|
+
Topology object retrieved from the scenario, containing the structure
|
|
123
|
+
of the water distribution network.
|
|
124
|
+
color_scheme : :class:`~epyt_flow.visualization.visualization_utils.ColorScheme`
|
|
125
|
+
Contains the selected ColorScheme for visualization.
|
|
126
|
+
pipe_parameters : :class:`~epyt_flow.visualization.visualization_utils.EdgeObject`
|
|
127
|
+
Class contains parameters for visualizing pipes in the correct format
|
|
128
|
+
for drawing.
|
|
129
|
+
junction_parameters : :class:`~epyt_flow.visualization.visualization_utils.JunctionObject`
|
|
130
|
+
Class contains parameters for visualizing junctions in the correct
|
|
131
|
+
format for drawing.
|
|
132
|
+
tank_parameters : :class:`~epyt_flow.visualization.visualization_utils.JunctionObject`
|
|
133
|
+
Class contains parameters for visualizing tanks in the correct format
|
|
134
|
+
for drawing.
|
|
135
|
+
reservoir_parameters : :class:`~epyt_flow.visualization.visualization_utils.JunctionObject`
|
|
136
|
+
Class contains parameters for visualizing reservoirs in the correct
|
|
137
|
+
format for
|
|
138
|
+
drawing.
|
|
139
|
+
valve_parameters : :class:`~epyt_flow.visualization.visualization_utils.JunctionObject`
|
|
140
|
+
Class contains parameters for visualizing valves in the correct format
|
|
141
|
+
for drawing.
|
|
142
|
+
pump_parameters : :class:`~epyt_flow.visualization.visualization_utils.JunctionObject`
|
|
143
|
+
Class contains parameters for visualizing pumps in the correct format
|
|
144
|
+
for drawing.
|
|
145
|
+
colorbars : `dict`
|
|
146
|
+
A dictionary containing the necessary data for drawing the required
|
|
147
|
+
colorbars.
|
|
148
|
+
labels : `dict`
|
|
149
|
+
A dictionary containing components as keys and drawing information for
|
|
150
|
+
the labels as values.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, scenario: ScenarioSimulator,
|
|
155
|
+
color_scheme: ColorScheme = epyt_flow_colors) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Initializes the class with a given scenario, sets up the topology,
|
|
158
|
+
SCADA data, and the classes containing parameters for visualizing
|
|
159
|
+
various hydraulic components (pipes, junctions, tanks, reservoirs,
|
|
160
|
+
valves, and pumps).
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
scenario : :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
|
|
165
|
+
An instance of the `ScenarioSimulator` class, used to simulate and
|
|
166
|
+
retrieve the system topology.
|
|
167
|
+
color_scheme : :class:`~epyt_flow.visualization.visualization_utils.ColorScheme`
|
|
168
|
+
Contains the selected ColorScheme for visualization. Default is
|
|
169
|
+
EPYT_FLOW.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
TypeError
|
|
174
|
+
If `scenario` is not an instance of
|
|
175
|
+
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
176
|
+
"""
|
|
177
|
+
if not isinstance(scenario, ScenarioSimulator):
|
|
178
|
+
raise TypeError("'scenario' must be an instance of " +
|
|
179
|
+
"'epyt_flow.simulation.ScenarioSimulator' " +
|
|
180
|
+
f"but not of '{type(scenario)}'")
|
|
181
|
+
|
|
182
|
+
self.__scenario = scenario
|
|
183
|
+
self.fig = None
|
|
184
|
+
self.ax = None
|
|
185
|
+
self.scada_data = None
|
|
186
|
+
markers = Marker()
|
|
187
|
+
self.topology = self.__scenario.get_topology()
|
|
188
|
+
|
|
189
|
+
pos_dict = {x: self.topology.get_node_info(x)['coord'] for x in
|
|
190
|
+
self.topology.get_all_nodes()}
|
|
191
|
+
|
|
192
|
+
self.color_scheme = color_scheme
|
|
193
|
+
|
|
194
|
+
self.pipe_parameters = EdgeObject(
|
|
195
|
+
[x[1] for x in self.topology.get_all_links()], pos_dict,
|
|
196
|
+
self.color_scheme.pipe_color)
|
|
197
|
+
self.junction_parameters = JunctionObject(
|
|
198
|
+
self.topology.get_all_junctions(), pos_dict,
|
|
199
|
+
node_color=self.color_scheme.node_color)
|
|
200
|
+
self.tank_parameters = JunctionObject(self.topology.get_all_tanks(),
|
|
201
|
+
pos_dict, node_size=100,
|
|
202
|
+
node_shape=markers.tank,
|
|
203
|
+
node_color=self.color_scheme.tank_color)
|
|
204
|
+
self.reservoir_parameters = JunctionObject(
|
|
205
|
+
self.topology.get_all_reservoirs(), pos_dict, node_size=100,
|
|
206
|
+
node_shape=markers.reservoir,
|
|
207
|
+
node_color=self.color_scheme.reservoir_color)
|
|
208
|
+
self.valve_parameters = JunctionObject(self.topology.get_all_valves(),
|
|
209
|
+
self._get_midpoints(
|
|
210
|
+
self.topology.get_all_valves()),
|
|
211
|
+
node_size=50,
|
|
212
|
+
node_shape=markers.valve,
|
|
213
|
+
node_color=self.color_scheme.valve_color)
|
|
214
|
+
self.pump_parameters = JunctionObject(self.topology.get_all_pumps(),
|
|
215
|
+
self._get_midpoints(
|
|
216
|
+
self.topology.get_all_pumps()),
|
|
217
|
+
node_size=50,
|
|
218
|
+
node_shape=markers.pump,
|
|
219
|
+
node_color=self.color_scheme.pump_color)
|
|
220
|
+
|
|
221
|
+
self.colorbars = {}
|
|
222
|
+
self.labels = {}
|
|
223
|
+
self.masks = {}
|
|
224
|
+
|
|
225
|
+
def _get_midpoints(self, elements: List[str]) -> dict[
|
|
226
|
+
str, tuple[float, float]]:
|
|
227
|
+
"""
|
|
228
|
+
Computes and returns the midpoints for drawing either valves or pumps
|
|
229
|
+
in a water distribution network.
|
|
230
|
+
|
|
231
|
+
For each element ID in the provided list, the method calculates the
|
|
232
|
+
midpoint between its start and end nodes' coordinates.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
elements : `list[str]`
|
|
237
|
+
A list of element IDs (e.g., pump IDs, valve IDs) for which to
|
|
238
|
+
compute the midpoints.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
elements_dict : `dict`
|
|
243
|
+
A dictionary where the keys are element IDs and the values are the
|
|
244
|
+
corresponding midpoints, represented as 2D coordinates [x, y].
|
|
245
|
+
"""
|
|
246
|
+
elements_pos_dict = {}
|
|
247
|
+
for element in elements:
|
|
248
|
+
if element in self.topology.pumps:
|
|
249
|
+
start_node, end_node = self.topology.get_pump_info(element)[
|
|
250
|
+
'end_points']
|
|
251
|
+
elif element in self.topology.valves:
|
|
252
|
+
start_node, end_node = self.topology.get_valve_info(element)[
|
|
253
|
+
'end_points']
|
|
254
|
+
else:
|
|
255
|
+
raise ValueError(f"Unknown element '{element}'")
|
|
256
|
+
start_pos = self.topology.get_node_info(start_node)['coord']
|
|
257
|
+
end_pos = self.topology.get_node_info(end_node)['coord']
|
|
258
|
+
pos = [(start_pos[0] + end_pos[0]) / 2,
|
|
259
|
+
(start_pos[1] + end_pos[1]) / 2]
|
|
260
|
+
elements_pos_dict[element] = pos
|
|
261
|
+
return elements_pos_dict
|
|
262
|
+
|
|
263
|
+
def _get_next_frame(self, frame_number: int) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Draws the next frame of a water distribution network animation.
|
|
266
|
+
|
|
267
|
+
This method updates a visualization animation with the hydraulic
|
|
268
|
+
components colored according to the scada data corresponding to the
|
|
269
|
+
current frame.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
frame_number : `int`
|
|
274
|
+
The current frame number used to retrieve the data corresponding to
|
|
275
|
+
that frame
|
|
276
|
+
"""
|
|
277
|
+
plt.clf()
|
|
278
|
+
self.ax = self.fig.add_subplot(111)
|
|
279
|
+
self.ax.axis('off')
|
|
280
|
+
|
|
281
|
+
nxp.draw_networkx_edges(self.topology, ax=self.ax,
|
|
282
|
+
label='Pipes',
|
|
283
|
+
**self.pipe_parameters.get_frame(frame_number))
|
|
284
|
+
nxp.draw_networkx_nodes(self.topology, ax=self.ax,
|
|
285
|
+
label='Junctions',
|
|
286
|
+
**self.junction_parameters.get_frame(
|
|
287
|
+
frame_number))
|
|
288
|
+
nxp.draw_networkx_nodes(self.topology, ax=self.ax,
|
|
289
|
+
label='Tanks',
|
|
290
|
+
**self.tank_parameters.get_frame(frame_number))
|
|
291
|
+
nxp.draw_networkx_nodes(self.topology, ax=self.ax,
|
|
292
|
+
label='Reservoirs',
|
|
293
|
+
**self.reservoir_parameters.get_frame(
|
|
294
|
+
frame_number))
|
|
295
|
+
nxp.draw_networkx_nodes(
|
|
296
|
+
self.topology, ax=self.ax,
|
|
297
|
+
label='Valves', **self.valve_parameters.get_frame(frame_number))
|
|
298
|
+
nxp.draw_networkx_nodes(
|
|
299
|
+
self.topology,
|
|
300
|
+
ax=self.ax, label='Pumps',
|
|
301
|
+
**self.pump_parameters.get_frame(frame_number))
|
|
302
|
+
|
|
303
|
+
for key, mask in self.masks.items():
|
|
304
|
+
if key == 'nodes':
|
|
305
|
+
nxp.draw_networkx_nodes(self.topology, ax=self.ax,
|
|
306
|
+
**self.junction_parameters.get_frame_mask(
|
|
307
|
+
mask,
|
|
308
|
+
self.color_scheme.node_color))
|
|
309
|
+
if key == 'pumps':
|
|
310
|
+
nxp.draw_networkx_nodes(
|
|
311
|
+
self.topology,
|
|
312
|
+
ax=self.ax,
|
|
313
|
+
**self.pump_parameters.get_frame_mask(mask,
|
|
314
|
+
self.color_scheme.pump_color))
|
|
315
|
+
if key == 'links':
|
|
316
|
+
nxp.draw_networkx_edges(self.topology, ax=self.ax,
|
|
317
|
+
**self.pipe_parameters.get_frame_mask(
|
|
318
|
+
frame_number,
|
|
319
|
+
self.color_scheme.pipe_color))
|
|
320
|
+
if key == 'tanks':
|
|
321
|
+
nxp.draw_networkx_nodes(self.topology, ax=self.ax,
|
|
322
|
+
**self.tank_parameters.get_frame_mask(
|
|
323
|
+
mask,
|
|
324
|
+
self.color_scheme.tank_color))
|
|
325
|
+
if key == 'valves':
|
|
326
|
+
nxp.draw_networkx_nodes(
|
|
327
|
+
self.topology, ax=self.ax,
|
|
328
|
+
**self.valve_parameters.get_frame_mask(mask,
|
|
329
|
+
self.color_scheme.valve_color))
|
|
330
|
+
|
|
331
|
+
self._draw_labels()
|
|
332
|
+
self.ax.legend(fontsize=6)
|
|
333
|
+
|
|
334
|
+
for colorbar_stats in self.colorbars.values():
|
|
335
|
+
self.fig.colorbar(ax=self.ax, **colorbar_stats)
|
|
336
|
+
|
|
337
|
+
def _interpolate_frames(self, num_inter_frames: int):
|
|
338
|
+
"""
|
|
339
|
+
Interpolates intermediate values between frames using cubic spline
|
|
340
|
+
interpolation for smoother animation.
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
num_inter_frames : `int`
|
|
345
|
+
Number of total frames after interpolation.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
num_inter_frames : `int`
|
|
350
|
+
Number of total frames after interpolation.
|
|
351
|
+
"""
|
|
352
|
+
for node_source in [self.junction_parameters, self.tank_parameters,
|
|
353
|
+
self.reservoir_parameters, self.valve_parameters,
|
|
354
|
+
self.pump_parameters]:
|
|
355
|
+
node_source.interpolate(num_inter_frames)
|
|
356
|
+
self.pipe_parameters.interpolate(num_inter_frames)
|
|
357
|
+
|
|
358
|
+
return num_inter_frames
|
|
359
|
+
|
|
360
|
+
def _draw_labels(self):
|
|
361
|
+
"""
|
|
362
|
+
Method accesses the dict `self.labels` and draws all generated labels
|
|
363
|
+
within.
|
|
364
|
+
"""
|
|
365
|
+
for k, v in self.labels.items():
|
|
366
|
+
if k in ['pipes']:
|
|
367
|
+
nxp.draw_networkx_edge_labels(self.topology, ax=self.ax, **v)
|
|
368
|
+
continue
|
|
369
|
+
nxp.draw_networkx_labels(self.topology, ax=self.ax, **v)
|
|
370
|
+
|
|
371
|
+
def _get_sensor_config_nodes_and_links(self):
|
|
372
|
+
"""
|
|
373
|
+
Iterates through the sensor config and collects all nodes and links
|
|
374
|
+
within, that have a sensor attached.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
highlighted_links : `list`
|
|
379
|
+
List of all links with sensors.
|
|
380
|
+
highlighted_nodes : `list`
|
|
381
|
+
List of all nodes with sensors.
|
|
382
|
+
"""
|
|
383
|
+
highlighted_nodes = []
|
|
384
|
+
highlighted_links = []
|
|
385
|
+
|
|
386
|
+
sensor_config = self.__scenario.sensor_config
|
|
387
|
+
highlighted_nodes += (sensor_config.pressure_sensors
|
|
388
|
+
+ sensor_config.demand_sensors
|
|
389
|
+
+ sensor_config.quality_node_sensors)
|
|
390
|
+
highlighted_links += (sensor_config.flow_sensors
|
|
391
|
+
+ sensor_config.quality_link_sensors)
|
|
392
|
+
return highlighted_nodes, highlighted_links
|
|
393
|
+
|
|
394
|
+
def add_labels(self, components: str or list or tuple = (),
|
|
395
|
+
font_size: int = 8):
|
|
396
|
+
"""
|
|
397
|
+
Adds labels to hydraulic components according to the specified
|
|
398
|
+
components.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
components : `str` or `list` or `tuple`, default is ()
|
|
403
|
+
Can either be 'all': all components, 'sensor_config': all nodes and
|
|
404
|
+
pipes which have a sensor attached, or a list of the component
|
|
405
|
+
names that are to be labeled: nodes, tanks, reservoirs, pipes,
|
|
406
|
+
valves, pumps. If the list is empty, all nodes are labeled.
|
|
407
|
+
font_size : `int`, default is 8
|
|
408
|
+
Font size of the labels.
|
|
409
|
+
"""
|
|
410
|
+
sc_nodes, sc_links = None, None
|
|
411
|
+
if components == 'all':
|
|
412
|
+
components = ['nodes', 'tanks', 'reservoirs', 'pipes', 'valves',
|
|
413
|
+
'pumps']
|
|
414
|
+
elif components == 'sensor_config':
|
|
415
|
+
components = ['nodes', 'tanks', 'reservoirs', 'pipes', 'valves',
|
|
416
|
+
'pumps']
|
|
417
|
+
sc_nodes, sc_links = self._get_sensor_config_nodes_and_links()
|
|
418
|
+
|
|
419
|
+
elif len(components) == 0:
|
|
420
|
+
components = ['nodes']
|
|
421
|
+
|
|
422
|
+
component_mapping = {
|
|
423
|
+
'nodes': self.junction_parameters,
|
|
424
|
+
'tanks': self.tank_parameters,
|
|
425
|
+
'reservoirs': self.reservoir_parameters,
|
|
426
|
+
'valves': self.valve_parameters,
|
|
427
|
+
'pumps': self.pump_parameters
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for component, parameters in component_mapping.items():
|
|
431
|
+
if component in components:
|
|
432
|
+
labels = {n: str(n) for n in parameters.nodelist if
|
|
433
|
+
sc_nodes is None or n in sc_nodes}
|
|
434
|
+
self.labels[component] = {'pos': parameters.pos,
|
|
435
|
+
'labels': labels,
|
|
436
|
+
'font_size': font_size}
|
|
437
|
+
if component in ['pumps', 'valves']:
|
|
438
|
+
self.labels[component]['verticalalignment'] = 'bottom'
|
|
439
|
+
if 'pipes' in components:
|
|
440
|
+
labels = {tuple(n[1]): n[0] for n in self.topology.get_all_links()
|
|
441
|
+
if sc_links is None or n[0] in sc_links}
|
|
442
|
+
self.labels['pipes'] = {'pos': self.pipe_parameters.pos,
|
|
443
|
+
'edge_labels': labels,
|
|
444
|
+
'font_size': font_size}
|
|
445
|
+
|
|
446
|
+
def show_animation(self, export_to_file: str = None,
|
|
447
|
+
return_animation: bool = False, duration: int = 5,
|
|
448
|
+
fps: int = 15, interpolate: bool = True) \
|
|
449
|
+
-> Optional[FuncAnimation]:
|
|
450
|
+
"""
|
|
451
|
+
Displays, exports, or returns an animation of a water distribution
|
|
452
|
+
network over time.
|
|
453
|
+
|
|
454
|
+
This method generates an animation of a network and either shows it or
|
|
455
|
+
returns the :class:`~FuncAnimation` object. Optionally, the animation
|
|
456
|
+
is saved to a file.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
export_to_file : `str`, optional
|
|
461
|
+
The file path where the animation should be saved, if provided.
|
|
462
|
+
Default is `None`.
|
|
463
|
+
return_animation : `bool`, optional
|
|
464
|
+
If `True`, the animation object is returned. If `False`, the
|
|
465
|
+
animation will be shown, but not returned. Default is `False`.
|
|
466
|
+
duration : `int`, default is 5
|
|
467
|
+
Duration of the animation in seconds.
|
|
468
|
+
fps : `int`, default is 15
|
|
469
|
+
Frames per seconds, is achieved through interpolation.
|
|
470
|
+
interpolate : `bool`, default is True
|
|
471
|
+
Whether to allow interpolating the sensor values or not. Necessary
|
|
472
|
+
for fixed fps.
|
|
473
|
+
|
|
474
|
+
Returns
|
|
475
|
+
-------
|
|
476
|
+
anim : :class:`~FuncAnimation` or None
|
|
477
|
+
Returns the animation object if `return_animation` is `True`.
|
|
478
|
+
Otherwise, returns `None`.
|
|
479
|
+
"""
|
|
480
|
+
self.fig = plt.figure(figsize=(6.4, 4.8), dpi=200)
|
|
481
|
+
|
|
482
|
+
total_frames = float('inf')
|
|
483
|
+
for node_source in [self.junction_parameters, self.tank_parameters,
|
|
484
|
+
self.reservoir_parameters, self.valve_parameters,
|
|
485
|
+
self.pump_parameters]:
|
|
486
|
+
if not isinstance(node_source.node_color, str) and len(
|
|
487
|
+
node_source.node_color) > 1:
|
|
488
|
+
total_frames = min(total_frames, len(node_source.node_color))
|
|
489
|
+
if hasattr(self.pipe_parameters, 'edge_color'):
|
|
490
|
+
if not isinstance(self.pipe_parameters.edge_color, str) and len(
|
|
491
|
+
self.pipe_parameters.edge_color) > 1:
|
|
492
|
+
total_frames = min(total_frames,
|
|
493
|
+
len(self.pipe_parameters.edge_color))
|
|
494
|
+
if hasattr(self.pipe_parameters, 'width'):
|
|
495
|
+
if not isinstance(self.pipe_parameters.width, str) and len(
|
|
496
|
+
self.pipe_parameters.width) > 1:
|
|
497
|
+
total_frames = min(total_frames,
|
|
498
|
+
len(self.pipe_parameters.width))
|
|
499
|
+
|
|
500
|
+
if total_frames == 0 or total_frames == float('inf'):
|
|
501
|
+
raise RuntimeError("The color or resize functions must be called "
|
|
502
|
+
"with a time_step range (pit) > 1 to enable "
|
|
503
|
+
"animations")
|
|
504
|
+
|
|
505
|
+
if interpolate:
|
|
506
|
+
total_frames = self._interpolate_frames(fps * duration)
|
|
507
|
+
|
|
508
|
+
anim = FuncAnimation(self.fig, self._get_next_frame,
|
|
509
|
+
frames=total_frames,
|
|
510
|
+
interval=round(duration * 100 / total_frames))
|
|
511
|
+
|
|
512
|
+
if export_to_file is not None:
|
|
513
|
+
anim.save(export_to_file, writer='ffmpeg', fps=fps)
|
|
514
|
+
if return_animation:
|
|
515
|
+
plt.close(self.fig)
|
|
516
|
+
return anim
|
|
517
|
+
plt.show()
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
def show_plot(self, export_to_file: str = None,
|
|
521
|
+
suppress_plot: bool = False) -> None:
|
|
522
|
+
"""
|
|
523
|
+
Displays a static plot of the water distribution network.
|
|
524
|
+
|
|
525
|
+
This method generates a static plot of the water distribution network,
|
|
526
|
+
visualizing pipes, junctions, tanks, reservoirs, valves, and pumps.
|
|
527
|
+
The plot can be displayed and saved to a file.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
export_to_file : `str`, optional
|
|
532
|
+
The file path where the plot should be saved, if provided.
|
|
533
|
+
Default is `None`.
|
|
534
|
+
suppress_plot : `bool`, default is False
|
|
535
|
+
If true, no plot is displayed after running this method.
|
|
536
|
+
"""
|
|
537
|
+
self.fig = plt.figure(figsize=(6.4, 4.8), dpi=200)
|
|
538
|
+
self._get_next_frame(0)
|
|
539
|
+
|
|
540
|
+
if export_to_file is not None:
|
|
541
|
+
plt.savefig(export_to_file, transparent=True, bbox_inches='tight',
|
|
542
|
+
dpi=900)
|
|
543
|
+
if not suppress_plot:
|
|
544
|
+
plt.show()
|
|
545
|
+
else:
|
|
546
|
+
plt.close(self.fig)
|
|
547
|
+
plt.clf()
|
|
548
|
+
|
|
549
|
+
def color_nodes(
|
|
550
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
551
|
+
parameter: str = 'pressure', statistic: str = 'mean',
|
|
552
|
+
pit: Optional[Union[int, Tuple[int, int]]] = None,
|
|
553
|
+
species: str = None,
|
|
554
|
+
colormap: str = 'viridis',
|
|
555
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
556
|
+
conversion: Optional[dict] = None,
|
|
557
|
+
show_colorbar: bool = False,
|
|
558
|
+
use_sensor_data: bool = False) -> None:
|
|
559
|
+
"""
|
|
560
|
+
Colors the nodes (junctions) in the water distribution network based on
|
|
561
|
+
the SCADA data and the specified parameters.
|
|
562
|
+
|
|
563
|
+
This method either takes or generates SCADA data, applies a statistic
|
|
564
|
+
to the chosen parameter, optionally groups the results and prepares the
|
|
565
|
+
results to be either displayed statically ot animated.
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
570
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
571
|
+
The SCADA data object containing node data or a numpy array of the
|
|
572
|
+
shape nodes*timesteps. If `None`, a simulation is run to generate
|
|
573
|
+
SCADA data. Default is `None`.
|
|
574
|
+
parameter : `str`, optional
|
|
575
|
+
The node data to visualize. Must be 'pressure', 'demand', or
|
|
576
|
+
'node_quality'. Default is 'pressure'.
|
|
577
|
+
statistic : `str`, optional
|
|
578
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
579
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
580
|
+
pit : `int`, `tuple(int, int)`, optional
|
|
581
|
+
The point in time or range of time steps for the 'time_step'
|
|
582
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
583
|
+
representing the start and end time steps. A tuple is necessary to
|
|
584
|
+
process the data for the :meth:`~ScenarioVisualizer.show_animation`
|
|
585
|
+
method. Default is `None`.
|
|
586
|
+
species: `str`, optional
|
|
587
|
+
Key of species. Only necessary for parameter
|
|
588
|
+
'bulk_species_concentration'.
|
|
589
|
+
colormap : `str`, optional
|
|
590
|
+
The colormap to use for visualizing node values. Default is
|
|
591
|
+
'viridis'.
|
|
592
|
+
intervals : `int`, `list[int]` or `list[float]`, optional
|
|
593
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
594
|
+
integer specifying the number of groups or a list of boundary
|
|
595
|
+
points. Default is `None`.
|
|
596
|
+
conversion : `dict`, optional
|
|
597
|
+
A dictionary of conversion parameters to convert SCADA data units.
|
|
598
|
+
Default is `None`.
|
|
599
|
+
show_colorbar : `bool`, optional
|
|
600
|
+
If `True`, a colorbar will be displayed on the plot to indicate the
|
|
601
|
+
range of node values. Default is `False`.
|
|
602
|
+
use_sensor_data : `bool`, optional
|
|
603
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
604
|
+
by the corresponding sensors in the system is used for the
|
|
605
|
+
visualization. Note: Not all components may have a sensor attached
|
|
606
|
+
and sensors may be subject to sensor faults or noise.
|
|
607
|
+
|
|
608
|
+
Raises
|
|
609
|
+
------
|
|
610
|
+
ValueError
|
|
611
|
+
If the `parameter` is not one of 'pressure', 'demand', or
|
|
612
|
+
'node_quality', or if `pit` is not correctly provided for the
|
|
613
|
+
'time_step' statistic.
|
|
614
|
+
|
|
615
|
+
"""
|
|
616
|
+
self.junction_parameters.cmap = colormap
|
|
617
|
+
|
|
618
|
+
if data is not None:
|
|
619
|
+
self.scada_data = data
|
|
620
|
+
elif not self.scada_data:
|
|
621
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
622
|
+
|
|
623
|
+
if conversion:
|
|
624
|
+
self.scada_data = self.scada_data.convert_units(**conversion)
|
|
625
|
+
|
|
626
|
+
# TODO: is there any way to make this look better (e.g. do a mapping somewhere??)
|
|
627
|
+
if parameter == 'pressure':
|
|
628
|
+
if use_sensor_data:
|
|
629
|
+
values, self.masks[
|
|
630
|
+
'nodes'] = self.scada_data.get_data_pressures_as_node_features()
|
|
631
|
+
else:
|
|
632
|
+
values = self.scada_data.pressure_data_raw
|
|
633
|
+
elif parameter == 'demand':
|
|
634
|
+
if use_sensor_data:
|
|
635
|
+
values, self.masks[
|
|
636
|
+
'nodes'] = self.scada_data.get_data_demands_as_node_features()
|
|
637
|
+
else:
|
|
638
|
+
values = self.scada_data.demand_data_raw
|
|
639
|
+
elif parameter == 'node_quality':
|
|
640
|
+
if use_sensor_data:
|
|
641
|
+
values, self.masks[
|
|
642
|
+
'nodes'] = self.scada_data.get_data_nodes_quality_as_node_features()
|
|
643
|
+
else:
|
|
644
|
+
values = self.scada_data.node_quality_data_raw
|
|
645
|
+
elif parameter == 'custom_data':
|
|
646
|
+
# Custom should have the dimensions (timesteps, nodes)
|
|
647
|
+
values = self.scada_data
|
|
648
|
+
elif parameter == 'bulk_species_concentration':
|
|
649
|
+
if not species:
|
|
650
|
+
raise ValueError('Species must be set when using bulk_species_'
|
|
651
|
+
'concentration.')
|
|
652
|
+
if use_sensor_data:
|
|
653
|
+
values, self.masks[
|
|
654
|
+
'nodes'] = self.scada_data.get_data_bulk_species_concentrations_as_node_features()
|
|
655
|
+
self.masks['nodes'] = self.masks['nodes'][:,
|
|
656
|
+
self.scada_data.sensor_config.bulk_species.index(
|
|
657
|
+
species)]
|
|
658
|
+
values = values[:, :,
|
|
659
|
+
self.scada_data.sensor_config.bulk_species.index(
|
|
660
|
+
species)]
|
|
661
|
+
else:
|
|
662
|
+
values = self.scada_data.bulk_species_node_concentration_raw[:,
|
|
663
|
+
self.scada_data.sensor_config.bulk_species.index(
|
|
664
|
+
species), :]
|
|
665
|
+
else:
|
|
666
|
+
raise ValueError(
|
|
667
|
+
'Parameter must be pressure, demand, node_quality or custom_'
|
|
668
|
+
'data.')
|
|
669
|
+
|
|
670
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
671
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
672
|
+
rng = pit
|
|
673
|
+
if pit[1] == -1:
|
|
674
|
+
rng = (pit[0], values.shape[0])
|
|
675
|
+
for frame in range(*rng):
|
|
676
|
+
if frame > values.shape[0] - 1:
|
|
677
|
+
break
|
|
678
|
+
self.junction_parameters.add_frame(statistic, values, frame,
|
|
679
|
+
intervals)
|
|
680
|
+
else:
|
|
681
|
+
self.junction_parameters.add_frame(statistic, values, pit,
|
|
682
|
+
intervals)
|
|
683
|
+
|
|
684
|
+
if show_colorbar:
|
|
685
|
+
if statistic == 'time_step':
|
|
686
|
+
label = str(parameter).capitalize() + ' at timestep ' + str(
|
|
687
|
+
pit)
|
|
688
|
+
else:
|
|
689
|
+
label = str(statistic).capitalize() + ' ' + str(
|
|
690
|
+
parameter).replace('_', ' ')
|
|
691
|
+
self.colorbars['junctions'] = {'mappable': plt.cm.ScalarMappable(
|
|
692
|
+
norm=mpl.colors.Normalize(
|
|
693
|
+
vmin=self.junction_parameters.vmin,
|
|
694
|
+
vmax=self.junction_parameters.vmax), cmap=colormap),
|
|
695
|
+
'label': label}
|
|
696
|
+
|
|
697
|
+
def color_links(
|
|
698
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
699
|
+
parameter: str = 'flow_rate', statistic: str = 'mean',
|
|
700
|
+
pit: Optional[Union[int, Tuple[int, int]]] = None,
|
|
701
|
+
species: str = None,
|
|
702
|
+
colormap: str = 'coolwarm',
|
|
703
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
704
|
+
conversion: Optional[dict] = None,
|
|
705
|
+
show_colorbar: bool = False,
|
|
706
|
+
use_sensor_data: bool = False) -> None:
|
|
707
|
+
"""
|
|
708
|
+
Colors the links (pipes) in the water distribution network based on the
|
|
709
|
+
SCADA data and the specified parameters.
|
|
710
|
+
|
|
711
|
+
This method either takes or generates SCADA data, applies a statistic
|
|
712
|
+
to the chosen parameter, optionally groups the results and prepares the
|
|
713
|
+
results to be either displayed statically ot animated.
|
|
714
|
+
|
|
715
|
+
Parameters
|
|
716
|
+
----------
|
|
717
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
718
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
719
|
+
The SCADA data object containing link data or a numpy array of the
|
|
720
|
+
shape links*timesteps. If `None`, a simulation is run to generate
|
|
721
|
+
SCADA data. Default is `None`.
|
|
722
|
+
parameter : `str`, optional
|
|
723
|
+
The link data to visualize. Options are 'flow_rate', 'velocity', or
|
|
724
|
+
'status'. Default is 'flow_rate'.
|
|
725
|
+
statistic : `str`, optional
|
|
726
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
727
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
728
|
+
pit : `int` or `tuple(int, int)`, optional
|
|
729
|
+
The point in time or range of time steps for the 'time_step'
|
|
730
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
731
|
+
representing the start and end time steps. A tuple is necessary to
|
|
732
|
+
process the data for the :func:`~ScenarioVisualizer.show_animation`
|
|
733
|
+
method. Default is `None`.
|
|
734
|
+
species: `str`, optional
|
|
735
|
+
Key of species. Only necessary for parameter
|
|
736
|
+
'bulk_species_concentration'.
|
|
737
|
+
colormap : `str`, optional
|
|
738
|
+
The colormap to use for visualizing link values. Default is
|
|
739
|
+
'coolwarm'.
|
|
740
|
+
intervals : `int`, `list[int]`, `list[float]`, optional
|
|
741
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
742
|
+
integer specifying the number of groups or a list of boundary
|
|
743
|
+
points. Default is `None`.
|
|
744
|
+
conversion : `dict`, optional
|
|
745
|
+
A dictionary of conversion parameters to convert SCADA data units.
|
|
746
|
+
Default is `None`.
|
|
747
|
+
show_colorbar : `bool`, optional
|
|
748
|
+
If `True`, a colorbar will be displayed on the plot to indicate the
|
|
749
|
+
range of values. Default is `False`.
|
|
750
|
+
use_sensor_data : `bool`, optional
|
|
751
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
752
|
+
by the corresponding sensors in the system is used for the
|
|
753
|
+
visualization. Note: Not all components may have a sensor attached
|
|
754
|
+
and sensors may be subject to sensor faults or noise.
|
|
755
|
+
|
|
756
|
+
Raises
|
|
757
|
+
------
|
|
758
|
+
ValueError
|
|
759
|
+
If `parameter` is not a valid link data parameter or if `pit` is
|
|
760
|
+
incorrectly provided for the 'time_step' statistic.
|
|
761
|
+
|
|
762
|
+
"""
|
|
763
|
+
sim_length = None
|
|
764
|
+
|
|
765
|
+
if data is not None:
|
|
766
|
+
self.scada_data = data
|
|
767
|
+
if not isinstance(self.scada_data, ScadaData):
|
|
768
|
+
sim_length = self.scada_data.shape[0]
|
|
769
|
+
elif not self.scada_data:
|
|
770
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
771
|
+
|
|
772
|
+
if conversion:
|
|
773
|
+
self.scada_data = self.scada_data.convert_units(**conversion)
|
|
774
|
+
|
|
775
|
+
self.pipe_parameters.edge_cmap = mpl.colormaps[colormap]
|
|
776
|
+
|
|
777
|
+
if sim_length is None:
|
|
778
|
+
sim_length = self.scada_data.sensor_readings_time.shape[0]
|
|
779
|
+
|
|
780
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
781
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
782
|
+
rng = pit
|
|
783
|
+
if pit[1] == -1:
|
|
784
|
+
rng = (pit[0], sim_length)
|
|
785
|
+
for frame in range(*rng):
|
|
786
|
+
if frame >= sim_length:
|
|
787
|
+
break
|
|
788
|
+
self.pipe_parameters.add_frame(self.topology, 'edge_color',
|
|
789
|
+
self.scada_data, parameter,
|
|
790
|
+
statistic, frame, species,
|
|
791
|
+
intervals, use_sensor_data)
|
|
792
|
+
else:
|
|
793
|
+
self.pipe_parameters.add_frame(self.topology, 'edge_color',
|
|
794
|
+
self.scada_data, parameter,
|
|
795
|
+
statistic, pit, species, intervals,
|
|
796
|
+
use_sensor_data)
|
|
797
|
+
|
|
798
|
+
if hasattr(self.pipe_parameters, 'mask'):
|
|
799
|
+
self.masks['links'] = self.pipe_parameters.mask
|
|
800
|
+
|
|
801
|
+
if show_colorbar:
|
|
802
|
+
if statistic == 'time_step':
|
|
803
|
+
label = (str(parameter).capitalize().replace('_', ' ')
|
|
804
|
+
+ ' at timestep ' + str(pit))
|
|
805
|
+
else:
|
|
806
|
+
label = str(statistic).capitalize() + ' ' + str(
|
|
807
|
+
parameter).replace('_', ' ')
|
|
808
|
+
self.colorbars['pipes'] = {'mappable': plt.cm.ScalarMappable(
|
|
809
|
+
norm=mpl.colors.Normalize(
|
|
810
|
+
vmin=self.pipe_parameters.edge_vmin,
|
|
811
|
+
vmax=self.pipe_parameters.edge_vmax), cmap=colormap),
|
|
812
|
+
'label': label}
|
|
813
|
+
|
|
814
|
+
def color_pumps(
|
|
815
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
816
|
+
parameter: str = 'efficiency', statistic: str = 'mean',
|
|
817
|
+
pit: Optional[Union[int, Tuple[int]]] = None,
|
|
818
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
819
|
+
colormap: str = 'viridis', show_colorbar: bool = False,
|
|
820
|
+
use_sensor_data: bool = False) -> None:
|
|
821
|
+
"""
|
|
822
|
+
Colors the pumps in the water distribution network based on SCADA data
|
|
823
|
+
and the specified parameters.
|
|
824
|
+
|
|
825
|
+
This method either takes or generates SCADA data, applies a statistic
|
|
826
|
+
to the chosen parameter, optionally groups the results and prepares the
|
|
827
|
+
results to be either displayed statically ot animated.
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
832
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
833
|
+
The SCADA data object containing pump data or a numpy array of the
|
|
834
|
+
shape pumps*timesteps. If `None`, a simulation is run to generate
|
|
835
|
+
SCADA data. Default is `None`.
|
|
836
|
+
parameter : `str`, optional
|
|
837
|
+
The pump data to visualize. Must be 'efficiency',
|
|
838
|
+
'energy_consumption', or 'state'. Default is 'efficiency'.
|
|
839
|
+
statistic : `str`, optional
|
|
840
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
841
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
842
|
+
pit : `int`, `tuple(int, int)`, optional
|
|
843
|
+
The point in time or range of time steps for the 'time_step'
|
|
844
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
845
|
+
representing the start and end time steps. A tuple is necessary to
|
|
846
|
+
process the data for the :meth:`~ScenarioVisualizer.show_animation`
|
|
847
|
+
method. Default is `None`.
|
|
848
|
+
intervals : `int`, `list[int]`, `list[float]`, optional
|
|
849
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
850
|
+
integer specifying the number of groups or a list of boundary
|
|
851
|
+
points. Default is `None`.
|
|
852
|
+
colormap : `str`, optional
|
|
853
|
+
The colormap to use for visualizing pump values. Default is
|
|
854
|
+
'viridis'.
|
|
855
|
+
show_colorbar : `bool`, optional
|
|
856
|
+
If `True`, a colorbar will be displayed on the plot to indicate the
|
|
857
|
+
range of pump values. Default is `False`.
|
|
858
|
+
use_sensor_data : `bool`, optional
|
|
859
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
860
|
+
by the corresponding sensors in the system is used for the
|
|
861
|
+
visualization. Note: Not all components may have a sensor attached
|
|
862
|
+
and sensors may be subject to sensor faults or noise.
|
|
863
|
+
|
|
864
|
+
Raises
|
|
865
|
+
------
|
|
866
|
+
ValueError
|
|
867
|
+
If the `parameter` is not one of 'efficiency',
|
|
868
|
+
'energy_consumption', or 'state', or if `pit` is not correctly
|
|
869
|
+
provided for the 'time_step' statistic.
|
|
870
|
+
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
self.pump_parameters.cmap = colormap
|
|
874
|
+
|
|
875
|
+
if data is not None:
|
|
876
|
+
self.scada_data = data
|
|
877
|
+
elif not self.scada_data:
|
|
878
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
879
|
+
|
|
880
|
+
if parameter == 'efficiency':
|
|
881
|
+
if use_sensor_data:
|
|
882
|
+
values, self.masks[
|
|
883
|
+
'pumps'] = self.scada_data.get_data_pumps_efficiency_as_node_features()
|
|
884
|
+
else:
|
|
885
|
+
values = self.scada_data.pumps_efficiency_data_raw
|
|
886
|
+
elif parameter == 'energy_consumption':
|
|
887
|
+
if use_sensor_data:
|
|
888
|
+
values, self.masks[
|
|
889
|
+
'pumps'] = self.scada_data.get_data_pumps_energyconsumption_as_node_features()
|
|
890
|
+
else:
|
|
891
|
+
values = self.scada_data.pumps_energyconsumption_data_raw
|
|
892
|
+
elif parameter == 'state':
|
|
893
|
+
if use_sensor_data:
|
|
894
|
+
values, self.masks[
|
|
895
|
+
'pumps'] = self.scada_data.get_data_pumps_state_as_node_features()
|
|
896
|
+
else:
|
|
897
|
+
values = self.scada_data.pumps_state_data_raw
|
|
898
|
+
elif parameter == 'custom_data':
|
|
899
|
+
values = self.scada_data
|
|
900
|
+
else:
|
|
901
|
+
raise ValueError(
|
|
902
|
+
'Parameter must be efficiency, energy_consumption, state or custom_data')
|
|
903
|
+
|
|
904
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
905
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
906
|
+
rng = pit
|
|
907
|
+
if pit[1] == -1:
|
|
908
|
+
rng = (pit[0], values.shape[0])
|
|
909
|
+
for frame in range(*rng):
|
|
910
|
+
if frame > values.shape[0] - 1:
|
|
911
|
+
break
|
|
912
|
+
self.pump_parameters.add_frame(statistic, values, frame,
|
|
913
|
+
intervals)
|
|
914
|
+
else:
|
|
915
|
+
self.pump_parameters.add_frame(statistic, values, pit, intervals)
|
|
916
|
+
|
|
917
|
+
if show_colorbar:
|
|
918
|
+
if statistic == 'time_step':
|
|
919
|
+
label = str(parameter).capitalize().replace(
|
|
920
|
+
'_', ' ') + ' at timestep ' + str(pit)
|
|
921
|
+
else:
|
|
922
|
+
label = str(statistic).capitalize() + ' ' + str(
|
|
923
|
+
parameter).replace('_', ' ')
|
|
924
|
+
self.colorbars['pumps'] = {'mappable': plt.cm.ScalarMappable(
|
|
925
|
+
norm=mpl.colors.Normalize(vmin=self.pump_parameters.vmin,
|
|
926
|
+
vmax=self.pump_parameters.vmax),
|
|
927
|
+
cmap=colormap), 'label': label}
|
|
928
|
+
|
|
929
|
+
def color_tanks(
|
|
930
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
931
|
+
statistic: str = 'mean',
|
|
932
|
+
pit: Optional[Union[int, Tuple[int, int]]] = None,
|
|
933
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
934
|
+
colormap: str = 'viridis', show_colorbar: bool = False,
|
|
935
|
+
use_sensor_data: bool = False) -> None:
|
|
936
|
+
"""
|
|
937
|
+
Colors the tanks in the water distribution network based on the SCADA
|
|
938
|
+
tank volume data and the specified statistic.
|
|
939
|
+
|
|
940
|
+
This method either takes or generates SCADA data, applies a statistic
|
|
941
|
+
to the tank volume data, optionally groups the results and prepares
|
|
942
|
+
them to be either displayed statically ot animated.
|
|
943
|
+
|
|
944
|
+
Parameters
|
|
945
|
+
----------
|
|
946
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
947
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
948
|
+
The SCADA data object containing tank data or a numpy array of the
|
|
949
|
+
shape tanks*timesteps. If `None`, a simulation is run to generate
|
|
950
|
+
SCADA data. Default is `None`.
|
|
951
|
+
statistic : `str`, optional
|
|
952
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
953
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
954
|
+
pit : `int`, `tuple(int, int)`, optional
|
|
955
|
+
The point in time or range of time steps for the 'time_step'
|
|
956
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
957
|
+
representing the start and end time steps. A tuple is necessary to
|
|
958
|
+
process the data for the :meth:`~ScenarioVisualizer.show_animation`
|
|
959
|
+
method. Default is `None`.
|
|
960
|
+
intervals : `int`, `list[int]`, `list[float]`, optional
|
|
961
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
962
|
+
integer specifying the number of groups or a list of boundary
|
|
963
|
+
points. Default is `None`.
|
|
964
|
+
colormap : `str`, optional
|
|
965
|
+
The colormap to use for visualizing tank values. Default is
|
|
966
|
+
'viridis'.
|
|
967
|
+
show_colorbar : `bool`, optional
|
|
968
|
+
If `True`, a colorbar will be displayed on the plot to indicate the
|
|
969
|
+
range of tank volume values. Default is `False`.
|
|
970
|
+
use_sensor_data : `bool`, optional
|
|
971
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
972
|
+
by the corresponding sensors in the system is used for the
|
|
973
|
+
visualization. Note: Not all components may have a sensor attached
|
|
974
|
+
and sensors may be subject to sensor faults or noise.
|
|
975
|
+
|
|
976
|
+
Raises
|
|
977
|
+
------
|
|
978
|
+
ValueError
|
|
979
|
+
If `pit` is not correctly provided for the 'time_step' statistic.
|
|
980
|
+
|
|
981
|
+
"""
|
|
982
|
+
self.tank_parameters.cmap = colormap
|
|
983
|
+
|
|
984
|
+
if data is not None:
|
|
985
|
+
self.scada_data = data
|
|
986
|
+
elif not self.scada_data:
|
|
987
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
988
|
+
|
|
989
|
+
if isinstance(self.scada_data, ScadaData):
|
|
990
|
+
if use_sensor_data:
|
|
991
|
+
values, self.masks[
|
|
992
|
+
'tanks'] = self.scada_data.get_data_tanks_water_volume_as_node_features()
|
|
993
|
+
else:
|
|
994
|
+
values = self.scada_data.tanks_volume_data_raw
|
|
995
|
+
parameter = 'tank volume'
|
|
996
|
+
else:
|
|
997
|
+
values = self.scada_data
|
|
998
|
+
parameter = 'custom data'
|
|
999
|
+
|
|
1000
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
1001
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
1002
|
+
rng = pit
|
|
1003
|
+
if pit[1] == -1:
|
|
1004
|
+
rng = (pit[0], values.shape[0])
|
|
1005
|
+
for frame in range(*rng):
|
|
1006
|
+
if frame > values.shape[0] - 1:
|
|
1007
|
+
break
|
|
1008
|
+
self.tank_parameters.add_frame(statistic, values, frame,
|
|
1009
|
+
intervals)
|
|
1010
|
+
else:
|
|
1011
|
+
self.tank_parameters.add_frame(statistic, values, pit, intervals)
|
|
1012
|
+
|
|
1013
|
+
if show_colorbar:
|
|
1014
|
+
if statistic == 'time_step':
|
|
1015
|
+
label = parameter.capitalize() + ' at timestep ' + str(pit)
|
|
1016
|
+
else:
|
|
1017
|
+
label = str(statistic).capitalize() + ' ' + parameter
|
|
1018
|
+
self.colorbars['tanks'] = {'mappable': plt.cm.ScalarMappable(
|
|
1019
|
+
norm=mpl.colors.Normalize(vmin=self.tank_parameters.vmin,
|
|
1020
|
+
vmax=self.tank_parameters.vmin),
|
|
1021
|
+
cmap=colormap), 'label': label}
|
|
1022
|
+
|
|
1023
|
+
def color_valves(
|
|
1024
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
1025
|
+
statistic: str = 'mean',
|
|
1026
|
+
pit: Optional[Union[int, Tuple[int, int]]] = None,
|
|
1027
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
1028
|
+
colormap: str = 'viridis', show_colorbar: bool = False,
|
|
1029
|
+
use_sensor_data: bool = False) -> None:
|
|
1030
|
+
"""
|
|
1031
|
+
Colors the valves in the water distribution network based on SCADA
|
|
1032
|
+
valve state data and the specified statistic.
|
|
1033
|
+
|
|
1034
|
+
This method either takes or generates SCADA data, applies a statistic
|
|
1035
|
+
to the valve state data, optionally groups the results and prepares
|
|
1036
|
+
them to be either displayed statically ot animated.
|
|
1037
|
+
|
|
1038
|
+
Parameters
|
|
1039
|
+
----------
|
|
1040
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
1041
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
1042
|
+
The SCADA data object containing valve data or a numpy array of the
|
|
1043
|
+
shape valves*timesteps. If `None`, a simulation is run to generate
|
|
1044
|
+
SCADA data. Default is `None`.
|
|
1045
|
+
statistic : `str`, optional
|
|
1046
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
1047
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
1048
|
+
pit : `int`, `tuple(int)`, optional
|
|
1049
|
+
The point in time or range of time steps for the 'time_step'
|
|
1050
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
1051
|
+
representing the start and end time steps. A tuple is necessary to
|
|
1052
|
+
process the data for the :meth:`~ScenarioVisualizer.show_animation`
|
|
1053
|
+
method. Default is `None`.
|
|
1054
|
+
intervals : `int`, `list[int]`, `list[float]`, optional
|
|
1055
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
1056
|
+
integer specifying the number of groups or a list of
|
|
1057
|
+
boundary points. Default is `None`.
|
|
1058
|
+
colormap : `str`, optional
|
|
1059
|
+
The colormap to use for visualizing valve state values. Default is
|
|
1060
|
+
'viridis'.
|
|
1061
|
+
show_colorbar : `bool`, optional
|
|
1062
|
+
If `True`, a colorbar will be displayed on the plot to indicate the
|
|
1063
|
+
range of valve state values. Default is `False`.
|
|
1064
|
+
use_sensor_data : `bool`, optional
|
|
1065
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
1066
|
+
by the corresponding sensors in the system is used for the
|
|
1067
|
+
visualization. Note: Not all components may have a sensor attached
|
|
1068
|
+
and sensors may be subject to sensor faults or noise.
|
|
1069
|
+
|
|
1070
|
+
Raises
|
|
1071
|
+
------
|
|
1072
|
+
ValueError
|
|
1073
|
+
If `pit` is not correctly provided for the 'time_step' statistic.
|
|
1074
|
+
|
|
1075
|
+
"""
|
|
1076
|
+
|
|
1077
|
+
self.valve_parameters.cmap = colormap
|
|
1078
|
+
|
|
1079
|
+
if data is not None:
|
|
1080
|
+
self.scada_data = data
|
|
1081
|
+
elif not self.scada_data:
|
|
1082
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
1083
|
+
|
|
1084
|
+
if isinstance(self.scada_data, ScadaData):
|
|
1085
|
+
if use_sensor_data:
|
|
1086
|
+
values, self.masks[
|
|
1087
|
+
'valves'] = self.scada_data.get_data_valves_state_as_node_features()
|
|
1088
|
+
else:
|
|
1089
|
+
values = self.scada_data.valves_state_data_raw
|
|
1090
|
+
parameter = 'valve state'
|
|
1091
|
+
else:
|
|
1092
|
+
values = self.scada_data
|
|
1093
|
+
parameter = 'custom data'
|
|
1094
|
+
|
|
1095
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
1096
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
1097
|
+
rng = pit
|
|
1098
|
+
if pit[1] == -1:
|
|
1099
|
+
rng = (pit[0], values.shape[0])
|
|
1100
|
+
for frame in range(*rng):
|
|
1101
|
+
if frame > values.shape[0] - 1:
|
|
1102
|
+
break
|
|
1103
|
+
self.valve_parameters.add_frame(statistic, values, frame,
|
|
1104
|
+
intervals)
|
|
1105
|
+
else:
|
|
1106
|
+
self.valve_parameters.add_frame(statistic, values, pit,
|
|
1107
|
+
intervals)
|
|
1108
|
+
|
|
1109
|
+
if show_colorbar:
|
|
1110
|
+
if statistic == 'time_step':
|
|
1111
|
+
label = parameter.capitalize() + ' at timestep ' + str(pit)
|
|
1112
|
+
else:
|
|
1113
|
+
label = str(statistic).capitalize() + ' ' + 'valve state'
|
|
1114
|
+
self.colorbars['valves'] = {'mappable': plt.cm.ScalarMappable(
|
|
1115
|
+
norm=mpl.colors.Normalize(vmin=self.valve_parameters.vmin,
|
|
1116
|
+
vmax=self.valve_parameters.vmax),
|
|
1117
|
+
cmap=colormap), 'label': label}
|
|
1118
|
+
|
|
1119
|
+
def resize_links(
|
|
1120
|
+
self, data: Optional[Union[ScadaData, np.ndarray]] = None,
|
|
1121
|
+
parameter: str = 'flow_rate', statistic: str = 'mean',
|
|
1122
|
+
line_widths: Tuple[int, int] = (1, 2),
|
|
1123
|
+
pit: Optional[Union[int, Tuple[int, int]]] = None,
|
|
1124
|
+
species: str = None,
|
|
1125
|
+
intervals: Optional[Union[int, List[Union[int, float]]]] = None,
|
|
1126
|
+
conversion: Optional[dict] = None,
|
|
1127
|
+
use_sensor_data: bool = False) -> None:
|
|
1128
|
+
"""
|
|
1129
|
+
Resizes the width of the links (pipes) in the water distribution
|
|
1130
|
+
network based on SCADA data and the specified parameters.
|
|
1131
|
+
|
|
1132
|
+
This method either takes or generates SCADA data, applies a statistic,
|
|
1133
|
+
optionally groups the results and prepares them to be either displayed
|
|
1134
|
+
statically ot animated as link width.
|
|
1135
|
+
|
|
1136
|
+
Parameters
|
|
1137
|
+
----------
|
|
1138
|
+
data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or
|
|
1139
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
1140
|
+
The SCADA data object containing link data or a numpy array of the
|
|
1141
|
+
shape links*timesteps. If `None`, a simulation is run to generate
|
|
1142
|
+
SCADA data. Default is `None`.
|
|
1143
|
+
parameter : `str`, optional
|
|
1144
|
+
The data used to resize to. Default is 'flow_rate'.
|
|
1145
|
+
statistic : `str`, optional
|
|
1146
|
+
The statistic to calculate for the data. Can be 'mean', 'min',
|
|
1147
|
+
'max', or 'time_step'. Default is 'mean'.
|
|
1148
|
+
line_widths : `tuple(int, int)`, optional
|
|
1149
|
+
A tuple specifying the range of line widths to use when resizing
|
|
1150
|
+
links based on the data. Default is (1, 2).
|
|
1151
|
+
pit : `int` or `tuple(int, int)`, optional
|
|
1152
|
+
The point in time or range of time steps for the 'time_step'
|
|
1153
|
+
statistic. If a tuple is provided, it should contain two integers
|
|
1154
|
+
representing the start and end time steps. A tuple is necessary to
|
|
1155
|
+
process the data for the :meth:`~ScenarioVisualizer.show_animation`
|
|
1156
|
+
method. Default is `None`.
|
|
1157
|
+
species: `str`, optional
|
|
1158
|
+
Key of species. Only necessary for parameter
|
|
1159
|
+
'bulk_species_concentration'.
|
|
1160
|
+
intervals : `int` or `list[int]` or `list[float]`, optional
|
|
1161
|
+
If provided, the data will be grouped into intervals. It can be an
|
|
1162
|
+
integer specifying the number of groups or a list of boundary
|
|
1163
|
+
points. Default is `None`.
|
|
1164
|
+
conversion : `dict`, optional
|
|
1165
|
+
A dictionary of conversion parameters to convert SCADA data units.
|
|
1166
|
+
Default is `None`.
|
|
1167
|
+
use_sensor_data : `bool`, optional
|
|
1168
|
+
If `True`, instead of using raw simulation data, the data recorded
|
|
1169
|
+
by the corresponding sensors in the system is used for the
|
|
1170
|
+
visualization. Note: Not all components may have a sensor attached
|
|
1171
|
+
and sensors may be subject to sensor faults or noise.
|
|
1172
|
+
"""
|
|
1173
|
+
sim_length = None
|
|
1174
|
+
|
|
1175
|
+
if data is not None:
|
|
1176
|
+
self.scada_data = data
|
|
1177
|
+
if not isinstance(self.scada_data, ScadaData):
|
|
1178
|
+
sim_length = self.scada_data.shape[0]
|
|
1179
|
+
elif not self.scada_data:
|
|
1180
|
+
self.scada_data = self.__scenario.run_simulation()
|
|
1181
|
+
|
|
1182
|
+
if conversion:
|
|
1183
|
+
self.scada_data = self.scada_data.convert_units(**conversion)
|
|
1184
|
+
|
|
1185
|
+
if sim_length is None:
|
|
1186
|
+
sim_length = self.scada_data.sensor_readings_time.shape[0]
|
|
1187
|
+
|
|
1188
|
+
if statistic == 'time_step' and isinstance(pit, tuple) and len(
|
|
1189
|
+
pit) == 2 and all(isinstance(i, int) for i in pit):
|
|
1190
|
+
rng = pit
|
|
1191
|
+
if pit[1] == -1:
|
|
1192
|
+
rng = (pit[0], sim_length)
|
|
1193
|
+
for frame in range(*rng):
|
|
1194
|
+
if frame >= sim_length:
|
|
1195
|
+
break
|
|
1196
|
+
self.pipe_parameters.add_frame(self.topology, 'edge_width',
|
|
1197
|
+
self.scada_data, parameter,
|
|
1198
|
+
statistic, frame, species,
|
|
1199
|
+
intervals, use_sensor_data)
|
|
1200
|
+
else:
|
|
1201
|
+
self.pipe_parameters.add_frame(self.topology, 'edge_width',
|
|
1202
|
+
self.scada_data, parameter,
|
|
1203
|
+
statistic, pit, species, intervals,
|
|
1204
|
+
use_sensor_data)
|
|
1205
|
+
self.pipe_parameters.rescale_widths(line_widths)
|
|
1206
|
+
|
|
1207
|
+
def hide_nodes(self) -> None:
|
|
1208
|
+
"""
|
|
1209
|
+
Hides all nodes (junctions) in the water distribution network
|
|
1210
|
+
visualization.
|
|
1211
|
+
|
|
1212
|
+
This method clears the node list from the `junction_parameters`
|
|
1213
|
+
class, effectively removing all nodes from view in the current
|
|
1214
|
+
visualization.
|
|
1215
|
+
"""
|
|
1216
|
+
self.junction_parameters.nodelist = []
|
|
1217
|
+
|
|
1218
|
+
def highlight_sensor_config(self) -> None:
|
|
1219
|
+
"""
|
|
1220
|
+
Highlights nodes and links that have sensors in the sensor_config in
|
|
1221
|
+
the water distribution network visualization.
|
|
1222
|
+
|
|
1223
|
+
This method identifies nodes and links equipped with different types of
|
|
1224
|
+
sensors from the
|
|
1225
|
+
:class:`~epyt_flow.simulation.sensor_config.SensorConfig` and
|
|
1226
|
+
updates their visual appearance. Nodes with sensors are highlighted
|
|
1227
|
+
with an orange border, while links with sensors are displayed with a
|
|
1228
|
+
dashed line style.
|
|
1229
|
+
"""
|
|
1230
|
+
highlighted_nodes, highlighted_links = self._get_sensor_config_nodes_and_links()
|
|
1231
|
+
|
|
1232
|
+
node_edges = [
|
|
1233
|
+
(17, 163, 252) if node in highlighted_nodes else (0, 0, 0) for node
|
|
1234
|
+
in self.topology]
|
|
1235
|
+
pipe_style = ['dashed' if link[0] in highlighted_links else 'solid' for
|
|
1236
|
+
link in self.topology.get_all_links()]
|
|
1237
|
+
|
|
1238
|
+
self.junction_parameters.add_attributes(
|
|
1239
|
+
{'linewidths': 1, 'edgecolors': node_edges})
|
|
1240
|
+
self.pipe_parameters.add_attributes({'style': pipe_style})
|