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,145 +1,147 @@
|
|
|
1
|
-
"""Motor parameter calculations for MCSA.
|
|
2
|
-
|
|
3
|
-
Computes synchronous speed, slip, rotor frequency, and expected fault
|
|
4
|
-
frequencies from motor nameplate data and operating conditions.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from dataclasses import asdict, dataclass
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class MotorParameters:
|
|
14
|
-
"""Computed operating parameters for an induction motor.
|
|
15
|
-
|
|
16
|
-
Attributes:
|
|
17
|
-
supply_freq_hz: Supply (line) frequency in Hz.
|
|
18
|
-
poles: Number of magnetic poles.
|
|
19
|
-
sync_speed_rpm: Synchronous speed in RPM.
|
|
20
|
-
rotor_speed_rpm: Measured rotor speed in RPM.
|
|
21
|
-
slip: Per-unit slip (0–1).
|
|
22
|
-
rotor_freq_hz: Mechanical rotational frequency in Hz.
|
|
23
|
-
slip_freq_hz: Slip frequency in Hz (s × f_supply).
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
supply_freq_hz: float
|
|
27
|
-
poles: int
|
|
28
|
-
sync_speed_rpm: float
|
|
29
|
-
rotor_speed_rpm: float
|
|
30
|
-
slip: float
|
|
31
|
-
rotor_freq_hz: float
|
|
32
|
-
slip_freq_hz: float
|
|
33
|
-
|
|
34
|
-
def to_dict(self) -> dict:
|
|
35
|
-
return asdict(self)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def calculate_motor_parameters(
|
|
39
|
-
supply_freq_hz: float,
|
|
40
|
-
poles: int,
|
|
41
|
-
rotor_speed_rpm: float,
|
|
42
|
-
) -> MotorParameters:
|
|
43
|
-
"""Calculate motor operating parameters from nameplate / measured data.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
supply_freq_hz: Supply frequency in Hz (e.g. 50 or 60).
|
|
47
|
-
poles: Number of poles (must be even, ≥ 2).
|
|
48
|
-
rotor_speed_rpm: Measured rotor speed in RPM.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
MotorParameters with all derived quantities.
|
|
52
|
-
|
|
53
|
-
Raises:
|
|
54
|
-
ValueError: If inputs are physically invalid.
|
|
55
|
-
"""
|
|
56
|
-
if supply_freq_hz <= 0:
|
|
57
|
-
raise ValueError(f"Supply frequency must be > 0, got {supply_freq_hz}")
|
|
58
|
-
if poles < 2 or poles % 2 != 0:
|
|
59
|
-
raise ValueError(f"Poles must be even and ≥ 2, got {poles}")
|
|
60
|
-
if rotor_speed_rpm < 0:
|
|
61
|
-
raise ValueError(f"Rotor speed must be ≥ 0, got {rotor_speed_rpm}")
|
|
62
|
-
|
|
63
|
-
sync_speed_rpm = 120.0 * supply_freq_hz / poles
|
|
64
|
-
|
|
65
|
-
if rotor_speed_rpm > sync_speed_rpm:
|
|
66
|
-
raise ValueError(
|
|
67
|
-
f"Rotor speed ({rotor_speed_rpm} RPM) exceeds synchronous speed "
|
|
68
|
-
f"({sync_speed_rpm} RPM) — not valid for a motor (generator mode)."
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
slip = (sync_speed_rpm - rotor_speed_rpm) / sync_speed_rpm
|
|
72
|
-
rotor_freq_hz = rotor_speed_rpm / 60.0
|
|
73
|
-
slip_freq_hz = slip * supply_freq_hz
|
|
74
|
-
|
|
75
|
-
return MotorParameters(
|
|
76
|
-
supply_freq_hz=supply_freq_hz,
|
|
77
|
-
poles=poles,
|
|
78
|
-
sync_speed_rpm=sync_speed_rpm,
|
|
79
|
-
rotor_speed_rpm=rotor_speed_rpm,
|
|
80
|
-
slip=slip,
|
|
81
|
-
rotor_freq_hz=rotor_freq_hz,
|
|
82
|
-
slip_freq_hz=slip_freq_hz,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def calculate_fault_frequencies(
|
|
87
|
-
params: MotorParameters,
|
|
88
|
-
harmonics: int = 3,
|
|
89
|
-
) -> dict:
|
|
90
|
-
"""Calculate expected fault frequencies for common induction‑motor faults.
|
|
91
|
-
|
|
92
|
-
Computes characteristic harmonic frequencies for:
|
|
93
|
-
- Broken Rotor Bars (BRB): sidebands at (1 ± 2ks)·f_s
|
|
94
|
-
- Eccentricity (static/dynamic): f_s ± k·f_r
|
|
95
|
-
- Stator inter‑turn faults: f_s ± 2k·f_r
|
|
96
|
-
- Mixed eccentricity: n·f_r (for n = 1 … harmonics)
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
params: Motor operating parameters.
|
|
100
|
-
harmonics: Number of harmonic orders to compute (default 3).
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Dictionary with fault type keys → lists of expected frequencies [Hz].
|
|
104
|
-
"""
|
|
105
|
-
fs = params.supply_freq_hz
|
|
106
|
-
s = params.slip
|
|
107
|
-
fr = params.rotor_freq_hz
|
|
108
|
-
|
|
109
|
-
# Broken Rotor Bars — sidebands (1 ± 2ks)·fs
|
|
110
|
-
brb_lower = [(1 - 2 * k * s) * fs for k in range(1, harmonics + 1)]
|
|
111
|
-
brb_upper = [(1 + 2 * k * s) * fs for k in range(1, harmonics + 1)]
|
|
112
|
-
|
|
113
|
-
# Eccentricity — fs ± k·fr
|
|
114
|
-
ecc_freqs = []
|
|
115
|
-
for k in range(1, harmonics + 1):
|
|
116
|
-
ecc_freqs.append({"harmonic_order": k, "lower": fs - k * fr, "upper": fs + k * fr})
|
|
117
|
-
|
|
118
|
-
# Stator inter‑turn — fs ± 2k·fr
|
|
119
|
-
stator_freqs = []
|
|
120
|
-
for k in range(1, harmonics + 1):
|
|
121
|
-
stator_freqs.append(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
"""Motor parameter calculations for MCSA.
|
|
2
|
+
|
|
3
|
+
Computes synchronous speed, slip, rotor frequency, and expected fault
|
|
4
|
+
frequencies from motor nameplate data and operating conditions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class MotorParameters:
|
|
14
|
+
"""Computed operating parameters for an induction motor.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
supply_freq_hz: Supply (line) frequency in Hz.
|
|
18
|
+
poles: Number of magnetic poles.
|
|
19
|
+
sync_speed_rpm: Synchronous speed in RPM.
|
|
20
|
+
rotor_speed_rpm: Measured rotor speed in RPM.
|
|
21
|
+
slip: Per-unit slip (0–1).
|
|
22
|
+
rotor_freq_hz: Mechanical rotational frequency in Hz.
|
|
23
|
+
slip_freq_hz: Slip frequency in Hz (s × f_supply).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
supply_freq_hz: float
|
|
27
|
+
poles: int
|
|
28
|
+
sync_speed_rpm: float
|
|
29
|
+
rotor_speed_rpm: float
|
|
30
|
+
slip: float
|
|
31
|
+
rotor_freq_hz: float
|
|
32
|
+
slip_freq_hz: float
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
return asdict(self)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calculate_motor_parameters(
|
|
39
|
+
supply_freq_hz: float,
|
|
40
|
+
poles: int,
|
|
41
|
+
rotor_speed_rpm: float,
|
|
42
|
+
) -> MotorParameters:
|
|
43
|
+
"""Calculate motor operating parameters from nameplate / measured data.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
supply_freq_hz: Supply frequency in Hz (e.g. 50 or 60).
|
|
47
|
+
poles: Number of poles (must be even, ≥ 2).
|
|
48
|
+
rotor_speed_rpm: Measured rotor speed in RPM.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
MotorParameters with all derived quantities.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If inputs are physically invalid.
|
|
55
|
+
"""
|
|
56
|
+
if supply_freq_hz <= 0:
|
|
57
|
+
raise ValueError(f"Supply frequency must be > 0, got {supply_freq_hz}")
|
|
58
|
+
if poles < 2 or poles % 2 != 0:
|
|
59
|
+
raise ValueError(f"Poles must be even and ≥ 2, got {poles}")
|
|
60
|
+
if rotor_speed_rpm < 0:
|
|
61
|
+
raise ValueError(f"Rotor speed must be ≥ 0, got {rotor_speed_rpm}")
|
|
62
|
+
|
|
63
|
+
sync_speed_rpm = 120.0 * supply_freq_hz / poles
|
|
64
|
+
|
|
65
|
+
if rotor_speed_rpm > sync_speed_rpm:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Rotor speed ({rotor_speed_rpm} RPM) exceeds synchronous speed "
|
|
68
|
+
f"({sync_speed_rpm} RPM) — not valid for a motor (generator mode)."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
slip = (sync_speed_rpm - rotor_speed_rpm) / sync_speed_rpm
|
|
72
|
+
rotor_freq_hz = rotor_speed_rpm / 60.0
|
|
73
|
+
slip_freq_hz = slip * supply_freq_hz
|
|
74
|
+
|
|
75
|
+
return MotorParameters(
|
|
76
|
+
supply_freq_hz=supply_freq_hz,
|
|
77
|
+
poles=poles,
|
|
78
|
+
sync_speed_rpm=sync_speed_rpm,
|
|
79
|
+
rotor_speed_rpm=rotor_speed_rpm,
|
|
80
|
+
slip=slip,
|
|
81
|
+
rotor_freq_hz=rotor_freq_hz,
|
|
82
|
+
slip_freq_hz=slip_freq_hz,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def calculate_fault_frequencies(
|
|
87
|
+
params: MotorParameters,
|
|
88
|
+
harmonics: int = 3,
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""Calculate expected fault frequencies for common induction‑motor faults.
|
|
91
|
+
|
|
92
|
+
Computes characteristic harmonic frequencies for:
|
|
93
|
+
- Broken Rotor Bars (BRB): sidebands at (1 ± 2ks)·f_s
|
|
94
|
+
- Eccentricity (static/dynamic): f_s ± k·f_r
|
|
95
|
+
- Stator inter‑turn faults: f_s ± 2k·f_r
|
|
96
|
+
- Mixed eccentricity: n·f_r (for n = 1 … harmonics)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
params: Motor operating parameters.
|
|
100
|
+
harmonics: Number of harmonic orders to compute (default 3).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary with fault type keys → lists of expected frequencies [Hz].
|
|
104
|
+
"""
|
|
105
|
+
fs = params.supply_freq_hz
|
|
106
|
+
s = params.slip
|
|
107
|
+
fr = params.rotor_freq_hz
|
|
108
|
+
|
|
109
|
+
# Broken Rotor Bars — sidebands (1 ± 2ks)·fs
|
|
110
|
+
brb_lower = [(1 - 2 * k * s) * fs for k in range(1, harmonics + 1)]
|
|
111
|
+
brb_upper = [(1 + 2 * k * s) * fs for k in range(1, harmonics + 1)]
|
|
112
|
+
|
|
113
|
+
# Eccentricity — fs ± k·fr
|
|
114
|
+
ecc_freqs = []
|
|
115
|
+
for k in range(1, harmonics + 1):
|
|
116
|
+
ecc_freqs.append({"harmonic_order": k, "lower": fs - k * fr, "upper": fs + k * fr})
|
|
117
|
+
|
|
118
|
+
# Stator inter‑turn — fs ± 2k·fr
|
|
119
|
+
stator_freqs = []
|
|
120
|
+
for k in range(1, harmonics + 1):
|
|
121
|
+
stator_freqs.append(
|
|
122
|
+
{"harmonic_order": k, "lower": fs - 2 * k * fr, "upper": fs + 2 * k * fr}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Mixed eccentricity — n·fr
|
|
126
|
+
mixed_ecc = [k * fr for k in range(1, harmonics + 1)]
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"motor_parameters": params.to_dict(),
|
|
130
|
+
"broken_rotor_bars": {
|
|
131
|
+
"description": "Sidebands at (1 ± 2k·s)·f_supply due to rotor asymmetry",
|
|
132
|
+
"lower_sidebands_hz": brb_lower,
|
|
133
|
+
"upper_sidebands_hz": brb_upper,
|
|
134
|
+
},
|
|
135
|
+
"eccentricity": {
|
|
136
|
+
"description": "Sidebands at f_supply ± k·f_rotor due to air‑gap non‑uniformity",
|
|
137
|
+
"sidebands": ecc_freqs,
|
|
138
|
+
},
|
|
139
|
+
"stator_faults": {
|
|
140
|
+
"description": "Sidebands at f_supply ± 2k·f_rotor due to stator winding asymmetry",
|
|
141
|
+
"sidebands": stator_freqs,
|
|
142
|
+
},
|
|
143
|
+
"mixed_eccentricity": {
|
|
144
|
+
"description": "Components at n·f_rotor (pure rotational harmonics)",
|
|
145
|
+
"frequencies_hz": mixed_ecc,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
"""Signal pre‑processing for MCSA.
|
|
2
|
-
|
|
3
|
-
Offset removal, normalisation, windowing, and digital filtering of
|
|
4
|
-
stator‑current time‑domain signals prior to spectral analysis.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from typing import Literal
|
|
10
|
-
|
|
11
|
-
import numpy as np
|
|
12
|
-
from numpy.typing import NDArray
|
|
13
|
-
from scipy import signal as sig
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def remove_dc_offset(x: NDArray[np.floating]) -> NDArray[np.floating]:
|
|
17
|
-
"""Remove the DC (mean) component from a signal."""
|
|
18
|
-
return x - np.mean(x)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def normalize_signal(
|
|
22
|
-
x: NDArray[np.floating],
|
|
23
|
-
nominal_current: float | None = None,
|
|
24
|
-
) -> NDArray[np.floating]:
|
|
25
|
-
"""Normalise signal amplitude.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
x: Input signal.
|
|
29
|
-
nominal_current: If provided, normalise to this value (p.u.).
|
|
30
|
-
Otherwise normalise by RMS.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
Normalised signal array.
|
|
34
|
-
"""
|
|
35
|
-
if nominal_current is not None and nominal_current > 0:
|
|
36
|
-
return x / nominal_current
|
|
37
|
-
rms = np.sqrt(np.mean(x ** 2))
|
|
38
|
-
if rms == 0:
|
|
39
|
-
return x
|
|
40
|
-
return x / rms
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def apply_window(
|
|
44
|
-
x: NDArray[np.floating],
|
|
45
|
-
window: Literal["hann", "hamming", "blackman", "flattop", "rectangular"] = "hann",
|
|
46
|
-
) -> NDArray[np.floating]:
|
|
47
|
-
"""Apply a window function to the signal.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
x: Input signal array.
|
|
51
|
-
window: Window type name.
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
Windowed signal.
|
|
55
|
-
"""
|
|
56
|
-
n = len(x)
|
|
57
|
-
if window == "rectangular":
|
|
58
|
-
return x.copy()
|
|
59
|
-
w = sig.get_window(window, n)
|
|
60
|
-
return x * w
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def bandpass_filter(
|
|
64
|
-
x: NDArray[np.floating],
|
|
65
|
-
fs: float,
|
|
66
|
-
low_hz: float,
|
|
67
|
-
high_hz: float,
|
|
68
|
-
order: int = 5,
|
|
69
|
-
) -> NDArray[np.floating]:
|
|
70
|
-
"""Apply a Butterworth bandpass filter.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
x: Input signal.
|
|
74
|
-
fs: Sampling frequency in Hz.
|
|
75
|
-
low_hz: Lower cutoff frequency.
|
|
76
|
-
high_hz: Upper cutoff frequency.
|
|
77
|
-
order: Filter order.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
Bandpass‑filtered signal.
|
|
81
|
-
"""
|
|
82
|
-
nyq = fs / 2.0
|
|
83
|
-
if low_hz <= 0 or high_hz >= nyq or low_hz >= high_hz:
|
|
84
|
-
raise ValueError(
|
|
85
|
-
f"Invalid cutoff frequencies: low={low_hz}, high={high_hz}, Nyquist={nyq}"
|
|
86
|
-
)
|
|
87
|
-
sos = sig.butter(order, [low_hz / nyq, high_hz / nyq], btype="bandpass", output="sos")
|
|
88
|
-
return sig.sosfiltfilt(sos, x)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def notch_filter(
|
|
92
|
-
x: NDArray[np.floating],
|
|
93
|
-
fs: float,
|
|
94
|
-
notch_freq_hz: float,
|
|
95
|
-
quality_factor: float = 30.0,
|
|
96
|
-
) -> NDArray[np.floating]:
|
|
97
|
-
"""Apply a notch (band‑reject) filter at a specific frequency.
|
|
98
|
-
|
|
99
|
-
Useful for removing strong supply harmonics or converter switching noise.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
x: Input signal.
|
|
103
|
-
fs: Sampling frequency in Hz.
|
|
104
|
-
notch_freq_hz: Centre frequency to reject.
|
|
105
|
-
quality_factor: Q factor (higher = narrower notch).
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Filtered signal.
|
|
109
|
-
"""
|
|
110
|
-
b, a = sig.iirnotch(notch_freq_hz, quality_factor, fs)
|
|
111
|
-
return sig.filtfilt(b, a, x)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def lowpass_filter(
|
|
115
|
-
x: NDArray[np.floating],
|
|
116
|
-
fs: float,
|
|
117
|
-
cutoff_hz: float,
|
|
118
|
-
order: int = 5,
|
|
119
|
-
) -> NDArray[np.floating]:
|
|
120
|
-
"""Apply a Butterworth lowpass filter (anti‑aliasing or smoothing).
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
x: Input signal.
|
|
124
|
-
fs: Sampling frequency in Hz.
|
|
125
|
-
cutoff_hz: Cutoff frequency in Hz.
|
|
126
|
-
order: Filter order.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Lowpass‑filtered signal.
|
|
130
|
-
"""
|
|
131
|
-
nyq = fs / 2.0
|
|
132
|
-
if cutoff_hz <= 0 or cutoff_hz >= nyq:
|
|
133
|
-
raise ValueError(f"Cutoff must be in (0, {nyq}), got {cutoff_hz}")
|
|
134
|
-
sos = sig.butter(order, cutoff_hz / nyq, btype="low", output="sos")
|
|
135
|
-
return sig.sosfiltfilt(sos, x)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def preprocess_pipeline(
|
|
139
|
-
x: NDArray[np.floating],
|
|
140
|
-
fs: float,
|
|
141
|
-
nominal_current: float | None = None,
|
|
142
|
-
window: Literal["hann", "hamming", "blackman", "flattop", "rectangular"] = "hann",
|
|
143
|
-
bandpass: tuple[float, float] | None = None,
|
|
144
|
-
notch_freqs: list[float] | None = None,
|
|
145
|
-
notch_q: float = 30.0,
|
|
146
|
-
) -> NDArray[np.floating]:
|
|
147
|
-
"""Full pre‑processing pipeline for a stator‑current signal.
|
|
148
|
-
|
|
149
|
-
Steps (in order):
|
|
150
|
-
1. DC offset removal
|
|
151
|
-
2. Optional notch filtering (e.g. supply harmonics)
|
|
152
|
-
3. Optional bandpass filtering
|
|
153
|
-
4. Normalisation
|
|
154
|
-
5. Windowing
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
x: Raw current signal.
|
|
158
|
-
fs: Sampling frequency in Hz.
|
|
159
|
-
nominal_current: Nominal current for normalisation (A). None → RMS.
|
|
160
|
-
window: Window function name.
|
|
161
|
-
bandpass: Optional (low, high) Hz tuple for bandpass filtering.
|
|
162
|
-
notch_freqs: Optional list of frequencies to notch out.
|
|
163
|
-
notch_q: Q factor for notch filters.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
Pre‑processed signal ready for spectral analysis.
|
|
167
|
-
"""
|
|
168
|
-
y = remove_dc_offset(x)
|
|
169
|
-
|
|
170
|
-
if notch_freqs:
|
|
171
|
-
for nf in notch_freqs:
|
|
172
|
-
y = notch_filter(y, fs, nf, quality_factor=notch_q)
|
|
173
|
-
|
|
174
|
-
if bandpass is not None:
|
|
175
|
-
y = bandpass_filter(y, fs, bandpass[0], bandpass[1])
|
|
176
|
-
|
|
177
|
-
y = normalize_signal(y, nominal_current)
|
|
178
|
-
y = apply_window(y, window)
|
|
179
|
-
|
|
180
|
-
return y
|
|
1
|
+
"""Signal pre‑processing for MCSA.
|
|
2
|
+
|
|
3
|
+
Offset removal, normalisation, windowing, and digital filtering of
|
|
4
|
+
stator‑current time‑domain signals prior to spectral analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from numpy.typing import NDArray
|
|
13
|
+
from scipy import signal as sig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def remove_dc_offset(x: NDArray[np.floating]) -> NDArray[np.floating]:
|
|
17
|
+
"""Remove the DC (mean) component from a signal."""
|
|
18
|
+
return x - np.mean(x)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_signal(
|
|
22
|
+
x: NDArray[np.floating],
|
|
23
|
+
nominal_current: float | None = None,
|
|
24
|
+
) -> NDArray[np.floating]:
|
|
25
|
+
"""Normalise signal amplitude.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
x: Input signal.
|
|
29
|
+
nominal_current: If provided, normalise to this value (p.u.).
|
|
30
|
+
Otherwise normalise by RMS.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Normalised signal array.
|
|
34
|
+
"""
|
|
35
|
+
if nominal_current is not None and nominal_current > 0:
|
|
36
|
+
return x / nominal_current
|
|
37
|
+
rms = np.sqrt(np.mean(x ** 2))
|
|
38
|
+
if rms == 0:
|
|
39
|
+
return x
|
|
40
|
+
return x / rms
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def apply_window(
|
|
44
|
+
x: NDArray[np.floating],
|
|
45
|
+
window: Literal["hann", "hamming", "blackman", "flattop", "rectangular"] = "hann",
|
|
46
|
+
) -> NDArray[np.floating]:
|
|
47
|
+
"""Apply a window function to the signal.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
x: Input signal array.
|
|
51
|
+
window: Window type name.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Windowed signal.
|
|
55
|
+
"""
|
|
56
|
+
n = len(x)
|
|
57
|
+
if window == "rectangular":
|
|
58
|
+
return x.copy()
|
|
59
|
+
w = sig.get_window(window, n)
|
|
60
|
+
return x * w # type: ignore[return-value]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def bandpass_filter(
|
|
64
|
+
x: NDArray[np.floating],
|
|
65
|
+
fs: float,
|
|
66
|
+
low_hz: float,
|
|
67
|
+
high_hz: float,
|
|
68
|
+
order: int = 5,
|
|
69
|
+
) -> NDArray[np.floating]:
|
|
70
|
+
"""Apply a Butterworth bandpass filter.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
x: Input signal.
|
|
74
|
+
fs: Sampling frequency in Hz.
|
|
75
|
+
low_hz: Lower cutoff frequency.
|
|
76
|
+
high_hz: Upper cutoff frequency.
|
|
77
|
+
order: Filter order.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Bandpass‑filtered signal.
|
|
81
|
+
"""
|
|
82
|
+
nyq = fs / 2.0
|
|
83
|
+
if low_hz <= 0 or high_hz >= nyq or low_hz >= high_hz:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Invalid cutoff frequencies: low={low_hz}, high={high_hz}, Nyquist={nyq}"
|
|
86
|
+
)
|
|
87
|
+
sos = sig.butter(order, [low_hz / nyq, high_hz / nyq], btype="bandpass", output="sos")
|
|
88
|
+
return sig.sosfiltfilt(sos, x)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def notch_filter(
|
|
92
|
+
x: NDArray[np.floating],
|
|
93
|
+
fs: float,
|
|
94
|
+
notch_freq_hz: float,
|
|
95
|
+
quality_factor: float = 30.0,
|
|
96
|
+
) -> NDArray[np.floating]:
|
|
97
|
+
"""Apply a notch (band‑reject) filter at a specific frequency.
|
|
98
|
+
|
|
99
|
+
Useful for removing strong supply harmonics or converter switching noise.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
x: Input signal.
|
|
103
|
+
fs: Sampling frequency in Hz.
|
|
104
|
+
notch_freq_hz: Centre frequency to reject.
|
|
105
|
+
quality_factor: Q factor (higher = narrower notch).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Filtered signal.
|
|
109
|
+
"""
|
|
110
|
+
b, a = sig.iirnotch(notch_freq_hz, quality_factor, fs)
|
|
111
|
+
return sig.filtfilt(b, a, x)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def lowpass_filter(
|
|
115
|
+
x: NDArray[np.floating],
|
|
116
|
+
fs: float,
|
|
117
|
+
cutoff_hz: float,
|
|
118
|
+
order: int = 5,
|
|
119
|
+
) -> NDArray[np.floating]:
|
|
120
|
+
"""Apply a Butterworth lowpass filter (anti‑aliasing or smoothing).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
x: Input signal.
|
|
124
|
+
fs: Sampling frequency in Hz.
|
|
125
|
+
cutoff_hz: Cutoff frequency in Hz.
|
|
126
|
+
order: Filter order.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Lowpass‑filtered signal.
|
|
130
|
+
"""
|
|
131
|
+
nyq = fs / 2.0
|
|
132
|
+
if cutoff_hz <= 0 or cutoff_hz >= nyq:
|
|
133
|
+
raise ValueError(f"Cutoff must be in (0, {nyq}), got {cutoff_hz}")
|
|
134
|
+
sos = sig.butter(order, cutoff_hz / nyq, btype="low", output="sos")
|
|
135
|
+
return sig.sosfiltfilt(sos, x)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def preprocess_pipeline(
|
|
139
|
+
x: NDArray[np.floating],
|
|
140
|
+
fs: float,
|
|
141
|
+
nominal_current: float | None = None,
|
|
142
|
+
window: Literal["hann", "hamming", "blackman", "flattop", "rectangular"] = "hann",
|
|
143
|
+
bandpass: tuple[float, float] | None = None,
|
|
144
|
+
notch_freqs: list[float] | None = None,
|
|
145
|
+
notch_q: float = 30.0,
|
|
146
|
+
) -> NDArray[np.floating]:
|
|
147
|
+
"""Full pre‑processing pipeline for a stator‑current signal.
|
|
148
|
+
|
|
149
|
+
Steps (in order):
|
|
150
|
+
1. DC offset removal
|
|
151
|
+
2. Optional notch filtering (e.g. supply harmonics)
|
|
152
|
+
3. Optional bandpass filtering
|
|
153
|
+
4. Normalisation
|
|
154
|
+
5. Windowing
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
x: Raw current signal.
|
|
158
|
+
fs: Sampling frequency in Hz.
|
|
159
|
+
nominal_current: Nominal current for normalisation (A). None → RMS.
|
|
160
|
+
window: Window function name.
|
|
161
|
+
bandpass: Optional (low, high) Hz tuple for bandpass filtering.
|
|
162
|
+
notch_freqs: Optional list of frequencies to notch out.
|
|
163
|
+
notch_q: Q factor for notch filters.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Pre‑processed signal ready for spectral analysis.
|
|
167
|
+
"""
|
|
168
|
+
y = remove_dc_offset(x)
|
|
169
|
+
|
|
170
|
+
if notch_freqs:
|
|
171
|
+
for nf in notch_freqs:
|
|
172
|
+
y = notch_filter(y, fs, nf, quality_factor=notch_q)
|
|
173
|
+
|
|
174
|
+
if bandpass is not None:
|
|
175
|
+
y = bandpass_filter(y, fs, bandpass[0], bandpass[1])
|
|
176
|
+
|
|
177
|
+
y = normalize_signal(y, nominal_current)
|
|
178
|
+
y = apply_window(y, window)
|
|
179
|
+
|
|
180
|
+
return y
|