Coverage for decorators.py: 57%
223 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"""
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
14import numpy as np
15import pandas as pd
17try:
18 from pyinstrument import Profiler
20 IS_PYINSTRUMENT = True
21except ImportError:
22 IS_PYINSTRUMENT = False
24from numtools.vgextended import loc_array
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)
39_NOT_FOUND = object()
42CACHES = defaultdict(list)
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 """
51 CACHED_PREFIX = "_cached_"
53 def __init__(self, func):
54 self.func = func
55 self.attrname = None
56 self.__doc__ = func.__doc__
57 self.lock = RLock()
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 )
68 def __get__(self, instance, owner=None):
69 if instance is None:
70 return self
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
104def timeit(method=None, loglevel="info"):
105 if not method:
106 return partial(timeit, loglevel=loglevel)
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
122 return timed
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
141 return wrap
144def dump_args(decorated_function):
145 """
146 Function decorator logging entry + exit and parameters of functions.
148 Entry and exit as logging.info, parameters as logging.DEBUG.
149 """
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))
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)
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)
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))
178 return out
180 return wrapper
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
192# ------------------------------------------------------------------------
193# elements
196def nb_nodes(self):
197 return len(self.gids_header)
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")
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)
227 payload = {"data": np.array(cells).T, "index": np.array(eids), "columns": gidnames}
228 return payload
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 }
250def eid2gids(self, keep_order=False, asdf=False):
251 """return a dictionnary {'eid' <int>: 'gids': <set>} based on MAIN CARD data
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
266 return dict(eid2gids)
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
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
307 return decorator
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
322def axis(cls):
323 CARDS_REGISTER.append(cls.__name__)
324 setattr(cls, "type", AXIS)
325 return cls
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
345 return decorator
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
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