-
Notifications
You must be signed in to change notification settings - Fork 104
new MeshBody type and BVH for fast measures
#267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2e8693b
88d074d
1d0a47f
c4c4f5f
bfad043
decccd6
9aa5414
1a44daa
509e81e
c09bf8a
e34c1d1
2e57ce1
5eb30c5
6a909db
ef61a0d
b7f392d
0aa2ad4
1f2c676
aa4a3f5
a9b5721
336a307
135330b
e1e5d48
593432c
5d7cf7b
431792c
1a56c84
c1e394a
941bbec
f0cc0a7
b57f69e
1a61cd9
74806ac
76bdd37
316e95e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
|
|
||
| [weakdeps] | ||
| AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e" | ||
|
|
@@ -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" | ||
|
|
@@ -68,4 +84,4 @@ UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" | |
| WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192" | ||
|
Comment on lines
67
to
84
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"] | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's needed if we combine |
||
|
|
||
| # Lazy constructors | ||
| Base.:∪(a::AbstractBody, b::AbstractBody) = SetBody(min,a,b) | ||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Adaptwhich might be required forSetBody.