epyt-flow 0.7.3__py3-none-any.whl → 0.8.1__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,31 +1,1273 @@
1
1
  """
2
2
  Module provides a class for visualizing scenarios.
3
3
  """
4
+ from typing import Optional, Union, List, Tuple, Iterable
5
+ from deprecated import deprecated
6
+
4
7
  import matplotlib.pyplot as plt
8
+ from matplotlib.animation import FuncAnimation
9
+ import matplotlib as mpl
10
+ import networkx.drawing.nx_pylab as nxp
11
+ import numpy as np
12
+ from svgpath2mpl import parse_path
5
13
 
6
14
  from .scenario_simulator import ScenarioSimulator
15
+ from .scada.scada_data import ScadaData
16
+
17
+ 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
+ '41.5 42 0 0 0 244 135 A 41.5 42 0 0 0 241.94922 122 L 278 122 '
19
+ 'L 278 93 L 203 93 L 203 93.011719 A 41.5 42 0 0 0 202.5 93 z')
20
+ RESERVOIR_PATH = ('M 325 41 A 43 24.5 0 0 0 282.05664 65 L 282 65 L 282 65.5 '
21
+ 'L 282 163 L 282 168 L 282 216 L 305 216 L 305 168 L 345 '
22
+ '168 L 345 216 L 368 216 L 368 168 L 368 163 L 368 65.5 L '
23
+ '368 65 L 367.98047 65 A 43 24.5 0 0 0 325 41 z')
24
+ TANK_PATH = ('M 325 41 A 43 24.5 0 0 0 282.05664 65 L 282 65 L 282 65.5 L 282 '
25
+ '185 L 368 185 L 368 65.5 L 368 65 L 367.98047 65 A 43 24.5 0 0'
26
+ ' 0 325 41 z')
27
+ VALVE_PATH = ('M 9.9999064 9.9999064 L 9.9999064 110 L 69.999862 59.999955 L '
28
+ '9.9999064 9.9999064 z M 69.999862 59.999955 L 129.99982 110 L '
29
+ '129.99982 9.9999064 L 69.999862 59.999955 z')
30
+
31
+
32
+ class Marker:
33
+ """
34
+ The Marker class provides svg representations of hydraulic components
35
+ (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
+
39
+ Attributes
40
+ ----------
41
+ pump : :class:`~matplotlib.path.Path` object
42
+ Marker for the pump, loaded from PUMP_PATH.
43
+ reservoir : :class:`~matplotlib.path.Path` object
44
+ Marker for the reservoir, loaded from RESERVOIR_PATH.
45
+ tank : :class:`~matplotlib.path.Path` object
46
+ Marker for the tank, loaded from TANK_PATH.
47
+ valve : :class:`~matplotlib.path.Path` object
48
+ Marker for the valve, loaded from VALVE_PATH.
49
+
50
+ Methods
51
+ -------
52
+ __marker_from_path(path, scale_p=1)
53
+ Loads and applies transformations to the marker shape from the given
54
+ path.
55
+ """
56
+ def __init__(self):
57
+ """
58
+ Initializes the Marker class and assigns :class:`~matplotlib.path.Path`
59
+ markers for pump, reservoir, tank, and valve components.
60
+ """
61
+ self.pump = self.__marker_from_path(PUMP_PATH, 2)
62
+ self.reservoir = self.__marker_from_path(RESERVOIR_PATH)
63
+ self.tank = self.__marker_from_path(TANK_PATH)
64
+ self.valve = self.__marker_from_path(VALVE_PATH)
65
+
66
+ @staticmethod
67
+ def __marker_from_path(path: str, scale_p: int = 1) -> mpl.path.Path:
68
+ """
69
+ Loads the marker from the specified path and adjusts it representation
70
+ by aligning, rotating and scaling it.
71
+
72
+ Parameters
73
+ ----------
74
+ path : `str`
75
+ The svg path describing the marker shape.
76
+ scale_p : `float`, optional
77
+ Scaling factor for the marker (default is 1).
78
+
79
+ Returns
80
+ -------
81
+ marker_tmp : :class:`~matplotlib.path.Path` object
82
+ The transformed marker object after loading and adjusting it.
83
+ """
84
+ marker_tmp = parse_path(path)
85
+ marker_tmp.vertices -= marker_tmp.vertices.mean(axis=0)
86
+ marker_tmp = marker_tmp.transformed(
87
+ mpl.transforms.Affine2D().rotate_deg(180))
88
+ marker_tmp = marker_tmp.transformed(
89
+ mpl.transforms.Affine2D().scale(-scale_p, scale_p))
90
+ return marker_tmp
7
91
 
8
92
 
9
- class ScenarioVisualizer():
93
+ class ScenarioVisualizer:
10
94
  """
11
- Class for visualizing a given scenario.
95
+ This class provides the necessary function to generate visualizations in
96
+ the form of plots or animations from water network data.
12
97
 
13
- Parameters
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.
102
+
103
+ Attributes
14
104
  ----------
15
- scenario : :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
16
- Scenario to be visualized.
105
+ __scenario : :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
106
+ ScenarioSimulator object containing the network topology and
107
+ configurations to obtain the simulation data which should be displayed.
108
+ fig : :class:`~matplotlib.pyplot.Figure` or None
109
+ Figure object used for plotting, created and customized by calling the
110
+ methods of this class, initialized as None.
111
+ ax : :class:`~matplotlib.axes.Axes` or None
112
+ The axes for plotting, initialized as None.
113
+ scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or None
114
+ SCADA data created by the ScenarioSimulator object, initialized as
115
+ None.
116
+ topology : :class:`~epyt_flow.topology.NetworkTopology`
117
+ Topology object retrieved from the scenario, containing the structure
118
+ of the water distribution network.
119
+ pos_dict : `dict`
120
+ A dictionary mapping nodes to their coordinates in the correct format
121
+ 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
130
+ 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.
138
+ colorbars : `dict`
139
+ A dictionary containing the necessary data for drawing the required
140
+ colorbars.
17
141
  """
18
- def __init__(self, scenario: ScenarioSimulator):
142
+ def __init__(self, scenario: ScenarioSimulator) -> None:
143
+ """
144
+ 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).
147
+
148
+ Parameters
149
+ ----------
150
+ scenario : :class:`epyt_flow.simulation.scenario_simulator.ScenarioSimulator`
151
+ An instance of the `ScenarioSimulator` class, used to simulate and
152
+ retrieve the system topology.
153
+
154
+ Raises
155
+ ------
156
+ TypeError
157
+ If `scenario` is not an instance of
158
+ :class:`epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
159
+
160
+ """
19
161
  if not isinstance(scenario, ScenarioSimulator):
20
162
  raise TypeError("'scenario' must be an instance of " +
21
163
  "'epyt_flow.simulation.ScenarioSimulator' " +
22
164
  f"but not of '{type(scenario)}'")
23
165
 
24
166
  self.__scenario = scenario
167
+ self.fig = None
168
+ self.ax = None
169
+ self.scada_data = None
170
+ markers = Marker()
171
+ 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 = {}
193
+ self.colorbars = {}
194
+
195
+ def __get_midpoints(self, elements: List[str]) -> dict[str, tuple[float, float]]:
196
+ """
197
+ Computes and returns the midpoints for drawing either valves or pumps
198
+ in a water distribution network.
199
+
200
+ For each element ID in the provided list, the method calculates the
201
+ midpoint between its start and end nodes' coordinates.
202
+
203
+ Parameters
204
+ ----------
205
+ elements : `list[str]`
206
+ A list of element IDs (e.g., pump IDs, valve IDs) for which to
207
+ compute the midpoints.
208
+
209
+ Returns
210
+ -------
211
+ elements_dict : `dict`
212
+ A dictionary where the keys are element IDs and the values are the
213
+ corresponding midpoints, represented as 2D coordinates [x, y].
214
+ """
215
+ elements_pos_dict = {}
216
+ for element in elements:
217
+ if element in self.topology.pumps:
218
+ start_node, end_node = self.topology.get_pump_info(element)[
219
+ 'end_points']
220
+ elif element in self.topology.valves:
221
+ start_node, end_node = self.topology.get_valve_info(element)[
222
+ 'end_points']
223
+ else:
224
+ raise ValueError(f"Unknown element '{element}'")
225
+ start_pos = self.topology.get_node_info(start_node)['coord']
226
+ end_pos = self.topology.get_node_info(end_node)['coord']
227
+ pos = [(start_pos[0] + end_pos[0]) / 2,
228
+ (start_pos[1] + end_pos[1]) / 2]
229
+ elements_pos_dict[element] = pos
230
+ return elements_pos_dict
231
+
232
+ def __get_next_frame(self, frame_number: int) -> None:
233
+ """
234
+ Draws the next frame of a water distribution network animation.
235
+
236
+ This method updates a visualization animation with the hydraulic
237
+ components colored according to the scada data corresponding to the
238
+ current frame.
239
+
240
+ Parameters
241
+ ----------
242
+ frame_number : `int`
243
+ The current frame number used to retrieve the data corresponding to
244
+ that frame
245
+ """
246
+ self.ax = self.fig.add_subplot(111)
247
+ self.ax.axis('off')
248
+
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,
276
+ label='Reservoirs',
277
+ **self.reservoir_parameters)
278
+ 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)
282
+ nxp.draw_networkx_nodes(
283
+ self.topology, self.__get_midpoints(self.topology.get_all_pumps()),
284
+ ax=self.ax, label='Pumps', **self.pump_parameters)
285
+
286
+ self.ax.legend(fontsize=6)
287
+
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]:
295
+
296
+ """
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.
303
+
304
+ Parameters
305
+ ----------
306
+ scada_data : :class:`~epyt_flow.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`.
327
+
328
+ Returns
329
+ -------
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
+
343
+ """
344
+
345
+ if scada_data:
346
+ self.scada_data = scada_data
347
+ elif not self.scada_data:
348
+ self.scada_data = self.__scenario.run_simulation()
349
+
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:
412
+ """
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.
420
+
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.
442
+
443
+ Returns
444
+ -------
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
+
456
+ """
457
+
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
491
+
492
+ @staticmethod
493
+ def __rescale(values: np.ndarray, scale_min_max: List,
494
+ values_min_max: List = None) -> List:
495
+ """
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.
502
+
503
+ Parameters
504
+ ----------
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
+
521
+ """
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]
534
+
535
+ def show_animation(self, export_to_file: str = None,
536
+ return_animation: bool = False)\
537
+ -> Optional[FuncAnimation]:
538
+ """
539
+ Displays, exports, or returns an animation of a water distribution
540
+ network over time.
541
+
542
+ This method generates an animation of a network and either shows it or
543
+ returns the :class:`~FuncAnimation` object. Optionally, the animation
544
+ is saved to a file.
545
+
546
+ Parameters
547
+ ----------
548
+ export_to_file : `str`, optional
549
+ The file path where the animation should be saved, if provided.
550
+ Default is `None`.
551
+ return_animation : `bool`, optional
552
+ If `True`, the animation object is returned. If `False`, the
553
+ animation will be shown, but not returned. Default is `False`.
554
+
555
+ Returns
556
+ -------
557
+ anim : :class:`~FuncAnimation` or None
558
+ Returns the animation object if `return_animation` is `True`.
559
+ Otherwise, returns `None`.
560
+
561
+ """
562
+ self.fig = plt.figure(figsize=(6.4, 4.8), dpi=200)
563
+
564
+ 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:
569
+ raise RuntimeError("The color or resize functions must be called "
570
+ "with a time_step range (pit) to enable "
571
+ "animations")
572
+
573
+ anim = FuncAnimation(self.fig, self.__get_next_frame,
574
+ frames=total_frames, interval=25)
575
+
576
+ if export_to_file is not None:
577
+ anim.save(export_to_file, writer='ffmpeg', fps=4)
578
+ if return_animation:
579
+ plt.close(self.fig)
580
+ return anim
581
+ plt.show()
582
+ return None
583
+
584
+ def show_plot(self, export_to_file: str = None) -> None:
585
+ """
586
+ Displays a static plot of the water distribution network.
587
+
588
+ This method generates a static plot of the water distribution network,
589
+ visualizing pipes, junctions, tanks, reservoirs, valves, and pumps.
590
+ The plot can be displayed and saved to a file.
591
+
592
+ Parameters
593
+ ----------
594
+ export_to_file : `str`, optional
595
+ The file path where the plot should be saved, if provided.
596
+ Default is `None`.
597
+
598
+ """
599
+ 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)
623
+
624
+ if export_to_file is not None:
625
+ plt.savefig(export_to_file, transparent=True, bbox_inches='tight',
626
+ dpi=200)
627
+ plt.show()
628
+
629
+ def color_nodes(
630
+ self, scada_data: Optional[ScadaData] = None,
631
+ parameter: str = 'pressure', statistic: str = 'mean',
632
+ pit: Optional[Union[int, Tuple[int]]] = None,
633
+ colormap: str = 'viridis',
634
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
635
+ conversion: Optional[dict] = None, show_colorbar: bool = False) ->\
636
+ None:
637
+ """
638
+ Colors the nodes (junctions) in the water distribution network based on
639
+ the SCADA data and the specified parameters.
640
+
641
+ This method either takes or generates SCADA data, applies a statistic
642
+ to the chosen parameter, optionally groups the results and prepares the
643
+ results to be either displayed statically ot animated.
644
+
645
+ Parameters
646
+ ----------
647
+ scada_data : :class:`~epyt_flow.scada.scad_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`.
650
+ parameter : `str`, optional
651
+ The node data to visualize. Must be 'pressure', 'demand', or
652
+ 'node_quality'. Default is 'pressure'.
653
+ statistic : `str`, optional
654
+ The statistic to calculate for the data. Can be 'mean', 'min',
655
+ 'max', or 'time_step'. Default is 'mean'.
656
+ pit : `int`, `tuple(int, int)`, optional
657
+ The point in time or range of time steps for the 'time_step'
658
+ statistic. If a tuple is provided, it should contain two integers
659
+ representing the start and end time steps. A tuple is necessary to
660
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
661
+ method. Default is `None`.
662
+ colormap : `str`, optional
663
+ The colormap to use for visualizing node values. Default is
664
+ 'viridis'.
665
+ intervals : `int`, `list[int]` or `list[float]`, optional
666
+ If provided, the data will be grouped into intervals. It can be an
667
+ integer specifying the number of groups or a list of boundary
668
+ points. Default is `None`.
669
+ conversion : `dict`, optional
670
+ A dictionary of conversion parameters to convert SCADA data units.
671
+ Default is `None`.
672
+ show_colorbar : `bool`, optional
673
+ If `True`, a colorbar will be displayed on the plot to indicate the
674
+ range of node values. Default is `False`.
675
+
676
+ Raises
677
+ ------
678
+ ValueError
679
+ If the `parameter` is not one of 'pressure', 'demand', or
680
+ 'node_quality', or if `pit` is not correctly provided for the
681
+ 'time_step' statistic.
682
+
683
+ """
684
+
685
+ self.junction_parameters.update({'cmap': colormap})
686
+
687
+ if scada_data:
688
+ self.scada_data = scada_data
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
+ if parameter == 'pressure':
696
+ values = self.scada_data.pressure_data_raw
697
+ elif parameter == 'demand':
698
+ values = self.scada_data.demand_data_raw
699
+ elif parameter == 'node_quality':
700
+ values = self.scada_data.node_quality_data_raw
701
+ else:
702
+ raise ValueError(
703
+ 'Parameter must be pressure, demand or node_quality')
704
+
705
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
706
+ 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):
714
+ if frame > values.shape[0] - 1:
715
+ 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
725
+ 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)})
733
+
734
+ if show_colorbar:
735
+ if statistic == 'time_step':
736
+ label = str(parameter).capitalize() + ' at timestep ' + str(
737
+ pit)
738
+ else:
739
+ label = str(statistic).capitalize() + ' ' + str(parameter)
740
+ self.colorbars['junctions'] = {'mappable': plt.cm.ScalarMappable(
741
+ norm=mpl.colors.Normalize(
742
+ vmin=self.junction_parameters['vmin'],
743
+ vmax=self.junction_parameters['vmax']), cmap=colormap),
744
+ 'label': label}
745
+
746
+ def color_links(
747
+ self, scada_data: Optional[ScadaData] = None,
748
+ parameter: str = 'flow_rate', statistic: str = 'mean',
749
+ pit: Optional[Union[int, Tuple[int]]] = None,
750
+ colormap: str = 'coolwarm',
751
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
752
+ conversion: Optional[dict] = None, show_colorbar: bool = False) ->\
753
+ None:
754
+ """
755
+ Colors the links (pipes) in the water distribution network based on the
756
+ SCADA data and the specified parameters.
757
+
758
+ This method either takes or generates SCADA data, applies a statistic
759
+ to the chosen parameter, optionally groups the results and prepares the
760
+ results to be either displayed statically ot animated.
761
+
762
+ Parameters
763
+ ----------
764
+ scada_data : :class:`~epyt_flow.scada.scada_data.ScadaData`, optional
765
+ The SCADA data object. If `None`, the method will run a simulation.
766
+ Default is `None`.
767
+ parameter : `str`, optional
768
+ The link data to visualize. Options are 'flow_rate', 'velocity', or
769
+ 'status'. Default is 'flow_rate'.
770
+ statistic : `str`, optional
771
+ The statistic to calculate for the data. Can be 'mean', 'min',
772
+ 'max', or 'time_step'. Default is 'mean'.
773
+ pit : `int` or `tuple(int, int)`, optional
774
+ The point in time or range of time steps for the 'time_step'
775
+ statistic. If a tuple is provided, it should contain two integers
776
+ representing the start and end time steps. A tuple is necessary to
777
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
778
+ method. Default is `None`.
779
+ colormap : `str`, optional
780
+ The colormap to use for visualizing link values. Default is
781
+ 'coolwarm'.
782
+ intervals : `int`, `list[int]`, `list[float]`, optional
783
+ If provided, the data will be grouped into intervals. It can be an
784
+ integer specifying the number of groups or a list of boundary
785
+ points. Default is `None`.
786
+ conversion : `dict`, optional
787
+ A dictionary of conversion parameters to convert SCADA data units.
788
+ Default is `None`.
789
+ show_colorbar : `bool`, optional
790
+ If `True`, a colorbar will be displayed on the plot to indicate the
791
+ range of values. Default is `False`.
792
+
793
+ Raises
794
+ ------
795
+ ValueError
796
+ If `parameter` is not a valid link data parameter or if `pit` is
797
+ incorrectly provided for the 'time_step' statistic.
798
+
799
+ """
800
+
801
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
802
+ 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:
817
+ 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
826
+ 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)})
834
+
835
+ if show_colorbar:
836
+ if statistic == 'time_step':
837
+ label = (str(parameter).capitalize().replace('_', ' ')
838
+ + ' at timestep ' + str(pit))
839
+ else:
840
+ label = str(statistic).capitalize() + ' ' + str(
841
+ parameter).replace('_', ' ')
842
+ self.colorbars['pipes'] = {'mappable': plt.cm.ScalarMappable(
843
+ norm=mpl.colors.Normalize(
844
+ vmin=self.pipe_parameters['edge_vmin'],
845
+ vmax=self.pipe_parameters['edge_vmax']), cmap=colormap),
846
+ 'label': label}
847
+
848
+ def color_pumps(
849
+ self, scada_data: Optional[ScadaData] = None,
850
+ parameter: str = 'efficiency', statistic: str = 'mean',
851
+ pit: Optional[Union[int, Tuple[int]]] = None,
852
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
853
+ colormap: str = 'viridis', show_colorbar: bool = False) -> None:
854
+ """
855
+ Colors the pumps in the water distribution network based on SCADA data
856
+ and the specified parameters.
857
+
858
+ This method either takes or generates SCADA data, applies a statistic
859
+ to the chosen parameter, optionally groups the results and prepares the
860
+ results to be either displayed statically ot animated.
861
+
862
+ Parameters
863
+ ----------
864
+ scada_data : :class:`~epyt_flow.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`.
867
+ parameter : `str`, optional
868
+ The pump data to visualize. Must be 'efficiency',
869
+ 'energy_consumption', or 'state'. Default is 'efficiency'.
870
+ statistic : `str`, optional
871
+ The statistic to calculate for the data. Can be 'mean', 'min',
872
+ 'max', or 'time_step'. Default is 'mean'.
873
+ pit : `int`, `tuple(int, int)`, optional
874
+ The point in time or range of time steps for the 'time_step'
875
+ statistic. If a tuple is provided, it should contain two integers
876
+ representing the start and end time steps. A tuple is necessary to
877
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
878
+ method. Default is `None`.
879
+ intervals : `int`, `list[int]`, `list[float]`, optional
880
+ If provided, the data will be grouped into intervals. It can be an
881
+ integer specifying the number of groups or a list of boundary
882
+ points. Default is `None`.
883
+ colormap : `str`, optional
884
+ The colormap to use for visualizing pump values. Default is
885
+ 'viridis'.
886
+ show_colorbar : `bool`, optional
887
+ If `True`, a colorbar will be displayed on the plot to indicate the
888
+ range of pump values. Default is `False`.
889
+
890
+ Raises
891
+ ------
892
+ ValueError
893
+ If the `parameter` is not one of 'efficiency',
894
+ 'energy_consumption', or 'state', or if `pit` is not correctly
895
+ provided for the 'time_step' statistic.
896
+
897
+ """
898
+
899
+ self.pump_parameters.update({'cmap': colormap})
900
+
901
+ if scada_data:
902
+ self.scada_data = scada_data
903
+ elif not self.scada_data:
904
+ self.scada_data = self.__scenario.run_simulation()
905
+
906
+ if parameter == 'efficiency':
907
+ values = self.scada_data.pumps_efficiency_data_raw
908
+ elif parameter == 'energy_consumption':
909
+ values = self.scada_data.pumps_energyconsumption_data_raw
910
+ elif parameter == 'state':
911
+ values = self.scada_data.pumps_state_data_raw
912
+ else:
913
+ raise ValueError(
914
+ 'Parameter must be efficiency, energy_consumption or state')
915
+
916
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
917
+ 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):
926
+ if frame > values.shape[0] - 1:
927
+ 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
937
+ 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)})
945
+
946
+ if show_colorbar:
947
+ if statistic == 'time_step':
948
+ label = str(parameter).capitalize().replace(
949
+ '_', ' ') + ' at timestep ' + str(pit)
950
+ else:
951
+ label = str(statistic).capitalize() + ' ' + str(
952
+ parameter).replace('_', ' ')
953
+ self.colorbars['pumps'] = {'mappable': plt.cm.ScalarMappable(
954
+ norm=mpl.colors.Normalize(vmin=self.pump_parameters['vmin'],
955
+ vmax=self.pump_parameters['vmax']),
956
+ cmap=colormap), 'label': label}
957
+
958
+ def color_tanks(
959
+ self, scada_data: Optional[ScadaData] = None,
960
+ statistic: str = 'mean',
961
+ pit: Optional[Union[int, Tuple[int]]] = None,
962
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
963
+ colormap: str = 'viridis', show_colorbar: bool = False) -> None:
964
+ """
965
+ Colors the tanks in the water distribution network based on the SCADA
966
+ tank volume data and the specified statistic.
967
+
968
+ This method either takes or generates SCADA data, applies a statistic
969
+ to the tank volume data, optionally groups the results and prepares
970
+ them to be either displayed statically ot animated.
971
+
972
+ Parameters
973
+ ----------
974
+ scada_data : :class:`~epyt_flow.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`.
978
+ statistic : `str`, optional
979
+ The statistic to calculate for the data. Can be 'mean', 'min',
980
+ 'max', or 'time_step'. Default is 'mean'.
981
+ pit : `int`, `tuple(int, int)`, optional
982
+ The point in time or range of time steps for the 'time_step'
983
+ statistic. If a tuple is provided, it should contain two integers
984
+ representing the start and end time steps. A tuple is necessary to
985
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
986
+ method. Default is `None`.
987
+ intervals : `int`, `list[int]`, `list[float]`, optional
988
+ If provided, the data will be grouped into intervals. It can be an
989
+ integer specifying the number of groups or a list of boundary
990
+ points. Default is `None`.
991
+ colormap : `str`, optional
992
+ The colormap to use for visualizing tank values. Default is
993
+ 'viridis'.
994
+ show_colorbar : `bool`, optional
995
+ If `True`, a colorbar will be displayed on the plot to indicate the
996
+ range of tank volume values. Default is `False`.
997
+
998
+ Raises
999
+ ------
1000
+ ValueError
1001
+ If `pit` is not correctly provided for the 'time_step' statistic.
1002
+
1003
+ """
1004
+ self.pump_parameters.update({'node_size': 10, 'cmap': colormap})
1005
+
1006
+ if scada_data:
1007
+ self.scada_data = scada_data
1008
+ elif not self.scada_data:
1009
+ self.scada_data = self.__scenario.run_simulation()
1010
+
1011
+ values = self.scada_data.tanks_volume_data_raw
1012
+
1013
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
1014
+ 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):
1023
+ if frame > values.shape[0] - 1:
1024
+ 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
1034
+ 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)})
1042
+
1043
+ if show_colorbar:
1044
+ if statistic == 'time_step':
1045
+ label = 'tank volume'.capitalize() + ' at timestep ' + str(pit)
1046
+ else:
1047
+ label = str(statistic).capitalize() + ' ' + 'tank volume'
1048
+ self.colorbars['tanks'] = {'mappable': plt.cm.ScalarMappable(
1049
+ norm=mpl.colors.Normalize(vmin=self.tank_parameters['vmin'],
1050
+ vmax=self.tank_parameters['vmax']),
1051
+ cmap=colormap), 'label': label}
1052
+
1053
+ def color_valves(
1054
+ self, scada_data: Optional[ScadaData] = None,
1055
+ statistic: str = 'mean',
1056
+ pit: Optional[Union[int, Tuple[int]]] = None,
1057
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
1058
+ colormap: str = 'viridis', show_colorbar: bool = False) -> None:
1059
+ """
1060
+ Colors the valves in the water distribution network based on SCADA
1061
+ valve state data and the specified statistic.
1062
+
1063
+ This method either takes or generates SCADA data, applies a statistic
1064
+ to the valve state data, optionally groups the results and prepares
1065
+ them to be either displayed statically ot animated.
1066
+
1067
+ Parameters
1068
+ ----------
1069
+ scada_data : :class:`~epyt_flow.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`.
1072
+ statistic : `str`, optional
1073
+ The statistic to calculate for the data. Can be 'mean', 'min',
1074
+ 'max', or 'time_step'. Default is 'mean'.
1075
+ pit : `int`, `tuple(int)`, optional
1076
+ The point in time or range of time steps for the 'time_step'
1077
+ statistic. If a tuple is provided, it should contain two integers
1078
+ representing the start and end time steps. A tuple is necessary to
1079
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
1080
+ method. Default is `None`.
1081
+ intervals : `int`, `list[int]`, `list[float]`, optional
1082
+ If provided, the data will be grouped into intervals. It can be an
1083
+ integer specifying the number of groups or a list of
1084
+ boundary points. Default is `None`.
1085
+ colormap : `str`, optional
1086
+ The colormap to use for visualizing valve state values. Default is
1087
+ 'viridis'.
1088
+ show_colorbar : `bool`, optional
1089
+ If `True`, a colorbar will be displayed on the plot to indicate the
1090
+ range of valve state values. Default is `False`.
1091
+
1092
+ Raises
1093
+ ------
1094
+ ValueError
1095
+ If `pit` is not correctly provided for the 'time_step' statistic.
1096
+
1097
+ """
1098
+
1099
+ self.valve_parameters.update({'node_size': 15, 'cmap': colormap})
1100
+
1101
+ if scada_data:
1102
+ self.scada_data = scada_data
1103
+ elif not self.scada_data:
1104
+ self.scada_data = self.__scenario.run_simulation()
1105
+
1106
+ values = self.scada_data.valves_state_data_raw
1107
+
1108
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
1109
+ 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):
1118
+ if frame > values.shape[0] - 1:
1119
+ 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
1129
+ 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)})
1137
+
1138
+ if show_colorbar:
1139
+ if statistic == 'time_step':
1140
+ label = 'valve state'.capitalize() + ' at timestep ' + str(pit)
1141
+ else:
1142
+ label = str(statistic).capitalize() + ' ' + 'valve state'
1143
+ self.colorbars['valves'] = {'mappable': plt.cm.ScalarMappable(
1144
+ norm=mpl.colors.Normalize(vmin=self.valve_parameters['vmin'],
1145
+ vmax=self.valve_parameters['vmax']),
1146
+ cmap=colormap), 'label': label}
1147
+
1148
+ def resize_links(
1149
+ self, scada_data: Optional[ScadaData] = None,
1150
+ parameter: str = 'flow_rate', statistic: str = 'mean',
1151
+ line_widths: Tuple[int] = (1, 2),
1152
+ pit: Optional[Union[int, Tuple[int]]] = None,
1153
+ intervals: Optional[Union[int, List[Union[int, float]]]] = None,
1154
+ conversion: Optional[dict] = None) -> None:
1155
+ """
1156
+ Resizes the width of the links (pipes) in the water distribution
1157
+ network based on SCADA data and the specified parameters.
1158
+
1159
+ This method either takes or generates SCADA data, applies a statistic,
1160
+ optionally groups the results and prepares them to be either displayed
1161
+ statically ot animated as link width.
1162
+
1163
+ Parameters
1164
+ ----------
1165
+ scada_data : :class:`~epyt_flow.scada.scada_data.ScadaData`, optional
1166
+ The SCADA data object. If `None`, a simulation will be run to
1167
+ generate it. Default is `None`.
1168
+ parameter : `str`, optional
1169
+ The data used to resize to. Default is 'flow_rate'.
1170
+ statistic : `str`, optional
1171
+ The statistic to calculate for the data. Can be 'mean', 'min',
1172
+ 'max', or 'time_step'. Default is 'mean'.
1173
+ line_widths : `tuple(int, int)`, optional
1174
+ A tuple specifying the range of line widths to use when resizing
1175
+ links based on the data. Default is (1, 2).
1176
+ pit : `int` or `tuple(int, int)`, optional
1177
+ The point in time or range of time steps for the 'time_step'
1178
+ statistic. If a tuple is provided, it should contain two integers
1179
+ representing the start and end time steps. A tuple is necessary to
1180
+ process the data for the :meth:`~ScenarioVisualizer.show_animation`
1181
+ method. Default is `None`.
1182
+ intervals : `int` or `list[int]` or `list[float]`, optional
1183
+ If provided, the data will be grouped into intervals. It can be an
1184
+ integer specifying the number of groups or a list of boundary
1185
+ points. Default is `None`.
1186
+ conversion : `dict`, optional
1187
+ A dictionary of conversion parameters to convert SCADA data units.
1188
+ Default is `None`.
1189
+ """
1190
+
1191
+ if statistic == 'time_step' and isinstance(pit, tuple) and len(
1192
+ 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:
1203
+ 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)))
1215
+ 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)})
1221
+
1222
+ def hide_nodes(self) -> None:
1223
+ """
1224
+ Hides all nodes (junctions) in the water distribution network
1225
+ visualization.
1226
+
1227
+ This method clears the node list from the `junction_parameters`
1228
+ dictionary, effectively removing all nodes from view in the current
1229
+ visualization.
1230
+ """
1231
+ self.junction_parameters['nodelist'] = []
1232
+
1233
+ def highlight_sensor_config(self) -> None:
1234
+ """
1235
+ Highlights nodes and links that have sensors in the sensor_config in
1236
+ the water distribution network visualization.
1237
+
1238
+ This method identifies nodes and links equipped with different types of
1239
+ sensors from the :class:`~epyt_flow.simulation.sensor_config.SensorConfig` and
1240
+ updates their visual appearance. Nodes with sensors are highlighted
1241
+ with an orange border, while links with sensors are displayed with a
1242
+ dashed line style.
1243
+ """
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)
1253
+
1254
+ node_edges = [
1255
+ (17, 163, 252) if node in highlighted_nodes else (0, 0, 0) for node
1256
+ in self.topology]
1257
+ pipe_style = ['dashed' if link in highlighted_links else 'solid' for
1258
+ link in self.topology]
1259
+
1260
+ self.junction_parameters.update(
1261
+ {'linewidths': 1, 'edgecolors': node_edges})
1262
+ self.pipe_parameters.update({'style': pipe_style})
25
1263
 
26
- def plot_topology(self, show_sensor_config: bool = False, export_to_file: str = None) -> None:
1264
+ @deprecated(reason="This function will be removed in feature versions, "
1265
+ "please use show_plot() instead.")
1266
+ def plot_topology(self, show_sensor_config: bool = False,
1267
+ export_to_file: str = None) -> None:
27
1268
  """
28
- Plots the topology of the water distribution network in the given scenario.
1269
+ Plots the topology of the water distribution network in the given
1270
+ scenario.
29
1271
 
30
1272
  Parameters
31
1273
  ----------
@@ -35,7 +1277,8 @@ class ScenarioVisualizer():
35
1277
  The default is False.
36
1278
  export_to_file : `str`, optional
37
1279
  Path to the file where the visualization will be stored.
38
- If None, visualization will be just shown but NOT be stored anywhere.
1280
+ If None, visualization will be just shown but NOT be stored
1281
+ anywhere.
39
1282
 
40
1283
  The default is None.
41
1284
  """
@@ -48,12 +1291,15 @@ class ScenarioVisualizer():
48
1291
  highlighted_links = []
49
1292
 
50
1293
  sensor_config = self.__scenario.sensor_config
51
- highlighted_nodes += sensor_config.pressure_sensors \
52
- + sensor_config.demand_sensors + sensor_config.quality_node_sensors
53
- highlighted_links += sensor_config.flow_sensors + sensor_config.quality_link_sensors
1294
+ highlighted_nodes += (sensor_config.pressure_sensors
1295
+ + sensor_config.demand_sensors
1296
+ + sensor_config.quality_node_sensors)
1297
+ highlighted_links += (sensor_config.flow_sensors
1298
+ + sensor_config.quality_link_sensors)
54
1299
 
55
1300
  self.__scenario.epanet_api.plot(highlightlink=highlighted_links,
56
- highlightnode=highlighted_nodes, figure=False)
1301
+ highlightnode=highlighted_nodes,
1302
+ figure=False)
57
1303
 
58
1304
  if export_to_file is not None:
59
1305
  plt.savefig(export_to_file, transparent=True, bbox_inches='tight')