epyt-flow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
- epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
- epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
- epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
- epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
- epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
- epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
- epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
- epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
- epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
- epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
- epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
- epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
- epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
- epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
- epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
- epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
- epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
- epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
- epyt_flow/EPANET/compile.sh +4 -0
- epyt_flow/VERSION +1 -0
- epyt_flow/__init__.py +24 -0
- epyt_flow/data/__init__.py +0 -0
- epyt_flow/data/benchmarks/__init__.py +11 -0
- epyt_flow/data/benchmarks/batadal.py +257 -0
- epyt_flow/data/benchmarks/batadal_data.py +28 -0
- epyt_flow/data/benchmarks/battledim.py +473 -0
- epyt_flow/data/benchmarks/battledim_data.py +51 -0
- epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
- epyt_flow/data/benchmarks/leakdb.py +592 -0
- epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
- epyt_flow/data/benchmarks/water_usage.py +123 -0
- epyt_flow/data/networks.py +650 -0
- epyt_flow/gym/__init__.py +4 -0
- epyt_flow/gym/control_gyms.py +47 -0
- epyt_flow/gym/scenario_control_env.py +101 -0
- epyt_flow/metrics.py +404 -0
- epyt_flow/models/__init__.py +2 -0
- epyt_flow/models/event_detector.py +31 -0
- epyt_flow/models/sensor_interpolation_detector.py +118 -0
- epyt_flow/rest_api/__init__.py +4 -0
- epyt_flow/rest_api/base_handler.py +70 -0
- epyt_flow/rest_api/res_manager.py +95 -0
- epyt_flow/rest_api/scada_data_handler.py +476 -0
- epyt_flow/rest_api/scenario_handler.py +352 -0
- epyt_flow/rest_api/server.py +106 -0
- epyt_flow/serialization.py +438 -0
- epyt_flow/simulation/__init__.py +5 -0
- epyt_flow/simulation/events/__init__.py +6 -0
- epyt_flow/simulation/events/actuator_events.py +259 -0
- epyt_flow/simulation/events/event.py +81 -0
- epyt_flow/simulation/events/leakages.py +404 -0
- epyt_flow/simulation/events/sensor_faults.py +267 -0
- epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
- epyt_flow/simulation/events/sensor_reading_event.py +170 -0
- epyt_flow/simulation/events/system_event.py +88 -0
- epyt_flow/simulation/parallel_simulation.py +147 -0
- epyt_flow/simulation/scada/__init__.py +3 -0
- epyt_flow/simulation/scada/advanced_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +1589 -0
- epyt_flow/simulation/scada/scada_data_export.py +255 -0
- epyt_flow/simulation/scenario_config.py +608 -0
- epyt_flow/simulation/scenario_simulator.py +1897 -0
- epyt_flow/simulation/scenario_visualizer.py +61 -0
- epyt_flow/simulation/sensor_config.py +1289 -0
- epyt_flow/topology.py +290 -0
- epyt_flow/uncertainty/__init__.py +3 -0
- epyt_flow/uncertainty/model_uncertainty.py +302 -0
- epyt_flow/uncertainty/sensor_noise.py +73 -0
- epyt_flow/uncertainty/uncertainties.py +555 -0
- epyt_flow/uncertainty/utils.py +206 -0
- epyt_flow/utils.py +306 -0
- epyt_flow-0.1.0.dist-info/LICENSE +21 -0
- epyt_flow-0.1.0.dist-info/METADATA +139 -0
- epyt_flow-0.1.0.dist-info/RECORD +131 -0
- epyt_flow-0.1.0.dist-info/WHEEL +5 -0
- epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides some helper functions regarding the implementation of uncertainty.
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.ndimage import gaussian_filter1d
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def smoothing(pattern: np.ndarray, sigma: float = 10.) -> np.ndarray:
|
|
9
|
+
"""
|
|
10
|
+
Smoothes a given pattern by applying a Gaussian filter.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
pattern : `numpy.ndarray`
|
|
15
|
+
The original pattern
|
|
16
|
+
sigma : `float`, optional
|
|
17
|
+
Standard deviation for the Gaussian filter.
|
|
18
|
+
|
|
19
|
+
The default is 10.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
`numpy.ndarray`
|
|
24
|
+
The smoothed pattern.
|
|
25
|
+
"""
|
|
26
|
+
return gaussian_filter1d(pattern, sigma=sigma)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def scale_to_range(pattern: np.ndarray, min_value: float, max_value: float) -> np.ndarray:
|
|
30
|
+
"""
|
|
31
|
+
Scales a given pattern to an interval.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
pattern : `numpy.ndarray`
|
|
36
|
+
The pattern to be scaled.
|
|
37
|
+
min_value : `float`
|
|
38
|
+
Lower bound of the pattern.
|
|
39
|
+
max_value : `float`
|
|
40
|
+
Upper bound of the pattern.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
`numpy.ndarray`
|
|
45
|
+
The scaled pattern.
|
|
46
|
+
"""
|
|
47
|
+
if min_value is None or max_value is None:
|
|
48
|
+
return pattern
|
|
49
|
+
|
|
50
|
+
min_pattern_val = np.min(pattern)
|
|
51
|
+
max_pattern_val = np.max(pattern)
|
|
52
|
+
|
|
53
|
+
return [(x - min_pattern_val) / (max_pattern_val - min_pattern_val) * (max_value - min_value)
|
|
54
|
+
+ min_value for x in pattern]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def generate_random_gaussian_noise(n_samples: int):
|
|
58
|
+
"""
|
|
59
|
+
Generates Gaussian noise using a random mean ([0,1]) and random standard deviation ([0,1]).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
n_samples : `int`
|
|
64
|
+
Number of random samples.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
`numpy.ndarray`
|
|
69
|
+
Gaussian noise.
|
|
70
|
+
"""
|
|
71
|
+
return np.random.normal(np.random.rand(), np.random.rand(), size=n_samples)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def generate_deep_random_gaussian_noise(n_samples: int, mean: float = None):
|
|
75
|
+
"""
|
|
76
|
+
Generates random Gaussian noise where the standard deviations (and mean) are changing over time.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
n_samples : `int`
|
|
81
|
+
Number of random samples.
|
|
82
|
+
mean : `float`, optional
|
|
83
|
+
Fixed mean at all points in time.
|
|
84
|
+
If None, random means are generated.
|
|
85
|
+
|
|
86
|
+
The default is None.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
`numpy.ndarray`
|
|
91
|
+
Random Gaussian noise.
|
|
92
|
+
"""
|
|
93
|
+
noise = []
|
|
94
|
+
|
|
95
|
+
if mean is None:
|
|
96
|
+
mean = create_deep_random_pattern(n_samples, min_value=-1., max_value=1.)
|
|
97
|
+
else:
|
|
98
|
+
mean = [mean] * n_samples
|
|
99
|
+
rand_std = create_deep_random_pattern(n_samples)
|
|
100
|
+
noise = np.array([np.random.normal(m, s) for m, s in zip(mean, rand_std)])
|
|
101
|
+
|
|
102
|
+
return noise
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_deep_random_pattern(n_samples: int, min_value: float = 0., max_value: float = 1.,
|
|
106
|
+
init_value: float = None) -> np.ndarray:
|
|
107
|
+
"""
|
|
108
|
+
Generates a random pattern.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
n_samples : `int`
|
|
113
|
+
Number of random samples -- i.e. length of the pattern.
|
|
114
|
+
min_value : `float`, optional
|
|
115
|
+
Lower bound of the pattern.
|
|
116
|
+
|
|
117
|
+
The default is zero.
|
|
118
|
+
max_value : `float`, optional
|
|
119
|
+
Upper bound of the pattern.
|
|
120
|
+
|
|
121
|
+
The default is one.
|
|
122
|
+
init_value : `float`, optional
|
|
123
|
+
Value of the first sample in the pattern.
|
|
124
|
+
If None, a random value is used.
|
|
125
|
+
|
|
126
|
+
The default is None.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
`numpy.ndarray`
|
|
131
|
+
Random pattern.
|
|
132
|
+
"""
|
|
133
|
+
pattern = []
|
|
134
|
+
start_value = init_value
|
|
135
|
+
|
|
136
|
+
while len(pattern) < n_samples:
|
|
137
|
+
if len(pattern) != 0:
|
|
138
|
+
start_value = pattern[-1]
|
|
139
|
+
|
|
140
|
+
pattern += _create_deep_random_pattern(start_value, min_value=min_value,
|
|
141
|
+
max_value=max_value)
|
|
142
|
+
|
|
143
|
+
pattern = pattern[:n_samples]
|
|
144
|
+
|
|
145
|
+
# Scaling to value range
|
|
146
|
+
pattern = scale_to_range(pattern, min_value, max_value)
|
|
147
|
+
|
|
148
|
+
return np.array(pattern)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _create_deep_random_pattern(start_value: float = None, min_length: int = 2, max_length: int = 5,
|
|
152
|
+
min_value: float = None, max_value: float = None) -> np.ndarray:
|
|
153
|
+
"""
|
|
154
|
+
Generates a random pattern of random length.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
start_value : `float`, optional
|
|
159
|
+
First value in the pattern.
|
|
160
|
+
If None, a random value is used.
|
|
161
|
+
|
|
162
|
+
The default is None.
|
|
163
|
+
min_length : `int`, optional
|
|
164
|
+
Minium length of the pattern.
|
|
165
|
+
|
|
166
|
+
The default is 2.
|
|
167
|
+
max_length : `int`
|
|
168
|
+
Maximum length of the pattern.
|
|
169
|
+
|
|
170
|
+
The default is 5.
|
|
171
|
+
min_value : `float`, optional
|
|
172
|
+
Lower bound of the pattern.
|
|
173
|
+
|
|
174
|
+
The default is zero.
|
|
175
|
+
max_value : `float`, optional
|
|
176
|
+
Upper bound of the pattern.
|
|
177
|
+
|
|
178
|
+
The default is one.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
`numpy.ndarray`
|
|
183
|
+
Random pattern.
|
|
184
|
+
"""
|
|
185
|
+
pattern = []
|
|
186
|
+
|
|
187
|
+
# Random parameters of pattern
|
|
188
|
+
if start_value is None:
|
|
189
|
+
start_value = np.random.rand()
|
|
190
|
+
length = np.random.randint(low=min_length, high=max_length)
|
|
191
|
+
vec = np.random.choice([-.1, .1])
|
|
192
|
+
|
|
193
|
+
# Generate pattern
|
|
194
|
+
cur_value = start_value
|
|
195
|
+
pattern.append(start_value)
|
|
196
|
+
|
|
197
|
+
for _ in range(length):
|
|
198
|
+
cur_value = cur_value + np.random.rand() * vec
|
|
199
|
+
pattern.append(cur_value)
|
|
200
|
+
if min_value is not None and max_value is not None:
|
|
201
|
+
if cur_value < min_value:
|
|
202
|
+
vec = .1
|
|
203
|
+
elif cur_value > max_value:
|
|
204
|
+
vec = -.1
|
|
205
|
+
|
|
206
|
+
return pattern
|
epyt_flow/utils.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides helper functions.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import math
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import requests
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def time_points_to_one_hot_encoding(time_points: list[int], total_length: int) -> list[int]:
|
|
16
|
+
"""
|
|
17
|
+
Converts a list of time points into a one-hot-encoding.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
time_points : `list[int]`
|
|
22
|
+
Time points to be one-hot-encoded.
|
|
23
|
+
total_length : `int`
|
|
24
|
+
Length of final one-hot-encoding.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
`list[int]`
|
|
29
|
+
One-hot-encoded time points.
|
|
30
|
+
"""
|
|
31
|
+
results = [0] * total_length
|
|
32
|
+
|
|
33
|
+
for t in time_points:
|
|
34
|
+
results[t] = 1
|
|
35
|
+
|
|
36
|
+
return results
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def volume_to_level(tank_volume: float, tank_diameter: float) -> float:
|
|
40
|
+
"""
|
|
41
|
+
Computes the water level in a tank containing a given volume of water.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
tank_volume : `float`
|
|
46
|
+
Water volume in the tank.
|
|
47
|
+
tank_diameter : `float`
|
|
48
|
+
Diameter of the tank.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
`float`
|
|
53
|
+
Water level in tank.
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(tank_volume, float):
|
|
56
|
+
raise TypeError("'tank_volume' must be an instace of 'float' " +
|
|
57
|
+
f"but not of '{type(tank_volume)}'")
|
|
58
|
+
if tank_volume < 0:
|
|
59
|
+
raise ValueError("'tank_volume' can not be negative")
|
|
60
|
+
if not isinstance(tank_diameter, float):
|
|
61
|
+
raise TypeError("'tank_diameter' must be an instace of 'float' " +
|
|
62
|
+
f"but not of '{type(tank_diameter)}'")
|
|
63
|
+
if tank_diameter <= 0:
|
|
64
|
+
raise ValueError("'tank_diameter' must be greater than zero")
|
|
65
|
+
|
|
66
|
+
return (4. / (math.pow(tank_diameter, 2) * math.pi)) * tank_volume
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_label: str = None,
|
|
70
|
+
y_axis_label: str = None, show: bool = True) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Plots a single or multiple time series.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
data : `numpy.ndarray`
|
|
77
|
+
Time series data -- each row in `data` corresponds to a complete time series.
|
|
78
|
+
labels : `list[str]`, optional
|
|
79
|
+
Labels for each time series in `data`.
|
|
80
|
+
If None, no labels are shown.
|
|
81
|
+
|
|
82
|
+
The default is None.
|
|
83
|
+
x_axis_label : `str`, optional
|
|
84
|
+
X axis label.
|
|
85
|
+
|
|
86
|
+
The default is None.
|
|
87
|
+
y_axis_label : `str`, optional
|
|
88
|
+
Y axis label.
|
|
89
|
+
|
|
90
|
+
The default is None.
|
|
91
|
+
show : `bool`, optional
|
|
92
|
+
If True, the plot/figure is shown in a window.
|
|
93
|
+
|
|
94
|
+
The default is True.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(data, np.ndarray):
|
|
97
|
+
raise TypeError(f"'data' must be an instance of 'numpy.ndarray' but not of '{type(data)}'")
|
|
98
|
+
if len(data.shape) != 2:
|
|
99
|
+
raise ValueError("'data' must be a 2d array where each row corresponds to a time series " +
|
|
100
|
+
"-- use '.reshape(1, -1)' in case of single time series")
|
|
101
|
+
if labels is not None:
|
|
102
|
+
if not isinstance(labels, list) or not all(isinstance(label, str) for label in labels):
|
|
103
|
+
raise TypeError("'labels' must be a instance of 'list[str]'")
|
|
104
|
+
if x_axis_label is not None:
|
|
105
|
+
if not isinstance(x_axis_label, str):
|
|
106
|
+
raise TypeError("'x_axis_label' must be an instance of 'str' " +
|
|
107
|
+
f"but not of '{type(x_axis_label)}'")
|
|
108
|
+
if y_axis_label is not None:
|
|
109
|
+
if not isinstance(y_axis_label, str):
|
|
110
|
+
raise TypeError("'y_axis_label' must be an instance of 'str' " +
|
|
111
|
+
f"but not of '{type(y_axis_label)}'")
|
|
112
|
+
if not isinstance(show, bool):
|
|
113
|
+
raise TypeError(f"'show' must be an instance of 'bool' but not of '{type(show)}'")
|
|
114
|
+
|
|
115
|
+
plt.figure()
|
|
116
|
+
|
|
117
|
+
labels = labels if labels is not None else [None] * data.shape[0]
|
|
118
|
+
|
|
119
|
+
for i in range(data.shape[0]):
|
|
120
|
+
plt.plot(data[i, :], ".-", label=labels[i])
|
|
121
|
+
|
|
122
|
+
if not any(label is None for label in labels):
|
|
123
|
+
plt.legend()
|
|
124
|
+
|
|
125
|
+
if x_axis_label is not None:
|
|
126
|
+
plt.xlabel(x_axis_label)
|
|
127
|
+
if y_axis_label is not None:
|
|
128
|
+
plt.ylabel(y_axis_label)
|
|
129
|
+
|
|
130
|
+
if show is True:
|
|
131
|
+
plt.show()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
|
|
135
|
+
confidence_interval: np.ndarray = None, show: bool = True) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Plots the prediction (e.g. forecast) of *single* time series together with the
|
|
138
|
+
ground truth time series. In addition, confidence intervals can be plotted as well.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
y : `numpy.ndarray`
|
|
143
|
+
Ground truth values.
|
|
144
|
+
y_pred : `numpy.ndarray`
|
|
145
|
+
Predicted values.
|
|
146
|
+
confidence_interval : `numpy.ndarray`, optional
|
|
147
|
+
Confidence interval (upper and lower value) for each prediction in `y_pred`.
|
|
148
|
+
If not None, the confidence interval is plotted as well.
|
|
149
|
+
|
|
150
|
+
The default is None.
|
|
151
|
+
show : `bool`, optional
|
|
152
|
+
If True, the plot/figure is shown in a window.
|
|
153
|
+
|
|
154
|
+
The default is True.
|
|
155
|
+
"""
|
|
156
|
+
if not isinstance(y_pred, np.ndarray):
|
|
157
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
158
|
+
f"but not of '{type(y_pred)}'")
|
|
159
|
+
if not isinstance(y, np.ndarray):
|
|
160
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
161
|
+
f"but not of '{type(y)}'")
|
|
162
|
+
if y_pred.shape != y.shape:
|
|
163
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
164
|
+
if len(y_pred.shape) != 1:
|
|
165
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
166
|
+
if len(y.shape) != 1:
|
|
167
|
+
raise ValueError("'y' must be a 1d array")
|
|
168
|
+
if not isinstance(show, bool):
|
|
169
|
+
raise TypeError(f"'show' must be an instance of 'bool' but not of '{type(show)}'")
|
|
170
|
+
|
|
171
|
+
plt.figure()
|
|
172
|
+
|
|
173
|
+
if confidence_interval is not None:
|
|
174
|
+
plt.fill_between(range(len(y_pred)),
|
|
175
|
+
y_pred - confidence_interval[0],
|
|
176
|
+
y_pred + confidence_interval[1],
|
|
177
|
+
alpha=0.5)
|
|
178
|
+
plt.plot(y_pred, ".-", label="Prediction")
|
|
179
|
+
plt.plot(y, ".-", label="Ground truth")
|
|
180
|
+
plt.legend()
|
|
181
|
+
|
|
182
|
+
if show is True:
|
|
183
|
+
plt.show()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def download_if_necessary(download_path: str, url: str, verbose: bool = True) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Downloads a file from a given URL if it does not already exist in a given path.
|
|
189
|
+
|
|
190
|
+
Note that if the path (folder) does not already exist, it will be created.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
download_path : `str`
|
|
195
|
+
Local path to the file -- if this path does not exist, the file will be downloaded from
|
|
196
|
+
the provided 'url' and stored in 'download_dir'.
|
|
197
|
+
url : `str`
|
|
198
|
+
Web-URL.
|
|
199
|
+
verbose : `bool`, optional
|
|
200
|
+
If True, a progress bar is shown while downloading the file.
|
|
201
|
+
|
|
202
|
+
The default is True.
|
|
203
|
+
"""
|
|
204
|
+
folder_path = str(Path(download_path).parent.absolute())
|
|
205
|
+
create_path_if_not_exist(folder_path)
|
|
206
|
+
|
|
207
|
+
if not os.path.isfile(download_path):
|
|
208
|
+
response = requests.get(url, stream=verbose, allow_redirects=True, timeout=1000)
|
|
209
|
+
|
|
210
|
+
if verbose is True:
|
|
211
|
+
content_length = int(response.headers.get('content-length', 0))
|
|
212
|
+
with open(download_path, "wb") as file, tqdm(desc=download_path,
|
|
213
|
+
total=content_length,
|
|
214
|
+
unit='B',
|
|
215
|
+
unit_scale=True,
|
|
216
|
+
unit_divisor=1024) as progress_bar:
|
|
217
|
+
for data in response.iter_content(chunk_size=1024):
|
|
218
|
+
size = file.write(data)
|
|
219
|
+
progress_bar.update(size)
|
|
220
|
+
else:
|
|
221
|
+
with open(download_path, "wb") as f_out:
|
|
222
|
+
f_out.write(response.content)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_path_if_not_exist(path_in: str) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Creates a directory and all its parent directories if they do not already exist.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
path_in : `str`
|
|
232
|
+
Path to be created.
|
|
233
|
+
"""
|
|
234
|
+
Path(path_in).mkdir(parents=True, exist_ok=True)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def unpack_zip_archive(f_in: str, folder_out: str) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Unpacks a .zip archive.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
f_in : `str`
|
|
244
|
+
Path to the .zip file.
|
|
245
|
+
folder_out : `str`
|
|
246
|
+
Path to the folder where the unpacked files will be stored.
|
|
247
|
+
"""
|
|
248
|
+
with zipfile.ZipFile(f_in, "r") as f:
|
|
249
|
+
f.extractall(folder_out)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_temp_folder() -> str:
|
|
253
|
+
"""
|
|
254
|
+
Gets a path to a temporary folder -- i.e. a folder for storing temporary files.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
`str`
|
|
259
|
+
Path to a temporary folder.
|
|
260
|
+
"""
|
|
261
|
+
return tempfile.gettempdir()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def to_seconds(days: int = None, hours: int = None, minutes: int = None) -> int:
|
|
265
|
+
"""
|
|
266
|
+
Converts a timestamp (i.e. days, hours, minutes) into seconds.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
days : `int`, optional
|
|
271
|
+
Days.
|
|
272
|
+
hours : `int`, optional
|
|
273
|
+
Hours.
|
|
274
|
+
minutes : `int`, optional
|
|
275
|
+
Minutes.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
`int`
|
|
280
|
+
Timestamp in seconds.
|
|
281
|
+
"""
|
|
282
|
+
sec = 0
|
|
283
|
+
|
|
284
|
+
if days is not None:
|
|
285
|
+
if not isinstance(days, int):
|
|
286
|
+
raise TypeError(f"'days' must be an instance of 'int' but not of {type(days)}")
|
|
287
|
+
if days <= 0:
|
|
288
|
+
raise ValueError("'days' must be positive")
|
|
289
|
+
|
|
290
|
+
sec += 24*60*60 * days
|
|
291
|
+
if hours is not None:
|
|
292
|
+
if not isinstance(hours, int):
|
|
293
|
+
raise TypeError(f"'hours' must be an instance of 'int' but not of {type(hours)}")
|
|
294
|
+
if hours <= 0:
|
|
295
|
+
raise ValueError("'hours' must be positive")
|
|
296
|
+
|
|
297
|
+
sec += 60*60 * hours
|
|
298
|
+
if minutes is not None:
|
|
299
|
+
if not isinstance(minutes, int):
|
|
300
|
+
raise TypeError(f"'minutes' must be an instance of 'int' but not of {type(minutes)}")
|
|
301
|
+
if minutes <= 0:
|
|
302
|
+
raise ValueError("'minutes' must be positive")
|
|
303
|
+
|
|
304
|
+
sec += 60 * minutes
|
|
305
|
+
|
|
306
|
+
return sec
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 EPyT-Flow Developers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: epyt-flow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EPyT-Flow -- EPANET Python Toolkit - Flow
|
|
5
|
+
Author-email: André Artelt <aartelt@techfak.uni-bielefeld.de>, "Marios S. Kyriakou" <kiriakou.marios@ucy.ac.cy>, "Stelios G. Vrachimis" <vrachimis.stelios@ucy.ac.cy>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/WaterFutures/EPyT-Flow
|
|
8
|
+
Project-URL: Documentation, https://epytflow.readthedocs.io/en/latest/
|
|
9
|
+
Project-URL: Repository, https://github.com/WaterFutures/EPyT-Flow.git
|
|
10
|
+
Project-URL: Issues, https://github.com/WaterFutures/EPyT-Flow/issues
|
|
11
|
+
Keywords: epanet,water,networks,hydraulics,quality,simulations
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: epyt >=1.1.3
|
|
24
|
+
Requires-Dist: requests >=2.31.0
|
|
25
|
+
Requires-Dist: scipy >=1.11.4
|
|
26
|
+
Requires-Dist: u-msgpack-python >=2.8.0
|
|
27
|
+
Requires-Dist: networkx >=3.2.1
|
|
28
|
+
Requires-Dist: scikit-learn >=1.4.0
|
|
29
|
+
Requires-Dist: tqdm >=4.66.2
|
|
30
|
+
Requires-Dist: openpyxl >=3.1.2
|
|
31
|
+
Requires-Dist: falcon >=3.1.3
|
|
32
|
+
Requires-Dist: multiprocess >=0.70.16
|
|
33
|
+
Requires-Dist: psutil
|
|
34
|
+
|
|
35
|
+
# EPyT-Flow -- EPANET Python Toolkit - Flow
|
|
36
|
+
|
|
37
|
+
EPyT-Flow is a Python package building on top of [EPyT](https://github.com/OpenWaterAnalytics/EPyT)
|
|
38
|
+
for providing easy access to water distribution network simulations.
|
|
39
|
+
It aims to provide a high-level interface for the easy generation of hydraulic and water quality scenario data.
|
|
40
|
+
However, it also provides access to low-level functions by [EPANET](https://github.com/USEPA/EPANET2.2)
|
|
41
|
+
and [EPANET-MSX](https://github.com/USEPA/EPANETMSX/).
|
|
42
|
+
|
|
43
|
+
EPyT-Flow provides easy access to popular benchmark data sets for event detection and localization.
|
|
44
|
+
Furthermore, it also provides an environment for developing and testing control algorithms.
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
EPyT-Flow supports Python 3.9 - 3.12
|
|
51
|
+
|
|
52
|
+
Note that [EPANET and EPANET-MSX sources](epyt_flow/EPANET/) are compiled and overrite the binaries
|
|
53
|
+
shipped by EPyT IF EPyT-Flow is installed on a Linux system. By this we not only aim to achieve
|
|
54
|
+
a better performance of the simulations but also avoid any compatibility problems of pre-compiled binaries.
|
|
55
|
+
|
|
56
|
+
### PyPI
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install epyt-flow
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Git
|
|
63
|
+
Download or clone the repository:
|
|
64
|
+
```
|
|
65
|
+
git clone https://github.com/WaterFutures/EPyT-Flow.git
|
|
66
|
+
cd EPyT-Flow
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Install all requirements as listed in [REQUIREMENTS.txt](REQUIREMENTS.txt):
|
|
70
|
+
```
|
|
71
|
+
pip install -r REQUIREMENTS.txt
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Install the toolbox:
|
|
75
|
+
```
|
|
76
|
+
pip install .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick Example
|
|
80
|
+
|
|
81
|
+
<a target="_blank" href="https://colab.research.google.com/github/WaterFutures/EPyT-Flow/blob/main/docs/examples/basic_usage.ipynb">
|
|
82
|
+
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
|
|
83
|
+
</a>
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from epyt_flow.data.benchmarks import load_leakdb_scenarios
|
|
87
|
+
from epyt_flow.simulation import ScenarioSimulator
|
|
88
|
+
from epyt_flow.utils import to_seconds
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
# Load first Hanoi scenario from LeakDB
|
|
93
|
+
network_config, = load_leakdb_scenarios(scenarios_id=["1"], use_net1=False)
|
|
94
|
+
|
|
95
|
+
# Create scenario
|
|
96
|
+
with ScenarioSimulator(scenario_config=network_config) as sim:
|
|
97
|
+
# Set simulation duration to two days
|
|
98
|
+
sim.set_general_parameters(simulation_duration=to_seconds(days=2))
|
|
99
|
+
|
|
100
|
+
# Place pressure sensors at nodes "13", "16", "22", and "30"
|
|
101
|
+
sim.set_pressure_sensors(sensor_locations=["13", "16", "22", "30"])
|
|
102
|
+
|
|
103
|
+
# Place a flow sensor at link/pipe "1"
|
|
104
|
+
sim.set_flow_sensors(sensor_locations=["1"])
|
|
105
|
+
|
|
106
|
+
# Run entire simulation
|
|
107
|
+
scada_data = sim.run_simulation()
|
|
108
|
+
|
|
109
|
+
# Show sensor readings over the entire simulation
|
|
110
|
+
print(f"Pressure readings: {scada_data.get_data_pressures()}")
|
|
111
|
+
print(f"Flow readings: {scada_data.get_data_flows()}")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
Documentation is available on readthedocs: [https://epytflow.readthedocs.io/en/latest/](https://epytflow.readthedocs.io/en/latest/)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT license -- see [LICENSE](LICENSE)
|
|
121
|
+
|
|
122
|
+
## How to Cite?
|
|
123
|
+
|
|
124
|
+
If you use this software, please cite it as follows:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
@misc{github:epytflow,
|
|
128
|
+
author = {André Artelt, Marios S. Kyriakou, Stelios G. Vrachimis, Demetrios G. Eliades, Barbara Hammer, Marios M. Polycarpou},
|
|
129
|
+
title = {EPyT-Flow -- EPANET Python Toolkit - Flow},
|
|
130
|
+
year = {2024},
|
|
131
|
+
publisher = {GitHub},
|
|
132
|
+
journal = {GitHub repository},
|
|
133
|
+
howpublished = {\url{https://github.com/WaterFutures/EPyT-Flow}}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## How to Contribute?
|
|
138
|
+
|
|
139
|
+
Contributions (e.g. creating issues, pull-requests, etc.) are welcome -- please make sure to read the [code of conduct](CODE_OF_CONDUCT.md) and follow the [developers' guidelines](DEVELOPERS.md).
|