epyt-flow 0.10.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/data/networks.py +27 -14
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +11 -13
- 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/serialization.py +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/backend/__init__.py +1 -0
- epyt_flow/simulation/backend/my_epyt.py +1056 -0
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/quality_events.py +3 -1
- epyt_flow/simulation/scada/scada_data.py +716 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +645 -119
- epyt_flow/simulation/sensor_config.py +18 -2
- epyt_flow/topology.py +24 -7
- epyt_flow/uncertainty/model_uncertainty.py +80 -62
- epyt_flow/uncertainty/sensor_noise.py +15 -4
- epyt_flow/uncertainty/uncertainties.py +71 -18
- epyt_flow/uncertainty/utils.py +40 -13
- epyt_flow/utils.py +45 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/visualization/scenario_visualizer.py +1240 -0
- epyt_flow/visualization/visualization_utils.py +738 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/METADATA +15 -4
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +35 -36
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/WHEEL +1 -1
- epyt_flow/gym/control_gyms.py +0 -47
- epyt_flow/metrics.py +0 -466
- epyt_flow/models/__init__.py +0 -2
- epyt_flow/models/event_detector.py +0 -31
- epyt_flow/models/sensor_interpolation_detector.py +0 -118
- epyt_flow/simulation/scada/advanced_control.py +0 -138
- epyt_flow/simulation/scenario_visualizer.py +0 -1307
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -5,9 +5,10 @@ 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
|
-
from typing import Generator, Union
|
|
11
|
+
from typing import Generator, Union, Optional
|
|
11
12
|
from copy import deepcopy
|
|
12
13
|
import shutil
|
|
13
14
|
import warnings
|
|
@@ -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.
|
|
@@ -112,7 +112,6 @@ class ScenarioSimulator():
|
|
|
112
112
|
self._model_uncertainty = ModelUncertainty()
|
|
113
113
|
self._sensor_noise = None
|
|
114
114
|
self._sensor_config = None
|
|
115
|
-
self._advanced_controls = []
|
|
116
115
|
self._custom_controls = []
|
|
117
116
|
self._simple_controls = []
|
|
118
117
|
self._complex_controls = []
|
|
@@ -120,6 +119,7 @@ class ScenarioSimulator():
|
|
|
120
119
|
self._sensor_reading_events = []
|
|
121
120
|
self.__running_simulation = False
|
|
122
121
|
|
|
122
|
+
# Check availability of custom EPANET libraries
|
|
123
123
|
custom_epanet_lib = None
|
|
124
124
|
custom_epanetmsx_lib = None
|
|
125
125
|
if sys.platform.startswith("linux") or sys.platform.startswith("darwin") :
|
|
@@ -136,48 +136,49 @@ class ScenarioSimulator():
|
|
|
136
136
|
if os.path.isfile(os.path.join(path_to_custom_libs, libepanetmsx_name)):
|
|
137
137
|
custom_epanetmsx_lib = os.path.join(path_to_custom_libs, libepanetmsx_name)
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
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
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
163
|
else:
|
|
160
|
-
|
|
161
|
-
shutil.copyfile(self.
|
|
162
|
-
|
|
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
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
shutil.copyfile(self.__f_msx_in, my_f_msx_in)
|
|
170
|
-
else:
|
|
171
|
-
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)
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
display_msg=epanet_verbose,
|
|
176
|
-
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)
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
# Do not raise exceptions in the case of EPANET warnings and errors
|
|
179
|
+
self.epanet_api.set_error_handling(False)
|
|
180
180
|
|
|
181
|
+
# Parse and initialize scenario
|
|
181
182
|
self._simple_controls = self._parse_simple_control_rules()
|
|
182
183
|
self._complex_controls = self._parse_complex_control_rules()
|
|
183
184
|
|
|
@@ -190,9 +191,6 @@ class ScenarioSimulator():
|
|
|
190
191
|
self._sensor_noise = scenario_config.sensor_noise
|
|
191
192
|
self._sensor_config = scenario_config.sensor_config
|
|
192
193
|
|
|
193
|
-
if scenario_config.advanced_controls is not None:
|
|
194
|
-
for control in scenario_config.advanced_controls:
|
|
195
|
-
self.add_advanced_control(control)
|
|
196
194
|
for control in scenario_config.custom_controls:
|
|
197
195
|
self.add_custom_control(control)
|
|
198
196
|
for control in scenario_config.simple_controls:
|
|
@@ -342,22 +340,6 @@ class ScenarioSimulator():
|
|
|
342
340
|
|
|
343
341
|
self._sensor_config = sensor_config
|
|
344
342
|
|
|
345
|
-
@property
|
|
346
|
-
def advanced_controls(self) -> list:
|
|
347
|
-
"""
|
|
348
|
-
Returns all advanced control modules.
|
|
349
|
-
|
|
350
|
-
Returns
|
|
351
|
-
-------
|
|
352
|
-
list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
|
|
353
|
-
All advanced control modules.
|
|
354
|
-
"""
|
|
355
|
-
warnings.warn("'AdvancedControlModule' is deprecated and will be removed in a " +
|
|
356
|
-
"future release -- use 'CustomControlModule' instead")
|
|
357
|
-
self._adapt_to_network_changes()
|
|
358
|
-
|
|
359
|
-
return deepcopy(self._advanced_controls)
|
|
360
|
-
|
|
361
343
|
@property
|
|
362
344
|
def custom_controls(self) -> list[CustomControlModule]:
|
|
363
345
|
"""
|
|
@@ -990,7 +972,6 @@ class ScenarioSimulator():
|
|
|
990
972
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
991
973
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
992
974
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
993
|
-
advanced_controls=None if len(self._advanced_controls) == 0 else self.advanced_controls,
|
|
994
975
|
custom_controls=self.custom_controls,
|
|
995
976
|
simple_controls=self.simple_controls,
|
|
996
977
|
complex_controls=self.complex_controls,
|
|
@@ -1041,6 +1022,7 @@ class ScenarioSimulator():
|
|
|
1041
1022
|
nodes_type = [self.epanet_api.TYPENODE[i] for i in self.epanet_api.getNodeTypeIndex()]
|
|
1042
1023
|
nodes_coord = [self.epanet_api.api.ENgetcoord(node_idx)
|
|
1043
1024
|
for node_idx in self.epanet_api.getNodeIndex()]
|
|
1025
|
+
nodes_comments = self.epanet_api.getNodeComment()
|
|
1044
1026
|
node_tank_names = self.epanet_api.getNodeTankNameID()
|
|
1045
1027
|
|
|
1046
1028
|
links_id = self.epanet_api.getLinkNameID()
|
|
@@ -1060,21 +1042,29 @@ class ScenarioSimulator():
|
|
|
1060
1042
|
|
|
1061
1043
|
# Build graph describing the topology
|
|
1062
1044
|
nodes = []
|
|
1063
|
-
for node_id, node_elevation, node_type,
|
|
1064
|
-
|
|
1045
|
+
for node_id, node_elevation, node_type, \
|
|
1046
|
+
node_coord, node_comment in zip(nodes_id, nodes_elevation, nodes_type, nodes_coord,
|
|
1047
|
+
nodes_comments):
|
|
1065
1048
|
node_info = {"elevation": node_elevation,
|
|
1066
1049
|
"coord": node_coord,
|
|
1050
|
+
"comment": node_comment,
|
|
1067
1051
|
"type": node_type}
|
|
1068
1052
|
if node_type == "TANK":
|
|
1069
1053
|
node_tank_idx = node_tank_names.index(node_id) + 1
|
|
1070
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])
|
|
1071
1060
|
|
|
1072
1061
|
nodes.append((node_id, node_info))
|
|
1073
1062
|
|
|
1074
1063
|
links = []
|
|
1075
|
-
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff,
|
|
1076
|
-
|
|
1077
|
-
|
|
1064
|
+
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff, \
|
|
1065
|
+
wall_coeff, loss_coeff in zip(links_id, links_type, links_data, links_diameter,
|
|
1066
|
+
links_length, links_roughness_coeff, links_bulk_coeff,
|
|
1067
|
+
links_wall_coeff, links_loss_coeff):
|
|
1078
1068
|
links.append((link_id, list(link),
|
|
1079
1069
|
{"type": link_type, "diameter": diameter, "length": length,
|
|
1080
1070
|
"roughness_coeff": roughness_coeff,
|
|
@@ -1110,7 +1100,7 @@ class ScenarioSimulator():
|
|
|
1110
1100
|
|
|
1111
1101
|
The default is None.
|
|
1112
1102
|
"""
|
|
1113
|
-
from
|
|
1103
|
+
from ..visualization import ScenarioVisualizer
|
|
1114
1104
|
ScenarioVisualizer(self).show_plot(export_to_file)
|
|
1115
1105
|
|
|
1116
1106
|
def randomize_demands(self) -> None:
|
|
@@ -1155,7 +1145,14 @@ class ScenarioSimulator():
|
|
|
1155
1145
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1156
1146
|
The pattern -- i.e. multiplier factors over time.
|
|
1157
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
|
+
|
|
1158
1152
|
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
1153
|
+
if pattern_idx == 0:
|
|
1154
|
+
raise ValueError(f"Unknown pattern '{pattern_id}'")
|
|
1155
|
+
|
|
1159
1156
|
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
1160
1157
|
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
1161
1158
|
for t in range(pattern_length)])
|
|
@@ -1183,7 +1180,41 @@ class ScenarioSimulator():
|
|
|
1183
1180
|
raise ValueError(f"Inconsistent pattern shape '{pattern.shape}' " +
|
|
1184
1181
|
"detected. Expected a one dimensional array!")
|
|
1185
1182
|
|
|
1186
|
-
self.epanet_api.addPattern(pattern_id, pattern)
|
|
1183
|
+
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
1184
|
+
if pattern_idx == 0:
|
|
1185
|
+
raise RuntimeError("Failed to add pattern! " +
|
|
1186
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
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
|
|
1187
1218
|
|
|
1188
1219
|
def get_node_demand_pattern(self, node_id: str) -> np.ndarray:
|
|
1189
1220
|
"""
|
|
@@ -1200,6 +1231,12 @@ class ScenarioSimulator():
|
|
|
1200
1231
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1201
1232
|
The demand pattern -- i.e. multiplier factors over time.
|
|
1202
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
|
+
|
|
1203
1240
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
1204
1241
|
demand_category = self.epanet_api.getNodeDemandCategoriesNumber()[node_idx]
|
|
1205
1242
|
demand_pattern_id = self.epanet_api.getNodeDemandPatternNameID()[demand_category][node_idx - 1]
|
|
@@ -1257,25 +1294,6 @@ class ScenarioSimulator():
|
|
|
1257
1294
|
self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
|
|
1258
1295
|
base_demand, demand_pattern_id)
|
|
1259
1296
|
|
|
1260
|
-
def add_advanced_control(self, control) -> None:
|
|
1261
|
-
"""
|
|
1262
|
-
Adds an advanced control module to the scenario simulation.
|
|
1263
|
-
|
|
1264
|
-
Parameters
|
|
1265
|
-
----------
|
|
1266
|
-
control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
|
|
1267
|
-
Advanced control module.
|
|
1268
|
-
"""
|
|
1269
|
-
self._adapt_to_network_changes()
|
|
1270
|
-
|
|
1271
|
-
from .scada.advanced_control import AdvancedControlModule
|
|
1272
|
-
if not isinstance(control, AdvancedControlModule):
|
|
1273
|
-
raise TypeError("'control' must be an instance of " +
|
|
1274
|
-
"'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
|
|
1275
|
-
f"'{type(control)}'")
|
|
1276
|
-
|
|
1277
|
-
self._advanced_controls.append(control)
|
|
1278
|
-
|
|
1279
1297
|
def add_custom_control(self, control: CustomControlModule) -> None:
|
|
1280
1298
|
"""
|
|
1281
1299
|
Adds a custom control module to the scenario simulation.
|
|
@@ -1934,9 +1952,6 @@ class ScenarioSimulator():
|
|
|
1934
1952
|
for event in self._system_events:
|
|
1935
1953
|
event.reset()
|
|
1936
1954
|
|
|
1937
|
-
if self._advanced_controls is not None:
|
|
1938
|
-
for c in self._advanced_controls:
|
|
1939
|
-
c.init(self.epanet_api)
|
|
1940
1955
|
if self._custom_controls is not None:
|
|
1941
1956
|
for control in self._custom_controls:
|
|
1942
1957
|
control.init(self.epanet_api)
|
|
@@ -2006,6 +2021,7 @@ class ScenarioSimulator():
|
|
|
2006
2021
|
result[data_type] = None
|
|
2007
2022
|
|
|
2008
2023
|
return ScadaData(**result,
|
|
2024
|
+
network_topo=self.get_topology(),
|
|
2009
2025
|
sensor_config=self._sensor_config,
|
|
2010
2026
|
sensor_reading_events=self._sensor_reading_events,
|
|
2011
2027
|
sensor_noise=self._sensor_noise,
|
|
@@ -2069,6 +2085,8 @@ class ScenarioSimulator():
|
|
|
2069
2085
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
2070
2086
|
hyd_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
2071
2087
|
|
|
2088
|
+
network_topo = self.get_topology()
|
|
2089
|
+
|
|
2072
2090
|
if use_quality_time_step_as_reporting_time_step is True:
|
|
2073
2091
|
quality_time_step = self.epanet_api.getMSXTimeStep()
|
|
2074
2092
|
reporting_time_step = quality_time_step
|
|
@@ -2159,7 +2177,7 @@ class ScenarioSimulator():
|
|
|
2159
2177
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2160
2178
|
"sensor_readings_time": np.array([0])}
|
|
2161
2179
|
else:
|
|
2162
|
-
data = ScadaData(sensor_config=self._sensor_config,
|
|
2180
|
+
data = ScadaData(network_topo=network_topo, sensor_config=self._sensor_config,
|
|
2163
2181
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
2164
2182
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
2165
2183
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
@@ -2203,7 +2221,8 @@ class ScenarioSimulator():
|
|
|
2203
2221
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2204
2222
|
"sensor_readings_time": np.array([total_time])}
|
|
2205
2223
|
else:
|
|
2206
|
-
data = ScadaData(
|
|
2224
|
+
data = ScadaData(network_topo=network_topo,
|
|
2225
|
+
sensor_config=self._sensor_config,
|
|
2207
2226
|
bulk_species_node_concentration_raw=
|
|
2208
2227
|
bulk_species_node_concentrations,
|
|
2209
2228
|
bulk_species_link_concentration_raw=
|
|
@@ -2284,6 +2303,7 @@ class ScenarioSimulator():
|
|
|
2284
2303
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
2285
2304
|
|
|
2286
2305
|
return ScadaData(**result,
|
|
2306
|
+
network_topo=self.get_topology(),
|
|
2287
2307
|
sensor_config=self._sensor_config,
|
|
2288
2308
|
sensor_reading_events=self._sensor_reading_events,
|
|
2289
2309
|
sensor_noise=self._sensor_noise,
|
|
@@ -2343,6 +2363,8 @@ class ScenarioSimulator():
|
|
|
2343
2363
|
requested_time_step = quality_time_step
|
|
2344
2364
|
reporting_time_step = quality_time_step
|
|
2345
2365
|
|
|
2366
|
+
network_topo = self.get_topology()
|
|
2367
|
+
|
|
2346
2368
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
2347
2369
|
|
|
2348
2370
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -2371,10 +2393,11 @@ class ScenarioSimulator():
|
|
|
2371
2393
|
pass
|
|
2372
2394
|
|
|
2373
2395
|
# Compute current time step
|
|
2374
|
-
t = self.epanet_api.
|
|
2396
|
+
t = self.epanet_api.api.ENrunQ()
|
|
2375
2397
|
total_time = t
|
|
2376
2398
|
|
|
2377
2399
|
# Fetch data
|
|
2400
|
+
error_code = self.epanet_api.get_last_error_code()
|
|
2378
2401
|
quality_node_data = self.epanet_api.getNodeActualQuality().reshape(1, -1)
|
|
2379
2402
|
quality_link_data = self.epanet_api.getLinkActualQuality().reshape(1, -1)
|
|
2380
2403
|
|
|
@@ -2383,12 +2406,15 @@ class ScenarioSimulator():
|
|
|
2383
2406
|
if return_as_dict is True:
|
|
2384
2407
|
data = {"node_quality_data_raw": quality_node_data,
|
|
2385
2408
|
"link_quality_data_raw": quality_link_data,
|
|
2386
|
-
"sensor_readings_time": np.array([total_time])
|
|
2409
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2410
|
+
"warnings_code": np.array([error_code])}
|
|
2387
2411
|
else:
|
|
2388
|
-
data = ScadaData(
|
|
2412
|
+
data = ScadaData(network_topo=network_topo,
|
|
2413
|
+
sensor_config=self._sensor_config,
|
|
2389
2414
|
node_quality_data_raw=quality_node_data,
|
|
2390
2415
|
link_quality_data_raw=quality_link_data,
|
|
2391
2416
|
sensor_readings_time=np.array([total_time]),
|
|
2417
|
+
warnings_code=np.array([error_code]),
|
|
2392
2418
|
sensor_reading_events=self._sensor_reading_events,
|
|
2393
2419
|
sensor_noise=self._sensor_noise,
|
|
2394
2420
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2401,9 +2427,9 @@ class ScenarioSimulator():
|
|
|
2401
2427
|
yield data
|
|
2402
2428
|
|
|
2403
2429
|
# Next
|
|
2404
|
-
tstep = self.epanet_api.
|
|
2430
|
+
tstep = self.epanet_api.api.ENstepQ()
|
|
2405
2431
|
|
|
2406
|
-
self.epanet_api.
|
|
2432
|
+
self.epanet_api.closeQualityAnalysis()
|
|
2407
2433
|
|
|
2408
2434
|
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2409
2435
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
@@ -2461,6 +2487,7 @@ class ScenarioSimulator():
|
|
|
2461
2487
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
2462
2488
|
|
|
2463
2489
|
result = ScadaData(**result,
|
|
2490
|
+
network_topo=self.get_topology(),
|
|
2464
2491
|
sensor_config=self._sensor_config,
|
|
2465
2492
|
sensor_reading_events=self._sensor_reading_events,
|
|
2466
2493
|
sensor_noise=self._sensor_noise,
|
|
@@ -2524,8 +2551,8 @@ class ScenarioSimulator():
|
|
|
2524
2551
|
|
|
2525
2552
|
self.__running_simulation = True
|
|
2526
2553
|
|
|
2527
|
-
self.epanet_api.
|
|
2528
|
-
self.epanet_api.
|
|
2554
|
+
self.epanet_api.api.ENopenH()
|
|
2555
|
+
self.epanet_api.api.ENopenQ()
|
|
2529
2556
|
self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
|
|
2530
2557
|
self.epanet_api.initializeQualityAnalysis(ToolkitConstants.EN_SAVE)
|
|
2531
2558
|
|
|
@@ -2533,6 +2560,8 @@ class ScenarioSimulator():
|
|
|
2533
2560
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
2534
2561
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
2535
2562
|
|
|
2563
|
+
network_topo = self.get_topology()
|
|
2564
|
+
|
|
2536
2565
|
if verbose is True:
|
|
2537
2566
|
print("Running EPANET ...")
|
|
2538
2567
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
@@ -2562,8 +2591,11 @@ class ScenarioSimulator():
|
|
|
2562
2591
|
event.step(total_time + tstep)
|
|
2563
2592
|
|
|
2564
2593
|
# Compute current time step
|
|
2565
|
-
t = self.epanet_api.
|
|
2566
|
-
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()
|
|
2567
2599
|
total_time = t
|
|
2568
2600
|
|
|
2569
2601
|
# Fetch data
|
|
@@ -2582,7 +2614,8 @@ class ScenarioSimulator():
|
|
|
2582
2614
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2583
2615
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2584
2616
|
|
|
2585
|
-
scada_data = ScadaData(
|
|
2617
|
+
scada_data = ScadaData(network_topo=network_topo,
|
|
2618
|
+
sensor_config=self._sensor_config,
|
|
2586
2619
|
pressure_data_raw=pressure_data,
|
|
2587
2620
|
flow_data_raw=flow_data,
|
|
2588
2621
|
demand_data_raw=demand_data,
|
|
@@ -2594,6 +2627,7 @@ class ScenarioSimulator():
|
|
|
2594
2627
|
pumps_energy_usage_data_raw=pumps_energy_usage_data,
|
|
2595
2628
|
pumps_efficiency_data_raw=pumps_efficiency_data,
|
|
2596
2629
|
sensor_readings_time=np.array([total_time]),
|
|
2630
|
+
warnings_code=np.array([error_code]),
|
|
2597
2631
|
sensor_reading_events=self._sensor_reading_events,
|
|
2598
2632
|
sensor_noise=self._sensor_noise,
|
|
2599
2633
|
frozen_sensor_config=frozen_sensor_config)
|
|
@@ -2611,7 +2645,8 @@ class ScenarioSimulator():
|
|
|
2611
2645
|
"tanks_volume_data_raw": tanks_volume_data,
|
|
2612
2646
|
"pumps_energy_usage_data_raw": pumps_energy_usage_data,
|
|
2613
2647
|
"pumps_efficiency_data_raw": pumps_efficiency_data,
|
|
2614
|
-
"sensor_readings_time": np.array([total_time])
|
|
2648
|
+
"sensor_readings_time": np.array([total_time]),
|
|
2649
|
+
"warnings_code": np.array([error_code])}
|
|
2615
2650
|
else:
|
|
2616
2651
|
data = scada_data
|
|
2617
2652
|
|
|
@@ -2623,17 +2658,15 @@ class ScenarioSimulator():
|
|
|
2623
2658
|
yield data
|
|
2624
2659
|
|
|
2625
2660
|
# Apply control modules
|
|
2626
|
-
for control in self._advanced_controls:
|
|
2627
|
-
control.step(scada_data)
|
|
2628
2661
|
for control in self._custom_controls:
|
|
2629
2662
|
control.step(scada_data)
|
|
2630
2663
|
|
|
2631
2664
|
# Next
|
|
2632
|
-
tstep = self.epanet_api.
|
|
2633
|
-
self.epanet_api.
|
|
2665
|
+
tstep = self.epanet_api.api.ENnextH()
|
|
2666
|
+
self.epanet_api.api.ENnextQ()
|
|
2634
2667
|
|
|
2635
|
-
self.epanet_api.
|
|
2636
|
-
self.epanet_api.
|
|
2668
|
+
self.epanet_api.api.ENcloseQ()
|
|
2669
|
+
self.epanet_api.api.ENcloseH()
|
|
2637
2670
|
|
|
2638
2671
|
self.__running_simulation = False
|
|
2639
2672
|
|
|
@@ -2643,16 +2676,6 @@ class ScenarioSimulator():
|
|
|
2643
2676
|
self.__running_simulation = False
|
|
2644
2677
|
raise ex
|
|
2645
2678
|
|
|
2646
|
-
def run_simulation_as_generator(self, hyd_export: str = None, verbose: bool = False,
|
|
2647
|
-
support_abort: bool = False,
|
|
2648
|
-
return_as_dict: bool = False,
|
|
2649
|
-
frozen_sensor_config: bool = False,
|
|
2650
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2651
|
-
warnings.warn("'run_simulation_as_generator' is deprecated and will be removed in " +
|
|
2652
|
-
"future releases -- use 'run_hydraulic_simulation_as_generator' instead")
|
|
2653
|
-
return self.run_hydraulic_simulation_as_generator(hyd_export, verbose, support_abort,
|
|
2654
|
-
return_as_dict, frozen_sensor_config)
|
|
2655
|
-
|
|
2656
2679
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2657
2680
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
2658
2681
|
"""
|
|
@@ -2760,6 +2783,7 @@ class ScenarioSimulator():
|
|
|
2760
2783
|
|
|
2761
2784
|
def set_general_parameters(self, demand_model: dict = None, simulation_duration: int = None,
|
|
2762
2785
|
hydraulic_time_step: int = None, quality_time_step: int = None,
|
|
2786
|
+
advanced_quality_time_step: int = None,
|
|
2763
2787
|
reporting_time_step: int = None, reporting_time_start: int = None,
|
|
2764
2788
|
flow_units_id: int = None, quality_model: dict = None) -> None:
|
|
2765
2789
|
"""
|
|
@@ -2792,6 +2816,12 @@ class ScenarioSimulator():
|
|
|
2792
2816
|
Quality time step -- i.e. the interval at which qualities are computed.
|
|
2793
2817
|
Should be much smaller than the hydraulic time step!
|
|
2794
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
|
+
|
|
2795
2825
|
The default is None.
|
|
2796
2826
|
reporting_time_step : `int`, optional
|
|
2797
2827
|
Report time step -- i.e. the interval at which hydraulics and quality states are
|
|
@@ -2904,6 +2934,14 @@ class ScenarioSimulator():
|
|
|
2904
2934
|
"greater than the hydraulic time step")
|
|
2905
2935
|
self.epanet_api.setTimeQualityStep(quality_time_step)
|
|
2906
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
|
+
|
|
2907
2945
|
if quality_model is not None:
|
|
2908
2946
|
if quality_model["type"] == "NONE":
|
|
2909
2947
|
self.epanet_api.setQualityType("none")
|
|
@@ -2945,6 +2983,202 @@ class ScenarioSimulator():
|
|
|
2945
2983
|
|
|
2946
2984
|
return list(set(events_times))
|
|
2947
2985
|
|
|
2986
|
+
def set_pump_energy_price_pattern(self, pump_id: str, pattern: np.ndarray,
|
|
2987
|
+
pattern_id: Optional[str] = None) -> None:
|
|
2988
|
+
"""
|
|
2989
|
+
Specifies/sets the energy price pattern of a given pump.
|
|
2990
|
+
|
|
2991
|
+
Overwrites any existing (energy price) patterns of the given pump.
|
|
2992
|
+
|
|
2993
|
+
Parameters
|
|
2994
|
+
----------
|
|
2995
|
+
pump_id : `str`
|
|
2996
|
+
ID of the pump.
|
|
2997
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2998
|
+
Pattern of multipliers.
|
|
2999
|
+
pattern_id : `str`, optional
|
|
3000
|
+
ID of the pattern.
|
|
3001
|
+
If not specified, 'energy_price_{pump_id}' will be used as the pattern ID.
|
|
3002
|
+
|
|
3003
|
+
The default is None.
|
|
3004
|
+
"""
|
|
3005
|
+
if not isinstance(pump_id, str):
|
|
3006
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3007
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3008
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3009
|
+
if not isinstance(pattern, np.ndarray):
|
|
3010
|
+
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
3011
|
+
f"but no of '{type(pattern)}'")
|
|
3012
|
+
if len(pattern.shape) > 1:
|
|
3013
|
+
raise ValueError("'pattern' must be 1-dimensional")
|
|
3014
|
+
if pattern_id is not None:
|
|
3015
|
+
if not isinstance(pattern_id, str):
|
|
3016
|
+
raise TypeError("'pattern_id' must be an instance of 'str' " +
|
|
3017
|
+
f"but not of '{type(pattern_id)}'")
|
|
3018
|
+
else:
|
|
3019
|
+
pattern_id = f"energy_price_{pump_id}"
|
|
3020
|
+
|
|
3021
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
3022
|
+
if pattern_idx != 0:
|
|
3023
|
+
warnings.warn(f"Overwriting existing pattern '{pattern_id}'")
|
|
3024
|
+
|
|
3025
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3026
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
3027
|
+
if pattern_idx != 0:
|
|
3028
|
+
warnings.warn(f"Overwriting existing energy price pattern of pump '{pump_id}'")
|
|
3029
|
+
|
|
3030
|
+
self.add_pattern(pattern_id, pattern)
|
|
3031
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
3032
|
+
self.epanet_api.setLinkPumpEPat(pattern_idx)
|
|
3033
|
+
|
|
3034
|
+
def get_pump_energy_price_pattern(self, pump_id: str) -> np.ndarray:
|
|
3035
|
+
"""
|
|
3036
|
+
Returns the energy price pattern of a given pump.
|
|
3037
|
+
|
|
3038
|
+
Parameters
|
|
3039
|
+
----------
|
|
3040
|
+
pump_id : `str`
|
|
3041
|
+
ID of the pump.
|
|
3042
|
+
|
|
3043
|
+
Returns
|
|
3044
|
+
-------
|
|
3045
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
3046
|
+
Energy price pattern. None, if none exists.
|
|
3047
|
+
"""
|
|
3048
|
+
if not isinstance(pump_id, str):
|
|
3049
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3050
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3051
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3052
|
+
|
|
3053
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3054
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
3055
|
+
if pattern_idx == 0:
|
|
3056
|
+
return None
|
|
3057
|
+
else:
|
|
3058
|
+
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
3059
|
+
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
3060
|
+
for t in range(pattern_length)])
|
|
3061
|
+
|
|
3062
|
+
def get_pump_energy_price(self, pump_id: str) -> float:
|
|
3063
|
+
"""
|
|
3064
|
+
Returns the energy price of a given pump.
|
|
3065
|
+
|
|
3066
|
+
Parameters
|
|
3067
|
+
----------
|
|
3068
|
+
pump_id : `str`
|
|
3069
|
+
ID of the pump.
|
|
3070
|
+
|
|
3071
|
+
Returns
|
|
3072
|
+
-------
|
|
3073
|
+
`float`
|
|
3074
|
+
Energy price.
|
|
3075
|
+
"""
|
|
3076
|
+
if not isinstance(pump_id, str):
|
|
3077
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3078
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3079
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3080
|
+
|
|
3081
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3082
|
+
return self.epanet_api.getLinkPumpECost(pump_idx)
|
|
3083
|
+
|
|
3084
|
+
def set_pump_energy_price(self, pump_id, price: float) -> None:
|
|
3085
|
+
"""
|
|
3086
|
+
Sets the energy price of a given pump.
|
|
3087
|
+
|
|
3088
|
+
Parameters
|
|
3089
|
+
----------
|
|
3090
|
+
pump_id : `str`
|
|
3091
|
+
ID of the pump.
|
|
3092
|
+
price : `float`
|
|
3093
|
+
Energy price.
|
|
3094
|
+
"""
|
|
3095
|
+
if not isinstance(pump_id, str):
|
|
3096
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3097
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3098
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3099
|
+
if not isinstance(price, float):
|
|
3100
|
+
raise TypeError(f"'price' must be an instance of 'float' but not of '{type(price)}'")
|
|
3101
|
+
if price <= 0:
|
|
3102
|
+
raise ValueError("'price' must be positive")
|
|
3103
|
+
|
|
3104
|
+
pump_idx = self._sensor_config.pumps.index(pump_id) + 1
|
|
3105
|
+
pumps_energy_price = self.epanet_api.getLinkPumpECost()
|
|
3106
|
+
pumps_energy_price[pump_idx - 1] = price
|
|
3107
|
+
|
|
3108
|
+
self.epanet_api.setLinkPumpECost(pumps_energy_price)
|
|
3109
|
+
|
|
3110
|
+
def set_initial_link_status(self, link_id: str, status: int) -> None:
|
|
3111
|
+
"""
|
|
3112
|
+
Sets the initial status (open or closed) of a given link.
|
|
3113
|
+
|
|
3114
|
+
Parameters
|
|
3115
|
+
----------
|
|
3116
|
+
link_id : `str`
|
|
3117
|
+
ID of the link.
|
|
3118
|
+
status : `int`
|
|
3119
|
+
Initial status of the link. Must be one of the following EPANET constants:
|
|
3120
|
+
|
|
3121
|
+
- EN_CLOSED = 0
|
|
3122
|
+
- EN_OPEN = 1
|
|
3123
|
+
"""
|
|
3124
|
+
if not isinstance(link_id, str):
|
|
3125
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
3126
|
+
if link_id not in self._sensor_config.pumps:
|
|
3127
|
+
raise ValueError("Invalid link ID '{link_id}'")
|
|
3128
|
+
if not isinstance(status, int):
|
|
3129
|
+
raise TypeError(f"'status' must be an instance of 'int' but not of '{type(status)}'")
|
|
3130
|
+
if status not in [ActuatorConstants.EN_CLOSED, ActuatorConstants.EN_OPEN]:
|
|
3131
|
+
raise ValueError(f"Invalid link status '{status}'")
|
|
3132
|
+
|
|
3133
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3134
|
+
self.epanet_api.setLinkInitialStatus(link_idx, status)
|
|
3135
|
+
|
|
3136
|
+
def set_initial_pump_speed(self, pump_id: str, speed: float) -> None:
|
|
3137
|
+
"""
|
|
3138
|
+
Sets the initial pump speed of a given pump.
|
|
3139
|
+
|
|
3140
|
+
Parameters
|
|
3141
|
+
----------
|
|
3142
|
+
pump_id : `str`
|
|
3143
|
+
ID of the pump.
|
|
3144
|
+
speed : `float`
|
|
3145
|
+
Initial speed of the pump.
|
|
3146
|
+
"""
|
|
3147
|
+
if not isinstance(pump_id, str):
|
|
3148
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3149
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3150
|
+
raise ValueError("Invalid pump ID '{tank_id}'")
|
|
3151
|
+
if not isinstance(speed, float):
|
|
3152
|
+
raise TypeError(f"'speed' must be an instance of 'int' but not of '{type(speed)}'")
|
|
3153
|
+
if speed < 0:
|
|
3154
|
+
raise ValueError("'speed' can not be negative")
|
|
3155
|
+
|
|
3156
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3157
|
+
self.epanet_api.setLinkInitialSetting(pump_idx, speed)
|
|
3158
|
+
|
|
3159
|
+
def set_initial_tank_level(self, tank_id, level: int) -> None:
|
|
3160
|
+
"""
|
|
3161
|
+
Sets the initial water level of a given tank.
|
|
3162
|
+
|
|
3163
|
+
Parameters
|
|
3164
|
+
----------
|
|
3165
|
+
tank_id : `str`
|
|
3166
|
+
ID of the tank.
|
|
3167
|
+
level : `int`
|
|
3168
|
+
Initial water level in the tank.
|
|
3169
|
+
"""
|
|
3170
|
+
if not isinstance(tank_id, str):
|
|
3171
|
+
raise TypeError(f"'tank_id' must be an instance of 'str' but not of '{type(tank_id)}'")
|
|
3172
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3173
|
+
raise ValueError("Invalid tank ID '{tank_id}'")
|
|
3174
|
+
if not isinstance(level, int):
|
|
3175
|
+
raise TypeError(f"'level' must be an instance of 'int' but not of '{type(level)}'")
|
|
3176
|
+
if level < 0:
|
|
3177
|
+
raise ValueError("'level' can not be negative")
|
|
3178
|
+
|
|
3179
|
+
tank_idx = self.epanet_api.getNodeIndex(tank_id)
|
|
3180
|
+
self.epanet_api.setNodeTankInitialLevel(tank_idx, level)
|
|
3181
|
+
|
|
2948
3182
|
def __warn_if_quality_set(self):
|
|
2949
3183
|
qual_info = self.epanet_api.getQualityInfo()
|
|
2950
3184
|
if qual_info.QualityCode != ToolkitConstants.EN_NONE:
|
|
@@ -3057,7 +3291,7 @@ class ScenarioSimulator():
|
|
|
3057
3291
|
if pattern is None and pattern_id is None:
|
|
3058
3292
|
raise ValueError("'pattern_id' and 'pattern' can not be None at the same time")
|
|
3059
3293
|
if pattern_id is None:
|
|
3060
|
-
pattern_id = f"
|
|
3294
|
+
pattern_id = f"qual_src_pat_{node_id}"
|
|
3061
3295
|
|
|
3062
3296
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
3063
3297
|
|
|
@@ -3065,11 +3299,220 @@ class ScenarioSimulator():
|
|
|
3065
3299
|
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
3066
3300
|
else:
|
|
3067
3301
|
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
3302
|
+
if pattern_idx == 0:
|
|
3303
|
+
raise RuntimeError("Failed to add/get pattern! " +
|
|
3304
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
3068
3305
|
|
|
3069
3306
|
self.epanet_api.api.ENsetnodevalue(node_idx, ToolkitConstants.EN_SOURCETYPE, source_type)
|
|
3070
3307
|
self.epanet_api.setNodeSourceQuality(node_idx, source_strength)
|
|
3071
3308
|
self.epanet_api.setNodeSourcePatternIndex(node_idx, pattern_idx)
|
|
3072
3309
|
|
|
3310
|
+
def set_initial_node_quality(self, node_id: str, initial_quality: float) -> None:
|
|
3311
|
+
"""
|
|
3312
|
+
Specifies the initial quality at a given node.
|
|
3313
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3314
|
+
or percent for source tracing.
|
|
3315
|
+
|
|
3316
|
+
Parameters
|
|
3317
|
+
----------
|
|
3318
|
+
node_id : `str`
|
|
3319
|
+
ID of the node.
|
|
3320
|
+
initial_quality : `float`
|
|
3321
|
+
Initial node quality.
|
|
3322
|
+
"""
|
|
3323
|
+
self.set_quality_parameters(initial_quality={node_id, initial_quality})
|
|
3324
|
+
|
|
3325
|
+
def set_quality_parameters(self, initial_quality: Optional[dict[str, float]] = None,
|
|
3326
|
+
order_wall: Optional[int] = None, order_tank: Optional[int] = None,
|
|
3327
|
+
order_bulk: Optional[int] = None,
|
|
3328
|
+
global_wall_reaction_coefficient: Optional[float] = None,
|
|
3329
|
+
global_bulk_reaction_coefficient: Optional[float] = None,
|
|
3330
|
+
local_wall_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3331
|
+
local_bulk_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3332
|
+
local_tank_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3333
|
+
limiting_potential: Optional[float] = None) -> None:
|
|
3334
|
+
"""
|
|
3335
|
+
Specifies some parameters of the EPANET quality analysis.
|
|
3336
|
+
Note that those parameters are only relevant for EPANET but not for EPANET-MSX.
|
|
3337
|
+
|
|
3338
|
+
Parameters
|
|
3339
|
+
----------
|
|
3340
|
+
initial_quality : `dict[str, float]`, optional
|
|
3341
|
+
Specifies the initial quality (value in the dictionary) at nodes
|
|
3342
|
+
(key in the dictionary).
|
|
3343
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3344
|
+
or percent for source tracing.
|
|
3345
|
+
|
|
3346
|
+
The default is None.
|
|
3347
|
+
order_wall : `int`, optional
|
|
3348
|
+
Specifies the order of reactions occurring in the bulk fluid at pipe walls.
|
|
3349
|
+
Value for wall reactions must be either 0 or 1.
|
|
3350
|
+
If not specified, the default reaction order is 1.0.
|
|
3351
|
+
|
|
3352
|
+
The default is None.
|
|
3353
|
+
order_bulk : `int`, optional
|
|
3354
|
+
Specifies the order of reactions occurring in the bulk fluid in tanks.
|
|
3355
|
+
Value must be either 0 or 1.
|
|
3356
|
+
If not specified, the default reaction order is 1.0.
|
|
3357
|
+
|
|
3358
|
+
The default is None.
|
|
3359
|
+
global_wall_reaction_coefficient : `float`, optional
|
|
3360
|
+
Specifies the global value for all pipe wall reaction coefficients (pipes and tanks).
|
|
3361
|
+
If not specified, the default value is zero.
|
|
3362
|
+
|
|
3363
|
+
The default is None.
|
|
3364
|
+
global_bulk_reaction_coefficient : `float`, optional
|
|
3365
|
+
Specifies the global value for all bulk reaction coefficients (pipes and tanks).
|
|
3366
|
+
If not specified, the default value is zero.
|
|
3367
|
+
|
|
3368
|
+
The default is None.
|
|
3369
|
+
local_wall_reaction_coefficient : `dict[str, float]`, optional
|
|
3370
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3371
|
+
|
|
3372
|
+
The default is None.
|
|
3373
|
+
local_bulk_reaction_coefficient : `dict[str, float]`, optional
|
|
3374
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3375
|
+
|
|
3376
|
+
The default is None.
|
|
3377
|
+
local_tank_reaction_coefficient : `dict[str, float]`, optional
|
|
3378
|
+
Overrides the global reaction coefficients for specific tanks (key in dictionary).
|
|
3379
|
+
|
|
3380
|
+
The default is None.
|
|
3381
|
+
limiting_potential : `float`, optional
|
|
3382
|
+
Specifies that reaction rates are proportional to the difference between the
|
|
3383
|
+
current concentration and some (specified) limiting potential value.
|
|
3384
|
+
|
|
3385
|
+
The default is None.
|
|
3386
|
+
"""
|
|
3387
|
+
if initial_quality is not None:
|
|
3388
|
+
if not isinstance(initial_quality, dict):
|
|
3389
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]' " +
|
|
3390
|
+
f"but not of '{type(initial_quality)}'")
|
|
3391
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3392
|
+
for key, value in initial_quality):
|
|
3393
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]'")
|
|
3394
|
+
for node_id, node_init_qual in initial_quality:
|
|
3395
|
+
if node_id not in self._sensor_config.nodes:
|
|
3396
|
+
raise ValueError(f"Invalid node ID '{node_id}'")
|
|
3397
|
+
if node_init_qual < 0:
|
|
3398
|
+
raise ValueError(f"{node_id}: Initial node quality can not be negative")
|
|
3399
|
+
|
|
3400
|
+
init_qual = self.epanet_api.getNodeInitialQuality()
|
|
3401
|
+
for node_id, node_init_qual in initial_quality:
|
|
3402
|
+
node_idx = self.epanet_api.getNodeIndex(node_id) - 1
|
|
3403
|
+
init_qual[node_idx] = node_init_qual
|
|
3404
|
+
|
|
3405
|
+
self.epanet_api.setNodeInitialQuality(init_qual)
|
|
3406
|
+
|
|
3407
|
+
if order_wall is not None:
|
|
3408
|
+
if not isinstance(order_wall, int):
|
|
3409
|
+
raise TypeError("'order_wall' must be an instance of 'int' " +
|
|
3410
|
+
f"but not of '{type(order_wall)}'")
|
|
3411
|
+
if order_wall not in [0, 1]:
|
|
3412
|
+
raise ValueError(f"Invalid value '{order_wall}' for order_wall")
|
|
3413
|
+
|
|
3414
|
+
self.epanet_api.setOptionsPipeWallReactionOrder(order_wall)
|
|
3415
|
+
|
|
3416
|
+
if order_bulk is not None:
|
|
3417
|
+
if not isinstance(order_bulk, int):
|
|
3418
|
+
raise TypeError("'order_bulk' must be an instance of 'int' " +
|
|
3419
|
+
f"but not of '{type(order_bulk)}'")
|
|
3420
|
+
if order_bulk not in [0, 1]:
|
|
3421
|
+
raise ValueError(f"Invalid value '{order_bulk}' for order_bulk")
|
|
3422
|
+
|
|
3423
|
+
self.epanet_api.setOptionsPipeBulkReactionOrder(order_bulk)
|
|
3424
|
+
|
|
3425
|
+
if order_tank is not None:
|
|
3426
|
+
if not isinstance(order_tank, int):
|
|
3427
|
+
raise TypeError("'order_tank' must be an instance of 'int' " +
|
|
3428
|
+
f"but not of '{type(order_tank)}'")
|
|
3429
|
+
if order_tank not in [0, 1]:
|
|
3430
|
+
raise ValueError(f"Invalid value '{order_tank}' for order_wall")
|
|
3431
|
+
|
|
3432
|
+
self.epanet_api.setOptionsTankBulkReactionOrder(order_tank)
|
|
3433
|
+
|
|
3434
|
+
if global_wall_reaction_coefficient is not None:
|
|
3435
|
+
if not isinstance(global_wall_reaction_coefficient, float):
|
|
3436
|
+
raise TypeError("'global_wall_reaction_coefficient' must be an instance of " +
|
|
3437
|
+
f"'float' but not of '{type(global_wall_reaction_coefficient)}'")
|
|
3438
|
+
|
|
3439
|
+
wall_reaction_coeff = self.epanet_api.getLinkWallReactionCoeff()
|
|
3440
|
+
for i in range(len(wall_reaction_coeff)):
|
|
3441
|
+
wall_reaction_coeff[i] = global_wall_reaction_coefficient
|
|
3442
|
+
|
|
3443
|
+
self.epanet_api.setLinkWallReactionCoeff(wall_reaction_coeff)
|
|
3444
|
+
|
|
3445
|
+
if global_bulk_reaction_coefficient is not None:
|
|
3446
|
+
if not isinstance(global_bulk_reaction_coefficient, float):
|
|
3447
|
+
raise TypeError("'global_bulk_reaction_coefficient' must be an instance of " +
|
|
3448
|
+
f"'float' but not of '{type(global_bulk_reaction_coefficient)}'")
|
|
3449
|
+
|
|
3450
|
+
bulk_reaction_coeff = self.epanet_api.getLinkBulkReactionCoeff()
|
|
3451
|
+
for i in range(len(bulk_reaction_coeff)):
|
|
3452
|
+
bulk_reaction_coeff[i] = global_bulk_reaction_coefficient
|
|
3453
|
+
|
|
3454
|
+
self.epanet_api.setLinkBulkReactionCoeff(bulk_reaction_coeff)
|
|
3455
|
+
|
|
3456
|
+
if local_wall_reaction_coefficient is not None:
|
|
3457
|
+
if not isinstance(local_wall_reaction_coefficient, dict):
|
|
3458
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3459
|
+
"of 'dict[str, float]' but not of " +
|
|
3460
|
+
f"'{type(local_wall_reaction_coefficient)}'")
|
|
3461
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3462
|
+
for key, value in local_wall_reaction_coefficient):
|
|
3463
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3464
|
+
"of 'dict[str, float]'")
|
|
3465
|
+
for link_id, _ in local_wall_reaction_coefficient:
|
|
3466
|
+
if link_id not in self._sensor_config.links:
|
|
3467
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3468
|
+
|
|
3469
|
+
for link_id, link_reaction_coeff in local_wall_reaction_coefficient:
|
|
3470
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3471
|
+
self.epanet_api.setLinkWallReactionCoeff(link_idx, link_reaction_coeff)
|
|
3472
|
+
|
|
3473
|
+
if local_bulk_reaction_coefficient is not None:
|
|
3474
|
+
if not isinstance(local_bulk_reaction_coefficient, dict):
|
|
3475
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3476
|
+
"of 'dict[str, float]' but not of " +
|
|
3477
|
+
f"'{type(local_bulk_reaction_coefficient)}'")
|
|
3478
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3479
|
+
for key, value in local_bulk_reaction_coefficient):
|
|
3480
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3481
|
+
"of 'dict[str, float]'")
|
|
3482
|
+
for link_id, _ in local_bulk_reaction_coefficient:
|
|
3483
|
+
if link_id not in self._sensor_config.links:
|
|
3484
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3485
|
+
|
|
3486
|
+
for link_id, link_reaction_coeff in local_bulk_reaction_coefficient:
|
|
3487
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3488
|
+
self.epanet_api.setLinkBulkReactionCoeff(link_idx, link_reaction_coeff)
|
|
3489
|
+
|
|
3490
|
+
if local_tank_reaction_coefficient is not None:
|
|
3491
|
+
if not isinstance(local_tank_reaction_coefficient, dict):
|
|
3492
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3493
|
+
"of 'dict[str, float]' but not of " +
|
|
3494
|
+
f"'{type(local_tank_reaction_coefficient)}'")
|
|
3495
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3496
|
+
for key, value in local_tank_reaction_coefficient):
|
|
3497
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3498
|
+
"of 'dict[str, float]'")
|
|
3499
|
+
for tank_id, _ in local_tank_reaction_coefficient:
|
|
3500
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3501
|
+
raise ValueError(f"Invalid tank ID '{tank_id}'")
|
|
3502
|
+
|
|
3503
|
+
for tank_id, tank_reaction_coeff in local_tank_reaction_coefficient:
|
|
3504
|
+
tank_idx = self.epanet_api.getNodeTankIndex(tank_id)
|
|
3505
|
+
self.epanet_api.setNodeTankBulkReactionCoeff(tank_idx, tank_reaction_coeff)
|
|
3506
|
+
|
|
3507
|
+
if limiting_potential is not None:
|
|
3508
|
+
if not isinstance(limiting_potential, float):
|
|
3509
|
+
raise TypeError("'limiting_potential' must be an instance of 'float' " +
|
|
3510
|
+
f"but not of '{type(limiting_potential)}'")
|
|
3511
|
+
if limiting_potential < 0:
|
|
3512
|
+
raise ValueError("'limiting_potential' can not be negative")
|
|
3513
|
+
|
|
3514
|
+
self.epanet_api.setOptionsLimitingConcentration(limiting_potential)
|
|
3515
|
+
|
|
3073
3516
|
def enable_sourcetracing_analysis(self, trace_node_id: str) -> None:
|
|
3074
3517
|
"""
|
|
3075
3518
|
Set source tracing analysis -- i.e. tracks the percentage of flow from a given node
|
|
@@ -3096,7 +3539,9 @@ class ScenarioSimulator():
|
|
|
3096
3539
|
source_type: int, pattern_id: str = None,
|
|
3097
3540
|
source_strength: int = 1.) -> None:
|
|
3098
3541
|
"""
|
|
3099
|
-
Adds a new external
|
|
3542
|
+
Adds a new external bulk species injection source at a particular node.
|
|
3543
|
+
|
|
3544
|
+
Only for EPANET-MSX scenarios.
|
|
3100
3545
|
|
|
3101
3546
|
Parameters
|
|
3102
3547
|
----------
|
|
@@ -3107,6 +3552,8 @@ class ScenarioSimulator():
|
|
|
3107
3552
|
is placed.
|
|
3108
3553
|
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
3109
3554
|
1d source pattern.
|
|
3555
|
+
|
|
3556
|
+
Note that the pattern time step is equivalent to the EPANET pattern time step.
|
|
3110
3557
|
source_type : `int`,
|
|
3111
3558
|
Type of the external (bulk or surface) species injection source -- must be one of
|
|
3112
3559
|
the following EPANET toolkit constants:
|
|
@@ -3153,3 +3600,82 @@ class ScenarioSimulator():
|
|
|
3153
3600
|
self.epanet_api.addMSXPattern(pattern_id, pattern)
|
|
3154
3601
|
self.epanet_api.setMSXSources(node_id, species_id, source_type_, source_strength,
|
|
3155
3602
|
pattern_id)
|
|
3603
|
+
|
|
3604
|
+
def set_bulk_species_node_initial_concentrations(self,
|
|
3605
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3606
|
+
) -> None:
|
|
3607
|
+
"""
|
|
3608
|
+
Species the initial bulk species concentration at nodes.
|
|
3609
|
+
|
|
3610
|
+
Only for EPANET-MSX scenarios.
|
|
3611
|
+
|
|
3612
|
+
Parameters
|
|
3613
|
+
----------
|
|
3614
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3615
|
+
Initial concentration of species (key) at nodes -- i.e.
|
|
3616
|
+
value: list of node ID and initial concentration.
|
|
3617
|
+
"""
|
|
3618
|
+
if not isinstance(inital_conc, dict) or \
|
|
3619
|
+
any(not isinstance(species_id, str) or not isinstance(node_initial_conc, list)
|
|
3620
|
+
for species_id, node_initial_conc in inital_conc.items()) or \
|
|
3621
|
+
any(not isinstance(node_initial_conc, tuple)
|
|
3622
|
+
for node_initial_conc in list(itertools.chain(*inital_conc.values()))) or \
|
|
3623
|
+
any(not isinstance(node_id, str) or not isinstance(conc, float)
|
|
3624
|
+
for node_id, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3625
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3626
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3627
|
+
inital_conc_values = list(itertools.chain(*inital_conc.values()))
|
|
3628
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3629
|
+
for species_id in inital_conc.keys()):
|
|
3630
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3631
|
+
if any(node_id not in self.sensor_config.nodes for node_id, _ in inital_conc_values):
|
|
3632
|
+
raise ValueError("Unknown node ID in 'inital_conc'")
|
|
3633
|
+
if any(conc < 0 for _, conc in inital_conc_values):
|
|
3634
|
+
raise ValueError("Initial node concentration can not be negative")
|
|
3635
|
+
|
|
3636
|
+
for species_id, node_initial_conc in inital_conc.items():
|
|
3637
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3638
|
+
|
|
3639
|
+
for node_id, initial_conc in node_initial_conc:
|
|
3640
|
+
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
3641
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_NODE, node_idx, species_idx,
|
|
3642
|
+
initial_conc)
|
|
3643
|
+
|
|
3644
|
+
def set_species_link_initial_concentrations(self,
|
|
3645
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3646
|
+
) -> None:
|
|
3647
|
+
"""
|
|
3648
|
+
Species the initial (bulk or surface) species concentration at links.
|
|
3649
|
+
|
|
3650
|
+
Only for EPANET-MSX scenarios.
|
|
3651
|
+
|
|
3652
|
+
Parameters
|
|
3653
|
+
----------
|
|
3654
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3655
|
+
Initial concentration of species (key) at links -- i.e.
|
|
3656
|
+
value: list of link ID and initial concentration.
|
|
3657
|
+
"""
|
|
3658
|
+
if not isinstance(inital_conc, dict) or \
|
|
3659
|
+
any(not isinstance(species_id, str) or not isinstance(link_initial_conc, list)
|
|
3660
|
+
for species_id, link_initial_conc in inital_conc.items()) or \
|
|
3661
|
+
any(not isinstance(link_initial_conc, tuple)
|
|
3662
|
+
for link_initial_conc in inital_conc.values()) or \
|
|
3663
|
+
any(not isinstance(link_id, str) or not isinstance(conc, float)
|
|
3664
|
+
for link_id, conc in inital_conc.values()):
|
|
3665
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3666
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3667
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3668
|
+
for species_id in inital_conc.keys()):
|
|
3669
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3670
|
+
if any(link_id not in self.sensor_config.links for link_id, _ in inital_conc.values()):
|
|
3671
|
+
raise ValueError("Unknown link ID in 'inital_conc'")
|
|
3672
|
+
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3673
|
+
raise ValueError("Initial link concentration can not be negative")
|
|
3674
|
+
|
|
3675
|
+
for species_id, link_initial_conc in inital_conc.items():
|
|
3676
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3677
|
+
|
|
3678
|
+
for link_id, initial_conc in link_initial_conc:
|
|
3679
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3680
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_LINK, link_idx, species_idx,
|
|
3681
|
+
initial_conc)
|