Skip to content

Commit 28f561a

Browse files
committed
new extension for level set reinitialization
1 parent 904acaf commit 28f561a

File tree

10 files changed

+278
-10
lines changed

10 files changed

+278
-10
lines changed

Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
1212
MMG_jll = "86086c02-e288-5929-a127-40944b0018b7"
1313
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
1414
MarchingCubes = "299715c1-40a9-479a-aaf9-4a633d36f717"
15+
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
1516

1617
[extensions]
1718
InterpolationsExt = "Interpolations"
1819
MMGSurfaceExt = ["MMG_jll", "MarchingCubes"]
1920
MMGVolumeExt = "MMG_jll"
2021
MakieExt = "Makie"
22+
ReinitializationExt = ["Interpolations", "NearestNeighbors"]
2123

2224
[compat]
2325
Interpolations = "0.15, 0.16"
2426
LinearAlgebra = "1"
2527
MMG_jll = "5"
2628
Makie = "0.21, 0.22, 0.23, 0.24"
2729
MarchingCubes = "0.1.10"
30+
NearestNeighbors = "0.4"
2831
StaticArrays = "1"
2932
julia = "1.9"

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ LevelSetMethods = "ca3df947-0575-4ae6-b9a3-78df393a7451"
88
LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589"
99
MMG_jll = "86086c02-e288-5929-a127-40944b0018b7"
1010
MarchingCubes = "299715c1-40a9-479a-aaf9-4a633d36f717"
11+
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
1112
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1213

1314
[sources]

docs/make.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ makedocs(;
4444
"time-integrators.md",
4545
"boundary-conditions.md",
4646
"Extensions" =>
47-
["extension-makie.md", "extension-mmg.md", "extension-interpolations.md"],
47+
["extension-makie.md", "extension-mmg.md", "extension-interpolations.md", "extension-reinitialization.md"],
4848
"Examples" => ["example-zalesak.md", "example-shape-optim.md"],
4949
numbered_pages,
5050
),
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Reinitialization
2+
3+
The `ReinitializationExt` extension provides a [`reinitialize!`](@ref) method to
4+
transform a level set function into a signed distance function by computing the closest
5+
point on the interface using Newton's method.
6+
7+
## Usage
8+
9+
After loading the required dependencies for this extension (`Interpolations` and
10+
`NearestNeighbors`), simply call `reinitialize!` on a `LevelSet` or a `LevelSetEquation` to
11+
reinitialize the level set function in-place:
12+
13+
```julia
14+
using LevelSetMethods
15+
using CairoMakie
16+
using NearestNeighbors
17+
using Interpolations
18+
19+
grid = CartesianGrid((-1, -1), (1, 1), (100, 100))
20+
# An ellipse
21+
sdf = LevelSet(x -> sqrt(x[1]^2 + x[2]^2) - 0.5, grid)
22+
ϕ = LevelSet(x -> x[1]^2 + x[2]^2 - 0.5^2, grid)
23+
LevelSetMethods.set_makie_theme!()
24+
fig = Figure(; size = (800, 300))
25+
ax1 = Axis(fig[1, 1]; title="Signed Distance")
26+
ax2 = Axis(fig[1, 2]; title="Before Reinitialization", ylabel = "", yticklabelsvisible = false)
27+
ax3 = Axis(fig[1, 3]; title="After Reinitialization", ylabel = "", yticklabelsvisible = false)
28+
29+
contour!(ax1, sdf; levels = [0.25, 0, 0.5], labels = true, labelsize = 14)
30+
contour!(ax2, ϕ; levels = [0.25, 0, 0.5], labels = true, labelsize = 14)
31+
32+
# Reinitialize using Newton's method
33+
reinitialize!(ϕ)
34+
contour!(ax3, ϕ; levels = [0.25, 0, 0.5], labels = true, labelsize = 14)
35+
fig
36+
```
37+
38+
You can easily check that the reinitialized level set function is indeed a signed distance:
39+
40+
```julia
41+
max_er = maximum(eachindex(grid)) do i
42+
abs(ϕ[i] - sdf[i])
43+
end
44+
@test max_er < 1e-10 # hide
45+
println("Maximum error after reinitialization: $max_er")
46+
```

docs/src/refs.bib

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,23 @@ @article{mitchell2007toolbox
3030
}
3131

3232
@article{allaire2004structural,
33-
title={Structural optimization using sensitivity analysis and a level-set method},
34-
author={Allaire, Gr{\'e}goire and Jouve, Fran{\c{c}}ois and Toader, Anca-Maria},
35-
journal={Journal of computational physics},
36-
volume={194},
37-
number={1},
38-
pages={363--393},
39-
year={2004},
40-
publisher={Elsevier}
33+
title = {Structural optimization using sensitivity analysis and a level-set method},
34+
author = {Allaire, Gr{\'e}goire and Jouve, Fran{\c{c}}ois and Toader, Anca-Maria},
35+
journal = {Journal of computational physics},
36+
volume = {194},
37+
number = {1},
38+
pages = {363--393},
39+
year = {2004},
40+
publisher = {Elsevier}
41+
}
42+
43+
@article{saye2014high,
44+
title = {High-order methods for computing distances to implicitly defined surfaces},
45+
author = {Saye, Robert},
46+
journal = {Communications in Applied Mathematics and Computational Science},
47+
volume = {9},
48+
number = {1},
49+
pages = {107--141},
50+
year = {2014},
51+
publisher = {Mathematical Sciences Publishers}
4152
}

ext/ReinitializationExt.jl

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
module ReinitializationExt
2+
3+
import LevelSetMethods as LSM
4+
using Interpolations
5+
using NearestNeighbors
6+
using LinearAlgebra
7+
using StaticArrays
8+
9+
function __init__()
10+
return @info "Loading extension for Newton reinitialization"
11+
end
12+
13+
function LSM.reinitialize!(eq::LSM.LevelSetEquation; kwargs...)
14+
LSM.reinitialize!(LSM.current_state(eq); kwargs...)
15+
return eq
16+
end
17+
18+
function LSM.reinitialize!(
19+
ϕ::LSM.LevelSet;
20+
upsample::Int = 4,
21+
maxiters::Int = 10,
22+
xtol::Float64 = 1.0e-8,
23+
ftol::Float64 = 1.0e-8,
24+
)
25+
grid = LSM.mesh(ϕ)
26+
vals = ϕ.vals
27+
itp = Interpolations.interpolate(ϕ)
28+
f(x) = itp(x...)
29+
∇f(x) = Interpolations.gradient(itp, x...)
30+
∇²f(x) = Interpolations.hessian(itp, x...)
31+
32+
# Sample the interface
33+
pts = _sample_interface(grid, f, ∇f, upsample, maxiters, ftol)
34+
tree = KDTree(pts)
35+
36+
for I in eachindex(grid)
37+
x = grid[I]
38+
# Find closest point in the cloud
39+
idx, dist = nn(tree, x)
40+
x0 = pts[idx]
41+
# Refine with Newton's method
42+
cp = _closest_point(f, ∇f, ∇²f, x, x0, maxiters, xtol, ftol)
43+
vals[I] = sign(vals[I]) * norm(x - cp)
44+
end
45+
return ϕ
46+
end
47+
48+
function _sample_interface(grid, f, ∇f, upsample, maxiter, ftol)
49+
pts = Vector{SVector{LSM.dimension(grid), Float64}}()
50+
for I in CartesianIndices(LSM.size(grid) .- 1)
51+
Ip = CartesianIndex(Tuple(I) .+ 1)
52+
lc, hc = grid[I], grid[Ip]
53+
samples = (lc .+ (hc .- lc) .* (SVector(j, k) .+ 0.5) ./ upsample for j in 0:(upsample - 1), k in 0:(upsample - 1))
54+
# Skip cells where all samples have the same sign
55+
all(x -> f(x) > 0, samples) || all(x -> f(x) < 0, samples) && continue
56+
# Go over samples and push them to the interface
57+
for x in samples
58+
pt = _project_to_interface(f, ∇f, x, maxiter, ftol)
59+
push!(pts, pt)
60+
end
61+
end
62+
return pts
63+
end
64+
65+
function _project_to_interface(f, ∇f, x0, maxiter, ftol)
66+
x = x0
67+
for _ in 1:maxiter
68+
val = f(x)
69+
val < ftol && break # close enough to the interface
70+
grad = ∇f(x)
71+
norm_grad = norm(grad)
72+
iszero(norm_grad) && break
73+
δx = val * grad / norm_grad^2
74+
x = x - δx
75+
end
76+
f(x) > ftol && @warn "projection to interface did not converge to $ftol at x=$x"
77+
return x
78+
end
79+
80+
function _closest_point(f, ∇f, ∇²f, xq::SVector, x0::SVector, maxiters, xtol, ftol)
81+
x = x0
82+
∇p_x0 = ∇f(x0)
83+
λ = dot(xq - x0, ∇p_x0) / dot(∇p_x0, ∇p_x0)
84+
85+
converged = false
86+
for _ in 1:maxiters
87+
px = f(x)
88+
∇p = ∇f(x)
89+
∇²p = ∇²f(x)
90+
91+
# System for Newton's method
92+
# ∇L = [ x - xq + λ∇p ] = 0
93+
# [ p(x) ]
94+
grad_L = vcat(x - xq + λ * ∇p, px)
95+
96+
# Hessian of the Lagrangian
97+
# H_L = [ I + λ∇²p ∇p ]
98+
# [ ∇p' 0 ]
99+
hess_L = hcat(vcat(I + λ * ∇²p, ∇p'), vcat(∇p, 0))
100+
101+
# Solve for the update
102+
δ = -hess_L \ grad_L
103+
δx = δ[1:(end - 1)]
104+
δλ = δ[end]
105+
106+
# Update variables
107+
α = 1.0 # TODO: reduce step size if not diverging?
108+
x = x + α * δx
109+
λ = λ + α * δλ
110+
111+
# Check for convergence
112+
if norm(δx) < xtol && norm(f(x)) < ftol
113+
converged = true
114+
break
115+
end
116+
end
117+
118+
converged || @warn "closest point search did not converge at xq=$xq"
119+
120+
return x
121+
end
122+
123+
end

src/LevelSetMethods.jl

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export CartesianGrid,
3030
WENO5,
3131
LevelSetEquation,
3232
integrate!,
33-
current_time
33+
current_time,
34+
reinitialize!
3435

3536
"""
3637
makie_theme()
@@ -49,4 +50,39 @@ function set_makie_theme! end
4950
function export_volume_mesh end
5051
function export_surface_mesh end
5152

53+
"""
54+
reinitialize!(ϕ::LevelSet; upsample=2, maxiters=10, xtol=1e-8)
55+
56+
Reinitializes the level set `ϕ` to a signed distance, modifying it in place.
57+
58+
The method works by first sampling the zero-level set of the interface, and then for each
59+
grid point, finding the closest point on the interface using a Newton-based method. The
60+
distance to the closest point is then used as the new value of the level set at that grid
61+
point, with the sign determined by the original level set value. See [saye2014high](@cite)
62+
for more details.
63+
64+
## Arguments
65+
66+
- `ϕ`: The level set to reinitialize.
67+
68+
## Keyword Arguments
69+
70+
- `upsample`: number of samples to take in each cell when sampling the interface.
71+
Higher values yield better initial guesses for the closest point search, but increase
72+
the computational cost.
73+
- `maxiters`: maximum number of iterations to use in the Newton's method
74+
to find the closest point on the interface.
75+
- `xtol`: convergence tolerance for the Newton's method. The iterations stop when
76+
the change in position is less than `xtol`.
77+
78+
!!! note
79+
This functionality is provided by the `ReinitializationExt` module, which
80+
requires loading `Interpolations.jl` and `NearestNeighbors.jl`.
81+
"""
82+
function reinitialize! end
83+
84+
# tomatoes tomatos ...
85+
reinitialise!(args...; kwargs...) = reinitialize!(args...; kwargs...)
86+
87+
5288
end # module

src/levelsetequation.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,20 @@ function _compute_terms(terms, ϕ, I, t)
233233
return _compute_term(term, ϕ, I, t)
234234
end
235235
end
236+
237+
"""
238+
grad_norm(ϕ::LevelSet[, I])
239+
240+
Compute the norm of the gradient of ϕ at index `I`, i.e. `|∇ϕ|`, or for all grid points
241+
if `I` is not provided.
242+
"""
243+
function grad_norm(ϕ::LevelSet)
244+
msg = """level-set must have boundary conditions to compute gradient. See
245+
`add_boundary_conditions`."""
246+
has_boundary_conditions(ϕ) || error(msg)
247+
idxs = eachindex(ϕ)
248+
return map(i -> _compute_∇_norm(sign(ϕ[i]), ϕ, i), idxs)
249+
end
250+
function grad_norm(eq::LevelSetEquation)
251+
return grad_norm(current_state(eq))
252+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
33
LevelSetMethods = "ca3df947-0575-4ae6-b9a3-78df393a7451"
44
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
55
MMG_jll = "86086c02-e288-5929-a127-40944b0018b7"
6+
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
67
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
78
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

test/test-reinitialization.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Test
2+
using LinearAlgebra
3+
using Interpolations
4+
using NearestNeighbors
5+
import LevelSetMethods as LSM
6+
7+
@testset "Newton Reinitialization" begin
8+
grid = LSM.CartesianGrid((-1, -1), (1, 1), (100, 100))
9+
10+
# A level set that is not a signed distance function
11+
sdf = (x) -> sqrt(x[1]^2 + x[2]^2) - 0.5
12+
ϕ = LSM.LevelSet(x -> (x[1]^2 + x[2]^2) - 0.5^2, grid)
13+
eq = LSM.LevelSetEquation(;
14+
terms = (), # No terms, just for holding the state
15+
levelset = ϕ,
16+
bc = LSM.NeumannGradientBC(),
17+
)
18+
@test abs(LSM.volume(ϕ) - π / 4) < 1.0e-2
19+
20+
# check that we recover a signed distance function
21+
LSM.reinitialize!(eq; upsample = 4, maxiters = 20, xtol = 1.0e-10, ftol = 1.0e-10)
22+
max_er, max_idx = findmax(eachindex(grid)) do i
23+
x = grid[i]
24+
norm(ϕ[i] - sdf(x))
25+
end
26+
@test max_er < 1.0e-8
27+
28+
# check that volume is OK
29+
@test abs(LSM.volume(ϕ) - π / 4) < 1.0e-2
30+
end

0 commit comments

Comments
 (0)