Skip to content

Commit b438af1

Browse files
authored
Merge pull request #3801 from CliMA/cc/mixing_length_updates_pt2
Update mixing length formulation
2 parents 6f30064 + badc472 commit b438af1

File tree

9 files changed

+242
-72
lines changed

9 files changed

+242
-72
lines changed

config/default_configs/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ turbconv:
324324
advection_test:
325325
help: "Switches off all grid-scale and subgrid-scale momentum tendencies [`false` (default), `true`]"
326326
value: false
327+
edmfx_scale_blending:
328+
help: "Method for blending physical scales in EDMFX mixing length calculation. [`SmoothMinimum` (default), `HardMinimum`]"
329+
value: "SmoothMinimum"
327330
implicit_sgs_advection:
328331
help: "Whether to treat the subgrid-scale vertical advection tendency implicitly [`false` (default), `true`]"
329332
value: false

reproducibility_tests/ref_counter.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
238
1+
239
22

33
# **README**
44
#
@@ -20,6 +20,9 @@
2020

2121

2222
#=
23+
239
24+
- Update mixing length formulation
25+
2326
238
2427
- Limit by Pr_max = 10 (new default in ClimaParams v0.10.30)
2528

src/cache/diagnostic_edmf_precomputed_quantities.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ NVTX.@annotate function set_diagnostic_edmf_precomputed_quantities_env_closures!
10111011
ᶜstrain_rate_norm,
10121012
ᶜprandtl_nvec,
10131013
ᶜtke_exch,
1014+
p.atmos.edmfx_model.scale_blending_method,
10141015
)
10151016
@. ᶜmixing_length = ᶜmixing_length_tuple.master
10161017

src/cache/prognostic_edmf_precomputed_quantities.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ NVTX.@annotate function set_prognostic_edmf_precomputed_quantities_explicit_clos
527527
ᶜstrain_rate_norm,
528528
ᶜprandtl_nvec,
529529
ᶜtke_exch,
530+
p.atmos.edmfx_model.scale_blending_method,
530531
)
531532

532533
@. ᶜmixing_length = ᶜmixing_length_tuple.master

src/parameters/Parameters.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ Base.@kwdef struct ClimaAtmosParameters{
113113
α_hyperdiff_tracer::FT
114114
# Vertical diffusion
115115
α_vert_diff_tracer::FT
116+
# Gryanik b_m coefficient
117+
coeff_b_m_gryanik::FT
116118
end
117119

118120
Base.eltype(::ClimaAtmosParameters{FT}) where {FT} = FT
@@ -139,6 +141,12 @@ end
139141
von_karman_const(ps::ACAP) =
140142
SF.Parameters.von_karman_const(surface_fluxes_params(ps))
141143

144+
# ------ MOST (Monin–Obukhov) stability-function coefficients ------
145+
146+
# Gryanik b_m
147+
# needed because surface_fluxes_params defaults to BusingerParams
148+
coefficient_b_m_gryanik(ps::ACAP) = ps.coeff_b_m_gryanik
149+
142150
# Insolation parameters
143151
day(ps::ACAP) = IP.day(insolation_params(ps))
144152
tot_solar_irrad(ps::ACAP) = IP.tot_solar_irrad(insolation_params(ps))

src/parameters/create_parameters.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ function ClimaAtmosParameters(toml_dict::TD) where {TD <: CP.AbstractTOMLDict}
3939
SurfaceFluxesParameters(toml_dict, UF.BusingerParams)
4040
SFP = typeof(surface_fluxes_params)
4141

42+
# Fetch Gryanik b_m coefficient (since surface_fluxes_params defaults to BusingerParams)
43+
coeff_b_m_gryanik_val = UF.GryanikParams(FT).b_m
44+
4245
surface_temp_params = SurfaceTemperatureParameters(toml_dict)
4346
STP = typeof(surface_temp_params)
4447

@@ -84,6 +87,7 @@ function ClimaAtmosParameters(toml_dict::TD) where {TD <: CP.AbstractTOMLDict}
8487
surface_temp_params,
8588
vert_diff_params,
8689
external_forcing_params,
90+
coeff_b_m_gryanik = coeff_b_m_gryanik_val,
8791
)
8892
end
8993

src/prognostic_equations/edmfx_closures.jl

Lines changed: 202 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -172,26 +172,48 @@ function lamb_smooth_minimum(l, smoothness_param, λ_floor)
172172
return numerator / max(eps(FT), denominator)
173173
end
174174

175+
function blend_scales(
176+
method::SmoothMinimumBlending,
177+
l::SA.SVector,
178+
turbconv_params,
179+
)
180+
FT = eltype(l)
181+
smin_ub = CAP.smin_ub(turbconv_params)
182+
smin_rm = CAP.smin_rm(turbconv_params)
183+
l_final = lamb_smooth_minimum(l, smin_ub, smin_rm)
184+
return max(l_final, FT(0))
185+
end
186+
187+
function blend_scales(
188+
method::HardMinimumBlending,
189+
l::SA.SVector,
190+
turbconv_params,
191+
)
192+
FT = eltype(l)
193+
return max(minimum(l), FT(0))
194+
end
195+
175196
"""
176-
mixing_length(params, ustar, ᶜz, sfc_tke, ᶜlinear_buoygrad, ᶜtke, obukhov_length, ᶜstrain_rate_norm, ᶜPr, ᶜtke_exch)
197+
mixing_length(params, ustar, ᶜz, z_sfc, ᶜdz,
198+
sfc_tke, ᶜlinear_buoygrad, ᶜtke, obukhov_length,
199+
ᶜstrain_rate_norm, ᶜPr, ᶜtke_exch, scale_blending_method)
177200
178201
where:
179-
- `params`: set with model parameters
180-
- `ustar`: friction velocity
181-
- `ᶜz`: height
182-
- `tke_sfc`: env kinetic energy at first cell center
183-
- `ᶜlinear_buoygrad`: buoyancy gradient
184-
- `ᶜtke`: env turbulent kinetic energy
185-
- `obukhov_length`: surface Monin Obukhov length
186-
- `ᶜstrain_rate_norm`: Frobenius norm of strain rate tensor
187-
- `ᶜPr`: Prandtl number
188-
- `ᶜtke_exch`: subdomain exchange term
189-
190-
Returns mixing length as a smooth minimum between
191-
wall-constrained length scale,
192-
production-dissipation balanced length scale,
193-
effective static stability length scale, and
194-
Smagorinsky length scale.
202+
- `params`: Parameter set (e.g., CLIMAParameters.AbstractParameterSet).
203+
- `ustar`: Friction velocity [m/s].
204+
- `ᶜz`: Cell center height [m].
205+
- `z_sfc`: Surface elevation [m].
206+
- `ᶜdz`: Cell vertical thickness [m].
207+
- `sfc_tke`: TKE near the surface (e.g., first cell center) [m^2/s^2].
208+
- `ᶜlinear_buoygrad`: N^2, Brunt-Väisälä frequency squared [1/s^2].
209+
- `ᶜtke`: Turbulent kinetic energy at cell center [m^2/s^2].
210+
- `obukhov_length`: Surface Monin-Obukhov length [m].
211+
- `ᶜstrain_rate_norm`: Frobenius norm of strain rate tensor [1/s].
212+
- `ᶜPr`: Turbulent Prandtl number [-].
213+
- `ᶜtke_exch`: TKE exchange term [m^2/s^3].
214+
- `scale_blending_method`: The method to use for blending physical scales.
215+
216+
Calculates the turbulent mixing length, limited by physical constraints and grid resolution.
195217
"""
196218
function mixing_length(
197219
params,
@@ -206,78 +228,187 @@ function mixing_length(
206228
ᶜstrain_rate_norm,
207229
ᶜPr,
208230
ᶜtke_exch,
231+
scale_blending_method,
209232
)
210233

211234
FT = eltype(params)
235+
eps_FT = eps(FT)
236+
237+
212238
turbconv_params = CAP.turbconv_params(params)
239+
sf_params = CAP.surface_fluxes_params(params) # Businger params
240+
213241
c_m = CAP.tke_ed_coeff(turbconv_params)
214242
c_d = CAP.tke_diss_coeff(turbconv_params)
215243
smin_ub = CAP.smin_ub(turbconv_params)
216244
smin_rm = CAP.smin_rm(turbconv_params)
217245
c_b = CAP.static_stab_coeff(turbconv_params)
218246
vkc = CAP.von_karman_const(params)
219247

220-
# compute the maximum mixing length at height z
221-
l_z = ᶜz - z_sfc
248+
# MOST stability function coefficients
249+
most_a_m = sf_params.ufp.a_m # Businger a_m
250+
most_b_m = sf_params.ufp.b_m # Businger b_m
251+
most_g_m = CAP.coefficient_b_m_gryanik(params) # Gryanik b_m
222252

223-
# compute the l_W - the wall constraint mixing length
224-
# which imposes an upper limit on the size of eddies near the surface
225-
# kz scale (surface layer)
226-
if obukhov_length < 0.0 #unstable
227-
l_W =
228-
vkc * (ᶜz - z_sfc) /
229-
max(sqrt(sfc_tke / ustar / ustar) * c_m, eps(FT)) *
230-
min((1 - 100 * (ᶜz - z_sfc) / obukhov_length)^FT(0.2), 1 / vkc)
231-
else # neutral or stable
232-
l_W =
233-
vkc * (ᶜz - z_sfc) /
234-
max(sqrt(sfc_tke / ustar / ustar) * c_m, eps(FT))
253+
# l_z: Geometric distance from the surface
254+
l_z = ᶜz - z_sfc
255+
# Ensure l_z is non-negative when ᶜz is numerically smaller than z_sfc.
256+
l_z = max(l_z, FT(0))
257+
258+
# l_W: Wall-constrained length scale (near-surface limit, to match
259+
# Monin-Obukhov Similarity Theory in the surface layer, with Businger-Dyer
260+
# type stability functions)
261+
tke_sfc_safe = max(sfc_tke, eps_FT)
262+
ustar_sq_safe = max(ustar * ustar, eps_FT) # ustar^2 may vanish in certain LES setups
263+
264+
# Denominator of the base length scale (always positive):
265+
# c_m * √(tke_sfc / u_*²) = c_m * √(e_sfc) / u_*
266+
# The value increases when u_* is small and decreases when e_sfc is small.
267+
l_W_denom_factor = sqrt(tke_sfc_safe / ustar_sq_safe)
268+
l_W_denom = max(c_m * l_W_denom_factor, eps_FT)
269+
270+
# Base length scale (neutral, but adjusted for TKE level)
271+
# l_W_base = κ * l_z / (c_m * sqrt(e_sfc) / u_star)
272+
# This can be Inf if l_W_denom is eps_FT and l_z is large.
273+
# This can be 0 if l_z is 0.
274+
# The expression approaches ∞ when l_W_denom ≈ eps_FT and l_z > eps_FT,
275+
# and approaches 0 when l_z → 0.
276+
l_W_base = vkc * l_z / l_W_denom
277+
278+
if obukhov_length < FT(0) # Unstable case
279+
obukhov_len_safe = min(obukhov_length, -eps_FT) # Ensure L < 0
280+
zeta = l_z / obukhov_len_safe # Stability parameter zeta = z/L (<0)
281+
282+
# Calculate MOST term (1 - b_m * zeta)
283+
# Since zeta is negative, this term is > 1
284+
inner_term = 1 - most_b_m * zeta
285+
286+
# Numerical safety check – by theory the value is ≥ 1.
287+
inner_term_safe = max(inner_term, eps_FT)
288+
289+
# Unstable-regime correction factor:
290+
# (1 − b_m ζ)^(1/4) = φ_m⁻¹,
291+
# where φ_m is the Businger stability function φ_m = (1 − b_m ζ)^(-1/4).
292+
stability_correction = sqrt(sqrt(inner_term_safe))
293+
l_W = l_W_base * stability_correction
294+
295+
else # Neutral or stable case
296+
# Ensure L > 0 for Monin-Obukhov length
297+
obukhov_len_safe_stable = max(obukhov_length, eps_FT)
298+
zeta = l_z / obukhov_len_safe_stable # zeta >= 0
299+
300+
# Stable/neutral-regime correction after Gryanik (2020):
301+
# φ_m = 1 + a_m ζ / (1 + g_m ζ)^(2/3),
302+
# a nonlinear refinement to the Businger formulation.
303+
phi_m_denom_term = (1 + most_g_m * zeta)
304+
# Guard against a negative base in the fractional power
305+
# (theoretically impossible for ζ ≥ 0 and g_m > 0, retained for robustness).
306+
phi_m_denom_cubed_sqrt = cbrt(phi_m_denom_term)
307+
phi_m_denom =
308+
max(phi_m_denom_cubed_sqrt * phi_m_denom_cubed_sqrt, eps_FT) # (val)^(2/3)
309+
310+
phi_m = 1 + (most_a_m * zeta) / phi_m_denom
311+
312+
# Stable-regime correction factor: 1 / φ_m.
313+
# phi_m should be >= 1 for stable/neutral
314+
stability_correction = 1 / max(phi_m, eps_FT)
315+
316+
# Apply the correction factor
317+
l_W = l_W_base * stability_correction
235318
end
236-
237-
# compute l_TKE - the production-dissipation balanced length scale
238-
a_pd = c_m * (2 * ᶜstrain_rate_norm - ᶜlinear_buoygrad / ᶜPr) * sqrt(ᶜtke)
239-
# Dissipation term
240-
c_neg = c_d * ᶜtke * sqrt(ᶜtke)
241-
if abs(a_pd) > eps(FT) && 4 * a_pd * c_neg > -(ᶜtke_exch * ᶜtke_exch)
242-
l_TKE = max(
243-
-(ᶜtke_exch / 2 / a_pd) +
244-
sqrt(ᶜtke_exch * ᶜtke_exch + 4 * a_pd * c_neg) / 2 / a_pd,
245-
0,
246-
)
247-
elseif abs(a_pd) < eps(FT) && abs(ᶜtke_exch) > eps(FT)
248-
l_TKE = c_neg / ᶜtke_exch
249-
else
250-
l_TKE = FT(0)
319+
l_W = max(l_W, FT(0)) # Ensure non-negative
320+
321+
# --- l_TKE: TKE production-dissipation balance scale ---
322+
tke_pos = max(ᶜtke, FT(0)) # Ensure TKE is not negative
323+
sqrt_tke_pos = sqrt(tke_pos)
324+
325+
# Net production of TKE from shear and buoyancy is approximated by
326+
# (S² − N²/Pr_t) · √TKE · l,
327+
# where S² denotes the gradient involved in shear production and
328+
# N²/Pr_t denotes the gradient involved in buoyancy production.
329+
# The factor below corresponds to that production term normalised by l.
330+
a_pd = c_m * (2 * ᶜstrain_rate_norm - ᶜlinear_buoygrad / ᶜPr) * sqrt_tke_pos
331+
332+
# Dissipation is modelled as c_d · k^{3/2} / l.
333+
# For the quadratic expression below, c_neg ≡ c_d · k^{3/2}.
334+
c_neg = c_d * tke_pos * sqrt_tke_pos
335+
336+
l_TKE = FT(0)
337+
# Solve for l_TKE in
338+
# a_pd · l_TKE − c_neg / l_TKE + ᶜtke_exch = 0
339+
# ⇒ a_pd · l_TKE² + ᶜtke_exch · l_TKE − c_neg = 0
340+
# yielding
341+
# l_TKE = (−ᶜtke_exch ± √(ᶜtke_exch² + 4 a_pd c_neg)) / (2 a_pd).
342+
if abs(a_pd) > eps_FT # If net of shear and buoyancy production (a_pd) is non-zero
343+
discriminant = ᶜtke_exch * ᶜtke_exch + 4 * a_pd * c_neg
344+
if discriminant >= FT(0) # Ensure real solution exists
345+
# Select the physically admissible (positive) root for l_TKE.
346+
# When a_pd > 0 (production exceeds dissipation) the root
347+
# (−ᶜtke_exch + √D) / (2 a_pd)
348+
# is positive. For a_pd < 0 the opposite root is required.
349+
l_TKE_sol1 = (-(ᶜtke_exch) + sqrt(discriminant)) / (2 * a_pd)
350+
# For a_pd < 0 (local destruction exceeds production) use
351+
# (−ᶜtke_exch − √D) / (2 a_pd).
352+
if a_pd > FT(0)
353+
l_TKE = l_TKE_sol1
354+
else # a_pd < FT(0)
355+
l_TKE = (-(ᶜtke_exch) - sqrt(discriminant)) / (2 * a_pd)
356+
end
357+
l_TKE = max(l_TKE, FT(0)) # Ensure it's non-negative
358+
end
359+
elseif abs(ᶜtke_exch) > eps_FT # If a_pd is zero, balance is between exchange and dissipation
360+
# ᶜtke_exch = c_neg / l_TKE => l_TKE = c_neg / ᶜtke_exch
361+
# Ensure division is safe and result is positive
362+
if ᶜtke_exch > eps_FT # Assuming positive exchange means TKE sink from env perspective
363+
l_TKE = c_neg / ᶜtke_exch # if c_neg is positive, l_TKE is positive
364+
elseif ᶜtke_exch < -eps_FT # Negative exchange means TKE source for env
365+
# -|ᶜtke_exch| = c_neg / l_TKE. If c_neg > 0, this implies l_TKE < 0, which is unphysical.
366+
# This case (a_pd=0, tke_exch < 0, c_neg > 0) implies TKE source and dissipation, no production.
367+
# Dissipation = Source. So, c_d * k_sqrt_k / l = -tke_exch. l = c_d * k_sqrt_k / (-tke_exch)
368+
l_TKE = c_neg / (-(ᶜtke_exch))
369+
end
370+
l_TKE = max(l_TKE, FT(0))
251371
end
252-
253-
# compute l_N - the effective static stability length scale.
254-
N_eff = sqrt(max(ᶜlinear_buoygrad, 0))
255-
if N_eff > 0.0
256-
l_N = min(sqrt(max(c_b * ᶜtke, 0)) / N_eff, l_z)
257-
else
258-
l_N = l_z
372+
# If a_pd = 0 and ᶜtke_exch = 0 (or c_neg = 0), l_TKE remains zero.
373+
374+
# --- l_N: Static-stability length scale (buoyancy limit), constrained by l_z ---
375+
N_eff_sq = max(ᶜlinear_buoygrad, FT(0)) # Use N^2 only if stable (N^2 > 0)
376+
l_N = l_z # Default to wall distance if not stably stratified or TKE is zero
377+
if N_eff_sq > eps_FT && tke_pos > eps_FT
378+
N_eff = sqrt(N_eff_sq)
379+
# l_N ~ sqrt(c_b * TKE) / N_eff
380+
l_N_physical = sqrt(c_b * tke_pos) / N_eff
381+
# Limit by distance from wall
382+
l_N = min(l_N_physical, l_z)
259383
end
384+
l_N = max(l_N, FT(0)) # Ensure non-negative
260385

261-
# compute l_smag - smagorinsky length scale
262-
l_smag = smagorinsky_lilly_length(
263-
CAP.c_smag(params),
264-
N_eff,
265-
ᶜdz,
266-
ᶜPr,
267-
ᶜstrain_rate_norm,
268-
)
269386

270-
# add limiters
271-
l = SA.SVector(
272-
l_N > l_z ? l_z : l_N,
273-
l_TKE > l_z ? l_z : l_TKE,
274-
l_W > l_z ? l_z : l_W,
275-
)
276-
# get soft minimum
277-
l_smin = lamb_smooth_minimum(l, smin_ub, smin_rm)
278-
l_limited = max(l_smag, min(l_smin, l_z))
387+
# --- Combine Scales ---
388+
389+
# Vector of *physical* scales (wall, TKE, stability)
390+
# These scales (l_W, l_TKE, l_N) are already ensured to be non-negative.
391+
# l_N is already limited by l_z. l_W and l_TKE are not necessarily.
392+
l_physical_scales = SA.SVector(l_W, l_TKE, l_N)
393+
394+
l_smin =
395+
blend_scales(scale_blending_method, l_physical_scales, turbconv_params)
396+
397+
# 1. Limit the combined physical scale by the distance from the wall.
398+
# This step mitigates excessive values of l_W or l_TKE.
399+
l_limited_phys_wall = min(l_smin, l_z)
400+
401+
# 2. Impose the grid-scale limit
402+
l_final = min(l_limited_phys_wall, ᶜdz)
403+
404+
# Final check: guarantee that the mixing length is at least a small positive
405+
# value. This prevents division-by-zero in
406+
# ε_d = C_d · TKE^{3/2} / l_mix
407+
# when TKE > 0. When TKE = 0, l_mix is inconsequential, but eps_FT
408+
# provides a conservative lower bound.
409+
l_final = max(l_final, eps_FT)
279410

280-
return MixingLength{FT}(l_limited, l_W, l_TKE, l_N)
411+
return MixingLength{FT}(l_final, l_W, l_TKE, l_N)
281412
end
282413

283414
"""

0 commit comments

Comments
 (0)