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/__init__.py
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
"""MCP Server for Motor Current Signature Analysis (MCSA).
|
|
2
|
-
|
|
3
|
-
Run as a CLI:
|
|
4
|
-
mcp-server-mcsa
|
|
5
|
-
|
|
6
|
-
Or via Python:
|
|
7
|
-
python -m mcp_server_mcsa
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
__version__ = "0.1.0"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def main() -> None:
|
|
16
|
-
"""CLI entry point for the MCSA MCP server."""
|
|
17
|
-
import argparse
|
|
18
|
-
|
|
19
|
-
parser = argparse.ArgumentParser(
|
|
20
|
-
prog="mcp-server-mcsa",
|
|
21
|
-
description="MCP server for Motor Current Signature Analysis (MCSA)",
|
|
22
|
-
)
|
|
23
|
-
parser.add_argument(
|
|
24
|
-
"--transport",
|
|
25
|
-
choices=["stdio"],
|
|
26
|
-
default="stdio",
|
|
27
|
-
help="MCP transport (default: stdio)",
|
|
28
|
-
)
|
|
29
|
-
args = parser.parse_args()
|
|
30
|
-
|
|
31
|
-
from mcp_server_mcsa.server import serve
|
|
32
|
-
|
|
33
|
-
serve(transport=args.transport)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if __name__ == "__main__":
|
|
37
|
-
main()
|
|
1
|
+
"""MCP Server for Motor Current Signature Analysis (MCSA).
|
|
2
|
+
|
|
3
|
+
Run as a CLI:
|
|
4
|
+
mcp-server-mcsa
|
|
5
|
+
|
|
6
|
+
Or via Python:
|
|
7
|
+
python -m mcp_server_mcsa
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
"""CLI entry point for the MCSA MCP server."""
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="mcp-server-mcsa",
|
|
21
|
+
description="MCP server for Motor Current Signature Analysis (MCSA)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--transport",
|
|
25
|
+
choices=["stdio"],
|
|
26
|
+
default="stdio",
|
|
27
|
+
help="MCP transport (default: stdio)",
|
|
28
|
+
)
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
from mcp_server_mcsa.server import serve
|
|
32
|
+
|
|
33
|
+
serve(transport=args.transport)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
mcp_server_mcsa/__main__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Allow running as ``python -m mcp_server_mcsa``."""
|
|
2
|
-
|
|
3
|
-
from mcp_server_mcsa import main
|
|
4
|
-
|
|
5
|
-
main()
|
|
1
|
+
"""Allow running as ``python -m mcp_server_mcsa``."""
|
|
2
|
+
|
|
3
|
+
from mcp_server_mcsa import main
|
|
4
|
+
|
|
5
|
+
main()
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"""MCSA analysis library — core signal processing and fault detection modules."""
|
|
2
|
-
|
|
3
|
-
from mcp_server_mcsa.analysis.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
__all__ = [
|
|
14
|
-
"MotorParameters",
|
|
15
|
-
"calculate_motor_parameters",
|
|
16
|
-
"BearingGeometry",
|
|
17
|
-
"BearingDefectFrequencies",
|
|
18
|
-
"calculate_bearing_defect_frequencies",
|
|
19
|
-
]
|
|
1
|
+
"""MCSA analysis library — core signal processing and fault detection modules."""
|
|
2
|
+
|
|
3
|
+
from mcp_server_mcsa.analysis.bearing import (
|
|
4
|
+
BearingDefectFrequencies,
|
|
5
|
+
BearingGeometry,
|
|
6
|
+
calculate_bearing_defect_frequencies,
|
|
7
|
+
)
|
|
8
|
+
from mcp_server_mcsa.analysis.motor import (
|
|
9
|
+
MotorParameters,
|
|
10
|
+
calculate_motor_parameters,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MotorParameters",
|
|
15
|
+
"calculate_motor_parameters",
|
|
16
|
+
"BearingGeometry",
|
|
17
|
+
"BearingDefectFrequencies",
|
|
18
|
+
"calculate_bearing_defect_frequencies",
|
|
19
|
+
]
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
"""Bearing defect frequency calculations.
|
|
2
|
-
|
|
3
|
-
Computes characteristic defect frequencies (BPFO, BPFI, BSF, FTF) from
|
|
4
|
-
bearing geometry and shaft speed, plus their expected sidebands in the
|
|
5
|
-
stator‑current spectrum.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import math
|
|
11
|
-
from dataclasses import asdict, dataclass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class BearingGeometry:
|
|
16
|
-
"""Physical geometry of a rolling‑element bearing.
|
|
17
|
-
|
|
18
|
-
Attributes:
|
|
19
|
-
n_balls: Number of rolling elements.
|
|
20
|
-
ball_dia_mm: Ball (roller) diameter in mm.
|
|
21
|
-
pitch_dia_mm: Pitch (cage) diameter in mm.
|
|
22
|
-
contact_angle_deg: Contact angle in degrees.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
n_balls: int
|
|
26
|
-
ball_dia_mm: float
|
|
27
|
-
pitch_dia_mm: float
|
|
28
|
-
contact_angle_deg: float = 0.0
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass(frozen=True)
|
|
32
|
-
class BearingDefectFrequencies:
|
|
33
|
-
"""Characteristic defect frequencies normalised to shaft speed.
|
|
34
|
-
|
|
35
|
-
All values are multiples of the shaft rotational frequency f_r.
|
|
36
|
-
Multiply by f_r (Hz) to get absolute frequencies.
|
|
37
|
-
|
|
38
|
-
Attributes:
|
|
39
|
-
bpfo: Ball Pass Frequency — Outer race.
|
|
40
|
-
bpfi: Ball Pass Frequency — Inner race.
|
|
41
|
-
bsf: Ball Spin Frequency.
|
|
42
|
-
ftf: Fundamental Train (cage) Frequency.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
bpfo: float
|
|
46
|
-
bpfi: float
|
|
47
|
-
bsf: float
|
|
48
|
-
ftf: float
|
|
49
|
-
|
|
50
|
-
def to_dict(self) -> dict:
|
|
51
|
-
return asdict(self)
|
|
52
|
-
|
|
53
|
-
def absolute(self, shaft_freq_hz: float) -> dict:
|
|
54
|
-
"""Return absolute frequencies in Hz given shaft speed."""
|
|
55
|
-
return {
|
|
56
|
-
"bpfo_hz": self.bpfo * shaft_freq_hz,
|
|
57
|
-
"bpfi_hz": self.bpfi * shaft_freq_hz,
|
|
58
|
-
"bsf_hz": self.bsf * shaft_freq_hz,
|
|
59
|
-
"ftf_hz": self.ftf * shaft_freq_hz,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def calculate_bearing_defect_frequencies(
|
|
64
|
-
geometry: BearingGeometry,
|
|
65
|
-
) -> BearingDefectFrequencies:
|
|
66
|
-
"""Compute bearing defect frequencies (normalised to shaft speed).
|
|
67
|
-
|
|
68
|
-
Standard kinematic equations for rolling‑element bearings.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
geometry: Bearing physical dimensions.
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Normalised defect frequencies (multiply by f_rotor to get Hz).
|
|
75
|
-
|
|
76
|
-
Raises:
|
|
77
|
-
ValueError: If geometry parameters are physically invalid.
|
|
78
|
-
"""
|
|
79
|
-
n = geometry.n_balls
|
|
80
|
-
d = geometry.ball_dia_mm
|
|
81
|
-
D = geometry.pitch_dia_mm
|
|
82
|
-
alpha = math.radians(geometry.contact_angle_deg)
|
|
83
|
-
|
|
84
|
-
if n < 1:
|
|
85
|
-
raise ValueError(f"Number of balls must be ≥ 1, got {n}")
|
|
86
|
-
if d <= 0 or D <= 0:
|
|
87
|
-
raise ValueError("Ball and pitch diameters must be > 0")
|
|
88
|
-
if d >= D:
|
|
89
|
-
raise ValueError("Ball diameter must be < pitch diameter")
|
|
90
|
-
|
|
91
|
-
cos_alpha = math.cos(alpha)
|
|
92
|
-
ratio = d / D
|
|
93
|
-
|
|
94
|
-
# Fundamental Train Frequency (cage speed / shaft speed)
|
|
95
|
-
ftf = 0.5 * (1.0 - ratio * cos_alpha)
|
|
96
|
-
|
|
97
|
-
# Ball Pass Frequency — Outer race
|
|
98
|
-
bpfo = n * ftf
|
|
99
|
-
|
|
100
|
-
# Ball Pass Frequency — Inner race
|
|
101
|
-
bpfi = 0.5 * n * (1.0 + ratio * cos_alpha)
|
|
102
|
-
|
|
103
|
-
# Ball Spin Frequency
|
|
104
|
-
bsf = (D / (2.0 * d)) * (1.0 - (ratio * cos_alpha) ** 2)
|
|
105
|
-
|
|
106
|
-
return BearingDefectFrequencies(bpfo=bpfo, bpfi=bpfi, bsf=bsf, ftf=ftf)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def bearing_current_sidebands(
|
|
110
|
-
defect_freqs: BearingDefectFrequencies,
|
|
111
|
-
shaft_freq_hz: float,
|
|
112
|
-
supply_freq_hz: float,
|
|
113
|
-
harmonics: int = 2,
|
|
114
|
-
) -> dict:
|
|
115
|
-
"""Compute expected stator‑current sidebands from bearing defects.
|
|
116
|
-
|
|
117
|
-
Bearing defects modulate motor torque, producing sidebands in the
|
|
118
|
-
stator current at f_supply ± k · f_defect.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
defect_freqs: Normalised bearing defect frequencies.
|
|
122
|
-
shaft_freq_hz: Shaft rotational frequency in Hz.
|
|
123
|
-
supply_freq_hz: Supply (line) frequency in Hz.
|
|
124
|
-
harmonics: Number of sideband orders.
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
Dictionary mapping defect type → list of sideband frequencies.
|
|
128
|
-
"""
|
|
129
|
-
abs_freqs = defect_freqs.absolute(shaft_freq_hz)
|
|
130
|
-
fs = supply_freq_hz
|
|
131
|
-
|
|
132
|
-
result = {}
|
|
133
|
-
for name, fdef in abs_freqs.items():
|
|
134
|
-
label = name.replace("_hz", "")
|
|
135
|
-
sidebands = []
|
|
136
|
-
for k in range(1, harmonics + 1):
|
|
137
|
-
sidebands.append({
|
|
138
|
-
"order": k,
|
|
139
|
-
"lower_hz": fs - k * fdef,
|
|
140
|
-
"upper_hz": fs + k * fdef,
|
|
141
|
-
})
|
|
142
|
-
result[label] = {
|
|
143
|
-
"defect_frequency_hz": fdef,
|
|
144
|
-
"current_sidebands": sidebands,
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return result
|
|
1
|
+
"""Bearing defect frequency calculations.
|
|
2
|
+
|
|
3
|
+
Computes characteristic defect frequencies (BPFO, BPFI, BSF, FTF) from
|
|
4
|
+
bearing geometry and shaft speed, plus their expected sidebands in the
|
|
5
|
+
stator‑current spectrum.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class BearingGeometry:
|
|
16
|
+
"""Physical geometry of a rolling‑element bearing.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
n_balls: Number of rolling elements.
|
|
20
|
+
ball_dia_mm: Ball (roller) diameter in mm.
|
|
21
|
+
pitch_dia_mm: Pitch (cage) diameter in mm.
|
|
22
|
+
contact_angle_deg: Contact angle in degrees.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
n_balls: int
|
|
26
|
+
ball_dia_mm: float
|
|
27
|
+
pitch_dia_mm: float
|
|
28
|
+
contact_angle_deg: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class BearingDefectFrequencies:
|
|
33
|
+
"""Characteristic defect frequencies normalised to shaft speed.
|
|
34
|
+
|
|
35
|
+
All values are multiples of the shaft rotational frequency f_r.
|
|
36
|
+
Multiply by f_r (Hz) to get absolute frequencies.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
bpfo: Ball Pass Frequency — Outer race.
|
|
40
|
+
bpfi: Ball Pass Frequency — Inner race.
|
|
41
|
+
bsf: Ball Spin Frequency.
|
|
42
|
+
ftf: Fundamental Train (cage) Frequency.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
bpfo: float
|
|
46
|
+
bpfi: float
|
|
47
|
+
bsf: float
|
|
48
|
+
ftf: float
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict:
|
|
51
|
+
return asdict(self)
|
|
52
|
+
|
|
53
|
+
def absolute(self, shaft_freq_hz: float) -> dict:
|
|
54
|
+
"""Return absolute frequencies in Hz given shaft speed."""
|
|
55
|
+
return {
|
|
56
|
+
"bpfo_hz": self.bpfo * shaft_freq_hz,
|
|
57
|
+
"bpfi_hz": self.bpfi * shaft_freq_hz,
|
|
58
|
+
"bsf_hz": self.bsf * shaft_freq_hz,
|
|
59
|
+
"ftf_hz": self.ftf * shaft_freq_hz,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def calculate_bearing_defect_frequencies(
|
|
64
|
+
geometry: BearingGeometry,
|
|
65
|
+
) -> BearingDefectFrequencies:
|
|
66
|
+
"""Compute bearing defect frequencies (normalised to shaft speed).
|
|
67
|
+
|
|
68
|
+
Standard kinematic equations for rolling‑element bearings.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
geometry: Bearing physical dimensions.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Normalised defect frequencies (multiply by f_rotor to get Hz).
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If geometry parameters are physically invalid.
|
|
78
|
+
"""
|
|
79
|
+
n = geometry.n_balls
|
|
80
|
+
d = geometry.ball_dia_mm
|
|
81
|
+
D = geometry.pitch_dia_mm
|
|
82
|
+
alpha = math.radians(geometry.contact_angle_deg)
|
|
83
|
+
|
|
84
|
+
if n < 1:
|
|
85
|
+
raise ValueError(f"Number of balls must be ≥ 1, got {n}")
|
|
86
|
+
if d <= 0 or D <= 0:
|
|
87
|
+
raise ValueError("Ball and pitch diameters must be > 0")
|
|
88
|
+
if d >= D:
|
|
89
|
+
raise ValueError("Ball diameter must be < pitch diameter")
|
|
90
|
+
|
|
91
|
+
cos_alpha = math.cos(alpha)
|
|
92
|
+
ratio = d / D
|
|
93
|
+
|
|
94
|
+
# Fundamental Train Frequency (cage speed / shaft speed)
|
|
95
|
+
ftf = 0.5 * (1.0 - ratio * cos_alpha)
|
|
96
|
+
|
|
97
|
+
# Ball Pass Frequency — Outer race
|
|
98
|
+
bpfo = n * ftf
|
|
99
|
+
|
|
100
|
+
# Ball Pass Frequency — Inner race
|
|
101
|
+
bpfi = 0.5 * n * (1.0 + ratio * cos_alpha)
|
|
102
|
+
|
|
103
|
+
# Ball Spin Frequency
|
|
104
|
+
bsf = (D / (2.0 * d)) * (1.0 - (ratio * cos_alpha) ** 2)
|
|
105
|
+
|
|
106
|
+
return BearingDefectFrequencies(bpfo=bpfo, bpfi=bpfi, bsf=bsf, ftf=ftf)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def bearing_current_sidebands(
|
|
110
|
+
defect_freqs: BearingDefectFrequencies,
|
|
111
|
+
shaft_freq_hz: float,
|
|
112
|
+
supply_freq_hz: float,
|
|
113
|
+
harmonics: int = 2,
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""Compute expected stator‑current sidebands from bearing defects.
|
|
116
|
+
|
|
117
|
+
Bearing defects modulate motor torque, producing sidebands in the
|
|
118
|
+
stator current at f_supply ± k · f_defect.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
defect_freqs: Normalised bearing defect frequencies.
|
|
122
|
+
shaft_freq_hz: Shaft rotational frequency in Hz.
|
|
123
|
+
supply_freq_hz: Supply (line) frequency in Hz.
|
|
124
|
+
harmonics: Number of sideband orders.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dictionary mapping defect type → list of sideband frequencies.
|
|
128
|
+
"""
|
|
129
|
+
abs_freqs = defect_freqs.absolute(shaft_freq_hz)
|
|
130
|
+
fs = supply_freq_hz
|
|
131
|
+
|
|
132
|
+
result = {}
|
|
133
|
+
for name, fdef in abs_freqs.items():
|
|
134
|
+
label = name.replace("_hz", "")
|
|
135
|
+
sidebands = []
|
|
136
|
+
for k in range(1, harmonics + 1):
|
|
137
|
+
sidebands.append({
|
|
138
|
+
"order": k,
|
|
139
|
+
"lower_hz": fs - k * fdef,
|
|
140
|
+
"upper_hz": fs + k * fdef,
|
|
141
|
+
})
|
|
142
|
+
result[label] = {
|
|
143
|
+
"defect_frequency_hz": fdef,
|
|
144
|
+
"current_sidebands": sidebands,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
@@ -1,97 +1,96 @@
|
|
|
1
|
-
"""Envelope (demodulation) analysis for MCSA.
|
|
2
|
-
|
|
3
|
-
Hilbert‑transform‑based amplitude demodulation to extract low‑frequency
|
|
4
|
-
modulation patterns caused by mechanical faults (bearing defects,
|
|
5
|
-
eccentricity, load oscillations).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import numpy as np
|
|
11
|
-
from numpy.typing import NDArray
|
|
12
|
-
from scipy import signal as sig
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def hilbert_envelope(
|
|
16
|
-
x: NDArray[np.floating],
|
|
17
|
-
) -> NDArray[np.floating]:
|
|
18
|
-
"""Compute the amplitude envelope of a signal via the Hilbert transform.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
x: Input signal (real‑valued).
|
|
22
|
-
|
|
23
|
-
Returns:
|
|
24
|
-
Instantaneous amplitude (envelope) array.
|
|
25
|
-
"""
|
|
26
|
-
analytic = sig.hilbert(x)
|
|
27
|
-
return np.abs(analytic)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def instantaneous_frequency(
|
|
31
|
-
x: NDArray[np.floating],
|
|
32
|
-
fs: float,
|
|
33
|
-
) -> NDArray[np.floating]:
|
|
34
|
-
"""Compute instantaneous frequency via the Hilbert transform.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
x: Input signal.
|
|
38
|
-
fs: Sampling frequency in Hz.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Instantaneous frequency array in Hz (length = len(x) - 1).
|
|
42
|
-
"""
|
|
43
|
-
analytic = sig.hilbert(x)
|
|
44
|
-
phase = np.unwrap(np.angle(analytic))
|
|
45
|
-
inst_freq = np.diff(phase) / (2.0 * np.pi) * fs
|
|
46
|
-
return inst_freq
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def envelope_spectrum(
|
|
50
|
-
x: NDArray[np.floating],
|
|
51
|
-
fs: float,
|
|
52
|
-
bandpass: tuple[float, float] | None = None,
|
|
53
|
-
filter_order: int = 5,
|
|
54
|
-
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
55
|
-
"""Compute the spectrum of the signal envelope.
|
|
56
|
-
|
|
57
|
-
Typical workflow for bearing / mechanical fault detection:
|
|
58
|
-
1. Optional bandpass to isolate a resonance band
|
|
59
|
-
2. Hilbert envelope
|
|
60
|
-
3. Remove DC of envelope
|
|
61
|
-
4. FFT of envelope → low‑frequency modulation spectrum
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
x: Input current signal.
|
|
65
|
-
fs: Sampling frequency in Hz.
|
|
66
|
-
bandpass: Optional (low, high) bandpass range in Hz before
|
|
67
|
-
computing the envelope (e.g. to isolate a resonance).
|
|
68
|
-
filter_order: Butterworth filter order for bandpass.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
(frequencies, amplitudes) of the envelope spectrum.
|
|
72
|
-
"""
|
|
73
|
-
y = x.copy()
|
|
74
|
-
|
|
75
|
-
# Optional bandpass
|
|
76
|
-
if bandpass is not None:
|
|
77
|
-
nyq = fs / 2.0
|
|
78
|
-
low, high = bandpass
|
|
79
|
-
if 0 < low < high < nyq:
|
|
80
|
-
sos = sig.butter(filter_order, [low / nyq, high / nyq], btype="bandpass", output="sos")
|
|
81
|
-
y = sig.sosfiltfilt(sos, y)
|
|
82
|
-
|
|
83
|
-
# Envelope
|
|
84
|
-
env = hilbert_envelope(y)
|
|
85
|
-
|
|
86
|
-
# Remove DC from envelope
|
|
87
|
-
env = env - np.mean(env)
|
|
88
|
-
|
|
89
|
-
# FFT of envelope
|
|
90
|
-
n = len(env)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
amps
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return freqs, amps
|
|
1
|
+
"""Envelope (demodulation) analysis for MCSA.
|
|
2
|
+
|
|
3
|
+
Hilbert‑transform‑based amplitude demodulation to extract low‑frequency
|
|
4
|
+
modulation patterns caused by mechanical faults (bearing defects,
|
|
5
|
+
eccentricity, load oscillations).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from scipy import signal as sig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def hilbert_envelope(
|
|
16
|
+
x: NDArray[np.floating],
|
|
17
|
+
) -> NDArray[np.floating]:
|
|
18
|
+
"""Compute the amplitude envelope of a signal via the Hilbert transform.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
x: Input signal (real‑valued).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Instantaneous amplitude (envelope) array.
|
|
25
|
+
"""
|
|
26
|
+
analytic: NDArray[np.complexfloating] = sig.hilbert(x) # type: ignore[assignment]
|
|
27
|
+
return np.abs(analytic)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def instantaneous_frequency(
|
|
31
|
+
x: NDArray[np.floating],
|
|
32
|
+
fs: float,
|
|
33
|
+
) -> NDArray[np.floating]:
|
|
34
|
+
"""Compute instantaneous frequency via the Hilbert transform.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
x: Input signal.
|
|
38
|
+
fs: Sampling frequency in Hz.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Instantaneous frequency array in Hz (length = len(x) - 1).
|
|
42
|
+
"""
|
|
43
|
+
analytic: NDArray[np.complexfloating] = sig.hilbert(x) # type: ignore[assignment]
|
|
44
|
+
phase = np.unwrap(np.angle(analytic))
|
|
45
|
+
inst_freq = np.diff(phase) / (2.0 * np.pi) * fs
|
|
46
|
+
return inst_freq
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def envelope_spectrum(
|
|
50
|
+
x: NDArray[np.floating],
|
|
51
|
+
fs: float,
|
|
52
|
+
bandpass: tuple[float, float] | None = None,
|
|
53
|
+
filter_order: int = 5,
|
|
54
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
55
|
+
"""Compute the spectrum of the signal envelope.
|
|
56
|
+
|
|
57
|
+
Typical workflow for bearing / mechanical fault detection:
|
|
58
|
+
1. Optional bandpass to isolate a resonance band
|
|
59
|
+
2. Hilbert envelope
|
|
60
|
+
3. Remove DC of envelope
|
|
61
|
+
4. FFT of envelope → low‑frequency modulation spectrum
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
x: Input current signal.
|
|
65
|
+
fs: Sampling frequency in Hz.
|
|
66
|
+
bandpass: Optional (low, high) bandpass range in Hz before
|
|
67
|
+
computing the envelope (e.g. to isolate a resonance).
|
|
68
|
+
filter_order: Butterworth filter order for bandpass.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
(frequencies, amplitudes) of the envelope spectrum.
|
|
72
|
+
"""
|
|
73
|
+
y = x.copy()
|
|
74
|
+
|
|
75
|
+
# Optional bandpass
|
|
76
|
+
if bandpass is not None:
|
|
77
|
+
nyq = fs / 2.0
|
|
78
|
+
low, high = bandpass
|
|
79
|
+
if 0 < low < high < nyq:
|
|
80
|
+
sos = sig.butter(filter_order, [low / nyq, high / nyq], btype="bandpass", output="sos")
|
|
81
|
+
y = sig.sosfiltfilt(sos, y)
|
|
82
|
+
|
|
83
|
+
# Envelope
|
|
84
|
+
env = hilbert_envelope(y)
|
|
85
|
+
|
|
86
|
+
# Remove DC from envelope
|
|
87
|
+
env = env - np.mean(env)
|
|
88
|
+
|
|
89
|
+
# FFT of envelope
|
|
90
|
+
n = len(env)
|
|
91
|
+
freqs = np.fft.rfftfreq(n, d=1.0 / fs)
|
|
92
|
+
X = np.fft.rfft(env)
|
|
93
|
+
amps = (2.0 / n) * np.abs(X)
|
|
94
|
+
amps[0] /= 2.0
|
|
95
|
+
|
|
96
|
+
return freqs, amps
|