Coverage for cards/elements.py: 38%

205 statements  

« 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 

9 

10import numpy as np 

11from numtools.vgextended import loc_array 

12 

13from nastranio.cardslib import ComplexCard, SimpleCard, SimpleCyclingCard 

14from nastranio.constants import VTKShapes, shapes 

15from nastranio.decorators import element 

16 

17 

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. 

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) 

40class 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 

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

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) 

111class 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) 

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. 

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) 

363class 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) 

389class 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) 

439class 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) 

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. 

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) 

498class 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) 

527class 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 

542if __name__ == "__main__": 

543 import doctest 

544 

545 doctest.testmod(optionflags=doctest.ELLIPSIS)