# pylint: disable=C0103,R0902,R0904,R0914,C0111
"""
All mass elements are defined in this file. This includes:
* CMASS1
* CMASS2
* CMASS3
* CMASS4
* CONM1
* CONM2
All mass elements are PointMassElement and Element objects.
"""
from __future__ import annotations
import warnings
from typing import Union, TYPE_CHECKING
import numpy as np
from pyNastran.utils.numpy_utils import integer_types
from pyNastran.bdf.field_writer_8 import set_blank_if_default, print_card_8
from pyNastran.bdf.cards.base_card import Element
from pyNastran.bdf.bdf_interface.assign_type import (
integer, integer_or_blank, double_or_blank)
from pyNastran.bdf.field_writer_16 import print_card_16
from pyNastran.bdf.field_writer_double import print_card_double
if TYPE_CHECKING: # pragma: no cover
from pyNastran.bdf.bdf import BDF
[docs]
def is_positive_semi_definite(A, tol=1e-8):
"""is the 3x3 matrix positive within tolerance"""
vals = np.linalg.eigvalsh(A)
return np.all(vals > -tol), vals
[docs]
class PointMassElement(Element):
def __init__(self):
Element.__init__(self)
[docs]
def repr_fields(self) -> list[Union[int, float, str, None]]:
return self.raw_fields()
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
return self.comment + print_card_16(card)
[docs]
def write_card_16(self, is_double: bool=False) -> str:
card = self.repr_fields()
return self.comment + print_card_16(card)
# class PointMass(BaseCard):
# def __init__(self, card, data):
# self.mass = None
#
# def Mass(self):
# return self.mass
[docs]
class CMASS1(PointMassElement):
"""
Defines a scalar mass element.
+--------+-----+-----+----+----+----+----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+========+=====+=====+====+====+====+====+
| CMASS1 | EID | PID | G1 | C1 | G2 | C2 |
+--------+-----+-----+----+----+----+----+
"""
type = 'CMASS1'
_field_map = {
1: 'eid', 2:'pid', 3:'g1', 4:'c1', 5:'g2', 6:'c2',
}
_properties = ['node_ids']
[docs]
@classmethod
def _init_from_empty(cls):
eid = 1
pid = 1
nids = [1, 2]
return CMASS1(eid, pid, nids, c1=0, c2=0, comment='')
def __init__(self, eid: int, pid: int, nids: list[int], c1: int=0, c2: int=0, comment: str=''):
"""
Creates a CMASS1 card
Parameters
----------
eid : int
element id
pid : int
property id (PMASS)
nids : list[int, int]
node ids
c1 / c2 : int; default=0
DOF for nid1 / nid2
comment : str; default=''
a comment for the card
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
self.eid = eid
self.pid = pid
self.c1 = c1
self.c2 = c2
self.nodes = self.prepare_node_ids(nids, allow_empty_nodes=True)
self.nodes_ref = None
self.pid_ref = None
[docs]
@classmethod
def add_card(cls, card, comment=''):
"""
Adds a CMASS1 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)
g1 = integer(card, 3, 'g1')
c1 = integer_or_blank(card, 4, 'c1')
g2 = integer_or_blank(card, 5, 'g2')
c2 = integer_or_blank(card, 6, 'c2')
assert len(card) <= 7, f'len(CMASS1 card) = {len(card):d}\ncard={card}'
return CMASS1(eid, pid, [g1, g2], c1, c2, comment=comment)
@classmethod
def add_op2_data(cls, data, comment=''):
"""
Adds a CMASS1 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]
g1 = data[2]
g2 = data[3]
c1 = data[4]
c2 = data[5]
return CMASS1(eid, pid, [g1, g2], c1, c2, comment=comment)
[docs]
def Mass(self):
return self.pid_ref.mass
def _verify(self, xref):
eid = self.eid
pid = self.Pid()
mass = self.Mass()
c1 = self.c1
c2 = self.c2
#self.nodes
assert isinstance(eid, integer_types), 'eid=%r' % eid
assert isinstance(pid, integer_types), 'pid=%r' % pid
assert isinstance(mass, float), 'mass=%r' % mass
assert c1 is None or isinstance(c1, integer_types), 'c1=%r' % c1
assert c2 is None or isinstance(c2, integer_types), 'c2=%r' % c2
[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 CMASS1 eid=%s' % self.eid
self.nodes_ref = model.EmptyNodes(self.node_ids, msg=msg)
self.pid_ref = model.PropertyMass(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 CMASS1 eid=%s' % self.eid
self.nodes_ref, missing_nodes = model.safe_empty_nodes(self.node_ids, msg=msg)
self.pid_ref = model.safe_property_mass(self.pid, self.eid, xref_errors, msg=msg)
if missing_nodes:
model.log.warning(missing_nodes)
[docs]
def uncross_reference(self) -> None:
"""Removes cross-reference links"""
self.nodes = [self.G1(), self.G2()]
self.pid = self.Pid()
self.nodes_ref = None
self.pid_ref = None
[docs]
def G1(self):
if self.nodes_ref is not None:
return self.nodes_ref[0].nid
return self.nodes[0]
[docs]
def G2(self):
if self.nodes[1] in [0, None]:
return 0
if self.nodes_ref is not None:
return self.nodes_ref[1].nid
return self.nodes[1]
[docs]
def Centroid(self):
"""
Centroid is assumed to be c=(g1+g2)/2.
If g2 is blank, then the centroid is the location of g1.
"""
factor = 0.
p1 = np.array([0., 0., 0.])
p2 = np.array([0., 0., 0.])
if self.nodes_ref[0] is not None:
p1 = self.nodes_ref[0].get_position()
factor += 1.
g2 = self.G2()
if g2 not in [None, 0]:
p2 = self.nodes_ref[1].get_position()
factor += 1.
c = (p1 + p2) / factor
return c
[docs]
def center_of_mass(self):
return self.Centroid()
@property
def node_ids(self):
g1 = self.G1()
g2 = self.G2()
nodes = []
if g1:
nodes.append(g1)
if g2:
nodes.append(g2)
return nodes
[docs]
def Pid(self):
if self.pid_ref is None:
return self.pid
return self.pid_ref.pid
[docs]
def raw_fields(self):
fields = ['CMASS1', self.eid, self.Pid(), self.G1(), self.c1,
self.G2(), self.c2]
return fields
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
if is_double:
return self.comment + print_card_double(card)
return self.comment + print_card_16(card)
[docs]
class CMASS2(PointMassElement):
"""
Defines a scalar mass element without reference to a property entry.
+--------+-----+-----+----+----+----+----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+========+=====+=====+====+====+====+====+
| CMASS2 | EID | M | G1 | C1 | G2 | C2 |
+--------+-----+-----+----+----+----+----+
"""
type = 'CMASS2'
_field_map = {
1: 'eid', 2:'mass', 3:'g1', 4:'c1', 5:'g2', 6:'c2',
}
cp_name_map = {
'M' : 'mass',
}
_properties = ['node_ids']
[docs]
@classmethod
def _init_from_empty(cls):
eid = 1
mass = 1.
nids = [1, 2]
return CMASS2(eid, mass, nids, c1=0, c2=0, comment='')
def __init__(self, eid, mass, nids, c1, c2, comment=''):
"""
Creates a CMASS2 card
Parameters
----------
eid : int
element id
mass : float
mass
nids : list[int, int]
node ids
c1 / c2 : int; default=None
DOF for nid1 / nid2
comment : str; default=''
a comment for the card
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
self.eid = eid
self.mass = mass
self.nodes = nids
self.c1 = c1
self.c2 = c2
self.nodes_ref = None
assert len(self.nodes) == 2, self.nodes
[docs]
@classmethod
def export_to_hdf5(cls, h5_file, model, eids):
"""exports the masses in a vectorized way"""
#comments = []
mass = []
nodes = []
components = []
for eid in eids:
element = model.masses[eid]
#comments.append(element.comment)
mass.append(element.mass)
nodes.append([nid if nid is not None else 0 for nid in element.nodes])
components.append([comp if comp is not None else 0
for comp in [element.c1, element.c2]])
#h5_file.create_dataset('_comment', data=comments)
h5_file.create_dataset('eid', data=eids)
h5_file.create_dataset('mass', data=mass)
h5_file.create_dataset('nodes', data=nodes)
h5_file.create_dataset('components', data=components)
[docs]
@classmethod
def add_card(cls, card, comment=''):
"""
Adds a CMASS2 card from ``BDF.add_card(...)``
Parameters
----------
card : BDFCard()
a BDFCard object
comment : str; default=''
a comment for the card
"""
eid = integer(card, 1, 'eid')
mass = double_or_blank(card, 2, 'mass', 0.)
g1 = integer_or_blank(card, 3, 'g1')
c1 = integer_or_blank(card, 4, 'c1')
g2 = integer_or_blank(card, 5, 'g2')
c2 = integer_or_blank(card, 6, 'c2')
assert len(card) <= 7, f'len(CMASS2 card) = {len(card):d}\ncard={card}'
return CMASS2(eid, mass, [g1, g2], c1, c2, comment=comment)
@classmethod
def add_op2_data(cls, data, comment=''):
"""
Adds a CMASS2 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]
mass = data[1]
g1 = data[2]
g2 = data[3]
c1 = data[4]
c2 = data[5]
#assert g1 > 0, data
if g1 == 0:
g1 = None
else:
assert g1 > 0, f'g1={g1}; g2={g2} c1={c1} c2={c2}'
if g2 == 0:
g2 = None
else:
assert g2 > 0, f'g2={g2}; g1={g1} c1={c1} c2={c2}'
assert 0 <= c1 <= 123456, 'c1=%s data=%s' % (c1, data)
assert 0 <= c2 <= 123456, 'c2=%s data=%s' % (c2, data)
return CMASS2(eid, mass, [g1, g2], c1, c2, comment=comment)
[docs]
def validate(self):
assert len(self.nodes) == 2, self.nodes
def _verify(self, xref):
eid = self.eid
pid = self.Pid()
mass = self.Mass()
c1 = self.c1
c2 = self.c2
#self.nodes
assert isinstance(eid, integer_types), 'eid=%r' % eid
assert isinstance(pid, integer_types), 'pid=%r' % pid
assert isinstance(mass, float), 'mass=%r' % mass
assert c1 is None or isinstance(c1, integer_types), 'c1=%r' % c1
assert c2 is None or isinstance(c2, integer_types), 'c2=%r' % c2
#@property
#def nodes(self):
#return [self.g1, self.g2]
@property
def node_ids(self):
g1 = self.G1()
g2 = self.G2()
nodes = []
if g1:
nodes.append(g1)
if g2:
nodes.append(g2)
return nodes
[docs]
def Mass(self):
return self.mass
[docs]
def Centroid(self):
"""
Centroid is assumed to be c=(g1+g2)/2.
If g2 is blank, then the centroid is the location of g1.
"""
factor = 0.
p1 = np.array([0., 0., 0.])
p2 = np.array([0., 0., 0.])
if self.nodes[0] is not None:
p1 = self.nodes_ref[0].get_position()
factor += 1.
if self.nodes[1] is not None:
p2 = self.nodes_ref[1].get_position()
factor += 1.
assert factor > 0., str(self)
c = (p1 + p2) / factor
return c
[docs]
def center_of_mass(self):
return self.Centroid()
[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 CMASS2 eid=%s' % self.eid
self.nodes_ref = model.EmptyNodes(self.nodes, 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 CMASS2 eid=%s' % self.eid
self.nodes_ref, missing_nodes = model.safe_empty_nodes(self.node_ids, msg=msg)
if missing_nodes:
model.log.warning(missing_nodes)
[docs]
def uncross_reference(self) -> None:
"""Removes cross-reference links"""
self.nodes = [self.G1(), self.G2()]
self.nodes_ref = None
[docs]
def G1(self):
if self.nodes_ref is not None and self.nodes_ref[0] is not None:
return self.nodes_ref[0].nid
return self.nodes[0]
[docs]
def G2(self):
if self.nodes_ref is not None and self.nodes_ref[1] is not None:
return self.nodes_ref[1].nid
return self.nodes[1]
[docs]
def raw_fields(self):
fields = ['CMASS2', self.eid, self.mass, self.G1(),
self.c1, self.G2(), self.c2]
return fields
[docs]
def repr_fields(self):
mass = set_blank_if_default(self.mass, 0.)
fields = ['CMASS2', self.eid, mass, self.G1(), self.c1,
self.G2(), self.c2]
return fields
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
if is_double:
return self.comment + print_card_double(card)
return self.comment + print_card_16(card)
[docs]
class CMASS3(PointMassElement):
"""
Defines a scalar mass element that is connected only to scalar points.
+--------+-----+-----+----+----+
| 1 | 2 | 3 | 4 | 5 |
+========+=====+=====+====+====+
| CMASS3 | EID | PID | S1 | S2 |
+--------+-----+-----+----+----+
"""
type = 'CMASS3'
_field_map = {
1: 'eid', 2:'pid', 3:'s1', 4:'s2',
}
_properties = ['node_ids']
[docs]
@classmethod
def _init_from_empty(cls):
eid = 1
pid = 1
nids = [1, 2]
return CMASS3(eid, pid, nids, comment='')
def __init__(self, eid, pid, nids, comment=''):
"""
Creates a CMASS3 card
Parameters
----------
eid : int
element id
pid : int
property id (PMASS)
nids : list[int, int]
SPOINT ids
comment : str; default=''
a comment for the card
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
self.eid = eid
self.pid = pid
self.nodes = nids
assert self.nodes[0] != self.nodes[1]
self.nodes_ref = None
self.pid_ref = None
[docs]
@classmethod
def add_card(cls, card, comment=''):
"""
Adds a CMASS3 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)
s1 = integer_or_blank(card, 3, 's1')
s2 = integer_or_blank(card, 4, 's2')
assert len(card) <= 5, f'len(CMASS3 card) = {len(card):d}\ncard={card}'
return CMASS3(eid, pid, [s1, s2], comment=comment)
@classmethod
def add_op2_data(cls, data, comment=''):
"""
Adds a CMASS3 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]
s1 = data[2]
s2 = data[3]
return CMASS3(eid, pid, [s1, s2], comment=comment)
[docs]
def Mass(self):
return self.pid_ref.mass
[docs]
def S1(self):
if self.nodes_ref is not None:
return self.nodes_ref[0].nid
return self.nodes[0]
[docs]
def S2(self):
if self.nodes_ref is not None:
return self.nodes_ref[1].nid
return self.nodes[1]
@property
def node_ids(self):
return [self.S1(), self.S2()]
[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 CMASS3 eid=%s' % self.eid
self.nodes_ref = model.EmptyNodes(self.node_ids, msg=msg)
self.pid_ref = model.PropertyMass(self.pid, msg=msg)
[docs]
def uncross_reference(self) -> None:
"""Removes cross-reference links"""
self.pid = self.Pid()
self.nodes_ref = None
self.pid_ref = None
#def Mass(self):
#return self.mass
[docs]
def Centroid(self):
return np.zeros(3)
[docs]
def center_of_mass(self):
return self.Centroid()
[docs]
def raw_fields(self):
fields = ['CMASS3', self.eid, self.Pid(), self.S1(), self.S2()]
return fields
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
if is_double:
return self.comment + print_card_double(card)
return self.comment + print_card_16(card)
[docs]
class CMASS4(PointMassElement):
"""
Defines a scalar mass element that is connected only to scalar points,
without reference to a property entry
+--------+-----+-----+----+----+
| 1 | 2 | 3 | 4 | 5 |
+========+=====+=====+====+====+
| CMASS4 | EID | M | S1 | S2 |
+--------+-----+-----+----+----+
"""
type = 'CMASS4'
#cp_name_map/update_by_cp_name
cp_name_map = {
'M' : 'mass',
}
_properties = ['node_ids']
_field_map = {
1: 'eid', 2:'mass', 3:'s1', 4:'s2',
}
[docs]
@classmethod
def _init_from_empty(cls):
eid = 1
pid = 1
nids = [1, 2]
return CMASS4(eid, pid, nids, comment='')
def __init__(self, eid, mass, nids, comment=''):
"""
Creates a CMASS4 card
Parameters
----------
eid : int
element id
mass : float
SPOINT mass
nids : list[int, int]
SPOINT ids
comment : str; default=''
a comment for the card
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
self.eid = eid
self.mass = mass
self.nodes = nids
assert self.nodes[0] != self.nodes[1]
self.nodes_ref = None
[docs]
@classmethod
def add_card(cls, card, icard=0, comment=''):
ioffset = icard * 4
eid = integer(card, 1 + ioffset, 'eid')
mass = double_or_blank(card, 2 + ioffset, 'mass', default=0.)
s1 = integer(card, 3 + ioffset, 's1')
s2 = integer_or_blank(card, 4 + ioffset, 's2', default=0)
assert len(card) <= 9, f'len(CMASS4 card) = {len(card):d}\ncard={card}'
return CMASS4(eid, mass, [s1, s2], comment=comment)
@classmethod
def add_op2_data(cls, data, comment=''):
"""
Adds a CMASS4 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]
mass = data[1]
s1 = data[2]
s2 = data[3]
return CMASS4(eid, mass, [s1, s2], comment=comment)
[docs]
def Mass(self):
return self.mass
[docs]
def Centroid(self):
return np.zeros(3)
[docs]
def center_of_mass(self):
return self.Centroid()
@property
def node_ids(self):
return [self.S1(), self.S2()]
[docs]
def S1(self):
if self.nodes_ref is not None and self.nodes_ref[0] is not None:
return self.nodes_ref[0].nid
return self.nodes[0]
[docs]
def S2(self):
if self.nodes_ref is not None and self.nodes_ref[1] is not None:
return self.nodes_ref[1].nid
return self.nodes[1]
[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 CMASS4 eid=%s' % self.eid
self.nodes_ref = model.EmptyNodes(self.nodes, msg=msg)
[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.nodes = [self.S1(), self.S2()]
self.nodes_ref = None
[docs]
def raw_fields(self):
fields = ['CMASS4', self.eid, self.mass] + self.node_ids
return fields
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
if is_double:
return self.comment + print_card_double(card)
return self.comment + print_card_16(card)
[docs]
class CONM1(PointMassElement):
"""
Concentrated Mass Element Connection, General Form
Defines a 6 x 6 symmetric mass matrix at a geometric grid point
+--------+-----+-----+-----+-----+-----+-----+-----+-----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+========+=====+=====+=====+=====+=====+=====+=====+=====+
| CONM1 | EID | G | CID | M11 | M21 | M22 | M31 | M32 |
+--------+-----+-----+-----+-----+-----+-----+-----+-----+
| | M33 | M41 | M42 | M43 | M44 | M51 | M52 | M53 |
+--------+-----+-----+-----+-----+-----+-----+-----+-----+
| | M54 | M55 | M61 | M62 | M63 | M64 | M65 | M66 |
+--------+-----+-----+-----+-----+-----+-----+-----+-----+
"""
type = 'CONM1'
_field_map = {
1: 'eid', 2:'nid', 3:'cid',
}
_properties = ['node_ids']
def _update_field_helper(self, n, value):
m = self.mass_matrix
if n == 4:
m[0, 0] = value
elif n == 5:
m[1, 0] = value
elif n == 6:
m[1, 1] = value
elif n == 7:
m[2, 0] = value
elif n == 8:
m[2, 1] = value
elif n == 9:
m[2, 2] = value
elif n == 10:
m[3, 0] = value
elif n == 11:
m[3, 1] = value
elif n == 12:
m[3, 2] = value
elif n == 13:
m[3, 3] = value
elif n == 14:
m[4, 0] = value
elif n == 15:
m[4, 1] = value
elif n == 16:
m[4, 2] = value
elif n == 17:
m[4, 3] = value
elif n == 18:
m[4, 4] = value
elif n == 19:
m[5, 0] = value
elif n == 20:
m[5, 1] = value
elif n == 21:
m[5, 2] = value
elif n == 22:
m[5, 3] = value
elif n == 23:
m[5, 4] = value
elif n == 24:
m[5, 5] = value
else:
raise KeyError('Field %r=%r is an invalid %s entry.' % (n, value, self.type))
[docs]
def update_by_cp_name(self, name, value):
if name == 'M11':
self.mass_matrix[0, 0] = value
#elif name == 'M21':
#self.mass_matrix[1, 0] = value
elif name == 'M22':
self.mass_matrix[1, 1] = value
elif name == 'M33':
self.mass_matrix[2, 2] = value
elif name == 'M44':
self.mass_matrix[3, 3] = value
#elif name == 'X1':
#self.X[0] = value
else:
raise NotImplementedError('element_type=%r has not implemented %r in cp_name_map' % (
self.type, name))
[docs]
@classmethod
def _init_from_empty(cls):
eid = 1
nid = 1
mass_matrix = np.zeros((6, 6))
return CONM1(eid, nid, mass_matrix, cid=0, comment='')
[docs]
def _finalize_hdf5(self, encoding):
"""hdf5 helper function"""
self.mass_matrix = np.asarray(self.mass_matrix)
def __init__(self, eid: int, nid: int, mass_matrix: np.ndarray,
cid: int=0, comment: str=''):
"""
Creates a CONM1 card
Parameters
----------
eid : int
element id
nid : int
the node to put the mass matrix
mass_matrix : (6, 6) float ndarray
the 6x6 mass matrix, M
cid : int; default=0
the coordinate system for the mass matrix
comment : str; default=''
a comment for the card
::
[M] = [M11 M21 M31 M41 M51 M61]
[ M22 M32 M42 M52 M62]
[ M33 M43 M53 M63]
[ M44 M54 M64]
[ Sym M55 M65]
[ M66]
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
self.mass_matrix = mass_matrix
self.eid = eid
self.nid = nid
self.cid = cid
self.nid_ref = None
self.cid_ref = None
[docs]
@classmethod
def add_card(cls, card: BDFCard, comment: str=''):
"""
Adds a CONM2 card from ``BDF.add_card(...)``
Parameters
----------
card : BDFCard()
a BDFCard object
comment : str; default=''
a comment for the card
"""
m = np.zeros((6, 6))
eid = integer(card, 1, 'eid')
nid = integer(card, 2, 'nid')
cid = integer_or_blank(card, 3, 'cid', 0)
m[0, 0] = double_or_blank(card, 4, 'M11', 0.)
m[1, 0] = double_or_blank(card, 5, 'M21', 0.)
m[1, 1] = double_or_blank(card, 6, 'M22', 0.)
m[2, 0] = double_or_blank(card, 7, 'M31', 0.)
m[2, 1] = double_or_blank(card, 8, 'M32', 0.)
m[2, 2] = double_or_blank(card, 9, 'M33', 0.)
m[3, 0] = double_or_blank(card, 10, 'M41', 0.)
m[3, 1] = double_or_blank(card, 11, 'M42', 0.)
m[3, 2] = double_or_blank(card, 12, 'M43', 0.)
m[3, 3] = double_or_blank(card, 13, 'M44', 0.)
m[4, 0] = double_or_blank(card, 14, 'M51', 0.)
m[4, 1] = double_or_blank(card, 15, 'M52', 0.)
m[4, 2] = double_or_blank(card, 16, 'M53', 0.)
m[4, 3] = double_or_blank(card, 17, 'M54', 0.)
m[4, 4] = double_or_blank(card, 18, 'M55', 0.)
m[5, 0] = double_or_blank(card, 19, 'M61', 0.)
m[5, 1] = double_or_blank(card, 20, 'M62', 0.)
m[5, 2] = double_or_blank(card, 21, 'M63', 0.)
m[5, 3] = double_or_blank(card, 22, 'M64', 0.)
m[5, 4] = double_or_blank(card, 23, 'M65', 0.)
m[5, 5] = double_or_blank(card, 24, 'M66', 0.)
assert len(card) <= 25, f'len(CONM1 card) = {len(card):d}\ncard={card}'
return CONM1(eid, nid, m, cid=cid, comment=comment)
@classmethod
def add_op2_data(cls, data, comment: str=''):
"""
Adds a CONM1 card from the OP2
Parameters
----------
data : list[varies]
a list of fields defined in OP2 format
comment : str; default=''
a comment for the card
"""
m = np.zeros((6, 6))
(eid, nid, cid, m1, m2a, m2b, m3a, m3b, m3c, m4a, m4b, m4c, m4d,
m5a, m5b, m5c, m5d, m5e, m6a, m6b, m6c, m6d, m6e, m6f) = data
m[0, 0] = m1 # M11
m[1, 0] = m2a # M21
m[1, 1] = m2b # M22
m[2, 0] = m3a # M31
m[2, 1] = m3b # M32
m[2, 2] = m3c # M33
m[3, 0] = m4a # M41
m[3, 1] = m4b # M42
m[3, 2] = m4c # M43
m[3, 3] = m4d # M44
m[4, 0] = m5a # M51
m[4, 1] = m5b # M52
m[4, 2] = m5c # M53
m[4, 3] = m5d # M54
m[4, 4] = m5e # M55
m[5, 0] = m6a # M61
m[5, 1] = m6b # M62
m[5, 2] = m6c # M63
m[5, 3] = m6d # M64
m[5, 4] = m6e # M65
m[5, 5] = m6f # M66
return CONM1(eid, nid, m, cid=cid, comment=comment)
def _verify(self, xref: bool):
eid = self.eid
assert isinstance(eid, integer_types), 'eid=%r' % eid
[docs]
def Mass(self) -> float:
mass = (self.mass_matrix[0, 0] + self.mass_matrix[1, 1] + self.mass_matrix[2, 2]) / 3.
return mass
[docs]
@staticmethod
def Centroid() -> np.ndarray:
return np.zeros(3, dtype='float64')
[docs]
def center_of_mass(self) -> np.ndarray:
return self.Centroid()
@property
def node_ids(self) -> list[int]:
return [self.Nid()]
[docs]
def Nid(self) -> int:
if self.nid_ref is not None:
return self.nid_ref.nid
return self.nid
[docs]
def Cid(self) -> int:
if self.cid_ref is not None:
return self.cid_ref.cid
return self.cid
[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 CONM1 eid=%s' % self.eid
self.nid_ref = model.Node(self.nid, msg=msg)
self.cid_ref = model.Coord(self.cid, 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 CONM1 eid=%s' % self.eid
self.nid_ref = model.Node(self.nid, msg=msg)
self.cid_ref = model.safe_coord(self.cid, self.eid, xref_errors, msg='')
[docs]
def uncross_reference(self) -> None:
"""Removes cross-reference links"""
self.nid = self.Nid()
self.cid = self.Cid()
self.nid_ref = None
self.cid_ref = None
[docs]
def MassMatrix(self):
return self.mass_matrix
[docs]
def raw_fields(self):
cid = set_blank_if_default(self.Cid(), 0)
nid = self.Nid()
m = self.mass_matrix
# lower triangular
list_fields = [
'CONM1', self.eid, nid, cid, m[0, 0], m[1, 0], m[1, 1],
m[2, 0], m[2, 1], m[2, 2], m[3, 0], m[3, 1], m[3, 2],
m[3, 3], m[4, 0], m[4, 1], m[4, 2], m[4, 3], m[4, 4],
m[5, 0], m[5, 1], m[5, 2], m[5, 3], m[5, 4], m[5, 5]]
return list_fields
[docs]
def repr_fields(self):
list_fields = self.raw_fields()
list_fields2 = list_fields[0:4]
for field in list_fields[4:]:
val = set_blank_if_default(field, 0.)
list_fields2.append(val)
return list_fields2
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
if is_double:
return self.comment + print_card_double(card)
return self.comment + print_card_16(card)
[docs]
class CONM2(PointMassElement):
"""
+-------+--------+-------+-------+---------+------+------+------+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+=======+========+=======+=======+=========+======+======+======+
| CONM2 | EID | NID | CID | MASS | X1 | X2 | X3 |
+-------+--------+-------+-------+---------+------+------+------+
| | I11 | I21 | I22 | I31 | I32 | I33 | |
+-------+--------+-------+-------+---------+------+------+------+
| CONM2 | 501274 | 11064 | | 132.274 | | | |
+-------+--------+-------+-------+---------+------+------+------+
"""
type = 'CONM2'
_field_map = {
1: 'eid', 2:'nid', 3:'cid', 4:'mass',
}
[docs]
def update_by_cp_name(self, name, value):
if name == 'M':
self.mass = value
elif name == 'X1':
self.X[0] = value
elif name == 'X2':
self.X[1] = value
elif name == 'X3':
self.X[2] = value
elif name == 'I11':
#I11, I21, I22, I31, I32, I33 = I
self.I[0] = value
elif name == 'I21':
self.I[1] = value
elif name == 'I22':
self.I[2] = value
elif name == 'I31':
self.I[3] = value
elif name == 'I32':
self.I[4] = value
elif name == 'I33':
self.I[5] = value
else:
raise NotImplementedError('element_type=%r has not implemented %r in cp_name_map' % (
self.type, name))
def _update_field_helper(self, n, value):
if n == 5:
self.X[0] = value
elif n == 6:
self.X[1] = value
elif n == 7:
self.X[2] = value
elif n == 9:
self.I[0] = value
elif n == 10:
self.I[1] = value
elif n == 11:
self.I[2] = value
elif n == 12:
self.I[3] = value
elif n == 13:
self.I[4] = value
elif n == 14:
self.I[5] = value
else:
raise KeyError('Field %r=%r is an invalid %s entry.' % (n, value, self.type))
[docs]
@classmethod
def export_to_hdf5(cls, h5_file, model, eids):
"""exports the elements in a vectorized way"""
unused_comments = []
nid = []
cid = []
mass = []
X = []
I = []
for eid in eids:
element = model.masses[eid]
#comments.append(element.comment)
nid.append(element.nid)
cid.append(element.cid)
mass.append(element.mass)
X.append(element.X)
I.append(element.I)
#h5_file.create_dataset('_comment', data=comments)
h5_file.create_dataset('eid', data=eids)
h5_file.create_dataset('nid', data=nid)
h5_file.create_dataset('cid', data=cid)
h5_file.create_dataset('X', data=X)
h5_file.create_dataset('I', data=I)
h5_file.create_dataset('mass', data=mass)
#self.eid = eid
#self.nid = nid
#self.cid = cid
#self.mass = mass
#self.X = np.asarray(X)
#self.I = np.asarray(I)
def __init__(self, eid, nid, mass, cid=0, X=None, I=None, comment=''):
"""
Creates a CONM2 card
Parameters
----------
eid : int
element id
nid : int
node id
mass : float
the mass of the CONM2
cid : int; default=0
coordinate frame of the offset (-1=absolute coordinates)
X : (3, ) list[float]; default=None -> [0., 0., 0.]
xyz offset vector relative to nid
I : (6, ) list[float]; default=None -> [0., 0., 0., 0., 0., 0.]
mass moment of inertia matrix about the CG
I11, I21, I22, I31, I32, I33 = I
comment : str; default=''
a comment for the card
"""
PointMassElement.__init__(self)
if comment:
self.comment = comment
#: Element identification number. (0 < Integer < 100,000,000)
self.eid = eid
#: Grid point identification number. (Integer > 0)
self.nid = nid
#: Coordinate system identification number.
#: For CID of -1; see X1, X2, X3 below.
#: (Integer > -1; Default = 0)
self.cid = cid
#: Mass value. (Real)
self.mass = mass
if X is None:
X = np.zeros(3)
#: Offset distances from the grid point to the center of gravity of
#: the mass in the coordinate system defined in field 4, unless
#: CID = -1, in which case X1, X2, X3 are the coordinates, not
#: offsets, of the center of gravity of the mass in the basic
#: coordinate system. (Real)
self.X = np.asarray(X)
if I is None:
I = np.zeros(6)
#: Mass moments of inertia measured at the mass center of gravity in
#: the coordinate system defined by field 4. If CID = -1, the basic
#: coordinate system is implied. (Real)
#: I11, I21, I22, I31, I32, I33 = I
self.I = np.asarray(I)
self.nid_ref = None
self.cid_ref = None
[docs]
def validate(self):
assert isinstance(self.cid, integer_types), self.cid
assert isinstance(self.mass, float), self.mass
assert self.mass >= 0., 'mass=%s' % self.mass
assert isinstance(self.X, np.ndarray), self.X
assert isinstance(self.I, np.ndarray), self.I
assert len(self.X) == 3, self.X
assert len(self.I) == 6, self.I
I11, I12, I22, I13, I23, I33 = self.I
I = np.array([
[I11, I12, I13],
[I12, I22, I23],
[I13, I23, I33],
], dtype='float32')
is_psd, eigi = is_positive_semi_definite(I)
if not is_psd:
msg = (f'The eig(I) >= 0. for CONM2 eid={self.eid:d}\n'
f'I=\n{I}\n'
f'eigenvalues={eigi}')
warnings.warn(msg)
[docs]
@classmethod
def add_card(cls, card, comment=''):
"""
Adds a CONM2 card from ``BDF.add_card(...)``
Parameters
----------
card : BDFCard()
a BDFCard object
comment : str; default=''
a comment for the card
"""
eid = integer(card, 1, 'eid')
nid = integer(card, 2, 'nid')
cid = integer_or_blank(card, 3, 'cid', 0)
mass = double_or_blank(card, 4, 'mass', 0.)
X = [
double_or_blank(card, 5, 'x1', 0.0),
double_or_blank(card, 6, 'x2', 0.0),
double_or_blank(card, 7, 'x3', 0.0),
]
I = [
double_or_blank(card, 9, 'I11', 0.0),
double_or_blank(card, 10, 'I21', 0.0),
double_or_blank(card, 11, 'I22', 0.0),
double_or_blank(card, 12, 'I31', 0.0),
double_or_blank(card, 13, 'I32', 0.0),
double_or_blank(card, 14, 'I33', 0.0),
]
assert len(card) <= 15, 'len(CONM2 card) = {len(card):d}\ncard={card}'
return CONM2(eid, nid, mass, cid=cid, X=X, I=I, comment=comment)
@classmethod
def add_op2_data(cls, data, comment=''):
"""
Adds a CONM2 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]
nid = data[1]
cid = data[2]
mass = data[3]
X = data[4:7]
I = data[7:]
return CONM2(eid, nid, mass, cid=cid, X=X, I=I, comment=comment)
def _verify(self, xref):
eid = self.eid
nid = self.Nid()
cid = self.Cid()
mass = self.Mass()
c = self.Centroid()
assert isinstance(eid, integer_types), 'CONM2: eid=%r' % eid
assert isinstance(nid, integer_types), 'CONM2: nid=%r' % nid
assert isinstance(cid, integer_types), 'CONM2: cid=%r' % cid
assert isinstance(mass, float), 'CONM2: mass=%r' % mass
for i in range(3):
assert isinstance(c[i], float), 'CONM2: centroid[%i]=%r' % (i, c[i])
[docs]
def Mass(self):
return self.mass
[docs]
def Inertia(self):
"""
Returns the 3x3 inertia matrix
.. warning:: doesnt handle offsets or coordinate systems
"""
I = self.I
A = [[I[0], -I[1], -I[3]],
[-I[1], I[2], -I[4]],
[-I[3], -I[4], I[5]]]
if self.Cid() in [0, -1]:
return A
else:
# transform to global
#dx = self.cid_ref.transform_node_to_global(self.X)
#matrix = self.cid_ref.beta()
warnings.warn(f'CONM2 (eid={self.eid}) inertia method for CID != 0 is not implemented')
return np.zeros((3, 3))
#A2 = A * matrix
#return A2 # correct for offset using dx???
[docs]
def offset(self, xyz_nid):
cid = self.Cid()
if cid == 0:
# no transform needed
X2 = xyz_nid + self.X
elif cid == -1:
# case X1, X2, X3 are the coordinates, not offsets, of the center of gravity of
# the mass in the basic coordinate system.
# 4. If CID = -1, offsets are internally computed as the difference between the grid
# point location and X1, X2, X3.
# this statement is not supported...
return self.X
else:
# Offset distances from the grid point to the center of gravity of the mass
# in the coordinate system
# If CID > 0, then X1, X2, and X3 are defined by a local Cartesian system, even
# if CID references a spherical or cylindrical coordinate system. This is similar
# to the manner in which displacement coordinate systems are defined.
# this statement is not supported...
# convert self.X into the global frame
x = self.cid_ref.transform_node_to_global(self.X)
# self.X is an offset
dx = x - self.cid_ref.origin
# the actual position of the CONM2
X2 = xyz_nid + dx
return X2
[docs]
def Centroid(self):
"""
This method seems way more complicated than it needs to be thanks
to all these little caveats that don't seem to be supported.
"""
cid = self.Cid()
if cid == 0:
# no transform needed
X2 = self.nid_ref.get_position() + self.X
elif cid == -1:
# case X1, X2, X3 are the coordinates, not offsets, of the center of gravity of
# the mass in the basic coordinate system.
# 4. If CID = -1, offsets are internally computed as the difference between the grid
# point location and X1, X2, X3.
# this statement is not supported...
return self.X
else:
# Offset distances from the grid point to the center of gravity of the mass
# in the coordinate system
# If CID > 0, then X1, X2, and X3 are defined by a local Cartesian system, even
# if CID references a spherical or cylindrical coordinate system. This is similar
# to the manner in which displacement coordinate systems are defined.
# this statement is not supported...
# convert self.X into the global frame
x = self.cid_ref.transform_node_to_global(self.X)
# self.X is an offset
dx = x - self.cid_ref.origin
# the actual position of the CONM2
X2 = self.nid_ref.get_position() + dx
return X2
[docs]
def Centroid_no_xref(self, model: BDF) -> np.ndarray:
"""
This method seems way more complicated than it needs to be thanks
to all these little caveats that don't seem to be supported.
"""
cid = self.cid
nid_ref = model.Node(self.nid)
if cid == 0:
# no transform needed
X2 = nid_ref.get_position() + self.X
elif cid == -1:
# case X1, X2, X3 are the coordinates, not offsets, of the center of gravity of
# the mass in the basic coordinate system.
# 4. If CID = -1, offsets are internally computed as the difference between the grid
# point location and X1, X2, X3.
# this statement is not supported...
return self.X
else:
cid_ref = model.Coord(self.cid)
# Offset distances from the grid point to the center of gravity of the mass
# in the coordinate system
# If CID > 0, then X1, X2, and X3 are defined by a local Cartesian system, even
# if CID references a spherical or cylindrical coordinate system. This is similar
# to the manner in which displacement coordinate systems are defined.
# this statement is not supported...
# convert self.X into the global frame
x = cid_ref.transform_node_to_global(self.X)
# self.X is an offset
dx = x - cid_ref.origin
# the actual position of the CONM2
X2 = nid_ref.get_position() + dx
return X2
[docs]
def centroid_mass_inertia(self) -> tuple[np.ndarray, float, np.ndarray]:
centroid = self.Centroid()
mass = self.mass
inertia = self.Inertia()
return centroid, mass, inertia
[docs]
def center_of_mass(self) -> np.ndarray:
return self.Centroid()
[docs]
def mass_matrix(self, fdtype: str='float32') -> np.ndarray:
"""gets the 6x6 mass matrix"""
mass = self.Mass()
unused_rx, unused_ry, unused_rz = self.X
mass_mat = np.zeros((6, 6), dtype=fdtype)
mass_mat[0, 0] = mass
mass_mat[1, 1] = mass
mass_mat[2, 2] = mass
#mass_mat[3, 3] = i11
#mass_mat[3, 4] = mass_mat[4, 3] = -i12
#mass_mat[3, 5] = mass_mat[5, 3] = -i13
#mass_mat[4, 4] = i22
#mass_mat[4, 5] = mass_mat[5, 4] = -i23
#mass_mat[5, 5] = i33
inertia = self.Inertia()
#inertia = np.ones((3, 3))
mass_mat[3:, 3:] = inertia
#print(mass_mat)
return mass_mat
[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 CONM2 eid=%s' % self.eid
self.nid_ref = model.Node(self.nid, msg=msg)
cid = self.Cid()
if cid != -1:
self.cid_ref = model.Coord(cid, 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 CONM2 eid=%s' % self.eid
self.nid_ref = model.Node(self.nid, msg=msg)
cid = self.Cid()
if cid != -1:
self.cid_ref = model.safe_coord(cid, self.eid, xref_errors, msg='')
[docs]
def uncross_reference(self) -> None:
"""Removes cross-reference links"""
self.nid = self.Nid()
self.cid = self.Cid()
self.nid_ref = None
self.cid_ref = None
@property
def node_ids(self):
return [self.Nid()]
[docs]
def Nid(self):
if self.nid_ref is not None:
return self.nid_ref.nid
return self.nid
[docs]
def Cid(self):
if self.cid_ref is not None:
return self.cid_ref.cid
return self.cid
[docs]
def raw_fields(self):
list_fields = (['CONM2', self.eid, self.Nid(), self.Cid(), self.mass] +
list(self.X) + [None] + list(self.I))
return list_fields
[docs]
def repr_fields(self):
I = []
for i in self.I:
if i == 0.:
I.append(None)
else:
I.append(i)
X = []
for x in self.X:
if x == 0.:
X.append(None)
else:
X.append(x)
cid = set_blank_if_default(self.Cid(), 0)
list_fields = (['CONM2', self.eid, self.Nid(), cid, self.mass] + X +
[None] + I)
return list_fields
[docs]
def write_card(self, size: int=8, is_double: bool=False) -> str:
card = self.repr_fields()
if size == 8:
return self.comment + print_card_8(card)
return self.comment + print_card_16(card)
[docs]
def write_card_16(self, is_double=False):
card = self.repr_fields()
return self.comment + print_card_16(card)