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
mcp_server_mcsa/server.py
CHANGED
|
@@ -1,955 +1,954 @@
|
|
|
1
|
-
"""MCP Server for Motor Current Signature Analysis (MCSA).
|
|
2
|
-
|
|
3
|
-
Provides tools for spectral analysis, fault frequency computation,
|
|
4
|
-
fault detection, and diagnostic assessment of electric‑motor stator
|
|
5
|
-
currents via the Model Context Protocol.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
from typing import Annotated
|
|
12
|
-
|
|
13
|
-
import numpy as np
|
|
14
|
-
from mcp.server.fastmcp import FastMCP
|
|
15
|
-
from pydantic import Field
|
|
16
|
-
|
|
17
|
-
from mcp_server_mcsa.analysis.bearing import (
|
|
18
|
-
BearingGeometry,
|
|
19
|
-
bearing_current_sidebands,
|
|
20
|
-
calculate_bearing_defect_frequencies,
|
|
21
|
-
)
|
|
22
|
-
from mcp_server_mcsa.analysis.envelope import (
|
|
23
|
-
envelope_spectrum,
|
|
24
|
-
hilbert_envelope,
|
|
25
|
-
)
|
|
26
|
-
from mcp_server_mcsa.analysis.fault_detection import (
|
|
27
|
-
band_energy_index,
|
|
28
|
-
bearing_fault_index,
|
|
29
|
-
brb_fault_index,
|
|
30
|
-
eccentricity_fault_index,
|
|
31
|
-
envelope_statistical_indices,
|
|
32
|
-
stator_fault_index,
|
|
33
|
-
)
|
|
34
|
-
from mcp_server_mcsa.analysis.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
from mcp_server_mcsa.analysis.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
from mcp_server_mcsa.analysis.
|
|
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
|
-
result =
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
#
|
|
443
|
-
#
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
#
|
|
473
|
-
#
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
env =
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
"
|
|
510
|
-
"
|
|
511
|
-
"
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
"
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
float(stft_result["frequencies_hz"][
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
float(stft_result["times_s"][
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
info =
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
"
|
|
650
|
-
"
|
|
651
|
-
"
|
|
652
|
-
"
|
|
653
|
-
"
|
|
654
|
-
"
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
#
|
|
660
|
-
#
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
#
|
|
695
|
-
#
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
"
|
|
755
|
-
|
|
756
|
-
"
|
|
757
|
-
"
|
|
758
|
-
"
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
"
|
|
762
|
-
|
|
763
|
-
"
|
|
764
|
-
"
|
|
765
|
-
"
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
"
|
|
769
|
-
"
|
|
770
|
-
|
|
771
|
-
"
|
|
772
|
-
"
|
|
773
|
-
"
|
|
774
|
-
"
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
"""
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
#
|
|
798
|
-
#
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
"
|
|
870
|
-
"
|
|
871
|
-
"
|
|
872
|
-
|
|
873
|
-
"
|
|
874
|
-
"
|
|
875
|
-
"
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
"
|
|
879
|
-
|
|
880
|
-
"
|
|
881
|
-
"
|
|
882
|
-
"
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
"
|
|
886
|
-
"
|
|
887
|
-
|
|
888
|
-
"
|
|
889
|
-
"
|
|
890
|
-
"
|
|
891
|
-
"
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
#
|
|
900
|
-
#
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
"""
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
-
|
|
915
|
-
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
-
|
|
922
|
-
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
- `
|
|
935
|
-
- `
|
|
936
|
-
- `
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
- `
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
#
|
|
950
|
-
#
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
mcp.run(transport=transport)
|
|
1
|
+
"""MCP Server for Motor Current Signature Analysis (MCSA).
|
|
2
|
+
|
|
3
|
+
Provides tools for spectral analysis, fault frequency computation,
|
|
4
|
+
fault detection, and diagnostic assessment of electric‑motor stator
|
|
5
|
+
currents via the Model Context Protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Annotated, Literal
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
from mcp_server_mcsa.analysis.bearing import (
|
|
18
|
+
BearingGeometry,
|
|
19
|
+
bearing_current_sidebands,
|
|
20
|
+
calculate_bearing_defect_frequencies,
|
|
21
|
+
)
|
|
22
|
+
from mcp_server_mcsa.analysis.envelope import (
|
|
23
|
+
envelope_spectrum,
|
|
24
|
+
hilbert_envelope,
|
|
25
|
+
)
|
|
26
|
+
from mcp_server_mcsa.analysis.fault_detection import (
|
|
27
|
+
band_energy_index,
|
|
28
|
+
bearing_fault_index,
|
|
29
|
+
brb_fault_index,
|
|
30
|
+
eccentricity_fault_index,
|
|
31
|
+
envelope_statistical_indices,
|
|
32
|
+
stator_fault_index,
|
|
33
|
+
)
|
|
34
|
+
from mcp_server_mcsa.analysis.file_io import (
|
|
35
|
+
get_signal_file_info,
|
|
36
|
+
load_signal,
|
|
37
|
+
)
|
|
38
|
+
from mcp_server_mcsa.analysis.motor import (
|
|
39
|
+
calculate_fault_frequencies,
|
|
40
|
+
calculate_motor_parameters,
|
|
41
|
+
)
|
|
42
|
+
from mcp_server_mcsa.analysis.preprocessing import preprocess_pipeline
|
|
43
|
+
from mcp_server_mcsa.analysis.spectral import (
|
|
44
|
+
compute_fft_spectrum,
|
|
45
|
+
compute_psd,
|
|
46
|
+
detect_peaks,
|
|
47
|
+
)
|
|
48
|
+
from mcp_server_mcsa.analysis.test_signal import generate_test_signal
|
|
49
|
+
from mcp_server_mcsa.analysis.timefreq import (
|
|
50
|
+
compute_stft,
|
|
51
|
+
track_frequency_over_time,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Server instance
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
mcp = FastMCP(
|
|
59
|
+
"mcsa",
|
|
60
|
+
instructions=(
|
|
61
|
+
"Motor Current Signature Analysis (MCSA) server. "
|
|
62
|
+
"Provides tools for spectral analysis of electric‑motor stator "
|
|
63
|
+
"currents to detect rotor, stator, bearing, and load faults. "
|
|
64
|
+
"Typical workflow: "
|
|
65
|
+
"(1) load a real signal from file (CSV/WAV/NPY) with load_signal_from_file "
|
|
66
|
+
" or generate a synthetic one with generate_test_current_signal, "
|
|
67
|
+
"(2) compute motor parameters from nameplate data, "
|
|
68
|
+
"(3) preprocess the signal, (4) compute the spectrum, "
|
|
69
|
+
"(5) detect faults using the appropriate fault‑detection tools, "
|
|
70
|
+
"(6) generate a full diagnostic report with run_full_diagnosis. "
|
|
71
|
+
"For real‑world signals use inspect_signal_file first to check "
|
|
72
|
+
"the file format, then load_signal_from_file to read the data. "
|
|
73
|
+
"For a one‑shot analysis from file use diagnose_from_file."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ===================================================================
|
|
79
|
+
# RESOURCE: Fault Signatures Reference
|
|
80
|
+
# ===================================================================
|
|
81
|
+
|
|
82
|
+
FAULT_SIGNATURES_REFERENCE = """# MCSA Fault Signature Reference
|
|
83
|
+
|
|
84
|
+
## Broken Rotor Bars (BRB)
|
|
85
|
+
- **Signature**: Sidebands at (1 ± 2s)·f_s around the supply fundamental
|
|
86
|
+
- **Index**: dB ratio of sideband amplitude to fundamental
|
|
87
|
+
- **Thresholds** (dB below fundamental):
|
|
88
|
+
- Healthy: ≤ -50 dB
|
|
89
|
+
- Incipient: -50 to -45 dB
|
|
90
|
+
- Moderate: -45 to -40 dB
|
|
91
|
+
- Severe: > -35 dB
|
|
92
|
+
- **Notes**: More visible at medium–high load; higher harmonics at (1 ± 2ks)·f_s
|
|
93
|
+
|
|
94
|
+
## Eccentricity (Static / Dynamic)
|
|
95
|
+
- **Signature**: Sidebands at f_s ± k·f_r (rotor frequency multiples)
|
|
96
|
+
- **Static eccentricity**: produces components at f_s ± f_r
|
|
97
|
+
- **Dynamic eccentricity**: produces components at f_s ± k·f_r, varying with load
|
|
98
|
+
- **Mixed eccentricity**: components at n·f_r (pure rotational harmonics)
|
|
99
|
+
|
|
100
|
+
## Stator Inter-Turn Short Circuit
|
|
101
|
+
- **Signature**: Sidebands at f_s ± 2k·f_r
|
|
102
|
+
- **Notes**: May also increase negative-sequence current component;
|
|
103
|
+
distinguish from supply unbalance by checking load dependency
|
|
104
|
+
|
|
105
|
+
## Bearing Defects
|
|
106
|
+
- **Signature**: Sidebands at f_s ± k·f_defect where f_defect is BPFO/BPFI/BSF/FTF
|
|
107
|
+
- **Defect frequencies** (normalised to shaft speed):
|
|
108
|
+
- BPFO = (n/2)·(1 - d/D·cos α)
|
|
109
|
+
- BPFI = (n/2)·(1 + d/D·cos α)
|
|
110
|
+
- BSF = (D/2d)·(1 - (d/D·cos α)²)
|
|
111
|
+
- FTF = (1/2)·(1 - d/D·cos α)
|
|
112
|
+
- **Notes**: Weak in stator current; confirm with envelope analysis or vibration data
|
|
113
|
+
|
|
114
|
+
## Load Faults (Cavitation, Misalignment)
|
|
115
|
+
- **Signature**: Broadband energy increase around f_s ("foot" pattern in PSD)
|
|
116
|
+
- **Index**: Band energy integration around the supply frequency
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@mcp.resource("mcsa://fault-signatures")
|
|
121
|
+
def fault_signatures_resource() -> str:
|
|
122
|
+
"""Reference table of MCSA fault signatures, frequencies, and empirical thresholds."""
|
|
123
|
+
return FAULT_SIGNATURES_REFERENCE
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ===================================================================
|
|
127
|
+
# TOOL 1: Calculate Motor Parameters
|
|
128
|
+
# ===================================================================
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
def calculate_motor_params(
|
|
132
|
+
supply_freq_hz: Annotated[float, Field(description="Supply (line) frequency in Hz, e.g. 50 or 60")],
|
|
133
|
+
poles: Annotated[int, Field(description="Number of magnetic poles (even, ≥ 2)")],
|
|
134
|
+
rotor_speed_rpm: Annotated[float, Field(description="Measured rotor speed in RPM")],
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Calculate motor operating parameters from nameplate and measured data.
|
|
137
|
+
|
|
138
|
+
Computes synchronous speed, slip, rotor frequency, and slip frequency.
|
|
139
|
+
These parameters are required inputs for fault frequency calculations.
|
|
140
|
+
"""
|
|
141
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
142
|
+
return json.dumps(params.to_dict(), indent=2)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ===================================================================
|
|
146
|
+
# TOOL 2: Calculate Fault Frequencies
|
|
147
|
+
# ===================================================================
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
def compute_fault_frequencies(
|
|
151
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
152
|
+
poles: Annotated[int, Field(description="Number of magnetic poles")],
|
|
153
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
154
|
+
harmonics: Annotated[int, Field(description="Number of harmonic orders to compute", default=3)] = 3,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""Calculate expected fault frequencies for common induction-motor faults.
|
|
157
|
+
|
|
158
|
+
Computes characteristic frequencies for broken rotor bars, eccentricity,
|
|
159
|
+
stator faults, and mixed eccentricity based on motor operating parameters.
|
|
160
|
+
Use these frequencies to know WHERE to look in the current spectrum.
|
|
161
|
+
"""
|
|
162
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
163
|
+
result = calculate_fault_frequencies(params, harmonics)
|
|
164
|
+
return json.dumps(result, indent=2, default=str)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ===================================================================
|
|
168
|
+
# TOOL 3: Calculate Bearing Defect Frequencies
|
|
169
|
+
# ===================================================================
|
|
170
|
+
|
|
171
|
+
@mcp.tool()
|
|
172
|
+
def compute_bearing_frequencies(
|
|
173
|
+
n_balls: Annotated[int, Field(description="Number of rolling elements in the bearing")],
|
|
174
|
+
ball_dia_mm: Annotated[float, Field(description="Ball (roller) diameter in mm")],
|
|
175
|
+
pitch_dia_mm: Annotated[float, Field(description="Pitch (cage) diameter in mm")],
|
|
176
|
+
contact_angle_deg: Annotated[float, Field(description="Contact angle in degrees", default=0.0)] = 0.0,
|
|
177
|
+
shaft_speed_rpm: Annotated[float, Field(description="Shaft speed in RPM (optional, for absolute Hz)", default=0.0)] = 0.0,
|
|
178
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz (optional, for current sidebands)", default=0.0)] = 0.0,
|
|
179
|
+
) -> str:
|
|
180
|
+
"""Calculate bearing characteristic defect frequencies (BPFO, BPFI, BSF, FTF).
|
|
181
|
+
|
|
182
|
+
Returns normalised frequencies (multiples of shaft speed) and, if shaft
|
|
183
|
+
speed is provided, absolute frequencies in Hz. If supply frequency is
|
|
184
|
+
also given, computes expected stator-current sidebands.
|
|
185
|
+
"""
|
|
186
|
+
geom = BearingGeometry(n_balls, ball_dia_mm, pitch_dia_mm, contact_angle_deg)
|
|
187
|
+
defects = calculate_bearing_defect_frequencies(geom)
|
|
188
|
+
|
|
189
|
+
result: dict = {
|
|
190
|
+
"bearing_geometry": {
|
|
191
|
+
"n_balls": n_balls,
|
|
192
|
+
"ball_dia_mm": ball_dia_mm,
|
|
193
|
+
"pitch_dia_mm": pitch_dia_mm,
|
|
194
|
+
"contact_angle_deg": contact_angle_deg,
|
|
195
|
+
},
|
|
196
|
+
"normalised_to_shaft_speed": defects.to_dict(),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if shaft_speed_rpm > 0:
|
|
200
|
+
shaft_freq = shaft_speed_rpm / 60.0
|
|
201
|
+
result["absolute_frequencies_hz"] = defects.absolute(shaft_freq)
|
|
202
|
+
|
|
203
|
+
if supply_freq_hz > 0:
|
|
204
|
+
result["current_sidebands"] = bearing_current_sidebands(
|
|
205
|
+
defects, shaft_freq, supply_freq_hz
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return json.dumps(result, indent=2)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ===================================================================
|
|
212
|
+
# TOOL 4: Preprocess Signal
|
|
213
|
+
# ===================================================================
|
|
214
|
+
|
|
215
|
+
@mcp.tool()
|
|
216
|
+
def preprocess_signal(
|
|
217
|
+
signal: Annotated[list[float], Field(description="Raw current signal as a list of amplitude values")],
|
|
218
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
219
|
+
nominal_current: Annotated[float | None, Field(description="Nominal current for normalisation (A). Omit for RMS normalisation", default=None)] = None,
|
|
220
|
+
window: Annotated[str, Field(description="Window function: hann, hamming, blackman, flattop, rectangular", default="hann")] = "hann",
|
|
221
|
+
bandpass_low_hz: Annotated[float | None, Field(description="Lower bandpass cutoff in Hz (optional)", default=None)] = None,
|
|
222
|
+
bandpass_high_hz: Annotated[float | None, Field(description="Upper bandpass cutoff in Hz (optional)", default=None)] = None,
|
|
223
|
+
notch_freqs_hz: Annotated[list[float] | None, Field(description="Frequencies to notch-filter (optional)", default=None)] = None,
|
|
224
|
+
) -> str:
|
|
225
|
+
"""Preprocess a stator-current signal for spectral analysis.
|
|
226
|
+
|
|
227
|
+
Applies (in order): DC offset removal → notch filtering → bandpass
|
|
228
|
+
filtering → normalisation → windowing. Returns the preprocessed signal.
|
|
229
|
+
"""
|
|
230
|
+
x = np.array(signal, dtype=np.float64)
|
|
231
|
+
|
|
232
|
+
bandpass = None
|
|
233
|
+
if bandpass_low_hz is not None and bandpass_high_hz is not None:
|
|
234
|
+
bandpass = (bandpass_low_hz, bandpass_high_hz)
|
|
235
|
+
|
|
236
|
+
y = preprocess_pipeline(
|
|
237
|
+
x, sampling_freq_hz,
|
|
238
|
+
nominal_current=nominal_current,
|
|
239
|
+
window=window, # type: ignore[arg-type]
|
|
240
|
+
bandpass=bandpass,
|
|
241
|
+
notch_freqs=notch_freqs_hz,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return json.dumps({
|
|
245
|
+
"preprocessed_signal": y.tolist(),
|
|
246
|
+
"n_samples": len(y),
|
|
247
|
+
"sampling_freq_hz": sampling_freq_hz,
|
|
248
|
+
"steps_applied": [
|
|
249
|
+
"dc_offset_removal",
|
|
250
|
+
*(["notch_filter"] if notch_freqs_hz else []),
|
|
251
|
+
*(["bandpass_filter"] if bandpass else []),
|
|
252
|
+
"normalisation",
|
|
253
|
+
f"window_{window}",
|
|
254
|
+
],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ===================================================================
|
|
259
|
+
# TOOL 5: Compute FFT Spectrum
|
|
260
|
+
# ===================================================================
|
|
261
|
+
|
|
262
|
+
@mcp.tool()
|
|
263
|
+
def compute_spectrum(
|
|
264
|
+
signal: Annotated[list[float], Field(description="Time-domain signal (preprocessed or raw)")],
|
|
265
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
266
|
+
n_fft: Annotated[int | None, Field(description="FFT length (zero-padding). Omit for auto", default=None)] = None,
|
|
267
|
+
max_freq_hz: Annotated[float | None, Field(description="Maximum frequency to return (Hz). Omit for full range", default=None)] = None,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Compute the single-sided amplitude spectrum (FFT) of a current signal.
|
|
270
|
+
|
|
271
|
+
Returns frequency and amplitude arrays. Optionally limit the maximum
|
|
272
|
+
frequency returned to reduce output size.
|
|
273
|
+
"""
|
|
274
|
+
x = np.array(signal, dtype=np.float64)
|
|
275
|
+
freqs, amps = compute_fft_spectrum(x, sampling_freq_hz, n_fft=n_fft, sided="one")
|
|
276
|
+
|
|
277
|
+
if max_freq_hz is not None:
|
|
278
|
+
mask = freqs <= max_freq_hz
|
|
279
|
+
freqs = freqs[mask]
|
|
280
|
+
amps = amps[mask]
|
|
281
|
+
|
|
282
|
+
return json.dumps({
|
|
283
|
+
"frequencies_hz": freqs.tolist(),
|
|
284
|
+
"amplitudes": amps.tolist(),
|
|
285
|
+
"n_bins": len(freqs),
|
|
286
|
+
"freq_resolution_hz": round(float(freqs[1] - freqs[0]), 6) if len(freqs) > 1 else 0,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ===================================================================
|
|
291
|
+
# TOOL 6: Compute PSD
|
|
292
|
+
# ===================================================================
|
|
293
|
+
|
|
294
|
+
@mcp.tool()
|
|
295
|
+
def compute_power_spectral_density(
|
|
296
|
+
signal: Annotated[list[float], Field(description="Time-domain signal")],
|
|
297
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
298
|
+
nperseg: Annotated[int | None, Field(description="FFT segment length. Omit for auto", default=None)] = None,
|
|
299
|
+
max_freq_hz: Annotated[float | None, Field(description="Maximum frequency to return (Hz)", default=None)] = None,
|
|
300
|
+
) -> str:
|
|
301
|
+
"""Compute Power Spectral Density using Welch's method.
|
|
302
|
+
|
|
303
|
+
Better for noisy signals and trend analysis than a raw FFT.
|
|
304
|
+
Returns frequency and PSD arrays.
|
|
305
|
+
"""
|
|
306
|
+
x = np.array(signal, dtype=np.float64)
|
|
307
|
+
freqs, psd = compute_psd(x, sampling_freq_hz, nperseg=nperseg)
|
|
308
|
+
|
|
309
|
+
if max_freq_hz is not None:
|
|
310
|
+
mask = freqs <= max_freq_hz
|
|
311
|
+
freqs = freqs[mask]
|
|
312
|
+
psd = psd[mask]
|
|
313
|
+
|
|
314
|
+
return json.dumps({
|
|
315
|
+
"frequencies_hz": freqs.tolist(),
|
|
316
|
+
"psd_values": psd.tolist(),
|
|
317
|
+
"n_bins": len(freqs),
|
|
318
|
+
"freq_resolution_hz": round(float(freqs[1] - freqs[0]), 6) if len(freqs) > 1 else 0,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ===================================================================
|
|
323
|
+
# TOOL 7: Detect Spectral Peaks
|
|
324
|
+
# ===================================================================
|
|
325
|
+
|
|
326
|
+
@mcp.tool()
|
|
327
|
+
def find_spectrum_peaks(
|
|
328
|
+
frequencies_hz: Annotated[list[float], Field(description="Frequency axis from spectrum/PSD")],
|
|
329
|
+
amplitudes: Annotated[list[float], Field(description="Amplitude or PSD values")],
|
|
330
|
+
min_height: Annotated[float | None, Field(description="Minimum peak height", default=None)] = None,
|
|
331
|
+
min_prominence: Annotated[float | None, Field(description="Minimum peak prominence", default=None)] = None,
|
|
332
|
+
min_distance_hz: Annotated[float | None, Field(description="Min distance between peaks in Hz", default=None)] = None,
|
|
333
|
+
freq_low_hz: Annotated[float | None, Field(description="Lower frequency bound for search", default=None)] = None,
|
|
334
|
+
freq_high_hz: Annotated[float | None, Field(description="Upper frequency bound for search", default=None)] = None,
|
|
335
|
+
max_peaks: Annotated[int, Field(description="Maximum number of peaks to return", default=20)] = 20,
|
|
336
|
+
) -> str:
|
|
337
|
+
"""Detect peaks in a frequency spectrum.
|
|
338
|
+
|
|
339
|
+
Returns a list of peaks sorted by amplitude (highest first) with
|
|
340
|
+
frequency, amplitude, and prominence values.
|
|
341
|
+
"""
|
|
342
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
343
|
+
amps = np.array(amplitudes, dtype=np.float64)
|
|
344
|
+
|
|
345
|
+
freq_range = None
|
|
346
|
+
if freq_low_hz is not None and freq_high_hz is not None:
|
|
347
|
+
freq_range = (freq_low_hz, freq_high_hz)
|
|
348
|
+
|
|
349
|
+
peaks = detect_peaks(
|
|
350
|
+
freqs, amps,
|
|
351
|
+
height=min_height,
|
|
352
|
+
prominence=min_prominence,
|
|
353
|
+
distance_hz=min_distance_hz,
|
|
354
|
+
freq_range=freq_range,
|
|
355
|
+
max_peaks=max_peaks,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return json.dumps({"peaks": peaks, "n_peaks_found": len(peaks)}, indent=2)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ===================================================================
|
|
362
|
+
# TOOL 8: Detect Broken Rotor Bars
|
|
363
|
+
# ===================================================================
|
|
364
|
+
|
|
365
|
+
@mcp.tool()
|
|
366
|
+
def detect_broken_rotor_bars(
|
|
367
|
+
frequencies_hz: Annotated[list[float], Field(description="Spectrum frequency axis (Hz)")],
|
|
368
|
+
amplitudes: Annotated[list[float], Field(description="Spectrum amplitude values")],
|
|
369
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
370
|
+
poles: Annotated[int, Field(description="Number of poles")],
|
|
371
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
372
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
373
|
+
) -> str:
|
|
374
|
+
"""Detect broken rotor bar faults from current spectrum.
|
|
375
|
+
|
|
376
|
+
Computes the BRB fault index by measuring sidebands at (1 ± 2s)·f_s
|
|
377
|
+
relative to the fundamental. Returns severity classification:
|
|
378
|
+
healthy / incipient / moderate / severe.
|
|
379
|
+
"""
|
|
380
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
381
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
382
|
+
amps = np.array(amplitudes, dtype=np.float64)
|
|
383
|
+
|
|
384
|
+
result = brb_fault_index(freqs, amps, params, tolerance_hz)
|
|
385
|
+
return json.dumps(result, indent=2, default=str)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ===================================================================
|
|
389
|
+
# TOOL 9: Detect Eccentricity
|
|
390
|
+
# ===================================================================
|
|
391
|
+
|
|
392
|
+
@mcp.tool()
|
|
393
|
+
def detect_eccentricity(
|
|
394
|
+
frequencies_hz: Annotated[list[float], Field(description="Spectrum frequency axis (Hz)")],
|
|
395
|
+
amplitudes: Annotated[list[float], Field(description="Spectrum amplitude values")],
|
|
396
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
397
|
+
poles: Annotated[int, Field(description="Number of poles")],
|
|
398
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
399
|
+
harmonics: Annotated[int, Field(description="Number of harmonic orders to check", default=3)] = 3,
|
|
400
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
401
|
+
) -> str:
|
|
402
|
+
"""Detect air-gap eccentricity faults from current spectrum.
|
|
403
|
+
|
|
404
|
+
Analyses sidebands at f_s ± k·f_r for static and dynamic eccentricity.
|
|
405
|
+
Returns severity classification.
|
|
406
|
+
"""
|
|
407
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
408
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
409
|
+
amps = np.array(amplitudes, dtype=np.float64)
|
|
410
|
+
|
|
411
|
+
result = eccentricity_fault_index(freqs, amps, params, harmonics, tolerance_hz)
|
|
412
|
+
return json.dumps(result, indent=2, default=str)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ===================================================================
|
|
416
|
+
# TOOL 10: Detect Stator Faults
|
|
417
|
+
# ===================================================================
|
|
418
|
+
|
|
419
|
+
@mcp.tool()
|
|
420
|
+
def detect_stator_faults(
|
|
421
|
+
frequencies_hz: Annotated[list[float], Field(description="Spectrum frequency axis (Hz)")],
|
|
422
|
+
amplitudes: Annotated[list[float], Field(description="Spectrum amplitude values")],
|
|
423
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
424
|
+
poles: Annotated[int, Field(description="Number of poles")],
|
|
425
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
426
|
+
harmonics: Annotated[int, Field(description="Number of harmonic orders", default=3)] = 3,
|
|
427
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
428
|
+
) -> str:
|
|
429
|
+
"""Detect stator inter-turn short circuit faults from current spectrum.
|
|
430
|
+
|
|
431
|
+
Analyses sidebands at f_s ± 2k·f_r caused by stator winding asymmetry.
|
|
432
|
+
"""
|
|
433
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
434
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
435
|
+
amps = np.array(amplitudes, dtype=np.float64)
|
|
436
|
+
|
|
437
|
+
result = stator_fault_index(freqs, amps, params, harmonics, tolerance_hz)
|
|
438
|
+
return json.dumps(result, indent=2, default=str)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ===================================================================
|
|
442
|
+
# TOOL 11: Detect Bearing Faults
|
|
443
|
+
# ===================================================================
|
|
444
|
+
|
|
445
|
+
@mcp.tool()
|
|
446
|
+
def detect_bearing_faults(
|
|
447
|
+
frequencies_hz: Annotated[list[float], Field(description="Spectrum frequency axis (Hz)")],
|
|
448
|
+
amplitudes: Annotated[list[float], Field(description="Spectrum amplitude values")],
|
|
449
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
450
|
+
bearing_defect_freq_hz: Annotated[float, Field(description="Bearing characteristic defect frequency in Hz (BPFO, BPFI, BSF, or FTF)")],
|
|
451
|
+
defect_type: Annotated[str, Field(description="Defect type label: bpfo, bpfi, bsf, or ftf", default="bpfo")] = "bpfo",
|
|
452
|
+
harmonics: Annotated[int, Field(description="Number of sideband orders", default=2)] = 2,
|
|
453
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
454
|
+
) -> str:
|
|
455
|
+
"""Detect bearing defect signatures in the stator-current spectrum.
|
|
456
|
+
|
|
457
|
+
Bearing faults modulate motor torque, creating sidebands at
|
|
458
|
+
f_s ± k·f_defect. Note: bearing signatures in current are typically
|
|
459
|
+
weak; envelope analysis or vibration data can improve detection.
|
|
460
|
+
"""
|
|
461
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
462
|
+
amps = np.array(amplitudes, dtype=np.float64)
|
|
463
|
+
|
|
464
|
+
result = bearing_fault_index(
|
|
465
|
+
freqs, amps, supply_freq_hz,
|
|
466
|
+
bearing_defect_freq_hz, defect_type, harmonics, tolerance_hz,
|
|
467
|
+
)
|
|
468
|
+
return json.dumps(result, indent=2, default=str)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ===================================================================
|
|
472
|
+
# TOOL 12: Compute Envelope Spectrum
|
|
473
|
+
# ===================================================================
|
|
474
|
+
|
|
475
|
+
@mcp.tool()
|
|
476
|
+
def compute_envelope_spectrum(
|
|
477
|
+
signal: Annotated[list[float], Field(description="Time-domain current signal")],
|
|
478
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
479
|
+
bandpass_low_hz: Annotated[float | None, Field(description="Lower bandpass cutoff before envelope (optional)", default=None)] = None,
|
|
480
|
+
bandpass_high_hz: Annotated[float | None, Field(description="Upper bandpass cutoff before envelope (optional)", default=None)] = None,
|
|
481
|
+
max_freq_hz: Annotated[float | None, Field(description="Max frequency to return (Hz)", default=None)] = None,
|
|
482
|
+
) -> str:
|
|
483
|
+
"""Compute the envelope spectrum of a current signal.
|
|
484
|
+
|
|
485
|
+
Uses the Hilbert transform to extract the amplitude envelope, then
|
|
486
|
+
computes its FFT. Useful for detecting bearing and mechanical faults
|
|
487
|
+
that modulate the current at low frequencies.
|
|
488
|
+
"""
|
|
489
|
+
x = np.array(signal, dtype=np.float64)
|
|
490
|
+
|
|
491
|
+
bp = None
|
|
492
|
+
if bandpass_low_hz is not None and bandpass_high_hz is not None:
|
|
493
|
+
bp = (bandpass_low_hz, bandpass_high_hz)
|
|
494
|
+
|
|
495
|
+
freqs, amps = envelope_spectrum(x, sampling_freq_hz, bandpass=bp)
|
|
496
|
+
|
|
497
|
+
if max_freq_hz is not None:
|
|
498
|
+
mask = freqs <= max_freq_hz
|
|
499
|
+
freqs = freqs[mask]
|
|
500
|
+
amps = amps[mask]
|
|
501
|
+
|
|
502
|
+
# Also compute statistical indices of the envelope
|
|
503
|
+
env = hilbert_envelope(x)
|
|
504
|
+
env = env - np.mean(env)
|
|
505
|
+
stats = envelope_statistical_indices(env)
|
|
506
|
+
|
|
507
|
+
return json.dumps({
|
|
508
|
+
"frequencies_hz": freqs.tolist(),
|
|
509
|
+
"amplitudes": amps.tolist(),
|
|
510
|
+
"n_bins": len(freqs),
|
|
511
|
+
"envelope_statistics": stats,
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# ===================================================================
|
|
516
|
+
# TOOL 13: Compute Band Energy
|
|
517
|
+
# ===================================================================
|
|
518
|
+
|
|
519
|
+
@mcp.tool()
|
|
520
|
+
def compute_band_energy(
|
|
521
|
+
frequencies_hz: Annotated[list[float], Field(description="PSD frequency axis (Hz)")],
|
|
522
|
+
psd_values: Annotated[list[float], Field(description="PSD values")],
|
|
523
|
+
centre_freq_hz: Annotated[float, Field(description="Centre of the frequency band (Hz)")],
|
|
524
|
+
bandwidth_hz: Annotated[float, Field(description="Total bandwidth for energy integration (Hz)", default=5.0)] = 5.0,
|
|
525
|
+
) -> str:
|
|
526
|
+
"""Compute the integrated spectral energy in a frequency band.
|
|
527
|
+
|
|
528
|
+
Useful as a generic fault/cavitation indicator — measures the energy
|
|
529
|
+
concentration around a characteristic frequency in the PSD.
|
|
530
|
+
"""
|
|
531
|
+
freqs = np.array(frequencies_hz, dtype=np.float64)
|
|
532
|
+
psd = np.array(psd_values, dtype=np.float64)
|
|
533
|
+
|
|
534
|
+
result = band_energy_index(freqs, psd, centre_freq_hz, bandwidth_hz)
|
|
535
|
+
return json.dumps(result, indent=2)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# ===================================================================
|
|
539
|
+
# TOOL 14: Compute STFT
|
|
540
|
+
# ===================================================================
|
|
541
|
+
|
|
542
|
+
@mcp.tool()
|
|
543
|
+
def compute_time_frequency(
|
|
544
|
+
signal: Annotated[list[float], Field(description="Time-domain current signal")],
|
|
545
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
546
|
+
nperseg: Annotated[int, Field(description="Window length per segment (samples)", default=256)] = 256,
|
|
547
|
+
target_freq_hz: Annotated[float | None, Field(description="Frequency to track over time (Hz). If provided, returns amplitude vs time for that frequency", default=None)] = None,
|
|
548
|
+
tolerance_hz: Annotated[float, Field(description="Tolerance for frequency tracking (Hz)", default=2.0)] = 2.0,
|
|
549
|
+
) -> str:
|
|
550
|
+
"""Compute Short-Time Fourier Transform (STFT) for time-frequency analysis.
|
|
551
|
+
|
|
552
|
+
For non-stationary conditions (variable speed/load, start-up transients).
|
|
553
|
+
If target_freq_hz is provided, also tracks that frequency's amplitude over time.
|
|
554
|
+
Returns a summary (not the full 2D matrix) to keep output manageable.
|
|
555
|
+
"""
|
|
556
|
+
x = np.array(signal, dtype=np.float64)
|
|
557
|
+
stft_result = compute_stft(x, sampling_freq_hz, nperseg=nperseg)
|
|
558
|
+
|
|
559
|
+
output: dict = {
|
|
560
|
+
"n_freq_bins": stft_result["n_freq_bins"],
|
|
561
|
+
"n_time_bins": stft_result["n_time_bins"],
|
|
562
|
+
"freq_range_hz": [
|
|
563
|
+
float(stft_result["frequencies_hz"][0]),
|
|
564
|
+
float(stft_result["frequencies_hz"][-1]),
|
|
565
|
+
],
|
|
566
|
+
"time_range_s": [
|
|
567
|
+
float(stft_result["times_s"][0]),
|
|
568
|
+
float(stft_result["times_s"][-1]),
|
|
569
|
+
],
|
|
570
|
+
"freq_resolution_hz": round(
|
|
571
|
+
float(stft_result["frequencies_hz"][1] - stft_result["frequencies_hz"][0]), 6
|
|
572
|
+
) if stft_result["n_freq_bins"] > 1 else 0,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Average spectrum over time
|
|
576
|
+
avg_spectrum = np.mean(stft_result["magnitude"], axis=1)
|
|
577
|
+
output["average_spectrum"] = {
|
|
578
|
+
"frequencies_hz": stft_result["frequencies_hz"].tolist(),
|
|
579
|
+
"amplitudes": avg_spectrum.tolist(),
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if target_freq_hz is not None:
|
|
583
|
+
tracking = track_frequency_over_time(stft_result, target_freq_hz, tolerance_hz)
|
|
584
|
+
output["frequency_tracking"] = tracking
|
|
585
|
+
|
|
586
|
+
return json.dumps(output, indent=2, default=str)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# ===================================================================
|
|
590
|
+
# TOOL 15: Inspect Signal File
|
|
591
|
+
# ===================================================================
|
|
592
|
+
|
|
593
|
+
@mcp.tool()
|
|
594
|
+
def inspect_signal_file(
|
|
595
|
+
file_path: Annotated[str, Field(description="Absolute path to the signal file (CSV, WAV, or NPY)")],
|
|
596
|
+
) -> str:
|
|
597
|
+
"""Inspect a signal file without fully loading it.
|
|
598
|
+
|
|
599
|
+
Returns file metadata: size, format details, estimated number of
|
|
600
|
+
samples, sampling frequency (for WAV), column headers (for CSV),
|
|
601
|
+
and array shape (for NPY). Use this before load_signal_from_file
|
|
602
|
+
to verify the file format and plan the loading parameters.
|
|
603
|
+
"""
|
|
604
|
+
info = get_signal_file_info(file_path)
|
|
605
|
+
return json.dumps(info, indent=2)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ===================================================================
|
|
609
|
+
# TOOL 16: Load Signal from File
|
|
610
|
+
# ===================================================================
|
|
611
|
+
|
|
612
|
+
@mcp.tool()
|
|
613
|
+
def load_signal_from_file(
|
|
614
|
+
file_path: Annotated[str, Field(description="Absolute path to the signal file (CSV, WAV, or NPY)")],
|
|
615
|
+
sampling_freq_hz: Annotated[float | None, Field(description="Sampling frequency in Hz. Required for NPY; optional for CSV if a time column exists; auto-detected for WAV", default=None)] = None,
|
|
616
|
+
signal_column: Annotated[int | str, Field(description="CSV column containing the current signal (0-based index or header name)", default=1)] = 1,
|
|
617
|
+
time_column: Annotated[int | str | None, Field(description="CSV column for time (index or name). Set to null if no time column", default=0)] = 0,
|
|
618
|
+
delimiter: Annotated[str | None, Field(description="CSV delimiter. Auto-detected if null (comma for .csv, tab for .tsv/.txt)", default=None)] = None,
|
|
619
|
+
channel: Annotated[int, Field(description="WAV channel index (0-based) for multi-channel files", default=0)] = 0,
|
|
620
|
+
skip_header: Annotated[int, Field(description="Number of CSV header rows to skip", default=1)] = 1,
|
|
621
|
+
max_rows: Annotated[int | None, Field(description="Max data rows to read from CSV (null = all)", default=None)] = None,
|
|
622
|
+
) -> str:
|
|
623
|
+
"""Load a motor-current signal from a file (CSV, WAV, or NumPy NPY).
|
|
624
|
+
|
|
625
|
+
Supports the most common formats used by industrial DAQ systems:
|
|
626
|
+
- **CSV/TSV/TXT**: Columnar data with optional time column.
|
|
627
|
+
The sampling frequency is inferred from the time column or
|
|
628
|
+
must be provided explicitly.
|
|
629
|
+
- **WAV**: Audio files from portable recorders or DAQ. Sampling
|
|
630
|
+
frequency is read from the WAV header.
|
|
631
|
+
- **NPY**: NumPy binary arrays. Sampling frequency must be provided.
|
|
632
|
+
|
|
633
|
+
Returns the signal, sampling frequency, number of samples, duration,
|
|
634
|
+
and file metadata. The returned signal can then be passed to
|
|
635
|
+
preprocess_signal, compute_spectrum, or run_full_diagnosis.
|
|
636
|
+
"""
|
|
637
|
+
result = load_signal(
|
|
638
|
+
file_path,
|
|
639
|
+
sampling_freq_hz=sampling_freq_hz,
|
|
640
|
+
signal_column=signal_column,
|
|
641
|
+
time_column=time_column,
|
|
642
|
+
delimiter=delimiter,
|
|
643
|
+
channel=channel,
|
|
644
|
+
skip_header=skip_header,
|
|
645
|
+
max_rows=max_rows,
|
|
646
|
+
)
|
|
647
|
+
return json.dumps({
|
|
648
|
+
"signal": result["signal"],
|
|
649
|
+
"sampling_freq_hz": result["sampling_freq_hz"],
|
|
650
|
+
"n_samples": result["n_samples"],
|
|
651
|
+
"duration_s": result["duration_s"],
|
|
652
|
+
"file_path": result["file_path"],
|
|
653
|
+
"format": result["format"],
|
|
654
|
+
"metadata": result.get("metadata"),
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# ===================================================================
|
|
659
|
+
# TOOL 17: Generate Test Signal
|
|
660
|
+
# ===================================================================
|
|
661
|
+
|
|
662
|
+
@mcp.tool()
|
|
663
|
+
def generate_test_current_signal(
|
|
664
|
+
duration_s: Annotated[float, Field(description="Signal duration in seconds", default=10.0)] = 10.0,
|
|
665
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz", default=5000.0)] = 5000.0,
|
|
666
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz", default=50.0)] = 50.0,
|
|
667
|
+
poles: Annotated[int, Field(description="Number of poles", default=4)] = 4,
|
|
668
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM", default=1470.0)] = 1470.0,
|
|
669
|
+
noise_level: Annotated[float, Field(description="Noise standard deviation (0-1 relative)", default=0.01)] = 0.01,
|
|
670
|
+
faults: Annotated[list[str] | None, Field(description="Faults to inject: 'brb', 'eccentricity', 'bearing'. Omit for healthy signal", default=None)] = None,
|
|
671
|
+
fault_severity: Annotated[float, Field(description="Fault component amplitude (0-1 relative)", default=0.02)] = 0.02,
|
|
672
|
+
) -> str:
|
|
673
|
+
"""Generate a synthetic motor-current test signal.
|
|
674
|
+
|
|
675
|
+
Creates a simulated stator-current waveform with the fundamental,
|
|
676
|
+
supply harmonics, noise, and optional fault signatures. Useful for
|
|
677
|
+
testing, validation, and demonstration of MCSA analysis tools.
|
|
678
|
+
"""
|
|
679
|
+
result = generate_test_signal(
|
|
680
|
+
duration_s=duration_s,
|
|
681
|
+
fs_sample=sampling_freq_hz,
|
|
682
|
+
supply_freq_hz=supply_freq_hz,
|
|
683
|
+
poles=poles,
|
|
684
|
+
rotor_speed_rpm=rotor_speed_rpm,
|
|
685
|
+
noise_std=noise_level,
|
|
686
|
+
faults=faults,
|
|
687
|
+
fault_severity=fault_severity,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
return json.dumps(result)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
# ===================================================================
|
|
694
|
+
# TOOL 18: Full Diagnostic Report
|
|
695
|
+
# ===================================================================
|
|
696
|
+
|
|
697
|
+
@mcp.tool()
|
|
698
|
+
def run_full_diagnosis(
|
|
699
|
+
signal: Annotated[list[float], Field(description="Raw time-domain current signal")],
|
|
700
|
+
sampling_freq_hz: Annotated[float, Field(description="Sampling frequency in Hz")],
|
|
701
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
702
|
+
poles: Annotated[int, Field(description="Number of poles")],
|
|
703
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
704
|
+
bearing_defect_freq_hz: Annotated[float | None, Field(description="Bearing defect frequency in Hz (optional, for bearing analysis)", default=None)] = None,
|
|
705
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
706
|
+
) -> str:
|
|
707
|
+
"""Run a comprehensive MCSA diagnostic analysis on a current signal.
|
|
708
|
+
|
|
709
|
+
Performs the full pipeline: preprocessing → spectrum → fault detection
|
|
710
|
+
for broken rotor bars, eccentricity, stator faults, and optionally
|
|
711
|
+
bearing defects. Returns a complete diagnostic report.
|
|
712
|
+
"""
|
|
713
|
+
x = np.array(signal, dtype=np.float64)
|
|
714
|
+
|
|
715
|
+
# Motor parameters
|
|
716
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
717
|
+
|
|
718
|
+
# Preprocess
|
|
719
|
+
x_proc = preprocess_pipeline(x, sampling_freq_hz, window="hann")
|
|
720
|
+
|
|
721
|
+
# Spectrum
|
|
722
|
+
freqs, amps = compute_fft_spectrum(x_proc, sampling_freq_hz, sided="one")
|
|
723
|
+
|
|
724
|
+
# PSD for band energy
|
|
725
|
+
freqs_psd, psd_vals = compute_psd(x, sampling_freq_hz)
|
|
726
|
+
|
|
727
|
+
# Fault detection
|
|
728
|
+
brb = brb_fault_index(freqs, amps, params, tolerance_hz)
|
|
729
|
+
ecc = eccentricity_fault_index(freqs, amps, params, tolerance_hz=tolerance_hz)
|
|
730
|
+
stator = stator_fault_index(freqs, amps, params, tolerance_hz=tolerance_hz)
|
|
731
|
+
|
|
732
|
+
# Band energy around fundamental
|
|
733
|
+
be = band_energy_index(freqs_psd, psd_vals, supply_freq_hz, bandwidth_hz=10.0)
|
|
734
|
+
|
|
735
|
+
# Envelope statistics
|
|
736
|
+
env = hilbert_envelope(x)
|
|
737
|
+
env_dc_removed = env - np.mean(env)
|
|
738
|
+
env_stats = envelope_statistical_indices(env_dc_removed)
|
|
739
|
+
|
|
740
|
+
# Bearing (if geometry provided)
|
|
741
|
+
bearing_result = None
|
|
742
|
+
if bearing_defect_freq_hz is not None:
|
|
743
|
+
bearing_result = bearing_fault_index(
|
|
744
|
+
freqs, amps, supply_freq_hz,
|
|
745
|
+
bearing_defect_freq_hz, "bearing", tolerance_hz=tolerance_hz,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Top peaks
|
|
749
|
+
peaks = detect_peaks(freqs, amps, prominence=0.001, max_peaks=10)
|
|
750
|
+
|
|
751
|
+
# Assemble report
|
|
752
|
+
report = {
|
|
753
|
+
"motor_parameters": params.to_dict(),
|
|
754
|
+
"signal_info": {
|
|
755
|
+
"n_samples": len(signal),
|
|
756
|
+
"sampling_freq_hz": sampling_freq_hz,
|
|
757
|
+
"duration_s": round(len(signal) / sampling_freq_hz, 3),
|
|
758
|
+
"freq_resolution_hz": round(float(freqs[1] - freqs[0]), 6) if len(freqs) > 1 else 0,
|
|
759
|
+
},
|
|
760
|
+
"top_spectral_peaks": peaks,
|
|
761
|
+
"fault_analysis": {
|
|
762
|
+
"broken_rotor_bars": brb,
|
|
763
|
+
"eccentricity": ecc,
|
|
764
|
+
"stator_inter_turn": stator,
|
|
765
|
+
"bearing": bearing_result,
|
|
766
|
+
},
|
|
767
|
+
"band_energy_around_fundamental": be,
|
|
768
|
+
"envelope_statistics": env_stats,
|
|
769
|
+
"summary": {
|
|
770
|
+
"brb_severity": brb["severity"],
|
|
771
|
+
"eccentricity_severity": ecc["severity"],
|
|
772
|
+
"stator_severity": stator["severity"],
|
|
773
|
+
"envelope_kurtosis": env_stats["kurtosis"],
|
|
774
|
+
"overall_assessment": _overall_assessment(brb, ecc, stator, env_stats),
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return json.dumps(report, indent=2, default=str)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _overall_assessment(brb: dict, ecc: dict, stator: dict, env_stats: dict) -> str:
|
|
782
|
+
"""Generate a brief overall assessment string."""
|
|
783
|
+
severities = [brb["severity"], ecc["severity"], stator["severity"]]
|
|
784
|
+
|
|
785
|
+
if "severe" in severities:
|
|
786
|
+
return "CRITICAL — One or more fault indicators at severe level. Immediate inspection recommended."
|
|
787
|
+
if "moderate" in severities:
|
|
788
|
+
return "WARNING — Moderate fault indication detected. Schedule inspection."
|
|
789
|
+
if "incipient" in severities:
|
|
790
|
+
return "WATCH — Incipient fault signatures detected. Increase monitoring frequency."
|
|
791
|
+
if env_stats["kurtosis"] > 6.0:
|
|
792
|
+
return "WATCH — Elevated envelope kurtosis may indicate mechanical impulsiveness."
|
|
793
|
+
return "NORMAL — No significant fault indicators detected."
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# ===================================================================
|
|
797
|
+
# TOOL 19: Diagnose from File (one-shot)
|
|
798
|
+
# ===================================================================
|
|
799
|
+
|
|
800
|
+
@mcp.tool()
|
|
801
|
+
def diagnose_from_file(
|
|
802
|
+
file_path: Annotated[str, Field(description="Absolute path to the signal file (CSV, WAV, or NPY)")],
|
|
803
|
+
supply_freq_hz: Annotated[float, Field(description="Supply frequency in Hz")],
|
|
804
|
+
poles: Annotated[int, Field(description="Number of poles")],
|
|
805
|
+
rotor_speed_rpm: Annotated[float, Field(description="Rotor speed in RPM")],
|
|
806
|
+
sampling_freq_hz: Annotated[float | None, Field(description="Sampling frequency in Hz (required for NPY, optional for CSV with time column, auto-detected for WAV)", default=None)] = None,
|
|
807
|
+
signal_column: Annotated[int | str, Field(description="CSV column for the current signal", default=1)] = 1,
|
|
808
|
+
time_column: Annotated[int | str | None, Field(description="CSV column for time (null if absent)", default=0)] = 0,
|
|
809
|
+
channel: Annotated[int, Field(description="WAV channel index", default=0)] = 0,
|
|
810
|
+
bearing_defect_freq_hz: Annotated[float | None, Field(description="Bearing defect frequency in Hz (optional)", default=None)] = None,
|
|
811
|
+
tolerance_hz: Annotated[float, Field(description="Frequency search tolerance in Hz", default=0.5)] = 0.5,
|
|
812
|
+
) -> str:
|
|
813
|
+
"""Load a signal from file and run the full MCSA diagnostic pipeline.
|
|
814
|
+
|
|
815
|
+
One-shot tool: reads the signal file, preprocesses, computes the
|
|
816
|
+
spectrum, runs all fault detectors, and returns a complete diagnostic
|
|
817
|
+
report. Ideal for batch or automated condition-monitoring workflows.
|
|
818
|
+
"""
|
|
819
|
+
# Load signal
|
|
820
|
+
loaded = load_signal(
|
|
821
|
+
file_path,
|
|
822
|
+
sampling_freq_hz=sampling_freq_hz,
|
|
823
|
+
signal_column=signal_column,
|
|
824
|
+
time_column=time_column,
|
|
825
|
+
channel=channel,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
x = np.array(loaded["signal"], dtype=np.float64)
|
|
829
|
+
fs_sample = loaded["sampling_freq_hz"]
|
|
830
|
+
|
|
831
|
+
# Motor parameters
|
|
832
|
+
params = calculate_motor_parameters(supply_freq_hz, poles, rotor_speed_rpm)
|
|
833
|
+
|
|
834
|
+
# Preprocess
|
|
835
|
+
x_proc = preprocess_pipeline(x, fs_sample, window="hann")
|
|
836
|
+
|
|
837
|
+
# Spectrum
|
|
838
|
+
freqs, amps = compute_fft_spectrum(x_proc, fs_sample, sided="one")
|
|
839
|
+
|
|
840
|
+
# PSD
|
|
841
|
+
freqs_psd, psd_vals = compute_psd(x, fs_sample)
|
|
842
|
+
|
|
843
|
+
# Fault detection
|
|
844
|
+
brb = brb_fault_index(freqs, amps, params, tolerance_hz)
|
|
845
|
+
ecc = eccentricity_fault_index(freqs, amps, params, tolerance_hz=tolerance_hz)
|
|
846
|
+
stator = stator_fault_index(freqs, amps, params, tolerance_hz=tolerance_hz)
|
|
847
|
+
|
|
848
|
+
# Band energy
|
|
849
|
+
be = band_energy_index(freqs_psd, psd_vals, supply_freq_hz, bandwidth_hz=10.0)
|
|
850
|
+
|
|
851
|
+
# Envelope
|
|
852
|
+
env = hilbert_envelope(x)
|
|
853
|
+
env_dc = env - np.mean(env)
|
|
854
|
+
env_stats = envelope_statistical_indices(env_dc)
|
|
855
|
+
|
|
856
|
+
# Bearing
|
|
857
|
+
bearing_result = None
|
|
858
|
+
if bearing_defect_freq_hz is not None:
|
|
859
|
+
bearing_result = bearing_fault_index(
|
|
860
|
+
freqs, amps, supply_freq_hz,
|
|
861
|
+
bearing_defect_freq_hz, "bearing", tolerance_hz=tolerance_hz,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# Peaks
|
|
865
|
+
peaks = detect_peaks(freqs, amps, prominence=0.001, max_peaks=10)
|
|
866
|
+
|
|
867
|
+
report = {
|
|
868
|
+
"source_file": loaded["file_path"],
|
|
869
|
+
"file_format": loaded["format"],
|
|
870
|
+
"motor_parameters": params.to_dict(),
|
|
871
|
+
"signal_info": {
|
|
872
|
+
"n_samples": loaded["n_samples"],
|
|
873
|
+
"sampling_freq_hz": fs_sample,
|
|
874
|
+
"duration_s": loaded["duration_s"],
|
|
875
|
+
"freq_resolution_hz": round(float(freqs[1] - freqs[0]), 6) if len(freqs) > 1 else 0,
|
|
876
|
+
},
|
|
877
|
+
"top_spectral_peaks": peaks,
|
|
878
|
+
"fault_analysis": {
|
|
879
|
+
"broken_rotor_bars": brb,
|
|
880
|
+
"eccentricity": ecc,
|
|
881
|
+
"stator_inter_turn": stator,
|
|
882
|
+
"bearing": bearing_result,
|
|
883
|
+
},
|
|
884
|
+
"band_energy_around_fundamental": be,
|
|
885
|
+
"envelope_statistics": env_stats,
|
|
886
|
+
"summary": {
|
|
887
|
+
"brb_severity": brb["severity"],
|
|
888
|
+
"eccentricity_severity": ecc["severity"],
|
|
889
|
+
"stator_severity": stator["severity"],
|
|
890
|
+
"envelope_kurtosis": env_stats["kurtosis"],
|
|
891
|
+
"overall_assessment": _overall_assessment(brb, ecc, stator, env_stats),
|
|
892
|
+
},
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return json.dumps(report, indent=2, default=str)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
# ===================================================================
|
|
899
|
+
# PROMPT: Guided analysis
|
|
900
|
+
# ===================================================================
|
|
901
|
+
|
|
902
|
+
@mcp.prompt()
|
|
903
|
+
def analyze_motor_current(
|
|
904
|
+
motor_type: str = "induction",
|
|
905
|
+
supply_freq_hz: str = "50",
|
|
906
|
+
poles: str = "4",
|
|
907
|
+
rotor_speed_rpm: str = "1470",
|
|
908
|
+
) -> str:
|
|
909
|
+
"""Step-by-step guided prompt for MCSA analysis of a motor current signal."""
|
|
910
|
+
return f"""You are performing Motor Current Signature Analysis (MCSA) on a {motor_type} motor.
|
|
911
|
+
|
|
912
|
+
Motor parameters:
|
|
913
|
+
- Supply frequency: {supply_freq_hz} Hz
|
|
914
|
+
- Poles: {poles}
|
|
915
|
+
- Rotor speed: {rotor_speed_rpm} RPM
|
|
916
|
+
|
|
917
|
+
Follow this diagnostic workflow:
|
|
918
|
+
|
|
919
|
+
1. **Load the current signal**:
|
|
920
|
+
- From a file: use `inspect_signal_file` to check the format, then `load_signal_from_file`
|
|
921
|
+
- Supported formats: CSV, TSV, WAV, NumPy NPY
|
|
922
|
+
- Or generate a synthetic signal with `generate_test_current_signal`
|
|
923
|
+
|
|
924
|
+
2. **Calculate motor parameters** with `calculate_motor_params` to get slip, synchronous speed, and rotor frequency.
|
|
925
|
+
|
|
926
|
+
3. **Compute expected fault frequencies** with `compute_fault_frequencies` to know where to look in the spectrum.
|
|
927
|
+
|
|
928
|
+
4. **Preprocess** the signal with `preprocess_signal` (DC removal, windowing, optional filtering).
|
|
929
|
+
|
|
930
|
+
5. **Compute the spectrum** with `compute_spectrum` and/or `compute_power_spectral_density`.
|
|
931
|
+
|
|
932
|
+
6. **Detect faults**:
|
|
933
|
+
- `detect_broken_rotor_bars` — checks (1 ± 2s)·f_s sidebands
|
|
934
|
+
- `detect_eccentricity` — checks f_s ± k·f_r sidebands
|
|
935
|
+
- `detect_stator_faults` — checks f_s ± 2k·f_r sidebands
|
|
936
|
+
- `detect_bearing_faults` — checks f_s ± k·f_defect (needs bearing geometry)
|
|
937
|
+
|
|
938
|
+
7. **Envelope analysis** with `compute_envelope_spectrum` for mechanical/bearing signatures.
|
|
939
|
+
|
|
940
|
+
8. Or use **one-shot shortcuts**:
|
|
941
|
+
- `run_full_diagnosis` — full pipeline from signal array
|
|
942
|
+
- `diagnose_from_file` — full pipeline directly from a file path
|
|
943
|
+
|
|
944
|
+
Report findings with severity levels and actionable recommendations.
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# ---------------------------------------------------------------------------
|
|
949
|
+
# Server entry
|
|
950
|
+
# ---------------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
def serve(transport: Literal["stdio", "sse", "streamable-http"] = "stdio") -> None:
|
|
953
|
+
"""Start the MCSA MCP server."""
|
|
954
|
+
mcp.run(transport=transport)
|