Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2e8693b
working stl body
Oct 8, 2024
88d074d
add MeshBodies
Oct 8, 2024
1d0a47f
better distance and fix .inp files
Oct 16, 2024
c4c4f5f
merge upstream/master
marinlauber Aug 21, 2025
bfad043
add MeshBody
marinlauber Aug 22, 2025
decccd6
bounding box limits
marinlauber Aug 22, 2025
9aa5414
remove Meshbodies from src
marinlauber Aug 22, 2025
1a44daa
fix normal computation with mapping
marinlauber Aug 22, 2025
509e81e
change WriteVTK dependency type
marinlauber Aug 22, 2025
c09bf8a
fix import
marinlauber Oct 10, 2025
e34c1d1
add bvh start
marinlauber Oct 16, 2025
2e57ce1
BVH working on CPU
marinlauber Dec 3, 2025
5eb30c5
working BVH on CPU
marinlauber Dec 5, 2025
6a909db
working MeshBody with BVH
marinlauber Dec 8, 2025
ef61a0d
Merge branch 'WaterLily-jl:master' into MeshBody
marinlauber Dec 9, 2025
b7f392d
merge upstream/master
Dec 17, 2025
0aa2ad4
add dev
marinlauber Jan 16, 2026
1f2c676
ImplicitBVH in MeshBody test working CPU/GPU
marinlauber Jan 26, 2026
aa4a3f5
Implicit BVH in the MeshBody
marinlauber Jan 27, 2026
a9b5721
remove old dev file
marinlauber Jan 27, 2026
336a307
add dist function for primitives and start tests
marinlauber Jan 28, 2026
135330b
update FileIO and MeshIO usage following PR merge
marinlauber Feb 3, 2026
e1e5d48
update docs for MeshBody
marinlauber Feb 3, 2026
593432c
clean constructor and add velocity field inside MeshBody
marinlauber Feb 3, 2026
5d7cf7b
add MeshBody tests
marinlauber Feb 3, 2026
431792c
extras in Project.toml for test
marinlauber Feb 3, 2026
1a56c84
forgot to update BVH once mesh it updated
marinlauber Feb 4, 2026
c1e394a
@adapt_structure on use MeshBody inside a SetBody on the GPU
marinlauber Feb 6, 2026
941bbec
correct multiple dispatch for update! and save!
marinlauber Feb 6, 2026
f0cc0a7
add other closest functions
marinlauber Feb 7, 2026
b57f69e
revert to working method
marinlauber Feb 16, 2026
1a61cd9
Clean up closest() and add stl tests
weymouth Mar 22, 2026
74806ac
Continue clean up
weymouth Mar 22, 2026
76bdd37
test closest directly
weymouth Mar 22, 2026
316e95e
Tiny updates
weymouth Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
name = "WaterLily"
uuid = "ed894a53-35f9-47f1-b17f-85db9237eebd"
authors = ["Gabriel Weymouth <gabriel.weymouth@gmail.com>"]
version = "1.5.2"
authors = ["Gabriel Weymouth <gabriel.weymouth@gmail.com>"]

[deps]
Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
EllipsisNotation = "da5c29d0-fa7d-589e-88eb-ea29b0a81949"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
ImplicitBVH = "932a18dc-bb55-4cd5-bdd6-1368ec9cea29"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192"
Comment on lines +7 to +25
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully, most of these can be moved to weakdeps.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't cleaned this yet, but it seems they could go in weakdeps for all of them, except Adapt which might be required for SetBody.


[weakdeps]
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
Expand All @@ -26,39 +32,49 @@ Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
Meshing = "e6723b4c-ebff-59f1-b4b7-d97aa5274f73"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
ReadVTK = "dc215faf-f008-4882-a9f7-a79a826fadc3"
WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192"

[extensions]
WaterLilyAMDGPUExt = "AMDGPU"
WaterLilyCUDAExt = "CUDA"
WaterLilyMakieExt = "Makie"
WaterLilyGeometryBasicsExt = "GeometryBasics"
WaterLilyJLD2Ext = "JLD2"
WaterLilyMakieExt = "Makie"
WaterLilyMeshingExt = ["Makie", "Meshing"]
WaterLilyPlotsExt = "Plots"
WaterLilyReadVTKExt = "ReadVTK"
WaterLilyWriteVTKExt = "WriteVTK"
WaterLilyWriteVTKExt = ["WriteVTK", "GeometryBasics"]

[compat]
Adapt = "4.3.0"
ConstructionBase = "1.6.0"
DocStringExtensions = "0.9"
EllipsisNotation = "1.8"
FileIO = "1.18.0"
ForwardDiff = "0.10.18, 1"
GeometryBasics = "0.5.10"
ImplicitBVH = "0.7.0"
KernelAbstractions = "0.9.1"
LoggingExtras = "1.1"
MeshIO = "0.5.3"
Preferences = "1.4"
Reexport = "^1.2.2"
Requires = "1.3"
StaticArrays = "^1.1.0"
WriteVTK = "1.21.2"
julia = "1.10"

[extras]
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
ImplicitBVH = "932a18dc-bb55-4cd5-bdd6-1368ec9cea29"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118"
Meshing = "e6723b4c-ebff-59f1-b4b7-d97aa5274f73"
PerformanceTestTools = "dc46b164-d16f-48ec-a853-60448fc869fe"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand All @@ -68,4 +84,4 @@ UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192"
Comment on lines 67 to 84
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these just be weakdeps? Or do we need them here as well?


[targets]
test = ["Test", "BenchmarkTools", "CUDA", "AMDGPU", "GPUArrays", "WriteVTK", "ReadVTK", "JLD2"]
test = ["Test", "BenchmarkTools", "CUDA", "AMDGPU", "GPUArrays", "WriteVTK", "ReadVTK", "JLD2", "GeometryBasics", "ImplicitBVH"]
229 changes: 229 additions & 0 deletions ext/WaterLilyGeometryBasicsExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
module WaterLilyGeometryBasicsExt

using WaterLily
import WaterLily: AbstractBody, SetBody, MeshBody, save!, update!
using FileIO, MeshIO, StaticArrays
using ImplicitBVH, GeometryBasics

struct Meshbody{T,M,B,F} <: AbstractBody
mesh::M
velocity::M
bvh::B
map::F
scale::T
boundary::Bool
half_thk::T
end
function MeshBody(mesh::M,vel::M,bvh::B;map=(x,t)->x,scale=1.f0,boundary=false,half_thk=1.866f0) where {M,B}
return Meshbody{eltype(scale),M,B,typeof(map)}(mesh,vel,bvh,map,scale,boundary,half_thk)
end
using Adapt
# make it GPU compatible
Adapt.@adapt_structure Meshbody

"""
MeshBody(mesh::Union{Mesh, String};
map::Function=(x,t)->x, boundary::Bool=false, half_thk::T=1.866f0,
scale::T=1.f0, mem=Array, primitives::Union{BBox, BSphere}) where T

Constructor for a MeshBody:

- `mesh::Union{Mesh, String}`: the GeometryBasics.Mesh or path to the mesh file to use to define the geometry.
- `map(x::AbstractVector,t::Real)::AbstractVector`: coordinate mapping function.
- `boundary::Bool`: whether the mesh is a boundary or not.
- `half_thk::T`: half thickness to apply if the mesh is not a boundary, the type defines the base type of the MeshBody, default is Float32.
- `scale::T`: scale factor to apply to the mesh points, the type defines the base type of the MeshBody, default is Float32.
- `mem`: memory location. `Array`, `CuArray`, `ROCm` to run on CPU, NVIDIA, or AMD devices, respectively.
- `primitive::Union{BBox, BSphere}`: bounding volume primitive to use in the ImplicitBVH. Default is Axis-Aligned Bounding Box.

"""
MeshBody(file_name::String; kwargs...) = MeshBody(load(file_name); kwargs...)
function MeshBody(mesh::Mesh; scale::T=1.f0, mem=Array, primitive=ImplicitBVH.BBox, kwargs...) where T
# device array of the mesh that we store
mesh = [hcat([mesh[i]...]...)*T(scale) for i in 1:length(mesh)] |> mem
# make the BVH
bvh = BVH(primitive{T}.(mesh), primitive{T})
# make the mesh and return
MeshBody(mesh, zero(mesh), bvh; scale=T(scale), kwargs...)
end

using LinearAlgebra: cross
# @fastmath @inline d²_fast(x::SVector,tri::SMatrix) = sum(abs2,x-center(tri))
@fastmath @inline d²_fast(x::SVector,tri::SMatrix) = sum(abs2,x-locate(x,tri))
@fastmath @inline normal(tri::SMatrix) = hat(SVector(cross(tri[:,2]-tri[:,1],tri[:,3]-tri[:,1])))
@fastmath @inline hat(v) = v/(√(v'*v)+eps(eltype(v)))
@fastmath @inline center(tri::SMatrix) = SVector(sum(tri,dims=2)/3)

import ImplicitBVH: BoundingVolume,BBox,BSphere
@fastmath @inline inside(x::SVector, b::BoundingVolume) = inside(x, b.volume)
@fastmath @inline inside(x::SVector, b::BBox) = all(b.lo.-4 .≤ x) && all(x .≤ b.up.+4)
@fastmath @inline inside(x::SVector, b::BSphere) = √sum(abs2,x .- b.x) - b.r ≤ 4

import WaterLily: ×
# linear shape function to interpolate inside element
@fastmath @inline shape_value(p::SVector{3,T},t) where T = SA{T}[√sum(abs2,×(t[:,2]-p,t[:,3]-p))
√sum(abs2,×(p-t[:,1],t[:,3]-t[:,1]))
√sum(abs2,×(t[:,2]-t[:,1],p-t[:,1]))]
@fastmath @inline get_velocity(p::SVector, tri, vel)= (dA=shape_value(p,tri); vel*dA/sum(dA))

# brute-force fallback when the BVH is not available
closest(x::SVector,mesh) = findmin(tri->d²_fast(x, tri),mesh)

# traverse the BVH
import ImplicitBVH: memory_index,unsafe_isvirtual
@inline function closest(x::SVector{D,T},bvh::ImplicitBVH.BVH,mesh;a=floatmax(T),verbose=false) where {D,T}
tree = bvh.tree; length_nodes = length(bvh.nodes)
u=ncheck=lcheck=tcheck=Int32(0) # initialize
# Depth-First-Search
i=1; while true
@inbounds j = memory_index(tree,i)
if j ≤ length_nodes # we are on a node
verbose && (ncheck += 1)
dist(x, bvh.nodes[j]) < a && (i = 2i; continue) # go deeper if closer than current best
else # we reached a leaf
verbose && (lcheck += 1)
if dist(x, bvh.leaves[j-length_nodes]) < a
verbose && (tcheck += 1)
@inbounds j = bvh.leaves[j-length_nodes].index # correct index in mesh
d = d²_fast(x, mesh[j])
d<a && (a=d; u=Int32(j)) # Replace current best
end
end
i = i>>trailing_ones(i)+1 # go to sibling, or uncle etc.
(i==1 || unsafe_isvirtual(tree, i)) && break # search complete!
end
verbose && println("Checked $ncheck nodes, $lcheck leaves, $tcheck triangles")
return a,u
end

# compute the square distance to primitive
dist(x, b::BSphere) = max(√sum(abs2,x .- b.x) - b.r, 0)^2
function dist(x, b::BBox)
c = (b.up .+ b.lo) ./ 2
r = (b.up .- b.lo) ./ 2
sum(abs2, max.(abs.(x .- c) .- r, 0))
end
dist(x, b::BoundingVolume) = dist(x, b.volume)

# locate the closest point p to x on triangle tri
function locate(x::SVector{T},tri::SMatrix{T}) where T
# unpack the triangle vertices
a,b,c = tri[:,1],tri[:,2],tri[:,3]
ab,ac,ap = b.-a,c.-a,x.-a
d1,d2 = sum(ab.*ap),sum(ac.*ap)
# is point `a` closest?
((d1 ≤ 0) && (d2 ≤ 0)) && (return a)
# is point `b` closest?
bp = x.-b
d3,d4 = sum(ab.*bp),sum(ac.*bp)
((d3 ≥ 0) && (d4 ≤ d3)) && (return b)
# is point `c` closest?
cp = x.-c
d5,d6 = sum(ab.*cp),sum(ac.*cp)
((d6 ≥ 0) && (d5 ≤ d6)) && (return c)
# is segment 'ab' closest?
vc = d1*d4 - d3*d2
((vc ≤ 0) && (d1 ≥ 0) && (d3 ≤ 0)) && (return a .+ ab.*d1 ./ (d1 - d3))
# is segment 'ac' closest?
vb = d5*d2 - d1*d6
((vb ≤ 0) && (d2 ≥ 0) && (d6 ≤ 0)) && (return a .+ ac.*d2 ./ (d2 - d6))
# is segment 'bc' closest?
va = d3*d6 - d5*d4
((va ≤ 0) && (d4 ≥ d3) && (d5 ≥ d6)) && (return b .+ (c .- b) .* (d4 - d3) ./ ((d4 - d3) + (d5 - d6)))
# closest is interior to `abc`
denom = one(T) / (va + vb + vc)
v,w= vb*denom,vc*denom
return a .+ ab .* v .+ ac .* w
end

# signed distance function
WaterLily.sdf(body::Meshbody,x,t;kwargs...) = measure(body,x,t;kwargs...)[1]

using ForwardDiff
# measure
function WaterLily.measure(body::Meshbody,x::SVector{D,T},t;fastd²=Inf) where {D,T}
# map to correct location
ξ = body.map(x,t)
# before we try the bvh
!inside(ξ,body.bvh.nodes[1]) && return (T(4),zero(x),zero(x))
# locate the point on the mesh
d²,u = closest(ξ,body.bvh,body.mesh;a = body.boundary ? floatmax(T) : T(16))
u==0 && return (T(4),zero(x),zero(x)) # no triangles within distance "a"
# compute the normal and distance
n,p = normal(body.mesh[u]),SVector(locate(ξ,body.mesh[u]))
# signed Euclidian distance
d = copysign(√d²,n'*(ξ-p))
!body.boundary && (d = abs(d)-body.half_thk) # if the mesh is not a boundary, we need to adjust the distance
d^2>fastd² && return (d,zero(x),zero(x)) # skip n,V
# velocity at the mesh point
dξdx = ForwardDiff.jacobian(x->body.map(x,t), ξ)
dξdt = -ForwardDiff.derivative(t->body.map(x,t), t)
# mesh deformation velocity
v = get_velocity(p, body.mesh[u], body.velocity[u])
return (d,dξdx\n,dξdx\dξdt+v)
end

# check if a point is inside the mesh, using ray-casting
@inline function check_inside(x₀::SVector{D,T}, bvh) where {D,T}
mem = typeof(bvh.nodes).name.wrapper
# rays in each dimension from starting point x₀
points, directions = mem(repeat(x₀, 1, 6,)), mem([1 -1 0 0 0 0; 0 0 1 -1 0 0; 0 0 0 0 1 -1])
# traverse the bvh
traversal = ImplicitBVH.traverse_rays(bvh, points, directions)
# check that we collide in each direction at least once, otherwise we are outside
d = sort(unique(getindex.(traversal.contacts, 2))) == collect(1:6) ? T(-8) : T(8)
# if we are inside, negative large distance
(d,zero(x₀),zero(x₀))
end

import WaterLily: @loop, update!
"""
update!(body::Meshbody{T},new_mesh::AbstractArray,dt=0;kwargs...)

Updates the mesh body position using the new mesh triangle coordinates.

xᵢ(t+Δt) = x[i]
vᵢ(t+Δt) = (xᵢ(t+Δt) - xᵢ(t))/dt
where `x[i]` is the new (t+Δt) position of the control point, `vᵢ` is the velocity at that control point.

"""
function update!(a::Meshbody{T},new_mesh::AbstractArray,dt=0) where T
Rs = CartesianIndices(a.mesh)
# if nonzero time step, update the velocity field
dt>0 && (@loop a.velocity[I] = (new_mesh[I]-a.mesh[I])/T(dt) over I in Rs)
@loop a.mesh[I] = new_mesh[I] over I in Rs
# update the BVH
update_bvh(a, bvh=BVH(ImplicitBVH.BBox{T}.(a.mesh), ImplicitBVH.BBox{T}))
end
update!(body::AbstractBody,args...) = body
update!(body::SetBody,args...) = SetBody(body.op,update!(body.a,args...),update!(body.b,args...))

import ConstructionBase: setproperties
update_bvh(body::Meshbody; bvh) = setproperties(body, bvh=bvh)

import WriteVTK: MeshCell, VTKCellTypes, vtk_grid, vtk_save
using Printf: @sprintf
"""

save!(writer::VTKWriter, body::Meshbody, t=writer.count[1])

Saves the mesh body as a VTK file using the WriteVTK package. The file name is generated using the writer's directory name, base file name, and the current count.
"""
function save!(w,a::Meshbody,t=w.count[1]) #where S<:AbstractSimulation{A,B,C,D,MeshBody}
k = w.count[1]
points = zeros(Float32, 3, 3length(a.mesh))
for (i,el) in enumerate(Array(a.mesh))
points[:,3i-2:3i] = el
end
cells = [MeshCell(VTKCellTypes.VTK_TRIANGLE, TriangleFace{Int}(3i+1,3i+2,3i+3)) for i in 0:length(a.mesh)-1]
vtk = vtk_grid(w.dir_name*@sprintf("/%s_%06i", w.fname, k), points, cells)
for (name,func) in w.output_attrib
# point/vector data must be oriented in the same way as the mesh
vtk[name] = ndims(func(a))==1 ? func(a) : permutedims(func(a))
end
vtk_save(vtk); w.count[1]=k+1
w.collection[round(t,digits=4)]=vtk
end
save!(w,a::AbstractBody,t) = nothing
save!(w,a::SetBody,t) = (save!(w,a.a,t); save!(w,a.b,t))
end # module
3 changes: 3 additions & 0 deletions src/Body.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ struct SetBody{O<:Function,Ta<:AbstractBody,Tb<:AbstractBody} <: AbstractBody
a::Ta
b::Tb
end
using Adapt
# make it GPU compatible
Adapt.@adapt_structure SetBody
Comment on lines +93 to +95
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? It would be nicer to leave Adapt as an weakdep if possible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's needed if we combine MeshBody and AbstractBodies, if I remember correctly.


# Lazy constructors
Base.:∪(a::AbstractBody, b::AbstractBody) = SetBody(min,a,b)
Expand Down
5 changes: 5 additions & 0 deletions src/WaterLily.jl
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ function plot_body_obs! end
# export
export viz!, get_body, plot_body_obs!

# default GeometryBasics extention
function MeshBody(args...;kwargs...); throw(MissingException("`MeshBody` requires `using GeometryBasics` to load the extension")); end
# export
export MeshBody

# Check number of threads when loading WaterLily
"""
check_nthreads()
Expand Down
Loading