import numpy as np
def grp_start_len(a):
"""Given a sorted 1D input array `a`, e.g., [0 0, 1, 2, 3, 4, 4, 4], this
routine returns the indices where the blocks of equal integers start and
how long the blocks are.
"""
# https://stackoverflow.com/a/50394587/353337
m = np.concatenate([[True], a[:-1] != a[1:], [True]])
idx = np.flatnonzero(m)
return idx[:-1], np.diff(idx)
def _column_stack(a, b):
# https://stackoverflow.com/a/39638773/353337
return np.stack([a, b], axis=1)
def unique_rows(a):
# The cleaner alternative `np.unique(a, axis=0)` is slow; cf.
# <https://github.com/numpy/numpy/issues/11136>.
b = np.ascontiguousarray(a).view(
np.dtype((np.void, a.dtype.itemsize * a.shape[1]))
)
a_unique, inv, cts = np.unique(b, return_inverse=True, return_counts=True)
a_unique = a_unique.view(a.dtype).reshape(-1, a.shape[1])
return a_unique, inv, cts
def cpt_tri_areas_and_ce_ratios(ei_dot_ej):
"""Given triangles (specified by their edges), this routine will return the
triangle areas and the signed distances of the triangle circumcenters to
the edge midpoints.
"""
cell_volumes = 0.5 * np.sqrt(
ei_dot_ej[2] * ei_dot_ej[0]
+ ei_dot_ej[0] * ei_dot_ej[1]
+ ei_dot_ej[1] * ei_dot_ej[2]
)
ce = -ei_dot_ej * 0.25 / cell_volumes[None]
return cell_volumes, ce
def compute_triangle_circumcenters(X, ei_dot_ei, ei_dot_ej):
"""
Computes the circumcenters of all given triangles.
"""
alpha = ei_dot_ei * ei_dot_ej
alpha_sum = alpha[0] + alpha[1] + alpha[2]
beta = alpha / alpha_sum[None]
a = X * beta[..., None]
cc = a[0] + a[1] + a[2]
return cc
[docs]
class VoroBuild(object):
"""
Class for handling triangular meshes.
"""
def __init__(self):
# nothing to do here
return
def __del__(self):
# nothing to do here
return
[docs]
def initVoronoi(self, nodes, cells, sort_cells=False):
"""Initialization.
"""
if sort_cells:
cells = np.sort(cells, axis=1)
cells = cells[cells[:, 0].argsort()]
self.node_coords = nodes
self._edge_lengths = None
assert (
len(nodes.shape) == 2
), "Illegal node coordinates shape {}".format(nodes.shape)
assert (
len(cells.shape) == 2 and cells.shape[1] == 3
), "Illegal cells shape {}".format(cells.shape)
# super(VoroBuild, self).__init__(nodes, cells)
self.node_is_used = np.zeros(len(nodes), dtype=bool)
self.node_is_used[cells] = True
self.cells = {"nodes": cells}
self._interior_ce_ratios = None
self._control_volumes = None
self._cell_partitions = None
self._cv_centroids = None
self._surface_areas = None
self.edges = None
self._cell_circumcenters = None
self._signed_cell_areas = None
self.subdomains = {}
self._is_interior_node = None
self._is_boundary_node = None
self.is_boundary_edge = None
self._is_boundary_facet = None
self._interior_edge_lengths = None
self._ce_ratios = None
self._edges_cells = None
self._edge_gid_to_edge_list = None
self._edge_to_edge_gid = None
self._cell_centroids = None
self.local_idx = np.array([[1, 2], [2, 0], [0, 1]]).T
nds = self.cells["nodes"].T
self.idx_hierarchy = nds[self.local_idx]
# The inverted local index.
# This array specifies for each of the three nodes which edge endpoints
# correspond to it. For the above local_idx, this should give
#
# [[(1, 1), (0, 2)], [(0, 0), (1, 2)], [(1, 0), (0, 1)]]
#
self.local_idx_inv = [
[tuple(i) for i in zip(*np.where(self.local_idx == k))]
for k in range(3)
]
# Create the corresponding edge coordinates.
self.half_edge_coords = (
self.node_coords[self.idx_hierarchy[1]]
- self.node_coords[self.idx_hierarchy[0]]
)
self.ei_dot_ej = np.einsum(
"ijk, ijk->ij",
self.half_edge_coords[[1, 2, 0]],
self.half_edge_coords[[2, 0, 1]],
)
e = self.half_edge_coords
self.ei_dot_ei = np.einsum("ijk, ijk->ij", e, e)
self.cell_volumes, self.ce_ratios = cpt_tri_areas_and_ce_ratios(
self.ei_dot_ej
)
return
@property
def edge_lengths(self):
if self._edge_lengths is None:
self._edge_lengths = np.sqrt(self.ei_dot_ei)
return self._edge_lengths
@property
def ce_ratios_per_interior_edge(self):
if self._interior_ce_ratios is None:
if "edges" not in self.cells:
self.create_edges()
self._ce_ratios = np.zeros(len(self.edges["nodes"]))
np.add.at(self._ce_ratios, self.cells["edges"].T, self.ce_ratios)
self._interior_ce_ratios = self._ce_ratios[
~self.is_boundary_edge_individual
]
return self._interior_ce_ratios
@property
def control_volumes(self):
if self._control_volumes is None:
v = self.cell_partitions
# Summing up the arrays first makes the work for np.add.at
# lighter.
ids = self.cells["nodes"].T
vals = np.array(
[
sum([v[i] for i in np.where(self.local_idx.T == k)[0]])
for k in range(3)
]
)
control_volume_data = [(ids, vals)]
# sum up from self.control_volume_data
self._control_volumes = np.zeros(len(self.node_coords))
for d in control_volume_data:
# TODO fastfunc
np.add.at(self._control_volumes, d[0], d[1])
return self._control_volumes
@property
def surface_areas(self):
if self._surface_areas is None:
self._surface_areas = self._compute_surface_areas(
np.arange(len(self.cells["nodes"]))
)
return self._surface_areas
@property
def control_volume_centroids(self):
# This function is necessary, e.g., for Lloyd's
# smoothing <https://en.wikipedia.org/wiki/Lloyd%27s_algorithm>.
#
# The centroid of any volume V is given by
#
# c = \int_V x / \int_V 1.
#
# The denominator is the control volume. The numerator can be computed
# by making use of the fact that the control volume around any vertex
# v_0 is composed of right triangles, two for each adjacent cell.
if self._cv_centroids is None:
_, v = self._compute_integral_x()
# Again, make use of the fact that edge k is opposite of node k in
# every cell. Adding the arrays first makes the work for
# np.add.at lighter.
ids = self.cells["nodes"].T
vals = np.array(
[v[1, 1] + v[0, 2], v[1, 2] + v[0, 0], v[1, 0] + v[0, 1]]
)
centroid_data = [(ids, vals)]
# add it all up
num_components = centroid_data[0][1].shape[-1]
self._cv_centroids = np.zeros((len(self.node_coords),
num_components))
for d in centroid_data:
# TODO fastfunc
np.add.at(self._cv_centroids, d[0], d[1])
# Divide by the control volume
self._cv_centroids /= self.control_volumes[:, None]
return self._cv_centroids
@property
def signed_cell_areas(self):
"""Signed area of a triangle in 2D.
"""
# http://mathworld.wolfram.com/TriangleArea.html
assert (
self.node_coords.shape[1] == 2
), "Signed areas only make sense for triangles in 2D."
if self._signed_cell_areas is None:
# One could make p contiguous by adding a copy(), but that's not
# really worth it here.
p = self.node_coords[self.cells["nodes"]].T
# <https://stackoverflow.com/q/50411583/353337>
self._signed_cell_areas = (
+p[0][2] * (p[1][0] - p[1][1])
+ p[0][0] * (p[1][1] - p[1][2])
+ p[0][1] * (p[1][2] - p[1][0])
) / 2
return self._signed_cell_areas
[docs]
def mark_boundary(self):
if self.edges is None:
self.create_edges()
assert self.is_boundary_edge is not None
self._is_boundary_node = np.zeros(len(self.node_coords), dtype=bool)
self._is_boundary_node[
self.idx_hierarchy[..., self.is_boundary_edge]] = True
self._is_interior_node = self.node_is_used & ~self.is_boundary_node
self._is_boundary_facet = self.is_boundary_edge
return
@property
def is_boundary_node(self):
if self._is_boundary_node is None:
self.mark_boundary()
return self._is_boundary_node
@property
def is_interior_node(self):
if self._is_interior_node is None:
self.mark_boundary()
return self._is_interior_node
@property
def is_boundary_facet(self):
if self._is_boundary_facet is None:
self.mark_boundary()
return self._is_boundary_facet
[docs]
def create_edges(self):
"""Set up edge-node and edge-cell relations.
"""
# Reshape into individual edges.
# Sort the columns to make it possible for `unique()` to identify
# individual edges.
s = self.idx_hierarchy.shape
a = np.sort(self.idx_hierarchy.reshape(s[0], -1).T)
a_unique, inv, cts = unique_rows(a)
assert np.all(
cts < 3
), "No edge has more than 2 cells. Are cells listed twice?"
self.is_boundary_edge = (cts[inv] == 1).reshape(s[1:])
self.is_boundary_edge_individual = cts == 1
self.edges = {"nodes": a_unique}
# cell->edges relationship
self.cells["edges"] = inv.reshape(3, -1).T
self._edges_cells = None
self._edge_gid_to_edge_list = None
# Store an index {boundary,interior}_edge -> edge_gid
self._edge_to_edge_gid = [
[],
np.where(self.is_boundary_edge_individual)[0],
np.where(~self.is_boundary_edge_individual)[0],
]
return
@property
def edges_cells(self):
if self._edges_cells is None:
self._compute_edges_cells()
return self._edges_cells
def _compute_edges_cells(self):
"""This creates interior edge->cells relations. While it's not
necessary for many applications, it sometimes does come in handy.
"""
if self.edges is None:
self.create_edges()
num_edges = len(self.edges["nodes"])
counts = np.zeros(num_edges, dtype=int)
np.add.at(
counts,
self.cells["edges"],
np.ones(self.cells["edges"].shape, dtype=int),
)
# <https://stackoverflow.com/a/50395231/353337>
edges_flat = self.cells["edges"].flat
idx_sort = np.argsort(edges_flat)
idx_start, count = grp_start_len(edges_flat[idx_sort])
res1 = idx_sort[idx_start[count == 1]][:, np.newaxis]
idx = idx_start[count == 2]
res2 = np.column_stack([idx_sort[idx], idx_sort[idx + 1]])
self._edges_cells = [
[], # no edges with zero adjacent cells
res1 // 3,
res2 // 3,
]
# For each edge, store the number of adjacent cells plus the index into
# the respective edge array.
self._edge_gid_to_edge_list = np.empty((num_edges, 2), dtype=int)
self._edge_gid_to_edge_list[:, 0] = count
c1 = count == 1
l1 = np.sum(c1)
self._edge_gid_to_edge_list[c1, 1] = np.arange(l1)
c2 = count == 2
l2 = np.sum(c2)
self._edge_gid_to_edge_list[c2, 1] = np.arange(l2)
assert l1 + l2 == len(count)
return
@property
def edge_gid_to_edge_list(self):
if self._edge_gid_to_edge_list is None:
self._compute_edges_cells()
return self._edge_gid_to_edge_list
@property
def face_partitions(self):
# face = edge for triangles.
# The partition is simply along the center of the edge.
edge_lengths = self.edge_lengths
return np.array([0.5 * edge_lengths, 0.5 * edge_lengths])
@property
def cell_partitions(self):
if self._cell_partitions is None:
# Compute the control volumes. Note that
# 0.5 * (0.5 * edge_length) * covolume
# = 0.25 * edge_length**2 * ce_ratio_edge_ratio
self._cell_partitions = 0.25 * self.ei_dot_ei * self.ce_ratios
return self._cell_partitions
@property
def cell_circumcenters(self):
if self._cell_circumcenters is None:
node_cells = self.cells["nodes"].T
self._cell_circumcenters = compute_triangle_circumcenters(
self.node_coords[node_cells], self.ei_dot_ei, self.ei_dot_ej
)
return self._cell_circumcenters
@property
def cell_centroids(self):
"""Computes the centroids (barycenters) of all triangles.
"""
if self._cell_centroids is None:
self._cell_centroids = (
np.sum(self.node_coords[self.cells["nodes"]], axis=1) / 3.0
)
return self._cell_centroids
@property
def cell_barycenters(self):
return self.cell_centroids
@property
def inradius(self):
# See <http://mathworld.wolfram.com/Incircle.html>.
abc = np.sqrt(self.ei_dot_ei)
return 2 * self.cell_volumes / np.sum(abc, axis=0)
@property
def circumradius(self):
# See <http://mathworld.wolfram.com/Incircle.html>.
a, b, c = np.sqrt(self.ei_dot_ei)
return (a * b * c) / np.sqrt(
(a + b + c) * (-a + b + c) * (a - b + c) * (a + b - c)
)
@property
def triangle_quality(self):
# q = 2 * r_in / r_out
# = (-a+b+c) * (+a-b+c) * (+a+b-c) / (a*b*c),
#
# where r_in is the incircle radius and r_out the circumcircle radius
# and a, b, c are the edge lengths.
a, b, c = np.sqrt(self.ei_dot_ei)
return (-a + b + c) * (a - b + c) * (a + b - c) / (a * b * c)
@property
def angles(self):
# The cosines of the angles are the negative dot products of the
# normalized edges adjacent to the angle.
norms = np.sqrt(self.ei_dot_ei)
normalized_ei_dot_ej = np.array(
[
self.ei_dot_ej[0] / norms[1] / norms[2],
self.ei_dot_ej[1] / norms[2] / norms[0],
self.ei_dot_ej[2] / norms[0] / norms[1],
]
)
return np.arccos(-normalized_ei_dot_ej)
def _compute_integral_x(self):
r"""Computes the integral of x,
\int_V x,
over all atomic "triangles", i.e., areas cornered by a node, an edge
midpoint, and a circumcenter.
"""
# The integral of any linear function over a triangle is the average of
# the values of the function in each of the three corners, times the
# area of the triangle.
right_triangle_vols = self.cell_partitions
node_edges = self.idx_hierarchy
corner = self.node_coords[node_edges]
edge_midpoints = 0.5 * (corner[0] + corner[1])
cc = self.cell_circumcenters
average = (corner + edge_midpoints[None] + cc[None, None]) / 3.0
contribs = right_triangle_vols[None, :, :, None] * average
return node_edges, contribs
def _compute_surface_areas(self, cell_ids):
"""For each edge, one half of the the edge goes to each of the end
points. Used for Neumann boundary conditions if on the boundary of the
mesh and transition conditions if in the interior.
"""
# Each of the three edges may contribute to the surface areas of all
# three vertices. Here, only the two adjacent nodes receive a
# contribution, but other approaches (e.g., the flat cell corrector),
# may contribute to all three nodes.
cn = self.cells["nodes"][cell_ids]
ids = np.stack([cn, cn, cn], axis=1)
half_el = 0.5 * self.edge_lengths[..., cell_ids]
zero = np.zeros([half_el.shape[1]])
vals = np.stack(
[
np.column_stack([zero, half_el[0], half_el[0]]),
np.column_stack([half_el[1], zero, half_el[1]]),
np.column_stack([half_el[2], half_el[2], zero]),
],
axis=1,
)
return ids, vals
[docs]
def compute_curl(self, vector_field):
r"""Computes the curl of a vector field over the mesh. While the vector
field is point-based, the curl will be cell-based. The approximation is
based on
.. math::
n\cdot curl(F) = \lim_{A\\to 0} |A|^{-1} <\int_{dGamma}, F> dr;
see <https://en.wikipedia.org/wiki/Curl_(mathematics)>. Actually, to
approximate the integral, one would only need the projection of the
vector field onto the edges at the midpoint of the edges.
"""
# Compute the projection of A on the edge at each edge midpoint.
# Take the average of `vector_field` at the endpoints to get the
# approximate value at the edge midpoint.
A = 0.5 * np.sum(vector_field[self.idx_hierarchy], axis=0)
# sum of <edge, A> for all three edges
sum_edge_dot_A = np.einsum("ijk, ijk->j", self.half_edge_coords, A)
# Get normalized vector orthogonal to triangle
z = np.cross(self.half_edge_coords[0], self.half_edge_coords[1])
# Now compute
#
# curl = z / ||z|| * sum_edge_dot_A / |A|.
#
# Since ||z|| = 2*|A|, one can save a sqrt and do
#
# curl = z * sum_edge_dot_A * 0.5 / |A|^2.
#
curl = z * (0.5 * sum_edge_dot_A / self.cell_volumes ** 2)[..., None]
return curl