Source code for polymesh.celldata

from typing import Union
from copy import copy, deepcopy
from functools import partial

import numpy as np
from numpy import ndarray

from dewloosh.core import classproperty
from neumann import atleast2d, atleast3d, repeat
from neumann.linalg.sparse import csr_matrix
from neumann.linalg import ReferenceFrame

from .base import PointDataBase, CellDataBase, PolyDataBase as PolyData
from .akwrap import AwkwardLike
from .utils import (
    avg_cell_data,
    distribute_nodal_data_bulk,
    distribute_nodal_data_sparse,
)


[docs]class CellData(CellDataBase): """ A class to handle data related to the cells of a polygonal mesh. Technically this is a wrapper around an Awkward data object instance. If you are not a developer, you probably don't have to ever create any instance of this class, but since it operates in the background of every polygonal data structure, it is useful to understand how it works. Parameters ---------- activity: numpy.ndarray, Optional 1d boolean array describing the activity of the elements. t or thickness: numpy.ndarray, Optional 1d float array of thicknesses. Only for 2d cells. Default is None. areas: numpy.ndarray, Optional 1d float array of cross sectional areas. Only for 1d cells. Default is None. fields: dict, Optional Every value of this dictionary is added to the dataset. Default is `None`. frames: numpy.ndarray, Optional Coordinate axes representing cartesian coordinate frames. Default is None. topo: numpy.ndarray, Optional 2d integer array representing node indices. Default is None. **kwargs: dict, Optional For every key and value pair where the value is a numpy array with a matching shape (has entries for all cells), the key is considered as a field and the value is added to the database. See Also -------- :class:`awkward.Array` :class:`awkward.Record` """ _attr_map_ = { "nodes": "_nodes", # node indices "frames": "_frames", # coordinate frames "ndf": "_ndf", # nodal distribution factors "id": "_id", # global indices of the cells "areas": "_areas", # areas of 1d cells "t": "_t", # thicknesses for 2d cells "activity": "_activity", # activity of the cells } def __init__( self, *args, pointdata: PointDataBase = None, wrap: AwkwardLike = None, topo: ndarray = None, fields: dict = None, activity: ndarray = None, frames: Union[ndarray, ReferenceFrame] = None, areas: Union[ndarray, float] = None, t: Union[ndarray, float] = None, db: AwkwardLike = None, container: PolyData = None, **kwargs, ): fields = {} if fields is None else fields assert isinstance(fields, dict) if len(fields) > 0: attr_map = self._attr_map_ fields = {attr_map.get(k, k): v for k, v in fields.items()} if db is not None: wrap = db elif wrap is None: nodes = None if len(args) > 0: if isinstance(args[0], ndarray): nodes = args[0] else: nodes = topo if isinstance(activity, ndarray): fields[self._dbkey_activity_] = activity if isinstance(nodes, ndarray): fields[self._dbkey_nodes_] = nodes N = nodes.shape[0] for k, v in kwargs.items(): if isinstance(v, ndarray): if v.shape[0] == N: fields[k] = v if isinstance(areas, np.ndarray): fields[self._dbkey_areas_] = areas super().__init__(*args, wrap=wrap, fields=fields, **kwargs) self._pointdata = pointdata self._container = container if self.db is not None: if frames is not None: if isinstance(frames, (ReferenceFrame, ndarray)): self.frames = frames else: msg = ( "'frames' must be a NumPy array, or a ", "neumann.linalg.ReferenceFrame instance.", ) raise TypeError(msg) if t is not None: self.t = t if areas is not None: self.A = areas def __deepcopy__(self, memo): return self.__copy__(memo) def __copy__(self, memo=None): cls = type(self) copy_function = copy if (memo is None) else partial(deepcopy, memo=memo) is_deep = memo is not None db = copy_function(self.db) pd = self.pointdata pd_copy = None if pd is not None: if is_deep: pd_copy = memo.get(id(pd), None) if pd_copy is None: pd_copy = copy_function(pd) result = cls(db=db, pointdata=pd_copy) if is_deep: memo[id(self)] = result result_dict = result.__dict__ for k, v in self.__dict__.items(): if not k in result_dict: setattr(result, k, copy_function(v)) return result @classproperty def _dbkey_nodes_(cls) -> str: return cls._attr_map_["nodes"] @classproperty def _dbkey_frames_(cls) -> str: return cls._attr_map_["frames"] @classproperty def _dbkey_areas_(cls) -> str: return cls._attr_map_["areas"] @classproperty def _dbkey_thickness_(cls) -> str: return cls._attr_map_["t"] @classproperty def _dbkey_activity_(cls) -> str: return cls._attr_map_["activity"] @classproperty def _dbkey_ndf_(cls) -> str: return cls._attr_map_["ndf"] @classproperty def _dbkey_id_(cls) -> str: return cls._attr_map_["id"] @property def has_id(self) -> ndarray: return self._dbkey_id_ in self._wrapped.fields @property def has_frames(self): return self._dbkey_frames_ in self._wrapped.fields @property def has_thickness(self): return self._dbkey_thickness_ in self._wrapped.fields @property def has_areas(self): return self._dbkey_areas_ in self._wrapped.fields @property def pointdata(self) -> PointDataBase: """ Returns the attached point database. This is what the topology of the cells are referring to. """ return self._pointdata @pointdata.setter def pointdata(self, value: PointDataBase): """ Sets the attached point database. This is what the topology of the cells are referring to. """ if value is not None: assert isinstance(value, PointDataBase) self._pointdata = value @property def pd(self) -> PointDataBase: """ Returns the attached point database. This is what the topology of the cells are referring to. """ return self.pointdata @pd.setter def pd(self, value: PointDataBase): """Sets the attached pointdata.""" self.pointdata = value @property def container(self) -> PolyData: """Returns the container object of the block.""" return self._container @container.setter def container(self, value: PolyData): """Sets the container of the block.""" assert isinstance(value, PolyData) self._container = value
[docs] def root(self) -> PolyData: """ Returns the top level container of the model the block is the part of. """ c = self.container return None if c is None else c.root()
[docs] def source(self) -> PolyData: """ Retruns the source of the cells. This is the PolyData block that stores the PointData object the topology of the cells are referring to. """ c = self.container return None if c is None else c.source()
def __getattr__(self, attr): """ Modified for being able to fetch data from pointcloud. """ if attr in self.__dict__: return getattr(self, attr) try: return getattr(self._wrapped, attr) except AttributeError: try: if self.pointdata is not None: if attr in self.pointdata.fields: data = self.pointdata[attr].to_numpy() topo = self.nodes return avg_cell_data(data, topo) except: pass name = self.__class__.__name__ raise AttributeError(f"'{name}' object has no attribute called {attr}") except Exception: name = self.__class__.__name__ raise AttributeError(f"'{name}' object has no attribute called {attr}")
[docs] def set_nodal_distribution_factors(self, factors: ndarray, key: str = None): """ Sets nodal distribution factors. Parameters ---------- factors: numpy.ndarray A 3d float array. The length of the array must equal the number pf cells in the block. key: str, Optional A key used to store the values in the database. This makes you able to use more nodal distribution strategies in one model. If not specified, a default key is used. """ if key is None: key = self.__class__._attr_map_[self._dbkey_ndf_] if len(factors) != len(self._wrapped): self._wrapped[key] = factors[self._wrapped.id] else: self._wrapped[key] = factors
[docs] def pull( self, data: Union[str, ndarray], ndf: Union[ndarray, csr_matrix] = None ) -> ndarray: """ Pulls data from the attached pointdata. The pulled data is either copied or distributed according to a measure. Parameters ---------- data: str or numpy.ndarray Either a field key to identify data in the database of the attached PointData, or a NumPy array. See Also -------- :func:`~polymesh.utils.utils.distribute_nodal_data_bulk` :func:`~polymesh.utils.utils.distribute_nodal_data_sparse` """ if isinstance(data, str): pd = self.source().pd nodal_data = pd[data].to_numpy() else: assert isinstance( data, ndarray ), "'data' must be a string or a NumPy array." nodal_data = data topo = self.nodes if ndf is None: ndf = np.ones_like(topo).astype(float) if len(nodal_data.shape) == 1: nodal_data = atleast2d(nodal_data, back=True) if isinstance(ndf, ndarray): d = distribute_nodal_data_bulk(nodal_data, topo, ndf) else: d = distribute_nodal_data_sparse(nodal_data, topo, self.id, ndf) # nE, nNE, nDATA return d
@property def fields(self): """Returns the fields in the database object.""" return self._wrapped.fields @property def nodes(self) -> ndarray: """Returns the topology of the cells.""" return self._wrapped[self._dbkey_nodes_].to_numpy() @nodes.setter def nodes(self, value: ndarray): """ Sets the topology of the cells. Parameters ---------- value: numpy.ndarray A 2d integer array. """ assert isinstance(value, ndarray) self._wrapped[self._dbkey_nodes_] = value @property def frames(self) -> ndarray: """Returns local coordinate frames of the cells.""" return self._wrapped[self._dbkey_frames_].to_numpy() @frames.setter def frames(self, value: ndarray): """ Sets local coordinate frames of the cells. Parameters ---------- value: numpy.ndarray A 3d float array. """ if isinstance(value, ReferenceFrame): frames = value.show() else: assert isinstance(value, ndarray) frames = value frames = atleast3d(frames) if len(frames) == 1: frames = repeat(frames[0], len(self._wrapped)) else: assert len(frames) == len(self._wrapped) self._wrapped[self._dbkey_frames_] = frames @property def t(self): """Returns the thicknesses of the cells.""" return self._wrapped[self._dbkey_thickness_].to_numpy() @t.setter def t(self, value: Union[float, int, ndarray]): """Returns the thicknesses of the cells.""" if isinstance(value, (int, float)): value = np.full(len(self), value) self._wrapped[self._dbkey_thickness_] = value @property def A(self): """Returns the thicknesses of the cells.""" return self._wrapped[self._dbkey_areas_].to_numpy() @A.setter def A(self, value: Union[float, int, ndarray]): """Returns the thicknesses of the cells.""" if isinstance(value, (int, float)): value = np.full(len(self), value) self._wrapped[self._dbkey_areas_] = value @property def id(self) -> ndarray: """Returns global indices of the cells.""" return self._wrapped[self._dbkey_id_].to_numpy() @id.setter def id(self, value: ndarray): """ Sets global indices of the cells. Parameters ---------- value: numpy.ndarray An 1d integer array. """ assert isinstance(value, ndarray) self._wrapped[self._dbkey_id_] = value @property def activity(self) -> ndarray: """Returns a 1d boolean array of cell activity.""" return self._wrapped[self._dbkey_activity_].to_numpy() @activity.setter def activity(self, value: ndarray): """ Sets cell activity with a 1d boolean array. Parameters ---------- value: numpy.ndarray An 1d bool array. """ if isinstance(value, bool): value = np.full(len(self), value, dtype=bool) self._wrapped[self._dbkey_activity_] = value