Source code for pyNastran.bdf.cards.aero.dynamic_loads

# coding: utf-8
"""
All aero cards are defined in this file.  This includes:

 * AERO
 * FLFACT
 * FLUTTER
 * GUST
 * MKAERO1 / MKAERO2

All cards are BaseCard objects.

"""
from __future__ import annotations
from itertools import count
from typing import TYPE_CHECKING
import numpy as np

from pyNastran.utils.numpy_utils import integer_types
from pyNastran.bdf.field_writer_8 import set_blank_if_default, print_card_8
from pyNastran.bdf.field_writer_16 import print_card_16
from pyNastran.bdf.cards.base_card import BaseCard
from pyNastran.utils.atmosphere import (
    make_flfacts_eas_sweep_constant_alt,
    make_flfacts_eas_sweep_constant_mach,
    make_flfacts_alt_sweep_constant_mach,
    make_flfacts_mach_sweep_constant_alt,
    make_flfacts_tas_sweep_constant_alt,
    atm_density, _velocity_factor)
from pyNastran.bdf.bdf_interface.assign_type import (
    integer, integer_or_blank, double, double_or_blank,
    fields, string_or_blank, double_string_or_blank)
if TYPE_CHECKING:  # pragma: no cover
    from pyNastran.bdf.bdf import BDF
    from pyNastran.bdf.bdf_interface.bdf_card import BDFCard


[docs] class Aero(BaseCard): """Base class for AERO and AEROS cards.""" def __init__(self): """ Common class for AERO, AEROS Attributes ---------- acsid : int; default=0 aerodyanmic coordinate system defines the direction of the wind sym_xz : int; default=0 xz symmetry flag (+1=symmetry; -1=antisymmetric) sym_xy : int; default=0 xy symmetry flag (+1=symmetry; -1=antisymmetric) """ BaseCard.__init__(self) self.sym_xy = None self.sym_xz = None self.acsid = None self.acsid_ref = None
[docs] def Acsid(self): try: return self.acsid_ref.cid except AttributeError: return self.acsid
@property def is_symmetric_xy(self): if self.sym_xy == 1: return True return False @property def is_symmetric_xz(self): if self.sym_xz == 1: return True return False @property def is_anti_symmetric_xy(self): if self.sym_xy == -1: return True return False @property def is_anti_symmetric_xz(self): if self.sym_xz == -1: return True return False
[docs] def set_ground_effect(self, enable): # TODO: verify if enable: self.sym_xy = -1 else: self.sym_xy = 1
[docs] class AERO(Aero): """ Gives basic aerodynamic parameters for unsteady aerodynamics. +------+-------+----------+------+--------+-------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +======+=======+==========+======+========+=======+=======+ | AERO | ACSID | VELOCITY | REFC | RHOREF | SYMXZ | SYMXY | +------+-------+----------+------+--------+-------+-------+ | AERO | 3 | 1.3+ | 100. | 1.-5 | 1 | -1 | +------+-------+----------+------+--------+-------+-------+ """ type = 'AERO' _properties = ['is_anti_symmetric_xy', 'is_anti_symmetric_xz', 'is_symmetric_xy', 'is_symmetric_xz'] _field_map = { 1: 'acsid', 2:'velocity', 3:'cRef', 4:'rhoRef', 5:'symXZ', 6:'symXY', }
[docs] @classmethod def _init_from_empty(cls): velocity = 1. cref = 1. rho_ref = 1. return AERO(velocity, cref, rho_ref, acsid=0, sym_xz=0, sym_xy=0, comment='')
def __init__(self, velocity, cref, rho_ref, acsid=0, sym_xz=0, sym_xy=0, comment=''): """ Creates an AERO card Parameters ---------- velocity : float the airspeed cref : float the aerodynamic chord rho_ref : float FLFACT density scaling factor acsid : int; default=0 aerodyanmic coordinate system defines the direction of the wind sym_xz : int; default=0 xz symmetry flag (+1=symmetry; -1=antisymmetric) sym_xy : int; default=0 xy symmetry flag (+1=symmetry; -1=antisymmetric) comment : str; default='' a comment for the card """ Aero.__init__(self) if comment: self.comment = comment #: Aerodynamic coordinate system identification if acsid is None: acsid = 0 self.acsid = acsid #: Velocity for aerodynamic force data recovery and to calculate the BOV #: parameter self.velocity = velocity #: Reference length for reduced frequency self.cref = cref #: Reference density self.rho_ref = rho_ref #: Symmetry key for the aero coordinate x-z plane. See Remark 6. #: (Integer = +1 for symmetry, 0 for no symmetry, and -1 for antisymmetry; #: Default = 0) self.sym_xz = sym_xz #: The symmetry key for the aero coordinate x-y plane can be used to #: simulate ground effect. (Integer = -1 for symmetry, 0 for no symmetry, #: and +1 for antisymmetry; Default = 0) self.sym_xy = sym_xy
[docs] def validate(self): msg = '' if not isinstance(self.acsid, integer_types): msg += 'acsid=%r must be an integer; type=%s' % ( self.acsid, type(self.acsid)) if not isinstance(self.sym_xz, integer_types): msg = 'sym_xz=%r must be an integer; type=%s' % ( self.sym_xz, type(self.sym_xz)) if not isinstance(self.sym_xy, integer_types): msg = 'sym_xy=%r must be an integer; type=%s' % ( self.sym_xy, type(self.sym_xy)) if msg: raise TypeError(msg + str(self))
[docs] def cross_reference(self, model: BDF) -> None: """ Cross reference aerodynamic coordinate system. Parameters ---------- model : BDF The BDF object. """ msg = ', which is required by AERO' self.acsid_ref = model.Coord(self.acsid, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): """ Safe cross reference aerodynamic coordinate system. Parameters ---------- model : BDF The BDF object. """ msg = ', which is required by AERO' self.acsid_ref = model.safe_coord(self.acsid, None, xref_errors, msg=msg)
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds an AERO card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ acsid = integer_or_blank(card, 1, 'acsid', 0) velocity = double_or_blank(card, 2, 'velocity') cref = double(card, 3, 'cRef') rho_ref = double(card, 4, 'rho_ref') sym_xz = integer_or_blank(card, 5, 'symXZ', 0) sym_xy = integer_or_blank(card, 6, 'symXY', 0) assert len(card) <= 7, f'len(AERO card) = {len(card):d}\ncard={card}' return AERO(velocity, cref, rho_ref, acsid=acsid, sym_xz=sym_xz, sym_xy=sym_xy, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): acsid = data[0] velocity = data[1] cref = data[2] rho_ref = data[3] sym_xz = data[4] sym_xy = data[5] assert len(data) == 6, 'data = %s' % data return AERO(acsid, velocity, cref, rho_ref, sym_xz, sym_xy, comment=comment) # T is the tabular function #angle = self.wg*self.t*(t-(x-self.x0)/self.V)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.acsid_ref = None
[docs] def update(self, maps): """ maps = { 'coord' : cid_map, } """ cid_map = maps['coord'] self.acsid = cid_map[self.acsid]
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[int/float/str] the fields that define the card """ list_fields = ['AERO', self.Acsid(), self.velocity, self.cref, self.rho_ref, self.sym_xz, self.sym_xy] return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list[varies] the fields that define the card """ sym_xz = set_blank_if_default(self.sym_xz, 0) sym_xy = set_blank_if_default(self.sym_xy, 0) list_fields = ['AERO', self.Acsid(), self.velocity, self.cref, self.rho_ref, sym_xz, sym_xy] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: """ Writes the card with the specified width and precision Parameters ---------- size : int (default=8) size of the field; {8, 16} is_double : bool (default=False) is this card double precision Returns ------- msg : str the string representation of the card """ card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class FLFACT(BaseCard): """ +--------+-----+----+------+-----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+====+======+=====+====+====+====+====+ | FLFACT | SID | F1 | F2 | F3 | F4 | F5 | F6 | F7 | +--------+-----+----+------+-----+----+----+----+----+ | | F8 | F9 | etc. | | | | | | +--------+-----+----+------+-----+----+----+----+----+ | FLFACT | 97 | .3 | .7 | 3.5 | | | | | +--------+-----+----+------+-----+----+----+----+----+ # delta quantity approach +--------+-----+-------+------+-------+----+--------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +========+=====+=======+======+=======+====+========+ | FLFACT | SID | F1 | THRU | FNF | NF | FMID | +--------+-----+-------+------+-------+----+--------+ | FLFACT | 201 | 0.200 | THRU | 0.100 | 11 | 0.1333 | +--------+-----+-------+------+-------+----+--------+ """ type = 'FLFACT'
[docs] @classmethod def _init_from_empty(cls): sid = 1 factors = [1.] return FLFACT(sid, factors, comment='')
def __init__(self, sid, factors, comment=''): """ Creates an FLFACT card, which defines factors used for flutter analysis. These factors define either: - density - mach - velocity - reduced frequency depending on the FLUTTER method chosen (e.g., PK, PKNL, PKNLS) Parameters ---------- sid : int the id of a density, reduced_frequency, mach, or velocity table the FLUTTER card defines the meaning factors : varies values : list[float, ..., float] list of factors list[f1, THRU, fnf, nf, fmid] f1 : float first value THRU : str the word THRU fnf : float second value nf : int number of values fmid : float; default=(f1 + fnf) / 2. the mid point to bias the array TODO: does f1 need be be greater than f2/fnf??? comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.sid = sid #self.f1 = f1 #self.fnf = fnf #self.nf = nf #self.fmid = fmid # the dumb string_types thing is because we also get floats if len(factors) > 1 and isinstance(factors[1], str) and factors[1] == 'THRU': #msg = 'embedded THRUs not supported yet on FLFACT card\n' nfactors = len(factors) if nfactors == 4: (f1, _thru, fnf, nf) = factors fmid = (f1 + fnf) / 2. elif nfactors == 5: (f1, _thru, fnf, nf, fmid) = factors #assert _thru.upper() == 'THRU', 'factors=%s' % str(factors) else: raise RuntimeError('factors must be length 4/5; factors=%s' % factors) i = np.linspace(0, nf, nf, endpoint=False) + 1 factors = ( (f1*(fnf - fmid) * (nf-i) + fnf * (fmid - f1) * (i-1)) / ( (fnf - fmid) * (nf-i) + (fmid - f1) * (i-1)) ) self.factors = np.asarray(factors)
[docs] def validate(self): if len(self.factors) == 0: raise ValueError('FLFACT sid=%s is empty; factors=%s' % (self.sid, str(self.factors)))
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds an FLFACT card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ sid = integer(card, 1, 'sid') assert len(card) > 2, 'len(FLFACT card)=%s; card=%s' % (len(card), card) field3 = double_string_or_blank(card, 3, 'THRU') if field3 is None: f1 = double(card, 2, 'f1') factors = [f1] assert len(card) == 3, 'len(FLFACT card)=%s; card=%s' % (len(card), card) elif isinstance(field3, float): factors = fields(double, card, 'factors', i=2, j=len(card)) elif isinstance(field3, str) and field3 == 'THRU': f1 = double(card, 2, 'f1') fnf = double(card, 4, 'fnf') nf = integer(card, 5, 'nf') fmid_default = (f1 + fnf) / 2. fmid = double_or_blank(card, 6, 'fmid', fmid_default) assert len(card) <= 7, 'len(FLFACT card)=%s; card=%s' % (len(card), card) factors = [f1, 'THRU', fnf, nf, fmid] else: raise SyntaxError('expected a float or string for FLFACT field 3; value=%r' % field3) return FLFACT(sid, factors, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): sid = data[0] factors = data[1:] return FLFACT(sid, factors, comment=comment)
[docs] def max(self): return self.factors.max()
[docs] def min(self): return self.factors.min()
#def uncross_reference(self) -> None: #pass
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['FLFACT', self.sid] + list(self.factors) return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.repr_fields() if size == 8: return self.comment + print_card_8(card) return self.comment + print_card_16(card)
FLUTTER_MSG = """ +---------+-----+--------+------+------+-------+-------+-------------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=====+========+======+======+=======+=======+=============+======+ | FLUTTER | SID | METHOD | DENS | MACH | RFREQ | IMETH | NVALUE/OMAX | EPS | +---------+-----+--------+------+------+-------+-------+-------------+------+ | FLUTTER | 19 | K | 119 | 219 | 319 | S | 5 | 1.-4 | +---------+-----+--------+------+------+-------+-------+-------------+------+""".strip()
[docs] class FLUTTER(BaseCard): """ Defines data needed to perform flutter analysis. +---------+-----+--------+------+------+-------+-------+-------------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=====+========+======+======+=======+=======+=============+======+ | FLUTTER | SID | METHOD | DENS | MACH | RFREQ | IMETH | NVALUE/OMAX | EPS | +---------+-----+--------+------+------+-------+-------+-------------+------+ | FLUTTER | 19 | K | 119 | 219 | 319 | S | 5 | 1.-4 | +---------+-----+--------+------+------+-------+-------+-------------+------+ """ type = 'FLUTTER' _field_map = { 1: 'sid', 2:'method', 3:'density', 4:'mach', 5:'reduced_freq_velocity', 6:'imethod', 8:'epsilon', } _properties = ['_field_map', 'headers', ]
[docs] @classmethod def _init_from_empty(cls): sid = 1 method = 'PKNL' density = 1 mach = 1 reduced_freq_velocity = 1 return FLUTTER(sid, method, density, mach, reduced_freq_velocity, imethod='L', nvalue=None, omax=None, epsilon=1.0e-3, comment='')
def _get_field_helper(self, n): """ Gets complicated parameters on the FLUTTER card Parameters ---------- n : int the field number to update Returns ------- value : int/float/str the value for the appropriate field """ if n == 7: if self.method in ['K', 'KE']: value = self.nvalue elif self.method in ['PKS', 'PKNLS']: value = self.omax else: value = self.nvalue return value else: raise KeyError('Field %r is an invalid FLUTTER entry.' % (n)) def _update_field_helper(self, n, value): """ Updates complicated parameters on the FLUTTER card Parameters ---------- n : int the field number to update value : int/float/str the value for the appropriate field """ if n == 7: if self.method in ['K', 'KE']: self.nvalue = value elif self.method in ['PKS', 'PKNLS']: self.omax = value else: self.nvalue = value else: raise KeyError('Field %r=%r is an invalid FLUTTER entry.' % (n, value)) def __init__(self, sid: int, method: str, density: int, mach: int, reduced_freq_velocity: int, imethod: str='L', nvalue=None, omax=None, epsilon: float=1.0e-3, comment: str='', validate: bool=False): """ Creates a FLUTTER card, which is required for a flutter (SOL 145) analysis. Parameters ---------- sid : int flutter id method : str valid methods = [K, KE, PKS, PKNLS, PKNL, PKE] density : int defines a series of air densities in units of mass/volume PARAM,WTMASS does not affect this AERO affects this references an FLFACT id mach : int defines a series of the mach numbers references an FLFACT id reduced_freq_velocity : int Defines a series of either: 1) reduced frequencies - K, KE 2) velocities - PK, PKNL, PKS, PKNLS depending on the method chosen. references an FLFACT id imethod : str; default='L' Choice of interpolation method for aerodynamic matrix interpolation. imethods : 1) L - linear 2) S - surface 3) TCUB - termwise cubic nvalue : int Number of eigenvalues beginning with the first eigenvalue for output and plots omax : float For the PKS and PKNLS methods, OMAX specifies the maximum frequency, in Hz., to be used in he flutter sweep. MSC only. epsilon : float; default=1.0e-3 Convergence parameter for k. Used in the PK and PKNL methods only comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.sid = sid if method in ['PK', 'PKNL', 'PKNLS']: imethod = 'L' #else: #assert imethod in ['S', 'L', None], imethod self.method = method self.density = density self.mach = mach # KFREQ - K, KE # VEL - PK, PKNL, PKS, PKNLS self.reduced_freq_velocity = reduced_freq_velocity # self.imethod = imethod self.nvalue = nvalue self.omax = omax self.epsilon = epsilon self.density_ref = None self.mach_ref = None self.reduced_freq_velocity_ref = None if validate: self.validate()
[docs] def validate(self): msg = '' if self.method not in {'K', 'KE', 'PK', 'PKNL', 'PKS', 'PKNLS'}: msg += f'method = {self.method!r}; allowed=[K, KE, PKS, PKNLS, PKNL, PK]\n' if self.imethod not in {'L', 'S', 'TCUB'}: msg += f'imethod = {self.imethod!r}; allowed=[L, S, TCUB]\n' if msg: raise ValueError(msg + str(self))
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a FLUTTER card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ sid = integer(card, 1, 'sid') method = string_or_blank(card, 2, 'method (K, KE, PKS, PKNLS, PKNL, PK)', default='L') density_id = integer(card, 3, 'density') mach_id = integer(card, 4, 'mach') reduced_freq_velocity_id = integer(card, 5, 'reduced_freq_velocity') omax = None imethod = string_or_blank(card, 6, 'imethod', default='L') if method in ['K', 'KE']: nvalue = integer_or_blank(card, 7, 'nvalue') assert imethod in ['L', 'S', 'TCUB'], 'imethod = %s' % imethod # linear-surface elif method in ['PKS', 'PKNLS']: nvalue = None omax = double_or_blank(card, 7, 'omax') elif method == 'PKNL': nvalue = integer_or_blank(card, 7, 'nvalue') elif method == 'PK': nvalue = integer_or_blank(card, 7, 'nvalue') else: raise NotImplementedError('FLUTTER method=%r' % method) assert method in ['K', 'KE', 'PK', 'PKS', 'PKNL', 'PKNLS', None], method epsilon = double_or_blank(card, 8, 'epsilon', default=1e-3) # not defined in QRG assert len(card) <= 9, f'len(FLUTTER card) = {len(card):d}\ncard={card}' return FLUTTER(sid, method, density_id, mach_id, reduced_freq_velocity_id, imethod=imethod, nvalue=nvalue, omax=omax, epsilon=epsilon, comment=comment)
[docs] def make_flfacts_eas_sweep(self, model: BDF, alt: float, eass: list[float], alt_units: str='m', velocity_units: str='m/s', density_units: str='kg/m^3', eas_units: str='m/s') -> None: self.deprecated('make_flfacts_eas_sweep', 'make_flfacts_eas_sweep_constant_alt', '1.4') self.make_flfacts_eas_sweep_constant_alt( model, alt, eass, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units)
[docs] def make_flfacts_eas_sweep_constant_alt(self, model: BDF, alt: float, eass: list[float], alt_units: str='m', velocity_units: str='m/s', density_units: str='kg/m^3', eas_units: str='m/s') -> None: """ Makes a sweep across equivalent airspeed for a constant altitude. Parameters ---------- model : BDF the BDF model object alt : float Altitude in alt_units eass : list[float] Equivalent airspeed in eas_units alt_units : str; default='m' the altitude units; ft, kft, m velocity_units : str; default='m/s' the velocity units; ft/s, m/s, in/s, knots density_units : str; default='kg/m^3' the density units; slug/ft^3, slinch/in^3, kg/m^3 eas_units : str; default='m/s' the equivalent airspeed units; ft/s, m/s, in/s, knots """ eass.sort() rho, mach, velocity = make_flfacts_eas_sweep_constant_alt( alt, eass, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units) flfact_rho = self.sid + 1 flfact_mach = self.sid + 2 flfact_velocity = self.sid + 3 flfact_eas = self.sid + 4 self.mach = flfact_mach self.reduced_freq_velocity = flfact_velocity self.density = flfact_rho comment = ' density: min=%.3e max=%.3e %s' % ( rho.min(), rho.max(), density_units, ) model.add_flfact(flfact_rho, rho, comment=comment) model.add_flfact(flfact_mach, mach, comment=' Mach: %s' % mach.min()) comment = ' velocity: min=%.3f max=%.3f %s' % ( velocity.min(), velocity.max(), velocity_units) model.add_flfact(flfact_velocity, velocity, comment=comment) # eas in velocity units comment = ' EAS: min=%.3f max=%.3f %s' % ( eass.min(), eass.max(), eas_units) model.add_flfact(flfact_eas, eass, comment=comment)
[docs] def make_flfacts_alt_sweep_constant_mach(self, model: BDF, mach, alts, eas_limit: float=1000., alt_units: str='m', velocity_units: str='m/s', density_units: str='kg/m^3', eas_units: str='m/s') -> None: """makes an altitude sweep""" alts.sort() alts = alts[::-1] rho, mach, velocity = make_flfacts_alt_sweep_constant_mach( mach, alts, eas_limit=eas_limit, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units) flfact_rho = self.sid + 1 flfact_mach = self.sid + 2 flfact_velocity = self.sid + 3 flfact_eas = self.sid + 4 flfact_alt = self.sid + 5 alts2 = alts[:len(rho)] assert len(rho) == len(alts2) comment = ' density: min=%.3e max=%.3e %s; alt min=%.0f max=%.0f %s' % ( rho.min(), rho.max(), density_units, alts2.min(), alts2.max(), alt_units, ) model.add_flfact(flfact_rho, rho, comment=comment) model.add_flfact(flfact_mach, mach, comment=' Mach: %s' % mach.min()) comment = ' velocity: min=%.3f max=%.3f %s' % ( velocity.min(), velocity.max(), velocity_units) model.add_flfact(flfact_velocity, velocity, comment=comment) # eas in velocity units rho0 = atm_density(0., alt_units=alt_units, density_units=density_units) eas = velocity * np.sqrt(rho / rho0) kvel = _velocity_factor(velocity_units, eas_units) eas_in_eas_units = eas * kvel comment = ' EAS: min=%.3f max=%.3f %s' % ( eas_in_eas_units.min(), eas_in_eas_units.max(), eas_units) model.add_flfact(flfact_eas, eas_in_eas_units, comment=comment) comment = ' Alt: min=%.3f max=%.3f %s' % (alts2.min(), alts2.max(), alt_units) model.add_flfact(flfact_alt, alts2, comment=comment)
[docs] def make_flfacts_tas_sweep_constant_alt(self, model: BDF, alt: float, tass, eas_limit: float=1000., alt_units: str='m', velocity_units: str='m/s', density_units: str='kg/m^3', eas_units: str='m/s') -> tuple[Any, Any, Any]: """makes an altitude sweep (dev...not validated)""" tass.sort() rho, mach, velocity = make_flfacts_tas_sweep_constant_alt( alt, tass, eas_limit=eas_limit, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units) flfact_rho = self.sid + 1 flfact_mach = self.sid + 2 flfact_velocity = self.sid + 3 flfact_eas = self.sid + 4 #flfact_alt = self.sid + 5 #alts2 = alts[:len(rho)] #assert len(rho) == len(alts2) comment = ' density: min=%.3e max=%.3e %s; alt min=%.0f max=%.0f %s' % ( rho.min(), rho.max(), density_units, alt, alt, alt_units, ) model.add_flfact(flfact_rho, rho, comment=comment) model.add_flfact(flfact_mach, mach, comment=' Mach: %s' % mach.min()) comment = ' velocity: min=%.3f max=%.3f %s' % ( velocity.min(), velocity.max(), velocity_units) model.add_flfact(flfact_velocity, velocity, comment=comment) # eas in velocity units rho0 = atm_density(0., alt_units=alt_units, density_units=density_units) eas = velocity * np.sqrt(rho / rho0) kvel = _velocity_factor(velocity_units, eas_units) eas_in_eas_units = eas * kvel comment = ' EAS: min=%.3f max=%.3f %s' % ( eas_in_eas_units.min(), eas_in_eas_units.max(), eas_units) model.add_flfact(flfact_eas, eas_in_eas_units, comment=comment)
#comment = ' Alt: min=%.3f max=%.3f %s' % (alt, al, alt_units) #model.add_flfact(flfact_alt, alts2, comment=comment)
[docs] def make_flfacts_mach_sweep(self, model, alt, machs, eas_limit=1000., alt_units='m', velocity_units='m/s', density_units='kg/m^3', eas_units='m/s') -> None: self.deprecated('make_flfacts_mach_sweep', 'make_flfacts_mach_sweep_constant_alt', '1.4') self.make_flfacts_mach_sweep_constant_alt( model, alt, machs, eas_limit=eas_limit, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units)
[docs] def make_flfacts_eas_sweep_constant_mach(self, model: BDF, mach, eass, gamma: float=1.4, alt_units='m', velocity_units='m/s', density_units='kg/m^3', eas_units='m/s', pressure_units='Pa'): """ eas = tas * sqrt(rho/rho0) ainf*Minf = V eas = ainf*Minf * sqrt(rho_inf/rho0) rho = p/RT eas = ainf*Minf * sqrt(p_inf/(R*T_inf*rho0)) = sqrt(gamma*R*Tinf) * Minf * sqrt(p_inf/(R*T_inf*rho0)) = Minf * sqrt(gamma*p_inf/rho0) """ neas = len(eass) machs = mach * np.ones(neas) rho, mach, velocity, alts = make_flfacts_eas_sweep_constant_mach( machs, eass, gamma=gamma, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units, pressure_units=pressure_units) machs2 = machs[:len(rho)] assert len(rho) == len(machs2) flfact_rho = self.sid + 1 flfact_mach = self.sid + 2 flfact_velocity = self.sid + 3 flfact_eas = self.sid + 4 comment = ' density: min=%.3e max=%.3e %s; alts min=%.0f %.0f %s' % ( rho.min(), rho.max(), density_units, alts.min(), alts.max(), alt_units, ) model.add_flfact(flfact_rho, rho, comment=comment) comment = ' Mach: min=%s max=%s' % (mach.min(), mach.max()) model.add_flfact(flfact_mach, mach, comment=comment) comment = ' velocity: min=%.3f max=%.3f %s' % ( velocity.min(), velocity.max(), velocity_units) model.add_flfact(flfact_velocity, velocity, comment=comment) # eas in velocity units rho0 = atm_density(0., alt_units=alt_units, density_units=density_units) eas = velocity * np.sqrt(rho / rho0) kvel = _velocity_factor(velocity_units, eas_units) eas_in_eas_units = eas * kvel comment = ' EAS: min=%.3f max=%.3f %s' % ( eas_in_eas_units.min(), eas_in_eas_units.max(), eas_units) model.add_flfact(flfact_eas, eas_in_eas_units, comment=comment)
[docs] def make_flfacts_mach_sweep_constant_alt(self, model: BDF, alt, machs, eas_limit=1000., alt_units='m', velocity_units='m/s', density_units='kg/m^3', eas_units='m/s'): """makes a mach sweep""" machs.sort() #machs = machs[::-1] rho, mach, velocity = make_flfacts_mach_sweep_constant_alt( alt, machs, eas_limit=eas_limit, alt_units=alt_units, velocity_units=velocity_units, density_units=density_units, eas_units=eas_units) machs2 = machs[:len(rho)] assert len(rho) == len(machs2) flfact_rho = self.sid + 1 flfact_mach = self.sid + 2 flfact_velocity = self.sid + 3 flfact_eas = self.sid + 4 comment = ' density: min=%.3e max=%.3e %s; alt %.0f %s' % ( rho.min(), rho.max(), density_units, alt, alt_units, ) model.add_flfact(flfact_rho, rho, comment=comment) comment = ' Mach: min=%s max=%s' % (mach.min(), mach.max()) model.add_flfact(flfact_mach, mach, comment=comment) comment = ' velocity: min=%.3f max=%.3f %s' % ( velocity.min(), velocity.max(), velocity_units) model.add_flfact(flfact_velocity, velocity, comment=comment) # eas in velocity units rho0 = atm_density(0., alt_units=alt_units, density_units=density_units) eas = velocity * np.sqrt(rho / rho0) kvel = _velocity_factor(velocity_units, eas_units) eas_in_eas_units = eas * kvel comment = ' EAS: min=%.3f max=%.3f %s' % ( eas_in_eas_units.min(), eas_in_eas_units.max(), eas_units) model.add_flfact(flfact_eas, eas_in_eas_units, comment=comment)
@property def headers(self): headers = ['density', 'mach'] if self.method in ['PK', 'PKS', 'PKNL', 'PKNLS']: headers.append('velocity') elif self.method in ['K', 'KE']: headers.append('reduced_frequency') else: raise NotImplementedError('FLUTTER method=%r' % self.method) return headers @classmethod def add_op2_data(cls, data, comment=''): assert len(data) == 8, 'FLUTTER = %s' % data sid = data[0] method = data[1] density = data[2] mach = data[3] reduced_freq_velocity = data[4] method = data[5] imethod = data[6] nvalue = data[7] omax = data[8] epsilon = None return FLUTTER(sid, method, density, mach, reduced_freq_velocity, imethod, nvalue, omax, epsilon, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: """ Cross links the card so referenced cards can be extracted directly Parameters ---------- model : BDF() the BDF object """ msg = ', which is required by FLUTTER sid=%s' % self.sid self.density_ref = model.FLFACT(self.density, msg=msg) self.mach_ref = model.FLFACT(self.mach, msg=msg) self.reduced_freq_velocity_ref = model.FLFACT(self.reduced_freq_velocity, msg=msg)
[docs] def safe_cross_reference(self, model): msg = ', which is required by FLUTTER sid=%s' % self.sid try: self.density_ref = model.FLFACT(self.density, msg=msg) except KeyError: pass try: self.mach_ref = model.FLFACT(self.mach, msg=msg) except KeyError: pass try: self.reduced_freq_velocity_ref = model.FLFACT(self.reduced_freq_velocity, msg=msg) except KeyError: pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.density = self.get_density() self.mach = self.get_mach() self.reduced_freq_velocity = self.get_rfreq_vel() self.density_ref = None self.mach_ref = None self.reduced_freq_velocity_ref = None
[docs] def get_density(self): if self.density_ref is not None: return self.density_ref.sid return self.density
[docs] def get_mach(self): if self.mach_ref is not None: return self.mach_ref.sid return self.mach
[docs] def get_rfreq_vel(self): if self.reduced_freq_velocity_ref is not None: return self.reduced_freq_velocity_ref.sid return self.reduced_freq_velocity
[docs] def _get_raw_nvalue_omax(self): if self.method in ['K', 'KE']: #assert self.imethod in ['L', 'S'], 'imethod = %s' % self.imethod return self.imethod, self.nvalue elif self.method in ['PKS', 'PKNLS']: return self.imethod, self.omax # PK, PKNL return self.imethod, self.nvalue
[docs] def _get_repr_nvalue_omax(self): if self.method in ['K', 'KE']: imethod = set_blank_if_default(self.imethod, 'L') #assert self.imethod in ['L', 'S'], 'imethod = %s' % self.imethods return imethod, self.nvalue elif self.method in ['PKS', 'PKNLS']: return self.imethod, self.omax # PK, PKNL return self.imethod, self.nvalue
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ (imethod, nvalue) = self._get_raw_nvalue_omax() list_fields = ['FLUTTER', self.sid, self.method, self.get_density(), self.get_mach(), self.get_rfreq_vel(), imethod, nvalue, self.epsilon] return list_fields
[docs] def repr_fields(self): (imethod, nvalue) = self._get_repr_nvalue_omax() epsilon = set_blank_if_default(self.epsilon, 0.001) list_fields = ['FLUTTER', self.sid, self.method, self.get_density(), self.get_mach(), self.get_rfreq_vel(), imethod, nvalue, epsilon] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class GUST(BaseCard): """ Defines a stationary vertical gust for use in aeroelastic response analysis. +------+-----+-------+-----+-----+------+ | 1 | 2 | 3 | 4 | 5 | 6 | +======+=====+=======+=====+=====+======+ | GUST | SID | DLOAD | WG | X0 | V | +------+-----+-------+-----+-----+------+ | GUST | 133 | 61 | 1.0 | 0. | 1.+4 | +------+-----+-------+-----+-----+------+ """ type = 'GUST' _field_map = { 1: 'sid', 2:'dload', 3:'wg', 4:'x0', 5:'V', }
[docs] @classmethod def _init_from_empty(cls): sid = 1 dload = 1 wg = 1. x0 = 0. return GUST(sid, dload, wg, x0, V=None, comment='')
def __init__(self, sid, dload, wg, x0, V=None, comment=''): """ Creates a GUST card, which defines a stationary vertical gust for use in aeroelastic response analysis. Parameters ---------- sid : int gust load id dload : int TLOADx or RLOADx entry that defines the time/frequency dependence wg : float Scale factor (gust velocity/forward velocity) for gust velocity x0 : float Streamwise location in the aerodynamic coordinate system of the gust reference point. V : float; default=None float : velocity of the vehicle (must be the same as the velocity on the AERO card) None : ??? comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.sid = sid self.dload = dload self.wg = wg self.x0 = x0 self.V = V
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a GUST card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ sid = integer(card, 1, 'sid') dload = integer(card, 2, 'dload') wg = double(card, 3, 'wg') x0 = double(card, 4, 'x0') V = double_or_blank(card, 5, 'V') assert len(card) <= 6, f'len(GUST card) = {len(card):d}\ncard={card}' return GUST(sid, dload, wg, x0, V=V, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): sid = data[0] dload = data[1] wg = data[2] x0 = data[3] V = data[4] assert len(data) == 5, 'data = %s' % data return GUST(sid, dload, wg, x0, V, comment=comment) #def Angle(self): #angle = self.wg * self.t * (t-(x-self.x0) / self.V) # T is the tabular #return angle #def uncross_reference(self) -> None: #pass def _verify(self, model, xref): if model.aero: pass #assert model.aero.V == self.V
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['GUST', self.sid, self.dload, self.wg, self.x0, self.V] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class MKAERO1(BaseCard): """ Provides a table of Mach numbers (m) and reduced frequencies (k) for aerodynamic matrix calculation. +---------+----+----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+====+====+====+====+====+====+====+====+ | MKAERO1 | m1 | m2 | m3 | m4 | m5 | m6 | m7 | m8 | +---------+----+----+----+----+----+----+----+----+ | | k1 | k2 | k3 | k4 | k5 | k6 | k7 | k8 | +---------+----+----+----+----+----+----+----+----+ """ type = 'MKAERO1'
[docs] @classmethod def _init_from_empty(cls): machs = [1.] reduced_freqs = [1.] return MKAERO1(machs, reduced_freqs, comment='')
def __init__(self, machs, reduced_freqs, comment=''): """ Creates an MKAERO1 card, which defines a set of mach and reduced frequencies. Parameters ---------- machs : list[float] series of Mach numbers reduced_freqs : list[float] series of reduced frequencies comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.machs = np.unique(machs) self.reduced_freqs = np.unique(reduced_freqs)
[docs] def validate(self): msg = '' if None in self.machs: msg += 'MKAERO1; None in machs=%s\n' % (self.machs) if None in self.reduced_freqs: msg += 'MKAERO1; None in rfreqs=%s\n' % (self.reduced_freqs) if len(self.machs) == 0: msg += 'MKAERO1; nmachs=%s machs=%s\n' % (len(self.machs), self.machs) if len(self.reduced_freqs) == 0: msg += 'MKAERO1; nrfreqs=%s rfreqs=%s' % (len(self.reduced_freqs), self.reduced_freqs) if msg: raise ValueError(msg.rstrip())
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an MKAERO1 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ machs = [] reduced_freqs = [] for i in range(1, 9): mach = double_or_blank(card, i, 'mach') if mach is not None: machs.append(mach) for i in range(9, 17): reduced_freq = double_or_blank(card, i, 'rFreq') if reduced_freq is not None: reduced_freqs.append(reduced_freq) assert len(machs) > 0, machs assert len(reduced_freqs) > 0, reduced_freqs return MKAERO1(machs, reduced_freqs, comment=comment)
[docs] def mklist(self) -> list[list[float]]: mklist = [] for mach in self.machs: for kfreq in self.reduced_freqs: mklist.append([mach, kfreq]) return mklist
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ #list_fields = ['MKAERO1'] #for (i, mach, rfreq) in zip(count(), self.machs, self.reduced_freqs): # list_fields += [mach, rfreq] # kind of a hack because there isn't a good way to do this for # duplicately-defined MKAERO1s machs = [None] * max(8, len(self.machs)) freqs = [None] * max(8, len(self.reduced_freqs)) for i, mach in enumerate(self.machs): machs[i] = mach for i, freq in enumerate(self.reduced_freqs): freqs[i] = freq list_fields = ['MKAERO1'] + machs + freqs return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nmachs = len(self.machs) nreduced_freqs = len(self.reduced_freqs) if nmachs > 8 or nreduced_freqs > 8: mach_sets = [] rfreq_sets = [] imach = 0 ifreq = 0 while imach < nmachs: mach_sets.append(self.machs[imach:imach+8]) imach += 8 while ifreq < nreduced_freqs: rfreq_sets.append(self.reduced_freqs[ifreq:ifreq+8]) ifreq += 8 msg = self.comment #print('mach_sets = %s' % mach_sets) #print('rfreq_sets = %s' % rfreq_sets) for mach_set in mach_sets: for rfreq_set in rfreq_sets: msg += MKAERO1(mach_set, rfreq_set).write_card( size=size, is_double=is_double) return msg machs = [None] * 8 reduced_freqs = [None] * 8 if not 0 < len(self.machs) <= 8: msg = 'MKAERO1; nmachs=%s machs=%s' % (len(self.machs), self.machs) raise ValueError(msg) if not 0 < len(self.reduced_freqs) <= 8: msg = 'MKAERO1; nrfreqs=%s rfreqs=%s' % (len(self.reduced_freqs), self.reduced_freqs) raise ValueError(msg) for i, mach in zip(count(), self.machs): machs[i] = mach for i, rfreq in zip(count(), self.reduced_freqs): reduced_freqs[i] = rfreq return self.comment + print_card_8(['MKAERO1'] + machs + reduced_freqs)
def __repr__(self): return self.write_card()
[docs] class MKAERO2(BaseCard): """ Provides a table of Mach numbers (m) and reduced frequencies (k) for aerodynamic matrix calculation. +---------+----+----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+====+====+====+====+====+====+====+====+ | MKAERO2 | m1 | k1 | m2 | k2 | m3 | k3 | m4 | k4 | +---------+----+----+----+----+----+----+----+----+ """ type = 'MKAERO2'
[docs] @classmethod def _init_from_empty(cls): machs = [1.] reduced_freqs = [1.] return MKAERO2(machs, reduced_freqs, comment='')
def __init__(self, machs, reduced_freqs, comment=''): """ Creates an MKAERO2 card, which defines a set of mach and reduced frequency pairs. Parameters ---------- machs : list[float] series of Mach numbers reduced_freqs : list[float] series of reduced frequencies comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.machs = machs self.reduced_freqs = reduced_freqs
[docs] def validate(self): if len(self.machs) == 0: msg = 'MKAERO2; nmachs=%s machs=%s' % (len(self.machs), self.machs) raise ValueError(msg) if len(self.reduced_freqs) == 0: msg = 'MKAERO2; nrfreqs=%s rfreqs=%s' % (len(self.reduced_freqs), self.reduced_freqs) raise ValueError(msg) if len(self.machs) != len(self.reduced_freqs): msg = 'MKAERO2; len(machs)=%s len(rfreqs)=%s; should be the same' % ( len(self.machs), len(self.reduced_freqs)) raise ValueError(msg)
[docs] @classmethod def add_card(cls, card: BDFCard, comment=''): """ Adds an MKAERO2 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ nfields = len(card) machs = [] reduced_freqs = [] for i in range(1, nfields, 2): machs.append(double(card, i, 'mach')) reduced_freqs.append(double(card, i + 1, 'rFreq')) return MKAERO2(machs, reduced_freqs, comment=comment)
[docs] def mklist(self): mklist = [] for mach, kfreq in zip(self.machs, self.reduced_freqs): mklist.append([mach, kfreq]) return mklist
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['MKAERO2'] for (mach, rfreq) in zip(self.machs, self.reduced_freqs): list_fields += [mach, rfreq] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: cards = [] list_fields = ['MKAERO2'] nvalues = 0 for mach, rfreq in zip(self.machs, self.reduced_freqs): list_fields += [mach, rfreq] nvalues += 1 if nvalues == 4: cards.append(print_card_8(list_fields)) list_fields = ['MKAERO2'] nvalues = 0 if nvalues: cards.append(print_card_8(list_fields)) else: if len(self.machs) != len(self.reduced_freqs) or len(self.machs) == 0: msg = 'MKAERO2: len(machs)=%s len(reduced_freqs)=%s' % ( len(self.machs), len(self.reduced_freqs)) raise ValueError(msg) return self.comment + ''.join(cards)
def __repr__(self): return self.write_card()