Skip to content

Commit

Permalink
Merge #101
Browse files Browse the repository at this point in the history
101: Implementation of Holtslag SCFs r=akshaysridhar a=gdecker1

<!--- THESE LINES ARE COMMENTED -->
## Purpose 
<!--- One sentence to describe the purpose of this PR, refer to any linked issues:
#14 -- this will link to issue 14
Closes #2 -- this will automatically close issue 2 on PR merge
-->

Add stability correction functions from Holtslag and De Bruin (1988) into ```UniversalFunctions.jl```.

Here is a [visual tabulation](https://docs.google.com/document/d/1T2krlLtTR-WMaMpYOvUqlcOWm2GK3-P5tluCzHc9ibg/edit?usp=sharing) of the equations in ```UniversalFunctions.jl``` for ease of reference.


## To-do
<!---  list of proposed tasks in the PR, move to "Content" on completion 
- Proposed task
-->

- [x] Implement Holtslag  SCF (phi) momentum and heat functions
- [x] Implement Holtslag integrated-SCF (psi) momentum and heat functions 
- [x] Implement Holtslag volume-averaged integrated-SCF (Psi) momentum and heat functions 
- [x] Add Holtslag to docs
- [x] Add Holtslag-specific asymptotic tests (phi equations diverge to infinity)



<!---
Review checklist

I have:
- followed the codebase contribution guide: https://clima.github.io/ClimateMachine.jl/latest/Contributing/
- followed the style guide: https://clima.github.io/ClimateMachine.jl/latest/DevDocs/CodeStyle/
- followed the documentation policy: https://github.com/CliMA/policies/wiki/Documentation-Policy
- checked that this PR does not duplicate an open PR.

In the Content, I have included 
- relevant unit tests, and integration tests, 
- appropriate docstrings on all functions, structs, and modules, and included relevant documentation.

-->

----
- [x] I have read and checked the items on the review checklist.


Co-authored-by: gdecker1 <[email protected]>
  • Loading branch information
bors[bot] and gdecker1 authored Aug 14, 2023
2 parents ca78057 + 302cc7f commit 95c1bd4
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 11 deletions.
13 changes: 13 additions & 0 deletions docs/bibliography.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
# Last author name (titlecase), followed by
# (no characters in-between) the year.
@article{Holtslag1988,
title = {Applied Modeling of the Nighttime Surface Energy Balance over Land},
author = {Holtslag, A. A. M., and H. A. R. De Bruin},
journal = {Journal of Applied Meteorology and Climatology},
volume = {27},
number = {6},
doi = {10.1175/1520-0450(1988)027<0689:AMOTNS>2.0.CO;2},
pages = {689–704},
year = {1988},
publisher = {Wiley Online Library}
}

@article{Nishizawa2018,
title = {A Surface Flux Scheme Based on the Monin-Obukhov Similarity for Finite Volume Models},
author = {Nishizawa, S and Kitamura, Y},
Expand Down
1 change: 1 addition & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ SurfaceFluxes.UniversalFunctions
SurfaceFluxes.UniversalFunctions.Gryanik
SurfaceFluxes.UniversalFunctions.Grachev
SurfaceFluxes.UniversalFunctions.Businger
SurfaceFluxes.UniversalFunctions.Holtslag
```

```@docs
Expand Down
2 changes: 1 addition & 1 deletion docs/src/plot_universal_functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Plots

L = FT(10);

ufts = (UF.GryanikType(), UF.BusingerType(), UF.GrachevType())
ufts = (UF.GryanikType(), UF.BusingerType(), UF.GrachevType(), UF.HoltslagType())

universal_functions(uft) = UF.universal_func(uft, L, create_uf_parameters(toml_dict, uft))

Expand Down
36 changes: 36 additions & 0 deletions parameters/create_parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@ import SurfaceFluxes as SF
import SurfaceFluxes.UniversalFunctions as UF
import Thermodynamics as TD

function create_uf_parameters(toml_dict, ::UF.HoltslagType)
FT = CP.float_type(toml_dict)

aliases = [
"Pr_0_Holtslag",
"a_m_Holtslag",
"a_h_Holtslag",
"b_m_Holtslag",
"b_h_Holtslag",
"c_m_Holtslag",
"c_h_Holtslag",
"d_m_Holtslag",
"d_h_Holtslag",
"ζ_a_Holtslag",
"γ_Holtslag",
]

pairs = CP.get_parameter_values!(toml_dict, aliases, "UniversalFunctions")
pairs = (; pairs...) # convert to NamedTuple

pairs = (;
Pr_0 = pairs.Pr_0_Holtslag,
a_m = pairs.a_m_Holtslag,
a_h = pairs.a_h_Holtslag,
b_m = pairs.b_m_Holtslag,
b_h = pairs.b_h_Holtslag,
c_m = pairs.c_m_Holtslag,
c_h = pairs.c_h_Holtslag,
d_m = pairs.d_m_Holtslag,
d_h = pairs.d_h_Holtslag,
ζ_a = pairs.ζ_a_Holtslag,
γ = pairs.γ_Holtslag,
)
return UF.HoltslagParams{FT}(; pairs...)
end

function create_uf_parameters(toml_dict, ::UF.GryanikType)
FT = CP.float_type(toml_dict)

Expand Down
4 changes: 4 additions & 0 deletions src/Parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ a_m(ps::SurfaceFluxesParameters) = a_m(uf_params(ps))
a_h(ps::SurfaceFluxesParameters) = a_h(uf_params(ps))
b_m(ps::SurfaceFluxesParameters) = b_m(uf_params(ps))
b_h(ps::SurfaceFluxesParameters) = b_h(uf_params(ps))
c_m(ps::SurfaceFluxesParameters) = c_m(uf_params(ps))
c_h(ps::SurfaceFluxesParameters) = c_h(uf_params(ps))
d_m(ps::SurfaceFluxesParameters) = d_m(uf_params(ps))
d_h(ps::SurfaceFluxesParameters) = d_h(uf_params(ps))
ζ_a(ps::SurfaceFluxesParameters) = ζ_a(uf_params(ps))
γ(ps::SurfaceFluxesParameters) = γ(uf_params(ps))

Expand Down
186 changes: 186 additions & 0 deletions src/UniversalFunctions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ universal functions:
- `Businger`
- `Gryanik`
- `Grachev`
- `Holtslag`
"""
module UniversalFunctions

Expand Down Expand Up @@ -80,6 +81,9 @@ a_h(uf::AUF) = uf.params.a_h
b_m(uf::AUF) = uf.params.b_m
b_h(uf::AUF) = uf.params.b_h
c_h(uf::AUF) = uf.params.c_h
c_m(uf::AUF) = uf.params.c_m
d_h(uf::AUF) = uf.params.d_h
d_m(uf::AUF) = uf.params.d_m
ζ_a(uf::AUF) = uf.params.ζ_a
γ(uf::AUF) = uf.params.γ

Expand Down Expand Up @@ -496,12 +500,194 @@ function psi(uf::Grachev, ζ, tt::HeatTransport)
end
end

#####
##### Holtslag
#####

Base.@kwdef struct HoltslagParams{FT} <: AbstractUniversalFunctionParameters{FT}
Pr_0::FT
a_m::FT
a_h::FT
b_m::FT
b_h::FT
c_m::FT
c_h::FT
d_m::FT
d_h::FT
ζ_a::FT
γ::FT
end

"""
Holtslag <: AbstractUniversalFunction{FT}
# References
- [Holtslag1988](@cite)
# Equations in reference:
`ϕ_m`: Derived from Eq. 12
`ϕ_h`: Derived from Eq. 12
`ψ_m`: Eq. 12
`ψ_h`: Eq. 12
# Holtslag and Bruin 1988 functions are used in stable conditions
# In unstable conditions the functions of Businger (1971) are
# assigned by default.
# Fields
$(DSE.FIELDS)
"""
struct Holtslag{FT, PS <: HoltslagParams} <: AbstractUniversalFunction{FT}
"Monin-Obhukov Length"
L::FT
params::PS
end

struct HoltslagType <: AbstractUniversalFunctionType end
Holtslag() = HoltslagType()

# Nishizawa2018 Eq. A7
f_momentum(uf::Holtslag, ζ) = sqrt(sqrt(1 - 15 * ζ))

# Nishizawa2018 Eq. A8
f_heat(uf::Holtslag, ζ) = sqrt(1 - 9 * ζ)

function phi(uf::Holtslag, ζ, tt::MomentumTransport)
FT = eltype(uf)
if 0 < ζ
# Derived from Holtslag1988 Eq. 12
_a_m = FT(a_m(uf))
_b_m = FT(b_m(uf))
_c_m = FT(c_m(uf))
_d_m = FT(d_m(uf))
_π_group = FT(π_group(uf, tt))

first_exp = _b_m * exp(-_d_m * ζ)
second_exp = -_b_m * _d_m * exp(-_d_m * ζ) *- _c_m / _d_m)

return _π_group + ζ * (_a_m + first_exp + second_exp)
else
# Nishizawa2018 Eq. A1 (L < 0)
f_m = f_momentum(uf, ζ)
return 1 / f_m
end
end

function phi(uf::Holtslag, ζ, tt::HeatTransport)
FT = eltype(uf)
if 0 < ζ
# Derived from Holtslag1988 Eq. 12
_a_h = FT(a_h(uf))
_b_h = FT(b_h(uf))
_c_h = FT(c_h(uf))
_d_h = FT(d_h(uf))
_π_group = FT(π_group(uf, tt))

first_exp = _b_h * exp(-_d_h * ζ)
second_exp = -_b_h * _d_h * exp(-_d_h * ζ) *- _c_h / _d_h)

return _π_group + ζ * (_a_h + first_exp + second_exp)
else
# Nishizawa2018 Eq. A2 (L < 0)
f_h = f_heat(uf, ζ)
return 1 / f_h
end
end

function psi(uf::Holtslag, ζ, tt::MomentumTransport)
FT = eltype(uf)
if 0 < ζ
# Holtslag1988 Eq. 12
_a_m = FT(a_m(uf))
_b_m = FT(b_m(uf))
_c_m = FT(c_m(uf))
_d_m = FT(d_m(uf))

return -_a_m * ζ - _b_m *- _c_m / _d_m) * exp(-_d_m * ζ) - _b_m * _c_m / _d_m
else
# Nishizawa2018 Eq. A3 (ζ < 0)
f_m = f_momentum(uf, ζ)
log_term = log((1 + f_m)^2 * (1 + f_m^2) / 8)
return log_term - 2 * atan(f_m) + FT(π) / 2
end
end

function psi(uf::Holtslag, ζ, tt::HeatTransport)
FT = eltype(uf)
if 0 < ζ
# Holtslag1988 Eq. 12
_a_h = FT(a_h(uf))
_b_h = FT(b_h(uf))
_c_h = FT(c_h(uf))
_d_h = FT(d_h(uf))

return -_a_h * ζ - _b_h *- _c_h / _d_h) * exp(-_d_h * ζ) - _b_h * _c_h / _d_h

else
# Nishizawa2018 Eq. A4 (ζ < 0)
f_h = f_heat(uf, ζ)
return 2 * log((1 + f_h) / 2)
end
end

function Psi(uf::Holtslag, ζ, tt::MomentumTransport)
FT = eltype(uf)
_a_m = FT(a_m(uf))
_b_m = FT(b_m(uf))
_c_m = FT(c_m(uf))
_d_m = FT(d_m(uf))
if ζ >= 0
# Derived from Holtslag1988 Eq. 12
exp_term1 = _c_m * ((1 - exp(-_d_m * ζ)) /* _d_m^2) - 1 / _d_m)
exp_term2 = (exp(-_d_m * ζ) - 1) /* _d_m^2)
exp_term3 = exp(-_d_m * ζ) / _d_m
return _b_m * (exp_term1 + exp_term2 + exp_term3) - _a_m * ζ / 2

elseif abs(ζ) < eps(FT) && ζ < 0
# Nishizawa2018 Eq. A13 (ζ < 0)
return -FT(15) * ζ / FT(8)

else
# Nishizawa2018 Eq. A5 (ζ < 0)
return -FT(9) * _a_m * ((_b_m * ζ + FT(1))^(FT(4 / 3)) - 1) / ζ / FT(4) / _b_m^FT(2) - FT(1)
end
end

function Psi(uf::Holtslag, ζ, tt::HeatTransport)
FT = eltype(uf)
_a_h = FT(a_h(uf))
_b_h = FT(b_h(uf))
_c_h = FT(c_h(uf))
_d_h = FT(d_h(uf))
if ζ >= 0
# Derived from Holtslag1988 Eq. 12
exp_term1 = _c_h * ((1 - exp(-_d_h * ζ)) /* _d_h^2) - 1 / _d_h)
exp_term2 = (exp(-_d_h * ζ) - 1) /* _d_h^2)
exp_term3 = exp(-_d_h * ζ) / _d_h
return _b_h * (exp_term1 + exp_term2 + exp_term3) - _a_h * ζ / 2

elseif abs(ζ) < eps(FT) && ζ < 0
# Nishizawa2018 Eq. A14 (ζ < 0)
return -9 * ζ / 4

else
# Nishizawa2018 Eq. A6 (ζ < 0)
f_h = f_heat(uf, ζ)
log_term = 2 * log((1 + f_h) / 2)
return log_term + 2 * (1 - f_h) / (9 * ζ) - 1
end
end

universal_func(::BusingerType, L_MO::Real, params::BusingerParams) = Businger(L_MO, params)
universal_func(::GryanikType, L_MO::Real, params::GryanikParams) = Gryanik(L_MO, params)
universal_func(::GrachevType, L_MO::Real, params::GrachevParams) = Grachev(L_MO, params)
universal_func(::HoltslagType, L_MO::Real, params::HoltslagParams) = Holtslag(L_MO, params)

universal_func_type(::Type{T}) where {T <: BusingerParams} = BusingerType()
universal_func_type(::Type{T}) where {T <: GryanikParams} = GryanikType()
universal_func_type(::Type{T}) where {T <: GrachevParams} = GrachevType()
universal_func_type(::Type{T}) where {T <: HoltslagParams} = HoltslagType()

end # module
6 changes: 3 additions & 3 deletions test/test_convergence.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ function check_over_moist_states(
end

@testset "Check convergence (dry thermodynamic states): Stable/Unstable" begin
for uf_type in [UF.BusingerType(), UF.GryanikType(), UF.GrachevType()]
for uf_type in [UF.BusingerType(), UF.GryanikType(), UF.GrachevType(), UF.HoltslagType()]
for FT in [Float32, Float64]
for profile_type in [DryProfiles(), MoistEquilProfiles()]
profiles_sfc, profiles_int = generate_profiles(FT, profile_type)
profiles_sfc, profiles_int = generate_profiles(FT, profile_type; uf_type = uf_type)
scheme = [SF.FVScheme(), SF.FDScheme()]
z0_momentum = Array{FT}(range(1e-6, stop = 1e-1, length = 2))
z0_thermal = Array{FT}(range(1e-6, stop = 1e-1, length = 2))
Expand Down Expand Up @@ -250,6 +250,6 @@ end
end
end
end
@info "Tested Businger/Gryanik/Grachev u.f., Float32/Float64, Dry/EquilMoist profiles, non-iterative solver on/off"
@info "Tested Businger/Gryanik/Grachev/Holtslag u.f., Float32/Float64, Dry/EquilMoist profiles, non-iterative solver on/off"

end
15 changes: 8 additions & 7 deletions test/test_universal_functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ universal_functions(uft, L) = UF.universal_func(uft, L, create_uf_parameters(tom
FT = Float32
ζ = FT(-2):FT(0.01):FT(200)
for L in (-FT(10), FT(10))
for uft in (UF.GryanikType(), UF.GrachevType(), UF.BusingerType())
for uft in (UF.GryanikType(), UF.GrachevType(), UF.BusingerType(), UF.HoltslagType())
uf = universal_functions(uft, L)
for transport in (UF.MomentumTransport(), UF.HeatTransport())
ϕ = UF.phi.(uf, ζ, transport)
Expand All @@ -38,7 +38,7 @@ universal_functions(uft, L) = UF.universal_func(uft, L, create_uf_parameters(tom
FT = Float32
ζ = (-FT(1), FT(0.5) * eps(FT), 2 * eps(FT))
for L in (-FT(10), FT(10))
for uft in (UF.GryanikType(), UF.GrachevType(), UF.BusingerType())
for uft in (UF.GryanikType(), UF.GrachevType(), UF.BusingerType(), UF.HoltslagType())
uf = universal_functions(uft, L)
for transport in (UF.MomentumTransport(), UF.HeatTransport())
ϕ = UF.phi.(uf, ζ, transport)
Expand All @@ -53,11 +53,12 @@ universal_functions(uft, L) = UF.universal_func(uft, L, create_uf_parameters(tom
FT = Float32
ζ = (-FT(1), -FT(0.5) * eps(FT), FT(0.5) * eps(FT), 2 * eps(FT))
for L in (-FT(10), FT(10))
uft = UF.BusingerType()
uf = universal_functions(uft, L)
for transport in (UF.MomentumTransport(), UF.HeatTransport())
Ψ = UF.Psi.(uf, ζ, transport)
@test eltype(Ψ) == FT
for uft in (UF.GryanikType(), UF.BusingerType(), UF.HoltslagType())
uf = universal_functions(uft, L)
for transport in (UF.MomentumTransport(), UF.HeatTransport())
Ψ = UF.Psi.(uf, ζ, transport)
@test eltype(Ψ) == FT
end
end
end
end
Expand Down

0 comments on commit 95c1bd4

Please sign in to comment.