Coverage for decorators.py: 57%

223 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-03-20 20:51 +0100

1""" 

2NastraIO decorators 

3""" 

4import inspect 

5import logging 

6import re 

7import time 

8import warnings 

9from collections import defaultdict 

10from functools import lru_cache, partial, wraps 

11from itertools import chain 

12from threading import RLock 

13 

14import numpy as np 

15import pandas as pd 

16 

17try: 

18 from pyinstrument import Profiler 

19 

20 IS_PYINSTRUMENT = True 

21except ImportError: 

22 IS_PYINSTRUMENT = False 

23 

24from numtools.vgextended import loc_array 

25 

26from nastranio.constants import ( 

27 AXIS, 

28 BOUNDARY, 

29 CARDS_REGISTER, 

30 ELEMENT, 

31 LOADING, 

32 MATERIAL, 

33 PROPERTY, 

34 UNKNOWN, 

35 GMSHElementTypes, 

36 VTKShapes, 

37) 

38 

39_NOT_FOUND = object() 

40 

41 

42CACHES = defaultdict(list) 

43 

44 

45# from nastranio.constants import ELEMENT, MATERIAL, AXIS, UNKNOWN, LOADING, PROPERTY, BOUNDARIES 

46class cached_property: 

47 """backported from python3.8; modify cached name attribute from "<name>" to 

48 "{CACHED_PREFIX}{name}" to easily clear cache from the class itself 

49 """ 

50 

51 CACHED_PREFIX = "_cached_" 

52 

53 def __init__(self, func): 

54 self.func = func 

55 self.attrname = None 

56 self.__doc__ = func.__doc__ 

57 self.lock = RLock() 

58 

59 def __set_name__(self, owner, name): 

60 if self.attrname is None: 

61 self.attrname = f"{self.CACHED_PREFIX}{name}" 

62 elif name != self.attrname: 

63 raise TypeError( 

64 "Cannot assign the same cached_property to two different names " 

65 f"({self.attrname!r} and {name!r})." 

66 ) 

67 

68 def __get__(self, instance, owner=None): 

69 if instance is None: 

70 return self 

71 

72 if self.attrname is None: 

73 raise TypeError( 

74 "Cannot use cached_property instance without calling __set_name__ on it." 

75 ) 

76 try: 

77 cache = instance.__dict__ 

78 except ( 

79 AttributeError 

80 ): # not all objects have __dict__ (e.g. class defines slots) 

81 msg = ( 

82 f"No '__dict__' attribute on {type(instance).__name__!r} " 

83 f"instance to cache {self.attrname!r} property." 

84 ) 

85 raise TypeError(msg) from None 

86 val = cache.get(self.attrname, _NOT_FOUND) 

87 if val is _NOT_FOUND: 

88 with self.lock: 

89 # check if another thread filled cache while we awaited lock 

90 val = cache.get(self.attrname, _NOT_FOUND) 

91 if val is _NOT_FOUND: 

92 val = self.func(instance) 

93 try: 

94 cache[self.attrname] = val 

95 except TypeError: 

96 msg = ( 

97 f"The '__dict__' attribute on {type(instance).__name__!r} instance " 

98 f"does not support item assignment for caching {self.attrname!r} property." 

99 ) 

100 raise TypeError(msg) from None 

101 return val 

102 

103 

104def timeit(method=None, loglevel="info"): 

105 if not method: 

106 return partial(timeit, loglevel=loglevel) 

107 

108 @wraps(method) 

109 def timed(*args, **kw): 

110 ts = time.time() 

111 result = method(*args, **kw) 

112 te = time.time() 

113 delta = te - ts 

114 name = method.__name__ 

115 msg = f'function or method "{name}" took: {delta:.2f} sec.' 

116 if not loglevel: 

117 print(msg) 

118 else: 

119 getattr(logging, loglevel)(msg) 

120 return result 

121 

122 return timed 

123 

124 

125def profile(f): 

126 @wraps(f) 

127 def wrap(*args, **kw): 

128 if not IS_PYINSTRUMENT: 

129 return f(*args, **kw) 

130 profiler = Profiler() 

131 profiler.start() 

132 result = f(*args, **kw) 

133 profiler.stop() 

134 name = f.__name__ 

135 print(80 * "=") 

136 print('Profiling "%s"' % name) 

137 print(profiler.output_text(unicode=True, color=True)) 

138 print(80 * "=") 

139 return result 

140 

141 return wrap 

142 

143 

144def dump_args(decorated_function): 

145 """ 

146 Function decorator logging entry + exit and parameters of functions. 

147 

148 Entry and exit as logging.info, parameters as logging.DEBUG. 

149 """ 

150 

151 @wraps(decorated_function) 

152 def wrapper(*dec_fn_args, **dec_fn_kwargs): 

153 # Log function entry 

154 func_name = decorated_function.__name__ 

155 log = logging.getLogger(func_name) 

156 log.debug("Entering {}()...".format(func_name)) 

157 

158 # get function params (args and kwargs) 

159 argnames = decorated_function.__code__.co_varnames 

160 if "self" in argnames: 

161 ix = argnames.index("self") 

162 clsname = dec_fn_args[ix].__class__.__name__ 

163 log.info("method of %s" % clsname) 

164 

165 args = { 

166 k: v for k, v in dict(zip(argnames, dec_fn_args)).items() if k != "self" 

167 } 

168 params = dict(args=args, kwargs=dec_fn_kwargs) 

169 

170 log.info( 

171 "\t" 

172 + ", ".join(["{}={}".format(str(k), repr(v)) for k, v in params.items()]) 

173 ) 

174 # Execute wrapped (decorated) function: 

175 out = decorated_function(*dec_fn_args, **dec_fn_kwargs) 

176 log.debug("Done running {}()!".format(func_name)) 

177 

178 return out 

179 

180 return wrapper 

181 

182 

183# Cards class decorators 

184# It difines at least (e.g.) unknown: 

185# * cls.type 

186# ======================================================================== 

187def unknown(cls): 

188 setattr(cls, "type", UNKNOWN) 

189 return cls 

190 

191 

192# ------------------------------------------------------------------------ 

193# elements 

194 

195 

196def nb_nodes(self): 

197 return len(self.gids_header) 

198 

199 

200def cells(self, nasgids=None): 

201 """ 

202 return element's cell definition. 

203 """ 

204 gidnames = self.gids_header[:] # ['G1', 'G2', 'G3'] 

205 cells = [] 

206 eids = self.carddata["main"][self.EID_FIELDNAME] 

207 # ======================================================================== 

208 # default nasgids and naseids 

209 # ======================================================================== 

210 if nasgids is None: 

211 nasgids = list(self._eid2gids.values()) 

212 nasgids = np.array(sorted(list(chain(*nasgids)))) 

213 logging.warning("no gids nor eids passed as reference") 

214 

215 # append number of nodes 

216 cells.append(np.array(len(self) * [self.nb_nodes()])) 

217 gidnames.insert(0, "nbpoints") 

218 # ------------------------------------------------------------------------ 

219 # map VTK nodes to NASTRAN nodes 

220 for gname in gidnames: 

221 if gname == "nbpoints": 

222 continue 

223 gids = self.carddata["main"][gname] 

224 gix = loc_array(nasgids, gids) 

225 cells.append(gix) 

226 

227 payload = {"data": np.array(cells).T, "index": np.array(eids), "columns": gidnames} 

228 return payload 

229 

230 

231def to_vtk(self, nasgids, debug=True): 

232 """return data to build VTU unstructured file""" 

233 payload = self.cells(nasgids=nasgids) 

234 # len(payload['index'])== len(self) for most of the cards 

235 # except RBEs 

236 cell_types = len(payload["index"]) * [VTKShapes[self.shape]] 

237 cells = payload["data"].reshape(-1) 

238 eids = payload["index"].tolist() 

239 # offset = np.array([0, 5, ...]) 

240 offset = np.arange(0, len(cells), self.nb_nodes() + 1) 

241 return { 

242 "cell_types": cell_types, 

243 "cells": cells, 

244 "eids": eids, 

245 "card": len(cell_types) * [self.card], 

246 "offset": offset, 

247 } 

248 

249 

250def eid2gids(self, keep_order=False, asdf=False): 

251 """return a dictionnary {'eid' <int>: 'gids': <set>} based on MAIN CARD data 

252 

253 Also eventually call a `eid2gids_complement()` hook for cards having additional data 

254 to provide (eg. RBE2/RBE3). 

255 """ 

256 if keep_order or asdf is True: 

257 eid2gids = self._eid2gids_ordered 

258 else: 

259 eid2gids = self._eid2gids 

260 if asdf: 

261 df = pd.DataFrame(eid2gids).T 

262 df.index.names = ["EID"] 

263 df.columns = self.gids_header 

264 return df 

265 

266 return dict(eid2gids) 

267 

268 

269def update_gid(self, eid, gidno, new_gid): 

270 data = self.carddata["main"] 

271 eid_ix = data["EID"].index(eid) 

272 header = self.gids_header[gidno] # -> eg. "GB" 

273 old_gid = data[header][eid_ix] 

274 data[header][eid_ix] = new_gid 

275 return eid_ix, old_gid 

276 

277 

278def element(dim, shape): 

279 def decorator(cls): 

280 CARDS_REGISTER.append(cls.__name__) 

281 setattr(cls, "type", ELEMENT) 

282 setattr(cls, "dim", dim) 

283 setattr(cls, "shape", shape) 

284 setattr(cls, "gmsh_eltype", GMSHElementTypes.get(shape)) 

285 if not hasattr(cls, "eid2gids"): 

286 setattr(cls, "eid2gids", eid2gids) 

287 if not hasattr(cls, "cells"): 

288 setattr(cls, "cells", cells) 

289 if not hasattr(cls, "nb_nodes"): 

290 setattr(cls, "nb_nodes", nb_nodes) 

291 if not hasattr(cls, "update_gid"): 

292 setattr(cls, "update_gid", update_gid) 

293 if not hasattr(cls, "to_vtk"): 

294 setattr(cls, "to_vtk", to_vtk) 

295 if not hasattr(cls, "GIDS_PATTERN"): 

296 setattr(cls, "GIDS_PATTERN", re.compile(r"^G\d+$")) 

297 if not hasattr(cls, "THK_PATTERN"): 

298 setattr(cls, "THK_PATTERN", re.compile(r"^T\d+$")) 

299 if not hasattr(cls, "EID_FIELDNAME"): 

300 setattr(cls, "EID_FIELDNAME", "EID") 

301 if not hasattr(cls, "PID_FIELDNAME") and "PID" in cls.TABLE: 

302 setattr(cls, "PID_FIELDNAME", "PID") 

303 if not hasattr(cls, "XID_FIELDNAME"): 

304 setattr(cls, "XID_FIELDNAME", getattr(cls, "EID_FIELDNAME")) 

305 return cls 

306 

307 return decorator 

308 

309 

310def fem_property(cls): 

311 CARDS_REGISTER.append(cls.__name__) 

312 setattr(cls, "type", PROPERTY) 

313 if not hasattr(cls, "PID_FIELDNAME") and "PID" in cls.TABLE: 

314 setattr(cls, "PID_FIELDNAME", "PID") 

315 if not hasattr(cls, "MATS_PATTERN"): 

316 setattr(cls, "MATS_PATTERN", re.compile(r"^MID\d*$")) 

317 if not hasattr(cls, "XID_FIELDNAME"): 

318 setattr(cls, "XID_FIELDNAME", getattr(cls, "PID_FIELDNAME")) 

319 return cls 

320 

321 

322def axis(cls): 

323 CARDS_REGISTER.append(cls.__name__) 

324 setattr(cls, "type", AXIS) 

325 return cls 

326 

327 

328def loading_type(typ=None): 

329 def decorator(cls): 

330 CARDS_REGISTER.append(cls.__name__) 

331 setattr(cls, "type", LOADING) 

332 if not hasattr(cls, "XID_FIELDNAME"): 

333 setattr(cls, "XID_FIELDNAME", "SID") 

334 if typ: 

335 if typ == "nodal": 

336 setattr(cls, "LOADING_TYPE", "nodes") 

337 elif typ == "elemental": 

338 setattr(cls, "LOADING_TYPE", "elements") 

339 else: 

340 raise ValueError(f"decorator 'loading_type' does not handle {typ=}") 

341 else: 

342 setattr(cls, "LOADING_TYPE", None) 

343 return cls 

344 

345 return decorator 

346 

347 

348def boundary(cls): 

349 CARDS_REGISTER.append(cls.__name__) 

350 setattr(cls, "type", BOUNDARY) 

351 if not hasattr(cls, "XID_FIELDNAME"): 

352 setattr(cls, "XID_FIELDNAME", "SID") 

353 return cls 

354 

355 

356def material(cls): 

357 CARDS_REGISTER.append(cls.__name__) 

358 setattr(cls, "type", MATERIAL) 

359 if not hasattr(cls, "MID_FIELDNAME"): 

360 setattr(cls, "MID_FIELDNAME", "MID") 

361 if not hasattr(cls, "XID_FIELDNAME"): 

362 setattr(cls, "XID_FIELDNAME", getattr(cls, "MID_FIELDNAME")) 

363 return cls