epyt-flow 0.7.3__py3-none-any.whl → 0.8.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 +2 -2
- epyt_flow/data/networks.py +14 -26
- epyt_flow/gym/scenario_control_env.py +129 -10
- epyt_flow/serialization.py +10 -3
- epyt_flow/simulation/events/__init__.py +1 -0
- epyt_flow/simulation/events/leakages.py +55 -12
- epyt_flow/simulation/events/quality_events.py +194 -0
- epyt_flow/simulation/events/system_event.py +5 -0
- epyt_flow/simulation/scada/scada_data.py +512 -64
- epyt_flow/simulation/scada/scada_data_export.py +7 -5
- epyt_flow/simulation/scenario_config.py +13 -2
- epyt_flow/simulation/scenario_simulator.py +275 -187
- epyt_flow/simulation/scenario_visualizer.py +1259 -13
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/METADATA +31 -30
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/RECORD +19 -18
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/WHEEL +1 -1
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/LICENSE +0 -0
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/top_level.txt +0 -0
epyt_flow/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8.1
|
|
@@ -491,9 +491,9 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
|
|
|
491
491
|
|
|
492
492
|
return dem_final
|
|
493
493
|
|
|
494
|
-
week_pattern_url = "https://
|
|
494
|
+
week_pattern_url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/CCWI-WDSA2018/" +\
|
|
495
495
|
"Dataset_Generator_Py3/weekPat_30min.mat"
|
|
496
|
-
year_offset_url = "https://
|
|
496
|
+
year_offset_url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/CCWI-WDSA2018/" +\
|
|
497
497
|
"Dataset_Generator_Py3/yearOffset_30min.mat"
|
|
498
498
|
|
|
499
499
|
download_if_necessary(os.path.join(download_dir, "weekPat_30min.mat"),
|
epyt_flow/data/networks.py
CHANGED
|
@@ -153,8 +153,7 @@ def load_net1(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
153
153
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
154
154
|
"""
|
|
155
155
|
f_in = os.path.join(download_dir, "Net1.inp")
|
|
156
|
-
url = "https://
|
|
157
|
-
"asce-tf-wdst/Net1.inp"
|
|
156
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Net1.inp"
|
|
158
157
|
|
|
159
158
|
download_if_necessary(f_in, url, verbose)
|
|
160
159
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -201,8 +200,7 @@ def load_net2(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
201
200
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
202
201
|
"""
|
|
203
202
|
f_in = os.path.join(download_dir, "Net2.inp")
|
|
204
|
-
url = "https://
|
|
205
|
-
"asce-tf-wdst/Net2.inp"
|
|
203
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Net2.inp"
|
|
206
204
|
|
|
207
205
|
download_if_necessary(f_in, url, verbose)
|
|
208
206
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -249,8 +247,7 @@ def load_net3(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
249
247
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
250
248
|
"""
|
|
251
249
|
f_in = os.path.join(download_dir, "Net3.inp")
|
|
252
|
-
url = "https://
|
|
253
|
-
"asce-tf-wdst/Net3.inp"
|
|
250
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Net3.inp"
|
|
254
251
|
|
|
255
252
|
download_if_necessary(f_in, url, verbose)
|
|
256
253
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -297,7 +294,7 @@ def load_net6(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
297
294
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
298
295
|
"""
|
|
299
296
|
f_in = os.path.join(download_dir, "Net6.inp")
|
|
300
|
-
url = "https://
|
|
297
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Net6.inp"
|
|
301
298
|
|
|
302
299
|
download_if_necessary(f_in, url, verbose)
|
|
303
300
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -344,8 +341,7 @@ def load_richmond(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
344
341
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
345
342
|
"""
|
|
346
343
|
f_in = os.path.join(download_dir, "Richmond_standard.inp")
|
|
347
|
-
url = "https://
|
|
348
|
-
"exeter-benchmarks/Richmond_standard.inp"
|
|
344
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Richmond_standard.inp"
|
|
349
345
|
|
|
350
346
|
download_if_necessary(f_in, url, verbose)
|
|
351
347
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -392,8 +388,7 @@ def load_micropolis(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
392
388
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
393
389
|
"""
|
|
394
390
|
f_in = os.path.join(download_dir, "MICROPOLIS_v1.inp")
|
|
395
|
-
url = "https://
|
|
396
|
-
"MICROPOLIS_v1.inp"
|
|
391
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/MICROPOLIS_v1.inp"
|
|
397
392
|
|
|
398
393
|
download_if_necessary(f_in, url, verbose)
|
|
399
394
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -440,8 +435,7 @@ def load_balerma(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
440
435
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
441
436
|
"""
|
|
442
437
|
f_in = os.path.join(download_dir, "Balerma.inp")
|
|
443
|
-
url = "https://
|
|
444
|
-
"asce-tf-wdst/Balerma.inp"
|
|
438
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Balerma.inp"
|
|
445
439
|
|
|
446
440
|
download_if_necessary(f_in, url, verbose)
|
|
447
441
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -488,8 +482,7 @@ def load_rural(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
488
482
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
489
483
|
"""
|
|
490
484
|
f_in = os.path.join(download_dir, "RuralNetwork.inp")
|
|
491
|
-
url = "https://
|
|
492
|
-
"asce-tf-wdst/RuralNetwork.inp"
|
|
485
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/RuralNetwork.inp"
|
|
493
486
|
|
|
494
487
|
download_if_necessary(f_in, url, verbose)
|
|
495
488
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -536,8 +529,7 @@ def load_bwsn1(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
536
529
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
537
530
|
"""
|
|
538
531
|
f_in = os.path.join(download_dir, "BWSN_Network_1.inp")
|
|
539
|
-
url = "https://
|
|
540
|
-
"asce-tf-wdst/BWSN_Network_1.inp"
|
|
532
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/BWSN_Network_1.inp"
|
|
541
533
|
|
|
542
534
|
download_if_necessary(f_in, url, verbose)
|
|
543
535
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -584,8 +576,7 @@ def load_bwsn2(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
584
576
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
585
577
|
"""
|
|
586
578
|
f_in = os.path.join(download_dir, "BWSN_Network_2.inp")
|
|
587
|
-
url = "https://
|
|
588
|
-
"asce-tf-wdst/BWSN_Network_2.inp"
|
|
579
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/BWSN_Network_2.inp"
|
|
589
580
|
|
|
590
581
|
download_if_necessary(f_in, url, verbose)
|
|
591
582
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -632,8 +623,7 @@ def load_anytown(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
632
623
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
633
624
|
"""
|
|
634
625
|
f_in = os.path.join(download_dir, "Anytown.inp")
|
|
635
|
-
url = "https://
|
|
636
|
-
"asce-tf-wdst/Anytown.inp"
|
|
626
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Anytown.inp"
|
|
637
627
|
|
|
638
628
|
download_if_necessary(f_in, url, verbose)
|
|
639
629
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -727,7 +717,7 @@ def load_ctown(download_dir: str = get_temp_folder(), verbose: bool = True,
|
|
|
727
717
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
728
718
|
"""
|
|
729
719
|
f_in = os.path.join(download_dir, "CTOWN.INP")
|
|
730
|
-
url = "https://
|
|
720
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/CTOWN.INP"
|
|
731
721
|
|
|
732
722
|
download_if_necessary(f_in, url, verbose)
|
|
733
723
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -783,8 +773,7 @@ def load_kentucky(wdn_id: int = 1, download_dir: str = get_temp_folder(),
|
|
|
783
773
|
raise ValueError(f"Unknown network 'ky{wdn_id}.inp'")
|
|
784
774
|
|
|
785
775
|
f_in = os.path.join(download_dir, f"ky{wdn_id}.inp")
|
|
786
|
-
url = "https://
|
|
787
|
-
f"asce-tf-wdst/ky{wdn_id}.inp"
|
|
776
|
+
url = f"https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/ky{wdn_id}.inp"
|
|
788
777
|
|
|
789
778
|
download_if_necessary(f_in, url, verbose)
|
|
790
779
|
return load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -836,8 +825,7 @@ def load_hanoi(download_dir: str = get_temp_folder(),
|
|
|
836
825
|
:class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`.
|
|
837
826
|
"""
|
|
838
827
|
f_in = os.path.join(download_dir, "Hanoi.inp")
|
|
839
|
-
url = "https://
|
|
840
|
-
"asce-tf-wdst/Hanoi.inp"
|
|
828
|
+
url = "https://filedn.com/lumBFq2P9S74PNoLPWtzxG4/EPyT-Flow/Networks/Hanoi.inp"
|
|
841
829
|
|
|
842
830
|
download_if_necessary(f_in, url, verbose)
|
|
843
831
|
config = load_inp(f_in, flow_units_id=flow_units_id)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module provides a base class for control environments.
|
|
3
3
|
"""
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
4
6
|
from abc import abstractmethod, ABC
|
|
5
7
|
from typing import Union
|
|
6
8
|
import warnings
|
|
7
9
|
import numpy as np
|
|
8
10
|
|
|
9
|
-
from ..simulation import ScenarioSimulator, ScenarioConfig, ScadaData
|
|
11
|
+
from ..simulation import ScenarioSimulator, ScenarioConfig, ScadaData, ToolkitConstants
|
|
12
|
+
from ..utils import get_temp_folder
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class ScenarioControlEnv(ABC):
|
|
@@ -21,12 +24,32 @@ class ScenarioControlEnv(ABC):
|
|
|
21
24
|
If True, environment is automatically reset if terminated.
|
|
22
25
|
|
|
23
26
|
The default is False.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
_scenario_sim : :class:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator`, protected
|
|
31
|
+
Scenario simulator of the control scenario.
|
|
32
|
+
_scenario_config : :class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
|
|
33
|
+
Scenario configuration.
|
|
34
|
+
_sim_generator : Generator[Union[:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`, dict], bool, None], protected
|
|
35
|
+
Generator for running the step-wise simulation.
|
|
36
|
+
_hydraulic_scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`, protected
|
|
37
|
+
SCADA data from the hydraulic simulation -- only used if EPANET-MSX is used in the control scenario.
|
|
24
38
|
"""
|
|
25
39
|
def __init__(self, scenario_config: ScenarioConfig, autoreset: bool = False, **kwds):
|
|
26
|
-
|
|
40
|
+
if not isinstance(scenario_config, ScenarioConfig):
|
|
41
|
+
raise TypeError("'scenario_config' must be an instance of " +
|
|
42
|
+
"'epyt_flow.simulation.ScenarioConfig' " +
|
|
43
|
+
"but not of '{type(scenario_config)}'")
|
|
44
|
+
if not isinstance(autoreset, bool):
|
|
45
|
+
raise TypeError("'autoreset' must be an instance of 'bool' " +
|
|
46
|
+
f"but not of '{type(autoreset)}'")
|
|
47
|
+
|
|
48
|
+
self._scenario_config = scenario_config
|
|
27
49
|
self._scenario_sim = None
|
|
28
50
|
self._sim_generator = None
|
|
29
51
|
self.__autoreset = autoreset
|
|
52
|
+
self._hydraulic_scada_data = None
|
|
30
53
|
|
|
31
54
|
super().__init__(**kwds)
|
|
32
55
|
|
|
@@ -54,15 +77,27 @@ class ScenarioControlEnv(ABC):
|
|
|
54
77
|
"""
|
|
55
78
|
try:
|
|
56
79
|
if self._sim_generator is not None:
|
|
57
|
-
self._sim_generator.send(True)
|
|
58
80
|
next(self._sim_generator)
|
|
81
|
+
self._sim_generator.send(True)
|
|
59
82
|
except StopIteration:
|
|
60
83
|
pass
|
|
61
84
|
|
|
62
85
|
if self._scenario_sim is not None:
|
|
63
86
|
self._scenario_sim.close()
|
|
64
87
|
|
|
65
|
-
def
|
|
88
|
+
def contains_events(self) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Check if the scenario contains any events.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
`bool`
|
|
95
|
+
True is the scenario contains any events, False otherwise.
|
|
96
|
+
"""
|
|
97
|
+
return len(self._scenario_config.system_events) != 0 or \
|
|
98
|
+
len(self._scenario_config.sensor_reading_events) != 0
|
|
99
|
+
|
|
100
|
+
def reset(self) -> Union[tuple[ScadaData, bool], ScadaData]:
|
|
66
101
|
"""
|
|
67
102
|
Resets the environment (i.e. simulation).
|
|
68
103
|
|
|
@@ -75,20 +110,38 @@ class ScenarioControlEnv(ABC):
|
|
|
75
110
|
self._scenario_sim.close()
|
|
76
111
|
|
|
77
112
|
self._scenario_sim = ScenarioSimulator(
|
|
78
|
-
scenario_config=self.
|
|
79
|
-
|
|
113
|
+
scenario_config=self._scenario_config)
|
|
114
|
+
|
|
115
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
116
|
+
# Run hydraulic simulation first
|
|
117
|
+
hyd_export = os.path.join(get_temp_folder(), f"epytflow_env_MSX_{uuid.uuid4()}.hyd")
|
|
118
|
+
sim = self._scenario_sim.run_hydraulic_simulation
|
|
119
|
+
self._hydraulic_scada_data = sim(hyd_export=hyd_export)
|
|
120
|
+
|
|
121
|
+
# Run advanced quality analysis (EPANET-MSX) on top of the computed hydraulics
|
|
122
|
+
gen = self._scenario_sim.run_advanced_quality_simulation_as_generator
|
|
123
|
+
self._sim_generator = gen(hyd_export, support_abort=True)
|
|
124
|
+
else:
|
|
125
|
+
gen = self._scenario_sim.run_hydraulic_simulation_as_generator
|
|
126
|
+
self._sim_generator = gen(support_abort=True)
|
|
80
127
|
|
|
81
128
|
return self._next_sim_itr()
|
|
82
129
|
|
|
83
|
-
def _next_sim_itr(self) -> ScadaData:
|
|
130
|
+
def _next_sim_itr(self) -> Union[tuple[ScadaData, bool], ScadaData]:
|
|
84
131
|
try:
|
|
85
132
|
next(self._sim_generator)
|
|
86
|
-
|
|
133
|
+
scada_data = self._sim_generator.send(False)
|
|
134
|
+
|
|
135
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
136
|
+
cur_time = int(scada_data.sensor_readings_time[0])
|
|
137
|
+
cur_hyd_scada_data = self._hydraulic_scada_data.\
|
|
138
|
+
extract_time_window(cur_time, cur_time)
|
|
139
|
+
scada_data.join(cur_hyd_scada_data)
|
|
87
140
|
|
|
88
141
|
if self.autoreset is True:
|
|
89
|
-
return
|
|
142
|
+
return scada_data
|
|
90
143
|
else:
|
|
91
|
-
return
|
|
144
|
+
return scada_data, False
|
|
92
145
|
except StopIteration:
|
|
93
146
|
if self.__autoreset is True:
|
|
94
147
|
return self.reset()
|
|
@@ -112,6 +165,10 @@ class ScenarioControlEnv(ABC):
|
|
|
112
165
|
- EN_CLOSED = 0
|
|
113
166
|
- EN_OPEN = 1
|
|
114
167
|
"""
|
|
168
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
169
|
+
raise RuntimeError("Can not execute actions affecting the hydraulics "+
|
|
170
|
+
"when running EPANET-MSX")
|
|
171
|
+
|
|
115
172
|
pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
|
|
116
173
|
pump_link_idx = self._scenario_sim.epanet_api.getLinkPumpIndex(pump_idx + 1)
|
|
117
174
|
self._scenario_sim.epanet_api.setLinkStatus(pump_link_idx, status)
|
|
@@ -127,6 +184,10 @@ class ScenarioControlEnv(ABC):
|
|
|
127
184
|
speed : `float`
|
|
128
185
|
New pump speed.
|
|
129
186
|
"""
|
|
187
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
188
|
+
raise RuntimeError("Can not execute actions affecting the hydraulics "+
|
|
189
|
+
"when running EPANET-MSX")
|
|
190
|
+
|
|
130
191
|
pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
|
|
131
192
|
pattern_idx = self._scenario_sim.epanet_api.getLinkPumpPatternIndex(pump_idx + 1)
|
|
132
193
|
|
|
@@ -154,6 +215,10 @@ class ScenarioControlEnv(ABC):
|
|
|
154
215
|
- EN_CLOSED = 0
|
|
155
216
|
- EN_OPEN = 1
|
|
156
217
|
"""
|
|
218
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
219
|
+
raise RuntimeError("Can not execute actions affecting the hydraulics "+
|
|
220
|
+
"when running EPANET-MSX")
|
|
221
|
+
|
|
157
222
|
valve_idx = self._scenario_sim.epanet_api.getLinkValveNameID().index(valve_id)
|
|
158
223
|
valve_link_idx = self._scenario_sim.epanet_api.getLinkValveIndex()[valve_idx]
|
|
159
224
|
self._scenario_sim.epanet_api.setLinkStatus(valve_link_idx, status)
|
|
@@ -173,11 +238,65 @@ class ScenarioControlEnv(ABC):
|
|
|
173
238
|
qual_value : `float`
|
|
174
239
|
New quality source value.
|
|
175
240
|
"""
|
|
241
|
+
if self._scenario_sim.f_msx_in is not None:
|
|
242
|
+
raise RuntimeError("Can not execute actions affecting the hydraulics "+
|
|
243
|
+
"when running EPANET-MSX")
|
|
244
|
+
|
|
176
245
|
node_idx = self._scenario_sim.epanet_api.getNodeIndex(node_id)
|
|
177
246
|
pattern_idx = self._scenario_sim.epanet_api.getPatternIndex(pattern_id)
|
|
178
247
|
self._scenario_sim.epanet_api.setNodeSourceQuality(node_idx, 1)
|
|
179
248
|
self._scenario_sim.epanet_api.setPattern(pattern_idx, np.array([qual_value]))
|
|
180
249
|
|
|
250
|
+
def set_node_species_source_value(self, species_id: str, node_id: str, source_type: int,
|
|
251
|
+
pattern_id: str, source_strength: float) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Sets the species source at a particular node to a specific value -- i.e.
|
|
254
|
+
setting the species injection amount at a particular location.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
species_id : `str`
|
|
259
|
+
ID of the species.
|
|
260
|
+
node_id : `str`
|
|
261
|
+
ID of the node.
|
|
262
|
+
source_type : `int`
|
|
263
|
+
Type of the external species injection source -- must be one of
|
|
264
|
+
the following EPANET toolkit constants:
|
|
265
|
+
|
|
266
|
+
- EN_CONCEN = 0
|
|
267
|
+
- EN_MASS = 1
|
|
268
|
+
- EN_SETPOINT = 2
|
|
269
|
+
- EN_FLOWPACED = 3
|
|
270
|
+
|
|
271
|
+
Description:
|
|
272
|
+
|
|
273
|
+
- E_CONCEN Sets the concentration of external inflow entering a node
|
|
274
|
+
- EN_MASS Injects a given mass/minute into a node
|
|
275
|
+
- EN_SETPOINT Sets the concentration leaving a node to a given value
|
|
276
|
+
- EN_FLOWPACED Adds a given value to the concentration leaving a node
|
|
277
|
+
pattern_id : `str`
|
|
278
|
+
ID of the source pattern.
|
|
279
|
+
source_strength : `float`
|
|
280
|
+
Amount of the injected species (source strength) --
|
|
281
|
+
i.e. interpreation of this number depends on `source_type`
|
|
282
|
+
"""
|
|
283
|
+
if self._scenario_sim.f_msx_in is None:
|
|
284
|
+
raise RuntimeError("You are not running EPANET-MSX")
|
|
285
|
+
|
|
286
|
+
source_type_ = "None"
|
|
287
|
+
if source_type == ToolkitConstants.EN_CONCEN:
|
|
288
|
+
source_type_ = "CONCEN"
|
|
289
|
+
elif source_type == ToolkitConstants.EN_MASS:
|
|
290
|
+
source_type_ = "MASS"
|
|
291
|
+
elif source_type == ToolkitConstants.EN_SETPOINT:
|
|
292
|
+
source_type_ = "SETPOINT"
|
|
293
|
+
elif source_type == ToolkitConstants.EN_FLOWPACED:
|
|
294
|
+
source_type_ = "FLOWPACED"
|
|
295
|
+
|
|
296
|
+
self._scenario_sim.epanet_api.setMSXPattern(pattern_id, [1])
|
|
297
|
+
self._scenario_sim.epanet_api.setMSXSources(node_id, species_id, source_type_,
|
|
298
|
+
source_strength, pattern_id)
|
|
299
|
+
|
|
181
300
|
@abstractmethod
|
|
182
301
|
def step(self, *actions) -> Union[tuple[ScadaData, float, bool], tuple[ScadaData, float]]:
|
|
183
302
|
"""
|
epyt_flow/serialization.py
CHANGED
|
@@ -47,6 +47,7 @@ NETWORK_TOPOLOGY_ID = 26
|
|
|
47
47
|
PUMP_STATE_EVENT_ID = 28
|
|
48
48
|
PUMP_SPEED_EVENT_ID = 29
|
|
49
49
|
VALVE_STATE_EVENT_ID = 30
|
|
50
|
+
SPECIESINJECTION_EVENT_ID = 31
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def my_packb(data: Any) -> bytes:
|
|
@@ -452,9 +453,15 @@ def save_to_file(f_out: str, data: Any, use_compression: bool = True) -> None:
|
|
|
452
453
|
def __encode_bsr_array(array: scipy.sparse.bsr_array
|
|
453
454
|
) -> tuple[tuple[int, int], tuple[list[float], tuple[list[int], list[int]]]]:
|
|
454
455
|
shape = array.shape
|
|
455
|
-
data =
|
|
456
|
-
rows =
|
|
457
|
-
cols =
|
|
456
|
+
data = []
|
|
457
|
+
rows = []
|
|
458
|
+
cols = []
|
|
459
|
+
|
|
460
|
+
array_ = array.tocsr() # Bug workaround: BSR arrays do not implement __getitem__
|
|
461
|
+
for i, j in zip(*array_.nonzero()):
|
|
462
|
+
rows.append(int(i))
|
|
463
|
+
cols.append(int(j))
|
|
464
|
+
data.append(float(array_[i, j]))
|
|
458
465
|
|
|
459
466
|
return shape, (data, (rows, cols))
|
|
460
467
|
|
|
@@ -60,8 +60,8 @@ class Leakage(SystemEvent, JsonSerializable):
|
|
|
60
60
|
if area is None and diameter is None:
|
|
61
61
|
raise ValueError("Either 'diameter' or 'area' must be given")
|
|
62
62
|
if area is not None and diameter is not None:
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
raise ValueError("Either 'diameter' or 'area' must be given, " +
|
|
64
|
+
"but not both at the same time")
|
|
65
65
|
if diameter is not None:
|
|
66
66
|
if not isinstance(diameter, float):
|
|
67
67
|
raise TypeError("'diameter must be an instance of 'float' but " +
|
|
@@ -92,7 +92,7 @@ class Leakage(SystemEvent, JsonSerializable):
|
|
|
92
92
|
self.__area = area
|
|
93
93
|
self.__profile = profile
|
|
94
94
|
|
|
95
|
-
self.
|
|
95
|
+
self.__leaky_node_idx = None
|
|
96
96
|
self.__leak_emitter_coef = None
|
|
97
97
|
self.__time_pattern_idx = 0
|
|
98
98
|
|
|
@@ -174,7 +174,7 @@ class Leakage(SystemEvent, JsonSerializable):
|
|
|
174
174
|
def get_attributes(self) -> dict:
|
|
175
175
|
return super().get_attributes() | {"link_id": self.__link_id, "diameter": self.__diameter,
|
|
176
176
|
"area": self.__area, "profile": self.__profile,
|
|
177
|
-
"node_id": self.
|
|
177
|
+
"node_id": self.__node_id
|
|
178
178
|
if self.__link_id is None else None}
|
|
179
179
|
|
|
180
180
|
def __eq__(self, other) -> bool:
|
|
@@ -238,10 +238,16 @@ class Leakage(SystemEvent, JsonSerializable):
|
|
|
238
238
|
g = 32.17405 # feet/s^2
|
|
239
239
|
else:
|
|
240
240
|
raise ValueError("Leakages are only implemented for the following flow units:\n" +
|
|
241
|
-
" EN_CMH (cubic
|
|
241
|
+
" EN_CMH (cubic meter/hr)\n EN_CFS (foot/sec)")
|
|
242
242
|
|
|
243
243
|
return discharge_coef * area * np.sqrt(2. * g)
|
|
244
244
|
|
|
245
|
+
def _get_new_link_id(self) -> str:
|
|
246
|
+
return f"leak_pipe_{self.__link_id}"
|
|
247
|
+
|
|
248
|
+
def _get_new_node_id(self) -> str:
|
|
249
|
+
return f"leak_node_{self.__link_id}"
|
|
250
|
+
|
|
245
251
|
def init(self, epanet_api: epyt.epanet) -> None:
|
|
246
252
|
super().init(epanet_api)
|
|
247
253
|
|
|
@@ -250,35 +256,72 @@ class Leakage(SystemEvent, JsonSerializable):
|
|
|
250
256
|
if self.__link_id not in self._epanet_api.getLinkNameID():
|
|
251
257
|
raise ValueError(f"Unknown link/pipe '{self.__link_id}'")
|
|
252
258
|
|
|
253
|
-
new_link_id =
|
|
254
|
-
new_node_id =
|
|
259
|
+
new_link_id = self._get_new_link_id()
|
|
260
|
+
new_node_id = self._get_new_node_id()
|
|
255
261
|
|
|
256
262
|
all_nodes_id = self._epanet_api.getNodeNameID()
|
|
257
263
|
if new_node_id in all_nodes_id:
|
|
258
264
|
raise ValueError(f"There is already a leak at pipe {self.link_id}")
|
|
259
265
|
|
|
260
266
|
self._epanet_api.splitPipe(self.link_id, new_link_id, new_node_id)
|
|
261
|
-
self.
|
|
267
|
+
self.__leaky_node_idx = self._epanet_api.getNodeIndex(new_node_id)
|
|
262
268
|
else:
|
|
263
269
|
if self.__node_id not in self._epanet_api.getNodeNameID():
|
|
264
270
|
raise ValueError(f"Unknown node '{self.__node_id}'")
|
|
265
271
|
|
|
266
|
-
self.
|
|
272
|
+
self.__leaky_node_idx = self._epanet_api.getNodeIndex(self.__node_id)
|
|
267
273
|
|
|
268
|
-
self._epanet_api.setNodeEmitterCoeff(self.
|
|
274
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_idx, 0.)
|
|
269
275
|
|
|
270
276
|
# Compute leak emitter coefficient
|
|
271
277
|
self.__leak_emitter_coef = self.compute_leak_emitter_coefficient(
|
|
272
278
|
self.compute_leak_area(self.area))
|
|
273
279
|
|
|
280
|
+
def cleanup(self) -> None:
|
|
281
|
+
if self.__link_id is not None:
|
|
282
|
+
pipe_idx = self._epanet_api.getLinkIndex(self.__link_id)
|
|
283
|
+
link_prop = self._epanet_api.getLinksInfo()
|
|
284
|
+
link_diameter = link_prop.LinkDiameter[pipe_idx - 1]
|
|
285
|
+
link_length = link_prop.LinkLength[pipe_idx - 1] * 2.
|
|
286
|
+
link_roughness_coeff = link_prop.LinkRoughnessCoeff[pipe_idx - 1]
|
|
287
|
+
link_minor_loss_coeff = link_prop.LinkMinorLossCoeff[pipe_idx - 1]
|
|
288
|
+
link_initial_status = link_prop.LinkInitialStatus[pipe_idx - 1]
|
|
289
|
+
link_initial_setting = link_prop.LinkInitialSetting[pipe_idx - 1]
|
|
290
|
+
link_bulk_reaction_coeff = link_prop.LinkBulkReactionCoeff[pipe_idx - 1]
|
|
291
|
+
link_wall_reaction_coeff = link_prop.LinkWallReactionCoeff[pipe_idx - 1]
|
|
292
|
+
|
|
293
|
+
node_a_idx = int(self._epanet_api.getLinkNodesIndex(pipe_idx)[0])
|
|
294
|
+
node_b_idx = int(self._epanet_api.getLinkNodesIndex(self._get_new_link_id())[1])
|
|
295
|
+
|
|
296
|
+
self._epanet_api.deleteLink(self._get_new_link_id())
|
|
297
|
+
self._epanet_api.deleteLink(self.__link_id)
|
|
298
|
+
self._epanet_api.deleteNode(self._get_new_node_id())
|
|
299
|
+
|
|
300
|
+
self._epanet_api.addLinkPipe(self.__link_id,
|
|
301
|
+
self._epanet_api.getNodeNameID(node_a_idx),
|
|
302
|
+
self._epanet_api.getNodeNameID(node_b_idx))
|
|
303
|
+
link_idx = self._epanet_api.getLinkIndex(self.__link_id)
|
|
304
|
+
self._epanet_api.setLinkNodesIndex(link_idx,
|
|
305
|
+
node_a_idx, node_b_idx)
|
|
306
|
+
self._epanet_api.setLinkPipeData(link_idx, link_length, link_diameter,
|
|
307
|
+
link_roughness_coeff,
|
|
308
|
+
link_minor_loss_coeff)
|
|
309
|
+
if link_minor_loss_coeff != 0:
|
|
310
|
+
self._epanet_api.setLinklinkMinorLossCoeff(link_idx, link_minor_loss_coeff)
|
|
311
|
+
self._epanet_api.setLinkInitialStatus(link_idx, link_initial_status)
|
|
312
|
+
self._epanet_api.setLinkInitialSetting(link_idx, link_initial_setting)
|
|
313
|
+
self._epanet_api.setLinkBulkReactionCoeff(link_idx, link_bulk_reaction_coeff)
|
|
314
|
+
self._epanet_api.setLinkWallReactionCoeff(link_idx, link_wall_reaction_coeff)
|
|
315
|
+
self._epanet_api.setLinkTypePipe(link_idx)
|
|
316
|
+
|
|
274
317
|
def reset(self) -> None:
|
|
275
318
|
self.__time_pattern_idx = 0
|
|
276
319
|
|
|
277
320
|
def exit(self, cur_time) -> None:
|
|
278
|
-
self._epanet_api.setNodeEmitterCoeff(self.
|
|
321
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_idx, 0.)
|
|
279
322
|
|
|
280
323
|
def apply(self, cur_time: int) -> None:
|
|
281
|
-
self._epanet_api.setNodeEmitterCoeff(self.
|
|
324
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_idx,
|
|
282
325
|
self.__leak_emitter_coef *
|
|
283
326
|
self.__profile[self.__time_pattern_idx])
|
|
284
327
|
self.__time_pattern_idx += 1
|