SignalProcessingTools 1.2.0__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/__init__.py +1 -0
- SignalProcessingTools/__version__.py +3 -0
- SignalProcessingTools/space_signal.py +189 -0
- SignalProcessingTools/time_signal.py +572 -0
- signalprocessingtools-1.2.0.dist-info/METADATA +157 -0
- signalprocessingtools-1.2.0.dist-info/RECORD +8 -0
- signalprocessingtools-1.2.0.dist-info/WHEEL +5 -0
- signalprocessingtools-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .__version__ import __version__
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from typing import Optional, List, Tuple
|
|
2
|
+
import numpy as np
|
|
3
|
+
import numpy.typing as npt
|
|
4
|
+
from .time_signal import TimeSignalProcessing, FilterDesign, Windows
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SpaceSignalProcessing:
|
|
8
|
+
"""
|
|
9
|
+
SignalProcessing class for processing signals in space.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, x: npt.NDArray[np.float64], values: npt.NDArray[np.float64], Fs: Optional[float] = None):
|
|
12
|
+
"""
|
|
13
|
+
Initializes the ProcessSignal object.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
:param x (npt.NDArray[np.float64]): coordinates of the signal
|
|
18
|
+
:param values (npt.NDArray[np.float64]): signal values
|
|
19
|
+
:param Fs (Optional[float]): sampling frequency of the signal (optional: default None)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
self.coordinates = x
|
|
23
|
+
self.signal_raw = values
|
|
24
|
+
self.n_points = values.shape[0]
|
|
25
|
+
|
|
26
|
+
# acquisition frequency
|
|
27
|
+
if Fs is None:
|
|
28
|
+
self.fs = int(np.ceil(1 / np.mean(np.diff(x))))
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
self.fs = Fs
|
|
32
|
+
|
|
33
|
+
# track quality indexes
|
|
34
|
+
self.d0 = None
|
|
35
|
+
self.d1 = None
|
|
36
|
+
self.d2 = None
|
|
37
|
+
self.d3 = None
|
|
38
|
+
# track descriptors
|
|
39
|
+
self.rms_bands = None
|
|
40
|
+
self.max_fast = None
|
|
41
|
+
self.max_fast_Dx = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compute_track_longitudinal_levels(self):
|
|
45
|
+
"""
|
|
46
|
+
Computes the track longitudinal levels, following EN 13848-1:2006.
|
|
47
|
+
|
|
48
|
+
The method computes the D0, D1, D2, and D3 components of the signal.
|
|
49
|
+
It uses the following frequency bands:
|
|
50
|
+
- D0: 1m < lambda <= 5m (1/5 Hz < f <= 1 Hz)
|
|
51
|
+
- D1: 3m < lambda <= 25m (1/25 Hz < f <= 1/3 Hz)
|
|
52
|
+
- D2: 25m < lambda <= 70m (1/70 Hz < f <= 1/25 Hz)
|
|
53
|
+
- D3: 70m < lambda <= 150m (1/150 Hz < f <= 1/70 Hz)
|
|
54
|
+
"""
|
|
55
|
+
|
|
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)
|
|
58
|
+
self.d0 = sig.signal
|
|
59
|
+
|
|
60
|
+
sig.reset()
|
|
61
|
+
sig.filter([1/25., 1/3.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
|
|
62
|
+
self.d1 = sig.signal
|
|
63
|
+
sig.reset()
|
|
64
|
+
|
|
65
|
+
sig.filter([1/70., 1/25.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
|
|
66
|
+
self.d2 = sig.signal
|
|
67
|
+
sig.reset()
|
|
68
|
+
|
|
69
|
+
sig.filter([1/150., 1/70.], 4, type_filter="bandpass", filter_design=FilterDesign.BUTTERWORTH)
|
|
70
|
+
self.d3 = sig.signal
|
|
71
|
+
sig.reset()
|
|
72
|
+
|
|
73
|
+
def compute_Hmax(self, convert_m2mm: bool = True):
|
|
74
|
+
"""
|
|
75
|
+
Computes the descriptor Hmax and Hrms according to Zandberg et al. (2022)
|
|
76
|
+
'Deriving parameters for the characterisation of the railway track quality in
|
|
77
|
+
relation to environmental vibration'
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
:param convert_m2mm (optional, default = True): if True, converts the results from m to mm
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# 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
|
+
|
|
96
|
+
|
|
97
|
+
# setting for the processing
|
|
98
|
+
self.DXmaxFast = 1
|
|
99
|
+
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
|
|
101
|
+
|
|
102
|
+
# RMS of the square root of the power spectral density
|
|
103
|
+
self.rms_bands = np.zeros(len(one_third_octave_bands))
|
|
104
|
+
# maximum effective value over the entire signal
|
|
105
|
+
self.max_fast = np.zeros(len(one_third_octave_bands))
|
|
106
|
+
# maximum effective value over the length Dx
|
|
107
|
+
self.max_fast_Dx = np.zeros(len(one_third_octave_bands))
|
|
108
|
+
|
|
109
|
+
# convert the signal from m to mm
|
|
110
|
+
if convert_m2mm:
|
|
111
|
+
self.signal = self.signal_raw * 1000
|
|
112
|
+
|
|
113
|
+
# compute the power spectral density
|
|
114
|
+
n_fft = int(np.max([2 ** (np.ceil(np.log2(len(self.signal)))), nb_fft_min]))
|
|
115
|
+
# if signal is odd length, add a zero to make it even
|
|
116
|
+
if len(self.signal) % 2 != 0:
|
|
117
|
+
signal = np.append(self.signal, 0)
|
|
118
|
+
coordinates = np.append(self.coordinates, self.coordinates[-1] + (self.coordinates[1] - self.coordinates[0]))
|
|
119
|
+
else:
|
|
120
|
+
signal = self.signal
|
|
121
|
+
coordinates = self.coordinates
|
|
122
|
+
sig = TimeSignalProcessing(coordinates, signal, Fs=self.fs, window=Windows.HAMMING,
|
|
123
|
+
window_size=len(signal))
|
|
124
|
+
sig.psd(nb_points=n_fft, detrend=False)
|
|
125
|
+
|
|
126
|
+
# compute the rsm psd
|
|
127
|
+
self.__rms_effective(sig.frequency_Pxx, sig.Pxx, one_third_octave_bands, derivative)
|
|
128
|
+
# compute the effective values
|
|
129
|
+
self.__effective_values(one_third_octave_bands, derivative)
|
|
130
|
+
|
|
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]):
|
|
134
|
+
"""
|
|
135
|
+
Computes RMS square root of power spectral density
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
:param frequency (npt.NDArray[np.float64]): frequency vector
|
|
140
|
+
:param Pxx (npt.NDArray[np.float64]): power spectral density
|
|
141
|
+
:param one_third_octave_bands (List[Tuple[float, float]]): frequency bands
|
|
142
|
+
:param derivative (List[int]): derivative order
|
|
143
|
+
"""
|
|
144
|
+
# frequency step
|
|
145
|
+
delta_f = frequency[1] - frequency[0]
|
|
146
|
+
|
|
147
|
+
# compute the rms value at each frequency band
|
|
148
|
+
for i, band in enumerate(one_third_octave_bands):
|
|
149
|
+
# find indexes where the bands exist
|
|
150
|
+
idx = np.where((frequency >= band[0]) & (frequency < band[1]))[0]
|
|
151
|
+
Pxx[idx] = (2 * np.pi * frequency[idx]) ** (2 * derivative[i]) * Pxx[idx]
|
|
152
|
+
self.rms_bands[i] = np.sqrt(np.sum(Pxx[idx] * delta_f))
|
|
153
|
+
|
|
154
|
+
def __effective_values(self, one_third_octave_bands: List[Tuple[float, float]], derivative: List[int]):
|
|
155
|
+
"""
|
|
156
|
+
Computes the effective values of the signal
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
:param one_third_octave_bands (List[Tuple[float, float]]): frequency bands
|
|
161
|
+
:param derivative (List[int]): derivative order
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
n = 4 # number of time constants
|
|
165
|
+
tau = 2 # time constant
|
|
166
|
+
|
|
167
|
+
fout = 1 / (1 - np.exp(-n))
|
|
168
|
+
|
|
169
|
+
dx = self.coordinates[1] - self.coordinates[0]
|
|
170
|
+
|
|
171
|
+
for i, band in enumerate(one_third_octave_bands):
|
|
172
|
+
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)
|
|
175
|
+
new_signal = sig.signal
|
|
176
|
+
|
|
177
|
+
while derivative_value != 0:
|
|
178
|
+
new_signal = np.diff(new_signal) / dx
|
|
179
|
+
derivative_value -= 1
|
|
180
|
+
|
|
181
|
+
ksi = np.linspace(0, n * tau, int(n * tau / dx + 1))
|
|
182
|
+
g = fout * np.exp(-ksi / tau)
|
|
183
|
+
|
|
184
|
+
convoluted_signal = np.sqrt(np.convolve(new_signal**2, g) * dx / tau)
|
|
185
|
+
self.max_fast[i] = np.max(convoluted_signal)
|
|
186
|
+
idx = np.floor((len(self.signal) - np.floor(self.DXmaxFast / dx)) / 2) + \
|
|
187
|
+
np.linspace(0, np.floor(self.DXmaxFast / dx)-1, int(np.floor(self.DXmaxFast / dx)))
|
|
188
|
+
|
|
189
|
+
self.max_fast_Dx[i] = np.max(convoluted_signal[idx.astype(int)])
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import sys
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
from scipy import integrate, signal
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilterDesign(Enum):
|
|
10
|
+
"""
|
|
11
|
+
Filter design types
|
|
12
|
+
"""
|
|
13
|
+
BUTTERWORTH = 1
|
|
14
|
+
CHEBYSHEV = 2
|
|
15
|
+
ELLIPTIC = 3
|
|
16
|
+
|
|
17
|
+
class IntegrationRules(Enum):
|
|
18
|
+
"""
|
|
19
|
+
Integration rules
|
|
20
|
+
"""
|
|
21
|
+
TRAPEZOID = 1
|
|
22
|
+
SIMPSON = 2
|
|
23
|
+
|
|
24
|
+
class Windows(Enum):
|
|
25
|
+
"""
|
|
26
|
+
Windows types
|
|
27
|
+
|
|
28
|
+
The values are the same as in `scipy.signal`. This is used for the PSD.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
HANN = 'hann'
|
|
32
|
+
HAMMING = 'hamming'
|
|
33
|
+
BLACKMAN = 'blackman'
|
|
34
|
+
RECTANGULAR = 'boxcar'
|
|
35
|
+
TRIANG = 'triang'
|
|
36
|
+
|
|
37
|
+
class TimeSignalProcessing:
|
|
38
|
+
"""
|
|
39
|
+
Signal processing class for time signals
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self,
|
|
42
|
+
time: npt.NDArray[np.float64],
|
|
43
|
+
signal: npt.NDArray[np.float64],
|
|
44
|
+
Fs: Optional[int] = None,
|
|
45
|
+
window: Optional[Windows] = None,
|
|
46
|
+
window_size: int = 0):
|
|
47
|
+
"""
|
|
48
|
+
Signal processing
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
:param time (npt.NDArray[np.float64]): Time vector
|
|
53
|
+
:param signal (npt.NDArray[np.float64]): Signal vector
|
|
54
|
+
:param Fs (Optional[int]): Acquisition frequency (optional: default None. Fs is computed based on time)
|
|
55
|
+
:param window (Optional[Windows]): Type of window to use (optional: default None - uses rectangular
|
|
56
|
+
window for entire signal)
|
|
57
|
+
:param window_size (int): Size of the window (optional: default 0 - uses signal length when window is None)
|
|
58
|
+
"""
|
|
59
|
+
self.time = time
|
|
60
|
+
self.signal = signal
|
|
61
|
+
self.signal_org = signal
|
|
62
|
+
self.frequency = None
|
|
63
|
+
self.amplitude = None
|
|
64
|
+
self.phase = None
|
|
65
|
+
self.spectrum = None
|
|
66
|
+
self.Pxx = None
|
|
67
|
+
self.frequency_Pxx = None
|
|
68
|
+
self.signal_inv = None
|
|
69
|
+
self.time_inv = None
|
|
70
|
+
self.v_eff = None
|
|
71
|
+
self.Sxx = None
|
|
72
|
+
self.frequency_Sxx = None
|
|
73
|
+
self.time_Sxx = None
|
|
74
|
+
self.fft_settings = {"nb_points": None,
|
|
75
|
+
"half_representation": False}
|
|
76
|
+
# Track operations performed on the signal
|
|
77
|
+
self.operations = []
|
|
78
|
+
|
|
79
|
+
# acquisition frequency
|
|
80
|
+
if not Fs:
|
|
81
|
+
Fs = int(np.ceil(1 / np.mean(np.diff(time))))
|
|
82
|
+
self.Fs = Fs
|
|
83
|
+
|
|
84
|
+
# windowing
|
|
85
|
+
signal_length = len(self.signal)
|
|
86
|
+
|
|
87
|
+
# When window is None, process entire signal with rectangular window
|
|
88
|
+
if window is None:
|
|
89
|
+
self.window = np.ones(signal_length)
|
|
90
|
+
self.window_size = signal_length
|
|
91
|
+
self.window_type = Windows.RECTANGULAR
|
|
92
|
+
self.nb_windows = 1
|
|
93
|
+
self.use_window = False
|
|
94
|
+
else:
|
|
95
|
+
if window_size == 0:
|
|
96
|
+
raise ValueError("When using a window the `window_size` must be specified")
|
|
97
|
+
if window_size % 2 != 0:
|
|
98
|
+
raise ValueError("Window length must be even")
|
|
99
|
+
if window not in Windows:
|
|
100
|
+
raise ValueError(f"Window type {window} not supported. Available types: {list(Windows)}")
|
|
101
|
+
if window_size > signal_length:
|
|
102
|
+
raise ValueError(f"Window length ({window_size}) cannot be greater than signal length ({signal_length}).")
|
|
103
|
+
|
|
104
|
+
self.window = self.__create_window(window, window_size)
|
|
105
|
+
self.window_size = window_size
|
|
106
|
+
self.window_type = window
|
|
107
|
+
self.nb_windows = int(np.ceil((signal_length / window_size) * 2 - 2))
|
|
108
|
+
self.use_window = True
|
|
109
|
+
|
|
110
|
+
# pad signal at the end if necessary to get full windows
|
|
111
|
+
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)})")
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
String representation of the SignalProcessing object
|
|
119
|
+
showing the current state and operations performed.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
str: A formatted string with information about the signal processing instance
|
|
124
|
+
"""
|
|
125
|
+
# Create basic signal info
|
|
126
|
+
info = [
|
|
127
|
+
f"SignalProcessing Object",
|
|
128
|
+
f"------------------------",
|
|
129
|
+
f"Signal length: {len(self.signal)} samples",
|
|
130
|
+
f"Sampling frequency: {self.Fs} Hz",
|
|
131
|
+
f"Signal duration: {self.time[-1]:.3f} seconds",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# Window information
|
|
135
|
+
info.append(f"Window type: {self.window_type.name}")
|
|
136
|
+
info.append(f"Window size: {self.window_size} samples")
|
|
137
|
+
if self.use_window:
|
|
138
|
+
info.append(f"Number of windows: {self.nb_windows}")
|
|
139
|
+
|
|
140
|
+
# Show operations that have been performed
|
|
141
|
+
if self.operations:
|
|
142
|
+
info.append("\nOperations performed:")
|
|
143
|
+
for i, op in enumerate(self.operations):
|
|
144
|
+
info.append(f"- {i + 1}. {op}")
|
|
145
|
+
else:
|
|
146
|
+
info.append("\nNo operations performed yet")
|
|
147
|
+
|
|
148
|
+
return "\n".join(info)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def __create_window(window_type: Windows, size: int) -> npt.NDArray[np.float64]:
|
|
152
|
+
"""
|
|
153
|
+
Create a window array of specified type and size
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
:param window_type (Windows): Type of window from Windows enum
|
|
158
|
+
:param size (int): Size of the window
|
|
159
|
+
:return (npt.NDArray[np.float64]): Window array of specified size
|
|
160
|
+
"""
|
|
161
|
+
if window_type == Windows.RECTANGULAR:
|
|
162
|
+
return np.ones(size)
|
|
163
|
+
elif window_type == Windows.HANN:
|
|
164
|
+
return np.hanning(size)
|
|
165
|
+
elif window_type == Windows.HAMMING:
|
|
166
|
+
return np.hamming(size)
|
|
167
|
+
elif window_type == Windows.BLACKMAN:
|
|
168
|
+
return np.blackman(size)
|
|
169
|
+
elif window_type == Windows.TRIANG:
|
|
170
|
+
return signal.triang(size)
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(f"Window type {window_type} not supported"
|
|
173
|
+
f"Available types: {list(Windows)}")
|
|
174
|
+
|
|
175
|
+
def fft(self,
|
|
176
|
+
nb_points: Optional[int] = None,
|
|
177
|
+
half_representation: bool = True):
|
|
178
|
+
"""
|
|
179
|
+
FFT of signal
|
|
180
|
+
If window is used, the FFT is computed for each window and averaged.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
:param nb_points (Optional[int]): number of points for FFT (optional: default None)
|
|
185
|
+
:param half_representation (bool): true if fft should be computed in half representation
|
|
186
|
+
(optional: default True)
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
# if window is used, set nfft to window size
|
|
190
|
+
if self.use_window:
|
|
191
|
+
nfft = self.window_size
|
|
192
|
+
sig = self.signal
|
|
193
|
+
odd_length = False
|
|
194
|
+
else:
|
|
195
|
+
# if nb_points is None: nb_points is signal length
|
|
196
|
+
if nb_points is None:
|
|
197
|
+
nfft = len(self.signal)
|
|
198
|
+
sig = self.signal
|
|
199
|
+
odd_length = False
|
|
200
|
+
|
|
201
|
+
# if length is even
|
|
202
|
+
if nfft % 2 != 0:
|
|
203
|
+
nfft = len(self.signal) + 1
|
|
204
|
+
sig = np.append(self.signal, 0.)
|
|
205
|
+
self.window = np.ones(nfft)
|
|
206
|
+
self.window_size = nfft
|
|
207
|
+
odd_length = True
|
|
208
|
+
|
|
209
|
+
# Normalize by the sum of the window samples.
|
|
210
|
+
# This compensates for the energy reduction caused by non-rectangular windows, aiming to preserve the
|
|
211
|
+
# peak amplitude of stationary sinusoids.
|
|
212
|
+
normalise_fct = np.sum(self.window)
|
|
213
|
+
|
|
214
|
+
spectrum_w = np.zeros((nfft, self.nb_windows), dtype="complex128")
|
|
215
|
+
hop_size = int(self.window_size * (1 - 0.5))
|
|
216
|
+
|
|
217
|
+
# for each window
|
|
218
|
+
for w in range(self.nb_windows):
|
|
219
|
+
|
|
220
|
+
idx_ini = w * hop_size
|
|
221
|
+
idx_end = idx_ini + self.window_size
|
|
222
|
+
|
|
223
|
+
# window signal
|
|
224
|
+
signal_w = self.window * sig[idx_ini:idx_end]
|
|
225
|
+
|
|
226
|
+
# fft window signal
|
|
227
|
+
spectrum_w[:, w] = np.fft.fft(signal_w, nfft) / normalise_fct
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
self.amplitude = np.mean(np.abs(spectrum_w), axis=1)
|
|
231
|
+
# self.phase = np.unwrap(np.angle(np.mean(spectrum_w, axis=1)))
|
|
232
|
+
self.phase = np.angle(np.mean(np.exp(1j * np.angle(spectrum_w)), axis=1))
|
|
233
|
+
|
|
234
|
+
# compute frequency
|
|
235
|
+
self.frequency = np.linspace(0, 1, nfft) * self.Fs
|
|
236
|
+
|
|
237
|
+
# half representation
|
|
238
|
+
if half_representation:
|
|
239
|
+
self.frequency = self.frequency[:int(nfft / 2)]
|
|
240
|
+
self.amplitude = 2 * self.amplitude[:int(nfft / 2)]
|
|
241
|
+
self.phase = self.phase[:int(nfft / 2)]
|
|
242
|
+
|
|
243
|
+
# FFT settings: needed to perform inverse FFT
|
|
244
|
+
self.fft_settings = {"nb_points": nfft,
|
|
245
|
+
"half_representation": half_representation,
|
|
246
|
+
"odd_length": odd_length}
|
|
247
|
+
|
|
248
|
+
# Add to operations list
|
|
249
|
+
op_info = f"FFT (points: {nfft}, half representation: {half_representation})"
|
|
250
|
+
self.operations.append(op_info)
|
|
251
|
+
|
|
252
|
+
def inv_fft(self):
|
|
253
|
+
"""
|
|
254
|
+
Inverse FFT of signal
|
|
255
|
+
|
|
256
|
+
If the signal was processed with a window during FFT,
|
|
257
|
+
the inverse FFT will also use the same windowed approach
|
|
258
|
+
with proper overlap-add reconstruction.
|
|
259
|
+
"""
|
|
260
|
+
# check if FFT was computed
|
|
261
|
+
|
|
262
|
+
if self.amplitude is None or self.phase is None:
|
|
263
|
+
raise ValueError("No FFT computed. Please compute FFT first.")
|
|
264
|
+
|
|
265
|
+
if self.fft_settings["half_representation"]:
|
|
266
|
+
raise NotImplementedError("Half representation not supported for inverse FFT. " \
|
|
267
|
+
"Please compute FFT with full representation.")
|
|
268
|
+
|
|
269
|
+
if self.use_window:
|
|
270
|
+
raise ValueError("Cannot perform inverse FFT on the windowed signal.")
|
|
271
|
+
|
|
272
|
+
# get FFT settings
|
|
273
|
+
odd_length = self.fft_settings["odd_length"]
|
|
274
|
+
|
|
275
|
+
# get FFT
|
|
276
|
+
amplitude = self.amplitude
|
|
277
|
+
phase = self.phase
|
|
278
|
+
|
|
279
|
+
# compute spectrum from amplitude and phase
|
|
280
|
+
spectrum = amplitude * np.exp(1j * phase)
|
|
281
|
+
spectrum_inv = np.fft.ifft(spectrum, len(spectrum))
|
|
282
|
+
# inverse of the FFT signal
|
|
283
|
+
self.signal_inv = np.real(spectrum_inv) * len(spectrum)
|
|
284
|
+
# time from frequency
|
|
285
|
+
self.time_inv = np.cumsum(np.ones(len(spectrum)) * 1 / self.Fs) - 1 / self.Fs
|
|
286
|
+
|
|
287
|
+
if odd_length:
|
|
288
|
+
# remove last sample
|
|
289
|
+
self.signal_inv = self.signal_inv[:-1]
|
|
290
|
+
self.time_inv = self.time_inv[:-1]
|
|
291
|
+
|
|
292
|
+
# Add to operations list
|
|
293
|
+
self.operations.append("Inverse FFT" + (" with windowing" if self.use_window else ""))
|
|
294
|
+
|
|
295
|
+
def integrate(self, rule: IntegrationRules = IntegrationRules.TRAPEZOID,
|
|
296
|
+
baseline: bool = False, moving: bool = False, hp: bool = False, ini_cond: float = 0.,
|
|
297
|
+
fpass: float = 0.5, n: int = 6):
|
|
298
|
+
"""
|
|
299
|
+
Numerical integration of signal
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
:param rule (IntegrationRules): integration rule (optional: default TRAPEZOID)
|
|
304
|
+
:param baseline (bool): base line correction (optional: default False)
|
|
305
|
+
:param moving (bool): moving average correction (optional: default False)
|
|
306
|
+
:param hp (bool): highpass filter correction at fpass (optional: default False)
|
|
307
|
+
:param ini_cond (float): initial conditions. (optional: default 0.0)
|
|
308
|
+
:param fpass (float): cut off frequency [Hz]. only used if hp=True (optional: default 0.5)
|
|
309
|
+
:param n (int): order of the filter. only used if hp=True (optional: default 6)
|
|
310
|
+
"""
|
|
311
|
+
# mean average correction
|
|
312
|
+
if moving:
|
|
313
|
+
self.signal = self.signal - np.mean(self.signal)
|
|
314
|
+
|
|
315
|
+
# integration rule
|
|
316
|
+
if rule == IntegrationRules.TRAPEZOID:
|
|
317
|
+
self.signal = integrate.cumulative_trapezoid(self.signal, self.time, initial=ini_cond)
|
|
318
|
+
elif rule == IntegrationRules.SIMPSON:
|
|
319
|
+
self.signal = integrate.cumulative_simpson(self.signal, x=self.time, initial=ini_cond)
|
|
320
|
+
else:
|
|
321
|
+
sys.exit("Integration rule not supported")
|
|
322
|
+
|
|
323
|
+
# baseline correction
|
|
324
|
+
if baseline:
|
|
325
|
+
fit = np.polyfit(self.time, self.signal, 2)
|
|
326
|
+
fit_int = np.polyval(fit, self.time)
|
|
327
|
+
self.signal = self.signal - fit_int
|
|
328
|
+
|
|
329
|
+
# high pass filter
|
|
330
|
+
if hp:
|
|
331
|
+
self.filter(fpass, n, type_filter="highpass")
|
|
332
|
+
|
|
333
|
+
# Add to operations list
|
|
334
|
+
op_details = []
|
|
335
|
+
op_details.append(f"rule: {rule.name}")
|
|
336
|
+
if baseline:
|
|
337
|
+
op_details.append("baseline correction")
|
|
338
|
+
if moving:
|
|
339
|
+
op_details.append("moving average correction")
|
|
340
|
+
if hp:
|
|
341
|
+
op_details.append(f"highpass filter (cutoff: {fpass} Hz, order: {n})")
|
|
342
|
+
|
|
343
|
+
self.operations.append(f"Integration ({', '.join(op_details)})")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def filter(self, Fpass: float, N: int, filter_design: FilterDesign = FilterDesign.ELLIPTIC,
|
|
347
|
+
type_filter: str = "lowpass", rp: float = 0.01, rs: int = 60):
|
|
348
|
+
"""
|
|
349
|
+
Filter signal
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
:param Fpass (float): cut off frequency [Hz]
|
|
354
|
+
:param N (int): order of the filter
|
|
355
|
+
:param filter_design (FilterDesign): filter design (optional: default ELLIPTIC)
|
|
356
|
+
:param type_filter (str): type of the filter (optional: default lowpass)
|
|
357
|
+
:param rp (float): maximum ripple allowed below unity gain in the passband. Specified in decibels, as a
|
|
358
|
+
positive number. (optional: default 0.01)
|
|
359
|
+
:param rs (int): minimum attenuation required in the stop band. Specified in decibels, as a positive number
|
|
360
|
+
(optional: default 60)
|
|
361
|
+
"""
|
|
362
|
+
# types allowed
|
|
363
|
+
types = ["lowpass", "highpass", "bandpass"]
|
|
364
|
+
|
|
365
|
+
# check if filter type is supported
|
|
366
|
+
if type_filter not in types:
|
|
367
|
+
sys.exit(f"ERROR: Type filter '{type_filter}' not available\n"
|
|
368
|
+
"Filter type must be in {types}")
|
|
369
|
+
|
|
370
|
+
# design filter
|
|
371
|
+
if filter_design == FilterDesign.ELLIPTIC:
|
|
372
|
+
z, p, k = signal.ellip(N, rp, rs, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
|
|
373
|
+
elif filter_design == FilterDesign.BUTTERWORTH:
|
|
374
|
+
z, p, k = signal.butter(N, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
|
|
375
|
+
elif filter_design == FilterDesign.CHEBYSHEV:
|
|
376
|
+
z, p, k = signal.cheby1(N, rp, np.array(Fpass) / (self.Fs / 2), btype=type_filter, output='zpk')
|
|
377
|
+
|
|
378
|
+
sos = signal.zpk2sos(z, p, k)
|
|
379
|
+
|
|
380
|
+
# Applies twice the filter to the signal to avoid phase distortion
|
|
381
|
+
self.signal = signal.sosfiltfilt(sos, self.signal)
|
|
382
|
+
|
|
383
|
+
# Add to operations list
|
|
384
|
+
self.operations.append(f"Filter ({type_filter}, cutoff: {Fpass} Hz, order: {N})")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def psd(self, detrend: str = "linear", nb_points: Optional[int] = None):
|
|
388
|
+
"""
|
|
389
|
+
PSD of signal
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
:param detrend (str): detrend method (optional: default linear)
|
|
394
|
+
:param nb_points (Optional[int]): number of points for FFT (optional: default None)
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
if detrend not in ["linear", False]:
|
|
398
|
+
raise ValueError("Detrend method not supported. Available methods: ['linear', False]")
|
|
399
|
+
|
|
400
|
+
# check if window is initialized
|
|
401
|
+
if not self.use_window:
|
|
402
|
+
raise ValueError("No window defined. Please define a window when initialising SignalProcessing.")
|
|
403
|
+
|
|
404
|
+
# if nb_points is None: nb_points is window length
|
|
405
|
+
if nb_points is None:
|
|
406
|
+
nfft = self.window_size
|
|
407
|
+
else:
|
|
408
|
+
nfft = nb_points
|
|
409
|
+
|
|
410
|
+
# compute PSD using Welch method
|
|
411
|
+
self.frequency_Pxx, self.Pxx = signal.welch(self.signal, fs=self.Fs, nperseg=self.window_size, nfft=nfft,
|
|
412
|
+
window=self.window_type.value, scaling='density', detrend=detrend)
|
|
413
|
+
|
|
414
|
+
# Add to operations list
|
|
415
|
+
self.operations.append(f"PSD (window: {self.window_type.name}, size: {self.window_size})")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def v_eff_SBR(self, n: int = 4, tau: float = 0.125):
|
|
419
|
+
"""
|
|
420
|
+
Compute v_eff of signal based on SBR deel B Hinder voor personen in gebouwen (2006)
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
:param n:(int) number of time constants. (optional: default 4)
|
|
425
|
+
:param tau: (float) time constant for the exponential decay (optional: default 0.125)
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
# Create exponential decay function `g` for running RMS calculation
|
|
429
|
+
fout = 1 / (1 - np.exp(-n))
|
|
430
|
+
qsi = np.linspace(0, n * tau, int(n * tau * self.Fs + 1))
|
|
431
|
+
g = fout * np.exp(-qsi / tau)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# Frequency weighting parameters
|
|
435
|
+
v0 = 1 / 1000 # Reference velocity [m/s]
|
|
436
|
+
f0 = 5.6 # Reference frequency [Hz]
|
|
437
|
+
|
|
438
|
+
# Handle even/odd signal length for FFT
|
|
439
|
+
if self.signal.shape[0] % 2 != 0:
|
|
440
|
+
nv1 = int(self.signal.shape[0] / 2 + 0.5)
|
|
441
|
+
nv2 = int(self.signal.shape[0] / 2 - 0.5)
|
|
442
|
+
else:
|
|
443
|
+
nv1 = int(self.signal.shape[0] / 2)
|
|
444
|
+
nv2 = int(self.signal.shape[0] / 2)
|
|
445
|
+
|
|
446
|
+
# Calculate frequency resolution
|
|
447
|
+
df = 1 / (1 / self.Fs * self.signal.shape[0])
|
|
448
|
+
freq = np.arange(df, (nv1 + 1) * df, df)
|
|
449
|
+
|
|
450
|
+
# Create high-pass weighting filter (human perception curve)
|
|
451
|
+
Hv = (1 / v0) * 1 / (np.sqrt(1 + (f0 / freq) ** 2))
|
|
452
|
+
Hv = np.append(0, Hv) # Add DC component
|
|
453
|
+
|
|
454
|
+
# Create low-pass filter with 50 Hz cutoff
|
|
455
|
+
cut_off_number = int(np.ceil(50 / df))
|
|
456
|
+
if cut_off_number < nv1:
|
|
457
|
+
Hv2 = np.zeros(Hv.shape[0])
|
|
458
|
+
Hv2[:cut_off_number+1] = 1
|
|
459
|
+
else:
|
|
460
|
+
Hv2 = np.ones(Hv.shape[0])
|
|
461
|
+
|
|
462
|
+
# Applies the frequency weighting functions
|
|
463
|
+
Fv = np.fft.fft(self.signal)
|
|
464
|
+
Fhv = Hv2 * Hv * Fv[:nv1+1]
|
|
465
|
+
Fv = np.append(Fhv, np.flipud(np.conj(Fhv[1:nv2])))
|
|
466
|
+
v_eff = np.real(np.fft.ifft(Fv))
|
|
467
|
+
|
|
468
|
+
# moving root-mean-square through convolution with the exponential decay function `g`
|
|
469
|
+
v_eff = np.sqrt( np.convolve(v_eff**2, g) * (1 / self.Fs) /tau)
|
|
470
|
+
|
|
471
|
+
self.v_eff = v_eff[:self.signal.shape[0]]
|
|
472
|
+
|
|
473
|
+
# Add to operations list
|
|
474
|
+
self.operations.append(f"Effective velocity (SBR) (n={n}, tau={tau})")
|
|
475
|
+
|
|
476
|
+
def reset(self):
|
|
477
|
+
"""
|
|
478
|
+
Reset signal to original signal and clear all processing results.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
# Reset signal to original
|
|
482
|
+
self.signal = self.signal_org.copy()
|
|
483
|
+
|
|
484
|
+
# Reset all processed data
|
|
485
|
+
self.frequency = None
|
|
486
|
+
self.amplitude = None
|
|
487
|
+
self.phase = None
|
|
488
|
+
self.spectrum = None
|
|
489
|
+
self.Pxx = None
|
|
490
|
+
self.frequency_Pxx = None
|
|
491
|
+
self.signal_inv = None
|
|
492
|
+
self.time_inv = None
|
|
493
|
+
self.v_eff = None
|
|
494
|
+
self.Sxx = None
|
|
495
|
+
self.frequency_Sxx = None
|
|
496
|
+
self.time_Sxx = None
|
|
497
|
+
|
|
498
|
+
# Reset FFT settings
|
|
499
|
+
self.fft_settings = {"nb_points": None, "half_representation": False}
|
|
500
|
+
|
|
501
|
+
# Clear operations history
|
|
502
|
+
self.operations = []
|
|
503
|
+
|
|
504
|
+
def spectrogram(self):
|
|
505
|
+
"""
|
|
506
|
+
Compute spectrogram of signal
|
|
507
|
+
"""
|
|
508
|
+
# compute spectrogram
|
|
509
|
+
f, t, Sxx = signal.spectrogram(self.signal, fs=self.Fs, window=self.window_type.value,
|
|
510
|
+
nperseg=self.window_size, noverlap=self.window_size // 8)
|
|
511
|
+
self.Sxx = Sxx
|
|
512
|
+
self.frequency_Sxx = f
|
|
513
|
+
self.time_Sxx = t
|
|
514
|
+
|
|
515
|
+
# Add to operations list
|
|
516
|
+
self.operations.append(f"Spectrogram (nperseg: {self.window_size}, noverlap: {self.window_size // 8})")
|
|
517
|
+
|
|
518
|
+
def one_third_octave_bands(self):
|
|
519
|
+
"""
|
|
520
|
+
Compute octave bands of the signal
|
|
521
|
+
|
|
522
|
+
It uses the base 2 calculation for the one-third octave bands, according to ISO 18405:2017.
|
|
523
|
+
|
|
524
|
+
"""
|
|
525
|
+
# ranges of the nominal one-third octave bands according to ISO 18405:2017
|
|
526
|
+
# https://en.wikipedia.org/wiki/Octave_band
|
|
527
|
+
initial_frequency_band_number = -20
|
|
528
|
+
final_frequency_band_number = 33
|
|
529
|
+
names = ("10", "12.5", "16", "20", "25", "31.5", "40", "50", "63", "80", "100", "125", "160", "200", "250", "315", "400",
|
|
530
|
+
"500", "630", "800", "1000", "1250", "1600", "2000", "2500", "3.150", "4000", "5000", "6300", "8000",
|
|
531
|
+
"10000", "12500", "16000", "20000")
|
|
532
|
+
|
|
533
|
+
# compute centre frequencies of the bands
|
|
534
|
+
f_centre = 1000 * (2 ** (np.arange(initial_frequency_band_number, final_frequency_band_number) / 3))
|
|
535
|
+
f_upper = f_centre * (2 ** (1 / 6))
|
|
536
|
+
f_lower = f_centre / (2 ** (1 / 6))
|
|
537
|
+
|
|
538
|
+
# sum the signal for the bands
|
|
539
|
+
if (self.Pxx is None) and (self.amplitude is None):
|
|
540
|
+
raise ValueError("No PSD nor FFT computed. Please compute either first.")
|
|
541
|
+
|
|
542
|
+
if self.Pxx is not None:
|
|
543
|
+
# determine the frequency bands
|
|
544
|
+
idx = np.where((f_lower < np.max(self.frequency_Pxx)) & (f_upper > np.min(self.frequency_Pxx)))[0]
|
|
545
|
+
if len(idx) == 0:
|
|
546
|
+
raise ValueError("No frequency bands found in the PSD. Please check the frequency bands.")
|
|
547
|
+
|
|
548
|
+
delta_f = self.frequency_Pxx[1] - self.frequency_Pxx[0]
|
|
549
|
+
|
|
550
|
+
# compute the PSD for the bands
|
|
551
|
+
self.octave_bands_Pxx = np.zeros(len(idx))
|
|
552
|
+
self.octave_bands_Pxx_power = np.zeros(len(idx))
|
|
553
|
+
|
|
554
|
+
for i, val in enumerate(idx):
|
|
555
|
+
self.octave_bands_Pxx[i] = float(names[val])
|
|
556
|
+
mask = (self.frequency_Pxx >= f_lower[val]) & (self.frequency_Pxx < f_upper[val])
|
|
557
|
+
self.octave_bands_Pxx_power[i] = np.sum(self.Pxx[mask] * delta_f)
|
|
558
|
+
|
|
559
|
+
if self.amplitude is not None:
|
|
560
|
+
# determine the frequency bands
|
|
561
|
+
idx = np.where((f_lower < np.max(self.frequency)) & (f_upper > np.min(self.frequency)))[0]
|
|
562
|
+
if len(idx) == 0:
|
|
563
|
+
raise ValueError("No frequency bands found in the FFT. Please check the frequency bands.")
|
|
564
|
+
|
|
565
|
+
# compute the FFT for the bands
|
|
566
|
+
self.octave_bands_fft = np.zeros(len(idx))
|
|
567
|
+
self.octave_bands_fft_power = np.zeros(len(idx))
|
|
568
|
+
|
|
569
|
+
for i, val in enumerate(idx):
|
|
570
|
+
self.octave_bands_fft[i] = float(names[val])
|
|
571
|
+
mask = (self.frequency >= f_lower[val]) & (self.frequency < f_upper[val])
|
|
572
|
+
self.octave_bands_fft_power[i] = np.sum(self.amplitude[mask]**2)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SignalProcessingTools
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Signal processing tools
|
|
5
|
+
Author: attr: SignalProcessingTools.__author__
|
|
6
|
+
Author-email: bruno.zuadacoelho@deltares.nl, aron.noordam@deltares.nl
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: matplotlib>=3.10
|
|
13
|
+
Requires-Dist: numpy>=2.2
|
|
14
|
+
Requires-Dist: scipy>=1.15
|
|
15
|
+
Provides-Extra: testing
|
|
16
|
+
Requires-Dist: pytest>=8.3; extra == "testing"
|
|
17
|
+
Requires-Dist: tox>=4.24; extra == "testing"
|
|
18
|
+
|
|
19
|
+
# SignalProcessingTools
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
A comprehensive Python package for time and space domain signal processing operations with a focus on vibration analysis and frequency-domain transformations.
|
|
24
|
+
The space domain operations focus on railway applications, while the time domain operations are more general.
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
SignalProcessingTools provides a suite of tools for analyzing, transforming, and processing data.
|
|
29
|
+
|
|
30
|
+
### Time domain operations:
|
|
31
|
+
* Fast Fourier Transforms (FFT) and inverse FFT
|
|
32
|
+
* Signal filtering
|
|
33
|
+
* Integration
|
|
34
|
+
* Power Spectral Density (PSD) using Welch's method
|
|
35
|
+
* Spectrogram generation
|
|
36
|
+
* Effective velocity calculations using SBR method
|
|
37
|
+
* 1/3 octave band analysis
|
|
38
|
+
* Windowing functions (Hann, Hamming, Blackman, etc.)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Space domain operations:
|
|
42
|
+
* D0, D1, D2, and D3 track longitudinal levels, following EN 13848-1:2006.
|
|
43
|
+
* Hmax and Hrms according to Zandberg et al. (2022).
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### Install from PyPI
|
|
48
|
+
You can install the package directly from PyPI using pip:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install SignalProcessingTools
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Install from Source
|
|
55
|
+
To install the package from the source, clone the repository and run the following commands:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/PlatypusBytes/SignalProcessing.git
|
|
59
|
+
cd SignalProcessing
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Basic Example of Time Domain Operations
|
|
66
|
+
|
|
67
|
+
#### FFT and signal integration
|
|
68
|
+
```python
|
|
69
|
+
import numpy as np
|
|
70
|
+
from SignalProcessingTools.time_signal import SignalProcessing, Windows
|
|
71
|
+
|
|
72
|
+
# Create a test signal
|
|
73
|
+
t = np.linspace(0, 10, 5001)
|
|
74
|
+
y = 1.75 * np.sin(2 * np.pi * 6 * t)
|
|
75
|
+
|
|
76
|
+
# Initialize the signal processor
|
|
77
|
+
sig = SignalProcessing(t, y)
|
|
78
|
+
|
|
79
|
+
# Perform FFT
|
|
80
|
+
sig.fft()
|
|
81
|
+
|
|
82
|
+
# Integrate the signal
|
|
83
|
+
sig.integrate(baseline=True, hp=True, fpass=1, n=6)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Windowed Processing and PSD and spectrogram
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Create a signal processor with Hamming window
|
|
90
|
+
sig = SignalProcessing(t, y, window=Windows.HAMMING, window_size=4096)
|
|
91
|
+
|
|
92
|
+
# Calculate Power Spectral Density
|
|
93
|
+
sig.psd()
|
|
94
|
+
|
|
95
|
+
# Generate a spectrogram
|
|
96
|
+
sig.spectrogram()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Signal Filtering
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Apply a low-pass filter to remove high frequency noise
|
|
103
|
+
sig.filter(10, 4, type_filter="lowpass")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Effective Velocity Calculation (SBR-B Method)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Calculate effective velocity using SBR method
|
|
110
|
+
sig.v_eff_SBR()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Basic Example of Spatial Domain Operations
|
|
114
|
+
|
|
115
|
+
#### D0, D1, D2, and D3 Calculation
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import numpy as np
|
|
119
|
+
from SignalProcessingTools.space_signal import SpatialSignal
|
|
120
|
+
from SignalProcessingTools.space_signal import EN13848
|
|
121
|
+
|
|
122
|
+
# Create test data
|
|
123
|
+
x = np.linspace(0, 100, 50001)
|
|
124
|
+
omega = 2 * np.pi * 6
|
|
125
|
+
y = 1.75 * np.sin(omega * x)
|
|
126
|
+
y_noise = y + 0.01 * np.sin(120 * x)
|
|
127
|
+
|
|
128
|
+
sig = SpaceSignalProcessing(x, y_noise)
|
|
129
|
+
# Compute track longitudinal levels
|
|
130
|
+
sig.compute_track_longitudinal_levels()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Hmax and Hrms Calculation
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
x_track = np.linspace(0, 500, 25001)
|
|
137
|
+
track_irregularity = (
|
|
138
|
+
0.002 * np.sin(2 * np.pi * 0.1 * x_track) +
|
|
139
|
+
0.001 * np.sin(2 * np.pi * 0.2 * x_track) +
|
|
140
|
+
0.0005 * np.sin(2 * np.pi * 0.4 * x_track) +
|
|
141
|
+
0.0002 * np.random.randn(len(x_track))
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
sig_hmax = SpaceSignalProcessing(x_track, track_irregularity)
|
|
145
|
+
# Compute Hmax parameters
|
|
146
|
+
sig_hmax.compute_Hmax(convert_m2mm=True)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Example Files
|
|
150
|
+
|
|
151
|
+
A comprehensive example demonstrating all features is provided for the [time signal](./example_time_signal.py) and [space signal](./example_space_signal.py).
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
This project is licensed under the MIT License - see the License file for details.
|
|
157
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
SignalProcessingTools/__init__.py,sha256=nMXg9GFsk8gCcyBnh1MAeUPvo4Zcuv3sdwGHZrxfpPE,36
|
|
2
|
+
SignalProcessingTools/__version__.py,sha256=n9uA3v0QoZoBPG8Ly-lXIYBpXp1Rjvx9jkmhMjs3LYU,106
|
|
3
|
+
SignalProcessingTools/space_signal.py,sha256=Pq_S2EimxSIjBWdz7pkaNDypOFdI6YXZE7PrsVbwueg,7572
|
|
4
|
+
SignalProcessingTools/time_signal.py,sha256=Ws8_BKx8X-bpnCoNNUng8nItNDoWNV-mq6Hj0zDp7Mg,22129
|
|
5
|
+
signalprocessingtools-1.2.0.dist-info/METADATA,sha256=xfkGVC8ee7nuIYGCA1FpVVMlWOGY76eWoqU-ltspxL4,4159
|
|
6
|
+
signalprocessingtools-1.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
7
|
+
signalprocessingtools-1.2.0.dist-info/top_level.txt,sha256=BzkLqG2CNDmcYLCI_-5cmnV91uDRfxNGotNulq6m4pk,22
|
|
8
|
+
signalprocessingtools-1.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SignalProcessingTools
|