Skip to content

Commit 230d4a2

Browse files
authored
Merge pull request #1930 from CliMA/zs/stretching
add hyperbolic tangent stretching
2 parents cfd8901 + a47998d commit 230d4a2

File tree

4 files changed

+151
-7
lines changed

4 files changed

+151
-7
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ClimaCore.jl Release Notes
33

44
main
55
-------
6+
- Added hyperbolic tangent stretching. PR [#1930](https://github.com/CliMA/ClimaCore.jl/pull/1930).
67

78
v0.14.11
89
-------

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Meshes.NormalizedBilinearMap
105105
Meshes.Uniform
106106
Meshes.ExponentialStretching
107107
Meshes.GeneralizedExponentialStretching
108+
Meshes.HyperbolicTangentStretching
108109
```
109110

110111
### Mesh utilities

src/Meshes/interval.jl

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Constuct a 1D mesh on `domain` with `nelems` elements, using `stretching`. Possi
1616
- [`Uniform()`](@ref)
1717
- [`ExponentialStretching(H)`](@ref)
1818
- [`GeneralizedExponentialStretching(dz_bottom, dz_top)`](@ref)
19+
- [`HyperbolicTangentStretching(dz_bottom)`](@ref)
1920
"""
2021
struct IntervalMesh{I <: IntervalDomain, V <: AbstractVector} <: AbstractMesh1D
2122
domain::I
@@ -165,6 +166,8 @@ model configurations).
165166
Then, the user can define a stretched mesh via
166167
167168
ClimaCore.Meshes.IntervalMesh(interval_domain, ExponentialStretching(H); nelems::Int, reverse_mode = false)
169+
170+
`faces` contain reference z without any warping.
168171
"""
169172
struct ExponentialStretching{FT} <: StretchingRule
170173
H::FT
@@ -213,6 +216,8 @@ For land configurations, use `reverse_mode` = `true` (default value `false`).
213216
Then, the user can define a generalized stretched mesh via
214217
215218
ClimaCore.Meshes.IntervalMesh(interval_domain, GeneralizedExponentialStretching(dz_bottom, dz_top); nelems::Int, reverse_mode = false)
219+
220+
`faces` contain reference z without any warping.
216221
"""
217222
struct GeneralizedExponentialStretching{FT} <: StretchingRule
218223
dz_bottom::FT
@@ -241,9 +246,9 @@ function IntervalMesh(
241246
throw(ArgumentError("dz_top must be ≤ dz_bottom"))
242247
end
243248

244-
# bottom coord height value, always min, for both atmos and land, since z-axis does not change
249+
# bottom coord height value is always min and top coord height value is always max
250+
# since the vertical coordinate is positive upward
245251
z_bottom = Geometry.component(domain.coord_min, 1)
246-
# top coord height value, always max, for both atmos and land, since z-axis does not change
247252
z_top = Geometry.component(domain.coord_max, 1)
248253
# but in case of reverse_mode, we temporarily swap them together with dz_bottom and dz_top
249254
# so that the following root solve algorithm does not need to change
@@ -256,7 +261,7 @@ function IntervalMesh(
256261
# define the inverse σ⁻¹ exponential stretching function
257262
exp_stretch(ζ, h) = ζ == 1 ? ζ : -h * log(1 - (1 - exp(-1 / h)) * ζ)
258263

259-
# nondimensional vertical coordinate (]0.0, 1.0])
264+
# nondimensional vertical coordinate ([0.0, 1.0])
260265
ζ_n = LinRange(one(FT_solve), nelems, nelems) / nelems
261266

262267
# find bottom height variation
@@ -292,7 +297,7 @@ function IntervalMesh(
292297
find_top,
293298
RootSolvers.SecantMethod(guess₋, guess₊),
294299
RootSolvers.CompactSolution(),
295-
RootSolvers.ResidualTolerance(FT_solve(1e-3)),
300+
RootSolvers.ResidualTolerance(FT_solve(tol)),
296301
)
297302
if h_top_sol.converged !== true
298303
error(
@@ -305,7 +310,7 @@ function IntervalMesh(
305310
h =
306311
h_bottom .+
307312
(ζ_n .- ζ_n[1]) * (h_top - h_bottom) / (ζ_n[end - 1] - ζ_n[1])
308-
faces = (z_bottom + (z_top - z_bottom)) * exp_stretch.(ζ_n, h)
313+
faces = z_bottom .+ (z_top - z_bottom) * exp_stretch.(ζ_n, h)
309314

310315
# add the bottom level
311316
faces = FT_solve[z_bottom; faces...]
@@ -318,6 +323,94 @@ function IntervalMesh(
318323
IntervalMesh(domain, CT.(faces))
319324
end
320325

326+
"""
327+
HyperbolicTangentStretching(dz_surface::FT)
328+
329+
Apply a hyperbolic tangent stretching to the domain when constructing elements.
330+
`dz_surface` is the target element grid spacing at the surface. In typical atmosphere
331+
configuration, it is the grid spacing at the bottom of the
332+
vertical column domain (m). On the other hand, for typical land configurations,
333+
it is the grid spacing at the top of the vertical column domain.
334+
335+
For an interval ``[z_0,z_1]``, this makes the elements uniformally spaced in
336+
``\\zeta``, where
337+
```math
338+
\\eta = 1 - \\frac{tanh[\\gamma(1-\\zeta)]}{tanh(\\gamma)},
339+
```
340+
where ``\\eta = \\frac{z - z_0}{z_1-z_0}``. The stretching parameter ``\\gamma``
341+
is chosen to achieve a given resolution `dz_surface` at the surface.
342+
343+
Then, the user can define a stretched mesh via
344+
345+
ClimaCore.Meshes.IntervalMesh(interval_domain, HyperbolicTangentStretching(dz_surface); nelems::Int, reverse_mode)
346+
347+
`reverse_mode` is default to false for atmosphere configurations. For land configurations,
348+
use `reverse_mode` = `true`.
349+
350+
`faces` contain reference z without any warping.
351+
"""
352+
struct HyperbolicTangentStretching{FT} <: StretchingRule
353+
dz_surface::FT
354+
end
355+
356+
function IntervalMesh(
357+
domain::IntervalDomain{CT},
358+
stretch::HyperbolicTangentStretching{FT};
359+
nelems::Int,
360+
FT_solve = Float64,
361+
tol::Union{FT, Nothing} = nothing,
362+
reverse_mode::Bool = false,
363+
) where {CT <: Geometry.Abstract1DPoint{FT}} where {FT}
364+
if nelems 1
365+
throw(ArgumentError("`nelems` must be ≥ 2"))
366+
end
367+
368+
dz_surface = FT_solve(stretch.dz_surface)
369+
tol === nothing && (tol = dz_surface * FT_solve(1e-6))
370+
371+
# bottom coord height value is always min and top coord height value is always max
372+
# since the vertical coordinate is positive upward
373+
z_bottom = Geometry.component(domain.coord_min, 1)
374+
z_top = Geometry.component(domain.coord_max, 1)
375+
# but in case of reverse_mode, we temporarily swap them
376+
# so that the following root solve algorithm does not need to change
377+
if reverse_mode
378+
z_bottom, z_top = Geometry.component(domain.coord_max, 1),
379+
-Geometry.component(domain.coord_min, 1)
380+
end
381+
382+
# define the hyperbolic tangent stretching function
383+
tanh_stretch(ζ, γ) = 1 - tanh* (1 - ζ)) / tanh(γ)
384+
385+
# nondimensional vertical coordinate ([0.0, 1.0])
386+
ζ_n = LinRange(one(FT_solve), nelems, nelems) / nelems
387+
388+
# find the stretching parameter given the grid spacing at the surface
389+
find_surface(γ) = dz_surface - z_top * tanh_stretch(ζ_n[1], γ)
390+
γ_sol = RootSolvers.find_zero(
391+
find_surface,
392+
RootSolvers.NewtonsMethodAD(FT_solve(1.0)),
393+
RootSolvers.CompactSolution(),
394+
RootSolvers.ResidualTolerance(FT_solve(tol)),
395+
)
396+
if γ_sol.converged !== true
397+
error(
398+
"gamma root failed to converge for dz_surface: $dz_surface on domain ($z_bottom, $z_top)",
399+
)
400+
end
401+
402+
faces = z_bottom .+ (z_top - z_bottom) * tanh_stretch.(ζ_n, γ_sol.root)
403+
404+
# add the bottom level
405+
faces = FT_solve[z_bottom; faces...]
406+
if reverse_mode
407+
reverse!(faces)
408+
faces = map(f -> eltype(faces)(-f), faces)
409+
faces[end] = faces[end] == -z_bottom ? z_bottom : faces[1]
410+
end
411+
monotonic_check(faces)
412+
IntervalMesh(domain, CT.(faces))
413+
end
321414

322415
"""
323416
truncate_mesh(

test/Meshes/interval.jl

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ end
214214
end
215215

216216
@testset "IntervalMesh GeneralizedExponentialStretching" begin
217-
# use normalized GCM profile heights (7.5km)
217+
# use normalized GCM profile heights (45km)
218218
@test_throws ArgumentError unit_intervalmesh(
219219
stretching = Meshes.GeneralizedExponentialStretching(
220220
0.02 / 45.0,
@@ -250,7 +250,7 @@ end
250250

251251

252252
@testset "IntervalMesh GeneralizedExponentialStretching reverse" begin
253-
# use normalized GCM profile heights (7.5km)
253+
# use normalized GCM profile heights (45km)
254254
@test_throws ArgumentError unit_intervalmesh(
255255
stretching = Meshes.GeneralizedExponentialStretching(
256256
7.0 / 45.0,
@@ -288,6 +288,55 @@ end
288288
@test fₑ - fₑ₋₁ 0.02 / 45.0 rtol = 1e-2
289289
end
290290

291+
@testset "IntervalMesh HyperbolicTangentStretching" begin
292+
# use normalized GCM profile heights (75km)
293+
@test_throws ArgumentError unit_intervalmesh(
294+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
295+
nelems = 0,
296+
)
297+
@test_throws ArgumentError unit_intervalmesh(
298+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
299+
nelems = 1,
300+
)
301+
# test a gcm like configuration
302+
nelems = 75
303+
dom, mesh = unit_intervalmesh(
304+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
305+
nelems = nelems, # 76 face levels
306+
)
307+
# test the bottom and top of the mesh coordinates are correct
308+
@test Meshes.coordinates(mesh, 1, 1) == Geometry.ZPoint(0.0)
309+
@test Meshes.coordinates(mesh, 1, nelems + 1) Geometry.ZPoint(1.0)
310+
# test the interval of the mesh coordinates at the surface is the same as dz_surface
311+
@test Meshes.coordinates(mesh, 1, 2) Geometry.ZPoint(0.03 / 75.0)
312+
end
313+
314+
@testset "IntervalMesh HyperbolicTangentStretching reverse" begin
315+
# use normalized GCM profile heights (75km)
316+
@test_throws ArgumentError unit_intervalmesh(
317+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
318+
nelems = 0,
319+
reverse_mode = true,
320+
)
321+
@test_throws ArgumentError unit_intervalmesh(
322+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
323+
nelems = 1,
324+
reverse_mode = true,
325+
)
326+
# test a gcm like configuration, for land
327+
nelems = 75
328+
dom, mesh = unit_intervalmesh(
329+
stretching = Meshes.HyperbolicTangentStretching(0.03 / 75.0),
330+
nelems = nelems, # 76 face levels
331+
reverse_mode = true,
332+
)
333+
# test the bottom and top of the mesh coordinates are correct
334+
@test Meshes.coordinates(mesh, 1, 1) == Geometry.ZPoint(-1.0)
335+
@test Meshes.coordinates(mesh, 1, nelems + 1) Geometry.ZPoint(0.0)
336+
# test the interval of the mesh coordinates at the surface is the same as dz_surface
337+
@test Meshes.coordinates(mesh, 1, nelems) Geometry.ZPoint(-0.03 / 75.0)
338+
end
339+
291340
@testset "Truncated IntervalMesh" begin
292341
FT = Float64
293342
nz = 55

0 commit comments

Comments
 (0)