SignalProcessingTools 1.2.2__py3-none-any.whl → 1.2.4__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.
- SignalProcessingTools/__version__.py +1 -1
- SignalProcessingTools/space_signal.py +66 -31
- SignalProcessingTools/time_signal.py +140 -64
- {signalprocessingtools-1.2.2.dist-info → signalprocessingtools-1.2.4.dist-info}/METADATA +4 -3
- signalprocessingtools-1.2.4.dist-info/RECORD +8 -0
- {signalprocessingtools-1.2.2.dist-info → signalprocessingtools-1.2.4.dist-info}/WHEEL +1 -1
- signalprocessingtools-1.2.2.dist-info/RECORD +0 -8
- {signalprocessingtools-1.2.2.dist-info → signalprocessingtools-1.2.4.dist-info}/top_level.txt +0 -0
|
@@ -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)))
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
SignalProcessingTools/__init__.py,sha256=nMXg9GFsk8gCcyBnh1MAeUPvo4Zcuv3sdwGHZrxfpPE,36
|
|
2
|
+
SignalProcessingTools/__version__.py,sha256=V_BrxIpTLkU66higGwyPgbY3Y9GEmDzhELMqlxzfhW0,106
|
|
3
|
+
SignalProcessingTools/space_signal.py,sha256=GOg0gLkAZWDWqZiOMd9GJLF8pLuEvk21ub_PtD21AWs,8303
|
|
4
|
+
SignalProcessingTools/time_signal.py,sha256=yeWnCW6CGOZGfhE9vv1OSSeroa14vdVP1RCpwlWz4EU,24324
|
|
5
|
+
signalprocessingtools-1.2.4.dist-info/METADATA,sha256=sWUWxUJx3ETAvLfSJ0NHUIwXQhh6yh6uO34xga0efHg,4485
|
|
6
|
+
signalprocessingtools-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
signalprocessingtools-1.2.4.dist-info/top_level.txt,sha256=BzkLqG2CNDmcYLCI_-5cmnV91uDRfxNGotNulq6m4pk,22
|
|
8
|
+
signalprocessingtools-1.2.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
SignalProcessingTools/__init__.py,sha256=nMXg9GFsk8gCcyBnh1MAeUPvo4Zcuv3sdwGHZrxfpPE,36
|
|
2
|
-
SignalProcessingTools/__version__.py,sha256=Lucz6_GwEnJ-LUvdGCXZC1n22nCjuvH9BJ1r985KRbI,106
|
|
3
|
-
SignalProcessingTools/space_signal.py,sha256=Pq_S2EimxSIjBWdz7pkaNDypOFdI6YXZE7PrsVbwueg,7572
|
|
4
|
-
SignalProcessingTools/time_signal.py,sha256=GAlA34B6qjkuvwsANixYS8rC7VR4jedpyg3IoMwIwXM,22505
|
|
5
|
-
signalprocessingtools-1.2.2.dist-info/METADATA,sha256=oBGbc0RGlUcw1j0S2ZNVyygKjmZsdWgdkVQzrBLMVp0,4432
|
|
6
|
-
signalprocessingtools-1.2.2.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
|
7
|
-
signalprocessingtools-1.2.2.dist-info/top_level.txt,sha256=BzkLqG2CNDmcYLCI_-5cmnV91uDRfxNGotNulq6m4pk,22
|
|
8
|
-
signalprocessingtools-1.2.2.dist-info/RECORD,,
|
{signalprocessingtools-1.2.2.dist-info → signalprocessingtools-1.2.4.dist-info}/top_level.txt
RENAMED
|
File without changes
|