Coverage for cards/elements.py: 38%
205 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-20 20:51 +0100
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-20 20:51 +0100
1"""
2NASTRAN Elements Cards collection
3"""
4import logging
5import re
6from collections import defaultdict
7from itertools import chain
8from pprint import pprint
10import numpy as np
11from numtools.vgextended import loc_array
13from nastranio.cardslib import ComplexCard, SimpleCard, SimpleCyclingCard
14from nastranio.constants import VTKShapes, shapes
15from nastranio.decorators import element
18@element("0d", shapes.VERTICE)
19class CONM2(SimpleCard):
20 """
21 Concentrated Mass Element Connection, Rigid Body Form
22 Defines a concentrated mass at a grid point.
25 ref: NX Nastran 12 Quick Reference Guide 12-4 (p.1488)
26 """
28 TABLE = """
29 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
30 |-------+-----+-----+-----+-----+-----+-----+----+---+----|
31 | CONM2 | EID | G | CID | M | X1 | X2 | X3 | | |
32 | | I11 | I21 | I22 | I31 | I32 | I33 | | | |
33 """
34 DEFAULTS = {"CID": 0}
36 GIDS_PATTERN = re.compile("^G$")
39@element("1d", shapes.LINE)
40class CROD(SimpleCard):
41 """
42 Rod Element Connection.
43 Defines a tension-compression-torsion element.
46 ref: NX Nastran 12 Quick Reference Guide 12-120 (p.1604)
47 """
49 TABLE = """
50 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
51 |------+-----+-----+----+----+---+----+---+---+----|
52 | CROD | EID | PID | G1 | G2 | | SM | | | |
53 """
55 DEFAULTS = {"C": 0, "NSM": 0}
58# ============================================================================
59# RBE3/RBE2
60# ============================================================================
63def _cells(cls, nasgids=None):
64 """
65 return RBE2 and RBE3 element's cell definition.
66 """
67 gidnames = ["nbpoints", cls.gids_header[0], "G2"] # ['GN', 'G2']
68 cells = [[], [], []]
69 eids = cls.carddata["main"][cls.EID_FIELDNAME]
70 eid2gids = cls._eid2gids
71 # ========================================================================
72 # default nasgids and naseids
73 # ========================================================================
74 if nasgids is None:
75 logging.warning("no gids nor eids passed as reference")
76 nasgids = list(cls._eid2gids.values())
77 nasgids = np.array(sorted(list(chain(*nasgids))))
78 naseids = np.array(sorted(list(cls.eid2gids().keys())))
79 # -------------------------------------------------------------------------
80 # for each element in cards,
81 # create a bunch of dummy 2D element per leg, iterating over RBE nodes
82 # calculate offset such as we ensure we do not point on existing EID
83 faked_eids = []
84 # print(cls.eid2gids())
85 for eid in eids:
86 master_gid = cls.array[np.where(cls.array["EID"] == eid)][cls.gids_header[0]][0]
87 slave_gids = eid2gids[eid].copy()
88 slave_gids.remove(master_gid)
89 nb_fake_elements = len(slave_gids) - 1
90 # append number of nodes
91 cells[0] += (1 + nb_fake_elements) * [2]
92 # ------------------------------------------------------------------------
93 # map VTK nodes to NASTRAN nodes
94 for slave_gid in slave_gids:
95 faked_eids.append(eid)
96 gids = [master_gid, slave_gid]
97 gids = np.array(gids)
98 gix = loc_array(nasgids, gids)
99 cells[1].append(gix[0])
100 cells[2].append(gix[1])
102 payload = {
103 "data": np.array(cells).T,
104 "index": np.array(faked_eids),
105 "columns": gidnames,
106 }
107 return payload
110@element("1d", shapes.MPC)
111class RBE3(ComplexCard):
112 """
113 Interpolation Constraint Element.
115 Defines the motion at a reference grid point as the weighted average
116 of the motions at a set of other grid points.
118 ref: NX Nastran 12 Quick Reference Guide 17-48 (p.2522)
119 """
121 TABLE = """
122 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
123 |------+---------+-------+---------+------+------+--------+------+--------+----|
124 | RBE3 | EID | | REFGRID | REFC | WT1 | C1 | G1,1 | G1,2 | |
125 | | -etc.- | WT2 | C2 | G2,1 | G2,2 | -etc.- | WT3 | C3 | |
126 | | G3,1 | G3,2 | -etc.- | WT4 | C4 | G4,1 | G4,2 | -etc.- | |
127 | | "UM" | GM1 | CM1 | GM2 | CM2 | GM3 | CM3 | | |
128 | | | GM4 | CM4 | GM5 | CM5 | -etc.- | | | |
129 | | "ALPHA" | ALPHA | | | | | | | |
130 """
132 GIDS_PATTERN = re.compile("^REFGRID$")
134 def __init__(self, name=None, data=None):
135 super().__init__(name, data=data)
136 # change self.fields and self.repeated
137 self.fields = {fid: fname for fid, fname in self.fields.items() if fid <= 5}
138 self.repeated = None
139 if "rbe3_wcg" not in self.carddata:
140 self.carddata["rbe3_wcg"] = []
141 if "rbe3_um" not in self.carddata:
142 self.carddata["rbe3_um"] = []
144 def eid2gids_complement(self, eid=None, ix=None):
145 """retrieve nodes stored in rbe3_wcg data"""
146 gids = set()
147 for data in self.carddata["rbe3_wcg"][ix]:
148 gids |= data["Gi"]
149 return gids
151 def append_checkin(self, fields):
152 """hook triggered right before `append`"""
153 # fixed fields go to (including) field#5 "REFC"
154 fixed_fields = fields[:4] # to be parsed as usual
155 self._remaining_fields = fields[4:]
156 return fixed_fields
158 def append_checkout(self, fields):
159 """hook triggered right after `append`"""
160 WCG_sequences = []
161 UM_sequences = []
162 buffer = None
163 # we begin with WCG sequence
164 FLAG = "WCG"
165 _sequence = WCG_sequences
166 fields = ["_", "_", "_", "_", "_", "_"] + self._remaining_fields
168 for field_nb, field in enumerate(fields):
169 try:
170 next_field = fields[field_nb + 1]
171 except:
172 next_field = "EoF"
173 if field == "_":
174 continue
175 # ----------------------------------------------------------------
176 # skip continuation fields (fields #X0 or fields#X1
177 if field_nb % 10 == 0 or (field_nb - 1) % 10 == 0:
178 continue
179 # ================================================================
180 # WCG sequence
181 # ================================================================
182 if FLAG == "WCG":
183 # ------------------------------------------------------------
184 # opening a new sub-sequence, and closing the previous one
185 # when we meet a float
186 if isinstance(field, float):
187 if buffer:
188 # eventually close previous sub-sequence
189 _sequence.append(buffer)
190 buffer = []
191 # ------------------------------------------------------------
192 # closing the WCG sequence:
193 # * first None field parsed (end of WCG, but not end of fields)
194 # * meeting "UM" or "ALPHA" flag
195 # * when reaching end of fields (next_field=='EoF')
196 if field in ("UM", "ALPHA", None) or next_field == "EoF":
197 if next_field == "EoF":
198 # current field is still good
199 buffer.append(field)
200 # end of WCG current sequence AND also of WCG sequences
201 if buffer:
202 _sequence.append(buffer)
203 buffer = []
204 if field == "UM":
205 FLAG = "UM"
206 _sequence = UM_sequences
207 if field == "ALPHA":
208 FLAG = "ALPHA"
209 continue
210 buffer.append(field)
211 continue
212 # ================================================================
213 # UM sequence
214 # ================================================================
215 elif FLAG == "UM":
216 # ------------------------------------------------------------
217 # opening a new UM subsequence when buffer's length is two
218 if len(buffer) == 2:
219 _sequence.append(buffer)
220 buffer = []
221 # field #x9, #x2 are empty, but still in UM sequence
222 if field is None:
223 # we theoritically fushed the buffer above, so...
224 assert len(buffer) == 0
225 continue
226 # ------------------------------------------------------------
227 # closing a UM sequence:
228 # * meeting "ALPHA" flag
229 # * when reaching end of fields (next_field=='EoF')
230 if next_field == "EoF":
231 if buffer:
232 if len(buffer) not in (0, 2):
233 assert len(buffer) == 1
234 buffer.append(field)
235 _sequence.append(buffer)
236 buffer = []
237 IS_ALPHA_DEFAULT = True
238 alpha = 0.0
239 break
240 if field == "ALPHA":
241 # end of WCG current sequence AND also of WCG sequences
242 if buffer:
243 if len(buffer) != 2:
244 __import__("pdb").set_trace()
245 _sequence.append(buffer)
246 buffer = []
247 if field == "ALPHA":
248 FLAG = "ALPHA"
249 continue
250 buffer.append(field)
251 continue
252 elif FLAG == "ALPHA":
253 IS_ALPHA_DEFAULT = False
254 alpha = field
255 break
256 else:
257 if buffer:
258 _sequence.append(buffer)
259 # default value
260 IS_ALPHA_DEFAULT = True
261 alpha = 0.0
262 self.carddata["main"]["ALPHA"].append(alpha)
263 # --------------------------------------------------------------------
264 # process WCG sequence
265 _all_wcg = []
266 for weight, dof, *grids in WCG_sequences:
267 _all_wcg.append({"W": weight, "C": dof, "Gi": set(grids)})
268 if _all_wcg in self.carddata["rbe3_wcg"]:
269 ix = self.carddata["rbe3_wcg"].index(_all_wcg)
270 else:
271 ix = len(self.carddata["rbe3_wcg"])
272 self.carddata["rbe3_wcg"].append(_all_wcg)
273 self.carddata["main"]["rbe3_wcgID"].append(ix)
274 # --------------------------------------------------------------------
275 # process UM sequence
276 # print(UM_sequences)
277 if UM_sequences in self.carddata["rbe3_um"]:
278 ix = self.carddata["rbe3_um"].index(UM_sequences)
279 else:
280 ix = len(self.carddata["rbe3_um"])
281 self.carddata["rbe3_um"].append(UM_sequences)
282 self.carddata["main"]["rbe3_umID"].append(ix)
283 return fields
285 def cells(self, nasgids=None):
286 """
287 return element's cell definition.
288 """
289 return _cells(self, nasgids)
292@element("1d", shapes.MPC)
293class RBE2(SimpleCyclingCard):
294 """
295 Rigid Body Element, Form 2
296 Defines a rigid body with independent degrees-of-freedom that are specified at a
297 single grid point and with dependent degrees-of-freedom that are specified at an
298 arbitrary number of grid points.
300 ref: NX Nastran 12 Quick Reference Guide 17-45 (p.2519)
302 >>> pb = RBE2()
303 >>> pb.append_fields_list([ 9, 8, 12, 10, 12, 14, 15, 16, '+',
304 ... '+', 20])
305 >>> pb.append_fields_list([ 10, 9, 12345, 10, 12, 14, 15, 16, '+',
306 ... '+', 100, 101, 5.])
307 >>> pb.append_fields_list([ 11, 109, 1245, 10, 12, 14, 15, 16, '+',
308 ... '+', 100, 101]) # no alpha specified. Expect default 0.0
309 >>> pprint(pb.carddata['main']) # doctest: +NORMALIZE_WHITESPACE
310 defaultdict(<class 'list'>,
311 {'ALPHA': [0.0, 5.0, 0.0],
312 'CM': [12, 12345, 1245],
313 'EID': [9, 10, 11],
314 'GN': [8, 9, 109],
315 'rbe2_gidsetID': [0, 1, 1]})
316 >>> pprint(pb.export_data()['rbe2_gidset']) # doctest: +NORMALIZE_WHITESPACE
317 [[{'GM': 10}, {'GM': 12}, {'GM': 14}, {'GM': 15}, {'GM': 16}, {'GM': 20}],
318 [{'GM': 10},
319 {'GM': 12},
320 {'GM': 14},
321 {'GM': 15},
322 {'GM': 16},
323 {'GM': 100},
324 {'GM': 101}]]
326 """
328 REPEATED_DATA_NAME = "gidset"
329 TABLE = """
330| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
331|------+-----+-----+-----+--------+-------+-----+-----+-----+----|
332| RBE2 | EID | GN | CM | GM1 | GM2 | GM3 | GM4 | GM5 | |
333| | GM6 | GM7 | GM8 | -etc.- | ALPHA | | | | |
334 """
335 DEFAULTS = {"ALPHA": 0.0}
336 GIDS_PATTERN = re.compile("^GN$")
338 def eid2gids_complement(self, eid=None, ix=None):
339 # also include linked nodes
340 try:
341 compdata = self.carddata[self.REPEATED_DATA_NAME][ix]
342 except IndexError as exc:
343 logging.error(f"{ix=} out of bounds for {self.REPEATED_DATA_NAME=}")
344 raise
345 compdata = set([gid for subd in compdata for _, gid in subd.items()])
346 return compdata
347 # for subset_id, nodes in enumerate(subset):
348 # gidset_nodes[gidset].add(nodes['GM'])
349 # eid2gidset = dict(zip(self.carddata['main'][self.EID_FIELDNAME], self.carddata['main']['rbe2_gidsetID']))
350 # return eid2gidset
352 def nb_nodes(self):
353 return 2
355 def cells(self, nasgids=None):
356 """
357 return element's cell definition.
358 """
359 return _cells(self, nasgids)
362@element("2d", shapes.TRIA)
363class CTRIA3(SimpleCard):
364 """
365 Triangular Plate Element Connection
366 Defines an isoparametric membrane-bending or plane strain triangular plate element.
368 ref: NX Nastran 12 Quick Reference Guide 12-147 (p.1631)
369 """
371 TABLE = """
372 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
373 |--------+-----+-------+----+----+----+------------+-------+---+----|
374 | CTRIA3 | EID | PID | G1 | G2 | G3 | THETA/MCID | ZOFFS | | |
375 | | | TFLAG | T1 | T2 | T3 | | | | |
376 """
377 DEFAULTS = {
378 "THETA/MCID": 0.0,
379 "ZOFFS": None,
380 "TFLAG": None,
381 "T1": None,
382 "T2": None,
383 "T3": None,
384 }
385 MULT_TYPES_FIELDS = {"THETA/MCID": {float: "THETA", int: "MCID"}}
388@element("2d", shapes.QUAD)
389class CQUAD4(SimpleCard):
390 """
391 Quadrilateral Plate Element Connection
393 Defines an isoparametric membrane-bending or plane strain quadrilateral plate element.
395 ref: NX Nastran 12 Quick Reference Guide 12-80 (p.1564)
397 >>> cq4 = CQUAD4()
398 >>> bulk = ["CQUAD4 1 1 5 12 15 13",
399 ... "CQUAD4 2 4 4524 4537 4569 4547 4",
400 ... "CQUAD4 3 3 1 2 3 4 4."]
401 >>> for b in bulk: cq4.parse(b)
402 >>> pprint(cq4.export_data()['main']) # doctest: +NORMALIZE_WHITESPACE
403 {'EID': [1, 2, 3],
404 'G1': [5, 4524, 1],
405 'G2': [12, 4537, 2],
406 'G3': [15, 4569, 3],
407 'G4': [13, 4547, 4],
408 'MCID': [None, 4, None],
409 'PID': [1, 4, 3],
410 'T1': [None, None, None],
411 'T2': [None, None, None],
412 'T3': [None, None, None],
413 'T4': [None, None, None],
414 'TFLAG': [None, None, None],
415 'THETA': [0.0, None, 4.0],
416 'THETA/MCID': [0.0, 4, 4.0],
417 'ZOFFS': [None, None, None]}
418 """
420 TABLE = """
421| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
422|--------+-----+-------+----+----+----+----+------------+-------+----|
423| CQUAD4 | EID | PID | G1 | G2 | G3 | G4 | THETA/MCID | ZOFFS | |
424| | | TFLAG | T1 | T2 | T3 | T4 | | | |
425"""
426 DEFAULTS = {
427 "THETA/MCID": 0.0,
428 "ZOFFS": None,
429 "TFLAG": None,
430 "T1": None,
431 "T2": None,
432 "T3": None,
433 "T4": None,
434 }
435 MULT_TYPES_FIELDS = {"THETA/MCID": {float: "THETA", int: "MCID"}}
438@element("2d", shapes.QUAD)
439class CSHEAR(SimpleCard):
440 """
441 Shear Panel Element Connection
443 Defines a Shear Panel element
445 ref: NX Nastran 12 Quick Reference Guide 12-126 (p.1610)
447 >>> cshear = CSHEAR()
448 >>> bulk = ["CSHEAR 1 1 5 12 15 13",
449 ... "CSHEAR 2 4 4524 4537 4569 4547"]
450 >>> for b in bulk: cshear.parse(b)
451 >>> pprint(cshear.export_data()['main']) # doctest: +NORMALIZE_WHITESPACE
452 {'EID': [1, 2],
453 'G1': [5, 4524],
454 'G2': [12, 4537],
455 'G3': [15, 4569],
456 'G4': [13, 4547],
457 'PID': [1, 4]}
458 """
460 TABLE = """
461| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
462|--------+-----+-------+----+----+----+----+----+----+----|
463| CSHEAR | EID | PID | G1 | G2 | G3 | G4 | | | |
464"""
465 DEFAULTS = {}
466 THK_PATTERN = None
469@element("1d", shapes.LINE)
470class CBUSH(SimpleCard):
471 """
472 Generalized Spring-and-Damper Connection
473 Defines a generalized spring-and-damper structural element that may be nonlinear
474 or frequency dependent.
476 ref: NX Nastran 12 Quick Reference Guide 11-39 (p.1399)
477 """
479 TABLE = """
480 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
481 |-------+-----+------+----+----+-------+----+----+-----+----|
482 | CBUSH | EID | PID | GA | GB | GO/X1 | X2 | X3 | CID | |
483 | | S | OCID | S1 | S2 | S3 | | | | |
484 """
485 GIDS_PATTERN = re.compile("^G[A|B]$")
486 DEFAULTS = {
487 "S": None,
488 "OCID": None,
489 "S1": None,
490 "S2": None,
491 "S3": None,
492 "CID": None,
493 }
494 MULT_TYPES_FIELDS = {"GO/X1": {float: "X1", int: "GO"}}
497@element("1d", shapes.LINE)
498class CBAR(SimpleCard):
499 """
500 Simple Beam Element Connection
501 Defines a simple beam element.
503 ref: NX Nastran 12 Quick Reference Guide 11-20 (p.1380)
504 """
506 TABLE = """
507 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
508 |------+-----+-----+-----+-----+-------+-----+-----+-----+----|
509 | CBAR | EID | PID | GA | GB | GO/X1 | X2 | X3 | | |
510 | | PA | PB | W1A | W2A | W3A | W1B | W2B | W3B | |
511 """
512 MULT_TYPES_FIELDS = {"GO/X1": {float: "X1", int: "GO"}}
513 GIDS_PATTERN = re.compile("^G[A|B]$")
514 DEFAULTS = {
515 "W1A": None,
516 "W2A": None,
517 "W3A": None,
518 "W1B": None,
519 "W2B": None,
520 "W3B": None,
521 "PA": None,
522 "PB": None,
523 }
526@element("1d", shapes.LINE)
527class CELAS2(SimpleCard):
528 """
529 Scalar Spring Property and Connection
530 Defines a scalar spring element without reference to a property entry.
532 ref: NX Nastran 12 Quick Reference Guide 11-77 (p.1437)
533 """
535 TABLE = """
536 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
537 |--------+-----+---+----+----+----+----+----+---+----|
538 | CELAS2 | EID | K | G1 | C1 | G2 | C2 | GE | S | |
539 """
542if __name__ == "__main__":
543 import doctest
545 doctest.testmod(optionflags=doctest.ELLIPSIS)