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

# coding: utf-8
# pylint: disable=R0902,R0904,R0914,C0302,C0111,C0103,R0913
"""
All aero cards are defined in this file.  This includes:

 * AECOMP
 * AEFACT
 * AELINK
 * AELIST
 * AEPARM
 * AESURF / AESURFS
 * CAERO1 / CAERO2 / CAERO3 / CAERO4 / CAERO5
 * PAERO1 / PAERO2 / PAERO3 / PAERO4 / PAERO5
 * SPLINE1 / SPLINE2 / SPLINE3 / SPLINE4 / SPLINE5
 * MONPNT1 / MONPNT2 / MONPNT3

All cards are BaseCard objects.

"""
from __future__ import annotations
import math
import copy
from itertools import count
from collections import defaultdict, namedtuple
import warnings

from typing import Optional, Any, TYPE_CHECKING

import numpy as np
import scipy

from pyNastran.utils.numpy_utils import integer_types, float_types
#from pyNastran.utils import object_attributes
from pyNastran.bdf.field_writer_8 import set_blank_if_default, print_card_8, print_float_8
from pyNastran.bdf.cards.base_card import BaseCard, expand_thru
from pyNastran.bdf.bdf_interface.assign_type import (
    fields, integer, integer_or_blank, double, double_or_blank, string,
    string_or_blank, integer_or_string, integer_string_or_blank,
    interpret_value, parse_components, components_or_blank, blank)
from pyNastran.bdf.bdf_interface.internal_get import (
    coord_id,
    caero_id, paero_id,
    set_id, aesurf_label,
    set_group, aefact_id, aelist_id)
from pyNastran.bdf.cards.utils import wipe_empty_fields
from pyNastran.bdf.cards.aero.utils import (
    points_elements_from_quad_points, create_axisymmetric_body)
if TYPE_CHECKING:  # pragma: no cover
    from pyNastran.nptyping_interface import NDArray3float
    from pyNastran.bdf.bdf import BDF
    from pyNastran.bdf.bdf_interface.bdf_card import BDFCard
    from pyNastran.bdf.cards.nodes import GRID
    from pyNastran.bdf.cards.bdf_sets import SET1
    from pyNastran.bdf.cards.optimization_nx import GROUP
    import matplotlib
    AxesSubplot = matplotlib.axes._subplots.AxesSubplot


[docs] class AECOMP(BaseCard): """ Defines a component for use in monitor point definition or external splines. +--------+-------+----------+-------+-------+-------+-------+-------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=======+==========+=======+=======+=======+=======+=======+=======+ | AECOMP | NAME | LISTTYPE | LIST1 | LIST2 | LIST3 | LIST4 | LIST5 | LIST6 | +--------+-------+----------+-------+-------+-------+-------+-------+-------+ | | LIST7 | etc. | | | | | | | +--------+-------+----------+-------+-------+-------+-------+-------+-------+ | AECOMP | WING | AELIST | 1001 | 1002 | | | | | +--------+-------+----------+-------+-------+-------+-------+-------+-------+ | AECOMP | WING | SET1 | 1001 | 1002 | | | | | +--------+-------+----------+-------+-------+-------+-------+-------+-------+ | AECOMP | WING | CAERO1 | 1001 | 2001 | | | | | +--------+-------+----------+-------+-------+-------+-------+-------+-------+ Attributes ---------- name : str The name. list_type : str {'SET1', 'AELIST', 'CAEROx'} lists : list[int] list of values of AECOMP lists """ type = 'AECOMP' allowed_list_types = ['SET1', 'AELIST', 'CAERO']
[docs] @classmethod def _init_from_empty(cls): name = 'name' list_type = 'CAERO' lists = [1] return AECOMP(name, list_type, lists)
def __init__(self, name: str, list_type: str, lists: int | list[int], comment: str='') -> None: """ Creates an AECOMP card Parameters ---------- name : str the name of the component list_type : str One of CAERO, AELIST or CMPID for aerodynamic components and SET1 for structural components. Aerodynamic components are defined on the aerodynamic ks-set mesh while the structural components are defined on the g-set mesh. lists : list[int, int, ...]; int The identification number of either SET1, AELIST or CAEROi entries that define the set of grid points that comprise the component comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment if isinstance(lists, integer_types): lists = [lists] elif not isinstance(lists, (list, tuple)): raise TypeError('AECOMP; type(lists)=%s and must be a list/tuple' % type(lists)) self.name = name self.list_type = list_type self.lists = lists self.lists_ref = None
[docs] def validate(self) -> None: if self.list_type not in ['SET1', 'AELIST', 'CAERO', 'CMPID']: msg = 'list_type=%r not in [SET1, AELIST, CAERO, CMPID]' % self.list_type raise RuntimeError(msg)
[docs] @classmethod def add_card(cls, card: Any, comment: str='') -> AECOMP: """ Adds an AECOMP card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ name = string(card, 1, 'name') list_type = string(card, 2, 'list_type') j = 1 lists = [] for i in range(3, len(card)): list_i = integer(card, i, '%s_%d' % (list_type, j)) lists.append(list_i) j += 1 return AECOMP(name, list_type, lists, 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 AECOMP name=%r' % self.name if self.list_type == 'SET1': self.lists_ref = [model.SET1(key, msg) for key in self.lists] elif self.list_type == 'AELIST': self.lists_ref = [model.AELIST(key, msg) for key in self.lists] elif self.list_type == 'CAERO': self.lists_ref = [model.CAero(key, msg) for key in self.lists] #elif self.list_type == 'CMPID': # AEQUAD4,/AETRIA3 else: # pragma: no cover raise NotImplementedError(self.list_type)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by AECOMP name=%r' % self.name #return lists_ref = [] if self.list_type == 'SET1': for key in self.lists: try: ref = model.SET1(key, msg) except KeyError: ref = None lists_ref.append(ref) elif self.list_type == 'AELIST': for key in self.lists: try: ref = model.AELIST(key, msg) except KeyError: ref = None lists_ref.append(ref) elif self.list_type == 'CAERO': for key in self.lists: try: ref = model.CAero(key, msg) except KeyError: ref = None lists_ref.append(ref) #elif self.list_type == 'CMPID': # AEQUAD4,/AETRIA3 else: # pragma: no cover raise NotImplementedError(self.list_type) self.lists_ref = lists_ref
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.lists = self.get_lists() self.lists_ref = None
[docs] def get_lists(self): if self.lists_ref is None: return self.lists if self.list_type == 'SET1': lists = [set1 if isinstance(set1, integer_types) else set1.sid for set1 in self.lists_ref] elif self.list_type == 'AELIST': lists = [aelist if isinstance(aelist, integer_types) else aelist.sid for aelist in self.lists_ref] elif self.list_type == 'CAERO': lists = [caero if isinstance(caero, integer_types) else caero.eid for caero in self.lists_ref] #elif self.list_type == 'CMPID': # AEQUAD4,/AETRIA3 else: # pragma: no cover raise NotImplementedError(self.list_type) return lists
[docs] def raw_fields(self): list_fields = ['AECOMP', self.name, self.list_type] + self.get_lists() return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: """ The writer method used by BDF.write_card() Parameters ----------- size : int; default=8 the size of the card (8/16) """ card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class AECOMPL(BaseCard): """ Makes a "AECOMP" that is a combination of other AECOMPs or AECOMPLs. +---------+--------+--------+--------+---------+--------+--------+--------+--------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+========+========+========+=========+========+========+========+========+ | AECOMPL | NAME | LABEL1 | LABEL2 | LABEL3 | LABEL4 | LABEL5 | LABEL6 | LABEL7 | +---------+--------+--------+--------+---------+--------+--------+--------+--------+ | | LABEL8 | etc. | | | | | | | +---------+--------+--------+--------+---------+--------+--------+--------+--------+ | AECOMPL | HORIZ | STAB | ELEV | BALANCE | | | | | +---------+--------+--------+--------+---------+--------+--------+--------+--------+ """ type = 'AECOMPL'
[docs] @classmethod def _init_from_empty(cls): name = 'HORIZ' labels = ['ELEV'] return AECOMPL(name, labels)
def __init__(self, name: str, labels: list[str], comment: str='') -> None: """ Creates an AECOMPL card Parameters ---------- name : str the name of the component labels : list[str, str, ...]; str A string of 8 characters referring to the names of other components defined by either AECOMP or other AECOMPL entries. comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment if isinstance(labels, str): labels = [labels] elif not isinstance(labels, (list, tuple)): raise TypeError('AECOMPL; type(labels)=%s and must be a list/tuple' % type(labels)) self.name = name self.labels = labels #self.labels_ref = None
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str='') -> AECOMPL: """ Adds an AECOMPL card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ name = string(card, 1, 'name') labels = [] j = 1 for i in range(2, len(card)): label = string(card, i, 'label_%d' % j) labels.append(label) j += 1 return AECOMPL(name, labels, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model, xref_errors): pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" pass
[docs] def raw_fields(self): list_fields = ['AECOMPL', self.name] + self.labels return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: """ The writer method used by BDF.write_card() Parameters ----------- size : int; default=8 the size of the card (8/16) """ card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class AEFACT(BaseCard): """ Defines real numbers for aeroelastic analysis. +--------+-----+----+--------+-----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+====+========+=====+====+====+====+====+ | AEFACT | SID | D1 | D2 | D3 | D4 | D5 | D6 | D7 | +--------+-----+----+--------+-----+----+----+----+----+ | | D8 | D9 | etc. | | | | | | +--------+-----+----+--------+-----+----+----+----+----+ | AEFACT | 97 |.3 | 0.7 | 1.0 | | | | | +--------+-----+----+--------+-----+----+----+----+----+ TODO: Are these defined in percentages and thus, should they be normalized if they are not? """ type = 'AEFACT'
[docs] @classmethod def _init_from_empty(cls): sid = 1 fractions = [0., 1.,] return AEFACT(sid, fractions)
def __init__(self, sid: int, fractions: list[float], comment: str=''): """ Creates an AEFACT card, which is used by the CAEROx / PAEROx card to adjust the spacing of the sub-paneleing (and grid point paneling in the case of the CAERO3). Parameters ---------- sid : int unique id fractions : list[float, ..., float] list of percentages ranging from 0-1 comment : str; default='' a comment for the card """ super(AEFACT, self).__init__() if comment: self.comment = comment #: Set identification number. (Unique Integer > 0) self.sid = sid #: Number (float) self.fractions = np.asarray(fractions, dtype='float64')
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an AEFACT card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ sid = integer(card, 1, 'sid') fractions = [] for i in range(2, len(card)): fraction = double(card, i, 'factor_%d' % (i - 1)) fractions.append(fraction) assert len(card) > 2, 'len(AEFACT card) = %i\n%s' % (len(card), card) return AEFACT(sid, fractions, comment=comment)
#def cross_reference(self, model: BDF) -> None: #pass #def uncross_reference(self): #"""Removes cross-reference links""" #pass
[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 = ['AEFACT', self.sid] + list(self.fractions) return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: """ The writer method used by BDF.write_card() Parameters ----------- size : int; default=8 the size of the card (8/16) """ card = self.repr_fields() return self.comment + print_card_8(card)
[docs] class AELIST(BaseCard): """ Defines a list of aerodynamic elements to undergo the motion prescribed with the AESURF Bulk Data entry for static aeroelasticity. +---------+------+------+------+------+------+------+------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+======+======+======+======+======+======+======+======+ | AELIST | SID | E1 | E2 | E3 | E4 | E5 | E6 | E7 | +---------+------+------+------+------+------+------+------+------+ | | E8 | etc. | | | | | | | +---------+------+------+------+------+------+------+------+------+ | AELIST | 75 | 1001 | THRU | 1075 | 1101 | THRU | 1109 | 1201 | +---------+------+------+------+------+------+------+------+------+ | | 1202 | | | | | | | | +---------+------+------+------+------+------+------+------+------+ Notes ----- 1. These entries are referenced by the AESURF entry. 2. When the THRU option is used, all intermediate grid points must exist. The word THRU may not appear in field 3 or 9 (2 or 9 for continuations). 3. Intervening blank fields are not allowed. """ type = 'AELIST'
[docs] @classmethod def _init_from_empty(cls): return AELIST(1, [1])
def __init__(self, sid: int, elements: list[int], comment: str='') -> None: """ Creates an AELIST card, which defines the aero boxes for an AESURF/SPLINEx. Parameters ---------- sid : int unique id elements : list[int, ..., int]; int list of box ids comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment if isinstance(elements, integer_types): elements = [elements] if isinstance(elements, np.ndarray): assert len(elements.shape) == 1, elements.shape elements = elements.tolist() if not isinstance(elements, (list, tuple)): raise TypeError('AELIST; type(elements)=%s and must be a list/tuple' % type(elements)) #: Set identification number. (Integer > 0) self.sid = sid #: List of aerodynamic boxes generated by CAERO1 entries to define a #: surface. (Integer > 0 or 'THRU') self.elements = expand_thru(elements) self.elements.sort()
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an AELIST card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ sid = integer(card, 1, 'sid') elements = fields(integer_string_or_blank, card, 'eid', i=2, j=len(card)) elements2 = [elem for elem in elements if elem is not None] return AELIST(sid, elements2, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model, xref_errors): pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" pass
[docs] def clean_ids(self): self.elements = list(set(self.elements)) self.elements.sort()
[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 = ['AELIST', self.sid] + self.elements 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 AEPARM(BaseCard): """ Defines a general aerodynamic trim variable degree-of-freedom (aerodynamic extra point). The forces associated with this controller will be derived from AEDW, AEFORCE and AEPRESS input data. +--------+----+--------+-------+ | 1 | 2 | 3 | 4 | +========+====+========+=======+ | AEPARM | ID | LABEL | UNITS | +--------+----+--------+-------+ | AEPARM | 5 | THRUST | LBS | +--------+----+--------+-------+ """ type = 'AEPARM' _field_map = { 1: 'id', 2: 'label', 3: 'units' }
[docs] @classmethod def _init_from_empty(cls): aeparm_id = 1 label = 'name' units = '' return AEPARM(aeparm_id, label, units)
def __init__(self, aeparm_id: int, label: str, units: str, comment: str='') -> None: """ Creates an AEPARM card, which defines a new trim variable. Parameters ---------- aeparm_id : int the unique id label : str the variable name units : str unused by Nastran comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.aeparm_id = aeparm_id self.label = label self.units = units
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an AEPARM card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ aeparm_id = integer(card, 1, 'aeparm_id') label = string(card, 2, 'label') units = card.field(3) units = '' if units is None else units assert len(card) <= 4, f'len(AEPARM card) = {len(card):d}\ncard={card}' return AEPARM(aeparm_id, label, units, comment=comment)
@classmethod def add_op2_data(cls, data, comment: str=''): """ Adds an AEPARM card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ aeparm_id = data[0] label = data[1] units = data[2] assert len(data) == 3, 'data = %s' % data return AEPARM(aeparm_id, label, units, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model, xref_errors): pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" pass
[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 = ['AEPARM', self.aeparm_id, self.label, self.units] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.raw_fields() return self.comment + print_card_8(card)
[docs] class AESURF(BaseCard): """ Specifies an aerodynamic control surface as a member of the set of aerodynamic extra points. The forces associated with this controller will be derived from rigid rotation of the aerodynamic model about the hinge line(s) and from AEDW, AEFORCE and AEPRESS input data. The mass properties of the control surface can be specified using an AESURFS entry. +--------+--------+-------+-------+-------+--------+--------+--------+--------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+========+=======+=======+=======+========+========+========+========+ | AESURF | ID | LABEL | CID1 | ALID1 | CID2 | ALID2 | EFF | LDW | +--------+--------+-------+-------+-------+--------+--------+--------+--------+ | | CREFC | CREFS | PLLIM | PULIM | HMLLIM | HMULIM | TQLLIM | TQULIM | +--------+--------+-------+-------+-------+--------+--------+--------+--------+ """ type = 'AESURF' _field_map = { 1: 'aesid', 2: 'label', 3: 'cid1', 4: 'alid1', 5: 'cid2', 6: 'alid2', 7: 'eff', 8: 'ldw', 9: 'crefc', 10: 'crefs', 11: 'pllim', 12: 'pulim', 13: 'hmllim', 14: 'hmulim', 15: 'tqllim', 16: 'tqulim', }
[docs] @classmethod def _init_from_empty(cls): aesurf_id = 1 label = 'name' cid1 = 1 aelist_id1 = 1 return AESURF(aesurf_id, label, cid1, aelist_id1, cid2=None, aelist_id2=None, eff=1.0, ldw='LDW', crefc=1.0, crefs=1.0, pllim=-np.pi/2., pulim=np.pi/2., hmllim=None, hmulim=None, tqllim=0, tqulim=0)
def __init__(self, aesurf_id: int, label: str, cid1: int, aelist_id1: int, cid2: Optional[int]=None, aelist_id2: Optional[int]=None, eff: float=1.0, ldw: str='LDW', crefc: float=1.0, crefs: float=1.0, pllim: float=-np.pi/2., pulim: float=np.pi/2., # hinge moment lower/upper limits hmllim: Optional[float]=None, hmulim: Optional[float]=None, # TABLEDi deflection limits vs. dynamic pressure tqllim: int=0, tqulim: int=0, comment: str='') -> None: """ Creates an AESURF card, which defines a control surface Parameters ---------- aesurf_id : int controller number label : str controller name cid1 / cid2 : int / None coordinate system id for primary/secondary control surface aelist_id1 / aelist_id2 : int / None AELIST id for primary/secondary control surface (alid1/alid2) eff : float; default=1.0 Control surface effectiveness ldw : str; default='LDW' Linear downwash flag; ['LDW', 'NODLW'] crefc : float; default=1.0 reference chord for the control surface crefs : float; default=1.0 reference area for the control surface pllim / pulim : float; default=-pi/2 / pi/2 Lower/Upper deflection limits for the control surface in radians hmllim / hmulim : float; default=None Lower/Upper hinge moment limits for the control surface in force-length units tqllim / tqulim : int; default=0 Set identification numbers of TABLEDi entries that provide the lower/upper deflection limits for the control surface as a function of the dynamic pressure comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment #: Controller identification number self.aesurf_id = aesurf_id #: Controller name. self.label = label #: Identification number of a rectangular coordinate system with a #: y-axis that defines the hinge line of the control surface #: component. self.cid1 = cid1 #: Identification of an AELIST Bulk Data entry that identifies all #: aerodynamic elements that make up the control surface #: component. (Integer > 0) self.aelist_id1 = aelist_id1 self.cid2 = cid2 self.aelist_id2 = aelist_id2 #: Control surface effectiveness. See Remark 4. (Real != 0.0; #: Default=1.0) self.eff = eff #: Linear downwash flag. See Remark 2. #: (Character, one of LDW or NOLDW; Default=LDW). self.ldw = ldw #: Reference chord length for the control surface. (Real>0.0; #: Default=1.0) self.crefc = crefc #: Reference surface area for the control surface. (Real>0.0; #: Default=1.0) self.crefs = crefs #: Lower and upper deflection limits for the control surface in #: radians. (Real, Default = +/- pi/2) self.pllim = pllim self.pulim = pulim #: Lower and upper hinge moment limits for the control surface in #: force-length units. (Real, Default = no limit) -> 1e8 self.hmllim = hmllim self.hmulim = hmulim #: Set identification numbers of TABLEDi entries that provide the #: lower and upper deflection limits for the control surface as a #: function of the dynamic pressure. (Integer>0, Default = no limit) self.tqllim = tqllim self.tqulim = tqulim self.cid1_ref = None self.cid2_ref = None self.aelist_id1_ref = None self.aelist_id2_ref = None self.tqllim_ref = None self.tqulim_ref = None assert self.ldw in {'LDW', 'NOLDW'}, self.ldw #def validate(self): #assert self.ldw in {'LDW', 'NOLDW'}, self.ldw
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an AESURF card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ aesurf_id = integer(card, 1, 'aesid') label = string(card, 2, 'label') cid1 = integer(card, 3, 'cid1') aelist_id1 = integer(card, 4, 'alid1') cid2 = integer_or_blank(card, 5, 'cid2') aelist_id2 = integer_or_blank(card, 6, 'alid2') eff = double_or_blank(card, 7, 'eff', default=1.0) ldw = string_or_blank(card, 8, 'ldw', default='LDW') crefc = double_or_blank(card, 9, 'crefc', default=1.0) crefs = double_or_blank(card, 10, 'crefs', default=1.0) pllim = double_or_blank(card, 11, 'pllim', default=-np.pi / 2.) pulim = double_or_blank(card, 12, 'pulim', default=np.pi / 2.) # hinge moment limit hmllim = double_or_blank(card, 13, 'hmllim') hmulim = double_or_blank(card, 14, 'hmulim') # lower and upper deflection limits for the control surface as a # function of the dynamic pressure. tqllim = integer_or_blank(card, 15, 'tqllim', default=0) tqulim = integer_or_blank(card, 16, 'tqulim', default=0) assert len(card) <= 17, f'len(AESURF card) = {len(card):d}\ncard={card}' return AESURF(aesurf_id, label, cid1, aelist_id1, cid2, aelist_id2, eff, ldw, crefc, crefs, pllim, pulim, hmllim, hmulim, tqllim, tqulim, comment=comment)
def _verify(self, xref: bool) -> None: """ Verifies all methods for this object work Parameters ---------- xref : bool has this model been cross referenced """ cid1 = self.Cid1() aelist1 = self.Aelist_id1() assert isinstance(cid1, integer_types), f'cid1={cid1!r}' assert isinstance(aelist1, integer_types), f'aelist1={aelist1!r}' cid2 = self.Cid2() aelist2 = self.Aelist_id2() assert cid2 is None or isinstance(cid2, integer_types), f'cid2={cid2!r}' assert aelist2 is None or isinstance(aelist2, integer_types), f'aelist2={aelist2!r}'
[docs] def Cid1(self) -> int: if self.cid1_ref is not None: return self.cid1_ref.cid return self.cid1
[docs] def Cid2(self) -> Optional[int]: if self.cid2_ref is not None: return self.cid2_ref.cid return self.cid2
[docs] def Aelist_id1(self) -> int: if self.aelist_id1_ref is not None: return self.aelist_id1_ref.sid return self.aelist_id1
[docs] def Aelist_id2(self) -> Optional[int]: if self.aelist_id2_ref is not None: return self.aelist_id2_ref.sid return self.aelist_id2
[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 AESURF eid=%s' % self.label self.cid1_ref = model.Coord(self.cid1, msg=msg) if self.cid2 is not None: self.cid2_ref = model.Coord(self.cid2, msg=msg) self.aelist_id1_ref = model.AELIST(self.aelist_id1, msg=msg) if self.aelist_id2: self.aelist_id2_ref = model.AELIST(self.aelist_id2, msg=msg) if self.tqllim: # integer self.tqllim_ref = model.TableD(self.tqllim, msg=msg) if self.tqulim: # integer self.tqulim_ref = model.TableD(self.tqulim, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by AESURF aesid=%s' % self.aesurf_id self.cid1_ref = model.safe_coord(self.cid1, self.aesurf_id, xref_errors, msg=msg) if self.cid2 is not None: self.cid2_ref = model.safe_coord(self.cid2, self.aesurf_id, xref_errors, msg=msg) self.aelist_id1_ref = model.safe_aelist(self.aelist_id1, self.aesurf_id, xref_errors, msg=msg) if self.aelist_id2: self.aelist_id2_ref = model.safe_aelist(self.aelist_id2, self.aesurf_id, xref_errors, msg=msg) if self.tqllim: # integer self.tqllim_ref = model.safe_tabled(self.tqllim, self.aesurf_id, xref_errors, msg=msg) if self.tqulim: # integer self.tqulim_ref = model.safe_tabled(self.tqulim, self.aesurf_id, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.cid1 = self.Cid1() self.cid2 = self.Cid2() self.cid1_ref = None self.cid2_ref = None self.aelist_id1 = self.Aelist_id1() self.aelist_id2 = self.Aelist_id2() self.aelist_id1_ref = None self.aelist_id2_ref = None #self.tqulim #self.tqllim self.tqllim_ref = None self.tqulim_ref = None
[docs] def update(self, unused_model, maps): coord_map = maps['coord'] aelist_map = maps['aelist'] self.cid1 = coord_map[self.cid1] if self.cid2: self.cid2 = coord_map[self.cid2] self.aelist_id1 = aelist_map[self.aelist_id1] if self.aelist_id2: self.aelist_id2 = aelist_map[self.aelist_id2]
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fieldsreset_camera[int/float/str] the fields that define the card """ list_fields = ['AESURF', self.aesurf_id, self.label, self.Cid1(), self.Aelist_id1(), self.Cid2(), self.Aelist_id2(), self.eff, self.ldw, self.crefc, self.crefs, self.pllim, self.pulim, self.hmllim, self.hmulim, self.tqllim, self.tqulim] return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list[int/float/str] the fields that define the card """ eff = set_blank_if_default(self.eff, 1.0) ldw = set_blank_if_default(self.ldw, 'LDW') crefc = set_blank_if_default(self.crefc, 1.0) crefs = set_blank_if_default(self.crefs, 1.0) pllim = set_blank_if_default(self.pllim, -np.pi / 2.) pulim = set_blank_if_default(self.pulim, np.pi / 2.) tqllim = set_blank_if_default(self.tqllim, 0) tqulim = set_blank_if_default(self.tqulim, 0) list_fields = ['AESURF', self.aesurf_id, self.label, self.Cid1(), self.Aelist_id1(), self.Cid2(), self.Aelist_id2(), eff, ldw, crefc, crefs, pllim, pulim, self.hmllim, self.hmulim, tqllim, tqulim] 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 AESURFS(BaseCard): """ Optional specification of the structural nodes associated with an aerodynamic control surface that has been defined on an AESURF entry. The mass associated with these structural nodes define the control surface moment(s) of inertia about the hinge line(s). Specifies rigid body motions to be used as trim variables in static aeroelasticity. +---------+------+-------+---+-------+---+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +=========+======+=======+===+=======+===+=======+ | AESURFS | ID | LABEL | | LIST1 | | LIST2 | +---------+------+-------+---+-------+---+-------+ | AESURFS | 6001 | ELEV | | 6002 | | 6003 | +---------+------+-------+---+-------+---+-------+ """ type = 'AESURFS'
[docs] @classmethod def _init_from_empty(cls): aesid = 1 label = 'name' list1 = 1 list2 = 2 return AESURFS(aesid, label, list1, list2)
def __init__(self, aesid: int, label: str, list1: int, list2: int=0, comment: str='') -> None: """ Creates an AESURFS card Parameters ---------- aesid : int the unique id label : str the AESURF name list1 : int the list (SET1) of node ids for the primary control surface(s) on the AESURF card list2 : int; default=0 the list (SET1) of node ids for the secondary control surface(s) on the AESURF card comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.aesid = aesid self.label = label self.list1 = list1 self.list2 = list2 self.label_ref = None self.list1_ref = None self.list2_ref = None
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds an AESURFS card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ aesid = integer(card, 1, 'ID') label = string(card, 2, 'label') list1 = integer(card, 4, 'list1') list2 = integer_or_blank(card, 6, 'list2', default=0) assert len(card) <= 7, f'len(AESURFS card) = {len(card):d}\ncard={card}' return AESURFS(aesid, label, list1, list2, comment=comment)
@classmethod def add_op2_data(cls, data, comment: str=''): aesid = data[0] label = data[1] list1 = data[2] list2 = data[3] assert len(data) == 4, 'data = %s' % data return AESURFS(aesid, label, list1, list2, 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 = f', which is required by AESURFS aesid={self.aesid}' self.label_ref = model.AESurf(self.label, msg) self.list1_ref = model.Set(self.list1, msg) self.list1_ref.cross_reference_set(model, 'Node', msg) if self.list2 != 0: self.list2_ref = model.Set(self.list2, msg=msg) self.list2_ref.cross_reference_set(model, 'Node', msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = f', which is required by AESURFS aesid={self.aesid}' self.label_ref = model.safe_aesurf(self.label, msg) try: self.list1_ref = model.Set(self.list1, msg=msg) self.list1_ref.cross_reference_set(model, 'Node', msg) except KeyError: pass if self.list2 != 0: try: self.list2_ref = model.Set(self.list2, msg=msg) self.list2_ref.cross_reference_set(model, 'Node', msg) except KeyError: pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.label = self.Label() self.list1 = self.List1() self.list2 = self.List2() self.label_ref = None self.list1_ref = None self.list2_ref = None
[docs] def Label(self) -> str: return aesurf_label(self.label_ref, self.label)
[docs] def List1(self) -> int: return set_id(self.list1_ref, self.list1)
[docs] def List2(self) -> int: return set_id(self.list2_ref, self.list2)
[docs] def raw_fields(self) -> list: """ Gets the fields in their unmodified form Returns ------- fields : list[int/float/str] the fields that define the card """ list_fields = ['AESURFS', self.aesid, self.Label(), None, self.List1(), None, self.List2()] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.raw_fields() return self.comment + print_card_8(card)
CAERO1_MSG = """ +--------+-----+-----+----+-------+--------+--------+--------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+====+=======+========+========+========+======+ | CAERO1 | EID | PID | CP | NSPAN | NCHORD | LSPAN | LCHORD | IGID | +--------+-----+-----+----+-------+--------+--------+--------+------+ | | X1 | Y1 | Z1 | X12 | X4 | Y4 | Z4 | X43 | +--------+-----+-----+----+-------+--------+--------+--------+------+""".strip()
[docs] class CAERO1(BaseCard): """ Defines an aerodynamic macro element (panel) in terms of two leading edge locations and side chords. This is used for Doublet-Lattice theory for subsonic aerodynamics and the ZONA51 theory for supersonic aerodynamics. +--------+-----+-----+----+-------+--------+--------+--------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+====+=======+========+========+========+======+ | CAERO1 | EID | PID | CP | NSPAN | NCHORD | LSPAN | LCHORD | IGID | +--------+-----+-----+----+-------+--------+--------+--------+------+ | | X1 | Y1 | Z1 | X12 | X4 | Y4 | Z4 | X43 | +--------+-----+-----+----+-------+--------+--------+--------+------+ :: 1 | \ | \ | \ | 4 | | | | 2------3 Attributes ---------- eid : int element id pid : int int : PAERO1 ID igroup : int Group number p1 : (1, 3) ndarray float xyz location of point 1 (leading edge; inboard) p4 : (1, 3) ndarray float xyz location of point 4 (leading edge; outboard) x12 : float distance along the flow direction from node 1 to node 2; (typically x, root chord) x43 : float distance along the flow direction from node 4 to node 3; (typically x, tip chord) cp : int int : coordinate system nspan : int int > 0 : N spanwise boxes distributed evenly int = 0 : use lchord nchord : int int > 0 : N chordwise boxes distributed evenly int = 0 : use lchord lspan : int int > 0 : AEFACT reference for non-uniform nspan int = 0 : use nspan lchord : int int > 0 : AEFACT reference for non-uniform nchord int = 0 : use nchord comment : str; default='' a comment for the card """ type = 'CAERO1' _field_map = { 1: 'sid', 2: 'pid', 3: 'cp', 4: 'nspan', 5: 'nchord', 6: 'lspan', 7: 'lchord', 8: 'igroup', 12: 'x12', 16: 'x43', } _properties = ['_field_map', 'shape', 'xy', 'min_max_eid', 'npanels'] def _get_field_helper(self, n: int) -> int | float: """ Gets complicated parameters on the CAERO1 card Parameters ---------- n : int the field number to update Returns ------- value : int/float the value for the appropriate field """ if n == 9: out = self.p1[0] elif n == 10: out = self.p1[1] elif n == 11: out = self.p1[2] elif n == 13: out = self.p4[0] elif n == 14: out = self.p4[1] elif n == 15: out = self.p4[2] else: # pragma: no cover raise KeyError('Field %r is an invalid CAERO1 entry.' % n) return out def _update_field_helper(self, n, value): """ Updates complicated parameters on the CAERO1 card Parameters ---------- n : int the field number to update value : int/float the value for the appropriate field """ if n == 9: self.p1[0] = value elif n == 10: self.p1[1] = value elif n == 11: self.p1[2] = value elif n == 13: self.p4[0] = value elif n == 14: self.p4[1] = value elif n == 15: self.p4[2] = value else: raise KeyError('Field %r=%r is an invalid CAERO1 entry.' % (n, value))
[docs] @classmethod def _init_from_empty(cls): eid = 1 pid = 1 igroup = 1 p1 = [0., 0., 0.] x12 = 1. p4 = [0., 10., 0.] x43 = 0.5 return CAERO1(eid, pid, igroup, p1, x12, p4, x43, cp=0, nspan=0, lspan=0, nchord=0, lchord=0)
[docs] def _finalize_hdf5(self, encoding): """hdf5 helper function""" self.p1 = np.asarray(self.p1) self.p4 = np.asarray(self.p4)
def __init__(self, eid: int, pid: int, igroup: int, p1: NDArray3float, x12: float, p4: NDArray3float, x43: float, cp: int=0, nspan: int=0, lspan: int=0, nchord: int=0, lchord: int=0, comment: str=''): """ Defines a CAERO1 card, which defines a simplified lifting surface (e.g., wing/tail). Parameters ---------- eid : int element id pid : int, PAERO1 int : PAERO1 ID PAERO1 : PAERO1 object (xref) igroup : int Group number p1 : (1, 3) ndarray float xyz location of point 1 (leading edge; inboard) p4 : (1, 3) ndarray float xyz location of point 4 (leading edge; outboard) x12 : float distance along the flow direction from node 1 to node 2; (typically x, root chord) x43 : float distance along the flow direction from node 4 to node 3; (typically x, tip chord) cp : int, Coord; default=0 int : coordinate system Coord : Coordinate object (xref) nspan : int; default=0 int > 0 : N spanwise boxes distributed evenly int = 0 : use lchord nchord : int; default=0 int > 0 : N chordwise boxes distributed evenly int = 0 : use lchord lspan : int, AEFACT; default=0 int > 0 : AEFACT reference for non-uniform nspan int = 0 : use nspan AEFACT : AEFACT object (xref) lchord : int, AEFACT; default=0 int > 0 : AEFACT reference for non-uniform nchord int = 0 : use nchord AEFACT : AEFACT object (xref) comment : str; default='' a comment for the card """ BaseCard.__init__(self) if cp is None: cp = 0 if lspan is None: lspan = 0 if nspan is None: nspan = 0 if nchord is None: nchord = 0 if lchord is None: lchord = 0 p1 = np.asarray(p1) p4 = np.asarray(p4) if comment: self.comment = comment #: Element identification number self.eid = eid #: Property identification number of a PAERO2 entry. self.pid = pid #: Coordinate system for locating point 1. self.cp = cp self.nspan = nspan self.lspan = lspan self.nchord = nchord self.lchord = lchord self.igroup = igroup self.p1 = p1 self.x12 = x12 self.p4 = p4 self.x43 = x43 self.pid_ref = None self.cp_ref = None self.lchord_ref = None self.lspan_ref = None self.ascid_ref = None self.box_ids = None #self._init_ids() #TODO: make this work here?
[docs] def validate(self): # we won't write the card if it's bad try: card_str = str(self) except: card_str = '' msg = '' is_failed = False if not isinstance(self.igroup, int): msg += 'igroup=%s and must be an integer\n' % self.igroup is_failed = True if not isinstance(self.cp, int): msg += 'cp=%s and must be an integer\n' % self.cp is_failed = True if not isinstance(self.x12, float): msg += 'x12=%s and must be a float\n' % self.x12 is_failed = True if not isinstance(self.x12, float): msg += 'x43=%s and must be a float\n' % self.x43 is_failed = True if not isinstance(self.p1, np.ndarray): msg += 'p1=%s and must be a numpy array\n' % self.p1 is_failed = True if not isinstance(self.p4, np.ndarray): msg += 'p4=%s and must be a numpy array\n' % self.p4 is_failed = True if is_failed: # types are borked, so don't print the card msg += CAERO1_MSG msg += self.get_stats() raise ValueError(msg) try: if len(self.p1) != 3: msg += 'p1=%s and must be a numpy array of length=3\n' % self.p1 is_failed = True if len(self.p4) != 3: msg += 'p4=%s and must be a numpy array of length=3\n' % self.p4 is_failed = True if is_failed: msg += CAERO1_MSG msg += card_str raise ValueError(msg) except TypeError: # len(self.p1) # TypeError: len() of unsized object is_failed = True if is_failed: msg += CAERO1_MSG #msg += card_str: # can't show this because it's messed up due to bad types/length msg += self.get_stats() raise ValueError(msg) if self.x12 <= 0. and self.x43 <= 0.: msg += f'X12={self.x12} and X43={self.x43}; one must be greater than or equal to 0\n' is_failed = True if self.nspan == 0 and self.lspan == 0: msg += 'NSPAN or LSPAN must be greater than 0; nspan=%r nlspan=%s\n' % ( self.nspan, self.lspan) is_failed = True if self.nspan != 0 and self.lspan != 0: msg += 'Either NSPAN or LSPAN must 0; nspan=%r nlspan=%s\n' % ( self.nspan, self.lspan) is_failed = True if self.nchord == 0 and self.lchord == 0: msg += 'NCHORD or LCHORD must be greater than 0; nchord=%r lchord=%s\n' % ( self.nchord, self.lchord) is_failed = True if self.nchord != 0 and self.lchord != 0: msg += 'Either NCHORD or LCHORD must 0; nchord=%r lchord=%s\n' % ( self.nchord, self.lchord) is_failed = True if is_failed: msg += str(self) msg += CAERO1_MSG raise ValueError(msg) assert len(self.p1) == 3, 'p1=%s' % self.p1 assert len(self.p4) == 3, 'p4=%s' % self.p4 # calculating area; assuming coordinate transformations don't matter p1 = self.p1 p4 = self.p4 p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) a = p3 - p1 b = p4 - p2 area = 0.5 * np.linalg.norm(np.cross(a, b)) assert area > 0, f'eid={self.eid} p1={p1} p2={p2} p3={p3} p4={p4} area={area}'
def _verify(self, xref: bool) -> None: cp = self.Cp() pid = self.Pid() assert isinstance(cp, integer_types), f'cp={cp!r}' assert isinstance(pid, integer_types), f'pid={pid!r}' self.flip_normal() self.flip_normal() if xref: ids = self.aefact_ids area = self.area() assert isinstance(area, float_types), f'area={area!r}'
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a CAERO1 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') pid = integer(card, 2, 'pid') cp = integer_or_blank(card, 3, 'cp', 0) nspan = integer_or_blank(card, 4, 'nspan', 0) nchord = integer_or_blank(card, 5, 'nchord', 0) lspan = integer_or_blank(card, 6, 'lspan', 0) lchord = integer_or_blank(card, 7, 'lchord', 0) igroup = integer(card, 8, 'igid') p1 = np.array([ double_or_blank(card, 9, 'x1', 0.0), double_or_blank(card, 10, 'y1', 0.0), double_or_blank(card, 11, 'z1', 0.0)]) x12 = double_or_blank(card, 12, 'x12', 0.) p4 = np.array([ double_or_blank(card, 13, 'x4', 0.0), double_or_blank(card, 14, 'y4', 0.0), double_or_blank(card, 15, 'z4', 0.0)]) x43 = double_or_blank(card, 16, 'x43', 0.) assert len(card) <= 17, f'len(CAERO1 card) = {len(card):d}\ncard={card}' return CAERO1(eid, pid, igroup, p1, x12, p4, x43, cp=cp, nspan=nspan, lspan=lspan, nchord=nchord, lchord=lchord, comment=comment)
[docs] @classmethod def add_quad(cls, eid: int, pid: int, span: int, chord: int, igroup: int, p1: np.ndarray, p2: np.ndarray, p3: np.ndarray, p4: np.ndarray, cp: int=0, spanwise: str='y', comment: str='') -> CAERO1: r""" :: 1 | \ | \ | \ | 4 | | | | 2------3 TODO: CP not handled correctly """ assert isinstance(p1, (list, np.ndarray)), p1 assert isinstance(p4, (list, np.ndarray)), p4 x12 = p2[0] - p1[0] x43 = p3[0] - p4[0] nspan = 0 lspan = 0 nchord = 0 lchord = 0 if spanwise.lower() == 'y': y41 = p4[1] - p1[1] y32 = p3[1] - p2[1] dspan = max(y41, y32) elif spanwise.lower() == 'z': y41 = p4[2] - p1[2] y32 = p3[2] - p2[2] dspan = max(y41, y32) else: # pragma: no cover raise NotImplementedError('spanwise=%r; expected=[y, z]' % spanwise.lower()) dx = max(x12, x43) if isinstance(span, integer_types): nspan = span elif isinstance(span, AEFACT): lspan = span.sid elif isinstance(span, float): nspan = int(math.ceil(dspan / span)) if nspan <= 0: msg = 'y41=%s y32=%s; dspan=%s span=%s nspan=%s; nspan must be greater than 0' % ( y41, y32, dspan, span, nspan) raise ValueError(msg) else: raise TypeError(span) if isinstance(chord, integer_types): nchord = chord elif isinstance(chord, AEFACT): lchord = chord.sid elif isinstance(chord, float): nchord = int(math.ceil(dx / chord)) if nchord <= 0: msg = 'x12=%s x43=%s; dx=%s chord=%s nchord=%s; nchord must be greater than 0' % ( x12, x43, dx, chord, nchord) raise ValueError(msg) else: # pragma: no cover raise TypeError(chord) return CAERO1(eid, pid, igroup, p1, x12, p4, x43, cp=cp, nspan=nspan, lspan=lspan, nchord=nchord, lchord=lchord, comment=comment)
[docs] def flip_normal(self) -> None: """flips the CAERO1 normal vector""" self.p1, self.p4 = self.p4, self.p1 self.x12, self.x43 = self.x43, self.x12
[docs] def _init_ids(self, dtype: str='int32') -> np.ndarray: """ Fill `self.box_ids` with the sub-box ids. Shape is (nchord, nspan) """ nchord, nspan = self.shape assert nchord >= 1, 'nchord=%s' % nchord assert nspan >= 1, 'nspan=%s' % nspan self.box_ids = np.zeros((nchord, nspan), dtype=dtype) npanels = nchord * nspan try: self.box_ids = np.arange(self.eid, self.eid + npanels, dtype=dtype).reshape(nspan, nchord) except OverflowError: if dtype == 'int64': # we already tried int64 msg = 'eid=%s lchord=%s lspan=%s nchord=%s' % ( self.eid, self.lchord, self.lspan, nchord) raise OverflowError(msg) self._init_ids(dtype='int64') return self.box_ids
@property def aefact_ids(self) -> list[int]: aefact_ids = [] lchord = self.get_LChord() lspan = self.get_LSpan() if lchord: aefact_ids.append(lchord) if lspan: aefact_ids.append(lspan) return aefact_ids
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Pid(self) -> int: return paero_id(self.pid_ref, self.pid)
[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 = f', which is required by CAERO1 eid={self.eid}' self.pid_ref = model.PAero(self.pid, msg=msg) self.cp_ref = model.Coord(self.cp, msg=msg) #model.log if model.sol in [144, 145, 146, 200]: self.ascid_ref = model.Acsid(msg=msg) else: self.ascid_ref = model.safe_acsid(msg=msg) if self.nchord == 0: assert isinstance(self.lchord, integer_types), self.lchord self.lchord_ref = model.AEFact(self.lchord, msg) if self.nspan == 0: assert isinstance(self.lspan, integer_types), self.lspan self.lspan_ref = model.AEFact(self.lspan, msg) self._init_ids()
[docs] def safe_cross_reference(self, model: BDF, xref_errors): """ Cross links the card so referenced cards can be extracted directly Parameters ---------- model : BDF() the BDF object """ msg = ', which is required by CAERO1 eid=%s' % self.eid try: self.pid_ref = model.PAero(self.pid, msg=msg) except KeyError: pass self.cp_ref = model.safe_coord(self.cp, self.eid, xref_errors, msg=msg) self.ascid_ref = model.safe_acsid(msg=msg) if self.nchord == 0: assert isinstance(self.lchord, integer_types), self.lchord self.lchord_ref = model.safe_aefact(self.lchord, self.eid, xref_errors, msg) if self.nspan == 0: assert isinstance(self.lspan, integer_types), self.lspan self.lspan_ref = model.safe_aefact(self.lspan, self.eid, xref_errors, msg) self._init_ids()
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.pid = self.Pid() self.cp = self.Cp() self.lchord = self.get_LChord() self.lspan = self.get_LSpan() self.pid_ref = None self.cp_ref = None self.lchord_ref = None self.lspan_ref = None self.ascid_ref = None
[docs] def update(self, maps: dict[str, dict[int, int]]) -> None: """ Cross links the card so referenced cards can be extracted directly """ #msg = ', which is required by CAERO1 eid=%s' % self.eid paero_map = maps['paero'] coord_map = maps['coord'] aefact_map = maps['aefact'] self.pid = paero_map[self.pid] self.cp = coord_map[self.cp] #self.acsid = coord_map[self.acsid] # AERO/AEROS card if self.nchord == 0: self.lchord = aefact_map[self.lchord] if self.nspan == 0: self.lspan = aefact_map[self.lspan]
#self._init_ids(model) @property def min_max_eid(self) -> list[int]: """ Gets the min and max element ids of the CAERO card Returns ------- min_max_eid : (2, ) list The [min_eid, max_eid] """ nchord, nspan = self.shape return [self.eid, self.eid + nchord * nspan]
[docs] def get_leading_edge_points(self) -> tuple[np.ndarray, np.ndarray]: """gets the leading edge points""" if self.cp_ref is None and self.cp == 0: p1 = self.p1 p4 = self.p4 else: p1 = self.cp_ref.transform_node_to_global(self.p1) p4 = self.cp_ref.transform_node_to_global(self.p4) return p1, p4
[docs] def get_points(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ Get the 4 corner points for the CAERO card Returns ------- p1234 : (4, 3) list List of 4 corner points in the global frame """ p1, p4 = self.get_leading_edge_points() if self.ascid_ref is None: # yes, this really does list + array addition p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) else: p2 = p1 + self.ascid_ref.transform_vector_to_global(np.array([self.x12, 0., 0.])) p3 = p4 + self.ascid_ref.transform_vector_to_global(np.array([self.x43, 0., 0.])) return (p1, p2, p3, p4)
[docs] def area(self) -> float: p1, p2, p3, p4 = self.get_points() a = p3 - p1 b = p4 - p2 area = 0.5 * np.linalg.norm(np.cross(a, b)) assert area > 0, f'eid={self.eid} p1={p1} p2={p2} p3={p3} p4={p4} area={area}' return area
[docs] def span_split(self, span_percentage: float) -> tuple[CAERO1, CAERO1]: """ :: 1 | \ | 14 | | \ | | 4 | | | | | | 2--23--3 Parameters ---------- span_percentage : float the value to split at p1 = [0., 0., 0.] x12 = 1.0 p4 = [0., 10., 0.] x43 = 1.0 nspan = 10 span_percentage = 0.10 nspan1 = nspan * span_percentage = 1 nspan2 = nspan * (1 - span_percentage) = 9 """ assert 0. < span_percentage < 1.0, span_percentage assert self.nspan > 0, self.nspan p1 = self.p1 p4 = self.p4 p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) p14: np.ndarray = p1 * (1 - span_percentage) + p4 * span_percentage p23: np.ndarray = p2 * (1 - span_percentage) + p3 * span_percentage x14_23: float = p14[0] - p23[0] nspan_a = max(1, int(self.nspan * span_percentage)) nspan_b = max(1, int(self.nspan * (1 - span_percentage))) eid_a = self.eid eid_b = self.eid + 1 pid = self.pid igroup = self.igroup cp = self.cp nchord = self.nchord lchord = self.lchord lspan = self.lspan comment_a = (self.comment + ' A').strip() comment_b = (self.comment + ' B').strip() caero_a = CAERO1(eid_a, pid, igroup, p1, self.x12, p14, x14_23, cp=cp, nspan=nspan_a, lspan=lspan, nchord=nchord, lchord=lchord, comment=comment_a) caero_b = CAERO1(eid_b, pid, igroup, p14, x14_23, p4, self.x43, cp=cp, nspan=nspan_b, lspan=lspan, nchord=nchord, lchord=lchord, comment=comment_b) return caero_a, caero_b
[docs] def span_chord_split(self, span_percentage: float, chord_percentage: float, iy: int=1) -> list[CAERO1]: """ :: 1 | \ | 14 | | \ | | 4 | | | 12-c--43 | | | | | | 2--23--3 Parameters ---------- span_percentage : float the value to split at chord_percentage : float the value to split at iy : int; default=1 the dimension to consider as y (if y and z vary, 1/2 will workj) p1 = [0., 0., 0.] x12 = 1.0 p4 = [0., 10., 0.] x43 = 1.0 nspan = 10 span_percentage = 0.10 nspan1 = nspan * span_percentage = 1 nspan2 = nspan * (1 - span_percentage) = 9 """ assert 0. <= span_percentage < 1.0, span_percentage assert 0. <= chord_percentage < 1.0, chord_percentage assert iy in {1, 2}, iy assert self.nspan > 0, self.nspan assert self.nchord > 0, self.nchord p1 = self.p1 p4 = self.p4 p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) p14: np.ndarray = p1 * (1 - span_percentage) + p4 * span_percentage p23: np.ndarray = p2 * (1 - span_percentage) + p3 * span_percentage p12: np.ndarray = p1 * (1 - chord_percentage) + p2 * chord_percentage p43: np.ndarray = p4 * (1 - chord_percentage) + p3 * chord_percentage pc: np.ndarray = p14 * (1 - chord_percentage) + p23 * chord_percentage x1_12: float = p12[0] - p1[0] x12_2: float = p2[0] - p12[0] x14_c: float = pc[0] - p14[0] xc_23: float = p23[0] - pc[0] x4_43: float = p43[0] - p4[0] x43_3: float = p3[0] - p43[0] nspan_a = max(1, int(self.nspan * span_percentage)) nspan_b = max(1, int(self.nspan * (1 - span_percentage))) nchord_a = max(1, int(self.nchord * chord_percentage)) nchord_b = max(1, int(self.nchord * (1 - chord_percentage))) eid_a = self.eid eid_b = self.eid pid = self.pid igroup = self.igroup cp = self.cp lchord = self.lchord lspan = self.lspan eid_a = self.eid eid_b = self.eid + 1 eid_c = self.eid + 2 eid_d = self.eid + 3 comment_a = (self.comment + ' A').strip() comment_b = (self.comment + ' B').strip() comment_c = (self.comment + ' C').strip() comment_d = (self.comment + ' D').strip() # LE caero_a = CAERO1(eid_a, pid, igroup, p1, x1_12, p14, x14_c, cp=cp, nspan=nspan_a, lspan=lspan, nchord=nchord_a, lchord=lchord, comment=comment_a) caero_b = CAERO1(eid_b, pid, igroup, p14, x14_c, p4, x4_43, cp=cp, nspan=nspan_b, lspan=lspan, nchord=nchord_a, lchord=lchord, comment=comment_b) y1 = p1[iy] y4 = p4[iy] y14 = p14[iy] y1_14 = abs(y14 - y1) y14_4 = abs(y4 - y14) caeros = [] #print(f'y1 = {y1}') #print(f'y4 = {y4}') #print(f'y1_14 = {y1_14}') #print(f'y14_4 = {y14_4}') inboard_le_chord = (x1_12 > 0. or x14_c > 0.0) outboard_le_chord = (x14_c > 0.0 or x4_43 > 0.) #print(f'inboard_le_chord = {inboard_le_chord}') #print(f'outboard_le_chord = {outboard_le_chord}') if (x1_12 > 0. or x14_c > 0.0) and y1_14 > 0.0: caeros.append(caero_a) if (x14_c > 0.0 or x4_43 > 0.) and y14_4 > 0.0: caeros.append(caero_b) # TE caero_c = CAERO1(eid_c, pid, igroup, p12, x12_2, pc, xc_23, cp=cp, nspan=nspan_a, lspan=lspan, nchord=nchord_b, lchord=lchord, comment=comment_c) caero_d = CAERO1(eid_d, pid, igroup, pc, xc_23, p43, x43_3, cp=cp, nspan=nspan_b, lspan=lspan, nchord=nchord_b, lchord=lchord, comment=comment_d) inboard_te_chord = (x12_2 > 0. or xc_23 > 0.0) outboard_te_chord = (xc_23 > 0.0 or x43_3 > 0.) #print(f'inboard_te_chord = {inboard_te_chord}') #print(f'outboard_te_chord = {outboard_te_chord}') if (x12_2 > 0. or xc_23 > 0.0) and y1_14 > 0.0: caeros.append(caero_c) if (xc_23 > 0.0 or x43_3 > 0.) and y14_4 > 0.0: caeros.append(caero_d) return caeros
[docs] def generic_split(self, caero_project: CAERO1) -> list[CAERO1]: # pragma: no cover """ the intention of this method is to vary the chord/span across the panel :: 1 | \ | \ | \ A--B 4 | | | | | | | | | | | | 2--C---3 Parameters ---------- caero_project : CAERO1 the caero to project TODO: not done... """ # p1x = np.array([self.p1[0], 0., 0.]) p1 = self.p1.copy() # - p1x p4 = self.p4.copy() # - p1x p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) q1 = caero_project.p1.copy() # - p1x q4 = caero_project.p4.copy() # - p1x q2 = q1 + np.array([caero_project.x12, 0., 0.]) q3 = q4 + np.array([caero_project.x43, 0., 0.]) # https://stackoverflow.com/questions/41797953/how-to-find-the-intersection-of-two-lines-given-a-point-on-each-line-and-a-paral #x = x1 + t1 * dx1 #y = y1 + t1 * dy1 #x = x2 + t2 * dx2 #y = y2 + t2 * dy2 # #x = x0a + dxa×t #y = y0a + dya×t #x = x0b + dxb×u #y = y0b + dyb×u def points_in_polygon(polygoni: np.ndarray, pts: np.ndarray) -> np.ndarray: contour2 = np.vstack((polygoni[1:], polygoni[:1])) test_diff = contour2 - polygoni mask1 = (pts[:, None] == polygoni).all(-1).any(-1) m1 = (polygoni[:, 1] > pts[:, None, 1]) != (contour2[:, 1] > pts[:, None, 1]) slope = ((pts[:, None, 0] - polygoni[:, 0]) * test_diff[:, 1]) - ( test_diff[:, 0] * (pts[:, None, 1] - polygoni[:, 1])) m2 = slope == 0 mask2 = (m1 & m2).any(-1) m3 = (slope < 0) != (contour2[:, 1] < polygoni[:, 1]) m4 = m1 & m3 counti = np.count_nonzero(m4, axis=-1) mask3 = ~(counti % 2 == 0) mask = mask1 | mask2 | mask3 return mask iy = 1 polygon = np.vstack([p1, p2, p3, p4])[:, [0, iy]] points = np.vstack([q1, q2, q3, q4])[:, [0, iy]] iq1, iq2, iq3, iq4 = points_in_polygon(polygon, points) caeros = [] nq = (iq1 + iq2 + iq3 + iq4) if nq == 4: # # p1--------p4 # | | # | q1-q4 | # | | | | # | q2-q3 | # | | # p2--------p3 raise RuntimeError('all points q in p') elif nq == 3: # p1--------p4 # | | # | q1-----|---q4 # | | | / # | | / # | q2-q3 | # | | # p2--------p3 raise RuntimeError('one point q not in p') elif nq == 2: # p1--------p4 # | | # | q1-----|---q4 # | | | | # | | | | # | q2-----|---q3 # | | # p2--------p3 raise RuntimeError('two point q not in p') elif nq == 1: # p1--------p4 # | | # | q1-----*---q4 # | | | | # p2--*-----p3 | # | | # q2---------q3 raise RuntimeError('primary case') elif nq == 0: return caeros else: # pragma: no cover raise RuntimeError(f'nq = {nq}') return caeros
[docs] def get_box_index(self, box_id: int) -> tuple[int, int]: """ Get the index of ``self.box_ids`` that corresponds to the given box id. Parameters ----------- box_id : int Box id to get the index of. Returns -------- index : tuple Index of ``self.box_ids`` that corresponds to the given box id. """ if box_id not in self.box_ids: self._box_id_error(box_id) index = np.where(self.box_ids == box_id) index = (index[0][0], index[1][0]) return index
[docs] def get_box_quarter_chord_center(self, box_id: int) -> np.ndarray: """ The the location of the quarter chord of the box along the centerline. Parameters ----------- box_id : int Box id. Returns -------- xyz_quarter_chord : ndarray Location of box quarter chord in global. """ return self._get_box_x_chord_center(box_id, 0.25)
[docs] def get_box_mid_chord_center(self, box_id: int) -> np.ndarray: """ The the location of the mid chord of the box along the centerline. Parameters ----------- box_id : int Box id. Returns -------- xyz_mid_chord : ndarray Location of box mid chord in global. """ return self._get_box_x_chord_center(box_id, 0.5)
[docs] def _get_box_x_chord_center(self, box_id: int, x_chord: float) -> np.ndarray: """ The location of the x_chord of the box along the centerline. """ if self.lchord != 0 or self.lspan != 0: raise NotImplementedError() ispan, ichord = self.get_box_index(box_id) le_vector = self.p4 - self.p1 delta_xyz = le_vector * ((ispan + 0.5)/self.nspan) yz = delta_xyz[1:3] + self.p1[1:3] chord = ((ispan + 0.5)/self.nspan) * (self.x43 - self.x12) + self.x12 x = (ichord + x_chord)/self.nchord * chord + self.p1[0] + delta_xyz[0] return np.array([x, yz[0], yz[1]])
[docs] def _box_id_error(self, box_id: int): """ Raise box_id IndexError. """ min_box = self.box_ids[0, 0] max_box = self.box_ids[-1, -1] msg = f'{box_id:d} not in range of aero box ids\nRange: {min_box:d} to {max_box:d}' raise IndexError(msg)
@property def npanels(self) -> int: nchord, nspan = self.shape return nchord * nspan @property def shape(self) -> tuple[int, int]: """returns (nelements_nchord, nelements_span)""" if self.nchord == 0: x = self.lchord_ref.fractions nchord = len(x) - 1 else: nchord = self.nchord if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan if nchord < 1 or nspan < 1: msg = 'CAERO1 eid=%s nchord=%s nspan=%s lchord=%s lspan=%s' % ( self.eid, self.nchord, self.nspan, self.lchord, self.lspan) raise RuntimeError(msg) return nchord, nspan
[docs] def get_panel_npoints_nelements(self) -> tuple[int, int]: """ Gets the number of sub-points and sub-elements for the CAERO card Returns ------- npoints : int The number of nodes for the CAERO nelements : int The number of elements for the CAERO """ nchord, nspan = self.shape nelements = nchord * nspan npoints = (nchord + 1) * (nspan + 1) return npoints, nelements
@property def xy(self) -> tuple[np.ndarray, np.ndarray]: """ Returns ------- x : (nchord,) ndarray The percentage x location in the chord-wise direction of each panel y : (nspan,) ndarray The percentage y location in the span-wise direction of each panel """ if self.nchord == 0: x = self.lchord_ref.fractions nchord = len(x) - 1 else: nchord = self.nchord x = np.linspace(0., 1., nchord + 1) if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan y = np.linspace(0., 1., nspan + 1) if nchord < 1 or nspan < 1: msg = 'CAERO1 eid=%s nchord=%s nspan=%s lchord=%s lspan=%s' % ( self.eid, self.nchord, self.nspan, self.lchord, self.lspan) raise RuntimeError(msg) return x, y
[docs] def panel_points_elements(self) -> tuple[np.ndarray, np.ndarray]: """ Gets the sub-points and sub-elements for the CAERO1 card Returns ------- points : (nnodes,3) ndarray of floats the array of points elements : (nelements,4) ndarray of integers the array of point ids """ p1, p2, p3, p4 = self.get_points() x, y = self.xy # We're reordering the points, so we get the node ids and element ids # to be consistent with Nastran. This is only useful if you're plotting # aero panel forces # # this gives us chordwise panels and chordwise nodes return points_elements_from_quad_points(p1, p4, p3, p2, y, x, dtype='int32')
# correct paneling, wrong orientation #return points_elements_from_quad_points(p1, p2, p3, p4, y, x, dtype='int32')
[docs] def set_points(self, points: list[float] | list[np.ndarray]) -> None: self.p1 = points[0] p2 = points[1] p3 = points[2] self.p4 = points[3] self.x12 = p2[0] - self.p1[0] self.x43 = p3[0] - self.p4[0] assert self.x12 >= 0., 'p1=%s p2=%s' % (self.p1, p2) assert self.x43 >= 0., 'p4=%s p3=%s' % (self.p4, p3) assert self.x12 > 0. or self.x43 > 0., 'points=%s' % (points) self.p1 = np.asarray(self.p1) self.p4 = np.asarray(self.p4)
[docs] def panel_integration_point(self) -> np.ndarray: """ Finally, consider the j-set of aerodynamic control points. The j-set is not a user set; it is a notational set to identify aerodynamic matrices used in the solution processing. Physically, these are points on the structure where the downwash vectors are computed. As with the k-set, the location of these points is a function of the aerodynamic method employed: - For Doublet-Lattice boxes, the control point is at the 75% chordwise station and spanwise center of the box (CAERO1; M<1) - For ZONA51 boxes, the control point is at the 95% chordwise station and the spanwise center of the box (CAERO1; M>1) - For Doublet-Lattice interference and slender body elements, the control point is along the axis of the element and at 50% of its length (CAERO2) - For all other theories, the aerodynamic control points are at the same physical location as the aerodynamic grid points discussed above """ if self.cp_ref is None: assert self.cp is None, f'cp={self.cp}; cp_ref=None' points, elements = self.panel_points_elements() n1 = elements[:, 0] n2 = elements[:, 1] n3 = elements[:, 2] n4 = elements[:, 3] p1 = points[n1, :] p4 = points[n2, :] p2 = points[n3, :] p3 = points[n4, :] le = (p1 + p4) * 0.5 # halfspan te = (p2 + p3) * 0.5 chord = te[:, 0] - le[:, 0] int_pt = le.copy() int_pt[:, 0] += chord * 0.75 return int_pt
[docs] def get_dlm_box_properties(self) -> dict: """Get DLM box geometric properties for all panels in this CAERO1. Returns a dict with arrays indexed per panel: - box_ids: (npanel,) int, Nastran element IDs - chords: (npanel,) float, box chord lengths - spans: (npanel,) float, box span widths - areas: (npanel,) float, box areas - normals: (npanel, 3) float, outward panel normals - receiving_points: (npanel, 3) float, 3/4-chord collocation points - sending_points: (npanel, 3) float, 1/4-chord doublet points """ points, elements = self.panel_points_elements() npanel = len(elements) # Corner node indices per element (pyNastran ordering: p1, p4, p2, p3) n1 = elements[:, 0] # inboard LE n2 = elements[:, 1] # outboard LE n3 = elements[:, 2] # outboard TE n4 = elements[:, 3] # inboard TE p_ile = points[n1] # inboard leading edge p_ole = points[n2] # outboard leading edge p_ote = points[n3] # outboard trailing edge p_ite = points[n4] # inboard trailing edge # Leading and trailing edge midpoints le_mid = 0.5 * (p_ile + p_ole) te_mid = 0.5 * (p_ite + p_ote) # Chord vector and length chord_vec = te_mid - le_mid chords = np.linalg.norm(chord_vec, axis=1) # Span vector and width span_vec_le = p_ole - p_ile spans = np.linalg.norm(span_vec_le, axis=1) # Area (cross product magnitude of diagonals / 2, or average chord * span) diag1 = p_ote - p_ile diag2 = p_ite - p_ole cross = np.cross(diag1, diag2) areas = 0.5 * np.linalg.norm(cross, axis=1) # Normal (unit normal from cross product) normals = cross / np.linalg.norm(cross, axis=1, keepdims=True) # Receiving point: 3/4-chord, mid-span (DLM collocation point) receiving_points = le_mid + 0.75 * chord_vec # Sending point: 1/4-chord, mid-span (DLM doublet line) sending_points = le_mid + 0.25 * chord_vec # Centroids (average of 4 corners) centroids = 0.25 * (p_ile + p_ole + p_ote + p_ite) # Box IDs: sequential from this CAERO1's EID box_ids = np.arange(self.eid, self.eid + npanel, dtype=np.int32) return { 'box_ids': box_ids, 'chords': chords, 'spans': spans, 'areas': areas, 'normals': normals, 'centroids': centroids, 'receiving_points': receiving_points, 'sending_points': sending_points, }
[docs] def shift(self, dxyz: np.ndarray) -> None: """shifts the aero panel""" self.p1 += dxyz self.p4 += dxyz
[docs] def plot(self, ax: AxesSubplot, show_eid: bool=True, show_nid: bool=True, name: str='', color: str='C0', fontsize: int=10, is_3d: bool=False, alpha: float=1.0) -> None: """plots the panels""" points, elements = self.panel_points_elements() centroids = [] for eid, elem in enumerate(elements[:, [0, 1, 2, 3, 0]]): pointsi = points[elem][:, [0, 1, 2]] x = pointsi[:, 0] y = pointsi[:, 1] z = pointsi[:, 2] if is_3d: ax.plot(x, y, z, color=color, alpha=alpha) else: ax.plot(x, y, color=color, alpha=alpha) box_id = self.eid + eid # if is_3d: # centroid = (x[:-1].sum() / 4, y[:-1].sum() / 4, z[:-1].sum() / 4) # else: centroid = (x[:-1].sum() / 4, y[:-1].sum() / 4) if show_eid: elem_name = f'e{box_id}' ax.annotate(elem_name, centroid, ha='center', color=color) if show_nid: for pid, point in zip(elem, pointsi): point_name = f'p{pid}' ax.annotate(point_name, point, ha='center', color=color) centroids.append(centroid) mean_centroid = np.mean(centroids, axis=0) if name: ax.annotate(name, mean_centroid, ha='center', color='k', fontsize=fontsize, weight='bold')
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list the fields that define the card """ lchord = self.get_LChord() lspan = self.get_LSpan() list_fields = (['CAERO1', self.eid, self.Pid(), self.Cp(), self.nspan, self.nchord, lspan, lchord, self.igroup, ] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) return list_fields
[docs] def get_LChord(self) -> int: return aefact_id(self.lchord_ref, self.lchord)
[docs] def get_LSpan(self) -> int: return aefact_id(self.lspan_ref, self.lspan)
[docs] def repr_fields(self) -> list: """ Gets the fields in their simplified form Returns ------- fields : LIST The fields that define the card """ cp = set_blank_if_default(self.Cp(), 0) nspan = set_blank_if_default(self.nspan, 0) nchord = set_blank_if_default(self.nchord, 0) lchord = set_blank_if_default(self.get_LChord(), 0) lspan = set_blank_if_default(self.get_LSpan(), 0) list_fields = (['CAERO1', self.eid, self.Pid(), cp, nspan, nchord, lspan, lchord, self.igroup] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) 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 CAERO2(BaseCard): """ Aerodynamic Body Connection Defines aerodynamic slender body and interference elements for Doublet-Lattice aerodynamics. +--------+-----+-----+----+-----+------+-----+------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+====+=====+======+=====+======+======+ | CAERO2 | EID | PID | CP | NSB | NINT | LSB | LINT | IGID | +--------+-----+-----+----+-----+------+-----+------+------+ | | X1 | Y1 | Z1 | X12 | | | | | +--------+-----+-----+----+-----+------+-----+------+------+ """ type = 'CAERO2' _field_map = { 1: 'sid', 2: 'pid', 3: 'cp', 4: 'nsb', 5: 'lsb', 6: 'nint', 7: 'lint', 8: 'igroup', 12: 'x12', } _properties = ['nboxes'] def _get_field_helper(self, n): """ Gets complicated parameters on the CAERO2 card Parameters ---------- n : int The field number to update Returns ------- value : int, float, None The value for the appropriate field """ if n == 9: out = self.p1[0] elif n == 10: out = self.p1[1] elif n == 11: out = self.p1[2] else: raise KeyError('Field %r is an invalid CAERO2 entry.' % n) return out def _update_field_helper(self, n: int, value): """ Updates complicated parameters on the CAERO2 card Parameters ---------- n : int The field number to update value : int, float, None The value for the appropriate field """ if n == 9: self.p1[0] = value elif n == 10: self.p1[1] = value elif n == 11: self.p1[2] = value else: raise KeyError('Field %r=%r is an invalid CAERO2 entry.' % (n, value))
[docs] @classmethod def _init_from_empty(cls): eid = 1 pid = 1 igroup = 1 p1 = [0., 0., 0.] x12 = 10. return CAERO2(eid, pid, igroup, p1, x12, cp=0, nsb=0, nint=0, lsb=0, lint=0)
def __init__(self, eid: int, pid: int, igroup: int, p1: np.ndarray, x12: float, cp: int=0, nsb: int=0, nint: int=0, lsb: int=0, lint: int=0, comment: str=''): """ Defines a CAERO2 card, which defines a slender body (e.g., fuselage/wingtip tank). Parameters ---------- eid : int element id pid : int int : PAERO2 ID igroup : int Group number p1 : (1, 3) ndarray float xyz location of point 1 (forward position) x12 : float length of the CAERO2 cp : int; default=0 int : coordinate system nsb : int; default=0 Number of slender body elements lsb : int; default=0 AEFACT id for defining the location of the slender body elements nint : int; default=0 Number of interference elements lint : int; default=0 AEFACT id for defining the location of interference elements comment : str; default='' a comment for the card """ BaseCard.__init__(self) if lsb is None: lsb = 0 if lint is None: lint = 0 if nint is None: nint = 0 if nsb is None: nsb = 0 if comment: self.comment = comment p1 = np.asarray(p1) #: Element identification number self.eid = eid #: Property identification number of a PAERO2 entry. self.pid = pid #: Coordinate system for locating point 1. self.cp = cp #: Number of slender body elements. If NSB > 0, then NSB equal #: divisions are assumed; if zero or blank, specify a list of #: divisions in LSB. (Integer >= 0) self.nsb = nsb #: Number of interference elements. If NINT > 0, then NINT equal #: divisions are assumed; if zero or blank, specify a list of #: divisions in LINT. (Integer >= 0) self.nint = nint #: ID of an AEFACT Bulk Data entry for slender body division #: points; used only if NSB is zero or blank. (Integer >= 0) self.lsb = lsb #: ID of an AEFACT data entry containing a list of division #: points for interference elements; used only if NINT is zero #: or blank. (Integer > 0) self.lint = lint #: Interference group identification. Aerodynamic elements with #: different IGIDs are uncoupled. (Integer >= 0) self.igroup = igroup #: Location of point 1 in coordinate system CP self.p1 = p1 #: Length of body in the x-direction of the aerodynamic coordinate #: system. (Real > 0) self.x12 = x12 self.pid_ref = None self.cp_ref = None self.lint_ref = None self.lsb_ref = None self.ascid_ref = None
[docs] def validate(self) -> None: #print('nsb=%s lsb=%s' % (self.nsb, self.lsb)) #print('nint=%s lint=%s' % (self.nint, self.lint)) assert isinstance(self.lsb, integer_types), self.lsb assert isinstance(self.lint, integer_types), self.lint assert len(self.p1) == 3, 'CAERO2: p1=%s' % self.p1 if self.nsb == 0 and self.lsb == 0: msg = 'CAERO2: nsb=%s lsb=%s; nsb or lsb must be > 0' % (self.nsb, self.lsb) raise ValueError(msg) if self.nint == 0 and self.lint == 0: msg = 'CAERO2: nint=%s lint=%s; nint or lint must be > 0' % (self.nint, self.lint) raise ValueError(msg) assert len(self.p1) == 3, 'CAERO2: p1=%s' % self.p1 assert isinstance(self.igroup, integer_types) and self.igroup > 0, f'CAERO2: igroup={self.igroup}'
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a CAERO2 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') pid = integer(card, 2, 'pid') cp = integer_or_blank(card, 3, 'cp', default=0) nsb = integer_or_blank(card, 4, 'nsb', default=0) nint = integer_or_blank(card, 5, 'nint', default=0) lsb = integer_or_blank(card, 6, 'nsb=%s lsb' % nsb, default=0) lint = integer_or_blank(card, 7, 'nint=%s lint' % nint, default=0) igroup = integer(card, 8, 'igroup') p1 = np.array([ double_or_blank(card, 9, 'x1', default=0.0), double_or_blank(card, 10, 'y1', default=0.0), double_or_blank(card, 11, 'z1', default=0.0)]) x12 = double_or_blank(card, 12, 'x12', default=0.) assert len(card) <= 13, f'len(CAERO2 card) = {len(card):d}\ncard={card}' return CAERO2(eid, pid, igroup, p1, x12, cp=cp, nsb=nsb, nint=nint, lsb=lsb, lint=lint, comment=comment)
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Pid(self) -> int: return paero_id(self.pid_ref, self.pid)
@property def aefact_ids(self) -> list[int]: aefact_ids = [] lsb = self.Lsb() lint = self.Lint() if lsb: aefact_ids.append(lsb) if lint: aefact_ids.append(lint) return aefact_ids
[docs] def Lsb(self) -> int: # AEFACT return aefact_id(self.lsb_ref, self.lsb)
[docs] def Lint(self) -> int: # AEFACT return aefact_id(self.lint_ref, self.lint)
@property def nboxes(self) -> int: if self.nsb > 0: return self.nsb return len(self.lsb_ref.fractions) # AEFACT
[docs] def _init_ids(self, dtype: str='int32'): self.box_ids = np.arange(0, self.nboxes, dtype=dtype)
[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 = f', which is required by CAERO2 eid={self.eid}' self.pid_ref = model.PAero(self.pid, msg=msg) # links to PAERO2 self.cp_ref = model.Coord(self.cp, msg=msg) if self.nsb == 0: self.lsb_ref = model.AEFact(self.lsb, msg=msg) if self.nint == 0: self.lint_ref = model.AEFact(self.lint, msg=msg) self.ascid_ref = model.Acsid(msg=msg) self._init_ids()
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by CAERO2 eid=%s' % self.eid self.pid_ref = model.safe_paero(self.pid, self.eid, xref_errors, msg=msg) # links to PAERO2 self.cp_ref = model.safe_coord(self.cp, self.eid, xref_errors, msg=msg) if self.nsb == 0: self.lsb_ref = model.safe_aefact(self.lsb, self.eid, xref_errors, msg=msg) if self.nint == 0: self.lint_ref = model.safe_aefact(self.lint, self.eid, xref_errors, msg=msg) self.ascid_ref = model.safe_acsid(msg=msg) self._init_ids()
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.pid = self.Pid() self.cp = self.Cp() if self.nsb == 0: self.lsb = self.Lsb() if self.nint == 0: self.lint = self.Lint() self.pid_ref = None self.cp_ref = None self.lint_ref = None self.lsb_ref = None self.ascid_ref = None
[docs] def get_points(self) -> tuple[np.ndarray, np.ndarray]: """creates a 1D representation of the CAERO2""" p1 = self.cp_ref.transform_node_to_global(self.p1) p2 = p1 + self.ascid_ref.transform_vector_to_global(np.array([self.x12, 0., 0.])) #print("x12 = %s" % self.x12) #print("pcaero[%s] = %s" % (self.eid, [p1,p2])) return p1, p2
[docs] def get_leading_edge_points(self) -> [np.ndarray]: return [self.get_points()[0]]
[docs] def get_panel_npoints_nelements(self) -> tuple[int, int]: station = self.get_station() nx = len(station) - 1 npoints = len(station) return npoints, nx
[docs] def get_station(self) -> np.ndarray: if self.nsb == 0: station = self.lsb_ref.fractions nx = len(station) - 1 # print('xstation = ', xstation) else: nx = self.nsb station = np.linspace(0., nx, num=nx+1) # *dx? assert nx > 0, 'nx=%s' % nx return station
[docs] def get_points_elements_3d(self): """ Gets the points/elements in 3d space as CQUAD4s The idea is that this is used by the GUI to display CAERO panels. TODO: doesn't support the aero coordinate system """ paero2 = self.pid_ref station_new = self.get_station() nx_new = len(station_new) - 1 if self.nsb == 0: xstation = self.lsb_ref.fractions nx = len(xstation) - 1 #print('xstation = ', xstation) else: nx = self.nsb station = np.linspace(0., nx, num=nx+1) # *dx? assert nx > 0, 'nx=%s' % nx assert nx == nx_new, (nx, nx_new) #print('paero2 - pid=%s lrsb=%s lrib=%s' % (paero2.pid, paero2.lrsb, paero2.lrib)) if paero2.lrsb in [0, None]: radii_slender = np.ones(nx + 1) * paero2.width else: radii_slender = paero2.lrsb_ref.fractions # TODO: not supported if paero2.lrib in [0, None]: unused_radii_interference = np.ones(nx + 1) * paero2.width else: #print('lrib = ', paero2.lrib) unused_radii_interference = paero2.lrib_ref.fractions radii = radii_slender # TODO: not supported #theta_interference1 = paero2.theta1 #theta_interference2 = paero2.theta2 if self.nsb != 0: p1, p2 = self.get_points() L = p2 - p1 #print('L=%s nx=%s' % (L, nx)) dxyz = L / nx #print('dxyz\n%s' % (dxyz)) dx, dy, dz = dxyz xstation = station * dx ystation = station * dy zstation = station * dz else: p1, p2 = self.get_points() L = p2 - p1 dxi = xstation.max() - xstation.min() #print('L=%s nx=%s dxi=%s' % (L, nx, dxi)) xratio = xstation / dxi #print('xstation/dxi=%s' % xratio) dxyz = np.zeros((nx+1, 3)) for i, xr in enumerate(xratio): dxyz[i, :] = xr * L ystation = dxyz[:, 1] zstation = dxyz[:, 2] #get_xyz_station_2d(self): # I think this just lets you know what directions it can pivot in # and therefore doesn't affect visualization #assert paero2.orient == 'ZY', paero2.orient aspect_ratio = paero2.AR assert len(radii) == (nx + 1), 'len(radii)=%s nx=%s' % (len(radii), nx) if len(xstation) != (nx + 1): msg = 'len(xstation)=%s nx=%s\nxstation=%s\n%s' % ( len(xstation), nx, xstation, str(self)) raise RuntimeError(msg) xyz, elems = create_axisymmetric_body( xstation, ystation, zstation, radii, aspect_ratio, p1) assert xyz is not None, str(self) return xyz, elems
[docs] def set_points(self, points: tuple[np.ndarray, np.ndarray]): self.p1 = np.asarray(points[0]) p2 = np.asarray(points[1]) x12 = p2 - self.p1 self.x12 = x12[0]
[docs] def shift(self, dxyz): """shifts the aero panel""" self.p1 += dxyz
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list The fields that define the card """ list_fields = (['CAERO2', self.eid, self.Pid(), self.Cp(), self.nsb, self.nint, self.Lsb(), self.Lint(), self.igroup, ] + list(self.p1) + [self.x12]) return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list The fields that define the card """ cp = set_blank_if_default(self.Cp(), 0) nint = set_blank_if_default(self.nint, 0) lsb = set_blank_if_default(self.Lsb(), 0) lint = set_blank_if_default(self.Lint(), 0) list_fields = (['CAERO2', self.eid, self.Pid(), cp, self.nsb, nint, lsb, lint, self.igroup, ] + list(self.p1) + [self.x12]) 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 CAERO3(BaseCard): type = 'CAERO3' _properties = ['shape', 'xy']
[docs] @classmethod def _init_from_empty(cls): eid = 1 pid = 1 list_w = 1 p1 = [0., 0., 0.] p4 = [0., 10., 0.] x12 = 10. x43 = 10. return CAERO3(eid, pid, list_w, p1, x12, p4, x43, cp=0, list_c1=None, list_c2=None, comment='')
def __init__(self, eid: int, pid: int, list_w: int, p1: np.ndarray, x12: float, p4: np.ndarray, x43: float, cp: int=0, list_c1=None, list_c2=None, comment: str=''): """ Creates a CAERO2 card, which defines a wing with a wing break/cant. Parameters ---------- eid : int element id pid : int PAERO3 property id p1 : (3,) float ndarray ??? x12 : float distance from p1 to p3 p4 : (3,) float ndarray ??? x43 : float distance from p4 to p3 cp : int; default=0 coordinate system for locating point 1 list_w : int ??? list_c1 : int; default=None defines an AEFACT for ??? list_c2 : int; default=None defines an AEFACT for ??? comment : str; default='' a comment for the card 1--------5----------4 | | | 7--9---11 | | | | | 2-----8--10--12-----3 """ assert cp != 100 BaseCard.__init__(self) if comment: self.comment = comment p1 = np.asarray(p1) p4 = np.asarray(p4) #: Element identification number self.eid = eid #: Property identification number of a PAERO3 entry. self.pid = pid #: Coordinate system for locating point 1. self.cp = cp # aefacts self.list_w = list_w self.list_c1 = list_c1 self.list_c2 = list_c2 self.p1 = p1 self.x12 = x12 self.p4 = p4 self.x43 = x43 self.pid_ref = None self.cp_ref = None self.ascid_ref = None self.list_w_ref = None self.list_c1_ref = None self.list_c2_ref = None self.box_ids = None
[docs] def validate(self): assert len(self.p1) == 3, 'p1=%s' % self.p1 assert len(self.p4) == 3, 'p4=%s' % self.p4 assert self.x12 > 0., 'x12=%s' % self.x12 assert self.x43 >= 0., 'x43=%s' % self.x43 assert isinstance(self.cp, int), 'cp=%r' % self.cp
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a CAERO3 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') pid = integer(card, 2, 'pid') cp = integer_or_blank(card, 3, 'cp', 0) list_w = integer(card, 4, 'list_w') list_c1 = integer_or_blank(card, 5, 'list_c1') list_c2 = integer_or_blank(card, 6, 'list_c2') p1 = np.array([ double_or_blank(card, 9, 'x1', 0.0), double_or_blank(card, 10, 'y1', 0.0), double_or_blank(card, 11, 'z1', 0.0)]) x12 = double(card, 12, 'x12') p4 = np.array([ double_or_blank(card, 13, 'x4', 0.0), double_or_blank(card, 14, 'y4', 0.0), double_or_blank(card, 15, 'z4', 0.0)]) x43 = double_or_blank(card, 16, 'x43', 0.0) assert len(card) <= 17, f'len(CAERO3 card) = {len(card):d}\ncard={card}' return CAERO3(eid, pid, list_w, p1, x12, p4, x43, cp=cp, list_c1=list_c1, list_c2=list_c2, 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 = f', which is required by CAERO3 eid={self.eid}' self.pid_ref = model.PAero(self.pid, msg=msg) # links to PAERO3 self.cp_ref = model.Coord(self.cp, msg=msg) if self.list_w is not None: self.list_w_ref = model.AEFact(self.list_w, msg=msg) if self.list_c1_ref is not None: self.list_c1_ref = model.AEFact(self.list_c1, msg=msg) if self.list_c2 is not None: self.list_c2_ref = model.AEFact(self.list_c2, msg=msg) self.ascid_ref = model.Acsid(msg=msg) self._init_ids()
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = f', which is required by CAERO3 eid={self.eid}' self.pid_ref = model.safe_paero(self.pid, self.eid, xref_errors, msg=msg) # links to PAERO3 self.cp_ref = model.safe_coord(self.cp, self.eid, xref_errors, msg=msg) if self.list_w is not None: self.list_w_ref = model.safe_aefact(self.list_w, self.eid, xref_errors, msg=msg) if self.list_c1 is not None: self.list_c1_ref = model.safe_aefact(self.list_c1, self.eid, xref_errors, msg=msg) if self.list_c2 is not None: self.list_c2_ref = model.safe_aefact(self.list_c2, self.eid, xref_errors, msg=msg) try: self.ascid_ref = model.Acsid(msg=msg) except KeyError: model.log.warning('cannot find an aero coordinate system for %s' % msg) self._init_ids()
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.pid = self.Pid() self.cp = self.Cp() if self.list_w != self.List_w(): self.list_w = self.List_w() if self.list_c1 != self.List_c1(): self.list_c1 = self.List_c1() if self.list_c2 != self.List_c2(): self.list_c2 = self.List_c2() self.pid_ref = None self.cp_ref = None self.ascid_ref = None self.list_w_ref = None self.list_c1_ref = None self.list_c2_ref = None
[docs] def get_points(self): """ Get the 4 corner points for the CAERO card Returns ------- p1234 : (4, 3) list List of 4 corner points in the global frame """ p1 = self.cp_ref.transform_node_to_global(self.p1) p4 = self.cp_ref.transform_node_to_global(self.p4) p2 = p1 + self.ascid_ref.transform_vector_to_global(np.array([self.x12, 0., 0.])) p3 = p4 + self.ascid_ref.transform_vector_to_global(np.array([self.x43, 0., 0.])) return [p1, p2, p3, p4]
[docs] def area(self) -> float: p1, p2, p3, p4 = self.get_points() a = p3 - p1 b = p4 - p2 area = 0.5 * np.linalg.norm(np.cross(a, b)) assert area > 0, f'eid={self.eid} p1={p1} p2={p2} p3={p3} p4={p4} area={area}' return area
[docs] def panel_points_elements(self): """ Gets the sub-points and sub-elements for the CAERO card Returns ------- points : (nnodes,3) ndarray of floats the array of points elements : (nelements,4) ndarray of integers the array of point ids """ p1, p2, p3, p4 = self.get_points() x, y = self.xy return points_elements_from_quad_points(p1, p4, p3, p2, y, x, dtype='int32')
#return points_elements_from_quad_points(p1, p2, p3, p4, x, y, dtype='int32')
[docs] def get_panel_npoints_nelements(self) -> tuple[int, int]: """ Gets the number of sub-points and sub-elements for the CAERO card Returns ------- npoints : int The number of nodes for the CAERO nelements : int The number of elements for the CAERO """ nchord, nspan = self.shape nelements = nchord * nspan npoints = (nchord + 1) * (nspan + 1) return npoints, nelements
[docs] def _init_ids(self, dtype: str='int32') -> np.ndarray: """ Fill `self.box_ids` with the sub-box ids. Shape is (nchord, nspan) """ nchord, nspan = self.shape assert nchord >= 1, 'nchord=%s' % nchord assert nspan >= 1, 'nspan=%s' % nspan self.box_ids = np.zeros((nchord, nspan), dtype=dtype) npanels = nchord * nspan try: self.box_ids = np.arange(self.eid, self.eid + npanels, dtype=dtype).reshape(nspan, nchord) except OverflowError: if dtype == 'int64': # we already tried int64 msg = 'eid=%s lchord=%s lspan=%s nchord=%s' % ( self.eid, self.lchord, self.lspan, nchord) raise OverflowError(msg) self._init_ids(dtype='int64') return self.box_ids
@property def shape(self) -> tuple[int, int]: """returns (nelements_nchord, nelements_span)""" nchord = 2 nspan = self.pid_ref.nbox return nchord, nspan @property def xy(self) -> tuple[np.ndarray, np.ndarray]: """ Returns ------- x : (nchord,) ndarray The percentage x location in the chord-wise direction of each panel y : (nspan,) ndarray The percentage y location in the span-wise direction of each panel """ nchord, nspan = self.shape x = np.linspace(0., 1., nchord + 1) y = np.linspace(0., 1., nspan + 1) if nchord < 1 or nspan < 1: msg = 'CAERO3 eid=%s nchord=%s nspan=%s' % ( self.eid, nchord, nspan) raise RuntimeError(msg) return x, y
[docs] def plot(self, ax: AxesSubplot) -> None: """plots the panels""" points, elements = self.panel_points_elements() for eid, elem in enumerate(elements[:, [0, 1, 2, 3, 0]]): pointsi = points[elem][:, [0, 1]] x = pointsi[:, 0] y = pointsi[:, 1] ax.plot(x, y, color='b') box_id = self.eid + eid centroid = (x[:-1].sum() / 4, y[:-1].sum() / 4) elem_name = f'e{box_id}' ax.annotate(elem_name, centroid, ha='center') for pid, point in zip(elem, pointsi): point_name = f'p{pid}' ax.annotate(point_name, point, ha='center')
#def get_points_elements_3d(self): #""" #Gets the points/elements in 3d space as CQUAD4s #The idea is that this is used by the GUI to display CAERO panels. #TODO: doesn't support the aero coordinate system #""" #paero2 = self.pid_ref
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Pid(self) -> int: return paero_id(self.pid_ref, self.pid)
[docs] def List_w(self) -> int: return aefact_id(self.list_w_ref, self.list_w)
[docs] def List_c1(self) -> int: return aefact_id(self.list_c1_ref, self.list_c1)
[docs] def List_c2(self) -> int: return aefact_id(self.list_c2_ref, self.list_c2)
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list The fields that define the card """ list_fields = (['CAERO3', self.eid, self.Pid(), self.Cp(), self.List_w(), self.List_c1(), self.List_c2(), None, None] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list The fields that define the card """ cp = set_blank_if_default(self.Cp(), 0) list_fields = (['CAERO3', self.eid, self.Pid(), cp, self.List_w(), self.List_c1(), self.List_c2(), None, None] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) 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 CAERO4(BaseCard): """ Aerodynamic Macro-Strip Element Connection Defines an aerodynamic macro element for Strip theory. +--------+-----+-----+----+-------+--------+--------+--------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+====+=======+========+========+========+======+ | CAERO4 | EID | PID | CP | NSPAN | NCHORD | | | | +--------+-----+-----+----+-------+--------+--------+--------+------+ | | X1 | Y1 | Z1 | X12 | X4 | Y4 | Z4 | X43 | +--------+-----+-----+----+-------+--------+--------+--------+------+ """ type = 'CAERO4' _properties = ['shape', 'xy']
[docs] @classmethod def _init_from_empty(cls): eid = 1 pid = 1 p1 = [0., 0., 0.] p4 = [0., 10., 0.] x12 = 10. x43 = 10. return CAERO4(eid, pid, p1, x12, p4, x43, cp=0, nspan=0, lspan=0, comment='')
def __init__(self, eid, pid, p1, x12, p4, x43, cp=0, nspan=0, lspan=0, comment=''): """ Defines a CAERO4 card, which defines a strip theory surface. Parameters ---------- eid : int element id pid : int int : PAERO4 ID p1 : (1, 3) ndarray float xyz location of point 1 (leading edge; inboard) p4 : (1, 3) ndarray float xyz location of point 4 (leading edge; outboard) x12 : float distance along the flow direction from node 1 to node 2 (typically x, root chord) x43 : float distance along the flow direction from node 4 to node 3 (typically x, tip chord) cp : int; default=0 int : coordinate system nspan : int; default=0 int > 0 : N spanwise boxes distributed evenly int = 0 : use lchord lspan : int; default=0 int > 0 : AEFACT reference for non-uniform nspan int = 0 : use nspan comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment #: Element identification number self.eid = eid #: Property identification number of a PAERO4 entry. self.pid = pid #: Coordinate system for locating point 1. self.cp = cp self.nspan = nspan self.lspan = lspan self.p1 = np.asarray(p1) self.x12 = x12 self.p4 = np.asarray(p4) self.x43 = x43 self.pid_ref = None self.cp_ref = None self.lspan_ref = None self.box_ids = None
[docs] def validate(self): if self.nspan == 0 and self.lspan == 0: msg = 'NSPAN or LSPAN must be greater than 0; nspan=%r nlspan=%s\n' % ( self.nspan, self.lspan) raise RuntimeError(msg) assert len(self.p1) == 3, 'p1=%s' % self.p1 assert len(self.p4) == 3, 'p4=%s' % self.p4 assert self.x12 > 0., 'x12=%s' % self.x12 assert self.x43 >= 0., 'x43=%s' % self.x43
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a CAERO4 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') pid = integer(card, 2, 'pid') cp = integer_or_blank(card, 3, 'cp', default=0) nspan = integer_or_blank(card, 4, 'nspan', default=0) lspan = integer_or_blank(card, 5, 'lspan', default=0) p1 = np.array([ double_or_blank(card, 9, 'x1', default=0.0), double_or_blank(card, 10, 'y1', default=0.0), double_or_blank(card, 11, 'z1', default=0.0)]) x12 = double_or_blank(card, 12, 'x12', default=0.) p4 = np.array([ double_or_blank(card, 13, 'x4', default=0.0), double_or_blank(card, 14, 'y4', default=0.0), double_or_blank(card, 15, 'z4', default=0.0)]) x43 = double_or_blank(card, 16, 'x43', default=0.) assert len(card) <= 17, f'len(CAERO4 card) = {len(card):d}\ncard={card}' return CAERO4(eid, pid, p1, x12, p4, x43, cp=cp, nspan=nspan, lspan=lspan, comment=comment)
[docs] def get_points(self) -> list[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: p1 = self.cp_ref.transform_node_to_global(self.p1) p4 = self.cp_ref.transform_node_to_global(self.p4) p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) return [p1, p2, p3, p4]
[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 CAERO4 eid=%s' % self.eid self.cp_ref = model.Coord(self.cp, msg=msg) self.pid_ref = model.PAero(self.pid, msg=msg) # links to PAERO4 (not added) if self.nspan == 0: assert isinstance(self.lspan, integer_types), self.lspan self.lspan_ref = model.AEFact(self.lspan, msg) self._init_ids()
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by CAERO4 eid=%s' % self.eid self.pid_ref = model.safe_paero(self.pid, self.eid, xref_errors, msg=msg) # links to PAERO4 (not added) self.cp_ref = model.safe_coord(self.cp, self.eid, xref_errors, msg=msg) if self.nspan == 0: assert isinstance(self.lspan, integer_types), self.lspan self.lspan_ref = model.safe_aefact(self.lspan, self.eid, xref_errors, msg) self._init_ids()
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.pid = self.Pid() self.cp = self.Cp() if self.nspan == 0: self.lspan = self.get_LSpan() self.pid_ref = None self.cp_ref = None self.lspan_ref = None
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Pid(self) -> int: return (paero_id (self.pid_ref, self.pid))
[docs] def _init_ids(self, dtype: str='int32') -> np.ndarray: """ Fill `self.box_ids` with the sub-box ids. Shape is (nchord, nspan) """ nchord, nspan = self.shape assert nchord >= 1, 'nchord=%s' % nchord assert nspan >= 1, 'nspan=%s' % nspan self.box_ids = np.zeros((nchord, nspan), dtype=dtype) ichord = -1 ispan = -1 try: for ichord in range(nchord): for ispan in range(nspan): self.box_ids[ichord, ispan] = self.eid + ichord + ispan * nchord except OverflowError: if dtype == 'int64': msg = 'eid=%s ichord=%s ispan=%s nchord=%s' % ( self.eid, ichord, ispan, nchord) raise OverflowError(msg) self._init_ids(dtype='int64')
@property def shape(self): """returns (nelements_nchord, nelements_span)""" nchord = 1 if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan if nspan < 1: msg = 'CAERO4 eid=%s nspan=%s lspan=%s' % ( self.eid, self.nspan, self.lspan) raise RuntimeError(msg) return nchord, nspan
[docs] def get_panel_npoints_nelements(self): """ Gets the number of sub-points and sub-elements for the CAERO card Returns ------- npoints : int The number of nodes for the CAERO nelements : int The number of elements for the CAERO """ nchord, nspan = self.shape nelements = nchord * nspan npoints = (nchord + 1) * (nspan + 1) return npoints, nelements
@property def xy(self): """ Returns ------- x : (nchord,) ndarray The percentage x location in the chord-wise direction of each panel y : (nspan,) ndarray The percentage y location in the span-wise direction of each panel """ x = np.linspace(0., 1., num=2) # nchord=1 if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan y = np.linspace(0., 1., nspan + 1) if nspan < 1: msg = 'CAERO4 eid=%s nspan=%s lspan=%s' % ( self.eid, self.nspan, self.lspan) raise RuntimeError(msg) return x, y
[docs] def panel_points_elements(self): """ Gets the sub-points and sub-elements for the CAERO card Returns ------- points : (nnodes,3) ndarray of floats the array of points elements : (nelements,4) ndarray of integers the array of point ids """ p1, p2, p3, p4 = self.get_points() x, y = self.xy return points_elements_from_quad_points(p1, p2, p3, p4, x, y, dtype='int32')
[docs] def plot(self, ax: AxesSubplot) -> None: """plots the panels""" points, elements = self.panel_points_elements() for eid, elem in enumerate(elements[:, [0, 1, 2, 3, 0]]): pointsi = points[elem][:, [0, 1]] x = pointsi[:, 0] y = pointsi[:, 1] ax.plot(x, y, color='b') box_id = self.eid + eid centroid = (x[:-1].sum() / 4, y[:-1].sum() / 4) elem_name = f'e{box_id}' ax.annotate(elem_name, centroid, ha='center') for pid, point in zip(elem, pointsi): point_name = f'p{pid}' ax.annotate(point_name, point, ha='center')
[docs] def get_LSpan(self) -> int: return aefact_id(self.lspan_ref, self.lspan)
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list The fields that define the card """ list_fields = (['CAERO4', self.eid, self.Pid(), self.Cp(), self.nspan, self.get_LSpan(), None, None, None,] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list The fields that define the card """ cp = set_blank_if_default(self.Cp(), 0) nspan = set_blank_if_default(self.nspan, 0) lspan = set_blank_if_default(self.get_LSpan(), 0) list_fields = (['CAERO4', self.eid, self.Pid(), cp, nspan, lspan, None, None, None,] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) 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 CAERO5(BaseCard): """ Defines an aerodynamic macro element for Piston theory. +--------+------+------+-----+-------+-------+-------+--------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+======+======+=====+=======+=======+=======+========+=======+ | CAERO5 | EID | PID | CP | NSPAN | LSPAN | NTHRY | NTHICK | | +--------+------+------+-----+-------+-------+-------+--------+-------+ | | X1 | Y1 | Z1 | X12 | X4 | Y4 | Z4 | X43 | +--------+------+------+-----+-------+-------+-------+--------+-------+ | CAERO5 | 6000 | 6001 | 100 | | 315 | 0 | 0 | | +--------+------+------+-----+-------+-------+-------+--------+-------+ | | 0.0 | 0.0 | 0.0 | 1.0 | 0.2 | 1.0 | 0. | 0.8 | +--------+------+------+-----+-------+-------+-------+--------+-------+ """ type = 'CAERO5'
[docs] @classmethod def _init_from_empty(cls): eid = 1 pid = 1 p1 = [0., 0., 0.] p4 = [0., 10., 0.] x12 = 1. x43 = 0.5 nspan = 5 return CAERO5(eid, pid, p1, x12, p4, x43, cp=0, nspan=nspan, lspan=0, ntheory=0, nthick=0, comment='')
def __init__(self, eid: int, pid: int, p1: list[float], x12: float, p4: list[float], x43: float, cp: int=0, nspan: int=0, lspan: int=0, ntheory: int=0, nthick: int=0, comment: str=''): """ Defines a CAERO5 card, which defines elements for Piston theory (high supersonic flow where the normal Mach is less than 1). Parameters ---------- eid : int element id pid : int PAERO5 ID p1 : (1, 3) ndarray float xyz location of point 1 (leading edge; inboard) p4 : (1, 3) ndarray float xyz location of point 4 (leading edge; outboard) x12 : float distance along the flow direction from node 1 to node 2; (typically x, root chord) x43 : float distance along the flow direction from node 4 to node 3; (typically x, tip chord) cp : int; default=0 int : coordinate system nspan : int; default=0 int > 0 : N spanwise boxes distributed evenly int = 0 : use lchord lspan : int; default=0 int > 0 : AEFACT reference for non-uniform nspan int = 0 : use nspan ntheory : int; default=0 ??? valid_theory = {0, 1, 2} nthick : int; default=0 ??? comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment #: Element identification number self.eid = eid #: Property identification number of a PAERO5 entry. self.pid = pid #: Coordinate system for locating point 1. self.cp = cp self.nspan = nspan self.lspan = lspan self.ntheory = ntheory self.nthick = nthick self.p1 = np.asarray(p1, dtype='float64') self.x12 = x12 self.p4 = np.asarray(p4, dtype='float64') self.x43 = x43 self.pid_ref = None self.cp_ref = None self.lspan_ref = None assert self.x12 > 0., 'x12=%s' % self.x12 if not (self.nspan > 0 or self.lspan > 0): msg = 'nspan=%r or lspan=%r must be > 0' % (self.nspan, self.lspan) raise ValueError(msg) if not (self.x12 > 0.0 or self.x43 > 0.0): msg = 'x12=%r or x43=%r must be > 0.0' % (self.x12, self.x43) raise ValueError(msg)
[docs] def validate(self): assert self.ntheory in [0, 1, 2], 'ntheory=%r' % self.ntheory
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a CAERO5 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') pid = integer(card, 2, 'pid') cp = integer_or_blank(card, 3, 'cp', 0) nspan = integer_or_blank(card, 4, 'nspan', 0) lspan = integer_or_blank(card, 5, 'lspan', 0) ntheory = integer_or_blank(card, 6, 'ntheory', 0) nthick = integer_or_blank(card, 7, 'nthick') # 8 - blank p1 = np.array([ double_or_blank(card, 9, 'x1', 0.0), double_or_blank(card, 10, 'y1', 0.0), double_or_blank(card, 11, 'z1', 0.0)]) x12 = double(card, 12, 'x12') p4 = np.array([ double_or_blank(card, 13, 'x4', 0.0), double_or_blank(card, 14, 'y4', 0.0), double_or_blank(card, 15, 'z4', 0.0)]) x43 = double_or_blank(card, 16, 'x43', 0.0) assert len(card) <= 17, f'len(CAERO3 card) = {len(card):d}\ncard={card}' return CAERO5(eid, pid, p1, x12, p4, x43, cp=cp, nspan=nspan, lspan=lspan, ntheory=ntheory, nthick=nthick, 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 CAERO5 eid=%s' % self.eid self.pid_ref = model.PAero(self.pid, msg=msg) self.cp_ref = model.Coord(self.cp, msg=msg) if self.nspan == 0: self.lspan_ref = model.AEFact(self.lspan, msg=msg) self._init_ids()
[docs] def safe_cross_reference(self, model: BDF, xref_errors): xref_errors = {} msg = ', which is required by CAERO5 eid=%s' % self.eid self.pid_ref = model.safe_paero(self.pid, self.eid, xref_errors, msg=msg) self.cp_ref = model.safe_coord(self.cp, self.eid, xref_errors, msg=msg) if self.nspan == 0: self.lspan_ref = model.safe_aefact(self.lspan, self.eid, xref_errors, msg=msg) self._init_ids()
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.pid = self.Pid() self.cp = self.Cp() if self.nspan == 0: self.lspan = self.LSpan() self.pid_ref = None self.cp_ref = None self.lspan_ref = None
[docs] def get_points(self): p1 = self.cp_ref.transform_node_to_global(self.p1) p4 = self.cp_ref.transform_node_to_global(self.p4) p2 = p1 + np.array([self.x12, 0., 0.]) p3 = p4 + np.array([self.x43, 0., 0.]) return [p1, p2, p3, p4]
[docs] def get_panel_npoints_nelements(self): msg = 'CAERO5 eid=%s nspan=%s lspan=%s' % ( self.eid, self.nspan, self.lspan) if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan assert nspan >= 1, msg nchord = 1 nelements = nchord * nspan npoints = (nchord + 1) * (nspan + 1) return npoints, nelements
@property def nboxes(self) -> int: if self.nspan > 0: return self.nspan return len(self.lspan_ref.fractions) # AEFACT
[docs] def _init_ids(self, dtype: str='int32') -> None: npanels = self.nboxes nspan = npanels #self.box_ids = np.arange(0, self.nboxes, dtype=dtype) self.box_ids = np.arange(self.eid, self.eid + npanels, dtype=dtype).reshape(nspan, 1) # .T
[docs] def panel_points_elements(self): p1, p2, p3, p4 = self.get_points() msg = 'CAERO5 eid=%s nspan=%s lspan=%s' % ( self.eid, self.nspan, self.lspan) if self.nspan == 0: y = self.lspan_ref.fractions nspan = len(y) - 1 else: nspan = self.nspan y = np.linspace(0., 1., nspan + 1) assert nspan >= 1, msg x = np.array([0., 1.], dtype='float64') assert nspan >= 1, msg return points_elements_from_quad_points(p1, p4, p3, p2, y, x, dtype='int32')
[docs] def c1_c2(self, mach: float): p1, unused_p2, unused_p3, p4 = self.get_points() #i = p2 - p1 #ihat = i / np.linalg.norm(i) #k = np.cross(ihat, p4-p1) #khat = k / np.linalg.norm(k) #jhat = np.cross(khat, ihat) #b = self.p4 - self.p1 L = np.linalg.norm(p4 - p1) # b = L * cos(Lambda) # ci = L * sin(Lambda) c1 = None c2 = None if self.ntheory == 0: # piston theory pass elif self.ntheory == 1: raise NotImplementedError('ntheory=%s' % self.ntheory) #gamma = 1.4 #lambda_sweep = 0. #c1 = 1. #secL = 1 / np.cos(lambda_sweep) #secL2 = secL ** 2 #ma2_secL2 = mach ** 2 - secL2 #c1 = mach / ma2_secL2 ** 0.5 #c2 = (mach ** 4 * (gamma + 1) - 4 * secL2 * ma2_secL2) / (4 * ma2_secL2 ** 2) else: gamma = 1.4 # the advance in x ci = p4[0] - p1[0] # sweep angle lambda_sweep = np.arcsin(ci / L) sec_lambda = 1 / np.cos(lambda_sweep) sec_lambda2 = sec_lambda ** 2 ma2_sec_lambda2 = mach ** 2 - sec_lambda2 c1 = mach / ma2_sec_lambda2 ** 0.5 c2 = ( (mach ** 4 * (gamma + 1) - 4 * sec_lambda2 * ma2_sec_lambda2) / (4 * ma2_sec_lambda2 ** 2) ) return c1, c2
[docs] def plot(self, ax: AxesSubplot) -> None: """plots the panels""" points, elements = self.panel_points_elements() for eid, elem in enumerate(elements[:, [0, 1, 2, 3, 0]]): pointsi = points[elem][:, [0, 1]] x = pointsi[:, 0] y = pointsi[:, 1] ax.plot(x, y, color='b') box_id = self.eid + eid centroid = (x[:-1].sum() / 4, y[:-1].sum() / 4) elem_name = f'e{box_id}' ax.annotate(elem_name, centroid, ha='center') for pid, point in zip(elem, pointsi): point_name = f'p{pid}' ax.annotate(point_name, point, ha='center')
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Pid(self) -> int: return paero_id(self.pid_ref, self.pid)
[docs] def LSpan(self) -> int: return aefact_id(self.lspan_ref, self.lspan)
[docs] def raw_fields(self): list_fields = ( ['CAERO5', self.eid, self.Pid(), self.Cp(), self.nspan, self.LSpan(), self.ntheory, self.nthick, None,] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) return list_fields
[docs] def repr_fields(self): """ Gets the fields in their simplified form Returns ------- fields : list The fields that define the card """ nspan = self.nspan lspan = self.LSpan() ntheory = self.ntheory cp = set_blank_if_default(self.Cp(), 0) list_fields = ( ['CAERO5', self.eid, self.Pid(), cp, nspan, lspan, ntheory, self.nthick, None,] + list(self.p1) + [self.x12] + list(self.p4) + [self.x43]) 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 MONPNT1(BaseCard): """ +---------+---------+------+-----+-----+-------+------+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=========+======+=====+=====+=======+======+====+====+ | MONPNT1 | NAME | LABEL | +---------+---------+------+-----+-----+-------+------+----+----+ | | AXES | COMP | CP | X | Y | Z | CD | | +---------+---------+------+-----+-----+-------+------+----+----+ | MONPNT1 | WING155 | Wing Integrated Load to Butline 155 | +---------+---------+------+-----+-----+-------+------+----+----+ | | 34 | WING | | 0.0 | 155.0 | 15.0 | | | +---------+---------+------+-----+-----+-------+------+----+----+ """ type = 'MONPNT1'
[docs] @classmethod def _init_from_empty(cls): name = 'WING' label = 'Wing Integrated Load to Butline' axes = '6' aecomp_name = 'FLAP' xyz = [0., 1., 2.] return MONPNT1(name, label, axes, aecomp_name, xyz, cp=0, cd=None, comment='')
def __init__(self, name: str, label: str, axes: str, aecomp_name: str, xyz: list[float], cp: int=0, cd: Optional[int]=None, comment: str=''): """ Creates a MONPNT1 card Parameters ---------- name : str Character string of up to 8 characters identifying the monitor point label : str A string comprising no more than 56 characters that identifies and labels the monitor point. axes : str components {1,2,3,4,5,6} aecomp_name : str name of the AECOMP/AECOMPL entry xyz : list[float, float, float]; default=None The coordinates in the CP coordinate system about which the loads are to be monitored. None : [0., 0., 0.] cp : int, Coord; default=0 coordinate system of XYZ cd : int; default=None -> cp the coordinate system for load outputs comment : str; default='' a comment for the card Notes ----- CD - MSC specific field """ BaseCard.__init__(self) if comment: self.comment = comment if cd is None: cd = cp xyz = np.asarray(xyz) self.name = name self.label = label self.axes = axes self.comp = aecomp_name self.cp = cp self.xyz = xyz self.cd = cd assert len(xyz) == 3, xyz self.cp_ref = None self.cd_ref = None assert len(self.comp) > 0, self.comp def __deepcopy__(self, memo_dict): card = MONPNT1( self.name, self.label, self.axes, self.comp, self.xyz.copy(), cp=self.Cp(), cd=self.Cd(), comment=self.comment) return card
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): name = string(card, 1, 'name') label_fields = [labeli for labeli in card[2:8] if labeli is not None] label = ''.join(label_fields).strip() assert len(label) <= 56, label axes = parse_components(card, 9, 'axes') comp = string(card, 10, 'comp') cp = integer_or_blank(card, 11, 'cp', 0) xyz = [ double_or_blank(card, 12, 'x', default=0.0), double_or_blank(card, 13, 'y', default=0.0), double_or_blank(card, 14, 'z', default=0.0), ] cd = integer_or_blank(card, 15, 'cd', cp) return MONPNT1(name, label, axes, comp, xyz, cp=cp, cd=cd, 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 MONPNT1 name=%s' % self.name self.cp_ref = model.Coord(self.cp, msg=msg) self.cd_ref = model.Coord(self.cd, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by MONPNT1 name=%s' % self.name self.cp_ref = model.safe_coord(self.cp, self.name, xref_errors, msg=msg) self.cd_ref = model.safe_coord(self.cd, self.name, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.cp = self.Cp() self.cd = self.Cd() self.cp_ref = None self.cd_ref = None
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Cd(self) -> int: return coord_id(self.cd_ref, self.cd)
[docs] def raw_fields(self) -> list: cp = self.Cp() cd = self.Cd() assert len(self.comp) > 0, self.get_stats() list_fields = [ 'MONPNT1', self.name, self.label.strip(), self.axes, self.comp, cp] + list(self.xyz) + [cd] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: cp = self.Cp() x, y, z = self.xyz cd = self.Cd() # Default = the coordinate system specified by the CP field if cd == cp: cd = '' msg = 'MONPNT1 %-8s%s\n' % (self.name, self.label) msg += ' %-8s%-8s%-8s%-8s%-8s%-8s%-8s\n' % ( self.axes, self.comp, cp, print_float_8(x), print_float_8(y), print_float_8(z), cd) #card = self.repr_fields() return self.comment + msg
def __repr__(self): return self.write_card()
[docs] class MONPNT2(BaseCard): """MSC Nastran specific card""" type = 'MONPNT2'
[docs] @classmethod def _init_from_empty(cls): name = 'WING' label = 'Wing Integrated Load to Butline' table = 'MYTABLE' element_types = ['CAT'] nddl_item = ['dog'] eid = 2 return MONPNT2(name, label, table, element_types, nddl_item, eid, comment='')
def __deepcopy__(self, memo_dict: dict[int, Any]) -> MONPNT3: card = MONPNT2(self.name, self.label, copy.deepcopy(self.tables), copy.deepcopy(self.element_types), copy.deepcopy(self.nddl_items), copy.deepcopy(self.eids), comment=self.comment) memo_dict[id(self)] = card return card def __init__(self, name: str, label: str, tables: list[str], element_types: list[str], nddl_items: list[str], eids: list[list[int]], comment: str=''): BaseCard.__init__(self) if comment: self.comment = comment self.name = name if isinstance(tables, str): tables = [tables] if isinstance(element_types, str): element_types = [element_types] if isinstance(nddl_items, str): nddl_items = [nddl_items] elif isinstance(nddl_items, int): # pragma: no cover raise TypeError(nddl_items) if isinstance(eids, int): eids = [[eids]] assert isinstance(tables, list), tables assert isinstance(element_types, list), element_types assert isinstance(nddl_items, list), nddl_items assert isinstance(eids, list), eids self.label = label self.tables = tables self.element_types = element_types self.nddl_items = nddl_items self.eids = eids
[docs] def validate(self): for table in self.tables: assert table in ['STRESS', 'FORCE', 'STRAIN'], self.tables
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): name = string(card, 1, 'name') label_fields = [labeli for labeli in card[2:8] if labeli is not None] label = ''.join(label_fields).strip() assert len(label) <= 56, label tables = [] element_types = [] nddl_items = [] eids = [] ifield = 9 i = 1 while ifield < len(card): table = string(card, ifield, f'table{i}') element_type = string(card, ifield+1, f'type{i}') nddl_item = integer_or_string(card, ifield+2, f'nddl_item{i}') #nddl_item = integer_or_blank(card, 11, 'nddl_item') eid = integer_or_blank(card, ifield+3, f'eid{i}') tables.append(table) element_types.append(element_type) nddl_items.append(nddl_item) eids.append([eid]) blank(card, ifield+4, f'eid-b{i}') blank(card, ifield+5, f'eid-c{i}') blank(card, ifield+6, f'eid-d{i}') blank(card, ifield+7, f'eid-e{i}') ifield += 8 return MONPNT2(name, label, tables, element_types, nddl_items, eids, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model: BDF, unused_xref_errors): self.cross_reference(model)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" pass
[docs] def raw_fields(self): list_fields = [ 'MONPNT2', self.name, self.label.strip()] for table, element_type, nddl_item, eids in zip(self.tables, self.element_types, self.nddl_items, self.eids): list_fields += [table, element_type, nddl_item] + eids return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: msg = 'MONPNT2 %-8s%s\n' % (self.name, self.label) for table, element_type, nddl_item, eids in zip(self.tables, self.element_types, self.nddl_items, self.eids): assert len(eids) == 1, eids #list_fields += [table, element_type, nddl_item] + eids msg += (' %-8s%-8s%-8s%-8s\n' % ( table, element_type, nddl_item, eids[0] )) #card = self.repr_fields() return self.comment + msg.rstrip() + '\n'
def __repr__(self) -> str: return self.write_card()
[docs] class MONPNT3(BaseCard): """ MSC +---------+---------+---------+---------+-----+-------+------+-------+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=========+=========+=========+=====+=======+======+=======+====+ | MONPNT3 | NAME | LABEL | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | AXES | GRIDSET | ELEMSET | X | Y | Z | XFLAG | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | CD | | | | | | | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | MONPNT3 | WING155 | Wing Integrated Load to Butline 155 | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | 34 | 37 | | 0.0 | 155.0 | 15.0 | | | +---------+---------+---------+---------+-----+-------+------+-------+----+ NX +---------+---------+---------+---------+-----+-------+------+-------+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=========+=========+=========+=====+=======+======+=======+====+ | MONPNT3 | NAME | LABEL | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | AXES | GRIDGRP | ELEMGRP | X | Y | Z | XFLAG | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | CD | | | | | | | | +---------+---------+---------+---------+-----+-------+------+-------+----+ | MONPNT3 | WING155 | Wing Integrated Load to Butline 155 | +---------+---------+---------+---------+-----+-------+------+-------+----+ | | 34 | 37 | | 0.0 | 155.0 | 15.0 | | | +---------+---------+---------+---------+-----+-------+------+-------+----+ Note: the GRIDSET/ELEMSET and GRIDGRP/ELEMGRP is different """ type = 'MONPNT3'
[docs] @classmethod def _init_from_empty(cls): name = 'WING' label = 'Wing Integrated Load to Butline' axes = '6' grid_set = 10 elem_set = 11 xyz = [0., 1., 2.] return MONPNT3(name, label, axes, xyz, node_set_group=grid_set, elem_set_group=elem_set, cp=0, cd=None, xflag='', comment='')
def __deepcopy__(self, memo_dict: dict[int, Any]) -> MONPNT3: card = MONPNT3( self.name, self.label, self.axes, copy.deepcopy(self.xyz), self.node_set_group, self.elem_set_group, cp=self.Cp(), cd=self.Cd(), xflag=self.xflag, comment=self.comment) memo_dict[id(self)] = card return card def __init__(self, name: str, label: str | int, axes: str, xyz: list[float], node_set_group: int=0, elem_set_group: int=0, cp: int=0, cd: Optional[int]=None, xflag: str='', comment: str=''): """ Parameters ---------- name : str A unique character string that identifies the monitor point. (8 characters maximum) label : str A string that identifies and labels the monitor point. (56 characters maximum) axes : str | int Component axes about which to sum. of the integers 1 through 6 with no embedded blanks) node_set_group: int; default=0 MSC: Refers to a SET1 entry that has a list of grids to be included NX: Refers to a GROUP entry that has a list of grids to be included elem_set_group: int; default=0 MSC: Refers to a SET1 entry that has a list of elements to be included NX: Refers to a GROUP entry that has a list of elements to be included cp : int; default=0 The identification number of a coordinate system in which the X1, X2, and X3 coordinates are defined. xyz: list[float] The coordinates in the CP coordinate system about which the forces are to be summed. xflag : str; deault='' -> no types excluded Exclusion flag excludes the indicated Grid Point Force types from summation at the monitor point. - "S": SPC forces are excluded. - "M": MPC forces are excluded. - "A", "L", or "P": applied loads (including thermal loads), are excluded. - "D": DMIGs at the monitored point are excluded. - C contact forces (MSC-SOL 400 only) cd : int; default=None -> cp The identification number of a coordinate system in which the results are output. """ BaseCard.__init__(self) if comment: self.comment = comment if cd is None: cd = cp if isinstance(axes, int): axes = str(axes) xyz = np.asarray(xyz) self.name = name self.label = label self.axes = axes #self.comp = comp self.node_set_group = node_set_group self.elem_set_group = elem_set_group self.xyz = xyz self.xflag = xflag self.cp = cp self.cd = cd self.cp_ref = None self.cd_ref = None self.node_ref = None self.elem_ref = None assert len(self.xyz) == 3, xyz
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): name = string(card, 1, 'name') label_fields = [labeli for labeli in card[2:8] if labeli is not None] label = ''.join(label_fields).strip() assert len(label) <= 56, label axes = parse_components(card, 9, 'axes') node_set = integer_or_blank(card, 10, 'node_set/group', default=0) #if isinstance(grid_set, int): elem_set = integer_or_blank(card, 11, 'elem_set/group', default=0) # else: # elem_set = string_or_blank(card, 11, 'elem_set/group', default='') cp = integer_or_blank(card, 12, 'cp', default=0) xyz = [ double_or_blank(card, 13, 'x', default=0.0), double_or_blank(card, 14, 'y', default=0.0), double_or_blank(card, 15, 'z', default=0.0), ] xflag = string_or_blank(card, 16, 'xflag', default='') cd = integer_or_blank(card, 17, 'cd', default=cp) return MONPNT3(name, label, axes, xyz, node_set, elem_set, cp=cp, cd=cd, xflag=xflag, 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 MONPNT3 name=%s' % self.name self.cp_ref = model.Coord(self.cp, msg=msg) self.cd_ref = model.Coord(self.cd, msg=msg) self.node_ref = xref_set_group(model, self.node_set_group, 'Node', msg) self.elem_ref = xref_set_group(model, self.elem_set_group, 'Elem', msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by MONPNT3 name=%s' % self.name self.cp_ref = model.safe_coord(self.cp, self.name, xref_errors, msg=msg) self.cd_ref = model.safe_coord(self.cd, self.name, xref_errors, msg=msg) self.node_ref = safe_xref_set_group(model, self.node_set_group, 'Node', msg) self.elem_ref = safe_xref_set_group(model, self.elem_set_group, 'Elem', msg)
[docs] def get_node_set_group(self) -> int: return set_group(self.node_ref, self.node_set_group)
[docs] def get_elem_set_group(self) -> int: return set_group(self.elem_ref, self.elem_set_group)
[docs] def uncross_reference(self) -> None: self.cp = self.Cp() self.cd = self.Cd() self.node_set_group = self.get_node_set_group() self.elem_set_group = self.get_elem_set_group() self.cp_ref = None self.cd_ref = None
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Cd(self) -> int: return coord_id(self.cd_ref, self.cd)
[docs] def raw_fields(self): list_fields = [ 'MONPNT3', self.name, self.label.strip(), self.axes, self.get_node_set_group(), self.get_elem_set_group(), self.Cp() ] + list(self.xyz) + [self.xflag, self.Cd()] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: cp = self.Cp() cd = self.Cd() if cp == cd: cd = '' xflag = self.xflag if xflag is None: xflag = '' x, y, z = self.xyz msg = 'MONPNT3 %-8s%s\n' % (self.name, self.label) msg += (' %-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s\n' ' %-8s' % ( self.axes, self.get_node_set_group(), self.get_elem_set_group(), cp, print_float_8(x), print_float_8(y), print_float_8(z), xflag, cd )) #card = self.repr_fields() return self.comment + msg.rstrip() + '\n'
def __repr__(self) -> str: return self.write_card()
[docs] def xref_set_group(model: BDF, set_group_id: int, xref_type: str, msg: str) -> Optional[SET1 | GROUP]: if set_group_id > 0: if model.is_msc: set_group_ref = model.Set(set_group_id, msg=msg) # {'Node', 'Point'} if xref_type == 'Node': set_group_ref.cross_reference_set( model, xref_type, msg=msg, allow_empty_nodes=False) elif xref_type == 'Elem': warnings.warn('skipping SET1/Elem xref') else: # pragma: no cover raise RuntimeError(f'xref_type={xref_type!r}') else: set_group_ref = model.Group(set_group_id, msg=msg) return set_group_ref return None
[docs] def safe_xref_set_group(model: BDF, set_group_id: int, xref_type: str, msg: str) -> Optional[SET1 | GROUP]: if set_group_id > 0: if model.is_msc: set_group_ref = model.Set(set_group_id, msg=msg) # {'Node', 'Point'} if xref_type == 'Node': set_group_ref.cross_reference_set( model, xref_type, msg=msg, allow_empty_nodes=False) elif xref_type == 'Elem': warnings.warn('skipping SET1/Elem xref') else: # pragma: no cover raise RuntimeError(f'xref_type={xref_type!r}') else: set_group_ref = model.Group(set_group_id, msg=msg) return set_group_ref return None
[docs] class MONDSP1(BaseCard): """ +---------+---------+------+-----+-----+-------+------+----+--------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=========+======+=====+=====+=======+======+====+========+ | MONPNT1 | NAME | LABEL | +---------+---------+------+-----+-----+-------+------+----+--------+ | | AXES | COMP | CP | X | Y | Z | CD | INDDOF | +---------+---------+------+-----+-----+-------+------+----+--------+ | MONPNT1 | WING155 | Wing Integrated Load to Butline 155 | +---------+---------+------+-----+-----+-------+------+----+--------+ | | 34 | WING | | 0.0 | 155.0 | 15.0 | | | +---------+---------+------+-----+-----+-------+------+----+--------+ """ type = 'MONDSP1'
[docs] @classmethod def _init_from_empty(cls): name = 'WING' label = 'Wing Integrated Load to Butline' axes = '6' aecomp_name = 'FLAP' xyz = [0., 1., 2.] return MONDSP1(name, label, axes, aecomp_name, xyz, cp=0, cd=None, ind_dof='123', comment='')
def __init__(self, name, label, axes, aecomp_name, xyz: list[float], cp: int=0, cd: Optional[int]=None, ind_dof: str='123', comment: str=''): """ Creates a MONDSP1 card Parameters ---------- name : str Character string of up to 8 characters identifying the monitor point label : str A string comprising no more than 56 characters that identifies and labels the monitor point. axes : str components {1,2,3,4,5,6} aecomp_name : str name of the AECOMP/AECOMPL entry xyz : list[float, float, float]; default=None The coordinates in the CP coordinate system about which the loads are to be monitored. None : [0., 0., 0.] cp : int, Coord; default=0 coordinate system of XYZ cd : int; default=None -> cp the coordinate system for load outputs ind_dof : str; default='123' the dofs to map comment : str; default='' a comment for the card Notes ----- MSC specific card """ BaseCard.__init__(self) if comment: self.comment = comment if cd is None: cd = cp xyz = np.asarray(xyz) self.name = name self.label = label self.axes = axes self.comp = aecomp_name self.cp = cp self.xyz = xyz self.cd = cd self.ind_dof = ind_dof assert len(xyz) == 3, xyz self.cp_ref = None self.cd_ref = None
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): row0 = card[0] row1 = card[1] assert len(card) == 2, card assert len(row0) > 8, row0 assert ',' not in row1, row1 if '\t' in row1: card_fields = row1.split('\t') row1 = row1.expandtabs(tabsize=8) name = row0[8:16] label = row0[16:72] card_fields = [ 'MONDSP1', name, label[:8], label[8:16], label[16:24], label[24:32], label[32:40], label[40:48], label[48:56], row1[8:16], row1[16:24], row1[24:32], row1[32:40], row1[40:48], row1[48:56], row1[56:64], row1[64:72]] card = BDFCard(card_fields, has_none=True) name = string(card, 1, 'name') #label_fields = [labeli for labeli in card[2:8] if labeli is not None] #label = ''.join(label_fields).strip() # assert len(label) <= 56, label axes = parse_components(card, 9, 'axes') #comp = str(integer(card, 10, 'comp')) comp = str(integer_or_string(card, 10, 'comp')) cp = integer_or_blank(card, 11, 'cp', default=0) xyz = [ double_or_blank(card, 12, 'x', default=0.0), double_or_blank(card, 13, 'y', default=0.0), double_or_blank(card, 14, 'z', default=0.0), ] cd = integer_or_blank(card, 15, 'cd', default=cp) ind_dof = components_or_blank(card, 16, 'ind_dof', default='123') return MONDSP1(name, label, axes, comp, xyz, cp=cp, cd=cd, ind_dof=ind_dof, 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 MONDSP1 name=%s' % self.name self.cp_ref = model.Coord(self.cp, msg=msg) self.cd_ref = model.Coord(self.cd, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by MONDSP1 name=%s' % self.name self.cp_ref = model.safe_coord(self.cp, self.name, xref_errors, msg=msg) self.cd_ref = model.safe_coord(self.cd, self.name, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.cp = self.Cp() self.cd = self.Cd() self.cp_ref = None self.cd_ref = None
[docs] def Cp(self) -> int: return coord_id(self.cp_ref, self.cp)
[docs] def Cd(self) -> int: return coord_id(self.cd_ref, self.cd)
[docs] def raw_fields(self): list_fields = [ 'MONDSP1', self.name, self.label.strip(), self.axes, self.comp, self.Cp(),] + list(self.xyz) + [self.Cd(), self.ind_dof] return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: cp = self.Cp() x, y, z = self.xyz cd = self.Cd() # Default = the coordinate system specified by the CP field if cd == cp: cd = '' msg = 'MONDSP1 %-8s%s\n' % (self.name, self.label) msg += ' %-8s%-8s%-8s%-8s%-8s%-8s%-8s%-8s\n' % ( self.axes, self.comp, cp, print_float_8(x), print_float_8(y), print_float_8(z), cd, self.ind_dof) #card = self.repr_fields() return self.comment + msg
def __repr__(self): return self.write_card()
[docs] class PAERO1(BaseCard): """ Defines associated bodies for the panels in the Doublet-Lattice method. +--------+-----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +========+=====+====+====+====+====+====+====+ | PAERO1 | PID | B1 | B2 | B3 | B4 | B5 | B6 | +--------+-----+----+----+----+----+----+----+ """ type = 'PAERO1' _field_map = {1: 'pid'} _properties = ['_field_map'] def _get_field_helper(self, n: int) -> int: """ Gets complicated parameters on the PAERO1 card Parameters ---------- n : int the field number to update Returns ------- value : int the value for the appropriate field """ return self.caero_body_ids[n - 1] def _update_field_helper(self, n: int, value: int) -> None: """ Updates complicated parameters on the PAERO1 card Parameters ---------- n : int the field number to update value : varies the value for the appropriate field """ self.caero_body_ids[n - 1] = value
[docs] @classmethod def _init_from_empty(cls): pid = 1 return PAERO1(pid, caero_body_ids=None, comment='')
def __init__(self, pid, caero_body_ids=None, comment=''): """ Creates a PAERO1 card, which defines associated bodies for the panels in the Doublet-Lattice method. Parameters ---------- pid : int PAERO1 id caero_body_ids : list[int]; default=None CAERO2 ids that are within the same IGROUP group comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment self.pid = pid if caero_body_ids is None: caero_body_ids = [] self.caero_body_ids = caero_body_ids self.caero_body_refs = []
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a PAERO1 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ pid = integer(card, 1, 'pid') caero_body_ids = [interpret_value(field, card) for field in card[2:]] caero_body_ids2 = [] for caero_body_id in caero_body_ids: if isinstance(caero_body_id, integer_types) and caero_body_id >= 0: caero_body_ids2.append(caero_body_id) elif caero_body_id is not None: msg = f'invalid caero_body_id value on PAERO1; caero_body_id={caero_body_id}' raise RuntimeError(msg) #else: #pass return PAERO1(pid, caero_body_ids, comment=comment)
def _verify(self, xref: bool) -> None: pass
[docs] def cross_reference(self, model: BDF) -> None: caero_body_refs = [] msg = f', which is required by PAERO1 pid={self.pid}' for caero_body_id in self.caero_body_ids: caero = model.CAero(caero_body_id, msg) assert caero.type == 'CAERO2', caero caero_body_refs.append(caero) self.caero_body_refs = caero_body_refs
[docs] def safe_cross_reference(self, model: BDF, xref_errors): self.cross_reference(model)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.caero_body_ids = self.get_caero_body_ids() self.caero_body_ids_ref = None
[docs] def get_caero_body_ids(self) -> list[int]: if self.caero_body_refs is None: return caero_body_ids = [] for caero_body in self.caero_body_refs: caero_body_id = caero_body.eid caero_body_ids.append(caero_body_id) return caero_body_ids
[docs] def raw_fields(self) -> list: """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['PAERO1', self.pid] + self.get_caero_body_ids() return list_fields
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: card = self.raw_fields() return self.comment + print_card_8(card)
[docs] class PAERO2(BaseCard): """ Defines the cross-sectional properties of aerodynamic bodies. +--------+------+--------+-------+------+------+------+------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+======+========+=======+======+======+======+======+======+ | PAERO2 | PID | ORIENT | WIDTH | AR | LRSB | LRIB | LTH1 | LTH2 | +--------+------+--------+-------+------+------+------+------+------+ | THI1 | THN1 | THI2 | THN2 | THI3 | THN3 | | | | +--------+------+--------+-------+------+------+------+------+------+ """ type = 'PAERO2' _field_map = { 1: 'pid', 2: 'orient', 3: 'width', 4: 'AR', 5: 'lrsb', 6: 'lrib', #7: 'lth1', 8: 'lth2', } _properties = ['_field_map', 'lth1', 'lth2', ] def _get_field_helper(self, n): """ Gets complicated parameters on the PAERO2 card Parameters ---------- n : int the field number to update Returns ------- value : varies the value for the appropriate field """ nnew = n - 8 spot = nnew // 2 i = nnew % 2 if i == 0: value = self.thi[spot] else: value = self.thn[spot] return value def _update_field_helper(self, n, value): """ Updates complicated parameters on the PAERO2 card Parameters ---------- n : int the field number to update value : varies the value for the appropriate field """ nnew = n - 8 spot = nnew // 2 i = nnew % 2 if i == 0: self.thi[spot] = value else: self.thn[spot] = value
[docs] @classmethod def _init_from_empty(cls): pid = 1 width = 10. AR = 1. thi = [None] thn = [None] orient = 'ZY' return PAERO2(pid, orient, width, AR, thi, thn, lrsb=None, lrib=None, lth=None, comment='')
#def _finalize_hdf5(self, encoding): #"""hdf5 helper function""" #pass #print(self.get_stats()) def __init__(self, pid, orient, width, AR, thi, thn, lrsb=None, lrib=None, lth=None, comment=''): """ Creates a PAERO2 card, which defines additional cross-sectional properties for the CAERO2 geometry. Parameters ---------- pid : int PAERO1 id orient : str Orientation flag. Type of motion allowed for bodies. Refers to the aerodynamic coordinate system of ACSID. See AERO entry. valid_orientations = {Z, Y, ZY} width : float Reference half-width of body and the width of the constant width interference tube AR : float Aspect ratio of the interference tube (height/width) thi / thn : list[int] The first (thi) and last (thn) interference element of a body to use the theta1/theta2 array lrsb : int; default=None int : AEFACT id containing a list of slender body half-widths at the end points of the slender body elements None : use width lrib : int; default=None int : AEFACT id containing a list of interference body half-widths at the end points of the interference elements None : use width lth : list[int, int]; default=None AEFACT id for defining theta arrays for interference calculations for theta1/theta2 comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment #: Property identification number. (Integer > 0) self.pid = pid #: Orientation flag. Type of motion allowed for bodies. Refers to #: the aerodynamic coordinate system of ACSID. See AERO entry. #: (Character = 'Z', 'Y', or 'ZY') self.orient = orient #: Reference half-width of body and the width of the constant width #: interference tube. (Real > 0.0) self.width = width #: Aspect ratio of the interference tube (height/width). float>0. self.AR = AR #: Identification number of an AEFACT entry containing a list of #: slender body half-widths at the end points of the #: slender body elements. If blank, the value of WIDTH will be used. #: (Integer > 0 or blank) self.lrsb = lrsb #: Identification number of an AEFACT entry containing a list of #: slender body half-widths at the end points of the #: interference elements. If blank, the value of WIDTH will be used. #: (Integer > 0 or blank) self.lrib = lrib #: Identification number of AEFACT entries for defining theta arrays for #: interference calculations. (Integer >= 0) if lth is None: lth = [None, None] self.lth = lth self.thi = thi self.thn = thn if self.lrsb == 0: self.lrsb = None if self.lrib == 0: self.lrib = None self.lrsb_ref = None self.lrib_ref = None @property def lth1(self) -> int: return self.lth[0] @property def lth2(self) -> int: return self.lth[1] @lth1.setter def lth1(self, lth1: int) -> None: self.lth[0] = lth1 @lth2.setter def lth2(self, lth2: int) -> None: self.lth[1] = lth2
[docs] def validate(self): assert self.orient in ['Z', 'Y', 'ZY'], 'PAERO2: orient=%r' % self.orient assert isinstance(self.AR, float), 'PAERO2: AR=%r type=%s' % (self.AR, type(self.AR)) assert isinstance(self.width, float), 'PAERO2: width=%r type=%s' % (self.width, type(self.width)) assert isinstance(self.thi, list), 'PAERO2: thi=%s type=%s' % (self.thi, type(self.thi)) assert isinstance(self.thn, list), 'PAERO2: thn=%s type=%s' % (self.thn, type(self.thn))
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a PAERO2 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ pid = integer(card, 1, 'pid') orient = string(card, 2, 'orient') width = double(card, 3, 'width') AR = double(card, 4, 'AR') lrsb = integer_or_blank(card, 5, 'lrsb') lrib = integer_or_blank(card, 6, 'lrib') lth1 = integer_or_blank(card, 7, 'lth1') lth2 = integer_or_blank(card, 8, 'lth2') thi = [] thn = [] list_fields = [interpret_value(field, card) for field in card[9:]] nfields = len(list_fields) lth = [lth1, lth2] for i in range(9, 9 + nfields, 2): thi.append(integer(card, i, 'lth')) thn.append(integer(card, i + 1, 'thn')) return PAERO2(pid, orient, width, AR, thi, thn, lrsb=lrsb, lrib=lrib, lth=lth, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: msg = ', which is required by PAERO2 eid=%s' % self.pid if self.lrsb is not None and isinstance(self.lrsb, integer_types): self.lrsb_ref = model.AEFact(self.lrsb, msg=msg) if self.lrib is not None and isinstance(self.lrib, integer_types): self.lrib_ref = model.AEFact(self.lrib, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by PAERO2 eid=%s' % self.pid if self.lrsb is not None and isinstance(self.lrsb, integer_types): self.lrsb_ref = model.safe_aefact(self.lrsb, self.pid, xref_errors, msg=msg) if self.lrib is not None and isinstance(self.lrib, integer_types): self.lrib_ref = model.safe_aefact(self.lrib, self.pid, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.lrsb = self.Lrsb() self.lrib = self.Lrib() self.lrsb_ref = None self.lrib_ref = None
[docs] def Lrsb(self): """AEFACT id""" return aefact_id(self.lrsb_ref, self.lrsb)
[docs] def Lrib(self): """AEFACT id""" return aefact_id(self.lrib_ref, self.lrib)
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['PAERO2', self.pid, self.orient, self.width, self.AR, self.Lrsb(), self.Lrib()] + self.lth for (thi, thn) in zip(self.thi, self.thn): list_fields += [thi, thn] 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 PAERO3(BaseCard): """ Defines the number of Mach boxes in the flow direction and the location of cranks and control surfaces of a Mach box lifting surface. +--------+------+------+-------+------+-----+------+------+------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+======+======+=======+======+=====+======+======+======+ | PAERO3 | PID | NBOX | NCTRL | | X5 | Y5 | X6 | Y6 | +--------+------+------+-------+------+-----+------+------+------+ | | X7 | Y7 | X8 | Y8 | X9 | Y9 | X10 | Y10 | +--------+------+------+-------+------+-----+------+------+------+ | | X11 | Y11 | X12 | Y12 | | | | | +--------+------+------+-------+------+-----+------+------+------+ | PAERO3 | 2001 | 15 | 1 | | 0. | 65. | | | +--------+------+------+-------+------+-----+------+------+------+ | | 78. | 65. | 108. | 65. | 82. | 97.5 | 112. | 97.5 | +--------+------+------+-------+------+-----+------+------+------+ | | 86. | 130. | 116. | 130. | | | | | +--------+------+------+-------+------+-----+------+------+------+ """ type = 'PAERO3' _field_map = { 1: 'pid', 2: 'orient', 3: 'width', 4: 'AR', } _properties = ['npoints'] def _get_field_helper(self, n: int): """ Gets complicated parameters on the PAERO3 card Parameters ---------- n : int the field number to update Returns ------- value : varies the value for the appropriate field """ nnew = n - 6 if nnew < 0: raise RuntimeError('field n=%i on PAERO3 is invalid' % n) spot = nnew // 2 i = nnew % 2 if i == 0: value = self.x[spot] else: value = self.y[spot] return value def _update_field_helper(self, n, value): """ Updates complicated parameters on the PAERO3 card Parameters ---------- n : int the field number to update value :varies the value for the appropriate field """ nnew = n - 6 if nnew < 0: raise RuntimeError('field n=%i on PAERO3 is invalid' % n) spot = nnew // 2 i = nnew % 2 if i == 0: self.x[spot] = value else: self.y[spot] = value
[docs] @classmethod def _init_from_empty(cls): pid = 1 nbox = 1 ncontrol_surfaces = 1 x = [0., 0., 0.] y = [0., 10., 0.] return PAERO3(pid, nbox, ncontrol_surfaces, x, y, comment='')
def __init__(self, pid, nbox, ncontrol_surfaces, x, y, comment=''): """ Creates a PAERO3 card, which defines the number of Mach boxes in the flow direction and the location of cranks and control surfaces of a Mach box lifting surface. Parameters ---------- pid : int PAERO1 id nbox : int Number of Mach boxes in the flow direction; 0 < nbox < 50 ncontrol_surfaces : int Number of control surfaces. (0, 1, or 2) x / y : list[float, None] float : locations of points 5 through 12, which are in the aerodynamic coordinate system, to define the cranks and control surface geometry. comment : str; default='' a comment for the card """ BaseCard.__init__(self) if comment: self.comment = comment #: Property identification number. (Integer > 0) self.pid = pid self.nbox = nbox self.ncontrol_surfaces = ncontrol_surfaces self.x = x self.y = y
[docs] def validate(self): assert len(self.x) == len(self.y), 'nx=%s ny=%s' % (len(self.x), len(self.y)) assert len(self.x) <= 8, 'nx=%s' % len(self.x) for i, xi, yi in zip(count(), self.x, self.y): if xi is None or yi is None: assert xi == yi, 'x%i=%s y%i=%s must be None or floats' % (i, xi, i, yi)
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a PAERO3 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ pid = integer(card, 1, 'pid') nbox = integer(card, 2, 'nbox') ncontrol_surfaces = integer(card, 3, 'ncontrol_surfaces') x = [] y = [] nfields = card.nfields j = 5 for i in range(5, nfields, 2): xi = double_or_blank(card, i, 'x%d' % j) yi = double_or_blank(card, i + 1, 'y%d' % j) x.append(xi) y.append(yi) j += 1 return PAERO3(pid, nbox, ncontrol_surfaces, x, y, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model: BDF, unused_xref_errors): return self.cross_reference(model)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" pass
@property def npoints(self) -> int: return len(self.x)
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['PAERO3', self.pid, self.nbox, self.ncontrol_surfaces, None] for (x, y) in zip(self.x, self.y): list_fields += [x, y] 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 PAERO4(BaseCard): """ Defines properties of each strip element for Strip theory. +--------+------+-------+--------+-------+-------+--------+--------+--------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+======+=======+========+=======+=======+========+========+========+ | PAERO4 | PID | CLA | LCLA | CIRC | LCIRC | DOC1 | CAOC1 | GAPOC1 | +--------+------+-------+--------+-------+-------+--------+--------+--------+ | | DOC2 | CAOC2 | GAPOC2 | DOC3 | CAOC3 | GAPOC3 | etc. | | +--------+------+-------+--------+-------+-------+--------+--------+--------+ | PAERO4 | 6001 | 1 | 501 | 0 | 0 | 0.0 | 0.0 | 0.0 | +--------+------+-------+--------+-------+-------+--------+--------+--------+ | | 0.50 | 0.25 | 0.02 | 0.53 | 0.24 | 0.0 | | | +--------+------+-------+--------+-------+-------+--------+--------+--------+ ## TODO: what happens for DOC4? """ type = 'PAERO4' _field_map = { 1: 'pid', # 2:'orient', 3:'width', 4:'AR', } # def _get_field_helper(self, n): # """ # Gets complicated parameters on the PAERO3 card # # Parameters # ---------- # n : int # the field number to update # # Returns # ------- # value : varies # the value for the appropriate field # """ # nnew = n - 6 # if nnew < 0: # raise RuntimeError('field n=%i on PAERO3 is invalid' % n) # spot = nnew // 2 # i = nnew % 2 # if i == 0: # value = self.x[spot] # else: # value = self.y[spot] # return value # # def _update_field_helper(self, n, value): # """ # Updates complicated parameters on the PAERO3 card # # Parameters # ---------- # n : int # the field number to update # value :varies # the value for the appropriate field # """ # nnew = n - 6 # if nnew < 0: # raise RuntimeError('field n=%i on PAERO3 is invalid' % n) # spot = nnew // 2 # i = nnew % 2 # if i == 0: # self.x[spot] = value # else: # self.y[spot] = value
[docs] @classmethod def _init_from_empty(cls): pid = 1 docs = [1, 2] caocs = [1, 2] gapocs = [1, 2] return PAERO4(pid, docs, caocs, gapocs, cla=0, lcla=0, circ=0, lcirc=0, comment='')
def __init__(self, pid, docs, caocs, gapocs, cla=0, lcla=0, circ=0, lcirc=0, comment=''): """ Parameters ---------- pid : int Property identification number. (Integer > 0) cla : int; default=0 Select Prandtl-Glauert correction. (Integer = -1, 0, 1) -1 Compressibility correction made to lift curve slope data for a reference Mach number. 0 No correction and no list needed. (Default) +1 No correction and lift curve slope provided by a list as a function of strip location and Mach number. lcla : int; default=0 ID number of the AEFACT entry that lists the lift curve slope on all strips for each Mach number on the MKAEROi entry. See Remark 2 below. (Integer = 0 if CLA = 0, > 0 if CLA ≠ 0) circ : int; default=0 Select Theodorsen’s function C(k) or the number of exponential coefficients used to approximate C(k). (Integer = 0, 1, 2, 3; Must be zero if CLA ≠ 0.) 0 Theodorsen function. 1, 2, 3 Approximate function with b0, b1, β1, ..., bn, βn n = 1, 2, 3. lcirc : int; default=0 Identification number of the AEFACT entry that lists the b, β values for each Mach number. See Remark 3, 4, and 5 below; variable b’s and β’s for each mi on the MKAEROi entry. (Integer = 0 if CIRC = 0, > 0 if CIRC ≠ 0) docs : list[float] d/c = distance of the control surface hinge aft of the quarter-chord divided by the strip chord (Real ≥ 0.0) caocs : list[float] ca/c = control surface chord divided by the strip chord. (Real ≥ 0.0) gapocs : list[float] g/c = control surface gap divided by the strip chord. (Real ≥ 0.0) """ BaseCard.__init__(self) if comment: self.comment = comment #: Property identification number. (Integer > 0) self.pid = pid self.cla = cla self.lcla = lcla self.circ = circ self.lcirc = lcirc self.docs = docs self.caocs = caocs self.gapocs = gapocs assert isinstance(docs, list), docs assert isinstance(caocs, list), caocs assert isinstance(gapocs, list), gapocs
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a PAERO4 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ pid = integer(card, 1, 'pid') cla = integer_or_blank(card, 2, 'cla', default=0) lcla = integer_or_blank(card, 3, 'lcla', default=0) # ??? circ = integer_or_blank(card, 4, 'circ', default=0) lcirc = integer_or_blank(card, 5, 'lcirc', default=0) # ??? nfields = card.nfields j = 0 docs = [] caocs = [] gapocs = [] for i in range(6, nfields, 3): doc = double(card, i, 'doc_%d' % j) caoc = double(card, i + 1, 'caoc_%d' % j) gapoc = double(card, i + 2, 'gapoc_%d' % j) docs.append(doc) caocs.append(caoc) gapocs.append(gapoc) j += 1 return PAERO4(pid, docs, caocs, gapocs, cla=cla, lcla=lcla, circ=circ, lcirc=lcirc, comment=comment)
[docs] def cross_reference(self, model: BDF) -> None: pass
[docs] def safe_cross_reference(self, model: BDF, xref_errors): pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" 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 = ['PAERO4', self.pid, self.cla, self.lcla, self.circ, self.lcirc] for doc, caoc, gapoc in zip(self.docs, self.caocs, self.gapocs): list_fields += [doc, caoc, gapoc] 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 PAERO5(BaseCard): """ +--------+-------+--------+--------+---------+-------+-------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +========+=======+========+========+=========+=======+=======+=======+ | PAERO5 | PID | NALPHA | LALPHA | NXIS | LXIS | NTAUS | LTAUS | +--------+-------+--------+--------+---------+-------+-------+-------+ | | CAOC1 | CAOC2 | CAOC3 | CAOC4 | CAOC5 | | | +--------+-------+--------+--------+---------+-------+-------+-------+ | PAERO5 | 7001 | 1 | 702 | 1 | 701 | 1 | 700 | +--------+-------+--------+--------+---------+-------+-------+-------+ | | 0.0 | 0.0 | 5.25 | 3.99375 | 0.0 | | | +--------+-------+--------+--------+---------+-------+-------+-------+ """ type = 'PAERO5' _properties = ['ltaus_id', 'lxis_id']
[docs] @classmethod def _init_from_empty(cls): pid = 1 caoci = [0., 0., 0.] return PAERO5(pid, caoci, nalpha=0, lalpha=0, nxis=0, lxis=0, ntaus=0, ltaus=0, comment='')
def __init__(self, pid: int, caoci, nalpha: int=0, lalpha: int=0, nxis=0, lxis: int=0, ntaus: int=0, ltaus: int=0, comment=''): BaseCard.__init__(self) if comment: self.comment = comment self.pid = pid self.nalpha = nalpha self.lalpha = lalpha # number of dimensionless chord coordinates in zeta () self.nxis = nxis # ID of AEFACT that lists zeta self.lxis = lxis # number of dimensionless thickess coordinates in tau self.ntaus = ntaus # ID of AEFACT that lists thickness ratios (t/c) self.ltaus = ltaus # ca/c - control surface chord / strip chord self.caoci = np.array(caoci, dtype='float64') self.lxis_ref = None self.ltaus_ref = None
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a PAERO5 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ pid = integer(card, 1, 'property_id') nalpha = integer_or_blank(card, 2, 'nalpha', default=0) lalpha = integer_or_blank(card, 3, 'lalpha', default=0) nxis = integer_or_blank(card, 4, 'nxis', default=0) lxis = integer_or_blank(card, 5, 'lxis', default=0) ntaus = integer_or_blank(card, 6, 'ntaus', default=0) ltaus = integer_or_blank(card, 7, 'ltaus', default=0) caoci = [] for n, i in enumerate(range(9, len(card))): ca = double(card, i, 'ca/ci_%i' % (n+1)) caoci.append(ca) return PAERO5(pid, caoci, nalpha=nalpha, lalpha=lalpha, nxis=nxis, lxis=lxis, ntaus=ntaus, ltaus=ltaus, comment=comment)
@property def lxis_id(self) -> int: return aefact_id(self.lxis_ref, self.lxis) @property def ltaus_id(self) -> int: return aefact_id(self.ltaus_ref, self.ltaus)
[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 PAERO5 eid=%s' % self.pid if self.lxis != 0: self.lxis_ref = model.AEFact(self.lxis_id, msg=msg) if self.ltaus != 0: self.ltaus_ref = model.AEFact(self.ltaus_id, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by PAERO5 eid=%s' % self.pid if self.lxis != 0: self.lxis_ref = model.safe_aefact(self.lxis_id, self.pid, xref_errors, msg=msg) if self.ltaus != 0: self.ltaus_ref = model.safe_aefact(self.ltaus_id, self.pid, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.lxis = self.lxis_id self.ltaus = self.ltaus_id self.lxis_ref = None self.ltaus_ref = None
[docs] def raw_fields(self): list_fields = ['PAERO5', self.pid, self.nalpha, self.lalpha, self.nxis, self.lxis_id, self.ntaus, self.ltaus_id] + list(self.caoci) return list_fields
[docs] def repr_fields(self): list_fields = self.raw_fields() list_fields.insert(8, None) 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)
# def integrals(self): # # chord location # x = self.lxis.fractions # # # thickness # y = self.ltaus.fractions # # # slope of airfoil semi-thickness # yp = derivative1(y/2, x) # # # x hinge # for xh in self.caoci: # I1 = integrate(yp, x, 0., 1.) # I2 = integrate(x * yp, x, 0., 1.) # I3 = integrate(x**2*yp, x, 0., 1.) # I4 = integrate(yp**2, x, 0., 1.) # I5 = integrate(x**2 * yp**2, x, 0., 1.) # # J1 = integrate(yp, x, xh, 1.) # J2 = integrate(x * yp, x, xh, 1.) # J3 = integrate(x**2*yp, x, xh, 1.) # J4 = integrate(yp**2, x, xh, 1.) # J5 = integrate(x**2 * yp**2, x, xh, 1.) # # return(I1, I2, I3, I4, I5, # J1, J2, J3, J4, J5)
[docs] class Spline(BaseCard): def __init__(self): BaseCard.__init__(self)
SPLINE1_MSG = """ +---------+-------+-------+------+------+------+----+------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=======+=======+======+======+======+====+======+=======+ | SPLINE1 | EID | CAERO | BOX1 | BOX2 | SETG | DZ | METH | USAGE | +---------+-------+-------+------+------+------+----+------+-------+ | | NELEM | MELEM | | | | | | | +---------+-------+-------+------+------+------+----+------+-------+ | SPLINE1 | 3 | 111 | 115 | 122 | 14 | 0. | | | +---------+-------+-------+------+------+------+----+------+-------+""".strip()
[docs] class SPLINE1(Spline): """ Surface Spline Methods Defines a surface spline for interpolating motion and/or forces for aeroelastic problems on aerodynamic geometries defined by regular arrays of aerodynamic points. +---------+-------+-------+------+------+------+----+------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=======+=======+======+======+======+====+======+=======+ | SPLINE1 | EID | CAERO | BOX1 | BOX2 | SETG | DZ | METH | USAGE | +---------+-------+-------+------+------+------+----+------+-------+ | | NELEM | MELEM | | | | | | | +---------+-------+-------+------+------+------+----+------+-------+ | SPLINE1 | 3 | 111 | 115 | 122 | 14 | 0. | | | +---------+-------+-------+------+------+------+----+------+-------+ """ type = 'SPLINE1' _field_map = { 1: 'eid', 2: 'caero', 3: 'box1', 4: 'box2', 5: 'setg', 6: 'dz', 7: 'method', 8: 'usage', 9: 'nelements', 10: 'melements', } _properties = ['aero_element_ids']
[docs] @classmethod def _init_from_empty(cls): eid = 1 caero = 1 box1 = 1 box2 = 2 setg = 3 return SPLINE1(eid, caero, box1, box2, setg, dz=0., method='IPS', usage='BOTH', nelements=10, melements=10, comment='')
def __init__(self, eid: int, caero: int, box1: int, box2: int, setg: int, dz: float=0., method: str='IPS', usage: str='BOTH', nelements: int=10, melements: int=10, comment: str=''): """ Creates a SPLINE1, which defines a surface spline. Parameters ---------- eid : int spline id caero : int CAEROx id that defines the plane of the spline box1 / box2 : int First/last box id that is used by the spline setg : int SETx id that defines the list of GRID points that are used by the surface spline dz : float; default=0.0 linear attachment flexibility dz = 0.; spline passes through all grid points method : str; default=IPS method for spline fit valid_methods = {IPS, TPS, FPS} IPS : Harder-Desmarais Infinite Plate Spline TPS : Thin Plate Spline FPS : Finite Plate Spline usage : str; default=BOTH Spline usage flag to determine whether this spline applies to the force transformation, displacement transformation, or both valid_usage = {FORCE, DISP, BOTH} nelements : int; default=10 The number of FE elements along the local spline x-axis if using the FPS option melements : int; default=10 The number of FE elements along the local spline y-axis if using the FPS option comment : str; default='' a comment for the card """ Spline.__init__(self) if comment: self.comment = comment self.eid = eid self.caero = caero self.box1 = box1 self.box2 = box2 self.setg = setg self.dz = dz self.method = method self.usage = usage self.nelements = nelements self.melements = melements self.caero_ref = None self.setg_ref = None
[docs] def validate(self): assert self.method in ['IPS', 'TPS', 'FPS'], 'method = %s' % self.method if self.method == 'FPS': assert self.nelements > 0, f'nelements={self.nelements} method={self.method}' assert self.melements > 0, f'melements={self.melements} method={self.method}' assert self.box2 >= self.box1, 'box1=%s box2=%s' % (self.box1, self.box2) assert self.usage in ['FORCE', 'DISP', 'BOTH'], 'usage = %s' % self.usage
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a SPLINE1 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') caero = integer(card, 2, 'caero') box1 = integer(card, 3, 'box1') box2 = integer(card, 4, 'box2') setg = integer(card, 5, 'setg') dz = double_or_blank(card, 6, 'dz', default=0.0) method = string_or_blank(card, 7, 'method', default='IPS') usage = string_or_blank(card, 8, 'usage', default='BOTH') nelements = integer_or_blank(card, 9, 'nelements', default=10) melements = integer_or_blank(card, 10, 'melements', default=10) assert len(card) <= 11, f'len(SPLINE1 card) = {len(card):d}\ncard={card}' return SPLINE1(eid, caero, box1, box2, setg, dz, method, usage, nelements, melements, comment=comment)
@classmethod def add_op2_data(cls, data, comment: str=''): eid = data[0] caero = data[1] box1 = data[2] box2 = data[3] setg = data[4] dz = data[5] method = data[6] usage = data[7] nelements = data[8] melements = data[9] assert len(data) == 10, 'data = %s' % data return SPLINE1(eid, caero, box1, box2, setg, dz, method, usage, nelements, melements, comment=comment) @property def aero_element_ids(self) -> np.ndarray: return np.arange(self.box1, self.box2 + 1)
[docs] def CAero(self) -> int: return caero_id(self.caero_ref, self.caero)
[docs] def Set(self) -> int: return set_id(self.setg_ref, self.setg)
[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 SPLINE1 eid=%s' % self.eid self.caero_ref = model.CAero(self.caero, msg=msg) self.setg_ref = model.Set(self.setg, msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.cross_reference_set(model, 'MACRO', msg=msg) else: self.setg_ref.cross_reference_set(model, 'Node', msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE1 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = f', which is required by SPLINE1 eid={self.eid}' self.caero_ref = model.safe_caero(self.caero, self.eid, xref_errors, msg=msg) #self.setg_ref = model.safe_set(self, self.setg, self.eid, xref_errors, msg=msg) #def safe_set(self, setg, set_type, self.eid, xref_errors, msg=''): try: self.setg_ref = model.Set(self.setg, msg=msg) try: self.setg_ref.safe_cross_reference(model, 'Node', msg=msg) except Exception: print(self.setg_ref) raise nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE1 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) model.log.warning(msg) msg = '' except KeyError: model.log.warning('failed to find SETx set_id=%s%s; allowed_sets=%s' % ( self.setg, msg, np.unique(list(model.sets.keys()))))
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.caero = self.CAero() self.setg = self.Set() self.caero_ref = None self.setg_ref = None
def _verify(self, xref: bool) -> None: self.aero_element_ids if xref: caero: CAERO1 = self.caero_ref p1, p2, p3, p4 = caero.get_points() # p1---p4 # | | # p2---p3 a = p3 - p1 b = p2 - p4 # print(f'p1 = {p1}') # print(f'p2 = {p2}') # print(f'p3 = {p3}') # print(f'p4 = {p4}') # print(f'a = {a}') # print(f'b = {b}') axb = np.cross(a, b) # print(f'axb = {axb}') normi = np.linalg.norm(axb) area = 0.5 * normi normal = axb / normi # print(f'area = {area}') # print(f'normal = {normal}') # just toss the "z" value of the spline # this is nice, so we can find the point in local xy space (so there's no xform for a wing spline) centroid = (p1 + p2 + p3 + p4) / 4. # centroid = np.zeros(3) # print(f'centroid = {centroid}') setg: SET1 = self.setg_ref p12 = (p1 + p2) / 2. p34 = (p3 + p4) / 2. j = p34 - p12 j /= np.linalg.norm(j) i = np.cross(j, normal) # print(f'i = {i}') xform = np.vstack([i, j, normal]).round(2) nodes_ref: list[GRID] = setg.ids_ref # print(f'xform:\n{xform}') local_spline_points = [] for ipoint, node in enumerate(nodes_ref): xyz = node.get_position() # Vector from the panel point to the point vector_to_point = xyz - centroid # Distance from the point to the panel along the normal distance_to_panel = np.dot(vector_to_point, normal) # Project the point onto the panel projected_point = xyz - distance_to_panel * normal # projected_point2 = xform @ (xyz - centroid) # projected_point3 = xform.T @ (xyz - centroid) # print(f'xyz[{ipoint}] = {xyz}') # print(f'projected_point1[{ipoint}] = proj = {projected_point}') # print(f'projected_point2[{ipoint}] = xform @ xyz = {projected_point2}\n') # print(f'projected_point3[{ipoint}] = xform.T @ xyz = {projected_point3}\n') local_spline_points.append(projected_point) del xform local_spline_points_array = np.array(local_spline_points) try: hull = scipy.spatial.ConvexHull(local_spline_points_array[:, :2]) area_hull = hull.area except scipy.spatial._qhull.QhullError: area_hull = np.nan # The points are now projected (with no z value) # Let's find the convex hull of points # Finally, compute the area of the convex hull #area_hull = 0.95 * area area_ratio = area_hull / area msg = ( f'Spline Points for SPLINE1 eid={self.eid} caero={self.caero} dont span the surface\n' f' area_hull/area = {area_hull:g}/{area:g} = {area_ratio:.3g}\n' f' local_spline_points:\n{local_spline_points_array}' ) if np.isnan(area_ratio) or area_ratio <= 0.25: # model.log.error(msg) # raise RuntimeError(msg) #warnings.warn(msg) pass
[docs] def raw_fields(self) -> list: """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['SPLINE1', self.eid, self.CAero(), self.box1, self.box2, self.Set(), self.dz, self.method, self.usage, self.nelements, self.melements] return list_fields
[docs] def repr_fields(self) -> list: dz = set_blank_if_default(self.dz, 0.) method = set_blank_if_default(self.method, 'IPS') usage = set_blank_if_default(self.usage, 'BOTH') nelements = set_blank_if_default(self.nelements, 10) melements = set_blank_if_default(self.melements, 10) list_fields = ['SPLINE1', self.eid, self.CAero(), self.box1, self.box2, self.Set(), dz, method, usage, nelements, melements] list_fields = wipe_empty_fields(list_fields) 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 SPLINE2(Spline): """ Linear Spline Defines a beam spline for interpolating motion and/or forces for aeroelastic problems on aerodynamic geometries defined by regular arrays of aerodynamic points. +---------+------+-------+-------+-------+------+----+------+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+======+=======+=======+=======+======+====+======+=====+ | SPLINE2 | EID | CAERO | ID1 | ID2 | SETG | DZ | DTOR | CID | +---------+------+-------+-------+-------+------+----+------+-----+ | | DTHX | DTHY | None | USAGE | | | | | +---------+------+-------+-------+-------+------+----+------+-----+ | SPLINE2 | 5 | 8 | 12 | 24 | 60 | 0. | 1.0 | 3 | +---------+------+-------+-------+-------+------+----+------+-----+ | | 1. | | | | | | | | +---------+------+-------+-------+-------+------+----+------+-----+ """ type = 'SPLINE2' _field_map = { 1: 'eid', 2: 'caero', 3: 'id1', 4: 'id2', 5: 'setg', 6: 'dz', 7: 'dtor', 8: 'cid', 9: 'dthx', 10: 'dthy', } _properties = ['aero_element_ids']
[docs] @classmethod def _init_from_empty(cls): eid = 1 caero = 1 box1 = 1 box2 = 2 setg = 1 return SPLINE2(eid, caero, box1, box2, setg, dz=0.0, dtor=1.0, cid=0, dthx=0., dthy=0., usage='BOTH', comment='')
def __init__(self, eid: int, caero: int, box1: int, box2: int, setg: int, dz: float=0.0, dtor: float=1.0, cid: int=0, dthx: float=0.0, dthy: float=0.0, usage: str='BOTH', comment: str=''): """ Creates a SPLINE2 card, which defines a beam spline. Parameters ---------- eid : int spline id caero : int CAEROx id that defines the plane of the spline box1 / box2 : int First/last box/body id that is used by the spline setg : int SETx id that defines the list of GRID points that are used by the beam spline dz : float; default=0.0 linear attachment flexibility dz = 0.; spline passes through all grid points dtor : float; default=1.0 Torsional flexibility ratio (EI/GJ). Use 1.0 for bodies (CAERO2). cid : int; default=0 Rectangular coordinate system for which the y-axis defines the axis of the spline. Not used for bodies, CAERO2 dthx : float; default=0. Rotational attachment flexibility. DTHX : Used for rotation about the spline's x-axis (in-plane bending rotations). It is not used for bodies (CAERO2). DTHY : Used for rotation about the spline's y-axis (torsion). It is used for slope of bodies. usage : str; default=BOTH Spline usage flag to determine whether this spline applies to the force transformation, displacement transformation, or both valid_usage = {FORCE, DISP, BOTH} comment : str; default='' a comment for the card """ Spline.__init__(self) if comment: self.comment = comment self.eid = eid self.caero = caero self.box1 = box1 self.box2 = box2 self.setg = setg self.dz = dz self.dtor = dtor self.cid = cid self.dthx = dthx self.dthy = dthy self.usage = usage self.cid_ref = None self.caero_ref = None self.setg_ref = None
[docs] def validate(self) -> None: assert self.box2 >= self.box1, 'box2=%s box1=%s' % (self.box2, self.box1)
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a SPLINE2 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') caero = integer(card, 2, 'caero') id1 = integer(card, 3, 'id1') id2 = integer(card, 4, 'id2') setg = integer(card, 5, 'setg') dz = double_or_blank(card, 6, 'dz', default=0.0) dtor = double_or_blank(card, 7, 'dtor', default=1.0) cid = integer_or_blank(card, 8, 'cid', default=0) dthx = double_or_blank(card, 9, 'dthx', default=0.) dthy = double_or_blank(card, 10, 'dthy', default=0.) usage = string_or_blank(card, 12, 'usage', default='BOTH') assert len(card) <= 13, f'len(SPLINE2 card = {len(card):d}\ncard={card}' return SPLINE2(eid, caero, id1, id2, setg, dz, dtor, cid, dthx, dthy, usage, comment=comment)
def _verify(self, xref: bool) -> None: self.aero_element_ids if xref: caero: CAERO1 | CAERO2 = self.caero_ref if isinstance(caero, CAERO1): p1, p2, p3, p4 = caero.get_points() centroid = (p1 + p2 + p3 + p4) / 4. area = caero.area() assert isinstance(area, float_types), f'area={area}' elif isinstance(caero, CAERO2): p1, p2 = caero.get_points() # p1---p4 # | | # p2---p3 ihat = p2 - p1 length = np.linalg.norm(ihat) assert isinstance(length, float_types), f'length={length}' # print(f'p1 = {p1}') # print(f'p2 = {p2}') centroid = (p1 + p2) / 2. # print(f'centroid = {centroid}') else: raise TypeError(str(caero)) setg: SET1 = self.setg_ref nodes_ref: list[GRID] = setg.ids_ref local_spline_points = [] for ipoint, node in enumerate(nodes_ref): xyz = node.get_position() local_spline_points.append(xyz) local_spline_points_array = np.array(local_spline_points)
[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 SPLINE2 eid=%s' % self.eid self.cid_ref = model.Coord(self.Cid(), msg=msg) self.caero_ref = model.CAero(self.CAero(), msg=msg) self.setg_ref = model.Set(self.Set(), msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.cross_reference_set(model, 'MACRO', msg=msg) else: self.setg_ref.cross_reference_set(model, 'Node', msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 2: msg = f'SPLINE2 requires at least 2 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = f', which is required by SPLINE2 eid={self.eid}' self.cid_ref = model.safe_coord(self.Cid(), self.eid, xref_errors, msg=msg) try: self.caero_ref = model.CAero(self.CAero(), msg=msg) except KeyError: pass try: self.setg_ref = model.Set(self.Set(), msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.safe_cross_reference(model, 'MACRO', msg=msg) else: self.setg_ref.safe_cross_reference(model, 'Node', msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 2: msg = f'SPLINE2 requires at least 2 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg) except KeyError: pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.cid = self.Cid() self.caero = self.CAero() self.setg = self.Set() self.cid_ref = None self.caero_ref = None self.setg_ref = None
@property def aero_element_ids(self) -> np.ndarray: return np.arange(self.box1, self.box2 + 1)
[docs] def Cid(self) -> int: return coord_id(self.cid_ref, self.cid)
[docs] def CAero(self) -> int: return caero_id(self.caero_ref, self.caero)
[docs] def Set(self) -> int: return set_id(self.setg_ref, self.setg)
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['SPLINE2', self.eid, self.CAero(), self.box1, self.box2, self.Set(), self.dz, self.dtor, self.Cid(), self.dthx, self.dthy, None, self.usage] return list_fields
[docs] def repr_fields(self): dz = set_blank_if_default(self.dz, 0.) usage = set_blank_if_default(self.usage, 'BOTH') list_fields = ['SPLINE2', self.eid, self.CAero(), self.box1, self.box2, self.Set(), dz, self.dtor, self.Cid(), self.dthx, self.dthy, None, usage] 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 SPLINE3(Spline): """ Defines a constraint equation for aeroelastic problems. Useful for control surface constraints. +---------+------+-------+-------+------+----+----+-----+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+======+=======+=======+======+====+====+=====+=======+ | SPLINE3 | EID | CAERO | BOXID | COMP | G1 | C1 | A1 | USAGE | +---------+------+-------+-------+------+----+----+-----+-------+ | | G2 | C2 | A2 | | G3 | C3 | A2 | | +---------+------+-------+-------+------+----+----+-----+-------+ | | G4 | C4 | A4 | etc. | | | | | +---------+------+-------+-------+------+----+----+-----+-------+ | SPLINE3 | 7000 | 107 | 109 | 6 | 5 | 3 | 1.0 | BOTH | +---------+------+-------+-------+------+----+----+-----+-------+ | | 43 | 5 | -1.0 | | | | | | +---------+------+-------+-------+------+----+----+-----+-------+ """ type = 'SPLINE3' _properties = ['node_ids'] _field_map = { 1: 'eid', 2: 'caero', 3: 'box_id', 7: 'a1', 8: 'usage', # 5:'g1', 6:'c1', # 9:G2,C2,A2... }
[docs] @classmethod def _init_from_empty(cls): eid = 1 caero = 1 box_id = 1 components = 1 nodes = [2, 3] displacement_components = [2, 3] coeffs = [2., 3.] return SPLINE3(eid, caero, box_id, components, nodes, displacement_components, coeffs, usage='BOTH')
def __init__(self, eid: int, caero: int, box_id: int, components: int, nodes: list[int], displacement_components: list[int], coeffs: list[float], usage: str='BOTH', comment: str=''): """ Creates a SPLINE3 card, which is useful for control surface constraints. Parameters ---------- eid : int spline id caero : int CAEROx id that defines the plane of the spline box_id : int Identification number of the aerodynamic box number. components : int The component of motion to be interpolated. 3, 5 (CAERO1) 2, 3, 5, 6 (CAERO2) 3 (CAERO3) 3, 5, 6 (CAERO4) 3, 5, 6 (CAERO5) 1, 2, 3, 5, 6 (3D Geometry) 2-lateral displacement 3-transverse displacement 5-pitch angle 6-relative control angle for CAERO4/5; yaw angle for CAERO2 nodes : list[int] Grid point identification number of the independent grid point. displacement_components : list[int] Component numbers in the displacement coordinate system. 1-6 (GRIDs) 0 (SPOINTs) coeffs : list[float] Coefficient of the constraint relationship. usage : str; default=BOTH Spline usage flag to determine whether this spline applies to the force transformation, displacement transformation, or both valid_usage = {FORCE, DISP, BOTH} comment : str; default='' a comment for the card """ Spline.__init__(self) if comment: self.comment = comment if isinstance(nodes, integer_types): nodes: list[int] = [nodes] if isinstance(displacement_components, integer_types): displacement_components: list[int] = [displacement_components] if isinstance(coeffs, float): coeffs = [coeffs] self.eid = eid self.caero = caero self.box_id = box_id # if isinstance(components, integer_types): # components = str(components) self.components = components self.usage = usage self.nodes = nodes self.displacement_components = displacement_components self.coeffs = coeffs self.nodes_ref = None self.caero_ref = None
[docs] def validate(self): msg = '' if self.components not in [0, 1, 2, 3, 4, 5, 6]: msg += 'components=%r must be [0, 1, 2, 3, 4, 5, 6]\n' % ( self.components) if not len(self.nodes) == len(self.displacement_components): msg += 'nnodes=%s ndisplacement_components=%s must be equal\n' % ( len(self.nodes), len(self.displacement_components)) if not len(self.nodes) == len(self.coeffs): msg += 'nnodes=%s ncoeffs=%s must be equal\n' % ( len(self.nodes), len(self.coeffs)) for i, disp_component in enumerate(self.displacement_components): if disp_component not in [0, 1, 2, 3, 4, 5, 6]: if not isinstance(disp_component, integer_types): msg += ( f'i={i} displacement_component={disp_component!r} must be an integer ' f'[0, 1, 2, 3, 4, 5, 6]; type={type(disp_component)}\n') else: msg += f'i={i} displacement_component={disp_component} must be [0, 1, 2, 3, 4, 5, 6]\n' if self.usage not in ['FORCE', 'DISP', 'BOTH']: msg += f'usage={self.usage} must be in [FORCE, DISP, BOTH]\n' if msg: msg += str(self) raise RuntimeError(msg) for node in self.nodes: assert isinstance(node, integer_types), self.nodes for displacement_component in self.displacement_components: assert isinstance(displacement_component, integer_types), self.displacement_components for coeff in self.coeffs: assert isinstance(coeff, float), self.coeffs
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a SPLINE3 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') caero = integer(card, 2, 'caero') box_id = integer(card, 3, 'box_id') components = integer(card, 4, 'comp') node = integer(card, 5, 'G1') displacement_component = integer(card, 6, 'C1') coeff = double(card, 7, 'A1') usage = string_or_blank(card, 8, 'usage', default='BOTH') nfields = len(card) - 1 nrows = nfields // 8 if nfields % 8: nrows += 1 nodes = [node] coeffs = [coeff] displacement_components = [displacement_component] i = 2 for irow in range(1, nrows): #print('G%i' % i) j = 1 + irow * 8 node = integer(card, j, 'G%i' % i) displacement_component = integer(card, j + 1, 'C%i' % i) coeff = double(card, j + 2, 'A%i' % i) nodes.append(node) coeffs.append(coeff) displacement_components.append(displacement_component) i += 1 if card.field(j + 4) or card.field(j + 5) or card.field(j + 6): node = integer(card, j + 4, 'G%i' % i) displacement_component = parse_components(card, j + 5, 'C%i' % i) coeff = double(card, j + 6, 'A%i' % i) nodes.append(node) coeffs.append(coeff) displacement_components.append(int(displacement_component)) i += 1 spline = SPLINE3(eid, caero, box_id, components, nodes, displacement_components, coeffs, usage=usage, comment=comment) return spline
[docs] def cross_reference(self, model: BDF) -> None: msg = ', which is required by SPLINE3 eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.caero_ref = model.CAero(self.caero, msg=msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = ', which is required by SPLINE3 eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.caero_ref = model.safe_caero(self.caero, self.eid, xref_errors, msg=msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.caero = self.CAero() self.nodes = self.node_ids self.nodes_ref = None self.caero_ref = None
[docs] def CAero(self) -> int: return caero_id(self.caero_ref, self.caero)
@property def node_ids(self): if self.nodes_ref is None: return self.nodes return [node.nid for node in self.nodes_ref]
[docs] def raw_fields(self): """ +---------+------+-------+-------+------+----+----+-----+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+======+=======+=======+======+====+====+=====+=======+ | SPLINE3 | EID | CAERO | BOXID | COMP | G1 | C1 | A1 | USAGE | +---------+------+-------+-------+------+----+----+-----+-------+ | | G2 | C2 | A2 | ---- | G3 | C3 | A2 | --- | +---------+------+-------+-------+------+----+----+-----+-------+ | | G4 | C4 | A4 | etc. | | | | | +---------+------+-------+-------+------+----+----+-----+-------+ """ list_fields = [ 'SPLINE3', self.eid, self.CAero(), self.box_id, self.components, self.nodes[0], self.displacement_components[0], self.coeffs[0], self.usage] for nid, disp_c, coeff in zip(self.nodes[1:], self.displacement_components[1:], self.coeffs[1:]): list_fields += [nid, disp_c, coeff, None] 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 SPLINE4(Spline): """ Surface Spline Methods Defines a curved surface spline for interpolating motion and/or forces for aeroelastic problems on general aerodynamic geometries using either the Infinite Plate, Thin Plate or Finite Plate splining method. NX +---------+-------+-------+--------+-------+------+----+------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=======+=======+========+=======+======+====+======+=======+ | SPLINE4 | EID | CAERO | AELIST | | SETG | DZ | METH | USAGE | +---------+-------+-------+--------+-------+------+----+------+-------+ | | NELEM | MELEM | | | | | | | +---------+-------+-------+--------+-------+------+----+------+-------+ | SPLINE4 | 3 | 111 | 115 | | 14 | 0. | IPS | | +---------+-------+-------+--------+-------+------+----+------+-------+ MSC +---------+-------+-------+--------+-------+------+----+------+-------+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=======+=======+========+=======+======+====+======+=======+ | SPLINE4 | EID | CAERO | AELIST | | SETG | DZ | METH | USAGE | +---------+-------+-------+--------+-------+------+----+------+-------+ | | NELEM | MELEM | FTYPE | RCORE | | | | | +---------+-------+-------+--------+-------+------+----+------+-------+ | SPLINE4 | 3 | 111 | 115 | | 14 | 0. | IPS | | +---------+-------+-------+--------+-------+------+----+------+-------+ """ type = 'SPLINE4' _properties = ['aero_element_ids'] _field_map = { 1: 'eid', 2: 'caero', 3: 'aelist', 5: 'setg', 6: 'dz', 7: 'method', 8: 'usage', 9: 'nelements', 10: 'melements', }
[docs] @classmethod def _init_from_empty(cls): eid = 1 caero = 1 aelist = 1 setg = 1 dz = 1. method = 'IPS' usage = 'BOTH' nelements = 1 melements = 1 return SPLINE4(eid, caero, aelist, setg, dz, method, usage, nelements, melements, comment='')
def __init__(self, eid: int, caero: int, aelist: int, setg: int, dz: float, method: str, usage: str, nelements: int, melements: int, ftype: Optional[str]=None, rcore: Optional[float]=None, comment: str=''): """ Creates a SPLINE4 card, which defines a curved Infinite Plate, Thin Plate, or Finite Plate Spline. Parameters ---------- eid : int spline id caero : int CAEROx id that defines the plane of the spline box1 / box2 : int First/last box id that is used by the spline setg : int SETx id that defines the list of GRID points that are used by the surface spline dz : float; default=0.0 linear attachment flexibility dz = 0.; spline passes through all grid points method : str; default=IPS method for spline fit valid_methods = {IPS, TPS, FPS} IPS : Harder-Desmarais Infinite Plate Spline TPS : Thin Plate Spline FPS : Finite Plate Spline usage : str; default=BOTH Spline usage flag to determine whether this spline applies to the force transformation, displacement transformation, or both valid_usage = {FORCE, DISP, BOTH} nelements / melements : int; default=10 The number of FE elements along the local spline x/y-axis if using the FPS option ftype: str; default=None MSC only rcore : float; default=None MSC only comment : str; default='' a comment for the card """ Spline.__init__(self) if comment: self.comment = comment self.eid = eid self.caero = caero self.aelist = aelist self.setg = setg self.dz = dz self.method = method self.usage = usage self.nelements = nelements self.melements = melements self.ftype = ftype self.rcore = rcore self.caero_ref = None self.setg_ref = None self.aelist_ref = None
[docs] def validate(self): assert self.method in ['IPS', 'TPS', 'FPS'], 'method = %s' % self.method assert self.usage in ['FORCE', 'DISP', 'BOTH'], 'uasge = %s' % self.usage
[docs] @classmethod def add_card(cls, card: BDFCard, comment: str=''): """ Adds a SPLINE4 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') caero = integer(card, 2, 'caero') aelist = integer(card, 3, 'aelist') # None setg = integer(card, 5, 'setg') dz = double_or_blank(card, 6, 'dz', default=0.0) method = string_or_blank(card, 7, 'method', default='IPS') usage = string_or_blank(card, 8, 'usage', default='BOTH') nelements = integer_or_blank(card, 9, 'nelements', default=10) melements = integer_or_blank(card, 10, 'melements', default=10) ftype = string_or_blank(card, 11, 'ftype', default='WF2') rcore = double_or_blank(card, 12, 'rcore') assert len(card) <= 13, f'len(SPLINE4 card = {len(card):d}\ncard={card}' return SPLINE4(eid, caero, aelist, setg, dz, method, usage, nelements, melements, ftype=ftype, rcore=rcore, comment=comment)
@classmethod def add_op2_data(cls, data, comment: str=''): eid = data[0] caero = data[1] aelist = data[2] setg = data[3] dz = data[4] method = data[5] usage = data[6] nelements = data[7] melements = data[8] assert len(data) == 9, f'data = {data}' return SPLINE4(eid, caero, aelist, setg, dz, method, usage, nelements, melements, comment=comment)
[docs] def CAero(self) -> int: return caero_id(self.caero_ref, self.caero)
[docs] def AEList(self) -> int: return aelist_id(self.aelist_ref, self.aelist)
[docs] def Set(self) -> int: return set_id(self.setg_ref, self.setg)
@property def aero_element_ids(self) -> list[int]: return self.aelist_ref.elements
[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 = f', which is required by SPLINE4 eid={self.eid}' self.caero_ref = model.CAero(self.CAero(), msg=msg) self.setg_ref = model.Set(self.Set(), msg=msg) self.aelist_ref = model.AEList(self.aelist, msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.cross_reference_set(model, 'MACRO', msg=msg) else: self.setg_ref.cross_reference_set(model, 'Node', msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE4 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): """ Cross links the card so referenced cards can be extracted directly Parameters ---------- model : BDF() the BDF object """ msg = f', which is required by SPLINE4 eid={self.eid}' self.caero_ref = model.safe_caero(self.caero, self.eid, xref_errors, msg=msg) self.aelist_ref = model.safe_aelist(self.aelist, self.eid, xref_errors, msg=msg) self.setg_ref = model.Set(self.Set(), msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.cross_reference_set(model, 'MACRO', msg=msg) else: self.setg_ref.cross_reference_set(model, 'Node', msg) nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE4 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise ValueError(msg)
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.caero = self.CAero() self.setg = self.Set() self.aelist = self.AEList() self.caero_ref = None self.setg_ref = None self.aelist_ref = None
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['SPLINE4', self.eid, self.CAero(), self.AEList(), None, self.Set(), self.dz, self.method, self.usage, self.nelements, self.melements, self.ftype, self.rcore] return list_fields
[docs] def repr_fields(self): dz = set_blank_if_default(self.dz, 0.) method = set_blank_if_default(self.method, 'IPS') usage = set_blank_if_default(self.usage, 'BOTH') nelements = set_blank_if_default(self.nelements, 10) melements = set_blank_if_default(self.melements, 10) list_fields = ['SPLINE4', self.eid, self.CAero(), self.AEList(), None, self.Set(), dz, method, usage, nelements, melements, self.ftype, self.rcore] list_fields = wipe_empty_fields(list_fields) 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 SPLINE5(Spline): """ Linear Spline Defines a 1D beam spline for interpolating motion and/or forces for aeroelastic problems on aerodynamic geometries defined by irregular arrays of aerodynamic points. The interpolating beam supports axial rotation and bending in the yz-plane. +=========+======+=======+========+=======+======+====+=======+=======+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+======+=======+========+=======+======+====+=======+=======+ | SPLINE5 | EID | CAERO | AELIST | | SETG | DZ | DTOR | CID | +---------+------+-------+--------+-------+------+----+-------+-------+ | | DTHX | DTHY | | USAGE | METH | | FTYPE | RCORE | +---------+------+-------+--------+-------+------+----+-------+-------+ METH, FTYPE, RCORE are in 2012+ (not MSC.2005r2 or NX.10) """ type = 'SPLINE5' _properties = ['aero_element_ids'] _field_map = { 1: 'eid', 2: 'caero', 3: 'aelist', 5: 'setg', 6: 'dz', 7: 'dtor', 8: 'cid', 9: 'thx', 10: 'thy', 12: 'usage', 13: 'meth', 15: 'ftype', 16: 'rcore', }
[docs] @classmethod def _init_from_empty(cls): eid = 1 caero = 1 aelist = 1 setg = 1 thx = 1. thy = 1. return SPLINE5(eid, caero, aelist, setg, thx, thy, dz=0., dtor=1.0, cid=0, usage='BOTH', method='BEAM', ftype='WF2', rcore=None, comment='')
def __init__(self, eid, caero, aelist, setg, thx, thy, dz=0., dtor=1.0, cid=0, usage='BOTH', method='BEAM', ftype='WF2', rcore=None, comment=''): Spline.__init__(self) if comment: self.comment = comment self.eid = eid self.caero = caero self.aelist = aelist self.setg = setg self.dz = dz self.dtor = dtor self.cid = cid self.thx = thx self.thy = thy self.usage = usage self.method = method self.ftype = ftype self.rcore = rcore self.cid_ref = None self.caero_ref = None self.setg_ref = None self.aelist_ref = None
[docs] def validate(self): assert isinstance(self.cid, integer_types), self.cid assert self.method in ['BEAM', 'RIS'], self.method assert self.ftype in ['WF0', 'WF2'], self.ftype
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a SPLINE5 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') caero = integer(card, 2, 'caero') aelist = integer(card, 3, 'aelist') # None setg = integer(card, 5, 'setq') dz = double_or_blank(card, 6, 'dz', default=0.0) dtor = double_or_blank(card, 7, 'dtor', default=1.0) cid = integer_or_blank(card, 8, 'cid', default=0) thx = double(card, 9, 'thx') thy = double(card, 10, 'thy') usage = string_or_blank(card, 12, 'usage', default='BOTH') # per nast/tpl/fmondsp.dat, METH can be a double(0.0) ??? method = string_or_blank(card, 13, 'meth', default='BEAM') ftype = string_or_blank(card, 15, 'ftype', default='WF2') rcore = double_or_blank(card, 16, 'rcore') #usage = string_or_blank(card, 12, 'usage', default='BOTH') # ??? assert len(card) <= 17, 'len(SPLINE5 card) = %i\n%s' % (len(card), card) return SPLINE5(eid, caero, aelist, setg, thx, thy, dz=dz, dtor=dtor, cid=cid, usage=usage, method=method, ftype=ftype, rcore=rcore, comment=comment)
@property def aero_element_ids(self): return self.aelist_ref.elements
[docs] def Cid(self) -> int: return coord_id(self.cid_ref, self.cid)
[docs] def CAero(self) -> int: return caero_id(self.caero_ref, self.caero)
[docs] def AEList(self) -> int: return aelist_id(self.aelist_ref, self.aelist)
[docs] def Set(self) -> int: return set_id(self.setg_ref, self.setg)
[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 SPLINE5 eid=%s' % self.eid self.cid_ref = model.Coord(self.cid, msg=msg) self.caero_ref = model.CAero(self.caero, msg=msg) self.setg_ref = model.Set(self.setg, msg=msg) self.aelist_ref = model.AEList(self.aelist, msg=msg) if self.setg_ref.type == 'SET2': self.setg_ref.cross_reference_set(model, 'MACRO', msg=msg) else: self.setg_ref.cross_reference_set(model, 'Node', msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE5 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg)
[docs] def safe_cross_reference(self, model: BDF, xref_errors): msg = f', which is required by SPLINE5 eid={self.eid}' self.cid_ref = model.safe_coord(self.cid, self.eid, xref_errors, msg=msg) self.caero_ref = model.safe_caero(self.caero, self.eid, xref_errors, msg=msg) try: self.setg_ref = model.Set(self.setg, msg=msg) nnodes = len(self.setg_ref.ids) if nnodes < 3: msg = f'SPLINE5 requires at least 3 nodes; nnodes={nnodes}\n' msg += str(self) msg += str(self.setg_ref) raise RuntimeError(msg) except KeyError: pass try: self.setg_ref.cross_reference_set(model, 'Node', msg) self.aelist_ref = model.AEList(self.aelist, msg=msg) except KeyError: pass
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.cid = self.Cid() self.caero = self.CAero() self.setg = self.Set() self.aelist = self.AEList() self.caero_ref = None self.setg_ref = None self.aelist_ref = None
[docs] def raw_fields(self): """ Gets the fields in their unmodified form Returns ------- fields : list[varies] the fields that define the card """ list_fields = ['SPLINE5', self.eid, self.CAero(), self.AEList(), None, self.Set(), self.dz, self.dtor, self.Cid(), self.thx, self.thy, None, self.usage, self.method, None, self.ftype, self.rcore] return list_fields
[docs] def repr_fields(self): dz = set_blank_if_default(self.dz, 0.) usage = set_blank_if_default(self.usage, 'BOTH') list_fields = ['SPLINE5', self.eid, self.CAero(), self.AEList(), None, self.Set(), dz, self.dtor, self.Cid(), self.thx, self.thy, None, usage, self.method, None, self.ftype, self.rcore] 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] def get_caero_count(model: BDF) -> tuple[int, int, int, int]: """ Get the count of panels for sizing purposes Parameters ---------- model : BDF the FEM model Returns ------- ncaeros : int number of CAEROx panels ncaeros_sub : int number of CAEROx sub-panels ncaeros_points : int number of CAERO points that define the panels (4 per CAERO1/3/4/5 aero box; 2 per CAERO2 panel) ncaero_sub_points : int number of CAEROx sub-points (1 per panel) CAERO2, BODY7 not supported """ ncaeros = 0 ncaeros_sub = 0 #ncaeros_cs = 0 ncaeros_points = 0 ncaero_sub_points = 0 # count caeros # sorting doesn't matter here because we're just trying to size the array for caero in model.caeros.values(): if hasattr(caero, 'panel_points_elements'): npoints, nelements = caero.get_panel_npoints_nelements() ncaeros_sub += npoints ncaero_sub_points += nelements elif caero.type in ['CAERO2', 'BODY7']: pass else: # pragma: no cover msg = '%r doesnt support panel_points_elements\n%s' % (caero.type, caero.rstrip()) raise NotImplementedError(msg) for unused_eid, caero in sorted(model.caeros.items()): if isinstance(caero, (CAERO1, CAERO3, CAERO4, CAERO5)) or caero.type == 'CAERO7': ncaeros_points += 4 ncaeros += 1 elif caero.type in ['CAERO2', 'BODY7']: points, elems = caero.get_points_elements_3d() if points is None: continue ncaeros_points += points.shape[0] ncaeros += elems.shape[0] else: # pragma: no cover msg = '%r doesnt support panel counter\n%s' % (caero.type, caero.rstrip()) raise NotImplementedError(msg) return ncaeros, ncaeros_sub, ncaeros_points, ncaero_sub_points
[docs] def get_caero_points(model: BDF, box_id_to_caero_element_map: dict[int, np.ndarray]) -> tuple[np.ndarray, bool]: has_caero = False num_prev = 0 ncaeros_sub = 0 if model.caeros: caero_points_list = [] for unused_eid, caero in sorted(model.caeros.items()): if caero.type in ('CAERO1', 'CAERO4', 'CAERO5', 'CAERO7'): box_ids = caero.box_ids assert box_ids.min() > 0, box_ids nboxes = len(box_ids.ravel()) if nboxes > 1000: print('skipping nboxes=%s for:\n%s' % (nboxes, str(caero))) continue ncaeros_sub += 1 pointsi, elementsi = caero.panel_points_elements() caero_points_list.append(pointsi) for i, box_id in enumerate(caero.box_ids.flat): box_id_to_caero_element_map[box_id] = elementsi[i, :] + num_prev num_prev += pointsi.shape[0] elif caero.type in ('CAERO2', 'BODY7'): pass else: print('caero\n%s' % caero) if ncaeros_sub: caero_points = np.vstack(caero_points_list) has_caero = True if ncaeros_sub == 0: caero_points = np.empty((0, 3)) return caero_points, has_caero
[docs] def get_caero_box_grid(model: BDF) -> tuple[np.ndarray, np.ndarray]: """builds the CAERO aerobox grid in 3d space""" j = 0 points = [] elements = [] for unused_eid, element in sorted(model.caeros.items()): if isinstance(element, (CAERO1, CAERO3, CAERO4, CAERO5)) or element.type == 'CAERO7': pointsi, elementsi = element.panel_points_elements() points.append(pointsi) elements.append(j + elementsi) #j += ipoint + 1 j += len(pointsi) else: model.log.info(f'skipping {element.type}') if len(points) == 0: points_array = np.zeros((0, 3), dtype='float32') elements_array = np.zeros((0, 4), dtype='int32') return points_array, elements_array if len(elements) == 1: points_array = np.vstack(points) elements_array = elements[0] # .reshape(1, 4) else: points_array = np.vstack(points) elements_array = np.vstack(elements) return points_array, elements_array
field_names = [ 'has_caero', 'caero_points', 'ncaeros', 'ncaeros_sub', 'ncaeros_cs', 'ncaeros_points', 'ncaero_sub_points', 'has_control_surface', 'box_id_to_caero_element_map', 'cs_box_ids', ] AeroPaneling = namedtuple('AeroPaneling', field_names)
[docs] def build_caero_paneling(model: BDF) -> tuple[str, list[str], AeroPaneling]: """ Creates the CAERO panel inputs including: - caero - caero_boxes - caero_control_surfaces - N control surfaces Parameters ---------- model : BDF() the bdf model Returns ------- all_control_surface_name : str name for all control surfaces actor ('' if no control surfaces) caero_control_surface_names: list[str] names of the control surfaces out : AeroPaneling namedtuple for: has_caero : bool are there any CAEROx panels? caero_points : (N_aero_points, 3) float ndarray the xyz points for the aero panels N_aero_points can be 0 ncaeros : int the number of aero sub-panels? ncaeros_sub : int ??? ncaeros_cs : int ??? ncaeros_points : int number of points for the caero coarse grid ncaero_sub_points : int number of points for the caero fine/aerobox grid has_control_surface : bool is there a control surface box_id_to_caero_element_map : dict[box_id] = box_index used to map the CAEROx box id to index in the ??? (aero panel elements) array, which will be used with cs_box_ids cs_box_ids : dict[control_surface_name] : list[panel ids] list of panels used by each aero panel """ ncaeros_cs = 0 has_control_surface = False box_id_to_caero_element_map = {} cs_box_ids = defaultdict(list) all_control_surface_name = '' caero_control_surface_names = [] log = model.log ncaeros, ncaeros_sub, ncaeros_points, ncaero_sub_points = get_caero_count(model) caero_points, has_caero = get_caero_points(model, box_id_to_caero_element_map) # check for any control surfcaes if model.aesurf: has_control_surface = True # ncaero_cs_points = 0 # self.gui.create_alternate_vtk_grid( # 'caero_control_surfaces', color=PINK_FLOAT, line_width=5, opacity=1.0, # representation='surface', is_visible=False) all_control_surface_name = 'caero_control_surfaces' # sort the control surfaces labels_to_aesurfs = {aesurf.label: aesurf for aesurf in model.aesurf.values()} if len(labels_to_aesurfs) != len(model.aesurf): msg = ( 'Expected same number of label->aesurf as aid->aesurf\n' 'labels_to_aesurfs = %r\n' 'model.aesurf = %r\n' % (labels_to_aesurfs, model.aesurf)) raise RuntimeError(msg) for unused_label, aesurf in sorted(model.aesurf.items()): if aesurf.type == 'AESURFZ': aero_element_ids = aesurf.aero_element_ids ncaeros_cs += len(aero_element_ids) cs_name = '%s_control_surface' % aesurf.label caero_control_surface_names.append(cs_name) # self.gui.create_alternate_vtk_grid( # cs_name, color=PINK_FLOAT, line_width=5, opacity=0.5, # representation='surface') cs_box_ids['caero_control_surfaces'].extend(aero_element_ids) cs_box_ids[cs_name].extend(aero_element_ids) else: aelist_ref = aesurf.aelist_id1_ref if aelist_ref is None: log.error('AESURF does not reference an AELIST\n%s' % ( aesurf.rstrip())) continue ncaeros_cs += len(aelist_ref.elements) cs_name = '%s_control_surface' % aesurf.label # self.gui.create_alternate_vtk_grid( # cs_name, color=PINK_FLOAT, line_width=5, opacity=0.5, # representation='surface') caero_control_surface_names.append(cs_name) cs_box_ids['caero_control_surfaces'].extend(aelist_ref.elements) cs_box_ids[cs_name].extend(aelist_ref.elements) if aesurf.aelist_id2 is not None: aelist_ref = aesurf.aelist_id2_ref ncaeros_cs += len(aelist_ref.elements) cs_box_ids[cs_name].extend(aelist_ref.elements) cs_box_ids['caero_control_surfaces'].extend(aelist_ref.elements) out = AeroPaneling( has_caero, caero_points, ncaeros, ncaeros_sub, ncaeros_cs, ncaeros_points, ncaero_sub_points, has_control_surface, box_id_to_caero_element_map, cs_box_ids, ) #print(all_control_surface_name, caero_control_surface_names) return all_control_surface_name, caero_control_surface_names, out
CAEROs = CAERO1 | CAERO2 | CAERO3 | CAERO4 | CAERO5 PAEROs = PAERO1 | PAERO2 | PAERO3 | PAERO4 | PAERO5 SPLINEs = SPLINE1 | SPLINE2 | SPLINE3 | SPLINE4 | SPLINE5