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,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides functions for registering and creating control environments.
|
|
3
|
+
"""
|
|
4
|
+
from .scenario_control_env import ScenarioControlEnv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
environments = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(env_name: str, env: ScenarioControlEnv) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Registers a new environment under a given name.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
env_name : `str`
|
|
17
|
+
Name of the environment -- must be unique among all environments.
|
|
18
|
+
env : :class:`epyt_flow.gym.scenario_control_env.ScenarioControlEnv`
|
|
19
|
+
Environment.
|
|
20
|
+
"""
|
|
21
|
+
if env_name in environments:
|
|
22
|
+
raise ValueError(f"Environment '{env_name}' already exists.")
|
|
23
|
+
if not issubclass(env, ScenarioControlEnv):
|
|
24
|
+
raise TypeError("'env' must be a subclass of " +
|
|
25
|
+
"'epyt_flow.gym.ScenarioControlEnv'")
|
|
26
|
+
|
|
27
|
+
environments[env_name] = env
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make(env_name: str, **kwds) -> ScenarioControlEnv:
|
|
31
|
+
"""
|
|
32
|
+
Creates an instance of a registered environment.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
env_name : `str`
|
|
37
|
+
Name of the environment.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
:class:`epyt_flow.gym.scenario_control_env.ScenarioControlEnv`
|
|
42
|
+
Environment.
|
|
43
|
+
"""
|
|
44
|
+
if env_name not in environments:
|
|
45
|
+
raise ValueError(f"Unknown environment '{env_name}'.")
|
|
46
|
+
|
|
47
|
+
return environments[env_name](**kwds)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a base class for control environments.
|
|
3
|
+
"""
|
|
4
|
+
from abc import abstractmethod, ABC
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
|
|
7
|
+
from ..simulation import ScenarioSimulator, ScenarioConfig, ScadaData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScenarioControlEnv(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Base class for a control environment challenge.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
scenario_config : :class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
|
|
17
|
+
Scenario configuration.
|
|
18
|
+
autoreset : `bool`, optional
|
|
19
|
+
If True, environment is automatically reset if terminated.
|
|
20
|
+
|
|
21
|
+
The default is False.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, scenario_config: ScenarioConfig, autoreset: bool = False, **kwds):
|
|
24
|
+
self.__scenario_config = scenario_config
|
|
25
|
+
self._scenario_sim = None
|
|
26
|
+
self._sim_generator = None
|
|
27
|
+
self.__autoreset = autoreset
|
|
28
|
+
|
|
29
|
+
super().__init__(**kwds)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def autoreset(self) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
True, if environment automatically resets after it terminated.
|
|
35
|
+
"""
|
|
36
|
+
return deepcopy(self.__autoreset)
|
|
37
|
+
|
|
38
|
+
def __enter__(self):
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def __exit__(self, *args):
|
|
42
|
+
self.close()
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Frees all resources.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
if self._sim_generator is not None:
|
|
50
|
+
self._sim_generator.send(True)
|
|
51
|
+
next(self._sim_generator)
|
|
52
|
+
except StopIteration:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
if self._scenario_sim is not None:
|
|
56
|
+
self._scenario_sim.close()
|
|
57
|
+
|
|
58
|
+
def reset(self) -> ScadaData:
|
|
59
|
+
"""
|
|
60
|
+
Resets the environment (i.e. simulation).
|
|
61
|
+
"""
|
|
62
|
+
if self._scenario_sim is not None:
|
|
63
|
+
self._scenario_sim.close()
|
|
64
|
+
|
|
65
|
+
self._scenario_sim = ScenarioSimulator(
|
|
66
|
+
scenario_config=self.__scenario_config)
|
|
67
|
+
self._sim_generator = self._scenario_sim.run_simulation_as_generator(support_abort=True)
|
|
68
|
+
|
|
69
|
+
return self._next_sim_itr()
|
|
70
|
+
|
|
71
|
+
def _next_sim_itr(self) -> ScadaData:
|
|
72
|
+
try:
|
|
73
|
+
next(self._sim_generator)
|
|
74
|
+
r = self._sim_generator.send(False)
|
|
75
|
+
|
|
76
|
+
if self.autoreset is True:
|
|
77
|
+
return r
|
|
78
|
+
else:
|
|
79
|
+
return r, False
|
|
80
|
+
except StopIteration:
|
|
81
|
+
if self.__autoreset is True:
|
|
82
|
+
return self.reset()
|
|
83
|
+
else:
|
|
84
|
+
return None, True
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def step(self) -> tuple[ScadaData, float, bool]:
|
|
88
|
+
"""
|
|
89
|
+
Performs the next step by applying an action and observing
|
|
90
|
+
the consequences (SCADA data, reward, terminated).
|
|
91
|
+
|
|
92
|
+
Note that `terminated` is only returned if `autoreset=False` otherwise
|
|
93
|
+
only SCADA data and reward are returned.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
`(ScadaData, float, bool)`
|
|
98
|
+
Triple of observations (:class:`~epyt_flow.simuation.scada.scada_data.ScadaData`),
|
|
99
|
+
reward (`float`), and terminated (`bool`).
|
|
100
|
+
"""
|
|
101
|
+
raise NotImplementedError()
|
epyt_flow/metrics.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
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
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def running_mse(y_pred: np.ndarray, y: np.ndarray):
|
|
10
|
+
"""
|
|
11
|
+
Computes the running Mean Squared Error (MSE).
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
y_pred : `numpy.ndarray`
|
|
16
|
+
Predicted outputs.
|
|
17
|
+
y : `numpy.ndarray`
|
|
18
|
+
Ground truth outputs.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
`float`
|
|
23
|
+
Running MSE.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(y_pred, np.ndarray):
|
|
26
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
27
|
+
f"but not of '{type(y_pred)}'")
|
|
28
|
+
if not isinstance(y, np.ndarray):
|
|
29
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
30
|
+
f"but not of '{type(y)}'")
|
|
31
|
+
if y_pred.shape != y.shape:
|
|
32
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
33
|
+
if len(y_pred.shape) != 1:
|
|
34
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
35
|
+
if len(y.shape) != 1:
|
|
36
|
+
raise ValueError("'y' must be a 1d array")
|
|
37
|
+
|
|
38
|
+
e_sq = np.square(y - y_pred)
|
|
39
|
+
r_mse = list(esq for esq in e_sq)
|
|
40
|
+
|
|
41
|
+
for i in range(1, len(y)):
|
|
42
|
+
r_mse[i] = ((i * r_mse[i - 1]) / (i + 1)) + (r_mse[i] / (i + 1))
|
|
43
|
+
|
|
44
|
+
return r_mse
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mape(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
|
|
48
|
+
"""
|
|
49
|
+
Computes the Mean Absolute Percentage Error (MAPE).
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
y_pred : `numpy.ndarray`
|
|
54
|
+
Predicted outputs.
|
|
55
|
+
y : `numpy.ndarray`
|
|
56
|
+
Ground truth outputs.
|
|
57
|
+
epsilon : `float`, optional
|
|
58
|
+
Small number added to predictions and ground truth to avoid division-by-zero.
|
|
59
|
+
|
|
60
|
+
The default is 0.05
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
`float`
|
|
65
|
+
MAPE score.
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(y_pred, np.ndarray):
|
|
68
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
69
|
+
f"but not of '{type(y_pred)}'")
|
|
70
|
+
if not isinstance(y, np.ndarray):
|
|
71
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
72
|
+
f"but not of '{type(y)}'")
|
|
73
|
+
if not isinstance(epsilon, float):
|
|
74
|
+
raise TypeError("'epsilon' must be an instance of 'float' " +
|
|
75
|
+
f"but not of '{type(epsilon)}'")
|
|
76
|
+
if y_pred.shape != y.shape:
|
|
77
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
78
|
+
if len(y_pred.shape) != 1:
|
|
79
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
80
|
+
if len(y.shape) != 1:
|
|
81
|
+
raise ValueError("'y' must be a 1d array")
|
|
82
|
+
|
|
83
|
+
y_ = y + epsilon
|
|
84
|
+
y_pred_ = y_pred + epsilon
|
|
85
|
+
return np.mean(np.abs((y_ - y_pred_) / y_))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def smape(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
|
|
89
|
+
"""
|
|
90
|
+
Computes the Symmetric Mean Absolute Percentage Error (SMAPE).
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
y_pred : `numpy.ndarray`
|
|
95
|
+
Predicted outputs.
|
|
96
|
+
y : `numpy.ndarray`
|
|
97
|
+
Ground truth outputs.
|
|
98
|
+
epsilon : `float`, optional
|
|
99
|
+
Small number added to predictions and ground truth to avoid division-by-zero.
|
|
100
|
+
|
|
101
|
+
The default is 0.05
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
`float`
|
|
106
|
+
SMAPE score.
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(y_pred, np.ndarray):
|
|
109
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
110
|
+
f"but not of '{type(y_pred)}'")
|
|
111
|
+
if not isinstance(y, np.ndarray):
|
|
112
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
113
|
+
f"but not of '{type(y)}'")
|
|
114
|
+
if not isinstance(epsilon, float):
|
|
115
|
+
raise TypeError("'epsilon' must be an instance of 'float' " +
|
|
116
|
+
f"but not of '{type(epsilon)}'")
|
|
117
|
+
if y_pred.shape != y.shape:
|
|
118
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
119
|
+
if len(y_pred.shape) != 1:
|
|
120
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
121
|
+
if len(y.shape) != 1:
|
|
122
|
+
raise ValueError("'y' must be a 1d array")
|
|
123
|
+
|
|
124
|
+
y_ = y + epsilon
|
|
125
|
+
y_pred_ = y_pred + epsilon
|
|
126
|
+
return 2. * np.mean(np.abs(y_ - y_pred_) / (np.abs(y_) + np.abs(y_pred_)))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def mase(y_pred: np.ndarray, y: np.ndarray, epsilon: float = .05) -> float:
|
|
130
|
+
"""
|
|
131
|
+
Computes the Mean Absolute Scaled Error (MASE).
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
y_pred : `numpy.ndarray`
|
|
136
|
+
Predicted outputs.
|
|
137
|
+
y : `numpy.ndarray`
|
|
138
|
+
Ground truth outputs.
|
|
139
|
+
epsilon : `float`, optional
|
|
140
|
+
Small number added to predictions and ground truth to avoid division-by-zero.
|
|
141
|
+
|
|
142
|
+
The default is 0.05
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
`float`
|
|
147
|
+
MASE score.
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(y_pred, np.ndarray):
|
|
150
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
151
|
+
f"but not of '{type(y_pred)}'")
|
|
152
|
+
if not isinstance(y, np.ndarray):
|
|
153
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
154
|
+
f"but not of '{type(y)}'")
|
|
155
|
+
if not isinstance(epsilon, float):
|
|
156
|
+
raise TypeError("'epsilon' must be an instance of 'float' " +
|
|
157
|
+
f"but not of '{type(epsilon)}'")
|
|
158
|
+
if y_pred.shape != y.shape:
|
|
159
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
160
|
+
if len(y_pred.shape) != 1:
|
|
161
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
162
|
+
if len(y.shape) != 1:
|
|
163
|
+
raise ValueError("'y' must be a 1d array")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
y_ = y + epsilon
|
|
167
|
+
y_pred_ = y_pred + epsilon
|
|
168
|
+
|
|
169
|
+
mae = mean_absolute_error(y_, y_pred_)
|
|
170
|
+
naive_error = np.mean(np.abs(y_[1:] - y_pred_[:-1]))
|
|
171
|
+
return mae / naive_error
|
|
172
|
+
except Exception:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def f1_micro_score(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
177
|
+
"""
|
|
178
|
+
Computes the F1 score using for a multi-class classification by
|
|
179
|
+
counting the total true positives, false negatives and false positives.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
y_pred : `numpy.ndarray`
|
|
184
|
+
Predicted labels.
|
|
185
|
+
y : `numpy.ndarray`
|
|
186
|
+
Ground truth labels.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
`float`
|
|
191
|
+
F1 score.
|
|
192
|
+
"""
|
|
193
|
+
if not isinstance(y_pred, np.ndarray):
|
|
194
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
195
|
+
f"but not of '{type(y_pred)}'")
|
|
196
|
+
if not isinstance(y, np.ndarray):
|
|
197
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
198
|
+
f"but not of '{type(y)}'")
|
|
199
|
+
if y_pred.shape != y.shape:
|
|
200
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
201
|
+
|
|
202
|
+
return skelarn_f1_scpre(y, y_pred, average="micro")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def roc_auc_score(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
206
|
+
"""
|
|
207
|
+
Computes the Area Under the Curve (AUC) of a classification.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
y_pred : `numpy.ndarray`
|
|
212
|
+
Predicted labels.
|
|
213
|
+
y : `numpy.ndarray`
|
|
214
|
+
Ground truth labels.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
`float`
|
|
219
|
+
ROC AUC score.
|
|
220
|
+
"""
|
|
221
|
+
if not isinstance(y_pred, np.ndarray):
|
|
222
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
223
|
+
f"but not of '{type(y_pred)}'")
|
|
224
|
+
if not isinstance(y, np.ndarray):
|
|
225
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
226
|
+
f"but not of '{type(y)}'")
|
|
227
|
+
if y_pred.shape != y.shape:
|
|
228
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
229
|
+
|
|
230
|
+
return skelarn_roc_auc_score(y, y_pred)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def true_positive_rate(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
234
|
+
"""
|
|
235
|
+
Computes the true positive rate (also called sensitivity).
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
y_pred : `numpy.ndarray`
|
|
240
|
+
Predicted labels.
|
|
241
|
+
y : `numpy.ndarray`
|
|
242
|
+
Ground truth labels.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
`float`
|
|
247
|
+
True positive rate.
|
|
248
|
+
"""
|
|
249
|
+
if not isinstance(y_pred, np.ndarray):
|
|
250
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
251
|
+
f"but not of '{type(y_pred)}'")
|
|
252
|
+
if not isinstance(y, np.ndarray):
|
|
253
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
254
|
+
f"but not of '{type(y)}'")
|
|
255
|
+
if y_pred.shape != y.shape:
|
|
256
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
257
|
+
if len(y_pred.shape) != 1:
|
|
258
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
259
|
+
if len(y.shape) != 1:
|
|
260
|
+
raise ValueError("'y' must be a 1d array")
|
|
261
|
+
if set(np.unique(y_pred)) != set([0, 1]):
|
|
262
|
+
raise ValueError("Labels must be either '0' or '1'")
|
|
263
|
+
|
|
264
|
+
tp = np.sum((y == 1) & (y_pred == 1))
|
|
265
|
+
fn = np.sum((y == 1) & (y_pred == 0))
|
|
266
|
+
|
|
267
|
+
return tp / (tp + fn)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def true_negative_rate(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
271
|
+
"""
|
|
272
|
+
Computes the true negative rate (also called specificity).
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
y_pred : `numpy.ndarray`
|
|
277
|
+
Predicted labels.
|
|
278
|
+
y : `numpy.ndarray`
|
|
279
|
+
Ground truth labels.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
`float`
|
|
284
|
+
True negative rate.
|
|
285
|
+
"""
|
|
286
|
+
if not isinstance(y_pred, np.ndarray):
|
|
287
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
288
|
+
f"but not of '{type(y_pred)}'")
|
|
289
|
+
if not isinstance(y, np.ndarray):
|
|
290
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
291
|
+
f"but not of '{type(y)}'")
|
|
292
|
+
if y_pred.shape != y.shape:
|
|
293
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
294
|
+
if len(y_pred.shape) > 1:
|
|
295
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
296
|
+
if len(y.shape) > 1:
|
|
297
|
+
raise ValueError("'y' must be a 1d array")
|
|
298
|
+
if set(np.unique(y_pred)) != set([0, 1]):
|
|
299
|
+
raise ValueError("Labels must be either '0' or '1'")
|
|
300
|
+
|
|
301
|
+
tn = np.sum((y == 0) & (y_pred == 0))
|
|
302
|
+
fp = np.sum((y == 0) & (y_pred == 1))
|
|
303
|
+
|
|
304
|
+
return tn / (tn + fp)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def precision_score(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
308
|
+
"""
|
|
309
|
+
Computes the precision of a classification.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
y_pred : `numpy.ndarray`
|
|
314
|
+
Predicted labels.
|
|
315
|
+
y : `numpy.ndarray`
|
|
316
|
+
Ground truth labels.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
`float`
|
|
321
|
+
Precision score.
|
|
322
|
+
"""
|
|
323
|
+
if not isinstance(y_pred, np.ndarray):
|
|
324
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
325
|
+
f"but not of '{type(y_pred)}'")
|
|
326
|
+
if not isinstance(y, np.ndarray):
|
|
327
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
328
|
+
f"but not of '{type(y)}'")
|
|
329
|
+
if y_pred.shape != y.shape:
|
|
330
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
331
|
+
if set(np.unique(y_pred)) != set([0, 1]):
|
|
332
|
+
raise ValueError("Labels must be either '0' or '1'")
|
|
333
|
+
|
|
334
|
+
tp = np.sum([np.all((y[i] == 1) & (y_pred[i] == 1)) for i in range(len(y))])
|
|
335
|
+
fp = np.sum([np.any((y[i] == 0) & (y_pred[i] == 1)) for i in range(len(y))])
|
|
336
|
+
|
|
337
|
+
return tp / (tp + fp)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def accuracy_score(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
341
|
+
"""
|
|
342
|
+
Computes the accuracy of a classification.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
y_pred : `numpy.ndarray`
|
|
347
|
+
Predicted labels.
|
|
348
|
+
y : `numpy.ndarray`
|
|
349
|
+
Ground truth labels.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
`float`
|
|
354
|
+
Accuracy score.
|
|
355
|
+
"""
|
|
356
|
+
if not isinstance(y_pred, np.ndarray):
|
|
357
|
+
raise TypeError("'y_pred' must be an instance of 'numpy.ndarray' " +
|
|
358
|
+
f"but not of '{type(y_pred)}'")
|
|
359
|
+
if not isinstance(y, np.ndarray):
|
|
360
|
+
raise TypeError("'y' must be an instance of 'numpy.ndarray' " +
|
|
361
|
+
f"but not of '{type(y)}'")
|
|
362
|
+
if y_pred.shape != y.shape:
|
|
363
|
+
raise ValueError(f"Shape mismatch: {y_pred.shape} vs. {y.shape}")
|
|
364
|
+
|
|
365
|
+
tp = np.sum([np.all(y[i] == y_pred[i]) for i in range(len(y))])
|
|
366
|
+
return tp / len(y)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def f1_score(y_pred: np.ndarray, y: np.ndarray) -> float:
|
|
370
|
+
"""
|
|
371
|
+
Computes the F1-score for a binary classification.
|
|
372
|
+
|
|
373
|
+
Parameters
|
|
374
|
+
----------
|
|
375
|
+
y_pred : `numpy.ndarray`
|
|
376
|
+
Predicted labels.
|
|
377
|
+
y : `numpy.ndarray`
|
|
378
|
+
Ground truth labels.
|
|
379
|
+
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
`float`
|
|
383
|
+
F1-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 len(y_pred.shape) != 1:
|
|
394
|
+
raise ValueError("'y_pred' must be a 1d array")
|
|
395
|
+
if len(y.shape) != 1:
|
|
396
|
+
raise ValueError("'y' must be a 1d array")
|
|
397
|
+
if set(np.unique(y_pred)) != set([0, 1]):
|
|
398
|
+
raise ValueError("Labels must be either '0' or '1'")
|
|
399
|
+
|
|
400
|
+
tp = np.sum((y == 1) & (y_pred == 1))
|
|
401
|
+
fp = np.sum((y == 0) & (y_pred == 1))
|
|
402
|
+
fn = np.sum((y == 1) & (y_pred == 0))
|
|
403
|
+
|
|
404
|
+
return (2. * tp) / (2. * tp + fp + fn)
|
|
@@ -0,0 +1,31 @@
|
|
|
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()
|
|
@@ -0,0 +1,118 @@
|
|
|
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`
|
|
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`
|
|
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`
|
|
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))
|