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,172 +1,171 @@
|
|
|
1
|
-
"""Spectral analysis utilities for MCSA.
|
|
2
|
-
|
|
3
|
-
FFT‑based spectrum, Welch PSD, and spectral peak detection.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from typing import Literal
|
|
9
|
-
|
|
10
|
-
import numpy as np
|
|
11
|
-
from numpy.typing import NDArray
|
|
12
|
-
from scipy import signal as sig
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def compute_fft_spectrum(
|
|
16
|
-
x: NDArray[np.floating],
|
|
17
|
-
fs: float,
|
|
18
|
-
n_fft: int | None = None,
|
|
19
|
-
sided: Literal["one", "two"] = "one",
|
|
20
|
-
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
21
|
-
"""Compute the amplitude spectrum via FFT.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
x: Input time‑domain signal (real‑valued).
|
|
25
|
-
fs: Sampling frequency in Hz.
|
|
26
|
-
n_fft: FFT length (zero‑padded). Default → len(x).
|
|
27
|
-
sided: ``"one"`` for single‑sided (positive freqs only),
|
|
28
|
-
``"two"`` for full two‑sided spectrum.
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
(frequencies, amplitudes) — both 1‑D arrays.
|
|
32
|
-
"""
|
|
33
|
-
n = n_fft or len(x)
|
|
34
|
-
X = np.fft.fft(x, n=n)
|
|
35
|
-
|
|
36
|
-
if sided == "one":
|
|
37
|
-
n_pos = n // 2 + 1
|
|
38
|
-
freqs = np.fft.rfftfreq(n, d=1.0 / fs)
|
|
39
|
-
amps = (2.0 / len(x)) * np.abs(X[:n_pos])
|
|
40
|
-
amps[0] /= 2.0 # DC component not doubled
|
|
41
|
-
return freqs, amps
|
|
42
|
-
else:
|
|
43
|
-
freqs = np.fft.fftfreq(n, d=1.0 / fs)
|
|
44
|
-
amps = (1.0 / len(x)) * np.abs(X)
|
|
45
|
-
return freqs, amps
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def compute_psd(
|
|
49
|
-
x: NDArray[np.floating],
|
|
50
|
-
fs: float,
|
|
51
|
-
nperseg: int | None = None,
|
|
52
|
-
noverlap: int | None = None,
|
|
53
|
-
window: str = "hann",
|
|
54
|
-
scaling: Literal["density", "spectrum"] = "density",
|
|
55
|
-
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
56
|
-
"""Compute Power Spectral Density using Welch's method.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
x: Input signal.
|
|
60
|
-
fs: Sampling frequency in Hz.
|
|
61
|
-
nperseg: FFT segment length. Default → len(x) // 8 or 256.
|
|
62
|
-
noverlap: Overlap between segments. Default → nperseg // 2.
|
|
63
|
-
window: Window function name.
|
|
64
|
-
scaling: ``"density"`` → V²/Hz, ``"spectrum"`` → V².
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
(frequencies, psd_values) arrays.
|
|
68
|
-
"""
|
|
69
|
-
if nperseg is None:
|
|
70
|
-
nperseg = min(len(x), max(256, len(x) // 8))
|
|
71
|
-
|
|
72
|
-
freqs, psd = sig.welch(
|
|
73
|
-
x, fs=fs, window=window, nperseg=nperseg,
|
|
74
|
-
noverlap=noverlap, scaling=scaling,
|
|
75
|
-
)
|
|
76
|
-
return freqs, psd
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def detect_peaks(
|
|
80
|
-
freqs: NDArray[np.floating],
|
|
81
|
-
amps: NDArray[np.floating],
|
|
82
|
-
height: float | None = None,
|
|
83
|
-
prominence: float | None = None,
|
|
84
|
-
distance_hz: float | None = None,
|
|
85
|
-
freq_range: tuple[float, float] | None = None,
|
|
86
|
-
max_peaks: int = 50,
|
|
87
|
-
) -> list[dict]:
|
|
88
|
-
"""Detect spectral peaks and return their properties.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
freqs: Frequency axis (Hz).
|
|
92
|
-
amps: Amplitude or PSD values.
|
|
93
|
-
height: Minimum peak height.
|
|
94
|
-
prominence: Minimum peak prominence.
|
|
95
|
-
distance_hz: Minimum distance between peaks in Hz.
|
|
96
|
-
freq_range: Optional (low, high) Hz range to search within.
|
|
97
|
-
max_peaks: Maximum number of peaks to return (sorted by amplitude).
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
List of dicts with ``frequency_hz``, ``amplitude``, ``prominence``.
|
|
101
|
-
"""
|
|
102
|
-
# Restrict to frequency range
|
|
103
|
-
if freq_range is not None:
|
|
104
|
-
mask = (freqs >= freq_range[0]) & (freqs <= freq_range[1])
|
|
105
|
-
|
|
106
|
-
freqs_sub = freqs[mask]
|
|
107
|
-
amps_sub = amps[mask]
|
|
108
|
-
else:
|
|
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
|
-
results
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
}
|
|
1
|
+
"""Spectral analysis utilities for MCSA.
|
|
2
|
+
|
|
3
|
+
FFT‑based spectrum, Welch PSD, and spectral peak detection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from scipy import signal as sig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_fft_spectrum(
|
|
16
|
+
x: NDArray[np.floating],
|
|
17
|
+
fs: float,
|
|
18
|
+
n_fft: int | None = None,
|
|
19
|
+
sided: Literal["one", "two"] = "one",
|
|
20
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
21
|
+
"""Compute the amplitude spectrum via FFT.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
x: Input time‑domain signal (real‑valued).
|
|
25
|
+
fs: Sampling frequency in Hz.
|
|
26
|
+
n_fft: FFT length (zero‑padded). Default → len(x).
|
|
27
|
+
sided: ``"one"`` for single‑sided (positive freqs only),
|
|
28
|
+
``"two"`` for full two‑sided spectrum.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
(frequencies, amplitudes) — both 1‑D arrays.
|
|
32
|
+
"""
|
|
33
|
+
n = n_fft or len(x)
|
|
34
|
+
X = np.fft.fft(x, n=n)
|
|
35
|
+
|
|
36
|
+
if sided == "one":
|
|
37
|
+
n_pos = n // 2 + 1
|
|
38
|
+
freqs = np.fft.rfftfreq(n, d=1.0 / fs)
|
|
39
|
+
amps = (2.0 / len(x)) * np.abs(X[:n_pos])
|
|
40
|
+
amps[0] /= 2.0 # DC component not doubled
|
|
41
|
+
return freqs, amps
|
|
42
|
+
else:
|
|
43
|
+
freqs = np.fft.fftfreq(n, d=1.0 / fs)
|
|
44
|
+
amps = (1.0 / len(x)) * np.abs(X)
|
|
45
|
+
return freqs, amps
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_psd(
|
|
49
|
+
x: NDArray[np.floating],
|
|
50
|
+
fs: float,
|
|
51
|
+
nperseg: int | None = None,
|
|
52
|
+
noverlap: int | None = None,
|
|
53
|
+
window: str = "hann",
|
|
54
|
+
scaling: Literal["density", "spectrum"] = "density",
|
|
55
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
56
|
+
"""Compute Power Spectral Density using Welch's method.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
x: Input signal.
|
|
60
|
+
fs: Sampling frequency in Hz.
|
|
61
|
+
nperseg: FFT segment length. Default → len(x) // 8 or 256.
|
|
62
|
+
noverlap: Overlap between segments. Default → nperseg // 2.
|
|
63
|
+
window: Window function name.
|
|
64
|
+
scaling: ``"density"`` → V²/Hz, ``"spectrum"`` → V².
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
(frequencies, psd_values) arrays.
|
|
68
|
+
"""
|
|
69
|
+
if nperseg is None:
|
|
70
|
+
nperseg = min(len(x), max(256, len(x) // 8))
|
|
71
|
+
|
|
72
|
+
freqs, psd = sig.welch(
|
|
73
|
+
x, fs=fs, window=window, nperseg=nperseg,
|
|
74
|
+
noverlap=noverlap, scaling=scaling,
|
|
75
|
+
)
|
|
76
|
+
return freqs, psd
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def detect_peaks(
|
|
80
|
+
freqs: NDArray[np.floating],
|
|
81
|
+
amps: NDArray[np.floating],
|
|
82
|
+
height: float | None = None,
|
|
83
|
+
prominence: float | None = None,
|
|
84
|
+
distance_hz: float | None = None,
|
|
85
|
+
freq_range: tuple[float, float] | None = None,
|
|
86
|
+
max_peaks: int = 50,
|
|
87
|
+
) -> list[dict]:
|
|
88
|
+
"""Detect spectral peaks and return their properties.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
freqs: Frequency axis (Hz).
|
|
92
|
+
amps: Amplitude or PSD values.
|
|
93
|
+
height: Minimum peak height.
|
|
94
|
+
prominence: Minimum peak prominence.
|
|
95
|
+
distance_hz: Minimum distance between peaks in Hz.
|
|
96
|
+
freq_range: Optional (low, high) Hz range to search within.
|
|
97
|
+
max_peaks: Maximum number of peaks to return (sorted by amplitude).
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of dicts with ``frequency_hz``, ``amplitude``, ``prominence``.
|
|
101
|
+
"""
|
|
102
|
+
# Restrict to frequency range
|
|
103
|
+
if freq_range is not None:
|
|
104
|
+
mask = (freqs >= freq_range[0]) & (freqs <= freq_range[1])
|
|
105
|
+
int(np.argmax(mask))
|
|
106
|
+
freqs_sub = freqs[mask]
|
|
107
|
+
amps_sub = amps[mask]
|
|
108
|
+
else:
|
|
109
|
+
freqs_sub = freqs
|
|
110
|
+
amps_sub = amps
|
|
111
|
+
|
|
112
|
+
# Convert distance_hz to samples
|
|
113
|
+
if distance_hz is not None and len(freqs_sub) > 1:
|
|
114
|
+
df = float(freqs_sub[1] - freqs_sub[0])
|
|
115
|
+
distance_samples = max(1, int(distance_hz / df))
|
|
116
|
+
else:
|
|
117
|
+
distance_samples = None
|
|
118
|
+
|
|
119
|
+
peak_idx, properties = sig.find_peaks(
|
|
120
|
+
amps_sub,
|
|
121
|
+
height=height,
|
|
122
|
+
prominence=prominence,
|
|
123
|
+
distance=distance_samples,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Build result list
|
|
127
|
+
results = []
|
|
128
|
+
for i, pi in enumerate(peak_idx):
|
|
129
|
+
entry: dict = {
|
|
130
|
+
"frequency_hz": float(freqs_sub[pi]),
|
|
131
|
+
"amplitude": float(amps_sub[pi]),
|
|
132
|
+
}
|
|
133
|
+
if "prominences" in properties:
|
|
134
|
+
entry["prominence"] = float(properties["prominences"][i])
|
|
135
|
+
results.append(entry)
|
|
136
|
+
|
|
137
|
+
# Sort by amplitude descending, limit
|
|
138
|
+
results.sort(key=lambda p: p["amplitude"], reverse=True)
|
|
139
|
+
return results[:max_peaks]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def amplitude_at_frequency(
|
|
143
|
+
freqs: NDArray[np.floating],
|
|
144
|
+
amps: NDArray[np.floating],
|
|
145
|
+
target_freq_hz: float,
|
|
146
|
+
tolerance_hz: float = 0.5,
|
|
147
|
+
) -> dict:
|
|
148
|
+
"""Find the amplitude at (or nearest to) a target frequency.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
freqs: Frequency axis.
|
|
152
|
+
amps: Amplitude values.
|
|
153
|
+
target_freq_hz: Frequency of interest.
|
|
154
|
+
tolerance_hz: Search tolerance around target.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dict with ``frequency_hz``, ``amplitude``, ``found`` flag.
|
|
158
|
+
"""
|
|
159
|
+
mask = np.abs(freqs - target_freq_hz) <= tolerance_hz
|
|
160
|
+
if not np.any(mask):
|
|
161
|
+
return {"frequency_hz": target_freq_hz, "amplitude": 0.0, "found": False}
|
|
162
|
+
|
|
163
|
+
subset_amps = amps[mask]
|
|
164
|
+
subset_freqs = freqs[mask]
|
|
165
|
+
best = int(np.argmax(subset_amps))
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"frequency_hz": float(subset_freqs[best]),
|
|
169
|
+
"amplitude": float(subset_amps[best]),
|
|
170
|
+
"found": True,
|
|
171
|
+
}
|