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.
@@ -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
- idx_offset = int(np.argmax(mask))
106
- freqs_sub = freqs[mask]
107
- amps_sub = amps[mask]
108
- else:
109
- idx_offset = 0
110
- freqs_sub = freqs
111
- amps_sub = amps
112
-
113
- # Convert distance_hz to samples
114
- if distance_hz is not None and len(freqs_sub) > 1:
115
- df = float(freqs_sub[1] - freqs_sub[0])
116
- distance_samples = max(1, int(distance_hz / df))
117
- else:
118
- distance_samples = None
119
-
120
- peak_idx, properties = sig.find_peaks(
121
- amps_sub,
122
- height=height,
123
- prominence=prominence,
124
- distance=distance_samples,
125
- )
126
-
127
- # Build result list
128
- results = []
129
- for i, pi in enumerate(peak_idx):
130
- entry: dict = {
131
- "frequency_hz": float(freqs_sub[pi]),
132
- "amplitude": float(amps_sub[pi]),
133
- }
134
- if "prominences" in properties:
135
- entry["prominence"] = float(properties["prominences"][i])
136
- results.append(entry)
137
-
138
- # Sort by amplitude descending, limit
139
- results.sort(key=lambda p: p["amplitude"], reverse=True)
140
- return results[:max_peaks]
141
-
142
-
143
- def amplitude_at_frequency(
144
- freqs: NDArray[np.floating],
145
- amps: NDArray[np.floating],
146
- target_freq_hz: float,
147
- tolerance_hz: float = 0.5,
148
- ) -> dict:
149
- """Find the amplitude at (or nearest to) a target frequency.
150
-
151
- Args:
152
- freqs: Frequency axis.
153
- amps: Amplitude values.
154
- target_freq_hz: Frequency of interest.
155
- tolerance_hz: Search tolerance around target.
156
-
157
- Returns:
158
- Dict with ``frequency_hz``, ``amplitude``, ``found`` flag.
159
- """
160
- mask = np.abs(freqs - target_freq_hz) <= tolerance_hz
161
- if not np.any(mask):
162
- return {"frequency_hz": target_freq_hz, "amplitude": 0.0, "found": False}
163
-
164
- subset_amps = amps[mask]
165
- subset_freqs = freqs[mask]
166
- best = int(np.argmax(subset_amps))
167
-
168
- return {
169
- "frequency_hz": float(subset_freqs[best]),
170
- "amplitude": float(subset_amps[best]),
171
- "found": True,
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
+ }