PyAutomationIO 0.0.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.
- automation/__init__.py +46 -0
- automation/alarms/__init__.py +563 -0
- automation/alarms/states.py +192 -0
- automation/alarms/trigger.py +64 -0
- automation/buffer.py +132 -0
- automation/core.py +1775 -0
- automation/dbmodels/__init__.py +23 -0
- automation/dbmodels/alarms.py +524 -0
- automation/dbmodels/core.py +86 -0
- automation/dbmodels/events.py +153 -0
- automation/dbmodels/logs.py +155 -0
- automation/dbmodels/machines.py +181 -0
- automation/dbmodels/opcua.py +81 -0
- automation/dbmodels/opcua_server.py +174 -0
- automation/dbmodels/tags.py +921 -0
- automation/dbmodels/users.py +259 -0
- automation/extensions/__init__.py +15 -0
- automation/extensions/api.py +149 -0
- automation/extensions/cors.py +18 -0
- automation/filter/__init__.py +19 -0
- automation/iad/__init__.py +3 -0
- automation/iad/frozen_data.py +54 -0
- automation/iad/out_of_range.py +51 -0
- automation/iad/outliers.py +51 -0
- automation/logger/__init__.py +0 -0
- automation/logger/alarms.py +426 -0
- automation/logger/core.py +265 -0
- automation/logger/datalogger.py +646 -0
- automation/logger/events.py +194 -0
- automation/logger/logdict.py +53 -0
- automation/logger/logs.py +203 -0
- automation/logger/machines.py +248 -0
- automation/logger/opcua_server.py +130 -0
- automation/logger/users.py +96 -0
- automation/managers/__init__.py +4 -0
- automation/managers/alarms.py +455 -0
- automation/managers/db.py +328 -0
- automation/managers/opcua_client.py +186 -0
- automation/managers/state_machine.py +183 -0
- automation/models.py +174 -0
- automation/modules/__init__.py +14 -0
- automation/modules/alarms/__init__.py +0 -0
- automation/modules/alarms/resources/__init__.py +10 -0
- automation/modules/alarms/resources/alarms.py +280 -0
- automation/modules/alarms/resources/summary.py +79 -0
- automation/modules/events/__init__.py +0 -0
- automation/modules/events/resources/__init__.py +10 -0
- automation/modules/events/resources/events.py +83 -0
- automation/modules/events/resources/logs.py +109 -0
- automation/modules/tags/__init__.py +0 -0
- automation/modules/tags/resources/__init__.py +8 -0
- automation/modules/tags/resources/tags.py +201 -0
- automation/modules/users/__init__.py +2 -0
- automation/modules/users/resources/__init__.py +10 -0
- automation/modules/users/resources/models/__init__.py +2 -0
- automation/modules/users/resources/models/roles.py +5 -0
- automation/modules/users/resources/models/users.py +14 -0
- automation/modules/users/resources/roles.py +38 -0
- automation/modules/users/resources/users.py +113 -0
- automation/modules/users/roles.py +121 -0
- automation/modules/users/users.py +335 -0
- automation/opcua/__init__.py +1 -0
- automation/opcua/models.py +541 -0
- automation/opcua/subscription.py +259 -0
- automation/pages/__init__.py +0 -0
- automation/pages/alarms.py +34 -0
- automation/pages/alarms_history.py +21 -0
- automation/pages/assets/styles.css +7 -0
- automation/pages/callbacks/__init__.py +28 -0
- automation/pages/callbacks/alarms.py +218 -0
- automation/pages/callbacks/alarms_summary.py +20 -0
- automation/pages/callbacks/db.py +222 -0
- automation/pages/callbacks/filter.py +238 -0
- automation/pages/callbacks/machines.py +29 -0
- automation/pages/callbacks/machines_detailed.py +581 -0
- automation/pages/callbacks/opcua.py +266 -0
- automation/pages/callbacks/opcua_server.py +244 -0
- automation/pages/callbacks/tags.py +495 -0
- automation/pages/callbacks/trends.py +119 -0
- automation/pages/communications.py +129 -0
- automation/pages/components/__init__.py +123 -0
- automation/pages/components/alarms.py +151 -0
- automation/pages/components/alarms_summary.py +45 -0
- automation/pages/components/database.py +128 -0
- automation/pages/components/gaussian_filter.py +69 -0
- automation/pages/components/machines.py +396 -0
- automation/pages/components/opcua.py +384 -0
- automation/pages/components/opcua_server.py +53 -0
- automation/pages/components/tags.py +253 -0
- automation/pages/components/trends.py +66 -0
- automation/pages/database.py +26 -0
- automation/pages/filter.py +55 -0
- automation/pages/machines.py +20 -0
- automation/pages/machines_detailed.py +41 -0
- automation/pages/main.py +63 -0
- automation/pages/opcua_server.py +28 -0
- automation/pages/tags.py +40 -0
- automation/pages/trends.py +35 -0
- automation/singleton.py +30 -0
- automation/state_machine.py +1672 -0
- automation/tags/__init__.py +2 -0
- automation/tags/cvt.py +1198 -0
- automation/tags/filter.py +55 -0
- automation/tags/tag.py +418 -0
- automation/tests/__init__.py +10 -0
- automation/tests/test_alarms.py +110 -0
- automation/tests/test_core.py +257 -0
- automation/tests/test_unit.py +21 -0
- automation/tests/test_user.py +155 -0
- automation/utils/__init__.py +164 -0
- automation/utils/decorators.py +222 -0
- automation/utils/npw.py +294 -0
- automation/utils/observer.py +21 -0
- automation/utils/units.py +118 -0
- automation/variables/__init__.py +55 -0
- automation/variables/adimentional.py +30 -0
- automation/variables/current.py +71 -0
- automation/variables/density.py +115 -0
- automation/variables/eng_time.py +68 -0
- automation/variables/force.py +90 -0
- automation/variables/length.py +104 -0
- automation/variables/mass.py +80 -0
- automation/variables/mass_flow.py +101 -0
- automation/variables/percentage.py +30 -0
- automation/variables/power.py +113 -0
- automation/variables/pressure.py +93 -0
- automation/variables/temperature.py +168 -0
- automation/variables/volume.py +70 -0
- automation/variables/volumetric_flow.py +100 -0
- automation/workers/__init__.py +2 -0
- automation/workers/logger.py +164 -0
- automation/workers/state_machine.py +207 -0
- automation/workers/worker.py +36 -0
- pyautomationio-0.0.0.dist-info/METADATA +198 -0
- pyautomationio-0.0.0.dist-info/RECORD +138 -0
- pyautomationio-0.0.0.dist-info/WHEEL +5 -0
- pyautomationio-0.0.0.dist-info/licenses/LICENSE +21 -0
- pyautomationio-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import functools, logging, sys
|
|
2
|
+
from ..modules.users.users import User, Users
|
|
3
|
+
from ..logger.events import EventsLoggerEngine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
events_engine = EventsLoggerEngine()
|
|
7
|
+
users = Users()
|
|
8
|
+
|
|
9
|
+
def decorator(declared_decorator):
|
|
10
|
+
"""
|
|
11
|
+
Create a decorator out of a function, which will be used as a wrapper
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@functools.wraps(declared_decorator)
|
|
15
|
+
def final_decorator(func=None, **kwargs):
|
|
16
|
+
# This will be exposed to the rest of your application as a decorator
|
|
17
|
+
def decorated(func):
|
|
18
|
+
# This will be exposed to the rest of your application as a decorated
|
|
19
|
+
# function, regardless how it was called
|
|
20
|
+
@functools.wraps(func)
|
|
21
|
+
def wrapper(*a, **kw):
|
|
22
|
+
# This is used when actually executing the function that was decorated
|
|
23
|
+
|
|
24
|
+
return declared_decorator(func, a, kw, **kwargs)
|
|
25
|
+
|
|
26
|
+
return wrapper
|
|
27
|
+
|
|
28
|
+
if func is None:
|
|
29
|
+
|
|
30
|
+
return decorated
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
# The decorator was called without arguments, so the function should be
|
|
34
|
+
# decorated immediately
|
|
35
|
+
return decorated(func)
|
|
36
|
+
|
|
37
|
+
return final_decorator
|
|
38
|
+
|
|
39
|
+
def set_event(message:str, classification:str, priority:int, criticity:int, description:str="", force:bool=False):
|
|
40
|
+
@decorator
|
|
41
|
+
def wrapper(func, args, kwargs):
|
|
42
|
+
from automation import PyAutomation
|
|
43
|
+
app = PyAutomation()
|
|
44
|
+
result = func(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
if result:
|
|
47
|
+
|
|
48
|
+
if "user" in kwargs:
|
|
49
|
+
|
|
50
|
+
user = kwargs.pop('user')
|
|
51
|
+
if isinstance(user, User):
|
|
52
|
+
|
|
53
|
+
_description = None
|
|
54
|
+
|
|
55
|
+
if isinstance(result, tuple):
|
|
56
|
+
|
|
57
|
+
_description = result[-1]
|
|
58
|
+
|
|
59
|
+
event, _ = events_engine.create(
|
|
60
|
+
message=message,
|
|
61
|
+
description=_description,
|
|
62
|
+
classification=classification,
|
|
63
|
+
priority=priority,
|
|
64
|
+
criticity=criticity,
|
|
65
|
+
user=user
|
|
66
|
+
)
|
|
67
|
+
if app.sio:
|
|
68
|
+
|
|
69
|
+
app.sio.emit("on.event", data=event.serialize())
|
|
70
|
+
else:
|
|
71
|
+
if force:
|
|
72
|
+
user = users.get_by_username(username="system")
|
|
73
|
+
event, _ = events_engine.create(
|
|
74
|
+
message=message,
|
|
75
|
+
description=description,
|
|
76
|
+
classification=classification,
|
|
77
|
+
priority=priority,
|
|
78
|
+
criticity=criticity,
|
|
79
|
+
user=user
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if app.sio:
|
|
83
|
+
|
|
84
|
+
app.sio.emit("on.event", data=event.serialize())
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
89
|
+
|
|
90
|
+
@decorator
|
|
91
|
+
def put_alarm_state(func, args, kwargs):
|
|
92
|
+
r"""
|
|
93
|
+
Documentation here
|
|
94
|
+
"""
|
|
95
|
+
from ..logger.alarms import AlarmsLoggerEngine
|
|
96
|
+
from .. import TIMEZONE
|
|
97
|
+
alarms_engine = AlarmsLoggerEngine()
|
|
98
|
+
result = func(*args, **kwargs)
|
|
99
|
+
alarm = args[0]
|
|
100
|
+
alarms_engine.put(
|
|
101
|
+
id=alarm.identifier,
|
|
102
|
+
state=alarm.state.state
|
|
103
|
+
)
|
|
104
|
+
if alarm.sio:
|
|
105
|
+
if alarm.timestamp:
|
|
106
|
+
timestamp = alarm.timestamp
|
|
107
|
+
timestamp = timestamp.astimezone(TIMEZONE)
|
|
108
|
+
alarm.timestamp = timestamp
|
|
109
|
+
if alarm.ack_timestamp:
|
|
110
|
+
ack_timestamp = alarm.ack_timestamp
|
|
111
|
+
ack_timestamp = ack_timestamp.astimezone(TIMEZONE)
|
|
112
|
+
alarm.ack_timestamp = ack_timestamp
|
|
113
|
+
|
|
114
|
+
alarm.sio.emit("on.alarm", data=alarm.serialize())
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def validate_types(**validations):
|
|
119
|
+
|
|
120
|
+
if "output" in validations:
|
|
121
|
+
|
|
122
|
+
_output = validations.pop('output')
|
|
123
|
+
|
|
124
|
+
if _output is None:
|
|
125
|
+
|
|
126
|
+
_output = type(None)
|
|
127
|
+
|
|
128
|
+
def decorator(func):
|
|
129
|
+
|
|
130
|
+
def wrapper(*args, **kwargs):
|
|
131
|
+
|
|
132
|
+
for key, _data_type in kwargs.items():
|
|
133
|
+
|
|
134
|
+
if key in validations:
|
|
135
|
+
|
|
136
|
+
if not isinstance(_data_type, validations[key]):
|
|
137
|
+
message = f"Expected Input {key} as {validations[key]}, but got {type(_data_type)} in {func}"
|
|
138
|
+
logging.error(message)
|
|
139
|
+
raise TypeError(message)
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
message = f"You didn't define {key} argument to validate in {func}"
|
|
143
|
+
logger = logging.getLogger("pyautomation")
|
|
144
|
+
logger.error(message)
|
|
145
|
+
raise KeyError(message)
|
|
146
|
+
|
|
147
|
+
# Call the wrapped function
|
|
148
|
+
result = func(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
# Validate the output type
|
|
151
|
+
if _output:
|
|
152
|
+
|
|
153
|
+
if isinstance(_output, tuple):
|
|
154
|
+
|
|
155
|
+
for counter, expected in enumerate(_output):
|
|
156
|
+
|
|
157
|
+
if not isinstance(result[counter], expected):
|
|
158
|
+
|
|
159
|
+
message = f"Expected output type ({counter}) {expected}, but got {type(result[counter])} in func {func}"
|
|
160
|
+
logger = logging.getLogger("pyautomation")
|
|
161
|
+
logger.error(message)
|
|
162
|
+
raise TypeError(message)
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
|
|
166
|
+
if not isinstance(result, _output):
|
|
167
|
+
message = f"Expected output type {_output}, but got {type(result)} in func {func}"
|
|
168
|
+
logger = logging.getLogger("pyautomation")
|
|
169
|
+
logger.error(message)
|
|
170
|
+
raise TypeError(message)
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
return wrapper
|
|
174
|
+
return decorator
|
|
175
|
+
|
|
176
|
+
@decorator
|
|
177
|
+
def logging_error_handler(func, args, kwargs):
|
|
178
|
+
r"""
|
|
179
|
+
Documentation here
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
|
|
183
|
+
result = func(*args, **kwargs)
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
except Exception as ex:
|
|
187
|
+
|
|
188
|
+
trace = []
|
|
189
|
+
tb = ex.__traceback__
|
|
190
|
+
while tb is not None:
|
|
191
|
+
trace.append({
|
|
192
|
+
"filename": tb.tb_frame.f_code.co_filename,
|
|
193
|
+
"name": tb.tb_frame.f_code.co_name,
|
|
194
|
+
"lineno": tb.tb_lineno
|
|
195
|
+
})
|
|
196
|
+
tb = tb.tb_next
|
|
197
|
+
msg = str({
|
|
198
|
+
'type': type(ex).__name__,
|
|
199
|
+
'message': str(ex),
|
|
200
|
+
'trace': trace
|
|
201
|
+
})
|
|
202
|
+
logger = logging.getLogger("pyautomation")
|
|
203
|
+
logger.error(msg=msg)
|
|
204
|
+
|
|
205
|
+
@decorator
|
|
206
|
+
def db_rollback(func, args, kwargs):
|
|
207
|
+
try:
|
|
208
|
+
self = args[0]
|
|
209
|
+
result = func(*args, **kwargs)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
_, _, e_traceback = sys.exc_info()
|
|
214
|
+
e_message = str(e)
|
|
215
|
+
e_line_number = e_traceback.tb_lineno
|
|
216
|
+
logger = logging.getLogger("pyautomation")
|
|
217
|
+
logger.warning(f"Rollback in [line {e_line_number}] {self.__class__.__name__}.{func.__name__} - {e_message}")
|
|
218
|
+
conn = self._db.connection()
|
|
219
|
+
conn.rollback()
|
|
220
|
+
result = func(*args, **kwargs)
|
|
221
|
+
|
|
222
|
+
return result
|
automation/utils/npw.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import pywt
|
|
2
|
+
import numpy as np
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
class Wavelet:
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
pipeline_length:float,
|
|
10
|
+
speed_of_sound:float,
|
|
11
|
+
family:str="db2"
|
|
12
|
+
):
|
|
13
|
+
|
|
14
|
+
# Constant definitions
|
|
15
|
+
self.C = speed_of_sound # Sound speed constant, (m/s)
|
|
16
|
+
self.L = pipeline_length # Pipeline length (m)
|
|
17
|
+
self.FAMILY = family
|
|
18
|
+
self.threshold_iqr = 95
|
|
19
|
+
|
|
20
|
+
def set_speed_of_sound(self, value:float)->None:
|
|
21
|
+
r"""
|
|
22
|
+
Documentation here
|
|
23
|
+
"""
|
|
24
|
+
self.C = value
|
|
25
|
+
|
|
26
|
+
def get_speed_of_sound(self)->float:
|
|
27
|
+
r"""
|
|
28
|
+
Documentation here
|
|
29
|
+
"""
|
|
30
|
+
return self.C
|
|
31
|
+
|
|
32
|
+
def set_threshold_iqr(self, value:float):
|
|
33
|
+
r"""
|
|
34
|
+
Documentation here
|
|
35
|
+
"""
|
|
36
|
+
if value < 0 or value > 100:
|
|
37
|
+
raise ValueError("Threshold IQR must be between 0 and 100")
|
|
38
|
+
self.threshold_iqr = value
|
|
39
|
+
|
|
40
|
+
def get_threshold_iqr(self)->float:
|
|
41
|
+
r"""
|
|
42
|
+
Documentation here
|
|
43
|
+
"""
|
|
44
|
+
return self.threshold_iqr
|
|
45
|
+
|
|
46
|
+
def set_pipeline_length(self, value:float)->None:
|
|
47
|
+
r"""
|
|
48
|
+
Documentation here
|
|
49
|
+
"""
|
|
50
|
+
self.L = value
|
|
51
|
+
|
|
52
|
+
def get_pipeline_length(self)->float:
|
|
53
|
+
r"""
|
|
54
|
+
Documentation here
|
|
55
|
+
"""
|
|
56
|
+
return self.L
|
|
57
|
+
|
|
58
|
+
def set_family(self, value:str)->None:
|
|
59
|
+
r"""
|
|
60
|
+
Set Wavelet Family
|
|
61
|
+
['bior1.1', 'bior1.3', 'bior1.5', 'bior2.2', 'bior2.4', 'bior2.6', 'bior2.8', 'bior3.1', 'bior3.3',
|
|
62
|
+
'bior3.5', 'bior3.7', 'bior3.9', 'bior4.4', 'bior5.5', 'bior6.8', 'cgau1', 'cgau2', 'cgau3', 'cgau4',
|
|
63
|
+
'cgau5', 'cgau6', 'cgau7', 'cgau8', 'cmor', 'coif1', 'coif2', 'coif3', 'coif4', 'coif5', 'coif6',
|
|
64
|
+
'coif7', 'coif8', 'coif9', 'coif10', 'coif11', 'coif12', 'coif13', 'coif14', 'coif15', 'coif16',
|
|
65
|
+
'coif17', 'db1', 'db2', 'db3', 'db4', 'db5', 'db6', 'db7', 'db8', 'db9', 'db10', 'db11', 'db12',
|
|
66
|
+
'db13', 'db14', 'db15', 'db16', 'db17', 'db18', 'db19', 'db20', 'db21', 'db22', 'db23', 'db24',
|
|
67
|
+
'db25', 'db26', 'db27', 'db28', 'db29', 'db30', 'db31', 'db32', 'db33', 'db34', 'db35', 'db36',
|
|
68
|
+
'db37', 'db38', 'dmey', 'fbsp', 'gaus1', 'gaus2', 'gaus3', 'gaus4', 'gaus5', 'gaus6', 'gaus7',
|
|
69
|
+
'gaus8', 'haar', 'mexh', 'morl', 'rbio1.1', 'rbio1.3', 'rbio1.5', 'rbio2.2', 'rbio2.4', 'rbio2.6',
|
|
70
|
+
'rbio2.8', 'rbio3.1', 'rbio3.3', 'rbio3.5', 'rbio3.7', 'rbio3.9', 'rbio4.4', 'rbio5.5', 'rbio6.8',
|
|
71
|
+
'shan', 'sym2', 'sym3', 'sym4', 'sym5', 'sym6', 'sym7', 'sym8', 'sym9', 'sym10', 'sym11', 'sym12',
|
|
72
|
+
'sym13', 'sym14', 'sym15', 'sym16', 'sym17', 'sym18', 'sym19', 'sym20']
|
|
73
|
+
"""
|
|
74
|
+
self.FAMILY = value
|
|
75
|
+
|
|
76
|
+
def get_family(self)->str:
|
|
77
|
+
r"""
|
|
78
|
+
Get Wavelet Family
|
|
79
|
+
"""
|
|
80
|
+
return self.FAMILY
|
|
81
|
+
|
|
82
|
+
def detect_anomaly(self, data):
|
|
83
|
+
r"""
|
|
84
|
+
Documentation here
|
|
85
|
+
"""
|
|
86
|
+
cA, cD = self.decomposition(data)
|
|
87
|
+
lower_anomaly, probabilities = self.find_singular_points(cD)
|
|
88
|
+
return lower_anomaly, probabilities
|
|
89
|
+
|
|
90
|
+
def find_singular_points(self, cD):
|
|
91
|
+
r"""
|
|
92
|
+
Find singular points in the wavelet detail coefficients.
|
|
93
|
+
"""
|
|
94
|
+
x = np.arange(len(cD))
|
|
95
|
+
slope, _ = np.polyfit(x, cD, 1)
|
|
96
|
+
|
|
97
|
+
# print(f"Slope: {slope}")
|
|
98
|
+
threshold = np.percentile(cD, 100 - self.threshold_iqr)
|
|
99
|
+
singular_points = np.where(cD < threshold)[0]
|
|
100
|
+
|
|
101
|
+
# Calcular probabilidad basada en la magnitud de la caída
|
|
102
|
+
if len(singular_points) > 0 and abs(slope) > 0.5:
|
|
103
|
+
magnitudes = np.abs(cD[singular_points] - threshold)
|
|
104
|
+
# Normalizar magnitudes a probabilidades entre 0 y 1
|
|
105
|
+
probabilities = magnitudes / np.max(magnitudes)
|
|
106
|
+
|
|
107
|
+
return singular_points, probabilities
|
|
108
|
+
return [], []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def map_to_original_position(self, anomaly, original_length, cD_length):
|
|
112
|
+
r"""
|
|
113
|
+
Map the indices of anomalies to the positions in the original data.
|
|
114
|
+
"""
|
|
115
|
+
# Assuming level=1 for simplicity, adjust if using different levels
|
|
116
|
+
original_position = None
|
|
117
|
+
if anomaly:
|
|
118
|
+
|
|
119
|
+
scaling_factor = anomaly / cD_length
|
|
120
|
+
original_position = (scaling_factor * original_length).astype(int)
|
|
121
|
+
|
|
122
|
+
return original_position
|
|
123
|
+
|
|
124
|
+
def decomposition(self, data):
|
|
125
|
+
r"""
|
|
126
|
+
Documentation here
|
|
127
|
+
"""
|
|
128
|
+
coeffs = pywt.wavedec(data, self.FAMILY, level=1)
|
|
129
|
+
cA = coeffs[0]
|
|
130
|
+
cD = coeffs[1]
|
|
131
|
+
return cA, cD
|
|
132
|
+
|
|
133
|
+
def wavedec(self, s, wavelet, mode='symmetric', level=None, axis=-1):
|
|
134
|
+
r"""
|
|
135
|
+
Multilevel 1D Discrete Wavelet Transform of signal $s$
|
|
136
|
+
|
|
137
|
+
**Parameters**
|
|
138
|
+
|
|
139
|
+
* **s:** (array_like) Input data
|
|
140
|
+
* **wavelet:** (Wavelet object or name string) Wavelet to use
|
|
141
|
+
* **mode:** (str) Signal extension mode.
|
|
142
|
+
* **level:** (int) Decomposition level (must be >= 0). If level is None (default)
|
|
143
|
+
then it will be calculated using the `dwt_max_level` function.
|
|
144
|
+
* **axis:** (int) Axis over which to compute the DWT. If not given, the last axis
|
|
145
|
+
is used.
|
|
146
|
+
|
|
147
|
+
**Returns**
|
|
148
|
+
|
|
149
|
+
* **[cA_n, cD_n, cD_n-1, ..., cD2, cD1]:** (list) Ordered list of coefficients arrays where
|
|
150
|
+
$n$ denotes the level of decomposition. The first element `(cA_n)` of the result is approximation
|
|
151
|
+
coefficients array and the following elements `[cD_n - cD1]` are details coefficients arrays.
|
|
152
|
+
|
|
153
|
+
## Snippet code
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
>>> coeffs = wavedec([1,2,3,4,5,6,7,8], 'db1', level=2)
|
|
157
|
+
>>> cA2, cD2, cD1 = coeffs
|
|
158
|
+
>>> cD1
|
|
159
|
+
array([-0.70710678, -0.70710678, -0.70710678, -0.70710678])
|
|
160
|
+
>>> cD2
|
|
161
|
+
array([-2., -2.])
|
|
162
|
+
>>> cA2
|
|
163
|
+
array([ 5., 13.])
|
|
164
|
+
>>> s = np.array([[1,1], [2,2], [3,3], [4,4], [5, 5], [6, 6], [7, 7], [8, 8]])
|
|
165
|
+
>>> coeffs = wavedec(s, 'db1', level=2, axis=0)
|
|
166
|
+
>>> cA2, cD2, cD1 = coeffs
|
|
167
|
+
>>> cD1
|
|
168
|
+
array([[-0.70710678, -0.70710678],
|
|
169
|
+
[-0.70710678, -0.70710678],
|
|
170
|
+
[-0.70710678, -0.70710678],
|
|
171
|
+
[-0.70710678, -0.70710678]])
|
|
172
|
+
>>> cD2
|
|
173
|
+
array([[-2., -2.],
|
|
174
|
+
[-2., -2.]])
|
|
175
|
+
>>> cA2
|
|
176
|
+
array([[ 5., 5.],
|
|
177
|
+
[13., 13.]])
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
"""
|
|
181
|
+
coeffs = pywt.wavedec(s, wavelet, mode=mode, level=level, axis=axis)
|
|
182
|
+
|
|
183
|
+
return coeffs
|
|
184
|
+
|
|
185
|
+
def time_flight(self, dt):
|
|
186
|
+
"""
|
|
187
|
+
x = L/2 ± (C * dt)/2
|
|
188
|
+
|
|
189
|
+
Donde:
|
|
190
|
+
x: distancia desde el sensor de entrada hasta la fuga
|
|
191
|
+
L: longitud total de la tubería
|
|
192
|
+
C: velocidad del sonido en el fluido
|
|
193
|
+
dt: tiempo entre detecciones (t2 - t1)
|
|
194
|
+
El signo ± depende de qué sensor detectó primero la onda
|
|
195
|
+
"""
|
|
196
|
+
if dt > 0: # La onda llegó primero al sensor de entrada
|
|
197
|
+
location = (self.L - self.C * abs(dt)) / 2
|
|
198
|
+
else: # La onda llegó primero al sensor de salida
|
|
199
|
+
location = (self.L + self.C * abs(dt)) / 2
|
|
200
|
+
|
|
201
|
+
return location
|
|
202
|
+
|
|
203
|
+
def compute_mode(self, arr):
|
|
204
|
+
unique_values, counts = np.unique(arr, return_counts=True)
|
|
205
|
+
max_count = np.max(counts)
|
|
206
|
+
modes = unique_values[counts == max_count]
|
|
207
|
+
return modes
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class NPW:
|
|
211
|
+
r"""
|
|
212
|
+
Documentation here
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, pipeline_length: int, speed_of_sound: float=1180, family: str="db2"):
|
|
216
|
+
|
|
217
|
+
self.wavelet = Wavelet(pipeline_length=pipeline_length, speed_of_sound=speed_of_sound, family=family)
|
|
218
|
+
|
|
219
|
+
def detect_anomaly(self, *pressure_signals:list[np.ndarray]):
|
|
220
|
+
r"""
|
|
221
|
+
Documentation here
|
|
222
|
+
"""
|
|
223
|
+
anomalies = list()
|
|
224
|
+
probabilities = list()
|
|
225
|
+
for signal in pressure_signals:
|
|
226
|
+
anomaly, probability = self.wavelet.detect_anomaly(signal)
|
|
227
|
+
|
|
228
|
+
if len(anomaly) >= 1:
|
|
229
|
+
anomalies.append(True)
|
|
230
|
+
probabilities.append(probability)
|
|
231
|
+
return anomalies, probabilities
|
|
232
|
+
|
|
233
|
+
def diagnose_anomaly(
|
|
234
|
+
self,
|
|
235
|
+
inlet_pressure:list[float],
|
|
236
|
+
outlet_pressure:list[float],
|
|
237
|
+
inlet_timestamps:list[datetime],
|
|
238
|
+
outlet_timestamps:list[datetime]
|
|
239
|
+
):
|
|
240
|
+
r"""
|
|
241
|
+
Documentation here
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
if not isinstance(inlet_timestamps, list) or not all(isinstance(x, datetime) for x in inlet_timestamps):
|
|
245
|
+
|
|
246
|
+
raise ValueError("inlet_timestamps must be a list of datetime objects when diagnosing anomalies on NPW Algorithm")
|
|
247
|
+
|
|
248
|
+
if not isinstance(outlet_timestamps, list) or not all(isinstance(x, datetime) for x in outlet_timestamps):
|
|
249
|
+
|
|
250
|
+
raise ValueError("outlet_timestamps must be a list of datetime objects when diagnosing anomalies on NPW Algorithm")
|
|
251
|
+
|
|
252
|
+
if not isinstance(inlet_pressure, list) or not all(isinstance(x, float) for x in inlet_pressure):
|
|
253
|
+
|
|
254
|
+
raise ValueError("inlet_pressure must be a list of float objects when diagnosing anomalies on NPW Algorithm")
|
|
255
|
+
|
|
256
|
+
if not isinstance(outlet_pressure, list) or not all(isinstance(x, float) for x in outlet_pressure):
|
|
257
|
+
|
|
258
|
+
raise ValueError("outlet_pressure must be a list of float objects when diagnosing anomalies on NPW Algorithm")
|
|
259
|
+
|
|
260
|
+
inlet_timestamp = None
|
|
261
|
+
outlet_timestamp = None
|
|
262
|
+
# Inlet Pressure
|
|
263
|
+
_, cD = self.wavelet.decomposition(inlet_pressure)
|
|
264
|
+
anomalies, _ = self.wavelet.find_singular_points(cD)
|
|
265
|
+
if len(anomalies) >= 1:
|
|
266
|
+
anomaly = anomalies[0]
|
|
267
|
+
pos = self.wavelet.map_to_original_position(anomaly=anomaly, original_length=len(inlet_pressure), cD_length=len(cD))
|
|
268
|
+
|
|
269
|
+
if pos:
|
|
270
|
+
|
|
271
|
+
inlet_timestamp = inlet_timestamps[pos]
|
|
272
|
+
|
|
273
|
+
# Outlet Pressure
|
|
274
|
+
_, cD = self.wavelet.decomposition(outlet_pressure)
|
|
275
|
+
anomalies, _ = self.wavelet.find_singular_points(cD)
|
|
276
|
+
if len(anomalies) >= 1:
|
|
277
|
+
anomaly = anomalies[0]
|
|
278
|
+
pos = self.wavelet.map_to_original_position(anomaly=anomaly, original_length=len(outlet_pressure), cD_length=len(cD))
|
|
279
|
+
if pos:
|
|
280
|
+
outlet_timestamp = outlet_timestamps[pos]
|
|
281
|
+
|
|
282
|
+
if inlet_timestamp and outlet_timestamp:
|
|
283
|
+
|
|
284
|
+
dt = outlet_timestamp - inlet_timestamp
|
|
285
|
+
location = self.wavelet.time_flight(dt=dt.total_seconds())
|
|
286
|
+
if location < 0:
|
|
287
|
+
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
if location > self.wavelet.L:
|
|
291
|
+
|
|
292
|
+
return self.wavelet.L
|
|
293
|
+
|
|
294
|
+
return location
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""pyautomation/utils/observer.py
|
|
3
|
+
|
|
4
|
+
This module implements Observer Utility Functions.
|
|
5
|
+
"""
|
|
6
|
+
import abc
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Observer(metaclass=abc.ABCMeta):
|
|
10
|
+
"""
|
|
11
|
+
Define an updating interface for objects that should be notified of
|
|
12
|
+
changes in a subject.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self._subject = None
|
|
17
|
+
self._observer_state = None
|
|
18
|
+
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def update(self, arg):
|
|
21
|
+
pass
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class UnitError(Exception):
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
class UnitSerializer(Enum):
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def list(cls):
|
|
10
|
+
return list(map(lambda c: c.value, cls))
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def serialize(cls):
|
|
14
|
+
|
|
15
|
+
return {unit.name: unit.value for unit in cls}
|
|
16
|
+
|
|
17
|
+
class EngUnit(object):
|
|
18
|
+
"""Generic class for engineering unit objects containing a float value and string unit."""
|
|
19
|
+
|
|
20
|
+
numerator = []
|
|
21
|
+
denominator = []
|
|
22
|
+
conversions = dict()
|
|
23
|
+
|
|
24
|
+
def __init__(self, value, unit):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.value = value
|
|
27
|
+
self.unit = unit
|
|
28
|
+
self.baseUnit = dict(zip(self.conversions.values(), self.conversions.keys()))[1]
|
|
29
|
+
|
|
30
|
+
def convert(self, to_unit):
|
|
31
|
+
"""Converts the object from one unit to another."""
|
|
32
|
+
from_unit = self.unit
|
|
33
|
+
to_unit = to_unit
|
|
34
|
+
return float(self.value) / float(self.conversions[from_unit]) * float(self.conversions[to_unit])
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def convert_values(self, values:list, from_unit:str, to_unit:str)->list:
|
|
38
|
+
r"""
|
|
39
|
+
Documentation here
|
|
40
|
+
"""
|
|
41
|
+
return [float(value) / float(self.conversions[from_unit]) * float(self.conversions[to_unit]) for value in values]
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def convert_value(cls, value:int|float, from_unit:str, to_unit:str)->float:
|
|
45
|
+
"""Unit value conversion
|
|
46
|
+
|
|
47
|
+
:param value: [int|float] Value to convert
|
|
48
|
+
:param from_unit: [str] Value's unit
|
|
49
|
+
:param to_unit: [str] Unit which you want to convert the value
|
|
50
|
+
:return: [float] Converted value into "to_unit"
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
>>> from automation.variables.pressure import Pressure
|
|
54
|
+
>>> Pressure.convert_value(value=2, from_unit="atm", to_unit="Pa")
|
|
55
|
+
202650.05476617732
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
return float(value) / float(cls.conversions[from_unit]) * float(cls.conversions[to_unit])
|
|
60
|
+
|
|
61
|
+
def change_unit(self, unit):
|
|
62
|
+
"""Converts the current value of the object to a new unit. Returns a float of the new value."""
|
|
63
|
+
self.value = self.convert(unit)
|
|
64
|
+
self.unit = unit
|
|
65
|
+
return float(self.value)
|
|
66
|
+
|
|
67
|
+
def set_value(self, value, unit):
|
|
68
|
+
"""Sets the value and unit of the object"""
|
|
69
|
+
self.value = value
|
|
70
|
+
self.unit = unit
|
|
71
|
+
|
|
72
|
+
def get_value(self):
|
|
73
|
+
"""Returns a list of the float value and unit of the object."""
|
|
74
|
+
return [float(self.value), self.unit]
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return str(self.value) + ' ' + self.unit
|
|
78
|
+
|
|
79
|
+
def __add__(self, other):
|
|
80
|
+
new_value = self.value + other.change_unit(self.unit)
|
|
81
|
+
return self.__class__(new_value, self.unit)
|
|
82
|
+
|
|
83
|
+
def __sub__(self, other):
|
|
84
|
+
new_value = self.value - other.change_unit(self.unit)
|
|
85
|
+
return self.__class__(new_value, self.unit)
|
|
86
|
+
|
|
87
|
+
def __mul__(self, other):
|
|
88
|
+
new_value = self.value * other.change_unit(self.unit)
|
|
89
|
+
return self.__class__(new_value, self.unit)
|
|
90
|
+
|
|
91
|
+
def __rmul__(self, other):
|
|
92
|
+
new_value = self.value * other.change_unit(self.unit)
|
|
93
|
+
return self.__class__(new_value, self.unit)
|
|
94
|
+
|
|
95
|
+
def __truediv__(self, other):
|
|
96
|
+
new_value = self.value / other.change_unit(self.unit)
|
|
97
|
+
return self.__class__(new_value, self.unit)
|
|
98
|
+
|
|
99
|
+
def __floordiv__(self, other):
|
|
100
|
+
new_value = self.value // other.change_unit(self.unit)
|
|
101
|
+
return self.__class__(new_value, self.unit)
|
|
102
|
+
|
|
103
|
+
def __pow__(self, other):
|
|
104
|
+
new_value = self.value ** other.change_unit(self.unit)
|
|
105
|
+
return self.__class__(new_value, self.unit)
|
|
106
|
+
|
|
107
|
+
def __lt__(self, other):
|
|
108
|
+
return self.value < other.change_unit(self.unit)
|
|
109
|
+
|
|
110
|
+
def __le__(self, other):
|
|
111
|
+
return self.value <= other.change_unit(self.unit)
|
|
112
|
+
|
|
113
|
+
def __gt__(self, other):
|
|
114
|
+
return self.value > other.change_unit(self.unit)
|
|
115
|
+
|
|
116
|
+
def __ge__(self, other):
|
|
117
|
+
return self.value >= other.change_unit(self.unit)
|
|
118
|
+
|