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.
Files changed (138) hide show
  1. automation/__init__.py +46 -0
  2. automation/alarms/__init__.py +563 -0
  3. automation/alarms/states.py +192 -0
  4. automation/alarms/trigger.py +64 -0
  5. automation/buffer.py +132 -0
  6. automation/core.py +1775 -0
  7. automation/dbmodels/__init__.py +23 -0
  8. automation/dbmodels/alarms.py +524 -0
  9. automation/dbmodels/core.py +86 -0
  10. automation/dbmodels/events.py +153 -0
  11. automation/dbmodels/logs.py +155 -0
  12. automation/dbmodels/machines.py +181 -0
  13. automation/dbmodels/opcua.py +81 -0
  14. automation/dbmodels/opcua_server.py +174 -0
  15. automation/dbmodels/tags.py +921 -0
  16. automation/dbmodels/users.py +259 -0
  17. automation/extensions/__init__.py +15 -0
  18. automation/extensions/api.py +149 -0
  19. automation/extensions/cors.py +18 -0
  20. automation/filter/__init__.py +19 -0
  21. automation/iad/__init__.py +3 -0
  22. automation/iad/frozen_data.py +54 -0
  23. automation/iad/out_of_range.py +51 -0
  24. automation/iad/outliers.py +51 -0
  25. automation/logger/__init__.py +0 -0
  26. automation/logger/alarms.py +426 -0
  27. automation/logger/core.py +265 -0
  28. automation/logger/datalogger.py +646 -0
  29. automation/logger/events.py +194 -0
  30. automation/logger/logdict.py +53 -0
  31. automation/logger/logs.py +203 -0
  32. automation/logger/machines.py +248 -0
  33. automation/logger/opcua_server.py +130 -0
  34. automation/logger/users.py +96 -0
  35. automation/managers/__init__.py +4 -0
  36. automation/managers/alarms.py +455 -0
  37. automation/managers/db.py +328 -0
  38. automation/managers/opcua_client.py +186 -0
  39. automation/managers/state_machine.py +183 -0
  40. automation/models.py +174 -0
  41. automation/modules/__init__.py +14 -0
  42. automation/modules/alarms/__init__.py +0 -0
  43. automation/modules/alarms/resources/__init__.py +10 -0
  44. automation/modules/alarms/resources/alarms.py +280 -0
  45. automation/modules/alarms/resources/summary.py +79 -0
  46. automation/modules/events/__init__.py +0 -0
  47. automation/modules/events/resources/__init__.py +10 -0
  48. automation/modules/events/resources/events.py +83 -0
  49. automation/modules/events/resources/logs.py +109 -0
  50. automation/modules/tags/__init__.py +0 -0
  51. automation/modules/tags/resources/__init__.py +8 -0
  52. automation/modules/tags/resources/tags.py +201 -0
  53. automation/modules/users/__init__.py +2 -0
  54. automation/modules/users/resources/__init__.py +10 -0
  55. automation/modules/users/resources/models/__init__.py +2 -0
  56. automation/modules/users/resources/models/roles.py +5 -0
  57. automation/modules/users/resources/models/users.py +14 -0
  58. automation/modules/users/resources/roles.py +38 -0
  59. automation/modules/users/resources/users.py +113 -0
  60. automation/modules/users/roles.py +121 -0
  61. automation/modules/users/users.py +335 -0
  62. automation/opcua/__init__.py +1 -0
  63. automation/opcua/models.py +541 -0
  64. automation/opcua/subscription.py +259 -0
  65. automation/pages/__init__.py +0 -0
  66. automation/pages/alarms.py +34 -0
  67. automation/pages/alarms_history.py +21 -0
  68. automation/pages/assets/styles.css +7 -0
  69. automation/pages/callbacks/__init__.py +28 -0
  70. automation/pages/callbacks/alarms.py +218 -0
  71. automation/pages/callbacks/alarms_summary.py +20 -0
  72. automation/pages/callbacks/db.py +222 -0
  73. automation/pages/callbacks/filter.py +238 -0
  74. automation/pages/callbacks/machines.py +29 -0
  75. automation/pages/callbacks/machines_detailed.py +581 -0
  76. automation/pages/callbacks/opcua.py +266 -0
  77. automation/pages/callbacks/opcua_server.py +244 -0
  78. automation/pages/callbacks/tags.py +495 -0
  79. automation/pages/callbacks/trends.py +119 -0
  80. automation/pages/communications.py +129 -0
  81. automation/pages/components/__init__.py +123 -0
  82. automation/pages/components/alarms.py +151 -0
  83. automation/pages/components/alarms_summary.py +45 -0
  84. automation/pages/components/database.py +128 -0
  85. automation/pages/components/gaussian_filter.py +69 -0
  86. automation/pages/components/machines.py +396 -0
  87. automation/pages/components/opcua.py +384 -0
  88. automation/pages/components/opcua_server.py +53 -0
  89. automation/pages/components/tags.py +253 -0
  90. automation/pages/components/trends.py +66 -0
  91. automation/pages/database.py +26 -0
  92. automation/pages/filter.py +55 -0
  93. automation/pages/machines.py +20 -0
  94. automation/pages/machines_detailed.py +41 -0
  95. automation/pages/main.py +63 -0
  96. automation/pages/opcua_server.py +28 -0
  97. automation/pages/tags.py +40 -0
  98. automation/pages/trends.py +35 -0
  99. automation/singleton.py +30 -0
  100. automation/state_machine.py +1672 -0
  101. automation/tags/__init__.py +2 -0
  102. automation/tags/cvt.py +1198 -0
  103. automation/tags/filter.py +55 -0
  104. automation/tags/tag.py +418 -0
  105. automation/tests/__init__.py +10 -0
  106. automation/tests/test_alarms.py +110 -0
  107. automation/tests/test_core.py +257 -0
  108. automation/tests/test_unit.py +21 -0
  109. automation/tests/test_user.py +155 -0
  110. automation/utils/__init__.py +164 -0
  111. automation/utils/decorators.py +222 -0
  112. automation/utils/npw.py +294 -0
  113. automation/utils/observer.py +21 -0
  114. automation/utils/units.py +118 -0
  115. automation/variables/__init__.py +55 -0
  116. automation/variables/adimentional.py +30 -0
  117. automation/variables/current.py +71 -0
  118. automation/variables/density.py +115 -0
  119. automation/variables/eng_time.py +68 -0
  120. automation/variables/force.py +90 -0
  121. automation/variables/length.py +104 -0
  122. automation/variables/mass.py +80 -0
  123. automation/variables/mass_flow.py +101 -0
  124. automation/variables/percentage.py +30 -0
  125. automation/variables/power.py +113 -0
  126. automation/variables/pressure.py +93 -0
  127. automation/variables/temperature.py +168 -0
  128. automation/variables/volume.py +70 -0
  129. automation/variables/volumetric_flow.py +100 -0
  130. automation/workers/__init__.py +2 -0
  131. automation/workers/logger.py +164 -0
  132. automation/workers/state_machine.py +207 -0
  133. automation/workers/worker.py +36 -0
  134. pyautomationio-0.0.0.dist-info/METADATA +198 -0
  135. pyautomationio-0.0.0.dist-info/RECORD +138 -0
  136. pyautomationio-0.0.0.dist-info/WHEEL +5 -0
  137. pyautomationio-0.0.0.dist-info/licenses/LICENSE +21 -0
  138. 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
@@ -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
+