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 CHANGED
@@ -1 +1 @@
1
- 0.7.3
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://github.com/KIOS-Research/LeakDB/raw/master/CCWI-WDSA2018/" +\
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://github.com/KIOS-Research/LeakDB/raw/master/CCWI-WDSA2018/" +\
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"),
@@ -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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://github.com/OpenWaterAnalytics/WNTR/raw/main/examples/networks/Net6.inp"
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://github.com/OpenWaterAnalytics/EPyT/raw/main/epyt/networks/asce-tf-wdst/" + \
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://github.com/OpenWaterAnalytics/EPyT/raw/main/epyt/networks/" + \
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://github.com/OpenWaterAnalytics/EPyT/raw/main/epyt/networks/" + \
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://github.com/OpenWaterAnalytics/EPyT/raw/main/epyt/networks/" + \
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://github.com/OpenWaterAnalytics/EPyT/raw/main/epyt/networks/" + \
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://github.com/scy-phy/www.batadal.net/raw/master/data/CTOWN.INP"
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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://raw.githubusercontent.com/OpenWaterAnalytics/EPyT/main/epyt/networks/" + \
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
- self.__scenario_config = scenario_config
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 reset(self) -> ScadaData:
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.__scenario_config)
79
- self._sim_generator = self._scenario_sim.run_simulation_as_generator(support_abort=True)
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
- r = self._sim_generator.send(False)
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 r
142
+ return scada_data
90
143
  else:
91
- return r, False
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
  """
@@ -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 = array.data.flatten().tolist()
456
- rows = array.nonzero()[0].tolist()
457
- cols = array.nonzero()[1].tolist()
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
 
@@ -1,5 +1,6 @@
1
1
  from .system_event import *
2
2
  from .leakages import *
3
+ from .quality_events import *
3
4
  from .sensor_reading_event import *
4
5
  from .sensor_faults import *
5
6
  from .sensor_reading_attack import *
@@ -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
- raise ValueError("Either 'diameter' or 'area' must be given, " +
64
- "but not both at the same time")
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.__leaky_node_id = None
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.__leaky_node_id
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 foot/sec)\n EN_CFS (cubic meter/hr)")
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 = f"leak_pipe_{self.link_id}"
254
- new_node_id = f"leak_node_{self.link_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.__leaky_node_id = self._epanet_api.getNodeIndex(new_node_id)
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.__leaky_node_id = self._epanet_api.getNodeIndex(self.__node_id)
272
+ self.__leaky_node_idx = self._epanet_api.getNodeIndex(self.__node_id)
267
273
 
268
- self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_id, 0.)
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.__leaky_node_id, 0.)
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.__leaky_node_id,
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