epyt-flow 0.15.0b1__py3-none-any.whl → 0.16.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/batadal.py +1 -1
- epyt_flow/gym/scenario_control_env.py +6 -12
- epyt_flow/serialization.py +19 -3
- epyt_flow/simulation/events/actuator_events.py +24 -24
- epyt_flow/simulation/events/leakages.py +45 -45
- epyt_flow/simulation/events/quality_events.py +23 -23
- epyt_flow/simulation/events/sensor_reading_attack.py +27 -27
- epyt_flow/simulation/events/sensor_reading_event.py +33 -33
- epyt_flow/simulation/scada/complex_control.py +103 -103
- epyt_flow/simulation/scada/scada_data.py +58 -30
- epyt_flow/simulation/scada/simple_control.py +33 -33
- epyt_flow/simulation/scenario_config.py +31 -0
- epyt_flow/simulation/scenario_simulator.py +218 -82
- epyt_flow/simulation/sensor_config.py +220 -108
- epyt_flow/topology.py +197 -6
- epyt_flow/uncertainty/model_uncertainty.py +23 -20
- epyt_flow/uncertainty/sensor_noise.py +16 -16
- epyt_flow/uncertainty/uncertainties.py +6 -4
- epyt_flow/utils.py +240 -15
- epyt_flow/visualization/scenario_visualizer.py +14 -5
- epyt_flow/visualization/visualization_utils.py +22 -18
- {epyt_flow-0.15.0b1.dist-info → epyt_flow-0.16.0.dist-info}/METADATA +8 -5
- {epyt_flow-0.15.0b1.dist-info → epyt_flow-0.16.0.dist-info}/RECORD +27 -27
- {epyt_flow-0.15.0b1.dist-info → epyt_flow-0.16.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.15.0b1.dist-info → epyt_flow-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {epyt_flow-0.15.0b1.dist-info → epyt_flow-0.16.0.dist-info}/top_level.txt +0 -0
|
@@ -136,41 +136,27 @@ class ScenarioSimulator():
|
|
|
136
136
|
self.__running_simulation = False
|
|
137
137
|
self.__uncertainties_applied = False
|
|
138
138
|
|
|
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)
|
|
145
|
-
|
|
146
139
|
def __file_exists(file_in: str) -> bool:
|
|
147
140
|
try:
|
|
148
141
|
return pathlib.Path(file_in).is_file()
|
|
149
142
|
except Exception:
|
|
150
143
|
return False
|
|
151
144
|
|
|
152
|
-
if not
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if not __file_exists(self.__f_msx_in):
|
|
162
|
-
my_f_msx_in = self.__f_msx_in
|
|
163
|
-
else:
|
|
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
|
|
145
|
+
if scenario_config is not None: # Extract .inp file from NetworkTopology if necessary
|
|
146
|
+
if __file_exists(self.__f_inp_in) is False:
|
|
147
|
+
network_topo = scenario_config.network_topo
|
|
148
|
+
if network_topo is not None:
|
|
149
|
+
warnings.info(".inp file not found -- extracting network data from NetworkTopology")
|
|
150
|
+
network_topo.to_inp_file(self.__f_inp_in)
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(".inp file does not exist and 'scenario_config' does not " +
|
|
153
|
+
"contain a specification of the network topology")
|
|
168
154
|
|
|
169
155
|
from epanet_plus import EPyT # Workaround: Sphinx autodoc "importlib.import_module TypeError: __mro_entries__"
|
|
170
|
-
self.epanet_api = EPyT(
|
|
156
|
+
self.epanet_api = EPyT(self.__f_inp_in, use_project=self.__f_msx_in is None)
|
|
171
157
|
|
|
172
158
|
if self.__f_msx_in is not None:
|
|
173
|
-
self.epanet_api.load_msx_file(
|
|
159
|
+
self.epanet_api.load_msx_file(self.__f_msx_in)
|
|
174
160
|
|
|
175
161
|
# Do not raise exceptions in the case of EPANET warnings and errors
|
|
176
162
|
self.epanet_api.set_error_handling(raise_exception_on_error=raise_exception_on_error,
|
|
@@ -188,7 +174,8 @@ class ScenarioSimulator():
|
|
|
188
174
|
|
|
189
175
|
self._model_uncertainty = scenario_config.model_uncertainty
|
|
190
176
|
self._sensor_noise = scenario_config.sensor_noise
|
|
191
|
-
|
|
177
|
+
if scenario_config.sensor_config is not None:
|
|
178
|
+
self._sensor_config = scenario_config.sensor_config
|
|
192
179
|
|
|
193
180
|
for control in scenario_config.custom_controls:
|
|
194
181
|
self.add_custom_control(control)
|
|
@@ -614,9 +601,6 @@ class ScenarioSimulator():
|
|
|
614
601
|
"""
|
|
615
602
|
self.epanet_api.close()
|
|
616
603
|
|
|
617
|
-
if self.__my_f_inp_in is not None:
|
|
618
|
-
shutil.rmtree(pathlib.Path(self.__my_f_inp_in).parent)
|
|
619
|
-
|
|
620
604
|
def __enter__(self):
|
|
621
605
|
return self
|
|
622
606
|
|
|
@@ -924,11 +908,19 @@ class ScenarioSimulator():
|
|
|
924
908
|
"""
|
|
925
909
|
return self.epanet_api.get_reporting_time_step()
|
|
926
910
|
|
|
927
|
-
def get_scenario_config(self) -> ScenarioConfig:
|
|
911
|
+
def get_scenario_config(self, include_network_topology: bool = True) -> ScenarioConfig:
|
|
928
912
|
"""
|
|
929
913
|
Gets the configuration of this scenario -- i.e. all information & elements
|
|
930
914
|
that completely describe this scenario.
|
|
931
915
|
|
|
916
|
+
Parameters
|
|
917
|
+
----------
|
|
918
|
+
include_network_topology : `bool`, optional
|
|
919
|
+
If True, the full specification of the network topology (incl. demand patterns)
|
|
920
|
+
will be included in the scenario configuration.
|
|
921
|
+
|
|
922
|
+
The default is True.
|
|
923
|
+
|
|
932
924
|
Returns
|
|
933
925
|
-------
|
|
934
926
|
:class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
|
|
@@ -944,7 +936,12 @@ class ScenarioSimulator():
|
|
|
944
936
|
"quality_model": self.get_quality_model(),
|
|
945
937
|
"demand_model": self.get_demand_model()}
|
|
946
938
|
|
|
939
|
+
network_topology = None
|
|
940
|
+
if include_network_topology is True:
|
|
941
|
+
network_topology = self.get_topology(include_demand_patterns=True)
|
|
942
|
+
|
|
947
943
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
944
|
+
network_topology=network_topology,
|
|
948
945
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
949
946
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
950
947
|
custom_controls=self.custom_controls,
|
|
@@ -981,10 +978,18 @@ class ScenarioSimulator():
|
|
|
981
978
|
|
|
982
979
|
return n_time_steps * n_quantities * n_bytes_per_quantity * .000001
|
|
983
980
|
|
|
984
|
-
def get_topology(self) -> NetworkTopology:
|
|
981
|
+
def get_topology(self, include_demand_patterns: bool = False) -> NetworkTopology:
|
|
985
982
|
"""
|
|
986
983
|
Gets the topology (incl. information such as elevations, pipe diameters, etc.) of this WDN.
|
|
987
984
|
|
|
985
|
+
Parameters
|
|
986
|
+
----------
|
|
987
|
+
include_demand_patterns : `bool`, optional
|
|
988
|
+
If True, demand patterns will be included -- be aware that this will increase
|
|
989
|
+
the object's memory footprint.
|
|
990
|
+
|
|
991
|
+
The default is False.
|
|
992
|
+
|
|
988
993
|
Returns
|
|
989
994
|
-------
|
|
990
995
|
:class:`~epyt_flow.topology.NetworkTopology`
|
|
@@ -993,6 +998,12 @@ class ScenarioSimulator():
|
|
|
993
998
|
self._adapt_to_network_changes()
|
|
994
999
|
|
|
995
1000
|
# Collect information about the topology of the water distribution network
|
|
1001
|
+
patterns = {}
|
|
1002
|
+
if include_demand_patterns is True:
|
|
1003
|
+
patterns = {pattern_id: self.epanet_api.get_pattern(
|
|
1004
|
+
self.epanet_api.getpatternindex(pattern_id))
|
|
1005
|
+
for pattern_id in self.epanet_api.get_all_patterns_id()}
|
|
1006
|
+
|
|
996
1007
|
nodes_id = self.epanet_api.get_all_nodes_id()
|
|
997
1008
|
nodes_elevation = [self.epanet_api.get_node_elevation(node_idx)
|
|
998
1009
|
for node_idx in self.epanet_api.get_all_nodes_idx()]
|
|
@@ -1002,7 +1013,16 @@ class ScenarioSimulator():
|
|
|
1002
1013
|
for node_idx in self.epanet_api.get_all_nodes_idx()]
|
|
1003
1014
|
nodes_comments = [self.epanet_api.get_node_comment(node_idx)
|
|
1004
1015
|
for node_idx in self.epanet_api.get_all_nodes_idx()]
|
|
1005
|
-
|
|
1016
|
+
nodes_base_demand = [self.epanet_api.get_node_base_demand(node_idx)
|
|
1017
|
+
for node_idx in self.epanet_api.get_all_nodes_idx()]
|
|
1018
|
+
|
|
1019
|
+
node_demand_patterns_id = []
|
|
1020
|
+
for node_idx in self.epanet_api.get_all_nodes_idx():
|
|
1021
|
+
r = []
|
|
1022
|
+
for pattern_idx in self.epanet_api.get_node_demand_patterns_idx(node_idx):
|
|
1023
|
+
if pattern_idx != 0:
|
|
1024
|
+
r.append(self.epanet_api.getpatternid(pattern_idx))
|
|
1025
|
+
node_demand_patterns_id.append(r)
|
|
1006
1026
|
|
|
1007
1027
|
links_id = self.epanet_api.get_all_links_id()
|
|
1008
1028
|
links_type = [self.epanet_api.get_link_type(link_idx)
|
|
@@ -1014,65 +1034,121 @@ class ScenarioSimulator():
|
|
|
1014
1034
|
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1015
1035
|
links_roughness_coeff = [self.epanet_api.get_link_roughness(link_idx)
|
|
1016
1036
|
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1017
|
-
links_bulk_coeff = [self.epanet_api.
|
|
1037
|
+
links_bulk_coeff = [self.epanet_api.get_link_bulk_reaction_coeff(link_idx)
|
|
1018
1038
|
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1019
|
-
links_wall_coeff = [self.epanet_api.
|
|
1039
|
+
links_wall_coeff = [self.epanet_api.get_link_wall_reaction_coeff(link_idx)
|
|
1020
1040
|
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1021
1041
|
links_loss_coeff = [self.epanet_api.get_link_minorloss(link_idx)
|
|
1022
1042
|
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1043
|
+
link_init_setting = [self.epanet_api.get_link_init_setting(link_idx)
|
|
1044
|
+
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1045
|
+
link_init_status = [self.epanet_api.get_link_init_status(link_idx)
|
|
1046
|
+
for link_idx in self.epanet_api.get_all_links_idx()]
|
|
1023
1047
|
|
|
1024
1048
|
pumps_id = self.epanet_api.get_all_pumps_id()
|
|
1025
1049
|
pumps_type = [self.epanet_api.get_pump_type(pump_idx)
|
|
1026
1050
|
for pump_idx in self.epanet_api.get_all_pumps_idx()]
|
|
1051
|
+
pumps_hcurve = [int(self.epanet_api.getlinkvalue(pump_idx, EpanetConstants.EN_PUMP_HCURVE))
|
|
1052
|
+
for pump_idx in self.epanet_api.get_all_pumps_idx()]
|
|
1027
1053
|
|
|
1028
1054
|
valves_id = self.epanet_api.get_all_valves_id()
|
|
1029
1055
|
|
|
1030
1056
|
# Build graph describing the topology
|
|
1057
|
+
curves = {}
|
|
1058
|
+
def __add_curve(curve_id: str) -> None:
|
|
1059
|
+
curve_type = self.epanet_api.getcurvetype(pump_hcurve_idx)
|
|
1060
|
+
len = self.epanet_api.getcurvelen(pump_hcurve_idx)
|
|
1061
|
+
curve_data = []
|
|
1062
|
+
for i in range(len):
|
|
1063
|
+
x, y = self.epanet_api.getcurvevalue(pump_hcurve_idx, i+1)
|
|
1064
|
+
curve_data.append((x, y))
|
|
1065
|
+
curves[curve_id] = (curve_type, curve_data)
|
|
1066
|
+
|
|
1031
1067
|
nodes = []
|
|
1032
1068
|
for node_id, node_elevation, node_type, \
|
|
1033
|
-
node_coord, node_comment
|
|
1034
|
-
|
|
1069
|
+
node_coord, node_comment, node_base_demand, node_demand_patterns in \
|
|
1070
|
+
zip(nodes_id, nodes_elevation, nodes_type, nodes_coord,
|
|
1071
|
+
nodes_comments, nodes_base_demand, node_demand_patterns_id):
|
|
1035
1072
|
node_info = {"elevation": node_elevation,
|
|
1036
1073
|
"coord": node_coord,
|
|
1037
1074
|
"comment": node_comment,
|
|
1038
|
-
"type": node_type
|
|
1075
|
+
"type": node_type,
|
|
1076
|
+
"base_demand": node_base_demand}
|
|
1077
|
+
if include_demand_patterns is True:
|
|
1078
|
+
node_info["demand_patterns_id"] = node_demand_patterns
|
|
1039
1079
|
if node_type == EpanetConstants.EN_TANK:
|
|
1040
1080
|
node_tank_idx = self.epanet_api.get_node_idx(node_id)
|
|
1041
1081
|
node_info["diameter"] = float(self.epanet_api.get_tank_diameter(node_tank_idx))
|
|
1042
|
-
node_info["volume"] = float(self.epanet_api.get_tank_volume(node_tank_idx))
|
|
1043
1082
|
node_info["max_level"] = float(self.epanet_api.get_tank_max_level(node_tank_idx))
|
|
1044
1083
|
node_info["min_level"] = float(self.epanet_api.get_tank_min_level(node_tank_idx))
|
|
1084
|
+
node_info["min_vol"] = float(self.epanet_api.get_tank_min_vol(node_tank_idx))
|
|
1045
1085
|
node_info["mixing_fraction"] = float(self.epanet_api.get_tank_mix_fraction(node_tank_idx))
|
|
1046
1086
|
node_info["mixing_model"] = int(self.epanet_api.get_tank_mix_model(node_tank_idx))
|
|
1087
|
+
node_info["init_vol"] = self.epanet_api.getnodevalue(node_tank_idx,
|
|
1088
|
+
EpanetConstants.EN_INITVOLUME)
|
|
1089
|
+
node_info["cylindric"] = self.epanet_api.getnodevalue(node_tank_idx,
|
|
1090
|
+
EpanetConstants.EN_VOLCURVE) == 0
|
|
1091
|
+
node_info["can_overflow"] = bool(self.epanet_api.can_tank_overflow(node_tank_idx))
|
|
1092
|
+
|
|
1093
|
+
node_info["vol_curve_id"] = ""
|
|
1094
|
+
tank_vol_curve_idx = int(self.epanet_api.get_tank_vol_curve_idx(node_tank_idx))
|
|
1095
|
+
if tank_vol_curve_idx != 0:
|
|
1096
|
+
curve_id = self.epanet_api.getcurveid(tank_vol_curve_idx)
|
|
1097
|
+
node_info["vol_curve_id"] = curve_id
|
|
1098
|
+
|
|
1099
|
+
if curve_id not in curves:
|
|
1100
|
+
__add_curve(curve_id)
|
|
1047
1101
|
|
|
1048
1102
|
nodes.append((node_id, node_info))
|
|
1049
1103
|
|
|
1050
1104
|
links = []
|
|
1051
1105
|
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff, \
|
|
1052
|
-
wall_coeff, loss_coeff
|
|
1053
|
-
|
|
1054
|
-
|
|
1106
|
+
wall_coeff, loss_coeff, initial_setting, initial_status in \
|
|
1107
|
+
zip(links_id, links_type, links_data, links_diameter, links_length,
|
|
1108
|
+
links_roughness_coeff, links_bulk_coeff, links_wall_coeff, links_loss_coeff,
|
|
1109
|
+
link_init_setting, link_init_status):
|
|
1055
1110
|
links.append((link_id, list(link),
|
|
1056
1111
|
{"type": link_type, "diameter": diameter, "length": length,
|
|
1057
1112
|
"roughness_coeff": roughness_coeff,
|
|
1058
1113
|
"bulk_coeff": bulk_coeff, "wall_coeff": wall_coeff,
|
|
1059
|
-
"loss_coeff": loss_coeff
|
|
1114
|
+
"loss_coeff": loss_coeff, "init_setting": initial_setting,
|
|
1115
|
+
"init_status": initial_status}))
|
|
1060
1116
|
|
|
1061
1117
|
pumps = {}
|
|
1062
|
-
for pump_id, pump_type in zip(pumps_id, pumps_type):
|
|
1118
|
+
for pump_id, pump_type, pump_hcurve_idx in zip(pumps_id, pumps_type, pumps_hcurve):
|
|
1063
1119
|
link_idx = links_id.index(pump_id)
|
|
1064
1120
|
link = links_data[link_idx]
|
|
1065
|
-
|
|
1121
|
+
pump_init_setting = link_init_setting[link_idx]
|
|
1122
|
+
pump_init_status = link_init_status[link_idx]
|
|
1123
|
+
|
|
1124
|
+
curve_id = None
|
|
1125
|
+
if pump_hcurve_idx != 0:
|
|
1126
|
+
curve_id = self.epanet_api.getcurveid(pump_hcurve_idx)
|
|
1127
|
+
|
|
1128
|
+
if curve_id not in curves:
|
|
1129
|
+
__add_curve(curve_id)
|
|
1130
|
+
|
|
1131
|
+
pumps[pump_id] = {"type": pump_type, "end_points": link,
|
|
1132
|
+
"init_setting": pump_init_setting,
|
|
1133
|
+
"init_status": pump_init_status,
|
|
1134
|
+
"curve_id": curve_id}
|
|
1066
1135
|
|
|
1067
1136
|
valves = {}
|
|
1068
1137
|
for valve_id in valves_id:
|
|
1069
1138
|
link_idx = links_id.index(valve_id)
|
|
1070
1139
|
link = links_data[link_idx]
|
|
1071
1140
|
valve_type = links_type[link_idx]
|
|
1072
|
-
|
|
1141
|
+
valve_diameter = links_diameter[link_idx]
|
|
1142
|
+
valve_init_setting = link_init_setting[link_idx]
|
|
1143
|
+
valve_init_status = link_init_status[link_idx]
|
|
1144
|
+
valves[valve_id] = {"type": valve_type, "end_points": link,
|
|
1145
|
+
"diameter": valve_diameter,
|
|
1146
|
+
"initial_setting": valve_init_setting,
|
|
1147
|
+
"initial_status": valve_init_status}
|
|
1073
1148
|
|
|
1074
1149
|
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links, pumps=pumps,
|
|
1075
|
-
valves=valves,
|
|
1150
|
+
valves=valves, curves=curves, patterns=patterns,
|
|
1151
|
+
units=self.get_units_category())
|
|
1076
1152
|
|
|
1077
1153
|
def plot_topology(self, export_to_file: str = None) -> None:
|
|
1078
1154
|
"""
|
|
@@ -1195,7 +1271,7 @@ class ScenarioSimulator():
|
|
|
1195
1271
|
else:
|
|
1196
1272
|
base_demand = 0
|
|
1197
1273
|
for demand_idx in range(n_demand_categories):
|
|
1198
|
-
base_demand += self.epanet_api.getbasedemand(node_idx
|
|
1274
|
+
base_demand += self.epanet_api.getbasedemand(node_idx, demand_idx + 1)
|
|
1199
1275
|
|
|
1200
1276
|
return base_demand
|
|
1201
1277
|
|
|
@@ -1223,11 +1299,14 @@ class ScenarioSimulator():
|
|
|
1223
1299
|
node_idx = self.epanet_api.get_node_idx(node_id)
|
|
1224
1300
|
|
|
1225
1301
|
if self.epanet_api.getnodetype(node_idx) != EpanetConstants.EN_RESERVOIR:
|
|
1226
|
-
demand_pattern_idx = self.epanet_api.getdemandpattern(node_idx)
|
|
1302
|
+
demand_pattern_idx = self.epanet_api.getdemandpattern(node_idx, 1)
|
|
1227
1303
|
else:
|
|
1228
1304
|
demand_pattern_idx = self.epanet_api.getnodevalue(node_idx, EpanetConstants.EN_PATTERN)
|
|
1229
1305
|
|
|
1230
|
-
|
|
1306
|
+
if demand_pattern_idx == 0:
|
|
1307
|
+
return None
|
|
1308
|
+
else:
|
|
1309
|
+
return self.get_pattern(self.epanet_api.getpatternid(demand_pattern_idx))
|
|
1231
1310
|
|
|
1232
1311
|
def set_node_demand_pattern(self, node_id: str, base_demand: float, demand_pattern_id: str,
|
|
1233
1312
|
demand_pattern: np.ndarray = None) -> None:
|
|
@@ -1959,7 +2038,8 @@ class ScenarioSimulator():
|
|
|
1959
2038
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1960
2039
|
frozen_sensor_config: bool = False,
|
|
1961
2040
|
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1962
|
-
reapply_uncertainties: bool = False
|
|
2041
|
+
reapply_uncertainties: bool = False,
|
|
2042
|
+
float_type: type = np.float32
|
|
1963
2043
|
) -> ScadaData:
|
|
1964
2044
|
"""
|
|
1965
2045
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -1989,6 +2069,10 @@ class ScenarioSimulator():
|
|
|
1989
2069
|
If True, the uncertainties are re-applied on the original properties.
|
|
1990
2070
|
|
|
1991
2071
|
The default is False.
|
|
2072
|
+
float_type : `type`, optional
|
|
2073
|
+
Floating point type (precision).
|
|
2074
|
+
|
|
2075
|
+
The default is 32bit -- i.e., numpy.float32
|
|
1992
2076
|
|
|
1993
2077
|
Returns
|
|
1994
2078
|
-------
|
|
@@ -2010,7 +2094,8 @@ class ScenarioSimulator():
|
|
|
2010
2094
|
frozen_sensor_config=frozen_sensor_config,
|
|
2011
2095
|
use_quality_time_step_as_reporting_time_step=
|
|
2012
2096
|
use_quality_time_step_as_reporting_time_step,
|
|
2013
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2097
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2098
|
+
float_type=float_type):
|
|
2014
2099
|
if result is None:
|
|
2015
2100
|
result = {}
|
|
2016
2101
|
for data_type, data in scada_data.items():
|
|
@@ -2038,7 +2123,8 @@ class ScenarioSimulator():
|
|
|
2038
2123
|
return_as_dict: bool = False,
|
|
2039
2124
|
frozen_sensor_config: bool = False,
|
|
2040
2125
|
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
2041
|
-
reapply_uncertainties: bool = False
|
|
2126
|
+
reapply_uncertainties: bool = False,
|
|
2127
|
+
float_type: type = np.float32,
|
|
2042
2128
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2043
2129
|
"""
|
|
2044
2130
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -2072,6 +2158,10 @@ class ScenarioSimulator():
|
|
|
2072
2158
|
If True, the uncertainties are re-applied on the original properties.
|
|
2073
2159
|
|
|
2074
2160
|
The default is False.
|
|
2161
|
+
float_type : `type`, optional
|
|
2162
|
+
Floating point type (precision).
|
|
2163
|
+
|
|
2164
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2075
2165
|
|
|
2076
2166
|
Returns
|
|
2077
2167
|
-------
|
|
@@ -2145,13 +2235,15 @@ class ScenarioSimulator():
|
|
|
2145
2235
|
if len(bulk_species_node_concentrations) == 0:
|
|
2146
2236
|
bulk_species_node_concentrations = None
|
|
2147
2237
|
else:
|
|
2148
|
-
bulk_species_node_concentrations = np.array(bulk_species_node_concentrations
|
|
2238
|
+
bulk_species_node_concentrations = np.array(bulk_species_node_concentrations,
|
|
2239
|
+
dtype=float_type). \
|
|
2149
2240
|
reshape((1, len(bulk_species_idx), n_nodes))
|
|
2150
2241
|
|
|
2151
2242
|
if len(bulk_species_link_concentrations) == 0:
|
|
2152
2243
|
bulk_species_link_concentrations = None
|
|
2153
2244
|
else:
|
|
2154
|
-
bulk_species_link_concentrations = np.array(bulk_species_link_concentrations
|
|
2245
|
+
bulk_species_link_concentrations = np.array(bulk_species_link_concentrations,
|
|
2246
|
+
dtype=float_type). \
|
|
2155
2247
|
reshape((1, len(bulk_species_idx), n_links))
|
|
2156
2248
|
|
|
2157
2249
|
# Surface species
|
|
@@ -2168,7 +2260,8 @@ class ScenarioSimulator():
|
|
|
2168
2260
|
if len(surface_species_concentrations) == 0:
|
|
2169
2261
|
surface_species_concentrations = None
|
|
2170
2262
|
else:
|
|
2171
|
-
surface_species_concentrations = np.array(surface_species_concentrations
|
|
2263
|
+
surface_species_concentrations = np.array(surface_species_concentrations,
|
|
2264
|
+
dtype=float_type). \
|
|
2172
2265
|
reshape((1, len(surface_species_idx), n_links))
|
|
2173
2266
|
|
|
2174
2267
|
return bulk_species_node_concentrations, bulk_species_link_concentrations, \
|
|
@@ -2271,7 +2364,8 @@ class ScenarioSimulator():
|
|
|
2271
2364
|
|
|
2272
2365
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
2273
2366
|
frozen_sensor_config: bool = False,
|
|
2274
|
-
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2367
|
+
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
2368
|
+
float_type: type = np.float32
|
|
2275
2369
|
) -> ScadaData:
|
|
2276
2370
|
"""
|
|
2277
2371
|
Runs a basic quality analysis using EPANET.
|
|
@@ -2297,6 +2391,10 @@ class ScenarioSimulator():
|
|
|
2297
2391
|
with the hydraulic simulation.
|
|
2298
2392
|
|
|
2299
2393
|
The default is False.
|
|
2394
|
+
float_type : `type`, optional
|
|
2395
|
+
Floating point type (precision).
|
|
2396
|
+
|
|
2397
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2300
2398
|
|
|
2301
2399
|
Returns
|
|
2302
2400
|
-------
|
|
@@ -2315,7 +2413,8 @@ class ScenarioSimulator():
|
|
|
2315
2413
|
return_as_dict=True,
|
|
2316
2414
|
frozen_sensor_config=frozen_sensor_config,
|
|
2317
2415
|
use_quality_time_step_as_reporting_time_step=
|
|
2318
|
-
use_quality_time_step_as_reporting_time_step
|
|
2416
|
+
use_quality_time_step_as_reporting_time_step,
|
|
2417
|
+
float_type=float_type):
|
|
2319
2418
|
if result is None:
|
|
2320
2419
|
result = {}
|
|
2321
2420
|
for data_type, data in scada_data.items():
|
|
@@ -2339,7 +2438,8 @@ class ScenarioSimulator():
|
|
|
2339
2438
|
support_abort: bool = False,
|
|
2340
2439
|
return_as_dict: bool = False,
|
|
2341
2440
|
frozen_sensor_config: bool = False,
|
|
2342
|
-
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2441
|
+
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
2442
|
+
float_type: type = np.float32
|
|
2343
2443
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2344
2444
|
"""
|
|
2345
2445
|
Runs a basic quality analysis using EPANET.
|
|
@@ -2371,6 +2471,10 @@ class ScenarioSimulator():
|
|
|
2371
2471
|
with the hydraulic simulation.
|
|
2372
2472
|
|
|
2373
2473
|
The default is False.
|
|
2474
|
+
float_type : `type`, optional
|
|
2475
|
+
Floating point type (precision).
|
|
2476
|
+
|
|
2477
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2374
2478
|
|
|
2375
2479
|
Returns
|
|
2376
2480
|
-------
|
|
@@ -2429,8 +2533,10 @@ class ScenarioSimulator():
|
|
|
2429
2533
|
error_code = self.epanet_api.get_last_error_code()
|
|
2430
2534
|
if last_error_code == 0:
|
|
2431
2535
|
last_error_code = error_code
|
|
2432
|
-
quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY)
|
|
2433
|
-
|
|
2536
|
+
quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY),
|
|
2537
|
+
dtype=float_type).reshape(1, -1)
|
|
2538
|
+
quality_link_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_QUALITY),
|
|
2539
|
+
dtype=float_type).reshape(1, -1)
|
|
2434
2540
|
|
|
2435
2541
|
# Yield results in a regular time interval only!
|
|
2436
2542
|
if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
|
|
@@ -2467,7 +2573,8 @@ class ScenarioSimulator():
|
|
|
2467
2573
|
|
|
2468
2574
|
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2469
2575
|
frozen_sensor_config: bool = False,
|
|
2470
|
-
reapply_uncertainties: bool = False
|
|
2576
|
+
reapply_uncertainties: bool = False,
|
|
2577
|
+
float_type: type = np.float32) -> ScadaData:
|
|
2471
2578
|
"""
|
|
2472
2579
|
Runs the hydraulic simulation of this scenario (incl. basic quality if set).
|
|
2473
2580
|
|
|
@@ -2495,6 +2602,10 @@ class ScenarioSimulator():
|
|
|
2495
2602
|
If True, the uncertainties are re-applied on the original properties.
|
|
2496
2603
|
|
|
2497
2604
|
The default is False.
|
|
2605
|
+
float_type : `type`, optional
|
|
2606
|
+
Floating point type (precision).
|
|
2607
|
+
|
|
2608
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2498
2609
|
|
|
2499
2610
|
Returns
|
|
2500
2611
|
-------
|
|
@@ -2514,7 +2625,8 @@ class ScenarioSimulator():
|
|
|
2514
2625
|
verbose=verbose,
|
|
2515
2626
|
return_as_dict=True,
|
|
2516
2627
|
frozen_sensor_config=frozen_sensor_config,
|
|
2517
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2628
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2629
|
+
float_type=float_type):
|
|
2518
2630
|
if result is None:
|
|
2519
2631
|
result = {}
|
|
2520
2632
|
for data_type, data in scada_data.items():
|
|
@@ -2544,7 +2656,8 @@ class ScenarioSimulator():
|
|
|
2544
2656
|
support_abort: bool = False,
|
|
2545
2657
|
return_as_dict: bool = False,
|
|
2546
2658
|
frozen_sensor_config: bool = False,
|
|
2547
|
-
reapply_uncertainties: bool = False
|
|
2659
|
+
reapply_uncertainties: bool = False,
|
|
2660
|
+
float_type: type = np.float32
|
|
2548
2661
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2549
2662
|
"""
|
|
2550
2663
|
Runs the hydraulic simulation of this scenario (incl. basic quality if set) and
|
|
@@ -2585,6 +2698,10 @@ class ScenarioSimulator():
|
|
|
2585
2698
|
If True, the uncertainties are re-applied on the original properties.
|
|
2586
2699
|
|
|
2587
2700
|
The default is False.
|
|
2701
|
+
float_type : `type`, optional
|
|
2702
|
+
Floating point type (precision).
|
|
2703
|
+
|
|
2704
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2588
2705
|
|
|
2589
2706
|
Returns
|
|
2590
2707
|
-------
|
|
@@ -2603,7 +2720,7 @@ class ScenarioSimulator():
|
|
|
2603
2720
|
|
|
2604
2721
|
self.epanet_api.openH()
|
|
2605
2722
|
self.epanet_api.openQ()
|
|
2606
|
-
self.epanet_api.initH(EpanetConstants.
|
|
2723
|
+
self.epanet_api.initH(EpanetConstants.EN_SAVE_AND_INIT)
|
|
2607
2724
|
self.epanet_api.initQ(EpanetConstants.EN_SAVE)
|
|
2608
2725
|
|
|
2609
2726
|
requested_total_time = self.epanet_api.get_simulation_duration()
|
|
@@ -2653,32 +2770,42 @@ class ScenarioSimulator():
|
|
|
2653
2770
|
last_error_code = error_code
|
|
2654
2771
|
|
|
2655
2772
|
# Fetch data
|
|
2656
|
-
pressure_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_PRESSURE)
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2773
|
+
pressure_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_PRESSURE),
|
|
2774
|
+
dtype=float_type).reshape(1, -1)
|
|
2775
|
+
flow_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_FLOW),
|
|
2776
|
+
dtype=float_type).reshape(1, -1)
|
|
2777
|
+
demand_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_DEMAND),
|
|
2778
|
+
dtype=float_type).reshape(1, -1)
|
|
2779
|
+
quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY),
|
|
2780
|
+
dtype=float_type).reshape(1, -1)
|
|
2781
|
+
quality_link_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_QUALITY),
|
|
2782
|
+
dtype=float_type).reshape(1, -1)
|
|
2661
2783
|
|
|
2662
2784
|
tanks_volume_data = None
|
|
2663
2785
|
if len(self.epanet_api.get_all_tanks_idx()) > 0:
|
|
2664
2786
|
tanks_volume_data = np.array([self.epanet_api.get_tank_volume(tank_idx)
|
|
2665
|
-
for tank_idx in self.epanet_api.get_all_tanks_idx()]
|
|
2787
|
+
for tank_idx in self.epanet_api.get_all_tanks_idx()],
|
|
2788
|
+
dtype=float_type).reshape(1, -1)
|
|
2666
2789
|
|
|
2667
2790
|
pumps_state_data = None
|
|
2668
2791
|
pumps_energy_usage_data = None
|
|
2669
2792
|
pumps_efficiency_data = None
|
|
2670
2793
|
if len(self.epanet_api.get_all_pumps_idx()) > 0:
|
|
2671
2794
|
pumps_state_data = np.array([self.epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_PUMP_STATE)
|
|
2672
|
-
for link_idx in self.epanet_api.get_all_pumps_idx()]
|
|
2795
|
+
for link_idx in self.epanet_api.get_all_pumps_idx()],
|
|
2796
|
+
dtype=float_type).reshape(1, -1)
|
|
2673
2797
|
pumps_energy_usage_data = np.array([self.epanet_api.get_pump_energy_usage(pump_idx)
|
|
2674
|
-
for pump_idx in self.epanet_api.get_all_pumps_idx()]
|
|
2798
|
+
for pump_idx in self.epanet_api.get_all_pumps_idx()],
|
|
2799
|
+
dtype=float_type).reshape(1, -1)
|
|
2675
2800
|
pumps_efficiency_data = np.array([self.epanet_api.get_pump_efficiency(pump_idx)
|
|
2676
|
-
for pump_idx in self.epanet_api.get_all_pumps_idx()]
|
|
2801
|
+
for pump_idx in self.epanet_api.get_all_pumps_idx()],
|
|
2802
|
+
dtype=float_type).reshape(1, -1)
|
|
2677
2803
|
|
|
2678
2804
|
valves_state_data = None
|
|
2679
2805
|
if len(self.epanet_api.get_all_valves_idx()) > 0:
|
|
2680
2806
|
valves_state_data = np.array([self.epanet_api.getlinkvalue(link_valve_idx, EpanetConstants.EN_STATUS)
|
|
2681
|
-
for link_valve_idx in self.epanet_api.get_all_valves_idx()]
|
|
2807
|
+
for link_valve_idx in self.epanet_api.get_all_valves_idx()],
|
|
2808
|
+
dtype=float_type).reshape(1, -1)
|
|
2682
2809
|
|
|
2683
2810
|
scada_data = ScadaData(network_topo=network_topo,
|
|
2684
2811
|
sensor_config=self._sensor_config,
|
|
@@ -2753,7 +2880,8 @@ class ScenarioSimulator():
|
|
|
2753
2880
|
|
|
2754
2881
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2755
2882
|
frozen_sensor_config: bool = False,
|
|
2756
|
-
reapply_uncertainties: bool = False
|
|
2883
|
+
reapply_uncertainties: bool = False,
|
|
2884
|
+
float_type: type = np.float32) -> ScadaData:
|
|
2757
2885
|
"""
|
|
2758
2886
|
Runs the simulation of this scenario.
|
|
2759
2887
|
|
|
@@ -2779,6 +2907,10 @@ class ScenarioSimulator():
|
|
|
2779
2907
|
If True, the uncertainties are re-applied on the original properties.
|
|
2780
2908
|
|
|
2781
2909
|
The default is False.
|
|
2910
|
+
float_type : `type`, optional
|
|
2911
|
+
Floating point type (precision).
|
|
2912
|
+
|
|
2913
|
+
The default is 32bit -- i.e., numpy.float32
|
|
2782
2914
|
|
|
2783
2915
|
Returns
|
|
2784
2916
|
-------
|
|
@@ -2799,7 +2931,8 @@ class ScenarioSimulator():
|
|
|
2799
2931
|
# Run hydraulic simulation step-by-step
|
|
2800
2932
|
result = self.run_hydraulic_simulation(hyd_export=hyd_export, verbose=verbose,
|
|
2801
2933
|
frozen_sensor_config=frozen_sensor_config,
|
|
2802
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2934
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2935
|
+
float_type=float_type)
|
|
2803
2936
|
|
|
2804
2937
|
# If necessary, run advanced quality simulation utilizing the computed hydraulics
|
|
2805
2938
|
if self.f_msx_in is not None:
|
|
@@ -2807,7 +2940,8 @@ class ScenarioSimulator():
|
|
|
2807
2940
|
result_msx = gen(hyd_file_in=hyd_export,
|
|
2808
2941
|
verbose=verbose,
|
|
2809
2942
|
frozen_sensor_config=frozen_sensor_config,
|
|
2810
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2943
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2944
|
+
float_type=float_type)
|
|
2811
2945
|
result.join(result_msx)
|
|
2812
2946
|
|
|
2813
2947
|
if hyd_export_old is not None:
|
|
@@ -3472,7 +3606,8 @@ class ScenarioSimulator():
|
|
|
3472
3606
|
if node_init_qual < 0:
|
|
3473
3607
|
raise ValueError(f"{node_id}: Initial node quality can not be negative")
|
|
3474
3608
|
|
|
3475
|
-
for
|
|
3609
|
+
for node_id, node_init_qual in initial_quality.items():
|
|
3610
|
+
node_idx = self.epanet_api.get_node_idx(node_id)
|
|
3476
3611
|
self.epanet_api.set_node_init_quality(node_idx, node_init_qual)
|
|
3477
3612
|
|
|
3478
3613
|
if order_wall is not None:
|
|
@@ -3720,17 +3855,18 @@ class ScenarioSimulator():
|
|
|
3720
3855
|
any(not isinstance(species_id, str) or not isinstance(link_initial_conc, list)
|
|
3721
3856
|
for species_id, link_initial_conc in inital_conc.items()) or \
|
|
3722
3857
|
any(not isinstance(link_initial_conc, tuple)
|
|
3723
|
-
for link_initial_conc in inital_conc.values()) or \
|
|
3858
|
+
for link_initial_conc in list(itertools.chain(*inital_conc.values()))) or \
|
|
3724
3859
|
any(not isinstance(link_id, str) or not isinstance(conc, float)
|
|
3725
|
-
for link_id, conc in inital_conc.values()):
|
|
3860
|
+
for link_id, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3726
3861
|
raise TypeError("'inital_conc' must be an instance of " +
|
|
3727
3862
|
"'dict[str, list[tuple[str, float]]'")
|
|
3728
3863
|
if any(species_id not in self.sensor_config.bulk_species
|
|
3729
3864
|
for species_id in inital_conc.keys()):
|
|
3730
3865
|
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3731
|
-
if any(link_id not in self.sensor_config.links for link_id, _ in
|
|
3866
|
+
if any(link_id not in self.sensor_config.links for link_id, _ in
|
|
3867
|
+
list(itertools.chain(*inital_conc.values()))):
|
|
3732
3868
|
raise ValueError("Unknown link ID in 'inital_conc'")
|
|
3733
|
-
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3869
|
+
if any(conc < 0 for _, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3734
3870
|
raise ValueError("Initial link concentration can not be negative")
|
|
3735
3871
|
|
|
3736
3872
|
for species_id, link_initial_conc in inital_conc.items():
|