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.
@@ -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 __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
159
-
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
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(my_f_inp_in, use_project=self.__f_msx_in is None)
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(my_f_msx_in)
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
- self._sensor_config = scenario_config.sensor_config
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
- node_tank_names = self.epanet_api.get_all_tanks_id()
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.get_link_bulk_raction_coeff(link_idx)
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.get_link_wall_raction_coeff(link_idx)
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 in zip(nodes_id, nodes_elevation, nodes_type, nodes_coord,
1034
- nodes_comments):
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 in zip(links_id, links_type, links_data, links_diameter,
1053
- links_length, links_roughness_coeff, links_bulk_coeff,
1054
- links_wall_coeff, links_loss_coeff):
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
- pumps[pump_id] = {"type": pump_type, "end_points": link}
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
- valves[valve_id] = {"type": valve_type, "end_points": link}
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, units=self.get_units_category())
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)[demand_idx + 1]
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
- return self.get_pattern(self.epanet_api.getpatternid(demand_pattern_idx))
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)).reshape(1, -1)
2433
- quality_link_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_QUALITY)).reshape(1, -1)
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) -> ScadaData:
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.EN_SAVE)
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)).reshape(1, -1)
2657
- flow_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_FLOW)).reshape(1, -1)
2658
- demand_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_DEMAND)).reshape(1, -1)
2659
- quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY)).reshape(1, -1)
2660
- quality_link_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_QUALITY)).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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) -> ScadaData:
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 node_idx in self.epanet_api.get_all_nodes_idx():
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 inital_conc.values()):
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():