├── .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 | --------------------------------------------------------------------------------