Coverage for src / sdynpy / modal / sdynpy_modal_test.py: 8%

818 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-11 16:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Class defining the typical data for a modal test used to automatically create 

4reports and plots 

5""" 

6""" 

7Copyright 2022 National Technology & Engineering Solutions of Sandia, 

8LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

9Government retains certain rights in this software. 

10 

11This program is free software: you can redistribute it and/or modify 

12it under the terms of the GNU General Public License as published by 

13the Free Software Foundation, either version 3 of the License, or 

14(at your option) any later version. 

15 

16This program is distributed in the hope that it will be useful, 

17but WITHOUT ANY WARRANTY; without even the implied warranty of 

18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19GNU General Public License for more details. 

20 

21You should have received a copy of the GNU General Public License 

22along with this program. If not, see <https://www.gnu.org/licenses/>. 

23""" 

24 

25import os 

26import tempfile 

27import numpy as np 

28import io 

29from ..core.sdynpy_shape import ShapeArray, mac, matrix_plot, rigid_body_check,load as shape_load 

30from ..core.sdynpy_data import (TransferFunctionArray, CoherenceArray, 

31 MultipleCoherenceArray, PowerSpectralDensityArray, 

32 join as data_join) 

33from ..core.sdynpy_geometry import (Geometry,GeometryPlotter,ShapePlotter) 

34from ..core.sdynpy_coordinate import CoordinateArray, coordinate_array as sd_coordinate_array 

35from .sdynpy_signal_processing_gui import SignalProcessingGUI 

36from .sdynpy_polypy import PolyPy_GUI 

37from .sdynpy_smac import SMAC_GUI 

38from ..fileio.sdynpy_rattlesnake import read_modal_data 

39from ..doc.sdynpy_latex import ( 

40 figure as latex_figure, table as latex_table, 

41 create_data_quality_summary, create_geometry_overview, 

42 create_mode_fitting_summary, create_mode_shape_figures, create_rigid_body_analysis) 

43from qtpy.QtCore import Qt 

44import matplotlib.pyplot as plt 

45from scipy.io import loadmat 

46from pathlib import Path 

47import netCDF4 as nc4 

48 

49def read_modal_fit_data(modal_fit_data): 

50 """ 

51 Reads Modal Fit Data from PolyPy_GUI which contains modes and FRF data 

52 

53 Parameters 

54 ---------- 

55 modal_fit_data : str or NpzFile 

56 Filename to or data loaded from a Modal Fit Data .npz file that is 

57 saved from SDynPy's mode fitters. 

58 

59 Returns 

60 ------- 

61 shapes : ShapeArray 

62 The modes fit to the structure 

63 experimental_frfs : TransferFunctionArray 

64 The FRFs to which the modes were fit 

65 resynthesized_frfs : TransferFunctionArray 

66 FRFs resynthesized from the fit modes 

67 residual_frfs : TransferFunctionArray 

68 FRF contribution from the residual terms in the modal fit. 

69 

70 """ 

71 if isinstance(modal_fit_data,str): 

72 modal_fit_data = np.load(modal_fit_data) 

73 shapes = modal_fit_data['shapes'].view(ShapeArray) 

74 experimental_frfs = modal_fit_data['frfs'].view(TransferFunctionArray) 

75 resynthesized_frfs = modal_fit_data['frfs_resynth'].view(TransferFunctionArray) 

76 residual_frfs = modal_fit_data['frfs_residual'].view(TransferFunctionArray) 

77 return shapes, experimental_frfs, resynthesized_frfs, residual_frfs 

78 

79class ModalTest: 

80 

81 def __init__(self, 

82 geometry = None, 

83 time_histories = None, 

84 autopower_spectra = None, 

85 frfs = None, 

86 coherence = None, 

87 fit_modes = None, 

88 resynthesized_frfs = None, 

89 response_unit = None, 

90 reference_unit = None, 

91 rigid_body_shapes = None, 

92 channel_table = None 

93 ): 

94 self.response_unit = response_unit 

95 self.reference_unit = reference_unit 

96 self.geometry = geometry 

97 self.time_histories = time_histories 

98 self.autopower_spectra = autopower_spectra 

99 self.frfs = frfs 

100 self.coherence = coherence 

101 self.fit_modes = fit_modes 

102 self.resynthesized_frfs = resynthesized_frfs 

103 self.rigid_body_shapes = rigid_body_shapes 

104 self.channel_table = channel_table 

105 # Handles for GUIs that exist 

106 self.spgui = None 

107 self.ppgui = None 

108 self.smacgui = None 

109 self.modeshape_plotter = None 

110 self.deflectionshape_plotter = None 

111 # Quantities of interest for computing spectral quantities 

112 self.reference_indices = None 

113 self.autopower_spectra_reference_indices = None 

114 if time_histories is not None: 

115 self.sample_rate = 1/time_histories.abscissa_spacing 

116 else: 

117 self.sample_rate = None 

118 self.num_samples_per_frame = None 

119 self.num_averages = None 

120 self.start_time = None 

121 self.end_time = None 

122 self.trigger = None 

123 self.trigger_channel_index = None 

124 self.trigger_slope = None 

125 self.trigger_level = None 

126 self.pretrigger = None 

127 self.overlap = None 

128 self.window = None 

129 self.frf_estimator = None 

130 # Quantities of interest from Curve Fitters 

131 self.fit_modes_information = None 

132 # Excitation Information 

133 self.excitation_information = None 

134 # Figures to save to documentation 

135 self.documentation_figures = {} 

136 

137 @classmethod 

138 def from_rattlesnake_modal_data(cls, input_file, geometry = None, 

139 fit_modes = None, resynthesized_frfs = None, 

140 rigid_body_shapes = None): 

141 if isinstance(input_file,str): 

142 input_file = nc4.Dataset(input_file) 

143 

144 environment = input_file.groups[input_file['environment_names'][0]] 

145 

146 time_histories, frfs, coherence, channel_table = read_modal_data(input_file) 

147 time_histories = data_join(time_histories) 

148 

149 rename_dict = {key:key.replace('_',' ').title() for key in channel_table.columns} 

150 channel_table = channel_table.rename(columns=rename_dict) 

151 

152 out = cls(geometry, time_histories, frfs = frfs, coherence = coherence, 

153 fit_modes = fit_modes, resynthesized_frfs = resynthesized_frfs, 

154 rigid_body_shapes = rigid_body_shapes, channel_table = channel_table) 

155 

156 # Get spectral processing parameters 

157 out.define_spectral_processing_parameters( 

158 reference_indices = np.array(environment['reference_channel_indices'][:]), 

159 num_samples_per_frame = environment.samples_per_frame, 

160 num_averages = environment.num_averages, 

161 start_time = None, 

162 end_time = None, 

163 trigger = environment.trigger_type, 

164 trigger_channel_index = environment.trigger_channel, 

165 trigger_slope = 'Positive' if environment.trigger_slope_positive else 'Negative', 

166 trigger_level = environment.trigger_level, 

167 pretrigger = environment.pretrigger, 

168 overlap = environment.overlap, 

169 window = environment.frf_window, 

170 frf_estimator = environment.frf_technique, 

171 sample_rate = input_file.sample_rate) 

172 

173 out.set_autopower_spectra(out.time_histories.cpsd( 

174 out.num_samples_per_frame, overlap = 0.0, window = 'boxcar' if out.window == 'rectangle' else out.window, 

175 averages_to_keep = out.num_averages, only_asds = True)) 

176 

177 reference_units = channel_table['Unit'][out.reference_indices].to_numpy() 

178 if np.all(reference_units == reference_units[0]): 

179 reference_units = reference_units[0] 

180 

181 response_units = channel_table['Unit'][environment['response_channel_indices'][...]].to_numpy() 

182 if np.all(response_units == response_units[0]): 

183 response_units = response_units[0] 

184 

185 out.set_units(response_units, reference_units) 

186 

187 if environment.signal_generator_type == 'burst': 

188 out.excitation_information = {'text': 

189 f'Excitation for this test used a burst random signal from {environment.signal_generator_min_frequency} ' 

190 f'to {environment.signal_generator_max_frequency:} Hz. The signal level was {environment.signal_generator_level:} ' 

191 f'V RMS and was on for {environment.signal_generator_on_fraction*100:}\\% of the ' 

192 'measurement frame.'} 

193 elif environment.signal_generator_type == 'random': 

194 out.excitation_information = {'text': 

195 f'Excitation for this test used a random signal from {environment.signal_generator_min_frequency} ' 

196 f'to {environment.signal_generator_max_frequency:} Hz. The signal level was {environment.signal_generator_level:} ' 

197 f'V RMS.'} 

198 elif environment.signal_generator_type == 'pseudorandom': 

199 out.excitation_information = {'text': 

200 f'Excitation for this test used a pseudorandom signal from {environment.signal_generator_min_frequency} ' 

201 f'to {environment.signal_generator_max_frequency:} Hz. The signal level was {environment.signal_generator_level:} ' 

202 f'V RMS.'} 

203 elif environment.signal_generator_type == 'chirp': 

204 out.excitation_information = {'text': 

205 f'Excitation for this test used a chirp signal from {environment.signal_generator_min_frequency} ' 

206 f'to {environment.signal_generator_max_frequency:} Hz. The signal level was {environment.signal_generator_level:} ' 

207 f'V peak amplitude.'} 

208 elif environment.signal_generator_type == 'sine': 

209 out.excitation_information = {'text': 

210 f'Excitation for this test used a sine signal at {environment.signal_generator_min_frequency}' 

211 f' Hz. The signal level was {environment.signal_generator_level:} V peak amplitude.'} 

212 elif environment.signal_generator_type == 'square': 

213 out.excitation_information = {'text': 

214 f'Excitation for this test used a square wave signal at {environment.signal_generator_min_frequency}' 

215 f' Hz. The signal level was {environment.signal_generator_level:} V peak amplitude.'} 

216 

217 return out 

218 

219 def set_rigid_body_shapes(self,rigid_body_shapes): 

220 self.rigid_body_shapes = rigid_body_shapes 

221 

222 def set_units(self,response_unit, reference_unit): 

223 self.response_unit = response_unit 

224 self.reference_unit = reference_unit 

225 

226 def set_geometry(self, geometry): 

227 self.geometry = geometry 

228 

229 def set_time_histories(self, time_histories): 

230 self.time_histories = time_histories 

231 self.sample_rate = 1/time_histories.abscissa 

232 

233 def set_autopower_spectra(self, autopower_spectra): 

234 self.autopower_spectra = autopower_spectra 

235 

236 def set_frfs(self, frfs): 

237 self.frfs = frfs 

238 

239 def set_coherence(self, coherence): 

240 self.coherence = coherence 

241 

242 def set_fit_modes(self, fit_modes): 

243 self.fit_modes = fit_modes 

244 

245 def set_resynthesized_frfs(self, resynthesized_frfs): 

246 self.resynthesized_frfs = resynthesized_frfs 

247 

248 def set_channel_table(self,channel_table): 

249 self.channel_table = channel_table 

250 

251 def compute_spectral_quantities_SignalProcessingGUI(self): 

252 if self.time_histories is None: 

253 raise ValueError('Time Histories must be defined in order to compute spectral quantities') 

254 self.spgui = SignalProcessingGUI(self.time_histories) 

255 if self.geometry is not None: 

256 self.spgui.geometry = self.geometry 

257 

258 @property 

259 def response_indices(self): 

260 response_indices = np.arange(self.time_histories.size)[ 

261 ~np.isin(np.arange(self.time_histories.size), self.reference_indices) 

262 ] 

263 return response_indices 

264 

265 def retrieve_spectral_quantities_SignalProcessingGUI(self): 

266 self.reference_indices = np.array([self.spgui.referencesSelector.item(i).data( 

267 Qt.UserRole) for i in range(self.spgui.referencesSelector.count())]) 

268 self.num_samples_per_frame = self.spgui.frameSizeSpinBox.value() 

269 self.num_averages = self.spgui.framesSpinBox.value() 

270 self.start_time = self.spgui.startTimeDoubleSpinBox.value() 

271 self.end_time = self.spgui.endTimeDoubleSpinBox.value() 

272 self.trigger = self.spgui.typeComboBox.currentIndex() == 1 

273 self.trigger_channel_index = self.spgui.channelComboBox.currentIndex() 

274 self.trigger_slope = self.spgui.slopeComboBox.currentIndex() == 0 

275 self.trigger_level = self.spgui.levelDoubleSpinBox.value() 

276 self.pretrigger = self.spgui.pretriggerDoubleSpinBox.value() 

277 self.overlap = self.spgui.overlapDoubleSpinBox.value()/100 

278 self.window = self.spgui.windowComboBox.currentText().lower() 

279 self.frf_estimator = self.spgui.frfComboBox.currentText().lower() 

280 self.spgui.frfCheckBox.setChecked(True) 

281 self.spgui.autospectraCheckBox.setChecked(True) 

282 self.spgui.coherenceCheckBox.setChecked(True) 

283 self.spgui.compute() 

284 self.autopower_spectra = self.spgui.autospectra_data 

285 self.autopower_spectra_reference_indices = np.arange(len(self.reference_indices)) 

286 self.frfs = self.spgui.frf_data 

287 self.coherence = self.spgui.coherence_data 

288 

289 def compute_spectral_quantities( 

290 self, reference_indices, start_time, end_time, num_samples_per_frame, 

291 overlap, window, frf_estimator): 

292 if self.time_histories is None: 

293 raise ValueError('Time Histories must be defined in order to compute spectral quantities') 

294 self.reference_indices = reference_indices 

295 self.autopower_spectra_reference_indices = reference_indices 

296 self.num_samples_per_frame = num_samples_per_frame 

297 self.start_time = start_time 

298 self.end_time = end_time 

299 self.trigger = False 

300 self.trigger_channel_index = None 

301 self.trigger_slope = None 

302 self.trigger_level = None 

303 self.pretrigger = None 

304 self.overlap = overlap 

305 self.window = window 

306 self.frf_estimator = frf_estimator 

307 

308 # Separate into references and responses 

309 time_data = self.time_histories.extract_elements_by_abscissa(start_time, end_time) 

310 references = time_data[self.reference_indices] 

311 

312 responses = time_data[self.response_indices] 

313 

314 self.num_averages = int(time_data.num_elements - (1-overlap)*num_samples_per_frame)//num_samples_per_frame + 1 

315 

316 # Compute FRFs, Coherence, and Autospectra 

317 self.frfs = TransferFunctionArray.from_time_data( 

318 references, responses, num_samples_per_frame, overlap, frf_estimator, 

319 window) 

320 

321 self.autopower_spectra = PowerSpectralDensityArray.from_time_data( 

322 time_data, num_samples_per_frame, overlap, window, only_asds = True) 

323 

324 if len(self.reference_indices) > 1: 

325 self.coherence = MultipleCoherenceArray.from_time_data( 

326 responses, num_samples_per_frame, overlap, window, 

327 references) 

328 else: 

329 self.coherence = CoherenceArray.from_time_data( 

330 responses, num_samples_per_frame, overlap, window, references) 

331 

332 def define_spectral_processing_parameters( 

333 self, reference_indices, num_samples_per_frame, num_averages, 

334 start_time, end_time, trigger, trigger_channel_index, 

335 trigger_slope, trigger_level, pretrigger, overlap, window, 

336 frf_estimator, sample_rate): 

337 self.reference_indices = reference_indices 

338 self.autopower_spectra_reference_indices = reference_indices 

339 self.num_samples_per_frame = num_samples_per_frame 

340 self.num_averages = num_averages 

341 self.start_time = start_time 

342 self.end_time = end_time 

343 self.trigger = trigger 

344 self.trigger_channel_index = trigger_channel_index 

345 self.trigger_slope = trigger_slope 

346 self.trigger_level = trigger_level 

347 self.pretrigger = pretrigger 

348 self.overlap = overlap 

349 self.window = window 

350 self.frf_estimator = frf_estimator 

351 self.sample_rate = sample_rate 

352 

353 def fit_modes_PolyPy(self): 

354 if self.frfs is None: 

355 raise ValueError('FRFs must be defined in order to fit modes') 

356 self.ppgui = PolyPy_GUI(self.frfs) 

357 

358 def retrieve_modes_PolyPy(self): 

359 self.ppgui.compute_shapes() 

360 self.fit_modes = self.ppgui.shapes 

361 self.resynthesized_frfs = self.resynthesized_frfs 

362 

363 self.fit_modes_information = {'text':[ 

364 'Modes were fit to the data using the PolyPy curve fitter implemented in SDynPy in {:} bands.'.format(len(self.ppgui.stability_diagrams))]} 

365 figure_index = 0 

366 # Now go through and get polynomial data from each frequency range 

367 for stability_diagram in self.ppgui.stability_diagrams: 

368 # First get data about the stabilization diagram 

369 min_frequency = stability_diagram.polypy.min_frequency 

370 max_frequency = stability_diagram.polypy.max_frequency 

371 min_order = np.min(stability_diagram.polypy.polynomial_orders) 

372 max_order = np.max(stability_diagram.polypy.polynomial_orders) 

373 num_selected_poles = len(stability_diagram.selected_poles) 

374 self.fit_modes_information['text'].append( 

375 ('The frequency band from {:0.2f} to {:0.2f} was analyzed with polynomials from order {:} to {:}. '+ 

376 '{:} poles were selected from this band. The stabilization diagram is shown in Figure {{figure{:}ref:}}.').format( 

377 min_frequency, max_frequency,min_order,max_order,num_selected_poles,figure_index)) 

378 # Go through and save out a figure for each stabilization diagram 

379 tempdir = tempfile.mkdtemp() 

380 filename = os.path.join(tempdir,'stability_diagram.png') 

381 # Turn off last highlighted closest mode 

382 if stability_diagram.previous_closest_marker_index is not None: 

383 if stability_diagram.previous_closest_marker_index in stability_diagram.selected_poles: 

384 order_index, pole_index = stability_diagram.pole_indices[stability_diagram.previous_closest_marker_index] 

385 pole = stability_diagram.polypy.pole_list[order_index][pole_index] 

386 if pole['part_stable']: 

387 brush = (0, 128, 0) 

388 elif pole['damp_stable'] or pole['freq_stable']: 

389 brush = 'b' 

390 else: 

391 brush = 'r' 

392 stability_diagram.pole_markers[stability_diagram.previous_closest_marker_index].setBrush(brush) 

393 else: 

394 stability_diagram.pole_markers[stability_diagram.previous_closest_marker_index].setBrush((0, 0, 0, 0)) 

395 stability_diagram.pole_plot.writeImage(filename) 

396 with open(filename,'rb') as f: 

397 image_bytes = f.read() 

398 self.fit_modes_information['figure'+str(figure_index)] = image_bytes 

399 self.fit_modes_information['figure'+str(figure_index)+'caption'] = ( 

400 'Stabilization Diagram from {:0.2f} to {:0.2f} Hz. '.format(min_frequency,max_frequency) + 

401 'Red Xs represent unstable poles. ' + 

402 'Blue Triangles represent that the frequency has stablized. ' + 

403 'Blue Squares represent that the frequency and damping have stablized. ' + 

404 'Green circles represent that the frequency, damping, and shape have stablized. ' + 

405 'Solid markers are poles that were selected in the final mode set.') 

406 figure_index += 1 

407 os.remove(filename) 

408 os.removedirs(tempdir) 

409 self.fit_modes_information['text'].append( 

410 ('Complex Modes' if self.ppgui.complex_modes_checkbox.isChecked() else 'Normal Modes') + ' were fit to the data. ' + 

411 ('Residuals' if self.ppgui.use_residuals_checkbox.isChecked() else 'No residuals') + ' were used when fitting mode shapes. ' + 

412 ('All frequency lines were used to fit mode shapes.' if self.ppgui.all_frequency_lines_checkbox.isChecked() else 

413 'Mode shapes were fit using {:} frequency lines around each resonance, and {:} frequency lines were used to fit residuals.'.format( 

414 self.ppgui.lines_at_resonance_spinbox.value(),self.ppgui.lines_at_residuals_spinbox.value()))) 

415 

416 def fit_modes_SMAC(self): 

417 raise NotImplementedError('SMAC has not been implemented yet') 

418 

419 def retrieve_modes_SMAC(self): 

420 raise NotImplementedError('SMAC has not been implemented yet') 

421 

422 def fit_modes_opoly(self): 

423 raise NotImplementedError('Opoly has not been implemented yet') 

424 

425 def retrieve_modes_opoly(self,fit_modes, 

426 opoly_progress = None, 

427 opoly_shape_info = None, 

428 opoly_mif_override = None, 

429 stabilization_subplots_kwargs = None, 

430 stabilization_plot_kwargs = None): 

431 stabilization_axis = None 

432 if stabilization_subplots_kwargs is None: 

433 stabilization_subplots_kwargs = {} 

434 if stabilization_plot_kwargs is None: 

435 stabilization_plot_kwargs = {} 

436 

437 if isinstance(fit_modes,str): 

438 if opoly_shape_info is None: 

439 opoly_shape_info = fit_modes+'.info.csv' 

440 if not os.path.exists(opoly_shape_info): 

441 opoly_shape_info = None 

442 fit_modes = shape_load(fit_modes) 

443 self.fit_modes = fit_modes 

444 

445 categories = ['Poly Sieve', 

446 'Poly Model', 

447 'Poly Range', 

448 'Stability', 

449 'Autonomous', 

450 'Shapes Sieve', 

451 'Shapes Model', 

452 'Shapes Range', 

453 'Pole List' 

454 ] 

455 

456 opoly_settings = {category:{} for category in categories} 

457 

458 if opoly_progress is not None: 

459 if isinstance(opoly_progress,str): 

460 opoly_progress = loadmat(opoly_progress) 

461 # Pull out all of the settings 

462 opoly_settings['OPoly Version'] = str(opoly_progress['OPOLY_PROGRESS_001']['APPINFO'][0,0]['Version'][0,0][0,0])+'.'+str(int( 

463 opoly_progress['OPOLY_PROGRESS_001']['APPINFO'][0,0]['Version'][0,0][0,1])) 

464 opoly_settings['OPoly Version'] = str(opoly_progress['OPOLY_PROGRESS_001']['APPINFO'][0,0]['Version'][0,0][0,0]) 

465 opoly_settings['Poly Sieve']['References'] = opoly_progress['OPOLY_PROGRESS_001']['IMATDATA'][0,0]['Sieve'][0,0]['Poles'][0,0]['References'][0,0].flatten() 

466 opoly_settings['Poly Sieve']['Responses'] = opoly_progress['OPOLY_PROGRESS_001']['IMATDATA'][0,0]['Sieve'][0,0]['Poles'][0,0]['Responses'][0,0].flatten() 

467 opoly_settings['Poly Model']['Method'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Method'][0,0][0]) 

468 opoly_settings['Poly Model']['Min Order'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['MinOrder'][0,0][0,0]) 

469 opoly_settings['Poly Model']['Step Order'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['StepOrder'][0,0][0,0]) 

470 opoly_settings['Poly Model']['Max Order'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['MaxOrder'][0,0][0,0]) 

471 opoly_settings['Poly Model']['Clear Ege Lines'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['ClearEdges'][0,0][0,0]) 

472 opoly_settings['Poly Model']['Solver Function'] = 'OPoly M-File' 

473 opoly_settings['Poly Model']['Frequency Basis'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['FreqSided'][0,0][0])+'-sided' 

474 opoly_settings['Poly Model']['Identity Order'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['IdentOrder'][0,0][0]) 

475 opoly_settings['Poly Model']['Residuals'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['Residuals'][0,0][0,0]) 

476 opoly_settings['Poly Model']['Fixed Numerator'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['FixedNumer'][0,0][0]) 

477 opoly_settings['Poly Model']['Weighting'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['Weighting'][0,0][0]) 

478 opoly_settings['Poly Model']['Real Participations'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['RealMPF'][0,0][0]) 

479 opoly_settings['Poly Model']['Keep All Poles'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['KeepAll'][0,0][0]) 

480 opoly_settings['Poly Model']['Overdetermination'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['PolyModel'][0,0]['Params'][0,0]['Overdeterm'][0,0][0,0]) 

481 opoly_settings['Poly Range']['Freq Range (Hz)'] = [str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['FreqRange'][0,0]['Lower'][0,0][0,0]),str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['FreqRange'][0,0]['Upper'][0,0][0,0])] 

482 opoly_settings['Poly Range']['Spectral Lines'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Setup'][0,0]['FreqRange'][0,0]['Lines'][0,0][0,0]) 

483 opoly_settings['Stability']['Frequency (%)'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Stability'][0,0]['Frequency'][0,0][0,0]*100) 

484 opoly_settings['Stability']['Damping (%)'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Stability'][0,0]['Damping'][0,0][0,0]*100) 

485 opoly_settings['Stability']['Vector (%)'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Stability'][0,0]['Vector'][0,0][0,0]*100) 

486 opoly_settings['Autonomous']['Minimum Pole Density'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Autonomous'][0,0]['PoleDensity'][0,0][0,0]) 

487 opoly_settings['Autonomous']['Pole Weighted Vector Order'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Autonomous'][0,0]['WeightedOrder'][0,0][0,0]) 

488 opoly_settings['Autonomous']['Pole Weighted Vector MAC'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Autonomous'][0,0]['WeightedMAC'][0,0][0,0]) 

489 opoly_settings['Autonomous']['Minimum Cluster Size'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Autonomous'][0,0]['ClusterSize'][0,0][0,0]) 

490 opoly_settings['Autonomous']['Cluster Inclusion Threshold'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Autonomous'][0,0]['ClusterInclusion'][0,0][0,0]) 

491 opoly_settings['Shapes Sieve']['References'] = opoly_progress['OPOLY_PROGRESS_001']['IMATDATA'][0,0]['Sieve'][0,0]['Shapes'][0,0]['References'][0,0].flatten() 

492 opoly_settings['Shapes Sieve']['Responses'] = opoly_progress['OPOLY_PROGRESS_001']['IMATDATA'][0,0]['Sieve'][0,0]['Shapes'][0,0]['Responses'][0,0].flatten() 

493 opoly_settings['Shapes Model']['Method'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['Method'][0,0][0]) 

494 opoly_settings['Shapes Model']['Type'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['Type'][0,0][0]) 

495 opoly_settings['Shapes Model']['FRF Parts'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['FrfParts'][0,0][0]) 

496 opoly_settings['Shapes Model']['Residuals'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['Residuals'][0,0][0]) 

497 opoly_settings['Shapes Model']['Solver Function'] = 'OPoly M-File' 

498 opoly_settings['Shapes Model']['Frequency Basis'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['Params'][0,0]['FreqSided'][0,0][0])+'-sided' 

499 opoly_settings['Shapes Model']['Refit Freq'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['ResiModel'][0,0]['RefitFreq'][0,0][0]) 

500 opoly_settings['Shapes Range']['Freq Range (Hz)'] = [str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['Refit'][0,0]['FreqRange'][0,0]['Lower'][0,0][0,0]),str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['Refit'][0,0]['FreqRange'][0,0]['Upper'][0,0][0,0])] 

501 opoly_settings['Shapes Range']['Spectral Lines'] = str(opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Shapes'][0,0]['Refit'][0,0]['FreqRange'][0,0]['Lines'][0,0][0,0]) 

502 keep = np.array([v[0,0] for v in opoly_progress['OPOLY_PROGRESS_001']['POLELIST'][0,0]['Checked'].flatten()]) 

503 opoly_settings['Pole List']['Model Order'] = np.array([v[0,0] for v in opoly_progress['OPOLY_PROGRESS_001']['POLELIST'][0,0]['ModelOrder'].flatten()])[keep.astype(bool)] 

504 opoly_settings['Pole List']['Pole Index'] = np.array([v[0,0] for v in opoly_progress['OPOLY_PROGRESS_001']['POLELIST'][0,0]['PoleIndex'].flatten()])[keep.astype(bool)] 

505 # Recreate the stabilization diagram in OPoly 

506 if opoly_mif_override is None: 

507 mif_type = opoly_progress['OPOLY_PROGRESS_001']['STATE'][0,0]['Poles'][0,0]['Plot'][0,0]['Function'][0,0][0,0][0] 

508 else: 

509 mif_type = opoly_mif_override 

510 if mif_type.lower() == 'cmif': 

511 mif_log = True 

512 mif = self.frfs.compute_cmif() 

513 elif mif_type.lower() == 'qmif': 

514 mif_log = True 

515 mif = self.frfs.compute_cmif(part='imag') 

516 elif mif_type.lower() == 'mmif': 

517 mif_log = False 

518 mif = self.frfs.compute_mmif() 

519 elif mif_type.lower() == 'nmif': 

520 mif_log = False 

521 mif = self.frfs.compute_nmif() 

522 elif mif_type.lower() == 'psmif': 

523 mif_log = True 

524 mif = self.frfs.compute_cmif()[0] 

525 else: 

526 raise ValueError('Unknown mode indicator function {:}'.format(mif_type)) 

527 poles = [v.flatten() for v in opoly_progress['OPOLY_PROGRESS_001'][0,0]['POLYDATA']['Poles'][0,:]] 

528 stabilities = [[str(v[0]) for v in row.flatten()] for row in opoly_progress['OPOLY_PROGRESS_001'][0,0]['POLYDATA']['Stability'][0,:]] 

529 orders = [str(row[0,0]) for row in opoly_progress['OPOLY_PROGRESS_001'][0,0]['POLYDATA']['ModelOrder'][0,:]] 

530 else: 

531 poles = None 

532 mif = self.frfs.compute_cmif() 

533 mif_log = True 

534 

535 if opoly_shape_info is not None: 

536 with open(opoly_shape_info,'r') as f: 

537 for line in f: 

538 if line[:5] == 'Notes': 

539 break 

540 parts = [v.strip() for v in line.split(',')] 

541 if parts[0] in categories: 

542 category,field,*data = parts 

543 opoly_settings[category][field] = data[0] if len(data) == 1 else data 

544 else: 

545 opoly_settings[parts[0]] = parts[1] if len(parts[1:]) == 1 else parts[1:] 

546 

547 try: 

548 fmin,fmax = opoly_settings['Shapes Range']['Freq Range (Hz)'] 

549 mif = mif.extract_elements_by_abscissa(float(fmin),float(fmax)) 

550 except KeyError: 

551 pass 

552 

553 stabilization_axis = mif.plot(True,stabilization_subplots_kwargs,stabilization_plot_kwargs) 

554 if mif_log: 

555 stabilization_axis.set_yscale('log') 

556 stabilization_axis.set_xlabel('Frequency (Hz)') 

557 

558 if poles is not None: 

559 ax_poles = stabilization_axis.twinx() 

560 legend_data = {} 

561 legend_choices = {'xf':'No Stability', 

562 'xt':'No Stability\nSelected For Final Mode Set', 

563 'sf':'Pole Stability', 

564 'st':'Pole Stability\nSelected For Final Mode Set', 

565 'of':'Vector Stability', 

566 'ot':'Vector Stability\nSelected For Final Mode Set', 

567 'af':'Autonomous Selection', 

568 'at':'Autonomous Selection\nSelected For Final Mode Set', 

569 } 

570 picked_model_orders = [] 

571 picked_pole_indices = [] 

572 for order,index in zip(opoly_settings['Pole List']['Model Order'], 

573 opoly_settings['Pole List']['Pole Index']): 

574 try: 

575 picked_model_orders.append(int(order)) 

576 except (ValueError,OverflowError): 

577 picked_model_orders.append('Auto') 

578 picked_pole_indices.append(int(index)-1) 

579 picked_pole_indices = np.array(picked_pole_indices) 

580 for order,pole,stability in zip(orders,poles,stabilities): 

581 try: 

582 order = int(order) 

583 except ValueError: 

584 order = 'Auto' 

585 if order == 0: 

586 continue 

587 for index,(freq,stab) in enumerate(zip(np.abs(pole),stability)): 

588 if order == 'Auto': 

589 kwargs = {'marker':'*','markeredgecolor':'y','markerfacecolor':'y','color':'none','markersize':8} 

590 l = 'a' 

591 elif stab == 'none': 

592 continue 

593 kwargs = {'marker':'x','markeredgecolor':'r','markerfacecolor':'r','color':'none','markersize':5} 

594 l = 'x' 

595 elif stab == 'freq': 

596 kwargs = {'marker':'s','markeredgecolor':'b','markerfacecolor':'b','color':'none','markersize':5} 

597 l = 's' 

598 elif stab == 'damp': 

599 kwargs = {'marker':'s','markeredgecolor':'b','markerfacecolor':'b','color':'none','markersize':5} 

600 l = 's' 

601 elif stab == 'vect': 

602 kwargs = {'marker':'o','markeredgecolor':'g','markerfacecolor':'g','color':'none','markersize':5} 

603 l = 'o' 

604 elif stab == '[]': 

605 continue 

606 kwargs = {'marker':'x','markeredgecolor':'r','markerfacecolor':'r','color':'none','markersize':5} 

607 l = 'x' 

608 # Check to see if the index is in the range 

609 same_orders = [order == o for o in picked_model_orders] 

610 is_picked = any(picked_pole_indices[same_orders] == index) 

611 if is_picked: 

612 s = 't' 

613 kwargs['markeredgecolor'] = 'k' 

614 else: 

615 s = 'f' 

616 kwargs['markerfacecolor'] = 'none' 

617 kwargs['markersize'] = 3 

618 legend_data[l+s] = ax_poles.plot(freq,(int(orders[-2])*2-int(orders[-3])) if order == 'Auto' else order,**kwargs)[0] 

619 # Create the legend 

620 legend_strings = [] 

621 legend_handles = [] 

622 for choice,string in legend_choices.items(): 

623 if choice in legend_data: 

624 legend_strings.append(string) 

625 legend_handles.append(legend_data[choice]) 

626 ax_poles.legend(legend_handles,legend_strings) 

627 ax_poles.set_ylabel('Polynomial Model Order') 

628 fig = ax_poles.figure 

629 # bio = io.BytesIO() 

630 # fig.savefig(bio,format='png') 

631 # bio.seek(0) 

632 # fig_bytes = bio.read() 

633 self.fit_modes_information = {'text':[ 

634 'Modes were fit to the data using the OPoly (version {:}) curve fitter implemented in the IMAT Matlab toolbox.'.format(opoly_settings['OPoly Version'])]} 

635 

636 self.fit_modes_information['text'].append(( 

637 'The frequency band from {:0.2f} to {:0.2f} was analyzed with polynomials from order {:} to {:} using the {:} method. '+ 

638 '{:} poles were selected from this band. The stabilization diagram is shown in Figure {{figure1ref:}}.' 

639 ).format(*[float(v) for v in opoly_settings['Poly Range']['Freq Range (Hz)']], 

640 opoly_settings['Poly Model']['Min Order'],opoly_settings['Poly Model']['Max Order'], 

641 opoly_settings['Poly Model']['Method'].upper(), 

642 len(opoly_settings['Pole List']['Pole Index']))) 

643 self.fit_modes_information['figure1'] = fig 

644 self.fit_modes_information['figure1caption'] = 'Stabilization Diagram from OPoly showing stable poles and those selected in the final mode set.' 

645 self.fit_modes_information['text'].append(( 

646 '{:} modes were used to fit the data. {:} residuals were used when fitting the mode shapes.').format( 

647 opoly_settings['Shapes Model']['Type'].title(), 

648 opoly_settings['Shapes Model']['Residuals'].replace('+',' and ').title())) 

649 

650 def edit_mode_comments(self,mif = 'cmif'): 

651 if self.fit_modes is None: 

652 raise ValueError('Modes have not yet been fit or assigned.') 

653 getattr(self,'plot_{:}'.format(mif))(measured=True,resynthesized=True,mark_modes=True) 

654 return self.fit_modes.edit_comments(self.geometry) 

655 

656 def compute_resynthesized_frfs(self): 

657 self.resynthesized_frfs = self.fit_modes.compute_frf(self.frfs.flatten()[0].abscissa, 

658 np.unique(self.frfs.response_coordinate), 

659 np.unique(self.frfs.reference_coordinate), 

660 )[self.frfs.coordinate] 

661 

662 def plot_reference_autospectra(self, plot_kwargs = {}, subplots_kwargs = {}): 

663 if self.autopower_spectra is None: 

664 raise ValueError('Autopower Spectra have not yet been computed or assigned') 

665 reference_apsd = self.autopower_spectra[self.autopower_spectra_reference_indices] 

666 ax = reference_apsd.plot(one_axis=False, plot_kwargs=plot_kwargs, subplots_kwargs = subplots_kwargs) 

667 for a in ax.flatten(): 

668 if self.reference_unit is not None: 

669 a.set_ylabel(a.get_ylabel()+'\n({:}$^2$/Hz)'.format(self.reference_unit)) 

670 a.set_xlabel('Frequency (Hz)') 

671 a.set_yscale('log') 

672 return ax.flatten()[0].figure, ax 

673 

674 def plot_drive_point_frfs(self, part='imag', plot_kwargs = {}, subplots_kwargs = {}): 

675 if self.frfs is None: 

676 raise ValueError('FRFs have not yet been computed or assigned') 

677 ax = self.frfs.get_drive_points().plot(one_axis=False,part=part, plot_kwargs=plot_kwargs, subplots_kwargs = subplots_kwargs) 

678 for a in ax.flatten(): 

679 if self.reference_unit is not None and self.response_unit is not None: 

680 a.set_ylabel(a.get_ylabel()+'\n({:}/{:})'.format(self.response_unit, self.reference_unit)) 

681 a.set_xlabel('Frequency (Hz)') 

682 return ax.flatten()[0].figure, ax 

683 

684 def plot_reciprocal_frfs(self, plot_kwargs = {}, subplots_kwargs = {}): 

685 if self.frfs is None: 

686 raise ValueError('FRFs have not yet been computed or assigned') 

687 reciprocal_frfs = self.frfs.get_reciprocal_data() 

688 axes = reciprocal_frfs[0].plot(one_axis=False, plot_kwargs=plot_kwargs, subplots_kwargs = subplots_kwargs) 

689 for ax, original_frf, reciprocal_frf in zip(axes.flatten(),*reciprocal_frfs): 

690 reciprocal_frf.plot(ax, **plot_kwargs) 

691 ax.legend([ 

692 '/'.join([str(coord) for coord in original_frf.coordinate]), 

693 '/'.join([str(coord) for coord in reciprocal_frf.coordinate])]) 

694 ax.set_xlabel('Frequency (Hz)') 

695 if self.reference_unit is not None and self.response_unit is not None: 

696 ax.set_ylabel(ax.get_ylabel()+'\n({:}/{:})'.format(self.response_unit, self.reference_unit)) 

697 return axes.flatten()[0].figure,axes 

698 

699 def plot_coherence_image(self): 

700 if self.coherence is None: 

701 raise ValueError('Coherence has not yet been computed or assigned') 

702 ax = self.coherence.plot_image(colorbar_min = 0, colorbar_max = 1) 

703 ax.set_ylabel('Degree of Freedom') 

704 ax.set_xlabel('Frequency (Hz)') 

705 return ax.figure, ax 

706 

707 def plot_drive_point_frf_coherence(self, plot_kwargs = {}, subplots_kwargs = {}): 

708 if self.frfs is None: 

709 raise ValueError('FRFs have not yet been computed or assigned') 

710 if self.coherence is None: 

711 raise ValueError('Coherence has not yet been computed or assigned') 

712 frf_ax, coh_ax = self.frfs.get_drive_points().plot_with_coherence(self.coherence, plot_kwargs = plot_kwargs, subplots_kwargs = subplots_kwargs) 

713 for a in frf_ax.flatten(): 

714 if self.reference_unit is not None and self.response_unit is not None: 

715 a.set_ylabel(a.get_ylabel()+'\n({:}/{:})'.format(self.response_unit, self.reference_unit)) 

716 a.set_xlabel('Frequency (Hz)') 

717 return frf_ax.flatten()[0].figure, frf_ax, coh_ax 

718 

719 def plot_cmif(self, measured = True, resynthesized = False, mark_modes = False, 

720 measured_plot_kwargs = {}, resynthesized_plot_kwargs = {}, 

721 subplots_kwargs = {}): 

722 """ 

723 Plots the complex mode indicator function 

724 

725 Parameters 

726 ---------- 

727 measured : bool, optional 

728 If True, plots the measured MIF. The default is True. 

729 resynthesized : bool, optional 

730 If True, plots resynthesized MIF. The default is False. 

731 mark_modes : bool, optional 

732 If True, plots a vertical line at the frequency of each mode. The 

733 default is False. 

734 measured_plot_kwargs : dict, optional 

735 Dictionary containing keyword arguments to specify how the measured 

736 data is plotted. The default is {}. 

737 resynthesized_plot_kwargs : dict, optional 

738 Dictionary containing keyword arguments to specify how the  

739 resynthesized data is plotted. The default is {}. 

740 subplots_kwargs : dict, optional 

741 Dictionary containing keyword arguments to specify how the figure 

742 and axes are created. This is passed to the plt.subplots function. 

743 The default is {}. 

744 

745 Raises 

746 ------ 

747 ValueError 

748 Raised if a required data has not been computed or assigned yet. 

749 

750 Returns 

751 ------- 

752 fig : matplotlib.figure.Figure 

753 A reference to the figure on which the plot is plotted. 

754 ax : matplotlib.axes.Axes 

755 A reference to the axes on which the plot is plotted. 

756 

757 """ 

758 fig, ax = plt.subplots(1,1,**subplots_kwargs) 

759 if measured and resynthesized: 

760 if self.frfs is None: 

761 raise ValueError('FRFs have not yet been computed or assigned') 

762 cmif = self.frfs.compute_cmif() 

763 cmif.plot(ax, plot_kwargs = measured_plot_kwargs) 

764 ax.set_yscale('log') 

765 ylim = ax.get_ylim() 

766 elif measured and not resynthesized: 

767 if self.frfs is None: 

768 raise ValueError('FRFs have not yet been computed or assigned') 

769 cmif = self.frfs.compute_cmif() 

770 cmif.plot(ax, plot_kwargs = measured_plot_kwargs, 

771 abscissa_markers = self.fit_modes.frequency 

772 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

773 'linestyle':'--', 

774 'alpha':0.5, 

775 'color':'k'}, 

776 abscissa_marker_labels = '{abscissa:0.1f}') 

777 ax.set_yscale('log') 

778 ylim = ax.get_ylim() 

779 else: 

780 ylim = None 

781 if resynthesized: 

782 if self.resynthesized_frfs is None: 

783 raise ValueError('Resynthesized FRFs have not yet been computed or assigned') 

784 cmif = self.resynthesized_frfs[self.frfs.coordinate].compute_cmif() 

785 cmif.plot(ax, plot_kwargs = resynthesized_plot_kwargs, 

786 abscissa_markers = self.fit_modes.frequency 

787 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

788 'linestyle':'--', 

789 'alpha':0.5, 

790 'color':'k'}, 

791 abscissa_marker_labels = '{abscissa:0.1f}') 

792 ax.set_yscale('log') 

793 if ylim is not None: 

794 ax.set_ylim(ylim) 

795 if measured and resynthesized: 

796 ax.legend([ax.lines[0],ax.lines[cmif.size]],['Measured','Resynthesized'],loc='upper right') 

797 ax.set_xlabel('Frequency (Hz)') 

798 if self.reference_unit is not None and self.response_unit is not None: 

799 ax.set_ylabel('Complex Mode Indicator Function ({:}/{:})'.format(self.response_unit,self.reference_unit)) 

800 else: 

801 ax.set_ylabel('Complex Mode Indicator Function') 

802 return fig, ax 

803 

804 def plot_qmif(self, measured = True, resynthesized = False, mark_modes = False, 

805 measured_plot_kwargs = {}, resynthesized_plot_kwargs = {}, 

806 subplots_kwargs = {}): 

807 """ 

808 Plots the complex mode indicator function computed from the imaginary 

809 part of the FRFs 

810 

811 Parameters 

812 ---------- 

813 measured : bool, optional 

814 If True, plots the measured MIF. The default is True. 

815 resynthesized : bool, optional 

816 If True, plots resynthesized MIF. The default is False. 

817 mark_modes : bool, optional 

818 If True, plots a vertical line at the frequency of each mode. The 

819 default is False. 

820 measured_plot_kwargs : dict, optional 

821 Dictionary containing keyword arguments to specify how the measured 

822 data is plotted. The default is {}. 

823 resynthesized_plot_kwargs : dict, optional 

824 Dictionary containing keyword arguments to specify how the  

825 resynthesized data is plotted. The default is {}. 

826 subplots_kwargs : dict, optional 

827 Dictionary containing keyword arguments to specify how the figure 

828 and axes are created. This is passed to the plt.subplots function. 

829 The default is {}. 

830 

831 Raises 

832 ------ 

833 ValueError 

834 Raised if a required data has not been computed or assigned yet. 

835 

836 Returns 

837 ------- 

838 fig : matplotlib.figure.Figure 

839 A reference to the figure on which the plot is plotted. 

840 ax : matplotlib.axes.Axes 

841 A reference to the axes on which the plot is plotted. 

842 

843 """ 

844 fig, ax = plt.subplots(1,1,**subplots_kwargs) 

845 if measured: 

846 if self.frfs is None: 

847 raise ValueError('FRFs have not yet been computed or assigned') 

848 cmif = self.frfs.compute_cmif(part='imag') 

849 cmif.plot(ax, plot_kwargs = measured_plot_kwargs) 

850 ax.set_yscale('log') 

851 ylim = ax.get_ylim() 

852 else: 

853 ylim = None 

854 if resynthesized: 

855 if self.resynthesized_frfs is None: 

856 raise ValueError('Resynthesized FRFs have not yet been computed or assigned') 

857 cmif = self.resynthesized_frfs[self.frfs.coordinate].compute_cmif(part='imag') 

858 cmif.plot(ax, plot_kwargs = resynthesized_plot_kwargs, 

859 abscissa_markers = self.fit_modes.frequency 

860 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

861 'linestyle':'--', 

862 'alpha':0.5, 

863 'color':'k'}, 

864 abscissa_marker_labels = '{abscissa:0.1f}') 

865 ax.set_yscale('log') 

866 if ylim is not None: 

867 ax.set_ylim(ylim) 

868 if measured and resynthesized: 

869 ax.legend([ax.lines[0],ax.lines[cmif.size]],['Measured','Resynthesized'],loc='upper right') 

870 ax.set_xlabel('Frequency (Hz)') 

871 if self.reference_unit is not None and self.response_unit is not None: 

872 ax.set_ylabel('Quadrature Mode Indicator Function ({:}/{:})'.format(self.response_unit,self.reference_unit)) 

873 else: 

874 ax.set_ylabel('Quadrature Mode Indicator Function') 

875 return fig, ax 

876 

877 def plot_psmif(self, measured = True, resynthesized = False, mark_modes = False, 

878 measured_plot_kwargs = {}, resynthesized_plot_kwargs = {}, 

879 subplots_kwargs = {}): 

880 """ 

881 Plots the first singular value of the complex mode indicator function 

882 

883 Parameters 

884 ---------- 

885 measured : bool, optional 

886 If True, plots the measured MIF. The default is True. 

887 resynthesized : bool, optional 

888 If True, plots resynthesized MIF. The default is False. 

889 mark_modes : bool, optional 

890 If True, plots a vertical line at the frequency of each mode. The 

891 default is False. 

892 measured_plot_kwargs : dict, optional 

893 Dictionary containing keyword arguments to specify how the measured 

894 data is plotted. The default is {}. 

895 resynthesized_plot_kwargs : dict, optional 

896 Dictionary containing keyword arguments to specify how the  

897 resynthesized data is plotted. The default is {}. 

898 subplots_kwargs : dict, optional 

899 Dictionary containing keyword arguments to specify how the figure 

900 and axes are created. This is passed to the plt.subplots function. 

901 The default is {}. 

902 

903 Raises 

904 ------ 

905 ValueError 

906 Raised if a required data has not been computed or assigned yet. 

907 

908 Returns 

909 ------- 

910 fig : matplotlib.figure.Figure 

911 A reference to the figure on which the plot is plotted. 

912 ax : matplotlib.axes.Axes 

913 A reference to the axes on which the plot is plotted. 

914 

915 """ 

916 fig, ax = plt.subplots(1,1,**subplots_kwargs) 

917 if measured: 

918 if self.frfs is None: 

919 raise ValueError('FRFs have not yet been computed or assigned') 

920 cmif = self.frfs.compute_cmif()[:1] 

921 cmif.plot(ax, plot_kwargs = measured_plot_kwargs) 

922 ax.set_yscale('log') 

923 ylim = ax.get_ylim() 

924 else: 

925 ylim = None 

926 if resynthesized: 

927 if self.resynthesized_frfs is None: 

928 raise ValueError('Resynthesized FRFs have not yet been computed or assigned') 

929 cmif = self.resynthesized_frfs[self.frfs.coordinate].compute_cmif()[:1] 

930 cmif.plot(ax, plot_kwargs = resynthesized_plot_kwargs, 

931 abscissa_markers = self.fit_modes.frequency 

932 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

933 'linestyle':'--', 

934 'alpha':0.5, 

935 'color':'k'}, 

936 abscissa_marker_labels = '{abscissa:0.1f}') 

937 ax.set_yscale('log') 

938 if ylim is not None: 

939 ax.set_ylim(ylim) 

940 if measured and resynthesized: 

941 ax.legend([ax.lines[0],ax.lines[cmif.size]],['Measured','Resynthesized'],loc='upper right') 

942 ax.set_xlabel('Frequency (Hz)') 

943 if self.reference_unit is not None and self.response_unit is not None: 

944 ax.set_ylabel('Principal Singular Value\nMode Indicator Function ({:}/{:})'.format(self.response_unit,self.reference_unit)) 

945 else: 

946 ax.set_ylabel('Principal Singular Value\nMode Indicator Function') 

947 return fig, ax 

948 

949 def plot_nmif(self, measured = True, resynthesized = False, mark_modes = False, 

950 measured_plot_kwargs = {}, resynthesized_plot_kwargs = {}, 

951 subplots_kwargs = {}): 

952 """ 

953 Plots the normal mode indicator function 

954 

955 Parameters 

956 ---------- 

957 measured : bool, optional 

958 If True, plots the measured MIF. The default is True. 

959 resynthesized : bool, optional 

960 If True, plots resynthesized MIF. The default is False. 

961 mark_modes : bool, optional 

962 If True, plots a vertical line at the frequency of each mode. The 

963 default is False. 

964 measured_plot_kwargs : dict, optional 

965 Dictionary containing keyword arguments to specify how the measured 

966 data is plotted. The default is {}. 

967 resynthesized_plot_kwargs : dict, optional 

968 Dictionary containing keyword arguments to specify how the  

969 resynthesized data is plotted. The default is {}. 

970 subplots_kwargs : dict, optional 

971 Dictionary containing keyword arguments to specify how the figure 

972 and axes are created. This is passed to the plt.subplots function. 

973 The default is {}. 

974 

975 Raises 

976 ------ 

977 ValueError 

978 Raised if a required data has not been computed or assigned yet. 

979 

980 Returns 

981 ------- 

982 fig : matplotlib.figure.Figure 

983 A reference to the figure on which the plot is plotted. 

984 ax : matplotlib.axes.Axes 

985 A reference to the axes on which the plot is plotted. 

986 

987 """ 

988 fig, ax = plt.subplots(1,1,**subplots_kwargs) 

989 if measured: 

990 if self.frfs is None: 

991 raise ValueError('FRFs have not yet been computed or assigned') 

992 cmif = self.frfs.compute_nmif() 

993 cmif.plot(ax, plot_kwargs = measured_plot_kwargs) 

994 ylim = ax.get_ylim() 

995 else: 

996 ylim = None 

997 if resynthesized: 

998 if self.resynthesized_frfs is None: 

999 raise ValueError('Resynthesized FRFs have not yet been computed or assigned') 

1000 cmif = self.resynthesized_frfs[self.frfs.coordinate].compute_nmif() 

1001 cmif.plot(ax, plot_kwargs = resynthesized_plot_kwargs, 

1002 abscissa_markers = self.fit_modes.frequency 

1003 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

1004 'linestyle':'--', 

1005 'alpha':0.5, 

1006 'color':'k'}, 

1007 abscissa_marker_labels = '{abscissa:0.1f}') 

1008 if ylim is not None: 

1009 ax.set_ylim(ylim) 

1010 if measured and resynthesized: 

1011 ax.legend([ax.lines[0],ax.lines[cmif.size]],['Measured','Resynthesized'],loc='upper right') 

1012 ax.set_xlabel('Frequency (Hz)') 

1013 ax.set_ylabel('Normal Mode Indicator Function') 

1014 return fig, ax 

1015 

1016 def plot_mmif(self, measured = True, resynthesized = False, mark_modes = False, 

1017 measured_plot_kwargs = {}, resynthesized_plot_kwargs = {}, 

1018 subplots_kwargs = {}): 

1019 """ 

1020 Plots the multi mode indicator function 

1021 

1022 Parameters 

1023 ---------- 

1024 measured : bool, optional 

1025 If True, plots the measured MIF. The default is True. 

1026 resynthesized : bool, optional 

1027 If True, plots resynthesized MIF. The default is False. 

1028 mark_modes : bool, optional 

1029 If True, plots a vertical line at the frequency of each mode. The 

1030 default is False. 

1031 measured_plot_kwargs : dict, optional 

1032 Dictionary containing keyword arguments to specify how the measured 

1033 data is plotted. The default is {}. 

1034 resynthesized_plot_kwargs : dict, optional 

1035 Dictionary containing keyword arguments to specify how the  

1036 resynthesized data is plotted. The default is {}. 

1037 subplots_kwargs : dict, optional 

1038 Dictionary containing keyword arguments to specify how the figure 

1039 and axes are created. This is passed to the plt.subplots function. 

1040 The default is {}. 

1041 

1042 Raises 

1043 ------ 

1044 ValueError 

1045 Raised if a required data has not been computed or assigned yet. 

1046 

1047 Returns 

1048 ------- 

1049 fig : matplotlib.figure.Figure 

1050 A reference to the figure on which the plot is plotted. 

1051 ax : matplotlib.axes.Axes 

1052 A reference to the axes on which the plot is plotted. 

1053 

1054 """ 

1055 fig, ax = plt.subplots(1,1,**subplots_kwargs) 

1056 if measured: 

1057 if self.frfs is None: 

1058 raise ValueError('FRFs have not yet been computed or assigned') 

1059 cmif = self.frfs.compute_mmif() 

1060 cmif.plot(ax, plot_kwargs = measured_plot_kwargs) 

1061 ylim = ax.get_ylim() 

1062 else: 

1063 ylim = None 

1064 if resynthesized: 

1065 if self.resynthesized_frfs is None: 

1066 raise ValueError('Resynthesized FRFs have not yet been computed or assigned') 

1067 cmif = self.resynthesized_frfs[self.frfs.coordinate].compute_mmif() 

1068 cmif.plot(ax, plot_kwargs = resynthesized_plot_kwargs, 

1069 abscissa_markers = self.fit_modes.frequency 

1070 if mark_modes else None, abscissa_marker_plot_kwargs={'linewidth':0.5, 

1071 'linestyle':'--', 

1072 'alpha':0.5, 

1073 'color':'k'}, 

1074 abscissa_marker_labels = '{abscissa:0.1f}') 

1075 if ylim is not None: 

1076 ax.set_ylim(ylim) 

1077 if measured and resynthesized: 

1078 ax.legend([ax.lines[0],ax.lines[cmif.size]],['Measured','Resynthesized'],loc='upper right') 

1079 ax.set_xlabel('Frequency (Hz)') 

1080 ax.set_ylabel('Multi Mode Indicator Function') 

1081 return fig, ax 

1082 

1083 def plot_deflection_shapes(self): 

1084 if self.frfs is None: 

1085 raise ValueError('FRFs have not yet been computed or assigned') 

1086 if self.geometry is None: 

1087 raise ValueError('Geometry must be assigned to plot deflection shapes') 

1088 frfs = self.frfs.reshape_to_matrix() 

1089 plotters = [] 

1090 for frf in frfs.T: 

1091 plotters.append(self.geometry.plot_deflection_shape(frf)) 

1092 return plotters 

1093 

1094 def plot_mac(self, *matrix_plot_args, **matrix_plot_kwargs): 

1095 if self.fit_modes is None: 

1096 raise ValueError('Modes have not yet been fit or assigned.') 

1097 mac_matrix = mac(self.fit_modes) 

1098 ax = matrix_plot(mac_matrix, *matrix_plot_args, **matrix_plot_kwargs) 

1099 return ax.figure, ax 

1100 

1101 def plot_modeshape(self): 

1102 if self.fit_modes is None: 

1103 raise ValueError('Modes have not yet been fit or assigned.') 

1104 if self.geometry is None: 

1105 raise ValueError('Geometry must be assigned to plot deflection shapes') 

1106 return self.geometry.plot_shape(self.fit_modes) 

1107 

1108 def plot_figures_for_documentation(self, 

1109 plot_geometry = True, 

1110 geometry_kwargs = {}, 

1111 plot_coordinate = True, 

1112 coordinate_kwargs = {}, 

1113 plot_rigid_body_checks = True, 

1114 rigid_body_checks_kwargs = {}, 

1115 plot_reference_autospectra = True, 

1116 reference_autospectra_kwargs = {}, 

1117 plot_drive_point_frfs = True, 

1118 drive_point_frfs_kwargs = {}, 

1119 plot_reciprocal_frfs = True, 

1120 reciprocal_frfs_kwargs = {}, 

1121 plot_frf_coherence = True, 

1122 frf_coherence_kwargs = {}, 

1123 plot_coherence_image = True, 

1124 coherence_image_kwargs = {}, 

1125 plot_cmif = True, 

1126 cmif_kwargs = {}, 

1127 plot_qmif = False, 

1128 qmif_kwargs = {}, 

1129 plot_nmif = False, 

1130 nmif_kwargs = {}, 

1131 plot_mmif = False, 

1132 mmif_kwargs = {}, 

1133 plot_modeshapes = True, 

1134 modeshape_kwargs = {}, 

1135 plot_mac = True, 

1136 mac_kwargs = {}, 

1137 ): 

1138 self.documentation_figures = {} 

1139 if plot_geometry: 

1140 if self.geometry is None: 

1141 print('Warning: Could not plot geometry; geometry is undefined.') 

1142 else: 

1143 plotter = self.geometry.plot(**geometry_kwargs)[0] 

1144 self.documentation_figures['geometry'] = plotter 

1145 if plot_coordinate: 

1146 if self.geometry is None: 

1147 print('Warning: Could not plot coordinates; geometry is undefined.') 

1148 elif self.time_histories is None: 

1149 print('Warning: Could not plot coordinates; time_histories is undefined') 

1150 else: 

1151 if not 'plot_kwargs' in coordinate_kwargs: 

1152 coordinate_kwargs['plot_kwargs'] = geometry_kwargs 

1153 # Check and see if the references are defined 

1154 if self.reference_indices is not None: 

1155 plotter = self.geometry.plot_coordinate( 

1156 self.time_histories[self.response_indices].coordinate.flatten(), 

1157 **coordinate_kwargs) 

1158 self.documentation_figures['response_coordinate'] = plotter 

1159 plotter = self.geometry.plot_coordinate( 

1160 self.time_histories[self.reference_indices].coordinate.flatten(), 

1161 **coordinate_kwargs) 

1162 self.documentation_figures['reference_coordinate'] = plotter 

1163 else: 

1164 plotter = self.geometry.plot_coordinate( 

1165 self.time_histories.coordinate.flatten(), 

1166 **coordinate_kwargs) 

1167 self.documentation_figures['coordinate'] = plotter 

1168 if plot_rigid_body_checks: 

1169 if self.geometry is None: 

1170 print('Warning: Could not plot rigid body checks; geometry is undefined.') 

1171 elif self.rigid_body_shapes is None: 

1172 print('Warning: Could not plot rigid body checks; rigid_body_shapes is undefined.') 

1173 else: 

1174 common_nodes = np.intersect1d(self.geometry.node.id,np.unique(self.rigid_body_shapes.coordinate.node)) 

1175 geometry = self.geometry.reduce(common_nodes) 

1176 rigid_shapes = self.rigid_body_shapes.reduce(common_nodes) 

1177 supicious_channels, *figures = rigid_body_check(geometry, rigid_shapes, 

1178 return_figures = True, **rigid_body_checks_kwargs) 

1179 num_figs = self.rigid_body_shapes.size+1 

1180 figures = figures[-num_figs:] 

1181 for i in range(num_figs-1): 

1182 self.documentation_figures['rigid_body_complex_plane_{:}'.format(i)] = figures[i] 

1183 self.documentation_figures['rigid_body_residuals'] = figures[-1] 

1184 plotter = self.geometry.plot_shape( 

1185 self.rigid_body_shapes, 

1186 plot_kwargs = geometry_kwargs) 

1187 self.documentation_figures['rigid_body_shapes'] = plotter 

1188 if plot_reference_autospectra: 

1189 if self.autopower_spectra is None: 

1190 print('Warning: Could not plot reference autospectra; autopower_spectra is undefined') 

1191 else: 

1192 fig,ax = self.plot_reference_autospectra(**reference_autospectra_kwargs) 

1193 self.documentation_figures['reference_autospectra'] = fig 

1194 if plot_drive_point_frfs: 

1195 if self.frfs is None: 

1196 print('Warning: Could not plot drive point FRFs; frfs is undefined') 

1197 else: 

1198 kwargs = {'part':'imag'} 

1199 kwargs.update(drive_point_frfs_kwargs) 

1200 fig,ax = self.plot_drive_point_frfs(**kwargs) 

1201 self.documentation_figures['drive_point_frf'] = fig 

1202 if plot_reciprocal_frfs: 

1203 if self.frfs is None: 

1204 print('Warning: Could not plot drive point FRFs; frfs is undefined') 

1205 else: 

1206 kwargs = {} 

1207 kwargs.update(reciprocal_frfs_kwargs) 

1208 fig,ax = self.plot_reciprocal_frfs(**kwargs) 

1209 self.documentation_figures['reciprocal_frfs'] = fig 

1210 if plot_frf_coherence: 

1211 if self.frfs is None: 

1212 print('Warning: Could not plot FRF coherence; frfs is undefined') 

1213 elif self.coherence is None: 

1214 print('Warning: Could not plot FRF coherence; coherence is undefined') 

1215 else: 

1216 fig, ax, cax = self.plot_drive_point_frf_coherence(**frf_coherence_kwargs) 

1217 self.documentation_figures['frf_coherence'] = fig 

1218 if plot_coherence_image: 

1219 if self.coherence is None: 

1220 print('Warning: Could not plot coherence; coherence is undefined') 

1221 else: 

1222 fig,ax = self.plot_coherence_image(**coherence_image_kwargs) 

1223 self.documentation_figures['coherence'] = fig 

1224 if plot_cmif: 

1225 if self.frfs is None: 

1226 print('Warning: Could not plot CMIF; frfs is undefined') 

1227 else: 

1228 fig,ax = self.plot_cmif(True,self.resynthesized_frfs is not None, 

1229 self.fit_modes is not None, cmif_kwargs, cmif_kwargs) 

1230 self.documentation_figures['cmif'] = fig 

1231 if plot_qmif: 

1232 if self.frfs is None: 

1233 print('Warning: Could not plot QMIF; frfs is undefined') 

1234 else: 

1235 fig,ax = self.plot_qmif(True,self.resynthesized_frfs is not None, 

1236 self.fit_modes is not None, qmif_kwargs, qmif_kwargs) 

1237 self.documentation_figures['qmif'] = fig 

1238 if plot_nmif: 

1239 if self.frfs is None: 

1240 print('Warning: Could not plot NMIF; frfs is undefined') 

1241 else: 

1242 fig,ax = self.plot_nmif(True,self.resynthesized_frfs is not None, 

1243 self.fit_modes is not None, nmif_kwargs, nmif_kwargs) 

1244 self.documentation_figures['nmif'] = fig 

1245 if plot_mmif: 

1246 if self.frfs is None: 

1247 print('Warning: Could not plot MMIF; frfs is undefined') 

1248 else: 

1249 fig,ax = self.plot_mmif(True,self.resynthesized_frfs is not None, 

1250 self.fit_modes is not None, mmif_kwargs, mmif_kwargs) 

1251 self.documentation_figures['mmif'] = fig 

1252 if plot_modeshapes: 

1253 if self.fit_modes is None: 

1254 print('Warning: Cannot plot modeshapes, fit_modes is undefined.') 

1255 elif self.geometry is None: 

1256 print('Warning: Cannot plot modeshapes, geometry is undefined.') 

1257 else: 

1258 plotter = self.geometry.plot_shape( 

1259 self.fit_modes, 

1260 plot_kwargs = geometry_kwargs,**modeshape_kwargs) 

1261 self.documentation_figures['mode_shapes'] = plotter 

1262 if plot_mac: 

1263 if self.fit_modes is None: 

1264 print('Warning: Cannot plot MAC, fit_modes is undefined.') 

1265 else: 

1266 fig,ax = self.plot_mac(**mac_kwargs) 

1267 self.documentation_figures['mac'] = fig 

1268 

1269 def create_documentation_latex( 

1270 self, 

1271 # Important stuff 

1272 coordinate_array='local', 

1273 fit_modes_table = None, 

1274 resynthesis_comparison='cmif', 

1275 resynthesis_figure = None, 

1276 one_file = True, 

1277 # Animation options 

1278 global_animation_style = '2d', 

1279 geometry_animation_frames=200, geometry_animation_frame_rate=20, 

1280 shape_animation_frames=20, shape_animation_frame_rate=20, 

1281 animation_style_geometry=None, 

1282 animation_style_rigid_body=None, 

1283 animation_style_mode_shape=None, 

1284 # Global Paths 

1285 latex_root=r'', figure_root=None, 

1286 # Figure names 

1287 geometry_figure_save_name=None, 

1288 coordinate_figure_save_name=None, 

1289 rigid_body_figure_save_names=None, 

1290 complex_plane_figure_save_names=None, 

1291 residual_figure_save_names=None, 

1292 reference_autospectra_figure_save_names=None, 

1293 drive_point_frfs_figure_save_names=None, 

1294 reciprocal_frfs_figure_save_names=None, 

1295 frf_coherence_figure_save_names=None, 

1296 coherence_figure_save_names=None, 

1297 fit_mode_information_save_names=None, 

1298 mac_plot_save_name=None, 

1299 resynthesis_plot_save_name=None, 

1300 mode_shape_save_names = None, 

1301 # Function KWARGS 

1302 plot_geometry_kwargs={}, 

1303 plot_shape_kwargs = {}, 

1304 plot_coordinate_kwargs = {}, 

1305 rigid_body_check_kwargs={}, 

1306 resynthesis_plot_kwargs=None, 

1307 fit_mode_table_kwargs={}, 

1308 mac_plot_kwargs=None, 

1309 # Include names 

1310 include_name_geometry=None, 

1311 include_name_signal_processing=None, 

1312 include_name_rigid_body=None, 

1313 include_name_data_quality=None, 

1314 include_name_mode_fitting=None, 

1315 include_name_mode_shape=None, 

1316 include_name_channel_table=None, 

1317 # Arguments for create_geometry_overview 

1318 geometry_figure_label='fig:geometry', 

1319 geometry_figure_caption='Geometry', 

1320 geometry_graphics_options=r'width=0.7\linewidth', 

1321 geometry_animate_graphics_options=r'width=0.7\linewidth,loop', 

1322 geometry_figure_placement='[h]', 

1323 coordinate_figure_label='fig:coordinate', 

1324 coordinate_figure_caption='Local Coordinate Directions (Red: X+, Green: Y+, Blue: Z+)', 

1325 coordinate_graphics_options=r'width=0.7\linewidth', 

1326 coordinate_animate_graphics_options=r'width=0.7\linewidth,loop', 

1327 coordinate_figure_placement='[h]', 

1328 # Arguments for create_rigid_body_analysis 

1329 figure_label_rigid_body='fig:rigid_shapes', 

1330 complex_plane_figure_label='fig:complex_plane', 

1331 residual_figure_label='fig:rigid_shape_residual', 

1332 figure_caption_rigid_body='Rigid body shapes extracted from test data.', 

1333 complex_plane_caption='Complex Plane of the extracted shapes.', 

1334 residual_caption='Rigid body residual showing non-rigid portions of the shapes.', 

1335 graphics_options_rigid_body=r'width=\linewidth', 

1336 complex_plane_graphics_options=r'width=\linewidth', 

1337 residual_graphics_options=r'width=0.7\linewidth', 

1338 animate_graphics_options_rigid_body=r'width=\linewidth,loop', 

1339 figure_placement_rigid_body='', 

1340 complex_plane_figure_placement='', 

1341 residual_figure_placement='', 

1342 subfigure_options_rigid_body=r'[t]{0.45\linewidth}', 

1343 subfigure_labels_rigid_body=None, 

1344 subfigure_captions_rigid_body=None, 

1345 complex_plane_subfigure_options=r'[t]{0.45\linewidth}', 

1346 complex_plane_subfigure_labels=None, 

1347 max_subfigures_per_page_rigid_body=None, 

1348 max_subfigures_first_page_rigid_body=None, 

1349 # Arguments for create_data_quality_summary 

1350 reference_autospectra_figure_label='fig:reference_autospectra', 

1351 reference_autospectra_figure_caption='Autospectra of the reference channels', 

1352 reference_autospectra_graphics_options=r'width=0.7\linewidth', 

1353 reference_autospectra_figure_placement='', 

1354 reference_autospectra_subfigure_options=r'[t]{0.45\linewidth}', 

1355 reference_autospectra_subfigure_labels=None, 

1356 reference_autospectra_subfigure_captions=None, 

1357 drive_point_frfs_figure_label='fig:drive_point_frf', 

1358 drive_point_frfs_figure_caption='Drive point frequency response functions', 

1359 drive_point_frfs_graphics_options=r'width=\linewidth', 

1360 drive_point_frfs_figure_placement='', 

1361 drive_point_frfs_subfigure_options=r'[t]{0.45\linewidth}', 

1362 drive_point_frfs_subfigure_labels=None, 

1363 drive_point_frfs_subfigure_captions=None, 

1364 reciprocal_frfs_figure_label='fig:reciprocal_frfs', 

1365 reciprocal_frfs_figure_caption='Reciprocal frequency response functions.', 

1366 reciprocal_frfs_graphics_options=r'width=0.7\linewidth', 

1367 reciprocal_frfs_figure_placement='', 

1368 reciprocal_frfs_subfigure_options=r'[t]{0.45\linewidth}', 

1369 reciprocal_frfs_subfigure_labels=None, 

1370 reciprocal_frfs_subfigure_captions=None, 

1371 frf_coherence_figure_label='fig:frf_coherence', 

1372 frf_coherence_figure_caption='Drive point frequency response functions with coherence overlaid', 

1373 frf_coherence_graphics_options=r'width=\linewidth', 

1374 frf_coherence_figure_placement='', 

1375 frf_coherence_subfigure_options=r'[t]{0.45\linewidth}', 

1376 frf_coherence_subfigure_labels=None, 

1377 frf_coherence_subfigure_captions=None, 

1378 coherence_figure_label='fig:coherence', 

1379 coherence_figure_caption='Coherence of all channels in the test.', 

1380 coherence_graphics_options=r'width=0.7\linewidth', 

1381 coherence_figure_placement='', 

1382 coherence_subfigure_options=r'[t]{0.45\linewidth}', 

1383 coherence_subfigure_labels=None, 

1384 coherence_subfigure_captions=None, 

1385 max_subfigures_per_page=None, 

1386 max_subfigures_first_page=None, 

1387 # Arguments for create_mode_fitting_summary 

1388 fit_modes_information_table_justification_string=None, 

1389 fit_modes_information_table_longtable=True, 

1390 fit_modes_information_table_header=True, 

1391 fit_modes_information_table_horizontal_lines=False, 

1392 fit_modes_information_table_placement='', 

1393 fit_modes_information_figure_graphics_options=r'width=0.7\linewidth', 

1394 fit_modes_information_figure_placement='', 

1395 fit_modes_table_justification_string=None, 

1396 fit_modes_table_label='tab:mode_fits', 

1397 fit_modes_table_caption='Modal parameters fit to the test data.', 

1398 fit_modes_table_longtable=True, 

1399 fit_modes_table_header=True, 

1400 fit_modes_table_horizontal_lines=False, 

1401 fit_modes_table_placement='', 

1402 fit_modes_table_header_override=None, 

1403 mac_plot_figure_label='fig:mac', 

1404 mac_plot_figure_caption='Modal Assurance Criterion Matrix from Fit Modes', mac_plot_graphics_options=r'width=0.7\linewidth', 

1405 mac_plot_figure_placement='', 

1406 resynthesis_plot_figure_label='fig:resynthesis', 

1407 resynthesis_plot_figure_caption='Test data compared to equivalent data computed from modal fits.', 

1408 resynthesis_plot_graphics_options=r'width=0.7\linewidth', 

1409 resynthesis_plot_figure_placement='', 

1410 # Arguments for create_mode_shape_figures 

1411 figure_label_mode_shape='fig:modeshapes', 

1412 figure_caption_mode_shape='Mode shapes extracted from test data.', 

1413 graphics_options_mode_shape=r'width=\linewidth', 

1414 animate_graphics_options_mode_shape=r'width=\linewidth,loop', 

1415 figure_placement_mode_shape='', 

1416 subfigure_options_mode_shape=r'[t]{0.45\linewidth}', 

1417 subfigure_labels_mode_shape=None, 

1418 subfigure_captions_mode_shape=None, 

1419 max_subfigures_per_page_mode_shape=None, max_subfigures_first_page_mode_shape=None, 

1420 ): 

1421 

1422 if one_file: 

1423 all_strings = [] 

1424 

1425 if len(self.documentation_figures) == 0: 

1426 print('Warning, you may need to create documentation figures by calling create_figures_for_documentation prior to calling this function!') 

1427 # Set up the files 

1428 if figure_root is None: 

1429 figure_root = os.path.join(latex_root,'figures') 

1430 

1431 Path(figure_root).mkdir(parents=True,exist_ok=True) 

1432 Path(latex_root).mkdir(parents=True,exist_ok=True) 

1433 

1434 if animation_style_geometry is None: 

1435 animation_style_geometry = global_animation_style 

1436 if 'geometry' in self.documentation_figures and (animation_style_geometry is None or animation_style_geometry.lower() != '3d'): 

1437 geometry = self.documentation_figures['geometry'] 

1438 else: 

1439 geometry = self.geometry 

1440 

1441 if isinstance(geometry,GeometryPlotter) and not isinstance(coordinate_array,GeometryPlotter): 

1442 if isinstance(coordinate_array,CoordinateArray): 

1443 coordinate_array = self.geometry.plot_coordinate(coordinate_array,**plot_coordinate_kwargs) 

1444 elif coordinate_array == 'local': 

1445 css_to_plot = self.geometry.coordinate_system.id[[not np.allclose(matrix,np.eye(3)) for matrix in self.geometry.coordinate_system.matrix[...,:3,:3]]] 

1446 nodes_to_plot = self.geometry.node.id[ 

1447 np.isin(self.geometry.node.disp_cs, css_to_plot) 

1448 ] 

1449 coordinate_array = sd_coordinate_array(nodes_to_plot,[1,2,3],force_broadcast=True) 

1450 coordinate_array = self.geometry.plot_coordinate(coordinate_array,**plot_coordinate_kwargs) 

1451 else: 

1452 coordinate_array = None 

1453 

1454 print('Creating Geometry Overview') 

1455 geometry_string = create_geometry_overview( 

1456 geometry, plot_geometry_kwargs, coordinate_array, plot_coordinate_kwargs, 

1457 animation_style_geometry, geometry_animation_frames, geometry_animation_frame_rate, 

1458 geometry_figure_label, geometry_figure_caption, geometry_graphics_options, geometry_animate_graphics_options, 

1459 geometry_figure_placement, geometry_figure_save_name, coordinate_figure_label, coordinate_figure_caption, 

1460 coordinate_graphics_options, coordinate_animate_graphics_options, coordinate_figure_placement, 

1461 coordinate_figure_save_name, latex_root, figure_root, 

1462 None if one_file else os.path.join(latex_root,'geometry.tex') if include_name_geometry is None else include_name_geometry 

1463 ) 

1464 

1465 if one_file: 

1466 all_strings.append(geometry_string) 

1467 

1468 if 'reference_autospectra' in self.documentation_figures: 

1469 reference_autospectra_figure = self.documentation_figures['reference_autospectra'] 

1470 else: 

1471 reference_autospectra_figure = None 

1472 if 'drive_point_frf' in self.documentation_figures: 

1473 drive_point_frfs_figure = self.documentation_figures['drive_point_frf'] 

1474 else: 

1475 drive_point_frfs_figure = None 

1476 if 'reciprocal_frfs' in self.documentation_figures: 

1477 reciprocal_frfs_figure = self.documentation_figures['reciprocal_frfs'] 

1478 else: 

1479 reciprocal_frfs_figure = None 

1480 if 'frf_coherence' in self.documentation_figures: 

1481 frf_coherence_figure = self.documentation_figures['frf_coherence'] 

1482 else: 

1483 frf_coherence_figure = None 

1484 if 'coherence' in self.documentation_figures: 

1485 coherence_figure = self.documentation_figures['coherence'] 

1486 else: 

1487 coherence_figure = None 

1488 

1489 if animation_style_rigid_body is None: 

1490 animation_style_rigid_body = global_animation_style 

1491 

1492 if 'rigid_body_shapes' in self.documentation_figures and (animation_style_rigid_body is None or animation_style_rigid_body.lower() != '3d'): 

1493 rigid_shapes = self.documentation_figures['rigid_body_shapes'] 

1494 else: 

1495 rigid_shapes = self.rigid_body_shapes 

1496 

1497 if 'rigid_body_complex_plane_0' in self.documentation_figures: 

1498 complex_plane_figures = [] 

1499 i = 0 

1500 while True: 

1501 try: 

1502 complex_plane_figures.append(self.documentation_figures['rigid_body_complex_plane_{:}'.format(i)]) 

1503 i += 1 

1504 except KeyError: 

1505 break 

1506 else: 

1507 complex_plane_figures = None 

1508 if 'rigid_body_residuals' in self.documentation_figures: 

1509 residual_figure = self.documentation_figures['rigid_body_residuals'] 

1510 else: 

1511 residual_figure = None 

1512 

1513 print('Creating Test Parameters Section') 

1514 test_parameters_string = ( 

1515 'The data acquisition system was set to acquire {sample_rate:} ' 

1516 'samples per second. To compute spectra, the time signals were ' 

1517 'split into measurement frames with {samples_per_frame:} samples ' 

1518 'per measurement frame. Measurement channels were split into {num_ref:} ' 

1519 'references and {num_resp} responses. {num_avg:} frames were acquired with ' 

1520 '{overlap:}\\% overlap and averaged to compute frequency response functions via the {estimator:} ' 

1521 'method.').format(sample_rate = self.sample_rate, 

1522 samples_per_frame = self.num_samples_per_frame, 

1523 num_ref = len(np.unique(self.frfs.reference_coordinate)), 

1524 num_resp = len(np.unique(self.frfs.response_coordinate)), 

1525 num_avg = self.num_averages, 

1526 overlap = float(self.overlap)*100, 

1527 estimator = self.frf_estimator, 

1528 ) 

1529 if (self.window is not None and self.window.lower() != 'rectangle' and 

1530 self.window.lower() != 'boxcar' and self.window.lower() != 'none' and 

1531 self.window.lower() != 'rectangular'): 

1532 test_parameters_string += ' A {window:} window was applied to each frame.' 

1533 if (self.trigger is not None and self.trigger.lower() != 'free run' and 

1534 self.trigger != 'none'): 

1535 test_parameters_string += ( 

1536 ' A trigger used to start the measurement of {frame:} frame. ' 

1537 'Channel {index:} was used to trigger the measurement with a {slope:} ' 

1538 'and at a level of {level:}% and a pretrigger of {pretrigger:}%.').format( 

1539 frame = 'every' if 'every' in self.trigger.lower() else 'first', 

1540 index = self.trigger_channel_index+1, 

1541 slope = self.trigger_slope.lower() + ' slope', 

1542 level = float(self.trigger_level)*100, 

1543 pretrigger = float(self.pretrigger)*100) 

1544 if not one_file: 

1545 with open(os.path.join(latex_root,'signal_processing.tex') if include_name_signal_processing is None else include_name_signal_processing,'w') as f: 

1546 f.write(test_parameters_string) 

1547 else: 

1548 all_strings.append(test_parameters_string) 

1549 

1550 print('Creating Rigid Body Analysis') 

1551 rigid_body_string = create_rigid_body_analysis( 

1552 self.geometry, rigid_shapes, complex_plane_figures, residual_figure, figure_label_rigid_body, 

1553 complex_plane_figure_label, residual_figure_label, figure_caption_rigid_body, complex_plane_caption, residual_caption, 

1554 graphics_options_rigid_body, complex_plane_graphics_options, residual_graphics_options, animate_graphics_options_rigid_body, 

1555 figure_placement_rigid_body, complex_plane_figure_placement, residual_figure_placement, subfigure_options_rigid_body, 

1556 subfigure_labels_rigid_body, subfigure_captions_rigid_body, complex_plane_subfigure_options, complex_plane_subfigure_labels, 

1557 max_subfigures_per_page_rigid_body, max_subfigures_first_page_rigid_body, rigid_body_figure_save_names, 

1558 complex_plane_figure_save_names, residual_figure_save_names, latex_root, figure_root, 

1559 animation_style_rigid_body, shape_animation_frames, shape_animation_frame_rate, plot_shape_kwargs, 

1560 rigid_body_check_kwargs, 

1561 None if one_file else os.path.join(latex_root,'rigid_body.tex') if include_name_rigid_body is None else include_name_rigid_body 

1562 ) 

1563 

1564 if one_file: 

1565 all_strings.append(rigid_body_string) 

1566 

1567 print('Creating Data Quality Summary') 

1568 data_quality_string = create_data_quality_summary( 

1569 reference_autospectra_figure, drive_point_frfs_figure, reciprocal_frfs_figure, frf_coherence_figure, coherence_figure, 

1570 reference_autospectra_figure_label, reference_autospectra_figure_caption, reference_autospectra_graphics_options, 

1571 reference_autospectra_figure_placement, reference_autospectra_subfigure_options, reference_autospectra_subfigure_labels, 

1572 reference_autospectra_subfigure_captions, drive_point_frfs_figure_label, drive_point_frfs_figure_caption, 

1573 drive_point_frfs_graphics_options, drive_point_frfs_figure_placement, drive_point_frfs_subfigure_options, 

1574 drive_point_frfs_subfigure_labels, drive_point_frfs_subfigure_captions, reciprocal_frfs_figure_label, 

1575 reciprocal_frfs_figure_caption, reciprocal_frfs_graphics_options, reciprocal_frfs_figure_placement, 

1576 reciprocal_frfs_subfigure_options, reciprocal_frfs_subfigure_labels, reciprocal_frfs_subfigure_captions, 

1577 frf_coherence_figure_label, frf_coherence_figure_caption, frf_coherence_graphics_options, frf_coherence_figure_placement, 

1578 frf_coherence_subfigure_options, frf_coherence_subfigure_labels, frf_coherence_subfigure_captions, coherence_figure_label, 

1579 coherence_figure_caption, coherence_graphics_options, coherence_figure_placement, coherence_subfigure_options, 

1580 coherence_subfigure_labels, coherence_subfigure_captions, max_subfigures_per_page, max_subfigures_first_page, 

1581 latex_root, figure_root, 

1582 None if one_file else os.path.join(latex_root,'data_quality.tex') if include_name_data_quality is None else include_name_data_quality, 

1583 reference_autospectra_figure_save_names, 

1584 drive_point_frfs_figure_save_names, reciprocal_frfs_figure_save_names, frf_coherence_figure_save_names, coherence_figure_save_names 

1585 ) 

1586 

1587 if one_file: 

1588 all_strings.append(data_quality_string) 

1589 

1590 if 'mac' in self.documentation_figures: 

1591 mac_figure = self.documentation_figures['mac'] 

1592 else: 

1593 mac_figure = None 

1594 

1595 print('Creating Mode Fitting Summary') 

1596 mode_fitting_string = create_mode_fitting_summary( 

1597 self.fit_modes_information, self.fit_modes, fit_modes_table, fit_mode_table_kwargs, mac_figure, mac_plot_kwargs, 

1598 self.frfs, self.resynthesized_frfs, resynthesis_comparison, resynthesis_figure, resynthesis_plot_kwargs, 

1599 latex_root, figure_root, fit_mode_information_save_names, mac_plot_save_name, 

1600 resynthesis_plot_save_name, 

1601 None if one_file else os.path.join(latex_root,'mode_fitting.tex') if include_name_mode_fitting is None else include_name_mode_fitting, 

1602 fit_modes_information_table_justification_string, 

1603 fit_modes_information_table_longtable, fit_modes_information_table_header, fit_modes_information_table_horizontal_lines, 

1604 fit_modes_information_table_placement, fit_modes_information_figure_graphics_options, fit_modes_information_figure_placement, 

1605 fit_modes_table_justification_string, fit_modes_table_label, fit_modes_table_caption, fit_modes_table_longtable, 

1606 fit_modes_table_header, fit_modes_table_horizontal_lines, fit_modes_table_placement, fit_modes_table_header_override, 

1607 mac_plot_figure_label, mac_plot_figure_caption, mac_plot_graphics_options, mac_plot_figure_placement, 

1608 resynthesis_plot_figure_label, resynthesis_plot_figure_caption, resynthesis_plot_graphics_options, resynthesis_plot_figure_placement 

1609 ) 

1610 

1611 if one_file: 

1612 all_strings.append(mode_fitting_string) 

1613 

1614 if animation_style_mode_shape is None: 

1615 animation_style_mode_shape = global_animation_style 

1616 

1617 if 'mode_shapes' in self.documentation_figures and (animation_style_mode_shape is None or animation_style_mode_shape.lower() not in ['3d','one3d']): 

1618 shapes = self.documentation_figures['mode_shapes'] 

1619 else: 

1620 shapes = self.fit_modes 

1621 

1622 print('Creating Mode Shape Figure') 

1623 mode_shape_string = create_mode_shape_figures( 

1624 self.geometry, shapes, figure_label_mode_shape, figure_caption_mode_shape, graphics_options_mode_shape, 

1625 animate_graphics_options_mode_shape, figure_placement_mode_shape, subfigure_options_mode_shape, subfigure_labels_mode_shape, 

1626 subfigure_captions_mode_shape, max_subfigures_per_page_mode_shape, max_subfigures_first_page_mode_shape, 

1627 mode_shape_save_names, latex_root, figure_root, animation_style_mode_shape, 

1628 shape_animation_frames, shape_animation_frame_rate, plot_shape_kwargs, 

1629 None if one_file else os.path.join(latex_root,'mode_shape.tex') if include_name_mode_shape is None else include_name_mode_shape 

1630 ) 

1631 

1632 if one_file: 

1633 all_strings.append(mode_shape_string) 

1634 

1635 if self.channel_table is not None: 

1636 print('Creating the Channel Table') 

1637 channel_table_string = latex_table(self.channel_table,table_label = 'tab:channel_table', 

1638 table_caption = 'Channel Table', longtable=True, header=True) 

1639 if not one_file: 

1640 with open(os.path.join(latex_root,'channel_table.tex') if include_name_channel_table is None else include_name_channel_table,'w') as f: 

1641 f.write(channel_table_string) 

1642 else: 

1643 all_strings.append(channel_table_string) 

1644 

1645 if one_file: 

1646 final_string = '\n\n\n'.join(all_strings) 

1647 if one_file is True: 

1648 with open(os.path.join(latex_root,'document.tex'),'w') as f: 

1649 f.write(final_string) 

1650 elif isinstance(one_file,str): 

1651 with open(one_file,'w') as f: 

1652 f.write(final_string) 

1653 return final_string 

1654 

1655 def create_documentation_word(self): 

1656 raise NotImplementedError('Not Implemented Yet') 

1657 

1658 def create_documentation_pptx(self): 

1659 raise NotImplementedError('Not Implemented Yet')