pyuff 1.0.0__3-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.
fft_tools.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ Tools to work wit fft data (this file is part of the pyFRF package);
3
+ especially, frequency response functions
4
+
5
+ """
6
+
7
+ import numpy as np
8
+
9
+ _FRF_TYPES = {'a': 2, 'v': 1, 'd': 0} # accelerance, mobility, receptance
10
+
11
+ def multiply(ffts, m):
12
+ """Multiplies ffts*m. ffts can be a single fft or an array of ffts.
13
+
14
+ :param ffts: array of fft data
15
+ :param m: multiplication vector
16
+ :return: multiplied array of fft data
17
+ """
18
+ out = np.zeros_like(ffts, dtype='complex')
19
+ if len(np.shape(ffts)) == 2: # list of
20
+ n = np.shape(ffts)[0] # number of frfs
21
+ for j in range(n):
22
+ out[j, :] = ffts[j, :] * m
23
+ else:
24
+ out[:] = ffts[:] * m
25
+
26
+ return out
27
+
28
+
29
+ def frequency_integration(ffts, omega, order=1):
30
+ """Integrates ffts (one or many) in the frequency domain.
31
+
32
+ :param ffts: [rad/s] : angular frequency vector
33
+ :param omega: angular frequency
34
+ :param order: order of integration
35
+ :return: integrated array of fft data
36
+ """
37
+ return multiply(ffts, np.power(-1.j / omega, order))
38
+
39
+
40
+ def frequency_derivation(ffts, omega, order=1):
41
+ """Derivates ffts (one or many) in the frequency domain.
42
+
43
+ :param ffts: array of fft data
44
+ :param omega: [rad/s] angular frequency vector
45
+ :param order: order of derivation
46
+ :return: derivated array of fft data
47
+ """
48
+ with np.errstate(invalid='ignore'):
49
+ return multiply(ffts, np.power(1.j * omega, order))
50
+
51
+ def convert_frf(input_frfs, omega, input_frf_type, output_frf_type):
52
+ """ Converting the frf accelerance/mobility/receptance
53
+
54
+ The most general case is when `input_frfs` is of shape:
55
+ `nr_inputs` * `nr_outputs` * `frf_len`
56
+
57
+ :param input_frfs: frequency response function vector (of dim 1, 2 or 3)
58
+ :param omega: [rad/s] angular frequency vector
59
+ :param input_frf_type: 'd' receptance, 'v' mobility, 'a' accelerance (of dim 0, 1, 2)
60
+ :param output_frf_type: 'd' receptance, 'v' mobility, 'a' accelerance (of dim 0, 1, 2)
61
+ :return: frequency response function vector (of dim 1, 2 or 3)
62
+ """
63
+ # put all data to 3D frf type (nr_inputs * nr_outputs * frf_len)
64
+ ini_shape = input_frfs.shape
65
+ if 1 <=len(ini_shape) > 3 :
66
+ raise Exception('Input frf should be if dimension 3 or smaller')
67
+ elif len(ini_shape) == 2:
68
+ input_frfs = np.expand_dims(input_frfs, axis=0)
69
+
70
+ if type(input_frf_type) == str:
71
+ input_frf_type = [ini_shape[0]*[input_frf_type]]
72
+ else:
73
+ input_frf_type = [input_frf_type]
74
+
75
+ if type(output_frf_type) == str:
76
+ output_frf_type = [ini_shape[0]*[output_frf_type]]
77
+ else:
78
+ output_frf_type = [output_frf_type]
79
+ elif len(ini_shape) == 1:
80
+ input_frfs = np.expand_dims(np.expand_dims(input_frfs, axis=0), axis=0)
81
+ input_frf_type = [[input_frf_type]]
82
+ output_frf_type = [[output_frf_type]]
83
+
84
+ # reshaping of frfs
85
+ (nr_inputs, nr_outputs, frf_len) = input_frfs.shape
86
+ nr_frfs = nr_inputs * nr_outputs
87
+ input_frfs = input_frfs.reshape(nr_frfs,-1)
88
+
89
+ # reshaping of input and output frf types
90
+ input_frf_type = np.asarray(input_frf_type)
91
+ output_frf_type = np.asarray(output_frf_type)
92
+ if len(input_frf_type.shape) != 2 or len(output_frf_type.shape) !=2:
93
+ raise Exception('Input and output frf type should be of dimension 2.')
94
+ input_frf_type = input_frf_type.flatten()
95
+ output_frf_type = output_frf_type.flatten()
96
+ if len(input_frf_type) != nr_frfs or len(output_frf_type) != nr_frfs:
97
+ raise Exception('Input and output frf type length should correspond to the number frfs.')
98
+
99
+ try:
100
+ input_frf_type = [_FRF_TYPES[_] for _ in input_frf_type]
101
+ output_frf_type = [_FRF_TYPES[_] for _ in output_frf_type]
102
+ except:
103
+ raise('Only frf types: d, v and a are supported.')
104
+
105
+ # do the conversion
106
+ output_frfs = np.zeros_like(input_frfs)
107
+ for i in range(nr_frfs):
108
+ order = output_frf_type[i] - input_frf_type[i]
109
+ if (order > 2) or (order <-2):
110
+ raise Exception('FRF conversion not supported.')
111
+ output_frfs[i, :] = frequency_derivation(input_frfs[i, :], omega, order=order)
112
+
113
+ #reshape back to original shape
114
+ if len(ini_shape) == 3:
115
+ return output_frfs.reshape((nr_inputs, nr_outputs, -1))
116
+ elif len(ini_shape) == 2:
117
+ return output_frfs.reshape((nr_outputs, -1))
118
+ elif len(ini_shape) == 1:
119
+ return output_frfs[0]
120
+
121
+
122
+ def correct_time_delay(fft, w, time_delay):
123
+ """
124
+ Corrects the ``fft`` with regards to the ``time_delay``.
125
+
126
+ :param fft: fft array
127
+ :param w: angular frequency [rad/s]
128
+ :param time_delay: time dalay in seconds
129
+ :return: corrected fft array
130
+ """
131
+ return fft / (np.exp(1j * w * time_delay))
132
+
133
+
134
+ def PSD(x, dt=1):
135
+ """ Power spectral density
136
+
137
+ :param x: time domain data
138
+ :param dt: delta time
139
+ :return: PSD, freq
140
+ """
141
+ X = np.fft.rfft(x)
142
+ freq = np.fft.rfftfreq(len(x), d=dt)
143
+ X = 2 * dt * np.abs(X.conj() * X)/ len(x)
144
+
145
+ return X, freq
146
+
147
+
148
+ def fft_adjusted_lower_limit(x, lim, nr):
149
+ """
150
+ Compute the fft of complex matrix x with adjusted summation limits:
151
+
152
+ y(j) = sum[k=-n-1, -n-2, ... , -low_lim-1, low_lim, low_lim+1, ... n-2,
153
+ n-1] x[k] * exp(-sqrt(-1)*j*k* 2*pi/n),
154
+ j = -n-1, -n-2, ..., -low_limit-1, low_limit, low_limit+1, ... n-2, n-1
155
+
156
+ :param x: Single-sided complex array to Fourier transform.
157
+ :param lim: lower limit index of the array x.
158
+ :param nr: number of points of interest
159
+ :return: Fourier transformed two-sided array x with adjusted lower limit.
160
+ Retruns [0, -1, -2, ..., -nr+1] and [0, 1, 2, ... , nr-1] values.
161
+
162
+ """
163
+ nf = 2 * (len(x) - lim) - 1
164
+
165
+ n = np.arange(-nr + 1, nr)
166
+
167
+ a = np.fft.fft(x, n=nf).real[n]
168
+ b = np.fft.fft(x[:lim], n=nf).real[n]
169
+ c = x[lim].conj() * np.exp(1j * 2 * np.pi * n * lim / nf)
170
+
171
+ res = 2 * (a - b) - c
172
+
173
+ return res[:nr][::-1], res[nr - 1:]
174
+
175
+
176
+ def check_fft_for_speed(data_length, exception_if_prime_above=20):
177
+ """To avoid slow FFT, raises an exception if largest prime above `exception_if_prime_above`.
178
+
179
+ See: http://stackoverflow.com/questions/23287/largest-prime-factor-of-a-number/
180
+
181
+ :param data_length: length of data for frf
182
+ :param exception_if_prime_above: raise exception if the largest prime number is above
183
+ :return: none
184
+ """
185
+
186
+ def prime_factors(n):
187
+ """Returns all prime factors of a positive integer
188
+
189
+ See: http://stackoverflow.com/questions/23287/largest-prime-factor-of-a-number/412942#412942
190
+
191
+ :param n: lenght
192
+ :return: array of prime numbers
193
+ """
194
+ factors = []
195
+ d = 2
196
+ while n > 1:
197
+ while n % d == 0:
198
+ factors.append(d)
199
+ n /= d
200
+ d += 1
201
+ if d * d > n:
202
+ if n > 1:
203
+ factors.append(n)
204
+ break
205
+ return factors
206
+
207
+ if np.max(prime_factors(data_length)) > exception_if_prime_above:
208
+ raise Exception('Change the number of time/frequency points or the FFT will run slow.')
209
+
210
+
211
+ def irfft_adjusted_lower_limit(x, low_lim, indices):
212
+ """
213
+ Compute the ifft of real matrix x with adjusted summation limits:
214
+
215
+ y(j) = sum[k=-n-2, ... , -low_lim-1, low_lim, low_lim+1, ... n-2,
216
+ n-1] x[k] * exp(sqrt(-1)*j*k* 2*pi/n),
217
+ j =-n-2, ..., -low_limit-1, low_limit, low_limit+1, ... n-2, n-1
218
+
219
+ :param x: Single-sided real array to Fourier transform.
220
+ :param low_lim: lower limit index of the array x.
221
+ :param indices: list of indices of interest
222
+ :return: Fourier transformed two-sided array x with adjusted lower limit.
223
+ Retruns values.
224
+ """
225
+
226
+ nf = 2 * (x.shape[1] - 1)
227
+ a = (np.fft.irfft(x, n=nf)[:, indices]) * nf
228
+ b = (np.fft.irfft(x[:, :low_lim], n=nf)[:, indices]) * nf
229
+ return a - b
230
+
231
+
232
+ if __name__ == '__main__':
233
+ plot_figure = False
234
+ # check_fft_for_speed(4) #fast
235
+ # check_fft_for_speed(59612) #slow
pyFRF.py ADDED
@@ -0,0 +1,988 @@
1
+ """
2
+
3
+ Module for FRF signal processing.
4
+
5
+ Classes:
6
+ class FRF: Frequency response function.
7
+ """
8
+
9
+ import numpy as np
10
+ import scipy
11
+ import warnings
12
+
13
+ __version__ = '1.0'
14
+ _EXC_TYPES = ['f', 'a', 'v', 'd', 'e'] # force for EMA and kinematics for OMA
15
+ _RESP_TYPES = ['a', 'v', 'd', 'e'] # acceleration, velocity, displacement, strain
16
+ _FRF_TYPES = ['H1', 'H2', 'Hv', 'ODS']
17
+ _FRF_FORM = ['accelerance', 'mobility', 'receptance']
18
+ _WGH_TYPES = ['linear', 'exponential']
19
+ _WINDOWS = ['none', 'force', 'exponential', 'hann', 'hamming', 'bartlett', 'blackman', 'kaiser']
20
+
21
+ _DIRECTIONS = ['scalar', '+x', '+y', '+z', '-x', '-y', '-z']
22
+ _DIRECTIONS_NR = [0, 1, 2, 3, -1, -2 - 3]
23
+
24
+
25
+
26
+ def direction_dict():
27
+ dir_dict = {a: b for a, b in zip(_DIRECTIONS, _DIRECTIONS_NR)}
28
+ return dir_dict
29
+
30
+ class FRF:
31
+ """
32
+ Perform Dual Channel Spectral Analysis
33
+
34
+ """
35
+
36
+ def __init__(self, sampling_freq,
37
+ exc=None,
38
+ resp=None,
39
+ exc_type='f', resp_type='a',
40
+ window='none',
41
+ resp_delay=0.,
42
+ weighting='linear',
43
+ fft_len=None,
44
+ nperseg=None,
45
+ noverlap=None,
46
+ archive_time_data=False,
47
+ frf_type='H1',
48
+ copy=True,
49
+ **kwargs):
50
+ """
51
+ Initiates the Data class:
52
+
53
+ :param sampling_freq: Sampling frequency of excitation and response signals.
54
+ int - same sampling frequency for both excitation and response signals.
55
+ tuple, list - different sampling frequencies for excitation and reponse signals.
56
+ First element represents excitation, second element represents response sampling frequency.
57
+ :type sampling_freq: int, tuple(int), list[int]
58
+ :param exc: Excitation array.
59
+ A 3D, 2D or 1D ndarray:
60
+ 3D (general shape, allows multiple inputs (MIMO, MISO)): `(number_of_measurements, excitation_DOFs, time_series)`.
61
+ 2D (multiple measurements, single input): `(number_of_measurements, time_series)`.
62
+ 1D (single measurement, single input): `(time_series)`.
63
+ :type exc: ndarray
64
+ :param resp: Response array.
65
+ A 3D, 2D or 1D ndarray:
66
+ 3D (general shape, allows, multiple outputs): `(number_of_measurements, response_DOFs, time_series)`.
67
+ 2D (multiple measurements, single output): `(number_of_measurements, time_series)`.
68
+ 1D (single measurement, single output): `(time_series)`.
69
+ :type resp: ndarray
70
+ :param exc_type: Excitation type, see _EXC_TYPES.
71
+ :type exc_type: str
72
+ :param resp_type: Response type, see _RESP_TYPES.
73
+ :type resp_type: str
74
+ :param window: Scipy window used for cross power spectral desnity computation or
75
+ excitation and reponse signals. see _WINDOWS.
76
+ str - used for csd.
77
+ tuple, list - first element is excitation signal window, second element is response signal window
78
+ :type window: str, tuple(str), list[str]
79
+ :param resp_delay: Response time delay (in seconds) with regards to the excitation.
80
+ :type resp_delay: float
81
+ :param weighting: weighting type used for averaging with continous measurements - see _WGH_TYPES.
82
+ If exponential weighting is used, specify the number of averages - example: 'exponential:5'
83
+ :type weighting: str
84
+ :param fft_len: The length of the FFT (zero-padding if longer than length of data),
85
+ If None, freq length matches time length.
86
+ :type fft_len: int
87
+ :param nperseg: Optional segment length, by default one segment (data length) is analyzed.
88
+ :type nperseg: int
89
+ :param noverlap: Optional segment overlap, by default ``noverlap = nperseg // 2``.
90
+ :type noverlap: int
91
+ :param archive_time_data: Archive the time data (this can consume a lot of memory).
92
+ :type archive_time_data: bool
93
+ :param frf_type: Default frf type returned at self.get_frf(), see _FRF_TYPES.
94
+ :type frf_type: str
95
+ :param copy: If true the excitation and response arrays are copied
96
+ (if data is not copied the applied window affects the source arrays).
97
+ :type copy: bool
98
+ """
99
+ # previous pyFRF kwargs:
100
+ if ("exc_window" in kwargs) or ("resp_window" in kwargs):
101
+ raise ValueError("The exc_window argument is no longer supported, use window argumet instead an see documentation.")
102
+ if ("n_averages" in kwargs):
103
+ raise ValueError("The n_averages argument is no longer supported. You can pass different array shapes of excitation "\
104
+ "and response data or provide N for exponential averaging through weighting argument.")
105
+
106
+
107
+ # sampling_freq info:
108
+ if isinstance(sampling_freq, int):
109
+ self.sampling_freq = sampling_freq
110
+ self.exc_sampling_freq = sampling_freq
111
+ self.resp_sampling_freq = sampling_freq
112
+ elif isinstance(sampling_freq, tuple) or isinstance(sampling_freq, list):
113
+ if len(sampling_freq) != 2:
114
+ raise Exception('''If parameter sampling_freq is passed as tuple or list, the length of it must be 2:
115
+ first element is excitation sampling frequency, second element is response sampling frequency''')
116
+ if isinstance(sampling_freq[0], int) and isinstance(sampling_freq[1], int):
117
+ self.exc_sampling_freq = sampling_freq[0]
118
+ self.resp_sampling_freq = sampling_freq[1]
119
+ # take the lower sampling rate, the signal with higher sampling rate will be downsampled to lower
120
+ if sampling_freq[0] >= sampling_freq[1]:
121
+ self.sampling_freq = sampling_freq[1]
122
+ else:
123
+ self.sampling_freq = sampling_freq[0]
124
+ else:
125
+ raise Exception('Both elements of tuple or list must be int type!')
126
+ else:
127
+ raise Exception('Invalid argument type for parameter sampling_freq! int, tuple and list supported')
128
+
129
+ # data info:
130
+ self._data_available = False
131
+ self.exc_type = exc_type
132
+ self.resp_type = resp_type
133
+ self.resp_delay = resp_delay
134
+
135
+ if isinstance(window, str):
136
+ self.csd_window = window.lower()
137
+ self.exc_window = 'none'
138
+ self.resp_window = 'none'
139
+ elif isinstance(window, tuple) or isinstance(window, list):
140
+ if len(window) != 2:
141
+ raise Exception('''If parameter window is passed as tuple or list, the length of it must be 2:
142
+ first element is excitation window, second element is response window''')
143
+ if isinstance(window[0], str) and isinstance(window[1], str):
144
+ self.csd_window = 'none'
145
+ self.exc_window = window[0].lower()
146
+ self.resp_window = window[1].lower()
147
+ else:
148
+ raise Exception('Both elements of tuple or list must be string type!')
149
+ else:
150
+ raise Exception('Invalid argument type for parameter window! string, tuple and list supported')
151
+
152
+
153
+ if self.resp_window.split(':')[0] == 'force':
154
+ #print("force window used on response signal")
155
+ warnings.warn("force window used on response signal")
156
+ if self.csd_window.split(':')[0] == 'force':
157
+ #print("force window used with csd")
158
+ warnings.warn("force window used with csd")
159
+
160
+ self.frf_type = frf_type
161
+ self.copy = copy
162
+
163
+ # ini
164
+ self.exc = np.array([])
165
+ self.resp = np.array([])
166
+ self.exc_archive = []
167
+ self.resp_archive = []
168
+ self.samples = None
169
+
170
+ # set averaging and weighting
171
+ self.weighting = weighting
172
+ self.frf_norm = 1.
173
+
174
+ if exc is not None:
175
+ if len(exc.shape) == 1:
176
+ exc = np.expand_dims(exc, (0,1))
177
+ if len(exc.shape) == 2:
178
+ exc = np.expand_dims(exc, 1)
179
+ if resp is not None:
180
+ if len(resp.shape) == 1:
181
+ resp = np.expand_dims(resp, (0,1))
182
+ if len(resp.shape) == 2:
183
+ resp = np.expand_dims(resp, 1)
184
+
185
+ # reshape if signals are added at the beginning (at object initialization) and if different sampling_freqs are specified:
186
+ # both signals have to last the same amount of time
187
+ if exc is not None and resp is not None:
188
+ if (self.exc_sampling_freq != self.resp_sampling_freq) and (exc.shape[2] != resp.shape[2]):
189
+ if self.exc_sampling_freq > self.resp_sampling_freq:
190
+ exc = scipy.signal.resample(exc, resp.shape[2], axis=2) # downsample excitation signal
191
+ elif self.resp_sampling_freq > self.exc_sampling_freq:
192
+ resp = scipy.signal.resample(resp, exc.shape[2], axis=2) # downsample response signal
193
+ # fft length
194
+ if fft_len is None:
195
+ if exc is None:
196
+ raise Exception('If no excitaton signal is given, fft_len needs to be defined!')
197
+ self.fft_len = exc.shape[2]
198
+ else:
199
+ self.fft_len = int(fft_len)
200
+ self.nperseg = nperseg
201
+ self.noverlap = noverlap
202
+
203
+ # save time data
204
+ self.archive_time_data = archive_time_data
205
+
206
+ # error checking
207
+ if not (self.frf_type in _FRF_TYPES):
208
+ raise Exception('wrong FRF type given %s (can be %s)'
209
+ % (self.frf_type, _FRF_TYPES))
210
+
211
+ if not (self.weighting.split(':')[0] in _WGH_TYPES):
212
+ raise Exception('wrong weighting type given %s (can be %s)'
213
+ % (self.weighting, _WGH_TYPES))
214
+
215
+ if not (self.exc_type in _EXC_TYPES):
216
+ raise Exception('wrong excitation type given %s (can be %s)'
217
+ % (self.exc_type, _EXC_TYPES))
218
+
219
+ if not (self.resp_type in _RESP_TYPES):
220
+ raise Exception('wrong response type given %s (can be %s)'
221
+ % (self.resp_type, _RESP_TYPES))
222
+
223
+ if not (self.csd_window.split(':')[0] in _WINDOWS):
224
+ raise Exception('wrong csd window type given %s (can be %s)'
225
+ % (self.exc_window, _WINDOWS))
226
+
227
+ if not (self.exc_window.split(':')[0] in _WINDOWS):
228
+ raise Exception('wrong excitation window type given %s (can be %s)'
229
+ % (self.exc_window, _WINDOWS))
230
+
231
+ if not (self.resp_window.split(':')[0] in _WINDOWS):
232
+ raise Exception('wrong response window type given %s (can be %s)'
233
+ % (self.resp_window, _WINDOWS))
234
+
235
+ self.total_meas = int(0) # total number of all measurements
236
+ self.ntimes_meas_added = int(0) # the number of times we added new measuremenets
237
+
238
+ if exc is not None and resp is not None:
239
+ self.add_data(exc, resp)
240
+
241
+
242
+ def add_data(self, exc, resp):
243
+ """
244
+ Adds new data - called at object creation if excitation and response signals are given.
245
+ Used also for continous data adding.
246
+
247
+ :param exc: Excitation array.
248
+ :param resp: Response array.
249
+ :return: True if data is added.
250
+ """
251
+ if len(exc.shape) == 1:
252
+ exc = np.expand_dims(exc, (0,1))
253
+ if len(exc.shape) == 2:
254
+ exc = np.expand_dims(exc, 1)
255
+
256
+ if len(resp.shape) == 1:
257
+ resp = np.expand_dims(resp, (0,1))
258
+ if len(resp.shape) == 2:
259
+ resp = np.expand_dims(resp, 1)
260
+
261
+ # reshape if signals are added later and if different sampling_freqs are specified at the beginning:
262
+ if (self.exc_sampling_freq != self.resp_sampling_freq) and (exc.shape[2] != resp.shape[2]):
263
+ if self.exc_sampling_freq > self.resp_sampling_freq:
264
+ exc = scipy.signal.resample(exc, resp.shape[2], axis=2) # downsample excitation signal
265
+ elif self.resp_sampling_freq > self.exc_sampling_freq:
266
+ resp = scipy.signal.resample(resp, exc.shape[2], axis=2) # downsample response signal
267
+
268
+ self._add_to_archive(exc, resp)
269
+
270
+ if self.copy:
271
+ self.exc = np.copy(exc)
272
+ self.resp = np.copy(resp)
273
+ else:
274
+ self.exc = exc
275
+ self.resp = resp
276
+
277
+ samples = exc.shape[2]
278
+ self.fft_len_cutoff = exc.shape[2]
279
+ if self.nperseg is None:
280
+ self.nperseg = samples
281
+ if self.nperseg > self.fft_len: # for less fft points and no averaging in csd
282
+ self.fft_len_cutoff = self.fft_len
283
+ self.nperseg = self.fft_len
284
+ elif self.nperseg > samples:
285
+ raise ValueError('nperseg must be less than samples.')
286
+ if self.nperseg > self.fft_len:
287
+ raise ValueError('fft_len must be greater than or equal to nperseg when specifying both.')
288
+ if self.noverlap is None:
289
+ self.noverlap = self.nperseg // 2
290
+ elif self.noverlap >= self.nperseg:
291
+ raise ValueError('noverlap must be less than nperseg.')
292
+
293
+ self._ini_lengths_and_csd_window(self.nperseg)
294
+ self._ini_exc_and_resp_window()
295
+ self._apply_exc_and_resp_window()
296
+
297
+ self.total_meas += exc.shape[0]
298
+ self.ntimes_meas_added += 1
299
+ #print("number of new measurements added: ", exc.shape[0])
300
+ #print("current measurement in row (total measurements): ", self.total_meas)
301
+ #print("number of times we added new measuremets: ", self.ntimes_meas_added)
302
+
303
+ self._get_frf_av()
304
+ self._data_available = True
305
+
306
+ # accelerance/mobility/receptance conversion frf multiplication factor:
307
+ if self.resp_type == 'a':
308
+ self.frf_conversion = np.power(-1.j / self.get_w_axis(), 2)
309
+ elif self.resp_type == 'v':
310
+ self.frf_conversion = np.power(-1.j / self.get_w_axis(), 1)
311
+ else:
312
+ self.frf_conversion = np.ones(self.get_w_axis().shape)
313
+
314
+ return True
315
+
316
+ def is_data_ok(self, exc, resp,
317
+ overflow_samples=3,
318
+ double_impact_limit=1e-3, verbose=0):
319
+ """
320
+ Checks the data for overflow and double-impacts.
321
+
322
+ :param exc: Excitation array.
323
+ :param resp: Response array.
324
+ :param overflow_samples: Number of samples that need to be equal to max for overflow identification.
325
+ :type overflow_samples: int
326
+ :param double_impact_limit: Ratio of freqency content of the double vs single hit.
327
+ Smaller number means more sensitivity
328
+ :type double_impact_limit: float
329
+ :param verbose: Prints overflow and double impact status. 0 or 1.
330
+ :type verbose: int
331
+ :return: True if data is ok, False if there is overflow or double impact present.
332
+ :rtype: bool
333
+ """
334
+ if exc.ndim != resp.ndim:
335
+ raise Exception("excitation and response array should be the same number of dimensions!")
336
+
337
+ overflow_exc = self._is_overflow(exc, overflow_samples=overflow_samples, verbose=verbose)
338
+ overflow_resp = self._is_overflow(resp, overflow_samples=overflow_samples, verbose=verbose)
339
+ double_impact = self._is_double_impact(exc, limit=double_impact_limit, verbose=verbose)
340
+
341
+ if exc.ndim == 3:
342
+ for measurement_num in range(exc.shape[0]):
343
+ for dof_num in range(exc.shape[1]):
344
+ if overflow_exc[measurement_num, dof_num] or \
345
+ overflow_resp[measurement_num, dof_num] or \
346
+ double_impact[measurement_num, dof_num]:
347
+ return False
348
+ return True
349
+ elif exc.ndim == 2:
350
+ for measurement_num in range(exc.shape[0]):
351
+ if overflow_exc[measurement_num] or \
352
+ overflow_resp[measurement_num] or \
353
+ double_impact[measurement_num]:
354
+ return False
355
+ return True
356
+ elif exc.ndim == 1:
357
+ if overflow_exc or overflow_resp or double_impact:
358
+ return False
359
+ else:
360
+ return True
361
+ else:
362
+ raise Exception('Incorrect input array shape!')
363
+
364
+
365
+ def _apply_exc_and_resp_window(self):
366
+ """
367
+ Apply windows to excitation and response signals.
368
+
369
+ :return:
370
+ """
371
+
372
+ self.exc *= self.exc_window_data
373
+ self.resp *= self.resp_window_data
374
+
375
+ def _get_window_sub(self, length, window='none'):
376
+ """
377
+ Returns the window time series and amplitude normalization term.
378
+
379
+ :param length: Length of window.
380
+ :type length: int
381
+ :param window: Window type.
382
+ :type window: str
383
+ :return: Window ndarray, amplitude norm
384
+ """
385
+ #print("window: ", window)
386
+ window = window.split(':')
387
+
388
+ if window[0] in ['hanning', 'hann']:
389
+ w = np.hanning(length)
390
+ elif window[0] in ['hamming']:
391
+ w = np.hamming(length)
392
+ elif window[0] in ['bartlett']:
393
+ w = np.bartlett(length)
394
+ elif window[0] in ['blackman']:
395
+ w = np.blackman(length)
396
+ elif window[0] in ['kaiser']:
397
+ beta = float(window[1])
398
+ w = np.kaiser(length, beta)
399
+ elif window[0] == 'force':
400
+ w = np.zeros(length)
401
+ force_window = float(window[1])
402
+ to1 = int(force_window * length)
403
+ w[:to1] = 1.
404
+ elif window[0] == 'exponential':
405
+ w = np.arange(length)
406
+ exponential_window = float(window[1])
407
+ w = np.exp(np.log(exponential_window) * w / (length - 1))
408
+ elif window[0] in ['none']:
409
+ w = np.ones(length)
410
+
411
+ if window[0] == 'force':
412
+ amplitude_norm = 2 / len(w)
413
+ else:
414
+ amplitude_norm = 2 / np.sum(w)
415
+
416
+ return w, amplitude_norm
417
+
418
+
419
+ # OK:
420
+ def get_f_axis(self):
421
+ """
422
+ Get frequency series.
423
+
424
+ :return: Frequency ndarray in Hz.
425
+ :rtype: ndarray
426
+ """
427
+ if not self._data_available:
428
+ raise Exception('No data has been added yet!')
429
+
430
+ return np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq)
431
+
432
+ # OK:
433
+ def get_w_axis(self):
434
+ """
435
+ Get angular freqency series.
436
+
437
+ :return: Frequency ndarray in rad/s.
438
+ :rtype: ndarray
439
+ """
440
+ if not self._data_available:
441
+ raise Exception('No data has been added yet!')
442
+
443
+ return 2 * np.pi * self.get_f_axis()
444
+
445
+ # OK:
446
+ def get_df(self):
447
+ """
448
+ Get delta frequency.
449
+
450
+ :return: Delta frequency in Hz.
451
+ :rtype: float
452
+ """
453
+ if not self._data_available:
454
+ raise Exception('No data has been added yet!')
455
+
456
+ return self.get_f_axis()[1]
457
+
458
+ # OK:
459
+ def get_t_axis(self):
460
+ """
461
+ Get time axis.
462
+
463
+ :return: Time axis ndarray.
464
+ :rtype: ndarray
465
+ """
466
+
467
+ if not self._data_available:
468
+ raise Exception('No data has been added yet!')
469
+
470
+ return np.arange(self.samples) / self.sampling_freq
471
+
472
+
473
+ def _get_frf_av(self):
474
+ """
475
+ Calculates the averaged FRF based on averaging and weighting type, using scipy.csd.
476
+
477
+
478
+ Literature:
479
+ [1] Haylen, Lammens, Sas: ISMA 2011 Modal Analysis Theory and Testing page: A.2.27
480
+ [2] http://zone.ni.com/reference/en-XX/help/371361E-01/lvanlsconcepts/average_improve_measure_freq/
481
+
482
+ :return:
483
+ """
484
+ # obtain cross and auto spectra for current data
485
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
486
+ S_XX = np.zeros((self.resp.shape[1], self.resp.shape[1], freq_len), dtype="complex128")
487
+ S_FF = np.zeros((self.exc.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
488
+ S_XF = np.zeros((self.resp.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
489
+ S_FX = np.zeros((self.exc.shape[1], self.resp.shape[1], freq_len), dtype="complex128")
490
+
491
+ #print("newly added measurements (csd calculating): ", self.resp.shape[0])
492
+
493
+ #print("nperseg: ", self.nperseg)
494
+ #print("samples: ", self.samples)
495
+ #print("noverlap: ", self.noverlap)
496
+ #print("fft_len: ", self.fft_len)
497
+
498
+ for k in range(self.resp.shape[0]): # for each measurement
499
+ for i in range(S_XX.shape[0]):
500
+ for j in range(S_XX.shape[1]):
501
+ S_XX[i,j] += scipy.signal.csd(self.resp[k][i][:self.fft_len_cutoff], self.resp[k][j][:self.fft_len_cutoff],
502
+ self.sampling_freq, window=self.csd_window_data, nperseg=self.nperseg,
503
+ noverlap=self.noverlap, nfft=self.fft_len)[1]
504
+ for i in range(S_FF.shape[0]):
505
+ for j in range(S_FF.shape[1]):
506
+ S_FF[i,j] += scipy.signal.csd(self.exc[k][i][:self.fft_len_cutoff], self.exc[k][j][:self.fft_len_cutoff],
507
+ self.sampling_freq, window=self.csd_window_data, nperseg=self.nperseg,
508
+ noverlap=self.noverlap, nfft=self.fft_len)[1]
509
+ for i in range(S_XF.shape[0]):
510
+ for j in range(S_XF.shape[1]):
511
+ S_XF[i,j] += scipy.signal.csd(self.resp[k][i][:self.fft_len_cutoff], self.exc[k][j][:self.fft_len_cutoff],
512
+ self.sampling_freq, window=self.csd_window_data, nperseg=self.nperseg,
513
+ noverlap=self.noverlap, nfft=self.fft_len)[1]
514
+ for i in range(S_FX.shape[0]):
515
+ for j in range(S_FX.shape[1]):
516
+ S_FX[i,j] += scipy.signal.csd(self.exc[k][i][:self.fft_len_cutoff], self.resp[k][j][:self.fft_len_cutoff],
517
+ self.sampling_freq, window=self.csd_window_data, nperseg=self.nperseg,
518
+ noverlap=self.noverlap, nfft=self.fft_len)[1]
519
+
520
+ if self.ntimes_meas_added == 1: # if the measurements are added for the first time:
521
+ #print(self.ntimes_meas_added, " time the measurements were added - csd")
522
+ self.S_XX = S_XX / self.resp.shape[0]
523
+ self.S_FF = S_FF / self.resp.shape[0]
524
+ self.S_XF = S_XF / self.resp.shape[0]
525
+ self.S_FX = S_FX / self.resp.shape[0]
526
+ else: # proportionaly add to average, based on total number of measurements and number of newly added measurements
527
+ num_new_meas = self.resp.shape[0] # number of newly added measurements
528
+ #print(self.ntimes_meas_added, " time the measurements were added - csd")
529
+ weighting = self.weighting.split(':')
530
+ if weighting[0] == 'linear':
531
+ #print("linear")
532
+ N = np.float64(self.total_meas)
533
+ elif weighting[0] == 'exponential':
534
+ #print("exponential")
535
+ if len(weighting) == 1: # not specified N of averages for exponential averaging
536
+ N_exp = np.float64(self.total_meas) # same as linear
537
+ elif weighting[1] == '': # not specified N of averages for exponential averaging
538
+ N_exp = np.float64(self.total_meas) # same as linear
539
+ else:
540
+ N_exp = int(weighting[1])
541
+ #print(N_exp)
542
+ if self.total_meas >= N_exp:
543
+ N = np.float64(N_exp)
544
+ else:
545
+ N = np.float64(self.total_meas)
546
+ else:
547
+ raise Exception("Incorrect weighting type!")
548
+
549
+ #print("N: ", N)
550
+
551
+ self.S_XX = num_new_meas / N * S_XX + (N - num_new_meas) / N * self.S_XX
552
+ self.S_FF = num_new_meas / N * S_FF + (N - num_new_meas) / N * self.S_FF
553
+ self.S_XF = num_new_meas / N * S_XF + (N - num_new_meas) / N * self.S_XF
554
+ self.S_FX = num_new_meas / N * S_FX + (N - num_new_meas) / N * self.S_FX
555
+
556
+
557
+ def get_ods_frf(self):
558
+ """
559
+ Operational deflection shape averaged estimator.
560
+
561
+ Numerical implementation of Equation (6) in [1].
562
+
563
+ Literature:
564
+ [1] Schwarz, Brian, and Mark Richardson. Measurements required for displaying
565
+ operating deflection shapes. Presented at IMAC XXII January 26 (2004): 29.
566
+
567
+ :return: ODS FRF estimator.
568
+ :rtype: ndarray
569
+ """
570
+
571
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
572
+ ods_frf = np.zeros((self.resp.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
573
+
574
+ if self.exc.shape[1] != 1: # MISO, MIMO
575
+ raise Exception("Currently not implemented for MISO and MIMO systems.")
576
+ else: # SISO, SIMO
577
+ for i in range(self.resp.shape[1]):
578
+ ods_frf[i,:,:] = np.sqrt(self.S_XX[i,i,:]) * self.S_XF[i,:,:] / np.abs(self.S_XF[i,:,:])
579
+ return ods_frf * self.frf_conversion
580
+
581
+
582
+ def get_resp_spectrum(self, amplitude_spectrum=True):
583
+ """
584
+ Get response amplitude/power spectrum.
585
+
586
+ :param amplitude_spectrum: True - get amplitude spectrum, False - power spectrum.
587
+ :return: Response spectrum.
588
+ :rtype: ndarray
589
+ """
590
+
591
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
592
+ amp = np.zeros((self.resp.shape[1], freq_len), dtype="complex128")
593
+
594
+ if self.exc.shape[1] != 1: # MISO, MIMO
595
+ raise Exception("Currently not implemented for MIMO systems.")
596
+ else: # SISO, SIMO
597
+ k = self.resp_window_amp_norm
598
+
599
+ for i in range(self.resp.shape[1]):
600
+ amp[i,:] = np.sqrt(np.abs(self.S_XX[i,i,:]))
601
+
602
+ if amplitude_spectrum:
603
+ return k * amp * self.frf_conversion
604
+ else:
605
+ return k * (amp * self.frf_conversion) ** 2
606
+
607
+
608
+ def get_exc_spectrum(self, amplitude_spectrum=True):
609
+ """
610
+ Get excitation amplitude/power spectrum.
611
+
612
+ :param amplitude_spectrum: True - get amplitude spectrum, False - power spectrum.
613
+ :return: Excitation spectrum.
614
+ :rtype: ndarray
615
+ """
616
+
617
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
618
+ amp = np.zeros((self.exc.shape[1], freq_len), dtype="complex128")
619
+
620
+ if self.exc.shape[1] != 1: # MISO, MIMO
621
+ raise Exception("Currently not implemented for MIMO systems.")
622
+ else: # SISO, SIMO
623
+ k = self.exc_window_amp_norm
624
+
625
+ amp[0,:] = np.sqrt(np.abs(self.S_FF[0,0,:]))
626
+
627
+ if amplitude_spectrum:
628
+ return k * amp
629
+ else:
630
+ return k * amp**2
631
+
632
+
633
+ def get_H1(self):
634
+ """
635
+ Get H1 FRF averaged estimator (receptance), preferable call via get_FRF().
636
+
637
+ :return: H1 FRF estimator matrix (ndarray) of shape (response DOF, excitation DOF, freqency points).
638
+ :rtype: ndarray
639
+ """
640
+ with np.errstate(invalid='ignore'):
641
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
642
+ H1 = np.zeros((self.resp.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
643
+
644
+ if self.exc.shape[1] == 1: # SISO, SIMO
645
+ #print("single input")
646
+ H1 = self.frf_norm * self.S_XF / self.S_FF
647
+ else: # MISO, MIMO
648
+ if (self.S_FF[:,:,0].shape == (2,2)) or (self.S_FF[:,:,0].shape == (3,3)):
649
+ for i in range(freq_len):
650
+ H1[:,:,i] = self.frf_norm * (self.S_XF[:,:,i] @ self._analytical_matrix_inverse(self.S_FF[:,:,i]))
651
+ else:
652
+ for i in range(freq_len):
653
+ H1[:,:,i] = self.frf_norm * (self.S_XF[:,:,i] @ np.linalg.inv(self.S_FF[:,:,i]))
654
+ return (H1 * self.frf_conversion) / self._correct_time_delay()
655
+
656
+ # OK:
657
+ def get_H2(self):
658
+ """
659
+ H2 FRF averaged estimator (receptance), preferable call via get_FRF().
660
+
661
+ :return: H2 FRF estimator matrix (ndarray) of shape (response DOF, excitation DOF, freqency points).
662
+ :rtype: ndarray
663
+ """
664
+ with np.errstate(invalid='ignore'):
665
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
666
+ H2 = np.zeros((self.resp.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
667
+
668
+ if self.exc.shape[1] == 1: # SISO, SIMO
669
+ #print("single input")
670
+ for i in range(self.resp.shape[1]):
671
+ H2[i,:,:] = self.frf_norm * (self.S_XX[i,i,:] / self.S_FX[:,i,:])
672
+ else:
673
+ if (self.S_FX[:,:,0].shape == (2,2)) or (self.S_FX[:,:,0].shape == (3,3)):
674
+ for i in range(freq_len):
675
+ H2[:,:,i] = self.frf_norm * (self.S_XX[:,:,i] @ self._analytical_matrix_inverse(self.S_FX[:,:,i]))
676
+ else:
677
+ if (self.S_FX[:,:,0].shape[0] == self.S_FX[:,:,0].shape[1]):
678
+ for i in range(freq_len):
679
+ H2[:,:,i] = self.frf_norm * (self.S_XX[:,:,i] @ np.linalg.inv(self.S_FX[:,:,i]))
680
+ else:
681
+ for i in range(freq_len):
682
+ H2[:,:,i] = self.frf_norm * (self.S_XX[:,:,i] @ np.linalg.pinv(self.S_FX[:,:,i]))
683
+ return (H2 * self.frf_conversion) / self._correct_time_delay()
684
+
685
+ def get_Hv(self):
686
+ """
687
+ Get Hv FRF averaged estimator (receptance), preferable call via get_FRF().
688
+
689
+ Literature:
690
+ [1] Kihong and Hammond: Fundamentals of Signal Processing for
691
+ Sound and Vibration Engineers, page 293.
692
+
693
+ :return: Hv FRF estimator matrix (ndarray).
694
+ :rtype: ndarray
695
+ """
696
+
697
+ with np.errstate(invalid='ignore'):
698
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
699
+ Hv = np.zeros((self.resp.shape[1], self.exc.shape[1], freq_len), dtype="complex128")
700
+
701
+ if self.exc.shape[1] != 1: # MISO, MIMO
702
+ raise Exception("Currently not implemented for MISO and MIMO systems.")
703
+
704
+ if self.exc.shape[1] == 1: # SISO, SIMO
705
+ k = 1 # ratio of the spectra of measurement noises
706
+ #print("single input")
707
+ for i in range(self.resp.shape[1]):
708
+ Hv[i,:,:] = self.frf_norm * ((self.S_XX[i,i,:] - k * self.S_FF[0,0,:] + np.sqrt(\
709
+ (k * self.S_FF[0,0,:] - self.S_XX[i,i,:]) ** 2 + 4 * k * np.conj(self.S_FX[0,i,:]) * self.S_FX[0,i,:]))\
710
+ / (2 * self.S_XF[i,0,:]))
711
+ return (Hv * self.frf_conversion )/ self._correct_time_delay()
712
+
713
+
714
+ def get_FRF(self, type='default', form='receptance'):
715
+ """
716
+ Returns the default FRF function set at init.
717
+
718
+ :param type: Choose default (as set at init) or H1, H2, Hv or ODS.
719
+ :type type: str
720
+ :param form: Choose receptance, mobility, accelerance.
721
+ :type form: str
722
+ :return: FRF estimator matrix (ndarray).
723
+ :rtype: ndarray
724
+ """
725
+ if type=='default':
726
+ type=self.frf_type
727
+ if not (type in _FRF_TYPES):
728
+ raise Exception('wrong FRF type given %s (can be %s)'
729
+ % (type, _FRF_TYPES))
730
+
731
+ if type == 'H1':
732
+ receptance = self.get_H1()
733
+ if type == 'H2':
734
+ receptance = self.get_H2()
735
+ if type == 'Hv':
736
+ receptance = self.get_Hv()
737
+ if type == 'ODS':
738
+ receptance = self.get_ods_frf()
739
+
740
+ if not (form in _FRF_FORM):
741
+ raise Exception('wrong FRF form given %s (can be %s)'
742
+ % (form, _FRF_FORM))
743
+
744
+ # convert to requested frf form:
745
+ if form=='accelerance':
746
+ return receptance * np.power(1.j * self.get_w_axis(), 2)
747
+ if form=='mobility':
748
+ return receptance * np.power(1.j * self.get_w_axis(), 1)
749
+ return receptance
750
+
751
+
752
+ def get_coherence(self):
753
+ """
754
+ Get coherence.
755
+
756
+ :return: Coherence ndarray of shape (response DOF, freqency points).
757
+ :rtype: ndarray
758
+ """
759
+ with np.errstate(invalid='ignore'):
760
+ if self.exc.shape[1] == 1: # SISO, SIMO
761
+ return np.abs(self.get_H1()[:,0,:] / self.get_H2()[:,0,:])
762
+ else: # MIMO, MISO
763
+ freq_len = np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq).shape[0]
764
+ coh = np.zeros((self.resp.shape[1], freq_len), dtype="complex128")
765
+
766
+ for i in range(self.resp.shape[1]):
767
+ if (self.S_FF[:,:,0].shape == (2,2)) or (self.S_FF[:,:,0].shape == (3,3)):
768
+ for j in range(freq_len):
769
+ coh[i,j] = ((self.S_XF[i,:,j] @ self._analytical_matrix_inverse(self.S_FF[:,:,j])) @ self.S_FX[:,i,j])
770
+ else:
771
+ for j in range(freq_len):
772
+ coh[i,j] = ((self.S_XF[i,:,j] @ np.linalg.inv(self.S_FF[:,:,j])) @ self.S_FX[:,i,j])
773
+ coh[i] = coh[i] / self.S_XX[i,i]
774
+ return coh
775
+
776
+
777
+ def _ini_exc_and_resp_window(self):
778
+ """
779
+ Sets the windows for exc and resp signals.
780
+
781
+ :return:
782
+ """
783
+
784
+ self.exc_window_data, self.exc_window_amp_norm = self._get_window_sub(length=self.exc.shape[2],
785
+ window=self.exc_window)
786
+ self.resp_window_data, self.resp_window_amp_norm = self._get_window_sub(length=self.resp.shape[2],
787
+ window=self.resp_window)
788
+
789
+ self.frf_norm = self.resp_window_amp_norm / self.exc_window_amp_norm
790
+ #print("frf_norm (resp/exc)): ", self.frf_norm)
791
+
792
+
793
+ def _ini_lengths_and_csd_window(self, length):
794
+ """
795
+ Sets the lengths used later in csd and sets csd_window.
796
+
797
+ :param length: Length of data expected.
798
+ :type length: int
799
+ :return:
800
+ """
801
+ #print("_ini_lengths_and_csd_window self.samples: ", self.samples)
802
+ #print("_ini_lengths_and_csd_window self.exc.shape[2]: ", self.exc.shape[2])
803
+ if self.samples is None:
804
+ self.samples = length
805
+ #elif self.samples != self.exc.shape[2]:
806
+ #raise ValueError('data length changed.')
807
+
808
+ self.csd_window_data, self.csd_window_amp_norm = self._get_window_sub(length=self.samples,
809
+ window=self.csd_window)
810
+
811
+ def _add_to_archive(self, exc, resp):
812
+ """
813
+ Add time data to the archive for later data analysis.
814
+
815
+ :param exc: Excitation array.
816
+ :param resp: Response array.
817
+ :return:
818
+ """
819
+ if self.archive_time_data:
820
+ self.resp_archive.append(resp)
821
+ self.exc_archive.append(exc)
822
+
823
+ def get_archive(self):
824
+ """
825
+ Returns the time archive. If not available, it returns None.
826
+
827
+ :return: (excitation, response) time archive.
828
+ """
829
+ if self.archive_time_data:
830
+ return self.exc_archive, self.resp_archive
831
+ else:
832
+ return None, None
833
+
834
+ def _is_overflow(self, data, overflow_samples=3, verbose=0):
835
+ """
836
+ Check data for overflow.
837
+
838
+ :param data: Excitation or response array.
839
+ :param overflow_samples: Number of samples that need to be equal to max or overflow identification.
840
+ :param verbose: 0 or 1 - prints overflow status.
841
+ :return: Overflow status (True/False).
842
+ :rtype: bool
843
+ """
844
+ def _overflow_check(time_series, os):
845
+ s = np.sort(np.abs(time_series))[::-1]
846
+ over = s == np.max(s)
847
+ if np.sum(over) >= os:
848
+ return True
849
+ else:
850
+ return False
851
+
852
+ if data.ndim == 3:
853
+ overflow_status_array = np.empty((data.shape[0], data.shape[1]), dtype=bool)
854
+ for measurement_num in range(data.shape[0]):
855
+ for dof_num in range(data.shape[1]):
856
+ overflow_status = _overflow_check(data[measurement_num, dof_num], overflow_samples)
857
+ if overflow_status:
858
+ if verbose == 1:
859
+ print(f'Overflow - measurement index number {measurement_num}, dof index number {dof_num}!')
860
+ overflow_status_array[measurement_num, dof_num] = overflow_status
861
+ return overflow_status_array
862
+ elif data.ndim == 2:
863
+ overflow_status_array = np.empty((data.shape[0]), dtype=bool)
864
+ for measurement_num in range(data.shape[0]):
865
+ overflow_status = _overflow_check(data[measurement_num], overflow_samples)
866
+ if overflow_status:
867
+ if verbose == 1:
868
+ print(f'Overflow - measurement index number {measurement_num}!')
869
+ overflow_status_array[measurement_num] = overflow_status
870
+ return overflow_status_array
871
+ elif data.ndim == 1:
872
+ overflow_status = _overflow_check(data, overflow_samples)
873
+ if overflow_status:
874
+ if verbose == 1:
875
+ print("Overflow!")
876
+ return overflow_status
877
+ else:
878
+ raise Exception('Incorrect input array shape!')
879
+
880
+ def _is_double_impact(self, data, limit=1e-3, verbose=0):
881
+ """
882
+ Check data for double-impact.
883
+
884
+ See: at the end of http://scholar.lib.vt.edu/ejournals/MODAL/ijaema_v7n2/trethewey/trethewey.pdf
885
+
886
+ :param data: Excitation array.
887
+ :param limit: Ratio of freqency content of the double vs single hit - smaller number means more sensitivity.
888
+ :param verbose: 0 or 1 - prints the ratio of the double PSD main lobe vse after lobe,
889
+ prints double impact status.
890
+ :return: Double-hit status (True/False).
891
+ :rtype: bool
892
+ """
893
+
894
+ def _double_impact_check(x):
895
+ skip_low_freq = 10
896
+ # first PSD
897
+ X = np.fft.rfft(x)[skip_low_freq:]
898
+ X = 2 / self.sampling_freq * np.real(X * np.conj(X))/len(x)
899
+ # second PSD: look for oscillations in PSD
900
+ X2 = np.fft.rfft(X)
901
+ X2 = 2 / self.sampling_freq * np.real(X2 * np.conj(X2))/len(X)
902
+ upto = int(0.01 * len(X2))
903
+ max_impact = np.max(X2[:upto])
904
+ max_after_impact = np.max(X2[upto:])
905
+ if verbose == 1:
906
+ print(f'Ratio of the double PSD main lobe vse after lobe {max_after_impact / max_impact:g}')
907
+ if max_after_impact / max_impact > limit:
908
+ return True
909
+ else:
910
+ return False
911
+
912
+ if data.ndim == 3:
913
+ double_hit_array = np.empty((data.shape[0], data.shape[1]), dtype=bool)
914
+ for measurement_num in range(data.shape[0]):
915
+ for dof_num in range(data.shape[1]):
916
+ double_hit = _double_impact_check(data[measurement_num, dof_num])
917
+ if double_hit:
918
+ if verbose == 1:
919
+ print(f'Double impact - measurement index number {measurement_num}, dof index number {dof_num}!')
920
+ double_hit_array[measurement_num, dof_num] = double_hit
921
+ return double_hit_array
922
+ elif data.ndim == 2:
923
+ double_hit_array = np.empty((data.shape[0]), dtype=bool)
924
+ for measurement_num in range(data.shape[0]):
925
+ double_hit = _double_impact_check(data[measurement_num])
926
+ if double_hit:
927
+ if verbose == 1:
928
+ print(f'Double impact - measurement index number {measurement_num}!')
929
+ double_hit_array[measurement_num] = double_hit
930
+ return double_hit_array
931
+ elif data.ndim == 1:
932
+ double_hit = _double_impact_check(data)
933
+ if double_hit:
934
+ if verbose == 1:
935
+ print("Double impact!")
936
+ return double_hit
937
+ else:
938
+ raise Exception('Incorrect input array shape!')
939
+
940
+ def _correct_time_delay(self):
941
+ """
942
+ Corrects the FRF with regards to the ``time_delay``.
943
+
944
+ :return: Time delay correction.
945
+ :rtype: ndarray
946
+ """
947
+ return (np.exp(1j * self.get_w_axis() * self.resp_delay))
948
+
949
+ def _analytical_matrix_inverse(self, matrix):
950
+ """
951
+ Calculate faster analytical inverse of 2x2 or 3x3 matrix.
952
+
953
+ :param matrix: Array of shape (2x2) or (3x3).
954
+ :return: Matrix inverse array.
955
+ :rtype: ndarray
956
+ """
957
+ if matrix.shape == (2,2):
958
+ a, b, c, d = matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]
959
+ det = a * d - b * c
960
+
961
+ if det == 0:
962
+ raise ValueError("Singular matrix.")
963
+
964
+ inverse_matrix = [[d / det, -b / det],
965
+ [-c / det, a / det]]
966
+
967
+ return np.array(inverse_matrix)
968
+
969
+ if matrix.shape == (3,3):
970
+ a, b, c = matrix[0]
971
+ d, e, f = matrix[1]
972
+ g, h, i = matrix[2]
973
+
974
+ det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
975
+
976
+ if det == 0:
977
+ raise ValueError("Singular matrix.")
978
+
979
+ inverse_matrix = [
980
+ [(e * i - f * h) / det, (c * h - b * i) / det, (b * f - c * e) / det],
981
+ [(f * g - d * i) / det, (a * i - c * g) / det, (c * d - a * f) / det],
982
+ [(d * h - e * g) / det, (g * b - a * h) / det, (a * e - b * d) / det]
983
+ ]
984
+
985
+ return np.array(inverse_matrix)
986
+
987
+ if __name__ == '__main__':
988
+ pass
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) acknowledgement: from 2014-2017 this package was part of the OpenModal project
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,84 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyuff
3
+ Version: 1.0
4
+ Summary: Frequency response function as used in structural dynamics.
5
+ Home-page: https://github.com/ladisk/pyFRF
6
+ Author: Janko Slavič, Luka Novak, Martin Česnik, et al.
7
+ Author-email: janko.slavic@fs.uni-lj.si
8
+ Keywords: FRF,MIMO,SIMO,ODS
9
+ License-File: LICENSE
10
+ Requires-Dist: colorama
11
+ Requires-Dist: numpy
12
+ Requires-Dist: py
13
+ Requires-Dist: pytest
14
+ Requires-Dist: lvm-read
15
+ Requires-Dist: pyExSi
16
+ Requires-Dist: scipy
17
+ Requires-Dist: matplotlib
18
+
19
+ pyFRF
20
+ =====
21
+
22
+ Frequency response function as used in structural dynamics.
23
+ -----------------------------------------------------------
24
+ For more information check out the showcase examples and see documentation_.
25
+
26
+ Basic ``pyFRF`` usage:
27
+ ----------------------
28
+
29
+ Make an instance of ``FRF`` class:
30
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31
+
32
+ .. code:: python
33
+
34
+ a = pyFRF.FRF(
35
+ sampling_freq,
36
+ exc=None,
37
+ resp=None,
38
+ exc_type='f', resp_type='a',
39
+ window='none',
40
+ weighting='linear',
41
+ fft_len=None,
42
+ nperseg=None,
43
+ noverlap=None,
44
+ archive_time_data=False,
45
+ frf_type='H1',
46
+ copy=True
47
+ )
48
+
49
+ Adding data:
50
+ ~~~~~~~~~~~~
51
+ We can add the excitation and response data at the beginning through ``exc`` and ``resp`` arguments, otherwise, the excitation and response
52
+ data can be added later via ``add_data()`` method:
53
+
54
+ .. code:: python
55
+
56
+ a.add_data(exc, resp)
57
+
58
+ Computing FRF:
59
+ ~~~~~~~~~~~~~~
60
+ Preferable way to get the frequency response functions is via ``get_FRF()`` method:
61
+
62
+ .. code:: python
63
+
64
+ frf = a.get_FRF(type="default", form="receptance")
65
+
66
+ We can also directly get the requested FRF via other methods: ``get_H1()``, ``get_H2()``, ``get_Hv()`` and, ``get_ods_frf()``:
67
+
68
+ .. code:: python
69
+
70
+ H1 = a.get_H1()
71
+ H2 = a.get_H2()
72
+ Hv = a.get_Hv()
73
+ ods_frf = a.get_ods_frf()
74
+
75
+ .. _documentation: https://pyfrf.readthedocs.io/en/latest/
76
+
77
+ |pytest|
78
+
79
+ |binder| to test the *Showcase.ipynb*.
80
+
81
+ .. |binder| image:: https://mybinder.org/badge_logo.svg
82
+ :target: https://mybinder.org/v2/gh/ladisk/pyFRF/main
83
+ .. |pytest| image:: https://github.com/ladisk/pyFRF/actions/workflows/python-package.yml/badge.svg
84
+ :target: https://github.com/ladisk/pyFRF/actions
@@ -0,0 +1,7 @@
1
+ fft_tools.py,sha256=tV5DIdwsjatu_Mb1OMfNaD0RFD45L63vpqb5jxxjuyQ,8002
2
+ pyFRF.py,sha256=MKjnnlKmc67JJ6LOATqOB90pXM1J23Qf4-qttY54gz8,42448
3
+ pyuff-1.0.dist-info/LICENSE,sha256=nNpQ1qmeoA6YNoM5AZ_cgAFdMzb6Sjl8mUGL_N9fONs,1130
4
+ pyuff-1.0.dist-info/METADATA,sha256=YLlBdqMRvMUV-IukNJu6Sjk57jfDLKBcMf82f3MDAN8,2213
5
+ pyuff-1.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
6
+ pyuff-1.0.dist-info/top_level.txt,sha256=tG3oPRHXAFuzQCUio_5JW-25pcCirq2NR9O-pHn8_xQ,16
7
+ pyuff-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ fft_tools
2
+ pyFRF