You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, weighted triangulations do not work, and so we error if the user tries to create one (actually, weighted triangulations of convex polygons do work with triangulate_convex, but anyway). The point of this issue is to (1) explain the theory behind weighted triangulations, (2) the algorithm, and (3) our implementation. I'll finish by giving an actually example of it breaking and explaining all the pieces that are in the code and tests thus far.
Weighted Triangulations
The following explanation is based on Delaunay Mesh Generation by Cheng, Dey, and Shewchuk (2013).
Suppose we have a set of points $\mathcal P \subset \mathbb R$. To each point $p_i \in \mathcal P$ we associate a weight $w_i$ and define the lifted point (or lifted companion) $p_i^+ = (x_i, y_i, x_i^2 + y_i^2 - w_i)$; this $z$ coordinate $x_i^2 + y_i^2 - w_i$ is the distance of the lifted point from the paraboloid $z = x^2 + y^2$, in particular:
$w_i > 0$: The point is below the paraboloid.
$w_i < 0$: The point is above the paraboloid.
$w_i = 0$: The point is on the paraboloid.
The weighted Delaunay triangulation, which I'll denote $\mathcal W\mathcal T(\mathcal P)$, is the projection of the underside of the convex hull of $\mathcal P^+$, the set of all lifted points, onto the plane. For example, here is a MATLAB script that can compute a weighted Delaunay triangulation (I also give this script here
The triangulations of convex polygons are computed with the code:
digits(16)
rng(123)
ctr = 79;
for pass = 1:3
if pass == 1
range = 6:20;
elseif pass == 2
range = 21:25:1500;
else
range = [100, 15000];
end
for n = range
if pass < 3
n0 = 0;
while n0 <= 3
points = randn(n, 2);
convex_poly = points(convhull(points), :);
points = convex_poly(1:end-1, :);
n0 = size(points, 1);
end
weights = randn(n0, 1);
else
theta = linspace(0, 2*pi, n);
x = cos(theta);
y = sin(theta);
points = [x(:), y(:)];
points = points(1:end-1, :);
n0 = size(points, 1);
weights = randn(n0, 1);
end
s = 10*rand;
u = rand;
if u < 1/250
weights = 0 * weights;
elseif (1/250 <= u) && (u < 1/5)
weights = weights.^2;
elseif (1/5 <= u) && (u < 0.6)
weights = s * weights;
elseif (u <= 0.6) && (u < 0.65)
sparse_prob = rand(n0);
weights(sparse_prob >= 0.95) = 0;
end
save_to_file(points, weights, ctr)
ctr = ctr + 1;
end
end
=#
) The save_to_file function computes the triangulation and saves it to a .txt file.
function [cells, lifted] = LiftingMap(points,weights)
num = size(points, 1);
lifted = zeros(num, 3);
for i =1:num
p = points(i, :);
lifted(i, :) = [p sum(p.^2) - weights(i)];
end
ch = convhull(lifted);
sep_ch = reshape(lifted(ch, :), [size(ch) 3]);
vecs = diff(sep_ch, 1, 2);
n = cross(vecs(:, 1, :), vecs(:, 2, :), 3);
downward_faces = n(:, :, 3) <=0;
cells = ch(downward_faces, :);
endfunction save_to_file(points, weights, n)
[lift_cells, ~] = LiftingMap(points,weights);
res = [(1+size(points, 1)) * ones(1, 3); [pointsweights]; lift_cells];
% so the points are in res(2:res(1), 1:2), the weights are in% res(2:res(1), 3), and the triangles are in res((res(1)+1):end, :).
writematrix(res, fullfile(pwd, 'tempmats', strcat(string(n), '.txt')));
end
One issue with weighted triangulations, and indeed a big part of the reason they don't yet work in this package, is the problem of submerged vertices. Given a vertex $v$, if the associated with $w_v$ is so small that its lifted companion $p_v^+$ is not on the underside of the convex hull of $\mathcal P^+$, then it will not appear in $\mathcal W\mathcal T(\mathcal P)$ at all. In the example below, the fourth vertex appears with a weight of $w_4 = -0.35$ but changing the weight to $w_4=-0.4$ makes it no longer appear in the triangulation, i.e. it becomes a submerged vertex.
Lastly, note that a weighted triangulation where $w_v \equiv w$ for all $v$, i.e. the weights are all constants, is just the Delaunay triangulation.
Computing Weighted Delaunay Triangulations (In Theory)
In this package, we use the Bowyer-Watson algorithm for computing triangulations. To understand how weighted Delaunay triangulations can be computed, the same algorithm can be used with some slight modifications. It might be helpful to read https://juliageometry.github.io/DelaunayTriangulation.jl/dev/math/delaunay/#Bowyer-Watson-Algorithm for the unweighted case before proceeding. To summarise, the Bowyer-Watson algorithm for inserting a point $u$ into a triangulation is:
Find one triangle whose open circumdisk contains $u$. (Actually, we find a triangle containing $u$ instead of just an open circumdisk, but anyway.)
Find all the others by a depth-first search.
Delete all the triangles containing $u$ in their open circumdisk.
For each edge of the evacuated polyhedral cavity, adjoin the vertices to $u$ to create a new triangle.
Note that this assumes the point $u$ is inside the triangulation. For points outside of the triangulation, we use ghost triangles as described here https://juliageometry.github.io/DelaunayTriangulation.jl/dev/math/delaunay/#Exterior-Points. For weighted triangulations, the rules for working with ghost triangles change slightly: A ghost triangle is deleted if a vertex is inserted in its outer halfplane, except perhaps if the vertex lies on the solid edge of the ghost triangle, in which case the ghost triangle is deleted if the new vertex is not submerged.
For the next modification for weighted triangulations, we need to replace how the incircle predicate is computed. In particular, instead of checking incircle(u, v, w, x) > 0 (see the DigCavity function in the mentioned textbook on p.63), we need to look at orient3(u⁺, v⁺, w⁺, x⁺), which tests whether $x^+$ is below the witness plane of the triangle $uvw$; the witness plane to a non-submerged point is a non-vertical plane in space that contains the triangle on the convex hull of $\mathcal P^+$ such that no other vertex in the convex hull is below the witness plane (see Def 2.5 in the book). It is also important that, when we put an input triangle whose open circumdisk contains $u$ into our algorithm, it must be a triangle whose witness plane is above the inserted vertice's lifted point. If $u$ is submerged, no such triangle exists and so it should not be inserted.
Finally, while the book does mention point location using the jump and march algorithm (as we do in this package), in two dimensions they also mention a conflict graph method (Section 2.6) which seems to have better handling for weighted triangulations. When I emailed Shewchuk previously about this, he also referred back to this conflict graph approach. I would really prefer to be able to avoid implementing this since I'm not sure how well it works with dynamic updates and I think we can get around it.
What's Implemented in the Package
Before I list what's actually not working with my implementation of weighted triangulations, let me list out what I've implemented in the package already (whether there are any bugs or not is to be determined I guess). Most of these functions have corresponding tests you can search for. Some of these functions were implemented so long ago that I don't really remember their purpose. A lot of tests are here https://github.com/JuliaGeometry/DelaunayTriangulation.jl/blob/41ccf2319bd637f92bcda3df817e91c776b83216/test/triangulation/weighted.jl, and some test triangulations are generated in the code at
. Some tests in test/triangulation/weighted.jl are broken because triangulate errors when weights are provided, but you can Revise the check_config function to skip over that if you want.
Tests the position of the vertex `ℓ` of `tri` relative to the circumcircle of the triangle `T = (i, j, k)`. The returned value is a [`Certificate`](@ref), which is one of:
- `Outside`: `ℓ` is outside of the circumcircle of `T`.
- `On`: `ℓ` is on the circumcircle of `T`.
- `Inside`: `ℓ` is inside the circumcircle of `T`.
!!! note "Ghost triangles"
The circumcircle of a ghost triangle is defined as the oriented outer halfplane of the solid edge of the triangle. See [`point_position_relative_to_oriented_outer_halfplane`](@ref).
!!! note "Weighted triangulations"
If `tri` is a weighted triangulation, then an orientation test is instead applied, testing the orientation of the lifted companions of each point to determine if
`ℓ` is above or below the witness plane relative to `(i, j, k)`. For ghost triangles, the same rule applies, although if the vertex is on the solid edge of the ghost triangle then,
in addition to checking [`point_position_relative_to_oriented_outer_halfplane`](@ref), we must check if the new vertex is not submerged by the adjoining solid triangle.
"""
point_position_relative_to_circumcircle
functionpoint_position_relative_to_circumcircle(tri::Triangulation, i, j, k, ℓ)
point_position_relative_to_witness_plane(tri::Triangulation, i, j, k, ℓ) -> Certificate
Given a positively oriented triangle `T = (i, j, k)` of `tri` and a vertex `ℓ` of `tri`, returns the position of `ℓ` relative to the witness plane of `T`. The returned value is a [`Certificate`](@ref), which is one of:
- `Above`: `ℓ` is above the witness plane of `T`.
- `On`: `ℓ` is on the witness plane of `T`.
- `Below`: `ℓ` is below the witness plane of `T`.
# Extended help
The witness plane of a triangle is defined as the plane through the triangle `(i⁺, j⁺, k⁺)` where, for example, `pᵢ⁺ = (x, y, x^2 + y^2 - wᵢ)` is the lifted companion of `i`
and `(x, y)` are the coordinates of the `i`th vertex. Moreover, by the orientation of `ℓ` relative to this witness plane we are referring to `ℓ⁺`'s position, not the plane point `ℓ`.
"""
functionpoint_position_relative_to_witness_plane(tri::Triangulation, i, j, k, ℓ)
get_distance_to_witness_plane(tri::Triangulation, i, V) -> Float64
Computes the distance between the lifted companion of the vertex `i` and the witness plane to the triangle `V`. If `V` is a ghost triangle
and `i` is not on its solid edge, then the distance is `-Inf` if it is below the ghost triangle's witness plane and `Inf` if it is above. If `V` is a ghost triangle and `i`
is on its solid edge, then the distance returned is the distance associated with the solid triangle adjoining `V`.
In general, the distance is positive if the lifted vertex is above the witness plane, negative if it is below,
and zero if it is on the plane.
See also [`point_position_relative_to_witness_plane`](@ref) and [`get_distance_to_plane`](@ref).
"""
functionget_distance_to_witness_plane(tri::Triangulation, i, V)
get_power_distance(tri::Triangulation, i, j) -> Float64
Returns the power distance between vertices `i` and `j`, defined by
`||pᵢ - pⱼ||^2 - wᵢ - wⱼ`, where `wᵢ` and `wⱼ` are the respective weights.
"""
functionget_power_distance(tri::Triangulation, i, j)
dij² =edge_length_sqr(tri, i, j)
wᵢ =get_weight(tri, i)
wⱼ =get_weight(tri, j)
return dij² - wᵢ - wⱼ
end
triangulate_convex
Triangulating convex polygons with weights actually already works. You can just pass triangulate_convex a set of weights and it'll work. This functions uses a slightly specialised version of the Bowyer-Watson algorithm though, so might not be too much to take from that. This is tested at
This part here is why the Bowyer-Watson algorithm is not implemented for weighted triangulations yet. I don't understand why it sometimes
fails here for submerged points. Need to look into it some more.
=#
cert =point_position_relative_to_circumcircle(tri, V, _new_point) # redirects to point_position_relative_to_witness_plane
is_outside(cert) &&return V # If the point is submerged, then we don't add it
end
A Broken Example
Let's now give an example of a weighted triangulation not being computed correctly. For this, I have locally disabled check_config to get past the errors. Consider
using Random
Random.seed!(123)
points = [0.01.00.00.25-0.5; 0.00.01.00.250.5]
weights = [0.0, 0.0, 0.0, -1.0, 1.0]
tri =triangulate(points; weights, insertion_order = [1, 2, 4, 5, 3])
# force insertion order since there are other issues like KeyErrors or infinite loops sometimes, don't know why. # Feel free to run it without forcing the order to see what else can happen.# It gets it correct for insertion_order = [1, 2, 3, 4, 5]!
The fourth vertex is supposed to be submerged, so that the triangulation we want is
but instead we get
triplot(tri)
The fourth vertex is not only not submerged, but it has a dangling edge attached to it and doesn't form an actual triangle. Look at all the problems this triangulation has:
julia> DelaunayTriangulation.validate_triangulation(tri) # on main
The adjacent2vertex map is inconsistent with the adjacent map. The vertex 5 and edge (2, 3) are inconsistent with the associated edge set Set([(3, -1), (1, 2), (-1, 1), (2, 3)]) from the adjacent2vertex map, as (2, 3) maps to 4 instead of 5.
The triangle (3, 5, 2) is incorrectly stored in the adjacent map, with the edge (2, 3) instead mapping to 4.
The Delaunay criterion does not hold for the triangle-vertex pair ((3, 4, 2), 1).
The triangle (3, 2, 4) is negatively oriented.
The edge iterator has duplicate edges.
The solid_edge iterator has duplicate edges.
What's going wrong here? Why is 4 not being submerged? Let's look at it step by step. Here is code that gets the triangulation one step at a time, using exactly what triangulate does.
Random.seed!(123)
points = [0.01.00.00.25-0.5; 0.00.01.00.250.5]
weights = [0.0, 0.0, 0.0, -1.0, 1.0]
tri =Triangulation(points; weights)
_tri =deepcopy(tri)
DelaunayTriangulation.initialise_bowyer_watson!(tri, [1, 2, 4, 5, 3])
_tri2 =deepcopy(tri)
remaining_points = [1, 2, 4, 5, 3][(begin+3):end]
initial_search_point = DelaunayTriangulation.get_initial_search_point(tri, 1,
remaining_points[1], [1, 2, 4, 5, 3], DelaunayTriangulation.default_num_samples,
Random.default_rng(), true)
DelaunayTriangulation.add_point_bowyer_watson!(tri, remaining_points[1], initial_search_point, Random.default_rng()) # new point is in (1, 4, -1)
_tri3 =deepcopy(tri)
initial_search_point = DelaunayTriangulation.get_initial_search_point(tri, 2,
remaining_points[2], [1, 2, 4, 5, 3], DelaunayTriangulation.default_num_samples,
Random.default_rng(), true)
DelaunayTriangulation.add_point_bowyer_watson!(tri, remaining_points[2], initial_search_point, Random.default_rng()) # new point is in (5, 4, -1)
_tri4 =deepcopy(tri)
Let's now validate at each stage.
julia> DelaunayTriangulation.validate_triangulation(_tri)
true
julia> DelaunayTriangulation.validate_triangulation(_tri2)
true
julia> DelaunayTriangulation.validate_triangulation(_tri3)
The triangle (5, 2, 4) is degenerately oriented.
false
julia> DelaunayTriangulation.validate_triangulation(_tri4)
The adjacent2vertex map is inconsistent with the adjacent map. The vertex 5 and edge (2, 3) are inconsistent with the associated edge set Set([(3, -1), (1, 2), (-1, 1), (2, 3)]) from the adjacent2vertex map, as (2, 3) maps to 4 instead of 5.
The triangle (3, 5, 2) is incorrectly stored in the adjacent map, with the edge (2, 3) instead mapping to 4.
The Delaunay criterion does not hold for the triangle-vertex pair ((3, 4, 2), 1).
The triangle (3, 2, 4) is negatively oriented.
The edge iterator has duplicate edges.
The solid_edge iterator has duplicate edges.
false
julia> DelaunayTriangulation.get_point(_tri3, 5, 2, 4)
((-0.5, 0.5), (1.0, 0.0), (0.25, 0.25))
There's signs of trouble starting at _tri3. Why is it trying to make a triangle from a degenerate arrangement?
We can probably go even further in debugging (believe me, I have tried many times), but that might be enough for someone to have some ideas. It probably would've been better to use an example that breaks without any collinearity issues to avoid that distraction, but alas.
I think a big issue is that somehow the submerged vertices, once in the triangulation (a submerged vertex might not be submerged for a given $\mathcal P$, but it could be for $\mathcal P \cup {p}$), are not currently being correctly identified as being submerged later on, and so they stay in the triangulation and cause issues. I don't know how to fix this.
The text was updated successfully, but these errors were encountered:
Currently, weighted triangulations do not work, and so we error if the user tries to create one (actually, weighted triangulations of convex polygons do work with
triangulate_convex
, but anyway). The point of this issue is to (1) explain the theory behind weighted triangulations, (2) the algorithm, and (3) our implementation. I'll finish by giving an actually example of it breaking and explaining all the pieces that are in the code and tests thus far.Weighted Triangulations
The following explanation is based on Delaunay Mesh Generation by Cheng, Dey, and Shewchuk (2013).
Suppose we have a set of points$\mathcal P \subset \mathbb R$ . To each point $p_i \in \mathcal P$ we associate a weight $w_i$ and define the lifted point (or lifted companion) $p_i^+ = (x_i, y_i, x_i^2 + y_i^2 - w_i)$ ; this $z$ coordinate $x_i^2 + y_i^2 - w_i$ is the distance of the lifted point from the paraboloid $z = x^2 + y^2$ , in particular:
The weighted Delaunay triangulation, which I'll denote$\mathcal W\mathcal T(\mathcal P)$ , is the projection of the underside of the convex hull of $\mathcal P^+$ , the set of all lifted points, onto the plane. For example, here is a MATLAB script that can compute a weighted Delaunay triangulation (I also give this script here
DelaunayTriangulation.jl/test/helper_functions.jl
Lines 1461 to 1571 in 41ccf23
save_to_file
function computes the triangulation and saves it to a.txt
file.One issue with weighted triangulations, and indeed a big part of the reason they don't yet work in this package, is the problem of submerged vertices. Given a vertex$v$ , if the associated with $w_v$ is so small that its lifted companion $p_v^+$ is not on the underside of the convex hull of $\mathcal P^+$ , then it will not appear in $\mathcal W\mathcal T(\mathcal P)$ at all. In the example below, the fourth vertex appears with a weight of $w_4 = -0.35$ but changing the weight to $w_4=-0.4$ makes it no longer appear in the triangulation, i.e. it becomes a submerged vertex.
Lastly, note that a weighted triangulation where$w_v \equiv w$ for all $v$ , i.e. the weights are all constants, is just the Delaunay triangulation.
Computing Weighted Delaunay Triangulations (In Theory)
In this package, we use the Bowyer-Watson algorithm for computing triangulations. To understand how weighted Delaunay triangulations can be computed, the same algorithm can be used with some slight modifications. It might be helpful to read https://juliageometry.github.io/DelaunayTriangulation.jl/dev/math/delaunay/#Bowyer-Watson-Algorithm for the unweighted case before proceeding. To summarise, the Bowyer-Watson algorithm for inserting a point$u$ into a triangulation is:
Note that this assumes the point$u$ is inside the triangulation. For points outside of the triangulation, we use ghost triangles as described here https://juliageometry.github.io/DelaunayTriangulation.jl/dev/math/delaunay/#Exterior-Points. For weighted triangulations, the rules for working with ghost triangles change slightly: A ghost triangle is deleted if a vertex is inserted in its outer halfplane, except perhaps if the vertex lies on the solid edge of the ghost triangle, in which case the ghost triangle is deleted if the new vertex is not submerged.
For the next modification for weighted triangulations, we need to replace how the$x^+$ is below the witness plane of the triangle $uvw$ ; the witness plane to a non-submerged point is a non-vertical plane in space that contains the triangle on the convex hull of $\mathcal P^+$ such that no other vertex in the convex hull is below the witness plane (see Def 2.5 in the book). It is also important that, when we put an input triangle whose open circumdisk contains $u$ into our algorithm, it must be a triangle whose witness plane is above the inserted vertice's lifted point. If $u$ is submerged, no such triangle exists and so it should not be inserted.
incircle
predicate is computed. In particular, instead of checkingincircle(u, v, w, x) > 0
(see theDigCavity
function in the mentioned textbook on p.63), we need to look atorient3(u⁺, v⁺, w⁺, x⁺)
, which tests whetherFinally, while the book does mention point location using the jump and march algorithm (as we do in this package), in two dimensions they also mention a conflict graph method (Section 2.6) which seems to have better handling for weighted triangulations. When I emailed Shewchuk previously about this, he also referred back to this conflict graph approach. I would really prefer to be able to avoid implementing this since I'm not sure how well it works with dynamic updates and I think we can get around it.
What's Implemented in the Package
Before I list what's actually not working with my implementation of weighted triangulations, let me list out what I've implemented in the package already (whether there are any bugs or not is to be determined I guess). Most of these functions have corresponding tests you can search for. Some of these functions were implemented so long ago that I don't really remember their purpose. A lot of tests are here https://github.com/JuliaGeometry/DelaunayTriangulation.jl/blob/41ccf2319bd637f92bcda3df817e91c776b83216/test/triangulation/weighted.jl, and some test triangulations are generated in the code at
DelaunayTriangulation.jl/test/helper_functions.jl
Lines 1461 to 1663 in 41ccf23
triangulate
errors when weights are provided, but you can Revise thecheck_config
function to skip over that if you want.add_point!(tri, x, y, w)
DelaunayTriangulation.jl/src/algorithms/triangulation/basic_operations/add_point.jl
Lines 111 to 120 in 41ccf23
point_position_relative_to_circumcircle
This function already uses an
is_weighted
check to convert to the test of the witness plane orientation.DelaunayTriangulation.jl/src/predicates/predicates.jl
Lines 476 to 520 in 41ccf23
point_position_relative_to_witness_plane
As it says on the tin.
DelaunayTriangulation.jl/src/predicates/predicates.jl
Lines 521 to 541 in 41ccf23
is_submerged
DelaunayTriangulation.jl/src/data_structures/triangulation/methods/weights.jl
Lines 156 to 177 in 41ccf23
get_weighted_nearest_neighbour
I have no idea why this is needed. Maybe it was for power diagrams later?
DelaunayTriangulation.jl/src/data_structures/triangulation/methods/weights.jl
Lines 123 to 154 in 41ccf23
get_distance_to_witness_plane
As above, I don't know why this is needed, except maybe it's for power diagrams or something.
DelaunayTriangulation.jl/src/data_structures/triangulation/methods/weights.jl
Lines 90 to 121 in 41ccf23
get_power_distance
Must be for power diagrams.
DelaunayTriangulation.jl/src/data_structures/triangulation/methods/weights.jl
Lines 77 to 88 in 41ccf23
triangulate_convex
Triangulating convex polygons with weights actually already works. You can just pass
triangulate_convex
a set of weights and it'll work. This functions uses a slightly specialised version of the Bowyer-Watson algorithm though, so might not be too much to take from that. This is tested atDelaunayTriangulation.jl/test/triangulation/weighted.jl
Lines 256 to 263 in 41ccf23
fi = 77
(the last iteration) when usingvalidate_triangulation
. I don't know why.. it used to work.Inside Bowyer-Watson
Also see
DelaunayTriangulation.jl/src/algorithms/triangulation/unconstrained_triangulation.jl
Lines 147 to 154 in ee4fa67
A Broken Example
Let's now give an example of a weighted triangulation not being computed correctly. For this, I have locally disabled
check_config
to get past the errors. ConsiderThe fourth vertex is supposed to be submerged, so that the triangulation we want is
but instead we get
triplot(tri)
The fourth vertex is not only not submerged, but it has a dangling edge attached to it and doesn't form an actual triangle. Look at all the problems this triangulation has:
What's going wrong here? Why is
4
not being submerged? Let's look at it step by step. Here is code that gets the triangulation one step at a time, using exactly whattriangulate
does.Let's now validate at each stage.
There's signs of trouble starting at
_tri3
. Why is it trying to make a triangle from a degenerate arrangement?We can probably go even further in debugging (believe me, I have tried many times), but that might be enough for someone to have some ideas. It probably would've been better to use an example that breaks without any collinearity issues to avoid that distraction, but alas.
I think a big issue is that somehow the submerged vertices, once in the triangulation (a submerged vertex might not be submerged for a given$\mathcal P$ , but it could be for $\mathcal P \cup {p}$ ), are not currently being correctly identified as being submerged later on, and so they stay in the triangulation and cause issues. I don't know how to fix this.
The text was updated successfully, but these errors were encountered: