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
« 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.
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.
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.
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"""
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
49def read_modal_fit_data(modal_fit_data):
50 """
51 Reads Modal Fit Data from PolyPy_GUI which contains modes and FRF data
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.
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.
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
79class ModalTest:
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 = {}
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)
144 environment = input_file.groups[input_file['environment_names'][0]]
146 time_histories, frfs, coherence, channel_table = read_modal_data(input_file)
147 time_histories = data_join(time_histories)
149 rename_dict = {key:key.replace('_',' ').title() for key in channel_table.columns}
150 channel_table = channel_table.rename(columns=rename_dict)
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)
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)
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))
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]
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]
185 out.set_units(response_units, reference_units)
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.'}
217 return out
219 def set_rigid_body_shapes(self,rigid_body_shapes):
220 self.rigid_body_shapes = rigid_body_shapes
222 def set_units(self,response_unit, reference_unit):
223 self.response_unit = response_unit
224 self.reference_unit = reference_unit
226 def set_geometry(self, geometry):
227 self.geometry = geometry
229 def set_time_histories(self, time_histories):
230 self.time_histories = time_histories
231 self.sample_rate = 1/time_histories.abscissa
233 def set_autopower_spectra(self, autopower_spectra):
234 self.autopower_spectra = autopower_spectra
236 def set_frfs(self, frfs):
237 self.frfs = frfs
239 def set_coherence(self, coherence):
240 self.coherence = coherence
242 def set_fit_modes(self, fit_modes):
243 self.fit_modes = fit_modes
245 def set_resynthesized_frfs(self, resynthesized_frfs):
246 self.resynthesized_frfs = resynthesized_frfs
248 def set_channel_table(self,channel_table):
249 self.channel_table = channel_table
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
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
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
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
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]
312 responses = time_data[self.response_indices]
314 self.num_averages = int(time_data.num_elements - (1-overlap)*num_samples_per_frame)//num_samples_per_frame + 1
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)
321 self.autopower_spectra = PowerSpectralDensityArray.from_time_data(
322 time_data, num_samples_per_frame, overlap, window, only_asds = True)
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)
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
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)
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
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())))
416 def fit_modes_SMAC(self):
417 raise NotImplementedError('SMAC has not been implemented yet')
419 def retrieve_modes_SMAC(self):
420 raise NotImplementedError('SMAC has not been implemented yet')
422 def fit_modes_opoly(self):
423 raise NotImplementedError('Opoly has not been implemented yet')
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 = {}
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
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 ]
456 opoly_settings = {category:{} for category in categories}
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
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:]
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
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)')
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'])]}
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()))
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)
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]
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
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
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
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
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
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
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 {}.
745 Raises
746 ------
747 ValueError
748 Raised if a required data has not been computed or assigned yet.
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.
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
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
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 {}.
831 Raises
832 ------
833 ValueError
834 Raised if a required data has not been computed or assigned yet.
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.
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
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
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 {}.
903 Raises
904 ------
905 ValueError
906 Raised if a required data has not been computed or assigned yet.
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.
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
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
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 {}.
975 Raises
976 ------
977 ValueError
978 Raised if a required data has not been computed or assigned yet.
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.
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
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
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 {}.
1042 Raises
1043 ------
1044 ValueError
1045 Raised if a required data has not been computed or assigned yet.
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.
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
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
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
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)
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
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 ):
1422 if one_file:
1423 all_strings = []
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')
1431 Path(figure_root).mkdir(parents=True,exist_ok=True)
1432 Path(latex_root).mkdir(parents=True,exist_ok=True)
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
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
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 )
1465 if one_file:
1466 all_strings.append(geometry_string)
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
1489 if animation_style_rigid_body is None:
1490 animation_style_rigid_body = global_animation_style
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
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
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)
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 )
1564 if one_file:
1565 all_strings.append(rigid_body_string)
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 )
1587 if one_file:
1588 all_strings.append(data_quality_string)
1590 if 'mac' in self.documentation_figures:
1591 mac_figure = self.documentation_figures['mac']
1592 else:
1593 mac_figure = None
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 )
1611 if one_file:
1612 all_strings.append(mode_fitting_string)
1614 if animation_style_mode_shape is None:
1615 animation_style_mode_shape = global_animation_style
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
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 )
1632 if one_file:
1633 all_strings.append(mode_shape_string)
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)
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
1655 def create_documentation_word(self):
1656 raise NotImplementedError('Not Implemented Yet')
1658 def create_documentation_pptx(self):
1659 raise NotImplementedError('Not Implemented Yet')