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 +235 -0
- pyFRF.py +988 -0
- pyuff-1.0.dist-info/LICENSE +21 -0
- pyuff-1.0.dist-info/METADATA +84 -0
- pyuff-1.0.dist-info/RECORD +7 -0
- pyuff-1.0.dist-info/WHEEL +5 -0
- pyuff-1.0.dist-info/top_level.txt +2 -0
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,,
|