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.
@@ -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,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.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}
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 in zip(links_id, links_type, links_data, links_diameter,
1054
- links_length, links_roughness_coeff, links_bulk_coeff,
1055
- 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):
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
- 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}
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
- 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}
1074
1148
 
1075
1149
  return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links, pumps=pumps,
1076
- valves=valves, units=self.get_units_category())
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
- 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))
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)).reshape(1, -1)
2434
- 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)
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) -> ScadaData:
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.EN_SAVE)
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)).reshape(1, -1)
2658
- flow_data = np.array(self.epanet_api.getlinkvalues(EpanetConstants.EN_FLOW)).reshape(1, -1)
2659
- demand_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_DEMAND)).reshape(1, -1)
2660
- quality_node_data = np.array(self.epanet_api.getnodevalues(EpanetConstants.EN_QUALITY)).reshape(1, -1)
2661
- 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)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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()]).reshape(1, -1)
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) -> ScadaData:
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 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)
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 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()))):
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():