epyt-flow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
  2. epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
  3. epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
  4. epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
  5. epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
  6. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
  7. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
  8. epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
  9. epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
  10. epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
  11. epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
  12. epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
  13. epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
  14. epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
  15. epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
  16. epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
  17. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
  18. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
  19. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
  20. epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
  21. epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
  22. epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
  23. epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
  24. epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
  25. epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
  26. epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
  27. epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
  28. epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
  29. epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
  30. epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
  31. epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
  32. epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
  33. epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
  34. epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
  35. epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
  36. epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
  37. epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
  38. epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
  39. epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
  40. epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
  41. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
  42. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
  43. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
  44. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
  45. epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
  46. epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
  47. epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
  48. epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
  49. epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
  50. epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
  51. epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
  52. epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
  53. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
  54. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
  55. epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
  56. epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
  57. epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
  58. epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
  59. epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
  60. epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
  61. epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
  62. epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
  63. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
  64. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
  65. epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
  66. epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
  67. epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
  68. epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
  69. epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
  70. epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
  71. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
  72. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
  73. epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
  74. epyt_flow/EPANET/compile.sh +4 -0
  75. epyt_flow/VERSION +1 -0
  76. epyt_flow/__init__.py +24 -0
  77. epyt_flow/data/__init__.py +0 -0
  78. epyt_flow/data/benchmarks/__init__.py +11 -0
  79. epyt_flow/data/benchmarks/batadal.py +257 -0
  80. epyt_flow/data/benchmarks/batadal_data.py +28 -0
  81. epyt_flow/data/benchmarks/battledim.py +473 -0
  82. epyt_flow/data/benchmarks/battledim_data.py +51 -0
  83. epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
  84. epyt_flow/data/benchmarks/leakdb.py +592 -0
  85. epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
  86. epyt_flow/data/benchmarks/water_usage.py +123 -0
  87. epyt_flow/data/networks.py +650 -0
  88. epyt_flow/gym/__init__.py +4 -0
  89. epyt_flow/gym/control_gyms.py +47 -0
  90. epyt_flow/gym/scenario_control_env.py +101 -0
  91. epyt_flow/metrics.py +404 -0
  92. epyt_flow/models/__init__.py +2 -0
  93. epyt_flow/models/event_detector.py +31 -0
  94. epyt_flow/models/sensor_interpolation_detector.py +118 -0
  95. epyt_flow/rest_api/__init__.py +4 -0
  96. epyt_flow/rest_api/base_handler.py +70 -0
  97. epyt_flow/rest_api/res_manager.py +95 -0
  98. epyt_flow/rest_api/scada_data_handler.py +476 -0
  99. epyt_flow/rest_api/scenario_handler.py +352 -0
  100. epyt_flow/rest_api/server.py +106 -0
  101. epyt_flow/serialization.py +438 -0
  102. epyt_flow/simulation/__init__.py +5 -0
  103. epyt_flow/simulation/events/__init__.py +6 -0
  104. epyt_flow/simulation/events/actuator_events.py +259 -0
  105. epyt_flow/simulation/events/event.py +81 -0
  106. epyt_flow/simulation/events/leakages.py +404 -0
  107. epyt_flow/simulation/events/sensor_faults.py +267 -0
  108. epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
  109. epyt_flow/simulation/events/sensor_reading_event.py +170 -0
  110. epyt_flow/simulation/events/system_event.py +88 -0
  111. epyt_flow/simulation/parallel_simulation.py +147 -0
  112. epyt_flow/simulation/scada/__init__.py +3 -0
  113. epyt_flow/simulation/scada/advanced_control.py +134 -0
  114. epyt_flow/simulation/scada/scada_data.py +1589 -0
  115. epyt_flow/simulation/scada/scada_data_export.py +255 -0
  116. epyt_flow/simulation/scenario_config.py +608 -0
  117. epyt_flow/simulation/scenario_simulator.py +1897 -0
  118. epyt_flow/simulation/scenario_visualizer.py +61 -0
  119. epyt_flow/simulation/sensor_config.py +1289 -0
  120. epyt_flow/topology.py +290 -0
  121. epyt_flow/uncertainty/__init__.py +3 -0
  122. epyt_flow/uncertainty/model_uncertainty.py +302 -0
  123. epyt_flow/uncertainty/sensor_noise.py +73 -0
  124. epyt_flow/uncertainty/uncertainties.py +555 -0
  125. epyt_flow/uncertainty/utils.py +206 -0
  126. epyt_flow/utils.py +306 -0
  127. epyt_flow-0.1.0.dist-info/LICENSE +21 -0
  128. epyt_flow-0.1.0.dist-info/METADATA +139 -0
  129. epyt_flow-0.1.0.dist-info/RECORD +131 -0
  130. epyt_flow-0.1.0.dist-info/WHEEL +5 -0
  131. epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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,2 @@
1
+ from .event_detector import *
2
+ from .sensor_interpolation_detector import *
@@ -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))