Skip to content

Commit

Permalink
Implement and test vertical mass borrowing limiters
Browse files Browse the repository at this point in the history
  • Loading branch information
charleskawczynski committed Nov 20, 2024
1 parent 68cc581 commit ec53007
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 1 deletion.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ ClimaCore.jl Release Notes
main
-------

- A new limiter, `VerticalMassBorrowingLimiter`, was added. Which redistributes all negative values from a given field while preserving mass.
PR [2084](https://github.com/CliMA/ClimaCore.jl/pull/2084)

v0.14.20
-------

Expand Down
11 changes: 11 additions & 0 deletions docs/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,14 @@ @article{Wiin1967
year = {1967},
publisher = {Wiley Online Library}
}

@article{zhang2018impact,
title={Impact of numerical choices on water conservation in the E3SM Atmosphere Model version 1 (EAMv1)},
author={Zhang, Kai and Rasch, Philip J and Taylor, Mark A and Wan, Hui and Leung, Ruby and Ma, Po-Lun and Golaz, Jean-Christophe and Wolfe, Jon and Lin, Wuyin and Singh, Balwinder and others},
journal={Geoscientific Model Development},
volume={11},
number={5},
pages={1971--1988},
year={2018},
publisher={Copernicus Publications G{\"o}ttingen, Germany}
}
3 changes: 2 additions & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,15 @@ Hypsography.ref_z_to_physical_z
The limiters supertype is
```@docs
Limiters.AbstractLimiter
Limiters.QuasiMonotoneLimiter
Limiters.VerticalMassBorrowingLimiter
```

This class of flux-limiters is applied only in the horizontal direction (on spectral advection operators).


### Interfaces
```@docs
Limiters.QuasiMonotoneLimiter
Limiters.compute_bounds!
Limiters.apply_limiter!
```
Expand Down
1 change: 1 addition & 0 deletions src/Limiters/Limiters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ abstract type AbstractLimiter end

# implementations
include("quasimonotone.jl")
include("vertical_mass_borrowing_limiter.jl")

end # end module
119 changes: 119 additions & 0 deletions src/Limiters/vertical_mass_borrowing_limiter.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import .DataLayouts as DL

"""
VerticalMassBorrowingLimiter(f::Fields.Field, q_min)
A vertical-only mass borrowing limier.
The mass borrower borrows tracer mass from an adjacent layer.
It conserves the mass and can avoid negative tracers.
At level k, it will first borrow the mass from the layer k+1 (lower level).
If the mass is not sufficient in layer k+1, it will borrow mass from
layer k+2. The borrower will proceed this process until the bottom layer.
If the tracer mass in the bottom layer goes negative, it will repeat the
process from the bottom to the top. In this way, the borrower works for
any shape of mass profiles.
This code was adapted from [E3SM](https://github.com/E3SM-Project/E3SM/blob/2c377c5ec9a5585170524b366ad85074ab1b1a5c/components/eam/src/physics/cam/massborrow.F90)
References:
- [zhang2018impact](@cite)
"""
struct VerticalMassBorrowingLimiter{F, T}
bmass::F
ic::F
q_min::T
end
function VerticalMassBorrowingLimiter(f::Fields.Field, q_min)
bmass = similar(Spaces.level(f, 1))
ic = similar(Spaces.level(f, 1))
return VerticalMassBorrowingLimiter(bmass, ic, q_min)
end


"""
apply_limiter!(q::Fields.Field, lim::VerticalMassBorrowingLimiter)
Apply the vertical mass borrowing
limiter `lim` to field `q`.
"""
apply_limiter!(q::Fields.Field, lim::VerticalMassBorrowingLimiter) =
apply_limiter!(q, axes(q), lim, ClimaComms.device(axes(q)))

function apply_limiter!(
q::Fields.Field,
space::Spaces.FiniteDifferenceSpace,
lim::VerticalMassBorrowingLimiter,
device::ClimaComms.AbstractCPUDevice,
)
cache = (; bmass = lim.bmass, ic = lim.ic, q_min = lim.q_min)
columnwise_massborrow_cpu(q, cache)
end

function apply_limiter!(
q::Fields.Field,
space::Spaces.ExtrudedFiniteDifferenceSpace,
lim::VerticalMassBorrowingLimiter,
device::ClimaComms.AbstractCPUDevice,
)
Fields.bycolumn(axes(q)) do colidx
cache =
(; bmass = lim.bmass[colidx], ic = lim.ic[colidx], q_min = lim.q_min)
columnwise_massborrow_cpu(q[colidx], cache)
end
end

# TODO: can we improve the performance?
# `bycolumn` on the CPU may be better here since we could multithread it.
function columnwise_massborrow_cpu(q::Fields.Field, cache) # column fields
(; bmass, ic, q_min) = cache

pdel = Fields.field_values(Fields.Δz_field(q))
# loop over tracers
nlevels = Spaces.nlevels(axes(q))
@. ic = 0
@. bmass = 0
q_vals = Fields.field_values(q)
# top to bottom
for f in 1:DataLayouts.ncomponents(q_vals)
for v in 1:nlevels
CI = CartesianIndex(1, 1, f, v, 1)
# new mass in the current layer
pdel_v = DL.getindex_field(pdel, CI)
nmass = DL.getindex_field(q_vals, CI) + bmass[] / pdel_v
if nmass > q_min[f]
# if new mass in the current layer is positive, don't borrow mass any more
DL.setindex_field!(q_vals, nmass, CI)
bmass[] = 0
else
# set mass to q_min in the current layer, and save bmass
bmass[] = (nmass - q_min[f]) * pdel_v
DL.setindex_field!(q_vals, q_min[f], CI)
ic[] = ic[] + 1
end
end

# bottom to top
for v in nlevels:-1:1
CI = CartesianIndex(1, 1, f, v, 1)
# if the surface layer still needs to borrow mass
if bmass[] < 0
pdel_v = DL.getindex_field(pdel, CI)
# new mass in the current layer
nmass = DL.getindex_field(q_vals, CI) + bmass[] / pdel_v
if nmass > q_min[f]
# if new mass in the current layer is positive, don't borrow mass any more
DL.setindex_field!(q_vals, nmass, CI)
bmass[] = 0
else
# if new mass in the current layer is negative, continue to borrow mass
bmass[] = (nmass - q_min[f]) * pdel_v
DL.setindex_field!(q_vals, q_min[f], CI)
end
end
end
end

return nothing
end
91 changes: 91 additions & 0 deletions test/Limiters/vertical_mass_borrowing_limiter.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#=
julia --project
using Revise; include(joinpath("test", "Limiters", "vertical_mass_borrowing_limiter.jl"))
=#
using ClimaComms
ClimaComms.@import_required_backends
using ClimaCore: Fields, Spaces, Limiters
using ClimaCore.RecursiveApply
using ClimaCore.Grids
using ClimaCore.CommonGrids
using Test
using Random

@testset "Vertical mass borrowing limiter - column" begin
Random.seed!(1234)
FT = Float64
z_elem = 10
z_min = 0
z_max = 1
device = ClimaComms.device()
grid = ColumnGrid(; z_elem, z_min, z_max, device)
cspace = Spaces.FiniteDifferenceSpace(grid, Grids.CellCenter())
context = ClimaComms.context(device)
ArrayType = ClimaComms.array_type(device)
tol = 0.01
perturb = 0.2

# Initialize fields
coords = Fields.coordinate_field(cspace)
q = map(coord -> 0.1, coords)
(; z) = coords
rand_data = ArrayType(rand(size(parent(q))...)) # [0-1]
rand_data = rand_data .- sum(rand_data) / length(rand_data) # make centered about 0 ([-0.5:0.5])
rand_data = (rand_data ./ maximum(rand_data)) .* perturb # rand_data now in [-0.2:0.2]
parent(q) .= parent(q) .+ rand_data # q in [0.1 ± 0.2]
sum_q_init = sum(q)
# Test that the minimum is below 0
@test length(parent(q)) == z_elem == 10
@test 0.3 count(x -> x < 0, parent(q)) / 10 0.5 # ensure reasonable percentage of points are negative

@test -2 * perturb minimum(q) -tol
limiter = Limiters.VerticalMassBorrowingLimiter(q, (0.0,))
Limiters.apply_limiter!(q, limiter)
@test 0 minimum(q)
@test isapprox(sum(q), sum_q_init; atol = 0.00000000001)
@test isapprox(sum(q), sum_q_init; rtol = 0.01)
end

@testset "Vertical mass borrowing limiter" begin
FT = Float64
z_elem = 10
z_min = 0
z_max = 1
radius = 10
h_elem = 10
n_quad_points = 4
grid = ExtrudedCubedSphereGrid(;
z_elem,
z_min,
z_max,
radius,
h_elem,
n_quad_points,
)
cspace = Spaces.ExtrudedFiniteDifferenceSpace(grid, Grids.CellCenter())
context = ClimaComms.context(device)
ArrayType = ClimaComms.array_type(device)
tol = 0.01
perturb = 0.2

# Initialize fields
coords = Fields.coordinate_field(cspace)
q = map(coord -> 0.1, coords)

rand_data = ArrayType(rand(size(parent(q))...)) # [0-1]
rand_data = rand_data .- sum(rand_data) / length(rand_data) # make centered about 0 ([-0.5:0.5])
rand_data = (rand_data ./ maximum(rand_data)) .* perturb # rand_data now in [-0.2:0.2]
parent(q) .= parent(q) .+ rand_data # q in [0.1 ± 0.2]
sum_q_init = sum(q)

# Test that the minimum is below 0
@test length(parent(q)) == z_elem * h_elem^2 * 6 * n_quad_points^2 == 96000
@test 0.10 count(x -> x < 0.0001, parent(q)) / 96000 1 # ensure 10% of points are negative

@test -0.11 minimum(q) -0.08
limiter = Limiters.VerticalMassBorrowingLimiter(q, (0.0,))
Limiters.apply_limiter!(q, limiter)
@test 0 minimum(q)
@test isapprox(sum(q), sum_q_init; atol = 0.013)
@test isapprox(sum(q), sum_q_init; rtol = 0.01)
end
Loading

0 comments on commit ec53007

Please sign in to comment.