epyt-flow 0.5.0__py3-none-any.whl → 0.7.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.
@@ -4,6 +4,7 @@ Module provides a class for scenario simulations.
4
4
  import sys
5
5
  import os
6
6
  import pathlib
7
+ import time
7
8
  from typing import Generator, Union
8
9
  from copy import deepcopy
9
10
  import shutil
@@ -21,8 +22,9 @@ from .sensor_config import SensorConfig, areaunit_to_id, massunit_to_id, quality
21
22
  qualityunit_to_str, MASS_UNIT_MG, \
22
23
  SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, SENSOR_TYPE_NODE_DEMAND, \
23
24
  SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, \
24
- SENSOR_TYPE_PUMP_STATE, SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, \
25
- SENSOR_TYPE_NODE_BULK_SPECIES, SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
25
+ SENSOR_TYPE_PUMP_STATE, SENSOR_TYPE_PUMP_EFFICIENCY, SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, \
26
+ SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, SENSOR_TYPE_NODE_BULK_SPECIES, \
27
+ SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
26
28
  from ..uncertainty import ModelUncertainty, SensorNoise
27
29
  from .events import SystemEvent, Leakage, ActuatorEvent, SensorFault, SensorReadingAttack, \
28
30
  SensorReadingEvent
@@ -92,6 +94,7 @@ class ScenarioSimulator():
92
94
  self.__controls = []
93
95
  self.__system_events = []
94
96
  self.__sensor_reading_events = []
97
+ self.__running_simulation = False
95
98
 
96
99
  custom_epanet_lib = None
97
100
  custom_epanetmsx_lib = None
@@ -109,12 +112,50 @@ class ScenarioSimulator():
109
112
  if os.path.isfile(os.path.join(path_to_custom_libs, libepanetmsx_name)):
110
113
  custom_epanetmsx_lib = os.path.join(path_to_custom_libs, libepanetmsx_name)
111
114
 
112
- self.epanet_api = epanet(self.__f_inp_in, ph=self.__f_msx_in is None,
113
- customlib=custom_epanet_lib, loadfile=True,
114
- display_msg=epanet_verbose)
115
+ with warnings.catch_warnings():
116
+ # Treat all warnings as exceptions when trying to load .inp and .msx files
117
+ warnings.simplefilter('error')
115
118
 
116
- if self.__f_msx_in is not None:
117
- self.epanet_api.loadMSXFile(self.__f_msx_in, customMSXlib=custom_epanetmsx_lib)
119
+ # Workaround for EPyT bug concerning parallel simulations (see EPyT issue #54):
120
+ # 1. Create random tmp folder (make sure it is unique!)
121
+ # 2. Copy .inp and .msx file there
122
+ # 3. Use those copies when loading EPyT
123
+ tmp_folder_path = os.path.join(get_temp_folder(), f"{random.randint(int(1e5), int(1e7))}{time.time()}")
124
+ pathlib.Path(tmp_folder_path).mkdir(parents=True, exist_ok=False)
125
+
126
+ def __file_exists(file_in: str) -> bool:
127
+ try:
128
+ return pathlib.Path(file_in).is_file()
129
+ except Exception:
130
+ return False
131
+
132
+ if not __file_exists(self.__f_inp_in):
133
+ my_f_inp_in = self.__f_inp_in
134
+ self.__my_f_inp_in = None
135
+ else:
136
+ my_f_inp_in = os.path.join(tmp_folder_path, pathlib.Path(self.__f_inp_in).name)
137
+ shutil.copyfile(self.__f_inp_in, my_f_inp_in)
138
+ self.__my_f_inp_in = my_f_inp_in
139
+
140
+ if self.__f_msx_in is not None:
141
+ if not __file_exists(self.__f_msx_in):
142
+ my_f_msx_in = self.__f_msx_in
143
+ self.__my_f_msx_in = None
144
+ else:
145
+ my_f_msx_in = os.path.join(tmp_folder_path, pathlib.Path(self.__f_msx_in).name)
146
+ shutil.copyfile(self.__f_msx_in, my_f_msx_in)
147
+ self.__my_f_msx_in = my_f_msx_in
148
+ else:
149
+ my_f_msx_in = None
150
+ self.__my_f_msx_in = None
151
+
152
+ self.epanet_api = epanet(my_f_inp_in, ph=self.__f_msx_in is None,
153
+ customlib=custom_epanet_lib, loadfile=True,
154
+ display_msg=epanet_verbose,
155
+ display_warnings=False)
156
+
157
+ if self.__f_msx_in is not None:
158
+ self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
118
159
 
119
160
  self.__sensor_config = self.__get_empty_sensor_config()
120
161
  if scenario_config is not None:
@@ -393,6 +434,9 @@ class ScenarioSimulator():
393
434
  new_sensor_config.quality_node_sensors = self.__sensor_config.quality_node_sensors
394
435
  new_sensor_config.quality_link_sensors = self.__sensor_config.quality_link_sensors
395
436
  new_sensor_config.pump_state_sensors = self.__sensor_config.pump_state_sensors
437
+ new_sensor_config.pump_efficiency_sensors = self.__sensor_config.pump_efficiency_sensors
438
+ new_sensor_config.pump_energyconsumption_sensors = self.__sensor_config.\
439
+ pump_energyconsumption_sensors
396
440
  new_sensor_config.valve_state_sensors = self.__sensor_config.valve_state_sensors
397
441
  new_sensor_config.tank_volume_sensors = self.__sensor_config.tank_volume_sensors
398
442
  new_sensor_config.bulk_species_node_sensors = self.__sensor_config.bulk_species_node_sensors
@@ -412,6 +456,9 @@ class ScenarioSimulator():
412
456
 
413
457
  self.epanet_api.unload()
414
458
 
459
+ if self.__my_f_inp_in is not None:
460
+ shutil.rmtree(pathlib.Path(self.__my_f_inp_in).parent)
461
+
415
462
  def __enter__(self):
416
463
  return self
417
464
 
@@ -697,6 +744,19 @@ class ScenarioSimulator():
697
744
  "units": qualityunit_to_id(qual_info.QualityChemUnits),
698
745
  "trace_node_id": qual_info.TraceNode}
699
746
 
747
+ def get_reporting_time_step(self) -> int:
748
+ """
749
+ Gets the reporting time steps -- i.e. time steps at which sensor readings are provided.
750
+
751
+ Is always a multiple of the hydraulic time step.
752
+
753
+ Returns
754
+ -------
755
+ `int`
756
+ Reporting time steps in seconds.
757
+ """
758
+ return self.epanet_api.getTimeReportingStep()
759
+
700
760
  def get_scenario_config(self) -> ScenarioConfig:
701
761
  """
702
762
  Gets the configuration of this scenario -- i.e. all information & elements
@@ -711,7 +771,7 @@ class ScenarioSimulator():
711
771
 
712
772
  general_params = {"hydraulic_time_step": self.get_hydraulic_time_step(),
713
773
  "quality_time_step": self.get_quality_time_step(),
714
- "reporting_time_step": self.epanet_api.getTimeReportingStep(),
774
+ "reporting_time_step": self.get_reporting_time_step(),
715
775
  "simulation_duration": self.get_simulation_duration(),
716
776
  "flow_units_id": self.get_flow_units(),
717
777
  "quality_model": self.get_quality_model(),
@@ -756,7 +816,7 @@ class ScenarioSimulator():
756
816
 
757
817
  Returns
758
818
  -------
759
- `epyt_flow.topology.NetworkTopology`
819
+ :class:`~epyt_flow.topology.NetworkTopology`
760
820
  Topology of this WDN as a graph.
761
821
  """
762
822
  self.__adapt_to_network_changes()
@@ -827,6 +887,9 @@ class ScenarioSimulator():
827
887
  """
828
888
  Randomizes all demand patterns.
829
889
  """
890
+ if self.__running_simulation is True:
891
+ raise RuntimeError("Can not change general parameters when simulation is running.")
892
+
830
893
  self.__adapt_to_network_changes()
831
894
 
832
895
  # Get all demand patterns
@@ -949,6 +1012,9 @@ class ScenarioSimulator():
949
1012
  event : :class:`~epyt_flow.simulation.events.system_event.SystemEvent`
950
1013
  System event.
951
1014
  """
1015
+ if self.__running_simulation is True:
1016
+ raise RuntimeError("Can not add events when simulation is running.")
1017
+
952
1018
  self.__adapt_to_network_changes()
953
1019
 
954
1020
  if not isinstance(event, SystemEvent):
@@ -968,6 +1034,9 @@ class ScenarioSimulator():
968
1034
  sensor_fault_event : :class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`
969
1035
  Sensor fault specifications.
970
1036
  """
1037
+ if self.__running_simulation is True:
1038
+ raise RuntimeError("Can not add events when simulation is running.")
1039
+
971
1040
  self.__adapt_to_network_changes()
972
1041
 
973
1042
  sensor_fault_event.validate(self.__sensor_config)
@@ -988,6 +1057,9 @@ class ScenarioSimulator():
988
1057
  sensor_reading_attack : :class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`
989
1058
  Sensor fault specifications.
990
1059
  """
1060
+ if self.__running_simulation is True:
1061
+ raise RuntimeError("Can not add events when simulation is running.")
1062
+
991
1063
  self.__adapt_to_network_changes()
992
1064
 
993
1065
  sensor_reading_attack.validate(self.__sensor_config)
@@ -1008,6 +1080,9 @@ class ScenarioSimulator():
1008
1080
  event : :class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`
1009
1081
  Sensor reading event.
1010
1082
  """
1083
+ if self.__running_simulation is True:
1084
+ raise RuntimeError("Can not add events when simulation is running.")
1085
+
1011
1086
  self.__adapt_to_network_changes()
1012
1087
 
1013
1088
  event.validate(self.__sensor_config)
@@ -1027,16 +1102,18 @@ class ScenarioSimulator():
1027
1102
  ----------
1028
1103
  sensor_type : `int`
1029
1104
  Sensor type. Must be one of the following:
1030
- - SENSOR_TYPE_NODE_PRESSURE = 1
1031
- - SENSOR_TYPE_NODE_QUALITY = 2
1032
- - SENSOR_TYPE_NODE_DEMAND = 3
1033
- - SENSOR_TYPE_LINK_FLOW = 4
1034
- - SENSOR_TYPE_LINK_QUALITY = 5
1035
- - SENSOR_TYPE_VALVE_STATE = 6
1036
- - SENSOR_TYPE_PUMP_STATE = 7
1037
- - SENSOR_TYPE_TANK_VOLUME = 8
1038
- - SENSOR_TYPE_BULK_SPECIES = 9
1039
- - SENSOR_TYPE_SURFACE_SPECIES = 10
1105
+ - SENSOR_TYPE_NODE_PRESSURE = 1
1106
+ - SENSOR_TYPE_NODE_QUALITY = 2
1107
+ - SENSOR_TYPE_NODE_DEMAND = 3
1108
+ - SENSOR_TYPE_LINK_FLOW = 4
1109
+ - SENSOR_TYPE_LINK_QUALITY = 5
1110
+ - SENSOR_TYPE_VALVE_STATE = 6
1111
+ - SENSOR_TYPE_PUMP_STATE = 7
1112
+ - SENSOR_TYPE_TANK_VOLUME = 8
1113
+ - SENSOR_TYPE_BULK_SPECIES = 9
1114
+ - SENSOR_TYPE_SURFACE_SPECIES = 10
1115
+ - SENSOR_TYPE_PUMP_EFFICIENCY = 12
1116
+ - SENSOR_TYPE_PUMP_ENERGYCONSUMPTION = 13
1040
1117
  sensor_locations : `list[str]` or `dict`
1041
1118
  Locations (IDs) of sensors either as a list or as a dict in the case of
1042
1119
  bulk and surface species.
@@ -1057,6 +1134,10 @@ class ScenarioSimulator():
1057
1134
  self.__sensor_config.valve_state_sensors = sensor_locations
1058
1135
  elif sensor_type == SENSOR_TYPE_PUMP_STATE:
1059
1136
  self.__sensor_config.pump_state_sensors = sensor_locations
1137
+ elif sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY:
1138
+ self.__sensor_config.pump_efficiency_sensors = sensor_locations
1139
+ elif sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
1140
+ self.__sensor_config.pump_energyconsumption_sensors = sensor_locations
1060
1141
  elif sensor_type == SENSOR_TYPE_TANK_VOLUME:
1061
1142
  self.__sensor_config.tank_volume_sensors = sensor_locations
1062
1143
  elif sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
@@ -1081,6 +1162,22 @@ class ScenarioSimulator():
1081
1162
  """
1082
1163
  self.set_sensors(SENSOR_TYPE_NODE_PRESSURE, sensor_locations)
1083
1164
 
1165
+ def place_pressure_sensors_everywhere(self, junctions_only: bool = False) -> None:
1166
+ """
1167
+ Places a pressure sensor at every node in the network.
1168
+
1169
+ Parameters
1170
+ ----------
1171
+ junctions_only : `bool`, optional
1172
+ If True, pressure sensors are only placed at junctions but not at tanks and reservoirs.
1173
+
1174
+ The default is False.
1175
+ """
1176
+ if junctions_only is True:
1177
+ self.set_pressure_sensors(self.epanet_api.getNodeJunctionNameID())
1178
+ else:
1179
+ self.set_pressure_sensors(self.__sensor_config.nodes)
1180
+
1084
1181
  def set_flow_sensors(self, sensor_locations: list[str]) -> None:
1085
1182
  """
1086
1183
  Sets the flow sensors -- i.e. measuring flows at some links/pipes in the network.
@@ -1092,6 +1189,12 @@ class ScenarioSimulator():
1092
1189
  """
1093
1190
  self.set_sensors(SENSOR_TYPE_LINK_FLOW, sensor_locations)
1094
1191
 
1192
+ def place_flow_sensors_everywhere(self) -> None:
1193
+ """
1194
+ Places a flow sensors at every link/pipe in the network.
1195
+ """
1196
+ self.set_flow_sensors(self.__sensor_config.links)
1197
+
1095
1198
  def set_demand_sensors(self, sensor_locations: list[str]) -> None:
1096
1199
  """
1097
1200
  Sets the demand sensors -- i.e. measuring demands at some nodes in the network.
@@ -1103,6 +1206,12 @@ class ScenarioSimulator():
1103
1206
  """
1104
1207
  self.set_sensors(SENSOR_TYPE_NODE_DEMAND, sensor_locations)
1105
1208
 
1209
+ def place_demand_sensors_everywhere(self) -> None:
1210
+ """
1211
+ Places a demand sensor at every node in the network.
1212
+ """
1213
+ self.set_demand_sensors(self.__sensor_config.nodes)
1214
+
1106
1215
  def set_node_quality_sensors(self, sensor_locations: list[str]) -> None:
1107
1216
  """
1108
1217
  Sets the node quality sensors -- i.e. measuring the water quality
@@ -1115,6 +1224,12 @@ class ScenarioSimulator():
1115
1224
  """
1116
1225
  self.set_sensors(SENSOR_TYPE_NODE_QUALITY, sensor_locations)
1117
1226
 
1227
+ def place_node_quality_sensors_everywhere(self) -> None:
1228
+ """
1229
+ Places a water quality sensor at every node in the network.
1230
+ """
1231
+ self.set_node_quality_sensors(self.__sensor_config.nodes)
1232
+
1118
1233
  def set_link_quality_sensors(self, sensor_locations: list[str]) -> None:
1119
1234
  """
1120
1235
  Sets the link quality sensors -- i.e. measuring the water quality
@@ -1127,6 +1242,12 @@ class ScenarioSimulator():
1127
1242
  """
1128
1243
  self.set_sensors(SENSOR_TYPE_LINK_QUALITY, sensor_locations)
1129
1244
 
1245
+ def place_link_quality_sensors_everywhere(self) -> None:
1246
+ """
1247
+ Places a water quality sensor at every link/pipe in the network.
1248
+ """
1249
+ self.set_link_quality_sensors(self.__sensor_config.links)
1250
+
1130
1251
  def set_valve_sensors(self, sensor_locations: list[str]) -> None:
1131
1252
  """
1132
1253
  Sets the valve state sensors -- i.e. retrieving the state of some valves in the network.
@@ -1138,7 +1259,16 @@ class ScenarioSimulator():
1138
1259
  """
1139
1260
  self.set_sensors(SENSOR_TYPE_VALVE_STATE, sensor_locations)
1140
1261
 
1141
- def set_pump_sensors(self, sensor_locations: list[str]) -> None:
1262
+ def place_valve_sensors_everywhere(self) -> None:
1263
+ """
1264
+ Places a valve state sensor at every valve in the network.
1265
+ """
1266
+ if len(self.__sensor_config.valves) == 0:
1267
+ warnings.warn("Network does not contain any valves", UserWarning)
1268
+
1269
+ self.set_valve_sensors(self.__sensor_config.valves)
1270
+
1271
+ def set_pump_state_sensors(self, sensor_locations: list[str]) -> None:
1142
1272
  """
1143
1273
  Sets the pump state sensors -- i.e. retrieving the state of some pumps in the network.
1144
1274
 
@@ -1149,6 +1279,81 @@ class ScenarioSimulator():
1149
1279
  """
1150
1280
  self.set_sensors(SENSOR_TYPE_PUMP_STATE, sensor_locations)
1151
1281
 
1282
+ def place_pump_state_sensors_everywhere(self) -> None:
1283
+ """
1284
+ Places a pump state sensor at every pump in the network.
1285
+ """
1286
+ if len(self.__sensor_config.pumps) == 0:
1287
+ warnings.warn("Network does not contain any pumps", UserWarning)
1288
+
1289
+ self.set_pump_state_sensors(self.__sensor_config.pumps)
1290
+
1291
+ def set_pump_efficiency_sensors(self, sensor_locations: list[str]) -> None:
1292
+ """
1293
+ Sets the pump efficiency sensors -- i.e. retrieving the efficiency of
1294
+ some pumps in the network.
1295
+
1296
+ Parameters
1297
+ ----------
1298
+ sensor_locations : `list[str]`
1299
+ Locations (IDs) of sensors.
1300
+ """
1301
+ self.set_sensors(SENSOR_TYPE_PUMP_EFFICIENCY, sensor_locations)
1302
+
1303
+ def place_pump_efficiency_sensors_everywhere(self) -> None:
1304
+ """
1305
+ Places a pump efficiency sensor at every pump in the network.
1306
+ """
1307
+ if len(self.__sensor_config.pumps) == 0:
1308
+ warnings.warn("Network does not contain any pumps", UserWarning)
1309
+
1310
+ self.set_pump_efficiency_sensors(self.__sensor_config.pumps)
1311
+
1312
+ def set_pump_energyconsumption_sensors(self, sensor_locations: list[str]) -> None:
1313
+ """
1314
+ Sets the pump energy consumption sensors -- i.e. retrieving the energy consumption of
1315
+ some pumps in the network.
1316
+
1317
+ Parameters
1318
+ ----------
1319
+ sensor_locations : `list[str]`
1320
+ Locations (IDs) of sensors.
1321
+ """
1322
+ self.set_sensors(SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, sensor_locations)
1323
+
1324
+ def place_pump_energyconsumption_sensors_everywhere(self) -> None:
1325
+ """
1326
+ Places a pump energy consumption sensor at every pump in the network.
1327
+ """
1328
+ if len(self.__sensor_config.pumps) == 0:
1329
+ warnings.warn("Network does not contain any pumps", UserWarning)
1330
+
1331
+ self.set_pump_energyconsumption_sensors(self.__sensor_config.pumps)
1332
+
1333
+ def set_pump_sensors(self, sensor_locations: list[str]) -> None:
1334
+ """
1335
+ Sets the pump sensors -- i.e. retrieving the state, efficiency, and energy consumption
1336
+ of some pumps in the network.
1337
+
1338
+ Parameters
1339
+ ----------
1340
+ sensor_locations : `list[str]`
1341
+ Locations (IDs) of sensors.
1342
+ """
1343
+ self.set_sensors(SENSOR_TYPE_PUMP_STATE, sensor_locations)
1344
+ self.set_sensors(SENSOR_TYPE_PUMP_EFFICIENCY, sensor_locations)
1345
+ self.set_sensors(SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, sensor_locations)
1346
+
1347
+ def place_pump_sensors_everywhere(self) -> None:
1348
+ """
1349
+ Palces pump sensors at every pump in the network -- i.e. retrieving the state, efficiency,
1350
+ and energy consumption of all pumps in the network.
1351
+ """
1352
+ if len(self.__sensor_config.pumps) == 0:
1353
+ warnings.warn("Network does not contain any pumps", UserWarning)
1354
+
1355
+ self.set_pump_sensors(self.__sensor_config.pumps)
1356
+
1152
1357
  def set_tank_sensors(self, sensor_locations: list[str]) -> None:
1153
1358
  """
1154
1359
  Sets the tank volume sensors -- i.e. measuring water volumes in some tanks in the network.
@@ -1160,6 +1365,15 @@ class ScenarioSimulator():
1160
1365
  """
1161
1366
  self.set_sensors(SENSOR_TYPE_TANK_VOLUME, sensor_locations)
1162
1367
 
1368
+ def place_tank_sensors_everywhere(self) -> None:
1369
+ """
1370
+ Places a water tank volume sensor at every tank in the network.
1371
+ """
1372
+ if len(self.__sensor_config.tanks) == 0:
1373
+ warnings.warn("Network does not contain any tanks", UserWarning)
1374
+
1375
+ self.set_tank_sensors(self.__sensor_config.tanks)
1376
+
1163
1377
  def set_bulk_species_node_sensors(self, sensor_info: dict) -> None:
1164
1378
  """
1165
1379
  Sets the bulk species node sensors -- i.e. measuring bulk species concentrations
@@ -1172,6 +1386,14 @@ class ScenarioSimulator():
1172
1386
  """
1173
1387
  self.set_sensors(SENSOR_TYPE_NODE_BULK_SPECIES, sensor_info)
1174
1388
 
1389
+ def place_bulk_species_node_sensors_everywhere(self) -> None:
1390
+ """
1391
+ Places bulk species concentration sensors at every node in the network for
1392
+ every bulk species.
1393
+ """
1394
+ self.set_bulk_species_node_sensors({species_id: self.__sensor_config.nodes
1395
+ for species_id in self.__sensor_config.bulk_species})
1396
+
1175
1397
  def set_bulk_species_link_sensors(self, sensor_info: dict) -> None:
1176
1398
  """
1177
1399
  Sets the bulk species link/pipe sensors -- i.e. measuring bulk species concentrations
@@ -1184,6 +1406,14 @@ class ScenarioSimulator():
1184
1406
  """
1185
1407
  self.set_sensors(SENSOR_TYPE_LINK_BULK_SPECIES, sensor_info)
1186
1408
 
1409
+ def place_bulk_species_link_sensors_everywhere(self) -> None:
1410
+ """
1411
+ Places bulk species concentration sensors at every link/pipe in the network
1412
+ for every bulk species.
1413
+ """
1414
+ self.set_bulk_species_link_sensors({species_id: self.__sensor_config.links
1415
+ for species_id in self.__sensor_config.bulk_species})
1416
+
1187
1417
  def set_surface_species_sensors(self, sensor_info: dict) -> None:
1188
1418
  """
1189
1419
  Sets the surface species sensors -- i.e. measuring surface species concentrations
@@ -1196,6 +1426,22 @@ class ScenarioSimulator():
1196
1426
  """
1197
1427
  self.set_sensors(SENSOR_TYPE_SURFACE_SPECIES, sensor_info)
1198
1428
 
1429
+ def place_surface_species_sensors_everywhere(self) -> None:
1430
+ """
1431
+ Places surface species concentration sensors at every link/pipe in the network
1432
+ for every surface species.
1433
+ """
1434
+ self.set_bulk_species_node_sensors({species_id: self.__sensor_config.links
1435
+ for species_id in
1436
+ self.__sensor_config.surface_species})
1437
+
1438
+ def place_sensors_everywhere(self) -> None:
1439
+ """
1440
+ Places sensors everywhere -- i.e. every possible quantity is monitored
1441
+ at every position in the network.
1442
+ """
1443
+ self.__sensor_config.place_sensors_everywhere()
1444
+
1199
1445
  def __prepare_simulation(self) -> None:
1200
1446
  self.__adapt_to_network_changes()
1201
1447
 
@@ -1234,6 +1480,9 @@ class ScenarioSimulator():
1234
1480
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1235
1481
  Quality simulation results as SCADA data.
1236
1482
  """
1483
+ if self.__running_simulation is True:
1484
+ raise RuntimeError("A simulation is already running.")
1485
+
1237
1486
  if self.__f_msx_in is None:
1238
1487
  raise ValueError("No .msx file specified")
1239
1488
 
@@ -1298,6 +1547,9 @@ class ScenarioSimulator():
1298
1547
  Generator containing the current EPANET-MSX simulation results as SCADA data
1299
1548
  (i.e. species concentrations).
1300
1549
  """
1550
+ if self.__running_simulation is True:
1551
+ raise RuntimeError("A simulation is already running.")
1552
+
1301
1553
  if self.__f_msx_in is None:
1302
1554
  raise ValueError("No .msx file specified")
1303
1555
 
@@ -1314,6 +1566,8 @@ class ScenarioSimulator():
1314
1566
 
1315
1567
  self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
1316
1568
 
1569
+ self.__running_simulation = True
1570
+
1317
1571
  bulk_species_idx = self.epanet_api.getMSXSpeciesIndex(self.__sensor_config.bulk_species)
1318
1572
  surface_species_idx = self.epanet_api.getMSXSpeciesIndex(
1319
1573
  self.__sensor_config.surface_species)
@@ -1406,6 +1660,7 @@ class ScenarioSimulator():
1406
1660
 
1407
1661
  # Run step-by-step simulation
1408
1662
  tleft = 1
1663
+ total_time = 0
1409
1664
  while tleft > 0:
1410
1665
  if support_abort is True: # Can the simulation be aborted? If so, handle it.
1411
1666
  abort = yield
@@ -1448,6 +1703,8 @@ class ScenarioSimulator():
1448
1703
  sensor_noise=self.__sensor_noise,
1449
1704
  frozen_sensor_config=frozen_sensor_config)
1450
1705
 
1706
+ self.__running_simulation = False
1707
+
1451
1708
  def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
1452
1709
  frozen_sensor_config: bool = False) -> ScadaData:
1453
1710
  """
@@ -1473,6 +1730,9 @@ class ScenarioSimulator():
1473
1730
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1474
1731
  Quality simulation results as SCADA data.
1475
1732
  """
1733
+ if self.__running_simulation is True:
1734
+ raise RuntimeError("A simulation is already running.")
1735
+
1476
1736
  result = None
1477
1737
 
1478
1738
  # Run simulation step-by-step
@@ -1533,6 +1793,9 @@ class ScenarioSimulator():
1533
1793
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1534
1794
  Generator with the current simulation results/states as SCADA data.
1535
1795
  """
1796
+ if self.__running_simulation is True:
1797
+ raise RuntimeError("A simulation is already running.")
1798
+
1536
1799
  requested_time_step = self.epanet_api.getTimeHydraulicStep()
1537
1800
  reporting_time_start = self.epanet_api.getTimeReportingStart()
1538
1801
  reporting_time_step = self.epanet_api.getTimeReportingStep()
@@ -1626,6 +1889,9 @@ class ScenarioSimulator():
1626
1889
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1627
1890
  Simulation results as SCADA data (i.e. sensor readings).
1628
1891
  """
1892
+ if self.__running_simulation is True:
1893
+ raise RuntimeError("A simulation is already running.")
1894
+
1629
1895
  self.__adapt_to_network_changes()
1630
1896
 
1631
1897
  result = None
@@ -1720,10 +1986,15 @@ class ScenarioSimulator():
1720
1986
  Generator with the current simulation results/states as SCADA data
1721
1987
  (i.e. sensor readings).
1722
1988
  """
1989
+ if self.__running_simulation is True:
1990
+ raise RuntimeError("A simulation is already running.")
1991
+
1723
1992
  self.__adapt_to_network_changes()
1724
1993
 
1725
1994
  self.__prepare_simulation()
1726
1995
 
1996
+ self.__running_simulation = True
1997
+
1727
1998
  self.epanet_api.openHydraulicAnalysis()
1728
1999
  self.epanet_api.openQualityAnalysis()
1729
2000
  self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
@@ -1745,11 +2016,6 @@ class ScenarioSimulator():
1745
2016
  tstep = 1
1746
2017
  first_itr = True
1747
2018
  while tstep > 0:
1748
- if support_abort is True: # Can the simulation be aborted? If so, handle it.
1749
- abort = yield
1750
- if abort is not False:
1751
- break
1752
-
1753
2019
  if first_itr is True: # Fix current time in the first iteration
1754
2020
  tstep = 0
1755
2021
  first_itr = False
@@ -1781,8 +2047,8 @@ class ScenarioSimulator():
1781
2047
  tanks_volume_data = self.epanet_api.getNodeTankVolume().reshape(1, -1)
1782
2048
 
1783
2049
  pump_idx = self.epanet_api.getLinkPumpIndex()
1784
- pump_energy_usage_data = self.epanet_api.getLinkEnergy(pump_idx).reshape(1, -1)
1785
- pump_efficiency_data = self.epanet_api.getLinkPumpEfficiency().reshape(1, -1)
2050
+ pumps_energy_usage_data = self.epanet_api.getLinkEnergy(pump_idx).reshape(1, -1)
2051
+ pumps_efficiency_data = self.epanet_api.getLinkPumpEfficiency().reshape(1, -1)
1786
2052
 
1787
2053
  link_valve_idx = self.epanet_api.getLinkValveIndex()
1788
2054
  valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
@@ -1796,8 +2062,8 @@ class ScenarioSimulator():
1796
2062
  pumps_state_data_raw=pumps_state_data,
1797
2063
  valves_state_data_raw=valves_state_data,
1798
2064
  tanks_volume_data_raw=tanks_volume_data,
1799
- pump_energy_usage_data=pump_energy_usage_data,
1800
- pump_efficiency_data=pump_efficiency_data,
2065
+ pumps_energy_usage_data_raw=pumps_energy_usage_data,
2066
+ pumps_efficiency_data_raw=pumps_efficiency_data,
1801
2067
  sensor_readings_time=np.array([total_time]),
1802
2068
  sensor_reading_events=self.__sensor_reading_events,
1803
2069
  sensor_noise=self.__sensor_noise,
@@ -1806,19 +2072,26 @@ class ScenarioSimulator():
1806
2072
  # Yield results in a regular time interval only!
1807
2073
  if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
1808
2074
  if return_as_dict is True:
1809
- yield {"pressure_data_raw": pressure_data,
1810
- "flow_data_raw": flow_data,
1811
- "demand_data_raw": demand_data,
1812
- "node_quality_data_raw": quality_node_data,
1813
- "link_quality_data_raw": quality_link_data,
1814
- "pumps_state_data_raw": pumps_state_data,
1815
- "valves_state_data_raw": valves_state_data,
1816
- "tanks_volume_data_raw": tanks_volume_data,
1817
- "pump_energy_usage_data": pump_energy_usage_data,
1818
- "pump_efficiency_data": pump_efficiency_data,
1819
- "sensor_readings_time": np.array([total_time])}
2075
+ data = {"pressure_data_raw": pressure_data,
2076
+ "flow_data_raw": flow_data,
2077
+ "demand_data_raw": demand_data,
2078
+ "node_quality_data_raw": quality_node_data,
2079
+ "link_quality_data_raw": quality_link_data,
2080
+ "pumps_state_data_raw": pumps_state_data,
2081
+ "valves_state_data_raw": valves_state_data,
2082
+ "tanks_volume_data_raw": tanks_volume_data,
2083
+ "pumps_energy_usage_data_raw": pumps_energy_usage_data,
2084
+ "pumps_efficiency_data_raw": pumps_efficiency_data,
2085
+ "sensor_readings_time": np.array([total_time])}
1820
2086
  else:
1821
- yield scada_data
2087
+ data = scada_data
2088
+
2089
+ if support_abort is True: # Can the simulation be aborted? If so, handle it.
2090
+ abort = yield
2091
+ if abort is not False:
2092
+ break
2093
+
2094
+ yield data
1822
2095
 
1823
2096
  # Apply control modules
1824
2097
  for control in self.__controls:
@@ -1831,9 +2104,12 @@ class ScenarioSimulator():
1831
2104
  self.epanet_api.closeQualityAnalysis()
1832
2105
  self.epanet_api.closeHydraulicAnalysis()
1833
2106
 
2107
+ self.__running_simulation = False
2108
+
1834
2109
  if hyd_export is not None:
1835
2110
  self.epanet_api.saveHydraulicFile(hyd_export)
1836
2111
  except Exception as ex:
2112
+ self.__running_simulation = False
1837
2113
  raise ex
1838
2114
 
1839
2115
  def set_model_uncertainty(self, model_uncertainty: ModelUncertainty) -> None:
@@ -1845,6 +2121,9 @@ class ScenarioSimulator():
1845
2121
  model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`
1846
2122
  Model uncertainty specifications.
1847
2123
  """
2124
+ if self.__running_simulation is True:
2125
+ raise RuntimeError("Can not set uncertainties when simulation is running.")
2126
+
1848
2127
  self.__adapt_to_network_changes()
1849
2128
 
1850
2129
  if not isinstance(model_uncertainty, ModelUncertainty):
@@ -1863,6 +2142,9 @@ class ScenarioSimulator():
1863
2142
  sensor_noise : :class:`~epyt_flow.uncertainties.sensor_noise.SensorNoise`
1864
2143
  Sensor noise specification.
1865
2144
  """
2145
+ if self.__running_simulation is True:
2146
+ raise RuntimeError("Can not set sensor noise when simulation is running.")
2147
+
1866
2148
  self.__adapt_to_network_changes()
1867
2149
 
1868
2150
  if not isinstance(sensor_noise, SensorNoise):
@@ -1945,8 +2227,35 @@ class ScenarioSimulator():
1945
2227
 
1946
2228
  The default is None.
1947
2229
  """
2230
+ if self.__running_simulation is True:
2231
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2232
+
1948
2233
  self.__adapt_to_network_changes()
1949
2234
 
2235
+ if flow_units_id is not None:
2236
+ if flow_units_id == ToolkitConstants.EN_CFS:
2237
+ self.epanet_api.setFlowUnitsCFS()
2238
+ elif flow_units_id == ToolkitConstants.EN_GPM:
2239
+ self.epanet_api.setFlowUnitsGPM()
2240
+ elif flow_units_id == ToolkitConstants.EN_MGD:
2241
+ self.epanet_api.setFlowUnitsMGD()
2242
+ elif flow_units_id == ToolkitConstants.EN_IMGD:
2243
+ self.epanet_api.setFlowUnitsIMGD()
2244
+ elif flow_units_id == ToolkitConstants.EN_AFD:
2245
+ self.epanet_api.setFlowUnitsAFD()
2246
+ elif flow_units_id == ToolkitConstants.EN_LPS:
2247
+ self.epanet_api.setFlowUnitsLPS()
2248
+ elif flow_units_id == ToolkitConstants.EN_LPM:
2249
+ self.epanet_api.setFlowUnitsLPM()
2250
+ elif flow_units_id == ToolkitConstants.EN_MLD:
2251
+ self.epanet_api.setFlowUnitsMLD()
2252
+ elif flow_units_id == ToolkitConstants.EN_CMH:
2253
+ self.epanet_api.setFlowUnitsCMH()
2254
+ elif flow_units_id == ToolkitConstants.EN_CMD:
2255
+ self.epanet_api.setFlowUnitsCMD()
2256
+ else:
2257
+ raise ValueError(f"Unknown flow units '{flow_units_id}'")
2258
+
1950
2259
  if demand_model is not None:
1951
2260
  self.epanet_api.setDemandModel(demand_model["type"], demand_model["pressure_min"],
1952
2261
  demand_model["pressure_required"],
@@ -1991,30 +2300,6 @@ class ScenarioSimulator():
1991
2300
  "greater than the hydraulic time step")
1992
2301
  self.epanet_api.setTimeQualityStep(quality_time_step)
1993
2302
 
1994
- if flow_units_id is not None:
1995
- if flow_units_id == ToolkitConstants.EN_CFS:
1996
- self.epanet_api.setFlowUnitsCFS()
1997
- elif flow_units_id == ToolkitConstants.EN_GPM:
1998
- self.epanet_api.setFlowUnitsGPM()
1999
- elif flow_units_id == ToolkitConstants.EN_MGD:
2000
- self.epanet_api.setFlowUnitsMGD()
2001
- elif flow_units_id == ToolkitConstants.EN_IMGD:
2002
- self.epanet_api.setFlowUnitsIMGD()
2003
- elif flow_units_id == ToolkitConstants.EN_AFD:
2004
- self.epanet_api.setFlowUnitsAFD()
2005
- elif flow_units_id == ToolkitConstants.EN_LPS:
2006
- self.epanet_api.setFlowUnitsLPS()
2007
- elif flow_units_id == ToolkitConstants.EN_LPM:
2008
- self.epanet_api.setFlowUnitsLPM()
2009
- elif flow_units_id == ToolkitConstants.EN_MLD:
2010
- self.epanet_api.setFlowUnitsMLD()
2011
- elif flow_units_id == ToolkitConstants.EN_CMH:
2012
- self.epanet_api.setFlowUnitsCMH()
2013
- elif flow_units_id == ToolkitConstants.EN_CMD:
2014
- self.epanet_api.setFlowUnitsCMD()
2015
- else:
2016
- raise ValueError(f"Unknown flow units '{flow_units_id}'")
2017
-
2018
2303
  if quality_model is not None:
2019
2304
  if quality_model["type"] == "NONE":
2020
2305
  self.epanet_api.setQualityType("none")
@@ -2067,6 +2352,9 @@ class ScenarioSimulator():
2067
2352
  Sets water age analysis -- i.e. estimates the water age (in hours) at
2068
2353
  all places in the network.
2069
2354
  """
2355
+ if self.__running_simulation is True:
2356
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2357
+
2070
2358
  self.__adapt_to_network_changes()
2071
2359
 
2072
2360
  self.__warn_if_quality_set()
@@ -2095,6 +2383,9 @@ class ScenarioSimulator():
2095
2383
 
2096
2384
  The default is MASS_UNIT_MG.
2097
2385
  """
2386
+ if self.__running_simulation is True:
2387
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2388
+
2098
2389
  self.__adapt_to_network_changes()
2099
2390
 
2100
2391
  self.__warn_if_quality_set()
@@ -2139,6 +2430,9 @@ class ScenarioSimulator():
2139
2430
 
2140
2431
  The default is 1.
2141
2432
  """
2433
+ if self.__running_simulation is True:
2434
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2435
+
2142
2436
  self.__adapt_to_network_changes()
2143
2437
 
2144
2438
  if self.epanet_api.getQualityInfo().QualityCode != ToolkitConstants.EN_CHEM:
@@ -2175,6 +2469,9 @@ class ScenarioSimulator():
2175
2469
  trace_node_id : `str`
2176
2470
  ID of the node traced in the source tracing analysis.
2177
2471
  """
2472
+ if self.__running_simulation is True:
2473
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2474
+
2178
2475
  self.__adapt_to_network_changes()
2179
2476
 
2180
2477
  if trace_node_id not in self.__sensor_config.nodes: