"""Polygon Meshing
This module contains the class definition for the PolyMesh class.
"""
# --------------------------------------------------------------------------- #
# #
# Import Modules #
# #
# --------------------------------------------------------------------------- #
from __future__ import division
from __future__ import print_function
import os
import subprocess
import sys
import tempfile
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pyvoro
from matplotlib import collections
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from scipy.spatial import distance
from microstructpy import _misc
from microstructpy import geometry
__all__ = ['PolyMesh']
__author__ = 'Kenneth (Kip) Hart'
# --------------------------------------------------------------------------- #
# #
# PolyMesh Class #
# #
# --------------------------------------------------------------------------- #
[docs]class PolyMesh(object):
"""Polygonal/Polyhedral mesh.
The PolyMesh class contains the points, edges, regions, etc. in a polygon
(2D) or polyhedron (3D) mesh.
The points attribute is a numpy array containing the (x, y) or (x, y, z)
coordinates of each point in the mesh. This is the only attribute that
contains floating point numbers. The rest contain indices/integers.
The facets attribute describes the interfaces between the polygons/
polyhedra. In 2D, these interfaces are line segments and each facet
contains the indices of the points at each end of the line segment. These
indices are unorderd. In 3D, the interfaces are polygons so each facet
contains the indices of the points on that polygon. These indices are
ordered such that neighboring keypoints are connected by line segments
that form the polygon.
The regions attribute contains the area (2D) or volume (3D). In 2D, a
region is given by an ordered list of facets, or edges, that enclose the
polygon. In 3D, the region is given by an un-ordered list of facets,
or polygons, that enclose the polyhedron.
For each region, there is also an associated seed number and material
phase. These data are stored in the seed_number and phase_number
attributes, which have the same length as the regions list.
Args:
points (list or numpy.ndarray): An Nx2 or Nx3 array of coordinates
in the mesh.
facets (list): List of facets between regions. In 2D, this is a list
of edges (Nx2). In 3D, this is a list of 3D polygons.
regions (list): A list of polygons (2D) or polyhedra (3D), with each
element of the list being a list of facet indices.
seed_numbers (list or numpy.ndarray): *(optional)* The seed number
associated with each region.
Defaults to 0 for all regions.
phase_numbers (list or numpy.ndarray): *(optional)* The phase number
associated with each region.
Defaults to 0 for all regions.
facet_neighbors (list or numpy.ndarray): *(optional)* The region
numbers on either side of each facet.
If not givien, a neighbor list is computed from ``regions``.
volumes (list or numpy.ndarray): *(optional)* The area/volume of each
region.
If not given, region volumes are calculated based on ``points``,
``facets``, and ``regions``.
"""
# ----------------------------------------------------------------------- #
# Constructors #
# ----------------------------------------------------------------------- #
def __init__(self, points, facets, regions, seed_numbers=None,
phase_numbers=None, facet_neighbors=None, volumes=None):
self.points = points
self.facets = facets
self.regions = regions
if facet_neighbors is None:
# Find facet neighbors
facet_neighs = [[-1, -1] for _ in facets]
n_neighs = [0 for _ in facets]
for r_num, region in enumerate(regions):
for f_num in region:
ind = n_neighs[f_num]
facet_neighs[f_num][ind] = r_num
n_neighs[f_num] += 1
# update negative neighbor numbers to follow the voro++
# convention, described in the %n section of this website:
# http://math.lbl.gov/voro++/doc/custom.html
pt_arr = np.array(points)
pt_mins = pt_arr.min(axis=0)
pt_maxs = pt_arr.max(axis=0)
for fnum, facet in enumerate(facets):
if facet_neighs[fnum][-1] != -1:
continue
f_pts = pt_arr[facet, :]
min_match = np.all(np.isclose(f_pts, pt_mins), axis=0)
if np.any(min_match):
ld = 3 - len(min_match)
mask = np.pad(min_match, (0, ld), 'constant',
constant_values=(False, False))
id = np.array([-1, -3, -5])[mask][0]
facet_neighs[fnum][-1] = id
max_match = np.all(np.isclose(f_pts, pt_maxs), axis=0)
if np.any(max_match):
ld = 3 - len(max_match)
mask = np.pad(max_match, (0, ld), 'constant',
constant_values=(False, False))
id = np.array([-2, -4, -6])[mask][0]
facet_neighs[fnum][-1] = id
self.facet_neighbors = facet_neighs
else:
self.facet_neighbors = facet_neighbors
if seed_numbers is None:
self.seed_numbers = [0 for _ in regions]
else:
self.seed_numbers = seed_numbers
if phase_numbers is None:
self.phase_numbers = [0 for _ in regions]
else:
self.phase_numbers = phase_numbers
if volumes is None:
vols = np.zeros(len(self.regions))
n = len(self.points[0])
for i, region in enumerate(self.regions):
# 'center' of region is arbitrary, since region is convex
cen = np.array(self.points)[self.facets[region[0]][0]]
for f_num in region:
facet = np.array(self.facets[f_num])
j_max = len(facet) - n + 2
# convert facet into (n-1)D simplices
for j in range(1, j_max):
inds = np.append(np.arange(j, j + n - 1), 0)
simplex = facet[inds]
facet_pts = np.array(self.points)[simplex]
rel_pos = facet_pts - cen
# simplex volume is |det([Dx1, Dy1; Dx2, Dy2])| in 2D
vols[i] += np.abs(np.linalg.det(rel_pos))
# the 1/2 out front in 2D and 1/6 in 3D
while n > 1:
vols /= n
n -= 1
self.volumes = vols
else:
self.volumes = volumes
# ----------------------------------------------------------------------- #
# Representation and String Functions #
# ----------------------------------------------------------------------- #
def __repr__(self):
repr_str = 'PolyMesh('
repr_str += repr(self.points)
repr_str += ', '
repr_str += repr(self.facets)
repr_str += ', '
repr_str += repr(self.regions)
for att in ('seed_numbers', 'phase_numbers'):
repr_str += ', '
vals = self.__dict__[att]
if all([n == 0 for n in vals]):
repr_str += repr(None)
else:
repr_str += repr(vals)
repr_str += ')'
return repr_str
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 Facets: ' + str(len(self.facets)) + '\n'
str_str += ''.join(['\t' + str(tuple(f))[1:-1] + '\n'
for f in self.facets])
str_str += 'Facet Neighbors: ' + str(len(self.facet_neighbors)) + '\n'
str_str += ''.join(['\t' + str(tuple(n))[1:-1] + '\n'
for n in self.facet_neighbors])
str_str += 'Mesh Regions: ' + str(len(self.regions)) + '\n'
str_str += ''.join(['\t' + str(tuple(r))[1:-1] + '\n'
for r in self.regions])
str_str += 'Seed Numbers: ' + str(len(self.seed_numbers)) + '\n'
str_str += ''.join(['\t' + str(n) + '\n' for n in self.seed_numbers])
str_str += 'Phase Numbers: ' + str(len(self.phase_numbers)) + '\n'
str_str += ''.join(['\t' + str(n) + '\n' for n in self.phase_numbers])
str_str += 'Volumes: ' + str(len(self.volumes)) + '\n'
str_str += '\n'.join(['\t' + str(v) for v in self.volumes])
return str_str
# ----------------------------------------------------------------------- #
# Read and Write Functions #
# ----------------------------------------------------------------------- #
[docs] def write(self, filename, format='txt'):
"""Write the mesh to a file.
This function writes the polygon/polyhedron mesh to a file.
See the :ref:`s_poly_file_io` section of the
:ref:`c_file_formats` guide for more information about the available
output file formats.
Args:
filename (str): Name of the file to be written.
format (str): *(optional)* {'txt' | 'poly' | 'ply' | 'vtk' }
Format of the data in the file. Defaults to ``'txt'``.
"""
if format in ('str', 'txt'):
with open(filename, 'w') as f:
f.write(str(self) + '\n')
elif format == 'poly':
nv = len(self.points)
nd = len(self.points[0])
nf = len(self.facets)
assert nd == 2
poly = '# Polygon Mesh\n'
poly += ' '.join([str(n) for n in (nv, 2, 0, 0)]) + '\n'
# vertices
poly += '# Vertices\n'
poly += ''.join([str(i) + ''.join([' {: e}'.format(x) for x in pt])
+ '\n' for i, pt in enumerate(self.points)])
# facets
poly += '# Segments\n'
poly += ' '.join([str(n) for n in (nf, 0)]) + '\n'
poly += ''.join([' '.join([str(n) for n in (nv + i, k1, k2)])
+ '\n' for i, (k1, k2) in enumerate(self.facets)])
elif format == 'ply':
nv = len(self.points)
nd = len(self.points[0])
nf = len(self.facets)
assert nd <= 3
axes = ['x', 'y', 'z'][:nd]
# header
ply = 'ply\n'
ply += 'format ascii 1.0\n'
ply += 'element vertex ' + str(nv) + '\n'
ply += ''.join(['property float32 ' + a + '\n' for a in axes])
ply += 'element face ' + str(nf) + '\n'
ply += 'property list uint8 int32 vertex_indices\n'
ply += 'end_header\n'
# vertices
ply += ''.join([' '.join(['{: e}'.format(x) for x in pt]) + '\n'
for pt in self.points])
# faces
ply += ''.join([str(len(f)) + ''.join([' ' + str(kp) for kp in f])
+ '\n' for f in self.facets])
with open(filename, 'w') as f:
f.write(ply)
elif format == 'vtk':
nv = len(self.points)
nd = len(self.points[0])
nf = len(self.facets)
assert nd == 3
p_size = nf + sum([len(f) for f in self.facets])
vtk = '# vtk DataFile Version 2.0\n'
vtk += 'Polyhedron Mesh\n'
vtk += 'ASCII\n'
vtk += 'DATASET POLYDATA\n'
# vertices
vtk += 'POINTS ' + str(nv) + ' float\n'
vtk += ''.join([' '.join(['{: e}'.format(x) for x in pt]) + '\n'
for pt in self.points])
vtk += '\n'
# faces
vtk += 'POLYGONS ' + str(nf) + ' ' + str(p_size) + '\n'
vtk += ''.join([str(len(f)) + ''.join([' ' + str(kp) for kp in f])
+ '\n' for f in self.facets])
with open(filename, 'w') as f:
f.write(vtk)
else:
e_str = 'Cannot understand format string ' + str(format) + '.'
raise ValueError(e_str)
[docs] @classmethod
def from_file(cls, filename):
"""Read PolyMesh from file.
This function reads in a polygon 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='txt'`` option.
Args:
filename (str): Name of file to read from.
Returns:
PolyMesh: The instance of the class written to the file.
"""
with open(filename, 'r') as file:
stage = 0
pts = []
facets = []
f_neighbors = []
regions = []
seed_numbers = []
phase_numbers = []
volumes = []
for line in file.readlines():
if 'Mesh Points'.lower() in line.lower():
n_pts = int(line.split(':')[1])
stage = 'points'
elif 'Mesh Facets'.lower() in line.lower():
n_fts = int(line.split(':')[1])
stage = 'facets'
elif 'Facet Neighbors'.lower() in line.lower():
n_nns = int(line.split(':')[1])
stage = 'facet neighbors'
elif 'Mesh Regions'.lower() in line.lower():
n_rns = int(line.split(':')[1])
stage = 'regions'
elif 'Seed Numbers'.lower() in line.lower():
n_sns = int(line.split(':')[1])
stage = 'seed numbers'
elif 'Phase Numbers'.lower() in line.lower():
n_pns = int(line.split(':')[1])
stage = 'phase numbers'
elif 'Volumes'.lower() in line.lower():
n_vols = int(line.split(':')[1])
stage = 'volumes'
else:
if stage == 'points':
pts.append([float(x) for x in line.split(',')])
elif stage == 'facets':
facets.append([int(kp) for kp in line.split(',')])
elif stage == 'facet neighbors':
f_neighbors.append([int(n) for n in line.split(',')])
elif stage == 'regions':
regions.append([int(f) for f in line.split(',')])
elif stage == 'seed numbers':
seed_numbers.append(_misc.from_str(line))
elif stage == 'phase numbers':
phase_numbers.append(_misc.from_str(line))
elif stage == 'volumes':
volumes.append(_misc.from_str(line))
else:
pass
# check the inputs
assert len(pts) == n_pts
assert len(facets) == n_fts
assert len(regions) == n_rns
assert len(seed_numbers) == n_sns
assert len(phase_numbers) == n_pns
assert len(volumes) == n_vols
if len(f_neighbors) == 0:
f_neighbors = None
else:
assert len(f_neighbors) == n_nns
return cls(pts, facets, regions, seed_numbers, phase_numbers,
volumes=volumes, facet_neighbors=f_neighbors)
# ----------------------------------------------------------------------- #
# Construct from Seed List #
# ----------------------------------------------------------------------- #
[docs] @classmethod
def from_seeds(cls, seedlist, domain):
"""Create from :class:`.SeedList` and a domain.
This function creates a polygon/polyhedron mesh from a seed list and
a domain. It relies on the pyvoro package, which wraps `Voro++`_.
The mesh is a Voronoi power diagram / Laguerre tessellationself.
The pyvoro package operates on rectangular domains, so other domains
are meshed in 2D by meshing in a bounding box then the boundary cells
are clipped to the domain boundary.
Currently non-rectangular domains in 3D are not supported.
Args:
seedlist (SeedList): A list of seeds in the microstructure.
domain (from :mod:`microstructpy.geometry`): The domain to be
filled by the seed.
Returns:
PolyMesh: A polygon/polyhedron mesh.
.. _`Voro++`: http://math.lbl.gov/voro++/
"""
# Collect all breakdowns
bkdwn2seed = np.array([], dtype='int')
bkdwns = np.array([])
for seed_num, seed in enumerate(seedlist):
if len(seed.breakdown) == 0:
seed.update_breakdown()
bkdwn = np.array(seed.breakdown).reshape(-1, domain.n_dim + 1)
in_mask = domain.within(bkdwn[:, :-1])
breakdown = bkdwn[in_mask]
m, n = breakdown.shape
bkdwns = np.concatenate((bkdwns.reshape(-1, n), breakdown))
bkdwn2seed = np.append(bkdwn2seed, np.full(m, seed_num))
n_pts = bkdwns.shape[0]
n_dim = bkdwns.shape[1] - 1
# modify point list and boundaries if necessary
geom = {2: geometry.Rectangle, 3: geometry.Box}
voro_dom = geom[n_dim](limits=domain.limits)
# get domain limits
lims = voro_dom.limits
# clip points from voro domain
flag_val = min(bkdwn2seed) - 1
n_pad = bkdwns.shape[0] - n_pts
bkdwn2seed = np.pad(bkdwn2seed, (0, n_pad), 'constant',
constant_values=(0, flag_val))
cens = bkdwns[:, :-1]
rads = bkdwns[:, -1]
# get block size
sz = 2 * max(rads)
if np.isclose(sz, 0):
sz = 0.1 * np.min([ub - lb for lb, ub in lims])
# remove extraneous breakdowns
removing_pts = True
while removing_pts:
# Create a temporary file to run pyvoro
call_str = 'import pyvoro\n\n'
call_str += 'pts = ['
beg_str = ',\n' + len('pts = [') * ' '
call_str += beg_str.join([str(np.array(p).tolist()) for p in cens])
call_str += ']\n\n'
call_str += 'lims = ' + str(np.array(lims).tolist()) + '\n\n'
call_str += 'sz = ' + str(sz) + '\n\n'
call_str += 'rads = ['
beg_str = ',\n' + len('rads = [') * ' '
call_str += beg_str.join([str(rad) for rad in rads])
call_str += ']\n\n'
call_str += 'pyvoro.compute_'
if n_dim == 2:
call_str += '2d_'
call_str += 'voronoi(pts, lims, sz, rads)\n'
file = tempfile.NamedTemporaryFile(mode='w', suffix='.py',
delete=False)
file.write(call_str)
call_filename = file.name
file.close()
# Run pyvoro
p = subprocess.Popen([sys.executable, call_filename],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
p_out, _ = p.communicate()
try:
p.terminate()
except OSError:
pass
os.remove(call_filename)
# if there is output, remove those cells from the list
out_str = p_out.decode('utf-8')
if out_str:
inds = [int(s) for s in out_str.split(':')[-1].split()]
mask = np.full(len(rads), True)
mask[inds] = False
cens = cens[mask]
rads = rads[mask]
bkdwn2seed = bkdwn2seed[mask]
else:
removing_pts = False
missing_seeds = set(range(len(seedlist))) - set(bkdwn2seed)
assert not missing_seeds, str(missing_seeds)
# compute voronoi diagram
voro_fun = {2: pyvoro.compute_2d_voronoi,
3: pyvoro.compute_voronoi}[n_dim]
voro = voro_fun(cens, lims, sz, rads)
# Get only the cells within the domain
cell_mask = np.full(len(bkdwn2seed), True, dtype='bool')
rect_doms = ['square', 'cube', 'rectangle', 'box', 'nbox']
if type(domain).__name__.lower() not in rect_doms:
for cell_num, cell in enumerate(voro):
cell_pts = np.array(cell['vertices'])
cell_mask[cell_num] = np.any(domain.within(cell_pts))
bkdwn2seed = bkdwn2seed[cell_mask]
new_cell_nums = np.full(len(cell_mask), -1, dtype='int')
new_cell_nums[cell_mask] = np.arange(np.sum(cell_mask))
reduced_voro = []
for old_cell_num, cell in enumerate(voro):
# update the numbers of adjacent cells
faces = cell['faces']
for face in faces:
old_adj_cell_num = face['adjacent_cell']
if old_adj_cell_num >= 0:
new_adj_cell_num = new_cell_nums[old_adj_cell_num]
face['adjacent_cell'] = new_adj_cell_num
cell['faces'] = faces
# add cell to voro
if cell_mask[old_cell_num]:
reduced_voro.append(cell)
# Clip cells to domain
voro = [_clip_cell(c, domain) for c in reduced_voro]
# create global key point and facet lists
pts_global = []
pts_conn = []
local_kp_conn = {}
for cell_num, cell_data in enumerate(voro):
pts_local = cell_data['vertices']
for face_data in cell_data['faces']:
adj_cell = face_data['adjacent_cell']
simplex_local = face_data['vertices']
for kp_local in simplex_local:
key = (cell_num, kp_local)
if key in local_kp_conn:
kp_global = local_kp_conn[key]
else:
kp_global = len(pts_global)
pt = pts_local[kp_local]
pts_global.append(pt)
pts_conn.append({cell_num: kp_local})
local_kp_conn[key] = kp_global
if (adj_cell >= 0) and (adj_cell < len(voro)):
conn_info = pts_conn[kp_global]
if adj_cell not in conn_info:
adj_cell_data = voro[adj_cell]
adj_pts_local = np.array(adj_cell_data['vertices'])
rel_pos = adj_pts_local - pts_global[kp_global]
sq_dist = np.sum(rel_pos * rel_pos, axis=-1)
adj_kp_local = np.argmin(sq_dist)
adj_key = (adj_cell, adj_kp_local)
local_kp_conn[adj_key] = kp_global
pts_conn[kp_global][adj_cell] = adj_kp_local
# create facet and region lists
facet_list = []
facet_neighbor_list = []
region_list = [[] for cell in voro]
for cell_num, cell_data in enumerate(voro):
for face_data in cell_data['faces']:
adj_cell_num = face_data['adjacent_cell']
if adj_cell_num >= len(voro):
adj_cell_num = -1
if adj_cell_num < cell_num:
neighbor_pair = (adj_cell_num, cell_num)
s_lcl = face_data['vertices']
s_glbl = [local_kp_conn[(cell_num, kp)] for kp in s_lcl]
face_num = len(facet_list)
facet_list.append(s_glbl)
facet_neighbor_list.append(neighbor_pair)
for f_cell_num in neighbor_pair:
if (f_cell_num >= 0) and (f_cell_num < len(voro)):
region_list[f_cell_num].append(face_num)
# create phase number list
phase_nums = [seedlist[i].phase for i in bkdwn2seed]
# Create volume list
vols = [cell['volume'] for cell in voro]
# return mesh
return cls(pts_global, facet_list, region_list, bkdwn2seed, phase_nums,
facet_neighbor_list, vols)
# ----------------------------------------------------------------------- #
# Plot Mesh #
# ----------------------------------------------------------------------- #
[docs] def plot(self, **kwargs):
"""Plot the mesh.
This function plots the polygon mesh.
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:
**kwargs: Keyword arguments for matplotlib.
"""
n_dim = len(self.points[0])
if n_dim == 2:
# create vertex loops for each poly
vloops = [kp_loop([self.facets[f] for f in r]) for r in
self.regions]
# create poly input
xy = [np.array([self.points[kp] for kp in lp]) for lp in vloops]
pc = collections.PolyCollection(xy, **kwargs)
ax = plt.gca()
ax.add_collection(pc)
ax.autoscale_view()
elif n_dim == 3:
if len(plt.gcf().axes) == 0:
ax = plt.axes(projection=Axes3D.name)
else:
ax = plt.gca()
xy = [np.array([self.points[kp] for kp in f]) for f in self.facets]
pc = Poly3DCollection(xy, **kwargs)
ax.add_collection(pc)
else:
raise NotImplementedError('Cannot plot in ' + str(n_dim) + 'D.')
[docs] def plot_facets(self, **kwargs):
"""Plot PolyMesh facets.
This function plots the facets of the polygon mesh, rather than the
regions.
In 2D, it adds a :class:`matplotlib.collections.LineCollection` to the
current axes.
In 3D, it adds a
:class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection`
with ``facecolors='none'``.
The keyword arguments are passed though to matplotlib.
Args:
**kwargs (dict): Keyword arguments for matplotlib.
"""
n_dim = len(self.points[0])
if n_dim == 2:
xy = [np.array([self.points[kp] for kp in f]) for f in self.facets]
pc = collections.LineCollection(xy, **kwargs)
ax = plt.gca()
ax.add_collection(pc)
ax.autoscale_view()
else:
kwargs_3d = {}
color_spec = False
for kw, val in kwargs.items():
if kw == 'colors':
color_spec = True
kwargs_3d['edgecolors'] = val
else:
kwargs_3d[kw] = val
if color_spec:
kwargs_3d['facecolors'] = 'none'
self.plot(**kwargs_3d)
# ----------------------------------------------------------------------- #
# Mesh Equality #
# ----------------------------------------------------------------------- #
def __eq__(self, other_mesh):
# check type
if type(other_mesh) is not PolyMesh:
print('not same type')
return False
# check that the lengths are all the same
same = True
same &= len(self.points) == len(other_mesh.points)
same &= len(self.facets) == len(other_mesh.facets)
same &= len(self.regions) == len(other_mesh.regions)
same &= len(self.seed_numbers) == len(other_mesh.seed_numbers)
same &= len(self.phase_numbers) == len(other_mesh.phase_numbers)
if not same:
print('not same length')
return False
# check that the vertices have the same coordinates
pt_dists = distance.cdist(self.points, other_mesh.points)
same_pt = np.isclose(pt_dists, 0)
same_ints = same_pt.astype(int)
same &= np.all(same_ints.sum(axis=0) == 1)
same &= np.all(same_ints.sum(axis=1) == 1)
if not same:
print('not same verts')
return False
kp_conv = np.argwhere(same_pt)
kp_other = kp_conv[:, 1]
print('transform')
print(np.array(kp_other))
# check that the facets are the same
facets_in_other_kps = [[kp_other[kp] for kp in f] for f in self.facets]
o_fnum = []
for i, s_facet in enumerate(facets_in_other_kps):
for j, o_facet in enumerate(other_mesh.facets):
if j in o_fnum:
continue
else:
if set(s_facet) == set(o_facet):
o_fnum.append(j)
break
if len(o_fnum) != i + 1:
print('not same facets')
return False
# check that the regions are the same
regions_in_other_fnums = [[o_fnum[f] for f in r] for r in self.regions]
o_rnum = []
for i, s_region in enumerate(regions_in_other_fnums):
for j, o_region in enumerate(other_mesh.regions):
if j in o_rnum:
continue
else:
if set(s_region) == set(o_region):
o_rnum.append(j)
break
if len(o_rnum) != i + 1:
print('not same regions')
return False
# check that the seed numbers are the same
s_seed_nums = np.array(self.seed_numbers)
o_seed_nums = np.array(other_mesh.seed_numbers)
same &= np.all(s_seed_nums == o_seed_nums[o_rnum])
print('checking seed numbers', same)
# check that the phase numbers are the same
s_phase_nums = np.array(self.phase_numbers)
o_phase_nums = np.array(other_mesh.phase_numbers)
same &= np.all(s_phase_nums == o_phase_nums[o_rnum])
print('checking phase numbers', same)
return same
def kp_loop(kp_pairs):
loop = list(kp_pairs[0])
kp_arr = np.array(kp_pairs[1:])
while kp_arr.shape[0] > 0:
kp_find = loop[-1]
has_kp = np.any(kp_arr == kp_find, axis=1)
row = kp_arr[has_kp]
loop.append(row[row != kp_find][0])
kp_arr = kp_arr[~has_kp]
assert loop[0] == loop[-1]
return loop[:-1]
def _clip_cell(cell_data, domain):
domain_name = type(domain).__name__.lower()
if domain_name in ['rectangle', 'square', 'box', 'cube']:
return cell_data
if domain.n_dim == 2:
pts = np.array(cell_data['vertices'])
if np.all(domain.within(pts)):
return cell_data
# split the edges that contain the boundary
new_adj = np.copy(cell_data['adjacency'])
new_faces = []
new_pts = np.copy(cell_data['vertices'])
new_kps = []
for face in cell_data['faces']:
adj_cell = face['adjacent_cell']
verts = face['vertices']
face_pts = pts[verts]
pts_within = domain.within(face_pts)
if np.all(pts_within) or np.all(~pts_within):
new_faces.append(face)
continue
crossing_pt = _segment_cross(face_pts, domain)
# Add point to list of vertices and face to list of faces
crossing_kp = len(new_pts)
new_pts = np.vstack((new_pts, crossing_pt.reshape(1, -1)))
new_kps.append(crossing_kp)
for kp_i, kp in enumerate(verts):
kp_other = verts[1 - kp_i]
new_adj[kp] = [kp_other, crossing_kp]
new_verts = [kp, crossing_kp]
new_faces.append({'adjacent_cell': adj_cell,
'vertices': new_verts})
# add divider face
new_faces.append({'adjacent_cell': -1, 'vertices': new_kps})
# Create cell within the domain
new_within = domain.within(new_pts)
new_within[new_kps] = True
within_pts = new_pts[new_within]
kp_conv = np.full(len(new_pts), -1, dtype='int')
kp_conv[new_within] = np.arange(np.sum(new_within))
within_adj = [[] for pt in within_pts]
within_faces = []
for face in new_faces:
adj_cell = face['adjacent_cell']
old_verts = face['vertices']
new_verts = [kp_conv[v] for v in old_verts]
within_face = {'adjacent_cell': adj_cell, 'vertices': new_verts}
if all([v >= 0 for v in new_verts]):
within_faces.append(within_face)
within_adj[new_verts[0]].append(new_verts[1])
within_adj[new_verts[1]].append(new_verts[0])
# Compute cell area
within_loop = kp_loop([f['vertices'] for f in within_faces])
within_area = _loop_area(within_pts, within_loop)
new_cell_data = {'adjacency': within_adj,
'faces': within_faces,
'original': cell_data['original'],
'vertices': within_pts,
'volume': within_area}
return new_cell_data
w_str = 'Cannot clip cells to fit to a ' + domain_name + '.'
w_str = ' Currently 3D geometries are not supported, other than boxes.'
warnings.warn(w_str, RuntimeWarning)
return cell_data
def _segment_cross(pts, domain):
end_pts = np.copy(pts)
ds = np.inf
while ds > 1e-12:
within = domain.within(end_pts)
pt = end_pts.mean(axis=0)
pt_within = domain.within(pt)
if within[0] == pt_within:
end_pts[0] = pt
else:
end_pts[1] = pt
dx = end_pts[1] - end_pts[0]
ds = np.linalg.norm(dx)
return pt
def _loop_area(pts, loop):
double_area = 0
n = len(loop)
for i in range(n):
ip1 = (i + 1) % n
xi = pts[i][0]
yi = pts[i][1]
xip1 = pts[ip1][0]
yip1 = pts[ip1][1]
det = xi * yip1 - xip1 * yi
double_area += det
return 0.5 * np.abs(double_area)