3f617442 |
from collections import Counter
|
176c745c |
from collections.abc import Iterable, Sized
from itertools import chain, combinations
from math import factorial
|
8ab9b762 |
import scipy.spatial
|
3a985b30 |
|
571850ce |
from numpy import square, zeros, subtract, array, ones, dot, asarray, concatenate, average, eye, mean, abs, sqrt
from numpy import sum as nsum
from numpy.linalg import det as ndet
from numpy.linalg import slogdet, solve, matrix_rank, norm
|
0cb79900 |
|
ae0f30db |
def fast_norm(v):
|
67f564e3 |
""" Manually take the vector norm for len 2, 3 vectors. Defaults to a square root of the dot product
for larger vectors.
Note that for large vectors, it is possible for integer overflow to occur.
For instance:
vec = [49024, 59454, 12599, -63721, 18517, 27961]
dot(vec, vec) = -1602973744
"""
len_v = len(v)
|
ae0f30db |
# notice this method can be even more optimised
|
67f564e3 |
if len_v == 2:
|
571850ce |
return sqrt(v[0] * v[0] + v[1] * v[1])
|
67f564e3 |
if len_v == 3:
|
571850ce |
return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
return sqrt(dot(v, v))
|
0cb79900 |
|
ae0f30db |
|
12816a92 |
def fast_2d_point_in_simplex(point, simplex, eps=1e-8):
|
02e8d766 |
(p0x, p0y), (p1x, p1y), (p2x, p2y) = simplex
|
12816a92 |
px, py = point
|
716dbce8 |
area = 0.5 * (-p1y * p2x + p0y * (p2x - p1x) + p1x * p2y + p0x * (p1y - p2y))
|
12816a92 |
|
716dbce8 |
s = 1 / (2 * area) * (+p0y * p2x + (p2y - p0y) * px - p0x * p2y + (p0x - p2x) * py)
|
98f2f23b |
if s < -eps or s > 1 + eps:
|
22ae0755 |
return False
|
716dbce8 |
t = 1 / (2 * area) * (+p0x * p1y + (p0y - p1y) * px - p0y * p1x + (p1x - p0x) * py)
|
12816a92 |
|
98f2f23b |
return (t >= -eps) and (s + t <= 1 + eps)
|
12816a92 |
|
d1df8944 |
def point_in_simplex(point, simplex, eps=1e-8):
|
571850ce |
# simplex is list
|
d1df8944 |
if len(point) == 2:
return fast_2d_point_in_simplex(point, simplex, eps)
|
571850ce |
x0 = array(simplex[0], dtype=float)
vectors = array(simplex[1:], dtype=float) - x0
alpha = solve(vectors.T, point - x0)
|
d1df8944 |
return all(alpha > -eps) and sum(alpha) < 1 + eps
|
906058ff |
def fast_2d_circumcircle(points):
|
98f2f23b |
"""Compute the center and radius of the circumscribed circle of a triangle
|
d8e4af90 |
Parameters
----------
|
9631afec |
points: 2D array-like
the points of the triangle to investigate
|
d8e4af90 |
Returns
-------
|
9631afec |
tuple
|
98f2f23b |
(center point : tuple(int), radius: int)
|
906058ff |
"""
|
571850ce |
points = array(points)
|
3a985b30 |
# transform to relative coordinates
|
906058ff |
pts = points[1:] - points[0]
|
02e8d766 |
(x1, y1), (x2, y2) = pts
|
3a985b30 |
# compute the length squared
|
98f2f23b |
l1 = x1 * x1 + y1 * y1
l2 = x2 * x2 + y2 * y2
|
906058ff |
|
3a985b30 |
# compute some determinants
|
716dbce8 |
dx = +l1 * y2 - l2 * y1
dy = -l1 * x2 + l2 * x1
aa = +x1 * y2 - x2 * y1
|
906058ff |
a = 2 * aa
|
3a985b30 |
# compute center
|
98f2f23b |
x = dx / a
y = dy / a
|
571850ce |
radius = sqrt(x * x + y * y) # radius = norm([x, y])
|
906058ff |
|
3a985b30 |
return (x + points[0][0], y + points[0][1]), radius
|
906058ff |
def fast_3d_circumcircle(points):
|
98f2f23b |
"""Compute the center and radius of the circumscribed shpere of a simplex.
|
fd3784c0 |
Parameters
----------
|
9631afec |
points: 2D array-like
the points of the triangle to investigate
|
fd3784c0 |
Returns
-------
|
9631afec |
tuple
|
98f2f23b |
(center point : tuple(int), radius: int)
|
906058ff |
"""
|
571850ce |
points = array(points)
|
906058ff |
pts = points[1:] - points[0]
|
02e8d766 |
(x1, y1, z1), (x2, y2, z2), (x3, y3, z3) = pts
|
906058ff |
|
bf4d6422 |
l1 = x1 * x1 + y1 * y1 + z1 * z1
l2 = x2 * x2 + y2 * y2 + z2 * z2
l3 = x3 * x3 + y3 * y3 + z3 * z3
|
906058ff |
# Compute some determinants:
|
716dbce8 |
dx = +l1 * (y2 * z3 - z2 * y3) - l2 * (y1 * z3 - z1 * y3) + l3 * (y1 * z2 - z1 * y2)
dy = +l1 * (x2 * z3 - z2 * x3) - l2 * (x1 * z3 - z1 * x3) + l3 * (x1 * z2 - z1 * x2)
dz = +l1 * (x2 * y3 - y2 * x3) - l2 * (x1 * y3 - y1 * x3) + l3 * (x1 * y2 - y1 * x2)
aa = +x1 * (y2 * z3 - z2 * y3) - x2 * (y1 * z3 - z1 * y3) + x3 * (y1 * z2 - z1 * y2)
|
98f2f23b |
a = 2 * aa
|
906058ff |
|
bf4d6422 |
center = (dx / a, -dy / a, dz / a)
|
ae0f30db |
radius = fast_norm(center)
|
716dbce8 |
center = (
center[0] + points[0][0],
center[1] + points[0][1],
center[2] + points[0][2],
)
|
906058ff |
|
bf4d6422 |
return center, radius
|
906058ff |
|
ae6af683 |
def fast_det(matrix):
|
571850ce |
matrix = asarray(matrix, dtype=float)
|
ae6af683 |
if matrix.shape == (2, 2):
return matrix[0][0] * matrix[1][1] - matrix[1][0] * matrix[0][1]
elif matrix.shape == (3, 3):
a, b, c, d, e, f, g, h, i = matrix.ravel()
|
716dbce8 |
return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
|
ae6af683 |
else:
|
571850ce |
return ndet(matrix)
|
ae6af683 |
|
d1df8944 |
def circumsphere(pts):
dim = len(pts) - 1
if dim == 2:
return fast_2d_circumcircle(pts)
if dim == 3:
return fast_3d_circumcircle(pts)
# Modified method from http://mathworld.wolfram.com/Circumsphere.html
|
571850ce |
mat = array([[nsum(square(pt)), *pt, 1] for pt in pts])
center = zeros(dim)
a = 1 / (2 * ndet(mat[:, 1:]))
factor = a
ind = ones((dim + 2,), bool)
|
d1df8944 |
for i in range(1, len(pts)):
|
571850ce |
ind[i - 1] = True
ind[i] = False
center[i - 1] = factor * ndet(mat[:, ind])
factor *= -1
|
d1df8944 |
x0 = pts[0]
|
571850ce |
vec = subtract(center, x0)
radius = sqrt(dot(vec, vec))
|
d1df8944 |
return tuple(center), radius
|
95e09758 |
def orientation(face, origin):
|
98f2f23b |
"""Compute the orientation of the face with respect to a point, origin.
|
95e09758 |
Parameters
----------
|
0cb79900 |
face : array-like, of shape (N-dim, N-dim)
|
95e09758 |
The hyperplane we want to know the orientation of
Do notice that the order in which you provide the points is critical
|
0cb79900 |
origin : array-like, point of shape (N-dim)
|
95e09758 |
The point to compute the orientation from
Returns
-------
|
9631afec |
0 if the origin lies in the same hyperplane as face,
-1 or 1 to indicate left or right orientation
|
95e09758 |
|
9631afec |
If two points lie on the same side of the face, the orientation will
be equal, if they lie on the other side of the face, it will be negated.
|
95e09758 |
"""
|
571850ce |
vectors = array(face)
sign, logdet = slogdet(vectors - origin)
|
95e09758 |
if logdet < -50: # assume it to be zero when it's close to zero
return 0
return sign
|
0d651f85 |
|
b02c5eb5 |
def is_iterable_and_sized(obj):
return isinstance(obj, Iterable) and isinstance(obj, Sized)
|
d5c3cbdd |
|
e5f3deb7 |
def simplex_volume_in_embedding(vertices) -> float:
"""Calculate the volume of a simplex in a higher dimensional embedding.
|
2bbad875 |
That is: dim > len(vertices) - 1. For example if you would like to know the
|
e5f3deb7 |
surface area of a triangle in a 3d space.
|
50295475 |
This algorithm has not been tested for numerical stability.
|
e5f3deb7 |
Parameters
----------
|
50295475 |
vertices : 2D arraylike of floats
|
e5f3deb7 |
Returns
-------
|
50295475 |
volume : int
|
e5f3deb7 |
the volume of the simplex with given vertices.
Raises
------
|
50295475 |
ValueError
|
e5f3deb7 |
if the vertices do not form a simplex (for example,
because they are coplanar, colinear or coincident).
"""
# Implements http://mathworld.wolfram.com/Cayley-MengerDeterminant.html
# Modified from https://codereview.stackexchange.com/questions/77593/calculating-the-volume-of-a-tetrahedron
|
571850ce |
vertices = asarray(vertices, dtype=float)
|
ca15e808 |
dim = len(vertices[0])
if dim == 2:
# Heron's formula
|
716dbce8 |
a, b, c = scipy.spatial.distance.pdist(vertices, metric="euclidean")
|
ca15e808 |
s = 0.5 * (a + b + c)
|
571850ce |
return sqrt(s * (s - a) * (s - b) * (s - c))
|
ca15e808 |
# β_ij = |v_i - v_k|²
|
716dbce8 |
sq_dists = scipy.spatial.distance.pdist(vertices, metric="sqeuclidean")
|
e5f3deb7 |
# Add border while compressed
num_verts = scipy.spatial.distance.num_obs_y(sq_dists)
|
571850ce |
bordered = concatenate((ones(num_verts), sq_dists))
|
e5f3deb7 |
# Make matrix and find volume
sq_dists_mat = scipy.spatial.distance.squareform(bordered)
|
c60d8ca7 |
coeff = -((-2) ** (num_verts - 1)) * factorial(num_verts - 1) ** 2
|
ae6af683 |
vol_square = fast_det(sq_dists_mat) / coeff
|
e5f3deb7 |
|
0360e795 |
if vol_square < 0:
if abs(vol_square) < 1e-15:
return 0
|
716dbce8 |
raise ValueError("Provided vertices do not form a simplex")
|
e5f3deb7 |
|
571850ce |
return sqrt(vol_square)
|
e5f3deb7 |
|
0d651f85 |
class Triangulation:
|
0cb79900 |
"""A triangulation object.
|
0d651f85 |
|
0cb79900 |
Parameters
----------
coords : 2d array-like of floats
|
8ab9b762 |
Coordinates of vertices.
|
0d651f85 |
|
0cb79900 |
Attributes
----------
vertices : list of float tuples
Coordinates of the triangulation vertices.
simplices : set of integer tuples
List with indices of vertices forming individual simplices
|
f7fd43ad |
vertex_to_simplices : list of sets
Set of simplices connected to a vertex, the index of the vertex is the
index of the list.
|
0cb79900 |
hull : set of int
Exterior vertices
|
8ab9b762 |
Raises
------
ValueError
|
2bbad875 |
if the list of coordinates is incorrect or the points do not form one
or more simplices in the
|
0cb79900 |
"""
|
d5c3cbdd |
def __init__(self, coords):
|
b02c5eb5 |
if not is_iterable_and_sized(coords):
|
c2b2487d |
raise TypeError("Please provide a 2-dimensional list of points")
|
d5c3cbdd |
coords = list(coords)
|
b02c5eb5 |
if not all(is_iterable_and_sized(coord) for coord in coords):
|
c2b2487d |
raise TypeError("Please provide a 2-dimensional list of points")
|
e83a8986 |
if len(coords) == 0:
|
2bbad875 |
raise ValueError("Please provide at least one simplex")
|
e83a8986 |
# raise now because otherwise the next line will raise a less
|
237409b9 |
|
0d651f85 |
dim = len(coords[0])
if any(len(coord) != dim for coord in coords):
raise ValueError("Coordinates dimension mismatch")
|
237409b9 |
if dim == 1:
raise ValueError("Triangulation class only supports dim >= 2")
|
b8ff5c97 |
if len(coords) < dim + 1:
|
e83a8986 |
raise ValueError("Please provide at least one simplex")
|
0af50676 |
|
d5c3cbdd |
coords = list(map(tuple, coords))
|
571850ce |
vectors = subtract(coords[1:], coords[0])
if matrix_rank(vectors) < dim:
|
716dbce8 |
raise ValueError(
"Initial simplex has zero volumes "
"(the points are linearly dependent)"
)
|
ce9c74a4 |
|
0d651f85 |
self.vertices = list(coords)
self.simplices = set()
|
f7fd43ad |
# initialise empty set for each vertex
self.vertex_to_simplices = [set() for _ in coords]
|
8ab9b762 |
|
2bbad875 |
# find a Delaunay triangulation to start with, then we will throw it
|
8ab9b762 |
# away and continue with our own algorithm
initial_tri = scipy.spatial.Delaunay(coords)
for simplex in initial_tri.simplices:
self.add_simplex(simplex)
|
0d651f85 |
def delete_simplex(self, simplex):
simplex = tuple(sorted(simplex))
self.simplices.remove(simplex)
for vertex in simplex:
self.vertex_to_simplices[vertex].remove(simplex)
def add_simplex(self, simplex):
simplex = tuple(sorted(simplex))
self.simplices.add(simplex)
for vertex in simplex:
self.vertex_to_simplices[vertex].add(simplex)
|
777b2459 |
def get_vertices(self, indices):
|
01fb1201 |
return [self.get_vertex(i) for i in indices]
def get_vertex(self, index):
if index is None:
return None
return self.vertices[index]
|
7b329bc9 |
|
f7fd43ad |
def get_reduced_simplex(self, point, simplex, eps=1e-8) -> list:
|
0d651f85 |
"""Check whether vertex lies within a simplex.
Returns
-------
vertices : list of ints
|
07ce0505 |
Indices of vertices of the simplex to which the vertex belongs.
An empty list indicates that the vertex is outside the simplex.
|
0d651f85 |
"""
|
98f2f23b |
# XXX: in the end we want to lose this method
|
0d651f85 |
if len(simplex) != self.dim + 1:
# We are checking whether point belongs to a face.
simplex = self.containing(simplex).pop()
|
571850ce |
x0 = array(self.vertices[simplex[0]])
vectors = array(self.get_vertices(simplex[1:])) - x0
alpha = solve(vectors.T, point - x0)
|
0d651f85 |
if any(alpha < -eps) or sum(alpha) > 1 + eps:
return []
|
ae0f30db |
result = [i for i, a in enumerate(alpha, 1) if a > eps]
if sum(alpha) < 1 - eps:
result.insert(0, 0)
|
0d651f85 |
return [simplex[i] for i in result]
|
beee622f |
def point_in_simplex(self, point, simplex, eps=1e-8):
|
d1df8944 |
vertices = self.get_vertices(simplex)
return point_in_simplex(point, vertices, eps)
|
12816a92 |
|
0d651f85 |
def locate_point(self, point):
"""Find to which simplex the point belongs.
Return indices of the simplex containing the point.
Empty tuple means the point is outside the triangulation
"""
for simplex in self.simplices:
|
beee622f |
if self.point_in_simplex(point, simplex):
|
12816a92 |
return simplex
|
0d651f85 |
return ()
@property
def dim(self):
return len(self.vertices[0])
def faces(self, dim=None, simplices=None, vertices=None):
"""Iterator over faces of a simplex or vertex sequence."""
if dim is None:
dim = self.dim
if simplices is not None and vertices is not None:
raise ValueError("Only one of simplices and vertices is allowed.")
if vertices is not None:
vertices = set(vertices)
simplices = chain(*(self.vertex_to_simplices[i] for i in vertices))
simplices = set(simplices)
elif simplices is None:
simplices = self.simplices
|
716dbce8 |
faces = (face for tri in simplices for face in combinations(tri, dim))
|
0d651f85 |
if vertices is not None:
return (face for face in faces if all(i in vertices for i in face))
else:
return faces
def containing(self, face):
"""Simplices containing a face."""
return set.intersection(*(self.vertex_to_simplices[i] for i in face))
|
725886b8 |
def _extend_hull(self, new_vertex, eps=1e-8):
|
50a53e4f |
# count multiplicities in order to get all hull faces
multiplicities = Counter(face for face in self.faces())
hull_faces = [face for face, count in multiplicities.items() if count == 1]
|
7d8a8523 |
|
c3b85d7d |
# compute the center of the convex hull, this center lies in the hull
# we do not really need the center, we only need a point that is
# guaranteed to lie strictly within the hull
|
7b329bc9 |
hull_points = self.get_vertices(self.hull)
|
571850ce |
pt_center = average(hull_points, axis=0)
|
c3b85d7d |
|
0d651f85 |
pt_index = len(self.vertices)
self.vertices.append(new_vertex)
|
de643c2c |
new_simplices = set()
|
0d651f85 |
for face in hull_faces:
|
92eca331 |
# do orientation check, if orientation is the same, it lies on
# the same side of the face, otherwise, it lies on the other
# side of the face
|
7b329bc9 |
pts_face = tuple(self.get_vertices(face))
|
92eca331 |
orientation_inside = orientation(pts_face, pt_center)
orientation_new_point = orientation(pts_face, new_vertex)
if orientation_inside == -orientation_new_point:
# if the orientation of the new vertex is zero or directed
# towards the center, do not add the simplex
|
381cedb9 |
simplex = (*face, pt_index)
if not self._simplex_is_almost_flat(simplex):
self.add_simplex(simplex)
new_simplices.add(simplex)
|
0d651f85 |
|
de643c2c |
if len(new_simplices) == 0:
|
0d651f85 |
# We tried to add an internal point, revert and raise.
for tri in self.vertex_to_simplices[pt_index]:
self.simplices.remove(tri)
del self.vertex_to_simplices[pt_index]
del self.vertices[pt_index]
raise ValueError("Candidate vertex is inside the hull.")
|
de643c2c |
return new_simplices
|
e9476950 |
|
ae0f30db |
def circumscribed_circle(self, simplex, transform):
|
98f2f23b |
"""Compute the center and radius of the circumscribed circle of a simplex.
|
9631afec |
Parameters
----------
simplex : tuple of ints
the simplex to investigate
Returns
-------
|
98f2f23b |
tuple (center point, radius)
|
9631afec |
The center and radius of the circumscribed circle
|
692f8b4e |
"""
|
571850ce |
pts = dot(self.get_vertices(simplex), transform)
|
d1df8944 |
return circumsphere(pts)
|
692f8b4e |
|
ae0f30db |
def point_in_cicumcircle(self, pt_index, simplex, transform):
# return self.fast_point_in_circumcircle(pt_index, simplex, transform)
|
725886b8 |
eps = 1e-8
|
ae0f30db |
center, radius = self.circumscribed_circle(simplex, transform)
|
571850ce |
pt = dot(self.get_vertices([pt_index]), transform)[0]
|
777b2459 |
|
571850ce |
return norm(center - pt) < (radius * (1 + eps))
|
725886b8 |
@property
|
ae0f30db |
def default_transform(self):
|
571850ce |
return eye(self.dim)
|
725886b8 |
|
ae0f30db |
def bowyer_watson(self, pt_index, containing_simplex=None, transform=None):
|
98f2f23b |
"""Modified Bowyer-Watson point adding algorithm.
|
3976816c |
|
98f2f23b |
Create a hole in the triangulation around the new point,
then retriangulate this hole.
|
3976816c |
|
0ccade4a |
Parameters
----------
|
9631afec |
pt_index: number
the index of the point to inspect
|
0ccade4a |
Returns
-------
|
9631afec |
deleted_simplices : set of tuples
Simplices that have been deleted
new_simplices : set of tuples
Simplices that have been added
|
692f8b4e |
"""
|
3976816c |
queue = set()
done_simplices = set()
|
692f8b4e |
|
ae0f30db |
transform = self.default_transform if transform is None else transform
|
725886b8 |
|
0af50676 |
if containing_simplex is None:
queue.update(self.vertex_to_simplices[pt_index])
else:
queue.add(containing_simplex)
|
3976816c |
bad_triangles = set()
while len(queue):
simplex = queue.pop()
done_simplices.add(simplex)
|
ae0f30db |
if self.point_in_cicumcircle(pt_index, simplex, transform):
|
3976816c |
self.delete_simplex(simplex)
|
695d4e09 |
todo_points = set(simplex)
|
3976816c |
bad_triangles.add(simplex)
|
1e33f914 |
# Get all simplices that share at least a point with the simplex
|
01fb1201 |
neighbors = self.get_neighbors_from_vertices(todo_points)
|
1e33f914 |
# Filter out the already evaluated simplices
|
01fb1201 |
neighbors = neighbors - done_simplices
neighbors = self.get_face_sharing_neighbors(neighbors, simplex)
queue.update(neighbors)
|
3976816c |
faces = list(self.faces(simplices=bad_triangles))
multiplicities = Counter(face for face in faces)
|
22ae0755 |
hole_faces = [face for face in faces if multiplicities[face] < 2]
|
3976816c |
for face in hole_faces:
if pt_index not in face:
|
381cedb9 |
simplex = (*face, pt_index)
if not self._simplex_is_almost_flat(simplex):
self.add_simplex(simplex)
|
692f8b4e |
|
4c7dbeeb |
new_triangles = self.vertex_to_simplices[pt_index]
return bad_triangles - new_triangles, new_triangles - bad_triangles
|
692f8b4e |
|
381cedb9 |
def _simplex_is_almost_flat(self, simplex):
return self._relative_volume(simplex) < 1e-8
def _relative_volume(self, simplex):
"""Compute the volume of a simplex divided by the average (Manhattan)
distance of its vertices. The advantage of this is that the relative
volume is only dependent on the shape of the simplex and not on the
absolute size. Due to the weird scaling, the only use of this method
is to check that a simplex is almost flat."""
|
571850ce |
vertices = array(self.get_vertices(simplex))
|
381cedb9 |
vectors = vertices[1:] - vertices[0]
|
571850ce |
average_edge_length = mean(abs(vectors))
|
381cedb9 |
return self.volume(simplex) / (average_edge_length ** self.dim)
|
ae0f30db |
def add_point(self, point, simplex=None, transform=None):
|
0d651f85 |
"""Add a new vertex and create simplices as appropriate.
Parameters
----------
point : float vector
Coordinates of the point to be added.
|
ae0f30db |
transform : N*N matrix of floats
|
725886b8 |
Multiplication matrix to apply to the point (and neighbouring
simplices) when running the Bowyer Watson method.
|
0d651f85 |
simplex : tuple of ints, optional
Simplex containing the point. Empty tuple indicates points outside
the hull. If not provided, the algorithm costs O(N), so this should
be used whenever possible.
"""
|
12816a92 |
point = tuple(point)
|
0d651f85 |
if simplex is None:
simplex = self.locate_point(point)
|
0af50676 |
actual_simplex = simplex
|
f7fd43ad |
self.vertex_to_simplices.append(set())
|
0af50676 |
|
0d651f85 |
if not simplex:
|
4c7dbeeb |
temporary_simplices = self._extend_hull(point)
|
3976816c |
pt_index = len(self.vertices) - 1
|
716dbce8 |
deleted_simplices, added_simplices = self.bowyer_watson(
pt_index, transform=transform
)
|
4c7dbeeb |
deleted = deleted_simplices - temporary_simplices
added = added_simplices | (temporary_simplices - deleted_simplices)
return deleted, added
|
0d651f85 |
else:
|
d84eed2d |
reduced_simplex = self.get_reduced_simplex(point, simplex)
|
0d651f85 |
if not reduced_simplex:
|
c8120cad |
self.vertex_to_simplices.pop() # revert adding vertex
|
716dbce8 |
raise ValueError("Point lies outside of the specified simplex.")
|
0d651f85 |
else:
simplex = reduced_simplex
if len(simplex) == 1:
|
f7fd43ad |
self.vertex_to_simplices.pop() # revert adding vertex
|
0d651f85 |
raise ValueError("Point already in triangulation.")
else:
|
0af50676 |
pt_index = len(self.vertices)
self.vertices.append(point)
|
ae0f30db |
return self.bowyer_watson(pt_index, actual_simplex, transform)
|
0d651f85 |
def volume(self, simplex):
|
571850ce |
prefactor = factorial(self.dim)
vertices = array(self.get_vertices(simplex))
|
7b329bc9 |
vectors = vertices[1:] - vertices[0]
|
ae6af683 |
return float(abs(fast_det(vectors)) / prefactor)
|
0d651f85 |
|
98f2f23b |
def volumes(self):
return [self.volume(sim) for sim in self.simplices]
|
0d651f85 |
def reference_invariant(self):
"""vertex_to_simplices and simplices are compatible."""
for vertex in range(len(self.vertices)):
|
716dbce8 |
if any(vertex not in tri for tri in self.vertex_to_simplices[vertex]):
|
0d651f85 |
return False
for simplex in self.simplices:
|
716dbce8 |
if any(simplex not in self.vertex_to_simplices[pt] for pt in simplex):
|
0d651f85 |
return False
return True
def vertex_invariant(self, vertex):
"""Simplices originating from a vertex don't overlap."""
raise NotImplementedError
|
01fb1201 |
def get_neighbors_from_vertices(self, simplex):
|
716dbce8 |
return set.union(*[self.vertex_to_simplices[p] for p in simplex])
|
01fb1201 |
def get_face_sharing_neighbors(self, neighbors, simplex):
"""Keep only the simplices sharing a whole face with simplex."""
|
716dbce8 |
return {
simpl for simpl in neighbors if len(set(simpl) & set(simplex)) == self.dim
} # they share a face
|
01fb1201 |
def get_simplices_attached_to_points(self, indices):
# Get all simplices that share at least a point with the simplex
neighbors = self.get_neighbors_from_vertices(indices)
return self.get_face_sharing_neighbors(neighbors, indices)
def get_opposing_vertices(self, simplex):
if simplex not in self.simplices:
raise ValueError("Provided simplex is not part of the triangulation")
neighbors = self.get_simplices_attached_to_points(simplex)
|
716dbce8 |
|
01fb1201 |
def find_opposing_vertex(vertex):
# find the simplex:
simp = next((x for x in neighbors if vertex not in x), None)
if simp is None:
return None
opposing = set(simp) - set(simplex)
assert len(opposing) == 1
return opposing.pop()
|
716dbce8 |
|
01fb1201 |
result = tuple(find_opposing_vertex(v) for v in simplex)
return result
|
50a53e4f |
@property
def hull(self):
"""Compute hull from triangulation.
|
0d651f85 |
Parameters
----------
|
eef11f0b |
check : bool, default: True
|
0d651f85 |
Whether to raise an error if the computed hull is different from
stored.
Returns
-------
hull : set of int
Vertices in the hull.
"""
counts = Counter(self.faces())
if any(i > 2 for i in counts.values()):
|
716dbce8 |
raise RuntimeError(
"Broken triangulation, a (N-1)-dimensional"
" appears in more than 2 simplices."
)
|
0d651f85 |
|
716dbce8 |
hull = {point for face, count in counts.items() if count == 1 for point in face}
|
0d651f85 |
return hull
def convex_invariant(self, vertex):
"""Hull is convex."""
raise NotImplementedError
|