Coverage for cards/elements.py: 38%

1"""

2 NASTRAN Elements Cards collection

3"""

4 import logging

5 import re

6 from collections import defaultdict

7 from itertools import chain

8 from pprint import pprint

9

10 import numpy as np

11 from numtools.vgextended import loc_array

12

13 from nastranio.cardslib import ComplexCard, SimpleCard, SimpleCyclingCard

14 from nastranio.constants import VTKShapes, shapes

15 from nastranio.decorators import element

16

17

18@element("0d", shapes.VERTICE)

19 class CONM2(SimpleCard):

20 """

21 Concentrated Mass Element Connection, Rigid Body Form

22 Defines a concentrated mass at a grid point.

23

24

25 ref: NX Nastran 12 Quick Reference Guide 12-4 (p.1488)

26 """

27

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}

35

36 GIDS_PATTERN = re.compile("^G$")

37

38

39@element("1d", shapes.LINE)

40 class CROD(SimpleCard):

41 """

42 Rod Element Connection.

43 Defines a tension-compression-torsion element.

44

45

46 ref: NX Nastran 12 Quick Reference Guide 12-120 (p.1604)

47 """

48

49 TABLE = """

50 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

51 |------+-----+-----+----+----+---+----+---+---+----|

52 | CROD | EID | PID | G1 | G2 | | SM | | | |

53 """

54

55 DEFAULTS = {"C": 0, "NSM": 0}

56

57

58# ============================================================================

59# RBE3/RBE2

60# ============================================================================

61

62

63 def _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])

101

102 payload = {

103 "data": np.array(cells).T,

104 "index": np.array(faked_eids),

105 "columns": gidnames,

106 }

107 return payload

108

109

110@element("1d", shapes.MPC)

111 class RBE3(ComplexCard):

112 """

113 Interpolation Constraint Element.

114

115 Defines the motion at a reference grid point as the weighted average

116 of the motions at a set of other grid points.

117

118 ref: NX Nastran 12 Quick Reference Guide 17-48 (p.2522)

119 """

120

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 """

131

132 GIDS_PATTERN = re.compile("^REFGRID$")

133

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"] = []

143

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

150

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

157

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

167

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

284

285 def cells(self, nasgids=None):

286 """

287 return element's cell definition.

288 """

289 return _cells(self, nasgids)

290

291

292@element("1d", shapes.MPC)

293 class 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.

299

300 ref: NX Nastran 12 Quick Reference Guide 17-45 (p.2519)

301

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}]]

325

326 """

327

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$")

337

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

351

352 def nb_nodes(self):

353 return 2

354

355 def cells(self, nasgids=None):

356 """

357 return element's cell definition.

358 """

359 return _cells(self, nasgids)

360

361

362@element("2d", shapes.TRIA)

363 class CTRIA3(SimpleCard):

364 """

365 Triangular Plate Element Connection

366 Defines an isoparametric membrane-bending or plane strain triangular plate element.

367

368 ref: NX Nastran 12 Quick Reference Guide 12-147 (p.1631)

369 """

370

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"}}

386

387

388@element("2d", shapes.QUAD)

389 class CQUAD4(SimpleCard):

390 """

391 Quadrilateral Plate Element Connection

392

393 Defines an isoparametric membrane-bending or plane strain quadrilateral plate element.

394

395 ref: NX Nastran 12 Quick Reference Guide 12-80 (p.1564)

396

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 """

419

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"}}

436

437

438@element("2d", shapes.QUAD)

439 class CSHEAR(SimpleCard):

440 """

441 Shear Panel Element Connection

442

443 Defines a Shear Panel element

444

445 ref: NX Nastran 12 Quick Reference Guide 12-126 (p.1610)

446

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 """

459

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

467

468

469@element("1d", shapes.LINE)

470 class 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.

475

476 ref: NX Nastran 12 Quick Reference Guide 11-39 (p.1399)

477 """

478

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"}}

495

496

497@element("1d", shapes.LINE)

498 class CBAR(SimpleCard):

499 """

500 Simple Beam Element Connection

501 Defines a simple beam element.

502

503 ref: NX Nastran 12 Quick Reference Guide 11-20 (p.1380)

504 """

505

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 }

524

525

526@element("1d", shapes.LINE)

527 class CELAS2(SimpleCard):

528 """

529 Scalar Spring Property and Connection

530 Defines a scalar spring element without reference to a property entry.

531

532 ref: NX Nastran 12 Quick Reference Guide 11-77 (p.1437)

533 """

534

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 """

540

541

542 if __name__ == "__main__":

543 import doctest

544

545 doctest.testmod(optionflags=doctest.ELLIPSIS)