Skip to content

Commit

Permalink
add hyperbolic tangent stretching
Browse files Browse the repository at this point in the history
  • Loading branch information
szy21 committed Aug 13, 2024
1 parent 7ab0acf commit a0d4694
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Meshes.NormalizedBilinearMap
Meshes.Uniform
Meshes.ExponentialStretching
Meshes.GeneralizedExponentialStretching
Meshes.HyperbolicTangentStretching
```

### Mesh utilities
Expand Down
90 changes: 88 additions & 2 deletions src/Meshes/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Constuct a 1D mesh on `domain` with `nelems` elements, using `stretching`. Possi
- [`Uniform()`](@ref)
- [`ExponentialStretching(H)`](@ref)
- [`GeneralizedExponentialStretching(dz_bottom, dz_top)`](@ref)
- [`HyperbolicTangentStretching(dz_bottom)`](@ref)
"""
struct IntervalMesh{I <: IntervalDomain, V <: AbstractVector} <: AbstractMesh1D
domain::I
Expand Down Expand Up @@ -256,7 +257,7 @@ function IntervalMesh(
# define the inverse σ⁻¹ exponential stretching function
exp_stretch(ζ, h) = ζ == 1 ? ζ : -h * log(1 - (1 - exp(-1 / h)) * ζ)

# nondimensional vertical coordinate (]0.0, 1.0])
# nondimensional vertical coordinate ([0.0, 1.0])
ζ_n = LinRange(one(FT_solve), nelems, nelems) / nelems

# find bottom height variation
Expand Down Expand Up @@ -292,7 +293,7 @@ function IntervalMesh(
find_top,
RootSolvers.SecantMethod(guess₋, guess₊),
RootSolvers.CompactSolution(),
RootSolvers.ResidualTolerance(FT_solve(1e-3)),
RootSolvers.ResidualTolerance(FT_solve(tol)),
)
if h_top_sol.converged !== true
error(
Expand All @@ -318,6 +319,91 @@ function IntervalMesh(
IntervalMesh(domain, CT.(faces))
end

"""
HyperbolicTangentStretching(dz_surface::FT)
Apply a hyperbolic tangent stretching to the domain when constructing elements.
`dz_surface` is the target element grid spacing at the surface. In typical atmosphere
configuration, it is the grid spacing at the bottom of the
vertical column domain (m). On the other hand, for typical land configurations,
it is the grid spacing at the top of the vertical column domain.
For an interval ``[z_0,z_1]``, this makes the elements uniformally spaced in
``\\zeta``, where
```math
\\eta = 1 - \\frac{tanh[\\gamma(1-\\zeta)]}{tanh(\\gamma)},
```
where ``\\eta = \\frac{z - z_0}{z_1-z_0}``. The stretching parameter ``\\gamma``
is chosen to achieve a given resolution `dz_surface` at the surface.
Then, the user can define a stretched mesh via
ClimaCore.Meshes.IntervalMesh(interval_domain, HyperbolicTangentStretching(dz_surface); nelems::Int, reverse_mode)
`reverse_mode` is default to false for atmosphere configurations. For land configurations,
use `reverse_mode` = `true`.
"""
struct HyperbolicTangentStretching{FT} <: StretchingRule
dz_surface::FT
end

function IntervalMesh(
domain::IntervalDomain{CT},
stretch::HyperbolicTangentStretching{FT};
nelems::Int,
FT_solve = Float64,
tol = 1e-5,
reverse_mode::Bool = false,
) where {CT <: Geometry.Abstract1DPoint{FT}} where {FT}
if nelems 1
throw(ArgumentError("`nelems` must be ≥ 2"))
end

dz_surface = FT_solve(stretch.dz_surface)

# bottom coord height value, always min, for both atmos and land, since z-axis does not change
z_bottom = Geometry.component(domain.coord_min, 1)
# top coord height value, always max, for both atmos and land, since z-axis does not change
z_top = Geometry.component(domain.coord_max, 1)
# but in case of reverse_mode, we temporarily swap them
# so that the following root solve algorithm does not need to change
if reverse_mode
z_bottom, z_top = Geometry.component(domain.coord_max, 1),
-Geometry.component(domain.coord_min, 1)
end

# define the hyperbolic tangent stretching function
tanh_stretch(ζ, γ) = 1 - tanh* (1 - ζ)) / tanh(γ)

# nondimensional vertical coordinate ([0.0, 1.0])
ζ_n = LinRange(one(FT_solve), nelems, nelems) / nelems

# find the stretching parameter given the grid spacing at the surface
find_surface(γ) = dz_surface - z_top * tanh_stretch(ζ_n[1], γ)
γ_sol = RootSolvers.find_zero(
find_surface,
RootSolvers.NewtonsMethodAD(FT_solve(1.0)),
RootSolvers.CompactSolution(),
RootSolvers.ResidualTolerance(FT_solve(tol)),
)
if γ_sol.converged !== true
error(
"gamma root failed to converge for dz_surface: $dz_surface on domain ($z_bottom, $z_top)",
)
end

faces = (z_bottom + (z_top - z_bottom)) * tanh_stretch.(ζ_n, γ_sol.root)

# add the bottom level
faces = FT_solve[z_bottom; faces...]
if reverse_mode
reverse!(faces)
faces = map(f -> eltype(faces)(-f), faces)
faces[end] = faces[end] == -z_bottom ? z_bottom : faces[1]
end
monotonic_check(faces)
IntervalMesh(domain, CT.(faces))
end

"""
truncate_mesh(
Expand Down
50 changes: 48 additions & 2 deletions test/Meshes/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ end
end

@testset "IntervalMesh GeneralizedExponentialStretching" begin
# use normalized GCM profile heights (7.5km)
# use normalized GCM profile heights (45km)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.GeneralizedExponentialStretching(
0.02 / 45.0,
Expand Down Expand Up @@ -250,7 +250,7 @@ end


@testset "IntervalMesh GeneralizedExponentialStretching reverse" begin
# use normalized GCM profile heights (7.5km)
# use normalized GCM profile heights (45km)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.GeneralizedExponentialStretching(
7.0 / 45.0,
Expand Down Expand Up @@ -288,6 +288,52 @@ end
@test fₑ - fₑ₋₁ 0.02 / 45.0 rtol = 1e-2
end

@testset "IntervalMesh HyperbolicTangentStretching" begin
# use normalized GCM profile heights (75km)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = 0,
)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = 1,
)
# test a gcm like configuration
dom, mesh = unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = 75, # 76 face levels
)
# test the mesh coordinates are eqv to dz_bottom
@test Meshes.coordinates(mesh, 1, 1) == Geometry.ZPoint(0.0)
@test Meshes.coordinates(mesh, 1, 2) Geometry.ZPoint(0.03 / 75.0) rtol =
1e-3
end

@testset "IntervalMesh HyperbolicTangentStretching reverse" begin
# use normalized GCM profile heights (75km)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = 0,
reverse_mode = true,
)
@test_throws ArgumentError unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = 1,
reverse_mode = true,
)
# test a gcm like configuration, for land
nelems = 75
dom, mesh = unit_intervalmesh(
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
nelems = nelems, # 76 face levels
reverse_mode = true,
)
# test the mesh coordinates are eqv to dz_bottom
@test Meshes.coordinates(mesh, 1, 1) == Geometry.ZPoint(-1.0)
@test Meshes.coordinates(mesh, 1, nelems) Geometry.ZPoint(-0.03 / 75.0) rtol =
1e-3
end

@testset "Truncated IntervalMesh" begin
FT = Float64
nz = 55
Expand Down

0 comments on commit a0d4694

Please sign in to comment.