epyt-flow 0.15.0__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 +217 -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 +189 -30
- epyt_flow/visualization/scenario_visualizer.py +14 -5
- epyt_flow/visualization/visualization_utils.py +18 -16
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/METADATA +7 -5
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/RECORD +27 -27
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {epyt_flow-0.15.0.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,66 +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
|
|
1039
|
-
|
|
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
|
|
1040
1079
|
if node_type == EpanetConstants.EN_TANK:
|
|
1041
1080
|
node_tank_idx = self.epanet_api.get_node_idx(node_id)
|
|
1042
1081
|
node_info["diameter"] = float(self.epanet_api.get_tank_diameter(node_tank_idx))
|
|
1043
|
-
node_info["volume"] = float(self.epanet_api.get_tank_volume(node_tank_idx))
|
|
1044
1082
|
node_info["max_level"] = float(self.epanet_api.get_tank_max_level(node_tank_idx))
|
|
1045
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))
|
|
1046
1085
|
node_info["mixing_fraction"] = float(self.epanet_api.get_tank_mix_fraction(node_tank_idx))
|
|
1047
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)
|
|
1048
1101
|
|
|
1049
1102
|
nodes.append((node_id, node_info))
|
|
1050
1103
|
|
|
1051
1104
|
links = []
|
|
1052
1105
|
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff, \
|
|
1053
|
-
wall_coeff, loss_coeff
|
|
1054
|
-
|
|
1055
|
-
|
|
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):
|
|
1056
1110
|
links.append((link_id, list(link),
|
|
1057
1111
|
{"type": link_type, "diameter": diameter, "length": length,
|
|
1058
1112
|
"roughness_coeff": roughness_coeff,
|
|
1059
1113
|
"bulk_coeff": bulk_coeff, "wall_coeff": wall_coeff,
|
|
1060
|
-
"loss_coeff": loss_coeff
|
|
1114
|
+
"loss_coeff": loss_coeff, "init_setting": initial_setting,
|
|
1115
|
+
"init_status": initial_status}))
|
|
1061
1116
|
|
|
1062
1117
|
pumps = {}
|
|
1063
|
-
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):
|
|
1064
1119
|
link_idx = links_id.index(pump_id)
|
|
1065
1120
|
link = links_data[link_idx]
|
|
1066
|
-
|
|
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}
|
|
1067
1135
|
|
|
1068
1136
|
valves = {}
|
|
1069
1137
|
for valve_id in valves_id:
|
|
1070
1138
|
link_idx = links_id.index(valve_id)
|
|
1071
1139
|
link = links_data[link_idx]
|
|
1072
1140
|
valve_type = links_type[link_idx]
|
|
1073
|
-
|
|
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}
|
|
1074
1148
|
|
|
1075
1149
|
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links, pumps=pumps,
|
|
1076
|
-
valves=valves,
|
|
1150
|
+
valves=valves, curves=curves, patterns=patterns,
|
|
1151
|
+
units=self.get_units_category())
|
|
1077
1152
|
|
|
1078
1153
|
def plot_topology(self, export_to_file: str = None) -> None:
|
|
1079
1154
|
"""
|
|
@@ -1224,11 +1299,14 @@ class ScenarioSimulator():
|
|
|
1224
1299
|
node_idx = self.epanet_api.get_node_idx(node_id)
|
|
1225
1300
|
|
|
1226
1301
|
if self.epanet_api.getnodetype(node_idx) != EpanetConstants.EN_RESERVOIR:
|
|
1227
|
-
demand_pattern_idx = self.epanet_api.getdemandpattern(node_idx)
|
|
1302
|
+
demand_pattern_idx = self.epanet_api.getdemandpattern(node_idx, 1)
|
|
1228
1303
|
else:
|
|
1229
1304
|
demand_pattern_idx = self.epanet_api.getnodevalue(node_idx, EpanetConstants.EN_PATTERN)
|
|
1230
1305
|
|
|
1231
|
-
|
|
1306
|
+
if demand_pattern_idx == 0:
|
|
1307
|
+
return None
|
|
1308
|
+
else:
|
|
1309
|
+
return self.get_pattern(self.epanet_api.getpatternid(demand_pattern_idx))
|
|
1232
1310
|
|
|
1233
1311
|
def set_node_demand_pattern(self, node_id: str, base_demand: float, demand_pattern_id: str,
|
|
1234
1312
|
demand_pattern: np.ndarray = None) -> None:
|
|
@@ -1960,7 +2038,8 @@ class ScenarioSimulator():
|
|
|
1960
2038
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1961
2039
|
frozen_sensor_config: bool = False,
|
|
1962
2040
|
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1963
|
-
reapply_uncertainties: bool = False
|
|
2041
|
+
reapply_uncertainties: bool = False,
|
|
2042
|
+
float_type: type = np.float32
|
|
1964
2043
|
) -> ScadaData:
|
|
1965
2044
|
"""
|
|
1966
2045
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -1990,6 +2069,10 @@ class ScenarioSimulator():
|
|
|
1990
2069
|
If True, the uncertainties are re-applied on the original properties.
|
|
1991
2070
|
|
|
1992
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
|
|
1993
2076
|
|
|
1994
2077
|
Returns
|
|
1995
2078
|
-------
|
|
@@ -2011,7 +2094,8 @@ class ScenarioSimulator():
|
|
|
2011
2094
|
frozen_sensor_config=frozen_sensor_config,
|
|
2012
2095
|
use_quality_time_step_as_reporting_time_step=
|
|
2013
2096
|
use_quality_time_step_as_reporting_time_step,
|
|
2014
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2097
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2098
|
+
float_type=float_type):
|
|
2015
2099
|
if result is None:
|
|
2016
2100
|
result = {}
|
|
2017
2101
|
for data_type, data in scada_data.items():
|
|
@@ -2039,7 +2123,8 @@ class ScenarioSimulator():
|
|
|
2039
2123
|
return_as_dict: bool = False,
|
|
2040
2124
|
frozen_sensor_config: bool = False,
|
|
2041
2125
|
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
2042
|
-
reapply_uncertainties: bool = False
|
|
2126
|
+
reapply_uncertainties: bool = False,
|
|
2127
|
+
float_type: type = np.float32,
|
|
2043
2128
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2044
2129
|
"""
|
|
2045
2130
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -2073,6 +2158,10 @@ class ScenarioSimulator():
|
|
|
2073
2158
|
If True, the uncertainties are re-applied on the original properties.
|
|
2074
2159
|
|
|
2075
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
|
|
2076
2165
|
|
|
2077
2166
|
Returns
|
|
2078
2167
|
-------
|
|
@@ -2146,13 +2235,15 @@ class ScenarioSimulator():
|
|
|
2146
2235
|
if len(bulk_species_node_concentrations) == 0:
|
|
2147
2236
|
bulk_species_node_concentrations = None
|
|
2148
2237
|
else:
|
|
2149
|
-
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). \
|
|
2150
2240
|
reshape((1, len(bulk_species_idx), n_nodes))
|
|
2151
2241
|
|
|
2152
2242
|
if len(bulk_species_link_concentrations) == 0:
|
|
2153
2243
|
bulk_species_link_concentrations = None
|
|
2154
2244
|
else:
|
|
2155
|
-
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). \
|
|
2156
2247
|
reshape((1, len(bulk_species_idx), n_links))
|
|
2157
2248
|
|
|
2158
2249
|
# Surface species
|
|
@@ -2169,7 +2260,8 @@ class ScenarioSimulator():
|
|
|
2169
2260
|
if len(surface_species_concentrations) == 0:
|
|
2170
2261
|
surface_species_concentrations = None
|
|
2171
2262
|
else:
|
|
2172
|
-
surface_species_concentrations = np.array(surface_species_concentrations
|
|
2263
|
+
surface_species_concentrations = np.array(surface_species_concentrations,
|
|
2264
|
+
dtype=float_type). \
|
|
2173
2265
|
reshape((1, len(surface_species_idx), n_links))
|
|
2174
2266
|
|
|
2175
2267
|
return bulk_species_node_concentrations, bulk_species_link_concentrations, \
|
|
@@ -2272,7 +2364,8 @@ class ScenarioSimulator():
|
|
|
2272
2364
|
|
|
2273
2365
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
2274
2366
|
frozen_sensor_config: bool = False,
|
|
2275
|
-
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
|
|
2276
2369
|
) -> ScadaData:
|
|
2277
2370
|
"""
|
|
2278
2371
|
Runs a basic quality analysis using EPANET.
|
|
@@ -2298,6 +2391,10 @@ class ScenarioSimulator():
|
|
|
2298
2391
|
with the hydraulic simulation.
|
|
2299
2392
|
|
|
2300
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
|
|
2301
2398
|
|
|
2302
2399
|
Returns
|
|
2303
2400
|
-------
|
|
@@ -2316,7 +2413,8 @@ class ScenarioSimulator():
|
|
|
2316
2413
|
return_as_dict=True,
|
|
2317
2414
|
frozen_sensor_config=frozen_sensor_config,
|
|
2318
2415
|
use_quality_time_step_as_reporting_time_step=
|
|
2319
|
-
use_quality_time_step_as_reporting_time_step
|
|
2416
|
+
use_quality_time_step_as_reporting_time_step,
|
|
2417
|
+
float_type=float_type):
|
|
2320
2418
|
if result is None:
|
|
2321
2419
|
result = {}
|
|
2322
2420
|
for data_type, data in scada_data.items():
|
|
@@ -2340,7 +2438,8 @@ class ScenarioSimulator():
|
|
|
2340
2438
|
support_abort: bool = False,
|
|
2341
2439
|
return_as_dict: bool = False,
|
|
2342
2440
|
frozen_sensor_config: bool = False,
|
|
2343
|
-
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
|
|
2344
2443
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2345
2444
|
"""
|
|
2346
2445
|
Runs a basic quality analysis using EPANET.
|
|
@@ -2372,6 +2471,10 @@ class ScenarioSimulator():
|
|
|
2372
2471
|
with the hydraulic simulation.
|
|
2373
2472
|
|
|
2374
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
|
|
2375
2478
|
|
|
2376
2479
|
Returns
|
|
2377
2480
|
-------
|
|
@@ -2430,8 +2533,10 @@ class ScenarioSimulator():
|
|
|
2430
2533
|
error_code = self.epanet_api.get_last_error_code()
|
|
2431
2534
|
if last_error_code == 0:
|
|
2432
2535
|
last_error_code = error_code
|
|
2433
|
-
quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY)
|
|
2434
|
-
|
|
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)
|
|
2435
2540
|
|
|
2436
2541
|
# Yield results in a regular time interval only!
|
|
2437
2542
|
if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
|
|
@@ -2468,7 +2573,8 @@ class ScenarioSimulator():
|
|
|
2468
2573
|
|
|
2469
2574
|
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2470
2575
|
frozen_sensor_config: bool = False,
|
|
2471
|
-
reapply_uncertainties: bool = False
|
|
2576
|
+
reapply_uncertainties: bool = False,
|
|
2577
|
+
float_type: type = np.float32) -> ScadaData:
|
|
2472
2578
|
"""
|
|
2473
2579
|
Runs the hydraulic simulation of this scenario (incl. basic quality if set).
|
|
2474
2580
|
|
|
@@ -2496,6 +2602,10 @@ class ScenarioSimulator():
|
|
|
2496
2602
|
If True, the uncertainties are re-applied on the original properties.
|
|
2497
2603
|
|
|
2498
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
|
|
2499
2609
|
|
|
2500
2610
|
Returns
|
|
2501
2611
|
-------
|
|
@@ -2515,7 +2625,8 @@ class ScenarioSimulator():
|
|
|
2515
2625
|
verbose=verbose,
|
|
2516
2626
|
return_as_dict=True,
|
|
2517
2627
|
frozen_sensor_config=frozen_sensor_config,
|
|
2518
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2628
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2629
|
+
float_type=float_type):
|
|
2519
2630
|
if result is None:
|
|
2520
2631
|
result = {}
|
|
2521
2632
|
for data_type, data in scada_data.items():
|
|
@@ -2545,7 +2656,8 @@ class ScenarioSimulator():
|
|
|
2545
2656
|
support_abort: bool = False,
|
|
2546
2657
|
return_as_dict: bool = False,
|
|
2547
2658
|
frozen_sensor_config: bool = False,
|
|
2548
|
-
reapply_uncertainties: bool = False
|
|
2659
|
+
reapply_uncertainties: bool = False,
|
|
2660
|
+
float_type: type = np.float32
|
|
2549
2661
|
) -> Generator[Union[tuple[ScadaData, bool], tuple[dict, bool]], bool, None]:
|
|
2550
2662
|
"""
|
|
2551
2663
|
Runs the hydraulic simulation of this scenario (incl. basic quality if set) and
|
|
@@ -2586,6 +2698,10 @@ class ScenarioSimulator():
|
|
|
2586
2698
|
If True, the uncertainties are re-applied on the original properties.
|
|
2587
2699
|
|
|
2588
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
|
|
2589
2705
|
|
|
2590
2706
|
Returns
|
|
2591
2707
|
-------
|
|
@@ -2604,7 +2720,7 @@ class ScenarioSimulator():
|
|
|
2604
2720
|
|
|
2605
2721
|
self.epanet_api.openH()
|
|
2606
2722
|
self.epanet_api.openQ()
|
|
2607
|
-
self.epanet_api.initH(EpanetConstants.
|
|
2723
|
+
self.epanet_api.initH(EpanetConstants.EN_SAVE_AND_INIT)
|
|
2608
2724
|
self.epanet_api.initQ(EpanetConstants.EN_SAVE)
|
|
2609
2725
|
|
|
2610
2726
|
requested_total_time = self.epanet_api.get_simulation_duration()
|
|
@@ -2654,32 +2770,42 @@ class ScenarioSimulator():
|
|
|
2654
2770
|
last_error_code = error_code
|
|
2655
2771
|
|
|
2656
2772
|
# Fetch data
|
|
2657
|
-
pressure_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_PRESSURE)
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
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)
|
|
2662
2783
|
|
|
2663
2784
|
tanks_volume_data = None
|
|
2664
2785
|
if len(self.epanet_api.get_all_tanks_idx()) > 0:
|
|
2665
2786
|
tanks_volume_data = np.array([self.epanet_api.get_tank_volume(tank_idx)
|
|
2666
|
-
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)
|
|
2667
2789
|
|
|
2668
2790
|
pumps_state_data = None
|
|
2669
2791
|
pumps_energy_usage_data = None
|
|
2670
2792
|
pumps_efficiency_data = None
|
|
2671
2793
|
if len(self.epanet_api.get_all_pumps_idx()) > 0:
|
|
2672
2794
|
pumps_state_data = np.array([self.epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_PUMP_STATE)
|
|
2673
|
-
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)
|
|
2674
2797
|
pumps_energy_usage_data = np.array([self.epanet_api.get_pump_energy_usage(pump_idx)
|
|
2675
|
-
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)
|
|
2676
2800
|
pumps_efficiency_data = np.array([self.epanet_api.get_pump_efficiency(pump_idx)
|
|
2677
|
-
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)
|
|
2678
2803
|
|
|
2679
2804
|
valves_state_data = None
|
|
2680
2805
|
if len(self.epanet_api.get_all_valves_idx()) > 0:
|
|
2681
2806
|
valves_state_data = np.array([self.epanet_api.getlinkvalue(link_valve_idx, EpanetConstants.EN_STATUS)
|
|
2682
|
-
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)
|
|
2683
2809
|
|
|
2684
2810
|
scada_data = ScadaData(network_topo=network_topo,
|
|
2685
2811
|
sensor_config=self._sensor_config,
|
|
@@ -2754,7 +2880,8 @@ class ScenarioSimulator():
|
|
|
2754
2880
|
|
|
2755
2881
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2756
2882
|
frozen_sensor_config: bool = False,
|
|
2757
|
-
reapply_uncertainties: bool = False
|
|
2883
|
+
reapply_uncertainties: bool = False,
|
|
2884
|
+
float_type: type = np.float32) -> ScadaData:
|
|
2758
2885
|
"""
|
|
2759
2886
|
Runs the simulation of this scenario.
|
|
2760
2887
|
|
|
@@ -2780,6 +2907,10 @@ class ScenarioSimulator():
|
|
|
2780
2907
|
If True, the uncertainties are re-applied on the original properties.
|
|
2781
2908
|
|
|
2782
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
|
|
2783
2914
|
|
|
2784
2915
|
Returns
|
|
2785
2916
|
-------
|
|
@@ -2800,7 +2931,8 @@ class ScenarioSimulator():
|
|
|
2800
2931
|
# Run hydraulic simulation step-by-step
|
|
2801
2932
|
result = self.run_hydraulic_simulation(hyd_export=hyd_export, verbose=verbose,
|
|
2802
2933
|
frozen_sensor_config=frozen_sensor_config,
|
|
2803
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2934
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2935
|
+
float_type=float_type)
|
|
2804
2936
|
|
|
2805
2937
|
# If necessary, run advanced quality simulation utilizing the computed hydraulics
|
|
2806
2938
|
if self.f_msx_in is not None:
|
|
@@ -2808,7 +2940,8 @@ class ScenarioSimulator():
|
|
|
2808
2940
|
result_msx = gen(hyd_file_in=hyd_export,
|
|
2809
2941
|
verbose=verbose,
|
|
2810
2942
|
frozen_sensor_config=frozen_sensor_config,
|
|
2811
|
-
reapply_uncertainties=reapply_uncertainties
|
|
2943
|
+
reapply_uncertainties=reapply_uncertainties,
|
|
2944
|
+
float_type=float_type)
|
|
2812
2945
|
result.join(result_msx)
|
|
2813
2946
|
|
|
2814
2947
|
if hyd_export_old is not None:
|
|
@@ -3473,7 +3606,8 @@ class ScenarioSimulator():
|
|
|
3473
3606
|
if node_init_qual < 0:
|
|
3474
3607
|
raise ValueError(f"{node_id}: Initial node quality can not be negative")
|
|
3475
3608
|
|
|
3476
|
-
for
|
|
3609
|
+
for node_id, node_init_qual in initial_quality.items():
|
|
3610
|
+
node_idx = self.epanet_api.get_node_idx(node_id)
|
|
3477
3611
|
self.epanet_api.set_node_init_quality(node_idx, node_init_qual)
|
|
3478
3612
|
|
|
3479
3613
|
if order_wall is not None:
|
|
@@ -3721,17 +3855,18 @@ class ScenarioSimulator():
|
|
|
3721
3855
|
any(not isinstance(species_id, str) or not isinstance(link_initial_conc, list)
|
|
3722
3856
|
for species_id, link_initial_conc in inital_conc.items()) or \
|
|
3723
3857
|
any(not isinstance(link_initial_conc, tuple)
|
|
3724
|
-
for link_initial_conc in inital_conc.values()) or \
|
|
3858
|
+
for link_initial_conc in list(itertools.chain(*inital_conc.values()))) or \
|
|
3725
3859
|
any(not isinstance(link_id, str) or not isinstance(conc, float)
|
|
3726
|
-
for link_id, conc in inital_conc.values()):
|
|
3860
|
+
for link_id, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3727
3861
|
raise TypeError("'inital_conc' must be an instance of " +
|
|
3728
3862
|
"'dict[str, list[tuple[str, float]]'")
|
|
3729
3863
|
if any(species_id not in self.sensor_config.bulk_species
|
|
3730
3864
|
for species_id in inital_conc.keys()):
|
|
3731
3865
|
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3732
|
-
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()))):
|
|
3733
3868
|
raise ValueError("Unknown link ID in 'inital_conc'")
|
|
3734
|
-
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3869
|
+
if any(conc < 0 for _, conc in list(itertools.chain(*inital_conc.values()))):
|
|
3735
3870
|
raise ValueError("Initial link concentration can not be negative")
|
|
3736
3871
|
|
|
3737
3872
|
for species_id, link_initial_conc in inital_conc.items():
|