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,438 @@
1
+ """
2
+ Module provides functions and classes for serialization.
3
+ """
4
+ from typing import Any, Union
5
+ from abc import abstractmethod, ABC
6
+ from io import BufferedIOBase
7
+ import importlib
8
+ import json
9
+ import gzip
10
+ import umsgpack
11
+ import numpy as np
12
+ import networkx
13
+ import scipy
14
+
15
+
16
+ SCIPY_BSRARRAY_ID = -3
17
+ NETWORKX_GRAPH_ID = -2
18
+ NUMPY_ARRAY_ID = -1
19
+ SENSOR_CONFIG_ID = 0
20
+ SCENARIO_CONFIG_ID = 1
21
+ MODEL_UNCERTAINTY_ID = 2
22
+ SENSOR_NOISE_ID = 3
23
+ ABSOLUTE_GAUSSIAN_UNCERTAINTY_ID = 4
24
+ RELATIVE_GAUSSIAN_UNCERTAINTY_ID = 5
25
+ ABSOLUTE_UNIFORM_UNCERTAINTY_ID = 6
26
+ RELATIVE_UNIFORM_UNCERTAINTY_ID = 7
27
+ PERCENTAGE_DEVIATON_UNCERTAINTY_ID = 8
28
+ ABSOLUTE_DEEP_UNIFORM_UNCERTAINTY_ID = 9
29
+ RELATIVE_DEEP_UNIFORM_UNCERTAINTY_ID = 10
30
+ ABSOLUTE_DEEP_GAUSSIAN_UNCERTAINTY_ID = 11
31
+ RELATIVE_DEEP_GAUSSIAN_UNCERTAINTY_ID = 12
32
+ ABSOLUTE_DEEP_UNCERTAINTY_ID = 13
33
+ RELATIVE_DEEP_UNCERTAINTY_ID = 14
34
+ SENSOR_FAULT_CONSTANT_ID = 15
35
+ SENSOR_FAULT_DRIFT_ID = 16
36
+ SENSOR_FAULT_GAUSSIAN_ID = 17
37
+ SENSOR_FAULT_PERCENTAGE_ID = 18
38
+ SENSOR_FAULT_STUCKATZERO_ID = 19
39
+ LEAKAGE_ID = 20
40
+ ABRUPT_LEAKAGE_ID = 21
41
+ INCIPIENT_LEAKAGE_ID = 22
42
+ SCADA_DATA_ID = 23
43
+ SENSOR_ATTACK_OVERRIDE_ID = 24
44
+ SENSOR_ATTACK_REPLAY_ID = 25
45
+ NETWORK_TOPOLOGY_ID = 26
46
+ PUMP_STATE_EVENT_ID = 28
47
+ PUMP_SPEED_EVENT_ID = 29
48
+ VALVE_STATE_EVENT_ID = 30
49
+
50
+
51
+ def my_packb(data: Any) -> bytes:
52
+ """
53
+ Overriden `umsgpack.packb` method to support custom serialization handlers.
54
+ """
55
+ return umsgpack.packb(data, ext_handlers=ext_handler_pack)
56
+
57
+
58
+ def my_unpackb(data: Any) -> Any:
59
+ """
60
+ Overriden `umsgpack.unpackb` method to support custom serialization handlers.
61
+ """
62
+ return umsgpack.unpackb(data, ext_handlers=ext_handler_unpack)
63
+
64
+
65
+ def serializable(my_id: int, my_file_ext: str) -> Any:
66
+ """
67
+ Decorator for a serializable class -- i.e. subclass of
68
+ :class:`~epyt_flow.serialization.Serializable`.
69
+
70
+ This decorator registers a new class as a serializable class.
71
+
72
+ Parameters
73
+ ----------
74
+ my_id : `int`
75
+ ID of the class.
76
+ my_file_ext : `str`
77
+ File extension.
78
+ """
79
+ def wrapper(my_class):
80
+ @staticmethod
81
+ def unpackb(data: bytes) -> Any:
82
+ return my_class(**my_unpackb(data))
83
+ setattr(my_class, "unpackb", unpackb)
84
+
85
+ @staticmethod
86
+ def file_ext() -> str:
87
+ return my_file_ext
88
+ setattr(my_class, "file_ext", file_ext)
89
+
90
+ return umsgpack.ext_serializable(my_id)(my_class)
91
+
92
+ return wrapper
93
+
94
+
95
+ class Serializable(ABC):
96
+ """
97
+ Base class for a serializable class -- must be used in conjunction with the
98
+ :func:`~epyt_flow.serialization.serializable` decorator.
99
+ """
100
+ def __init__(self, **kwds):
101
+ super().__init__(**kwds)
102
+
103
+ @abstractmethod
104
+ def get_attributes(self) -> dict:
105
+ """
106
+ Gets all attributes to be serialized -- these attributes are passed to the
107
+ constructor when the object is deserialized.
108
+
109
+ Returns
110
+ -------
111
+ `dict`
112
+ Dictionary of attributes -- i.e. pairs of attribute name + value.
113
+ """
114
+ return {}
115
+
116
+ def file_ext(self) -> str:
117
+ """
118
+ Returns the file extension of this class.
119
+
120
+ This function is automatically implemented by applying the
121
+ :func:`~epyt_flow.serialization.serializable` decorator.
122
+
123
+ Returns
124
+ -------
125
+ `str`
126
+ File extension.
127
+ """
128
+ raise NotImplementedError()
129
+
130
+ def packb(self) -> bytes:
131
+ """
132
+ Serializes the attributes of this object.
133
+
134
+ Returns
135
+ -------
136
+ `bytes`
137
+ Serialized object.
138
+ """
139
+ return my_packb(self.get_attributes())
140
+
141
+ @staticmethod
142
+ def load(data: Union[bytes, BufferedIOBase]) -> Any:
143
+ """
144
+ Deserializes an instance of this class.
145
+
146
+ Parameters
147
+ ----------
148
+ data : `bytes` or `io.BufferedIOBase`
149
+ Serialized data or stream from which serialized data can be read.
150
+
151
+ Returns
152
+ -------
153
+ `Any`
154
+ Deserialized object.
155
+ """
156
+ return load(data)
157
+
158
+ @staticmethod
159
+ def load_from_file(f_in: str, use_zip: bool = True) -> Any:
160
+ """
161
+ Deserializes an instance of this class from a (compressed) file.
162
+
163
+ Parameters
164
+ ----------
165
+ f_in : `str`
166
+ Path to the file from which to deserialize the object.
167
+ use_zip : `bool`, optional
168
+ If True, the file `f_in` is supposed to be zip compressed -- False,
169
+ if no compression was used when serializing the object.
170
+
171
+ The default is True.
172
+
173
+ Returns
174
+ -------
175
+ `Any`
176
+ Deserialized object.
177
+ """
178
+ return load_from_file(f_in, use_zip)
179
+
180
+ def dump(self, stream_out: BufferedIOBase = None) -> Any:
181
+ """
182
+ Serializes this object to a byte array.
183
+
184
+ Parameters
185
+ ----------
186
+ stream_out : `io.BufferedIOBase`, optional
187
+ Stream to which the serialized object is written.
188
+ If None, the serialized object is returned as a `bytes` array.
189
+
190
+ The default is None.
191
+
192
+ Returns
193
+ -------
194
+ `bytes`
195
+ If `stream_out` is None, the serialized object is returned as a `bytes` array.
196
+ """
197
+ return dump(self, stream_out)
198
+
199
+ def save_to_file(self, f_out: str, use_zip: bool = True) -> None:
200
+ """
201
+ Serializes this instance and stores it in a (compressed) file.
202
+
203
+ Parameters
204
+ ----------
205
+ f_in : `str`
206
+ Path to the file where this serialized object will be stored.
207
+ use_zip : `bool`, optional
208
+ If True, the file `f_in` will be zip compressed -- False,
209
+ if no compression is wanted.
210
+
211
+ The default is True.
212
+ """
213
+ if not f_out.endswith(self.file_ext()):
214
+ f_out += self.file_ext()
215
+
216
+ return save_to_file(f_out, self, use_zip)
217
+
218
+
219
+ def my_to_json(obj: Any) -> str:
220
+ """
221
+ Serializes a given object to JSON.
222
+
223
+ Parameters
224
+ ----------
225
+ obj : `Any`
226
+ Object to be serialized.
227
+
228
+ Returns
229
+ -------
230
+ `str`
231
+ JSON data.
232
+ """
233
+ def __json_serialize(obj_: Any) -> dict:
234
+ if isinstance(obj_, JsonSerializable):
235
+ my_class_name = (obj_.__module__, obj_.__class__.__name__)
236
+ return obj_.get_attributes() | {"__type__": my_class_name}
237
+ elif isinstance(obj_, np.ndarray):
238
+ return obj_.tolist()
239
+ else:
240
+ return obj_
241
+
242
+ return json.dumps(obj, default=__json_serialize)
243
+
244
+
245
+ def my_load_from_json(data: str) -> Any:
246
+ """
247
+ Loads (i.e. deserializes) an object from given JSON data.
248
+
249
+ Parameters
250
+ ----------
251
+ data : `str`
252
+ JSON data.
253
+
254
+ Returns
255
+ -------
256
+ `Any`
257
+ Deserialized object.
258
+ """
259
+ def __object_hook(obj: dict) -> dict:
260
+ if "__type__" in obj:
261
+ module_name, class_name = obj["__type__"]
262
+ cls = getattr(importlib.import_module(module_name), class_name)
263
+ del obj["__type__"]
264
+
265
+ for attr in obj:
266
+ if isinstance(attr, dict):
267
+ obj[attr] = __object_hook(obj[attr])
268
+
269
+ return cls(**obj)
270
+ return obj
271
+
272
+ return json.loads(data, object_hook=__object_hook)
273
+
274
+
275
+ class JsonSerializable(Serializable):
276
+ """
277
+ Base class for JSON serializable classes.
278
+ Inherits from :class:`~epyt_flow.serialization.Serializable`.
279
+ """
280
+
281
+ def to_json(self) -> str:
282
+ """
283
+ Serializes this instance to JSON.
284
+
285
+ Returns
286
+ -------
287
+ `str`
288
+ JSON data.
289
+ """
290
+ return my_to_json(self)
291
+
292
+ @staticmethod
293
+ def load_from_json(data: str) -> Any:
294
+ """
295
+ Loads (i.e. deserializes) an instance of this class from given JSON data.
296
+
297
+ Parameters
298
+ ----------
299
+ data : `str`
300
+ JSON data.
301
+
302
+ Returns
303
+ -------
304
+ `Any`
305
+ Deserialized instance of this class.
306
+ """
307
+ return my_load_from_json(data)
308
+
309
+
310
+ def load(data: Union[bytes, BufferedIOBase]) -> Any:
311
+ """
312
+ Deserializes data.
313
+
314
+ Parameters
315
+ ----------
316
+ data : `bytes` or `io.BufferedIOBase`
317
+ Serialized data or stream from which serialized data can be read.
318
+
319
+ Returns
320
+ -------
321
+ `Any`
322
+ Deserialized data.
323
+ """
324
+ if isinstance(data, bytes):
325
+ return my_unpackb(data)
326
+ elif isinstance(data, BufferedIOBase):
327
+ return my_unpackb(data.read())
328
+ else:
329
+ raise TypeError("Invalid type of 'data' -- must be either instance of 'bytes' or " +
330
+ f"'io.BufferedIOBase' but not of '{type(data)}'")
331
+
332
+
333
+ def dump(data: Any, stream_out: BufferedIOBase = None) -> Union[bytes, None]:
334
+ """
335
+ Serializes some given data to a byte array.
336
+
337
+ Parameters
338
+ ----------
339
+ stream_out : `io.BufferedIOBase`, optional
340
+ Stream to which the serialized object is written.
341
+ If None, the serialized object is returned as a `bytes` array.
342
+
343
+ The default is None.
344
+
345
+ Returns
346
+ -------
347
+ `bytes`
348
+ Serialized data if `stream_out` is None -- otherwise, nothing is returned.
349
+ """
350
+ if stream_out is None:
351
+ return my_packb(data)
352
+ else:
353
+ if not isinstance(stream_out, BufferedIOBase):
354
+ raise TypeError("'stream_out' must be an instance of 'io.BufferedIOBase' " +
355
+ f"but not of '{type(stream_out)}'")
356
+
357
+ stream_out.write(my_packb(data))
358
+
359
+
360
+ def load_from_file(f_in: str, use_compression: bool = True) -> Any:
361
+ """
362
+ Deserializes data from a (compressed) file.
363
+
364
+ Parameters
365
+ ----------
366
+ f_in : `str`
367
+ Path to the file from which to deserialize the data.
368
+ use_compression : `bool`, optional
369
+ If True, the file `f_in` is supposed to be gzip compressed -- False,
370
+ if no compression was used when serializing the data.
371
+
372
+ The default is True.
373
+
374
+ Returns
375
+ -------
376
+ `Any`
377
+ Deserialized data.
378
+ """
379
+ if use_compression is False:
380
+ with open(f_in, "rb") as f:
381
+ return umsgpack.unpack(f, ext_handlers=ext_handler_unpack)
382
+ else:
383
+ with gzip.open(f_in, "rb") as f:
384
+ return load(f.read())
385
+
386
+
387
+ def save_to_file(f_out: str, data: Any, use_compression: bool = True) -> None:
388
+ """
389
+ Serializes data and stores it in a (compressed) file.
390
+
391
+ Parameters
392
+ ----------
393
+ f_in : `str`
394
+ Path to the file where the serialized data will be stored.
395
+ use_compression : `bool`, optional
396
+ If True, the file `f_in` will be gzip compressed -- False, if no compression is wanted.
397
+
398
+ The default is True.
399
+ """
400
+ if use_compression is False:
401
+ with open(f_out, "wb") as f:
402
+ umsgpack.pack(data, f, ext_handlers=ext_handler_pack)
403
+ else:
404
+ with gzip.open(f_out, "wb") as f:
405
+ f.write(dump(data))
406
+
407
+
408
+ # Add numpy.ndarray, networkx.Graph, and scipy.sparse.bsr_array support
409
+ def __encode_bsr_array(array: scipy.sparse.bsr_array
410
+ ) -> tuple[tuple[int, int], tuple[list[float], tuple[list[int], list[int]]]]:
411
+ shape = array.shape
412
+ data = array.data.flatten().tolist()
413
+ rows = array.nonzero()[0].tolist()
414
+ cols = array.nonzero()[1].tolist()
415
+
416
+ return shape, (data, (rows, cols))
417
+
418
+
419
+ def __decode_bsr_array(ext_data: tuple[tuple[int, int],
420
+ tuple[list[float], tuple[list[int], list[int]]]]
421
+ ) -> scipy.sparse.bsr_array:
422
+ shape, data = ext_data
423
+ return scipy.sparse.bsr_array((data[0], (data[1][0], data[1][1])), shape=(shape[0], shape[1]))
424
+
425
+
426
+ ext_handler_pack = {np.ndarray:
427
+ lambda arr: umsgpack.Ext(NUMPY_ARRAY_ID, umsgpack.packb(arr.tolist())),
428
+ networkx.Graph:
429
+ lambda graph:
430
+ umsgpack.Ext(NETWORKX_GRAPH_ID,
431
+ umsgpack.packb(networkx.node_link_data(graph))),
432
+ scipy.sparse.bsr_array:
433
+ lambda arr: umsgpack.Ext(SCIPY_BSRARRAY_ID,
434
+ umsgpack.packb(__encode_bsr_array(arr)))}
435
+ ext_handler_unpack = {NUMPY_ARRAY_ID: lambda ext: np.array(umsgpack.unpackb(ext.data)),
436
+ NETWORKX_GRAPH_ID:
437
+ lambda ext: networkx.node_link_graph(umsgpack.unpackb(ext.data)),
438
+ SCIPY_BSRARRAY_ID: lambda ext: __decode_bsr_array(umsgpack.unpackb(ext.data))}
@@ -0,0 +1,5 @@
1
+ from .scenario_config import *
2
+ from .sensor_config import *
3
+ from .scenario_simulator import *
4
+ from .scenario_visualizer import *
5
+ from .parallel_simulation import *
@@ -0,0 +1,6 @@
1
+ from .system_event import *
2
+ from .leakages import *
3
+ from .sensor_reading_event import *
4
+ from .sensor_faults import *
5
+ from .sensor_reading_attack import *
6
+ from .actuator_events import *
@@ -0,0 +1,259 @@
1
+ """
2
+ Module provides implementations of different types of actuator events.
3
+ """
4
+ import warnings
5
+ from epyt.epanet import epanet
6
+ import numpy as np
7
+
8
+ from .system_event import SystemEvent
9
+ from ...serialization import serializable, JsonSerializable, PUMP_STATE_EVENT_ID, \
10
+ PUMP_SPEED_EVENT_ID, VALVE_STATE_EVENT_ID
11
+
12
+
13
+ class ActuatorConstants:
14
+ """
15
+ Class defining some constants related to actuator events.
16
+
17
+ Attributes
18
+ ----------
19
+ EN_CLOSED
20
+ Valve or pump is closed.
21
+ EN_OPEN
22
+ Valve or pump is open -- i.e. active.
23
+ """
24
+ EN_CLOSED = 0
25
+ EN_OPEN = 1
26
+
27
+
28
+ class ActuatorEvent(SystemEvent):
29
+ """
30
+ Base class of an actuator event.
31
+
32
+ .. note::
33
+ Note that actuator events are one-time events -- i.e.
34
+ they are executed only once at a given point in time.
35
+
36
+ Parameters
37
+ ----------
38
+ time : int
39
+ Time (in seconds since simulation start) at which this event is executed.
40
+ """
41
+ def __init__(self, time: int, **kwds):
42
+ super().__init__(start_time=time, end_time=time+1, **kwds)
43
+
44
+ def get_attributes(self) -> dict:
45
+ return {"time": self.start_time}
46
+
47
+
48
+ class PumpEvent(ActuatorEvent):
49
+ """
50
+ Base class of a pump event.
51
+
52
+ Parameters
53
+ ----------
54
+ pump_id : str
55
+ ID of the pump that is affected by this event.
56
+ """
57
+ def __init__(self, pump_id: str, **kwds):
58
+ self.__pump_id = pump_id
59
+
60
+ super().__init__(**kwds)
61
+
62
+ def init(self, epanet_api: epanet) -> None:
63
+ if self.__pump_id not in epanet_api.getLinkPumpNameID():
64
+ raise ValueError(f"Invalid pump ID '{self.__pump_id}'")
65
+
66
+ super().init(epanet_api)
67
+
68
+ def get_attributes(self) -> dict:
69
+ return super().get_attributes() | {"pump_id": self.__pump_id}
70
+
71
+ @property
72
+ def pump_id(self) -> str:
73
+ """
74
+ Gets the ID of the pump affected by this event.
75
+
76
+ Returns
77
+ -------
78
+ `str`
79
+ Pump ID.
80
+ """
81
+ return self.__pump_id
82
+
83
+
84
+ @serializable(PUMP_STATE_EVENT_ID, ".epytflow_pump_state_event")
85
+ class PumpStateEvent(PumpEvent, JsonSerializable):
86
+ """
87
+ Class implementing a pump state event.
88
+
89
+ Parameters
90
+ ----------
91
+ pump_state : `str`
92
+ New state of the pump -- i.e. the state of the pump is set to this value
93
+ while the event is active.
94
+
95
+ Must be one of the following constants defined in
96
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
97
+
98
+ - EN_CLOSED = 0
99
+ - EN_OPEN = 1
100
+ """
101
+ def __init__(self, pump_state: int, **kwds):
102
+ if not isinstance(pump_state, int):
103
+ raise TypeError("'pump_state' must be an instace of 'int' " +
104
+ f"but not of {type(pump_state)}")
105
+ if not 0 <= pump_state <= 1:
106
+ raise ValueError(f"Invalid pump state '{pump_state}' -- " +
107
+ "must be either EN_CLOSED (0) or EN_OPEN (1)")
108
+
109
+ self.__pump_state = pump_state
110
+
111
+ super().__init__(**kwds)
112
+
113
+ def get_attributes(self) -> dict:
114
+ return super().get_attributes() | {"pump_state": self.__pump_state}
115
+
116
+ @property
117
+ def pump_state(self) -> int:
118
+ """
119
+ Gets the new pump state.
120
+
121
+ Returns
122
+ -------
123
+ `int`
124
+ New pump state.
125
+
126
+ One of the following constants defined in
127
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
128
+
129
+ - EN_CLOSED = 0
130
+ - EN_OPEN = 1
131
+ """
132
+ return self.__pump_state
133
+
134
+ def apply(self, cur_time: int) -> None:
135
+ pump_idx = self._epanet_api.getLinkPumpNameID().index(self.pump_id) + 1
136
+ pump_link_idx = self._epanet_api.getLinkPumpIndex(pump_idx)
137
+ self._epanet_api.setLinkStatus(pump_link_idx, self.__pump_state)
138
+
139
+
140
+ @serializable(PUMP_SPEED_EVENT_ID, ".epytflow_pump_speed_event")
141
+ class PumpSpeedEvent(PumpEvent, JsonSerializable):
142
+ """
143
+ Class implementing a pump speed event.
144
+
145
+ Parameters
146
+ ----------
147
+ pump_speed : float
148
+ New pump speed -- i.e. the speed of the pump is set to this value while the event is active.
149
+ """
150
+ def __init__(self, pump_speed: float, **kwds):
151
+ if not isinstance(pump_speed, float):
152
+ raise TypeError("'pump_speed' must be an instance of 'float' " +
153
+ f"but not of {type(pump_speed)}")
154
+ if pump_speed <= 0:
155
+ raise ValueError("Pump speed must be positive")
156
+
157
+ self.__pump_speed = pump_speed
158
+
159
+ super().__init__(**kwds)
160
+
161
+ def get_attributes(self) -> dict:
162
+ return super().get_attributes() | {"pump_speed": self.__pump_speed}
163
+
164
+ @property
165
+ def pump_speed(self) -> float:
166
+ """
167
+ Gets the new pump speed.
168
+
169
+ Returns
170
+ -------
171
+ `float`
172
+ New pump speed.
173
+ """
174
+ return self.__pump_speed
175
+
176
+ def apply(self, cur_time: int) -> None:
177
+ pump_idx = self._epanet_api.getLinkPumpNameID().index(self.pump_id) + 1
178
+ pattern_idx = self._epanet_api.getLinkPumpPatternIndex(pump_idx)
179
+
180
+ if pattern_idx == 0:
181
+ warnings.warn(f"No pattern for pump '{self.pump_id}' found -- a new pattern is created")
182
+ pattern_idx = self._epanet_api.addPattern(f"pump_speed_{self.pump_id}")
183
+ self._epanet_api.setLinkPumpPatternIndex(pattern_idx)
184
+
185
+ self._epanet_api.setPattern(pattern_idx, np.array([self.__pump_speed]))
186
+
187
+
188
+ @serializable(VALVE_STATE_EVENT_ID, ".epytflow_valve_state_event")
189
+ class ValveStateEvent(ActuatorEvent, JsonSerializable):
190
+ """
191
+ Class implementing a valve state event.
192
+
193
+ Parameters
194
+ ----------
195
+ valve_id : `str`
196
+ ID of the valve that is affected by this event.
197
+ valve_state : `str`
198
+ New state of the valve -- i.e. the valve state is set to this value this event is executed.
199
+
200
+ Must be one of the following constants defined in
201
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
202
+
203
+ - EN_CLOSED = 0
204
+ - EN_OPEN = 1
205
+ """
206
+ def __init__(self, valve_id: str, valve_state: int, **kwds):
207
+ if not isinstance(valve_state, int):
208
+ raise TypeError("'valve_state' must be an instance of 'int' " +
209
+ f"but not of {type(valve_state)}")
210
+ if not 0 <= valve_state <= 1:
211
+ raise ValueError(f"Invalid valve state '{valve_state}' -- " +
212
+ "must be either EN_CLOSED (0) or EN_OPEN (1)")
213
+
214
+ self.__valve_id = valve_id
215
+ self.__valve_state = valve_state
216
+
217
+ super().__init__(**kwds)
218
+
219
+ def init(self, epanet_api: epanet) -> None:
220
+ if self.__valve_id not in epanet_api.getLinkValveNameID():
221
+ raise ValueError(f"Invalid valve ID '{self.__valve_id}'")
222
+
223
+ super().init(epanet_api)
224
+
225
+ def get_attributes(self) -> dict:
226
+ return super().get_attributes() | {"valve_id": self.__valve_id,
227
+ "valve_state": self.__valve_state}
228
+
229
+ @property
230
+ def valve_id(self) -> str:
231
+ """
232
+ Gets the ID of the valve affected by this event.
233
+
234
+ Returns
235
+ -------
236
+ `str`
237
+ Valve ID.
238
+ """
239
+ return self.__valve_id
240
+
241
+ @property
242
+ def valve_state(self) -> int:
243
+ """
244
+ Gets the new state of the valve.
245
+
246
+ Returns
247
+ -------
248
+ `int`
249
+ New valve state.
250
+
251
+ One of the following constants defined in
252
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
253
+ """
254
+ return self.__valve_state
255
+
256
+ def apply(self, cur_time: int) -> None:
257
+ valve_idx = self._epanet_api.getLinkValveNameID().index(self.__valve_id) + 1
258
+ valve_link_idx = self._epanet_api.getLinkValveIndex(valve_idx)
259
+ self._epanet_api.setLinkStatus(valve_link_idx, self.__valve_state)