SignalForge 0.0.2__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.
- SignalForge/__init__.py +6 -0
- SignalForge/nonstationary_nongaussian.py +522 -0
- SignalForge/single_chan_signal.py +1083 -0
- SignalForge/stationary_gaussian.py +110 -0
- SignalForge/stationary_nongaussian.py +934 -0
- SignalForge/utils.py +526 -0
- signalforge-0.0.2.dist-info/METADATA +23 -0
- signalforge-0.0.2.dist-info/RECORD +11 -0
- signalforge-0.0.2.dist-info/WHEEL +5 -0
- signalforge-0.0.2.dist-info/licenses/LICENSE +21 -0
- signalforge-0.0.2.dist-info/top_level.txt +1 -0
SignalForge/utils.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
import pyNNST
|
|
5
|
+
from statsmodels.tsa.stattools import kpss, adfuller
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def method_existance_check(method:str, methods:dict):
|
|
9
|
+
"""
|
|
10
|
+
Check if a method is in a dictionary of available options.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
method (str): The method name to check.
|
|
14
|
+
methods (dict): A dictionary of available methods.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
KeyError: If method is not in the dictionary keys.
|
|
18
|
+
"""
|
|
19
|
+
if method not in methods.keys():
|
|
20
|
+
options = ', '.join(f'"{key}"' for key in methods.keys())
|
|
21
|
+
raise KeyError(f'Index {method} not supported. Try with: {options}')
|
|
22
|
+
# raise KeyError(f'Index {method} not supported. Try with: {[f'"{key}"' for key in methods.keys()]}')
|
|
23
|
+
|
|
24
|
+
def step_interp(x, xp, yp):
|
|
25
|
+
"""
|
|
26
|
+
Step interpolation of `yp` over `xp`, evaluated at `x`.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
x (array-like): Points at which to evaluate the interpolated values.
|
|
30
|
+
xp (array-like): Known x-coordinates of data points.
|
|
31
|
+
yp (array-like): Known y-values at each xp.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
np.ndarray: Interpolated values at `x`.
|
|
35
|
+
"""
|
|
36
|
+
xp = np.asarray(xp)
|
|
37
|
+
yp = np.asarray(yp)
|
|
38
|
+
x_mid = (xp[:-1] + xp[1:]) / 2
|
|
39
|
+
x_extended = np.concatenate([[xp[0]], x_mid, [xp[-1]]])
|
|
40
|
+
y_extended = np.concatenate([[yp[0]], yp[1:], [yp[-1]]])
|
|
41
|
+
idx = np.searchsorted(x_extended, x, side='right') - 1
|
|
42
|
+
y = y_extended
|
|
43
|
+
idx = np.clip(idx, 0, len(yp)-1)
|
|
44
|
+
return y[idx]
|
|
45
|
+
|
|
46
|
+
def lin_interp_psd(fpsd:np.ndarray, psd:np.ndarray, n_points:int, fs = None):
|
|
47
|
+
"""
|
|
48
|
+
Linear interpolation of a PSD to a uniformly spaced frequency grid.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
fpsd (array-like): Original frequency vector.
|
|
52
|
+
psd (array-like): Original PSD values.
|
|
53
|
+
n_points (int): Number of points in the interpolated frequency vector.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
tuple: (interpolated frequency vector, interpolated PSD)
|
|
57
|
+
"""
|
|
58
|
+
if fs is None:
|
|
59
|
+
fs = fpsd[-1] * 2
|
|
60
|
+
new_fpsd = np.linspace(0,1,round(n_points))*fs/2
|
|
61
|
+
new_psd = np.interp(new_fpsd, fpsd, psd, left=0, right=0)
|
|
62
|
+
new_psd[0] = 0
|
|
63
|
+
return new_fpsd, new_psd
|
|
64
|
+
|
|
65
|
+
def log_interp_psd(fpsd, psd, n_points, fs = None):
|
|
66
|
+
"""
|
|
67
|
+
Log-log interpolation of a PSD to a uniformly spaced frequency grid.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
fpsd (array-like): Original frequency vector.
|
|
71
|
+
psd (array-like): Original PSD values.
|
|
72
|
+
n_points (int): Number of points in the interpolated frequency vector.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
tuple: (interpolated frequency vector, interpolated PSD)
|
|
76
|
+
"""
|
|
77
|
+
if fs is None:
|
|
78
|
+
fs = fpsd[-1] * 2
|
|
79
|
+
psd = psd.astype('float')
|
|
80
|
+
psd[psd<=0]= 1e-30
|
|
81
|
+
new_fpsd = np.linspace(0,1,round(n_points))*fs/2
|
|
82
|
+
new_psd = np.interp(np.log10(new_fpsd), np.log10(fpsd), np.log10(psd), left=-30, right=-30)
|
|
83
|
+
new_psd = 10**new_psd
|
|
84
|
+
new_psd[new_psd<=1e-30]= 0
|
|
85
|
+
new_psd[0] = 0
|
|
86
|
+
return new_fpsd, new_psd
|
|
87
|
+
|
|
88
|
+
def get_stat_mom(var, probability, order):
|
|
89
|
+
"""
|
|
90
|
+
Compute statistical moment of a given order from a PDF.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
var (np.ndarray): Grid of variable values.
|
|
94
|
+
probability (np.ndarray): PDF values corresponding to `var`.
|
|
95
|
+
order (int): Moment order.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
float: Statistical moment of the given order.
|
|
99
|
+
"""
|
|
100
|
+
return np.trapezoid(probability*var**order,var)
|
|
101
|
+
|
|
102
|
+
def alpha_spec_index(fpsd, psd, order, deriv_order):
|
|
103
|
+
"""
|
|
104
|
+
Compute the spectral index (alpha) of a process.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
fpsd (np.ndarray): Frequency vector.
|
|
108
|
+
psd (np.ndarray): Power spectral density.
|
|
109
|
+
order (int): Order of the moment.
|
|
110
|
+
deriv_order (int): Derivative order.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
float: Spectral index.
|
|
114
|
+
"""
|
|
115
|
+
m2n_x = get_stat_mom(fpsd, psd, 2*deriv_order+order)
|
|
116
|
+
m2n = get_stat_mom(fpsd, psd, 2*deriv_order)
|
|
117
|
+
m2n_2x = get_stat_mom(fpsd, psd, 2*(deriv_order+order))
|
|
118
|
+
return m2n_x/np.sqrt(m2n*m2n_2x)
|
|
119
|
+
|
|
120
|
+
def get_psd_impulse_response(fpsd,psd, N, fs = None):
|
|
121
|
+
"""
|
|
122
|
+
Compute an impulse response from a target PSD.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
fpsd (np.ndarray): Frequency vector.
|
|
126
|
+
psd (np.ndarray): PSD values.
|
|
127
|
+
N (int): Number of samples in the impulse response.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
np.ndarray: Time-domain impulse response.
|
|
131
|
+
"""
|
|
132
|
+
if fs is None:
|
|
133
|
+
fs = fpsd[-1] * 2
|
|
134
|
+
N_os = (N // 2 + 1)
|
|
135
|
+
_, psd_interp = lin_interp_psd(fpsd, psd,fs, N_os)
|
|
136
|
+
T = N/(fs)
|
|
137
|
+
freq_filter = psd_interp*T*fpsd[1]
|
|
138
|
+
h_t = np.fft.irfft((np.sqrt(freq_filter)), n = N)*fs
|
|
139
|
+
h_t = np.roll(h_t, N//2) # circular shift to make the impulse centred
|
|
140
|
+
return h_t
|
|
141
|
+
|
|
142
|
+
def get_stat_history(signal:np.ndarray, winsize:int, olap:int = 0, idx_type = 'rms'):
|
|
143
|
+
"""
|
|
144
|
+
Compute a statistical index over a moving window.
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
signal : np.ndarray
|
|
148
|
+
Time history signal.
|
|
149
|
+
winsize : int:
|
|
150
|
+
Window size.
|
|
151
|
+
olap : int, optional
|
|
152
|
+
The number of points to overlap on the moving window. If not specified, no overlap is considered
|
|
153
|
+
idx_type : str
|
|
154
|
+
Index type: 'mean', 'rms', 'var', or 'kurtosis'.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
stat_history: np.ndarray
|
|
158
|
+
Evolution of the statistical index over time.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
idx_types = {'mean':np.mean,
|
|
162
|
+
'rms':np.std,
|
|
163
|
+
'var':np.var,
|
|
164
|
+
'kurtosis':sp.stats.kurtosis}
|
|
165
|
+
|
|
166
|
+
method_existance_check(idx_type, idx_types)
|
|
167
|
+
|
|
168
|
+
winsize = int(np.round(winsize))
|
|
169
|
+
olap = int(np.round(olap))
|
|
170
|
+
step = winsize-olap
|
|
171
|
+
|
|
172
|
+
idx_history = np.zeros((len(signal) - winsize + 1)//(step)+1)
|
|
173
|
+
|
|
174
|
+
print(f'Calculating {idx_type} evolution')
|
|
175
|
+
k = 0
|
|
176
|
+
for i in tqdm(range(0, len(signal) - winsize + 1, step)):
|
|
177
|
+
index = idx_types[idx_type](signal[i : i + winsize])
|
|
178
|
+
idx_history[k] = index
|
|
179
|
+
k += 1
|
|
180
|
+
if 'kurtosis' in idx_type:
|
|
181
|
+
idx_history = idx_history+3
|
|
182
|
+
return idx_history
|
|
183
|
+
|
|
184
|
+
def get_nnst_index(signal, nperseg = 100, noverlap = 0, *args, **kwargs):
|
|
185
|
+
"""
|
|
186
|
+
Non-stationarity index identification using modified run-test.
|
|
187
|
+
Standard deviation of entire signal is compared to standard deviation of segmented signal,
|
|
188
|
+
and the number of variations (i.e., runs) is compared to expected value of variations to obtain
|
|
189
|
+
the non-stationarity index.
|
|
190
|
+
|
|
191
|
+
For the complete documentation please refer to: https://github.com/LolloCappo/pyNNST
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
signal: array-like
|
|
196
|
+
One dimensional time history
|
|
197
|
+
nperseg : int
|
|
198
|
+
Length of each segment
|
|
199
|
+
|
|
200
|
+
noverlap: int, optional
|
|
201
|
+
Number of points to overlap between segments. If None,
|
|
202
|
+
"noverlap = 0". Defaults to None.
|
|
203
|
+
|
|
204
|
+
confidence: int, optional
|
|
205
|
+
Confidence interval [90-95-98-99] %. If None,
|
|
206
|
+
"confidence = 95". Defaults to None.
|
|
207
|
+
|
|
208
|
+
*args, **kwargs: optional
|
|
209
|
+
Additional arguments for the method
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
test_results: dict
|
|
214
|
+
Results of the non-stationary test
|
|
215
|
+
|
|
216
|
+
Source
|
|
217
|
+
------
|
|
218
|
+
L. Capponi, M. Česnik, J. Slavič, F. Cianetti, e M. Boltežar, «Non-stationarity index in vibration fatigue: Theoretical and experimental research», Int. J. Fatigue, vol. 104, pp. 221–230, nov. 2017, doi: 10.1016/j.ijfatigue.2017.07.020.
|
|
219
|
+
"""
|
|
220
|
+
test = pyNNST.nnst(signal, nperseg = nperseg, noverlap=noverlap, *args, **kwargs)
|
|
221
|
+
test.idns() # perform index assessment
|
|
222
|
+
test_results = {
|
|
223
|
+
'outcome': test.get_outcome(),
|
|
224
|
+
'test': test.get_index(),
|
|
225
|
+
'winsize': test.nperseg,
|
|
226
|
+
'olap': test.noverlap,
|
|
227
|
+
'confidence': test.confidence,
|
|
228
|
+
}
|
|
229
|
+
return test_results
|
|
230
|
+
|
|
231
|
+
def get_adf_index(signal, maxlag=None, regression='c', autolag='AIC', *args, **kwargs):
|
|
232
|
+
"""
|
|
233
|
+
Augmented Dickey-Fuller unit root test from the statsmodels package.
|
|
234
|
+
|
|
235
|
+
The Augmented Dickey-Fuller test can be used to test for a unit root in a univariate process in the presence of serial correlation.
|
|
236
|
+
|
|
237
|
+
For the complete documentation please refer to: https://www.statsmodels.org/stable/generated/statsmodels.tsa.stattools.adfuller.html#statsmodels.tsa.stattools.adfuller
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
signal : array_like, 1d
|
|
242
|
+
The data series to test.
|
|
243
|
+
maxlag : {None, int}
|
|
244
|
+
Maximum lag which is included in test, default value of
|
|
245
|
+
12*(nobs/100)^{1/4} is used when ``None``.
|
|
246
|
+
regression : {"c","ct","ctt","n"}
|
|
247
|
+
Constant and trend order to include in regression.
|
|
248
|
+
|
|
249
|
+
* "c" : constant only (default).
|
|
250
|
+
* "ct" : constant and trend.
|
|
251
|
+
* "ctt" : constant, and linear and quadratic trend.
|
|
252
|
+
* "n" : no constant, no trend.
|
|
253
|
+
|
|
254
|
+
autolag : {"AIC", "BIC", "t-stat", None}
|
|
255
|
+
Method to use when automatically determining the lag length among the
|
|
256
|
+
values 0, 1, ..., maxlag.
|
|
257
|
+
|
|
258
|
+
* If "AIC" (default) or "BIC", then the number of lags is chosen to minimize the corresponding information criterion.
|
|
259
|
+
* "t-stat" based choice of maxlag. Starts with maxlag and drops a lag until the t-statistic on the last lag length is significant using a 5%-sized test.
|
|
260
|
+
* If None, then the number of included lags is set to maxlag.
|
|
261
|
+
|
|
262
|
+
*args, **kwargs : optional
|
|
263
|
+
Additional arguments for the method
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
test_results : dict
|
|
268
|
+
Dictionary containing the non-stationarity test results:
|
|
269
|
+
'outcome', 'test', 'p-value', 'lag', 'crit_values'
|
|
270
|
+
|
|
271
|
+
Source
|
|
272
|
+
------
|
|
273
|
+
Dickey, D. A., and W. A. Fuller. "Distribution of the Estimators for Autoregressive Time Series with a Unit Root." Journal of the American Statistical Association. Vol. 74, 1979, pp. 427–431.
|
|
274
|
+
"""
|
|
275
|
+
test = adfuller(signal, maxlag, regression, autolag, *args, **kwargs)
|
|
276
|
+
if test[1] < 0.05: # p-value
|
|
277
|
+
outcome = 'Stationary'
|
|
278
|
+
else:
|
|
279
|
+
outcome = 'Non-stationary'
|
|
280
|
+
test_results = {
|
|
281
|
+
'outcome': outcome,
|
|
282
|
+
'test': test[0],
|
|
283
|
+
'p-value': test[1],
|
|
284
|
+
'lag': test[2],
|
|
285
|
+
'crit_values': test[4]
|
|
286
|
+
}
|
|
287
|
+
return test_results
|
|
288
|
+
|
|
289
|
+
def get_kpss_index(signal, regression = 'c', nlags = 'auto', *args, **kwargs):
|
|
290
|
+
"""
|
|
291
|
+
Kwiatkowski-Phillips-Schmidt-Shin test for stationarity.
|
|
292
|
+
|
|
293
|
+
Computes the Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test for the null
|
|
294
|
+
hypothesis that x is level or trend stationary.
|
|
295
|
+
|
|
296
|
+
For the complete documentation please refer to: https://www.statsmodels.org/stable/generated/statsmodels.tsa.stattools.kpss.html#statsmodels.tsa.stattools.kpss
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
signal : array_like, 1d
|
|
301
|
+
The data series to test.
|
|
302
|
+
regression : str{"c", "ct"}
|
|
303
|
+
The null hypothesis for the KPSS test.
|
|
304
|
+
|
|
305
|
+
* "c" : The data is stationary around a constant (default).
|
|
306
|
+
* "ct" : The data is stationary around a trend.
|
|
307
|
+
|
|
308
|
+
nlags : {str, int}, optional
|
|
309
|
+
Indicates the number of lags to be used. If "auto" (default), lags
|
|
310
|
+
is calculated using the data-dependent method of Hobijn et al. (1998).
|
|
311
|
+
See also Andrews (1991), Newey & West (1994), and Schwert (1989). If
|
|
312
|
+
set to "legacy", uses int(12 * (n / 100)**(1 / 4)) , as outlined in
|
|
313
|
+
Schwert (1989).
|
|
314
|
+
*args, **kwargs : optional
|
|
315
|
+
Additional arguments for the method
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
test_results : dict
|
|
320
|
+
Dictionary containing the non-stationarity test results:
|
|
321
|
+
'outcome', 'test', 'p-value', 'lag', 'crit_values'
|
|
322
|
+
|
|
323
|
+
Source
|
|
324
|
+
------
|
|
325
|
+
Kwiatkowski, D., Phillips, P. C. B., Schmidt, P., Shin, Y. (1992). "Testing the null hypothesis of stationarity against the alternative of a unit root". Journal of Econometrics. 54 (1–3): 159–178. doi:10.1016/0304-4076(92)90104-Y.
|
|
326
|
+
"""
|
|
327
|
+
test = kpss(signal, regression=regression, nlags=nlags, *args, **kwargs)
|
|
328
|
+
if test[1] < 0.05: # p-value
|
|
329
|
+
outcome = 'Non-stationary'
|
|
330
|
+
else:
|
|
331
|
+
outcome = 'Stationary'
|
|
332
|
+
|
|
333
|
+
test_results = {
|
|
334
|
+
'outcome': outcome,
|
|
335
|
+
'test': test[0],
|
|
336
|
+
'p-value': test[1],
|
|
337
|
+
'lag': test[2],
|
|
338
|
+
'crit_values': test[3]
|
|
339
|
+
}
|
|
340
|
+
return test_results
|
|
341
|
+
|
|
342
|
+
def print_nonstat_results(ns_test, indent=0):
|
|
343
|
+
for key, value in ns_test.items():
|
|
344
|
+
print(' ' * indent + str(key) + ':\t', end=' ')
|
|
345
|
+
if isinstance(value, dict):
|
|
346
|
+
print()
|
|
347
|
+
print_nonstat_results(value, indent + 4)
|
|
348
|
+
else:
|
|
349
|
+
print(value)
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
def get_banded_spectral_kurtosis(
|
|
353
|
+
signal: np.ndarray,
|
|
354
|
+
dt:float,
|
|
355
|
+
n_bins:int = 10,
|
|
356
|
+
fl : int = 0,
|
|
357
|
+
fu : int = 0
|
|
358
|
+
):# * GCurti's theory
|
|
359
|
+
"""
|
|
360
|
+
Compute spectral kurtosis in frequency bands.
|
|
361
|
+
|
|
362
|
+
Parameters:
|
|
363
|
+
signal (np.ndarray): Input time history.
|
|
364
|
+
dt (float): Sampling interval.
|
|
365
|
+
n_bins (int): Number of frequency bands.
|
|
366
|
+
fl (float): Lower frequency limit.
|
|
367
|
+
fu (float): Upper frequency limit (default is Nyquist).
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
tuple: (center frequencies, spectral kurtosis values)
|
|
371
|
+
"""
|
|
372
|
+
f_ny = 1/2/dt
|
|
373
|
+
if fu <= fl: fu = f_ny
|
|
374
|
+
if not isinstance(n_bins, int):
|
|
375
|
+
n_bins = round(n_bins)
|
|
376
|
+
Nos = np.ceil(len(signal)/2)
|
|
377
|
+
Xts_full_woShift = dt*np.fft.fft(signal)
|
|
378
|
+
Xos_full = Xts_full_woShift[0:int(Nos)]
|
|
379
|
+
fper = np.linspace(0, f_ny, len(Xos_full))
|
|
380
|
+
N = len(Xts_full_woShift)
|
|
381
|
+
binsize = (fu-fl) / n_bins
|
|
382
|
+
fvec = np.arange(n_bins) * binsize + fl + binsize/ 2
|
|
383
|
+
SK = np.zeros(n_bins)
|
|
384
|
+
print('Evaluating kurtosis on frequency bands:')
|
|
385
|
+
for i in tqdm(np.arange(n_bins)):
|
|
386
|
+
Xos = np.zeros(len(Xos_full), dtype=np.complex128)
|
|
387
|
+
Xos[(fper>=(fl+binsize*i)) & (fper<=(fl+binsize*(i+1)))] = Xos_full[(fper>=(fl+binsize*i)) & (fper<=(fl+binsize*(i+1)))]
|
|
388
|
+
x = np.fft.irfft(Xos,n = N)/dt
|
|
389
|
+
SK[i] = sp.stats.kurtosis(x)+3
|
|
390
|
+
return fvec, SK
|
|
391
|
+
|
|
392
|
+
def get_welch_spectral_kurtosis(x:np.ndarray, Nfft:int, noverlap:int = None):
|
|
393
|
+
"""
|
|
394
|
+
Compute Welch-based spectral kurtosis.
|
|
395
|
+
|
|
396
|
+
Parameters:
|
|
397
|
+
x (np.ndarray): Input signal.
|
|
398
|
+
Nfft (int): FFT size.
|
|
399
|
+
noverlap (int): Number of overlapping samples.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
tuple: (normalized frequency vector, spectral kurtosis)
|
|
403
|
+
|
|
404
|
+
Source:
|
|
405
|
+
J. Antoni, «The spectral kurtosis: a useful tool for characterising non-stationary signals», Mech. Syst. Signal Process., vol. 20, fasc. 2, pp. 282–307, feb. 2006, doi: 10.1016/j.ymssp.2004.09.001.
|
|
406
|
+
"""
|
|
407
|
+
if noverlap is None:
|
|
408
|
+
noverlap = Nfft//2
|
|
409
|
+
|
|
410
|
+
# Convert scalar window input to actual window
|
|
411
|
+
Window = sp.signal.windows.hann(Nfft//2)
|
|
412
|
+
|
|
413
|
+
Window = np.asarray(Window).flatten()
|
|
414
|
+
Window = Window / np.linalg.norm(Window) # normalize window
|
|
415
|
+
|
|
416
|
+
x = np.asarray(x)
|
|
417
|
+
n = len(x)
|
|
418
|
+
nwind = len(Window)
|
|
419
|
+
|
|
420
|
+
if nwind <= noverlap:
|
|
421
|
+
raise ValueError('Window length must be > noverlap')
|
|
422
|
+
if Nfft < nwind:
|
|
423
|
+
raise ValueError('Window length must be <= Nfft')
|
|
424
|
+
|
|
425
|
+
step = nwind - noverlap
|
|
426
|
+
k = (n - noverlap) // step # number of windows
|
|
427
|
+
|
|
428
|
+
M4 = np.zeros(Nfft)
|
|
429
|
+
M2 = np.zeros(Nfft)
|
|
430
|
+
|
|
431
|
+
for i in range(k):
|
|
432
|
+
start = i * step
|
|
433
|
+
xw = Window * x[start:start + nwind]
|
|
434
|
+
Xw = np.fft.fft(xw, Nfft)
|
|
435
|
+
absX = np.abs(Xw)
|
|
436
|
+
M4 += absX**4
|
|
437
|
+
M2 += absX**2
|
|
438
|
+
|
|
439
|
+
M4 /= k
|
|
440
|
+
M2 /= k
|
|
441
|
+
SK = M4 / (M2**2) - 2
|
|
442
|
+
|
|
443
|
+
# Bias correction
|
|
444
|
+
W = np.abs(np.fft.fft(Window**2, Nfft))**2
|
|
445
|
+
Wb = np.array([W[(2*i) % Nfft] / W[0] for i in range(Nfft)])
|
|
446
|
+
SK -= Wb
|
|
447
|
+
|
|
448
|
+
f_norm = np.arange(Nfft) / Nfft
|
|
449
|
+
|
|
450
|
+
return f_norm[:Nfft//2], SK[:Nfft//2]
|
|
451
|
+
|
|
452
|
+
def get_stationary_gaussian(fpsd:np.ndarray, psd:np.ndarray, T:float, fs:float = None, seed = None, interp:str = 'lin'):
|
|
453
|
+
"""
|
|
454
|
+
Generate a stationary Gaussian signal with a target PSD.
|
|
455
|
+
|
|
456
|
+
Parameters:
|
|
457
|
+
fpsd (np.ndarray): Frequency vector.
|
|
458
|
+
psd (np.ndarray): Power spectral density values.
|
|
459
|
+
T (float): Duration [s].
|
|
460
|
+
seed (int, optional): Random seed for reproducibility.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
tuple: (time vector, generated signal)
|
|
464
|
+
|
|
465
|
+
Source:
|
|
466
|
+
D. E. Newland, An introduction to random vibrations, spectral & wavelet analysis, 3. ed., Unabridged republ. Mineola, NY: Dover Publ, 2005
|
|
467
|
+
"""
|
|
468
|
+
fpsd = np.array(fpsd)
|
|
469
|
+
psd = np.array(psd)
|
|
470
|
+
|
|
471
|
+
psd[np.isnan(psd)] = 0
|
|
472
|
+
|
|
473
|
+
if fs is None:
|
|
474
|
+
fs = fpsd[-1] * 2
|
|
475
|
+
|
|
476
|
+
N = int(fs * T)
|
|
477
|
+
|
|
478
|
+
N_os = N // 2 + 1
|
|
479
|
+
|
|
480
|
+
# Ensure zero-frequency component exists
|
|
481
|
+
# if fpsd[0] != 0:
|
|
482
|
+
# fpsd = np.concatenate(([0], fpsd))
|
|
483
|
+
# fpsd = np.concatenate(([0], psd))
|
|
484
|
+
|
|
485
|
+
# fvec = np.linspace(0, fs // 2, N_os)
|
|
486
|
+
mue2 = np.trapezoid(psd, fpsd)
|
|
487
|
+
|
|
488
|
+
# Interpolate PSD onto frequency vector
|
|
489
|
+
interpolators ={
|
|
490
|
+
'lin':lin_interp_psd,
|
|
491
|
+
'log':log_interp_psd
|
|
492
|
+
}
|
|
493
|
+
method_existance_check(interp, interpolators)
|
|
494
|
+
# per = np.interp(fvec, fpsd, psd)
|
|
495
|
+
fper, per = interpolators[interp](fpsd, psd, N_os, fs)
|
|
496
|
+
per = mue2 / (np.sum(per / T)) * per # Equivalent to dfpsd * inp['T'] * per
|
|
497
|
+
|
|
498
|
+
# Generate random phase and magnitude
|
|
499
|
+
Xabs = np.sqrt(per * T / 2)
|
|
500
|
+
if seed is not None:
|
|
501
|
+
np.random.seed(seed)
|
|
502
|
+
Xos = Xabs * np.exp(1j * np.random.uniform(-np.pi, np.pi, len(Xabs)))
|
|
503
|
+
np.random.seed(None) # Reset random seed
|
|
504
|
+
|
|
505
|
+
# Compute time-domain signal
|
|
506
|
+
signal = np.fft.irfft(Xos * fs, n=N)
|
|
507
|
+
t = np.arange(0, N) / fs
|
|
508
|
+
|
|
509
|
+
return t, signal # Return time-domain signal
|
|
510
|
+
|
|
511
|
+
def gaussian_bell(grid, var, mean = 0):
|
|
512
|
+
"""
|
|
513
|
+
Evaluate a Gaussian distribution over a grid.
|
|
514
|
+
|
|
515
|
+
Parameters:
|
|
516
|
+
grid (np.ndarray): Input grid.
|
|
517
|
+
var (float): Variance.
|
|
518
|
+
mean (float, optional): Mean. Default is 0.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
np.ndarray: Evaluated Gaussian PDF values.
|
|
522
|
+
|
|
523
|
+
Source:
|
|
524
|
+
Squires, G. L. (2001-08-30). Practical Physics (4 ed.). Cambridge University Press. doi:10.1017/cbo9781139164498. ISBN 978-0-521-77940-1.
|
|
525
|
+
"""
|
|
526
|
+
return 1/ np.sqrt(var*2*np.pi) * np.exp(-(grid-mean)**2 / (2 * var))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SignalForge
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Tools for analyzing and generating non-Gaussian, non-stationary signals in engineering applications.
|
|
5
|
+
Home-page: https://github.com/GiulioCurti/SignalForge
|
|
6
|
+
Author: Giulio Curti
|
|
7
|
+
Author-email: giulio.curti@dottorandi.unipg.it
|
|
8
|
+
License: MIT license
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=2.0
|
|
11
|
+
Requires-Dist: scipy
|
|
12
|
+
Requires-Dist: matplotlib>=3.10
|
|
13
|
+
Requires-Dist: tqdm
|
|
14
|
+
Requires-Dist: PyWavelets
|
|
15
|
+
Requires-Dist: pyNNST
|
|
16
|
+
Requires-Dist: statsmodels
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: summary
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
SignalForge/__init__.py,sha256=IWhcCQyaikp2PvGPVQyYdIIU_xD7SA1mS1sNmZqBqcw,197
|
|
2
|
+
SignalForge/nonstationary_nongaussian.py,sha256=6-W0sQb8xOh2loVOTr0cDmWo7WRz_Q4r4SRHMfya4Vk,21005
|
|
3
|
+
SignalForge/single_chan_signal.py,sha256=UShwhbd95pIrsBmCEbIAbZ-La4p1cQzQDPBgqPlQ8t8,40798
|
|
4
|
+
SignalForge/stationary_gaussian.py,sha256=GBwuizh0b7A1rqSCWoyR1uXLl3TQvrJge9y05oX5I4I,3158
|
|
5
|
+
SignalForge/stationary_nongaussian.py,sha256=ilaCwogWNYocSDjsWNUXi_8ZfVj4X1UEAIxkgRXIrtY,37532
|
|
6
|
+
SignalForge/utils.py,sha256=A_QYziRFEp6GM1M0Hus9oUR-q6qNo-yTd1mI25NpHAA,18160
|
|
7
|
+
signalforge-0.0.2.dist-info/licenses/LICENSE,sha256=M96bTSvnJ4ErjAzj_sH3CiRiH4UwyGmKgJxzlUcxGd0,1089
|
|
8
|
+
signalforge-0.0.2.dist-info/METADATA,sha256=JdxiTDuOtiBSsnId5kvktaCm9E8KFxFGLgvLP2ugrM8,663
|
|
9
|
+
signalforge-0.0.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
10
|
+
signalforge-0.0.2.dist-info/top_level.txt,sha256=OQ8Plijrs3HMds2-re0GC14gfI6dRTlZfRgyegGje4o,12
|
|
11
|
+
signalforge-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 GiulioCurti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SignalForge
|