├── .gitignore
├── LICENSE
├── OpenModal
├── DAQTask.py
├── RingBuffer.py
├── __init__.py
├── analysis
│ ├── __init__.py
│ ├── add_reconstruction_to_mdd.py
│ ├── ewins.py
│ ├── frfmax.py
│ ├── genFRF.py
│ ├── get_frf_peaks.py
│ ├── get_measured_sample.py
│ ├── get_simulated_sample.py
│ ├── identification.py
│ ├── lsce.py
│ ├── lscf.py
│ ├── lsfd.py
│ ├── stabilisation.py
│ └── utility_functions.py
├── anim_tools.py
├── daqprocess.py
├── fft_tools.py
├── frf.py
├── gui
│ ├── __init__.py
│ ├── export_window.py
│ ├── icons
│ │ ├── Icon_animation_widget.png
│ │ ├── Icon_fit_view.png
│ │ ├── add164.png
│ │ ├── analysis_4.png
│ │ ├── check.png
│ │ ├── check_empty.png
│ │ ├── configure.png
│ │ ├── cross89.png
│ │ ├── downarrow.png
│ │ ├── downarrow_small.png
│ │ ├── gear31.png
│ │ ├── geometry_big.png
│ │ ├── hammer.png
│ │ ├── icon_anim_pause.png
│ │ ├── icon_anim_play.png
│ │ ├── icon_size_grab.png
│ │ ├── icon_size_grab_2.png
│ │ ├── icon_size_grab_3.png
│ │ ├── icon_size_grab_4.png
│ │ ├── limes_logo.ico
│ │ ├── loader-ring.gif
│ │ ├── loader.gif
│ │ ├── measurement_3.png
│ │ ├── model.png
│ │ ├── pcdaq.png
│ │ ├── play.png
│ │ ├── play1.png
│ │ ├── play87.png
│ │ ├── radio_empty.png
│ │ ├── radio_hover.png
│ │ ├── radio_selected.png
│ │ ├── sizegrip.png
│ │ ├── stop.png
│ │ ├── stop40.png
│ │ ├── thumbsup.png
│ │ ├── thumbsup_empty.png
│ │ ├── uparrow.png
│ │ ├── uparrow_small.png
│ │ └── verification16.png
│ ├── preferences_window.py
│ ├── skeleton.py
│ ├── styles
│ │ └── style_template.css
│ ├── templates.py
│ ├── tooltips.py
│ └── widgets
│ │ ├── __init__.py
│ │ ├── analysis.py
│ │ ├── animation.py
│ │ ├── geometry.py
│ │ ├── languages.py
│ │ ├── measurement.py
│ │ ├── prototype.py
│ │ └── welcome.py
├── keys.py
├── meas_check.py
├── modaldata.py
├── openmodal.py
├── preferences.py
└── utils.py
├── README.md
├── openmodal_test_data.unv
├── requirements.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.xml
3 | *.txt
4 | *.pkl
5 | /.idea/*
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
2 | #
3 | # This file is part of OpenModal.
4 | #
5 | # OpenModal is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, version 3 of the License.
8 | #
9 | # OpenModal is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with OpenModal. If not, see .
--------------------------------------------------------------------------------
/OpenModal/RingBuffer.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | # -*- coding: UTF-8 -*-
20 | """Class for 2D buffer array. Based on this code: http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/
21 |
22 | Classes:
23 | class RingBuffer: Buffer for 2D array.
24 |
25 | Info:
26 | @author: janko.slavic@fs.uni-lj.si, 2014
27 | """
28 | import numpy as np
29 |
30 |
31 | class RingBuffer():
32 | """A 2D ring buffer using numpy arrays"""
33 |
34 | def __init__(self, channels, samples):
35 | self.data = np.zeros((channels, samples), dtype='float')
36 | self.index = 0
37 |
38 | def clear(self):
39 | """Clear buffer."""
40 | self.data = np.zeros_like(self.data)
41 | self.index = 0
42 |
43 | def extend(self, x, add_samples='all'):
44 | """adds array x to ring buffer"""
45 | if x[0].size==0:
46 | return
47 | if add_samples!='all':
48 | if add_samples<=0:
49 | return
50 | if add_samples.
17 |
18 |
19 |
--------------------------------------------------------------------------------
/OpenModal/analysis/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'FS'
20 |
--------------------------------------------------------------------------------
/OpenModal/analysis/add_reconstruction_to_mdd.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 | import pandas as pd
21 |
22 |
23 | def add_reconstruction_to_mdd(modaldata, model_id, lambdak, modal_constants, method, analysis_id, save_points):
24 | """
25 | Add the data from reconstruction to the mdd object.
26 |
27 | :param modaldata: mdd object
28 | :param model_id: model id
29 | :param lambdak: complex eigenvector
30 | :param modal_constants: modal constants
31 | :param method: analysis method ('lsce', 'lscf')
32 | :param analysis_id: analysis id
33 | :return: updated mdd object
34 | """
35 |
36 | # get selected rows
37 | selected_rows = modaldata.tables['measurement_index'].loc[:, 'model_id'] == model_id
38 |
39 | # delete old data for the selected index
40 | delete_old_data_model_id = modaldata.tables['analysis_index'].loc[:, 'model_id'] == model_id
41 | delete_old_data_analysis_id = modaldata.tables['analysis_index'].loc[:, 'analysis_id'] == analysis_id
42 |
43 | delete_old_data = delete_old_data_model_id & delete_old_data_analysis_id
44 |
45 | modaldata.tables['analysis_index'] = modaldata.tables['analysis_index'][~delete_old_data]
46 |
47 | # number of selected modes
48 | nmodes = len(lambdak)
49 |
50 | # modal_constants = modal_constants
51 |
52 | # Fill the anlysis_index DataFrame
53 | df_new = pd.DataFrame(np.nan * np.empty((nmodes, modaldata.tables['analysis_index'].shape[1])),
54 | columns=modaldata.tables['analysis_index'].columns)
55 | df_new.loc[:, 'eig'] = lambdak
56 | df_new.loc[:, 'analysis_id'] = analysis_id
57 | df_new.loc[:, 'model_id'] = model_id * np.ones(nmodes)
58 | df_new.loc[:, 'analysis_method'] = method
59 |
60 | mode_n = np.arange(nmodes) + 1
61 | df_new.loc[:, 'mode_n'] = mode_n
62 | df_new.loc[:, 'spots'] = save_points
63 |
64 | uffid_start = np.max(modaldata.tables['analysis_index'].loc[:, 'uffid'])
65 | uffid_start = 0 if np.isnan(uffid_start) else uffid_start
66 | uffid = np.arange(nmodes) + 1 + uffid_start # TODO: this should be deleted, when Miha commits the changes to animation.py
67 | df_new.loc[:, 'uffid'] = uffid # TODO: this should be deleted, when Miha commits the changes to animation.py
68 |
69 | modaldata.tables['analysis_index'] = modaldata.tables['analysis_index'].append(df_new,
70 | ignore_index=True)
71 | # delete any previous analysis data
72 | rows_to_delete_model_id = modaldata.tables['analysis_values'].loc[:, 'model_id'] == model_id
73 | rows_to_delete_analysis_id = modaldata.tables['analysis_values'].loc[:, 'analysis_id'] == analysis_id
74 |
75 | rows_to_delete = rows_to_delete_model_id & rows_to_delete_analysis_id
76 |
77 | modaldata.tables['analysis_values'] = modaldata.tables['analysis_values'][~rows_to_delete]
78 |
79 | # Determines wheather to animate reference or response nodes
80 | display_rsp_or_ref = determine_display_points(modaldata)
81 |
82 | # get the information about which index corresponds to translational movement: x, y or z
83 | xref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 1
84 | yref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 2
85 | zref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 3
86 |
87 | xref = np.tile(xref, nmodes)
88 | yref = np.tile(yref, nmodes)
89 | zref = np.tile(zref, nmodes)
90 |
91 | # get the information about which index corresponds to rotational movement: xy, xz or yz
92 | xyref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 4
93 | xzref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 5
94 | yzref = modaldata.tables['measurement_index'][selected_rows].loc[:, display_rsp_or_ref[1]] == 6
95 |
96 | xyref = np.tile(xyref, nmodes)
97 | xzref = np.tile(xzref, nmodes)
98 | yzref = np.tile(yzref, nmodes)
99 |
100 | # chose the value for the reference (to compute the modal shapes)
101 | # index = np.unravel_index(np.argmax(np.sum(np.abs(modal_constants), axis=2)),
102 | # dims=modal_constants.shape[:2]) # TODO: maybe max min would be better
103 | # reference = np.sqrt(modal_constants[index]) # Modal constant `a` with largest value is choosen due to division.
104 | #
105 | # # Computation of eigenvectors
106 | # r = modal_constants / reference
107 |
108 | r = modal_constants
109 |
110 | # Table for converting measurement index to analysis index
111 | ref_rsp = modaldata.tables['measurement_index'][selected_rows].loc[:, ['ref_node', 'ref_dir', 'rsp_node', 'rsp_dir']]
112 | node_nums = measurement_index_to_analysis_index(ref_rsp, display_rsp_or_ref[:2])
113 |
114 | # Repeat the node_nums according to the number of references
115 | rsp = display_rsp_or_ref[3]
116 | rsp = np.tile(rsp, display_rsp_or_ref[2])
117 |
118 | node_nums = pd.concat([node_nums]*display_rsp_or_ref[2])
119 | # node_nums['node'] = rsp
120 | # node_nums['dir'] = rsp.imag
121 |
122 | # Total number of measured points
123 | nr_of_data = node_nums.shape[0]
124 |
125 | # Fill the analysis_values DataFrame
126 | df_new = pd.DataFrame(np.nan * np.empty((nr_of_data * nmodes, modaldata.tables['analysis_values'].shape[1])),
127 | columns=modaldata.tables['analysis_values'].columns)
128 |
129 | df_new.loc[:, 'model_id'] = np.tile(model_id, nr_of_data * nmodes)
130 |
131 | df_new.loc[:, 'analysis_id'] = analysis_id
132 |
133 | df_new.loc[:, 'analysis_method'] = method
134 |
135 | df_new.loc[:, 'r1'] = np.zeros(nr_of_data * nmodes, dtype=complex)
136 | df_new.loc[:, 'r2'] = np.zeros(nr_of_data * nmodes, dtype=complex)
137 | df_new.loc[:, 'r3'] = np.zeros(nr_of_data * nmodes, dtype=complex)
138 |
139 | df_new.loc[:, 'r4'] = np.zeros(nr_of_data * nmodes, dtype=complex)
140 | df_new.loc[:, 'r5'] = np.zeros(nr_of_data * nmodes, dtype=complex)
141 | df_new.loc[:, 'r6'] = np.zeros(nr_of_data * nmodes, dtype=complex)
142 |
143 | eigenvector = r.T.reshape(-1)
144 |
145 | df_new.loc[np.tile(node_nums.loc[:, 'r1'].values, nmodes), 'r1'] = eigenvector[xref]
146 | df_new.loc[np.tile(node_nums.loc[:, 'r2'].values, nmodes), 'r2'] = eigenvector[yref]
147 | df_new.loc[np.tile(node_nums.loc[:, 'r3'].values, nmodes), 'r3'] = eigenvector[zref]
148 | df_new.loc[np.tile(node_nums.loc[:, 'r4'].values, nmodes), 'r4'] = eigenvector[xyref]
149 | df_new.loc[np.tile(node_nums.loc[:, 'r5'].values, nmodes), 'r5'] = eigenvector[xzref]
150 | df_new.loc[np.tile(node_nums.loc[:, 'r6'].values, nmodes), 'r6'] = eigenvector[yzref]
151 |
152 | df_new.loc[:, 'node_nums'] = np.tile(node_nums.index.values, nmodes)
153 | # df_new.loc[:, 'node_nums'].unique()
154 | # df_new.loc[:, 'node'] = np.tile(node_nums['node'].values, nmodes)
155 | # df_new.loc[:, 'dir'] = node_nums['dir'].values
156 | # df_new.loc[:, ['ref_node', 'ref_dir', 'rsp_node', 'rsp_dir']] = np.concatenate([ref_rsp.values]*nmodes)
157 |
158 | uffid = np.repeat(uffid, len(node_nums)) # TODO: this should be deleted, when Miha commits the changes to animation.py
159 |
160 | df_new.loc[:, 'uffid'] = uffid # TODO: this should be deleted, when Miha commits the changes to animation.py
161 |
162 | df_new.loc[:, 'mode_n'] = np.repeat(mode_n, len(node_nums))
163 |
164 | test = df_new.loc[:, ['node_nums', 'mode_n', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6']]
165 |
166 | test['r1c'] = test.r1.values.imag
167 | test['r2c'] = test.r2.values.imag
168 | test['r3c'] = test.r3.values.imag
169 | test['r4c'] = test.r4.values.imag
170 | test['r5c'] = test.r5.values.imag
171 | test['r6c'] = test.r6.values.imag
172 |
173 | test.r1 = test.r1.values.real
174 | test.r2 = test.r2.values.real
175 | test.r3 = test.r3.values.real
176 | test.r4 = test.r4.values.real
177 | test.r5 = test.r5.values.real
178 | test.r6 = test.r6.values.real
179 |
180 | test = test.groupby(['node_nums', 'mode_n']).sum()
181 |
182 | test.r1 = test.r1 + 1j * test.r1c
183 | test.r2 = test.r2 + 1j * test.r2c
184 | test.r3 = test.r3 + 1j * test.r3c
185 | test.r4 = test.r4 + 1j * test.r4c
186 | test.r5 = test.r5 + 1j * test.r5c
187 | test.r6 = test.r6 + 1j * test.r6c
188 |
189 | test = test.loc[:, ['r1', 'r2', 'r3', 'r4', 'r5', 'r6']]
190 |
191 | test = test.reset_index()
192 |
193 | test['model_id'] = model_id
194 | test['analysis_id'] = analysis_id
195 | test['analysis_method'] = method
196 |
197 | modaldata.tables['analysis_values'] = modaldata.tables['analysis_values'].append(test,
198 | ignore_index=True)
199 | return modaldata
200 |
201 |
202 | def determine_display_points(modaldata_object):
203 | """
204 | Determines wheter to display reference or respnse nodes
205 | :param modaldata_object: modaldata_object
206 | :return: A tuple containing two strings and the number of references.
207 | """
208 | nodes = modaldata_object.tables['measurement_index'].loc[:, ['ref_node', 'ref_dir', 'rsp_node', 'rsp_dir']]
209 | nodes = nodes.astype(int)
210 |
211 | ref = np.unique(nodes.loc[:, 'ref_node'] + 1j * nodes.loc[:, 'ref_dir'])
212 | rsp = np.unique(nodes.loc[:, 'rsp_node'] + 1j * nodes.loc[:, 'rsp_dir'])
213 |
214 | if len(rsp) > len(ref):
215 | return 'rsp_node', 'rsp_dir', len(ref), np.unique(nodes.loc[:, 'rsp_node'])
216 | else:
217 | return 'ref_node', 'ref_dir', len(rsp), np.unique(nodes.loc[:, 'ref_node'])
218 |
219 |
220 | def measurement_index_to_analysis_index(ref_and_rsp_node, display_ref_rsp):
221 | """
222 | Transforms measurement indices (from measurement_index table) into analysis
223 | indices. This transforms the (node number, direction) column description
224 | into 2D shape, with columns as modal vectors (r1, r2, r3, r4, r5, r6) and
225 | rows as node numbers.
226 |
227 | :param ref_and_rsp_node: pandas DataFrame containing ref_node, ref_dir,
228 | rsp_node and rsp_dir
229 | :param display_ref_rsp: Tuple for determining whether reference or
230 | response nodes should be animated
231 | (e. g. ('ref_node', 'ref_dir'))
232 | :return: 2D boolean arra9y.
233 | """
234 | df = pd.DataFrame(index=ref_and_rsp_node.loc[:, display_ref_rsp[0]].unique(), columns=np.arange(1, 7))
235 | df.iloc[:, :] = np.zeros_like(df, dtype=bool)
236 |
237 | # prepare indices
238 | indices = ref_and_rsp_node.loc[:, display_ref_rsp].values
239 | indices = indices[:, 0] + 1j * indices[:, 1]
240 |
241 | for i in indices:
242 | df.loc[i.real, i.imag] = True
243 |
244 | df.columns = ['r1', 'r2', 'r3', 'r4', 'r5', 'r6']
245 |
246 | return df
247 |
248 |
249 | def save_analysis_settings(settings, model_id, analysis_id, method='lscf', f_min=1, f_max=100, nmax=30, err_fn=1e-2,
250 | err_xi=5e-2):
251 | """
252 | Saves analysis settings to mdd file.
253 |
254 | :param settings: modaldata.tables['analysis_settings']
255 | :param model_id: model id
256 | :param analysis_id: analysis id
257 | :param method: identification method
258 | :param f_min: minimum frequency
259 | :param f_max: maximum frequency
260 | :param nmax: maximal model order used by the identification method
261 | :param err_fn: allowed natural frequency error in stabilisation diagrams
262 | :param err_xi: damping error in stabilisation diagrams
263 | :return: updated analysis settings data-table
264 | """
265 |
266 | save = [[model_id, analysis_id, method, f_min, f_max, nmax, err_fn, err_xi]]
267 |
268 | select_model_id = settings.loc[:, 'model_id'] == model_id
269 | select_analysis_id = settings.loc[:, 'analysis_id'] == analysis_id
270 |
271 | select_model = select_model_id & select_analysis_id
272 |
273 | if sum(select_model) == 1:
274 | settings[select_model] = save
275 | else:
276 | settings = settings.append(pd.DataFrame(save, columns=settings.columns), ignore_index=True)
277 |
278 | return settings
279 |
280 |
281 | def save_stabilisation_spots(analysis_stabilisation, data):
282 | """
283 | Saves stabilisation spots data
284 |
285 | :param analysis_stabilisation: analysis_stabilisation mdd table
286 | :param data: [model_id, analysis_id, method, pos, size,
287 | pen_color, pen_width, symbol, brush, damp]
288 | :return: updated analysis_stabilisation mdd table
289 | """
290 |
291 | model_id = data[0][0]
292 | analysis_id = data[0][1]
293 | # analysis_method = data[0][2] # only needed when loading
294 |
295 | select_model_id = analysis_stabilisation.loc[:, 'model_id'] == model_id
296 | select_analysis_id = analysis_stabilisation.loc[:, 'analysis_id'] == analysis_id
297 |
298 | delete_old_data = select_model_id & select_analysis_id
299 |
300 | analysis_stabilisation = analysis_stabilisation[~delete_old_data]
301 |
302 | data = pd.DataFrame(data, columns=analysis_stabilisation.columns)
303 | data.loc[:, ['model_id', 'analysis_id']] = data.loc[:, ['model_id', 'analysis_id']].astype(int)
304 | data.loc[:, ['size', 'pen_width', 'damp']] = data.loc[:, ['size', 'pen_width', 'damp']].astype(float)
305 | data.loc[:, 'pos'] = [complex(i) for i in (data.loc[:, 'pos'])]
306 |
307 | analysis_stabilisation = analysis_stabilisation.append(data, ignore_index=True)
308 |
309 | return analysis_stabilisation
310 |
--------------------------------------------------------------------------------
/OpenModal/analysis/ewins.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """ Ewins-Gleeson Identification method
20 |
21 | Identifies modal shapes and modal damping from FRF measurements.
22 |
23 | This method demands (needs) optional points. This optional points are
24 | the same for all the FRF measurements.
25 |
26 | This method was presented in: Ewins, D.J. and Gleeson, P. T.: A method
27 | for modal identification of lightly damped structures
28 |
29 | Functions:
30 | - ewins
31 | - reconstruction
32 | - test_ewins
33 |
34 | History:- april 2014: added convert_frf to ewins, reconstruction,
35 | Blaz Starc, blaz.starc@fs.uni-lj.si,
36 | - april 2013: reconstruction, Tadej Kranjc,
37 | tadej.kranjc@ladisk.si
38 | - 2012: ewins, test_ewins: Tadej Kranjc,
39 | tadej.kranjc@ladisk.si
40 |
41 | """
42 | import numpy as np
43 |
44 | from OpenModal.analysis.get_frf_peaks import get_frf_peaks
45 | from OpenModal.fft_tools import convert_frf
46 |
47 |
48 | def ewins(freq, H, nf='None', residues=False, type='d', o_fr=0):
49 | """
50 | Arguments:
51 | - freq - frequency vector
52 | - H - a row or a column of FRF matrix
53 | - o_fr - list of optionally chosen frequencies in Hz. If it is
54 | not set, optional points are set automatically and are
55 | near the natural frequencies (10 points higher than
56 | natural frequencies)
57 | - nf - natural frequencies [Hz]
58 | -ref - index of reference node
59 | - residues - if 1 residues of lower and higher residues are
60 | taken into account
61 | - if 0 residues of lower and higher residues are
62 | not taken into account
63 |
64 | - type - type of FRF function:
65 | -'d'- receptance
66 | -'v'- mobility
67 | -'a'- accelerance
68 | """
69 | om = freq * 2 * np.pi
70 | H2 = convert_frf(H, om, type, outputFRFtype = 'a') # converts FRF to accalerance
71 |
72 |
73 | if nf != 'None':
74 | ind = (np.round(nf / (freq[2] - freq[1])))
75 | ind = np.array(ind, dtype=int)
76 |
77 | if ind == 'None':
78 | print ('***ERROR***Natural frequencies are not defined')
79 | quit()
80 |
81 | #determination of N points which are chosen optionally
82 |
83 | if o_fr == 0 and residues == True:
84 | ind_opt = np.zeros((len(ind) + 2), dtype='int')
85 | ind_opt[0] = 20
86 | ind_opt[1:-1] = ind + 20
87 | ind_opt[-1] = len(freq) - 20
88 |
89 | if o_fr == 0 and residues == False:
90 | ind_opt = np.zeros((len(ind)), dtype='int')
91 | ind_opt = ind + 10
92 |
93 | if o_fr != 0:
94 | ind_opt = (np.round(o_fr / (freq[2] - freq[1])))
95 | ind_opt = np.array(ind_opt, dtype=int)
96 |
97 | #matrix of modal damping
98 |
99 | mi = np.zeros((len(ind), len(H2[0, :])))
100 | Real = np.real(H2[:, 0])
101 | #self.Imag=np.imag(self.FRF[:,0])
102 |
103 | R = np.zeros((len(ind_opt), len(ind_opt)))
104 | A_ref = np.zeros((len(ind_opt), len(H2[0, :]))) #modal constant of FRF
105 | FI = np.zeros((len(ind_opt), len(H2[0, :])), dtype=complex) #mass normalized modal
106 | #vector
107 |
108 | R[:, 0] = np.ones(len(ind_opt))
109 |
110 | for jj in range (0, len(ind_opt)):
111 | R[jj, -1] = -(om[ind_opt[jj]]) ** 2
112 |
113 | if residues == True:
114 | R[jj, 1:-1] = (om[ind_opt[jj]]) ** 2 / ((om[ind[:]]) ** 2 - (om[ind_opt[jj]]) ** 2)
115 | if residues == False:
116 | R[jj, :] = (om[ind_opt[jj]]) ** 2 / ((om[ind[:]]) ** 2 - (om[ind_opt[jj]]) ** 2)
117 |
118 | R = np.linalg.inv(R)
119 |
120 | for j in range (0, len(H2[:, 0])):
121 | Re_pt = np.real(H2[j, ind_opt])
122 | Re_pt = np.matrix(Re_pt)
123 |
124 | C = np.dot(R, Re_pt.transpose()) #modal constant of one FRF
125 | A_ref[:, j] = -C.flatten() #modal constant of all FRFs
126 |
127 |
128 | if residues == True:
129 | mi[:, j] = np.abs(C[1:-1].transpose()) / (np.abs(H2[j, ind[:]])) #modal damping
130 |
131 | if residues == False:
132 | mi[:, j] = np.abs(C[:].transpose()) / (np.abs(H2[j, ind[:]])) #modal damping
133 |
134 | # for kk in range(0, len(ind_opt)):
135 | # FI[kk, :] = -A_ref[kk, :] / np.sqrt(np.abs(A_ref[kk, refPT])) #calculation of
136 | # #normalized modal vector
137 |
138 | if residues == True:
139 | #return FI[1:-1, :], mi
140 | return A_ref, mi
141 |
142 | if residues == False:
143 | return A_ref, mi
144 |
145 | def reconstruction(freq, nfreq, A, d, damping='hysteretic', type='a', residues=False, LR=0, UR=0):
146 | """generates a FRF from modal parameters.
147 |
148 | There is option to consider the upper and lower residues (Ewins, D.J.
149 | and Gleeson, P. T.: A method for modal identification of lightly
150 | damped structures)
151 |
152 | #TODO: check the correctness of the viscous damping reconstruction
153 |
154 | Arguments:
155 | A - Modal constants of the considering FRF.
156 | nfreq - natural frequencies in Hz
157 | c - damping loss factor or damping ratio
158 | damp - type of damping:
159 | -'hysteretic'
160 | -'viscous'
161 | LR - lower residue
162 | UR - upper residue
163 |
164 | residues - if 'True' the residues of lower and higher residues
165 | are taken into account. The lower and upper residues
166 | are first and last component of A, respectively.
167 | type - type of FRF function:
168 | -'d'- receptance
169 | -'v'- mobility
170 | -'a'- accelerance
171 | """
172 |
173 |
174 | A = np.array(A, dtype='complex')
175 | d=np.array(d)
176 | om = np.array(2 * np.pi * freq)
177 | nom = np.array(2 * np.pi * nfreq)
178 |
179 | #in the case of single mode the 1D arrays have to be created
180 | if A.shape==():
181 | A_=A; d_=d ; nom_=nom
182 | A=np.zeros((1),dtype='complex'); d=np.zeros(1) ; nom=np.zeros(1)
183 | A[0]=A_ ; d[0]=d_
184 | nom[0]=nom_
185 |
186 | if residues:
187 | LR = np.array(LR)
188 | UR = np.array(UR)
189 |
190 | H = np.zeros(len(freq), dtype=complex)
191 |
192 | if damping == 'hysteretic':
193 | #calculation of a FRF
194 | for i in range(0, len(freq)):
195 | for k in range(0, len(nom)):
196 | H[i] = H[i] + A[k] / (nom[k] ** 2 - om[i] ** 2 + d[k] * 1j * nom[k] ** 2)
197 |
198 | if residues:
199 | for i in range(1, len(freq)):
200 | H[i] = H[i] + LR / om[i] ** 2 - UR
201 |
202 | if damping == 'viscous':
203 | H = np.zeros(len(freq), dtype=complex)
204 | for i in range(0, len(freq)):
205 | for k in range(0, len(nom)):
206 | H[i]=H[i]+ A[k] / (nom[k] ** 2 - om[i] ** 2 + 2.j * om[i] * nom[k] * d[k])
207 |
208 | H = convert_frf(H, om, 'd' ,type)
209 | return H
210 |
211 | def test_ewins():
212 | import matplotlib.pyplot as plt
213 | from OpenModal.analysis.get_simulated_sample import get_simulated_receptance
214 |
215 | residues = True
216 |
217 | freq, H, MC, eta, D = get_simulated_receptance(
218 | df_Hz=1, f_start=0, f_end=2000, measured_points=10, show=False, real_mode=True)
219 | #freq, H = get_measured_accelerance()
220 |
221 | #identification of natural frequencies
222 | ind_nf = get_frf_peaks(freq, H[1, :], freq_min_spacing=10)
223 |
224 | #identification of modal constants and damping
225 | A, mi = ewins(freq, H, freq[ind_nf], type='d', residues=residues)
226 |
227 | #reconstruction
228 | Nfrf = 4 #serial number of compared FRF
229 | H_rec = reconstruction(freq, freq[ind_nf], A[1:-1, Nfrf], mi[:, Nfrf],
230 | residues=residues, type='d', LR=A[0, Nfrf], UR=A[-1, Nfrf])
231 | H_rec = reconstruction(freq, freq[ind_nf], A[1:-1, Nfrf] , mi[:, Nfrf],
232 | residues=True, type='d', LR=A[0, Nfrf], UR=A[-1, Nfrf])
233 |
234 | #comparison of original and reconstructed FRF
235 |
236 | fig, [ax1, ax2] = plt.subplots(2, 1, sharex=True, figsize=(12, 10))
237 | ax1.semilogy(freq, np.abs(H[Nfrf, :]))
238 | ax1.semilogy(freq, np.abs(H_rec))
239 | ax2.plot(freq, 180 / np.pi * (np.angle(H[Nfrf, :])))
240 | ax2.plot(freq, 180 / np.pi * (np.angle(H_rec)))
241 | ax1.set_ylabel('Frequency [Hz]')
242 | ax1.set_ylabel('Magn [m s$^{-2}$ N$^{-1}$]')
243 | ax2.set_ylabel('Angle [deg]')
244 | plt.show()
245 |
246 |
247 | if residues:
248 | plt.plot(range(0, len(A[1, :])), np.real(A[1:-1]).T, 'r')
249 | else:
250 | plt.plot(range(0, len(A[0, :])), A, 'r')
251 | plt.plot(range(0, len(MC[0, :])), MC.T)
252 | plt.xlabel('Point')
253 | plt.ylabel('Modal constant')
254 | plt.show
255 |
256 | if __name__ == '__main__':
257 | test_ewins()
--------------------------------------------------------------------------------
/OpenModal/analysis/frfmax.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 |
21 | from OpenModal.analysis.utility_functions import myfunc
22 |
23 |
24 | def frfmax(H1, freq, threshold=10):
25 | """frfmax(self,H1,threshold) calculates natural frequencies from H1
26 |
27 | It picks all the FRF magnitude peaks, which are over the threshold setting. If a magnitude point is
28 | higher than two neighbor's points it is assumed as a peak. The problem of this method
29 | is, that it picks also the peaks which are a consequence of a noise.
30 |
31 | Arguments:
32 | H1 - FRF from which natural frequencies are identified.
33 | threshold - Only peaks, which are over the threshold are identified from FRF measurement
34 | peak
35 | """
36 | peak = np.vectorize(myfunc) #vectorization of myfunc
37 | ed = np.zeros(len(H1)) #ed is magnitude of FRF
38 | ed = np.abs(H1)
39 |
40 |
41 | ed[ed < threshold] = 0 #values of FRF below the threshold are zeroed out.
42 | ec = peak(ed[1:len(ed) - 2], ed[2:len(ed) - 1], ed[3:len(ed)]) #returns ones at index where NF
43 | #are, elsewhere returns zeros.
44 |
45 |
46 |
47 | nor = sum(ec) #sum of array ec is number of NF
48 | ec = ec * range(0, len(ec)) #by multiplying with list of index we get array of index where NF are
49 | ec = np.sort(ec) #sorting of array
50 | ind = ec[-nor:] #last nonzero elements are index of NF
51 | ind += 2 #two is added because 'myfunc' checks middle variable 'd'
52 |
53 | nf = np.array(freq[ind])
54 | return nf
--------------------------------------------------------------------------------
/OpenModal/analysis/genFRF.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """Module for creating FRF in frequency domain.
20 |
21 | Classes:
22 | class FRF: It creates FRF
23 | """
24 |
25 |
26 | import numpy as np
27 |
28 | import matplotlib.pyplot as plt
29 |
30 | from uff import UFF
31 |
32 |
33 |
34 | class FRF:
35 | """It creates FRF and FRF matrix from modal parameters and M,K,C matrices.
36 |
37 | Use frf_mp to generate FRF from modal parameters.
38 | Use matrixMCK to generate FRF matrix from M, K and C matrix
39 |
40 | Arguments:
41 | f_min - the low limit of frequency range
42 | f_max - the high limit of frequency range
43 | f_res - frequency resolution
44 | """
45 | def __init__(self, f_min, f_max, f_res):
46 | self.f_res = f_res
47 | self.f_min = f_min
48 | self.f_max = f_max
49 | self.freq = np.arange(f_min, f_max, self.f_res)
50 | self.om = np.array(2 * np.pi * self.freq)
51 |
52 |
53 | def frf_mp(self, A, nfreq, mi, residues='False', type='a'):
54 | """generates FRF from modal parameters
55 |
56 | Arguments:
57 | A - Modal constants of the considering FRF.
58 | nfreq - natural frequencies in Hz
59 | mi - modal dampings
60 | residues - if 'True' the residues of lower and higher residues are taken into account
61 | type - type of FRF function:
62 | -'d'- receptance
63 | -'v'- mobility
64 | -'a'- accelerance
65 | """
66 | self.A = np.array(A)
67 | self.nom = np.array(2 * np.pi * nfreq)
68 | self.mi = np.array(mi)
69 |
70 | #calculation of a FRF
71 | self.H = np.zeros(len(self.freq), dtype=complex)
72 | if residues == 'True':
73 | for i in range(0, len(self.freq)):
74 | for k in range(0, len(self.nom)):
75 | self.H[i] = self.H[i] + A[k + 1] / (self.nom[k] ** 2 - self.om[i] ** 2 + mi[k] * 1j * self.nom[k] ** 2)
76 | self.H[i] = self.H[i] * self.om[i] ** 2 + A[0] - A[-1] * self.om[i] ** 2
77 |
78 | if residues == 'False':
79 | for i in range(0, len(self.freq)):
80 | for k in range(0, len(self.nom)):
81 | self.H[i] = self.H[i] + A[k] / (self.nom[k] ** 2 - self.om[i] ** 2 + mi[k] * 1j * self.nom[k] ** 2)
82 |
83 | return self.H
84 |
85 | def matrixMKC(self, M, K, C):
86 | """generates FRF matrix from M, K and C matrices
87 |
88 | Arguments:
89 | M - mass matrix
90 | K - stiffness matrix
91 | C - viscous damping matrix
92 | """
93 | self.H = np.zeros((len(self.freq), len(M), len(M)), dtype=complex)
94 | for i in range(1, len(self.freq)):
95 | self.H[i, :, :] = np.linalg.inv(K - ((self.om[i]) ** 2) * M + 1.0j * (self.om[i])* C)
96 | return self.H
97 |
98 |
99 |
100 | def test1():
101 | A = [ -1.44424272e+00 , 9.24661162e-01, 3.83639351e-01, -9.83447395e-02, \
102 | - 5.20727677e-01, -8.60266461e-01, -1.09721165e+00, -1.19019865e+00, \
103 | - 1.20079778e+00, 3.56776014e-08]
104 | mi = [ 0.00657129 , 0.00159183 , 0.00120527 , 0.00107329, 0.00104117 , 0.00103019, \
105 | 0.00103257, 0.00108387]
106 | nf = [ 53., 146.5, 287.5, 476., 713., 1000., 1339., 1729. ]
107 | A = np.array(A)
108 | mi = np.array(mi)
109 | nf = np.array(nf)
110 | MX = FRF(0, 2000, 0.5)
111 | MX.frf_mp(A, nf, mi, residues='True')
112 |
113 |
114 | uf = UFF(r'..\..\..\tests\data\beam.uff')
115 | uffdataset58 = uf.read_sets()
116 | freq = uffdataset58[0]['x']
117 | H = np.zeros((len(uffdataset58[0]['data']), len(uffdataset58)), dtype='complex')
118 | for j in range(0, len(uffdataset58)):
119 | H[:, j] = uffdataset58[j]['data']
120 |
121 | print(freq[1], freq[-1])
122 | plt.semilogy(MX.freq, np.abs(MX.H))
123 | plt.semilogy(freq, np.abs(H[:, 1]))
124 | plt.show()
125 |
126 |
127 |
128 | def test2():
129 | m1 = 3. ;m2 = 1. ;m3 = 2.
130 | k1 = 5e7;k2 = 5e7;k3 = 1.5e3
131 | M1 = np.zeros((3, 3)) ; K1 = np.zeros((3, 3))
132 |
133 | K1[0, 0] = k1; K1[0, 1] = K1[1, 0] = -k1; K1[1, 1] = k1 + k2; K1[1, 2] = K1[2, 1] = -k2; K1[2, 2] = k2;
134 | M1[0, 0] = m1; M1[1, 1] = m2; M1[2, 2] = m3;
135 |
136 |
137 | SDOF = FRF(0, 2000, 5)
138 | SDOF.matrixMKC(M1, K1, K1 * 0.001)
139 | plt.semilogy(SDOF.freq, np.abs(SDOF.H[:, 0, 0]))
140 | plt.show()
141 |
142 |
143 |
144 | if __name__ == '__main__':
145 |
146 | test1()
147 | test2()
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/OpenModal/analysis/get_frf_peaks.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """ Searches for peaks of the FRF represented by h
20 |
21 | History:
22 | -mar 2013: janko.slavic@fs.uni-lj.si
23 | """
24 |
25 | import numpy as np
26 |
27 | def get_frf_peaks(f, h, freq_min_spacing=10):
28 | '''Searches for peaks of the FRF represented by h
29 |
30 | Keyword arguments:
31 | f -- frequency vector
32 | h -- Frequency response vector (can be complex)
33 | freq_min_spacing -- minimum spacing between two peaks (default 10)
34 |
35 | Note: units of f and freq_min_spacing are arbitrary, but need to be the same.
36 | '''
37 | df = f[1] - f[0]
38 | i_min_spacing = np.int(freq_min_spacing / df)
39 |
40 | peak_candidate = np.zeros(h.size - 2 * i_min_spacing - 2)
41 | peak_candidate[:] = True
42 | for spacing in range(i_min_spacing):
43 | h_b = np.abs(h[spacing:-2 * i_min_spacing + spacing - 2]) #before
44 | h_c = np.abs(h[i_min_spacing:-i_min_spacing - 2]) #central
45 | h_a = np.abs(h[i_min_spacing + spacing + 1:-i_min_spacing + spacing - 1]) #after
46 | peak_candidate = np.logical_and(peak_candidate, np.logical_and(h_c > h_b, h_c > h_a))
47 |
48 | ind = np.argwhere(peak_candidate == True)
49 | return ind.reshape(ind.size) + i_min_spacing#correction for central
--------------------------------------------------------------------------------
/OpenModal/analysis/get_measured_sample.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """Returns measured accelerance, mobility and receptance of a free-free beam (15mm x 30mm x 500mm).
20 |
21 | Measured at 11 points.
22 |
23 | History:
24 | -2013: janko.slavic@fs.uni-lj.si
25 |
26 | """
27 | import numpy as np
28 | import matplotlib.pyplot as plt
29 | import pyuff
30 |
31 | from OpenModal.fft_tools import convert_frf
32 |
33 |
34 | def get_measured_accelerance(show=False):
35 | uf = pyuff.UFF(r'paket.uff')
36 | uffdataset58 = uf.read_sets()
37 | freq = uffdataset58[0]
38 | H = np.zeros((len(uffdataset58[0]['data']), len(uffdataset58)), dtype='complex')
39 | for j in range(0, len(uffdataset58)):
40 | H[:, j] = uffdataset58[j]['data']
41 | if show:
42 | plt.semilogy(freq,np.abs(H[:,:]))
43 | plt.show()
44 | return freq, H
45 |
46 | def get_measured_mobility(show=False):
47 | freq, H = get_measured_accelerance(show=False)
48 | omega = 2*np.pi*freq
49 | H = convert_frf(H, omega, inputFRFtype = 'a', outputFRFtype = 'v')
50 | if show:
51 | plt.semilogy(freq,np.abs(H[:,:]))
52 | plt.show()
53 | return freq, H
54 |
55 | def get_measured_receptance(show=False):
56 | freq, H = get_measured_accelerance(show=False)
57 | omega = 2*np.pi*freq
58 | H = convert_frf(H, omega, inputFRFtype = 'a', outputFRFtype = 'd')
59 | if show:
60 | plt.semilogy(freq,np.abs(H[:,:]))
61 | plt.show()
62 | return freq, H
63 |
64 | if __name__ == '__main__':
65 | get_measured_accelerance(show=True)
66 | #get_measured_mobility(show=True)
67 | #get_measured_receptance(show=True)
--------------------------------------------------------------------------------
/OpenModal/analysis/get_simulated_sample.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | #!/usr/bin/python
20 | """Returns simulated receptance of 3DOF system.
21 |
22 | History:
23 | -mar 2013: janko.slavic@fs.uni-lj.si
24 |
25 | """
26 | import numpy as np
27 |
28 | def get_simulated_receptance(df_Hz=1, f_start=0, f_end=2000, measured_points=8, show=False, real_mode=False):
29 | '''Returns simulated receptance of 3DOF system.
30 |
31 | Keyword arguments:
32 | df_Hz=1 -- frequency resolution in Hz (default 1)
33 | f_start -- start frequency (default 0)
34 | f_end -- end frequency (default 2000)
35 | measured_points -- number of measured points (default 8)
36 | show -- show simulated receptance (default False)
37 | '''
38 | C = np.array([0.5 + 0.001j, 0.2 + 0.005j, 0.05 + 0.001j], dtype='complex') # mode residue
39 |
40 | if real_mode:
41 | C = np.real(C)
42 |
43 | eta = np.asarray([3e-3, 5e-3, 4e-3]) # damping loss factor
44 | df = df_Hz # freq resolution
45 | D = 1e-8 * (1 + 1.j) # residual of missing modes
46 |
47 | f = np.arange(f_start, f_end, step=df) # frequency range
48 |
49 | f0 = np.asarray([320.05, 850, 1680])
50 | w0 = f0 * 2 * np.pi #to rad/s
51 | w = f * 2 * np.pi
52 |
53 | n = w.size #number of samples
54 | N = measured_points #number of measured points
55 | M = eta.size #number of modes
56 |
57 | modes = np.zeros([M, N])
58 | for mode, m in zip(modes, range(M)):
59 | mode[:] = np.sin((m + 1) * np.pi * (0.5 + np.arange(N)) / (N))
60 | mode[:] = mode - np.mean(mode)
61 | if show:
62 | plt.plot(np.transpose(modes))
63 | plt.show()
64 | modal_constants = modes * np.transpose(np.asarray([C]))
65 |
66 | alpha = np.zeros([N, n], dtype='complex')
67 | for al, modes_at_pos in zip(alpha, np.transpose(modes)):
68 | for c, e, w0_, m, mode_at_pos in zip(C, eta, w0, range(M), modes_at_pos):
69 | c = c * (mode_at_pos)
70 | al[:] = al + c / (w0_ ** 2 - w ** 2 + 1.j * e * w0_ ** 2)
71 | if show:
72 | fig, [ax1, ax2] = plt.subplots(2, 1, sharex=True, figsize=(12, 10))
73 | ax1.plot(f, 20 * np.log10(np.abs(np.transpose(alpha))))
74 | ax2.plot(f, 180 / np.pi * (np.angle(np.transpose(alpha))))
75 |
76 | ax1.set_ylabel('Frequency [Hz]')
77 | ax1.set_ylabel('Magn [dB]')
78 | ax2.set_ylabel('Angle [deg]')
79 |
80 | plt.show()
81 |
82 | return f, alpha, modal_constants, eta, f0
83 |
84 | if __name__ == '__main__':
85 | import matplotlib.pyplot as plt
86 | get_simulated_receptance(show=True)
--------------------------------------------------------------------------------
/OpenModal/analysis/identification.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | #!/usr/bin/python
20 | # -*- coding: latin-1 -*-
21 | """Module which tests identification methods for estimation of modal
22 | parameters from FRF measurements
23 |
24 | Identification methods:
25 | -frfmax: natural frequency identification
26 | -ewins: Ewins Gleeson's identification method
27 | -Circle-fitting method for viscous and histeretic damping
28 | -rational fraction polynmials, orthogonal_polynomials:
29 | for obtain orthogonal polynomials which is required
30 | for rotational fraction polynomial identification method
31 | -Rational_Fraction_Polynomial: Rational fraction polynomial identification method
32 | -reconstruction: reconstruction of a FRF
33 | -ce: The Complex Exponential method
34 | -lsce: The Leas-Squares Complex Exponential Method
35 |
36 | Stabulisation charts:
37 | -stabilisation
38 |
39 | TODO: research a sign effect of mass normalized modal shapes (sign of identified and calculated
40 | modal shape are in some case different)
41 | TODO: improve the method FRFmax
42 |
43 | History:
44 | -may 2014: stabilisation, blaz.starc@fs.uni-lj.si
45 | -apr 2014: updated from python 2.7 to python 3.4, blaz.starc@fs.uni-lj.si
46 | -apr 2014: added test_ce, test_lsce; made seperate files for ce, ce_r, convert_frf, ewins, frfmax,
47 | get_frf_peaks, get_measured_accelerance, get_simulated_receptance, lsce, lsce_r,
48 | orthogonal_polynomials, Rational_Fraction_Polynomial, reconstruction; added _XTOL as a
49 | variable to circle_fit : blaz.starc@fs.uni-lj.si
50 | -apr 2013: reconstruction, tadej.kranjc@ladisk.si
51 | -mar 2013: get_simulated_receptance, reconstruction added to tests, added get_frf_peaks function and other small tweaks: janko.slavic@fs.uni-lj.si
52 | -feb 2013: Rational_Fraction_Polynomial and orthogonal_polynomials: uros.proso@fs.uni-lj.si
53 | -jan 2013: Circle-fitting method, martin.cesnik@fs.uni-lj.si
54 | -2012: ewins, tadej.kranjc@ladisk.si.
55 | """
56 |
57 | from OpenModal.analysis.ce import test_ce
58 | from OpenModal.analysis.lsce import test_lsce
59 | from OpenModal.analysis.ewins import test_ewins
60 | from OpenModal.analysis.circle_fit import test_circle_fit_visc
61 | from OpenModal.analysis.circle_fit import test_circle_fit_hist
62 | from OpenModal.analysis.rfp import test_rfp
63 | from OpenModal.analysis.stabilisation import test_ce_stabilisation
64 |
65 | if __name__ == '__main__':
66 | test_ewins()
67 | test_circle_fit_hist()
68 | test_circle_fit_visc()
69 | test_rfp()
70 | test_ce()
71 | test_lsce()
72 | test_ce_stabilisation()
73 |
--------------------------------------------------------------------------------
/OpenModal/analysis/lsce.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 | from OpenModal.analysis.utility_functions import prime_factors
21 |
22 |
23 | def lsce(frf, f, low_lim, nmax, dt, input_frf_type ='d', additional_timepoints=0,
24 | reconstruction='LSFD'):
25 | """ The Least-Squares Complex Exponential method (LSCE), introduced
26 | in [1], is the extension of the Complex Exponential method (CE) to
27 | a global procedure. It is therefore a SIMO method, processing
28 | simultaneously several IRFs obtained by exciting a structure at one
29 | single point and measuring the responses at several locations. With
30 | such a procedure, a consistent set of global parameters (natural
31 | frequencies and damping factors) is obtained, thus overcoming the
32 | variations obtained in the results for those parameters when
33 | applying the CE method on different IRFs.
34 |
35 | Literature:
36 | [1] Brown, D. L., Allemang, R. J. Zimmermann, R., Mergeay, M.,
37 | "Parameter Estimation Techniques For Modal Analysis"
38 | SAE Technical Paper Series, No. 790221, 1979
39 | [2] Ewins, D.J .; Modal Testing: Theory, practice and application,
40 | second edition. Reasearch Studies Press, John Wiley & Sons, 2000.
41 | [3] N. M. M. Maia, J. M. M. Silva, J. He, N. A. J. Lieven, R. M.
42 | Lin, G. W. Skingle, W. To, and A. P. V Urgueira. Theoretical
43 | and Experimental Modal Analysis. Reasearch Studio Press
44 | Ltd., 1997.
45 | [4] Kerschen, G., Golinval, J.-C., Experimental Modal Analysis,
46 | http://www.ltas-vis.ulg.ac.be/cmsms/uploads/File/Mvibr_notes.pdf
47 |
48 | :param frf: frequency response function array - receptance
49 | :param f: starting frequency
50 | :param low_lim: lower limit of the frf/f
51 | :param nmax: the maximal order of the polynomial
52 | :param dt: time sampling interval
53 | :param additional_timepoints - normed additional time points (default is
54 | 0% added time points, max. is 1 - all time points (100%) taken into
55 | computation)
56 | :return: list of complex eigenfrequencies
57 | """
58 |
59 | no = frf.shape[0] # number of outputs
60 | l = frf.shape[1] # length of receptance
61 | nf = 2*(l-low_lim-1) # number of DFT frequencies (nf >> n)
62 |
63 | irf = np.fft.irfft(frf[:, low_lim:], n=nf, axis=-1) # Impulse response function
64 |
65 | sr_list = []
66 | for n in range(1, nmax+1):
67 | nt = int(2 * n + additional_timepoints * (irf.shape[1] - 4 * n)) # number of time points for computation
68 |
69 | h = np.zeros((nt * no, 2 * n), dtype ='double') # the [h] (time-response) matrix
70 | hh = np.zeros(nt*no, dtype ='double') # {h'} vector, size (2N)x1
71 |
72 | for j in range(0, nt):
73 | for k in range(0, no):
74 | h[j + k*2 * n, :] = irf[k, j:j + 2 * n] # adding values to [h] matrix
75 | hh[j + k * 2 * n] = irf[k, (2 * n) + j] # adding values to {h'} vector
76 |
77 | # the computation of the autoregressive coefficients matrix
78 | beta = np.dot(np.linalg.pinv(h), -hh)
79 | sr = np.roots(np.append(beta, 1)[::-1]) # the roots of the polynomial
80 | sr = (np.log(sr)/dt).astype(complex) # the complex natural frequency
81 | sr += 2 * np.pi * f * 1j # for f_min different than 0 Hz
82 | sr_list.append(sr)
83 |
84 | if reconstruction == 'LSFD':
85 | return sr_list
86 | # elif reconstruction == 'LSCE':
87 | # return fr, xi, sr, vr, irf
88 | else:
89 | raise Exception('The reconstruction type can be either LSFD or LSCE.')
90 |
91 |
92 | # def lsce_reconstruction(n, f, sr, vr, irf, two_sided_frf = False):
93 | # """
94 | # Reconstruction of the least-squares complex exponential (CE) method.
95 | #
96 | # :param n: number of degrees of freedom
97 | # :param f: frequency vector [Hz]
98 | # :param sr: the complex natural frequency
99 | # :param vr: the roots of the polynomial
100 | # :param irf: impulse response function vector
101 | # :return: residues and reconstructed FRFs
102 | #
103 | # """
104 | # if two_sided_frf == False:
105 | # dt = 1/(len(f)*2*(f[1]-f[0])) # time step size
106 | # else:
107 | # dt = 1/(len(f)*(f[1]-f[0])) # time step size
108 | #
109 | # v = np.zeros((2*n, 2*n), dtype = 'complex')
110 | # for l in range(0, 2*n):
111 | # for k in range(0, 2*n):
112 | # v[k, l] = vr[l]**k
113 | #
114 | # hhh = np.zeros((2*n*len(irf)), dtype ='double') # {h''} vector
115 | # for j in range(0, 2*n):
116 | # for k in range(0, len(irf)):
117 | # hhh[j+ k*2*n] = irf[k, j]
118 | #
119 | # a = np.zeros((len(irf), 2*n), dtype = 'complex')
120 | # for i in range(0, len(irf)):
121 | # a[i, :] = np.linalg.solve(v, -hhh[i*2*n:(i+1)*2*n]) # the computation
122 | # # of residues
123 | # h = np.zeros(np.shape(irf)) # reconstructed irf
124 | #
125 | # for i in range(0, len(irf)):
126 | # for jk in range(0, np.shape(irf)[1]):
127 | # h[i, jk] = np.real(np.sum(a[i,:]*np.exp(sr*jk*dt)))
128 | #
129 | # return a, h
130 |
131 |
132 | def test_lsce():
133 | from OpenModal.analysis.utility_functions import complex_freq_to_freq_and_damp
134 |
135 | """ Test of the Least-Squares Complex Exponential Method """
136 | from OpenModal.analysis.get_simulated_sample import get_simulated_receptance
137 | import matplotlib.pyplot as plt
138 |
139 | f, frf, modal_sim, eta_sim, f0_sim = get_simulated_receptance(df_Hz=1,
140 | f_start=0, f_end=5001, measured_points=8, show=False, real_mode=False)
141 |
142 | low_lim = 100
143 | nf = (2*(len(f)-low_lim-1))
144 |
145 | while max(prime_factors(nf)) > 5:
146 | f = f[:-1]
147 | frf = frf[:, :-1]
148 | nf = (2*(len(f)-low_lim-1))
149 |
150 |
151 | df = (f[1] - f[0])
152 | nf = 2*(len(f)-low_lim-1)
153 | ts = 1 / (nf * df) # sampling period
154 |
155 | n = 12
156 | low_lim = 100
157 |
158 | sr = lsce(frf, f[low_lim], low_lim, n, ts, additional_timepoints=0, reconstruction='LSFD')
159 |
160 | fr, xi = complex_freq_to_freq_and_damp(sr[-2])
161 |
162 | print("fr\n", fr)
163 | print("xi\n", xi)
164 |
165 | # A, h = lsce_reconstruction(n, f, sr, vr, irf, two_sided_frf=False)
166 | #
167 | # plt.Figure()
168 | # fig, figure = plt.subplots(len(irf),2)
169 | # for i in range(0, len(irf)):
170 | # figure[i, 0].plot(np.abs(np.fft.rfft(irf[i, :])))
171 | # figure[i, 0].plot(np.abs(np.fft.rfft(h[i, :])))
172 | # figure[i, 0].semilogy()
173 | # figure[i, 0].set_xlabel('$f$ [Hz]')
174 | # figure[i, 0].set_ylabel('Magnitude [dB]')
175 | #
176 | # figure[i, 1].plot(-180+np.abs(np.angle(np.fft.rfft(irf[i, :])))*180/np.pi)
177 | # figure[i, 1].plot(-np.abs(np.angle(np.fft.rfft(h[i, :])))*180/np.pi)
178 | # figure[i, 1].set_xlabel('$f$ [Hz]')
179 | # figure[i, 1].set_ylabel('$Phase$ [deg]')
180 | # plt.show()
181 |
182 | if __name__ == '__main__':
183 | test_lsce()
--------------------------------------------------------------------------------
/OpenModal/analysis/lscf.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 | from OpenModal.analysis.get_simulated_sample import get_simulated_receptance
21 | from OpenModal.fft_tools import irfft_adjusted_lower_limit
22 | from OpenModal.analysis.utility_functions import toeplitz, prime_factors, complex_freq_to_freq_and_damp
23 |
24 |
25 | def lscf(frf, low_lim, n, dt, weighing_type='Unity', reconstruction='LSFD'):
26 | """
27 | LSCF - Least-Squares Complex frequency domain method
28 |
29 | The LSCF method is an frequency-domain Linear Least Squares
30 | estimator optimized for modal parameter estimation. The choice of
31 | the most important algorithm characteristics is based on the
32 | results in [1] (Section 5.3.3.) and can be summarized as:
33 |
34 | - Formulation: the normal equations [1]
35 | (Eq. 5.26: [sum(Tk - Sk.H * Rk^-1 * Sk)]*ThetaA=D*ThetaA = 0)
36 | are constructed for the common denominator discrete-time
37 | model in the Z-domain. Consequently, by looping over the
38 | outputs and inputs, the submatrices Rk, Sk, and Tk are
39 | formulated through the use of the FFT algorithm as Toeplitz
40 | structured (n+1) square matrices. Using complex coefficients,
41 | the FRF data within the frequency band of interest (FRF-zoom)
42 | is projected in the Z-domain in the interval of [0, 2*pi] in
43 | order to improve numerical conditioning. (In the case that
44 | real coefficients are used, the data is projected in the
45 | interval of [0, pi].) The projecting on an interval that does
46 | not completely describe the unity circle, say [0, alpha*2*pi]
47 | where alpha is typically 0.9-0.95. Deliberately over-modeling
48 | is best applied to cope with discontinuities. This is
49 | justified by the use of a discrete time model in the Z-domain,
50 | which is much more robust for a high order of the transfer
51 | function polynomials.
52 |
53 | - Solver: the normal equations can be solved for the
54 | denominator coefficients ThetaA by computing the Least-Squares
55 | (LS) or mixed Total-Least-Squares (TLS) solution. The inverse
56 | of the square matrix D for the LS solution is computed by
57 | means of a pseudo inverse operation for reasons of numerical
58 | stability, while the mixed LS-TLS solution is computed using
59 | an SVD (Singular Value Decomposition).
60 |
61 | Literature:
62 | [1] Verboven, P., Frequency-domain System Identification for
63 | Modal Analysis, Ph. D. thesis, Mechanical Engineering Dept.
64 | (WERK), Vrije Universiteit Brussel, Brussel, (Belgium),
65 | May 2002, (http://mech.vub.ac.be/avrg/PhD/thesis_PV_web.pdf)
66 |
67 | [2] Verboven, P., Guillaume, P., Cauberghe, B., Parloo, E. and
68 | Vanlanduit S., Stabilization Charts and Uncertainty Bounds
69 | For Frequency-Domain Linear Least Squares Estimators, Vrije
70 | Universiteit Brussel(VUB), Mechanical Engineering Dept.
71 | (WERK), Acoustic and Vibration Research Group (AVRG),
72 | Pleinlaan 2, B-1050 Brussels, Belgium,
73 | e-mail: Peter.Verboven@vub.ac.be, url:
74 | (http://sem-proceedings.com/21i/sem.org-IMAC-XXI-Conf-s02p01
75 | -Stabilization-Charts-Uncertainty-Bounds-Frequency-Domain-
76 | Linear-Least.pdf)
77 | [3] P. Guillaume, P. Verboven, S. Vanlanduit, H. Van der
78 | Auweraer, B. Peeters, A Poly-Reference Implementation of the
79 | Least-Squares Complex Frequency-Domain Estimator, Vrije
80 | Universiteit Brussel, LMS International
81 |
82 | :param frf: frequency response function - receptance
83 | :param low_lim: lower limit of the frf
84 | :param n: the order of the polynomial
85 | :param dt: time sampling interval
86 | :param weighing_type: weighing type (TO BE UPDATED)
87 | :param reconstruction: type of reconstruction - LSFD or LSCF
88 | :return: eigenfrequencies and the corresponding damping
89 | """
90 |
91 | n *= 2 # the poles should be complex conjugate, therefore we expect even polynomial order
92 |
93 | nr = frf.shape[0] # (number of inputs) * (number of outputs)
94 |
95 | l = frf.shape[1] # length of receptance
96 |
97 | nf = 2*(l-1) # number of DFT frequencies (nf >> n)
98 |
99 | indices_s = np.arange(-n, n+1)
100 | indices_t = np.arange(n+1)
101 | # Selection of the weighting function
102 |
103 | # Least-Squares (LS) Formulation based on Normal Matrix
104 | sk = -irfft_adjusted_lower_limit(frf, low_lim, indices_s)
105 | t = irfft_adjusted_lower_limit(frf.real**2 + frf.imag**2,
106 | low_lim, indices_t)
107 | r = -(np.fft.irfft(np.ones(low_lim), n=nf))[indices_t]*nf
108 | r[0] += nf
109 |
110 | s = []
111 | for i in range(nr):
112 | s.append(toeplitz(sk[i, n:], sk[i, :n+1][::-1]))
113 | t = toeplitz(np.sum(t[:, :n+1], axis=0))
114 | r = toeplitz(r)
115 |
116 | sr_list = []
117 | for j in range(2, n+1, 2):
118 | d = 0
119 | for i in range(nr):
120 | rinv = np.linalg.inv(r[:j+1, :j+1])
121 | snew = s[i][:j+1, :j+1]
122 | d -= np.dot(np.dot(snew[:j+1, :j+1].T, rinv), snew[:j+1, :j+1]) # sum
123 | d += t[:j+1, :j+1]
124 |
125 | a0an1 = np.linalg.solve(-d[0:j, 0:j], d[0:j, j])
126 | sr = np.roots(np.append(a0an1, 1)[::-1]) # the numerator coefficients
127 | sr = -np.log(sr) / dt # Z-domain (for discrete-time domain model)
128 | sr_list.append(sr)
129 |
130 | if reconstruction == 'LSFD':
131 | return sr_list
132 | # elif reconstruction == 'LSCF':
133 | # omegaf = np.exp(-1j * omega * ts) # generalized transform variable in Z-domain
134 | # return fr, xi, r, s, theta_a, omegaf, ni, no, n, omega
135 | else:
136 | raise Exception('The reconstruction type can be either LSFD or LSCF.')
137 |
138 |
139 | def test_lsfd():
140 | f, frf, modal_sim, eta_sim, f0_sim = get_simulated_receptance(
141 | df_Hz=1, f_start=0, f_end=5001, measured_points=8, show=False, real_mode=False)
142 |
143 | low_lim = 1500
144 | nf = (2*(len(f)-1))
145 |
146 | while max(prime_factors(nf)) > 5:
147 | f = f[:-1]
148 | frf = frf[:, :-1]
149 | nf = (2*(len(f)-1))
150 |
151 | df = (f[1] - f[0])
152 | nf = 2*(len(f)-1)
153 | ts = 1 / (nf * df) # sampling period
154 |
155 | sr = lscf(frf, low_lim, 6, ts, weighing_type='Unity', reconstruction='LSFD')
156 | fr, xi = complex_freq_to_freq_and_damp(sr[-1])
157 | print('Eigenfrequencies\n', fr)
158 | print('Damping factors\n', xi)
159 |
160 | if __name__ == '__main__':
161 | # test_lscf()
162 | test_lsfd()
163 |
--------------------------------------------------------------------------------
/OpenModal/analysis/lsfd.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 |
21 |
22 | def lsfd(lambdak, f, frf):
23 | """
24 | LSFD (Least-Squares Frequency domain) method is used in order
25 | to determine the residues and mode shapes from complex natural frquencies
26 | and the measured frequency response functions.
27 |
28 | :param lambdak: a vector of selected complex natural frequencies
29 | :param f: frequecy vector
30 | :param frf: frequency response functions
31 | :return: reconstructed FRF, modal constant(residue), lower residual, upper residual
32 | """
33 |
34 | ni = frf.shape[0] # number of references
35 | no = frf.shape[1] # number of responses
36 | n = frf.shape[2] # length of frequency vector
37 | nmodes = lambdak.shape[0] # number of modes
38 |
39 | omega = 2 * np.pi * f # angular frequency
40 |
41 | # Factors in the freqeuncy response function
42 | b = 1 / np.subtract.outer(1j * omega, lambdak).T
43 | c = 1 / np.subtract.outer(1j * omega, np.conj(lambdak)).T
44 |
45 | # Separate complex data to real and imaginary part
46 | hr = frf.real
47 | hi = frf.imag
48 | br = b.real
49 | bi = b.imag
50 | cr = c.real
51 | ci = c.imag
52 |
53 | # Stack the data together in order to obtain 2D matrix
54 | hri = np.dstack((hr, hi))
55 | bri = np.hstack((br+cr, bi+ci))
56 | cri = np.hstack((-bi+ci, br-cr))
57 |
58 | ur_multiplyer = np.ones(n)
59 | ur_zeros = np.zeros(n)
60 | lr_multiplyer = -1/(omega**2)
61 |
62 | urr = np.hstack((ur_multiplyer, ur_zeros))
63 | uri = np.hstack((ur_zeros, ur_multiplyer))
64 | lrr = np.hstack((lr_multiplyer, ur_zeros))
65 | lri = np.hstack((ur_zeros, lr_multiplyer))
66 |
67 | bcri = np.vstack((bri, cri, urr, uri, lrr, lri))
68 |
69 | # Reshape 3D array to 2D for least squares coputation
70 | hri = hri.reshape(ni*no, 2*n)
71 |
72 | # Compute the modal constants (residuals) and upper and lower residuals
73 | uv, _, _, _ = np.linalg.lstsq(bcri.T,hri.T)
74 |
75 | # Reshape 2D results to 3D
76 | uv = uv.T.reshape(ni, no, 2*nmodes+4)
77 |
78 | u = uv[:, :, :nmodes]
79 | v = uv[:, :, nmodes:-4]
80 | urr = uv[:, :, -4]
81 | uri = uv[:, :, -3]
82 | lrr = uv[:, :, -2]
83 | lri = uv[:, :, -1]
84 |
85 | a = u + 1j * v # Modal constant (residue)
86 | ur = urr + 1j * uri # Upper residual
87 | lr = lrr + 1j * lri # Lower residual
88 |
89 | # Reconstructed FRF matrix
90 | h = np.dot(uv, bcri)
91 | h = h[:, :, :n] + 1j * h[:, :, n:]
92 |
93 | return h, a, lr, ur
94 |
--------------------------------------------------------------------------------
/OpenModal/analysis/stabilisation.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """ Stabilisation chart
20 |
21 | Stabilisation charts are needed in various modal identification methods
22 | in order to determine the real modal parameters.
23 |
24 | History: - may 2014: stabilisation, stabilisation_plot,
25 | stabilisation_test, redundant_values: Blaz Starc,
26 | blaz.starc@fs.uni-lj.si
27 | """
28 |
29 | import numpy as np
30 | import matplotlib.pyplot as plt
31 | import pyqtgraph as pg
32 |
33 | from OpenModal.analysis.get_simulated_sample import get_simulated_receptance
34 | from OpenModal.analysis.utility_functions import complex_freq_to_freq_and_damp
35 |
36 |
37 |
38 | def stabilisation(sr, nmax, err_fn, err_xi):
39 | """
40 | A function that computes the stabilisation matrices needed for the
41 | stabilisation chart. The computation is focused on comparison of
42 | eigenfrequencies and damping ratios in the present step
43 | (N-th model order) with the previous step ((N-1)-th model order).
44 |
45 | :param sr: list of lists of complex natrual frequencies
46 | :param n: maximum number of degrees of freedom
47 | :param err_fn: relative error in frequency
48 | :param err_xi: relative error in damping
49 |
50 | :return fn_temp eigenfrequencies matrix
51 | :return xi_temp: updated damping matrix
52 | :return test_fn: updated eigenfrequencies stabilisation test matrix
53 | :return test_xi: updated damping stabilisation test matrix
54 |
55 | @author: Blaz Starc
56 | @contact: blaz.starc@fs.uni-lj.si
57 | """
58 |
59 | # TODO: check this later for optimisation # this doffers by LSCE and LSCF
60 | fn_temp = np.zeros((2*nmax, nmax), dtype = 'double')
61 | xi_temp = np.zeros((2*nmax, nmax), dtype = 'double')
62 | test_fn = np.zeros((2*nmax, nmax), dtype = 'int')
63 | test_xi = np.zeros((2*nmax, nmax), dtype = 'int')
64 |
65 | for nr, n in enumerate(range(nmax)):
66 | fn, xi = complex_freq_to_freq_and_damp(sr[nr])
67 | fn, xi = redundant_values(fn, xi, 1e-3) # elimination of conjugate values in
68 | # order to decrease computation time
69 | if n == 1:
70 | # first step
71 | fn_temp[0:len(fn), 0:1] = fn
72 | xi_temp[0:len(fn), 0:1] = xi
73 |
74 | else:
75 | # Matrix test is created for comparison between present(N-th) and
76 | # previous (N-1-th) data (eigenfrequencies). If the value equals:
77 | # --> 1, the data is within relative tolerance err_fn
78 | # --> 0, the data is outside the relative tolerance err_fn
79 | fn_test = np.zeros((len(fn), len(fn_temp[:, n - 1])), dtype ='int')
80 | for i in range(0, len(fn)):
81 | for j in range(0, len(fn_temp[0:2*(n), n-1])):
82 | if fn_temp[j, n-2] == 0:
83 | fn_test[i,j] = 0
84 | else:
85 | if np.abs((fn[i] - fn_temp[j, n-2])/fn_temp[j, n-2]) < err_fn:
86 | fn_test[i,j] = 1
87 | else: fn_test[i,j] = 0
88 |
89 | for i in range(0, len(fn)):
90 | test_fn[i, n - 1] = np.sum(fn_test[i, :]) # all rows are summed together
91 |
92 | # The same procedure as for eigenfrequencies is applied for damping
93 | xi_test = np.zeros((len(xi), len(xi_temp[:, n - 1])), dtype ='int')
94 | for i in range(0, len(xi)):
95 | for j in range(0, len(xi_temp[0:2*(n), n-1])):
96 | if xi_temp[j, n-2]==0:
97 | xi_test[i,j] = 0
98 | else:
99 | if np.abs((xi[i] - xi_temp[j, n-2])/xi_temp[j, n-2]) < err_xi:
100 | xi_test[i,j] = 1
101 | else: xi_test[i,j] = 0
102 | for i in range(0, len(xi)):
103 | test_xi[i, n - 1] = np.sum(xi_test[i, :])
104 |
105 | # If the frequency/damping values corresponded to the previous iteration,
106 | # a mean of the two values is computed, otherwise the value stays the same
107 | for i in range(0, len(fn)):
108 | for j in range(0, len(fn_temp[0:2*(n), n-1])):
109 | if fn_test[i,j] == 1:
110 | fn_temp[i, n - 1] = (fn[i] + fn_temp[j, n - 2]) / 2
111 | elif fn_test[i,j] == 0:
112 | fn_temp[i, n - 1] = fn[i]
113 | for i in range(0, len(fn)):
114 | for j in range(0, len(fn_temp[0:2*(n), n-1])):
115 | if xi_test[i,j] == 1:
116 | xi_temp[i, n - 1] = (xi[i] + xi_temp[j, n - 2]) / 2
117 | elif xi_test[i,j] == 0:
118 | xi_temp[i, n - 1] = xi[i]
119 |
120 | return fn_temp, xi_temp, test_fn, test_xi
121 |
122 |
123 | def stabilisation_plot_pyqtgraph(test_fn, test_xi, fn_temp, xi_temp):
124 | """
125 | A function which shows te stabilisation chart and returns the
126 | stabiliesed eigenfrquencies and damping ratios.
127 |
128 | Input:
129 | test_fn - test matrix giving information about the
130 | eigenfrequencies stabilisation
131 | test_xi - test matrix giving information about the
132 | damping stabilisation
133 | fn_temp - eigenfrequencies matrix
134 | xi_temp - damping matrix
135 | Nmax - highest model order
136 | f - frequency vector
137 | FRF - frequency response function (for plotting)
138 |
139 | Output:
140 | spots - spots for plotting in stabilisation chart
141 |
142 | @author: Blaz Starc
143 | @contact: blaz.starc@fs.uni-lj.si
144 | """
145 | a=np.argwhere((test_fn > 0) & (test_xi == 0)) # stable eigenfrequencues, unstable damping ratios
146 | b=np.argwhere((test_fn > 0) & (test_xi > 0)) # stable eigenfrequencies, stable damping ratios
147 | c=np.argwhere((test_fn == 0) & (test_xi == 0)) # unstable eigenfrequencues, unstable damping ratios
148 | d=np.argwhere((test_fn == 0) & (test_xi > 0)) # unstable eigenfrequencues, stable damping ratios
149 |
150 | spots = []
151 | xi = []
152 |
153 | for i in range(0,len(a)):
154 | spots.append({'pos': (fn_temp[a[i, 0], a[i, 1]], 1+a[i, 1]), 'size': 10,
155 | 'pen': {'color': 'w', 'width': 0.3}, 'symbol': 'd', 'brush': 'y'})
156 | xi.append(xi_temp[a[i, 0], a[i, 1]])
157 |
158 |
159 | # for i in range(0, len(c)):
160 | # spots.append({'pos': (fn_temp[c[i, 0], c[i, 1]], 1+c[i, 1]), 'size': 8,
161 | # 'pen': {'color': 'w', 'width': 0.3}, 'symbol': 't', 'brush': 'g'})
162 | # for i in range(0, len(d)):
163 | # spots.append({'pos': (fn_temp[d[i, 0], d[i, 1]], 1+d[i, 1]), 'size': 8,
164 | # 'pen': {'color': 'w', 'width': 0.3}, 'symbol': 's', 'brush': 'b'})
165 |
166 | for i in range(0, len(b)):
167 | spots.append({'pos': (fn_temp[b[i, 0], b[i, 1]], 1+b[i, 1]), 'size': 15,
168 | 'pen': {'color': 'w', 'width': 0.3}, 'symbol': '+', 'brush': 'r'})
169 | xi.append(xi_temp[b[i, 0], b[i, 1]])
170 |
171 | return spots, xi
172 |
173 | def stabilisation_plot(test_fn, test_xi, fn_temp, xi_temp, Nmax, f, FRF):
174 | """
175 | A function which shows te stabilisation chart and returns the
176 | stabiliesed eigenfrquencies and damping ratios.
177 |
178 | Input:
179 | test_fn - test matrix giving information about the
180 | eigenfrequencies stabilisation
181 | test_xi - test matrix giving information about the
182 | damping stabilisation
183 | fn_temp - eigenfrequencies matrix
184 | xi_temp - damping matrix
185 | Nmax - highest model order
186 | f - frequency vector
187 | FRF - frequency response function (for plotting)
188 |
189 | Output:
190 | stable_fn - stable eigenfrequencies values
191 | stable_xi - stable damping values
192 |
193 | @author: Blaz Starc
194 | @contact: blaz.starc@fs.uni-lj.si
195 | """
196 | a=np.argwhere((test_fn>0) & (test_xi==0)) # stable eigenfrequencues, unstable damping ratios
197 | b=np.argwhere((test_fn>0) & (test_xi>0) ) # stable eigenfrequencies, stable damping ratios
198 | c=np.argwhere((test_fn==0) & (test_xi==0)) # unstable eigenfrequencues, unstable damping ratios
199 | d=np.argwhere((test_fn==0) & (test_xi>0)) # unstable eigenfrequencues, stable damping ratios
200 |
201 | #import matplotlib.pyplot as plt
202 | plt.figure()
203 | for i in range(0,len(a)):
204 | p1=plt.scatter(fn_temp[a[i,0], a[i,1]], 1+a[i,1], s=80, c='b', marker='x')
205 | for i in range(0,len(b)):
206 | p2=plt.scatter(fn_temp[b[i,0], b[i,1]] ,1+b[i,1], s=100, c='r', marker='+')
207 | for i in range(0,len(c)):
208 | p3=plt.scatter(fn_temp[c[i,0], c[i,1]], 1+c[i,1], s=80, c='g', marker='1')
209 | for i in range(0,len(d)):
210 | p4=plt.scatter(fn_temp[d[i,0], d[i,1]], 1+d[i,1], s=80, c='m', marker='4')
211 | plt.plot(f, np.abs(FRF)/np.max(np.abs(FRF))*(0.8*Nmax))
212 | plt.xlabel('Frequency [Hz]')
213 | plt.ylabel('Model order')
214 | plt.xlim([np.min(f),np.max(f)])
215 | plt.ylim([-0.5, Nmax+1])
216 | #plt.legend([p1, p2, p3, p4], ["stable eignfr., unstable damp.",
217 | # "stable eignfr., stable damp.",
218 | # "unstable eignfr., unstable damp.",
219 | # "unstable eignfr., stable damp."])
220 | plt.show()
221 | stable_fn = np.zeros(len(b), dtype='double')
222 | stable_xi = np.zeros(len(b), dtype='double')
223 | for i in range(0,len(b)):
224 | stable_fn[i] = fn_temp[b[i,0],b[i,1]]
225 | stable_xi[i] = xi_temp[b[i,0],b[i,1]]
226 |
227 | return stable_fn, stable_xi
228 |
229 | def redundant_values(omega, xi, prec):
230 | """
231 | This function supresses the redundant values of frequency and damping
232 | vectors, which are the consequence of conjugate values
233 |
234 | Input:
235 | omega - eiqenfrquencies vector
236 | xi - damping ratios vector
237 | prec - absoulute precision in order to distinguish between two values
238 |
239 | @author: Blaz Starc
240 | @contact: blaz.starc@fs.uni-lj.si
241 | """
242 | N = len(omega)
243 | test_omega = np.zeros((N,N), dtype='int')
244 | for i in range(1,N):
245 | for j in range(0,i):
246 | if np.abs((omega[i] - omega[j])) < prec:
247 | test_omega[i,j] = 1
248 | else: test_omega[i,j] = 0
249 | test = np.zeros(N, dtype = 'int')
250 | for i in range(0,N):
251 | test[i] = np.sum(test_omega[i,:])
252 |
253 | omega_mod = omega[np.argwhere(test<1)]
254 | xi_mod = xi[np.argwhere(test<1)]
255 | return omega_mod, xi_mod
256 |
257 |
258 | def test_stabilisation():
259 | from OpenModal.analysis.lscf import lscf
260 | from OpenModal.analysis.utility_functions import prime_factors
261 |
262 | """ Test of the Complex Exponential Method and stabilisation """
263 | f, frf, modal_sim, eta_sim, f0_sim = get_simulated_receptance(
264 | df_Hz=1, f_start=0, f_end=5001, measured_points=8, show=False, real_mode=False)
265 |
266 | low_lim = 0
267 | nf = (2 * (len(f) - 1))
268 | print(nf)
269 | while max(prime_factors(nf)) > 5:
270 | f = f[:-1]
271 | frf = frf[:, :-1]
272 | nf = (2 * (len(f) - 1))
273 | print(nf)
274 |
275 | df = (f[1] - f[0])
276 | nf = 2 * (len(f) - 1)
277 | ts = 1 / (nf * df) # sampling period
278 |
279 | nmax = 30
280 | sr = lscf(frf, low_lim, nmax, ts, weighing_type='Unity', reconstruction='LSFD')
281 | # N = np.zeros(nmax, dtype = 'int')
282 |
283 | err_fn = 0.001
284 | err_xi = 0.005
285 |
286 | fn_temp,xi_temp, test_fn, test_xi= stabilisation(sr, nmax, err_fn, err_xi)
287 |
288 | stable_fn, stable_xi = stabilisation_plot(test_fn, test_xi, fn_temp, xi_temp, nmax, f, frf.T)
289 | # print(fn_temp)
290 |
291 |
292 | if __name__ == '__main__':
293 | test_stabilisation()
--------------------------------------------------------------------------------
/OpenModal/analysis/utility_functions.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 | def myfunc(c, d, e):
21 | if d > c and e < d :
22 | return 1
23 | else:
24 | return 0
25 |
26 | def coeffts(xData, yData):
27 | '''Returns coefficients of Newton interpolation function.
28 | '''
29 | m = len(xData) # Number of data points
30 | a = yData.copy()
31 | for k in range(1, m):
32 | a[k:m] = (a[k:m] - a[k - 1]) / (xData[k:m] - xData[k - 1])
33 | return a
34 |
35 |
36 | def evalPoly(a, xData, x):
37 | '''Interpolate values using coefficients of Newton polynomial.
38 | '''
39 | n = len(xData) - 1 # Degree of polynomial
40 | p = a[n]
41 | for k in range(1, n + 1):
42 | p = a[n - k] + (x - xData[n - k]) * p
43 | return p
44 |
45 |
46 | def circle_error(circle, *args):
47 | '''Function to be minimized to get the best fit of the circle iterating through (x0, y0, R0) based on the return value of the error.
48 | '''
49 | (x0, y0, R0) = circle
50 | transmiss = np.array(args[:])
51 |
52 | error = np.sum((R0 - np.sqrt((np.real(transmiss) - x0) ** 2 + (np.imag(transmiss) - y0) ** 2)) ** 2)
53 | return error
54 |
55 |
56 | def complex_freq_to_freq_and_damp(sr):
57 | """
58 | Convert the complex natural frequencies to natural frequencies and the
59 | corresponding dampings.
60 |
61 | :param sr: complex natural frequencies
62 | :return: natural frequency and damping
63 | """
64 |
65 | fr = np.abs(sr)
66 | xir = -sr.real/fr
67 | fr /= (2 * np.pi)
68 |
69 | return fr, xir
70 |
71 |
72 | def toeplitz(c, r=None):
73 | """
74 | Construct a Toeplitz matrix.
75 | The Toeplitz matrix has constant diagonals, with c as its first column
76 | and r as its first row. If r is not given, ``r == conjugate(c)`` is
77 | assumed.
78 | Parameters
79 | ----------
80 | c : array_like
81 | First column of the matrix. Whatever the actual shape of `c`, it
82 | will be converted to a 1-D array.
83 | r : array_like
84 | First row of the matrix. If None, ``r = conjugate(c)`` is assumed;
85 | in this case, if c[0] is real, the result is a Hermitian matrix.
86 | r[0] is ignored; the first row of the returned matrix is
87 | ``[c[0], r[1:]]``. Whatever the actual shape of `r`, it will be
88 | converted to a 1-D array.
89 | Returns
90 | -------
91 | A : (len(c), len(r)) ndarray
92 | The Toeplitz matrix. Dtype is the same as ``(c[0] + r[0]).dtype``.
93 | See also
94 | --------
95 | circulant : circulant matrix
96 | hankel : Hankel matrix
97 | Notes
98 | -----
99 | The behavior when `c` or `r` is a scalar, or when `c` is complex and
100 | `r` is None, was changed in version 0.8.0. The behavior in previous
101 | versions was undocumented and is no longer supported.
102 | Examples
103 | --------
104 | >>> from scipy.linalg import toeplitz
105 | >>> toeplitz([1,2,3], [1,4,5,6])
106 | array([[1, 4, 5, 6],
107 | [2, 1, 4, 5],
108 | [3, 2, 1, 4]])
109 | >>> toeplitz([1.0, 2+3j, 4-1j])
110 | array([[ 1.+0.j, 2.-3.j, 4.+1.j],
111 | [ 2.+3.j, 1.+0.j, 2.-3.j],
112 | [ 4.-1.j, 2.+3.j, 1.+0.j]])
113 | """
114 | c = np.asarray(c).ravel()
115 | if r is None:
116 | r = c.conjugate()
117 | else:
118 | r = np.asarray(r).ravel()
119 | # Form a 1D array of values to be used in the matrix, containing a reversed
120 | # copy of r[1:], followed by c.
121 | vals = np.concatenate((r[-1:0:-1], c))
122 | a, b = np.ogrid[0:len(c), len(r) - 1:-1:-1]
123 | indx = a + b
124 | # `indx` is a 2D array of indices into the 1D array `vals`, arranged so
125 | # that `vals[indx]` is the Toeplitz matrix.
126 | return vals[indx]
127 |
128 |
129 | def prime_factors(n):
130 | """Returns all the prime factors of a positive integer"""
131 | factors = []
132 | d = 2
133 | while n > 1:
134 | while n % d == 0:
135 | factors.append(d)
136 | n /= d
137 | d += 1
138 | if d*d > n:
139 | if n > 1:
140 | factors.append(n)
141 | break
142 | return factors
143 |
144 |
145 | def get_analysis_id(analysis_id):
146 | """
147 | Get the new analysis id
148 | :param analysis_id: analysis_index DataFrame
149 | :return: new analysis_id
150 | """
151 |
152 | if analysis_id.size == 0:
153 | analysis_id = 0
154 |
155 | else:
156 | analysis_id = np.nanmax(analysis_id.values) + 1
157 |
158 | return int(analysis_id)
--------------------------------------------------------------------------------
/OpenModal/daqprocess.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 | import numpy as np
21 | import time
22 | import RingBuffer as RingBuffer
23 | import DAQTask as DAQTask
24 | import multiprocessing as mp
25 |
26 | _DIRECTIONS = ['scalar', '+x', '+y', '+z', '-x', '-y', '-z']
27 | _DIRECTIONS_NR = [0, 1, 2, 3, -1, -2 - 3]
28 |
29 | def direction_dict():
30 | dir_dict = {a: b for a, b in zip(_DIRECTIONS, _DIRECTIONS_NR)}
31 | return dir_dict
32 |
33 | class MeasurementProcess(object):
34 | """Impact measurement handler.
35 |
36 | :param task_name: task name for the daq
37 | :param samples_per_channel: number of samples per channel.
38 | if 'auto' then: samples_per_channel=sampling_rate
39 | :param exc_channel: the number of excitation channel - necessary for triggering
40 | :param channel_delay: list of channel delays (lasers typical have a time delay close to 1ms)
41 | :param fft_len: the length of the FFT, if 'auto' then the freq length matches the time length
42 | :param trigger_level: amplitude level at which to trigger.
43 | :param pre_trigger_samples: how many samples should be pre-triggered
44 | """
45 | def __init__(self, task_name=None, samples_per_channel='auto',
46 | channel_delay=[0., 0.], exc_channel=0,
47 | fft_len='auto', trigger_level=5, pre_trigger_samples=10):
48 | """Constructor. Only the defaults are passed in at initialization. This starts the separate thread (which takes
49 | some time) and waits for the user. When the preferences are setup this same parameters are reloaded. At the
50 | moment, this is done from the parent script using Impact.__dict__."""
51 | super(MeasurementProcess, self).__init__()
52 |
53 | self.key_list = dict(excitation_type=None, task_name=None, samples_per_channel='auto',
54 | channel_delay=[0., 0.], exc_channel=0,
55 | fft_len='auto', trigger_level=5, pre_trigger_samples=10, n_averages=8)
56 |
57 | self.parameters = dict()
58 |
59 | # Trigger, keeps the thread alive.
60 | self.live_flag = mp.Value('b', False)
61 |
62 | self.setup_measurement_parameters(locals())
63 |
64 | def setup_measurement_parameters(self, parameters_dict):
65 | """Load parameters, not in __init__ but later. The object is initialized early, before the parameters are known,
66 | to reduce waiting time. Get the settings dictionary but only load what you need (written in self.key_list)."""
67 | # Setup parameters. Defaults taken from self.key_list when not available.
68 | for key in self.key_list.keys():
69 | if key in parameters_dict:
70 | self.__setattr__(key, parameters_dict[key])
71 | # Also prepare a dict with those same values
72 | else:
73 | self.__setattr__(key, self.key_list[key])
74 |
75 | def start_process(self):
76 | """Start a separate measurement process, do not start collecting the data just yet (run_measurement)."""
77 | # . Prepare separate measurement process.
78 |
79 | # Trigger, keeps the thread alive.
80 | # self.live_flag = mp.Value('b', True)
81 | self.live_flag.value = True
82 |
83 | # .. Trigger, starts the measurement - shared variable.
84 | self.run_flag = mp.Value('b', False)
85 |
86 | # .. Trigger, signals the signal level trigger was tripped. This trigger is picked up in the parent app.
87 | self.triggered = mp.Value('b', False)
88 |
89 | # Pipe to send parameters to the separate thread.
90 | self.properties_out, self.properties_in = mp.Pipe(False)
91 |
92 | # Pipe to send sampling_rate through. Later maybe also other data.
93 | self.task_info_out, self.task_info_in = mp.Pipe(False)
94 |
95 | # Start the thread. Pipe end and shared variables are passed.
96 | self.process = mp.Process(target=ThreadedDAQ, args=(self.live_flag, self.run_flag, self.properties_out,
97 | self.task_info_in, self.triggered))
98 | self.process.start()
99 |
100 | def stop_process(self, timeout=2):
101 | """Stop the process."""
102 | if self.run_flag.value:
103 | self.stop_measurement()
104 |
105 | if self.live_flag.value:
106 | self.live_flag.value = False
107 |
108 | # Close the pipes.
109 | self.properties_out.close()
110 | self.properties_in.close()
111 | self.task_info_in.close()
112 | self.task_info_out.close()
113 |
114 | self.process.join(timeout)
115 |
116 | def run_measurement(self):
117 | """Start measuring."""
118 | if self.run_flag.value:
119 | print('Process already running.')
120 | elif not self.run_flag.value:
121 | # First push fresh arguments to the separate process.
122 | pdict = dict(excitation_type=self.excitation_type, task_name=self.task_name,
123 | samples_per_channel=self.samples_per_channel,
124 | exc_channel=self.exc_channel,
125 | channel_delay=self.channel_delay,
126 | fft_len=self.fft_len, trigger_level=self.trigger_level,
127 | pre_trigger_samples=self.pre_trigger_samples,
128 | n_averages=self.n_averages)
129 |
130 | # Reinitialize pipes beforehand (pipes are closed each time the measurement is stopped).
131 | self.process_measured_data_out, self.process_measured_data_in = mp.Pipe(False)
132 | self.process_random_chunk_out, self.process_random_chunk_in = mp.Pipe(False)
133 |
134 | pdict['measured_data_pipe'] = self.process_measured_data_in
135 | pdict['random_chunk_pipe'] = self.process_random_chunk_in
136 |
137 | # Actually send the data over the pipe.
138 | self.properties_in.send(pdict)
139 |
140 | # Send a start signal to the process object.
141 | self.run_flag.value = True
142 |
143 | def stop_measurement(self):
144 | """Stop measuring."""
145 | if not self.run_flag.value:
146 | # Check if the process should already be stopped.
147 | raise Exception('Process already stopped.')
148 | elif self.run_flag.value:
149 | # Stop it and close the pipes. Both ends of the pipes should be closed at the same time, like below.
150 | self.run_flag.value = False
151 | self.triggered.value = False
152 | self.process_measured_data_in.close()
153 | self.process_measured_data_out.close()
154 | self.process_random_chunk_in.close()
155 | self.process_random_chunk_out.close()
156 |
157 |
158 | class ThreadedDAQ(object):
159 | """Code that runs in a separate thread, getting data from the hardware.
160 | """
161 | def __init__(self, live_flag, run_flag, properties, task_info, trigger, measured_data=None, task=None, exc_channel=None,
162 | trigger_level=None, pre_trigger_samples=None, n_averages=None):
163 | """Constructor."""
164 | super().__init__()
165 |
166 | self.properties = properties
167 | self.live_flag = live_flag
168 | self.run_flag = run_flag
169 | self.measured_data = measured_data
170 | self.random_chunk = None
171 | self.task_info = task_info
172 | self.exc_channel = exc_channel
173 | self.trigger_level = trigger_level
174 | self.pre_trigger_samples = pre_trigger_samples
175 | self.n_averages = n_averages
176 |
177 | self.triggered = trigger
178 | # self.triggered = False
179 |
180 | self.wait()
181 |
182 | def wait(self):
183 | """Wait for start signal."""
184 | while self.live_flag.value:
185 | if self.run_flag.value:
186 | self.inject_properties(self.properties.recv())
187 | # self.measurement_continuous()
188 | if self.type == 'impulse':
189 | self.measurement_triggered()
190 | elif self.type == 'random' or self.type == 'oma':
191 | self.measurement_continuous()
192 |
193 | time.sleep(0.01)
194 |
195 | def inject_properties(self, properties):
196 | """Get fresh arguments to the function before starting the measurement."""
197 | self.type = properties['excitation_type']
198 | self.task = DAQTask.DAQTask(properties['task_name'])
199 | self.sampling_rate = self.task.sample_rate
200 | self.task_info.send(self.sampling_rate)
201 | self.channel_list = self.task.channel_list
202 | self.samples_per_channel = self.task.samples_per_ch
203 | self.number_of_channels = self.task.number_of_ch
204 | self.exc_channel = properties['exc_channel']
205 | self.trigger_level = properties['trigger_level']
206 | self.pre_trigger_samples = properties['pre_trigger_samples']
207 | if properties['samples_per_channel'] is 'auto':
208 | self.samples_per_channel = self.task.samples_per_ch
209 | else:
210 | self.samples_per_channel = properties['samples_per_channel']
211 | self.samples_left_to_acquire = self.samples_per_channel
212 |
213 | # Reinitialize pipe always -- it is closed when measurement is stopped.
214 | self.measured_data = properties['measured_data_pipe']
215 | self.random_chunk = properties['random_chunk_pipe']
216 |
217 | self.ring_buffer = RingBuffer.RingBuffer(self.number_of_channels, self.samples_per_channel)
218 |
219 | def _add_data_if_triggered(self, data):
220 | # If trigger level crossed ...
221 | _trigger = np.abs(data[self.exc_channel]) > self.trigger_level
222 | if np.any(_trigger) and not self.internal_trigger:
223 | trigger_index = np.where(_trigger)[0][0]
224 | start = trigger_index - self.pre_trigger_samples
225 | self.samples_left_to_acquire+=start
226 | self.ring_buffer.extend(data, self.samples_left_to_acquire)
227 | self.samples_left_to_acquire = self.samples_left_to_acquire - data[0].size
228 | self.internal_trigger = True
229 | elif self.internal_trigger:
230 | self.ring_buffer.extend(data, self.samples_left_to_acquire)
231 | self.samples_left_to_acquire = self.samples_left_to_acquire - data[0].size
232 | else:
233 | self.ring_buffer.extend(data)
234 |
235 | def measurement_continuous(self):
236 | """Continuous measurement."""
237 | samples_left_local = self.samples_left_to_acquire
238 | while True:
239 | # TODO: Optimize below.
240 | if not self.run_flag.value:
241 | # self.measured_data.close()
242 | self.task.clear_task(False)
243 | # self.task = None
244 | break
245 | else:
246 | _data = self.task.acquire_base()
247 | self.ring_buffer.extend(_data, self.samples_left_to_acquire)
248 | # self.ring_buffer.extend(_data, self.samples_left_to_acquire)
249 | samples_left_local -= _data[0].size
250 |
251 |
252 | # TODO: Why this try/excepty? It always throws an error? Problems with sync between processes probably.
253 | try:
254 | self.measured_data.send(self.ring_buffer.get())
255 | if samples_left_local <= 0:
256 | self.triggered.value = True
257 | samples_left_local = self.samples_left_to_acquire
258 | self.random_chunk.send(self.ring_buffer.get())
259 | self.ring_buffer.clear()
260 | except:
261 | print('DAQ Except')
262 | if not self.run_flag.value:
263 | pass
264 | else:
265 | raise Exception
266 |
267 |
268 |
269 | def measurement_triggered(self, trigger=100):
270 | """Continuous measurement."""
271 | # Run continuously.
272 | self.internal_trigger = False
273 | while True:
274 | # Stop from within.
275 |
276 | # Check if stop condition, then do some cleanup and break out of loop.
277 | if not self.run_flag.value:
278 | # self.measured_data.close()
279 | self.task.clear_task(False)
280 | self.task = None
281 | break
282 | # TODO: Check what is happening with the triggers.
283 | elif self.samples_left_to_acquire < 0:
284 | if self.internal_trigger:
285 | self.triggered.value = True
286 | # self.internal_trigger = False
287 | else:
288 | # Otherwise, do the measurement and watch for trigger.
289 | data = self.task.acquire_base()
290 | self._add_data_if_triggered(data)
291 |
292 | try:
293 | self.measured_data.send(self.ring_buffer.get())
294 | except:
295 | if not self.run_flag.value:
296 | pass
297 | else:
298 | raise Exception
299 |
300 | # if self.samples_left_to_acquire == 0:
301 | # break
302 |
303 | def measurement_nsamples(self, n=1000):
304 | """Measure N number of samples."""
305 | # TODO: This doesn't work obviously.
306 | while True:
307 | if not self.run_flag.value:
308 | self.measured_data.close()
309 | self.task.clear_task(False)
310 | self.task = None
311 | break
312 | else:
313 | _data = self.task.acquire_base()
314 | self.ring_buffer.extend(_data)
315 | try:
316 | self.measured_data.send(self.ring_buffer.get())
317 | except:
318 | if not self.run_flag.value:
319 | pass
320 | else:
321 | raise Exception
322 |
323 | def test_ring_buffer():
324 | tt = ThreadedDAQ(live_flag=mp.Value('b', False), run_flag=mp.Value('b', True), properties=None, task_info='OpenModal Impact_', trigger=False)
325 | tt.samples_per_channel=10
326 | tt.number_of_channels=2
327 | tt.trigger_level=3.5
328 | tt.pre_trigger_samples=5
329 | tt.exc_channel=0
330 | tt.ring_buffer = RingBuffer.RingBuffer(tt.number_of_channels, tt.samples_per_channel)
331 | tt.samples_left_to_acquire=tt.samples_per_channel
332 | tt.internal_trigger=False
333 |
334 | _=np.arange(tt.samples_per_channel)
335 | data=np.array([_, _+0.1])
336 | tt._add_data_if_triggered(data)
337 | print(tt.ring_buffer.get())
338 | print(tt.samples_left_to_acquire)
339 | _+=len(_)
340 | data=np.array([_, _+0.1])
341 | tt._add_data_if_triggered(data)
342 | print(tt.ring_buffer.get())
343 | print(tt.samples_left_to_acquire)
344 |
345 |
346 |
347 | if __name__ == '__main__':
348 | test_ring_buffer()
349 |
350 |
--------------------------------------------------------------------------------
/OpenModal/fft_tools.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """ Tools to work wit fft data. Especially, frequency response functions
20 |
21 | History:
22 | -May 2015: code clean up and added separate testing
23 | -Jul 2014: FRF dimensions changed from frf[frequency,sample] to frf[sample, frequency], PEP cleaning, Janko Slavic
24 | -May 2014: cleaning and polishing of the code, Janko Slavic
25 | -Apr 2014: convert_frf, Blaz Starc
26 |
27 | @author: Janko Slavic, Blaz Starc
28 | @contact: janko.slavic@fs.uni-lj.si, blaz.starc@fs.uni-lj.si
29 |
30 | """
31 |
32 | import numpy as np
33 |
34 | _FRF_TYPES = {'a': 2, 'v': 1, 'd': 0} # accelerance, mobility, receptance
35 |
36 | def multiply(ffts, m):
37 | """Multiplies ffts*m. ffts can be a single fft or an array of ffts.
38 |
39 | :param ffts: array of fft data
40 | :param m: multiplication vector
41 | :return: multiplied array of fft data
42 | """
43 | out = np.zeros_like(ffts, dtype='complex')
44 | if len(np.shape(ffts)) == 2: # list of
45 | n = np.shape(ffts)[0] # number of frfs
46 | for j in range(n):
47 | out[j, :] = ffts[j, :] * m
48 | else:
49 | out[:] = ffts[:] * m
50 |
51 | return out
52 |
53 |
54 | def frequency_integration(ffts, omega, order=1):
55 | """Integrates ffts (one or many) in the frequency domain.
56 |
57 | :param ffts: [rad/s] : angular frequency vector
58 | :param omega: angular frequency
59 | :param order: order of integration
60 | :return: integrated array of fft data
61 | """
62 | return multiply(ffts, np.power(-1.j / omega, order))
63 |
64 |
65 | def frequency_derivation(ffts, omega, order=1):
66 | """Derivates ffts (one or many) in the frequency domain.
67 |
68 | :param ffts: array of fft data
69 | :param omega: [rad/s] angular frequency vector
70 | :param order: order of derivation
71 | :return: derivated array of fft data
72 | """
73 | return multiply(ffts, np.power(1.j * omega, order))
74 |
75 | def convert_frf(input_frfs, omega, input_frf_type, output_frf_type):
76 | """ Converting the frf accelerance/mobility/receptance
77 |
78 | The most general case is when `input_frfs` is of shape:
79 | `nr_inputs` * `nr_outputs` * `frf_len`
80 |
81 | :param input_frfs: frequency response function vector (of dim 1, 2 or 3)
82 | :param omega: [rad/s] angular frequency vector
83 | :param input_frf_type: 'd' receptance, 'v' mobility, 'a' accelerance (of dim 0, 1, 2)
84 | :param output_frf_type: 'd' receptance, 'v' mobility, 'a' accelerance (of dim 0, 1, 2)
85 | :return: frequency response function vector (of dim 1, 2 or 3)
86 | """
87 | # put all data to 3D frf type (nr_inputs * nr_outputs * frf_len)
88 | ini_shape = input_frfs.shape
89 | if 1 <=len(ini_shape) > 3 :
90 | raise Exception('Input frf should be if dimension 3 or smaller')
91 | elif len(ini_shape) == 2:
92 | input_frfs = np.expand_dims(input_frfs, axis=0)
93 |
94 | if type(input_frf_type) == str:
95 | input_frf_type = [ini_shape[0]*[input_frf_type]]
96 | else:
97 | input_frf_type = [input_frf_type]
98 |
99 | if type(output_frf_type) == str:
100 | output_frf_type = [ini_shape[0]*[output_frf_type]]
101 | else:
102 | output_frf_type = [output_frf_type]
103 | elif len(ini_shape) == 1:
104 | input_frfs = np.expand_dims(np.expand_dims(input_frfs, axis=0), axis=0)
105 | input_frf_type = [[input_frf_type]]
106 | output_frf_type = [[output_frf_type]]
107 |
108 | # reshaping of frfs
109 | (nr_inputs, nr_outputs, frf_len) = input_frfs.shape
110 | nr_frfs = nr_inputs * nr_outputs
111 | input_frfs = input_frfs.reshape(nr_frfs,-1)
112 |
113 | # reshaping of input and output frf types
114 | input_frf_type = np.asarray(input_frf_type)
115 | output_frf_type = np.asarray(output_frf_type)
116 | if len(input_frf_type.shape) != 2 or len(output_frf_type.shape) !=2:
117 | raise Exception('Input and output frf type should be of dimension 2.')
118 | input_frf_type = input_frf_type.flatten()
119 | output_frf_type = output_frf_type.flatten()
120 | if len(input_frf_type) != nr_frfs or len(output_frf_type) != nr_frfs:
121 | raise Exception('Input and output frf type length should correspond to the number frfs.')
122 |
123 | try:
124 | input_frf_type = [_FRF_TYPES[_] for _ in input_frf_type]
125 | output_frf_type = [_FRF_TYPES[_] for _ in output_frf_type]
126 | except:
127 | raise('Only frf types: d, v and a are supported.')
128 |
129 | # do the conversion
130 | output_frfs = np.zeros_like(input_frfs)
131 | for i in range(nr_frfs):
132 | order = output_frf_type[i] - input_frf_type[i]
133 | if (order > 2) or (order <-2):
134 | raise Exception('FRF conversion not supported.')
135 | output_frfs[i, :] = frequency_derivation(input_frfs[i, :], omega, order=order)
136 |
137 | #reshape back to original shape
138 | if len(ini_shape) == 3:
139 | return output_frfs.reshape((nr_inputs, nr_outputs, -1))
140 | elif len(ini_shape) == 2:
141 | return output_frfs.reshape((nr_outputs, -1))
142 | elif len(ini_shape) == 1:
143 | return output_frfs[0]
144 |
145 |
146 | def correct_time_delay(fft, w, time_delay):
147 | """
148 | Corrects the ``fft`` with regards to the ``time_delay``.
149 |
150 | :param fft: fft array
151 | :param w: angular frequency [rad/s]
152 | :param time_delay: time dalay in seconds
153 | :return: corrected fft array
154 | """
155 | return fft / (np.exp(1j * w * time_delay))
156 |
157 |
158 | def PSD(x, dt=1):
159 | """ Power spectral density
160 | :param x: time domain data
161 | :param dt: delta time
162 | :return: PSD, freq
163 | """
164 | X = np.fft.rfft(x)
165 | freq = np.fft.rfftfreq(len(x), d=dt)
166 | X = 2 * dt * np.abs(X.conj() * X / len(x))
167 |
168 | return X, freq
169 |
170 |
171 | def fft_adjusted_lower_limit(x, lim, nr):
172 | """
173 | Compute the fft of complex matrix x with adjusted summation limits:
174 |
175 | y(j) = sum[k=-n-1, -n-2, ... , -low_lim-1, low_lim, low_lim+1, ... n-2,
176 | n-1] x[k] * exp(-sqrt(-1)*j*k* 2*pi/n),
177 | j = -n-1, -n-2, ..., -low_limit-1, low_limit, low_limit+1, ... n-2, n-1
178 |
179 | :param x: Single-sided complex array to Fourier transform.
180 | :param lim: lower limit index of the array x.
181 | :param nr: number of points of interest
182 | :return: Fourier transformed two-sided array x with adjusted lower limit.
183 | Retruns [0, -1, -2, ..., -nr+1] and [0, 1, 2, ... , nr-1] values.
184 |
185 | """
186 | nf = 2 * (len(x) - lim) - 1
187 |
188 | n = np.arange(-nr + 1, nr)
189 |
190 | a = np.fft.fft(x, n=nf).real[n]
191 | b = np.fft.fft(x[:lim], n=nf).real[n]
192 | c = x[lim].conj() * np.exp(1j * 2 * np.pi * n * lim / nf)
193 |
194 | res = 2 * (a - b) - c
195 |
196 | return res[:nr][::-1], res[nr - 1:]
197 |
198 |
199 | def check_fft_for_speed(data_length, exception_if_prime_above=20):
200 | """To avoid slow FFT, raises an exception if largest prime above `exception_if_prime_above`.
201 |
202 | See: http://stackoverflow.com/questions/23287/largest-prime-factor-of-a-number/
203 |
204 | :param data_length: length of data for frf
205 | :param exception_if_prime_above: raise exception if the largest prime number is above
206 | :return: none
207 | """
208 |
209 | def prime_factors(n):
210 | """Returns all prime factors of a positive integer
211 |
212 | See: http://stackoverflow.com/questions/23287/largest-prime-factor-of-a-number/412942#412942
213 |
214 | :param n: lenght
215 | :return: array of prime numbers
216 | """
217 | factors = []
218 | d = 2
219 | while n > 1:
220 | while n % d == 0:
221 | factors.append(d)
222 | n /= d
223 | d += 1
224 | if d * d > n:
225 | if n > 1:
226 | factors.append(n)
227 | break
228 | return factors
229 |
230 | if np.max(prime_factors(data_length)) > exception_if_prime_above:
231 | raise Exception('Change the number of time/frequency points or the FFT will run slow.')
232 |
233 |
234 | def irfft_adjusted_lower_limit(x, low_lim, indices):
235 | """
236 | Compute the ifft of real matrix x with adjusted summation limits:
237 |
238 | y(j) = sum[k=-n-2, ... , -low_lim-1, low_lim, low_lim+1, ... n-2,
239 | n-1] x[k] * exp(sqrt(-1)*j*k* 2*pi/n),
240 | j =-n-2, ..., -low_limit-1, low_limit, low_limit+1, ... n-2, n-1
241 |
242 | :param x: Single-sided real array to Fourier transform.
243 | :param low_lim: lower limit index of the array x.
244 | :param indices: list of indices of interest
245 | :return: Fourier transformed two-sided array x with adjusted lower limit.
246 | Retruns values.
247 | """
248 |
249 | nf = 2 * (x.shape[1] - 1)
250 | a = (np.fft.irfft(x, n=nf)[:, indices]) * nf
251 | b = (np.fft.irfft(x[:, :low_lim], n=nf)[:, indices]) * nf
252 | return a - b
253 |
254 |
255 | if __name__ == '__main__':
256 | plot_figure = False
257 | # check_fft_for_speed(4) #fast
258 | # check_fft_for_speed(59612) #slow
259 |
--------------------------------------------------------------------------------
/OpenModal/frf.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """Module for FRF signal processing.
20 |
21 | Classes:
22 | class FRF: Handles 2 channel frequency response function.
23 |
24 | Info:
25 | 2014, jul, janko.slavic@fs.uni-lj.si: polishing and significant re-write
26 | 2014, jan, janko.slavic@fs.uni-lj.si:
27 | - added time delay correction in the frequency domain
28 | 2013, feb, janko.slavic@fs.uni-lj.si:
29 | - added coherence
30 | - added test case, added zero padding
31 | - added Force and Exponential window
32 | 2012, first version
33 |
34 | @author: Janko Slavic, Martin Cesnik, Matjaz Mrsnik
35 | @contact: janko.slavic@fs.uni-lj.si, martin.cesnik@fs.uni-lj.si, matjaz.mrsnik@ladisk.si
36 | """
37 |
38 | import numpy as np
39 | import OpenModal.fft_tools as fft_tools
40 |
41 | _EXC_TYPES = ['f', 'a', 'v', 'd', 'e'] # force for EMA and kinematics for OMA
42 | _RESP_TYPES = ['a', 'v', 'd', 'e'] # acceleration, velocity, displacement, strain
43 | _FRF_TYPES = ['H1', 'H2', 'vector', 'OMA']
44 | _WGH_TYPES = ['None', 'Linear', 'Exponential']
45 | _WINDOWS = ['None', 'Hann', 'Hamming', 'Force', 'Exponential']
46 |
47 | _DIRECTIONS = ['scalar', '+x', '+y', '+z', '-x', '-y', '-z']
48 | _DIRECTIONS_NR = [0, 1, 2, 3, -1, -2 - 3]
49 |
50 |
51 | def direction_dict():
52 | dir_dict = {a: b for a, b in zip(_DIRECTIONS, _DIRECTIONS_NR)}
53 | return dir_dict
54 |
55 |
56 | class FRF:
57 | """
58 | Perform Dual Channel Spectral Analysis
59 |
60 | :param sampling_freq: sampling frequency
61 | :param exc_type: excitation type, see _EXC_TYPES
62 | :param resp_type: response type, see _RESP_TYPES
63 | :param exc_window: excitation window, see _WINDOWS
64 | :param resp_window: response window, see _WINDOWS
65 | :param resp_delay: response time delay (in seconds) with regards to the excitation
66 | (use positive value for a delayed signal)
67 | :param weighting: weighting type for average calculation, see _WGH_TYPES
68 | :param n_averages: number of measurements, used for averaging
69 | :param fft_len: the length of the FFT
70 | If None then the freq length matches the time length
71 | :param nperseg: int, optional
72 | Length of each segment.
73 | If None, then the length corresponds to the data length
74 | :param noverlap: int, optional
75 | Number of points to overlap between segments.
76 | If None, ``noverlap = nperseg / 2``. Defaults to None.
77 | :param archive_time_data: archive the time data (this can consume a lot of memory)
78 | :param frf_type: default frf type returned at self.get_frf(), see _FRF_TYPES
79 | """
80 |
81 | def __init__(self, sampling_freq,
82 | exc=None,
83 | resp=None,
84 | exc_type='f', resp_type='a',
85 | exc_window='Force:0.01', resp_window='Exponential:0.01',
86 | resp_delay=0.,
87 | weighting='Exponential', n_averages=1,
88 | fft_len=None,
89 | nperseg=None,
90 | noverlap=None,
91 | archive_time_data=False,
92 | frf_type='H1'):
93 | """
94 | initiates the Data class:
95 |
96 | :param sampling_freq: sampling frequency
97 | :param exc: excitation array; if None, no data is added and init
98 | :param resp: response array
99 | :param exc_type: excitation type, see _EXC_TYPES
100 | :param resp_type: response type, see _RESP_TYPES
101 | :param exc_window: excitation window, see _WINDOWS
102 | :param resp_window: response window, see _WINDOWS
103 | :param resp_delay: response time delay (in seconds) with regards to the excitation.
104 | :param weighting: weighting type for average calculation, see _WGH_TYPES
105 | :param n_averages: number of measurements, used for averaging
106 | :param fft_len: the length of the FFT (zero-padding if longer than length of data)
107 | :param nperseg: optional segment length, by default one segment is analyzed
108 | :param noverlap: optional segment overlap, by default ``noverlap = nperseg / 2``
109 | :param archive_time_data: archive the time data (this can consume a lot of memory)
110 | :param frf_type: default frf type returned at self.get_frf(), see _FRF_TYPES
111 | :return:
112 | """
113 |
114 | # data info
115 | self.sampling_freq = sampling_freq
116 | self._data_available = False
117 | self.exc_type = exc_type
118 | self.resp_type = resp_type
119 | self.exc_window = exc_window
120 | self.resp_window = resp_window
121 | self.resp_delay = resp_delay
122 | self.frf_type = frf_type
123 |
124 | # ini
125 | self.exc = np.array([])
126 | self.resp = np.array([])
127 | self.exc_archive = []
128 | self.resp_archive = []
129 | self.samples = None
130 |
131 | # set averaging and weighting
132 | self.n_averages = n_averages
133 | self.weighting = weighting
134 | self.frf_norm = 1.
135 |
136 | # fft length
137 | self.fft_len = fft_len
138 | self.nperseg = nperseg
139 | self.noverlap = noverlap
140 |
141 | # save time data
142 | self.archive_time_data = archive_time_data
143 |
144 | # error checking
145 | if not (self.frf_type in _FRF_TYPES):
146 | raise Exception('wrong FRF type given %s (can be %s)'
147 | % (self.frf_type, _FRF_TYPES))
148 |
149 | if not (self.weighting in _WGH_TYPES):
150 | raise Exception('wrong weighting type given %s (can be %s)'
151 | % (self.weighting, _WGH_TYPES))
152 |
153 | if not (self.exc_type in _EXC_TYPES):
154 | raise Exception('wrong excitation type given %s (can be %s)'
155 | % (self.exc_type, _EXC_TYPES))
156 |
157 | if not (self.resp_type in _RESP_TYPES):
158 | raise Exception('wrong response type given %s (can be %s)'
159 | % (self.resp_type, _RESP_TYPES))
160 |
161 | if not (self.exc_window.split(':')[0] in _WINDOWS):
162 | raise Exception('wrong excitation window type given %s (can be %s)'
163 | % (self.exc_window, _WINDOWS))
164 |
165 | if not (self.resp_window.split(':')[0] in _WINDOWS):
166 | raise Exception('wrong response window type given %s (can be %s)'
167 | % (self.resp_window, _WINDOWS))
168 |
169 | self.curr_meas = np.int(0)
170 |
171 | if exc is not None and resp is not None:
172 | self.add_data(exc, resp)
173 |
174 | def add_data_for_overlapping(self, exc, resp):
175 | """Adds data and prepares accelerance FRF with the overlapping options
176 |
177 | :param exc: excitation array
178 | :param resp: response array
179 | :return:
180 | """
181 | self._add_to_archive(exc, resp)
182 | samples = len(exc)
183 | if self.nperseg is None:
184 | self.nperseg = samples
185 | elif self.nperseg >= samples:
186 | raise ValueError('nperseg must be less than samples.')
187 | if self.noverlap is None:
188 | self.noverlap = self.nperseg // 2
189 | elif self.noverlap >= self.nperseg:
190 | raise ValueError('noverlap must be less than nperseg.')
191 |
192 | self._ini_lengths_and_windows(self.nperseg)
193 | step = self.nperseg - self.noverlap
194 | indices = np.arange(0, samples - self.nperseg + 1, step)
195 | self.n_averages = len(indices)
196 | for k, ind in enumerate(indices):
197 | self.exc = exc[ind:ind + self.nperseg]
198 | self.resp = resp[ind:ind + self.nperseg]
199 |
200 | # add windows
201 | self._apply_window()
202 |
203 | # go into freq domain
204 | self._get_fft()
205 |
206 | # get averaged accelerance and coherence
207 | self._get_frf_av()
208 |
209 | # measurement number counter
210 | self.curr_meas += 1
211 | self._data_available = True
212 |
213 | def add_data(self, exc, resp):
214 | """Adds data and prepares accelerance FRF
215 |
216 | :param exc: excitation array
217 | :param resp: response array
218 | :return:
219 | """
220 | # add time data
221 | self._add_to_archive(exc, resp)
222 | self.exc = exc
223 | self.resp = resp
224 | self._ini_lengths_and_windows(len(self.exc))
225 |
226 | # add windows
227 | self._apply_window()
228 |
229 | # go into freq domain
230 | self._get_fft()
231 |
232 | # get averaged accelerance and coherence
233 | self._get_frf_av()
234 |
235 | # measurement number counter
236 | self.curr_meas += 1
237 | self._data_available = True
238 |
239 | def get_df(self):
240 | """Delta frequency in Hz
241 |
242 | :return: delta frequency in Hz
243 | """
244 | if not self._data_available:
245 | raise Exception('No data has been added yet!')
246 |
247 | return self.get_f_axis()[1]
248 |
249 | def get_f_axis(self):
250 | """
251 |
252 | :return: frequency vector in Hz
253 | """
254 | if not self._data_available:
255 | raise Exception('No data has been added yet!')
256 |
257 | return np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq)
258 |
259 | def get_t_axis(self):
260 | """Returns time axis.
261 |
262 | :return: return time axis
263 | """
264 |
265 | if not self._data_available:
266 | raise Exception('No data has been added yet!')
267 |
268 | return np.arange(self.samples) / self.sampling_freq
269 |
270 | def _apply_window(self):
271 | """Apply windows to exc and resp data
272 |
273 | :return:
274 | """
275 | self.exc *= self.exc_window_data
276 | self.resp *= self.resp_window_data
277 |
278 | def _get_window_sub(self, window='None'):
279 | """Returns the window time series and amplitude normalization term
280 |
281 | :param window: window string
282 | :return: w, amplitude_norm
283 | """
284 | window = window.split(':')
285 |
286 | if window[0] in ['Hamming', 'Hann']:
287 | w = np.hanning(self.samples)
288 | elif window[0] == 'Force':
289 | w = np.zeros(self.samples)
290 | force_window = float(window[1])
291 | to1 = np.long(force_window * self.samples)
292 | w[:to1] = 1.
293 | elif window[0] == 'Exponential':
294 | w = np.arange(self.samples)
295 | exponential_window = float(window[1])
296 | w = np.exp(np.log(exponential_window) * w / (self.samples - 1))
297 | else: # window = 'None'
298 | w = np.ones(self.samples)
299 |
300 |
301 | if window[0] == 'Force':
302 | amplitude_norm = 2 / len(w)
303 | else:
304 | amplitude_norm = 2 / np.sum(w)
305 |
306 | return w, amplitude_norm
307 |
308 | def _get_fft(self):
309 | """Calculates the fft ndarray of the most recent measurement data
310 |
311 | :return:
312 | """
313 | # define FRF - related variables (only for the first measurement)
314 |
315 | if self.curr_meas == 0:
316 | if self.fft_len is None:
317 | self.fft_len = self.samples
318 | self.w_axis = 2 * np.pi * np.fft.rfftfreq(self.fft_len, 1. / self.sampling_freq)
319 |
320 | self.Exc = np.fft.rfft(self.exc, self.fft_len)
321 | self.Resp = np.fft.rfft(self.resp, self.fft_len)
322 |
323 | if self.resp_type != 'e': # if not strain
324 | # convert response to 'a' type
325 | self.Resp = fft_tools.convert_frf(self.Resp, self.w_axis, input_frf_type=self.resp_type,
326 | output_frf_type='a')
327 |
328 | # correct delay
329 | if self.resp_delay != 0.:
330 | self.Exc = fft_tools.correct_time_delay(self.Exc, self.w_axis, self.resp_delay)
331 |
332 | def get_ods_frf(self):
333 | """Operational deflection shape averaged estimator
334 |
335 | Numerical implementation of Equation (6) in [1].
336 |
337 | Literature:
338 | [1] Schwarz, Brian, and Mark Richardson. Measurements required for displaying
339 | operating deflection shapes. Presented at IMAC XXII January 26 (2004): 29.
340 |
341 | :return: ODS FRF estimator
342 | """
343 | # 2 / self.samples added for proper amplitude
344 | # TODO check for proper norming if window changed
345 | return 2 / self.samples * (np.sqrt(self.S_XX) * self.S_XF / np.abs(self.S_XF))
346 |
347 | def get_resp_spectrum(self, amplitude_spectrum=True, last=True):
348 | """get response amplitude/power spectrum
349 |
350 | :param amplitude_spectrum: get amplitude spectrum else power
351 | :param last: return the last only (else the averaged value is returned)
352 | :return: response spectrum
353 | """
354 | k = self.resp_window_amp_norm
355 |
356 | if last:
357 | amp = np.abs(self.Resp)
358 | else:
359 | amp = np.sqrt(np.abs(self.S_XX))
360 |
361 | if amplitude_spectrum:
362 | return k * amp
363 | else:
364 | return k * amp ** 2
365 |
366 | def get_exc_spectrum(self, amplitude_spectrum=True, last=True):
367 | """get excitation amplitude/power spectrum
368 |
369 | :param amplitude_spectrum: get amplitude spectrum else power
370 | :param last: return the last only (else the averaged value is returned)
371 | :return: excitation spectrum
372 | """
373 | k = self.exc_window_amp_norm
374 |
375 | if last:
376 | amp = np.abs(self.Exc)
377 | else:
378 | amp = np.sqrt(np.abs(self.S_FF))
379 |
380 | if amplitude_spectrum:
381 | return k * amp
382 | else:
383 | return k * amp**2
384 |
385 | def get_H1(self):
386 | """H1 FRF averaged estimator
387 |
388 | :return: H1 FRF estimator
389 | """
390 | return self.frf_norm * self.S_FX / self.S_FF
391 |
392 | def get_H2(self):
393 | """H2 FRF averaged estimator
394 |
395 | :return: H2 FRF estimator
396 | """
397 | return self.frf_norm * self.S_XX / self.S_XF
398 |
399 | def get_Hv(self):
400 | """Hv FRF averaged estimator
401 |
402 | Literature:
403 | [1] Kihong and Hammond: Fundamentals of Signal Processing for
404 | Sound and Vibration Engineers, page 293.
405 |
406 | :return: Hv FRF estimator
407 | """
408 | k = 1 # ratio of the spectra of measurement noises
409 | return self.frf_norm * ((self.S_XX - k * self.S_FF + np.sqrt(
410 | (k * self.S_FF - self.S_XX) ** 2 + 4 * k * np.conj(self.S_FX) * self.S_FX)) / (2 * self.S_XF))
411 |
412 | def get_FRF_vector(self):
413 | """Vector FRF averaged estimator
414 |
415 | :return: FRF vector estimator
416 | """
417 | return self.frf_norm * self.S_X / self.S_F
418 |
419 | def get_FRF(self):
420 | """Returns the default FRF function set at init.
421 |
422 | :return: FRF estimator
423 | """
424 | if self.frf_type == 'H1':
425 | return self.get_H1()
426 | if self.frf_type == 'H2':
427 | return self.get_H2()
428 | if self.frf_type == 'vector':
429 | return self.get_FRF_vector()
430 | if self.frf_type == 'ODS':
431 | return self.get_ods_frf()
432 |
433 | def get_coherence(self):
434 | """Coherence
435 |
436 | :return: coherence
437 | """
438 | return np.abs(self.get_H1() / self.get_H2())
439 |
440 | def _get_frf_av(self):
441 | """Calculates the averaged FRF based on averaging and weighting type
442 |
443 |
444 | Literature:
445 | [1] Haylen, Lammens, Sas: ISMA 2011 Modal Analysis Theory and Testing page: A.2.27
446 | [2] http://zone.ni.com/reference/en-XX/help/371361E-01/lvanlsconcepts/average_improve_measure_freq/
447 |
448 | :return:
449 | """
450 | # obtain cross and auto spectra for current data
451 | S_FX = np.conj(self.Exc) * self.Resp
452 | S_FF = np.conj(self.Exc) * self.Exc
453 | S_XX = np.conj(self.Resp) * self.Resp
454 | S_XF = np.conj(self.Resp) * self.Exc
455 | # direct
456 | S_F = self.Exc
457 | S_X = self.Resp
458 |
459 | # obtain average spectra
460 | if self.curr_meas == 0:
461 | self.S_XX = S_XX
462 | self.S_FF = S_FF
463 | self.S_XF = S_XF
464 | self.S_FX = S_FX
465 | self.S_X = S_X
466 | self.S_F = S_F
467 | else:
468 | if self.weighting == 'Linear':
469 | N = np.float64(self.curr_meas) + 1
470 | else: # 'Exponential'
471 | N = np.float64(self.n_averages)
472 |
473 | self.S_XX = 1 / N * S_XX + (N - 1) / N * self.S_XX
474 | self.S_FF = 1 / N * S_FF + (N - 1) / N * self.S_FF
475 | self.S_XF = 1 / N * S_XF + (N - 1) / N * self.S_XF
476 | self.S_FX = 1 / N * S_FX + (N - 1) / N * self.S_FX
477 | self.S_X = 1 / N * S_X + (N - 1) / N * self.S_X
478 | self.S_F = 1 / N * S_F + (N - 1) / N * self.S_F
479 |
480 | def _ini_lengths_and_windows(self, length):
481 | """
482 | Sets the lengths used later in fft
483 |
484 | Parameters
485 | ----------
486 | length: length of data expected
487 | """
488 | if self.curr_meas != 0:
489 | return
490 | if self.samples is None:
491 | self.samples = length
492 | elif self.samples != len(self.exc):
493 | raise ValueError('data length changed.')
494 |
495 | self.exc_window_data, self.exc_window_amp_norm = self._get_window_sub(self.exc_window)
496 | self.resp_window_data, self.resp_window_amp_norm = self._get_window_sub(self.resp_window)
497 | self.frf_norm = self.exc_window_amp_norm**2 / self.resp_window_amp_norm**2
498 |
499 | def _add_to_archive(self, exc, resp):
500 | """Add time data to the archive for later data analysis
501 |
502 | :param exc: excitation data
503 | :param resp: response data
504 | :return:
505 | """
506 | if self.archive_time_data:
507 | self.resp_archive.append(resp)
508 | self.exc_archive.append(exc)
509 |
510 | def get_archive(self):
511 | """Returns the time archive. If not available, it returns None, None
512 |
513 | :return: (excitation, response) time archive
514 | """
515 | if self.archive_time_data:
516 | return self.exc_archive, self.resp_archive
517 | else:
518 | return None, None
519 |
520 |
521 | if __name__ == '__main__':
522 | pass
523 |
--------------------------------------------------------------------------------
/OpenModal/gui/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
--------------------------------------------------------------------------------
/OpenModal/gui/export_window.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 |
21 | import sys, subprocess, os
22 |
23 | try:
24 | import DAQTask as dq
25 | import daqprocess as dp
26 | except NotImplementedError as nie:
27 | dq = None
28 | dp = None
29 | from string import Template
30 |
31 | import qtawesome as qta
32 |
33 | # import DAQTask as dq
34 |
35 | from PyQt5 import QtCore, QtGui, QtWidgets
36 |
37 | import pyqtgraph as pg
38 |
39 | import numpy as np
40 |
41 |
42 | from OpenModal.gui.templates import COLOR_PALETTE
43 |
44 |
45 | MAX_WINDOW_LENGTH = 1e9
46 |
47 | class ExportSelector(QtWidgets.QWidget):
48 | """Measurement configuration window.
49 | """
50 | def __init__(self, desktop_widget, status_bar, modaldata_object, *args, **kwargs):
51 | super().__init__(*args, **kwargs)
52 |
53 | self.status_bar = status_bar
54 | self.modaldata_object = modaldata_object
55 | self.desktop_widget = desktop_widget
56 |
57 | self.data_types_list = ['nodes', 'lines', 'elements', 'measurements', 'analyses']
58 | self.data_types_names = ['Nodes', 'Lines', 'Elements', 'Measurements', 'Analysis results']
59 |
60 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
61 |
62 | p = self.palette()
63 | p.setColor(self.backgroundRole(), QtCore.Qt.white)
64 | self.setPalette(p)
65 |
66 | self.setAutoFillBackground(True)
67 | self.fields = dict()
68 |
69 | self.save = QtWidgets.QPushButton('Done')
70 | self.save.setObjectName('small')
71 | # self.save.setDisabled(True)
72 |
73 | self.dismiss = QtWidgets.QPushButton('Dismiss')
74 | self.dismiss.setObjectName('small')
75 |
76 | self.setGeometry(400, 50, 600, 800)
77 | self.setContentsMargins(25, 0, 25, 25)
78 |
79 | with open('gui/styles/style_template.css', 'r', encoding='utf-8') as fh:
80 | src = Template(fh.read())
81 | src = src.substitute(COLOR_PALETTE)
82 | self.setStyleSheet(src)
83 |
84 | hbox = QtWidgets.QHBoxLayout()
85 | # hbox.addWidget(self.left_menu)
86 |
87 | title_label = QtWidgets.QLabel('EXPORT DATA')
88 | font = title_label.font()
89 | font.setPointSize(8)
90 | font.setFamily('Verdana')
91 | title_label.setFont(font)
92 | title_label.setContentsMargins(5, 0, 0, 25)
93 | title_label.setObjectName('title_label')
94 |
95 | models_group = QtWidgets.QGroupBox('Models')
96 | models_group.setStyleSheet("QGroupBox {font-weight: bold;}")
97 | models_grid = QtWidgets.QGridLayout()
98 | models_grid.setContentsMargins(80, 20, 80, 20)
99 | models_grid.setColumnStretch(1, 0)
100 | models_grid.setColumnStretch(1, 2)
101 |
102 | self.model_db = self.modaldata_object.tables['info']
103 |
104 | models = ['{0} {1:.0f}'.format(model, model_id) for model, model_id in
105 | zip(self.model_db.model_name, self.model_db.model_id)]
106 |
107 | # models = ['Nosilec', 'Transformator', 'Jedro', 'Pralni stroj', 'Letalo']
108 |
109 | self.model_checkbox_widgets = [QtWidgets.QCheckBox() for model in models]
110 | model_label_widgets = [QtWidgets.QLabel(model) for model in models]
111 |
112 | for i, (checkbox, label) in enumerate(zip(self.model_checkbox_widgets,model_label_widgets)):
113 | models_grid.addWidget(checkbox, i//2, 0 + (i%2)*2)
114 | models_grid.addWidget(label, i//2, 1 + (i%2)*2, alignment=QtCore.Qt.AlignLeft)
115 | checkbox.setChecked(True)
116 |
117 | models_group.setLayout(models_grid)
118 |
119 | data_type_group = QtWidgets.QGroupBox('Data')
120 | data_type_group.setStyleSheet("QGroupBox {font-weight: bold;}")
121 | data_type_grid = QtWidgets.QGridLayout()
122 | data_type_grid.setContentsMargins(80, 20, 80, 20)
123 | data_type_grid.setColumnStretch(1, 0)
124 | data_type_grid.setColumnStretch(1, 2)
125 |
126 | data_types_keys = ['geometry', 'lines', 'elements_index', 'measurement_index', 'analysis_index']
127 | data_types_populated = [True if self.modaldata_object.tables[key].size != 0 else False
128 | for key in data_types_keys]
129 |
130 | self.data_type_checkbox_widgets = [QtWidgets.QCheckBox() for data_type in self.data_types_names]
131 | model_label_widgets = [QtWidgets.QLabel(data_type) for data_type in self.data_types_names]
132 |
133 | for i, (checkbox, label) in enumerate(zip(self.data_type_checkbox_widgets,model_label_widgets)):
134 | data_type_grid.addWidget(checkbox, i, 0)
135 | data_type_grid.addWidget(label, i, 1, alignment=QtCore.Qt.AlignLeft)
136 | if data_types_populated[i]:
137 | checkbox.setChecked(True)
138 |
139 | data_type_group.setLayout(data_type_grid)
140 |
141 | other_group = QtWidgets.QGroupBox('Separate Files for Data Types (UFF)')
142 | other_group.setStyleSheet("QGroupBox {font-weight: bold;}")
143 |
144 | one_file_radio = QtWidgets.QRadioButton()
145 | self.multiple_file_radio = QtWidgets.QRadioButton()
146 | one_file_radio_label = QtWidgets.QLabel('No')
147 | multiple_file_radio_label = QtWidgets.QLabel('Yes')
148 | one_file_radio.setChecked(True)
149 |
150 | h_files = QtWidgets.QGridLayout()
151 | h_files.setContentsMargins(80, 20, 80, 20)
152 | h_files.setColumnStretch(1, 0)
153 | h_files.setColumnStretch(1, 2)
154 | h_files.addWidget(self.multiple_file_radio, 0, 0)
155 | h_files.addWidget(multiple_file_radio_label, 0, 1)
156 | h_files.addWidget(one_file_radio, 0, 2)
157 | h_files.addWidget(one_file_radio_label, 0, 3)
158 |
159 | other_group.setLayout(h_files)
160 |
161 | button_export_xls = QtWidgets.QPushButton(qta.icon('fa.line-chart', color='white', scale_factor=1.2),
162 | ' Export CSV')
163 | button_export_xls.setObjectName('altpushbutton_')
164 | button_export_xls.clicked.connect(self.ExportCSV)
165 | button_export_xls_hbox = QtWidgets.QHBoxLayout()
166 | button_export_xls_hbox.addStretch()
167 | button_export_xls_hbox.addWidget(button_export_xls)
168 | button_export_xls_hbox.addStretch()
169 |
170 | button_export_unv = QtWidgets.QPushButton(qta.icon('fa.rocket', color='white', scale_factor=1.2),
171 | ' Export UFF')
172 | button_export_unv.setObjectName('altpushbutton_')
173 | button_export_unv.clicked.connect(self.ExportUff)
174 | button_export_unv_hbox = QtWidgets.QHBoxLayout()
175 | button_export_unv_hbox.addStretch()
176 | button_export_unv_hbox.addWidget(button_export_unv)
177 | button_export_unv_hbox.addStretch()
178 |
179 | title_layout = QtWidgets.QHBoxLayout()
180 | title_layout.addWidget(title_label)
181 | title_layout.addStretch()
182 |
183 | vbox = QtWidgets.QVBoxLayout()
184 | vbox.addLayout(title_layout)
185 | vbox.setContentsMargins(0, 1, 0, 0)
186 | vbox.addLayout(hbox)
187 | vbox.addWidget(models_group)
188 | vbox.addWidget(data_type_group)
189 | vbox.addWidget(other_group)
190 | vbox.addStretch()
191 | vbox.addLayout(button_export_xls_hbox)
192 | vbox.addLayout(button_export_unv_hbox)
193 | vbox.addStretch()
194 | # button_layout = QtGui.QHBoxLayout()
195 | # button_layout.addStretch()
196 | # button_layout.addWidget(self.save)
197 | # button_layout.addWidget(self.dismiss)
198 |
199 | # vbox.addStretch()
200 | # vbox.addLayout(button_layout)
201 | vbox.setContentsMargins(20, 20, 20, 20)
202 |
203 | vbox_outer = QtWidgets.QVBoxLayout()
204 | vbox_outer.setContentsMargins(0, 0, 0, 0)
205 | vbox_outer.addLayout(vbox)
206 | vbox_outer.addWidget(QtWidgets.QSizeGrip(self.parent()), 0, QtCore.Qt.AlignBottom |QtCore.Qt.AlignRight)
207 |
208 | self.setContentsMargins(0, 0, 0, 0)
209 | self.setLayout(vbox_outer)
210 |
211 |
212 | def paintEvent(self, event):
213 |
214 | self.painter = QtGui.QPainter()
215 | self.painter.begin(self)
216 |
217 | self.painter.setBrush(QtCore.Qt.white)
218 | self.painter.setPen(QtCore.Qt.lightGray)
219 |
220 | # .. Draw a rectangle around the main window.
221 | self.painter.drawRect(0, 0, self.width()-1, self.height()-1)
222 |
223 | self.painter.fillRect(QtCore.QRect(1, 1, self.width()-2, 40), QtGui.QColor(245, 245, 245))
224 |
225 | pen = QtGui.QPen()
226 | pen.setWidth(2)
227 | pen.setBrush(QtCore.Qt.gray)
228 | pen.setCapStyle(QtCore.Qt.RoundCap)
229 | pen.setJoinStyle(QtCore.Qt.RoundJoin)
230 | self.painter.setPen(pen)
231 | # close cross
232 | self.painter.drawLine(self.width() - 30, 30, self.width() - 10, 10)
233 | self.painter.drawLine(self.width() - 30, 10, self.width() - 10, 30)
234 |
235 | self.painter.end()
236 |
237 | def mouseMoveEvent(self, event):
238 | if event.buttons() and QtCore.Qt.LeftButton:
239 | self.move(event.globalPos() - self.mouse_drag_position)
240 | event.accept()
241 |
242 | def mousePressEvent(self, event):
243 |
244 | add = 0
245 |
246 | if event.button() == QtCore.Qt.LeftButton:
247 | if (event.pos().x() < (self.width() - 10 - add)) and (event.pos().x() > (self.width()-30-add))\
248 | and (event.pos().y() < (30+add)) and (event.pos().y() > (10+add)):
249 | self.close()
250 |
251 | self.mouse_drag_position = event.globalPos() - self.frameGeometry().topLeft()
252 |
253 | def ExportUff(self):
254 | """ File dialog for exporting uff files. """
255 | # if variant == 'PySide':
256 | # file_name, filtr = QtGui.QFileDialog.getSaveFileName(self, self.tr("Choose Folder"), "/.",
257 | # QtGui.QFileDialog.Directory)
258 | # elif variant == 'PyQt4':
259 |
260 | file_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Directory')
261 |
262 | # file_name = QtGui.QFileDialog.getSaveFileName(self, self.tr("Chose Folder"), "/.",
263 | # QtGui.QFileDialog.Directory)
264 |
265 | self.exportfile = file_name
266 |
267 | model_ids = [model_id for model_id, check_box_field in zip(self.model_db.model_id, self.model_checkbox_widgets)
268 | if check_box_field.isChecked()]
269 |
270 | data_types = [data_type for data_type, check_box_field in
271 | zip(self.data_types_list, self.data_type_checkbox_widgets) if check_box_field.isChecked()]
272 |
273 | separate_files_flag = self.multiple_file_radio.isChecked()
274 |
275 | print(model_ids)
276 |
277 | self.status_bar.setBusy('root', 'exporting')
278 |
279 | class IOThread(QtCore.QThread):
280 |
281 | def __init__(self, modaldata, file_name, model_ids=[], data_types=[], separate_files_flag=False):
282 | super().__init__()
283 |
284 | self.modaldata_object = modaldata
285 | self.file_name = file_name
286 | self.model_ids = model_ids
287 | self.data_types = data_types
288 | self.separate_files_flag = separate_files_flag
289 |
290 | def run(self):
291 | self.modaldata_object.export_to_uff(self.file_name, self.model_ids, self.data_types, self.separate_files_flag)
292 |
293 | self.thread = IOThread(self.modaldata_object, file_name, model_ids, data_types, separate_files_flag)
294 | self.thread.finished.connect(lambda: self.status_bar.setNotBusy('root'))
295 | self.thread.start()
296 | self.hide()
297 |
298 | def ExportCSV(self):
299 | """ File dialog for exporting uff files. """
300 | # if variant == 'PySide':
301 | # file_name, filtr = QtGui.QFileDialog.getSaveFileName(self, self.tr("Select Directory"), "/.",
302 | # QtGui.QFileDialog.Directory)
303 | # elif variant == 'PyQt4':
304 |
305 | file_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Directory')
306 |
307 | # file_name = QtGui.QFileDialog.getSaveFileName(self, self.tr("Chose Folder"), "/.",
308 | # QtGui.QFileDialog.Directory)
309 |
310 | self.exportfile = file_name
311 |
312 | model_ids = [model_id for model_id, check_box_field in zip(self.model_db.model_id, self.model_checkbox_widgets)
313 | if check_box_field.isChecked()]
314 |
315 | data_types = [data_type for data_type, check_box_field in
316 | zip(self.data_types_list, self.data_type_checkbox_widgets) if check_box_field.isChecked()]
317 |
318 | print(model_ids)
319 |
320 | self.status_bar.setBusy('root', 'exporting')
321 |
322 |
323 | class IOThread(QtCore.QThread):
324 |
325 | def __init__(self, modaldata, file_name, model_ids=[], data_types=[]):
326 | super().__init__()
327 |
328 | self.modaldata_object = modaldata
329 | self.file_name = file_name
330 | self.model_ids = model_ids
331 | self.data_types = data_types
332 |
333 | def run(self):
334 | self.modaldata_object.export_to_csv(self.file_name, self.model_ids, self.data_types)
335 |
336 | self.thread = IOThread(self.modaldata_object, file_name, model_ids, data_types)
337 | self.thread.finished.connect(lambda: self.status_bar.setNotBusy('root'))
338 | self.thread.start()
339 | self.hide()
340 |
--------------------------------------------------------------------------------
/OpenModal/gui/icons/Icon_animation_widget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/Icon_animation_widget.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/Icon_fit_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/Icon_fit_view.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/add164.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/add164.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/analysis_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/analysis_4.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/check.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/check_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/check_empty.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/configure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/configure.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/cross89.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/cross89.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/downarrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/downarrow.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/downarrow_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/downarrow_small.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/gear31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/gear31.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/geometry_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/geometry_big.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/hammer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/hammer.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_anim_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_anim_pause.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_anim_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_anim_play.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_size_grab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_size_grab.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_size_grab_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_size_grab_2.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_size_grab_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_size_grab_3.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/icon_size_grab_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/icon_size_grab_4.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/limes_logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/limes_logo.ico
--------------------------------------------------------------------------------
/OpenModal/gui/icons/loader-ring.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/loader-ring.gif
--------------------------------------------------------------------------------
/OpenModal/gui/icons/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/loader.gif
--------------------------------------------------------------------------------
/OpenModal/gui/icons/measurement_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/measurement_3.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/model.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/pcdaq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/pcdaq.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/play.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/play1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/play1.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/play87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/play87.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/radio_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/radio_empty.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/radio_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/radio_hover.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/radio_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/radio_selected.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/sizegrip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/sizegrip.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/stop.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/stop40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/stop40.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/thumbsup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/thumbsup.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/thumbsup_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/thumbsup_empty.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/uparrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/uparrow.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/uparrow_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/uparrow_small.png
--------------------------------------------------------------------------------
/OpenModal/gui/icons/verification16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmodal/OpenModal/f2961bee7cf4797088cf6bed6397ec8b183d8b96/OpenModal/gui/icons/verification16.png
--------------------------------------------------------------------------------
/OpenModal/gui/templates.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 |
21 |
22 | LIST_FONT_FAMILY = 'Consolas'
23 | LIST_FONT_SIZE = 10
24 |
25 | MENUBAR_WIDTH = 110
26 |
27 | # _COLOR_PALETTE_ORANGE = dict(primary='#d35400', hover='#e67e22')
28 | _COLOR_PALETTE_ORANGE = dict(primary='rgb(211, 84, 00)', primaryhex='#d35400', hover='rgb(230, 126, 34)', primarylight='rgba(211, 84, 00, 10%)',
29 | hoverlight='rgba(230, 126, 34, 20%)',selected='rgb(46, 204, 113)')
30 | # _COLOR_PALETTE_BW = dict(primary='#333333', hover='#666666')
31 | _COLOR_PALETTE_BW = dict(primary='rgb(51, 51, 51)', hover='rgb(102, 102, 102)')
32 |
33 | COLOR_PALETTE = _COLOR_PALETTE_ORANGE
--------------------------------------------------------------------------------
/OpenModal/gui/tooltips.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | tooltips = dict()
20 |
21 | tooltips['impulse_excitation'] = 'Measure using impulse excitation, such as impact hammer.'
22 | tooltips['random_excitation'] = 'Measure using broadband random excitation, usually done with shaker equipement.'
23 | tooltips['OMA_excitation'] = 'Operational modal analysis type of measurement, taking advantage of operational vibration.'
24 | tooltips['signal_selection'] = 'Select a DAQmx task, prepared using NI MAX.'
25 | tooltips['nimax'] = 'Run National Instruments Measurement and Automation Explorer; create a measurement task.'
26 | tooltips['window_length'] = 'Length of a window, relevant for frequency analysis (FFT) and plot range.'
27 | tooltips['zero_padding'] = 'Add zeros to signal, for improved frequency resolution.'
28 | tooltips['excitation_window'] = 'Type of excitation window.'
29 | tooltips['excitation_window_percent'] = 'Takes the part of the original window, where the amplitude is above x% (force window only).'
30 | tooltips['response_window'] = 'Type/shape of response window.'
31 | tooltips['response_window_percent'] = 'Takes the part of the original window, where the amplitude is above x% (force window only).'
32 | tooltips['averaging_type'] = 'Averaging strategy to use.'
33 | tooltips['averaging_number'] = 'Number of windows to average over, to obtain the final result.'
34 | tooltips['save_time_history'] = '''Save time history alongside the calculated results (FRFs). Parameters pertaining to
35 | frequency-domain transformation can be changed later on.'''
36 | tooltips['trigger_level'] = 'Amplitude level, which is considered an impulse.'
37 | tooltips['pre_trigger_samples'] = 'The number of samples to be added, before the trigger occurence.'
38 | tooltips['test_run'] = 'Run acquisition to test the preferences.'
39 | tooltips['toggle_PSD'] = 'Toggle between time-history and power-spectral density plot.'
--------------------------------------------------------------------------------
/OpenModal/gui/widgets/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
--------------------------------------------------------------------------------
/OpenModal/gui/widgets/languages.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Miha'
20 |
21 | LANG_DICT={
22 |
23 | 'en_GB':{
24 |
25 | '2D_view_cnt_menu_allFRF_txt':"All FRFs",
26 | '2D_view_cnt_menu_allFRF_statustip':"Plot all available FRFs"
27 |
28 | },
29 |
30 | 'sl_SI':{
31 |
32 | '2D_view_cnt_menu_allFRF_txt':"Vse FPF",
33 | '2D_view_cnt_menu_allFRF_statustip':"Izriši vse FPF, ki so na voljo."
34 | }
35 | }
--------------------------------------------------------------------------------
/OpenModal/gui/widgets/prototype.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 |
21 | from PyQt5 import QtGui, QtWidgets
22 |
23 | import OpenModal.preferences as preferences_
24 |
25 | class SubWidget(QtWidgets.QWidget):
26 | """Widget stub."""
27 | def __init__(self, modaldata, status_bar, lang, preferences=dict(), desktop_widget=None,
28 | preferences_window=None, action_new=None, action_open=None, parent=None):
29 | super(SubWidget, self).__init__(parent)
30 |
31 | self.settings = preferences
32 | self.desktop_widget = desktop_widget
33 | self.preferences_window = preferences_window
34 | self.action_new = action_new
35 | self.action_open = action_open
36 |
37 | if len(self.settings) == 0:
38 | for key, value in preferences_.DEFAULTS.items():
39 | self.settings[key] = value
40 |
41 | self._lang = lang
42 | self.modaldata = modaldata
43 | self.status_bar = status_bar
44 |
45 | self.setContentsMargins(0, 0, 0, 0)
46 |
47 | def reload(self, *args, **kwargs):
48 | """The method is called when new data is loaded into
49 | OpenModal, for example when a saved project is opened."""
50 | raise NotImplementedError
51 |
52 | def refresh(self):
53 | """The method is called when the widget is opened. When,
54 | for example, someone switches from Geometry to Measurement,
55 | refresh() is called on MeasurementWidget object."""
56 | self.reload()
57 |
58 | def closeEvent(self, *args, **kwargs):
59 | pass
60 |
--------------------------------------------------------------------------------
/OpenModal/gui/widgets/welcome.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 |
21 | from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
22 | # from PyQt4 import QtGui, QtCore, QtWebKit
23 |
24 | import qtawesome as qta
25 |
26 | import OpenModal.gui.widgets.prototype as prototype
27 |
28 | import OpenModal.gui.templates as templ
29 |
30 |
31 | class WelcomeWidget(prototype.SubWidget):
32 | """Welcome widget stub."""
33 | def __init__(self, *args, **kwargs):
34 | super(WelcomeWidget, self).__init__(*args, **kwargs)
35 | layout = QtWidgets.QHBoxLayout()
36 |
37 | view = QtWebEngineWidgets.QWebEngineView()
38 | view.load(QtCore.QUrl("http://openmodal.com/draft/alpha_greeting.html"))
39 |
40 | self.label = QtWidgets.QLabel('Welcome')
41 | self.label.setObjectName('big')
42 | # font = self.label.font()
43 | # font.setPointSize(25)
44 | # font.setFamily('Verdana')
45 | # self.label.setFont(font)
46 | self.label.setContentsMargins(15, 50, 50, 50)
47 |
48 | global_layout = QtWidgets.QVBoxLayout()
49 | # global_layout.addWidget(self.label)
50 | # global_layout.addStretch(1)
51 |
52 | choices_layout = QtWidgets.QVBoxLayout()
53 |
54 | self.button_start = QtWidgets.QPushButton(qta.icon('fa.rocket', color='white', scale_factor=1.2), 'New')
55 | self.button_start.setObjectName('altpushbutton')
56 | self.button_start.clicked.connect(self.action_new)
57 |
58 | # self.button_open_project = QtGui.QPushButton(qta.icon('fa.folder-open', color='white', active='fa.folder-open', color_active='white', scale_factor=1.2), 'Open')
59 | self.button_open_project = QtWidgets.QPushButton(qta.icon('fa.folder-open', color='white', scale_factor=1.2), 'Open')
60 | self.button_open_project.setObjectName('altpushbutton')
61 | self.button_open_project.clicked.connect(self.action_open)
62 |
63 | self.button_open_help = QtWidgets.QPushButton(qta.icon('fa.life-saver', color='#d35400'), 'Help')
64 | self.button_open_help.setObjectName('linkbutton')
65 | self.button_open_help.clicked.connect(lambda: view.load(QtCore.QUrl("http://openmodal.com/draft/first_steps.html")))
66 | # self.button_open_project.setMinimumHeight(40)
67 | # self.button_open_project.setMaximumHeight(40)
68 | choices_layout.addWidget(self.button_start)
69 | choices_layout.addWidget(self.button_open_project)
70 | # choices_layout.addStretch(1)
71 | choices_layout.addWidget(self.button_open_help)
72 | choices_layout.addStretch()
73 | choices_layout.setContentsMargins(20, 0, 20, 20)
74 |
75 | h_layout = QtWidgets.QHBoxLayout()
76 | # h_layout.addStretch()
77 | h_layout.addLayout(choices_layout)
78 | # h_layout.addStretch()
79 | view.setMinimumWidth(1000)
80 | # view.setMaximumWidth(1000)
81 | # h_layout.addStretch()
82 | h_layout.addWidget(view)
83 | # h_layout.addStretch()
84 | global_layout.setContentsMargins(50, 50, 50, 50)
85 |
86 | global_layout.addLayout(h_layout)
87 | # global_layout.addStretch()
88 |
89 |
90 | # layout.addWidget(view)
91 | self.setLayout(global_layout)
92 |
93 | def reload(self, *args, **kwargs):
94 | # Nothing so far
95 | pass
96 |
--------------------------------------------------------------------------------
/OpenModal/keys.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """Module for handling keys used in OpenModal.
20 |
21 | Typical use:
22 | keys['abscissa_axis_units_lab']['3']
23 |
24 | 'key': {
25 | '3': '', # acronym up to 3 characters
26 | '15': '', # short description up to 15 characters
27 | 'desc': '' # full description
28 | }
29 |
30 | """
31 |
32 | keys = {
33 | 'abscissa_axis_units_lab': {
34 | '3': 'x',
35 | '15': 'x axis units',
36 | 'desc': 'label for the units on the abscissa',
37 | },
38 |
39 | 'abscissa_force_unit_exp': {
40 | '3': 'exp',
41 | '15': 'unit exponent',
42 | 'desc': 'exponent for the force unit on the abscissa',
43 | },
44 |
45 | 'abscissa_inc': {
46 | '3': 'inc',
47 | '15': 'x axis incr.',
48 | 'desc': 'abscissa increment; 0 if spacing uneven',
49 | },
50 |
51 | 'abscissa_len_unit_exp': {
52 | '3': 'exp',
53 | '15': 'x axis unit exp',
54 | 'desc': 'exponent for the length unit on the abscissa',
55 | },
56 |
57 | 'abscissa_min': {
58 | '3': 'min',
59 | '15': 'x axis minimum',
60 | 'desc': 'abscissa minimum',
61 | },
62 |
63 | 'abscissa_spacing': {
64 | '3': 'spa',
65 | '15': 'x axis spacing',
66 | 'desc': 'abscissa spacing; 0=uneven, 1=even',
67 | },
68 |
69 | 'abscissa_spec_data_type': {
70 | '3': 'typ',
71 | '15': 'x axis type',
72 | 'desc': 'abscissa specific data type',
73 | },
74 |
75 | 'abscissa_temp_unit_exp': {
76 | '3': 'exp',
77 | '15': 'x axis unit exp',
78 | 'desc': 'exponent for the temperature unit on the abscissa',
79 | },
80 |
81 | 'analysis_id': {
82 | '3': 'aid',
83 | '15': 'analysis ID',
84 | 'desc': 'Analysis ID is used to distinguish between different analyses done in analysis tab.',
85 | },
86 |
87 | 'analysis_type': {
88 | '3': 'typ',
89 | '15': 'analysis type',
90 | 'desc': 'analysis type number; currently only normal mode (2), complex eigenvalue first order (displacement) (3), frequency response and (5) and complex eigenvalue second order (velocity) (7) are supported',
91 | },
92 |
93 | 'binary': {
94 | '3': 'bin',
95 | '15': 'Binary/ASCII',
96 | 'desc': '1 for Binary, 0 for ASCII format type',
97 | },
98 |
99 | 'byte_ordering': {
100 | '3': 'byt',
101 | '15': 'byte ordering',
102 | 'desc': 'byte ordering',
103 | },
104 |
105 | 'cmif': {
106 | '3': 'MIF',
107 | '15': 'CMIF',
108 | 'desc': 'The Complex Mode Indicator Function',
109 | },
110 |
111 | 'color': {
112 | '3': 'col',
113 | '15': 'color',
114 | 'desc': 'color number',
115 | },
116 |
117 | 'cyl_thz': {
118 | '3': 'thz',
119 | '15': 'x to y',
120 | 'desc': 'first Euler rotation', #for cylindrical coordinate system
121 | },
122 |
123 | 'damp_err': {
124 | '3': 'err',
125 | '15': 'damp. err.',
126 | 'desc': 'Damping error',
127 | },
128 |
129 | 'data': {
130 | '3': 'dat',
131 | '15': 'data',
132 | 'desc': 'data array',
133 | },
134 |
135 | 'data_ch': {
136 | '3': 'dat',
137 | '15': 'data char. nr.',
138 | 'desc': 'data-characteristic number',
139 | },
140 |
141 | 'data_type': {
142 | '3': 'typ',
143 | '15': 'data type',
144 | 'desc': 'data type number; 2 = real data, 5 = complex data',
145 | },
146 |
147 | 'date_db_created': {
148 | '3': 'crt',
149 | '15': 'date DB created',
150 | 'desc': 'date database created',
151 | },
152 |
153 | 'date_db_saved': {
154 | '3': 'sav',
155 | '15': 'date DB saved',
156 | 'desc': 'date database saved',
157 | },
158 |
159 | 'date_file_written': {
160 | '3': 'wrt',
161 | '15': 'file written',
162 | 'desc': 'date file was written',
163 | },
164 |
165 | 'db_app': {
166 | '3': 'nam',
167 | '15': 'DB app name',
168 | 'desc': 'name of the application that created the database',
169 | },
170 |
171 | 'def_cs': {
172 | '3': 'cs',
173 | '15': 'def. cs numbers ',
174 | 'desc': 'n deformation cs numbers',
175 | }, # what is this? (Blaz)
176 |
177 | 'description': {
178 | '3': 'des',
179 | '15': 'description',
180 | 'desc': 'description of the model',
181 | },
182 |
183 | 'disp_cs': {
184 | '3': 'cs',
185 | '15': 'disp cs numbers',
186 | 'desc': 'n displacement cs numbers',
187 | }, # what is this? (Blaz)
188 |
189 | 'eig': {
190 | '3': 'eig',
191 | '15': 'eigen frequency',
192 | 'desc': 'eigen frequency (complex number); applicable to analysis types 3 and 7 only',
193 | },
194 | 'eig_real': {
195 | '3': 'ere',
196 | '15': 'eigen freq [Hz]',
197 | 'desc': 'real part of eigen frequency; applicable to analysis types 3 and 7 only',
198 | },
199 | 'eig_xi': {
200 | '3': 'exi',
201 | '15': 'damping factor [/]',
202 | 'desc': 'damping factor; applicable to analysis types 3 and 7 only',
203 | },
204 | 'element_descriptor': {
205 | '3': 'eds',
206 | '15': 'element type',
207 | 'desc': 'description of element type',
208 | },
209 |
210 | 'element_id': {
211 | '3': 'eid',
212 | '15': 'element id',
213 | 'desc': 'id number of an element',
214 | },
215 |
216 | 'file_type': {
217 | '3': 'typ',
218 | '15': 'file type',
219 | 'desc': 'file type string',
220 | },
221 |
222 | 'force': {
223 | '3': 'for',
224 | '15': 'force',
225 | 'desc': 'force factor',
226 | },
227 |
228 | 'fp_format': {
229 | '3': 'fp',
230 | '15': 'fp format',
231 | 'desc': 'floating-point format',
232 | },
233 |
234 | 'freq': {
235 | '3': 'fre',
236 | '15': 'frequency',
237 | 'desc': 'frequency (Hz); applicable to analysis types 2 and 5 only',
238 | },
239 |
240 | 'freq_err': {
241 | '3': 'err',
242 | '15': 'freq. err.',
243 | 'desc': 'frequency error',
244 | },
245 |
246 |
247 | 'freq_max': {
248 | '3': 'max',
249 | '15': 'max. freq.',
250 | 'desc': 'maximal frequency',
251 | },
252 |
253 |
254 | 'freq_min': {
255 | '3': 'min',
256 | '15': 'min. freq.',
257 | 'desc': 'Minimal frequency',
258 | },
259 |
260 |
261 |
262 | 'freq_step_n': {
263 | '3': 'stp',
264 | '15': 'freq. step nr.',
265 | 'desc': 'frequency step number; applicable to analysis type 5 only',
266 | },
267 |
268 | 'frf': {
269 | '3': 'FRF',
270 | '15': 'FRF',
271 | 'desc': 'Frequency Response Function',
272 | },
273 |
274 | 'func_type': {
275 | '3': 'fun',
276 | '15': 'function type',
277 | 'desc': 'function type; only 1, 2, 3, 4 and 6 are supported',
278 | },
279 |
280 | 'id': {
281 | '3': 'id',
282 | '15': 'id',
283 | 'desc': 'id string',
284 | },
285 |
286 | 'id1': {
287 | '3': 'id1',
288 | '15': 'id1',
289 | 'desc': 'id1 string',
290 | },
291 |
292 | 'id2': {
293 | '3': 'id2',
294 | '15': 'id2',
295 | 'desc': 'id2 string',
296 | },
297 |
298 | 'id3': {
299 | '3': 'id3',
300 | '15': 'id3',
301 | 'desc': 'id3 string',
302 | },
303 |
304 | 'id4': {
305 | '3': 'id4',
306 | '15': 'id4',
307 | 'desc': 'id4 string',
308 | },
309 |
310 | 'id5': {
311 | '3': 'id5',
312 | '15': 'id5',
313 | 'desc': 'id5 string',
314 | },
315 |
316 | 'length': {
317 | '3': 'len',
318 | '15': 'length',
319 | 'desc': 'length factor',
320 | },
321 |
322 | 'lines': {
323 | '3': 'lin',
324 | '15': 'line numbers',
325 | 'desc': 'list of n line numbers',
326 | },
327 |
328 | 'load_case': {
329 | '3': 'loa',
330 | '15': 'load case',
331 | 'desc': 'load case number',
332 | },
333 |
334 | 'load_case_id': {
335 | '3': 'loa',
336 | '15': 'load case id',
337 | 'desc': 'id number for the load case',
338 | },
339 |
340 | 'max_order': {
341 | '3': 'max',
342 | '15': 'max. order',
343 | 'desc': 'maximum model order',
344 | },
345 |
346 | 'modal_a': {
347 | '3': 'mod',
348 | '15': 'modal a',
349 | 'desc': 'modal-a (complex number); applicable to analysis types 3 and 7 only',
350 | },
351 |
352 | 'modal_b': {
353 | '3': 'mod',
354 | '15': 'modal b',
355 | 'desc': 'modal-b (complex number); applicable to analysis types 3 and 7 only',
356 | },
357 |
358 | 'modal_damp_his': {
359 | '3': 'dmp',
360 | '15': 'modal damp. his',
361 | 'desc': 'modal hysteretic damping ratio; applicable to analysis type 2 only',
362 | },
363 |
364 | 'modal_damp_vis': {
365 | '3': 'dmp',
366 | '15': 'modal damp vis',
367 | 'desc': 'modal viscous damping ratio; applicable to analysis type 2 only',
368 | },
369 |
370 | 'modal_m': {
371 | '3': 'mod',
372 | '15': 'modal mass',
373 | 'desc': 'modal mass; applicable to analysis type 2 only',
374 | },
375 |
376 | 'mode_n': {
377 | '3': 'mod',
378 | '15': 'mode number',
379 | 'desc': 'mode number; applicable to analysis types 2, 3 and 7 only',
380 | },
381 |
382 | 'model_id': {
383 | '3': 'mid',
384 | '15': 'model ID',
385 | 'desc': 'id number of the model',
386 | },
387 |
388 | 'model_name': {
389 | '3': 'mod',
390 | '15': 'model name',
391 | 'desc': 'the name of the model',
392 | },
393 |
394 | 'model_type': {
395 | '3': 'mod',
396 | '15': 'model type',
397 | 'desc': 'model type number',
398 | },
399 |
400 | 'n_ascii_lines': {
401 | '3': 'nr',
402 | '15': 'ascii lines nr',
403 | 'desc': 'number of ascii lines',
404 | },
405 |
406 | 'n_bytes': {
407 | '3': 'nr',
408 | '15': 'nr of bytes',
409 | 'desc': 'number of bytes',
410 | },
411 |
412 | 'n_data_per_node': {
413 | '3': 'nr',
414 | '15': 'nr of data',
415 | 'desc': 'number of data per node (DOFs)',
416 | },
417 |
418 | 'n_nodes': {
419 | '3': 'nr',
420 | '15': 'nr of nodes',
421 | 'desc': 'number of nodes',
422 | },
423 |
424 | 'nr_of_nodes': {
425 | '3': 'nrn',
426 | '15': 'node count',
427 | 'desc': 'number of nodes per element',
428 | },
429 |
430 | 'node_nums': {
431 | '3': 'nr',
432 | '15': 'node nums',
433 | 'desc': 'node numbers',
434 | },
435 |
436 | 'num_pts': {
437 | '3': 'pts',
438 | '15': 'nr of pts',
439 | 'desc': 'number of data pairs for uneven abscissa or number of data values for even abscissa',
440 | },
441 |
442 | 'ord_data_type': {
443 | '3': 'typ',
444 | '15': 'ord data type',
445 | 'desc': 'ordinate data type',
446 | },
447 |
448 | 'orddenom_axis_units_lab': {
449 | '3': 'y',
450 | '15': 'y axis units',
451 | 'desc': 'label for the units on the ordinate denominator',
452 | },
453 |
454 | 'orddenom_force_unit_exp': {
455 | '3': 'exp',
456 | '15': 'unit exponent',
457 | 'desc': 'exponent for the force unit on the ordinate denominator',
458 | },
459 |
460 | 'orddenom_len_unit_exp': {
461 | '3': 'exp',
462 | '15': 'y axis unit exp',
463 | 'desc': 'exponent for the length unit on the ordinate denominator',
464 | },
465 |
466 | 'orddenom_spec_data_type': {
467 | '3': 'typ',
468 | '15': 'y axis type',
469 | 'desc': 'ordinate denominator specific data type',
470 | },
471 |
472 | 'orddenom_temp_unit_exp': {
473 | '3': 'exp',
474 | '15': 'y axis unit exp',
475 | 'desc': 'exponent for the temperature unit on the ordinate denominator',
476 | },
477 |
478 | 'ordinate_axis_units_lab': {
479 | '3': 'y',
480 | '15': 'y axis units',
481 | 'desc': 'label for the units on the ordinate',
482 | },
483 |
484 | 'ordinate_force_unit_exp': {
485 | '3': 'exp',
486 | '15': 'unit exponent',
487 | 'desc': 'exponent for the force unit on the ordinate',
488 | },
489 |
490 | 'ordinate_len_unit_exp': {
491 | '3': 'exp',
492 | '15': 'unit exponent',
493 | 'desc': 'exponent for the length unit on the ordinate',
494 | },
495 |
496 | 'ordinate_spec_data_type': {
497 | '3': 'typ',
498 | '15': 'y axis type',
499 | 'desc': 'ordinate specific data type',
500 | },
501 |
502 | 'ordinate_temp_unit_exp': {
503 | '3': 'exp',
504 | '15': 'unit exponent',
505 | 'desc': 'exponent for the temperature unit on the ordinate',
506 | },
507 |
508 | 'phi': {
509 | '3': 'phi',
510 | '15': 'phi',
511 | 'desc': 'phi coordinate of cylindrical coordinate system',
512 | },
513 |
514 | 'program': {
515 | '3': 'pro',
516 | '15': 'program',
517 | 'desc': 'name of the program',
518 | },
519 |
520 | 'r': {
521 | '3': 'r',
522 | '15': 'r',
523 | 'desc': 'r coordinate of cylindrical coordinate system',
524 | },
525 |
526 | 'r1': {
527 | '3': 'r1',
528 | '15': 'r1',
529 | 'desc': 'response array for each DOF; when response is complex only r1 through r3 will be used',
530 | },
531 |
532 | 'ref_dir': {
533 | '3': 'ref',
534 | '15': 'ref dir',
535 | 'desc': 'reference direction number',
536 | },
537 |
538 | 'ref_ent_name': {
539 | '3': 'ref',
540 | '15': 'ref ent name',
541 | 'desc': 'entity name for the reference',
542 | },
543 |
544 | 'ref_node': {
545 | '3': 'ref',
546 | '15': 'ref node',
547 | 'desc': 'reference node number',
548 | },
549 |
550 | 'rsp_dir': {
551 | '3': 'rsp',
552 | '15': 'rsp dir',
553 | 'desc': 'response direction number',
554 | },
555 |
556 | 'rsp_ent_name': {
557 | '3': 'rsp',
558 | '15': 'rsp ent name',
559 | 'desc': 'entity name for the response',
560 | },
561 |
562 | 'rsp_node': {
563 | '3': 'rsp',
564 | '15': 'rsp node',
565 | 'desc': 'response node number',
566 | },
567 |
568 | 'spec_data_type': {
569 | '3': 'spe',
570 | '15': 'spec data type',
571 | 'desc': 'specific data type number',
572 | },
573 |
574 | 'sum': {
575 | '3': 'SUM',
576 | '15': 'sum of elements',
577 | 'desc': 'sum of elements',
578 | },
579 |
580 | 'temp': {
581 | '3': 'tem',
582 | '15': 'temp',
583 | 'desc': 'temperature factor',
584 | },
585 |
586 | 'temp_mode': {
587 | '3': 'tem',
588 | '15': 'temp_mode',
589 | 'desc': 'temperature mode number',
590 | },
591 |
592 | 'temp_offset': {
593 | '3': 'tem',
594 | '15': 'temp offset',
595 | 'desc': 'temperature-offset factor',
596 | },
597 |
598 | 'thx': {
599 | '3': 'thx',
600 | '15': 'y to z',
601 | 'desc': 'third Euler rotation',
602 | },
603 |
604 | 'thy': {
605 | '3': 'thy',
606 | '15': 'x to z',
607 | 'desc': 'second Euler rotation',
608 | },
609 |
610 | 'thz': {
611 | '3': 'thz',
612 | '15': 'x to y',
613 | 'desc': 'first Euler rotation',
614 | },
615 |
616 | 'time_db_created': {
617 | '3': 'tim',
618 | '15': 'time DB created',
619 | 'desc': 'time database was created',
620 | },
621 |
622 | 'time_db_saved': {
623 | '3': 'tim',
624 | '15': 'time DB saved',
625 | 'desc': 'time database was saved',
626 | },
627 |
628 | 'time_file_written': {
629 | '3': 'tim',
630 | '15': 'time file writt',
631 | 'desc': 'time file was written',
632 | },
633 |
634 | 'trace_num': {
635 | '3': 'tra',
636 | '15': 'trace nr',
637 | 'desc': 'number of the trace',
638 | },
639 |
640 | 'type': {
641 | '3': 'typ',
642 | '15': 'type',
643 | 'desc': 'type number = 55',
644 | },
645 |
646 | 'uffid': {
647 | '3': 'ufd',
648 | '15': 'uff id',
649 | 'desc': 'identification number of uff dataset',
650 | },
651 |
652 | 'units_code': {
653 | '3': 'uni',
654 | '15': 'units code',
655 | 'desc': 'units code number',
656 | },
657 |
658 | 'units_description': {
659 | '3': 'uni',
660 | '15': 'units descr.',
661 | 'desc': 'units description',
662 | },
663 |
664 | 'ver_num': {
665 | '3': 'ver',
666 | '15': 'version',
667 | 'desc': 'version number',
668 | },
669 |
670 | 'version_db1': {
671 | '3': 'ver',
672 | '15': 'version DB1 str',
673 | 'desc': 'version string 1 of the database',
674 | },
675 |
676 | 'version_db2': {
677 | '3': 'ver',
678 | '15': 'version DB2 str',
679 | 'desc': 'version string 2 of the database',
680 | },
681 |
682 | 'x': {
683 | '3': 'x',
684 | '15': 'x',
685 | 'desc': 'abscissa array',
686 | },
687 |
688 | 'y': {
689 | '3': 'y',
690 | '15': 'y',
691 | 'desc': 'y-coordinates of the n nodes',
692 | },
693 |
694 | 'z': {
695 | '3': 'z',
696 | '15': 'z',
697 | 'desc': 'z-coordinates of the n nodes',
698 | },
699 |
700 | 'z_axis_axis_units_lab': {
701 | '3': 'z',
702 | '15': 'z axis units',
703 | 'desc': 'label for the units on the z axis',
704 | },
705 |
706 | 'z_axis_force_unit_exp': {
707 | '3': 'exp',
708 | '15': 'unit exponent',
709 | 'desc': 'exponent for the force unit on the z axis',
710 | },
711 |
712 | 'z_axis_len_unit_exp': {
713 | '3': 'exp',
714 | '15': 'unit exponent',
715 | 'desc': 'exponent for the length unit on the z axis',
716 | },
717 |
718 | 'z_axis_spec_data_type': {
719 | '3': 'typ',
720 | '15': 'z axis type',
721 | 'desc': 'z-axis specific data type',
722 | },
723 |
724 | 'z_axis_temp_unit_exp': {
725 | '3': 'exp',
726 | '15': 'unit exponent',
727 | 'desc': 'exponent for the temperature unit on the z axis',
728 | },
729 |
730 | 'z_axis_value': {
731 | '3': 'val',
732 | '15': 'z axis value',
733 | 'desc': 'z axis value',
734 | },
735 | }
736 |
737 | if __name__ == '__main__':
738 | for k, v in keys.items():
739 | print(k, v)
740 |
--------------------------------------------------------------------------------
/OpenModal/meas_check.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | import numpy as np
20 | from OpenModal.fft_tools import PSD
21 |
22 | def overload_check(data, min_overload_samples=3):
23 | """Check data for overload
24 |
25 | :param data: one or two (time, samples) dimensional array
26 | :param min_overload_samples: number of samples that need to be equal to max
27 | for overload
28 | :return: overload status
29 | """
30 | if data.ndim > 2:
31 | raise Exception('Number of dimensions of data should be 2 or less')
32 |
33 | def _overload_check(x):
34 | s = np.sort(np.abs(x))[::-1]
35 | over = s == np.max(s)
36 | if np.sum(over) >= min_overload_samples:
37 | return True
38 | else:
39 | return False
40 |
41 | if data.ndim == 2:
42 | over = [_overload_check(d) for d in data.T]
43 | return over
44 | else:
45 | over = _overload_check(data)
46 | return over
47 |
48 |
49 | def double_hit_check(data, dt=1, limit=1e-3, plot_figure=False):
50 | """Check data for double-hit
51 |
52 | See: at the end of http://scholar.lib.vt.edu/ejournals/MODAL/ijaema_v7n2/trethewey/trethewey.pdf
53 |
54 | :param data: one or two (time, samples) dimensional array
55 | :param dt: time step
56 | :param limit: ratio of freq content od the double vs single hit
57 | smaller number means more sensitivity
58 | :param plot_figure: plots the double psd of the data
59 | :return: double-hit status
60 | """
61 | if data.ndim > 2:
62 | raise Exception('Number of dimensions of data should be 2 or less!')
63 |
64 | def _double_hit_check(x):
65 | # first PSD
66 | W, fr = PSD(x, dt=dt)
67 | # second PSD: look for oscillations in PSD
68 | W2, fr2 = PSD(W, dt=fr[1])
69 | upto = int(0.01 * len(x))
70 | max_impact = np.max(W2[:upto])
71 | max_after_impact = np.max(W2[upto:])
72 | if plot_figure:
73 | import matplotlib.pyplot as plt
74 | plt.subplot(121)
75 | l = int(0.002*len(x))
76 | plt.plot(1000*dt*np.arange(l), x[:l])
77 | plt.xlabel('t [ms]')
78 | plt.ylabel('F [N]')
79 | plt.subplot(122)
80 | plt.semilogy((W2/np.max(W2))[:5*upto])
81 | plt.axhline(limit, color='r')
82 | plt.axvline(upto, color='g')
83 | plt.xlabel('Double freq')
84 | plt.ylabel('')
85 | plt.show()
86 |
87 | if max_after_impact / max_impact > limit:
88 | return True
89 | else:
90 | return False
91 |
92 | if data.ndim == 2:
93 | double_hit = [_double_hit_check(d) for d in data.T]
94 | return double_hit
95 | else:
96 | double_hit = _double_hit_check(data)
97 | return double_hit
98 |
--------------------------------------------------------------------------------
/OpenModal/openmodal.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 | import sys, time, os
19 |
20 | import multiprocessing as mp
21 | #
22 | # if __name__ == '__main__':
23 | # executable = os.path.join(os.path.dirname(sys.executable), 'openmodal.exe')
24 | # mp.set_executable(executable)
25 | # mp.freeze_support()
26 |
27 | from PyQt5 import QtGui, QtWidgets, QtWebEngineWidgets
28 |
29 | class Logger(object):
30 | def __init__(self, filename):
31 | self.terminal = sys.stderr
32 | self.log = open(filename, 'w')
33 |
34 | def write(self, message):
35 | self.terminal.write(message)
36 | self.log.write(message)
37 |
38 | def flush(self):
39 | self.terminal.flush()
40 | self.log.close()
41 |
42 | if os.path.isdir('log'):
43 | pass
44 | else:
45 | os.mkdir('log')
46 |
47 | #sys.stderr = Logger('log/{0:.0f}_log.txt'.format(time.time()))
48 |
49 | sys.path.append('../')
50 |
51 | if __name__ == '__main__':
52 | app = QtWidgets.QApplication(sys.argv)
53 | #TODO: do we need the following?
54 | #app.addLibraryPath('c:/Anaconda3/Lib/site-packages/PyQt5/plugins/')
55 |
56 | #pixmap = QtGui.QPixmap('gui/widgets/splash.png')
57 | #splash = QtGui.QSplashScreen(pixmap)
58 | #splash.show()
59 |
60 | #splash.showMessage('Importing modules ...')
61 | app.processEvents()
62 | import gui.skeleton as sk
63 |
64 | main_window = sk.FramelesContainer(app.desktop())
65 | #splash.showMessage('Building environment ...')
66 | app.processEvents()
67 |
68 | main_window.show()
69 |
70 | #splash.finish(main_window)
71 |
72 | #sys.exit(app.exec_())
73 | app.exec()
74 |
--------------------------------------------------------------------------------
/OpenModal/preferences.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | __author__ = 'Matjaz'
20 |
21 | # These values are used when user switches to different
22 | # type of excitation. The fields are automatically populated
23 | # with recommended values.
24 | EXCITATION_DEFAULTS = dict()
25 | EXCITATION_DEFAULTS['impulse'] = dict()
26 | EXCITATION_DEFAULTS['random'] = dict()
27 | EXCITATION_DEFAULTS['oma'] = dict()
28 |
29 | # Impulse excitation recommended settings
30 | EXCITATION_DEFAULTS['impulse']['exc_window'] = 'Force:0.01'
31 | EXCITATION_DEFAULTS['impulse']['resp_window'] = 'Exponential:0.01'
32 | EXCITATION_DEFAULTS['impulse']['weighting'] = 'None'
33 | EXCITATION_DEFAULTS['impulse']['n_averages'] = 10
34 |
35 | # Random excitation recommended settings.
36 | EXCITATION_DEFAULTS['random']['exc_window'] = 'Hann:0.01'
37 | EXCITATION_DEFAULTS['random']['resp_window'] = 'Hann:0.01'
38 | EXCITATION_DEFAULTS['random']['weighting'] = 'Linear'
39 | EXCITATION_DEFAULTS['random']['n_averages'] = 10
40 |
41 | # OMA excitation recommended settings.
42 | EXCITATION_DEFAULTS['oma']['exc_window'] = 'Hann:0.01'
43 | EXCITATION_DEFAULTS['oma']['resp_window'] = 'Hann:0.01'
44 | EXCITATION_DEFAULTS['oma']['weighting'] = 'Linear'
45 | EXCITATION_DEFAULTS['oma']['n_averages'] = 10
46 |
47 | # These are application-wide defaults.
48 | DEFAULTS = dict()
49 | DEFAULTS['excitation_type'] = 'impulse'
50 | DEFAULTS['channel_types'] = ['f'] + ['a']*13
51 | # TODO: Implement auto! (MAX value taken)
52 | DEFAULTS['samples_per_channel'] = 10000
53 | DEFAULTS['nodes'] = [0,0]
54 | DEFAULTS['directions'] = ['+x']*13
55 | DEFAULTS['trigger_level'] = 5
56 | DEFAULTS['pre_trigger_samples'] = 30
57 | DEFAULTS['exc_channel'] = 0
58 | DEFAULTS['resp_channels'] = [1]*13
59 | DEFAULTS['channel_delay'] = [0.]*14
60 |
61 | # Impulse excitation is the default choice.
62 | DEFAULTS['exc_window'] = EXCITATION_DEFAULTS['impulse']['exc_window']
63 | DEFAULTS['resp_window'] = EXCITATION_DEFAULTS['impulse']['resp_window']
64 | DEFAULTS['weighting'] = EXCITATION_DEFAULTS['impulse']['weighting']
65 | DEFAULTS['n_averages'] = EXCITATION_DEFAULTS['impulse']['n_averages']
66 |
67 | DEFAULTS['fft_len'] = 'auto'
68 | DEFAULTS['pre_trigger_samples'] = 30
69 | DEFAULTS['zero_padding'] = 0
70 | DEFAULTS['save_time_history'] = False
71 | DEFAULTS['roving_type'] = 'Ref. node'
72 | DEFAULTS['selected_model_id'] = 1
--------------------------------------------------------------------------------
/OpenModal/utils.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | """
20 | Some functions that will probably be used in different places,
21 | such as euler/direction vector conversion.
22 | """
23 |
24 | import numpy as np
25 | import pandas as pd
26 |
27 |
28 | def zyx_euler_to_rotation_matrix(th):
29 | """Convert the ZYX order (the one LMS uses) Euler
30 | angles to rotation matrix. Angles are given
31 | in radians.
32 |
33 | Note:
34 | Actually Tait-Bryant angles.
35 | """
36 | # -- Calculate sine and cosine values first.
37 | sz, sy, sx = [np.sin(value) for value in th]
38 | cz, cy, cx = [np.cos(value) for value in th]
39 |
40 | # -- Create and populate the rotation matrix.
41 | rotation_matrix = np.zeros((3, 3), dtype=float)
42 |
43 | rotation_matrix[0, 0] = cy * cz
44 | rotation_matrix[0, 1] = cz * sx * sy - cx * sz
45 | rotation_matrix[0, 2] = cx * cz * sy + sx * sz
46 | rotation_matrix[1, 0] = cy * sz
47 | rotation_matrix[1, 1] = cx * cz + sx * sy * sz
48 | rotation_matrix[1, 2] = -cz * sx + cx * sy * sz
49 | rotation_matrix[2, 0] = -sy
50 | rotation_matrix[2, 1] = cy * sx
51 | rotation_matrix[2, 2] = cx * cy
52 |
53 | return rotation_matrix
54 |
55 |
56 | def get_unique_rows(array):
57 | """
58 | Return unique rows of a numpy array.
59 | :param array: NumPy array
60 | :return: Array of unique rows
61 | """
62 | b = np.ascontiguousarray(array).view(np.dtype((np.void, array.dtype.itemsize * array.shape[1])))
63 | return np.unique(b).view(array.dtype).reshape(-1, array.shape[1])
64 |
65 |
66 | def unique_row_indices(a):
67 | """
68 | Assign unique row indices of an Nx2 array a.
69 | :param a: (N, 2) array
70 | :return: an (N,) array containing unique row indices
71 | :return: number of unique rows
72 | """
73 | a = a[:, 0] + 1j*a[:, 1]
74 | a = a.astype(complex)
75 | unique, inv = np.unique(a, return_inverse=True)
76 | return inv, len(unique)
77 |
78 |
79 | def get_frf_from_mdd(measurement_values, measurement_index):
80 | """
81 | Creates a 3D FRF array from the mdd file.
82 |
83 | The dimensions of the new array are (number of inputs, number of outputs, length of data)
84 |
85 | :param measurement_values: measurement values table of the mdd file
86 | :param measurement_index: measurement index table of the mdd file
87 | :return: FRF array
88 | """
89 | # Get unique row indices from reference nodes and reference directions
90 | inputs, ni = unique_row_indices(measurement_index.loc[:, ['ref_node', 'ref_dir']].values)
91 |
92 | # Get unique row indices from response nodes and response directions
93 | outputs, no = unique_row_indices(measurement_index.loc[:, ['rsp_node', 'rsp_dir']].values)
94 |
95 | # FRF length
96 | if measurement_index.shape[0] > 0:
97 | frf_len = np.sum(measurement_values.loc[:, 'measurement_id'] == measurement_index.iloc[0].loc['measurement_id'])
98 | f = measurement_values.loc[:, 'frq'][
99 | measurement_values.loc[:, 'measurement_id'] == 0].values
100 | else:
101 | frf_len = 0
102 | f = None
103 |
104 | # Create the 3D frf array
105 | frf = np.empty((ni, no, frf_len), dtype=complex)
106 | for i, meas_id in enumerate(measurement_index.loc[:, 'measurement_id']):
107 | frf[inputs[i], outputs[i]] = measurement_values.loc[:, 'amp'][
108 | measurement_values.loc[:, 'measurement_id'] == meas_id]
109 |
110 | return frf, f
111 |
112 |
113 | def get_frf_type(num_denom_type):
114 | """
115 | Get frf type from reference and response types. The supported frf types are:
116 | - accelerance
117 | - mobility
118 | - receptance
119 |
120 | :param num_denom_type: a numpy array containing response and reference
121 | type in columns. The type is defined according to Universal File Format.
122 | :return : a pandas dataframe with frf types
123 | """
124 |
125 | frf_type = pd.DataFrame(np.nan*np.zeros(num_denom_type.shape[0]), columns=['frf_type'], dtype=str)
126 |
127 | if frf_type.shape[0] > 0:
128 | for i, row in enumerate(num_denom_type):
129 | num_type = row[0]
130 | denom_type = row[1]
131 |
132 | if num_type == 12 and denom_type == 13:
133 | frf_type.loc[i, 'frf_type'] = 'a'
134 | elif num_type == 11 and denom_type == 13:
135 | frf_type[i].loc[i, 'frf_type'] = 'v'
136 | elif num_type == 8 and denom_type == 13:
137 | frf_type[i].loc[i, 'frf_type'] = 'd'
138 |
139 | # raise Exception('FRF type not recognised. Currently supported FRF types are: '
140 | # 'accelerance, mobility and receptance.')
141 | else:
142 | pass
143 |
144 | return frf_type
145 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenModal
2 | OpenModal is an open source experimental modal analysis software written in Python.
3 |
4 | Note: OM is not actively developed; you might want to check out a similar project without a full UI: https://github.com/ladisk/pyEMA.
5 |
6 | OpenModal combines geometry builder, measurement, identification and animation module to aid in experimental analysis of the dynamic properties of structures. It is written in Python and published under the GPL license.
7 |
8 | ## Running the software
9 | In order to develop the software on the same Python version and the corresponding library versions,
10 | a virtual environment is created in the folder *virtual-environment*.
11 |
12 | To run the virtual environment on Windows PowerShell, execute the following commands:
13 |
14 | ```
15 | .\virtual-environment\Scripts\Activate.ps1
16 | ```
17 |
18 | This should set the environment to `Python 3.6` and to the `PyQt4` and the associated librraries.
19 |
20 | To run the software, execute the following:
21 |
22 | ```
23 | cd openModal/
24 | python openmodal.py
25 | ```
26 |
27 | ## Support the project
28 | If you are interested in contributing to the code base or just by supporting the project, send us an e-mail to info@openmodal.com.
29 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs==1.4.3
2 | numpy==1.13.1
3 | packaging==16.8
4 | pandas==0.20.3
5 | PyDAQmx==1.3.2
6 | PyOpenGL==3.1.0
7 | pyparsing==2.2.0
8 | PyQt5==5.9
9 | pyqtgraph==0.10.0
10 | python-dateutil==2.6.1
11 | pytz==2017.2
12 | pyuff==1.1
13 | QtAwesome==0.4.4
14 | QtPy==1.2.1
15 | sip==4.19.3
16 | six==1.10.0
17 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 |
2 | # Copyright (C) 2014-2017 Matjaž Mršnik, Miha Pirnat, Janko Slavič, Blaž Starc (in alphabetic order)
3 | #
4 | # This file is part of OpenModal.
5 | #
6 | # OpenModal is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, version 3 of the License.
9 | #
10 | # OpenModal is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with OpenModal. If not, see .
17 |
18 |
19 | '''
20 |
21 | Setup script for OpenModal.
22 |
23 | Lines under multiprocessing import have to be uncommented first. Then ...
24 |
25 |
26 | Run the following command to build application exe:
27 | python setup.py build
28 |
29 | Run the following command to build msi installer:
30 | python setup.py bdist_msi
31 |
32 | '''
33 | import os
34 | import sys
35 | from cx_Freeze import setup, Executable
36 |
37 | os.environ['TCL_LIBRARY'] = "C:\\Users\\Matjaz\\Anaconda3\\tcl\\tcl8.6"
38 | os.environ['TK_LIBRARY'] = "C:\\Users\\Matjaz\\Anaconda3\\tcl\\tk8.6"
39 |
40 | base = None
41 | # if sys.platform == 'win32':
42 | # base = 'Win32GUI'
43 |
44 | def include_OpenGL():
45 | path_base = "C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\OpenGL"
46 | skip_count = len(path_base)
47 | zip_includes = [(path_base, "OpenGL")]
48 | for root, sub_folders, files in os.walk(path_base):
49 | for file_in_root in files:
50 | zip_includes.append(
51 | ("{}".format(os.path.join(root, file_in_root)),
52 | "{}".format(os.path.join("OpenGL", root[skip_count+1:], file_in_root))
53 | )
54 | )
55 | return zip_includes
56 |
57 | zip_includes=include_OpenGL()
58 |
59 | shortcut_table = [
60 | ("DesktopShortcut", # Shortcut
61 | "DesktopFolder", # Directory_
62 | "Open Modal", # Name
63 | "TARGETDIR", # Component_
64 | "[TARGETDIR]openmodal.exe", # Target
65 | None, # Arguments
66 | None, # Description
67 | None, # Hotkey
68 | r'OpenModal/gui/icons/limes_logo.ico', # Icon
69 | None, # IconIndex
70 | None, # ShowCmd
71 | 'TARGETDIR' # WkDir
72 | ),
73 | #
74 | # ("StartupShortcut", # Shortcut
75 | # "StartupFolder", # Directory_
76 | # "program", # Name
77 | # "TARGETDIR", # Component_
78 | # "[TARGETDIR]main.exe", # Target
79 | # None, # Arguments
80 | # None, # Description
81 | # None, # Hotkey
82 | # None, # Icon
83 | # None, # IconIndex
84 | # None, # ShowCmd
85 | # 'TARGETDIR' # WkDir
86 | # ),
87 |
88 | ]
89 |
90 | bdist_msi_options = {
91 | 'upgrade_code': '{111111-TROLOLO-FIRST-VERSION-CODE}',
92 | 'add_to_path': False,
93 | 'initial_target_dir': r'[ProgramFiles64Folder]\Open Modal',
94 | 'data': dict(Shortcut=shortcut_table)
95 | }
96 |
97 | options = {
98 | 'build_exe': {
99 | 'packages': ['traceback','numpy','matplotlib','qtawesome','six','tkinter','pandas','bisect',
100 | 'multiprocessing'],
101 | 'includes': ['PyQt4'],
102 | 'path': sys.path + ['OpenModal'],
103 | 'zip_includes': zip_includes,
104 | 'include_files' : [('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\scipy\\special\\_ufuncs.cp35-win_amd64.pyd','_ufuncs.cp35-win_amd64.pyd'),
105 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\bisect.py','bisect.py'),
106 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\scipy\\special\\_ufuncs_cxx.cp35-win_amd64.pyd','_ufuncs_cxx.cp35-win_amd64.pyd'),
107 | 'C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\qtawesome',
108 | #'C:\\_MPirnat\\Python\\pycharm\\OpenModalAlpha_freeze_v3\\OpenModal\\gui',
109 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\numpy\\core\\mkl_intel_thread.dll','mkl_intel_thread.dll'),
110 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\numpy\\core\\mkl_core.dll','mkl_core.dll'),
111 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\numpy\\core\\mkl_avx.dll','mkl_avx.dll'),
112 | ('C:\\Users\\Matjaz\\Anaconda3\\Lib\\site-packages\\numpy\\core\\libiomp5md.dll','libiomp5md.dll'),
113 | (r'OpenModal/gui/styles', r'gui/styles'),
114 | (r'OpenModal/gui/icons', r'gui/icons')]
115 | },
116 | 'bdist_msi': bdist_msi_options
117 |
118 | }
119 |
120 |
121 |
122 |
123 | executables = [
124 | Executable('OpenModal\openmodal.py', base=base)
125 | # shortcutName="OpenModal", shortcutDir="DesktopFolder",
126 | # icon=r'OpenModal/gui/icons/limes_logo.ico')
127 | ]
128 |
129 | setup(name='OpenModal',
130 | version='0.1',
131 | description='OpenModal first freeze',
132 | options=options,
133 | executables=executables
134 | )
135 |
--------------------------------------------------------------------------------