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