epyt-flow 0.10.0__py3-none-any.whl → 0.12.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 (42) hide show
  1. epyt_flow/VERSION +1 -1
  2. epyt_flow/data/benchmarks/gecco_water_quality.py +2 -2
  3. epyt_flow/data/benchmarks/leakdb.py +40 -5
  4. epyt_flow/data/benchmarks/water_usage.py +4 -3
  5. epyt_flow/data/networks.py +27 -14
  6. epyt_flow/gym/__init__.py +0 -3
  7. epyt_flow/gym/scenario_control_env.py +11 -13
  8. epyt_flow/rest_api/scenario/control_handlers.py +118 -0
  9. epyt_flow/rest_api/scenario/event_handlers.py +114 -1
  10. epyt_flow/rest_api/scenario/handlers.py +33 -0
  11. epyt_flow/rest_api/server.py +14 -2
  12. epyt_flow/serialization.py +1 -0
  13. epyt_flow/simulation/__init__.py +0 -1
  14. epyt_flow/simulation/backend/__init__.py +1 -0
  15. epyt_flow/simulation/backend/my_epyt.py +1056 -0
  16. epyt_flow/simulation/events/actuator_events.py +7 -1
  17. epyt_flow/simulation/events/quality_events.py +3 -1
  18. epyt_flow/simulation/scada/scada_data.py +716 -5
  19. epyt_flow/simulation/scenario_config.py +1 -40
  20. epyt_flow/simulation/scenario_simulator.py +645 -119
  21. epyt_flow/simulation/sensor_config.py +18 -2
  22. epyt_flow/topology.py +24 -7
  23. epyt_flow/uncertainty/model_uncertainty.py +80 -62
  24. epyt_flow/uncertainty/sensor_noise.py +15 -4
  25. epyt_flow/uncertainty/uncertainties.py +71 -18
  26. epyt_flow/uncertainty/utils.py +40 -13
  27. epyt_flow/utils.py +45 -1
  28. epyt_flow/visualization/__init__.py +2 -0
  29. epyt_flow/visualization/scenario_visualizer.py +1240 -0
  30. epyt_flow/visualization/visualization_utils.py +738 -0
  31. {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/METADATA +15 -4
  32. {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +35 -36
  33. {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/WHEEL +1 -1
  34. epyt_flow/gym/control_gyms.py +0 -47
  35. epyt_flow/metrics.py +0 -466
  36. epyt_flow/models/__init__.py +0 -2
  37. epyt_flow/models/event_detector.py +0 -31
  38. epyt_flow/models/sensor_interpolation_detector.py +0 -118
  39. epyt_flow/simulation/scada/advanced_control.py +0 -138
  40. epyt_flow/simulation/scenario_visualizer.py +0 -1307
  41. {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info/licenses}/LICENSE +0 -0
  42. {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/top_level.txt +0 -0
epyt_flow/metrics.py DELETED
@@ -1,466 +0,0 @@
1
- """
2
- This module provides different metrics for evaluation.
3
- """
4
- import numpy as np
5
- from sklearn.metrics import roc_auc_score as skelarn_roc_auc_score, f1_score as skelarn_f1_scpre, \
6
- mean_absolute_error, root_mean_squared_error, r2_score as sklearn_r2_score
7
-
8
-
9
- def r2_score(y_pred: np.ndarray, y: np.ndarray) -> float:
10
- """
11
- Computes the R^2 score (also called the coefficient of determination).
12
-
13
- Parameters
14
- ----------
15
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
16
- Predicted outputs.
17
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
18
- Ground truth outputs.
19
-
20
- Returns
21
- -------
22
- `float`
23
- R^2 score.
24
- """
25
- return sklearn_r2_score(y, y_pred)
26
-
27
-
28
- def running_r2_score(y_pred: np.ndarray, y: np.ndarray) -> list[float]:
29
- """
30
- Computes and returns the running R^2 score -- i.e. the R^2 score for every point in time.
31
-
32
- Parameters
33
- ----------
34
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
35
- Predicted outputs.
36
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
37
- Ground truth outputs.
38
-
39
- Returns
40
- -------
41
- `list[float]`
42
- The running R^2 score.
43
- """
44
- r = []
45
-
46
- for t in range(2, len(y_pred)):
47
- r.append(r2_score(y_pred[:t], y[:t]))
48
-
49
- return r
50
-
51
-
52
- def mean_squared_error(y_pred: np.ndarray, y: np.ndarray) -> float:
53
- """
54
- Computes the Mean Squared Error (MSE).
55
-
56
- Parameters
57
- ----------
58
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
59
- Predicted outputs.
60
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
61
- Ground truth outputs.
62
-
63
- Returns
64
- -------
65
- `float`
66
- MSE.
67
- """
68
- return root_mean_squared_error(y, y_pred)**2
69
-
70
-
71
- def running_mse(y_pred: np.ndarray, y: np.ndarray) -> list[float]:
72
- """
73
- Computes the running Mean Squared Error (MSE) -- i.e. the MSE for every point in time.
74
-
75
- Parameters
76
- ----------
77
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
78
- Predicted outputs.
79
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
80
- Ground truth outputs.
81
-
82
- Returns
83
- -------
84
- `float`
85
- Running MSE.
86
- """
87
- if not isinstance(y_pred, np.ndarray):
88
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
89
- f"but not of '{type(y_pred)}'")
90
- if not isinstance(y, np.ndarray):
91
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
92
- f"but not of '{type(y)}'")
93
- if y_pred.shape != y.shape:
94
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
95
- if len(y_pred.shape) != 1:
96
- raise ValueError("'y_pred' must be a 1d array")
97
- if len(y.shape) != 1:
98
- raise ValueError("'y' must be a 1d array")
99
-
100
- e_sq = np.square(y - y_pred)
101
- r_mse = list(esq for esq in e_sq)
102
-
103
- for i in range(1, len(y)):
104
- r_mse[i] = float((i * r_mse[i - 1]) / (i + 1)) + (r_mse[i] / (i + 1))
105
-
106
- return r_mse
107
-
108
-
109
- def mape(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
110
- """
111
- Computes the Mean Absolute Percentage Error (MAPE).
112
-
113
- Parameters
114
- ----------
115
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
116
- Predicted outputs.
117
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
118
- Ground truth outputs.
119
- epsilon : `float`, optional
120
- Small number added to predictions and ground truth to avoid division-by-zero.
121
-
122
- The default is 0.05
123
-
124
- Returns
125
- -------
126
- `float`
127
- MAPE score.
128
- """
129
- if not isinstance(y_pred, np.ndarray):
130
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
131
- f"but not of '{type(y_pred)}'")
132
- if not isinstance(y, np.ndarray):
133
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
134
- f"but not of '{type(y)}'")
135
- if not isinstance(epsilon, float):
136
- raise TypeError("'epsilon' must be an instance of 'float' " +
137
- f"but not of '{type(epsilon)}'")
138
- if y_pred.shape != y.shape:
139
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
140
- if len(y_pred.shape) != 1:
141
- raise ValueError("'y_pred' must be a 1d array")
142
- if len(y.shape) != 1:
143
- raise ValueError("'y' must be a 1d array")
144
-
145
- y_ = y + epsilon
146
- y_pred_ = y_pred + epsilon
147
- return np.mean(np.abs((y_ - y_pred_) / y_))
148
-
149
-
150
- def smape(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
151
- """
152
- Computes the Symmetric Mean Absolute Percentage Error (SMAPE).
153
-
154
- Parameters
155
- ----------
156
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
157
- Predicted outputs.
158
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
159
- Ground truth outputs.
160
- epsilon : `float`, optional
161
- Small number added to predictions and ground truth to avoid division-by-zero.
162
-
163
- The default is 0.05
164
-
165
- Returns
166
- -------
167
- `float`
168
- SMAPE score.
169
- """
170
- if not isinstance(y_pred, np.ndarray):
171
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
172
- f"but not of '{type(y_pred)}'")
173
- if not isinstance(y, np.ndarray):
174
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
175
- f"but not of '{type(y)}'")
176
- if not isinstance(epsilon, float):
177
- raise TypeError("'epsilon' must be an instance of 'float' " +
178
- f"but not of '{type(epsilon)}'")
179
- if y_pred.shape != y.shape:
180
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
181
- if len(y_pred.shape) != 1:
182
- raise ValueError("'y_pred' must be a 1d array")
183
- if len(y.shape) != 1:
184
- raise ValueError("'y' must be a 1d array")
185
-
186
- y_ = y + epsilon
187
- y_pred_ = y_pred + epsilon
188
- return 2. * np.mean(np.abs(y_ - y_pred_) / (np.abs(y_) + np.abs(y_pred_)))
189
-
190
-
191
- def mase(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
192
- """
193
- Computes the Mean Absolute Scaled Error (MASE).
194
-
195
- Parameters
196
- ----------
197
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
198
- Predicted outputs.
199
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
200
- Ground truth outputs.
201
- epsilon : `float`, optional
202
- Small number added to predictions and ground truth to avoid division-by-zero.
203
-
204
- The default is 0.05
205
-
206
- Returns
207
- -------
208
- `float`
209
- MASE score.
210
- """
211
- if not isinstance(y_pred, np.ndarray):
212
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
213
- f"but not of '{type(y_pred)}'")
214
- if not isinstance(y, np.ndarray):
215
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
216
- f"but not of '{type(y)}'")
217
- if not isinstance(epsilon, float):
218
- raise TypeError("'epsilon' must be an instance of 'float' " +
219
- f"but not of '{type(epsilon)}'")
220
- if y_pred.shape != y.shape:
221
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
222
- if len(y_pred.shape) != 1:
223
- raise ValueError("'y_pred' must be a 1d array")
224
- if len(y.shape) != 1:
225
- raise ValueError("'y' must be a 1d array")
226
-
227
- try:
228
- y_ = y + epsilon
229
- y_pred_ = y_pred + epsilon
230
-
231
- mae = mean_absolute_error(y_, y_pred_)
232
- naive_error = np.mean(np.abs(y_[1:] - y_pred_[:-1]))
233
- return mae / naive_error
234
- except Exception:
235
- return None
236
-
237
-
238
- def f1_micro_score(y_pred: np.ndarray, y: np.ndarray) -> float:
239
- """
240
- Computes the F1 score using for a multi-class classification by
241
- counting the total true positives, false negatives and false positives.
242
-
243
- Parameters
244
- ----------
245
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
246
- Predicted labels.
247
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
248
- Ground truth labels.
249
-
250
- Returns
251
- -------
252
- `float`
253
- F1 score.
254
- """
255
- if not isinstance(y_pred, np.ndarray):
256
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
257
- f"but not of '{type(y_pred)}'")
258
- if not isinstance(y, np.ndarray):
259
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
260
- f"but not of '{type(y)}'")
261
- if y_pred.shape != y.shape:
262
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
263
-
264
- return skelarn_f1_scpre(y, y_pred, average="micro")
265
-
266
-
267
- def roc_auc_score(y_pred: np.ndarray, y: np.ndarray) -> float:
268
- """
269
- Computes the Area Under the Curve (AUC) of a classification.
270
-
271
- Parameters
272
- ----------
273
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
274
- Predicted labels.
275
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
276
- Ground truth labels.
277
-
278
- Returns
279
- -------
280
- `float`
281
- ROC AUC score.
282
- """
283
- if not isinstance(y_pred, np.ndarray):
284
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
285
- f"but not of '{type(y_pred)}'")
286
- if not isinstance(y, np.ndarray):
287
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
288
- f"but not of '{type(y)}'")
289
- if y_pred.shape != y.shape:
290
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
291
-
292
- return skelarn_roc_auc_score(y, y_pred)
293
-
294
-
295
- def true_positive_rate(y_pred: np.ndarray, y: np.ndarray) -> float:
296
- """
297
- Computes the true positive rate (also called sensitivity).
298
-
299
- Parameters
300
- ----------
301
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
302
- Predicted labels.
303
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
304
- Ground truth labels.
305
-
306
- Returns
307
- -------
308
- `float`
309
- True positive rate.
310
- """
311
- if not isinstance(y_pred, np.ndarray):
312
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
313
- f"but not of '{type(y_pred)}'")
314
- if not isinstance(y, np.ndarray):
315
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
316
- f"but not of '{type(y)}'")
317
- if y_pred.shape != y.shape:
318
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
319
- if len(y_pred.shape) != 1:
320
- raise ValueError("'y_pred' must be a 1d array")
321
- if len(y.shape) != 1:
322
- raise ValueError("'y' must be a 1d array")
323
- if set(np.unique(y_pred)) != set([0, 1]):
324
- raise ValueError("Labels must be either '0' or '1'")
325
-
326
- tp = np.sum((y == 1) & (y_pred == 1))
327
- fn = np.sum((y == 1) & (y_pred == 0))
328
-
329
- return tp / (tp + fn)
330
-
331
-
332
- def true_negative_rate(y_pred: np.ndarray, y: np.ndarray) -> float:
333
- """
334
- Computes the true negative rate (also called specificity).
335
-
336
- Parameters
337
- ----------
338
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
339
- Predicted labels.
340
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
341
- Ground truth labels.
342
-
343
- Returns
344
- -------
345
- `float`
346
- True negative rate.
347
- """
348
- if not isinstance(y_pred, np.ndarray):
349
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
350
- f"but not of '{type(y_pred)}'")
351
- if not isinstance(y, np.ndarray):
352
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
353
- f"but not of '{type(y)}'")
354
- if y_pred.shape != y.shape:
355
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
356
- if len(y_pred.shape) > 1:
357
- raise ValueError("'y_pred' must be a 1d array")
358
- if len(y.shape) > 1:
359
- raise ValueError("'y' must be a 1d array")
360
- if set(np.unique(y_pred)) != set([0, 1]):
361
- raise ValueError("Labels must be either '0' or '1'")
362
-
363
- tn = np.sum((y == 0) & (y_pred == 0))
364
- fp = np.sum((y == 0) & (y_pred == 1))
365
-
366
- return tn / (tn + fp)
367
-
368
-
369
- def precision_score(y_pred: np.ndarray, y: np.ndarray) -> float:
370
- """
371
- Computes the precision of a classification.
372
-
373
- Parameters
374
- ----------
375
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
376
- Predicted labels.
377
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
378
- Ground truth labels.
379
-
380
- Returns
381
- -------
382
- `float`
383
- Precision score.
384
- """
385
- if not isinstance(y_pred, np.ndarray):
386
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
387
- f"but not of '{type(y_pred)}'")
388
- if not isinstance(y, np.ndarray):
389
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
390
- f"but not of '{type(y)}'")
391
- if y_pred.shape != y.shape:
392
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
393
- if set(np.unique(y_pred)) != set([0, 1]):
394
- raise ValueError("Labels must be either '0' or '1'")
395
-
396
- tp = np.sum([np.all((y[i] == 1) & (y_pred[i] == 1)) for i in range(len(y))])
397
- fp = np.sum([np.any((y[i] == 0) & (y_pred[i] == 1)) for i in range(len(y))])
398
-
399
- return tp / (tp + fp)
400
-
401
-
402
- def accuracy_score(y_pred: np.ndarray, y: np.ndarray) -> float:
403
- """
404
- Computes the accuracy of a classification.
405
-
406
- Parameters
407
- ----------
408
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
409
- Predicted labels.
410
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
411
- Ground truth labels.
412
-
413
- Returns
414
- -------
415
- `float`
416
- Accuracy score.
417
- """
418
- if not isinstance(y_pred, np.ndarray):
419
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
420
- f"but not of '{type(y_pred)}'")
421
- if not isinstance(y, np.ndarray):
422
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
423
- f"but not of '{type(y)}'")
424
- if y_pred.shape != y.shape:
425
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
426
-
427
- tp = np.sum([np.all(y[i] == y_pred[i]) for i in range(len(y))])
428
- return tp / len(y)
429
-
430
-
431
- def f1_score(y_pred: np.ndarray, y: np.ndarray) -> float:
432
- """
433
- Computes the F1-score for a binary classification.
434
-
435
- Parameters
436
- ----------
437
- y_pred : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
438
- Predicted labels.
439
- y : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
440
- Ground truth labels.
441
-
442
- Returns
443
- -------
444
- `float`
445
- F1-score.
446
- """
447
- if not isinstance(y_pred, np.ndarray):
448
- raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
449
- f"but not of '{type(y_pred)}'")
450
- if not isinstance(y, np.ndarray):
451
- raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
452
- f"but not of '{type(y)}'")
453
- if y_pred.shape != y.shape:
454
- raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
455
- if len(y_pred.shape) != 1:
456
- raise ValueError("'y_pred' must be a 1d array")
457
- if len(y.shape) != 1:
458
- raise ValueError("'y' must be a 1d array")
459
- if set(np.unique(y_pred)) != set([0, 1]):
460
- raise ValueError("Labels must be either '0' or '1'")
461
-
462
- tp = np.sum((y == 1) & (y_pred == 1))
463
- fp = np.sum((y == 0) & (y_pred == 1))
464
- fn = np.sum((y == 1) & (y_pred == 0))
465
-
466
- return (2. * tp) / (2. * tp + fp + fn)
@@ -1,2 +0,0 @@
1
- from .event_detector import *
2
- from .sensor_interpolation_detector import *
@@ -1,31 +0,0 @@
1
- """
2
- Module provides a base class for event detectors.
3
- """
4
- from abc import abstractmethod, ABC
5
-
6
- from ..simulation.scada import ScadaData
7
-
8
-
9
- class EventDetector(ABC):
10
- """
11
- Base class for event detectors.
12
- """
13
- def __init__(self, **kwds):
14
- super().__init__(**kwds)
15
-
16
- @abstractmethod
17
- def apply(self, scada_data: ScadaData) -> list[int]:
18
- """
19
- Applies this detector to given SCADA data and returns suspicious time points.
20
-
21
- Parameters
22
- ----------
23
- scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
24
- SCADA data in which to look for events (i.e. anomalies).
25
-
26
- Returns
27
- -------
28
- `list[int]`
29
- List of suspicious time points.
30
- """
31
- raise NotImplementedError()
@@ -1,118 +0,0 @@
1
- """
2
- Module provides a simple residual-based event detector that performs sensor interpolation.
3
- """
4
- from typing import Any, Union
5
- from copy import deepcopy
6
- import numpy as np
7
- from sklearn.linear_model import LinearRegression
8
-
9
- from .event_detector import EventDetector
10
- from ..simulation.scada import ScadaData
11
-
12
-
13
- class SensorInterpolationDetector(EventDetector):
14
- """
15
- Class implementing a residual-based event detector based on sensor interpolation.
16
-
17
- Parameters
18
- ----------
19
- regressor_type : `Any`, optional
20
- Regressor class that will be used for the sensor interpolation.
21
- Must implement the usual `fit` and `predict` functions.
22
-
23
- The default is `sklearn.linear_model.LinearRegression <https://scikit-learn.org/dev/modules/generated/sklearn.linear_model.LinearRegression.html>`_
24
- """
25
- def __init__(self, regressor_type: Any = LinearRegression, **kwds):
26
- self.__regressor_type = regressor_type
27
- self.__regressors = []
28
-
29
- super().__init__(**kwds)
30
-
31
- @property
32
- def regressor_type(self) -> Any:
33
- """
34
- Gets the class used for building the regressors in the sensor interpolation.
35
-
36
- Returns
37
- -------
38
- `Any`
39
- Regressor class.
40
- """
41
- return self.__regressor_type
42
-
43
- @property
44
- def regressors(self) -> list[Any]:
45
- """
46
- Gets the fitted sensor interpolation regressors.
47
-
48
- Returns
49
- -------
50
- `list[Any]`
51
- Fitted regressors.
52
- """
53
- return deepcopy(self.__regressors)
54
-
55
- def __eq__(self, other) -> bool:
56
- return self.__regressor_type == other.regressor_type and \
57
- all(self.__regressors == other.regressors)
58
-
59
- def fit(self, scada_data: Union[ScadaData, np.ndarray]) -> None:
60
- """
61
- Fit detector to given SCADA data -- assuming the given data represents
62
- the normal operating state.
63
-
64
- Parameters
65
- ----------
66
- scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
67
- SCADA data to fit this detector.
68
- """
69
- if isinstance(scada_data, ScadaData):
70
- data = scada_data.get_data()
71
- else:
72
- data = scada_data
73
-
74
- self.__regressors = []
75
- for output_idx in range(data.shape[1]):
76
- input_idx = list(range(data.shape[1]))
77
- input_idx.remove(output_idx)
78
-
79
- X = data[:, input_idx]
80
- y = data[:, output_idx]
81
-
82
- model = self.__regressor_type()
83
- model.fit(X, y)
84
-
85
- y_pred = model.predict(X)
86
- threshold = 1.2 * np.max(np.abs(y_pred - y))
87
-
88
- self.__regressors.append((input_idx, output_idx, model, threshold))
89
-
90
- def apply(self, scada_data: Union[ScadaData, np.ndarray]) -> list[int]:
91
- """
92
- Applies this detector to given SCADA data and returns suspicious time points.
93
-
94
- Parameters
95
- ----------
96
- scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` or `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
97
- SCADA data in which to look for events/anomalies.
98
-
99
- Returns
100
- -------
101
- `list[int]`
102
- List of suspicious time points.
103
- """
104
- suspicious_time_points = []
105
-
106
- if isinstance(scada_data, ScadaData):
107
- X = scada_data.get_data()
108
- else:
109
- X = scada_data
110
-
111
- for input_idx, output_idx, model, threshold in self.__regressors:
112
- y_pred = model.predict(X[:, input_idx])
113
- y = X[:, output_idx]
114
-
115
- suspicious_time_points += list(np.argwhere(np.abs(y_pred - y) > threshold).
116
- flatten())
117
-
118
- return list(set(suspicious_time_points))