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