Source code for mymesh.mesh

# -*- coding: utf-8 -*-
# Created on Wed Sep  1 16:20:47 2021
# @author: toj
"""

mesh class
==========


"""

from . import utils, implicit, improvement, contour, converter, quality, rays, register, curvature, visualize, check_numba, try_njit
from sys import getsizeof
import scipy
import numpy as np
import os, copy, warnings, pickle, json, itertools

[docs] class mesh: """ The :class:`mesh` class stores the nodes (``NodeCoords``) and elements (or node connectivity, ``NodeConn``) as its primary attributes. Parameters ---------- NodeCoords : array_like, optional Node coordinates, by default None. Node coordinates should be formatted as an nx3 or nx2 array of coordinates. NodeConn : list, array_like, optional Node connectivity of elements, by default None. Node connectivity is formatted as a 2D array or list of lists where each row (or inner list) is contains the node IDs of the nodes that make up the element. NodeConn can contain a uniform element type or a mix of different element types. Type : str or None, optional Mesh type, 'surf' for surface, 'vol' for volume, 'line' for line. If not provided, it will be determined automatically (:meth:`mesh.identify_type`), by default None verbose : bool, optional If true, some operations will print activity or other information, by default True Attributes ---------- NodeCoords : array_like, optional Node coordinates, by default None. Node coordinates should be formatted as an nx3 or nx2 array of coordinates. NodeConn : list, array_like, optional Node connectivity of elements, by default None. Node connectivity is formatted as a 2D array or list of lists where each row (or inner list) is contains the node IDs of the nodes that make up the element. NodeConn can contain a uniform element type or a mix of different element types. Type : str or None, optional Mesh type, 'surf' for surface, 'vol' for volume, 'line' for line. If not provided, it will be determined automatically (:meth:`mesh.identify_type`), by default None verbose : bool Verbosity mode of the mesh. If True, some operations will print activity or other information, by default True NodeData : dict Node data dictionary for storing scalar or vector data associated with each node in the mesh. Each entry should be an array_like with the same length as the number of nodes in the mesh. When using :meth:`mesh.write`, this data will be transferred to the written file if supported by that file type. ElemData : dict Element data dictionary for storing scalar or vector data associated with each element in the mesh. Each entry should be an array_like with the same length as the number of elements in the mesh. When using :meth:`mesh.write`, this data will be transferred to the written file if supported by that file type. NodeSets : dict Dictionary used for creating named sets of nodes. Each entry should be a set (or array_like) of node numbers. ElemSets : dict Dictionary used for creating named sets of elements. Each entry should be a set (or array_like) of element numbers. FaceSets : dict Dictionary used for creating named sets of faces. Each entry should be a set (or array_like) of face numbers. EdgeSets : dict Dictionary used for creating named sets of edges. Each entry should be a set (or array_like) of edge numbers. """ def __init__(self, NodeCoords=None, NodeConn=None, Type=None, verbose=True): # Primary attributes if NodeCoords is None: self.NodeCoords = np.empty((0,3)) else: if not isinstance(NodeCoords, (list,np.ndarray,tuple)): raise ValueError(f'NodeCoords must be a list, np.ndarray or tuple, not {str(type(NodeCoords)):s}.') self.NodeCoords = NodeCoords if NodeConn is None: self.NodeConn = [] else: if not isinstance(NodeConn, (list,np.ndarray,tuple)): raise ValueError(f'NodeConn must be a list, np.ndarray or tuple, not {str(type(NodeConn)):s}.') self.NodeConn = NodeConn if Type is None: self.Type = self.identify_type() else: self.Type = Type # Mesh settings self.verbose = verbose # Properties: self._ElemType = [] self._MeshNodes = None self._Surface = None self._SurfConn = None self._SurfNodes = None self._Boundary = None self._BoundaryConn = None self._BoundaryNodes = None self._NodeNeighbors = [] self._ElemNeighbors = [] self._ElemConn = [] self._SurfNodeNeighbors = [] self._SurfElemConn = [] self._ElemNormals = [] self._NodeNormals = [] self._Centroids = np.empty((0,3)) self._Faces = [] self._FaceConn = [] self._FaceElemConn = [] # For each face, gives the indices of connected elements (nan -> surface) self._Edges = [] self._EdgeConn = [] self._EdgeElemConn = [] self._NodeNormalsMethod = 'Angle' self._bounds = None self._aabb = None self._mvbb = None # Sets: self.NodeSets = {} self.ElemSets = {} self.EdgeSets = {} self.FaceSets = {} # Data: self.NodeData = {} self.ElemData = {} self._printlevel = 0 def __sizeof__(self): size = 0 # Base attributes size += getsizeof(self.NodeCoords) size += getsizeof(self.NodeConn) size += getsizeof(self.Type) # Property attributes size += getsizeof(self._Faces) size += getsizeof(self._FaceConn) size += getsizeof(self._FaceElemConn) size += getsizeof(self._Edges) size += getsizeof(self._EdgeConn) size += getsizeof(self._EdgeElemConn) size += getsizeof(self._SurfConn) size += getsizeof(self._NodeNeighbors) size += getsizeof(self._ElemConn) size += getsizeof(self._SurfNodeNeighbors) size += getsizeof(self._SurfElemConn) size += getsizeof(self._ElemNormals) size += getsizeof(self._NodeNormals) size += getsizeof(self._Centroids) # Sets & Data size += getsizeof(self.NodeSets) size += getsizeof(self.EdgeSets) size += getsizeof(self.FaceSets) size += getsizeof(self.ElemSets) size += getsizeof(self.NodeData) size += getsizeof(self.ElemData) return size def __repr__(self): if self.Type.lower() in ('surf', 'surface'): Type = 'Surface' elif self.Type in ('vol', 'volume'): Type = 'Volume' else: Type = 'Unknown' return 'Mesh Object\nType: {0:s}\n{1:d} Nodes\n{2:d} Elements'.format(Type,self.NNode,self.NElem) def __iter__(self): return iter((self.NodeCoords,self.NodeConn)) # Properties @property def points(self): """ Alias for ``NodeCoords`` """ return self.NodeCoords @points.setter def points(self,NodeCoords): self.NodeCoords = NodeCoords @property def cells(self): """ Alias for ``NodeConn`` """ return self.NodeConn @cells.setter def cells(self,NodeConn): self.NodeConn = NodeConn @property def ND(self): """ Number of spatial dimensions. This is based on how many components each node coordinate has. """ return np.shape(self.NodeCoords)[1] @property def NNode(self): """ Number of nodes in the mesh. """ return len(self.NodeCoords) @property def NElem(self): """ Number of elements in the mesh. """ return len(self.NodeConn) @property def NEdge(self): """ Number of edges in the mesh. If edges haven't been determined yet, they will be. """ return len(self.Edges) @property def NFace(self): """ Number of faces in the mesh. If faces haven't been determined yet, they will be. """ return len(self.Faces) @property def Faces(self): """ Element faces. Each face is defined as a list of node indices. """ if len(self._Faces) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element faces...',end='') self._printlevel+=1 self._Faces, self._FaceConn, self._FaceElemConn = self._get_faces() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._Faces @property def FaceConn(self): """ Element-face connectivity. For each element, lists the connected faces. See :ref:`connectivity` for more info. """ if len(self._FaceConn) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element-face connectivity...',end='') self._printlevel+=1 self._Faces, self._FaceConn, self._FaceElemConn = self._get_faces() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._FaceConn @property def FaceElemConn(self): """ Face-element connectivity. For each face, lists the connected elements. See :ref:`connectivity` for more info. """ if len(self._FaceElemConn) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element face-element connectivity...',end='') self._printlevel+=1 self._Faces, self._FaceConn, self._FaceElemConn = self._get_faces() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._FaceElemConn @property def Edges(self): """ Element edges. Each edge is defined as a list of node indices. """ if len(self._Edges) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element edges...',end='') self._printlevel+=1 self._Edges, self._EdgeConn, self._EdgeElemConn = self._get_edges() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._Edges @property def EdgeConn(self): """ Element-edge connectivity. For each element, lists the connected edges. See :ref:`connectivity` for more info. """ if len(self._EdgeConn) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element-edge connectivity...',end='') self._printlevel+=1 self._Edges, self._EdgeConn, self._EdgeElemConn = self._get_edges() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._EdgeConn @property def EdgeElemConn(self): """ Edge-element connectivity. For each edge, lists the connected elements. See :ref:`connectivity` for more info. """ if len(self._EdgeElemConn) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element edge-element connectivity...',end='') self._printlevel+=1 self._Edges, self._EdgeConn, self._EdgeElemConn = self._get_edges() if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._EdgeElemConn @property def MeshNodes(self): """ Array of node IDs contained within the mesh. If all nodes are connected to elements, this will just be equivalent to `np.arange(m.NNode)`, but if there are free nodes disconnected from any elements, those nodes will be excluded. """ if self._MeshNodes is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying mesh nodes...',end='') self._printlevel += 1 if type(self.NodeConn) is np.ndarray: self._MeshNodes = np.unique(self.NodeConn) else: self._MeshNodes = np.unique(np.fromiter(itertools.chain.from_iterable(self.NodeConn), np.int64)) self._MeshNodes = np.array(list({i for elem in self.NodeConn for i in elem})) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) self._printlevel -= 1 MeshNodes = self._MeshNodes return MeshNodes @property def SurfConn(self): """ Node connectivity for the surface mesh. If the mesh is already a surface, this is the same as ``NodeConn``. """ if self.Type == 'surf': SurfConn = self.NodeConn else: if self._SurfConn is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying surface...',end='') self._SurfConn = converter.solid2surface(*self) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) SurfConn = self._SurfConn return SurfConn @property def SurfNodes(self): """ Array of node IDs on the surface of a mesh. """ if self._SurfNodes is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying surface nodes...',end='') self._printlevel += 1 if type(self.SurfConn) is np.ndarray: self._SurfNodes = np.unique(self.SurfConn) else: self._SurfNodes = np.unique(np.fromiter(itertools.chain.from_iterable(self.SurfConn),np.int64)) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) self._printlevel -= 1 SurfNodes = self._SurfNodes return SurfNodes @property def Surface(self): """ :class:`mesh` object representation of the surface mesh. ``Surface.NodeCoords`` and ``Surface.NodeConn`` are aliases of ``NodeCoords`` and ``SurfConn``, so changes to one will change the other, however some operations may break this link. """ if self.Type != 'surf': if self._Surface is None: self._Surface = mesh(self.NodeCoords, self.SurfConn, 'surf', verbose=self.verbose) surf = self._Surface else: surf = self return surf @property def BoundaryConn(self): """ Node connectivity for the boundary mesh of a surface. """ if self._BoundaryConn is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying boundary...',end='') if self.Type == 'vol': self._BoundaryConn = converter.surf2edges(*self.Surface, ElemType='surf') elif self.Type == 'surf': self._BoundaryConn = converter.surf2edges(*self, ElemType='surf') elif self.Type == 'line': self._BoundaryConn = self.NodeConn else: raise ValueError('Invalid Type.') if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) BoundaryConn = self._BoundaryConn return BoundaryConn @property def BoundaryNodes(self): """ Array of node IDs on the boundary of a surface. """ if self._BoundaryNodes is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying boundary nodes...',end='') self._printlevel += 1 self._BoundaryNodes = np.array(list({i for elem in self.BoundaryConn for i in elem})) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) self._printlevel -= 1 BoundaryNodes = self._BoundaryNodes return BoundaryNodes @property def Boundary(self): """ :class:`mesh` object representation of the boundary mesh. ``Boundary.NodeCoords`` and ``Boundary.NodeConn`` are aliases of ``NodeCoords`` and ``BoundaryConn``, so changes to one will change the other, however some operations may break this link. Volume (Type='vol') or closed surface meshes will have no Boundary and this mesh will be empty. """ if self.Type != 'line': if self._Boundary is None: self._Boundary = mesh(self.NodeCoords, self.BoundaryConn, 'line', verbose=self.verbose) surf = self._Boundary else: surf = self return surf @property def NodeNeighbors(self): """ Node neighbors. For each node, lists adjacent nodes. See :ref:`connectivity` for more info. """ if self._NodeNeighbors == []: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying volume node neighbors...',end='') self._NodeNeighbors = utils.getNodeNeighbors(*self,ElemType=self.Type) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._NodeNeighbors @property def ElemNeighbors(self): """ Element neighbors. For each element, lists adjacent elements. See :ref:`connectivity` for more info. """ if self._ElemNeighbors == []: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element neighbors...',end='') if self.Type == 'surf': mode = 'edge' elif self.Type == 'vol': mode = 'face' self._ElemNeighbors = utils.getElemNeighbors(*self, mode=mode) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._ElemNeighbors @property def ElemConn(self): """ Node-Element connectivity. For each node, lists the connected elements. See :ref:`connectivity` for more info. """ if self._ElemConn == []: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying volume node element connectivity...',end='') self._ElemConn = utils.getElemConnectivity(*self) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._ElemConn @property def SurfNodeNeighbors(self): """ Node neighbors for the surface mesh. """ if self._SurfNodeNeighbors == []: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying surface node neighbors...',end='') self._printlevel+=1 self._SurfNodeNeighbors = utils.getNodeNeighbors(self.NodeCoords,self.SurfConn) if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._SurfNodeNeighbors @property def SurfElemConn(self): """ Node-Element connectivity for the surface mesh. """ if self._SurfElemConn == []: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying surface node element connectivity...',end='') self._printlevel+=1 self._SurfElemConn = utils.getElemConnectivity(self.NodeCoords,self.SurfConn) if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._SurfElemConn @property def ElemNormals(self): """ Surface element normal vectors. For volume meshes, these will be calculated in reference to the surface mesh (:attr:`SurfConn`). """ if np.size(self._ElemNormals) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Calculating surface element normals...',end='') self._printlevel+=1 self._ElemNormals = utils.CalcFaceNormal(self.NodeCoords,self.SurfConn) if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._ElemNormals @property def NodeNormalsMethod(self): """ Sets the method for calculating surface node normals. By default, "Angle". If the method gets changed, previously computed normal vectors will be cleared. See :func:`~mymesh.utils.Face2NodeNormal` for options and more details. """ return self._NodeNormalsMethod @NodeNormalsMethod.setter def NodeNormalsMethod(self,method): self._NodeNormals = [] self._NodeNormalsMethod = method @property def NodeNormals(self): """ Surface node normal vectors. Non-surface nodes will receive [np.nan, np.nan, np.nan]. There are several methods for computing surface normal vectors at the nodes, the method to be used can be set with :attr:`NodeNormalsMethod`. See :func:`~mymesh.utils.Face2NodeNormal` for more details. """ if np.size(self._NodeNormals) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Calculating surface node normals...',end='') self._printlevel+=1 self._NodeNormals = utils.Face2NodeNormal(self.NodeCoords,self.SurfConn,self.SurfElemConn,self.ElemNormals,method=self.NodeNormalsMethod) if self.verbose: self._printlevel-=1 print('Done', end='\n'+'\t'*self._printlevel) return self._NodeNormals @property def Centroids(self): """ Element centroids. """ if np.size(self._Centroids) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Calculating element centroids...',end='') self._Centroids = utils.Centroids(*self) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._Centroids @property def EulerCharacteristic(self): """ Topological Euler characteristic number. For volume meshes, the surface mesh is used. To prevent unattached nodes from influencing the vertex count, vertices are counted as the number of unique nodes referenced in :attr:`mesh.Edges`. .. math:: \\chi = V-E+F E: number of edges V: number of vertices F: number of faces """ if self.Type == 'vol': E = self.Surface.NEdge V = len(self.SurfNodes) F = self.Surface.NFace else: E = self.NEdge V = len(self.SurfNodes) F = self.NFace return V - E + F @property def Genus(self): """ Topological genus calculated from the Euler characteristic. For volume meshes, the surface mesh is used. .. math:: g = -(\\chi - 2)/2 """ # Check for boundary edges if len(self.Surface.BoundaryConn) != 0: b = len(utils.getConnectedNodes(self.NodeCoords, self.Surface.BoundaryConn)) else: b = 0 return -(self.EulerCharacteristic - 2 + b)/2 @property def ElemType(self): """ Element Types. """ if len(self._ElemType) == 0: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying element types...',end='') self._ElemType = utils.identify_elem(*self, Type=self.Type) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._ElemType @property def bounds(self): """ Bounds of the mesh, formatted as [min(x), max(x), min(y), max(y), min(z), max(z)]. """ if self._bounds is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying bounds...',end='') self._bounds = np.array([np.min(self.NodeCoords[self.MeshNodes,0]), np.max(self.NodeCoords[self.MeshNodes,0]), np.min(self.NodeCoords[self.MeshNodes,1]), np.max(self.NodeCoords[self.MeshNodes,1]), np.min(self.NodeCoords[self.MeshNodes,2]), np.max(self.NodeCoords[self.MeshNodes,2]), ]) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._bounds @property def aabb(self): """ Axis aligned bounding box of the mesh. See also :func:`mymesh.utils.AABB`. Examples -------- .. plot:: :context: close-figs import mymesh import numpy as np # Load the stanford bunny m = mymesh.demo_mesh('bunny') # Perform an arbitrary rotation transformation to the mesh m = m.Transform([np.pi/6, -np.pi/6, np.pi/6], transformation='rotation', InPlace=True) box = mymesh.primitives.Box(m.aabb, Type='surf') .. plot:: :context: close-figs :include-source: False m.merge(box) m.plot(show_faces=False, show_points=True, show_edges=True, view='xy') """ if self._aabb is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying axis aligned bounding box...',end='') self._aabb = utils.AABB(self.NodeCoords[self.MeshNodes]) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._aabb @property def mvbb(self): """ Minimum volume bounding box of the mesh. See also :func:`mymesh.utils.MVBB`. Examples -------- .. plot:: :context: close-figs import mymesh import numpy as np # Load the stanford bunny m = mymesh.demo_mesh('bunny') # Perform an arbitrary rotation transformation to the mesh m = m.Transform([np.pi/6, -np.pi/6, np.pi/6], transformation='rotation', InPlace=True) box = mymesh.primitives.Box(m.mvbb, Type='surf') .. plot:: :context: close-figs :include-source: False m.merge(box) m.plot(show_faces=False, show_points=True, show_edges=True, view='xy') """ if self._mvbb is None: if self.verbose: print('\n'+'\t'*self._printlevel+'Identifying minimum volume bounding box...',end='') self._mvbb = utils.MVBB(self.NodeCoords[self.MeshNodes]) if self.verbose: print('Done', end='\n'+'\t'*self._printlevel) return self._mvbb # Methods ## Maintenance Methods
[docs] def identify_type(self): """ Classify the mesh as either a surface or volume. A mesh is classified as a volume mesh ('vol') if any elements are unambiguous volume elements - pyramid (5 nodes), wedge (6), hexahedron (8), or if any of a random sample of 10 elements (or all elements if NElem < 10) has a volume less than machine precision (``np.finfo(float).eps``). Alternatively, a surface mesh ('surf') is identified if any of the elements is a triangle (3 nodes). In the case of a mesh containing both triangular and volume elements, the mesh will be classified arbitrarily by whichever appears first in NodeConn. This approach has a chance of mislabeling the mesh in some rare or non-standard scenarios, but attempts to be as efficient as possible to minimize overhead when creating a mesh. Potentially ambiguous meshes are: - Meshes containing both triangular elements and volume elements (this should generally be avoided as most functions aren't set up to handle that case). - Tetrahedral meshes with many degenerate elements with abs(vol) < machine precision. - Quadrilateral meshes with non-planar elements. In such cases, Type should be specified explicitly when creating the mesh object. Returns ------- Type : str Mesh type, either 'surf', 'vol', or 'empty'. """ Type = utils.identify_type(*self) return Type
[docs] def validate(self): """ Check validity of the mesh. This will verify that ``NodeCoords`` and ``NodeConn`` are of an appropriate type, confirm the mesh is non-empty, check that elements don't reference non-existant nodes, and that the ``Type`` is a valid option ('vol' or 'surf'). If any of these checks fail, an assertion error will be raised. Additionally, a warning will be issued if a volume mesh has any inverted elements. """ assert isinstance(self.NodeCoords, (list,np.ndarray,tuple)), f'Invalid type:{str(type(self.NodeCoords)):s} for mesh.NodeCoords.' assert len(self.NodeCoords) > 0, 'Undefined Node Coordinates' assert isinstance(self.NodeCoords, (list,np.ndarray,tuple)), f'Invalid type:{str(type(self.NodeCoords)):s} for mesh.NodeConn.' assert len(self.NodeConn), 'Undefined Node Connectivity' assert max([max(elem) for elem in self.NodeConn]) <= len(self.NodeCoords), 'NodeConn references undefined nodes' assert self.Type in ('vol', 'surf'), 'Invalid mesh type.' if self.Type == 'vol': v = quality.Volume(*self) if np.nanmin(v) < 0: warnings.warn(f'Mesh has {np.sum(v < 0):d} inverted elements.')
[docs] def reset(self,properties=None,keep=None): """ Reset some or all properties. If no arguments are provided, all cached mesh properties will be reset. Parameters ---------- properties : NoneType, str, or list, optional If specified as a string or list of strings, will reset the properties specified by those names. By default, all properties other than those specified by ``keep`` will be reset. keep : NoneType, str or list, optional If specified as a string or list of strings, the corresponding properties will not be reset. If the same entry is found in ``properties`` and ``keep``, the former takes precedence and the property will be cleared. By default, None. """ # Reset calculated mesh attributes if type(keep) is str: keep = [keep] elif keep is None: keep = [] if properties == None: if 'ElemType' not in keep: self._ElemType = [] if 'SurfConn' not in keep: self._SurfConn = None if 'SurfNodes' not in keep: self._SurfNodes = None if 'Surface' not in keep: self._Surface = None if 'BoundaryConn' not in keep: self._BoundaryConn = None if 'BoundaryNodes' not in keep: self._BoundaryNodes = None if 'NodeNeighbors' not in keep: self._NodeNeighbors = [] if 'ElemConn' not in keep: self._ElemConn = [] if 'SurfNodeNeighbors' not in keep: self._SurfNodeNeighbors = [] if 'SurfElemConn' not in keep: self._SurfElemConn = [] if 'ElemNormals' not in keep: self._ElemNormals = [] if 'NodeNormals' not in keep: self._NodeNormals = [] if 'Centroids' not in keep: self._Centroids = [] if 'Edges' not in keep: self._Edges = [] if 'Edges' not in keep or 'EdgeConn' not in keep: self._EdgeConn = [] if 'Edges' not in keep or 'EdgeElemConn' not in keep: self._EdgeElemConn = [] if 'Faces' not in keep: self._Faces = [] if 'Faces' not in keep or 'FaceConn' not in keep: self._FaceConn = [] if 'Faces' not in keep or 'FaceElemConn' not in keep: self._FaceElemConn = [] if 'Surface' not in keep: self._Surface = None if 'MeshNodes' not in keep: self._MeshNodes = None if 'bounds' not in keep: self._bounds = None if 'aabb' not in keep: self._aabb = None if 'mvbb' not in keep: self._mvbb = None elif type(properties) is list: for prop in properties: if prop[0] != '_': prop = '_'+prop setattr(self,prop,[]) elif type(properties) is str: if properties[0] != '_': properties = '_'+properties setattr(self,properties,[]) else: raise Exception('Invalid input.')
[docs] def cleanup(self,tol=1e-10): # TODO: This needs to be improved so other variables that point to nodes or elements are updated accordingly self.reset() self.NodeCoords,self.NodeConn,idx,newIds = utils.DeleteDuplicateNodes(self.NodeCoords,self.NodeConn,tol=tol,return_idx=True,return_inv=True) for key in self.NodeData.keys(): self.NodeData[key] = np.asarray(self.NodeData[key])[idx] self.NodeCoords,self.NodeConn,OrigIds = utils.RemoveNodes(self.NodeCoords,self.NodeConn) for key in self.NodeData.keys(): self.NodeData[key] = np.asarray(self.NodeData[key])[OrigIds]
[docs] def copy(self): """ Create a copy of the mesh. The copied mesh will be independent, with no references to the original mesh, meaning changes to one mesh won't influence the other. Returns ------- M : mymesh.mesh Copied mesh """ M = mesh(copy.copy(self.NodeCoords), copy.copy(self.NodeConn), self.Type) M._Faces = copy.copy(self._Faces) M._FaceConn = copy.copy(self._FaceConn) M._FaceElemConn = copy.copy(self._FaceElemConn) M._Edges = copy.copy(self._Edges) M.NodeSets = copy.copy(self.NodeSets) M.EdgeSets = copy.copy(self.EdgeSets) M.FaceSets = copy.copy(self.FaceSets) M.ElemSets = copy.copy(self.ElemSets) M.NodeData = copy.deepcopy(self.NodeData) M.ElemData = copy.deepcopy(self.ElemData) M._SurfConn = copy.copy(self._SurfConn) M._NodeNeighbors = copy.copy(self._NodeNeighbors) M._ElemConn = copy.copy(self._ElemConn) M._SurfNodeNeighbors = copy.copy(self._SurfNodeNeighbors) M._SurfElemConn = copy.copy(self._SurfElemConn) M._ElemNormals = copy.copy(self._ElemNormals) M._NodeNormals = copy.copy(self._NodeNormals) M._Centroids = copy.copy(self._Centroids) M.verbose = self.verbose return M
# Modification Methods
[docs] def addNodes(self,NodeCoords,NodeSet=None): """ Add nodes to the mesh. These nodes will be appended to the end of the coordinates array (:attr:`mesh.NodeCoords`). Parameters ---------- NodeCoords : array_like Coordinates of the new node(s). This can either be a single node (shape=(3,)) or multiple nodes (shape=(n,3)). NodeSet : str, optional If provided, name of a node set that the new nodes will be added to in mesh.NodeSets[<NodeSet>], by default None. """ if len(np.shape(NodeCoords)) == 1: NodeCoords = [NodeCoords] nnode = self.NNode if type(self.NodeCoords) is np.ndarray: self.NodeCoords = np.vstack([self.NodeCoords, NodeCoords]) else: if type(NodeCoords) is np.ndarray: NodeCoords = NodeCoords.tolist() elif type(NodeCoords) is not list: NodeCoords = list(NodeCoords) self.NodeCoords += NewCoords if NodeSet in self.NodeSets.keys(): self.NodeSets[NodeSet] = set(self.NodeSets[NodeSet]).union(range(nnode,self.NNode)) elif NodeSet: self.NodeSets[NodeSet] = set(range(nnode,self.NNode))
[docs] def addFaces(self,NewFaces,FaceSet=None): """ Add new faces to the mesh. These faces will be appended to the end of the list of faces (:meth:`mesh.Faces`). This should be done very carefully and only used for advanced manipulation of the mesh, as added faces may not make sense with the rest of the mesh. If the mesh has been changed and updated faces are desired, it's safest to use :meth:`mesh.reset` then recompute the faces with (:meth:`mesh.Faces`). Note that if Faces hasn't been referenced before (``mesh._Faces==[]``) they will be obtained in this function, so if the new faces are already part of the mesh, this may produce redundant faces. Parameters ---------- NewFaces : array_like Node connectivities of the new face(s). This can either be a single face (shape=(1,m)) or multiple faces (shape=(n,m) or list of lists with length n for mixed element type faces). If a single face, it must be a 2D array or list of a single list (i.e. [[a,b,c]]). FaceSet : str, optional If provided, name of a face set that the new faces will be added to in mesh.FaceSets[<FaceSet>], by default None. """ nface = self.NFace if type(self._Faces) is np.ndarray: # If faces are an array, see if the new faces are compatible, if not convert to list of lists try: newshape = np.shape(NewFaces) except: self._Faces = self._Faces.tolist() newshape = (len(NewFaces),-1) else: if type(NewFaces) is np.ndarray: NewFaces = NewFaces.tolist() elif type(NewFaces) is not list: NewFaces = list(NewFaces) newshape = (len(NewFaces),-1) if len(newshape) != 2: raise ValueError('NewFaces must be a 2D array or list of lists') if newshape[1] == -1: # List concatenation self._Faces += NewFaces else: self._Faces = np.vstack([self.Faces, NewFaces]) if FaceSet in self.FaceSets.keys(): self.FaceSets[FaceSet] = set(self.FaceSets[FaceSet]).union(range(nface,self.NFace)) elif FaceSet: self.FaceSets[FaceSet] = set(range(nface,self.NFace))
[docs] def addEdges(self,NewEdges,EdgeSet=None): """ Add new edges to the mesh. These edges will be appended to the end of the list of edges (:meth:`mesh.Edges`). This should be done very carefully and only used for advanced manipulation of the mesh, as added edges may not make sense with the rest of the mesh. If the mesh has been changed and updated edges are desired, it's safest to use :meth:`mesh.reset` then recompute the edges with (:meth:`mesh.Edges`). Note that if Edges hasn't been referenced before (``mesh._Edges==[]``) they will be obtained in this function, so if the new edges are already part of the mesh, this may produce redundant edges. Parameters ---------- NewEdges : array_like Node connectivities of the new edge(s). This can either be a single edge (shape=(1,m)) or multiple edges (shape=(n,m) or list of lists with length n for mixed element type edges). If a single edge, it must be a 2D array or list of a single list (i.e. [[a,b,c]]). EdgeSet : str, optional If provided, name of a edge set that the new edges will be added to in mesh.EdgeSets[<EdgeSet>], by default None. """ nedge = self.NEdge if type(self._Edges) is np.ndarray: # If edges are an array, see if the new edges are compatible, if not convert to list of lists try: newshape = np.shape(NewEdges) except: self._Edges = self._Edges.tolist() newshape = (len(NewEdges),-1) else: if type(NewEdges) is np.ndarray: NewEdges = NewEdges.tolist() elif type(NewEdges) is not list: NewEdges = list(NewEdges) newshape = (len(NewEdges),-1) if len(newshape) != 2: raise ValueError('NewEdges must be a 2D array or list of lists') if newshape[1] == -1: # List concatenation self._Edges += NewEdges else: self._Edges = np.vstack([self.Edges, NewEdges]) if EdgeSet in self.EdgeSets.keys(): self.EdgeSets[EdgeSet] = set(self.EdgeSets[EdgeSet]).union(range(nedge,self.NEdge)) elif EdgeSet: self.EdgeSets[EdgeSet] = set(range(nedge,self.NEdge))
[docs] def addElems(self,NodeConn,ElemSet=None,reset=True): """ Add new elements to the mesh. These elements will be appended to the end of the list of elements (:attr:`mesh.NodeConn`) and should reference nodes that already exist in the mesh. Note that properties that are calculated on demand are not updated by this operation - it's safest to use the reset=True option or use :meth:`mesh.reset` after this operation Parameters ---------- NodeConn : array_like Node connectivities of the new element(s). This can either be a single element (shape=(1,m)) or multiple elements (shape=(n,m) or list of lists with length n for mixed element type elements). If a single element, it must be a 2D array or list of a single list (i.e. [[a,b,c]]). ElemSet : str, optional If provided, name of a element set that the new elements will be added to in mesh.ElemSets[<ElemSet>], by default None. reset : bool If True, will reset all cached mesh properties (e.g. Faces, ElemNeighbors) """ nelem = self.NElem if type(self.NodeConn) is np.ndarray: # If NodeConn is an array, see if the new elements are compatible, if not convert to list of lists try: newshape = np.shape(NodeConn) except: self.NodeConn = self.NodeConn.tolist() newshape = (len(NodeConn),-1) if newshape[1] != self.NodeConn.shape[1]: self.NodeConn = self.NodeConn.tolist() if type(NodeConn) is np.ndarray: NodeConn = NodeConn.tolist() newshape = (len(NodeConn),-1) else: if type(NodeConn) is np.ndarray: NodeConn = NodeConn.tolist() elif type(NodeConn) is not list: NodeConn = list(NodeConn) newshape = (len(NodeConn),-1) if len(newshape) != 2: raise ValueError('NodeConn must be a 2D array or list of lists') if newshape[1] == -1: # List concatenation self.NodeConn += NodeConn else: self.NodeConn = np.vstack([self.NodeConn, NodeConn]) if ElemSet in self.ElemSets.keys(): self.ElemSets[ElemSet] = set(self.ElemSets[ElemSet]).union(range(nelem,self.NElem)) elif ElemSet: self.ElemSets[ElemSet] = set(range(nelem,self.NElem)) self._ElemType = [] # resetting, will be recalculated if requested if reset: self.reset()
[docs] def removeElems(self, ElemIds): KeepSet = set(range(self.NElem)).difference(ElemIds) KeepIds = np.array(list(KeepSet)) if len(KeepIds) == 0: if type(self.NodeConn) is np.ndarray: self.NodeConn = np.empty((0,np.shape(self.NodeConn)[1])) else: self.NodeConn = [] elif type(self.NodeConn) is np.ndarray: self.NodeConn = self.NodeConn[KeepIds] else: self.NodeConn = [self.NodeConn[i] for i in KeepIds] for key in self.ElemData.keys(): if len(KeepIds) > 0: self.ElemData[key] = np.asarray(self.ElemData[key])[KeepIds] else: shape = list(np.shape(self.ElemData[key])) shape[0] = 0 self.ElemData[key] = np.empty(shape) # TODO: remove from ElemSets self.reset()
[docs] def merge(self,Mesh2,cleanup=True,tol=1e-14): """ Merge multiple meshes together into the current mesh. Parameters ---------- Mesh2 : mymesh.mesh or list Second mesh, or list of meshes, to merge with the current mesh. cleanup : bool, optional Determines whether or not to :meth:``~mesh.cleanup`` the merged mesh, by default True. tol : float, optional Cleanup tolerance for identifying duplicate nodes (see :meth:``~mesh.cleanup``), by default 1e-14 """ if type(Mesh2) is list: MeshList = Mesh2 else: MeshList = [Mesh2] for M in MeshList: m = M.copy() # Original Stats NNode = self.NNode NElem = self.NElem # Add Nodes if len(M.NodeSets) > 1: keys = list(m.NodeSets.keys()) for i in range(len(keys)): keyName = keys[i] self.addNodes([m.NodeCoords[node] for node in m.NodeSets[keyName]],NodeSet=keyName) else: self.addNodes(m.NodeCoords) # Add Elems if len(m.ElemSets) > 1: keys = list(m.ElemSets.keys()) for i in range(len(keys)): keyName = keys[i] self.addElems([[node+NNode for node in m.NodeConn[elem]] for elem in m.ElemSets[keyName]],ElemSet=keyName) else: self.addElems([[node+NNode for node in m.NodeConn[elem]] for elem in range(len(m.NodeConn))]) # Add Node and Element Data for key in self.NodeData.keys(): if not key in m.NodeData.keys(): m.NodeData[key] = np.nan * np.ones_like(self.NodeData[key])[:m.NNode,...] if len(np.shape(self.NodeData[key])) == 1: # 1D data self.NodeData[key] = np.append(self.NodeData[key], m.NodeData[key]) else: self.NodeData[key] = np.vstack([self.NodeData[key], m.NodeData[key]]) for key in self.ElemData.keys(): if not key in m.ElemData.keys(): m.ElemData[key] = np.nan * np.ones_like(self.ElemData[key])[:m.NElem,...] if len(np.shape(self.ElemData[key])) == 1: # 1D data self.ElemData[key] = np.append(self.ElemData[key], m.ElemData[key]) else: self.ElemData[key] = np.vstack([self.ElemData[key], m.ElemData[key]]) # Cleanup if cleanup: self.cleanup(tol=tol)
[docs] def RenumberNodesBySet(self): # Re-organize the order of nodes based on their node sets and make required adjustments to other stored values setkeys = self.NodeSets.keys() # newIds is a list of node ids where the new index is located at the old index newIds = np.repeat(np.nan,len(self.NodeCoords)) end = 0 # Renumber nodes in node sets for key in setkeys: start = end end += len(self.NodeSets[key]) newIds[list(self.NodeSets[key])] = np.arange(start,end) self.NodeSets[key] = range(start,end) # Renumber any nodes that aren't in node sets newIds[np.isnan(newIds)] = np.arange(end,len(self.NodeCoords)) self.NodeCoords, self.NodeConn, self._Faces = utils.RelabelNodes(self.NodeCoords, self.NodeConn, newIds, faces=self._Faces) self.reset(keep=['Faces','FaceElemConn','FaceConn'])
[docs] def RenumberFacesBySet(self): setkeys = list(self.FaceSets.keys()) if any([len(set(self.FaceSets[key1]).intersection(self.FaceSets[key2]))>0 for i,key1 in enumerate(setkeys) for key2 in setkeys[i+1:]]): raise Exception('There must be no overlap between FaceSets') # newIds is a list of face ids where the new index is located at the old index newIds = np.repeat(np.nan,len(self.Faces)) end = 0 # Renumber faces in face sets for key in setkeys: start = end end += len(self.FaceSets[key]) newIds[list(self.FaceSets[key])] = np.arange(start,end,dtype=int) self.FaceSets[key] = range(start,end) # Renumber any faces that aren't in face sets newIds[np.isnan(newIds)] = np.arange(end,len(newIds),dtype=int) newIds = newIds.astype(int) # Reorder faces NewFaces = np.zeros(utils.PadRagged(self.Faces).shape,dtype=int) NewFaceElemConn = np.zeros(np.shape(self.FaceElemConn)) NewFaces[newIds,:] = utils.PadRagged(self.Faces) NewFaceElemConn[newIds] = self.FaceElemConn NewFaceConn = newIds[utils.PadRagged(self.FaceConn)] self._Faces = utils.ExtractRagged(NewFaces,dtype=int) self._FaceElemConn = NewFaceElemConn self._FaceConn = utils.ExtractRagged(NewFaceConn,dtype=int)
[docs] def CreateBoundaryLayer(self,nLayers,FixedNodes=set(),StiffnessFactor=1,Thickness=None,OptimizeTets=True,FaceSets='surf'): """ Generate boundary layer elements. Based partially on Bottasso, C. L., & Detomi, D. (2002). A procedure for tetrahedral boundary layer mesh generation. Engineering with Computers, 18(1), 66-79. https://doi.org/10.1007/s003660200006 :cite:p:`Bottasso2002` Currently surfaces must be strictly triangular. Parameters ---------- nLayers : int Number of element layers to generate. FixedNodes : set or list, optional Set of nodes that will be held fixed, by default set(). It is not necessary to specify any fixed nodes, and by default the starting nodes of the boundary layer will be held fixed. StiffnessFactor : int or float, optional Stiffness factor used for the spring network, by default 1 Thickness : float or NoneType, optional Specified value for the maximum total thickness of the boundary layers. If nLayers > 1, this thickness is subdivided by nLayers, by default None OptimizeTets : bool, optional If True, will perform tetrahedral mesh optimization (see improvement.TetOpt), by default True. FaceSets : str or list, optional FaceSet or list of FaceSets to generate boundary later elements on, by default ['surf']. While mesh.FaceSets can generally contain any element face, boundary layer face sets must be surface faces; however, the face ids contained within the face sets should index mesh.Faces, and not mesh.SurfConn. The default value of 'surf' can be used even if no sets exist in mesh.FaceSets and will generate boundary layer elements along the entire surface. If mesh.FaceSets is empty, or doesn't contain a key with the name 'surf', the surface mesh will be used, otherwise, mesh.FaceSets['surf'] will be used. """ if type(FixedNodes) != set: FixedNodes = set(FixedNodes) if type(FaceSets) is str: FaceSets = [FaceSets] # Create first layer with 0 thickness NOrigElem = self.NElem OrigConn = copy.copy(self.NodeConn) self.NodeNormalsMethod = 'MostVisible' NodeNormals = self.NodeNormals surfconn = self.SurfConn surfnodes = set(np.unique(surfconn).tolist()) if len(self.FaceSets) == 0: if len(FaceSets) > 1 or 'surf' not in FaceSets: raise Exception('Requested FaceSets are undefined.') ForceNodes = copy.copy(surfnodes) else: ForceNodes = set() for key in FaceSets: if key not in self.FaceSets.keys(): raise Exception('Requested set "{:s}" is undefined.'.format(key)) FaceIds = self.FaceSets[key] FaceNodes = set([n for i in FaceIds for n in self.Faces[i]]) ForceNodes.update(FaceNodes) NoGrowthNodes = surfnodes.difference(ForceNodes) FixedNodes.update(NoGrowthNodes) newsurfconn = [[node+len(self.NodeCoords) for node in elem] for elem in surfconn] newsurfnodes = np.unique(newsurfconn) BLConn = [elem + newsurfconn[i] for i,elem in enumerate(surfconn)] self.addNodes(self.NodeCoords) self.addElems(BLConn) self.reset() FixedNodes.update(newsurfnodes) Forces = [[0,0,0] if i not in ForceNodes else -1*np.array(NodeNormals[i]) for i in range(self.NNode)] allnodes = set([n for elem in self.NodeConn for n in elem]) FixedNodes.update(set(i for i in range(len(self.NodeCoords)) if i not in allnodes)) Fixed = np.array([1 if i in FixedNodes else 0 for i in range(self.NNode)]) # Oriented wedge->tet conversion - (Bottasso & Detomi) # surfedges = converter.solid2edges(self.NodeCoords,surfconn) surfedges,surfedgeconn,surfedgeelem = converter.solid2edges(self.NodeCoords,newsurfconn,return_EdgeElem=True,return_EdgeConn=True) UEdges,idx,inv = converter.edges2unique(surfedges,return_idx=True,return_inv=True) UEdgeConn = inv[surfedgeconn] NodeEdges = [[] for i in self.NodeCoords] for i,e in enumerate(UEdges): NodeEdges[e[0]].append(i) NodeEdges[e[1]].append(i) oriented = np.zeros(len(UEdges)) # 1 will indicate that the edge will be oriented as is, -1 indicates a flip for i,node in enumerate(newsurfnodes): for edge in NodeEdges[node]: if oriented[edge] == 0: if UEdges[edge][0] == node: oriented[edge] = 1 else: oriented[edge] = -1 OrientedEdges = copy.copy(UEdges) OrientedEdges[oriented==-1] = np.fliplr(UEdges[oriented==-1]) surfedges = np.array(surfedges) # Tetrahedronization: # For each triangle, ElemEdgeOrientations will have a 3 entries, corresponding to the orientation of the edges # For a particular edge in an element, True -> the oriented edge is oriented clockwise, False -> counterclockwise ElemEdgeOrientations = (OrientedEdges[UEdgeConn] == surfedges[surfedgeconn])[:,:,0] # The are 6 possible combinations Cases = -1*np.ones(len(surfconn)) Cases[np.all(ElemEdgeOrientations==[True,True,False],axis=1)] = 1 Cases[np.all(ElemEdgeOrientations==[True,False,True],axis=1)] = 2 Cases[np.all(ElemEdgeOrientations==[True,False,False],axis=1)] = 3 Cases[np.all(ElemEdgeOrientations==[False,True,True],axis=1)] = 4 Cases[np.all(ElemEdgeOrientations==[False,True,False],axis=1)] = 5 Cases[np.all(ElemEdgeOrientations==[False,False,True],axis=1)] = 6 # Each triangle in surfconn lines up with the indices of wedges in BLConn ArrayConn = np.asarray(BLConn) TetConn = -1*np.ones((len(BLConn)*3,4)) t1 = np.zeros((len(ArrayConn),4)) t2 = np.zeros((len(ArrayConn),4)) t3 = np.zeros((len(ArrayConn),4)) # Case 1: t1[Cases==1] = ArrayConn[Cases==1][:,[0,1,2,5]] t2[Cases==1] = ArrayConn[Cases==1][:,[1,4,5,0]] t3[Cases==1] = ArrayConn[Cases==1][:,[0,3,4,5]] # Case 2: t1[Cases==2] = ArrayConn[Cases==2][:,[0,1,2,4]] t2[Cases==2] = ArrayConn[Cases==2][:,[0,4,2,3]] t3[Cases==2] = ArrayConn[Cases==2][:,[4,5,2,3]] # Case 3: t1[Cases==3] = ArrayConn[Cases==3][:,[0,1,2,4]] t2[Cases==3] = ArrayConn[Cases==3][:,[0,4,2,5]] t3[Cases==3] = ArrayConn[Cases==3][:,[0,4,5,3]] # Case 4: t1[Cases==4] = ArrayConn[Cases==4][:,[0,1,2,3]] t2[Cases==4] = ArrayConn[Cases==4][:,[1,2,3,5]] t3[Cases==4] = ArrayConn[Cases==4][:,[1,5,3,4]] # Case 5: t1[Cases==5] = ArrayConn[Cases==5][:,[0,1,2,5]] t2[Cases==5] = ArrayConn[Cases==5][:,[1,5,3,4]] t3[Cases==5] = ArrayConn[Cases==5][:,[0,1,5,3]] # Case 6: t1[Cases==6] = ArrayConn[Cases==6][:,[0,1,2,3]] t2[Cases==6] = ArrayConn[Cases==6][:,[1,2,3,4]] t3[Cases==6] = ArrayConn[Cases==6][:,[4,5,2,3]] TetConn[0::3] = t1 TetConn[1::3] = t2 TetConn[2::3] = t3 TetConn = TetConn.astype(int).tolist() # RelevantElems = [elem for elem in self.NodeConn if not all([n in FixedNodes for n in elem])] RelevantElems = TetConn + [elem for elem in self.NodeConn if len(elem)==4] RelevantCoords,RelevantConn,NodeIds = converter.removeNodes(self.NodeCoords,RelevantElems) # TetConn = converter.solid2tets(RelevantCoords,RelevantConn) RelevantNodeNeighbors = utils.getNodeNeighbors(RelevantCoords,RelevantConn) RelevantElemConn = utils.getElemConnectivity(RelevantCoords,RelevantConn) RelevantForces = np.asarray(Forces)[NodeIds] RelevantFixed = Fixed[NodeIds] RelevantFixedNodes = set(np.where(RelevantFixed)[0]) NewCoords = np.asarray(self.NodeCoords) if Thickness: L0Override = Thickness else: L0Override = 'min' # Expand boundary layer NewRelevantCoords,U,(K,F) = improvement.SegmentSpringSmoothing(RelevantCoords,RelevantConn, RelevantNodeNeighbors,RelevantElemConn,StiffnessFactor=StiffnessFactor, FixedNodes=RelevantFixedNodes,Forces=RelevantForces,L0Override=L0Override,return_KF=True) if Thickness: # Find stiffness factor that gives desired thickness # Full solve: # def fun(k): # NewRelevantCoords,_ = improvement.SegmentSpringSmoothing(RelevantCoords,TetConn, # RelevantNodeNeighbors,RelevantElemConn,StiffnessFactor=k, # FixedNodes=RelevantFixedNodes,Forces=RelevantForces,L0Override=L0Override) # t = max(np.linalg.norm(U,axis=1)) # # print(k,t) # return abs(Thickness - t) # res = scipy.optimize.minimize_scalar(fun,(StiffnessFactor,StiffnessFactor/10),tol=ThicknessTol,options={'maxiter':ThicknessMaxIter}) # Scaled K matrix: # def fun(k): # U = scipy.sparse.linalg.spsolve((k/StiffnessFactor)*K.tocsc(), F).toarray() # t = max(np.linalg.norm(U,axis=1)) # print(k,t) # return abs(Thickness - t)s # res = scipy.optimize.minimize_scalar(fun,(StiffnessFactor,StiffnessFactor/10),tol=ThicknessTol,options={'maxiter':ThicknessMaxIter}) # k = res.x # Power Law: # t = max(np.linalg.norm(NewCoords[ForceNodes] - NewCoords[SurfNodes],axis=1)) t = np.nanmax(np.linalg.norm(U,axis=1)) alpha = t*StiffnessFactor k = alpha*Thickness**-1 U2 = scipy.sparse.linalg.spsolve(k*K.tocsc()/StiffnessFactor, F).toarray() # t2 = max(np.linalg.norm(U2,axis=1)) NewRelevantCoords = np.add(RelevantCoords, U2) NewCoords[NodeIds] = NewRelevantCoords NewCoords[list(FixedNodes)] = np.array(self.NodeCoords)[list(FixedNodes)] # Collapse transition elements if OptimizeTets: Tets = [elem for elem in self.NodeConn if len(elem)==4] skew = quality.Skewness(NewCoords,Tets) BadElems = set(np.where(skew>0.9)[0]) ElemNeighbors = utils.getElemNeighbors(NewCoords,Tets) BadElems.update([e for i in BadElems for e in ElemNeighbors[i]]) BadNodes = set([n for i in BadElems for n in Tets[i]]) SurfConn = converter.solid2surface(NewCoords,Tets) SurfNodes = set([n for elem in SurfConn for n in elem]) FreeNodes = BadNodes.difference(SurfNodes) NewCoords = improvement.TetOpt(NewCoords,Tets,FreeNodes=FreeNodes,objective='eta',method='BFGS',iterate=4) # Divide the boundary layer to create the specified number of layers if nLayers > 1: nNum = len(NewCoords) NewCoords2 = np.array(NewCoords) for i in range(NOrigElem,self.NElem): elem = self.NodeConn[i] NewNodes = [] NewElems = [[elem[0],elem[1],elem[2],elem[0],elem[1],elem[2]]] + [[] for j in range(nLayers-1)] for j in range(1,nLayers): NewNodes += [NewCoords2[elem[0]] + (NewCoords2[elem[3]]-NewCoords2[elem[0]])*j/nLayers, NewCoords2[elem[1]] + (NewCoords2[elem[4]]-NewCoords2[elem[1]])*j/nLayers, NewCoords2[elem[2]] + (NewCoords2[elem[5]]-NewCoords2[elem[2]])*j/nLayers] NewElems[j-1] = [NewElems[j-1][3],NewElems[j-1][4],NewElems[j-1][5],nNum,nNum+1,nNum+2] NewElems[j] = [nNum,nNum+1,nNum+2,nNum,nNum+1,nNum+2] nNum += 3 NewElems[-1] = [NewElems[-2][3],NewElems[-2][4],NewElems[-2][5],elem[3],elem[4],elem[5]] NewCoords2 = np.append(NewCoords2,np.array(NewNodes),axis=0) OrigConn += NewElems self.NodeCoords = NewCoords2.tolist() self.NodeConn = OrigConn else: self.NodeCoords = NewCoords # Reduce or remove degenerate wedges -- TODO: This can probably be made more efficient # self.cleanup() self.NodeCoords,self.NodeConn = utils.DeleteDuplicateNodes(self.NodeCoords,self.NodeConn) Unq = [np.unique(elem,return_index=True,return_inverse=True) for elem in self.NodeConn] key = utils.PadRagged([u[1][u[2]] for u in Unq],fillval=-1) Cases = -1*np.ones(self.NElem,dtype=int) # Fully degenerate wedges (triangles): Cases[np.all(key[:,0:6]==[0,1,2,0,1,2],axis=1)] = 0 Cases[np.all(key[:,0:6]==[3,4,5,3,4,5],axis=1)] = 0 # Double-edge degenerate wedges (tetrahedrons): Cases[np.all(key[:,0:6]==[0,1,2,3,1,2],axis=1)] = 1 Cases[np.all(key[:,0:6]==[0,1,2,0,4,2],axis=1)] = 2 Cases[np.all(key[:,0:6]==[0,1,2,0,1,5],axis=1)] = 3 Cases[np.all(key[:,0:6]==[0,4,5,3,4,5],axis=1)] = 4 Cases[np.all(key[:,0:6]==[3,1,5,3,4,5],axis=1)] = 5 Cases[np.all(key[:,0:6]==[3,4,2,3,4,5],axis=1)] = 6 # Single-edge degenerate wedges (pyramids): Cases[np.all(key[:,0:6]==[0,1,2,3,1,5],axis=1)] = 7 Cases[np.all(key[:,0:6]==[0,1,2,3,4,2],axis=1)] = 8 Cases[np.all(key[:,0:6]==[0,1,2,0,4,5],axis=1)] = 9 Cases[np.all(key[:,0:6]==[0,1,5,3,4,5],axis=1)] = 10 Cases[np.all(key[:,0:6]==[3,1,2,3,4,5],axis=1)] = 11 Cases[np.all(key[:,0:6]==[0,4,2,3,4,5],axis=1)] = 12 # Non-wedges nNodes = np.array([len(elem) for elem in self.NodeConn]) Cases[nNodes!=6] = -1 ProperKeys = [ [], # 0 [0,1,2,3], # 1 [0,1,2,4], # 2 [0,1,2,5], # 3 [0,4,5,3], # 4 [3,1,5,4], # 5 [3,4,2,5], # 6 [0,2,5,3,1], # 7 [0,3,4,1,2], # 8 [1,4,5,2,0], # 9 [0,3,4,1,5], # 10 [1,4,5,2,3], # 11 [0,2,5,3,4] # 12 ] RNodeConn = utils.PadRagged(self.NodeConn) for i,case in enumerate(Cases): if case == -1: continue else: self.NodeConn[i] = RNodeConn[i][ProperKeys[case]].tolist() self.NodeConn = [elem for elem in self.NodeConn if len(elem)>0] # Attempt to fix any element inversions # NewCoords = np.asarray(NewCoords) # NewRelevantCoords = NewCoords[NodeIds] V = quality.Volume(*self) if min(V) < 0: # print(sum(V<0)) # BLConn = [elem for elem in self.NodeConn if len(elem) == 6] # self.NodeCoords = improvement.FixInversions(self.NodeCoords,BLConn,FixedNodes=np.unique(converter.solid2surface(self.NodeCoords,self.NodeConn))) BadElems = set(np.where(V<0)[0]) ElemNeighbors = utils.getElemNeighbors(*self) BadElems.update([e for i in BadElems for e in ElemNeighbors[i]]) BadNodes = set([n for i in BadElems for n in self.NodeConn[i]]) # self.reset('SurfConn') SurfNodes = set([n for elem in self.SurfConn for n in elem]) FreeNodes = BadNodes.difference(SurfNodes) FixedNodes = set(range(self.NNode)).difference(FreeNodes) # NewRelevantCoords = improvement.TetOpt(NewRelevantCoords,RelevantConn,FreeNodes=FreeNodes,objective='eta',method='BFGS',iterate=4) self.NodeCoords = improvement.FixInversions(*self,FixedNodes=FixedNodes) # NewCoords[NodeIds] = NewRelevantCoords self.reset()
[docs] def Contour(self, scalars, threshold, threshold_direction=1, mixed_elements=True, Type=None, interpolation='linear'): """ Contour the mesh to extract an isosurface/isoline based on nodal scalar values. Parameters ---------- scalars : str or array_like Values to be used for thresholding, either a string indicating an entry in NodeData or ElemData, or an array_like of values. threshold : int, float Isosurface level that defines the boundary threshold_direction : signed int If threshold_direction is negative (default), values less than or equal to the threshold will be considered "inside" the mesh and the opposite if threshold_direction is positive, by default 1. mixed_elements : bool, optional If True, the generated mesh will have mixed element types (triangles/quadrilaterals, tetrahedra/wedges), otherwise a single element type (triangles, tetrahedra), by default False. Type : str, optional Specfies the mesh type ('vol', 'surf', or 'line') to produce by contouring. If None is provided, the output Type will be the same as the input Type, by default None. A volumetric mesh can be contoured to create a volumetric mesh or a surface mesh, and a surface mesh can be contoured to create a surface mesh or a line mesh. Contouring of line meshes isn't currently supported. Returns ------- M : mymesh.mesh Contoured mesh """ if isinstance(scalars, str): scalar_str = scalars scalars = self.NodeData[scalars] else: scalar_str = 'scalars' if threshold_direction < 0: flip = False else: flip = True NewCoords, NewConn = contour.MarchingElements(self.NodeCoords, self.NodeConn, scalars, threshold=threshold, flip=flip, mixed_elements=mixed_elements, Type=Type, interpolation=interpolation) M = mesh(NewCoords, NewConn, verbose=self.verbose) return M
[docs] def Threshold(self, scalars, threshold, mode=None, scalar_preference='elements', all_nodes=True, InPlace=False, cleanup=False): """ Threshold the mesh by scalar values at either nodes or elements. Parameters ---------- scalars : str or array_like Values to be used for thresholding, either a string indicating an entry in NodeData or ElemData, or an array_like of values. threshold : int, float, or tuple Thresholding value(s) to use. If provided as a two element tuple, they're taken to be lower and upper bounds. mode : str, optional Thresholding condition to determine which elements to keep in the mesh. Single threshold options: '>=' - Keeping condition is `value >= threshold`, default. '>' - Keeping condition is `value > threshold`. '<' - Keeping condition is `value < threshold`. '<=' - Keeping condition is `value <= threshold`. '==' - Keeping condition is `value == threshold`. '!=' - Keeping condition is `value != threshold`. Double threshold options: 'in' - Inside bounds, inclusive of thresholds, default. 'xin' - Inside bounds, exclusive of thresholds. 'out' - Outside bounds, inclusive of thresholds. 'xout' - Outside bounds, exclusive of threhsolds. '<=<=' - Keeping condition is `lower <= value <= upper`, equivalent to 'in'. '<<' - Keeping condition is `lower < value < upper`, equivalent to 'xin'. '>=>=' - Keeping condition is `(lower >= value) | (value >= upper)`, equivalent to 'out'. '>>' - Keeping condition is `(lower > value) | (value > upper)`, equivalent to 'xout' scalar_preference : str, optional If scalars is provided as a string and that string exists as an entry in both NodeData and ElemData, this determines which will be used. Must be either 'nodes' or 'elements', by default 'elements'. all_nodes : bool, optional If node data is being used, this determines whether to keep an element if all nodes pass the thresholding condition or if any nodes pass the condition, by default True InPlace : bool, optional If true, this mesh will be modified, otherwise a copy of the mesh will be created and modified, by default False. cleanup : bool, optional Option to run :meth:`mesh.cleanup`, removing nodes that are no longer in the cropped mesh, by default False. """ # Process inputs if isinstance(scalars, str): scalar_str = scalars if scalar_preference.lower() == 'elements': if scalar_str in self.ElemData.keys(): scalars = self.ElemData[scalar_str] scalar_type = 'elements' elif scalar_str in self.NodeData.keys(): scalars = self.NodeData[scalar_str] scalar_type = 'nodes' else: raise ValueError(f'{scalar_str:s} not in ElemData or NodeData.') elif scalar_preference.lower() == 'nodes': if scalar_str in self.NodeData.keys(): scalars = self.NodeData[scalar_str] scalar_type = 'nodes' elif scalar_str in self.ElemData.keys(): scalars = self.ElemData[scalar_str] scalar_type = 'elements' else: raise ValueError(f'{scalar_str:s} not in ElemData or NodeData.') else: raise ValueError(f'scalar_preference must be specified as "elements" or "nodes", not "{scalar_preference:s}".') else: scalar_str = 'scalars' if len(scalars) == self.NElem: scalar_type = 'elements' elif len(scalars) == self.NNode: scalar_type = 'nodes' else: raise ValueError(f'Provided scalars must much either the number of nodes or number of elements in the mesh.') if isinstance(threshold, (list, tuple, np.ndarray)): if mode is None: # Default for upper/lower bound mode = 'in' if mode == '<=<=': mode = 'in' elif mode == '<<': mode = 'xin' elif mode == '>=>=': mode = 'out' elif mode == '>>': mode = 'xout' lower = min(threshold) upper = max(threshold) else: if mode is None: mode = '>=' if mode == '>=': lower = threshold upper = np.inf mode = 'in' elif mode == '>': lower = threshold upper = np.inf mode = 'xin' elif mode == '<=': lower = -np.inf upper = threshold mode = 'in' elif mode == '<': lower = -np.inf upper = threshold mode = 'xin' elif mode == '==': lower = threshold upper = threshold mode = 'in' elif mode == '!=': lower = threshold upper = threshold mode = 'xout' else: raise ValueError('For single-threshold inputs, mode must be ">=", ">", "<=", or "<".') # Determine which elements to keep if mode == 'in': keep = (lower <= scalars) & (scalars <= upper) elif mode == 'xin': keep = (lower < scalars) & (scalars < upper) elif mode == 'out': keep = (lower >= scalars) | (scalars >= upper) elif mode == 'xout': keep = (lower > scalars) | (scalars > upper) else: raise ValueError('Invalid thresholding mode.') if scalar_type == 'nodes': if type(self.NodeConn) is np.ndarray: if all_nodes: keep = np.all(keep[self.NodeConn],axis=1) else: keep = np.any(keep[self.NodeConn],axis=1) else: if all_nodes: keep = np.array([np.all(keep[elem]) for elem in self.NodeConn]) else: keep = np.array([np.any(keep[elem]) for elem in self.NodeConn]) RemoveElems = np.where(~keep)[0] if InPlace: M = self else: M = self.copy() M.removeElems(RemoveElems) if cleanup: M.cleanup() return M
[docs] def Clip(self, pt=None, normal=[1,0,0], exact=False): """ Clip the mesh along a plane. Parameters ---------- pt : array_like or NoneType, optional Coordinates for a point on the clipping plane, by default None. If None, the center of the bounding box of the mesh will be used. normal : list, optional Normal vector of the clipping plane, by default [1,0,0] exact : bool, optional If True, the mesh will be clipped exactly along the plane, cutting through elements. Otherwise, elements on one side of the plane will be removed, but no elements will be cut. By default, False. Returns ------- clipped : mymesh.mesh Clipped mesh """ if pt is None: xmax, ymax, zmax = np.max(self.NodeCoords, axis=0) xmin, ymin, zmin = np.min(self.NodeCoords, axis=0) pt = np.array([(xmax+xmin)/2, (ymax+ymin)/2, (zmax+zmin)/2]) if exact: vals = implicit.plane(pt, normal)(*self.NodeCoords.T) clipped = self.Contour(vals, 0) else: vals = implicit.plane(pt, normal)(*self.Centroids.T) clipped = self.Threshold(vals, 0) return clipped
[docs] def Crop(self, bounds, method='centroids', InPlace=False, cleanup=False): """ Crop the mesh to specified bounds. Cropping with this method doesn't modify any elements, it keeps the elements of the original mesh that are within the cropping bounds. Parameters ---------- bounds : array_like Six element list of of cropping bounds, formatted as [xmin, xmax, ymin, ymax, zmin, zmax]. method : str, optional Cropping method, by default 'centroids' - 'centroids' - keep elements whose centroids are within the bounds - 'nodes' - keep elements who have all of their nodes within the bound InPlace : bool, optional If True, the original mesh will be modified in place, otherwise a copy will be made and modified, leaving the original mesh unaltered. By default False cleanup : bool, optional Option to run :meth:`mesh.cleanup`, removing nodes that are no longer in the cropped mesh, by default False. Returns ------- M : mymesh.mesh Cropped mesh. If InPlace=True, the output will be a reference to the same mesh instance as the input. """ if InPlace: M = self else: M = self.copy() if method.lower() == 'centroids': points = np.asarray(M.Centroids) keep = ((bounds[0] < points[:,0]) & (points[:,0] < bounds[1]) & (bounds[2] < points[:,1]) & (points[:,1] < bounds[3]) & (bounds[4] < points[:,2]) & (points[:,2] < bounds[5])) elif method.lower() == 'nodes': points = np.asarray(M.NodeCoords) keep_nodes = ((bounds[0] < points[:,0]) & (points[:,0] < bounds[1]) & (bounds[2] < points[:,1]) & (points[:,1] < bounds[3]) & (bounds[4] < points[:,2]) & (points[:,2] < bounds[5])) keep = np.array([np.all(keep_nodes[elem]) for elem in M.NodeConn]) else: raise ValueError(f'Invalid method: "{method:s}". Must be "centroids or nodes".') RemoveElems = np.where(~keep)[0] M.removeElems(RemoveElems) if cleanup: M.cleanup() return M
[docs] def Mirror(self, x=None, y=None, z=None, InPlace=False): """ Mirror the mesh across Cartesian planes. At least one of x, y, or z must be specified to mirror the mesh. If multiple planes are specified, the mesh will first be mirrored across the x plane, then the y, then the z - for example, given x=0, y=0, z=None, a point at (-1,-1,0) would be mirrored to (1,-1,0) and then to (1,1,0). Note that reflections across planes parallel to the Cartesian planes are commutative, so the order of reflections don't matter. Parameters ---------- x : float, optional YZ plane at X = x. The default is None. y : float, optional XZ plane at Y = y. The default is None. z : float, optional XY plane at Z = z. The default is None. InPlace : bool, optional If True, the original mesh will be modified in place, otherwise a copy will be made and modified, leaving the original mesh unaltered. By default False Returns ------- M : mymesh.mesh Cropped mesh. If InPlace=True, the output will be a reference to the same mesh instance as the input. """ if InPlace: M = self else: M = self.copy() M.NodeCoords, M.NodeConn = utils.MirrorMesh(M.NodeCoords, M.NodeConn, x=x, y=y, z=z) return M
[docs] def Transform(self, x, transformation='rigid', transform_kwargs={}, InPlace=False): """ Apply a transformation to the coordinates of the mesh. Parameters ---------- x : array_like Transformation parameters. The number of parameters depends on the transformation being used. For the default (rigid), it should be a 6 element array of translations in x, y, and z, and rotations about x, y, and z, respectively. transformation : str, optional Transformation model to use, by default 'rigid'. - :func:`"rigid" <mymesh.register.rigid>` - :func:`"rotation" <mymesh.register.rotation>` - :func:`"translation" <mymesh.register.translation>` - :func:`"similarity" <mymesh.register.similarity>` - :func:`"affine" <mymesh.register.affine>` transform_kwargs : dict, optional Optional arguments for transform functions. See available options in the documentation of the specific transform being used, by default {} InPlace : bool, optional If True, the original mesh will be modified in place, otherwise a copy will be made and modified, leaving the original mesh unaltered. By default False Examples -------- .. plot:: :context: close-figs import mymesh import numpy as np # Load the stanford bunny m1 = mymesh.demo_mesh('bunny') # Perform a rigid transformation to the mesh m2 = m1.Transform([0.02, 0.2, -0.05, np.pi/6, -np.pi/6, np.pi/6]) .. plot:: :context: close-figs :include-source: False mcopy = m1.copy() mcopy.NodeData['label'] = np.zeros(mcopy.NNode) m2.NodeData['label'] = np.ones(m2.NNode) mcopy.merge(m2) mcopy.plot(scalars='label', show_colorbar=False, view='xy') """ if type(transformation) is str: if transformation == 'rotation': T = register.rotation(x, **transform_kwargs) elif transformation == 'translation': T = register.translation(x, **transform_kwargs) elif transformation == 'rigid': T = register.rigid(x, **transform_kwargs) elif transformation == 'similarity': T = register.similarity(x, **transform_kwargs) elif transformation == 'affine': T = register.affine(x, **transform_kwargs) else: raise ValueError(f'Transformation: "{transformation}" not supported.') else: raise TypeError(f'Transformation must be a string, not {type(transformation)}') if InPlace: self.NodeCoords = register.transform_points(self.NodeCoords, T) M = self else: M = self.copy() M.reset() M.NodeCoords = register.transform_points(self.NodeCoords, T) return M
## Mesh Measurements Methods
[docs] def getQuality(self,metrics=['Skewness','Aspect Ratio'], verbose=None): """ Evaluate mesh quality. This will create a dict with entries corresponding to the specified quality metrics. This dict can be stored in mesh.ElemData by performing `m.ElemData.update(m.getQuality())` or `m.ElemData |= m.getQuality()` Parameters ---------- metrics : str or list, optional Quality metric, or list of quality metrics, to evaluate, by default ['Skewness','Aspect Ratio']. Available options are: - 'Skewness' : :func:`~mymesh.quality.Skewness` - 'Aspect Ratio' : :func:`~mymesh.quality.AspectRatio` - 'Inverse Orthogonal Quality' : :func:`~mymesh.quality.InverseOrthogonalQuality` - 'Orthogonal Quality' : :func:`~mymesh.quality.OrthogonalQuality` - 'Inverse Orthogonality' : :func:`~mymesh.quality.InverseOrthogonality` - 'Orthogonality' : :func:`~mymesh.quality.Orthogonality` - 'Min Dihedral' : :func:`~mymesh.quality.MinDihedral` - Reported in radians - 'Min Dihedral(deg)' : :func:`~mymesh.quality.MinDihedral` - Reported in degrees - 'Max Dihedral' : :func:`~mymesh.quality.MaxDihedral` - Reported in radians - 'Max Dihedral(deg)' : :func:`~mymesh.quality.MaxDihedral` - Reported in degrees - 'Mean Ratio' : :func:`~mymesh.quality.MeanRatio` - 'Volume' : :func:`~mymesh.quality.Volume` Note that not all metrics are suited to all element types. verbose : bool or NoneType, optional If True, quality reports will be printed. If None, this will be determined by the verbosity state (mesh.verbose) of the mesh object, by default None. Returns ------- qual : dict Dictionary of element qualities """ if verbose is None: verbose = self.verbose qual = {} if type(metrics) is str: metrics = [metrics] for metric in metrics: assert isinstance(metric, str), 'Invalid quality metric. Metrics must be strings.' m = metric.lower() if m == 'skewness': qual[metric] = quality.Skewness(*self,verbose=verbose) elif m == 'aspect ratio': qual[metric] = quality.AspectRatio(*self,verbose=verbose) elif m == 'inverse orthogonal quality': qual[metric] = quality.InverseOrthogonalQuality(*self,verbose=verbose) elif m == 'orthogonal quality': qual[metric] = quality.OrthogonalQuality(*self,verbose=verbose) elif m == 'inverse orthogonality': qual[metric] = quality.InverseOrthogonality(*self,verbose=verbose) elif m == 'orthogonality': qual[metric] = quality.Orthogonality(*self,verbose=verbose) elif m == 'min dihedral': qual[metric] = quality.MinDihedral(*self,verbose=verbose) elif m == 'min dihedral(deg)': qual[metric] = quality.MinDihedral(*self,verbose=verbose)*180/np.pi elif m == 'max dihedral': qual[metric] = quality.MaxDihedral(*self,verbose=verbose) elif m == 'max dihedral(deg)': qual[metric] = quality.MaxDihedral(*self,verbose=verbose)*180/np.pi elif m == 'mean ratio': qual[metric] = quality.MeanRatio(*self,verbose=verbose) elif m == 'volume': qual[metric] = quality.Volume(*self,verbose=verbose) else: raise Exception(f'Invalid quality metric "{metric:s}".') return qual
[docs] def getCurvature(self,metrics=['Max Principal','Min Principal', 'Curvedness', 'Shape Index', 'Mean', 'Gaussian'], nRings=1, SplitFeatures=False): """ Calculate mesh-based curavature values using :ref:`cubic surface fitting <theory-cubic-surface-fitting>`. See also :func:`mymesh.curvature.CubicFit`. Parameters ---------- metrics : list, optional List of curvature metrics to calculate. Options are: - 'Max Principal' - Maxiumum principal curvature - 'Min Principal' - Minimum principal curvature - 'Curvedness' - Curvedness (see :ref:`theory_curvedness`, :cite:`Koenderink1992a`) - 'Shape Index' - Shape index (see :ref:`theory_shape-index`, :cite:`Koenderink1992a`) - 'Mean' - Mean curvauture - 'Gaussian' - Gaussian curvature By default :code:`['Max Principal','Min Principal', 'Curvedness', 'Shape Index', 'Mean', 'Gaussian']`. nRings : int, optional Number of neighborhood 'rings' about each node to use in calculating the curvature, by default 1 SplitFeatures : bool, optional If true, will split the mesh along feature edges (see :func:`mymesh.utils.DetectFeatures`) and calculate the curvature of each surface separately, by default False Returns ------- Curvature : dict Dictionary containing selected output curvature values. """ if type(metrics) is str: metrics = [metrics] Curvature = {} if SplitFeatures: edges,corners = utils.DetectFeatures(self.NodeCoords,self.SurfConn) FeatureNodes = set(edges).union(corners) NodeRegions = utils.getConnectedNodes(self.NodeCoords,self.SurfConn,BarrierNodes=FeatureNodes) MaxPs = np.nan*np.ones((self.NNode,len(NodeRegions))) MinPs = np.nan*np.ones((self.NNode,len(NodeRegions))) for i,region in enumerate(NodeRegions): Elems = [elem for elem in self.SurfConn if all([n in region for n in elem])] # ElemNormals = [self.ElemNormals[i] for i,elem in enumerate(self.SurfConn) if all([n in region for n in elem])] ElemNormals = utils.CalcFaceNormal(self.NodeCoords,Elems) Neighbors,ElemConn = utils.getNodeNeighbors(self.NodeCoords,Elems) if nRings > 1: Neighbors = utils.getNodeNeighborhood(self.NodeCoords,Elems,nRings) NodeNormals = utils.Face2NodeNormal(self.NodeCoords,Elems,ElemConn,ElemNormals) MaxPs[:,i], MinPs[:,i] = curvature.CubicFit(self.NodeCoords,Elems,Neighbors,NodeNormals) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) MaxPrincipal = np.nanmean(MaxPs,axis=1) MinPrincipal = np.nanmean(MinPs,axis=1) else: if nRings == 1: Neighbors = self.SurfNodeNeighbors else: Neighbors = utils.getNodeNeighborhood(self.NodeCoords,self.SurfConn,nRings=nRings) MaxPrincipal,MinPrincipal = curvature.CubicFit(self.NodeCoords,self.SurfConn,Neighbors,self.NodeNormals) if 'Max Principal' in metrics: Curvature['Max Principal Curvature'] = MaxPrincipal if 'Max Principal' in metrics: Curvature['Min Principal Curvature'] = MinPrincipal if 'Shape Index' in metrics or 'Shape Category' in metrics: SI = curvature.ShapeIndex(MaxPrincipal,MinPrincipal) if 'Shape Index' in metrics: Curvature['Shape Index'] = SI if 'Shape Category' in metrics: SC = curvature.ShapeCategory(SI) Curvature['Shape Category'] = SC if 'Curvedness' in metrics: C = curvature.Curvedness(MaxPrincipal,MinPrincipal) Curvature['Curvedness'] = C if 'Gaussian' in metrics: G = curvature.GaussianCurvature(MaxPrincipal,MinPrincipal) Curvature['Gaussian Curvature'] = G if 'Mean' in metrics: M = curvature.MeanCurvature(MaxPrincipal,MinPrincipal) Curvature['Mean Curvature'] = M return Curvature
## File I/O Methods
[docs] def mymesh2meshio(self): """ Convert mesh object to a `meshio <https://github.com/nschloe/meshio>`_ mesh object. Returns ------- m : meshio._mesh.mesh meshio-type mesh object with nodes, elements (cells), and node/element data. """ try: import meshio except: raise ImportError('mesh.Mesh2Meshio() requires the meshio library. Install with: pip install meshio') celldict = dict() elemlengths = np.array([len(elem) for elem in self.NodeConn]) if len(self.ElemData) > 0: keys = self.ElemData.keys() for key in keys: celldata = [[],[],[],[],[],[],[],[],[],[]] data = np.asarray(self.ElemData[key]) if data.dtype == bool: data = data.astype(int) elif np.issubdtype(data.dtype, np.str_): data = np.array([int.from_bytes(x.encode('utf-8'), 'little') for x in data]) celldata[0] = data[elemlengths==2] # line celldata[1] = data[elemlengths==3] # tri celldata[2] = data[elemlengths==4] # quad/tet celldata[3] = data[elemlengths==5] # pyr celldata[4] = data[elemlengths==6] # tri6/wdg celldata[5] = data[elemlengths==8] # quad8/hex celldata[6] = data[elemlengths==10] # tet10 celldata[7] = data[elemlengths==13] # pyr13 celldata[8] = data[elemlengths==15] # wdg15 celldata[9] = data[elemlengths==20] # hex20 celldata = [c for c in celldata if len(c) > 0] celldict[key] = celldata NodeData = copy.copy(self.NodeData) if len(NodeData) > 0: for key in NodeData.keys(): data = np.asarray(NodeData[key]) if data.dtype == bool: data = data.astype(int) elif np.issubdtype(data.dtype, np.str_): data = np.array([int.from_bytes(x.encode('utf-8'), 'little') for x in data]) NodeData[key] = data if np.all(elemlengths == elemlengths[0]): ArrayConn = np.array(self.NodeConn,dtype=int) else: ArrayConn = np.array(self.NodeConn,dtype=object) edges, tris, tri6s, quads, quad8s, tets, tet10s, pyrs, pyr13s, wdgs, wdg15s, hexs, hex20s = [np.empty((0,0)) for i in range(13)] if np.any(elemlengths == 2): edges = np.stack(ArrayConn[elemlengths==2]) if np.any(elemlengths == 3): tris = np.stack(ArrayConn[elemlengths==3]) if self.Type == 'surf': if np.any(elemlengths == 4): quads = np.stack(ArrayConn[elemlengths==4]) if np.any(elemlengths == 6): tri6s = np.stack(ArrayConn[elemlengths==6]) if np.any(elemlengths == 8): quad8s = np.stack(ArrayConn[elemlengths==8]) tets = [] wdgs = [] hexs = [] else: quads = [] tri6s = [] quad8s = [] if np.any(elemlengths == 4): tets = np.stack(ArrayConn[elemlengths==4]) if np.any(elemlengths == 6): wdgs = np.stack(ArrayConn[elemlengths==6]) if np.any(elemlengths == 8): hexs = np.stack(ArrayConn[elemlengths==8]) if np.any(elemlengths == 5): pyrs = np.stack(ArrayConn[elemlengths==5]) if np.any(elemlengths == 10): tet10s = np.stack(ArrayConn[elemlengths==10]) if np.any(elemlengths == 13): pyr13s = np.stack(ArrayConn[elemlengths==13]) if np.any(elemlengths == 15): wdg15s = np.stack(ArrayConn[elemlengths==15]) if np.any(elemlengths == 20): hex20s = np.stack(ArrayConn[elemlengths==20]) if np.any(elemlengths == 13) or np.any(elemlengths == 15): raise Exception("meshio currently doesn't support quadratic (13-node) pyramids or quadratic (15-node) wedges.") elems = [e for e in [('line',edges),('triangle',tris),('triangle6',tri6s),('quad',quads),('quad8',quad8s),('tetra',tets),('tetra10',tet10s),('pyramid',pyrs),('pyramid13',pyr13s),('wedge',wdgs),('wedge15',wdg15s),('hexahedron',hexs),('hexahedron20',hex20s)] if len(e[1]) > 0] m = meshio.Mesh(self.NodeCoords, elems, point_data=NodeData, cell_data=celldict) m.point_sets = self.NodeSets m.cell_sets = self.ElemSets # TODO: This might not give the expected result return m
[docs] def write(self,filename,binary=True): """ Write mesh to file. Utilizes `meshio <https://github.com/nschloe/meshio>`_ to access a variety of filetypes. Parameters ---------- filename : str Name of file with appropriate extension binary : bool, optional If True, will write the a binary file (if applicable) rather than an ASCII version of the file, by default True. """ if self.NNode == 0: warnings.warn('Mesh empty - file not written.') return if self.NElem == 0: self.NodeConn = [[0,0,0]] ext = os.path.splitext(filename)[1].lower() if ext == '.stl' or ext == '.ply': # Surface only support if self.Type.lower() == 'vol': M = self.Surface.copy() else: M = self.copy() if ext == '.stl': # Triangle only support if 'tri6' in M.ElemType: M.NodeCoords, M.NodeConn = converter.quadratic2linear(M.NodeCoords, M.NodeConn) M.NodeCoords, M.NodeConn = converter.surf2tris(M.NodeCoords, M.NodeConn) else: M = self m = M.mymesh2meshio() # File types that support the binary option: binary_support = ['.msh', '.f3grid', '.ply', '.stl', '.vtk', '.vtu'] if binary is not None and ext in binary_support: m.write(filename,binary=binary) else: m.write(filename)
[docs] def meshio2mymesh(m): """ Convert a `meshio <https://github.com/nschloe/meshio>`_ mesh object to a MyMesh mesh object Parameters ---------- m : meshio._mesh.Mesh Meshio mesh object Returns ------- M : mymesh.mesh MyMesh mesh object """ try: import meshio except: raise ImportError('mesh.Meshio2Mesh() requires the meshio library. Install with: pip install meshio') if int(meshio.__version__.split('.')[0]) >= 5 and int(meshio.__version__.split('.')[1]) >= 2: if len(m.cells) == 1: # Single element type - keeps NodeConn as an array NodeConn = m.cells[0].data else: # Multiple element types - convert to list NodeConn = [elem for cells in m.cells for elem in cells.data.tolist()] else: # Support for older meshio version NodeConn = [elem for cells in m.cells for elem in cells[1].tolist()] NodeCoords = m.points M = mesh(NodeCoords,NodeConn) if len(m.point_data) > 0 : for key in m.point_data.keys(): M.NodeData[key] = np.asarray(m.point_data[key]) if len(m.cell_data) > 0: for key in m.cell_data.keys(): M.ElemData[key] = np.asarray([data for celldata in m.cell_data[key] for data in celldata]) M.NodeSets = m.point_sets M.ElemSets = m.cell_sets # TODO: This might not give the expected result return M
[docs] def read(file): """ Read a mesh file written in any file type supported by meshio Parameters ---------- file : str File path to a mesh file readable by meshio (.vtu, .vtk, .inp, .stl, ...) Returns ------- M : mesh.mesh Mesh object """ try: import meshio except: raise ImportError('mesh.read() requires the meshio library. Install with: pip install meshio') m = meshio.read(file) M = mesh.meshio2mymesh(m) return M
[docs] def imread(img, voxelsize, scalefactor=1, scaleorder=1, return_nodedata=False, return_gradient=False, gaussian_sigma=1, threshold=None, crop=None, threshold_direction=1): """ Load an image into a voxel mesh. :func:``~mymesh.converter.im2voxel`` is used to perform the conversion. Parameters ---------- img : str or np.ndarray If a str, should be the directory to an image stack of tiff or dicom files. If an array, shoud be a 3D array of image data. voxelsize : float Size of voxel (based on image resolution). scalefactor : float, optional Scale factor for resampling the image. If greater than 1, there will be more than 1 elements per voxel. If less than 1, will coarsen the image, by default 1. scaleorder : int, optional Interpolation order for scaling the image (see scipy.ndimage.zoom), by default 1. Must be 0-5. threshold : float, optional Voxel intensity threshold, by default None. If given, elements with all nodes less than threshold will be discarded. Returns ------- M : mesh.mesh Mesh object, containing image data for elements and nodes in M.ElemData['Image Data'] and M.NodeData['Image Data']. """ if return_nodedata: VoxelCoords, VoxelConn, VoxelData, NodeData = converter.im2voxel(img,voxelsize,scalefactor=scalefactor,scaleorder=scaleorder,return_nodedata=return_nodedata,return_gradient=return_gradient, gaussian_sigma=gaussian_sigma,threshold=threshold,crop=crop,threshold_direction=threshold_direction) else: VoxelCoords, VoxelConn, VoxelData = converter.im2voxel(img,voxelsize,scalefactor=scalefactor,scaleorder=scaleorder,return_nodedata=return_nodedata,return_gradient=return_gradient,gaussian_sigma=gaussian_sigma,threshold=threshold,crop=crop,threshold_direction=threshold_direction) M = mesh(VoxelCoords,VoxelConn) if return_gradient: M.ElemData['Image Data'],M.ElemData['Image Gradient'] = VoxelData if return_nodedata: M.NodeData['Image Data'], M.NodeData['Image Gradient'] = NodeData else: M.ElemData['Image Data'] = VoxelData if return_nodedata: M.NodeData['Image Data'] = NodeData return M
[docs] def to_meshio(self): """ Convert mesh object to a `meshio <https://github.com/nschloe/meshio>`_ mesh object. This is an alias to mymesh2meshio. Returns ------- m : meshio._mesh.mesh meshio-type mesh object with nodes, elements (cells), and node/element data. """ return self.mymesh2meshio()
[docs] def to_pyvista(self): """ Convert mesh object to a `pyvista <https://pyvista.org/>`_ unstructured grid object. Returns ------- pvmesh : pyvista.core.pointset.UnstructuredGrid meshio-type mesh object with nodes, elements (cells), and node/element data. """ try: import pyvista as pv except: raise ImportError('pyvista m ust be installed to create an pyvista mesh object. Install with: `pip install pyvista`') pvmesh = pv.wrap(self.to_meshio()) return pvmesh
## Visualization Methods
[docs] def view(self, **kwargs): """ Generate an interactive plot of the mesh. See :func:`mymesh.visualize.view` for a full list of optional arguments. Returns ------- out Output passed from :func:`mymesh.visualize.view`. """ out = visualize.View(self, **kwargs) return out
[docs] def plot(self, show=True, return_fig=False, clim=None, show_colorbar=True, title=None, **kwargs): """ Generate a static plot of the mesh. See :func:`mymesh.visualize.view` for a full list of optional arguments. Parameters ---------- show : bool, optional Display the plotted mesh, by default True. If False, the plot will be generated and can be returned, but won't be displayed. return_fig : bool, optional If True, a matplotlib figure and axis holding the plotted mesh will be returned, by default False clim : array_like, optional Two-element tuple, list, or array containing the lower and upper bound for the colorscale if a scalar is displayed, by default None. If None, the minimum and maximum values will be used. Returns ------- fig : matplotlib.figure.Figure matplotlib figure of the plotted mesh ax : matplotlib.axes._axes.Axes matplotlib axes of the plotted mesh """ try: import matplotlib import matplotlib.pyplot as plt except: raise ImportError('Matplotlib required for plotting. Install with `pip install matplotlib`.') kwargs['return_image'] = True kwargs['interactive'] = False kwargs['hide'] = True img = visualize.View(self, clim=clim, **kwargs) fig, ax = plt.subplots() ax.imshow(img) ax.set_axis_off() if 'scalars' in kwargs and kwargs['scalars'] is not None: scalars = kwargs['scalars'] if 'scalar_preference' in kwargs: scalar_preference = kwargs['scalar_preference'] else: scalar_preference = 'nodes' if type(scalars) is str: if scalar_preference.lower() == 'nodes': if scalars in self.NodeData.keys(): scalars = self.NodeData[scalars] elif scalars in self.ElemData.keys(): scalars = self.ElemData[scalars] else: raise ValueError(f'Scalar {scalars:s} not present in mesh.') elif scalar_preference.lower() == 'elements': if scalars in self.ElemData.keys(): scalars = self.ElemData[scalars] elif scalars in self.NodeData.keys(): scalars = self.NodeData[scalars] else: raise ValueError(f'Scalar {scalars:s} not present in mesh.') else: raise ValueError('scalar_preference must be "nodes" or "elements"') if clim is None: cmin, cmax = np.min(scalars), np.max(scalars) else: cmin, cmax = clim scale = matplotlib.cm.ScalarMappable(cmap='coolwarm') scale.set_clim(cmin, cmax) if show_colorbar: colorbar = matplotlib.pyplot.colorbar(scale, ax=ax) if title is not None: plt.title(title) if show: plt.show() if return_fig: return fig, ax
## Helper Functions def _get_faces(self): if self.NElem > 0: # Get all element faces faces,faceconn,faceelem = converter.solid2faces(self.NodeCoords,self.NodeConn,return_FaceConn=True,return_FaceElem=True) # Pad Ragged arrays in case of mixed-element meshes Rfaces = utils.PadRagged(faces) Rfaceconn = utils.PadRagged(faceconn) # Get all unique element faces (accounting for flipped versions of faces) _,idx,inv = np.unique(np.sort(Rfaces,axis=1),axis=0,return_index=True,return_inverse=True) RFaces = Rfaces[idx] FaceElem = faceelem[idx] RFaces = np.append(RFaces, np.repeat(-1,RFaces.shape[1])[None,:],axis=0) inv = np.append(inv,-1) RFaceConn = inv[Rfaceconn] # Faces attached to each element # Face-Element Connectivity FaceElemConn = np.nan*(np.ones((len(RFaces),2))) # Elements attached to each face FECidx = (FaceElem[RFaceConn] == np.repeat(np.arange(self.NElem)[:,None],RFaceConn.shape[1],axis=1)).astype(int) FaceElemConn[RFaceConn,FECidx] = np.repeat(np.arange(self.NElem)[:,None],RFaceConn.shape[1],axis=1) FaceElemConn = [[int(x) if not np.isnan(x) else x for x in y] for y in FaceElemConn[:-1]] Faces = utils.ExtractRagged(RFaces[:-1],dtype=int) FaceConn = utils.ExtractRagged(RFaceConn,dtype=int) return Faces, FaceConn, FaceElemConn else: return [], [], [] def _get_edges(self): # TODO: This might not work properly with mixed element types - but I think it shoud if self.NElem > 0: # Get all element edges edges, edgeconn, edgeelem = converter.solid2edges(self.NodeCoords,self.NodeConn,return_EdgeConn=True,return_EdgeElem=True,ElemType=self.Type) # Convert to unique edges Edges, UIdx, UInv = converter.edges2unique(edges,return_idx=True,return_inv=True) EdgeElem = np.asarray(edgeelem)[UIdx] EdgeConn = UInv[utils.PadRagged(edgeconn)] rows = EdgeConn.flatten() cols = np.repeat(np.arange(self.NElem),[len(x) for x in EdgeConn]) data = np.ones(len(rows)) mat = scipy.sparse.coo_matrix((data,(rows,cols)),shape=(len(Edges),self.NElem)).tocsr() EdgeElemConn = [list(mat.indices[mat.indptr[i]:mat.indptr[i+1]]) for i in range(mat.shape[0])] return Edges, EdgeConn, EdgeElemConn else: return [], [], []
[docs] def mesh2dmesh(self, ElemLabels=None): """ Convert to a dynamic mesh (:class:`dmesh`) Returns ------- D : mesh.dmesh dynamic mesh """ # Ensure relevant data is in properly typed numpy arrays try: NodeConn = np.array(self.NodeConn, dtype=np.int64) except: raise ValueError('Dynamic meshes are only valid for meshes with a single element type.') NodeCoords = np.array(self.NodeCoords, dtype=np.float64) if ElemLabels is None: ElemLabels = np.empty(0, np.int64) else: ElemLabels = np.asarray(ElemLabels, np.int64) if self.verbose: self.verbose = False _ = self.ElemConn self.verbose = True D = dmesh(NodeCoords, NodeConn, self.ElemConn, ElemLabels) return D
# dmesh class # Attributes: @try_njit(cache=True) def dmesh_get_raw_NodeCoords(self): return self.raw_NodeCoords @try_njit(cache=True) def dmesh_get_NNode(self): return self.NNode @try_njit(cache=True) def dmesh_get_NodeCoords(self): return self.raw_NodeCoords[:self.NNode] @try_njit(cache=True) def dmesh_get_raw_NodeConn(self): return self.raw_NodeConn @try_njit(cache=True) def dmesh_get_NElem(self): return self.NElem @try_njit(cache=True) def dmesh_get_NodeConn(self): return self.raw_NodeConn[:self.NElem] @try_njit(cache=True) def dmesh_get_ElemConn_head(self): return self.ElemConn_head @try_njit(cache=True) def dmesh_get_ElemConn_elem(self): return self.ElemConn_elem @try_njit(cache=True) def dmesh_get_ElemConn_next(self): return self.ElemConn_next @try_njit(cache=True) def dmesh_get_ElemConn_prev(self): return self.ElemConn_prev @try_njit(cache=True) def dmesh_get_ElemConn_tail(self): return self.ElemConn_tail @try_njit(cache=True) def dmesh_get_ElemConn_size(self): return self.ElemConn_size @try_njit(cache=True) def dmesh_get_ElemLabels(self): return self.ElemLabels # Method implementations: @try_njit(cache=True) def dmesh_addNodes(self,NodeCoords): """ Add nodes to the mesh. Parameters ---------- NodeCoords : array_like Coordinates of the new node(s). This can either be a single node (shape=(3,)) or multiple nodes (shape=(n,3)). """ NodeCoords = np.atleast_2d(NodeCoords) NewLength = self.NNode + len(NodeCoords) if len(self.raw_NodeCoords) < NewLength: # Amortized O(1) insertion by doubling - double the length of the array to make space for the new data. # If the new addition is more than double the current length, the array will be extended # to exactly fit the new nodes newsize = np.maximum(NewLength,len(self.raw_NodeCoords)*2) self.raw_NodeCoords = np.resize(self.raw_NodeCoords, (newsize,3)) # update the ElemConn structure as well self.ElemConn_head = np.resize(self.ElemConn_head, newsize) self.ElemConn_tail = np.resize(self.ElemConn_tail, newsize) self.raw_NodeCoords[self.NNode:NewLength] = NodeCoords self.ElemConn_head[self.NNode:NewLength] = -1 self.ElemConn_tail[self.NNode:NewLength] = -1 self.NNode = NewLength @try_njit(cache=True) def dmesh_addElem(self,NodeConn,Label=0): """ Add a new element to the mesh. The element should reference nodes that already exist in the mesh. Parameters ---------- NodeConn : array_like Node connectivity of the new element. This should be a single element (shape=(m,)) """ NewLength = self.NElem + 1 NewElemId = self.NElem if len(self.raw_NodeConn) < NewLength: # Amortized O(1) insertion by doubling - double the length of the array to make space for the new data. # If the new addition is more than double the current length, the array will be extended # to exactly fit the new elements self.raw_NodeConn = np.resize(self.raw_NodeConn, (int(np.maximum(NewLength,len(self.raw_NodeConn)*2)),np.shape(self.raw_NodeConn)[1])) if len(self.ElemLabels) > 0: self.ElemLabels = np.resize(self.ElemLabels, int(np.maximum(NewLength,len(self.ElemLabels)*2))) self.raw_NodeConn[NewElemId] = NodeConn if len(self.ElemLabels) > 0: self.ElemLabels[NewElemId] = Label # Update ElemConn with new connections for node in self.raw_NodeConn[NewElemId]: dmesh_addElemConn(self, node, NewElemId) self.NElem += 1 @try_njit(cache=True) def dmesh_getElemConn(self, NodeId): i = self.ElemConn_head[NodeId] ElemConn = [] while i != -1: ElemConn.append(self.ElemConn_elem[i]) i = self.ElemConn_next[i] return ElemConn @try_njit(cache=True) def dmesh_addElemConn(self, NodeId, ElemId): # Add a connection between an existing node and an element if len(self.ElemConn_elem) == self.ElemConn_size: # Amortized O(1) insertion by doubling self.ElemConn_elem = np.resize(self.ElemConn_elem, np.maximum(len(self.ElemConn_elem)*2,1)) self.ElemConn_next = np.resize(self.ElemConn_next, np.maximum(len(self.ElemConn_next)*2,1)) self.ElemConn_prev = np.resize(self.ElemConn_prev, np.maximum(len(self.ElemConn_prev)*2,1)) i = self.ElemConn_tail[NodeId] newIdx = self.ElemConn_size self.ElemConn_elem[newIdx] = ElemId if i == -1: # If this is the first connection for the node self.ElemConn_head[NodeId] = newIdx self.ElemConn_prev[newIdx] = -1 else: self.ElemConn_next[i] = newIdx self.ElemConn_prev[newIdx] = i self.ElemConn_tail[NodeId] = newIdx self.ElemConn_next[newIdx] = -1 self.ElemConn_size += 1 @try_njit(cache=True) def dmesh_removeElemConn(self, NodeId, ElemId): # Remove a connection between a node and an element # Find the position of the element i = self.ElemConn_head[NodeId] while i != -1: elem = self.ElemConn_elem[i] if elem == ElemId: # Remove if self.ElemConn_prev[i] == -1: # this node is the head self.ElemConn_head[NodeId] = self.ElemConn_next[i] else: self.ElemConn_next[self.ElemConn_prev[i]] = self.ElemConn_next[i] if self.ElemConn_next[i] == -1: # this node is the tail self.ElemConn_tail[NodeId] = self.ElemConn_prev[i] else: self.ElemConn_prev[self.ElemConn_next[i]] = self.ElemConn_prev[i] self.ElemConn_elem[i] = -1 self.ElemConn_next[i] = -1 self.ElemConn_prev[i] = -1 break else: i = self.ElemConn_next[i] # NOTE: Not currently tracking freed slots so the remnants of old connections # remain, taking up space. Could implement a tracker of free indices # to all for reuse @try_njit(cache=True) def dmesh_swapNode(self, NodeId1, NodeId2): # Swap all references of Node1 to Node2 elemconn1 = dmesh_getElemConn(self, NodeId1) elemconn2 = dmesh_getElemConn(self, NodeId2) for ElemId in elemconn1: elem = self.raw_NodeConn[ElemId] elem[elem == NodeId1] = NodeId2 self.raw_NodeConn[ElemId] = elem dmesh_removeElemConn(self, NodeId1, ElemId) if ElemId not in elemconn2: dmesh_addElemConn(self, NodeId2, ElemId) @try_njit(cache=True) def dmesh_removeElem(self, ElemId): # Remove an element from the mesh by swap removal # Swaps the position of the element definition in NodeConn to the position # of the last defined element. References to the last element are updated # to refer to the swapped position # NOTE: If removing multiple elements, be sure to remove them in reverse # order (largest first) LastId = self.NElem - 1 # Remove connections to the old element for node in self.raw_NodeConn[ElemId]: dmesh_removeElemConn(self, node,ElemId) if ElemId != LastId: # Swap element @ ElemId with the element in the last position self.raw_NodeConn[ElemId, :] = self.raw_NodeConn[LastId, :] #oldnodes if len(self.ElemLabels) > 0: self.ElemLabels[ElemId] = self.ElemLabels[LastId] # Update ElemConn for each node in the element for n in self.raw_NodeConn[LastId]: i = self.ElemConn_head[n] while i != -1: if self.ElemConn_elem[i] == LastId: self.ElemConn_elem[i] = ElemId break i = self.ElemConn_next[i] self.NElem -= 1 @try_njit(cache=True) def dmesh_removeElems(self, ElemIds): # This just ensures that elements are removed in the proper order for ElemId in np.sort(ElemIds)[::-1]: dmesh_removeElem(self, ElemId) def dmesh_init(raw_NodeCoords, raw_NodeConn, ElemConn, ElemLabels=None): NNode = len(raw_NodeCoords) NElem = len(raw_NodeConn) ElemConn_head = np.full(NNode, -1, dtype=np.int64) ElemConn_elem = np.array([e for elems in ElemConn for e in elems], dtype=np.int64) ElemConn_next = np.full(len(ElemConn_elem), -1, dtype=np.int64) ElemConn_prev = np.full(len(ElemConn_elem), -1, dtype=np.int64) ElemConn_tail = np.full(NNode, -1, dtype=np.int64) k = 0 for i,elems in enumerate(ElemConn): if len(elems) == 0: continue ElemConn_head[i] = k # ElemConn_prev[k] = -1 # Implied by initialization for e in elems[:-1]: ElemConn_next[k] = k+1 ElemConn_prev[k+1] = k k += 1 # ElemConn_next[k] = -1 # Implied by initialization ElemConn_tail[i] = k k += 1 ElemConn_size = len(ElemConn_elem) if ElemLabels is None: ElemLabels = np.empty(0, np.int64) return (raw_NodeCoords, NNode, raw_NodeConn, NElem, ElemConn_head, ElemConn_elem, ElemConn_next, ElemConn_prev, ElemConn_tail, ElemConn_size, ElemLabels ) if check_numba(): import numba from numba.experimental import jitclass from numba.experimental import structref from numba.extending import overload_method, overload_attribute # StructRef definition @structref.register class dmesh_type(numba.types.StructRef): def preprocess_fields(self, fields): return tuple((name, numba.types.unliteral(typ)) for name, typ in fields) # # Python proxy for the StructRef class dmesh(structref.StructRefProxy): """ A specialized mesh class intended for dynamic modification of mesh connectivity. The dmesh (Dynamic Mesh) class only supports single element type meshes, and only element types that contain a single face type (e.g. wedge elements aren't supported because they have both quadrilateral and triangular faces). It's recommended that a :class:`dmesh` object is created from a :class:`mesh` object using :meth:`mesh.mesh2dmesh`. Incorrect initialization of the mesh will likely lead to misbehavior. See also: :ref:`Dynamic mesh` Parameters ---------- NodeCoords : np.ndarray(dtype=np.float64) Node coordinates array with shape=(n,3) NodeConn : np.ndarray Node connectivity of elements with shape=(l,m) """ def __new__(cls, raw_NodeCoords, raw_NodeConn, ElemConn, ElemLabels=None): (raw_NodeCoords, NNode, raw_NodeConn, NElem, ElemConn_head, ElemConn_elem, ElemConn_next, ElemConn_prev, ElemConn_tail, ElemConn_size, ElemLabels) = dmesh_init(raw_NodeCoords, raw_NodeConn, ElemConn, ElemLabels) return structref.StructRefProxy.__new__(cls, raw_NodeCoords, NNode, raw_NodeConn, NElem, ElemConn_head, ElemConn_elem, ElemConn_next, ElemConn_prev, ElemConn_tail, ElemConn_size, ElemLabels ) def __repr__(self): return 'Dynamic Mesh Object\n{0:d} Nodes\n{1:d} Elements'.format(self.NNode,self.NElem) # Attributes: @property def raw_NodeCoords(self): return dmesh_get_raw_NodeCoords(self) @property def NodeCoords(self): return dmesh_get_NodeCoords(self) @property def NNode(self): return dmesh_get_NNode(self) @property def raw_NodeConn(self): return dmesh_get_raw_NodeConn(self) @property def NodeConn(self): return dmesh_get_NodeConn(self) @property def NElem(self): return dmesh_get_NElem(self) @property def ElemConn_head(self): return dmesh_get_ElemConn_head(self) @property def ElemConn_elem(self): return dmesh_get_ElemConn_elem(self) @property def ElemConn_next(self): return dmesh_get_ElemConn_next(self) @property def ElemConn_prev(self): return dmesh_get_ElemConn_prev(self) @property def ElemConn_tail(self): return dmesh_get_ElemConn_tail(self) @property def ElemConn_size(self): return dmesh_get_ElemConn_size(self) @property def ElemLabels(self): return dmesh_get_ElemLabels(self) # Methods: def addNodes(self,NodeCoords): dmesh_addNodes(self, NodeCoords) def addElem(self,NodeConn,Label=0): dmesh_addElem(self,NodeConn,Label) def getElemConn(self,NodeId): return dmesh_getElemConn(self,NodeId) def addElemConn(self, NodeId, ElemId): dmesh_addElemConn(self, NodeId, ElemId) def removeElemConn(self, NodeId, ElemId): dmesh_removeElemConn(self, NodeId, ElemId) def swapNode(self, NodeId1, NodeId2): dmesh_swapNode(self, NodeId1, NodeId2) def removeElem(self, ElemId): dmesh_removeElem(self, ElemId) def removeElems(self, ElemIds): dmesh_removeElems(self, ElemIds) # Attribute overloads @overload_attribute(dmesh_type, 'NodeCoords') def ol_dmesh_NodeCoords(self): def get(self): return dmesh_get_NodeCoords(self) return get @overload_attribute(dmesh_type, 'NodeConn') def ol_dmesh_NodeConn(self): def get(self): return dmesh_get_NodeConn(self) return get # Method overloads: @overload_method(dmesh_type, 'addNodes') def ol_dmesh_addNodes(self,NodeCoords): def impl(self,NodeCoords): return dmesh_addNodes(self,NodeCoords) return impl @overload_method(dmesh_type, 'addElem') def ol_dmesh_addElem(self,NodeConn,Label=0): def impl(self,NodeConn, Label=0): return dmesh_addElem(self,NodeConn,Label) return impl @overload_method(dmesh_type, 'getElemConn') def ol_dmesh_getElemConn(self, NodeId): def impl(self, NodeId): return dmesh_getElemConn(self, NodeId) return impl @overload_method(dmesh_type, 'addElemConn') def ol_dmesh_addElemConn(self, NodeId, ElemId): def impl(self, NodeId, ElemId): return dmesh_addElemConn(self, NodeId, ElemId) return impl @overload_method(dmesh_type, 'removeElemConn') def ol_dmesh_removeElemConn(self, NodeId, ElemId): def impl(self, NodeId, ElemId): return dmesh_removeElemConn(self, NodeId, ElemId) return impl @overload_method(dmesh_type, 'swapNode') def ol_dmesh_swapNode(self, NodeId1, NodeId2): def impl(self, NodeId1, NodeId2): return dmesh_swapNode(self, NodeId1, NodeId2) return impl @overload_method(dmesh_type, 'removeElem') def ol_dmesh_removeElem(self, ElemId): def impl(self, ElemId): return dmesh_removeElem(self, ElemId) return impl @overload_method(dmesh_type, 'removeElems') def ol_dmesh_removeElems(self, ElemIds): def impl(self, ElemIds): return dmesh_removeElems(self, ElemIds) return impl # Associate the proxy with the StructRef structref.define_proxy(dmesh, dmesh_type, ['raw_NodeCoords', 'NNode', 'raw_NodeConn', 'NElem', 'ElemConn_head', 'ElemConn_elem', 'ElemConn_next', 'ElemConn_prev', 'ElemConn_tail', 'ElemConn_size', 'ElemLabels' ]) else:
[docs] class dmesh(): """ A specialized mesh class intended for dynamic modification of mesh connectivity. The dmesh (Dynamic Mesh) class only supports single element type meshes, and only element types that contain a single face type (e.g. wedge elements aren't supported because they have both quadrilateral and triangular faces). It's recommended that a :class:`dmesh` object is created from a :class:`mesh` object using :meth:`mesh.mesh2dmesh`. Incorrect initialization of the mesh will likely lead to misbehavior. Parameters ---------- NodeCoords : np.ndarray(dtype=np.float64) Node coordinates array with shape=(n,3) NodeConn : np.ndarray Node connectivity of elements with shape=(l,m) """ def __init__(self, raw_NodeCoords, raw_NodeConn, ElemConn, ElemLabels=None): (self.raw_NodeCoords, self.NNode, self.raw_NodeConn, self.NElem, self.ElemConn_head, self.ElemConn_elem, self.ElemConn_next, self.ElemConn_prev, self.ElemConn_tail, self.ElemConn_size, self.ElemLabels) = dmesh_init(raw_NodeCoords, raw_NodeConn, ElemConn, ElemLabels) def __repr__(self): return 'Dynamic Mesh Object\n{0:d} Nodes\n{1:d} Elements'.format(self.NNode,self.NElem) # Attributes: @property def NodeCoords(self): return dmesh_get_NodeCoords(self) @property def NodeConn(self): return dmesh_get_NodeConn(self) # Methods:
[docs] def addNodes(self,NodeCoords): dmesh_addNodes(self, NodeCoords)
[docs] def addElem(self,NodeConn,Label=0): dmesh_addElem(self,NodeConn,Label)
[docs] def getElemConn(self,NodeId): return dmesh_getElemConn(self,NodeId)
[docs] def addElemConn(self, NodeId, ElemId): dmesh_addElemConn(self, NodeId, ElemId)
[docs] def removeElemConn(self, NodeId, ElemId): dmesh_removeElemConn(self, NodeId, ElemId)
[docs] def swapNode(self, NodeId1, NodeId2): dmesh_swapNode(self, NodeId1, NodeId2)
[docs] def removeElem(self, ElemId): dmesh_removeElem(self, ElemId)
[docs] def removeElems(self, ElemIds): dmesh_removeElems(self, ElemIds)