Skip to content

Commit aeb77f4

Browse files
authored
Merge pull request #65 from maltezfaria/reinitialization-ext
New extension for level set reinitialization
2 parents 904acaf + 92a3012 commit aeb77f4

13 files changed

+291
-29
lines changed

Project.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LevelSetMethods"
22
uuid = "ca3df947-0575-4ae6-b9a3-78df393a7451"
33
authors = ["Luiz M. Faria and Nicolas Lebbe"]
4-
version = "0.1.4"
4+
version = "0.1.5"
55

66
[deps]
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -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: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,20 @@ using GLMakie
44
using MMG_jll
55
using MarchingCubes
66
using Interpolations
7+
using NearestNeighbors
78
using StaticArrays
89
using DocumenterCitations
910

1011
bib = CitationBibliography(joinpath(@__DIR__, "src", "refs.bib"); style = :numeric)
1112

12-
DocMeta.setdocmeta!(
13-
LevelSetMethods,
14-
:DocTestSetup,
15-
:(using LevelSetMethods);
16-
recursive = true,
17-
)
18-
1913
const page_rename = Dict("developer.md" => "Developer docs") # Without the numbers
2014
const numbered_pages = [
2115
file for file in readdir(joinpath(@__DIR__, "src")) if
2216
file != "index.md" && splitext(file)[2] == ".md" && occursin(r"^\d", file)
2317
]
2418

2519
modules = [LevelSetMethods]
26-
for extension in [:MakieExt, :MMGSurfaceExt, :MMGVolumeExt, :InterpolationsExt]
20+
for extension in [:MakieExt, :MMGSurfaceExt, :MMGVolumeExt, :InterpolationsExt, :ReinitializationExt]
2721
ext = Base.get_extension(LevelSetMethods, extension)
2822
isnothing(ext) && "error loading $ext"
2923
push!(modules, ext)
@@ -37,14 +31,13 @@ makedocs(;
3731
format = Documenter.HTML(;
3832
canonical = "https://maltezfaria.github.io/LevelSetMethods.jl",
3933
collapselevel = 2,
40-
),
41-
pages = vcat(
34+
), pages = vcat(
4235
"Home" => "index.md",
4336
"terms.md",
4437
"time-integrators.md",
4538
"boundary-conditions.md",
4639
"Extensions" =>
47-
["extension-makie.md", "extension-mmg.md", "extension-interpolations.md"],
40+
["extension-makie.md", "extension-mmg.md", "extension-interpolations.md", "extension-reinitialization.md"],
4841
"Examples" => ["example-zalesak.md", "example-shape-optim.md"],
4942
numbered_pages,
5043
),

docs/src/extension-makie.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ end
5555

5656
!!! tip "Plotting a `LevelSetEquation`"
5757
Calling `plot` on a [`LevelSetEquation`](@ref) defaults to plotting the `LevelSet` given by its
58-
[`current_state`](@ref); exactly the same as calling `plot(current_state(equation))`.
58+
`current_state`; exactly the same as calling `plot(current_state(equation))`.
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/index.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ Here is what the `.gif` file looks like:
9696

9797
![Dumbbell](ls_intro.gif)
9898

99-
For more interesting applications and advanced usage, see the [examples section](@ref
100-
examples)!
99+
For more interesting applications and advanced usage, see the examples section!
101100

102101
!!! note "Other resources"
103102
There is an almost one-to-one correspondance between each of the [`LevelSetTerm`](@ref)s

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/MakieExt.jl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ function Makie.convert_arguments(
4646
return _contour_plot(ϕ)
4747
end
4848

49-
function Makie.convert_arguments(::Type{<:Volume}, ϕ::LSM.LevelSet)
49+
function Makie.convert_arguments(P::Type{<:Volume}, ϕ::LSM.LevelSet)
5050
LSM.dimension(ϕ) == 3 ||
5151
throw(ArgumentError("Volume plot only supported for 3D level-sets."))
52-
return _volume_plot(ϕ)
52+
x, y, z, v = _volume_plot(ϕ)
53+
# Delegate to Makie's existing conversion for arrays
54+
return Makie.convert_arguments(P, x, y, z, v)
5355
end
5456

5557
function _contour_plot(ϕ::LSM.LevelSet)
@@ -63,9 +65,9 @@ function _volume_plot(ϕ::LSM.LevelSet)
6365
msh = LSM.mesh(ϕ)
6466
x, y, z = LSM.xgrid(msh), LSM.ygrid(msh), LSM.zgrid(msh)
6567
v = LSM.values(ϕ)
66-
# NOTE: volume gives a warning when passed an AbstractVector for x,y,z,
67-
# and asks for a tuple instead
68-
return extrema(x), extrema(y), extrema(z), v
68+
# Return extrema as tuples - Makie will handle conversion
69+
xlims, ylims, zlims = extrema(x), extrema(y), extrema(z)
70+
return xlims, ylims, zlims, v
6971
end
7072

7173
Makie.@recipe(LevelSetPlot, eq) do scene

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

0 commit comments

Comments
 (0)