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.
Files changed (131) hide show
  1. epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
  2. epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
  3. epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
  4. epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
  5. epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
  6. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
  7. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
  8. epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
  9. epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
  10. epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
  11. epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
  12. epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
  13. epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
  14. epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
  15. epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
  16. epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
  17. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
  18. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
  19. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
  20. epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
  21. epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
  22. epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
  23. epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
  24. epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
  25. epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
  26. epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
  27. epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
  28. epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
  29. epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
  30. epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
  31. epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
  32. epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
  33. epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
  34. epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
  35. epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
  36. epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
  37. epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
  38. epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
  39. epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
  40. epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
  41. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
  42. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
  43. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
  44. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
  45. epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
  46. epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
  47. epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
  48. epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
  49. epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
  50. epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
  51. epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
  52. epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
  53. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
  54. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
  55. epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
  56. epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
  57. epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
  58. epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
  59. epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
  60. epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
  61. epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
  62. epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
  63. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
  64. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
  65. epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
  66. epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
  67. epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
  68. epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
  69. epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
  70. epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
  71. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
  72. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
  73. epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
  74. epyt_flow/EPANET/compile.sh +4 -0
  75. epyt_flow/VERSION +1 -0
  76. epyt_flow/__init__.py +24 -0
  77. epyt_flow/data/__init__.py +0 -0
  78. epyt_flow/data/benchmarks/__init__.py +11 -0
  79. epyt_flow/data/benchmarks/batadal.py +257 -0
  80. epyt_flow/data/benchmarks/batadal_data.py +28 -0
  81. epyt_flow/data/benchmarks/battledim.py +473 -0
  82. epyt_flow/data/benchmarks/battledim_data.py +51 -0
  83. epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
  84. epyt_flow/data/benchmarks/leakdb.py +592 -0
  85. epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
  86. epyt_flow/data/benchmarks/water_usage.py +123 -0
  87. epyt_flow/data/networks.py +650 -0
  88. epyt_flow/gym/__init__.py +4 -0
  89. epyt_flow/gym/control_gyms.py +47 -0
  90. epyt_flow/gym/scenario_control_env.py +101 -0
  91. epyt_flow/metrics.py +404 -0
  92. epyt_flow/models/__init__.py +2 -0
  93. epyt_flow/models/event_detector.py +31 -0
  94. epyt_flow/models/sensor_interpolation_detector.py +118 -0
  95. epyt_flow/rest_api/__init__.py +4 -0
  96. epyt_flow/rest_api/base_handler.py +70 -0
  97. epyt_flow/rest_api/res_manager.py +95 -0
  98. epyt_flow/rest_api/scada_data_handler.py +476 -0
  99. epyt_flow/rest_api/scenario_handler.py +352 -0
  100. epyt_flow/rest_api/server.py +106 -0
  101. epyt_flow/serialization.py +438 -0
  102. epyt_flow/simulation/__init__.py +5 -0
  103. epyt_flow/simulation/events/__init__.py +6 -0
  104. epyt_flow/simulation/events/actuator_events.py +259 -0
  105. epyt_flow/simulation/events/event.py +81 -0
  106. epyt_flow/simulation/events/leakages.py +404 -0
  107. epyt_flow/simulation/events/sensor_faults.py +267 -0
  108. epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
  109. epyt_flow/simulation/events/sensor_reading_event.py +170 -0
  110. epyt_flow/simulation/events/system_event.py +88 -0
  111. epyt_flow/simulation/parallel_simulation.py +147 -0
  112. epyt_flow/simulation/scada/__init__.py +3 -0
  113. epyt_flow/simulation/scada/advanced_control.py +134 -0
  114. epyt_flow/simulation/scada/scada_data.py +1589 -0
  115. epyt_flow/simulation/scada/scada_data_export.py +255 -0
  116. epyt_flow/simulation/scenario_config.py +608 -0
  117. epyt_flow/simulation/scenario_simulator.py +1897 -0
  118. epyt_flow/simulation/scenario_visualizer.py +61 -0
  119. epyt_flow/simulation/sensor_config.py +1289 -0
  120. epyt_flow/topology.py +290 -0
  121. epyt_flow/uncertainty/__init__.py +3 -0
  122. epyt_flow/uncertainty/model_uncertainty.py +302 -0
  123. epyt_flow/uncertainty/sensor_noise.py +73 -0
  124. epyt_flow/uncertainty/uncertainties.py +555 -0
  125. epyt_flow/uncertainty/utils.py +206 -0
  126. epyt_flow/utils.py +306 -0
  127. epyt_flow-0.1.0.dist-info/LICENSE +21 -0
  128. epyt_flow-0.1.0.dist-info/METADATA +139 -0
  129. epyt_flow-0.1.0.dist-info/RECORD +131 -0
  130. epyt_flow-0.1.0.dist-info/WHEEL +5 -0
  131. 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