epyt-flow 0.11.0__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/benchmarks/gecco_water_quality.py +2 -2
- epyt_flow/data/benchmarks/leakdb.py +40 -5
- epyt_flow/data/benchmarks/water_usage.py +4 -3
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +3 -10
- 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 +142 -59
- epyt_flow/topology.py +8 -7
- 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.12.0.dist-info}/METADATA +4 -4
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +24 -27
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.12.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.12.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.11.0.dist-info → epyt_flow-0.12.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]
|
|
@@ -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
|
|
@@ -2124,7 +2177,7 @@ class ScenarioSimulator():
|
|
|
2124
2177
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2125
2178
|
"sensor_readings_time": np.array([0])}
|
|
2126
2179
|
else:
|
|
2127
|
-
data = ScadaData(network_topo=
|
|
2180
|
+
data = ScadaData(network_topo=network_topo, sensor_config=self._sensor_config,
|
|
2128
2181
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
2129
2182
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
2130
2183
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
@@ -2168,7 +2221,7 @@ class ScenarioSimulator():
|
|
|
2168
2221
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2169
2222
|
"sensor_readings_time": np.array([total_time])}
|
|
2170
2223
|
else:
|
|
2171
|
-
data = ScadaData(network_topo=
|
|
2224
|
+
data = ScadaData(network_topo=network_topo,
|
|
2172
2225
|
sensor_config=self._sensor_config,
|
|
2173
2226
|
bulk_species_node_concentration_raw=
|
|
2174
2227
|
bulk_species_node_concentrations,
|
|
@@ -2310,6 +2363,8 @@ class ScenarioSimulator():
|
|
|
2310
2363
|
requested_time_step = quality_time_step
|
|
2311
2364
|
reporting_time_step = quality_time_step
|
|
2312
2365
|
|
|
2366
|
+
network_topo = self.get_topology()
|
|
2367
|
+
|
|
2313
2368
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
2314
2369
|
|
|
2315
2370
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -2338,10 +2393,11 @@ class ScenarioSimulator():
|
|
|
2338
2393
|
pass
|
|
2339
2394
|
|
|
2340
2395
|
# Compute current time step
|
|
2341
|
-
t = self.epanet_api.
|
|
2396
|
+
t = self.epanet_api.api.ENrunQ()
|
|
2342
2397
|
total_time = t
|
|
2343
2398
|
|
|
2344
2399
|
# Fetch data
|
|
2400
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2345
2401
|
quality_node_data = self.epanet_api.getNodeActualQuality().reshape(1, -1)
|
|
2346
2402
|
quality_link_data = self.epanet_api.getLinkActualQuality().reshape(1, -1)
|
|
2347
2403
|
|
|
@@ -2350,13 +2406,15 @@ class ScenarioSimulator():
|
|
|
2350
2406
|
if return_as_dict is True:
|
|
2351
2407
|
data = {"node_quality_data_raw": quality_node_data,
|
|
2352
2408
|
"link_quality_data_raw": quality_link_data,
|
|
2353
|
-
"sensor_readings_time": np.array([total_time])
|
|
2409
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2410
|
+
"warnings_code": np.array([error_code])}
|
|
2354
2411
|
else:
|
|
2355
|
-
data = ScadaData(network_topo=
|
|
2412
|
+
data = ScadaData(network_topo=network_topo,
|
|
2356
2413
|
sensor_config=self._sensor_config,
|
|
2357
2414
|
node_quality_data_raw=quality_node_data,
|
|
2358
2415
|
link_quality_data_raw=quality_link_data,
|
|
2359
2416
|
sensor_readings_time=np.array([total_time]),
|
|
2417
|
+
warnings_code=np.array([error_code]),
|
|
2360
2418
|
sensor_reading_events=self._sensor_reading_events,
|
|
2361
2419
|
sensor_noise=self._sensor_noise,
|
|
2362
2420
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2369,9 +2427,9 @@ class ScenarioSimulator():
|
|
|
2369
2427
|
yield data
|
|
2370
2428
|
|
|
2371
2429
|
# Next
|
|
2372
|
-
tstep = self.epanet_api.
|
|
2430
|
+
tstep = self.epanet_api.api.ENstepQ()
|
|
2373
2431
|
|
|
2374
|
-
self.epanet_api.
|
|
2432
|
+
self.epanet_api.closeQualityAnalysis()
|
|
2375
2433
|
|
|
2376
2434
|
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2377
2435
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
@@ -2493,8 +2551,8 @@ class ScenarioSimulator():
|
|
|
2493
2551
|
|
|
2494
2552
|
self.__running_simulation = True
|
|
2495
2553
|
|
|
2496
|
-
self.epanet_api.
|
|
2497
|
-
self.epanet_api.
|
|
2554
|
+
self.epanet_api.api.ENopenH()
|
|
2555
|
+
self.epanet_api.api.ENopenQ()
|
|
2498
2556
|
self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
|
|
2499
2557
|
self.epanet_api.initializeQualityAnalysis(ToolkitConstants.EN_SAVE)
|
|
2500
2558
|
|
|
@@ -2502,6 +2560,8 @@ class ScenarioSimulator():
|
|
|
2502
2560
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
2503
2561
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
2504
2562
|
|
|
2563
|
+
network_topo = self.get_topology()
|
|
2564
|
+
|
|
2505
2565
|
if verbose is True:
|
|
2506
2566
|
print("Running EPANET ...")
|
|
2507
2567
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
@@ -2531,8 +2591,11 @@ class ScenarioSimulator():
|
|
|
2531
2591
|
event.step(total_time + tstep)
|
|
2532
2592
|
|
|
2533
2593
|
# Compute current time step
|
|
2534
|
-
t = self.epanet_api.
|
|
2535
|
-
self.epanet_api.
|
|
2594
|
+
t = self.epanet_api.api.ENrunH()
|
|
2595
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2596
|
+
self.epanet_api.api.ENrunQ()
|
|
2597
|
+
if error_code == 0:
|
|
2598
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2536
2599
|
total_time = t
|
|
2537
2600
|
|
|
2538
2601
|
# Fetch data
|
|
@@ -2551,7 +2614,7 @@ class ScenarioSimulator():
|
|
|
2551
2614
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2552
2615
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2553
2616
|
|
|
2554
|
-
scada_data = ScadaData(network_topo=
|
|
2617
|
+
scada_data = ScadaData(network_topo=network_topo,
|
|
2555
2618
|
sensor_config=self._sensor_config,
|
|
2556
2619
|
pressure_data_raw=pressure_data,
|
|
2557
2620
|
flow_data_raw=flow_data,
|
|
@@ -2564,6 +2627,7 @@ class ScenarioSimulator():
|
|
|
2564
2627
|
pumps_energy_usage_data_raw=pumps_energy_usage_data,
|
|
2565
2628
|
pumps_efficiency_data_raw=pumps_efficiency_data,
|
|
2566
2629
|
sensor_readings_time=np.array([total_time]),
|
|
2630
|
+
warnings_code=np.array([error_code]),
|
|
2567
2631
|
sensor_reading_events=self._sensor_reading_events,
|
|
2568
2632
|
sensor_noise=self._sensor_noise,
|
|
2569
2633
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2581,7 +2645,8 @@ class ScenarioSimulator():
|
|
|
2581
2645
|
"tanks_volume_data_raw": tanks_volume_data,
|
|
2582
2646
|
"pumps_energy_usage_data_raw": pumps_energy_usage_data,
|
|
2583
2647
|
"pumps_efficiency_data_raw": pumps_efficiency_data,
|
|
2584
|
-
"sensor_readings_time": np.array([total_time])
|
|
2648
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2649
|
+
"warnings_code": np.array([error_code])}
|
|
2585
2650
|
else:
|
|
2586
2651
|
data = scada_data
|
|
2587
2652
|
|
|
@@ -2597,11 +2662,11 @@ class ScenarioSimulator():
|
|
|
2597
2662
|
control.step(scada_data)
|
|
2598
2663
|
|
|
2599
2664
|
# Next
|
|
2600
|
-
tstep = self.epanet_api.
|
|
2601
|
-
self.epanet_api.
|
|
2665
|
+
tstep = self.epanet_api.api.ENnextH()
|
|
2666
|
+
self.epanet_api.api.ENnextQ()
|
|
2602
2667
|
|
|
2603
|
-
self.epanet_api.
|
|
2604
|
-
self.epanet_api.
|
|
2668
|
+
self.epanet_api.api.ENcloseQ()
|
|
2669
|
+
self.epanet_api.api.ENcloseH()
|
|
2605
2670
|
|
|
2606
2671
|
self.__running_simulation = False
|
|
2607
2672
|
|
|
@@ -2718,6 +2783,7 @@ class ScenarioSimulator():
|
|
|
2718
2783
|
|
|
2719
2784
|
def set_general_parameters(self, demand_model: dict = None, simulation_duration: int = None,
|
|
2720
2785
|
hydraulic_time_step: int = None, quality_time_step: int = None,
|
|
2786
|
+
advanced_quality_time_step: int = None,
|
|
2721
2787
|
reporting_time_step: int = None, reporting_time_start: int = None,
|
|
2722
2788
|
flow_units_id: int = None, quality_model: dict = None) -> None:
|
|
2723
2789
|
"""
|
|
@@ -2750,6 +2816,12 @@ class ScenarioSimulator():
|
|
|
2750
2816
|
Quality time step -- i.e. the interval at which qualities are computed.
|
|
2751
2817
|
Should be much smaller than the hydraulic time step!
|
|
2752
2818
|
|
|
2819
|
+
The default is None.
|
|
2820
|
+
advanced_quality_time_step : `ìnt`, optional
|
|
2821
|
+
Time step in the advanced quality simuliation -- i.e. EPANET-MSX simulation.
|
|
2822
|
+
This number specifies the interval at which all species concentrations are.
|
|
2823
|
+
Should be much smaller than the hydraulic time step!
|
|
2824
|
+
|
|
2753
2825
|
The default is None.
|
|
2754
2826
|
reporting_time_step : `int`, optional
|
|
2755
2827
|
Report time step -- i.e. the interval at which hydraulics and quality states are
|
|
@@ -2862,6 +2934,14 @@ class ScenarioSimulator():
|
|
|
2862
2934
|
"greater than the hydraulic time step")
|
|
2863
2935
|
self.epanet_api.setTimeQualityStep(quality_time_step)
|
|
2864
2936
|
|
|
2937
|
+
if advanced_quality_time_step is not None:
|
|
2938
|
+
if not isinstance(advanced_quality_time_step, int) or \
|
|
2939
|
+
advanced_quality_time_step <= 0 or \
|
|
2940
|
+
advanced_quality_time_step > self.epanet_api.getTimeHydraulicStep():
|
|
2941
|
+
raise ValueError("'advanced_quality_time_step' must be a positive integer " +
|
|
2942
|
+
"that is not greater than the hydraulic time step")
|
|
2943
|
+
self.epanet_api.setMSXTimeStep(advanced_quality_time_step)
|
|
2944
|
+
|
|
2865
2945
|
if quality_model is not None:
|
|
2866
2946
|
if quality_model["type"] == "NONE":
|
|
2867
2947
|
self.epanet_api.setQualityType("none")
|
|
@@ -3459,7 +3539,7 @@ class ScenarioSimulator():
|
|
|
3459
3539
|
source_type: int, pattern_id: str = None,
|
|
3460
3540
|
source_strength: int = 1.) -> None:
|
|
3461
3541
|
"""
|
|
3462
|
-
Adds a new external
|
|
3542
|
+
Adds a new external bulk species injection source at a particular node.
|
|
3463
3543
|
|
|
3464
3544
|
Only for EPANET-MSX scenarios.
|
|
3465
3545
|
|
|
@@ -3472,6 +3552,8 @@ class ScenarioSimulator():
|
|
|
3472
3552
|
is placed.
|
|
3473
3553
|
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
3474
3554
|
1d source pattern.
|
|
3555
|
+
|
|
3556
|
+
Note that the pattern time step is equivalent to the EPANET pattern time step.
|
|
3475
3557
|
source_type : `int`,
|
|
3476
3558
|
Type of the external (bulk or surface) species injection source -- must be one of
|
|
3477
3559
|
the following EPANET toolkit constants:
|
|
@@ -3537,17 +3619,18 @@ class ScenarioSimulator():
|
|
|
3537
3619
|
any(not isinstance(species_id, str) or not isinstance(node_initial_conc, list)
|
|
3538
3620
|
for species_id, node_initial_conc in inital_conc.items()) or \
|
|
3539
3621
|
any(not isinstance(node_initial_conc, tuple)
|
|
3540
|
-
for node_initial_conc in inital_conc.values()) or \
|
|
3622
|
+
for node_initial_conc in list(itertools.chain(*inital_conc.values()))) or \
|
|
3541
3623
|
any(not isinstance(node_id, str) or not isinstance(conc, float)
|
|
3542
|
-
for node_id, conc in inital_conc.values()):
|
|
3624
|
+
for node_id, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3543
3625
|
raise TypeError("'inital_conc' must be an instance of " +
|
|
3544
3626
|
"'dict[str, list[tuple[str, float]]'")
|
|
3627
|
+
inital_conc_values = list(itertools.chain(*inital_conc.values()))
|
|
3545
3628
|
if any(species_id not in self.sensor_config.bulk_species
|
|
3546
3629
|
for species_id in inital_conc.keys()):
|
|
3547
3630
|
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3548
|
-
if any(node_id not in self.sensor_config.nodes for node_id, _ in
|
|
3631
|
+
if any(node_id not in self.sensor_config.nodes for node_id, _ in inital_conc_values):
|
|
3549
3632
|
raise ValueError("Unknown node ID in 'inital_conc'")
|
|
3550
|
-
if any(conc < 0 for _, conc in
|
|
3633
|
+
if any(conc < 0 for _, conc in inital_conc_values):
|
|
3551
3634
|
raise ValueError("Initial node concentration can not be negative")
|
|
3552
3635
|
|
|
3553
3636
|
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)
|
epyt_flow/utils.py
CHANGED
|
@@ -67,6 +67,36 @@ def volume_to_level(tank_volume: float, tank_diameter: float) -> float:
|
|
|
67
67
|
return (4. / (math.pow(tank_diameter, 2) * math.pi)) * tank_volume
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def level_to_volume(tank_level: float, tank_diameter: float) -> float:
|
|
71
|
+
"""
|
|
72
|
+
Computes the volume of water in a tank given the water level in the tank.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
tank_level : `float`
|
|
77
|
+
Water level in the tank.
|
|
78
|
+
tank_diameter : `float`
|
|
79
|
+
Diameter of the tank.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
`float`
|
|
84
|
+
Water volume in tank.
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(tank_level, float):
|
|
87
|
+
raise TypeError("'tank_level' must be an instace of 'float' " +
|
|
88
|
+
f"but not of '{type(tank_level)}'")
|
|
89
|
+
if tank_level < 0:
|
|
90
|
+
raise ValueError("'tank_level' can not be negative")
|
|
91
|
+
if not isinstance(tank_diameter, float):
|
|
92
|
+
raise TypeError("'tank_diameter' must be an instace of 'float' " +
|
|
93
|
+
f"but not of '{type(tank_diameter)}'")
|
|
94
|
+
if tank_diameter <= 0:
|
|
95
|
+
raise ValueError("'tank_diameter' must be greater than zero")
|
|
96
|
+
|
|
97
|
+
return tank_level * math.pow(0.5 * tank_diameter, 2) * math.pi
|
|
98
|
+
|
|
99
|
+
|
|
70
100
|
def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_label: str = None,
|
|
71
101
|
y_axis_label: str = None, y_ticks: tuple[list[float], list[str]] = None,
|
|
72
102
|
show: bool = True, save_to_file: str = None,
|