epyt-flow 0.1.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.
- epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
- epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
- epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
- epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
- epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
- epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
- epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
- epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
- epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
- epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
- epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
- epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
- epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
- epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
- epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
- epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
- epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
- epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
- epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
- epyt_flow/EPANET/compile.sh +4 -0
- epyt_flow/VERSION +1 -0
- epyt_flow/__init__.py +24 -0
- epyt_flow/data/__init__.py +0 -0
- epyt_flow/data/benchmarks/__init__.py +11 -0
- epyt_flow/data/benchmarks/batadal.py +257 -0
- epyt_flow/data/benchmarks/batadal_data.py +28 -0
- epyt_flow/data/benchmarks/battledim.py +473 -0
- epyt_flow/data/benchmarks/battledim_data.py +51 -0
- epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
- epyt_flow/data/benchmarks/leakdb.py +592 -0
- epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
- epyt_flow/data/benchmarks/water_usage.py +123 -0
- epyt_flow/data/networks.py +650 -0
- epyt_flow/gym/__init__.py +4 -0
- epyt_flow/gym/control_gyms.py +47 -0
- epyt_flow/gym/scenario_control_env.py +101 -0
- epyt_flow/metrics.py +404 -0
- epyt_flow/models/__init__.py +2 -0
- epyt_flow/models/event_detector.py +31 -0
- epyt_flow/models/sensor_interpolation_detector.py +118 -0
- epyt_flow/rest_api/__init__.py +4 -0
- epyt_flow/rest_api/base_handler.py +70 -0
- epyt_flow/rest_api/res_manager.py +95 -0
- epyt_flow/rest_api/scada_data_handler.py +476 -0
- epyt_flow/rest_api/scenario_handler.py +352 -0
- epyt_flow/rest_api/server.py +106 -0
- epyt_flow/serialization.py +438 -0
- epyt_flow/simulation/__init__.py +5 -0
- epyt_flow/simulation/events/__init__.py +6 -0
- epyt_flow/simulation/events/actuator_events.py +259 -0
- epyt_flow/simulation/events/event.py +81 -0
- epyt_flow/simulation/events/leakages.py +404 -0
- epyt_flow/simulation/events/sensor_faults.py +267 -0
- epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
- epyt_flow/simulation/events/sensor_reading_event.py +170 -0
- epyt_flow/simulation/events/system_event.py +88 -0
- epyt_flow/simulation/parallel_simulation.py +147 -0
- epyt_flow/simulation/scada/__init__.py +3 -0
- epyt_flow/simulation/scada/advanced_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +1589 -0
- epyt_flow/simulation/scada/scada_data_export.py +255 -0
- epyt_flow/simulation/scenario_config.py +608 -0
- epyt_flow/simulation/scenario_simulator.py +1897 -0
- epyt_flow/simulation/scenario_visualizer.py +61 -0
- epyt_flow/simulation/sensor_config.py +1289 -0
- epyt_flow/topology.py +290 -0
- epyt_flow/uncertainty/__init__.py +3 -0
- epyt_flow/uncertainty/model_uncertainty.py +302 -0
- epyt_flow/uncertainty/sensor_noise.py +73 -0
- epyt_flow/uncertainty/uncertainties.py +555 -0
- epyt_flow/uncertainty/utils.py +206 -0
- epyt_flow/utils.py +306 -0
- epyt_flow-0.1.0.dist-info/LICENSE +21 -0
- epyt_flow-0.1.0.dist-info/METADATA +139 -0
- epyt_flow-0.1.0.dist-info/RECORD +131 -0
- epyt_flow-0.1.0.dist-info/WHEEL +5 -0
- epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a base class for events.
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Event(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Base class for an event.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
start_time : `int`
|
|
15
|
+
Starting time (seconds since the simulation start) of this event.
|
|
16
|
+
end_time : `int`, optional
|
|
17
|
+
Time (seconds since the simulation start) when this event ends -- None if it never ends.
|
|
18
|
+
|
|
19
|
+
The default is None.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, start_time: int, end_time: int = None, **kwds):
|
|
22
|
+
if not isinstance(start_time, int) or start_time < 0:
|
|
23
|
+
raise ValueError("'start_time' must be a positive integer specifying the time " +
|
|
24
|
+
"at which this event starts.")
|
|
25
|
+
if end_time is not None and not isinstance(end_time, int):
|
|
26
|
+
raise ValueError("'end_time' must be either None or a positive integer specifiying " +
|
|
27
|
+
"the time at which this event ends.")
|
|
28
|
+
if end_time is not None:
|
|
29
|
+
if start_time >= end_time:
|
|
30
|
+
raise ValueError("'start_time' must be smaller than 'end_time'")
|
|
31
|
+
|
|
32
|
+
self.__start_time = start_time
|
|
33
|
+
self.__end_time = end_time if end_time is not None else math.inf
|
|
34
|
+
|
|
35
|
+
super().__init__(**kwds)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def start_time(self) -> int:
|
|
39
|
+
"""
|
|
40
|
+
Gets the start time (seconds since the simulation start) of this event.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
`int`
|
|
45
|
+
Start time of this event.
|
|
46
|
+
"""
|
|
47
|
+
return self.__start_time
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def end_time(self) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Gets the end time (seconds since the simulation start) of this event.
|
|
53
|
+
float("inf") if it never ends.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
`int`
|
|
58
|
+
End time of this event.
|
|
59
|
+
"""
|
|
60
|
+
return self.__end_time
|
|
61
|
+
|
|
62
|
+
def get_attributes(self) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
Gets all attributes to be serialized -- these attributes are passed to the
|
|
65
|
+
constructor when the object is deserialized.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
`dict`
|
|
70
|
+
Dictionary of attributes -- i.e. pairs of attribute name and value.
|
|
71
|
+
"""
|
|
72
|
+
return {"start_time": self.__start_time, "end_time": self.__end_time}
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
return f"start_time: {self.__start_time} end_time: {self.__end_time}"
|
|
76
|
+
|
|
77
|
+
def __eq__(self, other) -> bool:
|
|
78
|
+
if not isinstance(other, Event):
|
|
79
|
+
raise TypeError(f"Can not compare 'Event' instance with '{type(other)}' instance")
|
|
80
|
+
|
|
81
|
+
return self.__start_time == other.start_time and self.__end_time == other.end_time
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides classes for implementing leakages.
|
|
3
|
+
"""
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
import math
|
|
6
|
+
import numpy as np
|
|
7
|
+
import epyt
|
|
8
|
+
|
|
9
|
+
from .system_event import SystemEvent
|
|
10
|
+
from ...serialization import serializable, JsonSerializable, \
|
|
11
|
+
LEAKAGE_ID, ABRUPT_LEAKAGE_ID, INCIPIENT_LEAKAGE_ID
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@serializable(LEAKAGE_ID, ".epytflow_leakage")
|
|
15
|
+
class Leakage(SystemEvent, JsonSerializable):
|
|
16
|
+
"""
|
|
17
|
+
Base class for a leakage.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
link_id : `str`
|
|
22
|
+
ID of the link at which the leak is placed.
|
|
23
|
+
Note that if the leak is placed at a node, then 'link_id' must be None and the
|
|
24
|
+
ID of the node must be set in 'node_id'
|
|
25
|
+
diameter : `float`, optional
|
|
26
|
+
Diameter of this leak.
|
|
27
|
+
|
|
28
|
+
Alternatively, 'area' can be used for specifying the size of this leakage --
|
|
29
|
+
in this case, 'diameter' must be set to 'None'.
|
|
30
|
+
|
|
31
|
+
The default is None.
|
|
32
|
+
area : `float`, optional
|
|
33
|
+
Area of this leak.
|
|
34
|
+
|
|
35
|
+
Alternatively, 'diameter' can be used for specifying the size of this leakage --
|
|
36
|
+
in this case, 'area' must be set to 'None'.
|
|
37
|
+
|
|
38
|
+
The default is None.
|
|
39
|
+
profile : `numpy.ndarray`
|
|
40
|
+
Pattern of this leak.
|
|
41
|
+
node_id : `str`, optional
|
|
42
|
+
ID of the node at which the leak is placed.
|
|
43
|
+
This parameter must only be set if the leak is placed at a node instead of a link.
|
|
44
|
+
In this case, 'link_id' must be None.
|
|
45
|
+
|
|
46
|
+
The default is None.
|
|
47
|
+
"""
|
|
48
|
+
def __init__(self, link_id: str, profile: np.ndarray, diameter: float = None,
|
|
49
|
+
area: float = None, node_id: str = None, **kwds):
|
|
50
|
+
if link_id is not None and node_id is not None:
|
|
51
|
+
raise ValueError("Leak can not be placed at a link and node at the same time")
|
|
52
|
+
if link_id is None and node_id is None:
|
|
53
|
+
raise ValueError("Leak must be placed at either a link or a node -- " +
|
|
54
|
+
"expecting either 'link_id' or 'node_id' but both are None")
|
|
55
|
+
if link_id is not None:
|
|
56
|
+
if not isinstance(link_id, str):
|
|
57
|
+
raise TypeError("'link_id' must be an instance of 'str' " +
|
|
58
|
+
f"but not of '{type(link_id)}'")
|
|
59
|
+
if area is None and diameter is None:
|
|
60
|
+
raise ValueError("Either 'diameter' or 'area' must be given")
|
|
61
|
+
if area is not None and diameter is not None:
|
|
62
|
+
raise ValueError("Either 'diameter' or 'area' must be given, " +
|
|
63
|
+
"but not both at the same time")
|
|
64
|
+
if diameter is not None:
|
|
65
|
+
if not isinstance(diameter, float):
|
|
66
|
+
raise TypeError("'diameter must be an instance of 'float' but " +
|
|
67
|
+
f"not of '{type(diameter)}'")
|
|
68
|
+
if diameter <= 0:
|
|
69
|
+
raise ValueError("'diameter' must be greater than zero")
|
|
70
|
+
if area is not None:
|
|
71
|
+
if not isinstance(area, float):
|
|
72
|
+
raise TypeError("'area must be an instance of 'float' but " +
|
|
73
|
+
f"not of '{type(area)}'")
|
|
74
|
+
if area <= 0:
|
|
75
|
+
raise ValueError("'area' must be greater than zero")
|
|
76
|
+
if profile is not None:
|
|
77
|
+
if not isinstance(profile, np.ndarray):
|
|
78
|
+
raise TypeError("'profile' must be an instance of 'numpy.ndarray' but " +
|
|
79
|
+
f"not of '{type(profile)}'")
|
|
80
|
+
if len(profile.shape) > 1:
|
|
81
|
+
raise ValueError("'profile' must be a one-dimensional array " +
|
|
82
|
+
f"but not of shape '{profile.shape}'")
|
|
83
|
+
if node_id is not None:
|
|
84
|
+
if not isinstance(node_id, str):
|
|
85
|
+
raise TypeError("'node_id' must be an instance of 'str' " +
|
|
86
|
+
f"but not of '{type(node_id)}'")
|
|
87
|
+
|
|
88
|
+
self.__link_id = link_id
|
|
89
|
+
self.__node_id = node_id
|
|
90
|
+
self.__diameter = diameter
|
|
91
|
+
self.__area = area
|
|
92
|
+
self.__profile = profile
|
|
93
|
+
|
|
94
|
+
self.__leaky_node_id = None
|
|
95
|
+
self.__leak_emitter_coef = None
|
|
96
|
+
self.__time_pattern_idx = 0
|
|
97
|
+
|
|
98
|
+
super().__init__(**kwds)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def link_id(self) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Gets the ID of the link at which the leak is placed.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
`str`
|
|
108
|
+
ID of the link at which the leak is placed.
|
|
109
|
+
"""
|
|
110
|
+
return self.__link_id
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def node_id(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Gets the ID of the node at which the leak is placed.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
`str`
|
|
120
|
+
ID of the node at which the leak is placed.
|
|
121
|
+
"""
|
|
122
|
+
return self.__node_id
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def diameter(self) -> float:
|
|
126
|
+
"""
|
|
127
|
+
Gets the diameter of the leak.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
`float`
|
|
132
|
+
Diameter of the leak.
|
|
133
|
+
"""
|
|
134
|
+
return self.__diameter
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def area(self) -> float:
|
|
138
|
+
"""
|
|
139
|
+
Gets the area of the leak.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
`float`
|
|
144
|
+
Area of the leak.
|
|
145
|
+
"""
|
|
146
|
+
return self.__area if self.__area is not None else self.compute_leak_area(self.__diameter)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def profile(self) -> np.ndarray:
|
|
150
|
+
"""
|
|
151
|
+
Gets the pattern of the leak.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
`numpy.ndarray`
|
|
156
|
+
Pattern of the leak.
|
|
157
|
+
"""
|
|
158
|
+
return deepcopy(self.__profile)
|
|
159
|
+
|
|
160
|
+
@profile.setter
|
|
161
|
+
def profile(self, pattern: np.ndarray):
|
|
162
|
+
if not isinstance(pattern, np.ndarray):
|
|
163
|
+
raise TypeError("'profile' must be an instance of 'numpy.ndarray' but " +
|
|
164
|
+
f"not of '{type(pattern)}'")
|
|
165
|
+
if len(pattern.shape) > 1:
|
|
166
|
+
raise ValueError("'profile' must be a one-dimensional array " +
|
|
167
|
+
f"but not of shape '{pattern.shape}'")
|
|
168
|
+
|
|
169
|
+
self.__profile = pattern
|
|
170
|
+
|
|
171
|
+
def get_attributes(self) -> dict:
|
|
172
|
+
return super().get_attributes() | {"link_id": self.__link_id, "diameter": self.__diameter,
|
|
173
|
+
"area": self.__area, "profile": self.__profile,
|
|
174
|
+
"node_id": self.__leaky_node_id
|
|
175
|
+
if self.__link_id is None else None}
|
|
176
|
+
|
|
177
|
+
def __eq__(self, other) -> bool:
|
|
178
|
+
if not isinstance(other, Leakage):
|
|
179
|
+
raise TypeError(f"Can not compare 'Leakage' instance with '{type(other)}' instance")
|
|
180
|
+
|
|
181
|
+
return super().__eq__(other) and self.__link_id == other.link_id \
|
|
182
|
+
and self.__diameter == other.diameter and self.__profile == other.profile \
|
|
183
|
+
and self.__node_id == other.node_id and self.area == other.area
|
|
184
|
+
|
|
185
|
+
def __str__(self) -> str:
|
|
186
|
+
return f"{super().__str__()} link_id: {self.__link_id} diameter: {self.__diameter} " +\
|
|
187
|
+
f"area: {self.__area} profile: {self.__profile} node_id: {self.__node_id}"
|
|
188
|
+
|
|
189
|
+
def compute_leak_area(self, diameter: float) -> float:
|
|
190
|
+
"""
|
|
191
|
+
Computes the leak area given the diameter.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
diameter : `float`
|
|
196
|
+
Diameter (m) of the leak.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
`float`
|
|
201
|
+
Leak area in mm^2.
|
|
202
|
+
"""
|
|
203
|
+
return (np.pi * (diameter / 2) ** 2) * 10000.
|
|
204
|
+
|
|
205
|
+
def compute_leak_emitter_coefficient(self, area: float, discharge_coef: float = .75,
|
|
206
|
+
g: float = 9.80665) -> float:
|
|
207
|
+
"""
|
|
208
|
+
Computes the leak emitter coefficient.
|
|
209
|
+
|
|
210
|
+
emitter_coef = discharge_coef * area * sqrt(2*g) where g = 9.8, discharge_coef = .75
|
|
211
|
+
|
|
212
|
+
leak_demand = emitter_coef * pressure^alpha where alpha = .5
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
area : `float`
|
|
217
|
+
Leak area (mm^2) as computed in
|
|
218
|
+
:func:`epyt_flow.simulation.events.leakages.Leakage.compute_leak_area`.
|
|
219
|
+
discharge_coef : `float`, optional
|
|
220
|
+
Discharge coefficient.
|
|
221
|
+
|
|
222
|
+
The default is set to 0.75
|
|
223
|
+
g : `float`, optional
|
|
224
|
+
Gravitational constant. Do not change this!
|
|
225
|
+
|
|
226
|
+
The default is 9.8
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
`float`
|
|
231
|
+
Leak emitter coefficient.
|
|
232
|
+
"""
|
|
233
|
+
return discharge_coef * area * np.sqrt(2. * g)
|
|
234
|
+
|
|
235
|
+
def init(self, epanet_api: epyt.epanet) -> None:
|
|
236
|
+
super().init(epanet_api)
|
|
237
|
+
|
|
238
|
+
# Split pipe if leak is placed at a link/pipe
|
|
239
|
+
if self.__link_id is not None:
|
|
240
|
+
if self.__link_id not in self._epanet_api.getLinkNameID():
|
|
241
|
+
raise ValueError(f"Unknown link/pipe '{self.__link_id}'")
|
|
242
|
+
|
|
243
|
+
new_link_id = f"leak_pipe_{self.link_id}"
|
|
244
|
+
new_node_id = f"leak_node_{self.link_id}"
|
|
245
|
+
|
|
246
|
+
all_nodes_id = self._epanet_api.getNodeNameID()
|
|
247
|
+
if new_node_id in all_nodes_id:
|
|
248
|
+
raise ValueError(f"There is already a leak at pipe {self.link_id}")
|
|
249
|
+
|
|
250
|
+
self._epanet_api.splitPipe(self.link_id, new_link_id, new_node_id)
|
|
251
|
+
self.__leaky_node_id = self._epanet_api.getNodeIndex(new_node_id)
|
|
252
|
+
else:
|
|
253
|
+
if self.__node_id not in self._epanet_api.getNodeNameID():
|
|
254
|
+
raise ValueError(f"Unknown node '{self.__node_id}'")
|
|
255
|
+
|
|
256
|
+
self.__leaky_node_id = self._epanet_api.getNodeIndex(self.__node_id)
|
|
257
|
+
|
|
258
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_id, 0.)
|
|
259
|
+
|
|
260
|
+
# Compute leak emitter coefficient
|
|
261
|
+
self.__leak_emitter_coef = self.compute_leak_emitter_coefficient(
|
|
262
|
+
self.compute_leak_area(self.area))
|
|
263
|
+
|
|
264
|
+
def reset(self) -> None:
|
|
265
|
+
self.__time_pattern_idx = 0
|
|
266
|
+
|
|
267
|
+
def exit(self, cur_time) -> None:
|
|
268
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_id, 0.)
|
|
269
|
+
|
|
270
|
+
def apply(self, cur_time: int) -> None:
|
|
271
|
+
self._epanet_api.setNodeEmitterCoeff(self.__leaky_node_id,
|
|
272
|
+
self.__leak_emitter_coef *
|
|
273
|
+
self.__profile[self.__time_pattern_idx])
|
|
274
|
+
self.__time_pattern_idx += 1
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@serializable(ABRUPT_LEAKAGE_ID, ".epytflow_leakage_abrupt")
|
|
278
|
+
class AbruptLeakage(Leakage):
|
|
279
|
+
"""
|
|
280
|
+
Class implementing an abrupt leakage event.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
link_id : `str`
|
|
285
|
+
ID of the link at which the leak is placed.
|
|
286
|
+
diameter : `float`, optional
|
|
287
|
+
Diameter of the leak.
|
|
288
|
+
|
|
289
|
+
Alternatively, 'area' can be used to specify the size of this leak --
|
|
290
|
+
in this case, 'diameter' must be set to None.
|
|
291
|
+
|
|
292
|
+
The default is None.
|
|
293
|
+
area : `float`, optional
|
|
294
|
+
Area of the leakd.
|
|
295
|
+
|
|
296
|
+
Alternatively, 'diameter' can be used to specify the size of this leak --
|
|
297
|
+
in this case, 'area' must be set to None.
|
|
298
|
+
|
|
299
|
+
The default is None.
|
|
300
|
+
"""
|
|
301
|
+
def __init__(self, link_id: str, diameter: float = None, area: float = None, **kwds):
|
|
302
|
+
if "profile" not in kwds:
|
|
303
|
+
super().__init__(link_id=link_id, diameter=diameter, area=area, profile=None, **kwds)
|
|
304
|
+
else:
|
|
305
|
+
super().__init__(link_id=link_id, diameter=diameter, area=area, **kwds)
|
|
306
|
+
|
|
307
|
+
def init(self, epanet_api: epyt.epanet) -> None:
|
|
308
|
+
super().init(epanet_api)
|
|
309
|
+
|
|
310
|
+
# Set pattern
|
|
311
|
+
total_sim_duration = self._epanet_api.getTimeSimulationDuration()
|
|
312
|
+
time_step = self._epanet_api.getTimeHydraulicStep()
|
|
313
|
+
|
|
314
|
+
if self.end_time is not None:
|
|
315
|
+
n_leaky_time_points = math.ceil((self.end_time - self.start_time) / time_step)
|
|
316
|
+
else:
|
|
317
|
+
n_leaky_time_points = math.ceil((total_sim_duration - self.start_time) / time_step)
|
|
318
|
+
|
|
319
|
+
self.profile = np.ones(n_leaky_time_points)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@serializable(INCIPIENT_LEAKAGE_ID, ".epytflow_leakage_incipient")
|
|
323
|
+
class IncipientLeakage(Leakage):
|
|
324
|
+
"""
|
|
325
|
+
Class implementing an incipient leakage event.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
link_id : `str`
|
|
330
|
+
ID of the link at which the leak is placed.
|
|
331
|
+
diameter : `float`, optional
|
|
332
|
+
Maximum diameter of the leak -- i.e. small leak diameter in the beginning,
|
|
333
|
+
growing over time until peak time is reached.
|
|
334
|
+
|
|
335
|
+
Alternatively, 'area' can be used to specify the size of this leak --
|
|
336
|
+
in this case, 'diameter' must be set to None.
|
|
337
|
+
|
|
338
|
+
The default is None.
|
|
339
|
+
area : `float`, optional
|
|
340
|
+
Maximum area of the leak -- i.e. small leak area in the beginning,
|
|
341
|
+
growing over time until peak time is reached.
|
|
342
|
+
|
|
343
|
+
Alternatively, 'diameter' can be used to specify the size of this leak --
|
|
344
|
+
in this case, 'area' must be set to None.
|
|
345
|
+
|
|
346
|
+
The default is None.
|
|
347
|
+
peak_time : `int`
|
|
348
|
+
Time (seconds since the simulation start) when this leak reaches
|
|
349
|
+
its larges size (leak diameter).
|
|
350
|
+
"""
|
|
351
|
+
def __init__(self, link_id: str, peak_time: int, diameter: float = None,
|
|
352
|
+
area: float = None, **kwds):
|
|
353
|
+
if peak_time < kwds["start_time"] or (kwds["end_time"] is not None and
|
|
354
|
+
peak_time > kwds["end_time"]):
|
|
355
|
+
raise ValueError("'peak_time' must be greater than 'start_time' and " +
|
|
356
|
+
"smaller than 'end_time'")
|
|
357
|
+
|
|
358
|
+
self.__peak_time = peak_time
|
|
359
|
+
|
|
360
|
+
if "profile" not in kwds:
|
|
361
|
+
super().__init__(link_id=link_id, diameter=diameter, area=area, profile=None, **kwds)
|
|
362
|
+
else:
|
|
363
|
+
super().__init__(link_id=link_id, diameter=diameter, area=area, **kwds)
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def peak_time(self) -> int:
|
|
367
|
+
"""
|
|
368
|
+
Gets the peak time (seconds since the simulation start) of the leak.
|
|
369
|
+
|
|
370
|
+
Returns
|
|
371
|
+
-------
|
|
372
|
+
`int`
|
|
373
|
+
Peak time of the leak.
|
|
374
|
+
"""
|
|
375
|
+
return self.__peak_time
|
|
376
|
+
|
|
377
|
+
def get_attributes(self) -> dict:
|
|
378
|
+
return super().get_attributes() | {"peak_time": self.peak_time}
|
|
379
|
+
|
|
380
|
+
def __eq__(self, other) -> bool:
|
|
381
|
+
return super().__eq__(other) and self.peak_time == other.peak_time
|
|
382
|
+
|
|
383
|
+
def __str__(self) -> str:
|
|
384
|
+
return f"{super().__str__()} peak_time: {self.peak_time}"
|
|
385
|
+
|
|
386
|
+
def init(self, epanet_api: epyt.epanet) -> None:
|
|
387
|
+
super().init(epanet_api)
|
|
388
|
+
|
|
389
|
+
# Set pattern
|
|
390
|
+
total_sim_duration = self._epanet_api.getTimeSimulationDuration()
|
|
391
|
+
time_step = self._epanet_api.getTimeHydraulicStep()
|
|
392
|
+
|
|
393
|
+
if self.end_time is not None:
|
|
394
|
+
n_leaky_time_points = math.ceil((self.end_time - self.start_time) / time_step)
|
|
395
|
+
else:
|
|
396
|
+
n_leaky_time_points = math.ceil((total_sim_duration - self.start_time) / time_step)
|
|
397
|
+
|
|
398
|
+
profile = np.ones(n_leaky_time_points)
|
|
399
|
+
|
|
400
|
+
coeff = int((self.peak_time - self.start_time) / time_step)
|
|
401
|
+
for t in range(coeff):
|
|
402
|
+
profile[t] = (1. / coeff) + ((1. / coeff) * t) # Linear interpolation!
|
|
403
|
+
|
|
404
|
+
self.profile = profile
|