mcp-server-mcsa 0.1.0__py3-none-any.whl → 0.1.1__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.
- mcp_server_mcsa/__init__.py +37 -37
- mcp_server_mcsa/__main__.py +5 -5
- mcp_server_mcsa/analysis/__init__.py +19 -19
- mcp_server_mcsa/analysis/bearing.py +147 -147
- mcp_server_mcsa/analysis/envelope.py +96 -97
- mcp_server_mcsa/analysis/fault_detection.py +424 -425
- mcp_server_mcsa/analysis/file_io.py +429 -428
- mcp_server_mcsa/analysis/motor.py +147 -145
- mcp_server_mcsa/analysis/preprocessing.py +180 -180
- mcp_server_mcsa/analysis/spectral.py +171 -172
- mcp_server_mcsa/analysis/test_signal.py +232 -232
- mcp_server_mcsa/analysis/timefreq.py +132 -132
- mcp_server_mcsa/server.py +954 -955
- {mcp_server_mcsa-0.1.0.dist-info → mcp_server_mcsa-0.1.1.dist-info}/METADATA +3 -1
- mcp_server_mcsa-0.1.1.dist-info/RECORD +18 -0
- {mcp_server_mcsa-0.1.0.dist-info → mcp_server_mcsa-0.1.1.dist-info}/licenses/LICENSE +21 -21
- mcp_server_mcsa-0.1.0.dist-info/RECORD +0 -18
- {mcp_server_mcsa-0.1.0.dist-info → mcp_server_mcsa-0.1.1.dist-info}/WHEEL +0 -0
- {mcp_server_mcsa-0.1.0.dist-info → mcp_server_mcsa-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,425 +1,424 @@
|
|
|
1
|
-
"""Fault detection and severity assessment for MCSA.
|
|
2
|
-
|
|
3
|
-
Computes standardised fault indices from current spectra and provides
|
|
4
|
-
severity classification based on configurable thresholds.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
from numpy.typing import NDArray
|
|
11
|
-
|
|
12
|
-
from mcp_server_mcsa.analysis.motor import MotorParameters
|
|
13
|
-
from mcp_server_mcsa.analysis.spectral import amplitude_at_frequency
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
|
|
340
|
-
"
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
|
|
425
|
-
}
|
|
1
|
+
"""Fault detection and severity assessment for MCSA.
|
|
2
|
+
|
|
3
|
+
Computes standardised fault indices from current spectra and provides
|
|
4
|
+
severity classification based on configurable thresholds.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
12
|
+
from mcp_server_mcsa.analysis.motor import MotorParameters
|
|
13
|
+
from mcp_server_mcsa.analysis.spectral import amplitude_at_frequency
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Severity thresholds (dB below fundamental)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# These are widely‑used empirical guidelines for induction motors.
|
|
19
|
+
# They should be adapted to the specific motor/application.
|
|
20
|
+
BRB_THRESHOLDS = {
|
|
21
|
+
"healthy": -50.0, # dB — sideband ≤ -50 dB relative to fundamental
|
|
22
|
+
"incipient": -45.0, # dB — early-stage fault
|
|
23
|
+
"moderate": -40.0, # dB — developing fault
|
|
24
|
+
"severe": -35.0, # dB — immediate action recommended
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ECCENTRICITY_THRESHOLDS = {
|
|
28
|
+
"healthy": -50.0,
|
|
29
|
+
"incipient": -44.0,
|
|
30
|
+
"moderate": -38.0,
|
|
31
|
+
"severe": -30.0,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _db_ratio(a: float, ref: float) -> float:
|
|
36
|
+
"""Compute 20·log10(a / ref), safe for zero values."""
|
|
37
|
+
if ref <= 0 or a <= 0:
|
|
38
|
+
return -np.inf
|
|
39
|
+
return 20.0 * np.log10(a / ref)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _classify_severity(db_value: float, thresholds: dict[str, float]) -> str:
|
|
43
|
+
"""Classify severity from dB value and ordered thresholds."""
|
|
44
|
+
if db_value <= thresholds["healthy"]:
|
|
45
|
+
return "healthy"
|
|
46
|
+
elif db_value <= thresholds["incipient"]:
|
|
47
|
+
return "incipient"
|
|
48
|
+
elif db_value <= thresholds["moderate"]:
|
|
49
|
+
return "moderate"
|
|
50
|
+
else:
|
|
51
|
+
return "severe"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Broken Rotor Bars
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def brb_fault_index(
|
|
59
|
+
freqs: NDArray[np.floating],
|
|
60
|
+
amps: NDArray[np.floating],
|
|
61
|
+
params: MotorParameters,
|
|
62
|
+
tolerance_hz: float = 0.5,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Compute the Broken Rotor Bar (BRB) fault index.
|
|
65
|
+
|
|
66
|
+
The index is the ratio of the lower and upper sideband amplitudes
|
|
67
|
+
at (1 ± 2s)·f_s to the fundamental amplitude, expressed in dB.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
freqs: Frequency axis of the spectrum.
|
|
71
|
+
amps: Amplitude values of the spectrum.
|
|
72
|
+
params: Motor parameters (for slip and supply frequency).
|
|
73
|
+
tolerance_hz: Frequency search tolerance.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary with frequencies found, amplitudes, dB indices,
|
|
77
|
+
and severity classification.
|
|
78
|
+
"""
|
|
79
|
+
fs = params.supply_freq_hz
|
|
80
|
+
s = params.slip
|
|
81
|
+
|
|
82
|
+
f_lower = (1 - 2 * s) * fs
|
|
83
|
+
f_upper = (1 + 2 * s) * fs
|
|
84
|
+
|
|
85
|
+
fundamental = amplitude_at_frequency(freqs, amps, fs, tolerance_hz)
|
|
86
|
+
lower_sb = amplitude_at_frequency(freqs, amps, f_lower, tolerance_hz)
|
|
87
|
+
upper_sb = amplitude_at_frequency(freqs, amps, f_upper, tolerance_hz)
|
|
88
|
+
|
|
89
|
+
a_fund = fundamental["amplitude"]
|
|
90
|
+
a_lower = lower_sb["amplitude"]
|
|
91
|
+
a_upper = upper_sb["amplitude"]
|
|
92
|
+
|
|
93
|
+
db_lower = _db_ratio(a_lower, a_fund)
|
|
94
|
+
db_upper = _db_ratio(a_upper, a_fund)
|
|
95
|
+
db_combined = _db_ratio((a_lower + a_upper) / 2.0, a_fund) if a_fund > 0 else -np.inf
|
|
96
|
+
|
|
97
|
+
severity = _classify_severity(max(db_lower, db_upper), BRB_THRESHOLDS)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"fault_type": "broken_rotor_bars",
|
|
101
|
+
"fundamental": {
|
|
102
|
+
"expected_hz": fs,
|
|
103
|
+
**fundamental,
|
|
104
|
+
},
|
|
105
|
+
"lower_sideband": {
|
|
106
|
+
"expected_hz": round(f_lower, 4),
|
|
107
|
+
**lower_sb,
|
|
108
|
+
"db_relative": round(float(db_lower), 2),
|
|
109
|
+
},
|
|
110
|
+
"upper_sideband": {
|
|
111
|
+
"expected_hz": round(f_upper, 4),
|
|
112
|
+
**upper_sb,
|
|
113
|
+
"db_relative": round(float(db_upper), 2),
|
|
114
|
+
},
|
|
115
|
+
"combined_index_db": round(float(db_combined), 2),
|
|
116
|
+
"severity": severity,
|
|
117
|
+
"thresholds_db": BRB_THRESHOLDS,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Eccentricity
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def eccentricity_fault_index(
|
|
126
|
+
freqs: NDArray[np.floating],
|
|
127
|
+
amps: NDArray[np.floating],
|
|
128
|
+
params: MotorParameters,
|
|
129
|
+
harmonics: int = 3,
|
|
130
|
+
tolerance_hz: float = 0.5,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Compute eccentricity fault indices.
|
|
133
|
+
|
|
134
|
+
Searches for sidebands at f_s ± k·f_r (k = 1 … harmonics).
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
freqs: Frequency axis.
|
|
138
|
+
amps: Amplitude values.
|
|
139
|
+
params: Motor parameters.
|
|
140
|
+
harmonics: Number of harmonic orders.
|
|
141
|
+
tolerance_hz: Frequency tolerance.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary with sideband amplitudes, dB indices, severity.
|
|
145
|
+
"""
|
|
146
|
+
fs = params.supply_freq_hz
|
|
147
|
+
fr = params.rotor_freq_hz
|
|
148
|
+
fund = amplitude_at_frequency(freqs, amps, fs, tolerance_hz)
|
|
149
|
+
a_fund = fund["amplitude"]
|
|
150
|
+
|
|
151
|
+
sidebands = []
|
|
152
|
+
worst_db = -np.inf
|
|
153
|
+
|
|
154
|
+
for k in range(1, harmonics + 1):
|
|
155
|
+
f_lo = fs - k * fr
|
|
156
|
+
f_hi = fs + k * fr
|
|
157
|
+
sb_lo = amplitude_at_frequency(freqs, amps, f_lo, tolerance_hz)
|
|
158
|
+
sb_hi = amplitude_at_frequency(freqs, amps, f_hi, tolerance_hz)
|
|
159
|
+
|
|
160
|
+
db_lo = _db_ratio(sb_lo["amplitude"], a_fund)
|
|
161
|
+
db_hi = _db_ratio(sb_hi["amplitude"], a_fund)
|
|
162
|
+
|
|
163
|
+
worst_db = max(worst_db, db_lo, db_hi)
|
|
164
|
+
|
|
165
|
+
sidebands.append({
|
|
166
|
+
"harmonic_order": k,
|
|
167
|
+
"lower": {
|
|
168
|
+
"expected_hz": round(f_lo, 4),
|
|
169
|
+
**sb_lo,
|
|
170
|
+
"db_relative": round(float(db_lo), 2),
|
|
171
|
+
},
|
|
172
|
+
"upper": {
|
|
173
|
+
"expected_hz": round(f_hi, 4),
|
|
174
|
+
**sb_hi,
|
|
175
|
+
"db_relative": round(float(db_hi), 2),
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
severity = _classify_severity(float(worst_db), ECCENTRICITY_THRESHOLDS)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"fault_type": "eccentricity",
|
|
183
|
+
"fundamental": {
|
|
184
|
+
"expected_hz": fs,
|
|
185
|
+
**fund,
|
|
186
|
+
},
|
|
187
|
+
"sidebands": sidebands,
|
|
188
|
+
"worst_sideband_db": round(float(worst_db), 2),
|
|
189
|
+
"severity": severity,
|
|
190
|
+
"thresholds_db": ECCENTRICITY_THRESHOLDS,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Stator inter-turn short circuit
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def stator_fault_index(
|
|
199
|
+
freqs: NDArray[np.floating],
|
|
200
|
+
amps: NDArray[np.floating],
|
|
201
|
+
params: MotorParameters,
|
|
202
|
+
harmonics: int = 3,
|
|
203
|
+
tolerance_hz: float = 0.5,
|
|
204
|
+
) -> dict:
|
|
205
|
+
"""Compute stator inter‑turn fault indices.
|
|
206
|
+
|
|
207
|
+
Looks for sidebands at f_s ± 2k·f_r.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
freqs: Frequency axis.
|
|
211
|
+
amps: Amplitude values.
|
|
212
|
+
params: Motor parameters.
|
|
213
|
+
harmonics: Number of harmonic orders.
|
|
214
|
+
tolerance_hz: Frequency tolerance.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dictionary with sideband analysis and severity.
|
|
218
|
+
"""
|
|
219
|
+
fs = params.supply_freq_hz
|
|
220
|
+
fr = params.rotor_freq_hz
|
|
221
|
+
fund = amplitude_at_frequency(freqs, amps, fs, tolerance_hz)
|
|
222
|
+
a_fund = fund["amplitude"]
|
|
223
|
+
|
|
224
|
+
sidebands = []
|
|
225
|
+
worst_db = -np.inf
|
|
226
|
+
|
|
227
|
+
for k in range(1, harmonics + 1):
|
|
228
|
+
f_lo = fs - 2 * k * fr
|
|
229
|
+
f_hi = fs + 2 * k * fr
|
|
230
|
+
sb_lo = amplitude_at_frequency(freqs, amps, f_lo, tolerance_hz)
|
|
231
|
+
sb_hi = amplitude_at_frequency(freqs, amps, f_hi, tolerance_hz)
|
|
232
|
+
|
|
233
|
+
db_lo = _db_ratio(sb_lo["amplitude"], a_fund)
|
|
234
|
+
db_hi = _db_ratio(sb_hi["amplitude"], a_fund)
|
|
235
|
+
worst_db = max(worst_db, db_lo, db_hi)
|
|
236
|
+
|
|
237
|
+
sidebands.append({
|
|
238
|
+
"harmonic_order": k,
|
|
239
|
+
"lower": {
|
|
240
|
+
"expected_hz": round(f_lo, 4),
|
|
241
|
+
**sb_lo,
|
|
242
|
+
"db_relative": round(float(db_lo), 2),
|
|
243
|
+
},
|
|
244
|
+
"upper": {
|
|
245
|
+
"expected_hz": round(f_hi, 4),
|
|
246
|
+
**sb_hi,
|
|
247
|
+
"db_relative": round(float(db_hi), 2),
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
severity = _classify_severity(float(worst_db), ECCENTRICITY_THRESHOLDS)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"fault_type": "stator_inter_turn",
|
|
255
|
+
"fundamental": {
|
|
256
|
+
"expected_hz": fs,
|
|
257
|
+
**fund,
|
|
258
|
+
},
|
|
259
|
+
"sidebands": sidebands,
|
|
260
|
+
"worst_sideband_db": round(float(worst_db), 2),
|
|
261
|
+
"severity": severity,
|
|
262
|
+
"thresholds_db": ECCENTRICITY_THRESHOLDS,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Bearing faults (via stator current)
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def bearing_fault_index(
|
|
271
|
+
freqs: NDArray[np.floating],
|
|
272
|
+
amps: NDArray[np.floating],
|
|
273
|
+
supply_freq_hz: float,
|
|
274
|
+
bearing_defect_freq_hz: float,
|
|
275
|
+
defect_type: str = "bpfo",
|
|
276
|
+
harmonics: int = 2,
|
|
277
|
+
tolerance_hz: float = 0.5,
|
|
278
|
+
) -> dict:
|
|
279
|
+
"""Compute bearing fault indices from stator‑current spectrum.
|
|
280
|
+
|
|
281
|
+
Bearing defects produce torque oscillations that modulate the current,
|
|
282
|
+
creating sidebands at f_s ± k · f_defect.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
freqs: Frequency axis.
|
|
286
|
+
amps: Amplitude values.
|
|
287
|
+
supply_freq_hz: Supply frequency in Hz.
|
|
288
|
+
bearing_defect_freq_hz: Characteristic defect frequency in Hz
|
|
289
|
+
(BPFO, BPFI, BSF, or FTF).
|
|
290
|
+
defect_type: Label for the defect type.
|
|
291
|
+
harmonics: Number of sideband orders.
|
|
292
|
+
tolerance_hz: Frequency tolerance.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dictionary with sideband analysis.
|
|
296
|
+
"""
|
|
297
|
+
fs = supply_freq_hz
|
|
298
|
+
fd = bearing_defect_freq_hz
|
|
299
|
+
fund = amplitude_at_frequency(freqs, amps, fs, tolerance_hz)
|
|
300
|
+
a_fund = fund["amplitude"]
|
|
301
|
+
|
|
302
|
+
sidebands = []
|
|
303
|
+
worst_db = -np.inf
|
|
304
|
+
|
|
305
|
+
for k in range(1, harmonics + 1):
|
|
306
|
+
f_lo = fs - k * fd
|
|
307
|
+
f_hi = fs + k * fd
|
|
308
|
+
sb_lo = amplitude_at_frequency(freqs, amps, f_lo, tolerance_hz)
|
|
309
|
+
sb_hi = amplitude_at_frequency(freqs, amps, f_hi, tolerance_hz)
|
|
310
|
+
|
|
311
|
+
db_lo = _db_ratio(sb_lo["amplitude"], a_fund)
|
|
312
|
+
db_hi = _db_ratio(sb_hi["amplitude"], a_fund)
|
|
313
|
+
worst_db = max(worst_db, db_lo, db_hi)
|
|
314
|
+
|
|
315
|
+
sidebands.append({
|
|
316
|
+
"order": k,
|
|
317
|
+
"lower": {
|
|
318
|
+
"expected_hz": round(f_lo, 4),
|
|
319
|
+
**sb_lo,
|
|
320
|
+
"db_relative": round(float(db_lo), 2),
|
|
321
|
+
},
|
|
322
|
+
"upper": {
|
|
323
|
+
"expected_hz": round(f_hi, 4),
|
|
324
|
+
**sb_hi,
|
|
325
|
+
"db_relative": round(float(db_hi), 2),
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"fault_type": f"bearing_{defect_type}",
|
|
331
|
+
"defect_frequency_hz": round(fd, 4),
|
|
332
|
+
"fundamental": {
|
|
333
|
+
"expected_hz": fs,
|
|
334
|
+
**fund,
|
|
335
|
+
},
|
|
336
|
+
"sidebands": sidebands,
|
|
337
|
+
"worst_sideband_db": round(float(worst_db), 2),
|
|
338
|
+
"note": (
|
|
339
|
+
"Bearing signatures in stator current are typically weak. "
|
|
340
|
+
"Confirm with envelope analysis or vibration measurements."
|
|
341
|
+
),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
# Band energy index (cavitation, load faults)
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
def band_energy_index(
|
|
350
|
+
freqs: NDArray[np.floating],
|
|
351
|
+
psd: NDArray[np.floating],
|
|
352
|
+
centre_freq_hz: float,
|
|
353
|
+
bandwidth_hz: float = 5.0,
|
|
354
|
+
) -> dict:
|
|
355
|
+
"""Compute the integrated spectral energy in a frequency band.
|
|
356
|
+
|
|
357
|
+
Useful as generic fault/cavitation indicator: the energy in a band
|
|
358
|
+
around the supply frequency or other characteristic region.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
freqs: Frequency axis (from PSD).
|
|
362
|
+
psd: PSD values.
|
|
363
|
+
centre_freq_hz: Centre of the integration band.
|
|
364
|
+
bandwidth_hz: Total bandwidth for integration.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dictionary with band energy, limits used.
|
|
368
|
+
"""
|
|
369
|
+
low = centre_freq_hz - bandwidth_hz / 2.0
|
|
370
|
+
high = centre_freq_hz + bandwidth_hz / 2.0
|
|
371
|
+
|
|
372
|
+
mask = (freqs >= low) & (freqs <= high)
|
|
373
|
+
if not np.any(mask):
|
|
374
|
+
return {
|
|
375
|
+
"centre_freq_hz": centre_freq_hz,
|
|
376
|
+
"bandwidth_hz": bandwidth_hz,
|
|
377
|
+
"band_energy": 0.0,
|
|
378
|
+
"found": False,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
df = float(freqs[1] - freqs[0]) if len(freqs) > 1 else 1.0
|
|
382
|
+
energy = float(np.sum(psd[mask]) * df)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
"centre_freq_hz": centre_freq_hz,
|
|
386
|
+
"bandwidth_hz": bandwidth_hz,
|
|
387
|
+
"band_low_hz": round(low, 4),
|
|
388
|
+
"band_high_hz": round(high, 4),
|
|
389
|
+
"band_energy": energy,
|
|
390
|
+
"found": True,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
# Statistical indices on envelope
|
|
396
|
+
# ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
def envelope_statistical_indices(
|
|
399
|
+
envelope: NDArray[np.floating],
|
|
400
|
+
) -> dict:
|
|
401
|
+
"""Compute statistical indices of the envelope signal.
|
|
402
|
+
|
|
403
|
+
Kurtosis, skewness, crest factor, and RMS — indicators of impulsive
|
|
404
|
+
content from bearing or gear faults.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
envelope: Amplitude envelope of the current signal.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary of statistical indices.
|
|
411
|
+
"""
|
|
412
|
+
from scipy.stats import kurtosis, skew
|
|
413
|
+
|
|
414
|
+
rms = float(np.sqrt(np.mean(envelope ** 2)))
|
|
415
|
+
peak = float(np.max(np.abs(envelope)))
|
|
416
|
+
crest = peak / rms if rms > 0 else 0.0
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"rms": round(rms, 6),
|
|
420
|
+
"peak": round(peak, 6),
|
|
421
|
+
"crest_factor": round(crest, 4),
|
|
422
|
+
"kurtosis": round(float(kurtosis(envelope, fisher=True)), 4),
|
|
423
|
+
"skewness": round(float(skew(envelope)), 4),
|
|
424
|
+
}
|