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
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from .utils import *
|
|
6
|
+
from .single_chan_signal import SingleChanSignal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
'''
|
|
10
|
+
supporting functions / 'static methods' (no need of self object)
|
|
11
|
+
- get_winterstein: calculate non-gaussian signal using the Winterstein method
|
|
12
|
+
- get_cubic_polinomial: calculate non-gaussian signal using the cubic polymomial method
|
|
13
|
+
- get_zheng: calculate non-gaussian signal using the hyperbolic method
|
|
14
|
+
- get_sarkani: calculate non-gaussian signal using the Sarkani method
|
|
15
|
+
- get_zmnl: calculate non-gaussian signal using the improved non-linear method
|
|
16
|
+
- get_steinwolf: calculate non-gaussian signal using the Steinwolf method
|
|
17
|
+
- get_smallwood: calculate non-gaussian signal using the Smallwood shotnoise method
|
|
18
|
+
- get_vanbaren: calculate non-gaussian signal using the Vanbaren method
|
|
19
|
+
- get_arma: calculate non-gaussian signal using the ARMA method
|
|
20
|
+
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_signal_statistics(signal: np.ndarray) -> tuple:
|
|
25
|
+
"""
|
|
26
|
+
Internal function to compute basic statistics and theoretical Gaussian PDF for a signal.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
signal : np.ndarray
|
|
31
|
+
Input signal array.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
----------
|
|
35
|
+
rms : float
|
|
36
|
+
Root mean square (standard deviation) of the signal.
|
|
37
|
+
grid : np.ndarray
|
|
38
|
+
Grid over which the PDF is evaluated.
|
|
39
|
+
pdf_x : np.ndarray
|
|
40
|
+
Bestfit Gaussian PDF values evaluated over the grid.
|
|
41
|
+
"""
|
|
42
|
+
# Compute the root mean square (standard deviation) of the signal
|
|
43
|
+
rms = np.std(signal)
|
|
44
|
+
|
|
45
|
+
# Generate a grid of values spanning ±10 times the RMS
|
|
46
|
+
grid = np.linspace(-10 * rms, 10 * rms, 5000)
|
|
47
|
+
|
|
48
|
+
# Evaluate a Gaussian PDF with variance equal to rms squared over the grid
|
|
49
|
+
pdf_x = gaussian_bell(grid, var=rms**2)
|
|
50
|
+
|
|
51
|
+
return rms, grid, pdf_x
|
|
52
|
+
|
|
53
|
+
def get_winterstein(
|
|
54
|
+
signal: np.ndarray,
|
|
55
|
+
input_skewness: float,
|
|
56
|
+
input_kurtosis: float,
|
|
57
|
+
params: float = None
|
|
58
|
+
) -> tuple:
|
|
59
|
+
"""
|
|
60
|
+
Applies the Winterstein method to generate a non-Gaussian signal
|
|
61
|
+
with specified skewness and kurtosis from a Gaussian input.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
signal : np.ndarray
|
|
66
|
+
Input signal.
|
|
67
|
+
input_skewness : float
|
|
68
|
+
Desired skewness of the output signal.
|
|
69
|
+
input_kurtosis : float
|
|
70
|
+
Desired kurtosis of the output signal.
|
|
71
|
+
params : float
|
|
72
|
+
Optional dictionary of parameters. Currently supports:
|
|
73
|
+
- 2 or 3, for second or third order Winterstein transformation.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
---------
|
|
77
|
+
ng_signal : np.ndarray
|
|
78
|
+
Transformed signal
|
|
79
|
+
opt_params : dict
|
|
80
|
+
Dictionary of optimal parameters
|
|
81
|
+
|
|
82
|
+
Source
|
|
83
|
+
------
|
|
84
|
+
S. R. Winerstein, «Nonlinear Vibration Models for Extremes and Fatigue», J. Eng. Mech., vol. 114, fasc. 10, pp. 1772-1790, ott. 1988, doi: 10.1061/(ASCE)0733-9399(1988)114:10(1772)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if input_kurtosis > 12:
|
|
88
|
+
print(
|
|
89
|
+
'Kurtosis requested may be too high for correct representation via '
|
|
90
|
+
'Winterstein method. Consider using alternative methods.'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
K = input_kurtosis
|
|
94
|
+
S = input_skewness
|
|
95
|
+
|
|
96
|
+
# Normalize input signal to zero-mean, unit-variance
|
|
97
|
+
input_rms = sp.stats.tstd(signal)
|
|
98
|
+
x = signal / input_rms
|
|
99
|
+
|
|
100
|
+
# Choose transformation order
|
|
101
|
+
if params is not None and params == 2:
|
|
102
|
+
print('Estimating Winterstein second order non-linear transform...')
|
|
103
|
+
h_3 = S / (4 + 2 * np.sqrt(1 + 1.5 * (K - 3)))
|
|
104
|
+
h_4 = (np.sqrt(1 + 1.5 * (K - 3)) - 1) / 18
|
|
105
|
+
else:
|
|
106
|
+
print('Estimating Winterstein third order non-linear transform...')
|
|
107
|
+
h_3 = (S / 6) * (1 - 0.015 * np.abs(S) + 0.3 * S**2) / (1 + 0.2 * (K - 3))
|
|
108
|
+
h_4 = ((1 + 1.12 * (K - 3))**(1/3) - 1) / 10
|
|
109
|
+
h_4 *= (1 - 1.43 * S**2 / (K - 3))**(1 - 0.1 * K**0.8)
|
|
110
|
+
|
|
111
|
+
h_0 = 1 / np.sqrt(1 + 2 * h_3**2 + 6 * h_4**2)
|
|
112
|
+
|
|
113
|
+
# Apply the appropriate transformation
|
|
114
|
+
if K > 3:
|
|
115
|
+
ng_signal = h_0 * (x + h_3 * (x**2 - 1) + h_4 * (x**3 - 3 * x))
|
|
116
|
+
else:
|
|
117
|
+
c_0 = -h_3 / (3 * h_4)
|
|
118
|
+
c_1 = (1 / (2 * h_4)) * (h_3 / (3 * h_4) + x / h_0) - (h_3 / (3 * h_4))**3
|
|
119
|
+
c_2 = 1 / (3 * h_4) - (h_3**2 / (9 * h_4**2)) - 1
|
|
120
|
+
ng_signal = (
|
|
121
|
+
c_0
|
|
122
|
+
+ (c_1 + np.sqrt(c_1**2 + c_2**3))**(1 / 3)
|
|
123
|
+
+ (c_1 - np.sqrt(c_1**2 + c_2**3))**(1 / 3)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Rescale to original RMS
|
|
127
|
+
output_rms = sp.stats.tstd(ng_signal)
|
|
128
|
+
ng_signal *= input_rms / output_rms
|
|
129
|
+
|
|
130
|
+
opt_params = None
|
|
131
|
+
return ng_signal, opt_params
|
|
132
|
+
|
|
133
|
+
def get_cubic_polinomial(signal:np.ndarray, input_kurtosis:float, input_skewness:float = 0, params = None):
|
|
134
|
+
"""
|
|
135
|
+
Applies a cubic polynomial transformation to a signal to match specified skewness and kurtosis.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
-------
|
|
139
|
+
signal : np.ndarray
|
|
140
|
+
Input Gaussian signal.
|
|
141
|
+
input_kurtosis : float
|
|
142
|
+
Target kurtosis.
|
|
143
|
+
input_skewness : float
|
|
144
|
+
Target skewness.
|
|
145
|
+
param : dict {'a':[float, float, float]}
|
|
146
|
+
Transformation parameters to bypass the optimization procedure
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
------
|
|
150
|
+
ng_signal : np.ndarray
|
|
151
|
+
Transformed signal
|
|
152
|
+
opt_params : dict
|
|
153
|
+
Dictionary of optimal parameters
|
|
154
|
+
Source
|
|
155
|
+
----------
|
|
156
|
+
S. R. Winterstein, C. H. Lange, e S. Kumar, «Fitting: Subroutine to fit four-moment probability distributions to data», SAND--94-3039, 10123319, gen. 1995. doi: 10.2172/10123319.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def cubic_transform(x, a):
|
|
160
|
+
"""Cubic polynomial transformation."""
|
|
161
|
+
x_ng = a[0]*x+a[1]*x**2+a[2]*x**3
|
|
162
|
+
return x_ng
|
|
163
|
+
|
|
164
|
+
def nonlinear_constraint(a):
|
|
165
|
+
"""Nonlinear constraint to ensure a physically feasible cubic polynomial."""
|
|
166
|
+
a1, a2, a3 = a
|
|
167
|
+
return np.sqrt(3*a1*a3) - np.abs(a2) # Must be >= 0
|
|
168
|
+
|
|
169
|
+
## * OBJECTIVE FUNCTION WITH TIMEHISTORY
|
|
170
|
+
# def cubic_obj_fun(var):
|
|
171
|
+
# z = cubic_transform(x, var)
|
|
172
|
+
# obj_val = abs(np.abs(sp.stats.kurtosis(z) + 3 - input_kurtosis) + np.abs(sp.stats.skew(z) - input_skewness))
|
|
173
|
+
# return obj_val
|
|
174
|
+
|
|
175
|
+
def cubic_obj_fun(var):
|
|
176
|
+
"""
|
|
177
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
178
|
+
"""
|
|
179
|
+
z = cubic_transform(grid, var)
|
|
180
|
+
|
|
181
|
+
## * FOR EQUIVALENCE TO TIMESERIES TEST
|
|
182
|
+
# zt = cubic_transform(x, var)
|
|
183
|
+
# time_kurt = sp.stats.kurtosis(zt) + 3
|
|
184
|
+
|
|
185
|
+
transformed_kurtosis = np.trapz(pdf_x*z**4,grid)/np.trapz(pdf_x*z**2,grid)**2
|
|
186
|
+
transformed_skewness = np.trapz(pdf_x*z**3,grid)/np.trapz(pdf_x*z**2,grid)**(3/2)
|
|
187
|
+
obj_val = np.abs(transformed_kurtosis - input_kurtosis) + np.abs(transformed_skewness - input_skewness)
|
|
188
|
+
return obj_val
|
|
189
|
+
|
|
190
|
+
x = signal
|
|
191
|
+
input_rms, grid, pdf_x = _get_signal_statistics(x)
|
|
192
|
+
|
|
193
|
+
if params is not None: # Optimization bypass
|
|
194
|
+
try:
|
|
195
|
+
ng_signal = cubic_transform(x, params['a'])
|
|
196
|
+
except:
|
|
197
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'a':[float, float, float]}")
|
|
198
|
+
return ng_signal, params
|
|
199
|
+
|
|
200
|
+
constraint = {'type': 'ineq', 'fun': nonlinear_constraint}
|
|
201
|
+
optim_res = sp.optimize.minimize(
|
|
202
|
+
cubic_obj_fun,
|
|
203
|
+
x0=[0.1, 0, 0.1],
|
|
204
|
+
bounds=[(0, 100), (-100, 100), (0, 100)],
|
|
205
|
+
constraints=[constraint]
|
|
206
|
+
)
|
|
207
|
+
ng_signal = cubic_transform(x, optim_res.x)
|
|
208
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal))
|
|
209
|
+
opt_params = {'a': optim_res.x}
|
|
210
|
+
return ng_signal, opt_params
|
|
211
|
+
|
|
212
|
+
def get_zheng(signal:np.ndarray, input_kurtosis:float, input_skewness:float = 0, params = None):
|
|
213
|
+
"""
|
|
214
|
+
Applies Zheng's transformation to achieve a desired kurtosis and skewness.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
signal (np.ndarray):
|
|
219
|
+
Input Gaussian signal.
|
|
220
|
+
input_kurtosis (float):
|
|
221
|
+
Target kurtosis.
|
|
222
|
+
input_skewness (float, optional):
|
|
223
|
+
Target skewness. If None, assumes symmetry.
|
|
224
|
+
param : dict {'a': float, 'b': float}
|
|
225
|
+
Transformation parameters to bypass the optimization procedure
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
ng_signal : np.ndarray
|
|
230
|
+
Transformed signal
|
|
231
|
+
opt_params : dict
|
|
232
|
+
Dictionary of optimal parameters
|
|
233
|
+
Source
|
|
234
|
+
------
|
|
235
|
+
R. Zheng, H. Chen, e X. He, «Control Method for Multiple-Input Multiple-Output Non-Gaussian Random Vibration Test: MIMO Non-Gaussian Random Vibration Test», Packag. Technol. Sci., vol. 30, fasc. 7, pp. 331–345, lug. 2017, doi: 10.1002/pts.2303.
|
|
236
|
+
"""
|
|
237
|
+
def zheng_transform_leptokurtic(x, a, b):
|
|
238
|
+
"""zheng polynomial transformation for leptokurtic processes."""
|
|
239
|
+
return np.exp(a*x)-np.exp(-b*x)
|
|
240
|
+
|
|
241
|
+
def zheng_transform_platykurtic(x, a, b):
|
|
242
|
+
"""zheng polynomial transformation for platykurtic processes."""
|
|
243
|
+
return np.log(a*x+np.sqrt((b*x)**2+1))
|
|
244
|
+
|
|
245
|
+
def zheng_obj_fun(var, tranform_fun):
|
|
246
|
+
"""
|
|
247
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
248
|
+
"""
|
|
249
|
+
if len(var)==1:
|
|
250
|
+
z = tranform_fun(grid, var, var)
|
|
251
|
+
else:
|
|
252
|
+
z = tranform_fun(grid, var[0], var[1])
|
|
253
|
+
|
|
254
|
+
## * FOR EQUIVALENCE TO TIMESERIES TEST
|
|
255
|
+
# zt = cubic_transform(x, var)
|
|
256
|
+
# time_kurt = sp.stats.kurtosis(zt) + 3
|
|
257
|
+
|
|
258
|
+
transformed_kurtosis = np.trapz(pdf_x*z**4,grid)/np.trapz(pdf_x*z**2,grid)**2
|
|
259
|
+
transformed_skewness = np.trapz(pdf_x*z**3,grid)/np.trapz(pdf_x*z**2,grid)**(3/2)
|
|
260
|
+
obj_val = np.abs(transformed_kurtosis - input_kurtosis) + np.abs(transformed_skewness - input_skewness)
|
|
261
|
+
return obj_val
|
|
262
|
+
|
|
263
|
+
input_rms = np.std(signal)
|
|
264
|
+
x = signal / np.max(np.abs(signal)) # input signal needs to be normalized to its maximum value
|
|
265
|
+
_, grid, pdf_x = _get_signal_statistics(x)
|
|
266
|
+
|
|
267
|
+
if input_kurtosis>3:
|
|
268
|
+
transform_function = zheng_transform_leptokurtic
|
|
269
|
+
init_approx = 3.48*(input_kurtosis-3)**0.171-2 # only for a==b -> skewness = 0
|
|
270
|
+
else:
|
|
271
|
+
transform_function = zheng_transform_platykurtic
|
|
272
|
+
init_approx = 2.36*np.exp(3-input_kurtosis)-2 **0.171-2 # only for a==b -> skewness = 0
|
|
273
|
+
|
|
274
|
+
if params is not None: # Optimization bypass
|
|
275
|
+
try:
|
|
276
|
+
ng_signal = transform_function(x, params['a'], params['b'])
|
|
277
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal)) # bring back rms to original value
|
|
278
|
+
except:
|
|
279
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'a': float, 'b': float}")
|
|
280
|
+
return ng_signal, params
|
|
281
|
+
|
|
282
|
+
if input_skewness:
|
|
283
|
+
optim_res = sp.optimize.minimize(zheng_obj_fun, args = transform_function, x0=[2, 2], bounds=[(0, 10), (1, 3)])
|
|
284
|
+
a = optim_res.x[0]
|
|
285
|
+
b = optim_res.x[1]
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
# * Apoximated formulation a = b only used as initial guess for optimizator
|
|
289
|
+
optim_res = sp.optimize.minimize(zheng_obj_fun, args = transform_function, x0=init_approx, bounds=[(0, 10)])
|
|
290
|
+
a = optim_res.x[0]
|
|
291
|
+
b = a
|
|
292
|
+
|
|
293
|
+
ng_signal = transform_function(x, a, b)
|
|
294
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal)) # bring back rms to original value
|
|
295
|
+
opt_params = {'a': a, 'b': b}
|
|
296
|
+
return ng_signal, opt_params
|
|
297
|
+
|
|
298
|
+
def get_sarkani(signal:np.ndarray, input_kurtosis:float, input_skewness:float = 0, params = None):
|
|
299
|
+
"""
|
|
300
|
+
Applies Sarkani's transformation to achieve a desired kurtosis and skewness.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
signal : np.ndarray
|
|
305
|
+
Input Gaussian signal.
|
|
306
|
+
input_kurtosis : float
|
|
307
|
+
Target kurtosis.
|
|
308
|
+
input_skewness : float, optional
|
|
309
|
+
Target skewness. If None, assumes symmetry.
|
|
310
|
+
param : dict {'b1': float, 'b2': float}
|
|
311
|
+
Transformation parameters to bypass the optimization procedure
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
ng_signal : np.ndarray
|
|
316
|
+
Transformed signal
|
|
317
|
+
opt_params : dict
|
|
318
|
+
Dictionary of optimal parameters
|
|
319
|
+
Source
|
|
320
|
+
------
|
|
321
|
+
S. Sarkani, D. P. Kihl, e J. E. Beach, «Fatigue of welded joints under narrowband non-Gaussian loadings», Probabilistic Eng. Mech., vol. 9, fasc. 3, pp. 179–190, gen. 1994, doi: 10.1016/0266-8920(94)90003-5.
|
|
322
|
+
"""
|
|
323
|
+
def sarkani_transform(x, b1, b2):
|
|
324
|
+
"""Sarkani's nonlinear transformation."""
|
|
325
|
+
return x + b1*np.sign(x)*np.abs(x)**b2
|
|
326
|
+
|
|
327
|
+
## * OBJECTIVE FUNCTION WITH TIMEHISTORY
|
|
328
|
+
# def sarkani_time_obj_fun(var):
|
|
329
|
+
# beta = var[0]
|
|
330
|
+
# n = var[1]
|
|
331
|
+
# # C = np.sqrt(1+(2**(1/2/(n+1)*n*sp.special.gamma(n/2)*sx**(n-1)))*beta/(np.sqrt(np.pi))+(2**n*sp.special.gamma(n+0.5)*sx**(2*(n-1)))*beta**2/((np.sqrt(np.pi))))
|
|
332
|
+
# z = sarkani_transform(x, beta, n)
|
|
333
|
+
# obj_val = np.abs(sp.stats.kurtosis(z) + 3 - input_kurtosis)
|
|
334
|
+
# return obj_val
|
|
335
|
+
|
|
336
|
+
# * OBJECTIVE FUNCTION WITH PDF
|
|
337
|
+
def sarkani_obj_fun(var):
|
|
338
|
+
"""
|
|
339
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
340
|
+
"""
|
|
341
|
+
b1 = var[0]
|
|
342
|
+
b2 = var[1]
|
|
343
|
+
z = sarkani_transform(grid, b1, b2)
|
|
344
|
+
|
|
345
|
+
## * FOR EQUIVALENCE TO TIMESERIES TEST
|
|
346
|
+
# zt = sarkani_transform(x, beta, n)
|
|
347
|
+
# time_kurt = sp.stats.kurtosis(zt) + 3
|
|
348
|
+
|
|
349
|
+
transformed_kurtosis = np.trapz(pdf_x*z**4,grid)/np.trapz(pdf_x*z**2,grid)**2
|
|
350
|
+
obj_val = np.abs(transformed_kurtosis - input_kurtosis)
|
|
351
|
+
return obj_val
|
|
352
|
+
|
|
353
|
+
if input_skewness:
|
|
354
|
+
raise UserWarning('Skewness correction for sarkani method is not available. Please, set skewness = 0 or consider using another method')
|
|
355
|
+
|
|
356
|
+
input_rms, grid, pdf_x = _get_signal_statistics(signal)
|
|
357
|
+
|
|
358
|
+
if params is not None: # Optimization bypass
|
|
359
|
+
try:
|
|
360
|
+
ng_signal = sarkani_transform(signal, params['b1'], params['b2'])
|
|
361
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal)) # bring back rms to original value
|
|
362
|
+
except:
|
|
363
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'b1': float, 'b2': float}")
|
|
364
|
+
return ng_signal, params
|
|
365
|
+
|
|
366
|
+
optim_res = sp.optimize.minimize(sarkani_obj_fun, x0=[2, 2], bounds=[(0, 5), (1, 5)])
|
|
367
|
+
b1 = optim_res.x[0]
|
|
368
|
+
b2 = optim_res.x[1]
|
|
369
|
+
ng_signal = sarkani_transform(signal, b1, b2)
|
|
370
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal))
|
|
371
|
+
opt_params = {'b1': optim_res.x[0], 'b2': optim_res.x[1]}
|
|
372
|
+
return ng_signal, opt_params
|
|
373
|
+
|
|
374
|
+
def get_zmnl(signal:np.ndarray, fs:float, input_kurtosis:float, input_skewness:float = None, params = None):
|
|
375
|
+
"""
|
|
376
|
+
Applies the improved non linear transformation to achieve a desired kurtosis.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
signal : np.ndarray
|
|
381
|
+
Input Gaussian signal.
|
|
382
|
+
fs : float
|
|
383
|
+
Sampling frequency of the signal
|
|
384
|
+
input_kurtosis : float
|
|
385
|
+
Target kurtosis.
|
|
386
|
+
input_skewness : float, optional
|
|
387
|
+
Target skewness. If None, assumes symmetry.
|
|
388
|
+
param : dict {'beta': float, 'n': float}
|
|
389
|
+
Transformation parameters to bypass the optimization procedure
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
ng_signal : np.ndarray
|
|
394
|
+
Transformed signal
|
|
395
|
+
opt_params : dict
|
|
396
|
+
Dictionary of optimal parameters
|
|
397
|
+
Source
|
|
398
|
+
------
|
|
399
|
+
G. Wise, A. Traganitis, e J. Thomas, «The effect of a memoryless nonlinearity on the spectrum of a random process», IEEE Trans. Inf. Theory, vol. 23, fasc. 1, pp. 84–89, gen. 1977, doi: 10.1109/TIT.1977.1055658.
|
|
400
|
+
"""
|
|
401
|
+
def zengh_transform(x, beta, n, fs):
|
|
402
|
+
"""Improved nonlinear transformation."""
|
|
403
|
+
z = (x+beta*np.sign(x)*np.abs(x)**n)
|
|
404
|
+
z = z*(input_rms/np.std(z))
|
|
405
|
+
Xts_woShift = (1/fs)*np.fft.fft(z)
|
|
406
|
+
Nos = np.ceil((len(z)+1)/2)
|
|
407
|
+
Xos = Xts_woShift[0:int(Nos)]
|
|
408
|
+
transform_FFT_phi = np.angle(Xos)
|
|
409
|
+
z_amplitude_coherent= np.fft.irfft((FFT_amplitudes*np.exp(transform_FFT_phi*1j))*fs)
|
|
410
|
+
return z_amplitude_coherent
|
|
411
|
+
|
|
412
|
+
# * OBJECTIVE FUNCTION WITH TIMEHISTORY
|
|
413
|
+
def zmnl_obj_fun(var):
|
|
414
|
+
"""
|
|
415
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
416
|
+
"""
|
|
417
|
+
beta = var[0]
|
|
418
|
+
n = var[1]
|
|
419
|
+
z = zengh_transform(signal, beta, n, fs)
|
|
420
|
+
tranform_kurtosis = sp.stats.kurtosis(z) + 3
|
|
421
|
+
obj_val = np.abs(tranform_kurtosis - input_kurtosis)
|
|
422
|
+
return obj_val
|
|
423
|
+
|
|
424
|
+
if input_skewness:
|
|
425
|
+
raise UserWarning('Skewness correction for the improved non-linear method is not available. Please, set skewness = 0 or consider using another method')
|
|
426
|
+
|
|
427
|
+
input_rms = np.std(signal)
|
|
428
|
+
Xts_woShift = (1/fs)*np.fft.fft(signal)
|
|
429
|
+
Nos = np.ceil((len(signal)+1)/2)
|
|
430
|
+
Xos = Xts_woShift[0:int(Nos)]
|
|
431
|
+
FFT_amplitudes = np.abs(Xos)
|
|
432
|
+
|
|
433
|
+
if params is not None: # Optimization bypass
|
|
434
|
+
try:
|
|
435
|
+
ng_signal = zengh_transform(signal, params['beta'], params['n'])
|
|
436
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal)) # bring back rms to original value
|
|
437
|
+
except:
|
|
438
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'beta': float, 'n': float}")
|
|
439
|
+
return ng_signal, params
|
|
440
|
+
|
|
441
|
+
optim_res = sp.optimize.minimize(zmnl_obj_fun, x0=[2, 2], bounds=[(0, 5), (0, 5)])
|
|
442
|
+
beta = optim_res.x[0]
|
|
443
|
+
n = optim_res.x[1]
|
|
444
|
+
ng_signal = zengh_transform(signal, beta, n, fs)
|
|
445
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal))
|
|
446
|
+
opt_params = {'beta': optim_res.x[0], 'n': optim_res.x[1]}
|
|
447
|
+
return ng_signal, opt_params
|
|
448
|
+
|
|
449
|
+
def get_steinwolf(
|
|
450
|
+
signal:np.ndarray,
|
|
451
|
+
fs:float,
|
|
452
|
+
input_kurtosis:float,
|
|
453
|
+
input_skewness:float = None,
|
|
454
|
+
params = None
|
|
455
|
+
): #! FIX: INCONSISTENT AND WRONG RESULTS
|
|
456
|
+
"""
|
|
457
|
+
Applies the Steinwolf phase transformation to achieve a desired kurtosis.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
signal : np.ndarray
|
|
462
|
+
Input Gaussian signal.
|
|
463
|
+
fs : float
|
|
464
|
+
Sampling frequency of the signal
|
|
465
|
+
input_kurtosis : float
|
|
466
|
+
Target kurtosis.
|
|
467
|
+
input_skewness : float, optional
|
|
468
|
+
Target skewness. If None, assumes symmetry.
|
|
469
|
+
param : dict {'nd': int, 'seed': int}
|
|
470
|
+
Transformation parameters to bypass the optimization procedure
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
ng_signal : np.ndarray
|
|
475
|
+
Transformed signal
|
|
476
|
+
opt_params : dict
|
|
477
|
+
Dictionary of optimal parameters
|
|
478
|
+
Source
|
|
479
|
+
------
|
|
480
|
+
A. Steinwolf, «Random vibration testing with kurtosis control by IFFT phase manipulation», Mech. Syst. Signal Process., vol. 28, pp. 561–573, apr. 2012, doi: 10.1016/j.ymssp.2011.11.001.
|
|
481
|
+
"""
|
|
482
|
+
def get_Sz(A, phi): #! NOT TO BE USED (WIP)
|
|
483
|
+
"""Returns skewness obtained from the Fourier transform of the signal."""
|
|
484
|
+
N = len(A)
|
|
485
|
+
Sz = 0.5 * np.sum(A**2)
|
|
486
|
+
term1 = (3/4) * np.sum([A[j] * A[k]**2 * np.cos(phi[j] - 2*phi[k]) for k in range(N) for j in range(2*k, N)])
|
|
487
|
+
term2 = (3/2) * np.sum([A[j] * A[k] * A[m] * np.cos(phi[m] - phi[j] - phi[k])
|
|
488
|
+
for j in range(N) for k in range(j+1, N) for m in range(j+k, N)])
|
|
489
|
+
return Sz * (term1 + term2)
|
|
490
|
+
|
|
491
|
+
def get_Kz(A, phi): #! NOT TO BE USED (WIP)
|
|
492
|
+
"""Returns kurtosis obtained from the Fourier transform of the signal."""
|
|
493
|
+
N = len(A)
|
|
494
|
+
Kz = np.sum(A**2)**(-2)
|
|
495
|
+
term1 = (3/2) * np.sum(A**4) + 2 * np.sum([A[j]**3 * np.cos(phi[j] - 3*phi[k])
|
|
496
|
+
for k in range(N) for j in range(3*k, N)])
|
|
497
|
+
term2 = 6 * np.sum([A[j] * A[k] * A[n]**2 * np.cos(phi[j] - phi[k] - 2*phi[n])
|
|
498
|
+
for k in range(N) for n in range(N) if n != k for j in range(k+2*n, N)])
|
|
499
|
+
term3 = 6 * np.sum([A[j] * A[k] * A[n]**2 * np.cos(phi[j] + phi[k] - 2*phi[n])
|
|
500
|
+
for n in range(N) for j in range(N) for k in range(2*n-j,N) if j<k])
|
|
501
|
+
term4 = 12 * np.sum([A[j] * A[k] * A[n] * A[m] * np.cos(phi[j] + phi[k] - phi[n] - phi[m])
|
|
502
|
+
for j in range(N) for k in range(j+1, N) for n in range(j+1, N) for m in range(n+1, N) if j+k == n+m])
|
|
503
|
+
term5 = 12 * np.sum([A[j] * A[k] * A[n] * A[m] * np.cos(phi[j] + phi[k] + phi[n] - phi[m])
|
|
504
|
+
for j in range(N) for k in range(j+1, N) for n in range(k+1, N) for m in range(j+k+n, N)])
|
|
505
|
+
|
|
506
|
+
return 3+ Kz * (term1 + term2 + term3 + term4 + term5)
|
|
507
|
+
|
|
508
|
+
def steinwolf_transform(a, b, deterministic_armonics_id, fs):
|
|
509
|
+
"""Steinwolf phase transformation non-gaussian signal builder."""
|
|
510
|
+
deterministic_armonics_id = deterministic_armonics_id.astype(int)
|
|
511
|
+
E = np.zeros(len(deterministic_armonics_id))
|
|
512
|
+
D = np.zeros(len(deterministic_armonics_id))
|
|
513
|
+
for idx, k in enumerate(deterministic_armonics_id):
|
|
514
|
+
E[idx] = np.sum([a[n]*(a[j]**2-b[j]**2)-2*b[n]*a[j]*b[j]
|
|
515
|
+
for n in range(N) if n != k for j in range(k+2*n, N)])
|
|
516
|
+
D[idx] = np.sum([b[n]*(a[j]**2-b[j]**2)+2*a[j]*a[n]*b[j]
|
|
517
|
+
for n in range(N) if n != k for j in range(k+2*n, N)])
|
|
518
|
+
bk = A[deterministic_armonics_id]*np.sqrt(D**2/(D**2+E**2))*np.sign(D)
|
|
519
|
+
ak = np.sqrt(A[deterministic_armonics_id]**2-bk**2)*np.sign(E)
|
|
520
|
+
a[deterministic_armonics_id] = ak
|
|
521
|
+
b[deterministic_armonics_id] = bk
|
|
522
|
+
dt = 1/fs
|
|
523
|
+
Xos = np.array([complex(ai,bi) for ai, bi in zip(a, b)])
|
|
524
|
+
x = np.fft.irfft(Xos/dt)
|
|
525
|
+
return x
|
|
526
|
+
|
|
527
|
+
def steinwolf_obj_fun(params,fs, relevant_armonics):
|
|
528
|
+
"""
|
|
529
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
530
|
+
"""
|
|
531
|
+
deterministic_freqs_ids = relevant_armonics[range(params[0].astype(int))]
|
|
532
|
+
x = steinwolf_transform(a, b, deterministic_freqs_ids , fs)
|
|
533
|
+
transformed_kurtosis = sp.stats.kurtosis(x) + 3
|
|
534
|
+
return np.abs(transformed_kurtosis - input_kurtosis)
|
|
535
|
+
|
|
536
|
+
if input_skewness:
|
|
537
|
+
raise UserWarning('Skewness correction for the smallwood method is not available. Please, set skewness = 0 or consider using another method')
|
|
538
|
+
|
|
539
|
+
seed = np.random.randint(1e6,1e7-1)
|
|
540
|
+
input_rms = np.std(signal)
|
|
541
|
+
Xts_woShift = (1/fs)*np.fft.fft(signal)
|
|
542
|
+
Nos = np.ceil((len(signal)+1)/2)
|
|
543
|
+
Xos = Xts_woShift[0:int(Nos)]
|
|
544
|
+
A = np.abs(Xos)
|
|
545
|
+
relevant_armonics = np.flatnonzero(A > (input_rms / 10))
|
|
546
|
+
phi = np.angle(Xos)
|
|
547
|
+
N = len(A)
|
|
548
|
+
a = A*np.cos(phi)
|
|
549
|
+
b = -A*np.sin(phi)
|
|
550
|
+
|
|
551
|
+
if params is not None: # Optimization bypass
|
|
552
|
+
try:
|
|
553
|
+
rng = np.random.default_rng(seed=params['seed'])
|
|
554
|
+
rng.shuffle(relevant_armonics)
|
|
555
|
+
ng_signal = steinwolf_transform(a, b, params['nd'], fs)
|
|
556
|
+
ng_signal = ng_signal*(input_rms/np.std(ng_signal)) # bring back rms to original value
|
|
557
|
+
except:
|
|
558
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'nd': int, 'seed'}")
|
|
559
|
+
return ng_signal, params
|
|
560
|
+
rng = np.random.default_rng(seed=seed)
|
|
561
|
+
rng.shuffle(relevant_armonics)
|
|
562
|
+
optim_res = sp.optimize.minimize(steinwolf_obj_fun, args = (fs, relevant_armonics.flatten()), x0=[1], bounds=[(1,len(relevant_armonics)-1)])
|
|
563
|
+
ng_signal = steinwolf_transform(a, b, relevant_armonics[range(optim_res.x[0].astype(int))], fs)
|
|
564
|
+
ng_signal*(input_rms/np.std(ng_signal))
|
|
565
|
+
opt_params = {'nd': optim_res.x[0].astype(int), 'seed': seed}
|
|
566
|
+
return ng_signal, opt_params
|
|
567
|
+
|
|
568
|
+
def get_smallwood(
|
|
569
|
+
fpsd:np.ndarray,
|
|
570
|
+
psd: np.ndarray,
|
|
571
|
+
T: float,
|
|
572
|
+
input_kurtosis: float,
|
|
573
|
+
input_skewness: float = None,
|
|
574
|
+
seed: int = None,
|
|
575
|
+
params = None
|
|
576
|
+
):# ! FIX: NOT WORKING
|
|
577
|
+
"""
|
|
578
|
+
Uses the Smallwood filtered poisson shot to achieve a desired kurtosis.
|
|
579
|
+
|
|
580
|
+
Parameters
|
|
581
|
+
----------
|
|
582
|
+
fpsd : np.ndarray
|
|
583
|
+
Frequency vector of the power spectral density
|
|
584
|
+
psd : np.ndarray
|
|
585
|
+
Power density vector of the power spectral density
|
|
586
|
+
T : float
|
|
587
|
+
Time length of the final signal
|
|
588
|
+
input_kurtosis : float
|
|
589
|
+
Target kurtosis.
|
|
590
|
+
input_skewness : float, optional
|
|
591
|
+
Target skewness. If None, assumes symmetry.
|
|
592
|
+
seed : int
|
|
593
|
+
Seed for the random generator for the Poisson shot times and amplitudes.
|
|
594
|
+
param : dict {'lam': float, 'I': float, 'seed': int}
|
|
595
|
+
Transformation parameters to bypass the optimization procedure
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
ng_signal : np.ndarray
|
|
600
|
+
Transformed signal
|
|
601
|
+
opt_params : dict
|
|
602
|
+
Dictionary of optimal parameters
|
|
603
|
+
Source
|
|
604
|
+
------
|
|
605
|
+
D. O. Smallwood, «Generation of Stationary Non-Gaussian Time Histories with a Specified Cross-spectral Density», Shock Vib., vol. 4, fasc. 5–6, pp. 361–377, 1997, doi: 10.1155/1997/713593.
|
|
606
|
+
"""
|
|
607
|
+
def get_shot_noise_smallwood(A, lam, p, I, N, seed = None):
|
|
608
|
+
# nonlocal psd, fs
|
|
609
|
+
N = round(N)
|
|
610
|
+
h_t = get_psd_impulse_response(psd, fs, N)
|
|
611
|
+
if seed: np.random.seed(seed=seed)
|
|
612
|
+
tau = np.round(np.random.exponential(scale=lam, size=round(N/I))*fs).astype(int)
|
|
613
|
+
Ax = A * np.sign(np.random.binomial(size=round(N/I), n=1, p= p)-0.5)
|
|
614
|
+
np.random.seed(None)
|
|
615
|
+
shot_impulse = np.zeros(len(h_t))
|
|
616
|
+
for tau_k, Ai in zip(np.cumsum(tau),Ax):
|
|
617
|
+
if tau_k <len(h_t):
|
|
618
|
+
shot_impulse[tau_k] = Ai
|
|
619
|
+
shot_noise = np.convolve(shot_impulse,h_t,'same')
|
|
620
|
+
return shot_noise
|
|
621
|
+
|
|
622
|
+
def smallwood_opt_fun(params, seed):
|
|
623
|
+
"""
|
|
624
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
625
|
+
"""
|
|
626
|
+
A = 1
|
|
627
|
+
lam = params[0]
|
|
628
|
+
I = params[1]
|
|
629
|
+
ng_signal = get_shot_noise_smallwood(A, lam, p, I, N, seed=seed)
|
|
630
|
+
transformed_kurtosis = sp.stats.kurtosis(ng_signal) + 3
|
|
631
|
+
return np.abs(transformed_kurtosis - input_kurtosis)
|
|
632
|
+
|
|
633
|
+
if input_skewness:
|
|
634
|
+
raise UserWarning('Skewness correction for the smallwood method is not available. Please, set skewness = 0 or consider using another method')
|
|
635
|
+
|
|
636
|
+
if seed is None: seed = np.random.randint(1e6,1e7-1)
|
|
637
|
+
fs = 2*fpsd[-1]
|
|
638
|
+
N = round(T*fs)
|
|
639
|
+
inp_rms = np.sqrt(np.trapz(psd,fpsd))
|
|
640
|
+
h_t = get_psd_impulse_response(fpsd, psd, N)
|
|
641
|
+
h2 = np.sum((h_t)**2)/fs
|
|
642
|
+
h4 = np.sum((h_t)**4)/fs
|
|
643
|
+
lam_0 = h4/h2**2*(1/(input_kurtosis**4-3))
|
|
644
|
+
p = 0.5
|
|
645
|
+
|
|
646
|
+
if params is not None: # Optimization bypass
|
|
647
|
+
try:
|
|
648
|
+
seed = params['seed']
|
|
649
|
+
ng_signal = get_shot_noise_smallwood(A = 1, lam = params['lam'], N=N, p = p, I = params['I'])
|
|
650
|
+
ng_signal = ng_signal*(inp_rms/np.std(ng_signal)) # bring back rms to original value
|
|
651
|
+
except:
|
|
652
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'beta': float, 'n': float, 'seed': int}")
|
|
653
|
+
return ng_signal, params
|
|
654
|
+
|
|
655
|
+
optim_res = sp.optimize.minimize(smallwood_opt_fun,args = (seed), x0=[lam_0, 1.5], bounds=[(0.001,100), (1,2)])
|
|
656
|
+
ng_signal = get_shot_noise_smallwood(A = 1, lam = optim_res.x[0], N=N, p = p, I = optim_res.x[1])
|
|
657
|
+
ng_signal = ng_signal*(inp_rms/np.std(ng_signal))
|
|
658
|
+
opt_params = {'lam': optim_res.x[0], 'I': optim_res.x[1], 'seed': seed}
|
|
659
|
+
return ng_signal, opt_params
|
|
660
|
+
|
|
661
|
+
def get_vanbaren(
|
|
662
|
+
fpsd:np.ndarray,
|
|
663
|
+
psd: np.ndarray,
|
|
664
|
+
T: float,
|
|
665
|
+
input_kurtosis: float,
|
|
666
|
+
input_skewness: float = None,
|
|
667
|
+
seed: int = None,
|
|
668
|
+
params = None
|
|
669
|
+
):
|
|
670
|
+
"""
|
|
671
|
+
Uses the Vanbaren filtered poisson shot to achieve a desired kurtosis.
|
|
672
|
+
|
|
673
|
+
Parameters
|
|
674
|
+
----------
|
|
675
|
+
fpsd : np.ndarray
|
|
676
|
+
Frequency vector of the power spectral density
|
|
677
|
+
psd : np.ndarray
|
|
678
|
+
Power density vector of the power spectral density
|
|
679
|
+
T : float
|
|
680
|
+
Time length of the final signal
|
|
681
|
+
input_kurtosis : float
|
|
682
|
+
Target kurtosis.
|
|
683
|
+
input_skewness : float, optional
|
|
684
|
+
Target skewness. If None, assumes symmetry.
|
|
685
|
+
seed : int
|
|
686
|
+
Seed for the random generator for the Poisson shot times and amplitudes.
|
|
687
|
+
param : dict {'alpha': float, 'seed': int}
|
|
688
|
+
Transformation parameters to bypass the optimization procedure
|
|
689
|
+
Returns
|
|
690
|
+
-------
|
|
691
|
+
ng_signal : np.ndarray
|
|
692
|
+
Transformed signal
|
|
693
|
+
opt_params : dict
|
|
694
|
+
Dictionary of optimal parameters
|
|
695
|
+
Source
|
|
696
|
+
------
|
|
697
|
+
P. Van Baren, «System and method for simultaneously controlling spectrum and kurtosis of a random vibration», US20070185620
|
|
698
|
+
"""
|
|
699
|
+
def get_shot_noise_vanbaren(alpha, h_t, seed = None):
|
|
700
|
+
N = len(h_t)
|
|
701
|
+
gamma = 1/alpha
|
|
702
|
+
if seed: np.random.seed(seed=seed)
|
|
703
|
+
beta = np.random.exponential(scale=1/(alpha-1), size=N)
|
|
704
|
+
shot_impulse = np.abs(np.random.randn(N))<gamma
|
|
705
|
+
np.random.seed(seed=None)
|
|
706
|
+
shot_noise = np.convolve(h_t,beta*shot_impulse, 'same')
|
|
707
|
+
return shot_noise
|
|
708
|
+
|
|
709
|
+
def vanbaren_opt_fun(param, h_t, seed):
|
|
710
|
+
"""
|
|
711
|
+
Objective function to minimize the absolute error in kurtosis and skewness after transformation.
|
|
712
|
+
"""
|
|
713
|
+
alpha = param[0]
|
|
714
|
+
ng_signal =get_shot_noise_vanbaren(alpha, h_t, seed)
|
|
715
|
+
transformed_kurtosis = sp.stats.kurtosis(ng_signal) + 3
|
|
716
|
+
transformed_skewness = sp.stats.skew(ng_signal)
|
|
717
|
+
|
|
718
|
+
return np.abs(transformed_kurtosis - input_kurtosis) + np.abs(transformed_skewness)
|
|
719
|
+
|
|
720
|
+
if input_skewness:
|
|
721
|
+
raise UserWarning('Skewness correction for the smallwood method is not available. Please, set skewness = 0 or consider using another method')
|
|
722
|
+
|
|
723
|
+
if seed is None: seed = np.random.randint(1e6,1e7-1)
|
|
724
|
+
fs = 2*fpsd[-1]
|
|
725
|
+
N = round(T*fs)
|
|
726
|
+
inp_rms = np.sqrt(np.trapz(psd,fpsd))
|
|
727
|
+
# _, gauss_signal = get_stationary_gaussian(fpsd, psd/inp_rms**2, T)
|
|
728
|
+
h_t = get_psd_impulse_response(fpsd, psd, N//2)
|
|
729
|
+
|
|
730
|
+
if params is not None: # Optimization bypass
|
|
731
|
+
try:
|
|
732
|
+
ng_signal = get_shot_noise_vanbaren(alpha = params['alpha'], h_t = h_t, seed = params['seed'])
|
|
733
|
+
ng_signal = ng_signal*(inp_rms/np.std(ng_signal)) # bring back rms to original value
|
|
734
|
+
except:
|
|
735
|
+
raise KeyError("The 'param' argument must be a dictionary with the format: {'alpha': float, 'seed': int}")
|
|
736
|
+
return ng_signal, params
|
|
737
|
+
|
|
738
|
+
optim_res = sp.optimize.minimize(vanbaren_opt_fun, method = 'Nelder-Mead', args = (h_t, seed), x0=[10], bounds=[(1.001,100)], options = {'disp': True})
|
|
739
|
+
shot_noise = get_shot_noise_vanbaren(alpha = optim_res.x[0], h_t = h_t, seed = seed)
|
|
740
|
+
ng_signal = shot_noise
|
|
741
|
+
ng_signal = ng_signal*(inp_rms/np.std(ng_signal))
|
|
742
|
+
opt_params = {'alpha': optim_res.x[0], 'seed': seed}
|
|
743
|
+
return ng_signal, opt_params
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class StationaryNonGaussian(SingleChanSignal):
|
|
747
|
+
"""
|
|
748
|
+
Generates a stationary non-Gaussian signal with specified skewness and kurtosis
|
|
749
|
+
using a transformation method applied to a Gaussian signal with a target PSD.
|
|
750
|
+
StationaryNonGaussian is a child class of SingleChanSignal.
|
|
751
|
+
|
|
752
|
+
Parameters
|
|
753
|
+
----------
|
|
754
|
+
fpsd: np.ndarray
|
|
755
|
+
frequency vector of the power spectral density
|
|
756
|
+
psd: np.ndarray
|
|
757
|
+
power density vector of the power spectral density
|
|
758
|
+
T: float
|
|
759
|
+
time length of the signal
|
|
760
|
+
kurtosis: int
|
|
761
|
+
input kurtosis for the transformation model
|
|
762
|
+
dfpsd: float
|
|
763
|
+
frequency discretization of the psd to be stored
|
|
764
|
+
skewness: int
|
|
765
|
+
input kurtosis for the transformation model
|
|
766
|
+
method: str
|
|
767
|
+
tranformation method to be used for generating the non-gaussian signal
|
|
768
|
+
params: dict
|
|
769
|
+
optional parameters for the transformation model if optimization wants to be skipped
|
|
770
|
+
name: str
|
|
771
|
+
name of the process (used for plots)
|
|
772
|
+
var: str
|
|
773
|
+
name of the variable (used for plots)
|
|
774
|
+
unit: str
|
|
775
|
+
unit of measure of the signal (used for plots)
|
|
776
|
+
seed: int
|
|
777
|
+
seed for the random generator used in non-gaussian models
|
|
778
|
+
interp: 'lin' or 'log'
|
|
779
|
+
interpolation rule for increasing resolution on given PSD
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
def __init__(
|
|
783
|
+
self,
|
|
784
|
+
fpsd: np.ndarray,
|
|
785
|
+
psd: np.ndarray,
|
|
786
|
+
T: float,
|
|
787
|
+
kurtosis: int,
|
|
788
|
+
fs : float = None,
|
|
789
|
+
dfpsd: float = 0.5,
|
|
790
|
+
skewness: int = 0,
|
|
791
|
+
method: str = 'winter',
|
|
792
|
+
params=None,
|
|
793
|
+
name: str = "",
|
|
794
|
+
var: str = 'x',
|
|
795
|
+
unit: str = "$m/s^2$",
|
|
796
|
+
seed: int = None,
|
|
797
|
+
interp: str = 'lin'
|
|
798
|
+
):
|
|
799
|
+
method = method.lower()
|
|
800
|
+
interp = interp.lower()
|
|
801
|
+
|
|
802
|
+
# Define interpolation methods
|
|
803
|
+
interps = {
|
|
804
|
+
'lin': lin_interp_psd,
|
|
805
|
+
'log': log_interp_psd
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# Validate interpolation method
|
|
809
|
+
method_existance_check(interp, interps)
|
|
810
|
+
self._interp = interp
|
|
811
|
+
|
|
812
|
+
if len(fpsd) != len(psd):
|
|
813
|
+
raise AttributeError('The frequency and PSD vectors must have the same length.')
|
|
814
|
+
|
|
815
|
+
self.fpsd = fpsd
|
|
816
|
+
self.psd = psd
|
|
817
|
+
self.T = T
|
|
818
|
+
if fs is None:
|
|
819
|
+
self.fs = fpsd[-1] * 2 # Nyquist frequency
|
|
820
|
+
else:
|
|
821
|
+
self.fs = fs
|
|
822
|
+
|
|
823
|
+
# Generate the non-Gaussian signal
|
|
824
|
+
self.transform_params, self.x = self._get_nongaussian_timehistory(
|
|
825
|
+
seed=seed,
|
|
826
|
+
method=method,
|
|
827
|
+
skewness=skewness,
|
|
828
|
+
kurtosis=kurtosis,
|
|
829
|
+
params=params
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
self.signal_type = f'Stationary - NonGaussian ({method})'
|
|
833
|
+
|
|
834
|
+
super().__init__(
|
|
835
|
+
x=self.x,
|
|
836
|
+
fs=self.fs,
|
|
837
|
+
dfpsd=dfpsd,
|
|
838
|
+
name=name,
|
|
839
|
+
var=var,
|
|
840
|
+
unit=unit,
|
|
841
|
+
signal_type=self.signal_type
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
def _get_gaussian_timehistory(self, seed=None) -> np.ndarray:
|
|
845
|
+
"""
|
|
846
|
+
Internal function called by __init__
|
|
847
|
+
Generate a stationary Gaussian signal from the given PSD.
|
|
848
|
+
|
|
849
|
+
Returns
|
|
850
|
+
--------
|
|
851
|
+
opt_params : dict
|
|
852
|
+
Dictionary of optimal parameters
|
|
853
|
+
signal : np.ndarray
|
|
854
|
+
Transformed signal
|
|
855
|
+
"""
|
|
856
|
+
_, signal = get_stationary_gaussian(self.fpsd, self.psd, self.T, fs = self.fs, seed = seed, interp= self._interp)
|
|
857
|
+
return signal
|
|
858
|
+
|
|
859
|
+
def _get_nongaussian_timehistory(
|
|
860
|
+
self,
|
|
861
|
+
skewness: float,
|
|
862
|
+
kurtosis: float,
|
|
863
|
+
method: str = 'winter',
|
|
864
|
+
params=None,
|
|
865
|
+
seed=None
|
|
866
|
+
) -> tuple:
|
|
867
|
+
"""
|
|
868
|
+
Internal function called by __init__
|
|
869
|
+
Transforms a Gaussian signal into a non-Gaussian signal using the specified method.
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
tuple: (parameters used for the transformation, transformed signal)
|
|
873
|
+
"""
|
|
874
|
+
# Generate Gaussian signal
|
|
875
|
+
gaussian_signal = self._get_gaussian_timehistory(seed=seed)
|
|
876
|
+
|
|
877
|
+
# Define transformation methods
|
|
878
|
+
methods = {
|
|
879
|
+
'winter': get_winterstein,
|
|
880
|
+
'cubic': get_cubic_polinomial,
|
|
881
|
+
'zheng': get_zheng,
|
|
882
|
+
'sarkani': get_sarkani,
|
|
883
|
+
'zmnl': get_zmnl,
|
|
884
|
+
'steinwolf': get_steinwolf,
|
|
885
|
+
'smallwood': get_smallwood,
|
|
886
|
+
'vanbaren': get_vanbaren
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
# Validate method
|
|
890
|
+
method_existance_check(method, methods)
|
|
891
|
+
|
|
892
|
+
print(
|
|
893
|
+
f'Estimating non-Gaussian signal from parameters '
|
|
894
|
+
f'(skew = {skewness}, kurtosis = {kurtosis}) using "{method}" method.'
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
if method in ['zmnl', 'steinwolf']:
|
|
898
|
+
signal, opt_params = methods[method](
|
|
899
|
+
gaussian_signal,
|
|
900
|
+
input_skewness=skewness,
|
|
901
|
+
input_kurtosis=kurtosis,
|
|
902
|
+
fs=self.fs,
|
|
903
|
+
params=params
|
|
904
|
+
)
|
|
905
|
+
elif method in ['vanbaren', 'smallwood']:
|
|
906
|
+
signal, opt_params = methods[method](
|
|
907
|
+
self.fpsd,
|
|
908
|
+
self.psd,
|
|
909
|
+
self.T,
|
|
910
|
+
input_skewness=skewness,
|
|
911
|
+
input_kurtosis=kurtosis,
|
|
912
|
+
params=params
|
|
913
|
+
)
|
|
914
|
+
else:
|
|
915
|
+
signal, opt_params = methods[method](
|
|
916
|
+
gaussian_signal,
|
|
917
|
+
input_skewness=skewness,
|
|
918
|
+
input_kurtosis=kurtosis,
|
|
919
|
+
params=params
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
# Output statistics
|
|
923
|
+
output_skew = sp.stats.skew(signal)
|
|
924
|
+
output_kurt = sp.stats.kurtosis(signal) + 3
|
|
925
|
+
print(
|
|
926
|
+
f'Non-Gaussian signal generated with "{method}" method: '
|
|
927
|
+
f'skew = {output_skew:.1f}, kurt = {output_kurt:.1f}'
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
return opt_params, signal
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
if __name__ == "__main__":
|
|
934
|
+
pass
|