Source code for pyNastran.bdf.cards.elements.solid

# pylint: disable=R0902,R0904,R0914
"""
All solid elements are defined in this file.  This includes:

 * CHEXA8
 * CHEXA20
 * CPENTA6
 * CPENTA15
 * CTETRA4
 * CTETRA10
 * CIHEX1
 * CIHEX2
 * CHEXA1
 * CHEXA2

 * CHEXCZ
 * CPENTCZ

All solid elements are SolidElement and Element objects.

"""
from __future__ import annotations
from typing import Union, Any, TYPE_CHECKING
import numpy as np
from numpy import dot, cross
from numpy.linalg import norm  # type: ignore

from pyNastran.bdf.cards.elements.elements import Element
from pyNastran.utils.mathematics import Area
from pyNastran.bdf.bdf_interface.assign_type import integer, integer_or_blank
if TYPE_CHECKING:  # pragma: no cover
    from pyNastran.bdf.bdf import BDF


HEXA_FACE_MAPPER = {
    (7, 5) : (7, 6, 5, 4),
    (5, 7) : (7, 6, 5, 4),
    (6, 4) : (7, 6, 5, 4),
    (4, 6) : (7, 6, 5, 4),

    (0, 2) : (0, 1, 2, 3),
    (2, 0) : (0, 1, 2, 3),
    (1, 3) : (0, 1, 2, 3),
    (3, 1) : (0, 1, 2, 3),

    (0, 7) : (0, 3, 7, 4),
    (7, 0) : (0, 3, 7, 4),
    (3, 4) : (0, 3, 7, 4),
    (4, 3) : (0, 3, 7, 4),

    (5, 2) : (5, 6, 2, 1),
    (2, 5) : (5, 6, 2, 1),
    (6, 1) : (5, 6, 2, 1),
    (1, 6) : (5, 6, 2, 1),

    (4, 1) : (4, 5, 1, 0),
    (1, 4) : (4, 5, 1, 0),
    (5, 0) : (4, 5, 1, 0),
    (0, 5) : (4, 5, 1, 0),

    (2, 7) : (2, 6, 7, 3),
    (7, 2) : (2, 6, 7, 3),
    (6, 3) : (2, 6, 7, 3),
    (3, 6) : (2, 6, 7, 3),
}
_chexa_faces = (
    (7, 6, 5, 4),
    (0, 1, 2, 3),
    (0, 3, 7, 4),
    (5, 6, 2, 1),
    (4, 5, 1, 0),
    (2, 6, 7, 3),
)

[docs] def volume4(n1: Any, n2: Any, n3: Any, n4: Any) -> float: r""" Gets the volume, :math:`V`, of the tetrahedron. .. math:: V = \frac{(a-d) \cdot \left( (b-d) \times (c-d) \right) }{6} """ volume = -dot(n1 - n4, cross(n2 - n4, n3 - n4)) / 6. return volume
[docs] def area_centroid(n1: Any, n2: Any, n3: Any, n4: Any) -> tuple[float, float]: """ Gets the area, :math:`A`, and centroid of a quad.:: 1-----2 | / | | / | 4-----3 """ area = 0.5 * norm(cross(n3 - n1, n4 - n2)) centroid = (n1 + n2 + n3 + n4) / 4. return area, centroid
nnodes_map = { 'CTETRA' : (4, 10), 'CPENTA' : (6, 15), 'CPYRAM' : (5, 13), 'CHEXA' : (8, 20), }
[docs] class SolidElement(Element): _field_map = {1: 'nid', 2:'pid'} _properties = ['faces'] def __init__(self): Element.__init__(self) self.nodes_ref = None self.pid_ref = None
[docs] @classmethod def export_to_hdf5(cls, h5_file, model, eids): """exports the elements in a vectorized way""" nnodes = nnodes_map[cls.type] comments = [] element0 = model.elements[eids[0]] nnodes0 = len(element0.nodes) nnodes_high_map = { 4 : 10, 10 : 10, # CTETRA 5 : 13, 13 : 13, # CYRAM 6 : 15, 15 : 15, # CPENTA 8 : 20, 20 : 20, # CHEXA } nnodes_low_map = { 4 : 4, 10 : 4, # CTETRA 5 : 5, 13 : 5, # CYRAM 6 : 6, 15 : 6, # CPENTA 8 : 8, 20 : 8, # CHEXA } neids = len(eids) nnodes = nnodes_high_map[nnodes0] nnodes_low = nnodes_low_map[nnodes0] shape = (neids, nnodes) try: pids, nodes = _get_nodes_array(model, shape, eids, dtype='int32') except OverflowError: pids, nodes = _get_nodes_array(model, shape, eids, dtype='int64') #h5_file.create_dataset('_comment', data=comments) h5_file.create_dataset('eid', data=eids) h5_file.create_dataset('pid', data=pids) if nodes[:, nnodes_low:].max() == 0: nodes = nodes[:, :nnodes_low] h5_file.create_dataset('nodes', data=nodes)
def _update_field_helper(self, n, value): if n - 3 < len(self.nodes): self.nodes[n - 3] = value else: raise KeyError('Field %r=%r is an invalid %s entry.' % (n, value, self.type))
[docs] def cross_reference(self, model: BDF) -> None: raise NotImplementedError('Element type=%r must implement cross_reference')
[docs] def uncross_reference(self) -> None: """Removes cross-reference links""" self.nodes = self.node_ids self.pid = self.Pid() self.nodes_ref = None self.pid_ref = None
[docs] def E(self) -> float: return self.pid_ref.mid_ref.E()
[docs] def G(self) -> float: return self.pid_ref.mid_ref.G()
[docs] def Nu(self) -> float: return self.pid_ref.mid_ref.Nu()
[docs] def Volume(self) -> float: """ Base volume method that should be overwritten """ return 0.
[docs] def Mass(self) -> float: """ Calculates the mass of the solid element Mass = Rho * Volume """ rho = self.Rho() if rho == 0.0: return 0.0 mass = rho * self.Volume() #if mass == 0.0: #print(self.pid_ref.mid_ref) #print(self.pid_ref.mid_ref.get_stats()) #print(' rho=%e volume=%e' % (self.Rho(), self.Volume())) return mass
[docs] def Mid(self) -> int: """ Returns the material ID as an integer """ return self.pid_ref.Mid()
[docs] def Rho(self) -> float: """ Returns the density """ try: return self.pid_ref.Rho() except AttributeError: print("self.pid = %s" % (self.pid)) #print("self.pid_ref.mid_ref = %s" % (str(self.pid_ref.mid_ref))) raise
[docs] def get_face_area_centroid_normal(self, nid_opposite, nid=None): return self.get_face_area_centroid_normal(nid_opposite, nid)
[docs] def raw_fields(self): list_fields = [self.type, self.eid, self.Pid()] + self.node_ids return list_fields
[docs] def center_of_mass(self): return self.Centroid()
[docs] def _get_nodes_array(model: BDF, shape, eids, dtype='int32'): pids = [] nodes = np.zeros(shape, dtype=dtype) for i, eid in enumerate(eids): element = model.elements[eid] #comments.append(element.comment) pids.append(element.pid) nodes[i, :len(element.nodes)] = [nid if nid is not None else 0 for nid in element.nodes] return pids, nodes
[docs] class CHEXA8(SolidElement): """ +-------+-----+-----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=======+=====+=====+====+====+====+====+====+====+ | CHEXA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +-------+-----+-----+----+----+----+----+----+----+ | | G7 | G8 | | | | | | | +-------+-----+-----+----+----+----+----+----+----+ """ type = 'CHEXA'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: data = [self.eid, self.Pid()] + self.node_ids msg = ('CHEXA %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d\n' % tuple(data)) return self.comment + msg
[docs] def write_card_16(self, is_double=False): data = [self.eid, self.Pid()] + self.node_ids msg = ('CHEXA* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d\n' % tuple(data)) return self.comment + msg
def __init__(self, eid, pid, nids, comment=''): """ Creates a CHEXA8 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=8 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid self.nodes = self.prepare_node_ids(nids) assert len(self.nodes) == 8
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CHEXA8 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') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), integer(card, 9, 'nid7'), integer(card, 10, 'nid8') ] assert len(card) == 11, f'len(CHEXA8 card) = {len(card):d}\ncard={card}' return CHEXA8(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CHEXA8 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = data[2:] assert len(data) == 10, 'len(data)=%s data=%s' % (len(data), data) return CHEXA8(eid, pid, nids, 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 CHEXA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CHEXA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[2], nodes[3]], 2 : [nodes[0], nodes[1], nodes[5], nodes[4]], 3 : [nodes[1], nodes[2], nodes[6], nodes[5]], 4 : [nodes[2], nodes[3], nodes[7], nodes[6]], 5 : [nodes[3], nodes[0], nodes[4], nodes[7]], 6 : [nodes[4], nodes[5], nodes[6], nodes[7]], } return faces
[docs] def material_coordinate_system(self, xyz=None): """http://www.ipes.dk/Files/Ipes/Filer/nastran_2016_doc_release.pdf""" #if normal is None: #normal = self.Normal() # k = kmat if xyz is None: x1 = self.nodes_ref[0].get_position() x2 = self.nodes_ref[1].get_position() x3 = self.nodes_ref[2].get_position() x4 = self.nodes_ref[3].get_position() x5 = self.nodes_ref[4].get_position() x6 = self.nodes_ref[5].get_position() x7 = self.nodes_ref[6].get_position() x8 = self.nodes_ref[7].get_position() else: x1 = xyz[:, 0] x2 = xyz[:, 1] x3 = xyz[:, 2] x4 = xyz[:, 3] x5 = xyz[:, 4] x6 = xyz[:, 5] x7 = xyz[:, 6] x8 = xyz[:, 7] #CORDM=-2 centroid = (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8) / 8. xe = ((x2+x3+x6+x7) - (x1+x4+x8+x5)) / 4. xe /= np.linalg.norm(xe) v = ((x3+x7+x8+x4) - (x1+x2+x6+x5)) / 4 ze = np.cross(xe, v) ze /= np.linalg.norm(ze) ye = np.cross(ze, xe) ye /= np.linalg.norm(ye) return centroid, xe, ye, ze
def _verify(self, xref: bool): _verify_solid_elem_linear(self, xref)
[docs] def Centroid(self): """ Averages the centroids at the two faces """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions() c1 = area_centroid(n1, n2, n3, n4)[1] c2 = area_centroid(n5, n6, n7, n8)[1] centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """Calculate the volume of the hex""" # https://www.osti.gov/servlets/purl/632793/ #volume = ( #det3(x7 - x0, x1 - x0, x3 - x5) + #det3(x7 - x0, x4 - x0, x5 - x6) + #det3(x7 - x0, x2 - x0, x6 - x3) #) / 6. # swap points # x2 / x3 # x6 / x7 #def det3(a, b, c): #return np.det(np.vstack(a, b, c)) #volume = ( #det3(x6 - x0, x1 - x0, x2 - x5) + #det3(x6 - x0, x4 - x0, x5 - x7) + #det3(x6 - x0, x3 - x0, x7 - x2) #) / 6. # add 1 #volume = ( #det3(x7 - x1, x2 - x1, x3 - x6) + #det3(x7 - x1, x5 - x1, x6 - x8) + #det3(x7 - x1, x4 - x1, x8 - x3) #) / 6. (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions() (area1, c1) = area_centroid(n1, n2, n3, n4) (area2, c2) = area_centroid(n5, n6, n7, n8) volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=False) return nids
[docs] def get_face(self, nid_opposite, nid): nids = self.node_ids[:8] return chexa_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G3 - the grid point diagonally opposite of G1 """ nids = self.node_ids[:8] return chexa_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:8])
[docs] def get_edge_ids(self): """ Return the edge IDs # top (5-6-7-8) # btm (1-2-3-4) # left (1-2-3-4) # right (5-6-7-8) # front (1-5-8-4) # back (2-6-7-3) """ node_ids = self.node_ids return [ # btm (1-2-3-4) tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # top (5-6-7-8) tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[6]])), tuple(sorted([node_ids[6], node_ids[7]])), tuple(sorted([node_ids[7], node_ids[4]])), # up - (4-8, 3-7, 1-5, 2-6) tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[5]])), tuple(sorted([node_ids[2], node_ids[6]])), tuple(sorted([node_ids[3], node_ids[7]])), ]
[docs] def flip_normal(self): ## TODO verify """flips the element inside out""" # reverse the lower and upper quad faces n1, n2, n3, n4, n5, n6, n7, n8 = self.nodes self.nodes = [n1, n4, n3, n2, n5, n8, n7, n6,] if self.nodes_ref is not None: n1_ref, n2_ref, n3_ref, n4_ref, n5_ref, n6_ref, n7_ref, n8_ref = self.nodes_ref self.nodes_ref = [ n1_ref, n4_ref, n3_ref, n2_ref, n5_ref, n8_ref, n7_ref, n6_ref,]
[docs] class CHEXA20(SolidElement): """ +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=======+=====+=====+=====+=====+=====+=====+=====+=====+ | CHEXA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G15 | G16 | G17 | G18 | G19 | G20 | | | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CHEXA'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXA %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d%8s%8s%8s%8s%8s%8s\n' ' %8s%8s%8s%8s%8s%8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXA* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s%16s%16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
def __init__(self, eid, pid, nids, comment=''): """ Creates a CHEXA20 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=20 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid nnodes = len(nids) if nnodes < 20: nids.extend((20 - nnodes) * [None]) self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True) msg = 'len(nids)=%s nids=%s' % (len(nids), nids) assert len(self.nodes) == 20, msg
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CHEXA20 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') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), integer(card, 9, 'nid7'), integer(card, 10, 'nid8'), integer_or_blank(card, 11, 'nid9'), integer_or_blank(card, 12, 'nid10'), integer_or_blank(card, 13, 'nid11'), integer_or_blank(card, 14, 'nid12'), integer_or_blank(card, 15, 'nid13'), integer_or_blank(card, 16, 'nid14'), integer_or_blank(card, 17, 'nid15'), integer_or_blank(card, 18, 'nid16'), integer_or_blank(card, 19, 'nid17'), integer_or_blank(card, 20, 'nid18'), integer_or_blank(card, 21, 'nid19'), integer_or_blank(card, 22, 'nid20'), ] assert len(card) <= 23, f'len(CHEXA20 card) = {len(card):d}\ncard={card}' return CHEXA20(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CHEXA20 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = [d if d > 0 else None for d in data[2:]] return CHEXA20(eid, pid, nids, 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 CHEXA eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CHEXA eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ (n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20) = self.node_ids faces = { 1 : [n1, n2, n3, n4, n9, n10, n11, n12], 2 : [n1, n2, n6, n5, n9, n18, n13, n17], 3 : [n2, n3, n7, n6, n10, n19, n14, n18], 4 : [n3, n4, n8, n7, n11, n10, n15, n19], 5 : [n4, n1, n5, n8, n12, n17, n16, n20], 6 : [n5, n6, n7, n8, n13, n14, n15, n16], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # top tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[6]])), tuple(sorted([node_ids[6], node_ids[7]])), tuple(sorted([node_ids[7], node_ids[4]])), # sides tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[5]])), tuple(sorted([node_ids[2], node_ids[6]])), tuple(sorted([node_ids[3], node_ids[7]])), ]
[docs] def get_face(self, nid_opposite, nid): nids = self.node_ids[:8] return chexa_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G3 - the grid point diagonally opposite of G1 """ nids = self.node_ids[:8] return chexa_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:8])
def _verify(self, xref: bool) -> None: _verify_solid_elem_quadratic(self, xref, 8)
[docs] def Centroid(self): """ .. seealso:: CHEXA8.Centroid """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions()[:8] c1 = area_centroid(n1, n2, n3, n4)[1] c2 = area_centroid(n5, n6, n7, n8)[1] centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """ .. seealso:: CHEXA8.Volume """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions()[:8] (area1, c1) = area_centroid(n1, n2, n3, n4) (area2, c2) = area_centroid(n5, n6, n7, n8) volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=True) return nids
[docs] class CHEXCZ(CHEXA20): """ +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=======+=====+=====+=====+=====+=====+=====+=====+=====+ | CHEXA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G15 | G16 | G17 | G18 | G19 | G20 | | | +-------+-----+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CHEXCZ'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXCZ %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d%8s%8s%8s%8s%8s%8s\n' ' %8s%8s%8s%8s%8s%8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXCZ* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s%16s%16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] class CIHEX1(CHEXA8): type = 'CIHEX1'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: data = [self.eid, self.Pid()] + self.node_ids msg = ('CIHEX1 %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d\n' % tuple(data)) return self.comment + msg
[docs] def write_card_16(self, is_double=False): data = [self.eid, self.Pid()] + self.node_ids msg = ('CIHEX1* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d\n' % tuple(data)) return self.comment + msg
def __init__(self, eid, pid, nids, comment=''): CHEXA8.__init__(self, eid, pid, nids, comment=comment)
[docs] class CIHEX2(CHEXA20): type = 'CIHEX2'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CIHEX2 %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d%8s%8s%8s%8s%8s%8s\n' ' %8s%8s%8s%8s%8s%8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CIHEX2* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s%16s%16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
def __init__(self, eid, pid, nids, comment=''): CHEXA20.__init__(self, eid, pid, nids, comment=comment)
[docs] class CHEXA1(SolidElement): """ +-------+-----+-----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=======+=====+=====+====+====+====+====+====+====+ | CHEXA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +-------+-----+-----+----+----+----+----+----+----+ | | G7 | G8 | | | | | | | +-------+-----+-----+----+----+----+----+----+----+ """ type = 'CHEXA1'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: data = [self.eid, self.Mid()] + self.node_ids msg = ('CHEXA1 %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d\n' % tuple(data)) return self.comment + msg
[docs] def write_card_16(self, is_double=False): data = [self.eid, self.Pid()] + self.node_ids msg = ('CHEXA1* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d\n' % tuple(data)) return self.comment + msg
def __init__(self, eid: int, mid: int, nids: list[int], comment=''): """ Creates a CHEXA1 Parameters ---------- eid : int element id mid : int property id (MAT1) nids : list[int] node ids; n=8 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Material ID self.mid = mid self.nodes = self.prepare_node_ids(nids) assert len(self.nodes) == 8
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CHEXA1 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') mid = integer(card, 2, 'mid') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), integer(card, 9, 'nid7'), integer(card, 10, 'nid8') ] assert len(card) == 11, f'len(CHEXA1 card) = {len(card):d}\ncard={card}' return CHEXA1(eid, mid, nids, comment=comment)
#@classmethod #def add_op2_data(cls, data, comment=''): #""" #Adds a CHEXA8 card from the OP2 #Parameters #---------- #data : list[varies] #a list of fields defined in OP2 format #comment : str; default='' #a comment for the card #""" #eid = data[0] #mid = data[1] #nids = data[2:] #assert len(data) == 10, 'len(data)=%s data=%s' % (len(data), data) #return CHEXA1(eid, mid, nids, 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 CHEXA1 eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.mid_ref = model.Material(self.mid, msg=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 = ', which is required by CHEXA1 eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.mid_ref = model.safe_material(self.mid, self.eid, xref_errors, msg=msg)
[docs] def Mid(self) -> int: if self.mid_ref is None: return self.mid return self.mid_ref.mid
[docs] def Rho(self) -> float: return self.mid_ref.Rho()
[docs] def Nu(self) -> float: return self.mid_ref.Nu()
[docs] def E(self) -> float: return self.mid_ref.E()
[docs] def G(self) -> float: return self.mid_ref.G()
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[2], nodes[3]], 2 : [nodes[0], nodes[1], nodes[5], nodes[4]], 3 : [nodes[1], nodes[2], nodes[6], nodes[5]], 4 : [nodes[2], nodes[3], nodes[7], nodes[6]], 5 : [nodes[3], nodes[0], nodes[4], nodes[7]], 6 : [nodes[4], nodes[5], nodes[6], nodes[7]], } return faces
[docs] def material_coordinate_system(self, xyz=None): """http://www.ipes.dk/Files/Ipes/Filer/nastran_2016_doc_release.pdf""" #if normal is None: #normal = self.Normal() # k = kmat if xyz is None: x1 = self.nodes_ref[0].get_position() x2 = self.nodes_ref[1].get_position() x3 = self.nodes_ref[2].get_position() x4 = self.nodes_ref[3].get_position() x5 = self.nodes_ref[4].get_position() x6 = self.nodes_ref[5].get_position() x7 = self.nodes_ref[6].get_position() x8 = self.nodes_ref[7].get_position() else: x1 = xyz[:, 0] x2 = xyz[:, 1] x3 = xyz[:, 2] x4 = xyz[:, 3] x5 = xyz[:, 4] x6 = xyz[:, 5] x7 = xyz[:, 6] x8 = xyz[:, 7] #CORDM=-2 centroid = (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8) / 8. xe = ((x2+x3+x6+x7) - (x1+x4+x8+x5)) / 4. xe /= np.linalg.norm(xe) v = ((x3+x7+x8+x4) - (x1+x2+x6+x5)) / 4 ze = np.cross(xe, v) ze /= np.linalg.norm(ze) ye = np.cross(ze, xe) ye /= np.linalg.norm(ye) return centroid, xe, ye, ze
def _verify(self, xref): eid = self.eid mid = self.Mid() assert isinstance(eid, int) assert isinstance(mid, int) for i, nid in enumerate(self.node_ids): assert isinstance(nid, int), 'nid%i is not an integer; nid=%s' %(i, nid) if xref: centroid = self.Centroid() volume = self.Volume() assert isinstance(volume, float) and volume > 0, f'Volume={volume} must be >0;\n{str(self)}' for i in range(3): assert isinstance(centroid[i], float)
[docs] def Centroid(self): """ Averages the centroids at the two faces """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions() c1 = area_centroid(n1, n2, n3, n4)[1] c2 = area_centroid(n5, n6, n7, n8)[1] centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """Calculate the volume of the hex""" # https://www.osti.gov/servlets/purl/632793/ #volume = ( #det3(x7 - x0, x1 - x0, x3 - x5) + #det3(x7 - x0, x4 - x0, x5 - x6) + #det3(x7 - x0, x2 - x0, x6 - x3) #) / 6. # swap points # x2 -> x3 # x3 -> x2 # # x6 -> x7 # x7 -> x6 #def det3(a, b, c): #return np.det(np.vstack(a, b, c)) #volume = ( #det3(x6 - x0, x1 - x0, x2 - x5) + #det3(x6 - x0, x4 - x0, x5 - x7) + #det3(x6 - x0, x3 - x0, x7 - x3) #) / 6. # add 1 #volume = ( #det3(x7 - x1, x2 - x1, x3 - x6) + #det3(x7 - x1, x5 - x1, x6 - x8) + #det3(x7 - x1, x4 - x1, x8 - x4) #) / 6. (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions() (area1, c1) = area_centroid(n1, n2, n3, n4) (area2, c2) = area_centroid(n5, n6, n7, n8) volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=False) return nids
[docs] def get_face(self, nid_opposite, nid): nids = self.node_ids[:8] return chexa_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G3 - the grid point diagonally opposite of G1 """ nids = self.node_ids[:8] return chexa_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:8])
[docs] def get_edge_ids(self): """ Return the edge IDs # top (5-6-7-8) # btm (1-2-3-4) # left (1-2-3-4) # right (5-6-7-8) # front (1-5-8-4) # back (2-6-7-3) """ node_ids = self.node_ids return [ # btm (1-2-3-4) tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # top (5-6-7-8) tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[6]])), tuple(sorted([node_ids[6], node_ids[7]])), tuple(sorted([node_ids[7], node_ids[4]])), # up - (4-8, 3-7, 1-5, 2-6) tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[5]])), tuple(sorted([node_ids[2], node_ids[6]])), tuple(sorted([node_ids[3], node_ids[7]])), ]
[docs] def flip_normal(self): ## TODO verify """flips the element inside out""" # reverse the lower and upper quad faces n1, n2, n3, n4, n5, n6, n7, n8 = self.nodes self.nodes = [n1, n4, n3, n2, n5, n8, n7, n6,] if self.nodes_ref is not None: n1_ref, n2_ref, n3_ref, n4_ref, n5_ref, n6_ref, n7_ref, n8_ref = self.nodes_ref self.nodes_ref = [ n1_ref, n4_ref, n3_ref, n2_ref, n5_ref, n8_ref, n7_ref, n6_ref,]
[docs] class CHEXA2(SolidElement): """ +--------+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+=====+=====+=====+=====+=====+=====+ | CHEXA2 | EID | MID | G1 | G2 | G3 | G4 | G5 | G6 | +--------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | +--------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G15 | G16 | G17 | G18 | G19 | G20 | | | +--------+-----+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CHEXA2'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXA2 %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8d%8d%8s%8s%8s%8s%8s%8s\n' ' %8s%8s%8s%8s%8s%8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[8:]] data = [self.eid, self.Pid()] + nodes[:8] + nodes2 msg = ('CHEXA2* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' '* %16d%16d%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s%16s%16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
def __init__(self, eid, mid, nids, comment=''): """ Creates a CHEXA2 Parameters ---------- eid : int element id mid : int material id (MAT1) nids : list[int] node ids; n=20 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Material ID self.mid = mid nnodes = len(nids) if nnodes < 20: nids.extend((20 - nnodes) * [None]) self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True) msg = 'len(nids)=%s nids=%s' % (len(nids), nids) assert len(self.nodes) == 20, msg
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CHEXA2 card from ``BDF.add_card(...)`` Parameters ---------- card : BDFCard() a BDFCard object comment : str; default='' a comment for the card """ eid = integer(card, 1, 'eid') mid = integer(card, 2, 'mid') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), integer(card, 9, 'nid7'), integer(card, 10, 'nid8'), integer_or_blank(card, 11, 'nid9'), integer_or_blank(card, 12, 'nid10'), integer_or_blank(card, 13, 'nid11'), integer_or_blank(card, 14, 'nid12'), integer_or_blank(card, 15, 'nid13'), integer_or_blank(card, 16, 'nid14'), integer_or_blank(card, 17, 'nid15'), integer_or_blank(card, 18, 'nid16'), integer_or_blank(card, 19, 'nid17'), integer_or_blank(card, 20, 'nid18'), integer_or_blank(card, 21, 'nid19'), integer_or_blank(card, 22, 'nid20'), ] assert len(card) <= 23, f'len(CHEXA2 card) = {len(card):d}\ncard={card}' return CHEXA2(eid, mid, nids, comment=comment)
#@classmethod #def add_op2_data(cls, data, comment=''): #""" #Adds a CHEXA20 card from the OP2 #Parameters #---------- #data : list[varies] #a list of fields defined in OP2 format #comment : str; default='' #a comment for the card #""" #eid = data[0] #pid = data[1] #nids = [d if d > 0 else None for d in data[2:]] #return CHEXA20(eid, pid, nids, 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 CHEXA2 eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.mid_ref = model.Material(self.mid, msg=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 = ', which is required by CHEXA2 eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.mid_ref = model.safe_material(self.mid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ (n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20) = self.node_ids faces = { 1 : [n1, n2, n3, n4, n9, n10, n11, n12], 2 : [n1, n2, n6, n5, n9, n18, n13, n17], 3 : [n2, n3, n7, n6, n10, n19, n14, n18], 4 : [n3, n4, n8, n7, n11, n10, n15, n19], 5 : [n4, n1, n5, n8, n12, n17, n16, n20], 6 : [n5, n6, n7, n8, n13, n14, n15, n16], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # top tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[6]])), tuple(sorted([node_ids[6], node_ids[7]])), tuple(sorted([node_ids[7], node_ids[4]])), # sides tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[5]])), tuple(sorted([node_ids[2], node_ids[6]])), tuple(sorted([node_ids[3], node_ids[7]])), ]
[docs] def get_face(self, nid_opposite, nid): nids = self.node_ids[:8] return chexa_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G3 - the grid point diagonally opposite of G1 """ nids = self.node_ids[:8] return chexa_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:8])
def _verify(self, xref): eid = self.eid mid = self.Mid() unused_edges = self.get_edge_ids() assert isinstance(eid, int) assert isinstance(mid, int) for i, nid in enumerate(self.node_ids): assert nid is None or isinstance(nid, int), 'nid%i is not an integer/blank; nid=%s' %(i, nid) if xref: centroid = self.Centroid() volume = self.Volume() assert isinstance(volume, float) and volume > 0, f'Volume={volume} must be >0;\n{str(self)}' for i in range(3): assert isinstance(centroid[i], float)
[docs] def Centroid(self): """ .. seealso:: CHEXA8.Centroid """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions()[:8] c1 = area_centroid(n1, n2, n3, n4)[1] c2 = area_centroid(n5, n6, n7, n8)[1] centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """ .. seealso:: CHEXA8.Volume """ (n1, n2, n3, n4, n5, n6, n7, n8) = self.get_node_positions()[:8] (area1, c1) = area_centroid(n1, n2, n3, n4) (area2, c2) = area_centroid(n5, n6, n7, n8) volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=True) return nids
[docs] class CPENTA6(SolidElement): r""" +--------+-----+-----+----+----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+====+====+====+====+====+====+ | CPENTA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +--------+-----+-----+----+----+----+----+----+----+ :: 3 6 *----------* / \ / \ / A \ / c \ *---*-----*-----* 1 2 4 5 V = (A1+A2)/2 * norm(c1-c2) C = (c1-c2)/2 """ type = 'CPENTA'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = 'CPENTA %8d%8d%8d%8d%8d%8d%8d%8d\n' % tuple(data) return self.comment + msg
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = ('CPENTA* %16d%16d%16d%16d\n' '* %16d%16d%16d%16d\n' % tuple(data)) return self.comment + msg
def __init__(self, eid, pid, nids, comment=''): """ Creates a CPENTA6 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=6 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid self.nodes = self.prepare_node_ids(nids) assert len(self.nodes) == 6
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CPENTA6 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') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), ] assert len(card) == 9, f'len(CPENTA6 card) = {len(card):d}\ncard={card}' return CPENTA6(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CPENTA6 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = data[2:] assert len(data) == 8, 'len(data)=%s data=%s' % (len(data), data) return CPENTA6(eid, pid, nids, 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 CPENTA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CPENTA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
[docs] def material_coordinate_system(self, xyz=None): """http://www.ipes.dk/Files/Ipes/Filer/nastran_2016_doc_release.pdf""" #if normal is None: #normal = self.Normal() # k = kmat if xyz is None: x1 = self.nodes_ref[0].get_position() x2 = self.nodes_ref[1].get_position() x3 = self.nodes_ref[2].get_position() x4 = self.nodes_ref[3].get_position() x5 = self.nodes_ref[4].get_position() x6 = self.nodes_ref[5].get_position() else: x1 = xyz[:, 0] x2 = xyz[:, 1] x3 = xyz[:, 2] x4 = xyz[:, 3] x5 = xyz[:, 4] x6 = xyz[:, 5] #CORDM=-2 centroid = self.Centroid() origin = (x1 + x4) / 2. xe = (x2 + x3 + x5 + x6) - origin xe /= np.linalg.norm(xe) v = ((x1 + x3 + x4 + x6) - (x1 + x2 + x4 + x5)) / 4. ze = np.cross(xe, v) ze /= np.linalg.norm(ze) ye = np.cross(ze, xe) ye /= np.linalg.norm(ye) return centroid, xe, ye, ze
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[2]], 2 : [nodes[3], nodes[4], nodes[5]], 3 : [nodes[0], nodes[1], nodes[4], nodes[3]], 4 : [nodes[1], nodes[2], nodes[5], nodes[4]], 5 : [nodes[2], nodes[0], nodes[3], nodes[5]], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[0]])), # top tuple(sorted([node_ids[3], node_ids[4]])), tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[3]])), # sides tuple(sorted([node_ids[0], node_ids[3]])), tuple(sorted([node_ids[1], node_ids[4]])), tuple(sorted([node_ids[2], node_ids[5]])), ]
[docs] def get_face(self, nid, nid_opposite=None): nids = self.node_ids[:6] return cpenta_face(nid, nid_opposite, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite=None): nids = self.node_ids[:6] return cpenta_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:6])
[docs] def get_face_nodes_and_area(self, nid, nid_opposite): nids = self.node_ids[:6] indx1 = nids.index(nid) indx2 = nids.index(nid_opposite) # offset so it's easier to map the nodes with the QRG pack = [indx1 + 1, indx2 + 1] pack.sort() mapper = { # reverse points away from the element [1, 2]: [1, 2, 3], # close [2, 3]: [1, 2, 3], [1, 3]: [1, 2, 3], [4, 5]: [4, 5, 6], # far-reverse [5, 6]: [4, 5, 6], [4, 6]: [4, 5, 6], [1, 5]: [1, 2, 5, 4], # bottom [2, 4]: [1, 2, 5, 4], [1, 6]: [1, 3, 6, 4], # left-reverse [3, 4]: [1, 3, 6, 4], [2, 6]: [2, 5, 6, 3], # right [3, 5]: [2, 5, 6, 3], } pack2 = mapper[pack] if len(pack2) == 3: (n1, n2, n3) = pack2 face_node_ids = [n1, n2, n3] n1i = nids.index(n1 - 1) n2i = nids.index(n2 - 1) n3i = nids.index(n3 - 1) p1 = self.nodes_ref[n1i].get_position() p2 = self.nodes_ref[n2i].get_position() p3 = self.nodes_ref[n3i].get_position() area = 0.5 * norm(cross(p1 - p2, p1 - p3)) else: (n1, n2, n3, n4) = pack2 n1i = nids.index(n1 - 1) n2i = nids.index(n2 - 1) n3i = nids.index(n3 - 1) n4i = nids.index(n4 - 1) face_node_ids = [n1, n2, n3, n4] p1 = self.nodes_ref[n1i].get_position() p2 = self.nodes_ref[n2i].get_position() p3 = self.nodes_ref[n3i].get_position() p4 = self.nodes_ref[n4i].get_position() area = 0.5 * norm(cross(p1 - p3, p2 - p4)) return [face_node_ids, area]
def _verify(self, xref): _verify_solid_elem_linear(self, xref)
[docs] def Centroid(self): (n1, n2, n3, n4, n5, n6) = self.get_node_positions() c1 = (n1 + n2 + n3) / 3. c2 = (n4 + n5 + n6) / 3. centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """Calculate the volume of the penta""" (n1, n2, n3, n4, n5, n6) = self.get_node_positions() area1 = 0.5 * norm(cross(n3 - n1, n2 - n1)) area2 = 0.5 * norm(cross(n6 - n4, n5 - n4)) c1 = (n1 + n2 + n3) / 3. c2 = (n4 + n5 + n6) / 3. volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
#return volume4(n1, n2, n3, n4) + volume4(n2, n3, n4, n5) + volume4(n2, n4, n5, n6)
[docs] def raw_fields(self): list_fields = ['CPENTA', self.eid, self.Pid()] + self.node_ids return list_fields
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=False) return nids
[docs] def cpenta_face(nid, nid_opposite, nids): assert len(nids) == 6, nids indx1 = nids.index(nid) if nid_opposite is None: if indx1 in [0, 1, 2]: pack2 = tuple([2, 1, 0]) elif indx1 in [3, 4, 5]: pack2 = tuple([3, 4, 5]) else: raise RuntimeError(indx1) assert len(pack2) == 3, pack2 else: indx2 = nids.index(nid_opposite) # offset so it's easier to map the nodes with the QRG pack = tuple(sorted([indx1 + 1, indx2 + 1])) _cpenta_mapper = { # reverse points away from the element #(1, 2) : [1, 2, 3], # close #(2, 3) : [1, 2, 3], #(1, 3) : [1, 2, 3], #(4, 5) : [4, 5, 6], # far-reverse #(5, 6) : [4, 5, 6], #(4, 6) : [4, 5, 6], (1, 5) : [4, 5, 2, 1], # bottom (2, 4) : [4, 5, 2, 1], (1, 6) : [1, 3, 6, 4], # left-reverse (3, 4) : [1, 3, 6, 4], (2, 6) : [2, 5, 6, 3], # right (3, 5) : [2, 5, 6, 3], } try: pack2 = _cpenta_mapper[pack] except KeyError: print('PLOAD4; remove a node') raise pack2 = [i - 1 for i in pack2] return pack2
[docs] def cpenta_face_area_centroid_normal(nid: int, nid_opposite: int, nids: list[int], nodes_ref): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int / None G3 - the grid point diagonally opposite of G1 """ face = cpenta_face(nid, nid_opposite, nids) if nid_opposite is None: n1i, n2i, n3i = face p1 = nodes_ref[n1i].get_position() p2 = nodes_ref[n2i].get_position() p3 = nodes_ref[n3i].get_position() a = p3 - p1 b = p2 - p1 centroid = (p1 + p2 + p3) / 3. else: # uses a backwards face? n1i, n2i, n3i, n4i = face p1 = nodes_ref[n1i].get_position() p2 = nodes_ref[n2i].get_position() p3 = nodes_ref[n3i].get_position() p4 = nodes_ref[n4i].get_position() a = p1 - p3 b = p2 - p4 centroid = (p1 + p2 + p3 + p4) / 4. normal = cross(a, b) n = norm(normal) area = 0.5 * n return face, area, centroid, normal / n
[docs] def chexa_face(nid_opposite, nid, nids): assert len(nids) == 8, nids g1i = nids.index(nid_opposite) g3i = nids.index(nid) for face in _chexa_faces: if g1i in face and g3i in face: found_face = face found_face = HEXA_FACE_MAPPER[tuple([g1i, g3i])] return found_face
[docs] def chexa_face_area_centroid_normal(nid, nid_opposite, nids, nodes_ref): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G3 - the grid point diagonally opposite of G1 nodes_ref : list[GRID] the GRID objects # top (7-6-5-4) # btm (0-1-2-3) # left (0-3-7-4) # right (5-6-2-1) # front (4-5-1-0) # back (2-6-7-3) """ face = chexa_face(nid_opposite, nid, nids) nid1, nid2, nid3, nid4 = face n1 = nodes_ref[nid1].get_position() n2 = nodes_ref[nid2].get_position() n3 = nodes_ref[nid3].get_position() n4 = nodes_ref[nid4].get_position() axb = cross(n3 - n1, n4 - n2) areai = norm(axb) centroid = (n1 + n2 + n3 + n4) / 4. area = 0.5 * areai normal = axb / areai return face, area, centroid, normal
[docs] class CPENTA15(SolidElement): """ +---------+-----+-----+----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=====+=====+====+=====+=====+=====+=====+=====+ | CPENTA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +---------+-----+-----+----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | +---------+-----+-----+----+-----+-----+-----+-----+-----+ | | G15 | | | | | | | | +---------+-----+-----+----+-----+-----+-----+-----+-----+ """ type = 'CPENTA' def __init__(self, eid, pid, nids, comment=''): """ Creates a CPENTA15 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=15 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid nnodes = len(nids) if nnodes < 15: nids.extend((15 - nnodes) * [None]) self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True) assert len(self.nodes) == 15
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CPENTA15 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') nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer(card, 8, 'nid6'), integer_or_blank(card, 9, 'nid7'), integer_or_blank(card, 10, 'nid8'), integer_or_blank(card, 11, 'nid9'), integer_or_blank(card, 12, 'nid10'), integer_or_blank(card, 13, 'nid11'), integer_or_blank(card, 14, 'nid12'), integer_or_blank(card, 15, 'nid13'), integer_or_blank(card, 16, 'nid14'), integer_or_blank(card, 17, 'nid15'), ] assert len(card) <= 18, f'len(CPENTA15 card) = {len(card):d}\ncard={card}' return CPENTA15(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CPENTA15 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = [d if d > 0 else None for d in data[2:]] assert len(data) == 17, 'len(data)=%s data=%s' % (len(data), data) return CPENTA15(eid, pid, nids, 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 CPENTA eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CPENTA eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15 = self.node_ids faces = { 1 : [n1, n2, n3, n7, n8, n9], 2 : [n4, n5, n6, n10, n11, n12], 3 : [n1, n2, n5, n4, n7, n14, n10, n13], 4 : [n2, n3, n6, n5, n8, n15, n11, n14], 5 : [n3, n1, n4, n6, n9, n13, n12, n15], } return faces
[docs] def get_face(self, nid, nid_opposite): nids = self.node_ids[:6] return cpenta_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite=None): nids = self.node_ids[:6] return cpenta_face_area_centroid_normal(nid, nid_opposite, nids, self.nodes_ref[:6])
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[0]])), # top tuple(sorted([node_ids[3], node_ids[4]])), tuple(sorted([node_ids[4], node_ids[5]])), tuple(sorted([node_ids[5], node_ids[3]])), # sides tuple(sorted([node_ids[0], node_ids[3]])), tuple(sorted([node_ids[1], node_ids[4]])), tuple(sorted([node_ids[2], node_ids[5]])), ]
def _verify(self, xref: bool) -> None: _verify_solid_elem_quadratic(self, xref, 6)
[docs] def Centroid(self): """ .. seealso:: CPENTA6.Centroid """ (n1, n2, n3, n4, n5, n6) = self.get_node_positions()[:6] c1 = (n1 + n2 + n3) / 3. c2 = (n4 + n5 + n6) / 3. centroid = (c1 + c2) / 2. return centroid
[docs] def Volume(self): """ .. seealso:: CPENTA6.Volume """ (n1, n2, n3, n4, n5, n6) = self.get_node_positions()[:6] area1 = Area(n3 - n1, n2 - n1) area2 = Area(n6 - n4, n5 - n4) c1 = (n1 + n2 + n3) / 3. c2 = (n4 + n5 + n6) / 3. volume = (area1 + area2) / 2. * norm(c1 - c2) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=True) return nids
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[6:]] data = [self.eid, self.Pid()] + nodes[:6] + nodes2 msg = ('CPENTA %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8s%8s%8s%8s%8s%8s%8s%8s\n' ' %8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%16d' % node for node in nodes[6:]] data = [self.eid, self.Pid()] + nodes[:6] + nodes2 msg = ('CPENTA* %16d%16d%16d%16d\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] class CPENTCZ(CPENTA15): """ +---------+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +=========+=====+=====+=====+=====+=====+=====+=====+=====+ | CPENTCZ | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +---------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | +---------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G15 | | | | | | | | +---------+-----+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CPENTCZ'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[6:]] data = [self.eid, self.Pid()] + nodes[:6] + nodes2 msg = ('CPENTCZ %8d%8d%8d%8d%8d%8d%8d%8d\n' ' %8s%8s%8s%8s%8s%8s%8s%8s\n' ' %8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%16d' % node for node in nodes[6:]] data = [self.eid, self.Pid()] + nodes[:6] + nodes2 msg = ('CPENTCZ*%16d%16d%16d%16d\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%16s%16s\n' '* %16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] class CPYRAM5(SolidElement): """ +--------+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +========+=====+=====+=====+=====+=====+=====+=====+ | CPYRAM | EID | PID | G1 | G2 | G3 | G4 | G5 | +--------+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CPYRAM' def __init__(self, eid, pid, nids, comment=''): SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid self.nodes = self.prepare_node_ids(nids) msg = 'len(nids)=%s nids=%s' % (len(nids), nids) assert len(self.nodes) <= 20, msg
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CPYRAM5 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_or_blank(card, 2, 'pid', eid) nids = [integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5')] assert len(card) == 8, f'len(CPYRAM5 1card) = {len(card):d}\ncard={card}' return CPYRAM5(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CPYRAM5 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = [d if d > 0 else None for d in data[2:]] return CPYRAM5(eid, pid, nids, 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 CPYRAM eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CPYRAM eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[2], nodes[3]], 2 : [nodes[0], nodes[1], nodes[4]], 3 : [nodes[1], nodes[2], nodes[4]], 4 : [nodes[2], nodes[3], nodes[4]], 5 : [nodes[3], nodes[0], nodes[4]], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # sides tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[4]])), tuple(sorted([node_ids[2], node_ids[4]])), tuple(sorted([node_ids[3], node_ids[4]])), ]
def _verify(self, xref: bool): _verify_solid_elem_linear(self, xref)
[docs] def Centroid(self): """ .. seealso:: CPYRAM5.Centroid """ (n1, n2, n3, n4, n5) = self.get_node_positions() c1 = area_centroid(n1, n2, n3, n4)[1] centroid = (c1 + n5) / 2. return centroid
[docs] def Volume(self): """ .. seealso:: CPYRAM5.Volume V = (l * w) * h / 3 V = A * h / 3 """ (n1, n2, n3, n4, n5) = self.get_node_positions() area1, c1 = area_centroid(n1, n2, n3, n4) volume = area1 / 3. * norm(c1 - n5) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=False) return nids
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = ('CPYRAM %8d%8d%8d%8d%8d%8d%8d' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = ('CPYRAM* %16d%16d%16d%16d\n' '* %16d%16d%16d' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] class CPYRAM13(SolidElement): """ +--------+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+=====+=====+=====+=====+=====+=====+ | CPYRAM | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +--------+-----+-----+-----+-----+-----+-----+-----+-----+ | | G7 | G8 | G9 | G10 | G11 | G12 | | | +--------+-----+-----+-----+-----+-----+-----+-----+-----+ """ type = 'CPYRAM' def __init__(self, eid, pid, nids, comment=''): SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid nnodes = len(nids) if nnodes < 13: nids.extend((13 - nnodes) * [None]) self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True) msg = 'len(nids)=%s nids=%s' % (len(nids), nids) assert len(self.nodes) == 13, msg
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CPYRAM13 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_or_blank(card, 2, 'pid', eid) nids = [ integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer(card, 7, 'nid5'), integer_or_blank(card, 8, 'nid6'), integer_or_blank(card, 9, 'nid7'), integer_or_blank(card, 10, 'nid8'), integer_or_blank(card, 11, 'nid9'), integer_or_blank(card, 12, 'nid10'), integer_or_blank(card, 13, 'nid11'), integer_or_blank(card, 14, 'nid12'), integer_or_blank(card, 15, 'nid13') ] assert len(card) <= 16, f'len(CPYRAM13 1card) = {len(card):d}\ncard={card}' return CPYRAM13(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CPYRAM13 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = [d if d > 0 else None for d in data[2:]] return CPYRAM13(eid, pid, nids, 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 CPYRAM eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CPYRAM eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes[:5], msg=msg) + model.EmptyNodes(self.nodes[5:], msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ node_ids = self.node_ids n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13 = node_ids faces = { 1 : [n1, n2, n3, n4, n6, n7, n8, n9], 2 : [n1, n2, n5, n6, n11, n10], 3 : [n2, n3, n5, n7, n12, n11], 4 : [n3, n4, n5, n8, n13, n12], 5 : [n4, n1, n5, n9, n10, n13], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[3]])), tuple(sorted([node_ids[3], node_ids[0]])), # sides tuple(sorted([node_ids[0], node_ids[4]])), tuple(sorted([node_ids[1], node_ids[4]])), tuple(sorted([node_ids[2], node_ids[4]])), tuple(sorted([node_ids[3], node_ids[4]])), ]
def _verify(self, xref: bool) -> None: _verify_solid_elem_quadratic(self, xref, 5)
[docs] def Centroid(self): """ .. seealso:: CPYRAM5.Centroid """ (n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13) = self.get_node_positions() c1 = area_centroid(n1, n2, n3, n4)[1] centroid = (c1 + n5) / 2. return centroid
[docs] def Volume(self) -> float: """ .. seealso:: CPYRAM5.Volume V = (l * w) * h / 3 V = A * h / 3 """ (n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13) = self.get_node_positions() area1, c1 = area_centroid(n1, n2, n3, n4) volume = area1 / 3. * norm(c1 - n5) return abs(volume)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=True) return nids
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[5:]] data = [self.eid, self.Pid()] + nodes[:5] + nodes2 msg = ('CPYRAM %8d%8d%8d%8d%8d%8d%8d%8s\n' ' %8s%8s%8s%8s%8s%8s%s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%16d' % node for node in nodes[5:]] data = [self.eid, self.Pid()] + nodes[:5] + nodes2 msg = ('CPYRAM* %16d%16d%16d%16d\n' '* %16d%16d%16d%16s\n' '* %16s%16s%16s%16s\n' '* %16s%16s%s\n' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] class CTETRA4(SolidElement): """ +--------+-----+-----+----+----+----+----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +========+=====+=====+====+====+====+====+ | CTETRA | EID | PID | G1 | G2 | G3 | G4 | +--------+-----+-----+----+----+----+----+ """ type = 'CTETRA' @property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[3]], 2 : [nodes[0], nodes[3], nodes[2]], 3 : [nodes[1], nodes[2], nodes[3]], 4 : [nodes[0], nodes[2], nodes[1]], } return faces @property def ansys_faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with ANSYS numbering. .. warning:: higher order element ids not verified with ANSYS. Examples -------- >>> print(element.faces) """ nodes = self.node_ids faces = { 1 : [nodes[0], nodes[1], nodes[2]], 2 : [nodes[0], nodes[1], nodes[3]], 3 : [nodes[1], nodes[2], nodes[3]], 4 : [nodes[2], nodes[0], nodes[3]], } return faces
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = 'CTETRA %8d%8d%8d%8d%8d%8d\n' % tuple(data) return self.comment + msg
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids data = [self.eid, self.Pid()] + nodes msg = ('CTETRA* %16d%16d%16d%16d\n' '* %16d%16d\n' % tuple(data)) return self.comment + msg
def __init__(self, eid, pid, nids, comment=''): """ Creates a CTETRA4 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=4 comment : str; default='' a comment for the card """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid self.nodes = self.prepare_node_ids(nids) assert len(self.nodes) == 4
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CTETRA4 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') nids = [integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), ] assert len(card) == 7, f'len(CTETRA4 card) = {len(card):d}\ncard={card}' return CTETRA4(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CTETRA4 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = data[2:] assert len(data) == 6, 'len(data)=%s data=%s' % (len(data), data) return CTETRA4(eid, pid, nids, 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 CTETRA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 = ', which is required by CTETRA eid=%s' % self.eid self.nodes_ref = model.Nodes(self.nodes, msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
[docs] def material_coordinate_system(self, xyz=None): """ Returns ------- centroid: (3,) float ndarray the centoid xe, ye, ze: (3,) float ndarray the element coordinate system """ centroid, xe, ye, ze = _ctetra_element_coordinate_system(self, xyz=None) return centroid, xe, ye, ze
def _verify(self, xref): _verify_solid_elem_linear(self, xref)
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[0]])), # sides tuple(sorted([node_ids[0], node_ids[3]])), tuple(sorted([node_ids[1], node_ids[3]])), tuple(sorted([node_ids[2], node_ids[3]])), ]
[docs] def Volume(self): """Calculate the volume of the tet""" (n1, n2, n3, n4) = self.get_node_positions() return volume4(n1, n2, n3, n4)
[docs] def Centroid(self): (n1, n2, n3, n4) = self.get_node_positions() return (n1 + n2 + n3 + n4) / 4.
[docs] def get_face_nodes(self, nid_opposite, nid=None): assert nid is None, nid nids = self.node_ids[:4] indx = nids.index(nid_opposite) nids.pop(indx) return nids
[docs] def get_face(self, nid_opposite, nid): nids = self.node_ids[:6] return ctetra_face(nid_opposite, nid, nids)
[docs] def get_face_area_centroid_normal(self, nid, nid_opposite): return ctetra_face_area_centroid_normal(nid, nid_opposite, self.node_ids, self.nodes_ref)
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=False) return nids
[docs] def flip_normal(self): ## TODO verify """flips the element inside out""" # flip n2 with n3 n1, n2, n3, n4 = self.nodes self.nodes = [n1, n3, n2, n4] if self.nodes_ref is not None: n1_ref, n2_ref, n3_ref, n4_ref = self.nodes_ref self.nodes_ref = [n1_ref, n3_ref, n2_ref, n4_ref]
[docs] def ctetra_face(nid, nid_opposite, nids): assert len(nids) == 4, nids g1i = nids.index(nid) g4i = nids.index(nid_opposite) _ctetra_faces = ( (3, 1, 0), (0, 1, 2), (3, 2, 1), (0, 2, 3), ) for face in _ctetra_faces: if g1i in face and g4i not in face: found_face = face return found_face
[docs] def ctetra_face_area_centroid_normal(nid, nid_opposite, nids, nodes_ref): """ Parameters ---------- nid : int G1 - a grid point on the corner of a face nid_opposite : int G4 - a grid point not being loaded """ face = ctetra_face(nid, nid_opposite, nids) nid1, nid2, nid3 = face n1 = nodes_ref[nid1].get_position() n2 = nodes_ref[nid2].get_position() n3 = nodes_ref[nid3].get_position() axb = cross(n2 - n1, n3 - n1) normi = norm(axb) centroid = (n1 + n2 + n3) / 3. area = 0.5 * normi assert area > 0, area normal = axb / normi return face, area, centroid, normal
[docs] class CTETRA10(SolidElement): """ +--------+-----+-----+-----+-----+-----+----+-----+-----+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +========+=====+=====+=====+=====+=====+====+=====+=====+ | CTETRA | EID | PID | G1 | G2 | G3 | G4 | G5 | G6 | +--------+-----+-----+-----+-----+-----+----+-----+-----+ | | G7 | G8 | G9 | G10 | | | | | +--------+-----+-----+-----+-----+-----+----+-----+-----+ | CTETRA | 1 | 1 | 239 | 229 | 516 | 99 | 335 | 103 | +--------+-----+-----+-----+-----+-----+----+-----+-----+ | | 265 | 334 | 101 | 102 | | | | | +--------+-----+-----+-----+-----+-----+----+-----+-----+ """ type = 'CTETRA'
[docs] def write_card(self, size: int=8, is_double: bool=False) -> str: nodes = self.node_ids nodes2 = ['' if node is None else '%8d' % node for node in nodes[4:]] data = [self.eid, self.Pid()] + nodes[:4] + nodes2 msg = ('CTETRA %8d%8d%8d%8d%8d%8d%8s%8s\n' ' %8s%8s%8s%8s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def write_card_16(self, is_double=False): nodes = self.node_ids nodes2 = ['' if node is None else '%16d' % node for node in nodes[4:]] data = [self.eid, self.Pid()] + nodes[:4] + nodes2 msg = ('CTETRA* %16d%16d%16d%16d\n' '* %16d%16d%16s%16s\n' '* %16s%16s%16s%16s' % tuple(data)) return self.comment + msg.rstrip() + '\n'
[docs] def get_face_area_centroid_normal(self, nid_opposite, nid=None): return ctetra_face_area_centroid_normal(nid_opposite, nid, self.node_ids[:4], self.nodes_ref[:4])
def __init__(self, eid, pid, nids, comment=''): """ Creates a CTETRA10 Parameters ---------- eid : int element id pid : int property id (PSOLID, PLSOLID) nids : list[int] node ids; n=10 """ SolidElement.__init__(self) if comment: self.comment = comment #: Element ID self.eid = eid #: Property ID self.pid = pid nnodes = len(nids) if nnodes < 10: nids.extend((10 - nnodes) * [None]) self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True) assert len(self.nodes) == 10
[docs] @classmethod def add_card(cls, card, comment=''): """ Adds a CTETRA10 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') nids = [integer(card, 3, 'nid1'), integer(card, 4, 'nid2'), integer(card, 5, 'nid3'), integer(card, 6, 'nid4'), integer_or_blank(card, 7, 'nid5'), integer_or_blank(card, 8, 'nid6'), integer_or_blank(card, 9, 'nid7'), integer_or_blank(card, 10, 'nid8'), integer_or_blank(card, 11, 'nid9'), integer_or_blank(card, 12, 'nid10'), ] assert len(card) <= 13, f'len(CTETRA10 card) = {len(card):d}\ncard={card}' return CTETRA10(eid, pid, nids, comment=comment)
@classmethod def add_op2_data(cls, data, comment=''): """ Adds a CTETRA10 card from the OP2 Parameters ---------- data : list[varies] a list of fields defined in OP2 format comment : str; default='' a comment for the card """ eid = data[0] pid = data[1] nids = [d if d > 0 else None for d in data[2:]] assert len(data) == 12, 'len(data)=%s data=%s' % (len(data), data) return CTETRA10(eid, pid, nids, 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 CTETRA eid=%s' % self.eid self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg) self.pid_ref = model.Property(self.pid, msg=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 CTETRA eid={self.eid}' self.nodes_ref = model.Nodes(self.nodes[:4], msg=msg) + model.EmptyNodes(self.nodes[4:], msg=msg) self.pid_ref = model.safe_property(self.pid, self.eid, xref_errors, msg=msg)
[docs] def material_coordinate_system(self, xyz=None): """ Returns ------- centroid: (3,) float ndarray the centoid xe, ye, ze: (3,) float ndarray the element coordinate system """ centroid, xe, ye, ze = _ctetra_element_coordinate_system(self, xyz=None) return centroid, xe, ye, ze
@property def faces(self): """ Gets the faces of the element Returns ------- faces : dict[int] = [face1, face2, ...] key = face number value = a list of nodes (integer pointers) as the values. .. note:: The order of the nodes are consistent with normals that point outwards The face numbering is meaningless .. note:: The order of the nodes are consistent with ANSYS numbering; is this current? .. warning:: higher order element ids not verified with ANSYS; is this current? Examples -------- >>> print(element.faces) """ n1, n2, n3, n4, n5, n6, n7, n8, n9, n10 = self.node_ids faces = { 1 : [n1, n2, n3, n5, n6, n7], #More? 2 : [n1, n2, n4, n5, n9, n8], 3 : [n2, n3, n4, n6, n10, n9], 4 : [n3, n1, n4, n7, n8, n10], } return faces
[docs] def get_edge_ids(self): """ Return the edge IDs """ node_ids = self.node_ids return [ # base tuple(sorted([node_ids[0], node_ids[1]])), tuple(sorted([node_ids[1], node_ids[2]])), tuple(sorted([node_ids[2], node_ids[0]])), # sides tuple(sorted([node_ids[0], node_ids[3]])), tuple(sorted([node_ids[1], node_ids[3]])), tuple(sorted([node_ids[2], node_ids[3]])), ]
def _verify(self, xref: bool) -> None: _verify_solid_elem_quadratic(self, xref, 4) #def N_10(self, g1, g2, g3, g4): #N1 = g1 * (2 * g1 - 1) #N2 = g2 * (2 * g2 - 1) #N3 = g3 * (2 * g3 - 1) #N4 = g4 * (2 * g4 - 1) #N5 = 4 * g1 * g2 #N6 = 4 * g2 * g3 #N7 = 4 * g3 * g1 #N8 = 4 * g1 * g4 #N9 = 4 * g2 * g4 #N10 = 4 * g3 * g4 #return (N1, N2, N3, N4, N5, N6, N7, N8, N9, N10)
[docs] def Volume(self): """ Gets the volume, :math:`V`, of the primary tetrahedron. .. seealso:: CTETRA4.Volume """ (n1, n2, n3, n4) = self.get_node_positions()[:4] return volume4(n1, n2, n3, n4)
[docs] def Centroid(self): """ Gets the cenroid of the primary tetrahedron. .. seealso:: CTETRA4.Centroid """ (n1, n2, n3, n4) = self.get_node_positions()[:4] return (n1 + n2 + n3 + n4) / 4.
[docs] def get_face_nodes(self, nid_opposite, nid=None): nids = self.node_ids[:4] indx = nids.index(nid_opposite) nids.pop(indx) return nids
@property def node_ids(self): nids = self._node_ids(nodes=self.nodes_ref, allow_empty_nodes=True) return nids
[docs] def _ctetra_element_coordinate_system(element: Union[CTETRA4, CTETRA10], xyz=None): """ Returns ------- centroid: (3,) float ndarray the centoid xe, ye, ze: (3,) float ndarray the element coordinate system http://www.ipes.dk/Files/Ipes/Filer/nastran_2016_doc_release.pdf""" # this is the #if normal is None: #normal = element.Normal() # k = kmat if xyz is None: x1 = element.nodes_ref[0].get_position() x2 = element.nodes_ref[1].get_position() x3 = element.nodes_ref[2].get_position() x4 = element.nodes_ref[3].get_position() else: x1 = xyz[:, 0] x2 = xyz[:, 1] x3 = xyz[:, 2] x4 = xyz[:, 3] #CORDM=-2 centroid = (x1 + x2 + x3 + x4) / 4. xe = (x2 + x3 + x4) / 3. - x1 xe /= np.linalg.norm(xe) v = ((x1 + x3 + x4) - (x1 + x2 + x4)) / 3. ze = np.cross(xe, v) ze /= np.linalg.norm(ze) ye = np.cross(ze, xe) ye /= np.linalg.norm(ye) return centroid, xe, ye, ze
[docs] def _verify_solid_elem_linear(elem: Union[CTETRA4, CPYRAM5, CPENTA6, CHEXA8], xref: bool): eid = elem.eid pid = elem.Pid() assert isinstance(eid, int) assert isinstance(pid, int) for i, nid in enumerate(elem.node_ids): assert isinstance(nid, int), 'nid%i is not an integer; nid=%s' % (i, nid) if xref: centroid = elem.Centroid() volume = elem.Volume() assert isinstance(volume, float), f'Volume={volume!r} must be a float; type={str(volume.__class__.__name__)}' if volume < 0: raise RuntimeError(f'Volume={volume} must be > 0\n{str(elem)}') for i in range(3): assert isinstance(centroid[i], float)
[docs] def _verify_solid_elem_quadratic(elem: Union[CTETRA10, CPYRAM13, CPENTA15, CHEXA20], xref: bool, nnodes_min: int) -> None: eid = elem.eid pid = elem.Pid() unused_edges = elem.get_edge_ids() assert isinstance(eid, int) assert isinstance(pid, int) nids = elem.node_ids for i in range(nnodes_min): nid = nids[i] assert isinstance(nid, int), 'nid%i is not an integer; nid=%s' % (i, nid) nnodes = len(nids) for i in range(nnodes_min, nnodes): nid = nids[i] assert nid is None or isinstance(nid, int), 'nid%i is not an integer/blank; nid=%s' % (i, nid) if xref: centroid = elem.Centroid() volume = elem.Volume() assert isinstance(volume, float), f'Volume={volume!r} must be a float; type={str(volume.__class__.__name__)}' if volume < 0.0: raise RuntimeError(f'Volume={volume} must be > 0\n{str(elem)}') for i in range(3): assert isinstance(centroid[i], float)