epyt-flow 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/benchmarks/leakdb.py +1 -0
- epyt_flow/metrics.py +66 -4
- epyt_flow/serialization.py +33 -0
- epyt_flow/simulation/scada/scada_data.py +715 -16
- epyt_flow/simulation/scada/scada_data_export.py +5 -1
- epyt_flow/simulation/scenario_simulator.py +306 -99
- epyt_flow/simulation/sensor_config.py +49 -2
- epyt_flow/topology.py +3 -3
- epyt_flow/uncertainty/model_uncertainty.py +11 -5
- epyt_flow/uncertainty/uncertainties.py +8 -0
- epyt_flow/utils.py +69 -2
- {epyt_flow-0.6.0.dist-info → epyt_flow-0.7.1.dist-info}/METADATA +50 -7
- {epyt_flow-0.6.0.dist-info → epyt_flow-0.7.1.dist-info}/RECORD +17 -17
- {epyt_flow-0.6.0.dist-info → epyt_flow-0.7.1.dist-info}/WHEEL +1 -1
- {epyt_flow-0.6.0.dist-info → epyt_flow-0.7.1.dist-info}/LICENSE +0 -0
- {epyt_flow-0.6.0.dist-info → epyt_flow-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -94,6 +94,7 @@ class ScenarioSimulator():
|
|
|
94
94
|
self.__controls = []
|
|
95
95
|
self.__system_events = []
|
|
96
96
|
self.__sensor_reading_events = []
|
|
97
|
+
self.__running_simulation = False
|
|
97
98
|
|
|
98
99
|
custom_epanet_lib = None
|
|
99
100
|
custom_epanetmsx_lib = None
|
|
@@ -150,7 +151,8 @@ class ScenarioSimulator():
|
|
|
150
151
|
|
|
151
152
|
self.epanet_api = epanet(my_f_inp_in, ph=self.__f_msx_in is None,
|
|
152
153
|
customlib=custom_epanet_lib, loadfile=True,
|
|
153
|
-
display_msg=epanet_verbose
|
|
154
|
+
display_msg=epanet_verbose,
|
|
155
|
+
display_warnings=False)
|
|
154
156
|
|
|
155
157
|
if self.__f_msx_in is not None:
|
|
156
158
|
self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
@@ -432,7 +434,7 @@ class ScenarioSimulator():
|
|
|
432
434
|
new_sensor_config.quality_node_sensors = self.__sensor_config.quality_node_sensors
|
|
433
435
|
new_sensor_config.quality_link_sensors = self.__sensor_config.quality_link_sensors
|
|
434
436
|
new_sensor_config.pump_state_sensors = self.__sensor_config.pump_state_sensors
|
|
435
|
-
new_sensor_config.
|
|
437
|
+
new_sensor_config.pump_efficiency_sensors = self.__sensor_config.pump_efficiency_sensors
|
|
436
438
|
new_sensor_config.pump_energyconsumption_sensors = self.__sensor_config.\
|
|
437
439
|
pump_energyconsumption_sensors
|
|
438
440
|
new_sensor_config.valve_state_sensors = self.__sensor_config.valve_state_sensors
|
|
@@ -456,8 +458,6 @@ class ScenarioSimulator():
|
|
|
456
458
|
|
|
457
459
|
if self.__my_f_inp_in is not None:
|
|
458
460
|
shutil.rmtree(pathlib.Path(self.__my_f_inp_in).parent)
|
|
459
|
-
if self.__my_f_msx_in is not None:
|
|
460
|
-
shutil.rmtree(pathlib.Path(self.__my_f_msx_in).parent)
|
|
461
461
|
|
|
462
462
|
def __enter__(self):
|
|
463
463
|
return self
|
|
@@ -744,6 +744,19 @@ class ScenarioSimulator():
|
|
|
744
744
|
"units": qualityunit_to_id(qual_info.QualityChemUnits),
|
|
745
745
|
"trace_node_id": qual_info.TraceNode}
|
|
746
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
|
+
|
|
747
760
|
def get_scenario_config(self) -> ScenarioConfig:
|
|
748
761
|
"""
|
|
749
762
|
Gets the configuration of this scenario -- i.e. all information & elements
|
|
@@ -758,7 +771,7 @@ class ScenarioSimulator():
|
|
|
758
771
|
|
|
759
772
|
general_params = {"hydraulic_time_step": self.get_hydraulic_time_step(),
|
|
760
773
|
"quality_time_step": self.get_quality_time_step(),
|
|
761
|
-
"reporting_time_step": self.
|
|
774
|
+
"reporting_time_step": self.get_reporting_time_step(),
|
|
762
775
|
"simulation_duration": self.get_simulation_duration(),
|
|
763
776
|
"flow_units_id": self.get_flow_units(),
|
|
764
777
|
"quality_model": self.get_quality_model(),
|
|
@@ -874,6 +887,9 @@ class ScenarioSimulator():
|
|
|
874
887
|
"""
|
|
875
888
|
Randomizes all demand patterns.
|
|
876
889
|
"""
|
|
890
|
+
if self.__running_simulation is True:
|
|
891
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
892
|
+
|
|
877
893
|
self.__adapt_to_network_changes()
|
|
878
894
|
|
|
879
895
|
# Get all demand patterns
|
|
@@ -996,6 +1012,9 @@ class ScenarioSimulator():
|
|
|
996
1012
|
event : :class:`~epyt_flow.simulation.events.system_event.SystemEvent`
|
|
997
1013
|
System event.
|
|
998
1014
|
"""
|
|
1015
|
+
if self.__running_simulation is True:
|
|
1016
|
+
raise RuntimeError("Can not add events when simulation is running.")
|
|
1017
|
+
|
|
999
1018
|
self.__adapt_to_network_changes()
|
|
1000
1019
|
|
|
1001
1020
|
if not isinstance(event, SystemEvent):
|
|
@@ -1015,6 +1034,9 @@ class ScenarioSimulator():
|
|
|
1015
1034
|
sensor_fault_event : :class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`
|
|
1016
1035
|
Sensor fault specifications.
|
|
1017
1036
|
"""
|
|
1037
|
+
if self.__running_simulation is True:
|
|
1038
|
+
raise RuntimeError("Can not add events when simulation is running.")
|
|
1039
|
+
|
|
1018
1040
|
self.__adapt_to_network_changes()
|
|
1019
1041
|
|
|
1020
1042
|
sensor_fault_event.validate(self.__sensor_config)
|
|
@@ -1035,6 +1057,9 @@ class ScenarioSimulator():
|
|
|
1035
1057
|
sensor_reading_attack : :class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`
|
|
1036
1058
|
Sensor fault specifications.
|
|
1037
1059
|
"""
|
|
1060
|
+
if self.__running_simulation is True:
|
|
1061
|
+
raise RuntimeError("Can not add events when simulation is running.")
|
|
1062
|
+
|
|
1038
1063
|
self.__adapt_to_network_changes()
|
|
1039
1064
|
|
|
1040
1065
|
sensor_reading_attack.validate(self.__sensor_config)
|
|
@@ -1055,6 +1080,9 @@ class ScenarioSimulator():
|
|
|
1055
1080
|
event : :class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`
|
|
1056
1081
|
Sensor reading event.
|
|
1057
1082
|
"""
|
|
1083
|
+
if self.__running_simulation is True:
|
|
1084
|
+
raise RuntimeError("Can not add events when simulation is running.")
|
|
1085
|
+
|
|
1058
1086
|
self.__adapt_to_network_changes()
|
|
1059
1087
|
|
|
1060
1088
|
event.validate(self.__sensor_config)
|
|
@@ -1134,11 +1162,21 @@ class ScenarioSimulator():
|
|
|
1134
1162
|
"""
|
|
1135
1163
|
self.set_sensors(SENSOR_TYPE_NODE_PRESSURE, sensor_locations)
|
|
1136
1164
|
|
|
1137
|
-
def place_pressure_sensors_everywhere(self) -> None:
|
|
1165
|
+
def place_pressure_sensors_everywhere(self, junctions_only: bool = False) -> None:
|
|
1138
1166
|
"""
|
|
1139
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.
|
|
1140
1175
|
"""
|
|
1141
|
-
|
|
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)
|
|
1142
1180
|
|
|
1143
1181
|
def set_flow_sensors(self, sensor_locations: list[str]) -> None:
|
|
1144
1182
|
"""
|
|
@@ -1442,6 +1480,9 @@ class ScenarioSimulator():
|
|
|
1442
1480
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
1443
1481
|
Quality simulation results as SCADA data.
|
|
1444
1482
|
"""
|
|
1483
|
+
if self.__running_simulation is True:
|
|
1484
|
+
raise RuntimeError("A simulation is already running.")
|
|
1485
|
+
|
|
1445
1486
|
if self.__f_msx_in is None:
|
|
1446
1487
|
raise ValueError("No .msx file specified")
|
|
1447
1488
|
|
|
@@ -1506,6 +1547,9 @@ class ScenarioSimulator():
|
|
|
1506
1547
|
Generator containing the current EPANET-MSX simulation results as SCADA data
|
|
1507
1548
|
(i.e. species concentrations).
|
|
1508
1549
|
"""
|
|
1550
|
+
if self.__running_simulation is True:
|
|
1551
|
+
raise RuntimeError("A simulation is already running.")
|
|
1552
|
+
|
|
1509
1553
|
if self.__f_msx_in is None:
|
|
1510
1554
|
raise ValueError("No .msx file specified")
|
|
1511
1555
|
|
|
@@ -1522,6 +1566,8 @@ class ScenarioSimulator():
|
|
|
1522
1566
|
|
|
1523
1567
|
self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
|
|
1524
1568
|
|
|
1569
|
+
self.__running_simulation = True
|
|
1570
|
+
|
|
1525
1571
|
bulk_species_idx = self.epanet_api.getMSXSpeciesIndex(self.__sensor_config.bulk_species)
|
|
1526
1572
|
surface_species_idx = self.epanet_api.getMSXSpeciesIndex(
|
|
1527
1573
|
self.__sensor_config.surface_species)
|
|
@@ -1598,63 +1644,75 @@ class ScenarioSimulator():
|
|
|
1598
1644
|
|
|
1599
1645
|
if reporting_time_start == 0:
|
|
1600
1646
|
if return_as_dict is True:
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1647
|
+
data = {"bulk_species_node_concentration_raw": bulk_species_node_concentrations,
|
|
1648
|
+
"bulk_species_link_concentration_raw": bulk_species_link_concentrations,
|
|
1649
|
+
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1650
|
+
"sensor_readings_time": np.array([0])}
|
|
1605
1651
|
else:
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1652
|
+
data = ScadaData(sensor_config=self.__sensor_config,
|
|
1653
|
+
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
1654
|
+
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1655
|
+
surface_species_concentration_raw=surface_species_concentrations,
|
|
1656
|
+
sensor_readings_time=np.array([0]),
|
|
1657
|
+
sensor_reading_events=self.__sensor_reading_events,
|
|
1658
|
+
sensor_noise=self.__sensor_noise,
|
|
1659
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
1614
1660
|
|
|
1615
|
-
# Run step-by-step simulation
|
|
1616
|
-
tleft = 1
|
|
1617
|
-
while tleft > 0:
|
|
1618
1661
|
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
1619
1662
|
abort = yield
|
|
1620
|
-
if abort is
|
|
1621
|
-
|
|
1663
|
+
if abort is True:
|
|
1664
|
+
return None
|
|
1622
1665
|
|
|
1666
|
+
yield data
|
|
1667
|
+
|
|
1668
|
+
# Run step-by-step simulation
|
|
1669
|
+
tleft = 1
|
|
1670
|
+
total_time = 0
|
|
1671
|
+
while tleft > 0:
|
|
1623
1672
|
# Compute current time step
|
|
1624
1673
|
total_time, tleft = self.epanet_api.stepMSXQualityAnalysisTimeLeft()
|
|
1625
1674
|
|
|
1626
|
-
if verbose is True:
|
|
1627
|
-
try:
|
|
1628
|
-
next(progress_bar)
|
|
1629
|
-
except StopIteration:
|
|
1630
|
-
pass
|
|
1631
|
-
|
|
1632
1675
|
# Fetch data at regular time intervals
|
|
1633
1676
|
if total_time % hyd_time_step == 0:
|
|
1677
|
+
if verbose is True:
|
|
1678
|
+
try:
|
|
1679
|
+
next(progress_bar)
|
|
1680
|
+
except StopIteration:
|
|
1681
|
+
pass
|
|
1682
|
+
|
|
1634
1683
|
bulk_species_node_concentrations, bulk_species_link_concentrations, \
|
|
1635
1684
|
surface_species_concentrations = __get_concentrations()
|
|
1636
1685
|
|
|
1637
1686
|
# Report results in a regular time interval only!
|
|
1638
1687
|
if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
|
|
1639
1688
|
if return_as_dict is True:
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1689
|
+
data = {"bulk_species_node_concentration_raw":
|
|
1690
|
+
bulk_species_node_concentrations,
|
|
1691
|
+
"bulk_species_link_concentration_raw":
|
|
1692
|
+
bulk_species_link_concentrations,
|
|
1693
|
+
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1694
|
+
"sensor_readings_time": np.array([total_time])}
|
|
1646
1695
|
else:
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1696
|
+
data = ScadaData(sensor_config=self.__sensor_config,
|
|
1697
|
+
bulk_species_node_concentration_raw=
|
|
1698
|
+
bulk_species_node_concentrations,
|
|
1699
|
+
bulk_species_link_concentration_raw=
|
|
1700
|
+
bulk_species_link_concentrations,
|
|
1701
|
+
surface_species_concentration_raw=
|
|
1702
|
+
surface_species_concentrations,
|
|
1703
|
+
sensor_readings_time=np.array([total_time]),
|
|
1704
|
+
sensor_reading_events=self.__sensor_reading_events,
|
|
1705
|
+
sensor_noise=self.__sensor_noise,
|
|
1706
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
1707
|
+
|
|
1708
|
+
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
1709
|
+
abort = yield
|
|
1710
|
+
if abort is not False:
|
|
1711
|
+
break
|
|
1712
|
+
|
|
1713
|
+
yield data
|
|
1714
|
+
|
|
1715
|
+
self.__running_simulation = False
|
|
1658
1716
|
|
|
1659
1717
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1660
1718
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
@@ -1681,6 +1739,9 @@ class ScenarioSimulator():
|
|
|
1681
1739
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
1682
1740
|
Quality simulation results as SCADA data.
|
|
1683
1741
|
"""
|
|
1742
|
+
if self.__running_simulation is True:
|
|
1743
|
+
raise RuntimeError("A simulation is already running.")
|
|
1744
|
+
|
|
1684
1745
|
result = None
|
|
1685
1746
|
|
|
1686
1747
|
# Run simulation step-by-step
|
|
@@ -1741,6 +1802,9 @@ class ScenarioSimulator():
|
|
|
1741
1802
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
1742
1803
|
Generator with the current simulation results/states as SCADA data.
|
|
1743
1804
|
"""
|
|
1805
|
+
if self.__running_simulation is True:
|
|
1806
|
+
raise RuntimeError("A simulation is already running.")
|
|
1807
|
+
|
|
1744
1808
|
requested_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
1745
1809
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
1746
1810
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
@@ -1761,11 +1825,6 @@ class ScenarioSimulator():
|
|
|
1761
1825
|
tstep = 1
|
|
1762
1826
|
first_itr = True
|
|
1763
1827
|
while tstep > 0:
|
|
1764
|
-
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
1765
|
-
abort = yield
|
|
1766
|
-
if abort is not False:
|
|
1767
|
-
break
|
|
1768
|
-
|
|
1769
1828
|
if first_itr is True: # Fix current time in the first iteration
|
|
1770
1829
|
tstep = 0
|
|
1771
1830
|
first_itr = False
|
|
@@ -1788,27 +1847,36 @@ class ScenarioSimulator():
|
|
|
1788
1847
|
# Yield results in a regular time interval only!
|
|
1789
1848
|
if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
|
|
1790
1849
|
if return_as_dict is True:
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1850
|
+
data = {"node_quality_data_raw": quality_node_data,
|
|
1851
|
+
"link_quality_data_raw": quality_link_data,
|
|
1852
|
+
"sensor_readings_time": np.array([total_time])}
|
|
1794
1853
|
else:
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1854
|
+
data = ScadaData(sensor_config=self.__sensor_config,
|
|
1855
|
+
node_quality_data_raw=quality_node_data,
|
|
1856
|
+
link_quality_data_raw=quality_link_data,
|
|
1857
|
+
sensor_readings_time=np.array([total_time]),
|
|
1858
|
+
sensor_reading_events=self.__sensor_reading_events,
|
|
1859
|
+
sensor_noise=self.__sensor_noise,
|
|
1860
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
1861
|
+
|
|
1862
|
+
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
1863
|
+
abort = yield
|
|
1864
|
+
if abort is True:
|
|
1865
|
+
break
|
|
1866
|
+
|
|
1867
|
+
yield data
|
|
1802
1868
|
|
|
1803
1869
|
# Next
|
|
1804
1870
|
tstep = self.epanet_api.nextQualityAnalysisStep()
|
|
1805
1871
|
|
|
1806
1872
|
self.epanet_api.closeHydraulicAnalysis()
|
|
1807
1873
|
|
|
1808
|
-
def
|
|
1809
|
-
|
|
1874
|
+
def run_hydraulic_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
1875
|
+
frozen_sensor_config: bool = False) -> ScadaData:
|
|
1810
1876
|
"""
|
|
1811
|
-
Runs the simulation of this scenario.
|
|
1877
|
+
Runs the hydraulic simulation of this scenario (incl. basic quality if set).
|
|
1878
|
+
|
|
1879
|
+
Note that this function does not call EPANET-MSX even if an .msx file was provided.
|
|
1812
1880
|
|
|
1813
1881
|
Parameters
|
|
1814
1882
|
----------
|
|
@@ -1834,16 +1902,15 @@ class ScenarioSimulator():
|
|
|
1834
1902
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
1835
1903
|
Simulation results as SCADA data (i.e. sensor readings).
|
|
1836
1904
|
"""
|
|
1905
|
+
if self.__running_simulation is True:
|
|
1906
|
+
raise RuntimeError("A simulation is already running.")
|
|
1907
|
+
|
|
1837
1908
|
self.__adapt_to_network_changes()
|
|
1838
1909
|
|
|
1839
1910
|
result = None
|
|
1840
1911
|
|
|
1841
|
-
hyd_export_old = hyd_export
|
|
1842
|
-
if self.__f_msx_in is not None:
|
|
1843
|
-
hyd_export = os.path.join(get_temp_folder(), f"epytflow_MSX_{uuid.uuid4()}.hyd")
|
|
1844
|
-
|
|
1845
1912
|
# Run hydraulic simulation step-by-step
|
|
1846
|
-
gen = self.
|
|
1913
|
+
gen = self.run_hydraulic_simulation_as_generator
|
|
1847
1914
|
for scada_data in gen(hyd_export=hyd_export,
|
|
1848
1915
|
verbose=verbose,
|
|
1849
1916
|
return_as_dict=True,
|
|
@@ -1865,32 +1932,18 @@ class ScenarioSimulator():
|
|
|
1865
1932
|
sensor_noise=self.__sensor_noise,
|
|
1866
1933
|
frozen_sensor_config=frozen_sensor_config)
|
|
1867
1934
|
|
|
1868
|
-
# If necessary, run advanced quality simulation utilizing the computed hydraulics
|
|
1869
|
-
if self.f_msx_in is not None:
|
|
1870
|
-
gen = self.run_advanced_quality_simulation
|
|
1871
|
-
result_msx = gen(hyd_file_in=hyd_export,
|
|
1872
|
-
verbose=verbose,
|
|
1873
|
-
frozen_sensor_config=frozen_sensor_config)
|
|
1874
|
-
result.join(result_msx)
|
|
1875
|
-
|
|
1876
|
-
if hyd_export_old is not None:
|
|
1877
|
-
shutil.copyfile(hyd_export, hyd_export_old)
|
|
1878
|
-
|
|
1879
|
-
try:
|
|
1880
|
-
# temp solution
|
|
1881
|
-
os.remove(hyd_export)
|
|
1882
|
-
except:
|
|
1883
|
-
warnings.warn(f"Failed to remove temporary file '{hyd_export}'")
|
|
1884
|
-
|
|
1885
1935
|
return result
|
|
1886
1936
|
|
|
1887
|
-
def
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1937
|
+
def run_hydraulic_simulation_as_generator(self, hyd_export: str = None, verbose: bool = False,
|
|
1938
|
+
support_abort: bool = False,
|
|
1939
|
+
return_as_dict: bool = False,
|
|
1940
|
+
frozen_sensor_config: bool = False,
|
|
1941
|
+
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1892
1942
|
"""
|
|
1893
|
-
Runs the simulation of this scenario
|
|
1943
|
+
Runs the hydraulic simulation of this scenario (incl. basic quality if set) and
|
|
1944
|
+
provides the results as a generator.
|
|
1945
|
+
|
|
1946
|
+
Note that this function does not run EPANET-MSX, even if an .msx file was provided.
|
|
1894
1947
|
|
|
1895
1948
|
Parameters
|
|
1896
1949
|
----------
|
|
@@ -1928,10 +1981,15 @@ class ScenarioSimulator():
|
|
|
1928
1981
|
Generator with the current simulation results/states as SCADA data
|
|
1929
1982
|
(i.e. sensor readings).
|
|
1930
1983
|
"""
|
|
1984
|
+
if self.__running_simulation is True:
|
|
1985
|
+
raise RuntimeError("A simulation is already running.")
|
|
1986
|
+
|
|
1931
1987
|
self.__adapt_to_network_changes()
|
|
1932
1988
|
|
|
1933
1989
|
self.__prepare_simulation()
|
|
1934
1990
|
|
|
1991
|
+
self.__running_simulation = True
|
|
1992
|
+
|
|
1935
1993
|
self.epanet_api.openHydraulicAnalysis()
|
|
1936
1994
|
self.epanet_api.openQualityAnalysis()
|
|
1937
1995
|
self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
|
|
@@ -1958,11 +2016,11 @@ class ScenarioSimulator():
|
|
|
1958
2016
|
first_itr = False
|
|
1959
2017
|
|
|
1960
2018
|
if verbose is True:
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2019
|
+
if (total_time + tstep) % requested_time_step == 0:
|
|
2020
|
+
try:
|
|
2021
|
+
next(progress_bar)
|
|
2022
|
+
except StopIteration:
|
|
2023
|
+
pass
|
|
1966
2024
|
|
|
1967
2025
|
# Apply system events in a regular time interval only!
|
|
1968
2026
|
if (total_time + tstep) % requested_time_step == 0:
|
|
@@ -2025,7 +2083,7 @@ class ScenarioSimulator():
|
|
|
2025
2083
|
|
|
2026
2084
|
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
2027
2085
|
abort = yield
|
|
2028
|
-
if abort is
|
|
2086
|
+
if abort is True:
|
|
2029
2087
|
break
|
|
2030
2088
|
|
|
2031
2089
|
yield data
|
|
@@ -2041,11 +2099,77 @@ class ScenarioSimulator():
|
|
|
2041
2099
|
self.epanet_api.closeQualityAnalysis()
|
|
2042
2100
|
self.epanet_api.closeHydraulicAnalysis()
|
|
2043
2101
|
|
|
2102
|
+
self.__running_simulation = False
|
|
2103
|
+
|
|
2044
2104
|
if hyd_export is not None:
|
|
2045
2105
|
self.epanet_api.saveHydraulicFile(hyd_export)
|
|
2046
2106
|
except Exception as ex:
|
|
2107
|
+
self.__running_simulation = False
|
|
2047
2108
|
raise ex
|
|
2048
2109
|
|
|
2110
|
+
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2111
|
+
frozen_sensor_config: bool = False) -> ScadaData:
|
|
2112
|
+
"""
|
|
2113
|
+
Runs the simulation of this scenario.
|
|
2114
|
+
|
|
2115
|
+
Parameters
|
|
2116
|
+
----------
|
|
2117
|
+
hyd_export : `str`, optional
|
|
2118
|
+
Path to an EPANET .hyd file for storing the simulated hydraulics -- these hydraulics
|
|
2119
|
+
can be used later for an advanced quality analysis using EPANET-MSX.
|
|
2120
|
+
|
|
2121
|
+
If None, the simulated hydraulics will NOT be exported to an EPANET .hyd file.
|
|
2122
|
+
|
|
2123
|
+
The default is None.
|
|
2124
|
+
verbose : `bool`, optional
|
|
2125
|
+
If True, method will be verbose (e.g. showing a progress bar).
|
|
2126
|
+
|
|
2127
|
+
The default is False.
|
|
2128
|
+
frozen_sensor_config : `bool`, optional
|
|
2129
|
+
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
2130
|
+
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
2131
|
+
|
|
2132
|
+
The default is False.
|
|
2133
|
+
|
|
2134
|
+
Returns
|
|
2135
|
+
-------
|
|
2136
|
+
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
2137
|
+
Simulation results as SCADA data (i.e. sensor readings).
|
|
2138
|
+
"""
|
|
2139
|
+
if self.__running_simulation is True:
|
|
2140
|
+
raise RuntimeError("A simulation is already running.")
|
|
2141
|
+
|
|
2142
|
+
self.__adapt_to_network_changes()
|
|
2143
|
+
|
|
2144
|
+
result = None
|
|
2145
|
+
|
|
2146
|
+
hyd_export_old = hyd_export
|
|
2147
|
+
if self.__f_msx_in is not None:
|
|
2148
|
+
hyd_export = os.path.join(get_temp_folder(), f"epytflow_MSX_{uuid.uuid4()}.hyd")
|
|
2149
|
+
|
|
2150
|
+
# Run hydraulic simulation step-by-step
|
|
2151
|
+
result = self.run_hydraulic_simulation(hyd_export=hyd_export, verbose=verbose,
|
|
2152
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
2153
|
+
|
|
2154
|
+
# If necessary, run advanced quality simulation utilizing the computed hydraulics
|
|
2155
|
+
if self.f_msx_in is not None:
|
|
2156
|
+
gen = self.run_advanced_quality_simulation
|
|
2157
|
+
result_msx = gen(hyd_file_in=hyd_export,
|
|
2158
|
+
verbose=verbose,
|
|
2159
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
2160
|
+
result.join(result_msx)
|
|
2161
|
+
|
|
2162
|
+
if hyd_export_old is not None:
|
|
2163
|
+
shutil.copyfile(hyd_export, hyd_export_old)
|
|
2164
|
+
|
|
2165
|
+
try:
|
|
2166
|
+
# temp solution
|
|
2167
|
+
os.remove(hyd_export)
|
|
2168
|
+
except:
|
|
2169
|
+
warnings.warn(f"Failed to remove temporary file '{hyd_export}'")
|
|
2170
|
+
|
|
2171
|
+
return result
|
|
2172
|
+
|
|
2049
2173
|
def set_model_uncertainty(self, model_uncertainty: ModelUncertainty) -> None:
|
|
2050
2174
|
"""
|
|
2051
2175
|
Specifies the model uncertainties.
|
|
@@ -2055,6 +2179,9 @@ class ScenarioSimulator():
|
|
|
2055
2179
|
model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`
|
|
2056
2180
|
Model uncertainty specifications.
|
|
2057
2181
|
"""
|
|
2182
|
+
if self.__running_simulation is True:
|
|
2183
|
+
raise RuntimeError("Can not set uncertainties when simulation is running.")
|
|
2184
|
+
|
|
2058
2185
|
self.__adapt_to_network_changes()
|
|
2059
2186
|
|
|
2060
2187
|
if not isinstance(model_uncertainty, ModelUncertainty):
|
|
@@ -2073,6 +2200,9 @@ class ScenarioSimulator():
|
|
|
2073
2200
|
sensor_noise : :class:`~epyt_flow.uncertainties.sensor_noise.SensorNoise`
|
|
2074
2201
|
Sensor noise specification.
|
|
2075
2202
|
"""
|
|
2203
|
+
if self.__running_simulation is True:
|
|
2204
|
+
raise RuntimeError("Can not set sensor noise when simulation is running.")
|
|
2205
|
+
|
|
2076
2206
|
self.__adapt_to_network_changes()
|
|
2077
2207
|
|
|
2078
2208
|
if not isinstance(sensor_noise, SensorNoise):
|
|
@@ -2155,6 +2285,9 @@ class ScenarioSimulator():
|
|
|
2155
2285
|
|
|
2156
2286
|
The default is None.
|
|
2157
2287
|
"""
|
|
2288
|
+
if self.__running_simulation is True:
|
|
2289
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2290
|
+
|
|
2158
2291
|
self.__adapt_to_network_changes()
|
|
2159
2292
|
|
|
2160
2293
|
if flow_units_id is not None:
|
|
@@ -2277,6 +2410,9 @@ class ScenarioSimulator():
|
|
|
2277
2410
|
Sets water age analysis -- i.e. estimates the water age (in hours) at
|
|
2278
2411
|
all places in the network.
|
|
2279
2412
|
"""
|
|
2413
|
+
if self.__running_simulation is True:
|
|
2414
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2415
|
+
|
|
2280
2416
|
self.__adapt_to_network_changes()
|
|
2281
2417
|
|
|
2282
2418
|
self.__warn_if_quality_set()
|
|
@@ -2305,6 +2441,9 @@ class ScenarioSimulator():
|
|
|
2305
2441
|
|
|
2306
2442
|
The default is MASS_UNIT_MG.
|
|
2307
2443
|
"""
|
|
2444
|
+
if self.__running_simulation is True:
|
|
2445
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2446
|
+
|
|
2308
2447
|
self.__adapt_to_network_changes()
|
|
2309
2448
|
|
|
2310
2449
|
self.__warn_if_quality_set()
|
|
@@ -2349,6 +2488,9 @@ class ScenarioSimulator():
|
|
|
2349
2488
|
|
|
2350
2489
|
The default is 1.
|
|
2351
2490
|
"""
|
|
2491
|
+
if self.__running_simulation is True:
|
|
2492
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2493
|
+
|
|
2352
2494
|
self.__adapt_to_network_changes()
|
|
2353
2495
|
|
|
2354
2496
|
if self.epanet_api.getQualityInfo().QualityCode != ToolkitConstants.EN_CHEM:
|
|
@@ -2385,6 +2527,9 @@ class ScenarioSimulator():
|
|
|
2385
2527
|
trace_node_id : `str`
|
|
2386
2528
|
ID of the node traced in the source tracing analysis.
|
|
2387
2529
|
"""
|
|
2530
|
+
if self.__running_simulation is True:
|
|
2531
|
+
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2532
|
+
|
|
2388
2533
|
self.__adapt_to_network_changes()
|
|
2389
2534
|
|
|
2390
2535
|
if trace_node_id not in self.__sensor_config.nodes:
|
|
@@ -2393,3 +2538,65 @@ class ScenarioSimulator():
|
|
|
2393
2538
|
self.__warn_if_quality_set()
|
|
2394
2539
|
self.set_general_parameters(quality_model={"type": "TRACE",
|
|
2395
2540
|
"trace_node_id": trace_node_id})
|
|
2541
|
+
|
|
2542
|
+
def add_species_injection_source(self, species_id: str, node_id: str, pattern: np.ndarray,
|
|
2543
|
+
source_type: int, pattern_id: str = None,
|
|
2544
|
+
source_strength: int = 1.) -> None:
|
|
2545
|
+
"""
|
|
2546
|
+
Adds a new external (bulk or surface) species injection source at a particular node.
|
|
2547
|
+
|
|
2548
|
+
Parameters
|
|
2549
|
+
----------
|
|
2550
|
+
species_id : `str`
|
|
2551
|
+
ID of the (bulk or surface) species.
|
|
2552
|
+
node_id : `str`
|
|
2553
|
+
ID of the node at which this external (bulk or surface) species injection source
|
|
2554
|
+
is placed.
|
|
2555
|
+
pattern : `numpy.ndarray`
|
|
2556
|
+
1d source pattern.
|
|
2557
|
+
source_type : `int`,
|
|
2558
|
+
Type of the external (bulk or surface) species injection source -- must be one of
|
|
2559
|
+
the following EPANET toolkit constants:
|
|
2560
|
+
|
|
2561
|
+
- EN_CONCEN = 0
|
|
2562
|
+
- EN_MASS = 1
|
|
2563
|
+
- EN_SETPOINT = 2
|
|
2564
|
+
- EN_FLOWPACED = 3
|
|
2565
|
+
|
|
2566
|
+
Description:
|
|
2567
|
+
|
|
2568
|
+
- E_CONCEN Sets the concentration of external inflow entering a node
|
|
2569
|
+
- EN_MASS Injects a given mass/minute into a node
|
|
2570
|
+
- EN_SETPOINT Sets the concentration leaving a node to a given value
|
|
2571
|
+
- EN_FLOWPACED Adds a given value to the concentration leaving a node
|
|
2572
|
+
pattern_id : `str`, optional
|
|
2573
|
+
ID of the source pattern.
|
|
2574
|
+
|
|
2575
|
+
If None, a pattern_id will be generated automatically -- be aware that this
|
|
2576
|
+
could conflict with existing pattern IDs (in this case, an exception is raised).
|
|
2577
|
+
|
|
2578
|
+
The default is None.
|
|
2579
|
+
source_strength : `int`, optional
|
|
2580
|
+
Injection source strength -- i.e. injection = source_strength * pattern.
|
|
2581
|
+
|
|
2582
|
+
The default is 1.
|
|
2583
|
+
"""
|
|
2584
|
+
source_type_ = "None"
|
|
2585
|
+
if source_type == ToolkitConstants.EN_CONCEN:
|
|
2586
|
+
source_type_ = "CONCEN"
|
|
2587
|
+
elif source_type == ToolkitConstants.EN_MASS:
|
|
2588
|
+
source_type_ = "MASS"
|
|
2589
|
+
elif source_type == ToolkitConstants.EN_SETPOINT:
|
|
2590
|
+
source_type_ = "SETPOINT"
|
|
2591
|
+
elif source_type == ToolkitConstants.EN_FLOWPACED:
|
|
2592
|
+
source_type_ = "FLOWPACED"
|
|
2593
|
+
|
|
2594
|
+
if pattern_id is None:
|
|
2595
|
+
pattern_id = f"{species_id}_{node_id}"
|
|
2596
|
+
if pattern_id in self.epanet_api.getMSXPatternsNameID():
|
|
2597
|
+
raise ValueError("Invalid 'pattern_id' -- " +
|
|
2598
|
+
f"there already exists a pattern with ID '{pattern_id}'")
|
|
2599
|
+
|
|
2600
|
+
self.epanet_api.addMSXPattern(pattern_id, pattern)
|
|
2601
|
+
self.epanet_api.setMSXSources(node_id, species_id, source_type_, source_strength,
|
|
2602
|
+
pattern_id)
|