From bcd2da8c22edb3b16793c7c12347c503f406785d Mon Sep 17 00:00:00 2001 From: SteveDoyle2 Date: Tue, 3 Dec 2024 23:51:18 -0800 Subject: [PATCH] simplifying units handling of FlutterResponse by using a convert_units function and not changing the baseline --- pyNastran/f06/flutter_response.py | 143 +++++++----------- pyNastran/f06/gui_flutter.py | 59 +++++--- pyNastran/f06/parse_flutter.py | 6 +- pyNastran/op2/op2_interface/op2_f06_common.py | 12 +- pyNastran/op2/op2_interface/op2_reader.py | 21 +-- pyNastran/op2/result_objects/op2_results.py | 18 ++- 6 files changed, 132 insertions(+), 127 deletions(-) diff --git a/pyNastran/f06/flutter_response.py b/pyNastran/f06/flutter_response.py index 36e16dd8b..894ed0b4a 100644 --- a/pyNastran/f06/flutter_response.py +++ b/pyNastran/f06/flutter_response.py @@ -35,7 +35,7 @@ def __repr__(self) -> str: #from pyNastran.utils import object_stats #print(object_stats(self)) #configuration : 'AEROSG2D' - #f06_units : {'velocity': 'm/s', 'density': 'kg/m^3', 'altitude': 'm', 'dynamic_pressure': 'Pa', 'eas': 'm/s'} + #in_units : {'velocity': 'm/s', 'density': 'kg/m^3', 'altitude': 'm', 'dynamic_pressure': 'Pa', 'eas': 'm/s'} #ialt : 11 #idamping : 5 #idensity : 2 @@ -62,7 +62,7 @@ def __repr__(self) -> str: 'FlutterResponse:\n' f'subcase= {self.subcase:d}\n' f'{xyz_sym}' - f'f06_units = {self.f06_units}\n' + f'in_units = {self.in_units}\n' f'out_units = {self.out_units}\n' f'names = {self.names}; n={len(self.names)}\n\n' f'method = {self.method!r}\n' @@ -81,11 +81,14 @@ def __repr__(self) -> str: ) return msg + def get_stats(self) -> str: + return f'FlutterResponse(isubcase={self.subcase})' + @classmethod def from_nx(cls, method: str, fdata: np.ndarray, subcase_id: int=1, cref: float=1.0, is_xysym: bool=False, is_xzsym: bool=False, - f06_units: dict[str, str]=None): + in_units: dict[str, str]=None): """ Parameters ---------- @@ -102,8 +105,8 @@ def from_nx(cls, method: str, fdata: np.ndarray, is the model symmetric about the xz plane? """ - if f06_units is None: - f06_units = get_flutter_units('english_in') + if in_units is None: + in_units = get_flutter_units('english_in') b = cref / 2.0 configuration = 'AEROSG2D' # TODO: what is this? xysym = '???' @@ -147,27 +150,9 @@ def from_nx(cls, method: str, fdata: np.ndarray, results[:, :, [2, 3, 4, 5, 6]] = fdata[:, :, [0, 1, 2, 3, 4]] modes = np.arange(nmodes, dtype='int32') + 1 - #f06_units = get_flutter_units('english_in') - #{ - #'altitude': 'ft', - #'density': 'slinch/in^3', - #'velocity': 'in/s', - #'eas': 'in/s', - #'dynamic_pressure': 'psi', - #} - #out_units = get_flutter_units('english_kt') - #out_units = get_flutter_units('english_in') - out_units = deepcopy(f06_units) - #{ - #'altitude': 'ft', - #'density': 'slug/ft^3', - #'velocity': 'knots', - #'eas': 'knots', - #'dynamic_pressure': 'psf', - #} resp = FlutterResponse( subcase_id, configuration, xysym, xzsym, mach, density_ratio, - method, modes, results, f06_units=f06_units, out_units=out_units, + method, modes, results, in_units=in_units, make_alt=False) if 0: # pragma: no cover resp.plot_root_locus(modes=None, fig=None, axes=None, xlim=None, ylim=None, @@ -192,8 +177,7 @@ def __init__(self, subcase: int, configuration: str, xysym: str, xzsym: str, mach: float, density_ratio: float, method: str, modes: list[int], results: Any, - f06_units: None | str | dict[str, str]=None, - out_units: None | str | dict[str, str]=None, + in_units: None | str | dict[str, str]=None, make_alt: bool=True) -> None: """ Parameters @@ -218,13 +202,13 @@ def __init__(self, subcase: int, configuration: str, (e.g. units on Mach don't do anything). All allowable fields are shown below. - f06_units : dict[str] = str (default=None -> no units conversion) + in_units : dict[str] = str (default=None -> no units conversion) The units to read from the F06. PK method: - f06_units = {'velocity' : 'in/s'} + in_units = {'velocity' : 'in/s'} The velocity units are the units for the FLFACT card in the BDF PKNL method: - f06_units = {'velocity' : 'in/s', 'density' : 'slinch/in^3', 'altitude' : 'ft', 'dynamic_pressure': 'psi'} + in_units = {'velocity' : 'in/s', 'density' : 'slinch/in^3', 'altitude' : 'ft', 'dynamic_pressure': 'psi'} The velocity/density units are the units for the FLFACT card in the BDF out_units dict[str] = str (default=None -> no units conversion) @@ -252,18 +236,10 @@ def __init__(self, subcase: int, configuration: str, unused """ - self.f06_units = f06_units - self.out_units = out_units + self.in_units = in_units + self.out_units = '' self.make_alt = make_alt - required_keys = ['altitude', 'velocity', 'eas', 'density', 'dynamic_pressure'] - if f06_units is None and out_units is None: - pass - else: - for key in required_keys: - assert key in f06_units, 'key=%r not in f06_units=%s' % (key, f06_units) - assert key in out_units, 'key=%r not in out_units=%s' % (key, out_units) - for key in f06_units: - assert key in required_keys, 'key=%r not in required_keys=%s' % (key, required_keys) + in_units = get_flutter_units(in_units) self.subcase = subcase self.configuration = configuration @@ -307,8 +283,16 @@ def __init__(self, subcase: int, configuration: str, else: # pragma: no cover raise NotImplementedError(method) - self.results = results - self._set_data(self.f06_units, self.out_units) + #------------------------------------------- + in_units2 = get_flutter_units(in_units) + if self.method in ['PK', 'KE']: + pass + elif self.method == 'PKNL': + results = self._set_pknl_results(in_units2, results) + else: # pragma: no cover + raise NotImplementedError(self.method) + self.results_in = results + self.results = self.results_in # c - cyan # b - black @@ -337,46 +321,34 @@ def __init__(self, subcase: int, configuration: str, self._colors: list[str] = [] self.generate_symbols() - def _set_data(self, - in_units: Optional[str | dict[str, str]], - out_units: Optional[str | dict[str, str]]) -> None: - in_units2 = get_flutter_units(in_units) - out_units2 = get_flutter_units(out_units) - results = self.results - if self.method in ['PK', 'KE']: - kvel = _get_unit_factor(in_units2, out_units2, 'velocity')[0] - # (imode, istep, iresult) - results[:, :, self.ivelocity] *= kvel - elif self.method == 'PKNL': - results = self._set_pknl_results(in_units2, out_units2, results) - else: # pragma: no cover - raise NotImplementedError(self.method) - self.results = results + def set_out_units(self, out_units: str | dict[str, str]) -> None: + self.convert_units(out_units) def convert_units(self, out_units: Optional[str | dict[str, str]]) -> None: - - out_units2 = get_flutter_units(out_units) - kvel = _get_unit_factor(self.out_units, out_units2, 'velocity')[0] - keas = _get_unit_factor(self.out_units, out_units2, 'eas')[0] - kdensity = _get_unit_factor(self.out_units, out_units2, 'density')[0] - kpressure = _get_unit_factor(self.out_units, out_units2, 'dynamic_pressure')[0] - kalt = _get_unit_factor(self.out_units, out_units2, 'altitude')[0] - - self.results[:, :, self.ivelocity] *= kvel + results = self.results_in.copy() + out_units_dict = get_flutter_units(out_units) + kvel = _get_unit_factor(self.in_units, out_units_dict, 'velocity')[0] + keas = _get_unit_factor(self.in_units, out_units_dict, 'eas')[0] + kdensity = _get_unit_factor(self.in_units, out_units_dict, 'density')[0] + kpressure = _get_unit_factor(self.in_units, out_units_dict, 'dynamic_pressure')[0] + kalt = _get_unit_factor(self.in_units, out_units_dict, 'altitude')[0] + + # (imode, istep, iresult) + results[:, :, self.ivelocity] *= kvel if self.method in ['PK', 'KE']: pass elif self.method == 'PKNL': - self.results[:, :, self.idensity] *= kdensity - self.results[:, :, self.iq] *= kpressure - self.results[:, :, self.ieas] *= keas - self.results[:, :, self.ialt] *= kalt + results[:, :, self.idensity] *= kdensity + results[:, :, self.iq] *= kpressure + results[:, :, self.ieas] *= keas + results[:, :, self.ialt] *= kalt else: # pragma: no cover raise NotImplementedError(self.method) - self.out_units = out_units2 + self.out_units = out_units_dict + self.results = results def _set_pknl_results(self, in_units: dict[str, str], - out_units: dict[str, str], results: np.ndarray) -> np.ndarray: assert results.shape[2] in [9, 11, 12], results.shape density_units_in = in_units['density'] @@ -391,29 +363,22 @@ def _set_pknl_results(self, rho_ref = atm_density(0., R=1716., alt_units='ft', density_units=density_units_in) - rho_units_in = in_units['density'] vel_units_in = in_units['velocity'] q_units_in = in_units['dynamic_pressure'] - if _is_q_units_consistent(rho_units_in, vel_units_in, q_units_in): + if _is_q_units_consistent(density_units_in, vel_units_in, q_units_in): q = 0.5 * rho * vel**2 else: # pragma: no cover - raise NotImplementedError((rho_units_in, vel_units_in, q_units_in)) + raise NotImplementedError((density_units_in, vel_units_in, q_units_in)) #eas = (2 * q / rho_ref)**0.5 # eas = V * sqrt(rho / rhoSL) - keas = _get_unit_factor(in_units, out_units, 'eas')[0] - eas = vel * np.sqrt(rho / rho_ref) * keas - #density_units2 = self.out_units['density'] + eas = vel * np.sqrt(rho / rho_ref) - altitude_units = out_units['altitude'] + altitude_units = in_units['altitude'] #print('density_units_in=%r density_units2=%r' % (density_units_in, density_units2)) kdensityi = convert_density(1., density_units_in, 'slug/ft^3') - kvel = _get_unit_factor(in_units, out_units, 'velocity')[0] - kdensity = _get_unit_factor(in_units, out_units, 'density')[0] - kpressure = _get_unit_factor(in_units, out_units, 'dynamic_pressure')[0] - vel *= kvel resultsi = results[:, :, :9] assert resultsi.shape[2] == 9, resultsi.shape @@ -427,11 +392,8 @@ def _set_pknl_results(self, else: alt = np.full(vel.shape, np.nan, dtype=vel.dtype) - rho *= kdensity - results2 = np.dstack([resultsi, eas, q * kpressure, alt]) - - results2[:, :, self.idensity] = rho - results2[:, :, self.ivelocity] = vel + results2 = np.dstack([resultsi, eas, q, alt]) + #results2[:, :, self.idensity] = rho return results2 def generate_symbols(self, colors=None, symbols=None, imethod: int=0): @@ -1304,7 +1266,7 @@ def _plot_type_to_ix_xlabel(self, plot_type: str) -> tuple[int, str]: elif plot_type == 'freq': ix = self.ifreq xlabel = 'Frequency [Hz]' - elif plot_type in ['1/kfreq', 'ikfreq']: + elif plot_type in ['1/kfreq', 'ikfreq', 'inv_kfreq', 'kfreq_inv']: ix = self.ikfreq_inv xlabel = r'1/KFreq [1/rad]; $2V / (\omega c) $' elif plot_type == 'eigr': @@ -1539,6 +1501,9 @@ def get_flutter_units(units: Optional[str | dict[str, str]]) -> dict[str, str]: 'english_in, english_ft, english_kt]') else: # pragma: no cover assert isinstance(units, dict), f'units={units!r}' + required_keys = ['altitude', 'velocity', 'eas', 'density', 'dynamic_pressure'] + for key in required_keys: + assert key in units, 'key=%r not in units=%s' % (key, units) units_dict = units return units_dict diff --git a/pyNastran/f06/gui_flutter.py b/pyNastran/f06/gui_flutter.py index 4bb8d3e66..75dacb694 100644 --- a/pyNastran/f06/gui_flutter.py +++ b/pyNastran/f06/gui_flutter.py @@ -31,7 +31,6 @@ QLINEEDIT_RED = 'QLineEdit {background-color: red;}' from cpylog import SimpleLogger -#import pyNastran from pyNastran.gui.utils.qt.pydialog import QFloatEdit, make_font from pyNastran.gui.qt_files.named_dock_widget import NamedDockWidget from pyNastran.gui.qt_files.loggable_gui import LoggableGui @@ -355,12 +354,8 @@ def setup_widgets(self) -> None: self.font_size_edit.setValue(self.font_size) self.f06_filename_label = QLabel('F06 Filename:') - self.f06_filename_edit = QLineEdit('') - # self.f06_filename_edit.setText(F06_FILENAME) + self.f06_filename_edit = QLineEdit() self.f06_filename_browse = QPushButton('Browse...') - # self.f06_filename_edit.setDisabled(True) - #self.f06_filename_browse.setDisabled(True) - #self.f06_filename_browse.setVisible(False) self.show_points_checkbox = QCheckBox('Show Points') self.show_lines_checkbox = QCheckBox('Show Lines') @@ -567,12 +562,6 @@ def setup_layout(self) -> None: grid = QGridLayout() irow = 0 - - # grid.addWidget(self.f06_filename_label, irow, 0) - # grid.addWidget(self.f06_filename_edit, irow, 1) - # grid.addWidget(self.f06_filename_browse, irow, 2) - # irow += 1 - grid.addWidget(self.font_size_label, irow, 0) grid.addWidget(self.font_size_edit, irow, 1) irow += 1 @@ -750,14 +739,10 @@ def on_load_f06(self) -> None: self.f06_filename_edit.setStyleSheet(QLINEEDIT_WHITE) f06_units = self.units_in_pulldown.currentText() out_units = self.units_out_pulldown.currentText() - try: - self.responses: FlutterResponse = make_flutter_response( - f06_filename, - f06_units=f06_units, out_units=out_units, - log=self.log) - except Exception as e: - self.log.error(str(e)) - return + + self.responses = load_f06_op2( + f06_filename, self.log, + f06_units, out_units) subcases = list(self.responses.keys()) if len(subcases) == 0: @@ -1301,6 +1286,40 @@ def _to_str(value: Optional[int | float]) -> str: str_value = str(value) return str_value +def load_f06_op2(f06_filename: str, log: SimpleLogger, + f06_units: str, + out_units: str) -> list[FlutterResponse]: + responses = [] + ext = os.path.splitext(f06_filename)[1].lower() + if ext == '.f06': + try: + responses: FlutterResponse = make_flutter_response( + f06_filename, + f06_units=f06_units, out_units=out_units, + log=log) + except Exception as e: + log.error(str(e)) + return responses + elif ext == '.op2': + from pyNastran.op2.op2 import OP2 + model = OP2(log=log) + results_to_include = ['eigenvectors'] + model.set_results(results_to_include) + try: + model.read_op2(build_dataframe=False) + except Exception as e: + log.error(str(e)) + return responses + responses = model.op2_results.vg_vf_response + if len(responses) == 0: + log.error('Could not find OVG table in op2') + else: + log.error('Could not find OVG table in op2') + raise RuntimeError + + return responses + + def main(f06_filename: str='') -> None: # pragma: no cover # kills the program when you hit Cntl+C from the command line # doesn't save the current state as presumably there's been an error diff --git a/pyNastran/f06/parse_flutter.py b/pyNastran/f06/parse_flutter.py index 9ead9cf97..ddf9ea5e0 100644 --- a/pyNastran/f06/parse_flutter.py +++ b/pyNastran/f06/parse_flutter.py @@ -104,7 +104,8 @@ def make_flutter_response(f06_filename: PathLike, flutter = FlutterResponse(subcase, configuration, xysym, xzsym, mach, density_ratio, method, modes, results, - f06_units=f06_units, out_units=out_units) + in_units=f06_units) + flutter.set_out_units(out_units) #_remove_neutrinos(flutter, log) flutters[subcase] = flutter modes = [] @@ -258,7 +259,8 @@ def make_flutter_response(f06_filename: PathLike, flutter = FlutterResponse(subcase, configuration, xysym, xzsym, mach, density_ratio, method, modes, results, - f06_units=f06_units, out_units=out_units) + in_units=f06_units) + flutter.set_out_units(out_units) flutters[subcase] = flutter return flutters diff --git a/pyNastran/op2/op2_interface/op2_f06_common.py b/pyNastran/op2/op2_interface/op2_f06_common.py index eda3d470a..f265c1a23 100644 --- a/pyNastran/op2/op2_interface/op2_f06_common.py +++ b/pyNastran/op2/op2_interface/op2_f06_common.py @@ -1,6 +1,6 @@ from __future__ import annotations import getpass -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING from numpy import unique, int32, int64 from pyNastran import is_release @@ -1753,7 +1753,7 @@ def _get_table_types_testing(self) -> list[str]: #def get_f06_stats(self): #return self.get_op2_stats() - def get_op2_stats(self, short=False): + def get_op2_stats(self, short: bool=False) -> str: """ Gets info about the contents of the different attributes of the OP2 class. @@ -1801,7 +1801,7 @@ def __init__(self): OP2_F06_Common.__init__(self) -def _get_op2_stats(model: OP2, short=False): +def _get_op2_stats(model: OP2, short: bool=False) -> str: """see OP2.get_op2_stats(...)""" msg = [] #msg += model.op2_results.responses.get_stats(short=short) @@ -1938,7 +1938,11 @@ def _get_op2_stats_full(model: OP2, table_types: list[str], continue table_type_print = 'op2_results.' + table_type if '.' in table_type else table_type - if table_type == 'superelement_tables': + op2_result_tables = [ + #'vg_vf_response', + 'superelement_tables', + ] + if table_type in op2_result_tables: for key in table: msg.append(f'{table_type_print}[{key}]\n') continue diff --git a/pyNastran/op2/op2_interface/op2_reader.py b/pyNastran/op2/op2_interface/op2_reader.py index d663b2862..017e4a07d 100644 --- a/pyNastran/op2/op2_interface/op2_reader.py +++ b/pyNastran/op2/op2_interface/op2_reader.py @@ -151,7 +151,7 @@ def __init__(self, op2: OP2): b'DESTAB' : (partial(read_destab, self), 'creates op2_results.responses.desvars'), #b'MEFF' : self.read_meff, - b'OVG': (partial(read_ovg, self), 'aeroelastic velocity'), + b'OVG': (partial(read_ovg, self), 'aeroelastic velocity; creates op2_results.vg_vf_responses'), b'INTMOD' : (self.read_intmod, '???'), b'HISADD' : (partial(read_hisadd, self), 'optimization history; op2_results.responses.convergence_data'), @@ -4293,14 +4293,17 @@ def read_ovg(op2_reader: OP2Reader) -> None: cref = aero.cref is_xysym = aero.is_symmetric_xy is_xzsym = aero.is_symmetric_xz - op2.in_units = { - 'density': 'slinch/in^3', 'velocity': 'in/s', - 'altitude': 'ft', 'eas': 'in/s', 'dynamic_pressure': 'psi', - } - resp = FlutterResponse.from_nx(method, fdata2, - subcase_id=subcase_id, cref=cref, - is_xysym=is_xysym, is_xzsym=is_xzsym, - f06_units=op2.in_units) + + if not hasattr(op2, 'in_units'): + op2.in_units = { + 'density': 'slinch/in^3', 'velocity': 'in/s', + 'altitude': 'ft', 'eas': 'in/s', 'dynamic_pressure': 'psi', + } + resp = FlutterResponse.from_nx( + method, fdata2, + subcase_id=subcase_id, cref=cref, + is_xysym=is_xysym, is_xzsym=is_xzsym, + f06_units=op2.in_units) op2.op2_results.vg_vf_response[subcase_id] = resp return diff --git a/pyNastran/op2/result_objects/op2_results.py b/pyNastran/op2/result_objects/op2_results.py index ef083d2ba..9581e0d4f 100644 --- a/pyNastran/op2/result_objects/op2_results.py +++ b/pyNastran/op2/result_objects/op2_results.py @@ -1,4 +1,5 @@ -from typing import Any +from __future__ import annotations +from typing import Any, TYPE_CHECKING import numpy as np from pyNastran.op2.op2_interface.random_results import ( @@ -11,6 +12,9 @@ PSDObjects, ) from pyNastran.op2.result_objects.design_response import Responses +if TYPE_CHECKING: + from pyNastran.f06.flutter_response import FlutterResponse + class Results: """storage object for even more op2_results (see op2.op2_results)""" @@ -87,7 +91,7 @@ def __init__(self): self.cstm = None self.trmbd = {} self.trmbu = {} - self.vg_vf_response = {} + self.vg_vf_response: dict[int, FlutterResponse] = {} self.superelement_tables = {} def _get_sum_objects_map(self): @@ -138,6 +142,11 @@ def _get_sum_objects(self) -> list[Any]: self.RADEATC, self.RAFEATC, self.RASEATC, self.RAEEATC, self.RAGEATC, self.RAPEATC, self.RANEATC, self.RAREATC, self.RAQEATC, self.srss, self.abs, self.nrl, + # no dicts + #self.cstm, self.trmbd, self.trmbu, + #self.vg_vf_response, + #self.superelement_tables, + ] return sum_objs @@ -149,6 +158,7 @@ def _get_base_objects_map(self) -> dict[str, Any]: 'contact_slide_distance', 'glue_contact_slide_distance', 'contact_displacements', 'superelement_tables', 'cstm', 'trmbu', 'trmbd', + 'vg_vf_response', ] base_objs_map = {} for base_name in base_names: @@ -166,7 +176,9 @@ def get_table_types(self) -> list[str]: 'contact_slide_distance', 'glue_contact_slide_distance', 'contact_displacements', 'bolt_results', 'superelement_tables', - 'cstm', 'trmbu', 'trmbd', + # no dicts? + #'cstm', 'trmbu', 'trmbd', + 'vg_vf_response', ] sum_objs = self._get_sum_objects() for objs in sum_objs: