SignalProcessingTools 1.2.2__tar.gz → 1.2.4__tar.gz
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.
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/PKG-INFO +4 -3
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/__version__.py +1 -1
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/space_signal.py +66 -31
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/time_signal.py +140 -64
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/PKG-INFO +4 -3
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/requires.txt +3 -2
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/setup.cfg +3 -2
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/tests/test_space_signal.py +21 -14
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/tests/test_time_signal.py +103 -43
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/ReadMe.md +0 -0
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/__init__.py +0 -0
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/SOURCES.txt +0 -0
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/dependency_links.txt +0 -0
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/top_level.txt +0 -0
- {signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SignalProcessingTools
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Signal processing tools
|
|
5
5
|
Author: attr: SignalProcessingTools.__author__
|
|
6
6
|
Author-email: bruno.zuadacoelho@deltares.nl, aron.noordam@deltares.nl
|
|
@@ -13,8 +13,9 @@ Requires-Dist: matplotlib>=3.10
|
|
|
13
13
|
Requires-Dist: numpy>=2.2
|
|
14
14
|
Requires-Dist: scipy>=1.15
|
|
15
15
|
Provides-Extra: testing
|
|
16
|
-
Requires-Dist: pytest>=8.
|
|
17
|
-
Requires-Dist: tox>=4.
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "testing"
|
|
17
|
+
Requires-Dist: tox>=4.13; extra == "testing"
|
|
18
|
+
Requires-Dist: pre_commit>=4.2.0; extra == "testing"
|
|
18
19
|
|
|
19
20
|
# SignalProcessingTools
|
|
20
21
|
|
{signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/space_signal.py
RENAMED
|
@@ -8,7 +8,11 @@ class SpaceSignalProcessing:
|
|
|
8
8
|
"""
|
|
9
9
|
SignalProcessing class for processing signals in space.
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
def __init__(self,
|
|
13
|
+
x: npt.NDArray[np.float64],
|
|
14
|
+
values: npt.NDArray[np.float64],
|
|
15
|
+
Fs: Optional[float] = None):
|
|
12
16
|
"""
|
|
13
17
|
Initializes the ProcessSignal object.
|
|
14
18
|
|
|
@@ -40,7 +44,6 @@ class SpaceSignalProcessing:
|
|
|
40
44
|
self.max_fast = None
|
|
41
45
|
self.max_fast_Dx = None
|
|
42
46
|
|
|
43
|
-
|
|
44
47
|
def compute_track_longitudinal_levels(self):
|
|
45
48
|
"""
|
|
46
49
|
Computes the track longitudinal levels, following EN 13848-1:2006.
|
|
@@ -53,20 +56,34 @@ class SpaceSignalProcessing:
|
|
|
53
56
|
- D3: 70m < lambda <= 150m (1/150 Hz < f <= 1/70 Hz)
|
|
54
57
|
"""
|
|
55
58
|
|
|
56
|
-
sig = TimeSignalProcessing(self.coordinates,
|
|
57
|
-
|
|
59
|
+
sig = TimeSignalProcessing(self.coordinates,
|
|
60
|
+
self.signal_raw,
|
|
61
|
+
Fs=self.fs)
|
|
62
|
+
sig.filter([1 / 5., 1.],
|
|
63
|
+
4,
|
|
64
|
+
type_filter="bandpass",
|
|
65
|
+
filter_design=FilterDesign.BUTTERWORTH)
|
|
58
66
|
self.d0 = sig.signal
|
|
59
67
|
|
|
60
68
|
sig.reset()
|
|
61
|
-
sig.filter([1/25., 1/3.],
|
|
69
|
+
sig.filter([1 / 25., 1 / 3.],
|
|
70
|
+
4,
|
|
71
|
+
type_filter="bandpass",
|
|
72
|
+
filter_design=FilterDesign.BUTTERWORTH)
|
|
62
73
|
self.d1 = sig.signal
|
|
63
74
|
sig.reset()
|
|
64
75
|
|
|
65
|
-
sig.filter([1/70., 1/25.],
|
|
76
|
+
sig.filter([1 / 70., 1 / 25.],
|
|
77
|
+
4,
|
|
78
|
+
type_filter="bandpass",
|
|
79
|
+
filter_design=FilterDesign.BUTTERWORTH)
|
|
66
80
|
self.d2 = sig.signal
|
|
67
81
|
sig.reset()
|
|
68
82
|
|
|
69
|
-
sig.filter([1/150., 1/70.],
|
|
83
|
+
sig.filter([1 / 150., 1 / 70.],
|
|
84
|
+
4,
|
|
85
|
+
type_filter="bandpass",
|
|
86
|
+
filter_design=FilterDesign.BUTTERWORTH)
|
|
70
87
|
self.d3 = sig.signal
|
|
71
88
|
sig.reset()
|
|
72
89
|
|
|
@@ -82,22 +99,23 @@ class SpaceSignalProcessing:
|
|
|
82
99
|
"""
|
|
83
100
|
|
|
84
101
|
# octave bands used for the processing
|
|
85
|
-
one_third_octave_bands = [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
one_third_octave_bands = [
|
|
103
|
+
[.08, .10],
|
|
104
|
+
[.10, .126],
|
|
105
|
+
[.126, .16],
|
|
106
|
+
[.16, .20],
|
|
107
|
+
[.20, .253],
|
|
108
|
+
[.253, .32],
|
|
109
|
+
[.32, .40],
|
|
110
|
+
[.40, .50],
|
|
111
|
+
[.50, .63],
|
|
112
|
+
]
|
|
96
113
|
|
|
97
114
|
# setting for the processing
|
|
98
115
|
self.DXmaxFast = 1
|
|
99
116
|
nb_fft_min = 256 # minimum number of samples for the power spectral density
|
|
100
|
-
derivative = [0, 0, 0, 2, 2, 2, 2, 2,
|
|
117
|
+
derivative = [0, 0, 0, 2, 2, 2, 2, 2,
|
|
118
|
+
2] # number of times that each frequency band is derived
|
|
101
119
|
|
|
102
120
|
# RMS of the square root of the power spectral density
|
|
103
121
|
self.rms_bands = np.zeros(len(one_third_octave_bands))
|
|
@@ -111,26 +129,34 @@ class SpaceSignalProcessing:
|
|
|
111
129
|
self.signal = self.signal_raw * 1000
|
|
112
130
|
|
|
113
131
|
# compute the power spectral density
|
|
114
|
-
n_fft = int(
|
|
132
|
+
n_fft = int(
|
|
133
|
+
np.max([2**(np.ceil(np.log2(len(self.signal)))), nb_fft_min]))
|
|
115
134
|
# if signal is odd length, add a zero to make it even
|
|
116
135
|
if len(self.signal) % 2 != 0:
|
|
117
136
|
signal = np.append(self.signal, 0)
|
|
118
|
-
coordinates = np.append(
|
|
137
|
+
coordinates = np.append(
|
|
138
|
+
self.coordinates, self.coordinates[-1] +
|
|
139
|
+
(self.coordinates[1] - self.coordinates[0]))
|
|
119
140
|
else:
|
|
120
141
|
signal = self.signal
|
|
121
142
|
coordinates = self.coordinates
|
|
122
|
-
sig = TimeSignalProcessing(coordinates,
|
|
143
|
+
sig = TimeSignalProcessing(coordinates,
|
|
144
|
+
signal,
|
|
145
|
+
Fs=self.fs,
|
|
146
|
+
window=Windows.HAMMING,
|
|
123
147
|
window_size=len(signal))
|
|
124
148
|
sig.psd(nb_points=n_fft, detrend=False)
|
|
125
149
|
|
|
126
150
|
# compute the rsm psd
|
|
127
|
-
self.__rms_effective(sig.frequency_Pxx, sig.Pxx,
|
|
151
|
+
self.__rms_effective(sig.frequency_Pxx, sig.Pxx,
|
|
152
|
+
one_third_octave_bands, derivative)
|
|
128
153
|
# compute the effective values
|
|
129
154
|
self.__effective_values(one_third_octave_bands, derivative)
|
|
130
155
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
one_third_octave_bands: List[Tuple[float, float]],
|
|
156
|
+
def __rms_effective(self, frequency: npt.NDArray[np.float64],
|
|
157
|
+
Pxx: npt.NDArray[np.float64],
|
|
158
|
+
one_third_octave_bands: List[Tuple[float, float]],
|
|
159
|
+
derivative: List[int]):
|
|
134
160
|
"""
|
|
135
161
|
Computes RMS square root of power spectral density
|
|
136
162
|
|
|
@@ -148,10 +174,13 @@ class SpaceSignalProcessing:
|
|
|
148
174
|
for i, band in enumerate(one_third_octave_bands):
|
|
149
175
|
# find indexes where the bands exist
|
|
150
176
|
idx = np.where((frequency >= band[0]) & (frequency < band[1]))[0]
|
|
151
|
-
Pxx[idx] = (2 * np.pi * frequency[idx])
|
|
177
|
+
Pxx[idx] = (2 * np.pi * frequency[idx])**(2 *
|
|
178
|
+
derivative[i]) * Pxx[idx]
|
|
152
179
|
self.rms_bands[i] = np.sqrt(np.sum(Pxx[idx] * delta_f))
|
|
153
180
|
|
|
154
|
-
def __effective_values(self, one_third_octave_bands: List[Tuple[float,
|
|
181
|
+
def __effective_values(self, one_third_octave_bands: List[Tuple[float,
|
|
182
|
+
float]],
|
|
183
|
+
derivative: List[int]):
|
|
155
184
|
"""
|
|
156
185
|
Computes the effective values of the signal
|
|
157
186
|
|
|
@@ -170,8 +199,13 @@ class SpaceSignalProcessing:
|
|
|
170
199
|
|
|
171
200
|
for i, band in enumerate(one_third_octave_bands):
|
|
172
201
|
derivative_value = derivative[i]
|
|
173
|
-
sig = TimeSignalProcessing(self.coordinates,
|
|
174
|
-
|
|
202
|
+
sig = TimeSignalProcessing(self.coordinates,
|
|
203
|
+
self.signal,
|
|
204
|
+
Fs=self.fs)
|
|
205
|
+
sig.filter(np.array(band),
|
|
206
|
+
N=3,
|
|
207
|
+
type_filter="bandpass",
|
|
208
|
+
filter_design=FilterDesign.BUTTERWORTH)
|
|
175
209
|
new_signal = sig.signal
|
|
176
210
|
|
|
177
211
|
while derivative_value != 0:
|
|
@@ -181,7 +215,8 @@ class SpaceSignalProcessing:
|
|
|
181
215
|
ksi = np.linspace(0, n * tau, int(n * tau / dx + 1))
|
|
182
216
|
g = fout * np.exp(-ksi / tau)
|
|
183
217
|
|
|
184
|
-
convoluted_signal = np.sqrt(
|
|
218
|
+
convoluted_signal = np.sqrt(
|
|
219
|
+
np.convolve(new_signal**2, g) * dx / tau)
|
|
185
220
|
self.max_fast[i] = np.max(convoluted_signal)
|
|
186
221
|
idx = np.floor((len(self.signal) - np.floor(self.DXmaxFast / dx)) / 2) + \
|
|
187
222
|
np.linspace(0, np.floor(self.DXmaxFast / dx)-1, int(np.floor(self.DXmaxFast / dx)))
|
{signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/time_signal.py
RENAMED
|
@@ -14,6 +14,7 @@ class FilterDesign(Enum):
|
|
|
14
14
|
CHEBYSHEV = 2
|
|
15
15
|
ELLIPTIC = 3
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
class IntegrationRules(Enum):
|
|
18
19
|
"""
|
|
19
20
|
Integration rules
|
|
@@ -21,6 +22,7 @@ class IntegrationRules(Enum):
|
|
|
21
22
|
TRAPEZOID = 1
|
|
22
23
|
SIMPSON = 2
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
class Windows(Enum):
|
|
25
27
|
"""
|
|
26
28
|
Windows types
|
|
@@ -34,10 +36,12 @@ class Windows(Enum):
|
|
|
34
36
|
RECTANGULAR = 'boxcar'
|
|
35
37
|
TRIANG = 'triang'
|
|
36
38
|
|
|
39
|
+
|
|
37
40
|
class TimeSignalProcessing:
|
|
38
41
|
"""
|
|
39
42
|
Signal processing class for time signals
|
|
40
43
|
"""
|
|
44
|
+
|
|
41
45
|
def __init__(self,
|
|
42
46
|
time: npt.NDArray[np.float64],
|
|
43
47
|
signal: npt.NDArray[np.float64],
|
|
@@ -71,8 +75,7 @@ class TimeSignalProcessing:
|
|
|
71
75
|
self.Sxx = None
|
|
72
76
|
self.frequency_Sxx = None
|
|
73
77
|
self.time_Sxx = None
|
|
74
|
-
self.fft_settings = {"nb_points": None,
|
|
75
|
-
"half_representation": False}
|
|
78
|
+
self.fft_settings = {"nb_points": None, "half_representation": False}
|
|
76
79
|
# Track operations performed on the signal
|
|
77
80
|
self.operations = []
|
|
78
81
|
|
|
@@ -93,25 +96,38 @@ class TimeSignalProcessing:
|
|
|
93
96
|
self.use_window = False
|
|
94
97
|
else:
|
|
95
98
|
if window_size == 0:
|
|
96
|
-
raise ValueError(
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"When using a window the `window_size` must be specified")
|
|
97
101
|
if window_size % 2 != 0:
|
|
98
102
|
raise ValueError("Window length must be even")
|
|
99
103
|
if window not in Windows:
|
|
100
|
-
raise ValueError(
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Window type {window} not supported. Available types: {list(Windows)}"
|
|
106
|
+
)
|
|
101
107
|
if window_size > signal_length:
|
|
102
|
-
raise ValueError(
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Window length ({window_size}) cannot be greater than signal length ({signal_length})."
|
|
110
|
+
)
|
|
103
111
|
|
|
104
112
|
self.window = self.__create_window(window, window_size)
|
|
105
113
|
self.window_size = window_size
|
|
106
114
|
self.window_type = window
|
|
107
|
-
self.nb_windows = int(
|
|
115
|
+
self.nb_windows = int(
|
|
116
|
+
np.ceil((signal_length / window_size) * 2 - 2))
|
|
108
117
|
self.use_window = True
|
|
109
118
|
|
|
110
119
|
# pad signal at the end if necessary to get full windows
|
|
111
120
|
if signal_length % window_size != 0:
|
|
112
|
-
self.signal = np.append(
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
self.signal = np.append(
|
|
122
|
+
self.signal,
|
|
123
|
+
np.zeros(window_size - (signal_length % window_size)))
|
|
124
|
+
self.time = np.append(
|
|
125
|
+
self.time, self.time[-1] + np.cumsum(
|
|
126
|
+
np.ones(window_size - (signal_length % window_size)) *
|
|
127
|
+
(1 / Fs)))
|
|
128
|
+
self.operations.append(
|
|
129
|
+
f"Signal padded with zeros (original length: {signal_length}, new length: {len(self.signal)})"
|
|
130
|
+
)
|
|
115
131
|
|
|
116
132
|
def __str__(self) -> str:
|
|
117
133
|
"""
|
|
@@ -148,7 +164,8 @@ class TimeSignalProcessing:
|
|
|
148
164
|
return "\n".join(info)
|
|
149
165
|
|
|
150
166
|
@staticmethod
|
|
151
|
-
def __create_window(window_type: Windows,
|
|
167
|
+
def __create_window(window_type: Windows,
|
|
168
|
+
size: int) -> npt.NDArray[np.float64]:
|
|
152
169
|
"""
|
|
153
170
|
Create a window array of specified type and size
|
|
154
171
|
|
|
@@ -234,10 +251,10 @@ class TimeSignalProcessing:
|
|
|
234
251
|
# peak amplitude of stationary sinusoids.
|
|
235
252
|
spectrum_w[:, w] = np.fft.fft(signal_w, nfft) / normalise_fct
|
|
236
253
|
|
|
237
|
-
|
|
238
254
|
self.amplitude = np.mean(np.abs(spectrum_w), axis=1)
|
|
239
255
|
# self.phase = np.unwrap(np.angle(np.mean(spectrum_w, axis=1)))
|
|
240
|
-
self.phase = np.angle(
|
|
256
|
+
self.phase = np.angle(
|
|
257
|
+
np.mean(np.exp(1j * np.angle(spectrum_w)), axis=1))
|
|
241
258
|
|
|
242
259
|
# compute frequency
|
|
243
260
|
self.frequency = np.linspace(0, 1, nfft) * self.Fs
|
|
@@ -249,9 +266,11 @@ class TimeSignalProcessing:
|
|
|
249
266
|
self.phase = self.phase[:int(nfft / 2)]
|
|
250
267
|
|
|
251
268
|
# FFT settings: needed to perform inverse FFT
|
|
252
|
-
self.fft_settings = {
|
|
253
|
-
|
|
254
|
-
|
|
269
|
+
self.fft_settings = {
|
|
270
|
+
"nb_points": nfft,
|
|
271
|
+
"half_representation": half_representation,
|
|
272
|
+
"odd_length": odd_length
|
|
273
|
+
}
|
|
255
274
|
|
|
256
275
|
# Add to operations list
|
|
257
276
|
op_info = f"FFT (points: {nfft}, half representation: {half_representation})"
|
|
@@ -275,7 +294,8 @@ class TimeSignalProcessing:
|
|
|
275
294
|
"Please compute FFT with full representation.")
|
|
276
295
|
|
|
277
296
|
if self.use_window:
|
|
278
|
-
raise ValueError(
|
|
297
|
+
raise ValueError(
|
|
298
|
+
"Cannot perform inverse FFT on the windowed signal.")
|
|
279
299
|
|
|
280
300
|
# get FFT settings
|
|
281
301
|
odd_length = self.fft_settings["odd_length"]
|
|
@@ -290,7 +310,8 @@ class TimeSignalProcessing:
|
|
|
290
310
|
# inverse of the FFT signal
|
|
291
311
|
self.signal_inv = np.real(spectrum_inv) * len(spectrum)
|
|
292
312
|
# time from frequency
|
|
293
|
-
self.time_inv = np.cumsum(
|
|
313
|
+
self.time_inv = np.cumsum(
|
|
314
|
+
np.ones(len(spectrum)) * 1 / self.Fs) - 1 / self.Fs
|
|
294
315
|
|
|
295
316
|
if odd_length:
|
|
296
317
|
# remove last sample
|
|
@@ -298,11 +319,17 @@ class TimeSignalProcessing:
|
|
|
298
319
|
self.time_inv = self.time_inv[:-1]
|
|
299
320
|
|
|
300
321
|
# Add to operations list
|
|
301
|
-
self.operations.append("Inverse FFT" +
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
self.operations.append("Inverse FFT" +
|
|
323
|
+
(" with windowing" if self.use_window else ""))
|
|
324
|
+
|
|
325
|
+
def integrate(self,
|
|
326
|
+
rule: IntegrationRules = IntegrationRules.TRAPEZOID,
|
|
327
|
+
baseline: bool = False,
|
|
328
|
+
moving: bool = False,
|
|
329
|
+
hp: bool = False,
|
|
330
|
+
ini_cond: float = 0.,
|
|
331
|
+
fpass: float = 0.5,
|
|
332
|
+
n: int = 6):
|
|
306
333
|
"""
|
|
307
334
|
Numerical integration of signal
|
|
308
335
|
|
|
@@ -322,9 +349,13 @@ class TimeSignalProcessing:
|
|
|
322
349
|
|
|
323
350
|
# integration rule
|
|
324
351
|
if rule == IntegrationRules.TRAPEZOID:
|
|
325
|
-
self.signal = integrate.cumulative_trapezoid(self.signal,
|
|
352
|
+
self.signal = integrate.cumulative_trapezoid(self.signal,
|
|
353
|
+
self.time,
|
|
354
|
+
initial=ini_cond)
|
|
326
355
|
elif rule == IntegrationRules.SIMPSON:
|
|
327
|
-
self.signal = integrate.cumulative_simpson(self.signal,
|
|
356
|
+
self.signal = integrate.cumulative_simpson(self.signal,
|
|
357
|
+
x=self.time,
|
|
358
|
+
initial=ini_cond)
|
|
328
359
|
else:
|
|
329
360
|
sys.exit("Integration rule not supported")
|
|
330
361
|
|
|
@@ -346,13 +377,18 @@ class TimeSignalProcessing:
|
|
|
346
377
|
if moving:
|
|
347
378
|
op_details.append("moving average correction")
|
|
348
379
|
if hp:
|
|
349
|
-
op_details.append(
|
|
380
|
+
op_details.append(
|
|
381
|
+
f"highpass filter (cutoff: {fpass} Hz, order: {n})")
|
|
350
382
|
|
|
351
383
|
self.operations.append(f"Integration ({', '.join(op_details)})")
|
|
352
384
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
385
|
+
def filter(self,
|
|
386
|
+
Fpass: float,
|
|
387
|
+
N: int,
|
|
388
|
+
filter_design: FilterDesign = FilterDesign.ELLIPTIC,
|
|
389
|
+
type_filter: str = "lowpass",
|
|
390
|
+
rp: float = 0.01,
|
|
391
|
+
rs: int = 60):
|
|
356
392
|
"""
|
|
357
393
|
Filter signal
|
|
358
394
|
|
|
@@ -377,11 +413,23 @@ class TimeSignalProcessing:
|
|
|
377
413
|
|
|
378
414
|
# design filter
|
|
379
415
|
if filter_design == FilterDesign.ELLIPTIC:
|
|
380
|
-
z, p, k = signal.ellip(N,
|
|
416
|
+
z, p, k = signal.ellip(N,
|
|
417
|
+
rp,
|
|
418
|
+
rs,
|
|
419
|
+
np.array(Fpass) / (self.Fs / 2),
|
|
420
|
+
btype=type_filter,
|
|
421
|
+
output='zpk')
|
|
381
422
|
elif filter_design == FilterDesign.BUTTERWORTH:
|
|
382
|
-
z, p, k = signal.butter(N,
|
|
423
|
+
z, p, k = signal.butter(N,
|
|
424
|
+
np.array(Fpass) / (self.Fs / 2),
|
|
425
|
+
btype=type_filter,
|
|
426
|
+
output='zpk')
|
|
383
427
|
elif filter_design == FilterDesign.CHEBYSHEV:
|
|
384
|
-
z, p, k = signal.cheby1(N,
|
|
428
|
+
z, p, k = signal.cheby1(N,
|
|
429
|
+
rp,
|
|
430
|
+
np.array(Fpass) / (self.Fs / 2),
|
|
431
|
+
btype=type_filter,
|
|
432
|
+
output='zpk')
|
|
385
433
|
|
|
386
434
|
sos = signal.zpk2sos(z, p, k)
|
|
387
435
|
|
|
@@ -389,8 +437,8 @@ class TimeSignalProcessing:
|
|
|
389
437
|
self.signal = signal.sosfiltfilt(sos, self.signal)
|
|
390
438
|
|
|
391
439
|
# Add to operations list
|
|
392
|
-
self.operations.append(
|
|
393
|
-
|
|
440
|
+
self.operations.append(
|
|
441
|
+
f"Filter ({type_filter}, cutoff: {Fpass} Hz, order: {N})")
|
|
394
442
|
|
|
395
443
|
def psd(self, detrend: str = "linear", nb_points: Optional[int] = None):
|
|
396
444
|
"""
|
|
@@ -403,11 +451,15 @@ class TimeSignalProcessing:
|
|
|
403
451
|
"""
|
|
404
452
|
|
|
405
453
|
if detrend not in ["linear", False]:
|
|
406
|
-
raise ValueError(
|
|
454
|
+
raise ValueError(
|
|
455
|
+
"Detrend method not supported. Available methods: ['linear', False]"
|
|
456
|
+
)
|
|
407
457
|
|
|
408
458
|
# check if window is initialized
|
|
409
459
|
if not self.use_window:
|
|
410
|
-
raise ValueError(
|
|
460
|
+
raise ValueError(
|
|
461
|
+
"No window defined. Please define a window when initialising SignalProcessing."
|
|
462
|
+
)
|
|
411
463
|
|
|
412
464
|
# if nb_points is None: nb_points is window length
|
|
413
465
|
if nb_points is None:
|
|
@@ -416,12 +468,18 @@ class TimeSignalProcessing:
|
|
|
416
468
|
nfft = nb_points
|
|
417
469
|
|
|
418
470
|
# compute PSD using Welch method
|
|
419
|
-
self.frequency_Pxx, self.Pxx = signal.welch(
|
|
420
|
-
|
|
471
|
+
self.frequency_Pxx, self.Pxx = signal.welch(
|
|
472
|
+
self.signal,
|
|
473
|
+
fs=self.Fs,
|
|
474
|
+
nperseg=self.window_size,
|
|
475
|
+
nfft=nfft,
|
|
476
|
+
window=self.window_type.value,
|
|
477
|
+
scaling='density',
|
|
478
|
+
detrend=detrend)
|
|
421
479
|
|
|
422
480
|
# Add to operations list
|
|
423
|
-
self.operations.append(
|
|
424
|
-
|
|
481
|
+
self.operations.append(
|
|
482
|
+
f"PSD (window: {self.window_type.name}, size: {self.window_size})")
|
|
425
483
|
|
|
426
484
|
def v_eff_SBR(self, n: int = 4, tau: float = 0.125):
|
|
427
485
|
"""
|
|
@@ -438,13 +496,12 @@ class TimeSignalProcessing:
|
|
|
438
496
|
qsi = np.linspace(0, n * tau, int(n * tau * self.Fs + 1))
|
|
439
497
|
g = fout * np.exp(-qsi / tau)
|
|
440
498
|
|
|
441
|
-
|
|
442
499
|
# Frequency weighting parameters
|
|
443
500
|
v0 = 1 / 1000 # Reference velocity [m/s]
|
|
444
|
-
f0 = 5.6
|
|
501
|
+
f0 = 5.6 # Reference frequency [Hz]
|
|
445
502
|
|
|
446
503
|
# Handle even/odd signal length for FFT
|
|
447
|
-
if self.signal.shape[0]
|
|
504
|
+
if self.signal.shape[0] % 2 != 0:
|
|
448
505
|
nv1 = int(self.signal.shape[0] / 2 + 0.5)
|
|
449
506
|
nv2 = int(self.signal.shape[0] / 2 - 0.5)
|
|
450
507
|
else:
|
|
@@ -456,25 +513,25 @@ class TimeSignalProcessing:
|
|
|
456
513
|
freq = np.arange(df, (nv1 + 1) * df, df)
|
|
457
514
|
|
|
458
515
|
# Create high-pass weighting filter (human perception curve)
|
|
459
|
-
Hv = (1 / v0) * 1 / (np.sqrt(1 + (f0 / freq)
|
|
516
|
+
Hv = (1 / v0) * 1 / (np.sqrt(1 + (f0 / freq)**2))
|
|
460
517
|
Hv = np.append(0, Hv) # Add DC component
|
|
461
518
|
|
|
462
519
|
# Create low-pass filter with 50 Hz cutoff
|
|
463
520
|
cut_off_number = int(np.ceil(50 / df))
|
|
464
521
|
if cut_off_number < nv1:
|
|
465
522
|
Hv2 = np.zeros(Hv.shape[0])
|
|
466
|
-
Hv2[:cut_off_number+1] = 1
|
|
523
|
+
Hv2[:cut_off_number + 1] = 1
|
|
467
524
|
else:
|
|
468
525
|
Hv2 = np.ones(Hv.shape[0])
|
|
469
526
|
|
|
470
527
|
# Applies the frequency weighting functions
|
|
471
528
|
Fv = np.fft.fft(self.signal)
|
|
472
|
-
Fhv = Hv2 * Hv * Fv[:nv1+1]
|
|
529
|
+
Fhv = Hv2 * Hv * Fv[:nv1 + 1]
|
|
473
530
|
Fv = np.append(Fhv, np.flipud(np.conj(Fhv[1:nv2])))
|
|
474
531
|
v_eff = np.real(np.fft.ifft(Fv))
|
|
475
532
|
|
|
476
533
|
# moving root-mean-square through convolution with the exponential decay function `g`
|
|
477
|
-
v_eff = np.sqrt(
|
|
534
|
+
v_eff = np.sqrt(np.convolve(v_eff**2, g) * (1 / self.Fs) / tau)
|
|
478
535
|
|
|
479
536
|
self.v_eff = v_eff[:self.signal.shape[0]]
|
|
480
537
|
|
|
@@ -514,14 +571,19 @@ class TimeSignalProcessing:
|
|
|
514
571
|
Compute spectrogram of signal
|
|
515
572
|
"""
|
|
516
573
|
# compute spectrogram
|
|
517
|
-
f, t, Sxx = signal.spectrogram(self.signal,
|
|
518
|
-
|
|
574
|
+
f, t, Sxx = signal.spectrogram(self.signal,
|
|
575
|
+
fs=self.Fs,
|
|
576
|
+
window=self.window_type.value,
|
|
577
|
+
nperseg=self.window_size,
|
|
578
|
+
noverlap=self.window_size // 8)
|
|
519
579
|
self.Sxx = Sxx
|
|
520
580
|
self.frequency_Sxx = f
|
|
521
581
|
self.time_Sxx = t
|
|
522
582
|
|
|
523
583
|
# Add to operations list
|
|
524
|
-
self.operations.append(
|
|
584
|
+
self.operations.append(
|
|
585
|
+
f"Spectrogram (nperseg: {self.window_size}, noverlap: {self.window_size // 8})"
|
|
586
|
+
)
|
|
525
587
|
|
|
526
588
|
def one_third_octave_bands(self):
|
|
527
589
|
"""
|
|
@@ -534,24 +596,31 @@ class TimeSignalProcessing:
|
|
|
534
596
|
# https://en.wikipedia.org/wiki/Octave_band
|
|
535
597
|
initial_frequency_band_number = -20
|
|
536
598
|
final_frequency_band_number = 33
|
|
537
|
-
names = ("10", "12.5", "16", "20", "25", "31.5", "40", "50", "63",
|
|
538
|
-
"
|
|
539
|
-
"
|
|
599
|
+
names = ("10", "12.5", "16", "20", "25", "31.5", "40", "50", "63",
|
|
600
|
+
"80", "100", "125", "160", "200", "250", "315", "400", "500",
|
|
601
|
+
"630", "800", "1000", "1250", "1600", "2000", "2500", "3.150",
|
|
602
|
+
"4000", "5000", "6300", "8000", "10000", "12500", "16000",
|
|
603
|
+
"20000")
|
|
540
604
|
|
|
541
605
|
# compute centre frequencies of the bands
|
|
542
|
-
f_centre = 1000 * (2
|
|
543
|
-
|
|
544
|
-
|
|
606
|
+
f_centre = 1000 * (2**(np.arange(initial_frequency_band_number,
|
|
607
|
+
final_frequency_band_number) / 3))
|
|
608
|
+
f_upper = f_centre * (2**(1 / 6))
|
|
609
|
+
f_lower = f_centre / (2**(1 / 6))
|
|
545
610
|
|
|
546
611
|
# sum the signal for the bands
|
|
547
612
|
if (self.Pxx is None) and (self.amplitude is None):
|
|
548
|
-
raise ValueError(
|
|
613
|
+
raise ValueError(
|
|
614
|
+
"No PSD nor FFT computed. Please compute either first.")
|
|
549
615
|
|
|
550
616
|
if self.Pxx is not None:
|
|
551
617
|
# determine the frequency bands
|
|
552
|
-
idx = np.where((f_lower < np.max(self.frequency_Pxx))
|
|
618
|
+
idx = np.where((f_lower < np.max(self.frequency_Pxx))
|
|
619
|
+
& (f_upper > np.min(self.frequency_Pxx)))[0]
|
|
553
620
|
if len(idx) == 0:
|
|
554
|
-
raise ValueError(
|
|
621
|
+
raise ValueError(
|
|
622
|
+
"No frequency bands found in the PSD. Please check the frequency bands."
|
|
623
|
+
)
|
|
555
624
|
|
|
556
625
|
delta_f = self.frequency_Pxx[1] - self.frequency_Pxx[0]
|
|
557
626
|
|
|
@@ -561,14 +630,19 @@ class TimeSignalProcessing:
|
|
|
561
630
|
|
|
562
631
|
for i, val in enumerate(idx):
|
|
563
632
|
self.octave_bands_Pxx[i] = float(names[val])
|
|
564
|
-
mask = (self.frequency_Pxx
|
|
565
|
-
|
|
633
|
+
mask = (self.frequency_Pxx
|
|
634
|
+
>= f_lower[val]) & (self.frequency_Pxx < f_upper[val])
|
|
635
|
+
self.octave_bands_Pxx_power[i] = np.sum(self.Pxx[mask] *
|
|
636
|
+
delta_f)
|
|
566
637
|
|
|
567
638
|
if self.amplitude is not None:
|
|
568
639
|
# determine the frequency bands
|
|
569
|
-
idx = np.where((f_lower < np.max(self.frequency))
|
|
640
|
+
idx = np.where((f_lower < np.max(self.frequency))
|
|
641
|
+
& (f_upper > np.min(self.frequency)))[0]
|
|
570
642
|
if len(idx) == 0:
|
|
571
|
-
raise ValueError(
|
|
643
|
+
raise ValueError(
|
|
644
|
+
"No frequency bands found in the FFT. Please check the frequency bands."
|
|
645
|
+
)
|
|
572
646
|
|
|
573
647
|
# compute the FFT for the bands
|
|
574
648
|
self.octave_bands_fft = np.zeros(len(idx))
|
|
@@ -576,5 +650,7 @@ class TimeSignalProcessing:
|
|
|
576
650
|
|
|
577
651
|
for i, val in enumerate(idx):
|
|
578
652
|
self.octave_bands_fft[i] = float(names[val])
|
|
579
|
-
mask = (self.frequency >= f_lower[val]) & (self.frequency
|
|
580
|
-
|
|
653
|
+
mask = (self.frequency >= f_lower[val]) & (self.frequency
|
|
654
|
+
< f_upper[val])
|
|
655
|
+
self.octave_bands_fft_power[i] = np.sum(
|
|
656
|
+
self.amplitude[mask]**2)
|
{signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SignalProcessingTools
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Signal processing tools
|
|
5
5
|
Author: attr: SignalProcessingTools.__author__
|
|
6
6
|
Author-email: bruno.zuadacoelho@deltares.nl, aron.noordam@deltares.nl
|
|
@@ -13,8 +13,9 @@ Requires-Dist: matplotlib>=3.10
|
|
|
13
13
|
Requires-Dist: numpy>=2.2
|
|
14
14
|
Requires-Dist: scipy>=1.15
|
|
15
15
|
Provides-Extra: testing
|
|
16
|
-
Requires-Dist: pytest>=8.
|
|
17
|
-
Requires-Dist: tox>=4.
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "testing"
|
|
17
|
+
Requires-Dist: tox>=4.13; extra == "testing"
|
|
18
|
+
Requires-Dist: pre_commit>=4.2.0; extra == "testing"
|
|
18
19
|
|
|
19
20
|
# SignalProcessingTools
|
|
20
21
|
|
|
@@ -32,12 +32,16 @@ def test_track_quality_index(test_data):
|
|
|
32
32
|
with open("./tests/data/track_alignment_results.txt", "r") as fi:
|
|
33
33
|
data = json.load(fi)
|
|
34
34
|
|
|
35
|
-
assert np.allclose(sig.coordinates,
|
|
35
|
+
assert np.allclose(sig.coordinates,
|
|
36
|
+
data["coordinates"],
|
|
37
|
+
rtol=1e-5,
|
|
38
|
+
atol=1e-8)
|
|
36
39
|
assert np.allclose(sig.d0, data["D0"], rtol=1e-5, atol=1e-8)
|
|
37
40
|
assert np.allclose(sig.d1, data["D1"], rtol=1e-5, atol=1e-8)
|
|
38
41
|
assert np.allclose(sig.d2, data["D2"], rtol=1e-5, atol=1e-8)
|
|
39
42
|
assert np.allclose(sig.d3, data["D3"], rtol=1e-5, atol=1e-8)
|
|
40
43
|
|
|
44
|
+
|
|
41
45
|
def test_Hmax(test_data):
|
|
42
46
|
"""
|
|
43
47
|
Test the Hmax function
|
|
@@ -48,19 +52,22 @@ def test_Hmax(test_data):
|
|
|
48
52
|
|
|
49
53
|
sig.compute_Hmax()
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
rms_band_matlab = np.array([
|
|
56
|
+
2087.55705457531, 1139.29553877343, 793.047457091564, 548.095561097181,
|
|
57
|
+
648.015015656438, 521.608760233929, 790.563948013129, 886.097705321285,
|
|
58
|
+
1342.44544888507
|
|
59
|
+
])
|
|
60
|
+
h_max_matlab = np.array([
|
|
61
|
+
6557.20346546820, 3764.49040821894, 2505.54201229720, 1727.21064286318,
|
|
62
|
+
1208.73808675389, 1182.77849807824, 1996.50964604940, 2682.54554936051,
|
|
63
|
+
3681.41028314498
|
|
64
|
+
])
|
|
65
|
+
h_max_dx_matlab = np.array([
|
|
66
|
+
293.263231128877, 783.641358934000, 1300.63422139907, 524.680132894043,
|
|
67
|
+
139.978582308885, 583.726452878631, 662.410999843909, 1240.10908108192,
|
|
68
|
+
1127.56942541745
|
|
69
|
+
])
|
|
63
70
|
|
|
64
71
|
assert np.allclose(sig.rms_bands, rms_band_matlab, rtol=1e-3, atol=1e-8)
|
|
65
72
|
assert np.allclose(sig.max_fast, h_max_matlab, rtol=1e-3, atol=1e-8)
|
|
66
|
-
assert np.allclose(sig.max_fast_Dx, h_max_dx_matlab, rtol=1e-3, atol=1e-8)
|
|
73
|
+
assert np.allclose(sig.max_fast_Dx, h_max_dx_matlab, rtol=1e-3, atol=1e-8)
|
|
@@ -4,9 +4,11 @@ import numpy as np
|
|
|
4
4
|
from SignalProcessingTools.time_signal import TimeSignalProcessing, IntegrationRules, Windows
|
|
5
5
|
|
|
6
6
|
TOL = 3e-3
|
|
7
|
+
FLOAT_TOL = 1e-12
|
|
7
8
|
FREQ = 6
|
|
8
9
|
AMP = 1.75
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
@pytest.fixture
|
|
11
13
|
def test_data():
|
|
12
14
|
"""
|
|
@@ -18,6 +20,7 @@ def test_data():
|
|
|
18
20
|
y_noise = y + 0.01 * np.sin(120 * x)
|
|
19
21
|
return x, y, y_noise
|
|
20
22
|
|
|
23
|
+
|
|
21
24
|
def test_fft(test_data):
|
|
22
25
|
"""
|
|
23
26
|
Test the fft function
|
|
@@ -32,10 +35,10 @@ def test_fft(test_data):
|
|
|
32
35
|
assert len(sig.signal) == 50001
|
|
33
36
|
assert len(sig.time) == 50001
|
|
34
37
|
|
|
35
|
-
np.testing.assert_almost_equal(sig.frequency[np.argmax(sig.amplitude)],
|
|
38
|
+
np.testing.assert_almost_equal(sig.frequency[np.argmax(sig.amplitude)],
|
|
39
|
+
FREQ, 2)
|
|
36
40
|
np.testing.assert_almost_equal(np.max(sig.amplitude), AMP, 2)
|
|
37
41
|
|
|
38
|
-
|
|
39
42
|
# results full representation
|
|
40
43
|
sig.fft(half_representation=False)
|
|
41
44
|
|
|
@@ -43,16 +46,23 @@ def test_fft(test_data):
|
|
|
43
46
|
assert len(sig.signal) == 50001
|
|
44
47
|
assert len(sig.time) == 50001
|
|
45
48
|
|
|
46
|
-
np.testing.assert_almost_equal(
|
|
47
|
-
|
|
49
|
+
np.testing.assert_almost_equal(
|
|
50
|
+
sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])],
|
|
51
|
+
FREQ, 2)
|
|
52
|
+
np.testing.assert_almost_equal(
|
|
53
|
+
np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), AMP / 2, 2)
|
|
48
54
|
|
|
49
55
|
# example with spectral leakage
|
|
50
56
|
y = 1.75 * np.sin(2.675 * 2 * np.pi * x)
|
|
51
57
|
sig = TimeSignalProcessing(x, y)
|
|
52
58
|
sig.fft()
|
|
53
59
|
|
|
54
|
-
np.testing.assert_almost_equal(
|
|
55
|
-
|
|
60
|
+
np.testing.assert_almost_equal(
|
|
61
|
+
sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])],
|
|
62
|
+
2.675, 2)
|
|
63
|
+
np.testing.assert_almost_equal(
|
|
64
|
+
np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), 1.137, 2)
|
|
65
|
+
|
|
56
66
|
|
|
57
67
|
def test_fft_nb_points(test_data):
|
|
58
68
|
"""
|
|
@@ -65,10 +75,11 @@ def test_fft_nb_points(test_data):
|
|
|
65
75
|
sig.fft(nb_points=2**18)
|
|
66
76
|
|
|
67
77
|
# check if signal lenght has been adapted to window size
|
|
68
|
-
assert len(sig.amplitude) == (2**18)/2
|
|
69
|
-
assert len(sig.frequency) == (2**18)/2
|
|
78
|
+
assert len(sig.amplitude) == (2**18) / 2
|
|
79
|
+
assert len(sig.frequency) == (2**18) / 2
|
|
70
80
|
|
|
71
|
-
np.testing.assert_almost_equal(sig.frequency[np.argmax(sig.amplitude)],
|
|
81
|
+
np.testing.assert_almost_equal(sig.frequency[np.argmax(sig.amplitude)],
|
|
82
|
+
FREQ, 2)
|
|
72
83
|
np.testing.assert_almost_equal(np.max(sig.amplitude), AMP, 2)
|
|
73
84
|
|
|
74
85
|
# results full representation
|
|
@@ -78,8 +89,11 @@ def test_fft_nb_points(test_data):
|
|
|
78
89
|
assert len(sig.amplitude) == 2**18
|
|
79
90
|
assert len(sig.frequency) == 2**18
|
|
80
91
|
|
|
81
|
-
np.testing.assert_almost_equal(
|
|
82
|
-
|
|
92
|
+
np.testing.assert_almost_equal(
|
|
93
|
+
sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])],
|
|
94
|
+
FREQ, 2)
|
|
95
|
+
np.testing.assert_almost_equal(
|
|
96
|
+
np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), AMP / 2, 2)
|
|
83
97
|
|
|
84
98
|
|
|
85
99
|
def test_fft_window(test_data):
|
|
@@ -89,7 +103,9 @@ def test_fft_window(test_data):
|
|
|
89
103
|
x, y, _ = test_data
|
|
90
104
|
|
|
91
105
|
# assert that sig raises a Value error
|
|
92
|
-
with pytest.raises(
|
|
106
|
+
with pytest.raises(
|
|
107
|
+
ValueError,
|
|
108
|
+
match="When using a window the `window_size` must be specified"):
|
|
93
109
|
sig = TimeSignalProcessing(x, y, window=Windows.HAMMING)
|
|
94
110
|
|
|
95
111
|
# test with window - half representation
|
|
@@ -100,8 +116,11 @@ def test_fft_window(test_data):
|
|
|
100
116
|
assert len(sig.signal) == 54000
|
|
101
117
|
assert len(sig.time) == 54000
|
|
102
118
|
|
|
103
|
-
np.testing.assert_almost_equal(
|
|
104
|
-
|
|
119
|
+
np.testing.assert_almost_equal(
|
|
120
|
+
sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude))])],
|
|
121
|
+
FREQ, 2)
|
|
122
|
+
np.testing.assert_almost_equal(
|
|
123
|
+
np.max(sig.amplitude[:int(len(sig.amplitude))]), AMP, 2)
|
|
105
124
|
|
|
106
125
|
# full representation
|
|
107
126
|
sig.fft(half_representation=False)
|
|
@@ -110,8 +129,11 @@ def test_fft_window(test_data):
|
|
|
110
129
|
assert len(sig.signal) == 54000
|
|
111
130
|
assert len(sig.time) == 54000
|
|
112
131
|
|
|
113
|
-
np.testing.assert_almost_equal(
|
|
114
|
-
|
|
132
|
+
np.testing.assert_almost_equal(
|
|
133
|
+
sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude))])],
|
|
134
|
+
FREQ, 2)
|
|
135
|
+
np.testing.assert_almost_equal(
|
|
136
|
+
np.max(sig.amplitude[:int(len(sig.amplitude))]), AMP / 2, 2)
|
|
115
137
|
|
|
116
138
|
|
|
117
139
|
def test_ifft(test_data):
|
|
@@ -125,7 +147,11 @@ def test_ifft(test_data):
|
|
|
125
147
|
sig.fft(half_representation=True)
|
|
126
148
|
|
|
127
149
|
# assert that sig raises a Value error
|
|
128
|
-
with pytest.raises(
|
|
150
|
+
with pytest.raises(
|
|
151
|
+
NotImplementedError,
|
|
152
|
+
match=
|
|
153
|
+
"Half representation not supported for inverse FFT. Please compute FFT with full representation."
|
|
154
|
+
):
|
|
129
155
|
sig.inv_fft()
|
|
130
156
|
|
|
131
157
|
# test with window - full representation
|
|
@@ -137,8 +163,9 @@ def test_ifft(test_data):
|
|
|
137
163
|
# check if signal lenght has been adapted to window size
|
|
138
164
|
assert len(sig.signal) == len(sig.signal_inv)
|
|
139
165
|
|
|
140
|
-
rmse = np.sqrt(np.sum((sig.signal - sig.signal_inv)
|
|
141
|
-
assert(rmse < TOL)
|
|
166
|
+
rmse = np.sqrt(np.sum((sig.signal - sig.signal_inv)**2) / len(y))
|
|
167
|
+
assert (rmse < TOL)
|
|
168
|
+
|
|
142
169
|
|
|
143
170
|
def test_ifft_window(test_data):
|
|
144
171
|
"""
|
|
@@ -151,7 +178,11 @@ def test_ifft_window(test_data):
|
|
|
151
178
|
sig.fft(half_representation=True)
|
|
152
179
|
|
|
153
180
|
# assert that sig raises a Value error
|
|
154
|
-
with pytest.raises(
|
|
181
|
+
with pytest.raises(
|
|
182
|
+
NotImplementedError,
|
|
183
|
+
match=
|
|
184
|
+
"Half representation not supported for inverse FFT. Please compute FFT with full representation."
|
|
185
|
+
):
|
|
155
186
|
sig.inv_fft()
|
|
156
187
|
|
|
157
188
|
# test with window - half representation
|
|
@@ -159,7 +190,9 @@ def test_ifft_window(test_data):
|
|
|
159
190
|
sig.fft(half_representation=False)
|
|
160
191
|
|
|
161
192
|
# assert that sig raises a Value error
|
|
162
|
-
with pytest.raises(
|
|
193
|
+
with pytest.raises(
|
|
194
|
+
ValueError,
|
|
195
|
+
match="Cannot perform inverse FFT on the windowed signal."):
|
|
163
196
|
sig.inv_fft()
|
|
164
197
|
|
|
165
198
|
|
|
@@ -174,13 +207,18 @@ def test_int(test_data):
|
|
|
174
207
|
omega = 2 * np.pi * FREQ
|
|
175
208
|
int_sig = -AMP * np.cos(omega * x) / omega
|
|
176
209
|
|
|
177
|
-
rmse = np.sqrt(np.sum((sig.signal - int_sig)
|
|
178
|
-
assert(rmse < TOL)
|
|
210
|
+
rmse = np.sqrt(np.sum((sig.signal - int_sig)**2) / len(int_sig))
|
|
211
|
+
assert (rmse < TOL)
|
|
179
212
|
|
|
180
213
|
sig = TimeSignalProcessing(x, y)
|
|
181
|
-
sig.integrate(baseline=True,
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
sig.integrate(baseline=True,
|
|
215
|
+
hp=True,
|
|
216
|
+
rule=IntegrationRules.SIMPSON,
|
|
217
|
+
fpass=1,
|
|
218
|
+
n=6)
|
|
219
|
+
rmse = np.sqrt(np.sum((sig.signal - int_sig)**2) / len(int_sig))
|
|
220
|
+
assert (rmse < TOL)
|
|
221
|
+
|
|
184
222
|
|
|
185
223
|
def test_filter(test_data):
|
|
186
224
|
"""
|
|
@@ -191,15 +229,21 @@ def test_filter(test_data):
|
|
|
191
229
|
sig.filter(10, 4, type_filter="lowpass")
|
|
192
230
|
|
|
193
231
|
# compare between 200 and -200 to avoid edge effects
|
|
194
|
-
rmse = np.sqrt(
|
|
195
|
-
|
|
232
|
+
rmse = np.sqrt(
|
|
233
|
+
np.sum((sig.signal[200:-200] - y[200:-200])**2) / len(y[200:-200]))
|
|
234
|
+
assert (rmse < TOL)
|
|
235
|
+
|
|
196
236
|
|
|
197
237
|
def test_psd(test_data):
|
|
198
238
|
"""
|
|
199
239
|
Test the psd function
|
|
200
240
|
"""
|
|
201
241
|
x, y, _ = test_data
|
|
202
|
-
with pytest.raises(
|
|
242
|
+
with pytest.raises(
|
|
243
|
+
ValueError,
|
|
244
|
+
match=
|
|
245
|
+
"No window defined. Please define a window when initialising SignalProcessing."
|
|
246
|
+
):
|
|
203
247
|
sig = TimeSignalProcessing(x, y)
|
|
204
248
|
sig.psd()
|
|
205
249
|
|
|
@@ -207,14 +251,20 @@ def test_psd(test_data):
|
|
|
207
251
|
sig.psd()
|
|
208
252
|
|
|
209
253
|
# power
|
|
210
|
-
power_sinus_wave = AMP
|
|
254
|
+
power_sinus_wave = AMP**2 / 2
|
|
211
255
|
bin_width = sig.Fs / sig.window_size
|
|
212
|
-
ENBW = np.sum(sig.window
|
|
256
|
+
ENBW = np.sum(sig.window**2) / (np.sum(sig.window)**2) * sig.window_size
|
|
213
257
|
peak_psd = power_sinus_wave / (ENBW * bin_width)
|
|
214
258
|
|
|
215
|
-
np.testing.assert_almost_equal(sig.frequency_Pxx[np.argmax(sig.Pxx)], FREQ,
|
|
259
|
+
np.testing.assert_almost_equal(sig.frequency_Pxx[np.argmax(sig.Pxx)], FREQ,
|
|
260
|
+
2)
|
|
216
261
|
assert (np.abs((np.max(sig.Pxx) - peak_psd) / peak_psd) < 0.035)
|
|
217
262
|
|
|
263
|
+
# test the time interpolation
|
|
264
|
+
assert len(sig.time) == np.ceil(50001 / 4000) * 4000
|
|
265
|
+
assert ((np.diff(sig.time) - 1 / 500) < FLOAT_TOL).all()
|
|
266
|
+
|
|
267
|
+
|
|
218
268
|
def test_v_eff():
|
|
219
269
|
"""
|
|
220
270
|
Test the v_eff function
|
|
@@ -229,7 +279,7 @@ def test_v_eff():
|
|
|
229
279
|
v_eff = fi.read().splitlines()
|
|
230
280
|
v_eff = np.array([list(map(float, r.split(";"))) for r in v_eff])
|
|
231
281
|
|
|
232
|
-
time = np.linspace(0, (raw.shape[0]-1) / 500, raw.shape[0])
|
|
282
|
+
time = np.linspace(0, (raw.shape[0] - 1) / 500, raw.shape[0])
|
|
233
283
|
|
|
234
284
|
# compute veff
|
|
235
285
|
for i in range(raw.shape[1]):
|
|
@@ -237,6 +287,7 @@ def test_v_eff():
|
|
|
237
287
|
sig.v_eff_SBR()
|
|
238
288
|
np.testing.assert_almost_equal(sig.v_eff, np.array(v_eff)[:, i], 2)
|
|
239
289
|
|
|
290
|
+
|
|
240
291
|
def test_str_representation(test_data):
|
|
241
292
|
"""
|
|
242
293
|
Test the __str__ method to verify operations are tracked correctly
|
|
@@ -277,6 +328,7 @@ def test_str_representation(test_data):
|
|
|
277
328
|
assert any("Filter" in op for op in sig.operations)
|
|
278
329
|
assert any("PSD" in op for op in sig.operations)
|
|
279
330
|
|
|
331
|
+
|
|
280
332
|
def test_spectrogram(test_data):
|
|
281
333
|
"""
|
|
282
334
|
Test the spectrogram function
|
|
@@ -287,10 +339,11 @@ def test_spectrogram(test_data):
|
|
|
287
339
|
|
|
288
340
|
# check if signal lenght has been adapted to window size
|
|
289
341
|
assert sig.Sxx.shape == (301, 95)
|
|
290
|
-
assert sig.time_Sxx.shape == (95,)
|
|
291
|
-
assert sig.frequency_Sxx.shape == (301,)
|
|
342
|
+
assert sig.time_Sxx.shape == (95, )
|
|
343
|
+
assert sig.frequency_Sxx.shape == (301, )
|
|
292
344
|
|
|
293
|
-
np.testing.assert_almost_equal(
|
|
345
|
+
np.testing.assert_almost_equal(
|
|
346
|
+
sig.frequency_Sxx[np.where(sig.Sxx == np.max(sig.Sxx))[0][0]], FREQ, 0)
|
|
294
347
|
np.testing.assert_almost_equal(np.max(sig.Sxx), 1.27, 2)
|
|
295
348
|
|
|
296
349
|
# # plot spectrogram
|
|
@@ -349,7 +402,10 @@ def test_reset(test_data):
|
|
|
349
402
|
assert sig.time_Sxx is None
|
|
350
403
|
|
|
351
404
|
# Verify FFT settings are reset
|
|
352
|
-
assert sig.fft_settings == {
|
|
405
|
+
assert sig.fft_settings == {
|
|
406
|
+
"nb_points": None,
|
|
407
|
+
"half_representation": False
|
|
408
|
+
}
|
|
353
409
|
|
|
354
410
|
# Verify operations history is cleared
|
|
355
411
|
assert len(sig.operations) == 0
|
|
@@ -357,6 +413,7 @@ def test_reset(test_data):
|
|
|
357
413
|
# Verify string representation shows no operations
|
|
358
414
|
assert "No operations performed yet" in str(sig)
|
|
359
415
|
|
|
416
|
+
|
|
360
417
|
def test_one_third_octave(test_data):
|
|
361
418
|
"""
|
|
362
419
|
Test the one third octave function
|
|
@@ -364,11 +421,13 @@ def test_one_third_octave(test_data):
|
|
|
364
421
|
x, y, _ = test_data
|
|
365
422
|
|
|
366
423
|
# definition of frequencies used in the test
|
|
367
|
-
freqs_used = [
|
|
424
|
+
freqs_used = [
|
|
425
|
+
10, 12.5, 16, 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250
|
|
426
|
+
]
|
|
368
427
|
# compute the power at 20 Hz
|
|
369
|
-
f_center = 1000 * (2
|
|
370
|
-
f_max = f_center * (2
|
|
371
|
-
f_min = f_center / (2
|
|
428
|
+
f_center = 1000 * (2**((-17) / 3))
|
|
429
|
+
f_max = f_center * (2**(1 / 6))
|
|
430
|
+
f_min = f_center / (2**(1 / 6))
|
|
372
431
|
|
|
373
432
|
# test FFT
|
|
374
433
|
sig = TimeSignalProcessing(x, y)
|
|
@@ -379,7 +438,7 @@ def test_one_third_octave(test_data):
|
|
|
379
438
|
|
|
380
439
|
idx = np.where((sig.frequency >= f_min) & (sig.frequency < f_max))[0]
|
|
381
440
|
|
|
382
|
-
assert sig.octave_bands_fft_power[3] == np.sum(sig.amplitude[idx]
|
|
441
|
+
assert sig.octave_bands_fft_power[3] == np.sum(sig.amplitude[idx]**2)
|
|
383
442
|
assert sig.octave_bands_fft[3] == 20
|
|
384
443
|
|
|
385
444
|
# test PSDF
|
|
@@ -388,7 +447,8 @@ def test_one_third_octave(test_data):
|
|
|
388
447
|
sig.one_third_octave_bands()
|
|
389
448
|
assert all(sig.octave_bands_Pxx == freqs_used)
|
|
390
449
|
|
|
391
|
-
idx = np.where((sig.frequency_Pxx >= f_min)
|
|
450
|
+
idx = np.where((sig.frequency_Pxx >= f_min)
|
|
451
|
+
& (sig.frequency_Pxx < f_max))[0]
|
|
392
452
|
delta_freq = sig.frequency_Pxx[1] - sig.frequency_Pxx[0]
|
|
393
453
|
assert sig.octave_bands_Pxx_power[3] == np.sum(sig.Pxx[idx] * delta_freq)
|
|
394
454
|
assert sig.octave_bands_Pxx[3] == 20
|
|
File without changes
|
{signalprocessingtools-1.2.2 → signalprocessingtools-1.2.4}/SignalProcessingTools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|