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,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()
|