SignalProcessingTools 1.2.3__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.
Files changed (15) hide show
  1. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/PKG-INFO +2 -1
  2. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools/__version__.py +1 -1
  3. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools/space_signal.py +66 -31
  4. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools/time_signal.py +140 -64
  5. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/PKG-INFO +2 -1
  6. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/requires.txt +1 -0
  7. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/setup.cfg +1 -0
  8. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/tests/test_space_signal.py +21 -14
  9. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/tests/test_time_signal.py +103 -43
  10. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/ReadMe.md +0 -0
  11. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools/__init__.py +0 -0
  12. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/SOURCES.txt +0 -0
  13. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/dependency_links.txt +0 -0
  14. {signalprocessingtools-1.2.3 → signalprocessingtools-1.2.4}/SignalProcessingTools.egg-info/top_level.txt +0 -0
  15. {signalprocessingtools-1.2.3 → 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
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
@@ -15,6 +15,7 @@ Requires-Dist: scipy>=1.15
15
15
  Provides-Extra: testing
16
16
  Requires-Dist: pytest>=8.0; extra == "testing"
17
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
 
@@ -1,3 +1,3 @@
1
1
  __title__ = "SignalProcessingTools"
2
- __version__ = "1.2.3"
2
+ __version__ = "1.2.4"
3
3
  __author__ = "Bruno Zuada Coelho, Aron Noordam"
@@ -8,7 +8,11 @@ class SpaceSignalProcessing:
8
8
  """
9
9
  SignalProcessing class for processing signals in space.
10
10
  """
11
- def __init__(self, x: npt.NDArray[np.float64], values: npt.NDArray[np.float64], Fs: Optional[float] = None):
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, self.signal_raw, Fs=self.fs)
57
- sig.filter([1/5., 1.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
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.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
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.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
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.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
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 = [[.08, .10],
86
- [.10, .126],
87
- [.126, .16],
88
- [.16, .20],
89
- [.20, .253],
90
- [.253, .32],
91
- [.32, .40],
92
- [.40, .50],
93
- [.50, .63],
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, 2] # number of times that each frequency band is derived
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(np.max([2 ** (np.ceil(np.log2(len(self.signal)))), nb_fft_min]))
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(self.coordinates, self.coordinates[-1] + (self.coordinates[1] - self.coordinates[0]))
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, signal, Fs=self.fs, window=Windows.HAMMING,
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, one_third_octave_bands, derivative)
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
- def __rms_effective(self, frequency: npt.NDArray[np.float64], Pxx: npt.NDArray[np.float64],
133
- one_third_octave_bands: List[Tuple[float, float]], derivative: List[int]):
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]) ** (2 * derivative[i]) * Pxx[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, float]], derivative: List[int]):
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, self.signal, Fs=self.fs)
174
- sig.filter(np.array(band), N=3, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
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(np.convolve(new_signal**2, g) * dx / tau)
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("When using a window the `window_size` must be specified")
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(f"Window type {window} not supported. Available types: {list(Windows)}")
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(f"Window length ({window_size}) cannot be greater than signal length ({signal_length}).")
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(np.ceil((signal_length / window_size) * 2 - 2))
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(self.signal, np.zeros(window_size - (signal_length % window_size)))
113
- self.time = np.append(self.time, np.zeros(window_size - (signal_length % window_size)))
114
- self.operations.append(f"Signal padded with zeros (original length: {signal_length}, new length: {len(self.signal)})")
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, size: int) -> npt.NDArray[np.float64]:
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(np.mean(np.exp(1j * np.angle(spectrum_w)), axis=1))
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 = {"nb_points": nfft,
253
- "half_representation": half_representation,
254
- "odd_length": odd_length}
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("Cannot perform inverse FFT on the windowed signal.")
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(np.ones(len(spectrum)) * 1 / self.Fs) - 1 / self.Fs
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" + (" with windowing" if self.use_window else ""))
302
-
303
- def integrate(self, rule: IntegrationRules = IntegrationRules.TRAPEZOID,
304
- baseline: bool = False, moving: bool = False, hp: bool = False, ini_cond: float = 0.,
305
- fpass: float = 0.5, n: int = 6):
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, self.time, initial=ini_cond)
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, x=self.time, initial=ini_cond)
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(f"highpass filter (cutoff: {fpass} Hz, order: {n})")
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
- def filter(self, Fpass: float, N: int, filter_design: FilterDesign = FilterDesign.ELLIPTIC,
355
- type_filter: str = "lowpass", rp: float = 0.01, rs: int = 60):
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, rp, rs, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
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, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
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, rp, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
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(f"Filter ({type_filter}, cutoff: {Fpass} Hz, order: {N})")
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("Detrend method not supported. Available methods: ['linear', False]")
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("No window defined. Please define a window when initialising SignalProcessing.")
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(self.signal, fs=self.Fs, nperseg=self.window_size, nfft=nfft,
420
- window=self.window_type.value, scaling='density', detrend=detrend)
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(f"PSD (window: {self.window_type.name}, size: {self.window_size})")
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 # Reference frequency [Hz]
501
+ f0 = 5.6 # Reference frequency [Hz]
445
502
 
446
503
  # Handle even/odd signal length for FFT
447
- if self.signal.shape[0] % 2 != 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) ** 2))
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( np.convolve(v_eff**2, g) * (1 / self.Fs) /tau)
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, fs=self.Fs, window=self.window_type.value,
518
- nperseg=self.window_size, noverlap=self.window_size // 8)
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(f"Spectrogram (nperseg: {self.window_size}, noverlap: {self.window_size // 8})")
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", "80", "100", "125", "160", "200", "250", "315", "400",
538
- "500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3.150", "4000", "5000", "6300", "8000",
539
- "10000", "12500", "16000", "20000")
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 ** (np.arange(initial_frequency_band_number, final_frequency_band_number) / 3))
543
- f_upper = f_centre * (2 ** (1 / 6))
544
- f_lower = f_centre / (2 ** (1 / 6))
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("No PSD nor FFT computed. Please compute either first.")
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)) & (f_upper > np.min(self.frequency_Pxx)))[0]
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("No frequency bands found in the PSD. Please check the frequency bands.")
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 >= f_lower[val]) & (self.frequency_Pxx < f_upper[val])
565
- self.octave_bands_Pxx_power[i] = np.sum(self.Pxx[mask] * delta_f)
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)) & (f_upper > np.min(self.frequency)))[0]
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("No frequency bands found in the FFT. Please check the frequency bands.")
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 < f_upper[val])
580
- self.octave_bands_fft_power[i] = np.sum(self.amplitude[mask]**2)
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
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
@@ -15,6 +15,7 @@ Requires-Dist: scipy>=1.15
15
15
  Provides-Extra: testing
16
16
  Requires-Dist: pytest>=8.0; extra == "testing"
17
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
 
@@ -5,3 +5,4 @@ scipy>=1.15
5
5
  [testing]
6
6
  pytest>=8.0
7
7
  tox>=4.13
8
+ pre_commit>=4.2.0
@@ -24,6 +24,7 @@ python_requires = >=3.10
24
24
  testing =
25
25
  pytest>=8.0
26
26
  tox>=4.13
27
+ pre_commit>=4.2.0
27
28
 
28
29
  [egg_info]
29
30
  tag_build =
@@ -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, data["coordinates"], rtol=1e-5, atol=1e-8)
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
- rms_band_matlab = np.array([2087.55705457531, 1139.29553877343, 793.047457091564, 548.095561097181,
53
- 648.015015656438, 521.608760233929, 790.563948013129, 886.097705321285,
54
- 1342.44544888507])
55
- h_max_matlab = np.array([6557.20346546820, 3764.49040821894, 2505.54201229720, 1727.21064286318,
56
- 1208.73808675389, 1182.77849807824, 1996.50964604940, 2682.54554936051,
57
- 3681.41028314498])
58
- h_max_dx_matlab = np.array([293.263231128877, 783.641358934000, 1300.63422139907, 524.680132894043,
59
- 139.978582308885, 583.726452878631, 662.410999843909, 1240.10908108192,
60
- 1127.56942541745])
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)], FREQ, 2)
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(sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])], FREQ, 2)
47
- np.testing.assert_almost_equal(np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), AMP / 2, 2)
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(sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])], 2.675, 2)
55
- np.testing.assert_almost_equal(np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), 1.137, 2)
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)], FREQ, 2)
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(sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude) / 2)])], FREQ, 2)
82
- np.testing.assert_almost_equal(np.max(sig.amplitude[:int(len(sig.amplitude) / 2)]), AMP / 2, 2)
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(ValueError, match="When using a window the `window_size` must be specified"):
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(sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude))])], FREQ, 2)
104
- np.testing.assert_almost_equal(np.max(sig.amplitude[:int(len(sig.amplitude))]), AMP, 2)
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(sig.frequency[np.argmax(sig.amplitude[:int(len(sig.amplitude))])], FREQ, 2)
114
- np.testing.assert_almost_equal(np.max(sig.amplitude[:int(len(sig.amplitude))]), AMP / 2, 2)
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(NotImplementedError, match="Half representation not supported for inverse FFT. Please compute FFT with full representation."):
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) ** 2) / len(y))
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(NotImplementedError, match="Half representation not supported for inverse FFT. Please compute FFT with full representation."):
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(ValueError, match="Cannot perform inverse FFT on the windowed signal."):
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) ** 2) / len(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, hp=True, rule=IntegrationRules.SIMPSON, fpass=1, n=6)
182
- rmse = np.sqrt(np.sum((sig.signal - int_sig) ** 2) / len(int_sig))
183
- assert(rmse < TOL)
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(np.sum((sig.signal[200:-200] - y[200:-200]) ** 2) / len(y[200:-200]))
195
- assert(rmse < TOL)
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(ValueError, match="No window defined. Please define a window when initialising SignalProcessing."):
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 ** 2 / 2
254
+ power_sinus_wave = AMP**2 / 2
211
255
  bin_width = sig.Fs / sig.window_size
212
- ENBW = np.sum(sig.window ** 2) / (np.sum(sig.window)**2) * sig.window_size
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, 2)
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(sig.frequency_Sxx[np.where(sig.Sxx==np.max(sig.Sxx))[0][0]], FREQ, 0)
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 == {"nb_points": None, "half_representation": False}
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 = [10, 12.5, 16, 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250]
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 ** ((-17) / 3))
370
- f_max = f_center * (2 ** (1 / 6))
371
- f_min = f_center / (2 ** (1 / 6))
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] ** 2)
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) & (sig.frequency_Pxx < f_max))[0]
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