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,267 @@
1
+ """
2
+ Module provides classes for implementing different sensor faults.
3
+ """
4
+ from abc import abstractmethod
5
+ import numpy as np
6
+
7
+ from .sensor_reading_event import SensorReadingEvent
8
+ from ...serialization import serializable, JsonSerializable, SENSOR_FAULT_CONSTANT_ID, \
9
+ SENSOR_FAULT_DRIFT_ID, SENSOR_FAULT_GAUSSIAN_ID, SENSOR_FAULT_PERCENTAGE_ID, \
10
+ SENSOR_FAULT_STUCKATZERO_ID
11
+
12
+
13
+ class SensorFault(SensorReadingEvent):
14
+ """
15
+ Base class for a sensor fault
16
+ """
17
+ # Acknowledgement: This Python implementation is based on
18
+ # https://github.com/eldemet/sensorfaultmodels/blob/main/sensorfaultmodels.m
19
+ # and https://github.com/Mariosmsk/sensorfaultmodels/blob/main/sensorfaultmodels.py
20
+
21
+ def compute_multiplier(self, cur_time: int) -> float:
22
+ """
23
+ Computes the multiplier for a given time stamp.
24
+
25
+ Parameters
26
+ ----------
27
+ cur_time : `int`
28
+ Time in seconds.
29
+
30
+ Returns
31
+ -------
32
+ `float`
33
+ Multiplier.
34
+ """
35
+ b1 = 0
36
+ b2 = 0
37
+ a1 = 1
38
+ a2 = 1
39
+
40
+ if cur_time >= self.start_time:
41
+ b1 = 1 - np.exp(- a1 * (cur_time - self.start_time))
42
+
43
+ if cur_time >= self.end_time:
44
+ b2 = 1 - np.exp(- a2 * (cur_time - self.end_time))
45
+
46
+ return b1 - b2
47
+
48
+ @abstractmethod
49
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
50
+ cur_time: int) -> float:
51
+ """
52
+ Applies this sensor fault to a given single sensor reading value --
53
+ i.e. the sensor reading value is perturbed by this fault.
54
+
55
+ Parameters:
56
+ -----------
57
+ cur_multiplier : `float`
58
+ Current multiplier -- i.e. controls the "strength" of the fault.
59
+ sensor_reading : `float`
60
+ Sensor reading value.
61
+ cur_time : `int`
62
+ Current time stamp (in seconds) in the simulation.
63
+
64
+ Returns
65
+ -------
66
+ `float`
67
+ Perturbed sensor reading value.
68
+ """
69
+ raise NotImplementedError()
70
+
71
+ def apply(self, sensor_readings: np.ndarray,
72
+ sensor_readings_time: np.ndarray) -> np.ndarray:
73
+ for i in range(sensor_readings.shape[0]):
74
+ t = sensor_readings_time[i]
75
+ sensor_readings[i] = self.apply_sensor_fault(self.compute_multiplier(t),
76
+ sensor_readings[i], t)
77
+
78
+ return sensor_readings
79
+
80
+
81
+ @serializable(SENSOR_FAULT_CONSTANT_ID, ".epytflow_sensorfault_constant")
82
+ class SensorFaultConstant(SensorFault, JsonSerializable):
83
+ """
84
+ Class implementing a constant shift sensor fault.
85
+
86
+ Parameters
87
+ ----------
88
+ constant_shift : `float`
89
+ Constant that is added to the sensor reading.
90
+ """
91
+ def __init__(self, constant_shift: float, **kwds):
92
+ if not isinstance(constant_shift, float):
93
+ raise TypeError("'constant_shift' must be an instance of 'float' but no of " +
94
+ f"'{type(constant_shift)}'")
95
+
96
+ self.__constant_shift = constant_shift
97
+
98
+ super().__init__(**kwds)
99
+
100
+ @property
101
+ def constant_shift(self) -> float:
102
+ """
103
+ Gets the Constant that is added to the sensor reading.
104
+
105
+ Returns
106
+ -------
107
+ `float`
108
+ Constant that is added to the sensor reading.
109
+ """
110
+ return self.__constant_shift
111
+
112
+ def get_attributes(self) -> dict:
113
+ return super().get_attributes() | {"constant_shift": self.__constant_shift}
114
+
115
+ def __eq__(self, other) -> bool:
116
+ return super().__eq__(other) and self.__constant_shift == other.constant_shift
117
+
118
+ def __str__(self) -> str:
119
+ return f"{type(self).__name__} {super().__str__()} constant: {self.__constant_shift}"
120
+
121
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
122
+ cur_time: int) -> float:
123
+ return sensor_reading + cur_multiplier * self.__constant_shift
124
+
125
+
126
+ @serializable(SENSOR_FAULT_DRIFT_ID, ".epytflow_sensorfault_drift")
127
+ class SensorFaultDrift(SensorFault, JsonSerializable):
128
+ """
129
+ Class implementing a drift sensor fault.
130
+
131
+ Parameters
132
+ ----------
133
+ coef : `float`
134
+ Coefficient of the drift.
135
+ """
136
+ def __init__(self, coef: float, **kwds):
137
+ self.__coef = coef
138
+
139
+ super().__init__(**kwds)
140
+
141
+ @property
142
+ def coef(self) -> float:
143
+ """
144
+ Gets the coefficient of the drift.
145
+
146
+ Returns
147
+ -------
148
+ `float`
149
+ Coefficient of the drift.
150
+ """
151
+ return self.__coef
152
+
153
+ def get_attributes(self) -> dict:
154
+ return super().get_attributes() | {"coef": self.__coef}
155
+
156
+ def __eq__(self, other) -> bool:
157
+ return super().__eq__(other) and self.__coef == other.coef
158
+
159
+ def __str__(self) -> str:
160
+ return f"{type(self).__name__} {super().__str__()} coef: {self.__coef}"
161
+
162
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
163
+ cur_time: int) -> float:
164
+ return sensor_reading + cur_multiplier * (self.__coef * (cur_time - self.start_time))
165
+
166
+
167
+ @serializable(SENSOR_FAULT_GAUSSIAN_ID, ".epytflow_sensorfault_gaussian")
168
+ class SensorFaultGaussian(SensorFault, JsonSerializable):
169
+ """
170
+ Class implementing a Gaussian shift sensor fault -- i.e.
171
+ adding Gaussian noise (centered at zero) to the sensor reading.
172
+
173
+ Parameters
174
+ ----------
175
+ std : `float`
176
+ Standard deviation of the Gaussian noise.
177
+ """
178
+ def __init__(self, std: float, **kwds):
179
+ if not isinstance(std, float) or not std > 0:
180
+ raise ValueError("'std' must be an instance of 'float' and be greater than 0")
181
+
182
+ self.__std = std
183
+
184
+ super().__init__(**kwds)
185
+
186
+ @property
187
+ def std(self) -> float:
188
+ """
189
+ Gets the standard deviation of the Gaussian noise.
190
+
191
+ Returns
192
+ -------
193
+ `float`
194
+ Standard deviation of the Gaussian noise.
195
+ """
196
+ return self.__std
197
+
198
+ def get_attributes(self) -> dict:
199
+ return super().get_attributes() | {"std": self.__std}
200
+
201
+ def __eq__(self, other) -> bool:
202
+ return super().__eq__(other) and self.__std == other.std
203
+
204
+ def __str__(self) -> str:
205
+ return f"{type(self).__name__} {super().__str__()} std: {self.__std}"
206
+
207
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
208
+ cur_time: int) -> float:
209
+ return sensor_reading + cur_multiplier * np.random.normal(loc=0, scale=self.__std)
210
+
211
+
212
+ @serializable(SENSOR_FAULT_PERCENTAGE_ID, ".epytflow_sensorfault_percentage",)
213
+ class SensorFaultPercentage(SensorFault, JsonSerializable):
214
+ """
215
+ Class implementing a percentage shift sensor fault.
216
+
217
+ Parameters
218
+ ----------
219
+ coef : `float`
220
+ Coefficient (percentage) of the shift -- i.e. coef must be in (0,].
221
+ """
222
+ def __init__(self, coef: float, **kwds):
223
+ if not isinstance(coef, float) or not coef > 0:
224
+ raise ValueError("'coef' must be an instance of 'float' and be greater than zero.")
225
+
226
+ self.__coef = coef
227
+
228
+ super().__init__(**kwds)
229
+
230
+ @property
231
+ def coef(self) -> float:
232
+ """
233
+ Gets the coefficient (percentage) of the shift.
234
+
235
+ Returns
236
+ -------
237
+ `float`
238
+ Coefficient (percentage) of the shift.
239
+ """
240
+ return self.__coef
241
+
242
+ def get_attributes(self) -> dict:
243
+ return super().get_attributes() | {"coef": self.__coef}
244
+
245
+ def __eq__(self, other) -> bool:
246
+ return super().__eq__(other) and self.__coef == other.coef
247
+
248
+ def __str__(self) -> str:
249
+ return f"{type(self).__name__} {super().__str__()} coef: {self.__coef}"
250
+
251
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
252
+ cur_time: int) -> float:
253
+ return sensor_reading + cur_multiplier * self.__coef * sensor_reading
254
+
255
+
256
+ @serializable(SENSOR_FAULT_STUCKATZERO_ID, ".epytflow_sensorfault_zero")
257
+ class SensorFaultStuckZero(SensorFault, JsonSerializable):
258
+ """
259
+ Class implementing a stuck-at-zero sensor fault -- i.e. sensor reading is set to zero.
260
+ """
261
+
262
+ def __str__(self) -> str:
263
+ return f"{type(self).__name__} {super().__str__()}"
264
+
265
+ def apply_sensor_fault(self, cur_multiplier: float, sensor_reading: float,
266
+ cur_time: int) -> float:
267
+ return sensor_reading + cur_multiplier * (-1. * sensor_reading)
@@ -0,0 +1,185 @@
1
+ """
2
+ This module provides classes for implementing different types of sensor reading attacks.
3
+ """
4
+ from copy import deepcopy
5
+ import numpy as np
6
+
7
+ from .sensor_reading_event import SensorReadingEvent
8
+ from ...serialization import serializable, JsonSerializable, SENSOR_ATTACK_OVERRIDE_ID, \
9
+ SENSOR_ATTACK_REPLAY_ID
10
+
11
+
12
+ class SensorReadingAttack(SensorReadingEvent):
13
+ """
14
+ Base class of a sensor reading attack.
15
+ """
16
+
17
+
18
+ @serializable(SENSOR_ATTACK_OVERRIDE_ID, ".epytflow_sensorattack_override")
19
+ class SensorOverrideAttack(SensorReadingAttack, JsonSerializable):
20
+ """
21
+ Class implementing a sensor override attack -- i.e. sensor reading values are overwritten
22
+ by pre-defined values.
23
+
24
+ If the override attack is running out of pre-defined sensor reading values,
25
+ it repeats the given values from the beginning onwards.
26
+
27
+ Parameters
28
+ ----------
29
+ new_sensor_values : `numpy.ndarray`
30
+ New sensor reading values -- i.e. these values replace the true sensor reading values.
31
+ """
32
+ def __init__(self, new_sensor_values: np.ndarray, **kwds):
33
+ if not isinstance(new_sensor_values, np.ndarray):
34
+ raise TypeError("'new_sensor_values' must be an instance of 'numpy.ndarray' " +
35
+ f"but not of '{type(new_sensor_values)}'")
36
+ if len(new_sensor_values.shape) != 1:
37
+ raise ValueError("'new_sensor_values' must be a 1-dimensional array")
38
+ if len(new_sensor_values) == 0:
39
+ raise ValueError("'new_sensor_values' can not be empty")
40
+
41
+ self.__new_sensor_values = new_sensor_values
42
+ self.__cur_replay_idx = 0
43
+
44
+ super().__init__(**kwds)
45
+
46
+ @property
47
+ def new_sensor_values(self) -> np.ndarray:
48
+ """
49
+ Get the new sensor reading values -- i.e. these values replace the
50
+ true sensor reading values.
51
+
52
+ Returns
53
+ -------
54
+ `np.ndarray`
55
+ New sensor readings.
56
+ """
57
+ return deepcopy(self.__new_sensor_values)
58
+
59
+ def get_attributes(self) -> dict:
60
+ return super().get_attributes() | {"new_sensor_values": self.__new_sensor_values}
61
+
62
+ def __eq__(self, other) -> bool:
63
+ if not isinstance(other, SensorOverrideAttack):
64
+ raise TypeError("Can not compare 'SensorOverrideAttack' instance " +
65
+ f"with '{type(other)}' instance")
66
+
67
+ return super().__eq__(other) and self.__new_sensor_values == other.new_sensor_values
68
+
69
+ def __str__(self) -> str:
70
+ return f"{type(self).__name__} {super().__str__()} " +\
71
+ f"new_sensor_values: {self.__new_sensor_values}"
72
+
73
+ def apply(self, sensor_readings: np.ndarray,
74
+ sensor_readings_time: np.ndarray) -> np.ndarray:
75
+ for i in range(sensor_readings.shape[0]):
76
+ t = sensor_readings_time[i]
77
+
78
+ if self.start_time <= t <= self.end_time:
79
+ sensor_readings[i] = self.__new_sensor_values[self.__cur_replay_idx]
80
+ self.__cur_replay_idx = (self.__cur_replay_idx + 1) % len(self.__new_sensor_values)
81
+
82
+ return sensor_readings
83
+
84
+
85
+ @serializable(SENSOR_ATTACK_REPLAY_ID, ".epytflow_sensorattack_replay")
86
+ class SensorReplayAttack(SensorReadingAttack, JsonSerializable):
87
+ """
88
+ Class implementing a sensor replay attack -- i.e. sensor readings are replaced by
89
+ historical recordings.
90
+
91
+ If the provided time window of historical recordings is smaller than the time window of the
92
+ attack, it repeats the historical values from the beginning onwards.
93
+
94
+ Parameters
95
+ ----------
96
+ replay_data_time_window_start : `int`
97
+ Start (seconds since simulation start) of the time window that is used in the replay
98
+ of sensor readings.
99
+ replay_data_time_window_end : `int`
100
+ End (seconds since simulation start) of the time window that is used in the replay
101
+ of sensor readings.
102
+ """
103
+ def __init__(self, replay_data_time_window_start: int, replay_data_time_window_end: int,
104
+ **kwds):
105
+ if not isinstance(replay_data_time_window_start, int):
106
+ raise TypeError("'replay_data_time_window_start' must be an instance of 'int' " +
107
+ f"but not of {type(replay_data_time_window_start)}")
108
+ if not isinstance(replay_data_time_window_end, int):
109
+ raise TypeError("'replay_data_time_window_end' must be an instance of 'int' " +
110
+ f"but not of {type(replay_data_time_window_end)}")
111
+ if replay_data_time_window_start > replay_data_time_window_end or \
112
+ replay_data_time_window_start < 0:
113
+ raise ValueError("Invalid values for 'replay_data_time_window_start' and/or " +
114
+ "'replay_data_time_window_end' detected.")
115
+
116
+ self.__new_sensor_values = np.zeros(replay_data_time_window_end -
117
+ replay_data_time_window_start)
118
+ self.__sensor_data_time_window_start = replay_data_time_window_start
119
+ self.__sensor_data_time_window_end = replay_data_time_window_end
120
+ self.__cur_hist_idx = 0
121
+ self.__cur_replay_idx = 0
122
+
123
+ super().__init__(**kwds)
124
+
125
+ if self.__sensor_data_time_window_start > self.start_time:
126
+ raise ValueError("'replay_data_time_window_start' must be less than 'start_time'")
127
+
128
+ @property
129
+ def sensor_data_time_window_start(self) -> int:
130
+ """
131
+ Gets the start time (seconds since simulation start) of the time window
132
+ that is used in the replay of sensor readings.
133
+
134
+ Returns
135
+ -------
136
+ `int`
137
+ Start time.
138
+ """
139
+ return self.__sensor_data_time_window_start
140
+
141
+ @property
142
+ def sensor_data_time_window_end(self) -> int:
143
+ """
144
+ Gets the end time (seconds since simulation start) of the time window
145
+ that is used in the replay of sensor readings.
146
+
147
+ Returns
148
+ -------
149
+ `int`
150
+ End time.
151
+ """
152
+ return self.__sensor_data_time_window_end
153
+
154
+ def get_attributes(self) -> dict:
155
+ my_attributes = {"new_sensor_values": self.__new_sensor_values,
156
+ "replay_data_time_window_start": self.__sensor_data_time_window_start,
157
+ "replay_data_time_window_end": self.__sensor_data_time_window_end}
158
+
159
+ return super().get_attributes() | my_attributes
160
+
161
+ def __eq__(self, other) -> bool:
162
+ if not isinstance(other, SensorReplayAttack):
163
+ raise TypeError("Can not compare 'SensorReplayAttack' instance " +
164
+ f"with '{type(other)}' instance")
165
+
166
+ return super().__eq__(other) and self.__new_sensor_values == other.new_sensor_values
167
+
168
+ def __str__(self) -> str:
169
+ return f"{type(self).__name__} {super().__str__()} " +\
170
+ f"new_sensor_values: {self.__new_sensor_values}"
171
+
172
+ def apply(self, sensor_readings: np.ndarray,
173
+ sensor_readings_time: np.ndarray) -> np.ndarray:
174
+ for i in range(sensor_readings.shape[0]):
175
+ t = sensor_readings_time[i]
176
+
177
+ if self.__sensor_data_time_window_start <= t <= self.__sensor_data_time_window_end:
178
+ self.__new_sensor_values[self.__cur_hist_idx] = sensor_readings[i]
179
+ self.__cur_hist_idx += 1
180
+
181
+ if self.start_time <= t <= self.end_time:
182
+ sensor_readings[i] = self.__new_sensor_values[self.__cur_replay_idx]
183
+ self.__cur_replay_idx = (self.__cur_replay_idx + 1) % len(self.__new_sensor_values)
184
+
185
+ return sensor_readings
@@ -0,0 +1,170 @@
1
+ """
2
+ Module provides a base class for sensor reading events such as sensor faults.
3
+ """
4
+ from abc import abstractmethod
5
+ import warnings
6
+ import numpy
7
+
8
+ from .event import Event
9
+ from ..sensor_config import SensorConfig, SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, \
10
+ SENSOR_TYPE_NODE_DEMAND, SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, \
11
+ SENSOR_TYPE_VALVE_STATE, SENSOR_TYPE_PUMP_STATE, SENSOR_TYPE_TANK_VOLUME, \
12
+ SENSOR_TYPE_NODE_BULK_SPECIES, SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
13
+
14
+
15
+ class SensorReadingEvent(Event):
16
+ """
17
+ Base class for a sensor reading event -- i.e. an event directly affecting sensor readings.
18
+
19
+ Parameters
20
+ ----------
21
+ sensor_id : `str`
22
+ ID of the sensor that is affected by this event.
23
+ sensor_type : `int`
24
+ Type of the sensor that is specified in 'sensor_id'.
25
+ Must be one of the following pre-defined constants:
26
+
27
+ - SENSOR_TYPE_NODE_PRESSURE = 1
28
+ - SENSOR_TYPE_NODE_QUALITY = 2
29
+ - SENSOR_TYPE_NODE_DEMAND = 3
30
+ - SENSOR_TYPE_LINK_FLOW = 4
31
+ - SENSOR_TYPE_LINK_QUALITY = 5
32
+ - SENSOR_TYPE_VALVE_STATE = 6
33
+ - SENSOR_TYPE_PUMP_STATE = 7
34
+ - SENSOR_TYPE_TANK_VOLUME = 8
35
+ - SENSOR_TYPE_NODE_BULK_SPECIES = 9
36
+ - SENSOR_TYPE_NODE_LINK_SPECIES = 10
37
+ - SENSOR_TYPE_SURFACE_SPECIES = 11
38
+ """
39
+ def __init__(self, sensor_id: str, sensor_type: int, **kwds):
40
+ if not isinstance(sensor_id, str):
41
+ raise TypeError("'sensor_id' must be an instance of 'str' but not of " +
42
+ f"'{type(sensor_id)}'")
43
+ if not isinstance(sensor_type, int):
44
+ raise TypeError("'sensor_type' mut be an instance of 'int' but not of " +
45
+ f"'{type(sensor_type)}'")
46
+ if not 1 <= sensor_type <= 10:
47
+ raise ValueError("Invalid value of 'sensor_type'")
48
+
49
+ self.__sensor_id = sensor_id
50
+ self.__sensor_type = sensor_type
51
+
52
+ super().__init__(**kwds)
53
+
54
+ def validate(self, sensor_config: SensorConfig) -> None:
55
+ """
56
+ Validates this sensor reading event -- i.e. checks whether the affected
57
+ sensor is part of the given sensor configuration.
58
+
59
+ Parameters
60
+ ----------
61
+ sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
62
+ Sensor configuration.
63
+ """
64
+ if not isinstance(sensor_config, SensorConfig):
65
+ raise TypeError("'sensor_config' must be an instance of " +
66
+ "'epyt_flow.simulation.SensorConfig' but not of " +
67
+ f"'{type(sensor_config)}'")
68
+
69
+ def __show_warning() -> None:
70
+ warnings.warn("Event does not have any effect because there is " +
71
+ f"no sensor at '{self.__sensor_id}'")
72
+
73
+ if self.__sensor_type == SENSOR_TYPE_NODE_PRESSURE:
74
+ if self.__sensor_id not in sensor_config.pressure_sensors:
75
+ __show_warning()
76
+ elif self.__sensor_type == SENSOR_TYPE_NODE_QUALITY:
77
+ if self.__sensor_id not in sensor_config.quality_node_sensors:
78
+ __show_warning()
79
+ elif self.__sensor_type == SENSOR_TYPE_NODE_DEMAND:
80
+ if self.__sensor_id not in sensor_config.demand_sensors:
81
+ __show_warning()
82
+ elif self.__sensor_type == SENSOR_TYPE_LINK_FLOW:
83
+ if self.__sensor_id not in sensor_config.flow_sensors:
84
+ __show_warning()
85
+ elif self.__sensor_type == SENSOR_TYPE_LINK_QUALITY:
86
+ if self.__sensor_id not in sensor_config.quality_link_sensors:
87
+ __show_warning()
88
+ elif self.__sensor_type == SENSOR_TYPE_VALVE_STATE:
89
+ if self.__sensor_id not in sensor_config.valve_state_sensors:
90
+ __show_warning()
91
+ elif self.__sensor_type == SENSOR_TYPE_PUMP_STATE:
92
+ if self.__sensor_id not in sensor_config.pump_state_sensors:
93
+ __show_warning()
94
+ elif self.__sensor_type == SENSOR_TYPE_TANK_VOLUME:
95
+ if self.__sensor_id not in sensor_config.tank_volume_sensors:
96
+ __show_warning()
97
+ elif self.__sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
98
+ if self.__sensor_id not in sensor_config.bulk_species_node_sensors:
99
+ __show_warning()
100
+ elif self.__sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
101
+ if self.__sensor_id not in sensor_config.bulk_species_link_sensors:
102
+ __show_warning()
103
+ elif self.__sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
104
+ if self.__sensor_id not in sensor_config.surface_species_sensors:
105
+ __show_warning()
106
+
107
+ @property
108
+ def sensor_id(self) -> str:
109
+ """
110
+ Gets the ID of the node or link that is affected by this event.
111
+
112
+ Returns
113
+ -------
114
+ `str`
115
+ Node or link ID.
116
+ """
117
+ return self.__sensor_id
118
+
119
+ @property
120
+ def sensor_type(self) -> int:
121
+ """
122
+ Gets the sensor type code.
123
+
124
+ Returns
125
+ -------
126
+ `int`
127
+ Sensor type code.
128
+ """
129
+ return self.__sensor_type
130
+
131
+ def get_attributes(self) -> dict:
132
+ return super().get_attributes() | {"sensor_id": self.__sensor_id,
133
+ "sensor_type": self.__sensor_type}
134
+
135
+ def __eq__(self, other) -> bool:
136
+ if not isinstance(other, SensorReadingEvent):
137
+ raise TypeError("Can not compare 'SensorReadingEvent' instance " +
138
+ f"with '{type(other)}' instance")
139
+
140
+ return super().__eq__(other) and self.__sensor_id == other.sensor_id \
141
+ and self.__sensor_type == other.sensor_type
142
+
143
+ def __str__(self) -> str:
144
+ return f"{super().__str__()} sensor_id: {self.__sensor_id} " +\
145
+ f"sensor_type: {self.__sensor_type}"
146
+
147
+ def __call__(self, sensor_readings: numpy.ndarray,
148
+ sensor_readings_time: numpy.ndarray) -> numpy.ndarray:
149
+ return self.apply(sensor_readings, sensor_readings_time)
150
+
151
+ @abstractmethod
152
+ def apply(self, sensor_readings: numpy.ndarray,
153
+ sensor_readings_time: numpy.ndarray) -> numpy.ndarray:
154
+ """
155
+ Applies the sensor reading event to sensor reading values -- i.e.
156
+ modify the sensor readings.
157
+
158
+ Parameters
159
+ ----------
160
+ sensor_readings : `numpy.ndarray`
161
+ Original sensor readings.
162
+ sensor_readings_time : `numpy.ndarray`
163
+ Time (seconds since simulation start) for each sensor reading row in 'sensor_readings'.
164
+
165
+ Returns
166
+ -------
167
+ `numpy.ndarray`
168
+ Modified sensor readings.
169
+ """
170
+ raise NotImplementedError()