Skip to content
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

Fix bug when using Lagrangian particles on Flat topologies #3550

Merged
merged 10 commits into from
Apr 22, 2024
28 changes: 22 additions & 6 deletions src/Fields/interpolate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ using Oceananigans.Architectures: child_architecture
"""
index_binary_search(val, vec, N)

Return indices `low, high` of `vec`tor for which
Return indices `low, high` of `vec`tor for which

```julia
vec[low] ≤ val && vec[high] ≥ val
Expand All @@ -29,9 +29,9 @@ Code credit: https://gist.github.com/cuongld2/8e4fed9ba44ea2b4598f90e7d5b6c612/1
low = 0
high = N - 1

while low + 1 < high
while low + 1 < high
mid = middle_point(low, high)
if @inbounds vec[mid + 1] == val
if @inbounds vec[mid + 1] == val
return (mid + 1, mid + 1)
elseif @inbounds vec[mid + 1] < val
low = mid
Expand Down Expand Up @@ -211,6 +211,23 @@ end
return (ii, jj, kk)
end

"""
truncate_fractional_indices(fi, fj, fk)

Truncate _fractional_ indices output from fractional indices `fi, fj, fk` to integer indices, dealing
with `nothing` indices for `Flat` domains.
"""
@inline function truncate_fractional_indices(fi, fj, fk)
i = truncate_fractional_index(fi)
j = truncate_fractional_index(fj)
k = truncate_fractional_index(fk)
return (i, j, k)
end

@inline truncate_fractional_index(::Nothing) = 1
@inline truncate_fractional_index(fi) = Base.unsafe_trunc(Int, fi)


"""
interpolate(at_node, from_field, from_loc, from_grid)

Expand Down Expand Up @@ -277,7 +294,7 @@ end
k⁻, k⁺, ζ = iz

return @inbounds ϕ₁(ξ, η, ζ) * getindex(data, i⁻, j⁻, k⁻, in...) +
ϕ₂(ξ, η, ζ) * getindex(data, i⁻, j⁻, k⁺, in...) +
ϕ₂(ξ, η, ζ) * getindex(data, i⁻, j⁻, k⁺, in...) +
ϕ₃(ξ, η, ζ) * getindex(data, i⁻, j⁺, k⁻, in...) +
ϕ₄(ξ, η, ζ) * getindex(data, i⁻, j⁺, k⁺, in...) +
ϕ₅(ξ, η, ζ) * getindex(data, i⁺, j⁻, k⁻, in...) +
Expand Down Expand Up @@ -316,7 +333,7 @@ function interpolate!(to_field::Field, from_field::AbstractField)
to_arch = architecture(to_field)
from_arch = architecture(from_field)

# In case architectures are `Distributed` we
# In case architectures are `Distributed` we
# verify that the fields are on the same child architecture
to_arch = child_architecture(to_arch)
from_arch = child_architecture(from_arch)
Expand All @@ -338,4 +355,3 @@ function interpolate!(to_field::Field, from_field::AbstractField)

return nothing
end

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ using Oceananigans.Grids: XYFlatGrid, YZFlatGrid, XZFlatGrid
using Oceananigans.ImmersedBoundaries: immersed_cell
using Oceananigans.Architectures: device, architecture
using Oceananigans.Fields: interpolate, datatuple, compute!, location
using Oceananigans.Fields: fractional_indices
using Oceananigans.Fields: fractional_indices, truncate_fractional_indices
using Oceananigans.TimeSteppers: AbstractLagrangianParticles
using Oceananigans.Utils: prettysummary, launch!, SumOfArrays

Expand Down Expand Up @@ -135,7 +135,7 @@ step_lagrangian_particles!(::Nothing, model, Δt) = nothing
function step_lagrangian_particles!(particles::LagrangianParticles, model, Δt)
# Update the properties of the Lagrangian particles
update_lagrangian_particle_properties!(particles, model, Δt)

# Compute dynamics
particles.dynamics(particles, model, Δt)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Oceananigans.Utils: instantiate
using Oceananigans.Utils: instantiate
using Oceananigans.Models: total_velocities

#####
Expand Down Expand Up @@ -28,6 +28,13 @@ is outside the Periodic interval `(xᴸ, xᴿ)`.
@inline enforce_boundary_conditions(::Periodic, x, xᴸ, xᴿ, Cʳ) = ifelse(x > xᴿ, xᴸ + (x - xᴿ),
ifelse(x < xᴸ, xᴿ - (xᴸ - x), x))

"""
enforce_boundary_conditions(::Flat, x, xᴸ, xᴿ, Cʳ)

Do nothing on Flat dimensions.
"""
@inline enforce_boundary_conditions(::Flat, x, xᴸ, xᴿ, Cʳ) = x

const f = Face()
const c = Center()

Expand All @@ -41,23 +48,21 @@ bouncing the particle off the immersed boundary with a coefficient or `restituti
X = flattened_node((x, y, z), ibg)

# Determine current particle cell
i, j, k = fractional_indices(X, ibg.underlying_grid, (c, c, c))
i = Base.unsafe_trunc(Int, i)
j = Base.unsafe_trunc(Int, j)
k = Base.unsafe_trunc(Int, k)

fi, fj, fk = fractional_indices(X, ibg.underlying_grid, (c, c, c))
i, j, k = truncate_fractional_indices(fi, fj, fk)

if immersed_cell(i, j, k, ibg)
# Determine whether particle was _previously_ in a non-immersed cell
i⁻, j⁻, k⁻ = previous_particle_indices

if !immersed_cell(i⁻, j⁻, k⁻, ibg)
# Left-right bounds of the previous, non-immersed cell
xᴿ, yᴿ, zᴿ = node(i⁻+1, j⁻+1, k⁻+1, ibg, f, f, f)
xᴸ, yᴸ, zᴸ = node(i⁻, j⁻, k⁻, ibg, f, f, f)

Cʳ = restitution
x⁺ = enforce_boundary_conditions(Bounded(), x, xᴸ, xᴿ, Cʳ)
y⁺ = enforce_boundary_conditions(Bounded(), y, yᴸ, yᴿ, Cʳ)
x⁺ = enforce_boundary_conditions(Bounded(), x, xᴸ, xᴿ, Cʳ)
y⁺ = enforce_boundary_conditions(Bounded(), y, yᴸ, yᴿ, Cʳ)
z⁺ = enforce_boundary_conditions(Bounded(), z, zᴸ, zᴿ, Cʳ)

end
Expand All @@ -76,10 +81,8 @@ given `velocities`, time-step `Δt, and coefficient of `restitution`.
X = flattened_node((x, y, z), grid)

# Obtain current particle indices
i, j, k = fractional_indices(X, grid, c, c, c)
i = Base.unsafe_trunc(Int, i)
j = Base.unsafe_trunc(Int, j)
k = Base.unsafe_trunc(Int, k)
fi, fj, fk = fractional_indices(X, grid, c, c, c)
i, j, k = truncate_fractional_indices(fi, fj, fk)

current_particle_indices = (i, j, k)

Expand All @@ -91,7 +94,7 @@ given `velocities`, time-step `Δt, and coefficient of `restitution`.
# Advect particles, calculating the advection metric for a curvilinear grid.
# Note that all supported grids use length coordinates in the vertical, so we do not
# transform the vertical velocity nor invoke the k-index.
ξ = x_metric(i, j, grid)
ξ = x_metric(i, j, grid)
η = y_metric(i, j, grid)

x⁺ = x + ξ * u * Δt
Expand Down Expand Up @@ -137,7 +140,7 @@ end
@inline y_metric(i, j, grid::RectilinearGrid) = 1
@inline y_metric(i, j, grid::LatitudeLongitudeGrid{FT}) where FT = 1 / grid.radius * FT(360 / 2π)

@kernel function _advect_particles!(particles, restitution, grid::AbstractUnderlyingGrid, Δt, velocities)
@kernel function _advect_particles!(particles, restitution, grid::AbstractUnderlyingGrid, Δt, velocities)
p = @index(Global)

@inbounds begin
Expand All @@ -146,12 +149,12 @@ end
z = particles.z[p]
end

x⁺, y⁺, z⁺ = advect_particle((x, y, z), p, restitution, grid, Δt, velocities)
x⁺, y⁺, z⁺ = advect_particle((x, y, z), p, restitution, grid, Δt, velocities)

@inbounds begin
particles.x[p] = x⁺
particles.y[p] = y⁺
particles.z[p] = z⁺
particles.x[p] = x⁺
particles.y[p] = y⁺
particles.z[p] = z⁺
end
end

Expand All @@ -166,4 +169,3 @@ function advect_lagrangian_particles!(particles, model, Δt)

return nothing
end

87 changes: 42 additions & 45 deletions test/test_lagrangian_particle_tracking.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ include("dependencies_for_runtests.jl")

using NCDatasets
using StructArrays
using Oceananigans.Architectures: on_architecture
using Oceananigans.Architectures: architecture, on_architecture

struct TestParticle{T}
x :: T
y :: T
z :: T
u :: T
v :: T
w :: T
s :: T
x::T
y::T
z::T
u::T
v::T
w::T
s::T
end

function particle_tracking_simulation(; grid, particles, timestepper=:RungeKutta3, velocities=nothing)
Expand All @@ -22,41 +22,31 @@ function particle_tracking_simulation(; grid, particles, timestepper=:RungeKutta
jld2_filepath = "test_particles.jld2"
sim.output_writers[:particles_jld2] =
JLD2OutputWriter(model, (; particles=model.particles),
filename="test_particles", schedule=IterationInterval(1))
filename="test_particles", schedule=IterationInterval(1))

nc_filepath = "test_particles.nc"
sim.output_writers[:particles_nc] =
NetCDFOutputWriter(model, model.particles, filename=nc_filepath, schedule=IterationInterval(1))
NetCDFOutputWriter(model, model.particles, filename=nc_filepath, schedule=IterationInterval(1))

sim.output_writers[:checkpointer] = Checkpointer(model, schedule=IterationInterval(1),
dir = ".", prefix = "particles_checkpoint")
dir=".", prefix="particles_checkpoint")

return sim, jld2_filepath, nc_filepath
end

function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretched=false)
topo = (Periodic, Periodic, Bounded)

Nx = Ny = Nz = 5
if vertically_stretched
# Slightly stretched at the top
z = [-1, -0.5, 0.0, 0.4, 0.7, 1]
else
z = (-1, 1)
end
function run_simple_particle_tracking_tests(grid, timestepper)

grid = RectilinearGrid(arch; topology=topo, size=(Nx, Ny, Nz),
x=(-1, 1), y=(-1, 1), z)
arch = architecture(grid)

P = 10

#####
##### Test default particle
#####
xs = on_architecture(arch, 0.6*ones(P))
ys = on_architecture(arch, 0.58*ones(P))
zs = on_architecture(arch, 0.8*ones(P))

xs = on_architecture(arch, 0.6 * ones(P))
ys = on_architecture(arch, 0.58 * ones(P))
zs = on_architecture(arch, 0.8 * ones(P))

particles = LagrangianParticles(x=xs, y=ys, z=zs)
@test particles isa LagrangianParticles
Expand All @@ -77,21 +67,21 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc
##### Test Boundary restitution
#####

initial_z = CUDA.@allowscalar grid.zᵃᵃᶜ[grid.Nz - 1]
top_boundary = CUDA.@allowscalar grid.zᵃᵃᶠ[grid.Nz + 1]
initial_z = CUDA.@allowscalar grid.zᵃᵃᶜ[grid.Nz-1]
top_boundary = CUDA.@allowscalar grid.zᵃᵃᶠ[grid.Nz+1]

x, y, z = on_architecture.(Ref(arch), ([0.0], [0.0], [initial_z]))

particles = LagrangianParticles(; x, y, z)
u, v, w = VelocityFields(grid)
u, v, w = VelocityFields(grid)

Δt = 0.01
interior(w, :, :, grid.Nz) .= (0.1 + top_boundary - initial_z) / Δt
interior(w, :, :, grid.Nz-1) .= (0.2 + top_boundary - initial_z) / Δt
interior(w, :, :, grid.Nz) .= (0.1 + top_boundary - initial_z) / Δt
interior(w, :, :, grid.Nz - 1) .= (0.2 + top_boundary - initial_z) / Δt

velocities = PrescribedVelocityFields(; u, v, w)

model = HydrostaticFreeSurfaceModel(; grid, particles, velocities, buoyancy=nothing, tracers = ())
model = HydrostaticFreeSurfaceModel(; grid, particles, velocities, buoyancy=nothing, tracers=())

time_step!(model, Δt)

Expand All @@ -101,7 +91,7 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc
#####
##### Test custom particle "SpeedTrackingParticle"
#####

xs = on_architecture(arch, zeros(P))
ys = on_architecture(arch, zeros(P))
zs = on_architecture(arch, 0.5 * ones(P))
Expand All @@ -115,7 +105,7 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc

velocities = VelocityFields(grid)
u, v, w = velocities
speed = Field(√(u*u + v*v + w*w))
speed = Field(√(u * u + v * v + w * w))
tracked_fields = merge(velocities, (; s=speed))

# applying v component of advection with background field to ensure it is included
Expand All @@ -128,7 +118,7 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc

model = NonhydrostaticModel(; grid, timestepper,
velocities, particles=lagrangian_particles,
background_fields = (v = background_v, ))
background_fields=(v=background_v,))

set!(model, u=1)

Expand All @@ -144,13 +134,13 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc
NetCDFOutputWriter(model, model.particles, filename=nc_filepath, schedule=IterationInterval(1))

sim.output_writers[:checkpointer] = Checkpointer(model, schedule=IterationInterval(1),
dir = ".", prefix = "particles_checkpoint")
dir=".", prefix="particles_checkpoint")

rm(jld2_filepath)
rm(nc_filepath)
rm("particles_checkpoint_iteration1.jld2")

sim, jld2_filepath, nc_filepath = particle_tracking_simulation(; grid, particles=lagrangian_particles, timestepper, velocities)
sim, jld2_filepath, nc_filepath = particle_tracking_simulation(; grid, particles=lagrangian_particles, timestepper, velocities)
model = sim.model
run!(sim)

Expand Down Expand Up @@ -274,12 +264,19 @@ function run_simple_particle_tracking_tests(arch, timestepper; vertically_stretc
return nothing
end

@testset "Lagrangian particle tracking" begin
for arch in archs, timestepper in (:QuasiAdamsBashforth2, :RungeKutta3)
@info " Testing uniform grid Lagrangian particle tracking [$(typeof(arch)), $timestepper]..."
run_simple_particle_tracking_tests(arch, timestepper; vertically_stretched=false)
lagrangian_particle_test_grid(arch, ::Periodic, z) =
RectilinearGrid(arch; topology=(Periodic, Periodic, Bounded), size=(5, 5, 5), x=(-1, 1), y=(-1, 1), z)
lagrangian_particle_test_grid(arch, ::Flat, z) =
RectilinearGrid(arch; topology=(Periodic, Flat, Bounded), size=(5, 5), x=(-1, 1), z)

@info " Testing stretched grid Lagrangian particle tracking [$(typeof(arch)), $timestepper]..."
run_simple_particle_tracking_tests(arch, timestepper; vertically_stretched=true)
@testset "Lagrangian particle tracking" begin
timesteppers = (:QuasiAdamsBashforth2, :RungeKutta3)
y_topologies = (Periodic(), Flat())
vertical_grids = (uniform=(-1, 1), stretched=[-1, -0.5, 0.0, 0.4, 0.7, 1])

for arch in archs, timestepper in timesteppers, y_topo in y_topologies, (z_grid_type, z) in pairs(vertical_grids)
@info " Testing Lagrangian particle tracking [$(typeof(arch)), $timestepper] with y $(typeof(y_topo)) on vertically $z_grid_type grid ..."
grid = lagrangian_particle_test_grid(arch, y_topo, z)
run_simple_particle_tracking_tests(grid, timestepper)
end
end