Source code for microstructpy.meshing.trimesh

"""Triangle/Tetrahedron Meshing

This module contains the class definition for the TriMesh class.

"""
# --------------------------------------------------------------------------- #
#                                                                             #
# Import Modules                                                              #
#                                                                             #
# --------------------------------------------------------------------------- #


from __future__ import division
from __future__ import print_function

import meshpy.tet
import meshpy.triangle
import numpy as np
import pygmsh as pg
from matplotlib import collections
from matplotlib import patches
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection

from microstructpy import _misc

__all__ = ['TriMesh']
__author__ = 'Kenneth (Kip) Hart'


# --------------------------------------------------------------------------- #
#                                                                             #
# TriMesh Class                                                               #
#                                                                             #
# --------------------------------------------------------------------------- #
[docs]class TriMesh(object): """Triangle/Tetrahedron mesh. The TriMesh class contains the points, facets, and elements in a triangle/ tetrahedron mesh, also called an unstructured grid. The points attribute is an Nx2 or Nx3 list of points in the mesh. The elements attribute contains the Nx3 or Nx4 list of the points at the corners of each triangle/tetrahedron. A list of facets can also be included, though it is optional and does not need to include every facet in the mesh. Attributes can also be assigned to the elements and facets, though they are also optional. Args: points (list, numpy.ndarray): List of coordinates in the mesh. elements (list, numpy.ndarray): List of indices of the points at the corners of each element. The shape should be Nx3 in 2D or Nx4 in 3D. element_attributes (list, numpy.ndarray): *(optional)* A number associated with each element. Defaults to None. facets (list, numpy.ndarray): *(optional)* A list of facets in the mesh. The shape should be Nx2 in 2D or Nx3 in 3D. Defaults to None. facet_attributes (list, numpy.ndarray): *(optional)* A number associated with each facet. Defaults to None. """ # ----------------------------------------------------------------------- # # Constructors # # ----------------------------------------------------------------------- # def __init__(self, points, elements, element_attributes=None, facets=None, facet_attributes=None): self.points = points self.elements = elements self.element_attributes = element_attributes self.facets = facets self.facet_attributes = facet_attributes
[docs] @classmethod def from_file(cls, filename): """Read TriMesh from file. This function reads in a triangular mesh from a file and creates an instance from that file. Currently the only supported file type is the output from :meth:`.write` with the ``format='str'`` option. Args: filename (str): Name of file to read from. Returns: TriMesh: An instance of the class. """ with open(filename, 'r') as file: stage = 0 pts = [] elems = [] elem_atts = [] facets = [] facet_atts = [] n_eas = 0 n_facets = 0 n_fas = 0 for line in file.readlines(): if 'Mesh Points'.lower() in line.lower(): n_pts = int(line.split(':')[1]) stage = 'points' elif 'Mesh Elements'.lower() in line.lower(): n_elems = int(line.split(':')[1]) stage = 'elements' elif 'Element Attributes'.lower() in line.lower(): n_eas = int(line.split(':')[1]) stage = 'element attributes' elif 'Facets'.lower() in line.lower(): n_facets = int(line.split(':')[1]) stage = 'facets' elif 'Facet Attributes'.lower() in line.lower(): n_fas = int(line.split(':')[1]) stage = 'facet attributes' else: if stage == 'points': pts.append([float(x) for x in line.split(',')]) elif stage == 'elements': elems.append([int(kp) for kp in line.split(',')]) elif stage == 'element attributes': elem_atts.append(_misc.from_str(line)) elif stage == 'facets': if n_facets > 0: facets.append([int(kp) for kp in line.split(',')]) elif stage == 'facet attributes': if n_fas > 0: facet_atts.append(_misc.from_str(line)) else: pass # check the inputs assert len(pts) == n_pts assert len(elems) == n_elems assert len(elem_atts) == n_eas assert len(facets) == n_facets assert len(facet_atts) == n_fas return cls(pts, elems, elem_atts, facets, facet_atts)
[docs] @classmethod def from_polymesh(cls, polymesh, phases=None, mesher='Triangle/Tetgen', min_angle=0, max_volume=float('inf'), max_edge_length=float('inf'), mesh_size=float('inf')): """Create TriMesh from PolyMesh. This constuctor creates a triangle/tetrahedron mesh from a polygon mesh (:class:`.PolyMesh`). Polygons of the same seed number are merged and the element attribute is set to the seed number it is within. The facets between seeds are saved to the mesh and the index of the facet is stored in the facet attributes. Since the PolyMesh can include phase numbers for each region, additional information about the phases can be included as an input. The "phases" input should be a list of material phase dictionaries, formatted according to the :ref:`phase_dict_guide` guide. The minimum angle, maximum volume, and maximum edge length options provide quality controls for the mesh. The phase type option can take one of several values, described below. * **crystalline**: granular, solid * **amorphous**: glass, matrix * **void**: crack, hole The **crystalline** option creates a mesh where cells of the same seed number are merged, but cells are not merged across seeds. _This is the default material type._ The **amorphous** option creates a mesh where cells of the same phase number are merged to create an amorphous region in the mesh. Finally, the **void** option will merge neighboring void cells and treat them as holes in the mesh. Args: polymesh (PolyMesh): A polygon/polyhedron mesh. phases (list): *(optional)* A list of dictionaries containing options for each phase. Default is ``{'material_type': 'solid', 'max_volume': float('inf')}``. mesher (str): {'Triangle/TetGen' | 'Triangle' | 'TetGen' | 'gmsh'} specify the mesh generator. Default is 'Triangle/TetGen'. min_angle (float): The minimum interior angle, in degrees, of an element. This option is used with Triangle or TetGen and in 3D is the minimum *dihedral* angle. Defaults to 0. max_volume (float): The default maximum cell volume, used if one is not set for each phase. This option is used with Triangle or TetGen. Defaults to infinity, which turns off this control. max_edge_length (float): The maximum edge length of elements along grain boundaries. This option is used with Triangle and gmsh. Defaults to infinity, which turns off this control. mesh_size (float): The target size of the mesh elements. This option is used with gmsh. Default is infinity, whihch turns off this control. """ key = mesher.lower().strip() if key in ('triangle/tetgen', 'triangle', 'tetgen'): tri_args = _call_meshpy(polymesh, phases, min_angle, max_volume, max_edge_length) elif key == 'gmsh': tri_args = _call_gmsh(polymesh, phases, mesh_size, max_edge_length) return cls(*tri_args)
# ----------------------------------------------------------------------- # # String and Representation Functions # # ----------------------------------------------------------------------- # def __str__(self): nv = len(self.points) nd = len(self.points[0]) pt_fmt = '\t' pt_fmt += ', '.join(['{pt[' + str(i) + ']: e}' for i in range(nd)]) str_str = 'Mesh Points: ' + str(nv) + '\n' str_str += ''.join([pt_fmt.format(pt=p) + '\n' for p in self.points]) str_str += 'Mesh Elements: ' + str(len(self.elements)) + '\n' str_str += '\n'.join(['\t' + str(tuple(e))[1:-1] for e in self.elements]) try: str_str += '\nElement Attributes: ' str_str += str(len(self.element_attributes)) + '\n' str_str += '\n'.join(['\t' + str(a) for a in self.element_attributes]) except TypeError: pass try: str_str += '\nFacets: ' + str(len(self.facets)) + '\n' str_str += '\n'.join(['\t' + str(tuple(f))[1:-1] for f in self.facets]) except TypeError: pass try: str_str += '\nFacet Attributes: ' str_str += str(len(self.facet_attributes)) + '\n' str_str += '\n'.join(['\t' + str(a) for a in self.facet_attributes]) except TypeError: pass return str_str def __repr__(self): repr_str = 'TriMesh(' repr_str += ', '.join([repr(v) for v in (self.points, self.elements, self.element_attributes, self.facets, self.facet_attributes)]) repr_str += ')' return repr_str # ----------------------------------------------------------------------- # # Write Function # # ----------------------------------------------------------------------- #
[docs] def write(self, filename, format='txt', seeds=None, polymesh=None): """Write mesh to file. This function writes the contents of the mesh to a file. The format options are 'abaqus', 'tet/tri', 'txt', and 'vtk'. See the :ref:`s_tri_file_io` section of the :ref:`c_file_formats` guide for more details on these formats. Args: filename (str): The name of the file to write. In the cases of TetGen/Triangle, this is the basename of the files. format (str): {'abaqus' | 'tet/tri' | 'txt' | 'vtk'} *(optional)* The format of the output file. Default is 'txt'. seeds (SeedList): *(optional)* List of seeds. If given, VTK files will also include the phase number of of each element in the mesh. This assumes the ``element_attributes`` field contains the seed number of each element. polymesh (PolyMesh): *(optional)* Polygonal mesh used for generating the triangular mesh. If given, will add surface unions to Abaqus files - for easier specification of boundary conditions. """ # NOQA: E501 fmt = format.lower() if fmt == 'abaqus': # write top matter abaqus = '*Heading\n' abaqus += '** Job name: microstructure ' abaqus += 'Model name: microstructure_model\n' abaqus += '** Generated by: MicroStructPy\n' # write parts abaqus += '**\n** PARTS\n**\n' abaqus += '*Part, name=Part-1\n' abaqus += '*Node\n' abaqus += ''.join([str(i + 1) + ''.join([', ' + str(x) for x in pt]) + '\n' for i, pt in enumerate(self.points)]) n_dim = len(self.points[0]) elem_type = {2: 'CPS3', 3: 'C3D4'}[n_dim] abaqus += '*Element, type=' + elem_type + '\n' abaqus += ''.join([str(i + 1) + ''.join([', ' + str(int(kp) + 1) for kp in elm]) + '\n' for i, elm in enumerate(self.elements)]) # Element sets - seed number elset_n_per = 16 elem_atts = np.array(self.element_attributes) for att in np.unique(elem_atts): elset_name = 'Set-E-Seed-' + str(att) elset_str = '*Elset, elset=' + elset_name + '\n' elem_groups = [[]] for elem_ind, elem_att in enumerate(elem_atts): if ~np.isclose(elem_att, att): continue if len(elem_groups[-1]) >= elset_n_per: elem_groups.append([]) elem_groups[-1].append(elem_ind + 1) for group in elem_groups: elset_str += ','.join([str(i) for i in group]) elset_str += '\n' abaqus += elset_str # Element Sets - phase number if seeds is not None: phase_nums = np.array([seed.phase for seed in seeds]) for phase_num in np.unique(phase_nums): mask = phase_nums == phase_num seed_nums = np.nonzero(mask)[0] elset_name = 'Set-E-Material-' + str(phase_num) elset_str = '*Elset, elset=' + elset_name + '\n' groups = [[]] for seed_num in seed_nums: if seed_num not in elem_atts: continue if len(groups[-1]) >= elset_n_per: groups.append([]) seed_elset_name = 'Set-E-Seed-' + str(seed_num) groups[-1].append(seed_elset_name) for group in groups: elset_str += ','.join(group) elset_str += '\n' abaqus += elset_str # Surfaces - Exterior and Interior facets = np.array(self.facets) facet_atts = np.array(self.facet_attributes) face_ids = {2: [2, 3, 1], 3: [3, 4, 2, 1]}[n_dim] for att in np.unique(facet_atts): facet_name = 'Surface-' + str(att) surf_str = '*Surface, name=' + facet_name + ', type=element\n' att_facets = facets[facet_atts == att] for facet in att_facets: mask = np.isin(self.elements, facet) n_match = mask.astype('int').sum(axis=1) i_elem = np.argmax(n_match) elem_id = i_elem + 1 i_missing = np.argmin(mask[i_elem]) face_id = face_ids[i_missing] surf_str += str(elem_id) + ', S' + str(face_id) + '\n' abaqus += surf_str # Surfaces - Exterior poly_neighbors = np.array(polymesh.facet_neighbors) poly_mask = np.any(poly_neighbors < 0, axis=1) neigh_nums = np.min(poly_neighbors, axis=1) u_neighs = np.unique(neigh_nums[poly_mask]) for neigh_num in u_neighs: mask = neigh_nums == neigh_num facet_name = 'Ext-Surface-' + str(-neigh_num) surf_str = '*Surface, name=' + facet_name + ', combine=union\n' for i, flag in enumerate(mask): if flag: surf_str += 'Surface-' + str(i) + '\n' abaqus += surf_str # End Part abaqus += '*End Part\n\n' # Assembly abaqus += '**\n' abaqus += '** ASSEMBLY\n' abaqus += '**\n' abaqus += '*Assembly, name=assembly\n' abaqus += '**\n' # Instances abaqus += '*Instance, name=I-Part-1, part=Part-1\n' abaqus += '*End Instance\n' # End Assembly abaqus += '**\n' abaqus += '*End Assembly\n' with open(filename, 'w') as file: file.write(abaqus) elif fmt in ('str', 'txt'): with open(filename, 'w') as file: file.write(str(self) + '\n') elif fmt == 'tet/tri': # create boundary markers bnd_mkrs = np.full(len(self.points), 0, dtype='int') facet_arr = np.array(self.facets) f_bnd_mkrs = np.full(len(self.facets), 0, dtype='int') elem_arr = np.array(self.elements) for elem in self.elements: for i in range(len(elem)): e_facet = np.delete(elem, i) f_mask = np.full(elem_arr.shape[0], True) for kp in e_facet: f_mask &= np.any(elem_arr == kp, axis=-1) if np.sum(f_mask) == 1: bnd_mkrs[e_facet] = 1 f_mask = np.full(facet_arr.shape[0], True) for kp in e_facet: f_mask &= np.any(facet_arr == kp, axis=-1) f_bnd_mkrs[f_mask] = 1 # write vertices n_pts, n_dim = np.array(self.points).shape nodes = ' '.join([str(n) for n in (n_pts, n_dim, 0, 1)]) + '\n' nodes += ''.join([str(i) + ''.join([' ' + str(x) for x in pt]) + ' ' + str(bnd_mkrs[i]) + '\n' for i, pt in enumerate(self.points)]) with open(filename + '.node', 'w') as file: file.write(nodes) # write elements n_ele, n_kp = np.array(self.elements).shape is_att = self.element_attributes is not None n_att = int(is_att) eles = ' '.join([str(n) for n in (n_ele, n_kp, n_att)]) + '\n' for i, simplex in enumerate(self.elements): e_str = ' '.join([str(kp) for kp in simplex]) if is_att: e_str += ' ' + str(self.element_attributes[i]) e_str += '\n' eles += e_str with open(filename + '.ele', 'w') as file: file.write(eles) # Write edges/faces if self.facets is not None: ext = {2: '.edge', 3: '.face'}[n_dim] n_facet, n_kp = np.array(self.facets).shape edge = ' '.join([str(n) for n in (n_facet, n_kp, 1)]) edge += ''.join([str(i) + ''.join([' ' + str(k) for k in f]) + ' ' + str(mkr) + '\n' for f, mkr in zip(self.facets, f_bnd_mkrs)]) with open(filename + ext, 'w') as file: file.write(edge) elif fmt == 'vtk': n_kp = len(self.elements[0]) mesh_type = {3: 'Triangular', 4: 'Tetrahedral'}[n_kp] pt_fmt = '{: f} {: f} {: f}\n' # write heading vtk = '# vtk DataFile Version 2.0\n' vtk += '{} mesh\n'.format(mesh_type) vtk += 'ASCII\n' vtk += 'DATASET UNSTRUCTURED_GRID\n' # Write points vtk += 'POINTS ' + str(len(self.points)) + ' float\n' if len(self.points[0]) == 2: vtk += ''.join([pt_fmt.format(x, y, 0) for x, y in self.points]) else: vtk += ''.join([pt_fmt.format(x, y, z) for x, y, z in self.points]) # write elements n_elem = len(self.elements) cell_fmt = str(n_kp) + n_kp * ' {}' + '\n' cell_sz = (1 + n_kp) * n_elem vtk += '\nCELLS ' + str(n_elem) + ' ' + str(cell_sz) + '\n' vtk += ''.join([cell_fmt.format(*el) for el in self.elements]) # write cell type vtk += '\nCELL_TYPES ' + str(n_elem) + '\n' cell_type = {3: '5', 4: '10'}[n_kp] vtk += ''.join(n_elem * [cell_type + '\n']) # write element attributes try: int(self.element_attributes[0]) att_type = 'int' except TypeError: att_type = 'float' vtk += '\nCELL_DATA ' + str(n_elem) + '\n' vtk += 'SCALARS element_attributes ' + att_type + ' 1 \n' vtk += 'LOOKUP_TABLE element_attributes\n' vtk += ''.join([str(a) + '\n' for a in self.element_attributes]) # Write phase numbers if seeds is not None: vtk += '\nSCALARS phase_numbers int 1 \n' vtk += 'LOOKUP_TABLE phase_numbers\n' vtk += ''.join([str(seeds[a].phase) + '\n' for a in self.element_attributes]) with open(filename, 'w') as file: file.write(vtk) else: e_str = 'Cannot write file type ' + str(format) + ' yet.' raise NotImplementedError(e_str)
# ----------------------------------------------------------------------- # # Plot Function # # ----------------------------------------------------------------------- #
[docs] def plot(self, index_by='element', material=[], loc=0, **kwargs): """Plot the mesh. This method plots the mesh using matplotlib. In 2D, this creates a :class:`matplotlib.collections.PolyCollection` and adds it to the current axes. In 3D, it creates a :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection` and adds it to the current axes. The keyword arguments are passed though to matplotlib. Args: index_by (str): *(optional)* {'element' | 'attribute'} Flag for indexing into the other arrays passed into the function. For example, ``plot(index_by='attribute', color=['blue', 'red'])`` will plot the elements with ``element_attribute`` equal to 0 in blue, and elements with ``element_attribute`` equal to 1 in red. Note that in 3D the facets are plotted instead of the elements, so kwarg lists must be based on ``facets`` and ``facet_attributes``. Defaults to 'element'. material (list): *(optional)* Names of material phases. One entry per material phase (the ``index_by`` argument is ignored). If this argument is set, a legend is added to the plot with one entry per material. Note that the ``element_attributes`` in 2D or the ``facet_attributes`` in 3D must be the material numbers for the legend to be formatted properly. loc (int or str): *(optional)* The location of the legend, if 'material' is specified. This argument is passed directly through to :func:`matplotlib.pyplot.legend`. Defaults to 0, which is 'best' in matplotlib. **kwargs: Keyword arguments that are passed through to matplotlib. """ n_dim = len(self.points[0]) if n_dim == 2 or plt.gcf().axes: ax = plt.gca() else: ax = plt.gcf().add_subplot(projection=Axes3D.name) n_obj = _misc.ax_objects(ax) if n_obj > 0: xlim = ax.get_xlim() ylim = ax.get_ylim() else: xlim = [float('inf'), -float('inf')] ylim = [float('inf'), -float('inf')] if n_dim == 2: _plot_2d(ax, self, index_by, **kwargs) else: if n_obj > 0: zlim = ax.get_zlim() else: zlim = [float('inf'), -float('inf')] xy = [np.array([self.points[kp] for kp in f]) for f in self.facets] plt_kwargs = {} for key, value in kwargs.items(): if type(value) in (list, np.array): plt_value = [] for f_num, f_att in enumerate(self.facet_attributes): if index_by == 'element': ind = f_num elif index_by == 'attribute': ind = int(f_att) else: e_str = 'Cannot index by {}.'.format(index_by) raise ValueError(e_str) if ind < len(value): v = value[ind] else: v = 'none' plt_value.append(v) else: plt_value = value plt_kwargs[key] = plt_value pc = Poly3DCollection(xy, **plt_kwargs) ax.add_collection(pc) # Add legend if material and index_by == 'attribute': p_kwargs = [{'label': m} for m in material] for key, value in kwargs.items(): if type(value) not in (list, np.array): for kws in p_kwargs: kws[key] = value for i, m in enumerate(material): if type(value) in (list, np.array): p_kwargs[i][key] = value[i] else: p_kwargs[i][key] = value # Replace plural keywords for p_kw in p_kwargs: for kw in _misc.mpl_plural_kwargs: if kw in p_kw: p_kw[kw[:-1]] = p_kw[kw] del p_kw[kw] handles = [patches.Patch(**p_kw) for p_kw in p_kwargs] ax.legend(handles=handles, loc=loc) # Adjust Axes mins = np.array(self.points).min(axis=0) maxs = np.array(self.points).max(axis=0) xlim = (min(xlim[0], mins[0]), max(xlim[1], maxs[0])) ylim = (min(ylim[0], mins[1]), max(ylim[1], maxs[1])) if n_dim == 2: plt.axis('square') plt.xlim(xlim) plt.ylim(ylim) elif n_dim == 3: zlim = (min(zlim[0], mins[2]), max(zlim[1], maxs[2])) ax.set_xlim(xlim) ax.set_ylim(ylim) ax.set_zlim(zlim) _misc.axisEqual3D(ax)
# --------------------------------------------------------------------------- # # # # RasterMesh Class # # # # --------------------------------------------------------------------------- #
[docs]class RasterMesh(TriMesh): """Raster mesh. The RasterMesh class contains the points and elements in a raster mesh, also called an regular grid. The points attribute is an Nx2 or Nx3 list of points in the mesh. The elements attribute contains the Nx4 or Nx8 list of the points at the corners of each pixel/voxel. A list of facets can also be included, though it is optional and does not need to include every facet in the mesh. Attributes can also be assigned to the elements and facets, though they are also optional. Args: points (list, numpy.ndarray): List of coordinates in the mesh. elements (list, numpy.ndarray): List of indices of the points at the corners of each element. The shape should be Nx3 in 2D or Nx4 in 3D. element_attributes (list, numpy.ndarray): *(optional)* A number associated with each element. Defaults to None. facets (list, numpy.ndarray): *(optional)* A list of facets in the mesh. The shape should be Nx2 in 2D or Nx3 in 3D. Defaults to None. facet_attributes (list, numpy.ndarray): *(optional)* A number associated with each facet. Defaults to None. """ # ----------------------------------------------------------------------- # # Constructors # # ----------------------------------------------------------------------- # # Inherited from TriMesh
[docs] @classmethod def from_polymesh(cls, polymesh, mesh_size, phases=None): """Create RasterMesh from PolyMesh. This constuctor creates a raster mesh from a polygon mesh (:class:`.PolyMesh`). Polygons of the same seed number are merged and the element attribute is set to the seed number it is within. The facets between seeds are saved to the mesh and the index of the facet is stored in the facet attributes. Since the PolyMesh can include phase numbers for each region, additional information about the phases can be included as an input. The "phases" input should be a list of material phase dictionaries, formatted according to the :ref:`phase_dict_guide` guide. The mesh_size option determines the side length of each pixel/voxel. Element attributes are sampled at the center of each pixel/voxel. If an edge of a domain is not an integer multiple of the mesh_size, it will be clipped. For example, if mesh_size is 3 and an edge has bounds [0, 11], the sides of the pixels will be at 0, 3, 6, and 9 while the centers of the pixels will be at 1.5, 4.5, 7.5. The phase type option can take one of several values, described below. * **crystalline**: granular, solid * **amorphous**: glass, matrix * **void**: crack, hole The **crystalline** option creates a mesh where cells of the same seed number are merged, but cells are not merged across seeds. _This is the default material type._ The **amorphous** option creates a mesh where cells of the same phase number are merged to create an amorphous region in the mesh. Finally, the **void** option will merge neighboring void cells and treat them as holes in the mesh. Args: polymesh (PolyMesh): A polygon/polyhedron mesh. mesh_size (float): The side length of each pixel/voxel. phases (list): *(optional)* A list of dictionaries containing options for each phase. Default is ``{'material_type': 'solid', 'max_volume': float('inf')}``. """ # 1. Create node and element grids p_pts = np.array(polymesh.points) mins = p_pts.min(axis=0) maxs = p_pts.max(axis=0) lens = (maxs - mins)*(1 + 1e-9) sides = [lb + np.arange(0, dlen, mesh_size) for lb, dlen in zip(mins, lens)] mgrid = np.meshgrid(*sides) nodes = np.array([g.flatten() for g in mgrid]).T node_nums = np.arange(mgrid[0].size).reshape(mgrid[0].shape) n_dim = len(mins) if n_dim == 2: m, n = node_nums.shape kp1 = node_nums[:(m-1), :(n-1)].flatten() kp2 = node_nums[1:m, :(n-1)].flatten() kp3 = node_nums[1:m, 1:n].flatten() kp4 = node_nums[:(m-1), 1:n].flatten() elems = np.array([kp1, kp2, kp3, kp4]).T elif n_dim == 3: m, n, p = node_nums.shape kp1 = node_nums[:(m-1), :(n-1), :(p-1)].flatten() kp2 = node_nums[1:m, :(n-1), :(p-1)].flatten() kp3 = node_nums[1:m, 1:n, :(p-1)].flatten() kp4 = node_nums[:(m-1), 1:n, :(p-1)].flatten() kp5 = node_nums[:(m-1), :(n-1), 1:p].flatten() kp6 = node_nums[1:m, :(n-1), 1:p].flatten() kp7 = node_nums[1:m, 1:n, 1:p].flatten() kp8 = node_nums[:(m-1), 1:n, 1:p].flatten() elems = np.array([kp1, kp2, kp3, kp4, kp5, kp6, kp7, kp8]).T else: raise NotImplementedError # 2. Compute element centers cens = nodes[elems[:, 0]] + 0.5 * mesh_size # 3. For each region: i_remain = np.arange(cens.shape[0]) elem_regs = np.full(cens.shape[0], -1) elem_atts = np.full(cens.shape[0], -1) for r_num, region in enumerate(polymesh.regions): # A. Create a bounding box r_kps = np.unique([k for f in region for k in polymesh.facets[f]]) r_pts = p_pts[r_kps] r_mins = r_pts.min(axis=0) r_maxs = r_pts.max(axis=0) # B. Isolate element centers with box r_i_remain = np.copy(i_remain) for i, lb in enumerate(r_mins): ub = r_maxs[i] x = cens[r_i_remain, i] in_range = (x >= lb) & (x <= ub) r_i_remain = r_i_remain[in_range] # C. For each facet, remove centers on the wrong side # note: regions are convex, so mean pt is on correct side of facets r_cen = r_pts.mean(axis=0) for f in region: f_kps = polymesh.facets[f] f_pts = p_pts[f_kps] u_in, f_cen = _facet_in_normal(f_pts, r_cen) rel_pos = cens[r_i_remain] - f_cen dp = rel_pos.dot(u_in) inside = dp >= 0 r_i_remain = r_i_remain[inside] # D. Assign remaining centers to region elem_regs[r_i_remain] = r_num elem_atts[r_i_remain] = polymesh.seed_numbers[r_num] i_remain = np.setdiff1d(i_remain, r_i_remain) # 4. Combine regions of the same seed number if phases is not None: conv_dict = _amorphous_seed_numbers(polymesh, phases) elem_atts = np.array([conv_dict.get(s, s) for s in elem_atts]) # 5. Define remaining facets, inherit their attributes facets = [] facet_atts = [] for f_num, f_neighs in enumerate(polymesh.facet_neighbors): n1, n2 = f_neighs if n1 >= 0: e1 = elems[elem_regs == n1] e2 = elems[elem_regs == n2] # Shift +x e1_s = e1[:, 1] e2_s = e2[:, 0] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: if n_dim == 2: facet = elem[[1, 2]] else: facet = elem[[1, 2, 6, 5]] facets.append(facet) facet_atts.append(f_num) # Shift -x e1_s = e1[:, 0] e2_s = e2[:, 1] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: if n_dim == 2: facet = elem[[3, 0]] else: facet = elem[[0, 4, 7, 3]] facets.append(facet) facet_atts.append(f_num) # Shift +y e1_s = e1[:, 3] e2_s = e2[:, 0] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: if n_dim == 2: facet = elem[[2, 3]] else: facet = elem[[2, 3, 7, 6]] facets.append(facet) facet_atts.append(f_num) # Shift -y e1_s = e1[:, 0] e2_s = e2[:, 3] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: if n_dim == 2: facet = elem[[0, 1]] else: facet = elem[[0, 1, 5, 4]] facets.append(facet) facet_atts.append(f_num) if n_dim < 3: continue # Shift +z e1_s = e1[:, 4] e2_s = e1[:, 0] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: facet = elem[[4, 5, 6, 7]] facets.append(facet) facet_atts.append(f_num) # Shift -z e1_s = e1[:, 0] e2_s = e1[:, 4] mask = np.isin(e1_s, e2_s) for elem in e1[mask]: facet = elem[[0, 1, 2, 3]] facets.append(facet) facet_atts.append(f_num) elif n1 == -1: # -x face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 0], 0] mask = np.isclose(x2, mins[0]) for elem in e2[mask]: if n_dim == 2: facet = elem[[3, 0]] else: facet = elem[[0, 4, 7, 3]] facets.append(facet) facet_atts.append(f_num) elif n1 == -2: # +x face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 1], 0] mask = np.isclose(x2, maxs[0]) for elem in e2[mask]: if n_dim == 2: facet = elem[[1, 2]] else: facet = elem[[1, 2, 6, 5]] facets.append(facet) facet_atts.append(f_num) elif n1 == -3: # -y face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 0], 1] mask = np.isclose(x2, mins[1]) for elem in e2[mask]: if n_dim == 2: facet = elem[[0, 1]] else: facet = elem[[0, 1, 5, 4]] facets.append(facet) facet_atts.append(f_num) elif n1 == -4: # +y face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 2], 1] mask = np.isclose(x2, maxs[1]) for elem in e2[mask]: if n_dim == 2: facet = elem[[2, 3]] else: facet = elem[[2, 3, 7, 6]] facets.append(facet) facet_atts.append(f_num) elif n1 == -5: # -z face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 0], 2] mask = np.isclose(x2, mins[2]) for elem in e2[mask]: facet = elem[[0, 1, 2, 3]] facets.append(facet) facet_atts.append(f_num) elif n1 == -6: # +z face e2 = elems[elem_regs == n2] x2 = nodes[e2[:, 4], 2] mask = x2 == maxs[2] for elem in e2[mask]: facet = elem[[4, 5, 6, 7]] facets.append(facet) facet_atts.append(f_num) # 6. Remove voids and excess cells if phases is not None: att_rm = [-1] for i, phase in enumerate(phases): if phase.get('material_type', 'solid') in _misc.kw_void: r_mask = np.array(polymesh.phase_numbers) == i seeds = np.unique(np.array(polymesh.seed_numbers)[r_mask]) att_rm.extend(list(seeds)) # Remove elements rm_mask = np.isin(elem_atts, att_rm) elems = elems[~rm_mask] elem_atts = elem_atts[~rm_mask] # Re-number nodes nodes_mask = np.isin(np.arange(nodes.shape[0]), elems) n_remain = np.sum(nodes_mask) node_n_conv = np.arange(nodes.shape[0]) node_n_conv[nodes_mask] = np.arange(n_remain) nodes = nodes[nodes_mask] elems = node_n_conv[elems] if len(facets) > 0: f_keep = np.all(nodes_mask[facets], axis=1) facets = node_n_conv[np.array(facets)[f_keep, :]] facet_atts = np.array(facet_atts)[f_keep] return cls(nodes, elems, elem_atts, facets, facet_atts)
# ----------------------------------------------------------------------- # # String and Representation Functions # # ----------------------------------------------------------------------- # # __str__ inherited from TriMesh def __repr__(self): repr_str = 'RasterMesh(' repr_str += ', '.join([repr(v) for v in (self.points, self.elements, self.element_attributes, self.facets, self.facet_attributes)]) repr_str += ')' return repr_str # ----------------------------------------------------------------------- # # Write Function # # ----------------------------------------------------------------------- #
[docs] def write(self, filename, format='txt', seeds=None, polymesh=None): """Write mesh to file. This function writes the contents of the mesh to a file. The format options are 'abaqus', 'txt', and 'vtk'. See the :ref:`s_tri_file_io` section of the :ref:`c_file_formats` guide for more details on these formats. VTK files use the `RECTILINEAR_GRID` data type. Args: filename (str): The name of the file to write. format (str): {'abaqus' | 'txt' | 'vtk'} *(optional)* The format of the output file. Default is 'txt'. seeds (SeedList): *(optional)* List of seeds. If given, VTK files will also include the phase number of of each element in the mesh. This assumes the ``element_attributes`` field contains the seed number of each element. polymesh (PolyMesh): *(optional)* Polygonal mesh used for generating the raster mesh. If given, will add surface unions to Abaqus files - for easier specification of boundary conditions. """ # NOQA: E501 fmt = format.lower() if fmt == 'abaqus': # write top matter abaqus = '*Heading\n' abaqus += '** Job name: microstructure ' abaqus += 'Model name: microstructure_model\n' abaqus += '** Generated by: MicroStructPy\n' # write parts abaqus += '**\n** PARTS\n**\n' abaqus += '*Part, name=Part-1\n' abaqus += '*Node\n' abaqus += ''.join([str(i + 1) + ''.join([', ' + str(x) for x in pt]) + '\n' for i, pt in enumerate(self.points)]) n_dim = len(self.points[0]) elem_type = {2: 'CPS4', 3: 'C3D8'}[n_dim] abaqus += '*Element, type=' + elem_type + '\n' abaqus += ''.join([str(i + 1) + ''.join([', ' + str(kp + 1) for kp in elem]) + '\n' for i, elem in enumerate(self.elements)]) # Element sets - seed number elset_n_per = 16 elem_atts = np.array(self.element_attributes) for att in np.unique(elem_atts): elset_name = 'Set-E-Seed-' + str(att) elset_str = '*Elset, elset=' + elset_name + '\n' elem_groups = [[]] for elem_ind, elem_att in enumerate(elem_atts): if ~np.isclose(elem_att, att): continue if len(elem_groups[-1]) >= elset_n_per: elem_groups.append([]) elem_groups[-1].append(elem_ind + 1) for group in elem_groups: elset_str += ','.join([str(i) for i in group]) elset_str += '\n' abaqus += elset_str # Element Sets - phase number if seeds is not None: phase_nums = np.array([seed.phase for seed in seeds]) for phase_num in np.unique(phase_nums): mask = phase_nums == phase_num seed_nums = np.nonzero(mask)[0] elset_name = 'Set-E-Material-' + str(phase_num) elset_str = '*Elset, elset=' + elset_name + '\n' groups = [[]] for seed_num in seed_nums: if seed_num not in elem_atts: continue if len(groups[-1]) >= elset_n_per: groups.append([]) seed_elset_name = 'Set-E-Seed-' + str(seed_num) groups[-1].append(seed_elset_name) for group in groups: elset_str += ','.join(group) elset_str += '\n' abaqus += elset_str # Surfaces - Exterior and Interior facets = np.array(self.facets) facet_atts = np.array(self.facet_attributes) face_ids = {2: [2, 3, 1], 3: [3, 4, 2, 1]}[n_dim] for att in np.unique(facet_atts): facet_name = 'Surface-' + str(att) surf_str = '*Surface, name=' + facet_name + ', type=element\n' att_facets = facets[facet_atts == att] for facet in att_facets: mask = np.isin(self.elements, facet) n_match = mask.astype('int').sum(axis=1) i_elem = np.argmax(n_match) elem_id = i_elem + 1 i_missing = np.argmin(mask[i_elem]) face_id = face_ids[i_missing] surf_str += str(elem_id) + ', S' + str(face_id) + '\n' abaqus += surf_str # Surfaces - Exterior poly_neighbors = np.array(polymesh.facet_neighbors) poly_mask = np.any(poly_neighbors < 0, axis=1) neigh_nums = np.min(poly_neighbors, axis=1) u_neighs = np.unique(neigh_nums[poly_mask]) for neigh_num in u_neighs: mask = neigh_nums == neigh_num facet_name = 'Ext-Surface-' + str(-neigh_num) surf_str = '*Surface, name=' + facet_name + ', combine=union\n' for i, flag in enumerate(mask): if flag: surf_str += 'Surface-' + str(i) + '\n' abaqus += surf_str # End Part abaqus += '*End Part\n\n' # Assembly abaqus += '**\n' abaqus += '** ASSEMBLY\n' abaqus += '**\n' abaqus += '*Assembly, name=assembly\n' abaqus += '**\n' # Instances abaqus += '*Instance, name=I-Part-1, part=Part-1\n' abaqus += '*End Instance\n' # End Assembly abaqus += '**\n' abaqus += '*End Assembly\n' with open(filename, 'w') as file: file.write(abaqus) elif fmt in ('str', 'txt'): with open(filename, 'w') as file: file.write(str(self) + '\n') elif fmt == 'vtk': n_kp = len(self.elements[0]) mesh_type = {4: 'Pixel', 8: 'Voxel'}[n_kp] pt_fmt = '{: f} {: f} {: f}\n' # Dimensions pts = np.array(self.points) coords = [np.unique(ax) for ax in pts.T] if len(coords) < 3: coords.append([0]) # force z=0 for 2D meshes dims = [len(c) for c in coords] n_dim = len(dims) # write heading vtk = '# vtk DataFile Version 2.0\n' vtk += '{} mesh\n'.format(mesh_type) vtk += 'ASCII\n' vtk += 'DATASET RECTILINEAR_GRID\n' vtk += 'DIMENSIONS {} {} {}\n'.format(*dims) # write points for ind, ax in enumerate(['X', 'Y', 'Z']): vtk += '{}_COORDINATES {} float\n'.format(ax, dims[ind]) line = '' for x in coords[ind]: x_str = '{:f}'.format(x) if len(line) == 0: line = x_str elif len(line) + len(' ') + len(x_str) < 80: line += ' ' + x_str else: vtk += line + '\n' line = x_str vtk += line + '\n' # write element attributes vtk += 'CELL_DATA {}\n'.format(len(self.element_attributes)) vtk += 'SCALARS element_attributes float\n' vtk += 'LOOKUP_TABLE default\n' line = '' phase_nums = '' phase_line = '' pts = np.array(self.points) elems = np.sort(self.elements) if len(coords[-1]) == 1: # 2D for y_ind in range(len(coords[1][:-1])): y_mask_ind = pts[:, 1] == coords[1][y_ind] y_mask_ip1 = pts[:, 1] == coords[1][y_ind] y_mask = y_mask_ind | y_mask_ip1 for x_ind in range(len(coords[0][:-1])): # mask self.points x_mask_ind = pts[:, 0] == coords[0][x_ind] x_mask_ip1 = pts[:, 0] == coords[0][x_ind + 1] x_mask = x_mask_ind | x_mask_ip1 mask = x_mask & y_mask el = np.where(mask) e_ind = np.where(np.all(elems == el, axis=1))[0][0] # element attribute att = self.element_attributes[e_ind] att_str = '{:f}'.format(att) if len(line) == 0: line += att_str elif len(line) + len(' ') + len(att_str) < 80: line += ' ' + att_str else: vtk += line + '\n' line = att_str # phase number if seeds is not None: phase = seeds[att].phase p_str = str(int(phase)) if len(phase_line) == 0: phase_line = p_str elif len(line) + len(' ') + len(p_str) < 80: phase_line += ' ' + p_str else: phase_nums += phase_line + '\n' phase_line = p_str vtk += line + '\n' if seeds is not None: vtk += 'SCALARS phase_numbers int\n' vtk += 'LOOKUP_TABLE default\n' vtk += phase_nums + phase_line + '\n' else: for z_ind in range(len(coords[2][:-1])): z_mask_ind = pts[:, 2] == coords[2][z_ind] z_mask_ip1 = pts[:, 2] == coords[2][z_ind + 1] z_mask = z_mask_ind | z_mask_ip1 for y_ind in range(len(coords[1][:-1])): y_mask_ind = pts[:, 1] == coords[1][y_ind] y_mask_ip1 = pts[:, 1] == coords[1][y_ind + 1] y_mask = y_mask_ind | y_mask_ip1 for x_ind in range(len(coords[0][:-1])): # mask self.points x_mask_ind = pts[:, 0] == coords[0][x_ind] x_mask_ip1 = pts[:, 0] == coords[0][x_ind + 1] x_mask = x_mask_ind | x_mask_ip1 mask = x_mask & y_mask & z_mask el = np.where(mask) e_ind = np.where(np.all(elems == el, axis=1))[0][0] # element attribute att = self.element_attributes[e_ind] att_str = '{:f}'.format(att) if len(line) == 0: line += att_str elif len(line) + len(' ') + len(att_str) < 80: line += ' ' + att_str else: vtk += line + '\n' line = att_str # phase number if seeds is not None: phase = seeds[att].phase p_str = str(int(phase)) if len(phase_line) == 0: phase_line = p_str elif len(line) + len(' ') + len(p_str) < 80: phase_line += ' ' + p_str else: phase_nums += phase_line + '\n' phase_line = p_str vtk += line + '\n' if seeds is not None: vtk += 'SCALARS phase_numbers int\n' vtk += 'LOOKUP_TABLE default\n' vtk += phase_nums + phase_line + '\n' with open(filename, 'w') as file: file.write(vtk) else: e_str = 'Cannot write file type ' + str(format) + ' yet.' raise NotImplementedError(e_str)
# ----------------------------------------------------------------------- # # As Array Functions # # ----------------------------------------------------------------------- # @property def mesh_size(self): """Side length of elements.""" e0 = self.elements[0] s0 = np.array(self.points[e0[1]]) - np.array(self.points[e0[0]]) return np.linalg.norm(s0)
[docs] def as_array(self, element_attributes=True): """numpy.ndarray containing element attributes. Array contains -1 where there are no elements (e.g. circular domains). Args: element_attributes (bool): *(optional)* Flag to return element attributes in the array. Set to True return attributes and set to False to return element indices. Defaults to True. Returns: numpy.ndarray: Array of values of element atttributes, or indices. """ # 1. Convert 1st node of each element into array indices pts = np.array(self.points) mins = pts.min(axis=0) sz = self.mesh_size corner_pts = pts[np.array(self.elements)[:, 0]] rel_pos = corner_pts - mins elem_tups = np.round(rel_pos / sz).astype(int) # 2. Create array full of -1 values inds_maxs = elem_tups.max(axis=0) arr = np.full(inds_maxs + 1, -1) # 3. For each element: populate array with element attributes if element_attributes: vals = self.element_attributes else: vals = np.arange(elem_tups.shape[0]) for t, v in zip(elem_tups, vals): arr[tuple(t)] = v return arr
# ----------------------------------------------------------------------- # # Plot Function # # ----------------------------------------------------------------------- #
[docs] def plot(self, index_by='element', material=[], loc=0, **kwargs): """Plot the mesh. This method plots the mesh using matplotlib. In 2D, this creates a :class:`matplotlib.collections.PolyCollection` and adds it to the current axes. In 3D, it creates a :meth:`mpl_toolkits.mplot3d.axes3d.Axes3D.voxels` and adds it to the current axes. The keyword arguments are passed though to matplotlib. Args: index_by (str): *(optional)* {'element' | 'attribute'} Flag for indexing into the other arrays passed into the function. For example, ``plot(index_by='attribute', color=['blue', 'red'])`` will plot the elements with ``element_attribute`` equal to 0 in blue, and elements with ``element_attribute`` equal to 1 in red. Defaults to 'element'. material (list): *(optional)* Names of material phases. One entry per material phase (the ``index_by`` argument is ignored). If this argument is set, a legend is added to the plot with one entry per material. Note that the ``element_attributes`` must be the material numbers for the legend to be formatted properly. loc (int or str): *(optional)* The location of the legend, if 'material' is specified. This argument is passed directly through to :func:`matplotlib.pyplot.legend`. Defaults to 0, which is 'best' in matplotlib. **kwargs: Keyword arguments that are passed through to matplotlib. """ n_dim = len(self.points[0]) if n_dim == 2: ax = plt.gca() else: ax = plt.gcf().gca(projection=Axes3D.name) n_obj = _misc.ax_objects(ax) if n_obj > 0: xlim = ax.get_xlim() ylim = ax.get_ylim() else: xlim = [float('inf'), -float('inf')] ylim = [float('inf'), -float('inf')] if n_dim == 2: _plot_2d(ax, self, index_by, **kwargs) else: if n_obj > 0: zlim = ax.get_zlim() else: zlim = [float('inf'), -float('inf')] inds = self.as_array(element_attributes=index_by=='attribute') plt_kwargs = {} for key, value in kwargs.items(): if type(value) in (list, np.array): plt_value = np.empty(inds.shape, dtype=object) for i, val_i in enumerate(value): plt_value[inds == i] = val_i if 'color' in key: unset_mask = plt_value == None plt_value[unset_mask] = 'k' inds[unset_mask] = -1 else: plt_value = value plt_kwargs[key] = plt_value # Scale axes pts = np.array(self.points) mins = pts.min(axis=0) sz = self.mesh_size pt_tups = np.round((pts - mins) / sz).astype(int) maxs = pt_tups.max(axis=0) grids = np.indices(maxs + 1, dtype=float) for pt, pt_tup in zip(pts, pt_tups): for i, x in enumerate(pt): grids[i][tuple(pt_tup)] = x ax.voxels(*grids, inds >= 0, **plt_kwargs) # Add legend if material and index_by == 'attribute': p_kwargs = [{'label': m} for m in material] for key, value in kwargs.items(): if type(value) not in (list, np.array): for kws in p_kwargs: kws[key] = value for i, m in enumerate(material): if type(value) in (list, np.array): p_kwargs[i][key] = value[i] else: p_kwargs[i][key] = value # Replace plural keywords for p_kw in p_kwargs: for kw in _misc.mpl_plural_kwargs: if kw in p_kw: p_kw[kw[:-1]] = p_kw[kw] del p_kw[kw] handles = [patches.Patch(**p_kw) for p_kw in p_kwargs] ax.legend(handles=handles, loc=loc) # Adjust Axes mins = np.array(self.points).min(axis=0) maxs = np.array(self.points).max(axis=0) xlim = (min(xlim[0], mins[0]), max(xlim[1], maxs[0])) ylim = (min(ylim[0], mins[1]), max(ylim[1], maxs[1])) if n_dim == 2: plt.axis('square') plt.xlim(xlim) plt.ylim(ylim) elif n_dim == 3: zlim = (min(zlim[0], mins[2]), max(zlim[1], maxs[2])) ax.set_xlim(xlim) ax.set_ylim(ylim) ax.set_zlim(zlim) _misc.axisEqual3D(ax)
def facet_check(neighs, polymesh, phases): if any([n < 0 for n in neighs]): add_facet = True else: seed_nums = [polymesh.seed_numbers[n] for n in neighs] phase_nums = [polymesh.phase_numbers[n] for n in neighs] m1, m2 = [phases[n].get('material_type', 'solid') for n in phase_nums] same_seed = seed_nums[0] == seed_nums[1] same_phase = phase_nums[0] == phase_nums[1] if (m1 in _misc.kw_solid) and same_seed: add_facet = False elif (m1 in _misc.kw_amorph) and same_phase: add_facet = False elif (m1 in _misc.kw_void) and (m2 in _misc.kw_void): add_facet = False else: add_facet = True return add_facet def _pt_ab(i, pt): return str(i + 1) + ''.join([', ' + str(x) for x in pt]) + '\n' def _call_meshpy(polymesh, phases=None, min_angle=0, max_volume=float('inf'), max_edge_length=float('inf')): # condition the phases input if phases is None: default_dict = {'material_type': 'solid', 'max_volume': float('inf')} n_phases = int(np.max(polymesh.phase_numbers)) + 1 phases = [default_dict for _ in range(n_phases)] # create point and facet lists kps = {} pts = [] facets = [] facet_neighs = [] facet_nums = [] for i in range(len(polymesh.facets)): facet = polymesh.facets[i] neighs = polymesh.facet_neighbors[i] if facet_check(neighs, polymesh, phases): new_facet = [] for kp_old in facet: if kp_old not in kps: kp_new = len(pts) pts.append(polymesh.points[kp_old]) kps[kp_old] = kp_new else: kp_new = kps[kp_old] new_facet.append(kp_new) facets.append(new_facet) facet_neighs.append(neighs) facet_nums.append(i + 1) # Subdivide facets n_dim = len(pts[0]) if n_dim == 2: n_subs = np.ones(len(facets), dtype='int') for i, facet in enumerate(facets): pt1 = np.array(pts[facet[0]]) pt2 = np.array(pts[facet[1]]) rel_pos = pt2 - pt1 n_float = np.linalg.norm(rel_pos) / max_edge_length n_int = max(1, np.ceil(n_float)) n_subs[i] = n_int sub_out = meshpy.triangle.subdivide_facets(n_subs, pts, facets, facet_nums) pts, facets, facet_nums = sub_out # create groups/regions pts_arr = np.array(polymesh.points) regions = [] holes = [] ungrouped = np.full(len(polymesh.regions), True, dtype='?') while np.any(ungrouped): cell_ind = np.argmax(ungrouped) # compute cell center facet_list = polymesh.regions[cell_ind] cell_kps = {kp for n in facet_list for kp in polymesh.facets[n]} cell_cen = pts_arr[list(cell_kps)].mean(axis=0) # seed number and phase type seed_num = int(polymesh.seed_numbers[cell_ind]) phase_num = polymesh.phase_numbers[cell_ind] phase = phases[phase_num] phase_type = phase.get('material_type', 'crystalline') phase_vol = phase.get('max_volume', max_volume) # get all cell numbers in group cell_nums = set([cell_ind]) old_len = len(cell_nums) searching_front = True while searching_front: front = set() for n in cell_nums: neighs = set() for facet_num in polymesh.regions[n]: f_neighs = polymesh.facet_neighbors[facet_num] neigh_ind = [i for i in f_neighs if i != n][0] if neigh_ind < 0: continue if not facet_check(f_neighs, polymesh, phases): neighs.add(neigh_ind) assert ungrouped[list(neighs)].all() front.update(neighs) cell_nums |= front new_len = len(cell_nums) searching_front = new_len != old_len old_len = new_len ungrouped[list(cell_nums)] = False # update appropriate list if phase_type in _misc.kw_void: holes.append(cell_cen) else: regions.append(cell_cen.tolist() + [seed_num, phase_vol]) # build inputs if n_dim == 2: info = meshpy.triangle.MeshInfo() else: info = meshpy.tet.MeshInfo() info.set_points(pts) info.set_facets(facets, facet_nums) info.set_holes(holes) info.regions.resize(len(regions)) for i, r in enumerate(regions): info.regions[i] = tuple(r) # run MeshPy if n_dim == 2: tri_mesh = meshpy.triangle.build(info, attributes=True, volume_constraints=True, max_volume=max_volume, min_angle=min_angle, generate_faces=True) else: opts = meshpy.tet.Options('pq') opts.mindihedral = min_angle opts.maxvolume = float('inf') opts.fixedvolume = 1 opts.regionattrib = 1 opts.facesout = 1 tri_mesh = meshpy.tet.build(info, options=opts) # return mesh tri_pts = np.array(tri_mesh.points) tri_elems = np.array(tri_mesh.elements) tri_e_atts = np.array(tri_mesh.element_attributes, dtype='int') tri_faces = np.array(tri_mesh.faces) tri_f_atts = np.array(tri_mesh.face_markers) f_mask = tri_f_atts > 0 tri_f = tri_faces[f_mask] tri_fa = tri_f_atts[f_mask] - 1 tri_args = (tri_pts, tri_elems, tri_e_atts, tri_f, tri_fa) return tri_args def _call_gmsh(pmesh, phases, res, edge_res): if res == float('inf'): res = None # If edge length not specified, default to mesh size input if edge_res == float('inf'): edge_res = res amorph_seeds = _amorphous_seed_numbers(pmesh, phases) # ---------------------------------------------------------------------- # # CREATE CONNECTIVITY DATA # ---------------------------------------------------------------------- # # Extract edges from facets list facets_info = {} edges_info = {} edge_keys = [] edge_lines = [] n_edges = 0 for i, f in enumerate(pmesh.facets): # Determine if facet should be skipped (interior to seed) keep = True ns = pmesh.facet_neighbors[i] if min(ns) >= 0: keep = pmesh.seed_numbers[ns[0]] != pmesh.seed_numbers[ns[1]] if not keep: continue facets_info[i] = {'facet': f, 'seeds': []} n = len(f) facet_kp_pairs = [(f[k], f[(k + 1) % n]) for k in range(n)] edge_numbers = [] edge_signs = [] for pair in facet_kp_pairs: key = tuple(sorted(pair)) if pair == key: edge_sign = 1 else: edge_sign = -1 if key not in edge_keys: edges_info[key] = {'ind': n_edges, 'facets': [], 'seeds': []} edge_keys.append(key) n_edges += 1 edges_info[key]['facets'].append(i) edge_num = edges_info[key]['ind'] # Add seeds neighs = pmesh.facet_neighbors[i] edges_info[key]['neighbors'] = neighs for neigh_cell in neighs: if neigh_cell < 0: seed_num = neigh_cell else: seed_num = pmesh.seed_numbers[neigh_cell] edges_info[key]['seeds'].append(seed_num) edge_numbers.append(edge_num) edge_signs.append(edge_sign) facets_info[i]['neighbors'] = pmesh.facet_neighbors[i] facets_info[i]['edge_numbers'] = edge_numbers facets_info[i]['edge_signs'] = edge_signs for cell_num, seed_num in enumerate(pmesh.seed_numbers): facet_nums = [f for f in pmesh.regions[cell_num] if f in facets_info] for facet_num in facet_nums: facets_info[facet_num]['seeds'].append(seed_num) # ---------------------------------------------------------------------- # # CREATE GEOMETRY # ---------------------------------------------------------------------- with pg.geo.Geometry() as geom: # Add points pt_arr = np.array(pmesh.points) pts = [geom.add_point(_pt3d(pt), edge_res) for pt in pmesh.points] n_dim = len(pmesh.points[0]) # Add edges to geometry phys_facets = [] phys_seeds = [] for edge in edge_keys: line = geom.add_line(*[pts[kp] for kp in edge]) edge_lines.append(line) if n_dim == 2: lbl = 'facet-{}'.format(edges_info[edge]['facets'][0]) geom.add_physical(edge_lines[-1], lbl) if facet_check(edges_info[edge]['neighbors'], pmesh, phases): phys_facets.append(lbl) if n_dim == 2: # Add surfaces to geometry loops = [] surfs = [] seed_facets = {} seed_phases = {} for i, r in enumerate(pmesh.regions): s = pmesh.seed_numbers[i] seed_facets.setdefault(s, set()).symmetric_difference_update(r) seed_phases[s] = pmesh.phase_numbers[i] for i in seed_facets: region = list(seed_facets[i]) sorted_pairs = _sort_facets([pmesh.facets[f] for f in region]) loop = [] for facet in sorted_pairs: key = tuple(sorted(facet)) if facet[0] == key[0]: sgn = 1 else: sgn = -1 n = edges_info[key]['ind'] line = edge_lines[n] if sgn > 0: loop.append(line) else: loop.append(-line) loops.append(geom.add_curve_loop(loop)) surfs.append(geom.add_plane_surface(loops[-1])) lbl = 'seed-' + str(i) geom.add_physical(surfs[-1], lbl) p_num = seed_phases[i] mat_type = phases[p_num].get('material_type', 'solid') if mat_type not in _misc.kw_void: phys_seeds.append(lbl) # Add mesh size control points to 'centers' of regions if res is not None: kps = list({kp for p in sorted_pairs for kp in p}) cen = pt_arr[kps].mean(axis=0) # estimate of center pt = geom.add_point(_pt3d(cen), res) geom.in_surface(pt, surfs[-1]) elif n_dim == 3: # Add surfaces to geometry loops = [] surfs = [] seed_surfs = {} surf_kps = {} seed_phases = dict(zip(pmesh.seed_numbers, pmesh.phase_numbers)) for i in facets_info: info = facets_info[i] facet_seeds = info['seeds'] to_add = len(facet_seeds) < 2 or facet_seeds[0] != facet_seeds[1] if not to_add: surfs.append('') continue loop = [] for n, sgn in zip(info['edge_numbers'], info['edge_signs']): line = edge_lines[n] if sgn > 0: loop.append(line) else: loop.append(-line) loops.append(geom.add_curve_loop(loop)) surfs.append(geom.add_plane_surface(loops[-1])) surf_kps[surfs[-1]] = set(info['facet']) f_lbl = 'facet-' + str(i) geom.add_physical(surfs[-1], 'facet-' + str(i)) if facet_check(info['neighbors'], pmesh, phases): phys_facets.append(f_lbl) for seed_num in facet_seeds: if seed_num not in seed_surfs: seed_surfs[seed_num] = [] seed_surfs[seed_num].append(surfs[-1]) # Add volumes to geometry surf_loops = [] volumes = [] for seed_num in seed_surfs: surf_loop = seed_surfs[seed_num] surf_loops.append(geom.add_surface_loop(surf_loop)) volumes.append(geom.add_volume(surf_loops[-1])) lbl = 'seed-' + str(seed_num) geom.add_physical(volumes[-1], lbl) p_num = seed_phases[seed_num] mat_type = phases[p_num].get('material_type', 'solid') if mat_type not in _misc.kw_void: phys_seeds.append(lbl) # Add mesh size control points to 'centers' of regions if res is not None: kps = set().union(*[surf_kps[s] for s in surf_loop]) cen = pt_arr[list(kps)].mean(axis=0) # estimate center pt = geom.add_point(_pt3d(cen), res) geom.in_volume(pt, volumes[-1]) else: raise ValueError('Points cannot have dimension ' + str(n_dim) + '.') mesh = geom.generate_mesh() # ---------------------------------------------------------------------- # # CREATE MICROSTRUCTPY.MESHING.TRIMESH # ---------------------------------------------------------------------- # f_ind = {2: 0, 3: 1}[n_dim] e_ind = {2: 1, 3: 2}[n_dim] pts = np.array(mesh.points)[:, :n_dim] facets = mesh.cells[f_ind].data # Sort Element Keypoints for Positive Volume tets = np.array([e[_sort_element([mesh.points[k] for k in e])] for e in mesh.cells[e_ind].data]) tet_atts = np.array([-1 for tet in tets]) facet_atts = np.array([-1 for f in facets]) tet_set = np.array([False for tet in tets]) facet_set = np.array([False for f in facets]) n_facets = len(mesh.cells[f_ind].data) for key, elem_sets in mesh.cell_sets.items(): set_kind, set_num_str = key.split('-') att = int(set_num_str) if set_kind == 'seed' and key in phys_seeds: elem_set = elem_sets[e_ind] - n_facets tet_atts[elem_set] = amorph_seeds.get(att, att) tet_set[elem_set] = True elif set_kind == 'facet' and key in phys_facets: elem_set = elem_sets[f_ind] facet_atts[elem_set] = att facet_set[elem_set] = True tets = tets[tet_set] tet_atts = tet_atts[tet_set] facets = facets[facet_set] facet_atts = facet_atts[facet_set] tri_args = (pts, tets, tet_atts, facets, facet_atts) return tri_args def _sort_element(elem_pts): n_pts = len(elem_pts) n_dim = n_pts - 1 if n_dim == 2: v1 = elem_pts[1] - elem_pts[0] v2 = elem_pts[2] - elem_pts[0] cp = np.cross(v1, v2)[-1] if cp < 0: return np.array([0, 2, 1]) return np.arange(3) elif n_dim == 3: v1 = elem_pts[1] - elem_pts[0] v2 = elem_pts[2] - elem_pts[0] v3 = elem_pts[3] - elem_pts[0] cp = np.cross(v1, v2) dp = cp.dot(v3) if dp < 0: return np.array([0, 1, 3, 2]) return np.arange(4) else: raise ValueError('Cannot sort for n pts: ' + str(n_pts)) def _sort_facets(pairs): remaining_inds = [i for i in range(1, len(pairs))] sorted_inds = [0] s_pairs = [pairs[0]] while remaining_inds: last_kp = s_pairs[-1][-1] for ind, i in enumerate(remaining_inds): pair = pairs[i] if last_kp in pair: break sorted_inds.append(i) del remaining_inds[ind] if last_kp == pair[0]: s_pairs.append(pair) else: s_pairs.append(list(reversed(pair))) return s_pairs def _amorphous_seed_numbers(pmesh, phases): phase_nums = np.array(pmesh.phase_numbers) is_amorph = np.array([p.get('material_type', 'solid') in _misc.kw_amorph for p in phases]) amorph_mask = is_amorph[phase_nums] neighs = np.array(pmesh.facet_neighbors) neighs = neighs[np.min(neighs, axis=1) >= 0] neighs_mask = phase_nums[neighs[:, 0]] == phase_nums[neighs[:, 1]] neighs_mask &= amorph_mask[neighs[:, 0]] amorph_neighs = neighs[neighs_mask] new_seed_numbers = np.array(pmesh.seed_numbers) changes_made = True while changes_made: changes_made = False for pair in amorph_neighs: seeds = new_seed_numbers[pair] if seeds[0] != seeds[1]: changes_made = True new_seed_numbers[pair] = np.min(seeds) conv_dict = {s1: s2 for s1, s2 in zip(pmesh.seed_numbers, new_seed_numbers) if s1 != s2} return conv_dict def _pt3d(pt): pt3d = np.zeros(3) pt3d[:len(pt)] = pt return pt3d def _facet_in_normal(pts, cen_pt): n_dim = len(cen_pt) if n_dim == 2: ptA = pts[0] ptB = pts[1] vt = ptB - ptA vn = np.array([-vt[1], vt[0]]) else: ptA = pts[0] ptB = pts[1] ptC = pts[2] v1 = ptB - ptA v2 = ptC - ptA vn = np.cross(v1, v2) sgn = vn.dot(cen_pt - ptA) vn *= sgn # flip so center is inward un = vn / np.linalg.norm(vn) return un, pts.mean(axis=0) def _plot_2d(ax, mesh, index_by, **kwargs): simps = np.array(mesh.elements) pts = np.array(mesh.points) xy = pts[simps, :] plt_kwargs = {} for key, value in kwargs.items(): if type(value) in (list, np.array): plt_value = [] for e_num, e_att in enumerate(mesh.element_attributes): if index_by == 'element': ind = e_num elif index_by == 'attribute': ind = int(e_att) else: e_str = 'Cannot index by {}.'.format(index_by) raise ValueError(e_str) try: v = value[ind] except IndexError: v = 'none' plt_value.append(v) else: plt_value = value plt_kwargs[key] = plt_value pc = collections.PolyCollection(xy, **plt_kwargs) ax.add_collection(pc) ax.autoscale_view()