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.
@@ -0,0 +1,6 @@
1
+ __version__ = '0.0.2'
2
+ from .single_chan_signal import *
3
+ from .stationary_gaussian import *
4
+ from .stationary_nongaussian import *
5
+ from .nonstationary_nongaussian import *
6
+ from .utils import *
@@ -0,0 +1,522 @@
1
+ import numpy as np
2
+ import scipy as sp
3
+ import matplotlib.pyplot as plt
4
+ from tqdm import tqdm
5
+
6
+ from .utils import *
7
+ from .single_chan_signal import SingleChanSignal
8
+
9
+ '''
10
+ supporting functions / 'static methods' (no need of self object)
11
+ - get_beta_amplitude_modulation: adds amplitude modulated non-stationarity to gaussian signal via beta distribution
12
+ - get_rayleigh_amplitude_modulation: adds amplitude modulated non-stationarity to gaussian signal via rayleigh distribution
13
+ - get_trapp_amplitude_modulation: adds amplitude modulated non-stationarity to gaussian signal via gaussian distribution
14
+ - get_frequency_modulation: designes frequency modulate non-stationary process from given carrier function and starting psd
15
+ '''
16
+
17
+ def _add_am_carrier(noise:np.ndarray, carrier:np.ndarray):
18
+ """
19
+ private function for adding amplitude modulation carrier to gaussian noise standardizing rms to the noise
20
+ """
21
+ nonstat_sign = noise*carrier
22
+ rms_correction = np.std(noise) / np.std(nonstat_sign)
23
+ nonstat_sign = nonstat_sign * rms_correction
24
+ return nonstat_sign
25
+
26
+ def _oneband_am_opt_fun(
27
+ params:list,
28
+ gauss_signal:np.ndarray,
29
+ time_vector:np.ndarray,
30
+ carrier_fun:np.ndarray,
31
+ input_kurtosis:float,
32
+ seed:int
33
+ ):
34
+ """
35
+ optimization function for oneband ampiltude modulated non stationary signals
36
+ """
37
+ carrier = carrier_fun(*params, time_vector, seed)
38
+ nonstat_sign = _add_am_carrier(gauss_signal, carrier)
39
+ transformed_kurtosis = sp.stats.kurtosis(nonstat_sign) + 3
40
+ return np.abs(transformed_kurtosis - input_kurtosis)
41
+
42
+ def _get_banded_am_opt(
43
+ gauss_signal:np.ndarray,
44
+ T:float,
45
+ input_kurtosis:float,
46
+ flims:list,
47
+ amplitude_modulation_fun:np.ndarray,
48
+ seed:int,
49
+ opt_settings:dict
50
+ ):
51
+ """
52
+ optimization function for multiband ampiltude modulated non stationary signals
53
+ """
54
+ time_vector = np.linspace(0,1,len(gauss_signal))*T
55
+ fs = T/len(gauss_signal)
56
+ Nfw = len(input_kurtosis)
57
+ flims = np.linspace(flims[0],flims[1],Nfw+1)
58
+ fft_os = np.fft.rfft(gauss_signal)/fs
59
+ fvec = np.fft.rfftfreq(len(gauss_signal), fs)
60
+ nonstat_sign = np.zeros(len(gauss_signal))
61
+ carrier = np.zeros((Nfw,len(gauss_signal)))
62
+ for i in tqdm(range(Nfw)):
63
+ Xos_win = np.zeros(len(fft_os), dtype=np.complex_)
64
+ fwindow = (fvec >= flims[i]) & (fvec < flims[i+1])
65
+ Xos_win[fwindow] = fft_os[fwindow]
66
+ banded_signal = np.fft.irfft(Xos_win*fs)
67
+ optim_res = sp.optimize.minimize(_oneband_am_opt_fun, args = (banded_signal, time_vector, amplitude_modulation_fun, input_kurtosis[i], seed),**opt_settings)
68
+ carrier[i,:] = amplitude_modulation_fun(*optim_res.x, time_vector, seed)
69
+ nonstat_sign += _add_am_carrier(banded_signal, carrier[i,:])
70
+ return nonstat_sign, carrier
71
+
72
+ def get_beta_amplitude_modulation(
73
+ gauss_signal: np.ndarray,
74
+ T: float,
75
+ input_kurtosis: float,
76
+ flims: list = None,
77
+ seed : int = None
78
+ ):
79
+ """
80
+ Apply beta-distributed amplitude modulation to a Gaussian signal.
81
+
82
+ Parameters
83
+ ----------
84
+ gauss_signal : array-like
85
+ Input Gaussian signal.
86
+ T : float
87
+ Signal duration in seconds.
88
+ input_kurtosis : float or array-like
89
+ Desired kurtosis of the output signal.
90
+ flims : tuple, optional
91
+ Frequency limits for banded modulation.
92
+ seed : int, optional
93
+ Seed for random number generation.
94
+
95
+ Returns
96
+ -------
97
+ nonstat_signal : np.ndarray
98
+ Non stationary resulting signal
99
+ carrier : dict
100
+ Dictionary containing "name" of carrier and "carrier" time history
101
+
102
+
103
+ Source
104
+ ------
105
+ D. Smallwood, «Vibration with Non-Gaussian Noise», J. IEST, vol. 52, fasc. 2, pp. 13–30, ott. 2009, doi: 10.17764/jiet.52.2.gh0444564n8765k1.
106
+ """
107
+ def get_beta_carrier(alpha, Ntw, time_vector, seed = None):
108
+ """
109
+ Generate a beta-distributed amplitude modulation carrier.
110
+
111
+ Parameters:
112
+ alpha : float
113
+ Shape parameter for the beta distribution.
114
+ Ntw : int
115
+ Number of time windows.
116
+ time_vector : array-like
117
+ Time axis for the signal.
118
+ seed : int, optional
119
+ Seed for reproducibility.
120
+
121
+ Returns:
122
+ carrier : array-like
123
+ Normalized amplitude modulation carrier.
124
+ """
125
+ beta = alpha
126
+ t_mod = np.linspace(0,max(time_vector), round(Ntw))
127
+ if seed: np.random.seed(seed=seed)
128
+ beta_points = np.random.beta(alpha, beta, round(Ntw))
129
+ beta_points = 2*beta_points
130
+ beta_points[[-1,0]] = 0
131
+ np.random.seed(seed=None)
132
+ spline_interpolator = sp.interpolate.CubicSpline(t_mod, beta_points)
133
+ beta_carrier = spline_interpolator(time_vector)/np.max(beta_points)
134
+ return beta_carrier
135
+ if np.isscalar(input_kurtosis):
136
+ time_vector = np.linspace(0,1,len(gauss_signal))*T
137
+ optim_res = sp.optimize.minimize(
138
+ _oneband_am_opt_fun,
139
+ args = (gauss_signal, time_vector, get_beta_carrier, input_kurtosis, seed),
140
+ x0=[1,40],
141
+ bounds=[(0.1,5), (30,len(gauss_signal)//2)]
142
+ )
143
+ carrier = get_beta_carrier(*optim_res.x, time_vector, seed)
144
+ nonstat_sign = _add_am_carrier(gauss_signal, carrier)
145
+ carrier = [carrier]
146
+ else:
147
+ opt_settings = {'x0':[1,40], 'bounds':[(0.5,5), (30,len(gauss_signal)//2)]}
148
+ nonstat_sign, carrier = _get_banded_am_opt(
149
+ gauss_signal,
150
+ T,
151
+ input_kurtosis,
152
+ flims,
153
+ get_beta_carrier,
154
+ seed,
155
+ opt_settings)
156
+ return nonstat_sign, {'name': 'Beta amplitude modulation [-]', 'carrier': carrier}
157
+
158
+ def get_rayleigh_amplitude_modulation(
159
+ gauss_signal: np.ndarray,
160
+ T: float,
161
+ input_kurtosis: float,
162
+ flims: list = None,
163
+ seed: int = None
164
+ ):
165
+ """
166
+ Apply Rayleigh-distributed amplitude modulation to a Gaussian signal.
167
+
168
+ Parameters
169
+ ----------
170
+ gauss_signal : array-like
171
+ Input Gaussian signal.
172
+ T : float
173
+ Signal duration in seconds.
174
+ input_kurtosis : float or array-like
175
+ Desired kurtosis of the output signal.
176
+ flims : tuple, optional
177
+ Frequency limits for banded modulation.
178
+ seed : int, optional
179
+ Seed for random number generation.
180
+
181
+ Returns
182
+ -------
183
+ nonstat_signal : np.ndarray
184
+ Non stationary resulting signal
185
+ carrier : dict
186
+ Dictionary containing "name" of carrier and "carrier" time history
187
+
188
+ Source
189
+ ------
190
+ D. Smallwood, «Vibration with Non-Gaussian Noise», J. IEST, vol. 52, fasc. 2, pp. 13–30, ott. 2009, doi: 10.17764/jiet.52.2.gh0444564n8765k1.
191
+ """
192
+ def get_ray_carrier(sigma, Ntw, time_vector, seed = None):
193
+ """
194
+ Generate a Rayleigh-distributed amplitude modulation carrier.
195
+
196
+ Parameters:
197
+ sigma : float
198
+ Scale parameter of the Rayleigh distribution.
199
+ Ntw : int
200
+ Number of time windows.
201
+ time_vector : array-like
202
+ Time axis for the signal.
203
+ seed : int, optional
204
+ Seed for reproducibility.
205
+
206
+ Returns:
207
+ carrier : array-like
208
+ Normalized amplitude modulation carrier.
209
+ """
210
+ t_mod = np.linspace(0,max(time_vector), round(Ntw))
211
+ if seed: np.random.seed(seed=seed)
212
+ ray_points = np.random.rayleigh(sigma, round(Ntw))
213
+ ray_points[[-1,0]] = 0
214
+ np.random.seed(seed=None)
215
+ spline_interpolator = sp.interpolate.CubicSpline(t_mod, ray_points)
216
+ ray_carrier = spline_interpolator(time_vector)/np.max(ray_points)
217
+ return ray_carrier
218
+
219
+ if np.isscalar(input_kurtosis):
220
+ time_vector = np.linspace(0,1,len(gauss_signal))*T
221
+ optim_res = sp.optimize.minimize(
222
+ _oneband_am_opt_fun,
223
+ args = (gauss_signal, time_vector, get_ray_carrier, input_kurtosis, seed),
224
+ x0=[2,40],
225
+ bounds=[(0.1,10), (30,len(gauss_signal)//2)]
226
+ )
227
+ carrier = get_ray_carrier(*optim_res.x, time_vector, seed)
228
+ nonstat_sign = _add_am_carrier(gauss_signal, carrier)
229
+ carrier = [carrier]
230
+ else:
231
+ if flims is None: raise KeyError('Parameter "flims" must be defined to get a specified spectral kurtosis')
232
+ opt_settings = {'x0':[2,40], 'bounds':[(0.1,10), (30,len(gauss_signal)//2)]}
233
+ nonstat_sign, carrier = _get_banded_am_opt(gauss_signal, T, input_kurtosis, flims, get_ray_carrier, seed, opt_settings)
234
+ return nonstat_sign, {'name': 'Rayleigh amplitude modulation [-]', 'carrier': carrier}
235
+
236
+ def get_trapp_amplitude_modulation(
237
+ gauss_signal: np.ndarray,
238
+ T: float,
239
+ input_kurtosis:float,
240
+ flims:list = None,
241
+ seed : int = None
242
+ ): # TODO: ADD MULTIBAND MODULATION
243
+ """
244
+ Apply Gaussian-envelope-based amplitude modulation using a transformed power-law Rayleigh carrier.
245
+
246
+ Parameters
247
+ ----------
248
+ gauss_signal : array-like
249
+ Input Gaussian signal.
250
+ T : float
251
+ Duration in seconds.
252
+ input_kurtosis : float
253
+ Desired signal kurtosis.
254
+ flims : tuple, optional
255
+ Placeholder for banded modulation (not yet implemented).
256
+ seed : int, optional
257
+ Random seed for reproducibility.
258
+
259
+ Returns
260
+ -------
261
+ nonstat_signal : np.ndarray
262
+ Non stationary resulting signal
263
+ carrier : dict
264
+ Dictionary containing "name" of carrier and "carrier" time history
265
+ Source
266
+ ------
267
+ A. Trapp, M. J. Makua, e P. Wolfsteiner, «Fatigue assessment of amplitude-modulated non-stationary random vibration loading», Procedia Struct. Integr., vol. 17, pp. 379–386, 2019, doi: 10.1016/j.prostr.2019.08.050.
268
+ """
269
+ def get_trapp_carrier(p, delta_m, gaussian_carier):
270
+ """Generate amplitude modulation carrier based on power-law transformation."""
271
+ ray_carrier = np.abs(gaussian_carier)**p+delta_m
272
+ return ray_carrier
273
+
274
+ def trapp_am_opt_fun(params, gauss_carrier, input_kurtosis):
275
+ """Optimization objective: match output signal kurtosis and reduce skew."""
276
+ carrier = get_trapp_carrier(*params, gaussian_carier=gauss_carrier)
277
+ nonstat_sign = _add_am_carrier(gauss_signal, carrier)
278
+ transformed_kurtosis = sp.stats.kurtosis(nonstat_sign) + 3
279
+ transformed_skew = sp.stats.skew(nonstat_sign)
280
+ return np.abs(transformed_kurtosis - input_kurtosis)+np.abs(transformed_skew)
281
+
282
+ if flims:
283
+ raise UserWarning('Multiband amplitude modulation not implemented yet for this method.')
284
+ if seed: np.random.seed(seed=seed)
285
+ fs = len(gauss_signal)/T
286
+ fpsd = np.array([0,.2,.3,.4,.5,fs/2])
287
+ psd = np.array([0,0,1,1,0,0])*2/(fpsd[4]+fpsd[3]-fpsd[2]-fpsd[1])
288
+ _,gauss_carrier = get_stationary_gaussian(fpsd, psd, T, seed = None)
289
+ gauss_carrier = sp.signal.windows.tukey(len(gauss_carrier),alpha = 0.01)*gauss_carrier
290
+
291
+ if np.isscalar(input_kurtosis):
292
+ optim_res = sp.optimize.minimize(
293
+ trapp_am_opt_fun,
294
+ args = (gauss_carrier, input_kurtosis),
295
+ x0=[2,0],
296
+ bounds=[(0.1,100), (0,10)]
297
+ )
298
+ carrier = get_trapp_carrier(*optim_res.x, gaussian_carier = gauss_carrier)
299
+ nonstat_sign = _add_am_carrier(gauss_signal, carrier)
300
+ carrier = [carrier]
301
+ # else:
302
+ # if flims is None: raise KeyError('Parameter "flims" must be defined to get a specified spectral kurtosis')
303
+ # opt_settings = {'x0':[2,0], 'bounds':[(0.1,100), (0,10)]}
304
+ # nonstat_sign = _get_banded_am_opt(gauss_signal, T, input_kurtosis, flims, get_trapp_carrier, seed, opt_settings)
305
+
306
+ return nonstat_sign, {'name': 'Gaussian amplitude modulation [-]', 'carrier': carrier}
307
+
308
+ def get_frequency_modulation(
309
+ Sx:np.ndarray,
310
+ SFT: sp.signal.ShortTimeFFT,
311
+ modulation_function:np.ndarray
312
+ ):
313
+ """
314
+ Apply frequency shift modulation to a spectrogram Sx using a given modulation function.
315
+
316
+ Parameters
317
+ ---------
318
+ Sx : np.ndarray
319
+ Input spectrogram (complex-valued).
320
+ SFT : sp.signal.ShortTimeFFT
321
+ Object with STFT/ISTFT functionality and attributes `delta_t`, `f`.
322
+ modulation_function : array-like
323
+ Frequency shift values over time.
324
+
325
+ Returns
326
+ -------
327
+ nonstat_signal : np.ndarray
328
+ Non stationary resulting signal
329
+ carrier : dict
330
+ Dictionary containing "name" of carrier and "carrier" time history
331
+ Source
332
+ ------
333
+ M. Clerc e S. Mallat, «Estimating deformations of stationary processes», Ann. Stat., vol. 31, fasc. 6, dic. 2003, doi: 10.1214/aos/1074290327.
334
+ """
335
+ if not isinstance(Sx,np.ndarray): Sx = np.asarray(Sx)
336
+ base_fft_window = np.pad(np.mean(np.abs(Sx), axis=1),(np.size(Sx,0), np.size(Sx,0)))
337
+ time_bins = np.size(Sx,axis = 1)
338
+ T = (time_bins-1)*SFT.delta_t
339
+ original_times = np.linspace(0,T, len(modulation_function))
340
+ sft_times = np.arange(0,time_bins) * SFT.delta_t
341
+ df_sft = np.mean(np.diff(SFT.f))
342
+ sft_modulation = np.interp(sft_times, original_times, modulation_function)/df_sft
343
+ Sx_angle = np.angle(Sx)
344
+ Sx_angle =np.random.uniform(-np.pi, np.pi, (np.size(Sx,0), np.size(Sx,1)))
345
+ Sx_mag_modulated = np.zeros((np.size(Sx,0), np.size(Sx,1)))
346
+ for i, nf in enumerate(sft_modulation):
347
+ Sx_mag_modulated[:,i] = np.roll(base_fft_window, round(nf))[np.size(Sx,0):2*np.size(Sx,0)]
348
+ Sx_modulated = Sx_mag_modulated*np.exp(1j * Sx_angle)
349
+ nonstat_sign = SFT.istft(Sx_modulated)
350
+ carrier = np.interp(
351
+ np.linspace(0,1,len(nonstat_sign)),
352
+ np.linspace(0,1,len(modulation_function)),
353
+ modulation_function)
354
+ return nonstat_sign, {'name': 'Frequency shift [Hz]', 'carrier': [carrier]}
355
+
356
+
357
+ class NonStationaryNonGaussian(SingleChanSignal):
358
+ """
359
+ Generate a non-stationary, non-Gaussian signal based on a given PSD.
360
+ NonStationaryNonGaussian is a child class of SingleChanSignal.
361
+
362
+ Parameters:
363
+ fpsd : np.ndarray
364
+ Frequency vector for the input PSD.
365
+ psd : np.ndarray
366
+ Power spectral density values.
367
+ T : float
368
+ Total duration of the signal [s].
369
+ fs : float, optional
370
+ Wanted sampling frequency. If None -> 2*fpsd[-1]
371
+ dfpsd : float, optional
372
+ Frequency resolution [Hz]. Default is 0.5.
373
+ method : str, optional
374
+ Modulation method: 'beta_am', 'ray_am', 'trapp_am', or 'fm'. Default is 'beta_am'.
375
+ params : dict, optional
376
+ Dictionary of parameters for the modulation method.
377
+ name : str, optional
378
+ Name of the signal.
379
+ var str, optional
380
+ Variable symbol, e.g., 'x'.
381
+ unit : str, optional
382
+ Signal unit. Default is '$m/s^2$'.
383
+ seed : int, optional
384
+ Random seed for reproducibility.
385
+ interp : str, optional
386
+ PSD interpolation method: 'lin' or 'log'. Default is 'lin'.
387
+ """
388
+ def __init__(
389
+ self,
390
+ fpsd:np.ndarray,
391
+ psd:np.ndarray,
392
+ T:float,
393
+ fs:float = None,
394
+ dfpsd:float = 0.5,
395
+ method:str = 'beta_am',
396
+ params = None,
397
+ name:str="",
398
+ var:str='x',
399
+ unit:str="$m/s^2$",
400
+ seed:int=None,
401
+ interp:str ='lin'):
402
+
403
+ method = method.lower()
404
+ interp = interp.lower()
405
+ interps = {'lin': lin_interp_psd,
406
+ 'log': log_interp_psd}
407
+
408
+ method_existance_check(interp, interps)
409
+
410
+ if fs is None:
411
+ self.fs = fpsd[-1] * 2 # Nyquist frequency
412
+ else:
413
+ self.fs = fs
414
+
415
+ if len(fpsd)!=len(psd):
416
+ raise AttributeError('The frequency vector and the PSD vector are not the same size.')
417
+ self.dfpsd = dfpsd
418
+ self.fpsd,self.psd = interps[interp](fpsd, psd, n_points = int(fpsd[-1]/dfpsd), fs = self.fs)
419
+ self.T = T
420
+ self.x, self.carrier = self._get_nonstationary_timehistory(method = method, params = params, seed_gauss = seed)
421
+ self.signal_type = f'Nonstationary - NonGaussian ({method})'
422
+ super().__init__(
423
+ x = self.x,
424
+ fs = self.fs,
425
+ dfpsd = dfpsd,
426
+ name = name,
427
+ var = var,
428
+ unit = unit,
429
+ signal_type=self.signal_type
430
+ )
431
+
432
+
433
+ def _get_gaussian_timehistory(self, seed = None):
434
+ """
435
+ Internal function called by __init__
436
+ Generate a stationary Gaussian signal from the given PSD.
437
+
438
+ Returns:
439
+ signal : np.ndarray
440
+ The generated Gaussian signal.
441
+ """
442
+ _, signal = get_stationary_gaussian(self.fpsd, self.psd,self.T, seed)
443
+ return signal
444
+
445
+ def _get_nonstationary_timehistory(self, params, method = 'beta_am', seed_gauss = None, seed_mod = None):
446
+ """
447
+ Internal function called by __init__
448
+ Designes a non-stationary process based off the parameters and methods imposed by the user.
449
+
450
+ Parameters:
451
+ params : dict
452
+ Parameters for the modulation method.
453
+ method : str, optional
454
+ Modulation method.
455
+ seed_gauss : int, optional
456
+ Seed for Gaussian signal.
457
+ seed_mod : int, optional
458
+ Seed for modulation carrier.
459
+
460
+ Returns:
461
+ tuple: Non-stationary signal and modulation carrier info.
462
+ """
463
+ stationary_gaussian_signal = self._get_gaussian_timehistory(seed = seed_gauss)
464
+ methods = {'beta_am': get_beta_amplitude_modulation,
465
+ 'ray_am': get_rayleigh_amplitude_modulation,
466
+ 'trapp_am': get_trapp_amplitude_modulation,
467
+ 'fm' : get_frequency_modulation,
468
+ }
469
+ method_existance_check(method, methods)
470
+ print(f'Estimating non-stationary signal from given parameters with "{method}" method')
471
+
472
+ if method in ['fm']:
473
+ nperseg = round(self.fs/2/self.dfpsd)
474
+ self.x = stationary_gaussian_signal
475
+ Sx, SFT = self.get_sftf(nperseg = nperseg, hop = nperseg//2, nargout=2)
476
+ nonstationary_signal, carrier = methods[method](Sx, SFT, modulation_function = params)
477
+ else:
478
+ nonstationary_signal, carrier = methods[method](stationary_gaussian_signal, self.T, **params, seed=seed_mod)
479
+
480
+ output_skewness = sp.stats.skew(nonstationary_signal)
481
+ output_kurtosis = sp.stats.kurtosis(nonstationary_signal)+3
482
+ print(f'Non-stationary signal generated with "{method}" method: skew = {output_skewness:.1f}, kurt = {output_kurtosis:.1f}')
483
+ return nonstationary_signal, carrier
484
+
485
+ def plot_carrier(self, ax = None, ylims = None, xlims = None):
486
+ """
487
+ Plot the modulation carrier used to transform the Gaussian signal.
488
+ Parameters
489
+ ----------
490
+ ax : matplotlib.axes.Axes, optional
491
+ Axis to plot on. Creates a new one if None.
492
+ ylims : tuple, optional
493
+ y-axis limits.
494
+ xlims : tuple, optional
495
+ x-axis limits.
496
+
497
+ Returns
498
+ -------
499
+ ax : matplotlib.axes.Axes
500
+ The axes with the plotted carrier.
501
+ """
502
+ print(f'Plotting Timehistory')
503
+ if ax is None:
504
+ _, ax = plt.subplots(figsize=(10, 4))
505
+ color_list = ['k']+[f'C{i:d}' for i in range(len(self.carrier['carrier']))]
506
+ for i, band_carrier in enumerate(self.carrier['carrier']):
507
+ ax.plot(self.t,band_carrier,color_list[i], label = f"Carrier n.{i+1}")
508
+ ax.set_xlabel("Time [s]")
509
+ ax.set_ylabel(self.carrier['name'])
510
+ ax.legend()
511
+ ax.grid(True,which = 'both')
512
+ ax.minorticks_on()
513
+ ax.set_title(f'Carrier function {self.name}')
514
+ if xlims is not None:
515
+ ax.set_xlim(xlims)
516
+ if ylims is not None:
517
+ ax.set_ylim(ylims)
518
+ return ax
519
+
520
+
521
+ if __name__ == "__main__":
522
+ pass