Skip to content

Commit 2f58f5e

Browse files
committed
feat(edmfx): add horizontal diffusion of prognostic EDMFX updrafts
Diffuse the prognostic updraft variables horizontally, mirroring `edmfx_vertical_diffusion` with the mixing length limited by the horizontal node spacing: moist static energy and total specific humidity at `K_h`, and the updraft SGS tracers at `α_vert_diff_tracer · K_h`, all weighted by the updraft density. The updraft dry-air mass is unchanged by the water flux, so `ρa` receives the counterpart tendency `ρa/(1 - q_tot)` times the `q_tot` tendency, mirroring the hyperdiffusion treatment. Always explicit; enabled by the opt-in `edmfx_horizontal_diffusion` config option (default false). Extend the tests: horizontally uniform updraft fields produce no tendency, perturbed fields produce one with the stated `ρa`/`q_tot` pairing, and the single-column tendency is identically zero.
1 parent ee9a351 commit 2f58f5e

8 files changed

Lines changed: 134 additions & 2 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ ClimaAtmos.jl Release Notes
44
main
55
----
66

7+
- [#4659](https://github.com/CliMA/ClimaAtmos.jl/pull/4659) ![][badge-✨feature/enhancement] Add horizontal diffusion of prognostic EDMFX updrafts (moist static energy, total specific humidity with its area-weighted density counterpart, and SGS tracers), enabled by the opt-in `edmfx_horizontal_diffusion` config option (default `false`).
78
- [#4657](https://github.com/CliMA/ClimaAtmos.jl/pull/4657) ![][badge-✨feature/enhancement] Add a horizontal component to the EDMFX SGS diffusive flux, enabled by the opt-in `edmfx_sgs_horizontal_diffusive_flux` config option (default `false`), for high-resolution box configurations: scalar and TKE fluxes, the momentum stress `τ = -2 K_u S` with the full strain rate, the corresponding TKE shear production from horizontal gradients, and diagnostics `lmixh`, `edth`, and `evuh`.
89
- [#4648](https://github.com/CliMA/ClimaAtmos.jl/pull/4648) ![][badge-🔥behavioralΔ] Carry the energy of sedimenting water at each subdomain's own value under `PrognosticEDMFX`. Each sedimenting species transports its specific internal, potential, and kinetic energy `e_int(T) + Φ + Kin(w, u)`; the grid-mean sedimentation energy flux is set equal to the sum of the updraft and environment fluxes, each the subdomain sedimentation mass flux times its specific energy (with `Φ` common to all subdomains and the environment mass flux taken as the grid-mean residual `ρqw - ρaʲqʲwʲ`). The updraft kinetic energy uses the updraft velocities; the environment kinetic energy uses the grid-mean terminal velocity, since the environment terminal velocity is not stored separately.
910
- [#4653](https://github.com/CliMA/ClimaAtmos.jl/pull/4653) ![][badge-🔥behavioralΔ] Improve the manual implicit Jacobian (the tendencies are unchanged; behavior changes only through the single-Newton-iteration implicit solve).

config/default_configs/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,9 @@ edmfx_nh_pressure:
434434
edmfx_vertical_diffusion:
435435
help: "If set to true, it switches on vertical diffusion of prognostic EDMFX updrafts. [`true`, `false` (default)]"
436436
value: false
437+
edmfx_horizontal_diffusion:
438+
help: "If set to true, it switches on horizontal diffusion of prognostic EDMFX updrafts, with the mixing length limited by the horizontal node spacing. Intended for high-resolution box runs. [`true`, `false` (default)]"
439+
value: false
437440
edmfx_entr_model:
438441
help: "EDMFX entrainment closure. [`nothing` (default), `PiGroups`, `Generalized`]"
439442
value: ~

docs/src/edmf_horizontal_diffusion.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ The corresponding shear production of TKE uses the strain rate built from horizo
4242

4343
which is positive definite; the production from vertical gradients and its stencil are unchanged. This mirrors the decoupled Smagorinsky-Lilly split, in which each directional norm is built from that direction's gradients.
4444

45+
## Updraft horizontal diffusion
46+
47+
The separate option `edmfx_horizontal_diffusion` (default `false`) switches on horizontal diffusion of the prognostic updraft variables, mirroring the vertical updraft diffusion of `edmfx_vertical_diffusion` with the horizontal mixing length: for each updraft,
48+
49+
```math
50+
\partial_t \chi^j \mathrel{+}= \frac{1}{\rho^j} \nabla_h \cdot (\rho^j \, K_h \, \nabla_h \chi^j),
51+
```
52+
53+
for the moist static energy, the total specific humidity, and the updraft SGS tracers (the latter scaled by ``\alpha``). The updraft dry-air mass is unchanged by the water flux, so the area-weighted density ``\rho a^j`` receives the counterpart tendency ``\rho a^j/(1 - q_\text{tot}^j)`` times the ``q_\text{tot}^j`` tendency, mirroring the hyperdiffusion treatment.
54+
4555
The horizontal flux mirrors the vertical EDMFX diffusive flux: the same variables, the same tracer set, and the same scaling, with horizontal rather than vertical operators.
4656

4757
## Anisotropic length scale

src/config/model_getters.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ function AtmosTurbconv(config::AtmosConfig, params, ::Type{FT}) where {FT}
782782
sgs_diffusive_flux_horizontal = pa["edmfx_sgs_horizontal_diffusive_flux"],
783783
nh_pressure = pa["edmfx_nh_pressure"],
784784
vertical_diffusion = pa["edmfx_vertical_diffusion"],
785+
horizontal_diffusion = pa["edmfx_horizontal_diffusion"],
785786
filter = pa["edmfx_filter"],
786787
scale_blending_method,
787788
)

src/prognostic_equations/mass_flux_closures.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,65 @@ function edmfx_vertical_diffusion_tendency!(
236236
end
237237
end
238238

239+
edmfx_horizontal_diffusion_tendency!(Yₜ, Y, p, t, turbconv_model) = nothing
240+
241+
function edmfx_horizontal_diffusion_tendency!(
242+
Yₜ, Y, p, t, turbconv_model::PrognosticEDMFX,
243+
)
244+
p.atmos.edmfx_model.horizontal_diffusion isa Val{true} || return nothing
245+
iscolumn(axes(Y.c)) && return nothing
246+
(; params) = p
247+
(; ᶜρʲs, ᶜlinear_buoygrad, ᶜstrain_rate_norm) = p.precomputed
248+
turbconv_params = CAP.turbconv_params(params)
249+
n = n_mass_flux_subdomains(turbconv_model)
250+
251+
ᶜtke = @. lazy(specific(Y.c.ρtke, Y.c.ρ))
252+
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(Y.c)))
253+
ᶜmixing_length_h = p.scratch.ᶜtemp_scalar
254+
ᶜmixing_length_h .= ᶜmixing_length(
255+
Y, p; grid_scale = Δx, buoyancy_gradient = ᶜlinear_buoygrad,
256+
)
257+
ᶜK_u_h = @. lazy(eddy_viscosity(turbconv_params, ᶜtke, ᶜmixing_length_h))
258+
ᶜprandtl_nvec =
259+
@. lazy(turbulent_prandtl_number(params, ᶜlinear_buoygrad, ᶜstrain_rate_norm))
260+
ᶜK_h_h = p.scratch.ᶜtemp_scalar_2
261+
@. ᶜK_h_h = eddy_diffusivity(ᶜK_u_h, ᶜprandtl_nvec)
262+
263+
ᶜq_totʲₜ_diffusion = p.scratch.ᶜtemp_scalar_3
264+
for j in 1:n
265+
ᶜρʲ = ᶜρʲs.:($j)
266+
ᶜmseʲ = Y.c.sgsʲs.:($j).mse
267+
ᶜq_totʲ = Y.c.sgsʲs.:($j).q_tot
268+
@. Yₜ.c.sgsʲs.:($$j).mse +=
269+
wdivₕ(ᶜρʲ * ᶜK_h_h * gradₕ(ᶜmseʲ)) / ᶜρʲ
270+
@. ᶜq_totʲₜ_diffusion =
271+
wdivₕ(ᶜρʲ * ᶜK_h_h * gradₕ(ᶜq_totʲ)) / ᶜρʲ
272+
@. Yₜ.c.sgsʲs.:($$j).q_tot += ᶜq_totʲₜ_diffusion
273+
# The updraft dry-air mass is unchanged by the water flux, so the
274+
# area-weighted density responds to the change in total specific
275+
# humidity, mirroring the hyperdiffusion treatment.
276+
@. Yₜ.c.sgsʲs.:($$j).ρa +=
277+
Y.c.sgsʲs.:($$j).ρa / (1 - ᶜq_totʲ) * ᶜq_totʲₜ_diffusion
278+
end
279+
280+
if !isempty(sgs_tracer_names(Y))
281+
α_diff_microphysics = CAP.α_vert_diff_tracer(params)
282+
ᶜρʲ = ᶜρʲs.:(1)
283+
# Sedimenting microphysics species are diffused with
284+
# α_vert_diff_tracer * K_h, passive tracers with the unscaled K_h,
285+
# matching the vertical updraft diffusion.
286+
for χ_name in sgs_tracer_names(Y)
287+
α =
288+
χ_name in sgs_sedimenting_tracer_candidates ?
289+
α_diff_microphysics : one(α_diff_microphysics)
290+
ᶜχʲ = MatrixFields.get_field(Y.c.sgsʲs.:(1), χ_name)
291+
ᶜχʲₜ = MatrixFields.get_field(Yₜ.c.sgsʲs.:(1), χ_name)
292+
@. ᶜχʲₜ += wdivₕ(ᶜρʲ * ᶜK_h_h * α * gradₕ(ᶜχʲ)) / ᶜρʲ
293+
end
294+
end
295+
return nothing
296+
end
297+
239298
# Private helper: clips grid-mean condensate tracers to non-negative values and
240299
# rescales the condensate sum so it cannot exceed the available total moisture.
241300
function enforce_grid_mean_microphysics_constraints!(Y, p, t)

src/prognostic_equations/remaining_tendency.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ NVTX.@annotate function additional_tendency!(Yₜ, Y, p, t)
337337
horizontal_constant_diffusion_tendency!(Yₜ, Y, p, t, chd)
338338

339339
edmfx_sgs_horizontal_diffusive_flux_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
340+
edmfx_horizontal_diffusion_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
340341

341342
# Optional tendency to bring negative small tracers back from negative
342343
# at the cost of water vapor.

src/types.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,8 @@ const ValTF = Union{Val{true}, Val{false}}
811811

812812
struct EDMFXModel{
813813
EEM, EDM,
814-
ESMF <: ValTF, ESDF <: ValTF, ESDFH <: ValTF, ENP <: ValTF, EVD <: ValTF, EF <: ValTF,
814+
ESMF <: ValTF, ESDF <: ValTF, ESDFH <: ValTF, ENP <: ValTF, EVD <: ValTF,
815+
EHD <: ValTF, EF <: ValTF,
815816
SBM <: AbstractScaleBlendingMethod,
816817
}
817818
entr_model::EEM
@@ -821,6 +822,7 @@ struct EDMFXModel{
821822
sgs_diffusive_flux_horizontal::ESDFH
822823
nh_pressure::ENP
823824
vertical_diffusion::EVD
825+
horizontal_diffusion::EHD
824826
filter::EF
825827
scale_blending_method::SBM
826828
end
@@ -836,6 +838,7 @@ function EDMFXModel(;
836838
sgs_diffusive_flux_horizontal::Union{Bool, ValTF} = false,
837839
nh_pressure::Union{Bool, ValTF} = false,
838840
vertical_diffusion::Union{Bool, ValTF} = false,
841+
horizontal_diffusion::Union{Bool, ValTF} = false,
839842
filter::Union{Bool, ValTF} = false,
840843
scale_blending_method,
841844
kwargs...,
@@ -851,6 +854,7 @@ function EDMFXModel(;
851854
parse_val_tf(sgs_diffusive_flux_horizontal),
852855
parse_val_tf(nh_pressure),
853856
parse_val_tf(vertical_diffusion),
857+
parse_val_tf(horizontal_diffusion),
854858
parse_val_tf(filter),
855859
scale_blending_method,
856860
)

test/prognostic_equations/edmfx_horizontal_diffusion_tests.jl

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,52 @@ box_config_dict(; extra...) = bomex_edmfx_config_dict(;
163163
@test parent(ᶜl_buoygrad) == parent(ᶜl_default)
164164
end
165165

166+
@testset "EDMFX updraft horizontal diffusion (Bomex box)" begin
167+
config = CA.AtmosConfig(
168+
box_config_dict(; edmfx_horizontal_diffusion = true);
169+
job_id = "edmfx_updraft_horizontal_diffusion_box_test",
170+
)
171+
(; Y, p, simulation) = generate_test_simulation(config)
172+
t = simulation.integrator.t
173+
FT = eltype(Y)
174+
175+
@. Y.c.ρtke = FT(0.5) * Y.c.ρ
176+
# A nonzero updraft area makes the ρa counterpart nonvacuous.
177+
@. Y.c.sgsʲs.:(1).ρa = FT(0.1) * Y.c.ρ
178+
179+
up_row_max(Yₜ) = (
180+
maximum(abs, parent(Yₜ.c.sgsʲs.:(1).mse)),
181+
maximum(abs, parent(Yₜ.c.sgsʲs.:(1).q_tot)),
182+
maximum(abs, parent(Yₜ.c.sgsʲs.:(1).ρa)),
183+
)
184+
Yₜ = similar(Y)
185+
Yₜ .= zero(eltype(Yₜ))
186+
CA.edmfx_horizontal_diffusion_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
187+
uniform_mse, uniform_q_tot, uniform_ρa = up_row_max(Yₜ)
188+
189+
ᶜx = Fields.coordinate_field(Y.c).x
190+
ᶜpert = @. 1 + FT(0.1) * sin(FT(2π) * ᶜx / FT(6400))
191+
@. Y.c.sgsʲs.:(1).q_tot *= ᶜpert
192+
@. Y.c.sgsʲs.:(1).mse *= 1 + FT(1e-3) * sin(FT(2π) * ᶜx / FT(6400))
193+
194+
Yₜ .= zero(eltype(Yₜ))
195+
CA.edmfx_horizontal_diffusion_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
196+
perturbed_mse, perturbed_q_tot, perturbed_ρa = up_row_max(Yₜ)
197+
@test perturbed_mse > 0
198+
@test perturbed_q_tot > 0
199+
@test perturbed_ρa > 0
200+
@test uniform_mse <= FT(1e-10) * perturbed_mse
201+
@test uniform_q_tot <= FT(1e-10) * perturbed_q_tot
202+
@test uniform_ρa <= FT(1e-10) * perturbed_ρa
203+
204+
# The ρa tendency is the q_tot tendency scaled by ρa / (1 - q_tot).
205+
ᶜρa_expected = similar(Y.c.ρ)
206+
@. ᶜρa_expected =
207+
Y.c.sgsʲs.:(1).ρa / (1 - Y.c.sgsʲs.:(1).q_tot) *
208+
Yₜ.c.sgsʲs.:(1).q_tot
209+
@test parent(Yₜ.c.sgsʲs.:(1).ρa) parent(ᶜρa_expected) rtol = FT(1e-12)
210+
end
211+
166212
@testset "EDMFX horizontal diffusive flux vanishes on a column" begin
167213
config = CA.AtmosConfig(
168214
bomex_edmfx_config_dict(;
@@ -172,6 +218,7 @@ end
172218
dt = "120secs",
173219
t_end = "30mins",
174220
z_elem = 30,
221+
edmfx_horizontal_diffusion = true,
175222
);
176223
job_id = "edmfx_horizontal_diffusion_column_test",
177224
)
@@ -182,7 +229,7 @@ end
182229
@. Y.c.ρtke = FT(0.5) * Y.c.ρ
183230

184231
# Spectral operators return exact zeros on a column, so all existing
185-
# single-column results are unaffected by the option.
232+
# single-column results are unaffected by the options.
186233
Yₜ = similar(Y)
187234
Yₜ .= zero(eltype(Yₜ))
188235
CA.edmfx_sgs_horizontal_diffusive_flux_tendency!(
@@ -193,6 +240,12 @@ end
193240
end
194241
@test maximum(abs, parent(Yₜ.c.uₕ)) == 0
195242
@test maximum(abs, parent(Yₜ.f.u₃)) == 0
243+
244+
Yₜ .= zero(eltype(Yₜ))
245+
CA.edmfx_horizontal_diffusion_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
246+
@test maximum(abs, parent(Yₜ.c.sgsʲs.:(1).mse)) == 0
247+
@test maximum(abs, parent(Yₜ.c.sgsʲs.:(1).q_tot)) == 0
248+
@test maximum(abs, parent(Yₜ.c.sgsʲs.:(1).ρa)) == 0
196249
end
197250

198251
@testset "Incompatible horizontal SGS closures are rejected" begin

0 commit comments

Comments
 (0)