Coverage for decorators.py: 57%

1"""

2 NastraIO decorators

3"""

4 import inspect

5 import logging

6 import re

7 import time

8 import warnings

9 from collections import defaultdict

10 from functools import lru_cache, partial, wraps

11 from itertools import chain

12 from threading import RLock

13

14 import numpy as np

15 import pandas as pd

16

17 try:

18 from pyinstrument import Profiler

19

20 IS_PYINSTRUMENT = True

21 except ImportError:

22 IS_PYINSTRUMENT = False

23

24 from numtools.vgextended import loc_array

25

26 from 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

42 CACHES = defaultdict(list)

43

44

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

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

104 def 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

125 def 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

144 def 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# ========================================================================

187 def unknown(cls):

188 setattr(cls, "type", UNKNOWN)

189 return cls

190

191

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

193# elements

194

195

196 def nb_nodes(self):

197 return len(self.gids_header)

198

199

200 def 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

231 def 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

250 def 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

269 def 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

278 def 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

310 def 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

322 def axis(cls):

323 CARDS_REGISTER.append(cls.__name__)

324 setattr(cls, "type", AXIS)

325 return cls

326

327

328 def 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

348 def 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

356 def 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