diff --git a/pyNastran/f06/flutter_response.py b/pyNastran/f06/flutter_response.py index cfa6fa23b..4c92b20d3 100644 --- a/pyNastran/f06/flutter_response.py +++ b/pyNastran/f06/flutter_response.py @@ -251,9 +251,14 @@ def __init__(self, subcase: int, configuration: str, """ if eigr_eigi_velocity is None: - eigr_eigi_velocity = np.array((0,3), dtype='float64') + eigr_eigi_velocity = np.zeros((0,3), dtype='float64') if eigenvector is None: eigenvector = np.array([], dtype='complex128') + else: + assert eigenvector.ndim == 2, eigenvector.shape + + assert eigr_eigi_velocity.ndim == 2, eigr_eigi_velocity + assert eigr_eigi_velocity.shape[1] == 3, eigr_eigi_velocity self.eigenvector = eigenvector self.eigr_eigi_velocity = eigr_eigi_velocity @@ -650,6 +655,138 @@ def plot_root_locus(self, modes=None, png_filename=png_filename, **legend_kwargs) + def plot_modal_participation(self, modes=None, + fig=None, axes=None, + #eigr_lim=None, eigi_lim=None, + ncol: int=0, + show: bool=True, clear: bool=False, + close: bool=False, legend: bool=True, + #noline: bool=False, nopoints: bool=False, + freq_tol: float=-1.0, + png_filename=None, + **legend_kwargs): + """ + Plots a root locus + + Parameters + ---------- + modes : list[int] / int ndarray; (default=None -> all) + the modes; typically 1 to N + fig : plt.Figure + the figure object + axes : plt.Axes + the axes object + eigr_lim : list[float/None, float/None] + the x plot limits + eigi_lim : list[float/None, float/None] + the y plot limits + show : bool; default=True + show the plot + clear : bool; default=False + clear the plot + legend : bool; default=True + show the legend + kwargs : dict; default=None + key : various matplotlib parameters + value : depends + + Legend kwargs + ------------- + loc : str + 'best' + fancybox : bool; default=False + makes the box look cool + framealpha : float; 0.0 <= alpha <= 1.0 + 1.0 - no transparency / opaque + 0.0 - fully transparent + + """ + modes, imodes = _get_modes_imodes(self.modes, modes) + legend_kwargs = get_legend_kwargs(legend_kwargs) + if fig is None: + fig = plt.figure() + axes = fig.add_subplot(111) + + xlabel = r'Eigenvalue (Real); $\omega \zeta$' + ylabel = r'Eigenvalue (Imaginary); $\omega$' + # ix = self.ieigr + # iy = self.ieigi + # scatter = True + + ivel = 0 + imode = 1 + #print(f'eigr_eigi_velocity.shape: {self.eigr_eigi_velocity.shape}') + #print(f'eigenvector.shape: {self.eigenvector.shape}') + #print(f'eigr_eigi_velocity:\n{self.eigr_eigi_velocity}') + + # MSC + #eigr_eigi_velocity: + #[[-9.88553e-02 1.71977e+01 1.52383e+02] + # [-1.71903e-01 6.60547e+01 1.52383e+02]] + eigr_eigi_velocity = self.eigr_eigi_velocity[ivel, :] + eigri, eigii, velocityi = eigr_eigi_velocity + + omega_damping = eigri + omega = eigii + + #xlabel = r'Eigenvalue (Real); $\omega \gamma$' + #xlabel = r'Eigenvalue (Imaginary); $\omega$' + freq = omega / (2 * np.pi) + damping_g = 2 * omega_damping / omega + title = f'Modal Participation Factors of Mode {imode+1}\n' + title += f'omega={omega:.2f}; freq={freq:.2f} Hz; g={damping_g:.4g}' + if np.isfinite(velocityi): + title += f' V={velocityi:.1f}' + #print(title) + axes.set_title(title) + axes.grid(True) + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + + #print(f'eigr_eigi_velocity:\n{self.eigr_eigi_velocity}') + #print(f'eigenvector:\n{self.eigenvector}') + + eig = self.eigenvector[imodes, :] + eigr = eig.real + eigi = eig.imag + # abs_eigr = np.linalg.norm(eigr) + # abs_eigi = np.linalg.norm(eigi) + # if abs_eigr == 0.0: + # abs_eigr = 1.0 + # if abs_eigi == 0.0: + # abs_eigi = 1.0 + + # don't normalize + abs_eigr = 1.0 + abs_eigi = 1.0 + + reals = eigr / abs_eigr + imags = eigi / abs_eigi + for i, imodei, mode in zip(count(), imodes, modes): + #real = self.eigenvector[imode1, :].real, label=f'iMode1={imode1+1}') + reali = reals[i, imode] + imagi = imags[i, imode] + text = str(mode) + axes.scatter(reali, imagi, label=f'Mode {mode}') + #print(f'{i}: {reali}, {imagi}, {text!r}') + axes.text(reali, imagi, text, ha='center', va='center') + + if legend: + # bbox_to_anchor=(1.125, 1.), ncol=ncol, + axes.legend(**legend_kwargs) + + if show: + plt.show() + if png_filename: + plt.savefig(png_filename) + if clear: + fig.clear() + if close: + plt.close() + # for imode1 in range(nmodes1): + # ax11.plot(self.eigenvector[imode1, :].real, label=f'iMode1={imode1+1}') + # ax12.plot(self.eigenvector[imode1, :].imag, label=f'iMode1={imode1+1}') + def _plot_x_y(self, ix: int, iy: int, xlabel: str, ylabel: str, scatter: bool, @@ -1191,7 +1328,7 @@ def export_to_csv(self, csv_filename: PathLike, ' 1, 0.0000E+00, 0.0000E+00, 0.0000E+00, 0.0000E+00' ' 2, 4.9374E-01, -1.6398E-03, -5.4768E-04, -2.3136E-04' """ - imodes = self._imodes(modes) + imodes = _imodes(self.results.shape, modes) assert len(imodes) > 0, imodes headers = ['ipoint'] for name in self.names: @@ -1240,7 +1377,7 @@ def export_to_veas(self, veas_filename: PathLike, ' 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 1.4277E+01 7.7705E+01 8.8016E+01 2.2346E+02 2.4656E+02 3.2366E+02 4.5518E+02 4.9613E+02 6.7245E+02 7.3197E+02 8.0646E+02 0.0000E+00' ' 4.9374E-01 -1.6398E-03 -5.4768E-04 -2.3136E-04 -3.5776E-04 -6.2358E-05 -3.0348E-04 -7.7492E-05 -2.0301E-05 -1.7733E-04 -9.2383E-05 -1.2849E-05 -2.8854E-05 4.9374E-01 1.4272E+01 7.7726E+01 8.8010E+01 2.2347E+02 2.4655E+02 3.2364E+02 4.5516E+02 4.9612E+02 6.7245E+02 7.3195E+02 8.0646E+02 0.0000E+00' """ - modes, nmodes = self._modes_nmodes(modes) + modes, nmodes = _modes_nmodes(self.results.shape, modes) damping_modes = [] omega_modes = [] @@ -1266,32 +1403,6 @@ def export_to_veas(self, veas_filename: PathLike, str_values = (' %11.4E' % value for value in values) veas_file.write(''.join(str_values) + '\n') - def _imodes(self, modes: Optional[np.ndarray, slice[int] | - tuple[int] | list[int]]) -> np.ndarray: - """gets the imodes from the modes""" - if modes is None: - nmodes = self.results.shape[0] - imodes = range(nmodes) - elif isinstance(modes, slice): - nmodes = self.results.shape[0] - all_modes = np.arange(0, nmodes, dtype='int32') - imodes = all_modes[modes] - elif isinstance(modes, (list, tuple)): - modes = np.array(modes, dtype='int32') - imodes = modes - 1 - else: - imodes = modes - 1 - return imodes - - def _modes_nmodes(self, modes: Optional[Iterable[int]]) -> tuple[Iterable[int], int]: - """gets the modes and nmodes""" - if modes is None: - nmodes = self.results.shape[0] - modes = range(1, nmodes + 1) - else: - nmodes = max(modes) - return modes, nmodes - def export_to_f06(self, f06_filename: PathLike, modes: Optional[list[int]]=None, page_stamp: Optional[str]=None, @@ -1307,7 +1418,7 @@ def export_to_f06_file(self, f06_file: str, modes: Optional[list[int]]=None, page_stamp: Optional[str]=None, page_num: int=1) -> int: - imodes = self._imodes(modes) + imodes = _imodes(self.results.shape, modes) if page_stamp is None: page_stamp = 'PAGE %i' for imode in imodes: @@ -1355,7 +1466,7 @@ def export_to_zona(self, zona_filename: PathLike, modes, imodes = _get_modes_imodes(self.modes, modes) #unused_legend_items = ['Mode %i' % mode for mode in modes] - ix, unused_xlabel = self._plot_type_to_ix_xlabel(plot_type) + ix, unused_xlabel, xunit = self._plot_type_to_ix_xlabel(plot_type) # these are the required damping levels to plot msg = '' @@ -1532,7 +1643,38 @@ def object_methods(self, mode: str='public', keys_to_skip=None): return object_methods(self, mode=mode, keys_to_skip=keys_to_skip) -def _get_modes_imodes(all_modes, modes): +def _imodes(results_shape: tuple[int, int], + modes: Optional[np.ndarray, slice[int] | + tuple[int] | list[int]]) -> np.ndarray: + """gets the imodes from the modes""" + if modes is None: + nmodes = results_shape[0] + imodes = range(nmodes) + elif isinstance(modes, slice): + nmodes = results_shape[0] + all_modes = np.arange(0, nmodes, dtype='int32') + imodes = all_modes[modes] + elif isinstance(modes, (list, tuple)): + modes = np.array(modes, dtype='int32') + imodes = modes - 1 + else: + imodes = modes - 1 + return imodes + + +def _modes_nmodes(results_shape: tuple[int, int], + modes: Optional[Iterable[int]]) -> tuple[Iterable[int], int]: + """gets the modes and nmodes""" + if modes is None: + nmodes = results_shape[0] + modes = range(1, nmodes + 1) + else: + nmodes = max(modes) + return modes, nmodes + + +def _get_modes_imodes(all_modes: np.ndaray, + modes: Optional[slice | np.ndarray]): """gets the index of the modes to plot""" if modes is None: modes = all_modes diff --git a/pyNastran/f06/gui_flutter.py b/pyNastran/f06/gui_flutter.py index 3898a5002..ef1970eb5 100644 --- a/pyNastran/f06/gui_flutter.py +++ b/pyNastran/f06/gui_flutter.py @@ -47,7 +47,7 @@ from pyNastran.op2.op2 import OP2 X_PLOT_TYPES = ['eas', 'tas', 'rho', 'q', 'mach', 'alt', 'kfreq', 'ikfreq'] -PLOT_TYPES = ['x-damp-freq', 'x-damp-kfreq', 'root-locus'] +PLOT_TYPES = ['x-damp-freq', 'x-damp-kfreq', 'root-locus', 'modal-participation'] UNITS_IN = ['english_in', 'english_kt', 'english_ft', 'si', 'si_mm'] UNITS_OUT = UNITS_IN @@ -487,6 +487,7 @@ def setup_widgets(self) -> None: self.units_in_pulldown.setToolTip(units_msg) iunits_in = UNITS_IN.index('english_in') self.units_in_pulldown.setCurrentIndex(iunits_in) + self.units_in_pulldown.setToolTip('Sets the units for the F06/OP2; set when loaded') self.units_out_label = QLabel('Units Out:') self.units_out_pulldown = QComboBox() @@ -494,6 +495,7 @@ def setup_widgets(self) -> None: self.units_out_pulldown.setToolTip(units_msg) iunits_out = UNITS_IN.index('english_kt') self.units_out_pulldown.setCurrentIndex(iunits_out) + self.units_out_pulldown.setToolTip('Sets the units for the plot; may be updated') self.output_directory_label = QLabel('Output Directory:') self.output_directory_edit = QLineEdit('', self) @@ -503,11 +505,15 @@ def setup_widgets(self) -> None: self.VL_label = QLabel('VL, Limit:') self.VL_edit = QFloatEdit('') + self.VL_edit.setToolTip('Makes a vertical line for VL') + self.VF_label = QLabel('VF, Flutter:') self.VF_edit = QFloatEdit('') + self.VF_edit.setToolTip('Makes a vertical line for VF') + self.damping_label = QLabel('Damping, g:') self.damping_edit = QFloatEdit('') - + self.damping_edit.setToolTip('Enables the flutter crossing (e.g., 0.03 for 3%)') self.f06_load_button = QPushButton('Load F06', self) self.ok_button = QPushButton('Run', self) @@ -536,6 +542,7 @@ def on_plot_type(self) -> None: show_freq = False show_damp = False show_root_locus = False + show_modal_participation = False #PLOT_TYPES = ['x-damp-freq', 'x-damp-kfreq', 'root-locus'] assert plot_type in PLOT_TYPES, plot_type @@ -558,6 +565,9 @@ def on_plot_type(self) -> None: elif plot_type == 'root-locus': show_root_locus = True show_kfreq = False + elif plot_type == 'modal-participation': + show_modal_participation = True + show_kfreq = False else: # pragma: no cover raise RuntimeError(f'plot_type={plot_type!r}') @@ -582,8 +592,10 @@ def on_plot_type(self) -> None: show_xlim = False #assert show_xlim is False, show_xlim - self.x_plot_type_label.setVisible(not show_root_locus) - self.x_plot_type_pulldown.setVisible(not show_root_locus) + show_eigenvalue = show_root_locus or show_modal_participation + show_xaxis = not show_eigenvalue + self.x_plot_type_label.setVisible(show_xaxis) + self.x_plot_type_pulldown.setVisible(show_xaxis) self.eas_lim_label.setVisible(show_eas_lim) self.eas_lim_edit_min.setVisible(show_eas_lim) @@ -1068,7 +1080,7 @@ def plot(self, modes: list[int]) -> None: fig = plt.figure(1) fig.clear() - if plot_type != 'root-locus': + if plot_type not in {'root-locus', 'modal-participation'}: gridspeci = gridspec.GridSpec(2, 4) damp_axes = fig.add_subplot(gridspeci[0, :3]) freq_axes = fig.add_subplot(gridspeci[1, :3], sharex=damp_axes) @@ -1101,6 +1113,17 @@ def plot(self, modes: list[int]) -> None: legend=True, png_filename=png_filename, ) + if plot_type == 'modal-participation': + png_filename = base + '_modal-participation.png' + axes = fig.add_subplot(111) + response.plot_modal_participation( + fig=fig, axes=axes, + modes=modes, #eigr_lim=self.eigr_lim, eigi_lim=self.eigi_lim, + freq_tol=freq_tol, + show=True, clear=False, close=False, + legend=True, + png_filename=png_filename, + ) elif plot_type == 'x-damp-kfreq': #xlabel: eas #ylabel1 = r'Structural Damping; $g = 2 \gamma $' @@ -1247,7 +1270,7 @@ def get_xlim(self) -> tuple[Limit, Limit, Limit, Limit, ] is_passed = all(is_passed_flags) # if not is_passed: - # self.log.warning(f'is_passed_flags = {is_passed_flags}') + #self.log.warning(f'is_passed_flags = {is_passed_flags}') #print(f'freq_tol = {freq_tol}') out = ( eas_lim, tas_lim, mach_lim, alt_lim, q_lim, rho_lim, xlim, @@ -1308,6 +1331,21 @@ def validate(self) -> bool: export_to_f06 = self.export_f06_checkbox.isChecked() export_to_zona = self.export_zona_checkbox.isChecked() + subcases = list(self.responses) + subcase0 = subcases[0] + response = self.responses[subcase0] + + failed_modal_partipation = ( + (self.plot_type == 'modal-participation') and + ((response.eigr_eigi_velocity is None) or + (response.eigenvector is None)) + ) + is_passed_modal_partipation = not failed_modal_partipation + # ( + # (self.plot_type == 'modal-participation') and + # (response.eigr_eigi_velocity is not None) + # ) or (self.plot_type != 'modal-participation')) + data = { 'log_scale_x': self.log_xscale_checkbox.isChecked(), 'log_scale_y1': self.log_yscale1_checkbox.isChecked(), @@ -1350,7 +1388,8 @@ def validate(self) -> bool: } self.units_in = units_in self.units_out = units_out - is_passed = all([is_valid_xlim, is_subcase_valid]) + is_passed = all([is_valid_xlim, is_subcase_valid, is_passed_modal_partipation]) + #self.log.warning(f'is_passed_modal_partipation = {is_passed_modal_partipation}') if is_passed: self.data = data #self.xlim = xlim @@ -1365,7 +1404,7 @@ def validate(self) -> bool: return is_passed def on_open_new_window(self): - #return + return try: from pyNastran.f06.gui_flutter_vtk import VtkWindow except ImportError as e: @@ -1519,7 +1558,7 @@ def load_f06_op2(f06_filename: str, log: SimpleLogger, log=log) except Exception as e: log.error(str(e)) - raise + #raise return model, responses elif ext == '.op2': try: diff --git a/pyNastran/f06/parse_flutter.py b/pyNastran/f06/parse_flutter.py index affb2e861..f4e0ff852 100644 --- a/pyNastran/f06/parse_flutter.py +++ b/pyNastran/f06/parse_flutter.py @@ -249,6 +249,7 @@ def make_flutter_response(f06_filename: PathLike, if len(eigenvectors): eigr_eigi_velocity = np.array(eigr_eigi_velocity_list, dtype='float64') # eigr, eigi, velo + assert eigr_eigi_velocity.ndim == 2, eigr_eigi_velocity eigenvectors_array = np.column_stack(eigenvectors) eigenvectors = [] eigr_eigi_velocity_list = [] @@ -599,6 +600,14 @@ def _make_flutter_subcase_plot(modes, flutter: FlutterResponse, subcase: int, clear=clear, legend=True, png_filename=filenamei, show=False, close=close) + if flutter.eigr_eigi_velocity is not None: + flutter.plot_modal_participation(modes=modes, + fig=None, axes=None, + freq_tol=-1.0, + ncol=ncol, + clear=clear, legend=True, + png_filename=None, + show=False, close=close) if plot_kfreq_damping: filenamei = None if kfreq_damping_filename is None else (kfreq_damping_filename % subcase)