epyt-flow 0.11.0__py3-none-any.whl → 0.13.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-MSX/Src/msxtoolkit.c +1 -1
- epyt_flow/VERSION +1 -1
- epyt_flow/data/benchmarks/gecco_water_quality.py +2 -2
- epyt_flow/data/benchmarks/leakdb.py +40 -5
- epyt_flow/data/benchmarks/water_usage.py +4 -3
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +5 -12
- epyt_flow/rest_api/scenario/control_handlers.py +118 -0
- epyt_flow/rest_api/scenario/event_handlers.py +114 -1
- epyt_flow/rest_api/scenario/handlers.py +33 -0
- epyt_flow/rest_api/server.py +14 -2
- epyt_flow/simulation/backend/__init__.py +1 -0
- epyt_flow/simulation/backend/my_epyt.py +1056 -0
- epyt_flow/simulation/events/quality_events.py +3 -1
- epyt_flow/simulation/scada/scada_data.py +201 -12
- epyt_flow/simulation/scenario_simulator.py +179 -87
- epyt_flow/topology.py +8 -7
- epyt_flow/uncertainty/sensor_noise.py +2 -9
- epyt_flow/utils.py +30 -0
- epyt_flow/visualization/scenario_visualizer.py +159 -69
- epyt_flow/visualization/visualization_utils.py +144 -17
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/METADATA +4 -4
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/RECORD +26 -29
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/WHEEL +1 -1
- epyt_flow/gym/control_gyms.py +0 -55
- epyt_flow/metrics.py +0 -471
- epyt_flow/models/__init__.py +0 -2
- epyt_flow/models/event_detector.py +0 -36
- epyt_flow/models/sensor_interpolation_detector.py +0 -123
- epyt_flow/simulation/scada/advanced_control.py +0 -138
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
7
|
import time
|
|
8
|
+
import itertools
|
|
8
9
|
from datetime import timedelta
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from typing import Generator, Union, Optional
|
|
@@ -15,9 +16,8 @@ import random
|
|
|
15
16
|
import math
|
|
16
17
|
import uuid
|
|
17
18
|
import numpy as np
|
|
18
|
-
from epyt import epanet
|
|
19
|
-
from epyt.epanet import ToolkitConstants
|
|
20
19
|
from tqdm import tqdm
|
|
20
|
+
from epyt.epanet import ToolkitConstants
|
|
21
21
|
|
|
22
22
|
from .scenario_config import ScenarioConfig
|
|
23
23
|
from .sensor_config import SensorConfig, areaunit_to_id, massunit_to_id, qualityunit_to_id, \
|
|
@@ -62,7 +62,7 @@ class ScenarioSimulator():
|
|
|
62
62
|
|
|
63
63
|
Attributes
|
|
64
64
|
----------
|
|
65
|
-
epanet_api :
|
|
65
|
+
epanet_api : :class:`~epyt_flow.simulation.backend.my_epyt.EPyT`
|
|
66
66
|
API to EPANET and EPANET-MSX.
|
|
67
67
|
_model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`, protected
|
|
68
68
|
Model uncertainty.
|
|
@@ -119,6 +119,7 @@ class ScenarioSimulator():
|
|
|
119
119
|
self._sensor_reading_events = []
|
|
120
120
|
self.__running_simulation = False
|
|
121
121
|
|
|
122
|
+
# Check availability of custom EPANET libraries
|
|
122
123
|
custom_epanet_lib = None
|
|
123
124
|
custom_epanetmsx_lib = None
|
|
124
125
|
if sys.platform.startswith("linux") or sys.platform.startswith("darwin") :
|
|
@@ -135,48 +136,49 @@ class ScenarioSimulator():
|
|
|
135
136
|
if os.path.isfile(os.path.join(path_to_custom_libs, libepanetmsx_name)):
|
|
136
137
|
custom_epanetmsx_lib = os.path.join(path_to_custom_libs, libepanetmsx_name)
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
# Workaround for EPyT bug concerning parallel simulations (see EPyT issue #54):
|
|
140
|
+
# 1. Create random tmp folder (make sure it is unique!)
|
|
141
|
+
# 2. Copy .inp and .msx file there
|
|
142
|
+
# 3. Use those copies when loading EPyT
|
|
143
|
+
tmp_folder_path = os.path.join(get_temp_folder(), f"{random.randint(int(1e5), int(1e7))}{time.time()}")
|
|
144
|
+
pathlib.Path(tmp_folder_path).mkdir(parents=True, exist_ok=False)
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
pathlib.Path(tmp_folder_path).mkdir(parents=True, exist_ok=False)
|
|
146
|
+
def __file_exists(file_in: str) -> bool:
|
|
147
|
+
try:
|
|
148
|
+
return pathlib.Path(file_in).is_file()
|
|
149
|
+
except Exception:
|
|
150
|
+
return False
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if not __file_exists(self.__f_inp_in):
|
|
153
|
+
my_f_inp_in = self.__f_inp_in
|
|
154
|
+
self.__my_f_inp_in = None
|
|
155
|
+
else:
|
|
156
|
+
my_f_inp_in = os.path.join(tmp_folder_path, pathlib.Path(self.__f_inp_in).name)
|
|
157
|
+
shutil.copyfile(self.__f_inp_in, my_f_inp_in)
|
|
158
|
+
self.__my_f_inp_in = my_f_inp_in
|
|
154
159
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
if self.__f_msx_in is not None:
|
|
161
|
+
if not __file_exists(self.__f_msx_in):
|
|
162
|
+
my_f_msx_in = self.__f_msx_in
|
|
158
163
|
else:
|
|
159
|
-
|
|
160
|
-
shutil.copyfile(self.
|
|
161
|
-
|
|
164
|
+
my_f_msx_in = os.path.join(tmp_folder_path, pathlib.Path(self.__f_msx_in).name)
|
|
165
|
+
shutil.copyfile(self.__f_msx_in, my_f_msx_in)
|
|
166
|
+
else:
|
|
167
|
+
my_f_msx_in = None
|
|
162
168
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
shutil.copyfile(self.__f_msx_in, my_f_msx_in)
|
|
169
|
-
else:
|
|
170
|
-
my_f_msx_in = None
|
|
169
|
+
from .backend import EPyT # Workaround: Sphinx autodoc "importlib.import_module TypeError: __mro_entries__"
|
|
170
|
+
self.epanet_api = EPyT(my_f_inp_in, ph=self.__f_msx_in is None,
|
|
171
|
+
customlib=custom_epanet_lib, loadfile=True,
|
|
172
|
+
display_msg=epanet_verbose,
|
|
173
|
+
display_warnings=False)
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
display_msg=epanet_verbose,
|
|
175
|
-
display_warnings=False)
|
|
175
|
+
if self.__f_msx_in is not None:
|
|
176
|
+
self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
# Do not raise exceptions in the case of EPANET warnings and errors
|
|
179
|
+
self.epanet_api.set_error_handling(False)
|
|
179
180
|
|
|
181
|
+
# Parse and initialize scenario
|
|
180
182
|
self._simple_controls = self._parse_simple_control_rules()
|
|
181
183
|
self._complex_controls = self._parse_complex_control_rules()
|
|
182
184
|
|
|
@@ -1050,6 +1052,11 @@ class ScenarioSimulator():
|
|
|
1050
1052
|
if node_type == "TANK":
|
|
1051
1053
|
node_tank_idx = node_tank_names.index(node_id) + 1
|
|
1052
1054
|
node_info["diameter"] = float(self.epanet_api.getNodeTankDiameter(node_tank_idx))
|
|
1055
|
+
node_info["volume"] = float(self.epanet_api.getNodeTankVolume(node_tank_idx))
|
|
1056
|
+
node_info["max_level"] = float(self.epanet_api.getNodeTankMaximumWaterLevel(node_tank_idx))
|
|
1057
|
+
node_info["min_level"] = float(self.epanet_api.getNodeTankMinimumWaterLevel(node_tank_idx))
|
|
1058
|
+
node_info["mixing_fraction"] = float(self.epanet_api.getNodeTankMixingFraction(node_tank_idx))
|
|
1059
|
+
#node_info["mixing_model"] = int(self.epanet_api.getNodeTankMixingModelCode(node_tank_idx)[0])
|
|
1053
1060
|
|
|
1054
1061
|
nodes.append((node_id, node_info))
|
|
1055
1062
|
|
|
@@ -1138,7 +1145,14 @@ class ScenarioSimulator():
|
|
|
1138
1145
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1139
1146
|
The pattern -- i.e. multiplier factors over time.
|
|
1140
1147
|
"""
|
|
1148
|
+
if not isinstance(pattern_id, str):
|
|
1149
|
+
raise TypeError("'pattern_id' must be an instance of 'str' " +
|
|
1150
|
+
f"but not of '{type(pattern_id)}'")
|
|
1151
|
+
|
|
1141
1152
|
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
1153
|
+
if pattern_idx == 0:
|
|
1154
|
+
raise ValueError(f"Unknown pattern '{pattern_id}'")
|
|
1155
|
+
|
|
1142
1156
|
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
1143
1157
|
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
1144
1158
|
for t in range(pattern_length)])
|
|
@@ -1171,6 +1185,37 @@ class ScenarioSimulator():
|
|
|
1171
1185
|
raise RuntimeError("Failed to add pattern! " +
|
|
1172
1186
|
"Maybe pattern name contains invalid characters or is too long?")
|
|
1173
1187
|
|
|
1188
|
+
def get_node_base_demand(self, node_id: str) -> float:
|
|
1189
|
+
"""
|
|
1190
|
+
Returns the base demand of a given node. None, if there does not exist any base demand.
|
|
1191
|
+
|
|
1192
|
+
Note that base demands are summed up in the case of different demand categories.
|
|
1193
|
+
|
|
1194
|
+
Parameters
|
|
1195
|
+
----------
|
|
1196
|
+
node_id : `str`
|
|
1197
|
+
ID of the node.
|
|
1198
|
+
|
|
1199
|
+
Returns
|
|
1200
|
+
-------
|
|
1201
|
+
`float`
|
|
1202
|
+
Base demand.
|
|
1203
|
+
"""
|
|
1204
|
+
if node_id not in self._sensor_config.nodes:
|
|
1205
|
+
raise ValueError(f"Unknown node '{node_id}'")
|
|
1206
|
+
|
|
1207
|
+
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
1208
|
+
n_demand_categories = self.epanet_api.getNodeDemandCategoriesNumber(node_idx)
|
|
1209
|
+
|
|
1210
|
+
if n_demand_categories == 0:
|
|
1211
|
+
return None
|
|
1212
|
+
else:
|
|
1213
|
+
base_demand = 0
|
|
1214
|
+
for demand_category in range(n_demand_categories):
|
|
1215
|
+
base_demand += self.epanet_api.getNodeBaseDemands(node_idx)[demand_category + 1]
|
|
1216
|
+
|
|
1217
|
+
return base_demand
|
|
1218
|
+
|
|
1174
1219
|
def get_node_demand_pattern(self, node_id: str) -> np.ndarray:
|
|
1175
1220
|
"""
|
|
1176
1221
|
Returns the values of the demand pattern of a given node --
|
|
@@ -1186,6 +1231,12 @@ class ScenarioSimulator():
|
|
|
1186
1231
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1187
1232
|
The demand pattern -- i.e. multiplier factors over time.
|
|
1188
1233
|
"""
|
|
1234
|
+
if not isinstance(node_id, str):
|
|
1235
|
+
raise TypeError("'node_id' must be an instance of 'str' " +
|
|
1236
|
+
f"but not of '{type(node_id)}'")
|
|
1237
|
+
if node_id not in self._sensor_config.nodes:
|
|
1238
|
+
raise ValueError(f"Unknown node '{node_id}'")
|
|
1239
|
+
|
|
1189
1240
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
1190
1241
|
demand_category = self.epanet_api.getNodeDemandCategoriesNumber()[node_idx]
|
|
1191
1242
|
demand_pattern_id = self.epanet_api.getNodeDemandPatternNameID()[demand_category][node_idx - 1]
|
|
@@ -1948,12 +1999,12 @@ class ScenarioSimulator():
|
|
|
1948
1999
|
result = None
|
|
1949
2000
|
|
|
1950
2001
|
gen = self.run_advanced_quality_simulation_as_generator
|
|
1951
|
-
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2002
|
+
for scada_data, _ in gen(hyd_file_in=hyd_file_in,
|
|
2003
|
+
verbose=verbose,
|
|
2004
|
+
return_as_dict=True,
|
|
2005
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
2006
|
+
use_quality_time_step_as_reporting_time_step=
|
|
2007
|
+
use_quality_time_step_as_reporting_time_step):
|
|
1957
2008
|
if result is None:
|
|
1958
2009
|
result = {}
|
|
1959
2010
|
for data_type, data in scada_data.items():
|
|
@@ -1981,7 +2032,7 @@ class ScenarioSimulator():
|
|
|
1981
2032
|
return_as_dict: bool = False,
|
|
1982
2033
|
frozen_sensor_config: bool = False,
|
|
1983
2034
|
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1984
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2035
|
+
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
1985
2036
|
"""
|
|
1986
2037
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
1987
2038
|
|
|
@@ -2015,7 +2066,7 @@ class ScenarioSimulator():
|
|
|
2015
2066
|
-------
|
|
2016
2067
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
2017
2068
|
Generator containing the current EPANET-MSX simulation results as SCADA data
|
|
2018
|
-
(i.e. species concentrations).
|
|
2069
|
+
(i.e. species concentrations) and a boolean indicating whether the simulation terminated or not.
|
|
2019
2070
|
"""
|
|
2020
2071
|
if self.__running_simulation is True:
|
|
2021
2072
|
raise RuntimeError("A simulation is already running.")
|
|
@@ -2034,6 +2085,8 @@ class ScenarioSimulator():
|
|
|
2034
2085
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
2035
2086
|
hyd_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
2036
2087
|
|
|
2088
|
+
network_topo = self.get_topology()
|
|
2089
|
+
|
|
2037
2090
|
if use_quality_time_step_as_reporting_time_step is True:
|
|
2038
2091
|
quality_time_step = self.epanet_api.getMSXTimeStep()
|
|
2039
2092
|
reporting_time_step = quality_time_step
|
|
@@ -2122,13 +2175,16 @@ class ScenarioSimulator():
|
|
|
2122
2175
|
data = {"bulk_species_node_concentration_raw": bulk_species_node_concentrations,
|
|
2123
2176
|
"bulk_species_link_concentration_raw": bulk_species_link_concentrations,
|
|
2124
2177
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2125
|
-
"sensor_readings_time": np.array([0])
|
|
2178
|
+
"sensor_readings_time": np.array([0]),
|
|
2179
|
+
"warnings_code": np.array([0]) # TODO: Replace with MSX error
|
|
2180
|
+
}
|
|
2126
2181
|
else:
|
|
2127
|
-
data = ScadaData(network_topo=
|
|
2182
|
+
data = ScadaData(network_topo=network_topo, sensor_config=self._sensor_config,
|
|
2128
2183
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
2129
2184
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
2130
2185
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
2131
2186
|
sensor_readings_time=np.array([0]),
|
|
2187
|
+
warnings_code=np.array([0]), # TODO: Replace with MSX error
|
|
2132
2188
|
sensor_reading_events=self._sensor_reading_events,
|
|
2133
2189
|
sensor_noise=self._sensor_noise,
|
|
2134
2190
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2138,7 +2194,7 @@ class ScenarioSimulator():
|
|
|
2138
2194
|
if abort is True:
|
|
2139
2195
|
return None
|
|
2140
2196
|
|
|
2141
|
-
yield data
|
|
2197
|
+
yield (data, False)
|
|
2142
2198
|
|
|
2143
2199
|
# Run step-by-step simulation
|
|
2144
2200
|
tleft = 1
|
|
@@ -2166,9 +2222,11 @@ class ScenarioSimulator():
|
|
|
2166
2222
|
"bulk_species_link_concentration_raw":
|
|
2167
2223
|
bulk_species_link_concentrations,
|
|
2168
2224
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2169
|
-
"sensor_readings_time": np.array([total_time])
|
|
2225
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2226
|
+
"warnings_code": np.array([0]), # TODO: Replace with MSX error
|
|
2227
|
+
}
|
|
2170
2228
|
else:
|
|
2171
|
-
data = ScadaData(network_topo=
|
|
2229
|
+
data = ScadaData(network_topo=network_topo,
|
|
2172
2230
|
sensor_config=self._sensor_config,
|
|
2173
2231
|
bulk_species_node_concentration_raw=
|
|
2174
2232
|
bulk_species_node_concentrations,
|
|
@@ -2177,6 +2235,7 @@ class ScenarioSimulator():
|
|
|
2177
2235
|
surface_species_concentration_raw=
|
|
2178
2236
|
surface_species_concentrations,
|
|
2179
2237
|
sensor_readings_time=np.array([total_time]),
|
|
2238
|
+
warnings_code=np.array([0]), # TODO: Replace with MSX error
|
|
2180
2239
|
sensor_reading_events=self._sensor_reading_events,
|
|
2181
2240
|
sensor_noise=self._sensor_noise,
|
|
2182
2241
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2186,7 +2245,7 @@ class ScenarioSimulator():
|
|
|
2186
2245
|
if abort is not False:
|
|
2187
2246
|
break
|
|
2188
2247
|
|
|
2189
|
-
yield data
|
|
2248
|
+
yield (data, tleft <= 0)
|
|
2190
2249
|
|
|
2191
2250
|
self.__running_simulation = False
|
|
2192
2251
|
|
|
@@ -2231,12 +2290,12 @@ class ScenarioSimulator():
|
|
|
2231
2290
|
|
|
2232
2291
|
# Run simulation step-by-step
|
|
2233
2292
|
gen = self.run_basic_quality_simulation_as_generator
|
|
2234
|
-
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2293
|
+
for scada_data, _ in gen(hyd_file_in=hyd_file_in,
|
|
2294
|
+
verbose=verbose,
|
|
2295
|
+
return_as_dict=True,
|
|
2296
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
2297
|
+
use_quality_time_step_as_reporting_time_step=
|
|
2298
|
+
use_quality_time_step_as_reporting_time_step):
|
|
2240
2299
|
if result is None:
|
|
2241
2300
|
result = {}
|
|
2242
2301
|
for data_type, data in scada_data.items():
|
|
@@ -2261,7 +2320,7 @@ class ScenarioSimulator():
|
|
|
2261
2320
|
return_as_dict: bool = False,
|
|
2262
2321
|
frozen_sensor_config: bool = False,
|
|
2263
2322
|
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2264
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2323
|
+
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2265
2324
|
"""
|
|
2266
2325
|
Runs a basic quality analysis using EPANET.
|
|
2267
2326
|
|
|
@@ -2296,11 +2355,13 @@ class ScenarioSimulator():
|
|
|
2296
2355
|
Returns
|
|
2297
2356
|
-------
|
|
2298
2357
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
2299
|
-
Generator with the current simulation results/states as SCADA data
|
|
2358
|
+
Generator with the current simulation results/states as SCADA data and a
|
|
2359
|
+
boolean indicating whether the simulation terminated or not.
|
|
2300
2360
|
"""
|
|
2301
2361
|
if self.__running_simulation is True:
|
|
2302
2362
|
raise RuntimeError("A simulation is already running.")
|
|
2303
2363
|
|
|
2364
|
+
requested_total_time = self.epanet_api.getTimeSimulationDuration()
|
|
2304
2365
|
requested_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
2305
2366
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
2306
2367
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
@@ -2310,6 +2371,8 @@ class ScenarioSimulator():
|
|
|
2310
2371
|
requested_time_step = quality_time_step
|
|
2311
2372
|
reporting_time_step = quality_time_step
|
|
2312
2373
|
|
|
2374
|
+
network_topo = self.get_topology()
|
|
2375
|
+
|
|
2313
2376
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
2314
2377
|
|
|
2315
2378
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -2338,10 +2401,11 @@ class ScenarioSimulator():
|
|
|
2338
2401
|
pass
|
|
2339
2402
|
|
|
2340
2403
|
# Compute current time step
|
|
2341
|
-
t = self.epanet_api.
|
|
2404
|
+
t = self.epanet_api.api.ENrunQ()
|
|
2342
2405
|
total_time = t
|
|
2343
2406
|
|
|
2344
2407
|
# Fetch data
|
|
2408
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2345
2409
|
quality_node_data = self.epanet_api.getNodeActualQuality().reshape(1, -1)
|
|
2346
2410
|
quality_link_data = self.epanet_api.getLinkActualQuality().reshape(1, -1)
|
|
2347
2411
|
|
|
@@ -2350,13 +2414,15 @@ class ScenarioSimulator():
|
|
|
2350
2414
|
if return_as_dict is True:
|
|
2351
2415
|
data = {"node_quality_data_raw": quality_node_data,
|
|
2352
2416
|
"link_quality_data_raw": quality_link_data,
|
|
2353
|
-
"sensor_readings_time": np.array([total_time])
|
|
2417
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2418
|
+
"warnings_code": np.array([error_code])}
|
|
2354
2419
|
else:
|
|
2355
|
-
data = ScadaData(network_topo=
|
|
2420
|
+
data = ScadaData(network_topo=network_topo,
|
|
2356
2421
|
sensor_config=self._sensor_config,
|
|
2357
2422
|
node_quality_data_raw=quality_node_data,
|
|
2358
2423
|
link_quality_data_raw=quality_link_data,
|
|
2359
2424
|
sensor_readings_time=np.array([total_time]),
|
|
2425
|
+
warnings_code=np.array([error_code]),
|
|
2360
2426
|
sensor_reading_events=self._sensor_reading_events,
|
|
2361
2427
|
sensor_noise=self._sensor_noise,
|
|
2362
2428
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2366,12 +2432,12 @@ class ScenarioSimulator():
|
|
|
2366
2432
|
if abort is True:
|
|
2367
2433
|
break
|
|
2368
2434
|
|
|
2369
|
-
yield data
|
|
2435
|
+
yield (data, total_time < requested_total_time)
|
|
2370
2436
|
|
|
2371
2437
|
# Next
|
|
2372
|
-
tstep = self.epanet_api.
|
|
2438
|
+
tstep = self.epanet_api.api.ENstepQ()
|
|
2373
2439
|
|
|
2374
|
-
self.epanet_api.
|
|
2440
|
+
self.epanet_api.closeQualityAnalysis()
|
|
2375
2441
|
|
|
2376
2442
|
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2377
2443
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
@@ -2413,10 +2479,10 @@ class ScenarioSimulator():
|
|
|
2413
2479
|
|
|
2414
2480
|
# Run hydraulic simulation step-by-step
|
|
2415
2481
|
gen = self.run_hydraulic_simulation_as_generator
|
|
2416
|
-
for scada_data in gen(hyd_export=hyd_export,
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2482
|
+
for scada_data, _ in gen(hyd_export=hyd_export,
|
|
2483
|
+
verbose=verbose,
|
|
2484
|
+
return_as_dict=True,
|
|
2485
|
+
frozen_sensor_config=frozen_sensor_config):
|
|
2420
2486
|
if result is None:
|
|
2421
2487
|
result = {}
|
|
2422
2488
|
for data_type, data in scada_data.items():
|
|
@@ -2441,7 +2507,7 @@ class ScenarioSimulator():
|
|
|
2441
2507
|
support_abort: bool = False,
|
|
2442
2508
|
return_as_dict: bool = False,
|
|
2443
2509
|
frozen_sensor_config: bool = False,
|
|
2444
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2510
|
+
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2445
2511
|
"""
|
|
2446
2512
|
Runs the hydraulic simulation of this scenario (incl. basic quality if set) and
|
|
2447
2513
|
provides the results as a generator.
|
|
@@ -2482,7 +2548,7 @@ class ScenarioSimulator():
|
|
|
2482
2548
|
-------
|
|
2483
2549
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
2484
2550
|
Generator with the current simulation results/states as SCADA data
|
|
2485
|
-
(i.e. sensor readings).
|
|
2551
|
+
(i.e. sensor readings) and a boolean indicating whether the simulation terminated or not.
|
|
2486
2552
|
"""
|
|
2487
2553
|
if self.__running_simulation is True:
|
|
2488
2554
|
raise RuntimeError("A simulation is already running.")
|
|
@@ -2493,15 +2559,18 @@ class ScenarioSimulator():
|
|
|
2493
2559
|
|
|
2494
2560
|
self.__running_simulation = True
|
|
2495
2561
|
|
|
2496
|
-
self.epanet_api.
|
|
2497
|
-
self.epanet_api.
|
|
2562
|
+
self.epanet_api.api.ENopenH()
|
|
2563
|
+
self.epanet_api.api.ENopenQ()
|
|
2498
2564
|
self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
|
|
2499
2565
|
self.epanet_api.initializeQualityAnalysis(ToolkitConstants.EN_SAVE)
|
|
2500
2566
|
|
|
2567
|
+
requested_total_time = self.epanet_api.getTimeSimulationDuration()
|
|
2501
2568
|
requested_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
2502
2569
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
2503
2570
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
2504
2571
|
|
|
2572
|
+
network_topo = self.get_topology()
|
|
2573
|
+
|
|
2505
2574
|
if verbose is True:
|
|
2506
2575
|
print("Running EPANET ...")
|
|
2507
2576
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
@@ -2531,8 +2600,11 @@ class ScenarioSimulator():
|
|
|
2531
2600
|
event.step(total_time + tstep)
|
|
2532
2601
|
|
|
2533
2602
|
# Compute current time step
|
|
2534
|
-
t = self.epanet_api.
|
|
2535
|
-
self.epanet_api.
|
|
2603
|
+
t = self.epanet_api.api.ENrunH()
|
|
2604
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2605
|
+
self.epanet_api.api.ENrunQ()
|
|
2606
|
+
if error_code == 0:
|
|
2607
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2536
2608
|
total_time = t
|
|
2537
2609
|
|
|
2538
2610
|
# Fetch data
|
|
@@ -2551,7 +2623,7 @@ class ScenarioSimulator():
|
|
|
2551
2623
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2552
2624
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2553
2625
|
|
|
2554
|
-
scada_data = ScadaData(network_topo=
|
|
2626
|
+
scada_data = ScadaData(network_topo=network_topo,
|
|
2555
2627
|
sensor_config=self._sensor_config,
|
|
2556
2628
|
pressure_data_raw=pressure_data,
|
|
2557
2629
|
flow_data_raw=flow_data,
|
|
@@ -2564,6 +2636,7 @@ class ScenarioSimulator():
|
|
|
2564
2636
|
pumps_energy_usage_data_raw=pumps_energy_usage_data,
|
|
2565
2637
|
pumps_efficiency_data_raw=pumps_efficiency_data,
|
|
2566
2638
|
sensor_readings_time=np.array([total_time]),
|
|
2639
|
+
warnings_code=np.array([error_code]),
|
|
2567
2640
|
sensor_reading_events=self._sensor_reading_events,
|
|
2568
2641
|
sensor_noise=self._sensor_noise,
|
|
2569
2642
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2581,7 +2654,8 @@ class ScenarioSimulator():
|
|
|
2581
2654
|
"tanks_volume_data_raw": tanks_volume_data,
|
|
2582
2655
|
"pumps_energy_usage_data_raw": pumps_energy_usage_data,
|
|
2583
2656
|
"pumps_efficiency_data_raw": pumps_efficiency_data,
|
|
2584
|
-
"sensor_readings_time": np.array([total_time])
|
|
2657
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2658
|
+
"warnings_code": np.array([error_code])}
|
|
2585
2659
|
else:
|
|
2586
2660
|
data = scada_data
|
|
2587
2661
|
|
|
@@ -2590,18 +2664,18 @@ class ScenarioSimulator():
|
|
|
2590
2664
|
if abort is True:
|
|
2591
2665
|
break
|
|
2592
2666
|
|
|
2593
|
-
yield data
|
|
2667
|
+
yield (data, total_time < requested_total_time)
|
|
2594
2668
|
|
|
2595
2669
|
# Apply control modules
|
|
2596
2670
|
for control in self._custom_controls:
|
|
2597
2671
|
control.step(scada_data)
|
|
2598
2672
|
|
|
2599
2673
|
# Next
|
|
2600
|
-
tstep = self.epanet_api.
|
|
2601
|
-
self.epanet_api.
|
|
2674
|
+
tstep = self.epanet_api.api.ENnextH()
|
|
2675
|
+
self.epanet_api.api.ENnextQ()
|
|
2602
2676
|
|
|
2603
|
-
self.epanet_api.
|
|
2604
|
-
self.epanet_api.
|
|
2677
|
+
self.epanet_api.api.ENcloseQ()
|
|
2678
|
+
self.epanet_api.api.ENcloseH()
|
|
2605
2679
|
|
|
2606
2680
|
self.__running_simulation = False
|
|
2607
2681
|
|
|
@@ -2718,6 +2792,7 @@ class ScenarioSimulator():
|
|
|
2718
2792
|
|
|
2719
2793
|
def set_general_parameters(self, demand_model: dict = None, simulation_duration: int = None,
|
|
2720
2794
|
hydraulic_time_step: int = None, quality_time_step: int = None,
|
|
2795
|
+
advanced_quality_time_step: int = None,
|
|
2721
2796
|
reporting_time_step: int = None, reporting_time_start: int = None,
|
|
2722
2797
|
flow_units_id: int = None, quality_model: dict = None) -> None:
|
|
2723
2798
|
"""
|
|
@@ -2750,6 +2825,12 @@ class ScenarioSimulator():
|
|
|
2750
2825
|
Quality time step -- i.e. the interval at which qualities are computed.
|
|
2751
2826
|
Should be much smaller than the hydraulic time step!
|
|
2752
2827
|
|
|
2828
|
+
The default is None.
|
|
2829
|
+
advanced_quality_time_step : `ìnt`, optional
|
|
2830
|
+
Time step in the advanced quality simuliation -- i.e. EPANET-MSX simulation.
|
|
2831
|
+
This number specifies the interval at which all species concentrations are.
|
|
2832
|
+
Should be much smaller than the hydraulic time step!
|
|
2833
|
+
|
|
2753
2834
|
The default is None.
|
|
2754
2835
|
reporting_time_step : `int`, optional
|
|
2755
2836
|
Report time step -- i.e. the interval at which hydraulics and quality states are
|
|
@@ -2862,6 +2943,14 @@ class ScenarioSimulator():
|
|
|
2862
2943
|
"greater than the hydraulic time step")
|
|
2863
2944
|
self.epanet_api.setTimeQualityStep(quality_time_step)
|
|
2864
2945
|
|
|
2946
|
+
if advanced_quality_time_step is not None:
|
|
2947
|
+
if not isinstance(advanced_quality_time_step, int) or \
|
|
2948
|
+
advanced_quality_time_step <= 0 or \
|
|
2949
|
+
advanced_quality_time_step > self.epanet_api.getTimeHydraulicStep():
|
|
2950
|
+
raise ValueError("'advanced_quality_time_step' must be a positive integer " +
|
|
2951
|
+
"that is not greater than the hydraulic time step")
|
|
2952
|
+
self.epanet_api.setMSXTimeStep(advanced_quality_time_step)
|
|
2953
|
+
|
|
2865
2954
|
if quality_model is not None:
|
|
2866
2955
|
if quality_model["type"] == "NONE":
|
|
2867
2956
|
self.epanet_api.setQualityType("none")
|
|
@@ -3459,7 +3548,7 @@ class ScenarioSimulator():
|
|
|
3459
3548
|
source_type: int, pattern_id: str = None,
|
|
3460
3549
|
source_strength: int = 1.) -> None:
|
|
3461
3550
|
"""
|
|
3462
|
-
Adds a new external
|
|
3551
|
+
Adds a new external bulk species injection source at a particular node.
|
|
3463
3552
|
|
|
3464
3553
|
Only for EPANET-MSX scenarios.
|
|
3465
3554
|
|
|
@@ -3472,6 +3561,8 @@ class ScenarioSimulator():
|
|
|
3472
3561
|
is placed.
|
|
3473
3562
|
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
3474
3563
|
1d source pattern.
|
|
3564
|
+
|
|
3565
|
+
Note that the pattern time step is equivalent to the EPANET pattern time step.
|
|
3475
3566
|
source_type : `int`,
|
|
3476
3567
|
Type of the external (bulk or surface) species injection source -- must be one of
|
|
3477
3568
|
the following EPANET toolkit constants:
|
|
@@ -3537,17 +3628,18 @@ class ScenarioSimulator():
|
|
|
3537
3628
|
any(not isinstance(species_id, str) or not isinstance(node_initial_conc, list)
|
|
3538
3629
|
for species_id, node_initial_conc in inital_conc.items()) or \
|
|
3539
3630
|
any(not isinstance(node_initial_conc, tuple)
|
|
3540
|
-
for node_initial_conc in inital_conc.values()) or \
|
|
3631
|
+
for node_initial_conc in list(itertools.chain(*inital_conc.values()))) or \
|
|
3541
3632
|
any(not isinstance(node_id, str) or not isinstance(conc, float)
|
|
3542
|
-
for node_id, conc in inital_conc.values()):
|
|
3633
|
+
for node_id, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3543
3634
|
raise TypeError("'inital_conc' must be an instance of " +
|
|
3544
3635
|
"'dict[str, list[tuple[str, float]]'")
|
|
3636
|
+
inital_conc_values = list(itertools.chain(*inital_conc.values()))
|
|
3545
3637
|
if any(species_id not in self.sensor_config.bulk_species
|
|
3546
3638
|
for species_id in inital_conc.keys()):
|
|
3547
3639
|
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3548
|
-
if any(node_id not in self.sensor_config.nodes for node_id, _ in
|
|
3640
|
+
if any(node_id not in self.sensor_config.nodes for node_id, _ in inital_conc_values):
|
|
3549
3641
|
raise ValueError("Unknown node ID in 'inital_conc'")
|
|
3550
|
-
if any(conc < 0 for _, conc in
|
|
3642
|
+
if any(conc < 0 for _, conc in inital_conc_values):
|
|
3551
3643
|
raise ValueError("Initial node concentration can not be negative")
|
|
3552
3644
|
|
|
3553
3645
|
for species_id, node_initial_conc in inital_conc.items():
|
epyt_flow/topology.py
CHANGED
|
@@ -78,7 +78,7 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
78
78
|
links: list[tuple[str, tuple[str, str], dict]],
|
|
79
79
|
pumps: dict,
|
|
80
80
|
valves: dict,
|
|
81
|
-
units: int
|
|
81
|
+
units: int,
|
|
82
82
|
**kwds):
|
|
83
83
|
super().__init__(name=f_inp, **kwds)
|
|
84
84
|
|
|
@@ -88,11 +88,6 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
88
88
|
self.__valves = valves
|
|
89
89
|
self.__units = units
|
|
90
90
|
|
|
91
|
-
if units is None:
|
|
92
|
-
warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
|
|
93
|
-
" -- support of such old files will be removed in the next release!",
|
|
94
|
-
DeprecationWarning)
|
|
95
|
-
|
|
96
91
|
for node_id, node_info in nodes:
|
|
97
92
|
node_elevation = node_info["elevation"]
|
|
98
93
|
node_type = node_info["type"]
|
|
@@ -561,7 +556,8 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
561
556
|
|
|
562
557
|
# Nodes
|
|
563
558
|
node_data = {"id": [], "type": [], "elevation": [], "geometry": []}
|
|
564
|
-
tank_data = {"id": [], "
|
|
559
|
+
tank_data = {"id": [], "volume": [], "max_level": [], "min_level": [], "mixing_fraction": [],
|
|
560
|
+
"elevation": [], "diameter": [], "geometry": []}
|
|
565
561
|
reservoir_data = {"id": [], "elevation": [], "geometry": []}
|
|
566
562
|
for node_id in self.get_all_nodes():
|
|
567
563
|
node_info = self.get_node_info(node_id)
|
|
@@ -575,6 +571,11 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
575
571
|
tank_data["id"].append(node_id)
|
|
576
572
|
tank_data["elevation"].append(node_info["elevation"])
|
|
577
573
|
tank_data["diameter"].append(node_info["diameter"])
|
|
574
|
+
tank_data["volume"].append(node_info["volume"])
|
|
575
|
+
tank_data["max_level"].append(node_info["max_level"])
|
|
576
|
+
tank_data["min_level"].append(node_info["min_level"])
|
|
577
|
+
tank_data["mixing_fraction"].append(node_info["mixing_fraction"])
|
|
578
|
+
#tank_data["mixing_model"].append(node_info["mixing_model"])
|
|
578
579
|
tank_data["geometry"].append(Point(node_info["coord"]))
|
|
579
580
|
elif node_info["type"] == "RESERVOIR":
|
|
580
581
|
reservoir_data["id"].append(node_id)
|
|
@@ -3,7 +3,6 @@ Module provides a class for implementing sensor noise (e.g. uncertainty in senso
|
|
|
3
3
|
"""
|
|
4
4
|
from typing import Optional, Callable
|
|
5
5
|
from copy import deepcopy
|
|
6
|
-
import warnings
|
|
7
6
|
import numpy
|
|
8
7
|
from numpy.random import default_rng
|
|
9
8
|
|
|
@@ -32,18 +31,12 @@ class SensorNoise(JsonSerializable):
|
|
|
32
31
|
|
|
33
32
|
The default is None.
|
|
34
33
|
"""
|
|
35
|
-
def __init__(self,
|
|
36
|
-
global_uncertainty: Optional[Uncertainty] = None,
|
|
34
|
+
def __init__(self, global_uncertainty: Optional[Uncertainty] = None,
|
|
37
35
|
local_uncertainties: Optional[dict[int, str, Uncertainty]] = None,
|
|
38
36
|
seed: Optional[int] = None,
|
|
39
37
|
**kwds):
|
|
40
|
-
if uncertainty is not None:
|
|
41
|
-
global_uncertainty = uncertainty
|
|
42
|
-
warnings.warn("'uncertainty' is deprecated and will be removed in future releases. " +
|
|
43
|
-
"Use 'global_uncertainty' instead")
|
|
44
|
-
|
|
45
38
|
if not isinstance(global_uncertainty, Uncertainty):
|
|
46
|
-
raise TypeError("'
|
|
39
|
+
raise TypeError("'global_uncertainty' must be an instance of " +
|
|
47
40
|
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
48
41
|
f"'{type(global_uncertainty)}'")
|
|
49
42
|
if local_uncertainties is not None:
|