mcp-server-mcsa 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -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()
@@ -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.motor import (
4
- MotorParameters,
5
- calculate_motor_parameters,
6
- )
7
- from mcp_server_mcsa.analysis.bearing import (
8
- BearingGeometry,
9
- BearingDefectFrequencies,
10
- calculate_bearing_defect_frequencies,
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
- n_pos = n // 2 + 1
92
- freqs = np.fft.rfftfreq(n, d=1.0 / fs)
93
- X = np.fft.rfft(env)
94
- amps = (2.0 / n) * np.abs(X)
95
- amps[0] /= 2.0
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