Skip to content

Commit ee9a351

Browse files
committed
feat(edmfx): add horizontal component to the EDMFX SGS diffusive flux
The EDMFX environment SGS diffusive flux was vertical-only. Add its horizontal component, enabled by the opt-in option `edmfx_sgs_horizontal_diffusive_flux` (default false), for high-resolution box runs. On single-column geometry the tendency returns immediately, so column results are unchanged by the option. The scalar fluxes mirror the vertical EDMFX diffusive flux: the same variables (total enthalpy, total specific humidity with its air-mass coupling, the environment SGS tracers scaled by `α_vert_diff_tracer`, and TKE) and the same TKE-based eddy diffusivity, with horizontal rather than vertical operators. The momentum tendency is the horizontal weak divergence of the SGS stress `τ = -2 K_u S`, with the full three-dimensional strain rate, on cell centers for the horizontal wind and on cell faces for the vertical wind, mirroring the Smagorinsky-Lilly stress split by divergence direction. The corresponding TKE shear production from horizontal gradients, `2 ρ K_u S_h : S_h` with `S_h` the strain rate built from horizontal gradients only, is positive definite; the production from vertical gradients and its stencil are unchanged. All terms are always explicit and never enter the implicit Jacobian; the vertical flux stays implicit when `implicit_diffusion` is set. The anisotropy follows the Smagorinsky-Lilly length-scale split: the eddy diffusivity is isotropic in TKE, so only the mixing length differs by direction through its grid-scale limiter (the cell thickness for the vertical flux, the horizontal node spacing for the horizontal flux). `mixing_length_lopez_gomez_2020` generalizes its limiter argument (`ᶜdz` to `grid_scale`); `ᶜmixing_length` gains `grid_scale` and `buoyancy_gradient` keywords (defaults: the vertical cell thickness and the centered `ᶜlinear_buoygrad`), and the horizontal call sites pass both explicitly. Add diagnostics for the horizontal closure fields (`lmixh`, `edth`, `evuh`). Combining the option with Smagorinsky-Lilly or anisotropic minimum dissipation, which already apply horizontal SGS diffusion, is rejected at model construction. Add tests: horizontally uniform states and single-column geometry produce no tendency, the moist air mass tendency matches the `ρq_tot` tendency bitwise, the grid-scale limit is directional and attains a binding cap, the TKE production is positive definite under uniform TKE, and incompatible closure combinations are rejected. Add docs/src/edmf_horizontal_diffusion.md.
1 parent 8f5bbc9 commit ee9a351

14 files changed

Lines changed: 546 additions & 11 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+
- [#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`.
78
- [#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.
89
- [#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).
910
- Document that the existing height-form pressure-gradient-force Jacobian blocks are the exact linearization of the Exner-form tendency `cp_d θ_v ∇Π` (stencils unchanged): the thermal-buoyancy and pressure-buoyancy terms cancel via the equation-of-state identity `δρ/ρ = (1-κ_d) δp/p - δθ_v/θ_v`, leaving the acoustic operator `∇δp/ρ` in every column perturbed at fixed density plus a single buoyancy term in the `ρ` column, so sound and gravity waves are both treated fully implicitly.

config/default_configs/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ edmfx_sgs_mass_flux:
455455
edmfx_sgs_diffusive_flux:
456456
help: "If set to true, it switches on EDMFX SGS diffusive flux. [`true`, `false` (default)]"
457457
value: false
458+
edmfx_sgs_horizontal_diffusive_flux:
459+
help: "If set to true, it switches on the horizontal component of the EDMFX SGS diffusive flux. Intended for high-resolution box runs. [`true`, `false` (default)]"
460+
value: false
458461
updraft_number:
459462
help: "Sets the number of updrafts for the EDMF scheme"
460463
value: 1

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ makedocs(;
5656
"Physics & Parameterizations" => [
5757
"Microphysics" => "microphysics.md",
5858
"EDMF: Prognostic Equations" => "edmf_equations.md",
59+
"EDMF: Horizontal Diffusion" => "edmf_horizontal_diffusion.md",
5960
"Gravity Wave Drag" => "gravity_wave.md",
6061
"Ocean Surface Albedo" => "surface_albedo.md",
6162
"Topography Representation" => "topography.md",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Horizontal EDMF diffusion
2+
3+
The EDMFX environment sub-grid scale (SGS) diffusive flux is, by default, vertical only: it parameterizes turbulent transport along the column with an eddy-diffusivity closure and applies the resulting tendency to the grid mean. The horizontal component adds the analogous horizontal down-gradient flux. It is enabled with the configuration option `edmfx_sgs_horizontal_diffusive_flux` (default `false`).
4+
5+
## Formulation
6+
7+
The horizontal flux uses the same TKE-based eddy diffusivity as the vertical flux,
8+
9+
```math
10+
K = c_m \, l \, \sqrt{e} \,/\, \mathrm{Pr}_t ,
11+
```
12+
13+
where ``l`` is the environment mixing length, ``e`` is the environment turbulent kinetic energy (TKE), ``c_m`` is the eddy-diffusivity coefficient, and ``\mathrm{Pr}_t`` the turbulent Prandtl number. The grid-mean tendencies are, on cell centers,
14+
15+
```math
16+
\partial_t (\rho e_\text{tot}) \mathrel{+}= \nabla_h \cdot \left[ \rho \, K_h \left( \nabla_h s_\text{d} + \sum_{\mu} (h_\mu + \Phi) \, \nabla_h q_\mu \right) \right],
17+
```
18+
19+
```math
20+
\partial_t (\rho \chi) \mathrel{+}= \nabla_h \cdot (\rho \, K_h \, \nabla_h \chi),
21+
\qquad
22+
\partial_t (\rho e) \mathrel{+}= \nabla_h \cdot (\rho \, K_u \, \nabla_h e),
23+
```
24+
25+
where ``\nabla_h`` and ``\nabla_h\cdot`` are the horizontal gradient and weak divergence, ``s_\text{d}`` is the dry static energy, ``h_\mu`` and ``q_\mu`` are the specific enthalpy and specific humidity of the water species ``\mu \in \{\text{vap}, \text{liq}, \text{ice}\}``, ``\Phi`` is the geopotential, ``K_u`` is the eddy viscosity and ``K_h = K_u/\mathrm{Pr}_t``. The total enthalpy uses the dry-static-energy + water-enthalpy decomposition of the vertical flux, which avoids the spurious enthalpy flux that diffusing ``h_\text{tot}`` directly would carry with the dry-air mass. The tracer ``\chi`` ranges over the total specific humidity and the environment SGS tracers — the microphysics species (cloud, precipitation, and, for two-moment schemes, number concentrations) and any passive tracers — i.e. the same set the vertical flux diffuses. Sedimenting microphysics species are scaled by the tracer diffusion factor ``\alpha`` (`α_vert_diff_tracer`); other tracers use the unscaled ``K_h``, matching the vertical flux. The total-specific-humidity flux additionally enters the air-mass tendency ``\partial_t \rho``; condensate and precipitation fluxes do not.
26+
27+
The momentum tendency is the horizontal weak divergence of the subgrid-scale stress ``\tau = -2 K_u \mathcal{S}``, with ``\mathcal{S}`` the full three-dimensional strain rate of the grid-mean velocity,
28+
29+
```math
30+
\partial_t u_h \mathrel{-}= \frac{1}{\rho} \nabla_h \cdot (\rho \, \tau),
31+
\qquad
32+
\partial_t u_3 \mathrel{-}= \frac{1}{\rho} \nabla_h \cdot (\rho \, \tau),
33+
```
34+
35+
evaluated on cell centers for the horizontal wind and on cell faces for the vertical wind, mirroring the Smagorinsky-Lilly stress split by divergence direction; the vertical stress divergence is handled by the vertical diffusion pathway.
36+
37+
The corresponding shear production of TKE uses the strain rate built from horizontal gradients only, ``\mathcal{S}_h``,
38+
39+
```math
40+
\partial_t (\rho e) \mathrel{+}= 2 \rho \, K_u \, \mathcal{S}_h : \mathcal{S}_h ,
41+
```
42+
43+
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.
44+
45+
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.
46+
47+
## Anisotropic length scale
48+
49+
The eddy diffusivity depends on direction only through the mixing length ``l``: the TKE ``e`` is isotropic. In the Lopez-Gomez et al. (2020) closure the mixing length is the smooth minimum of physical scales (wall distance, TKE production–dissipation balance, static stability), limited from above by a grid scale,
50+
51+
```math
52+
l = \min(l_\text{phys}, \, \Delta).
53+
```
54+
55+
Unlike the Smagorinsky-Lilly closure, whose length scale is purely geometric (``l = c_s \Delta``), the EDMF length is physically based; only its grid-scale limiter is geometric. The vertical and horizontal fluxes therefore share the same physical length but use different limiters — the cell thickness ``\Delta z`` for the vertical flux and the horizontal node spacing ``\Delta x`` for the horizontal flux:
56+
57+
```math
58+
l_v = \min(l_\text{phys}, \, \Delta z),
59+
\qquad
60+
l_h = \min(l_\text{phys}, \, \Delta x).
61+
```
62+
63+
This mirrors the Smagorinsky-Lilly horizontal/vertical split, in which the two length scales are ``c_s \Delta x`` and ``c_s \Delta z``. Two limits follow:
64+
65+
- where ``l_\text{phys} < \min(\Delta x, \Delta z)`` (for example small near-surface
66+
eddies), ``l_h = l_v = l_\text{phys}`` and the diffusivity is isotropic;
67+
- where ``l_\text{phys} > \max(\Delta x, \Delta z)``, the ratio ``l_h / l_v = \Delta x / \Delta z`` recovers the Smagorinsky grid anisotropy.
68+
69+
## When to enable it
70+
71+
Horizontal EDMF diffusion is intended for box configurations whose horizontal grid spacing is comparable to or smaller than the physical mixing length, so that the horizontal limiter ``\min(l_\text{phys}, \Delta x)`` binds. This is the high-resolution and gray-zone regime, where the horizontal grid is fine enough for an anisotropic SGS length scale to matter.
72+
73+
At coarse horizontal resolution (for example global runs with ``\Delta x`` of tens of kilometers) the horizontal limiter rarely binds, the horizontal diffusivity reduces to the isotropic environment value, and the term is typically negligible next to resolved horizontal transport. The option is off by default for that reason.
74+
75+
The horizontal term is a grid-mean closure applied independently of the vertical flux and of the diffusion mode: it is always explicit, since the horizontal operators are not part of the column implicit solve, while the vertical flux remains implicit when `implicit_diffusion` is set. The Smagorinsky-Lilly and anisotropic-minimum-dissipation closures already supply horizontal SGS diffusion of the same fields, so combining either with this option is rejected at model construction.
76+
77+
Because the term is explicit, it adds a horizontal diffusive stability limit on the timestep, ``\Delta t \lesssim \Delta x^2 / (2 K_h)``. At the fine horizontal resolutions where this closure is intended, the timestep is in practice already set by the explicit horizontal acoustic limit ``\Delta t \lesssim \Delta x / c_s``, which is the more restrictive of the two.
78+
79+
## Configuration
80+
81+
```yaml
82+
edmfx_sgs_horizontal_diffusive_flux: true # default: false
83+
```
84+
85+
## Diagnostics
86+
87+
Three diagnostic variables expose the horizontal closure fields:
88+
89+
- `lmixh`: the mixing length with the grid-scale limit set by the horizontal node
90+
spacing, ``l_h`` [m];
91+
- `edth`: the horizontal eddy diffusivity for scalars, ``K_h`` [m² s⁻¹];
92+
- `evuh`: the horizontal eddy viscosity, ``K_u`` [m² s⁻¹].
93+
94+
The vertical counterparts are `lmix`, `edt`, and `evu`.

src/config/model_getters.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,16 @@ function check_case_consistency(parsed_args)
664664
"Unknown `config = $(repr(config))`. Valid options are: $(join(valid_configs, ", "))."
665665
)
666666

667+
if parsed_args["edmfx_sgs_horizontal_diffusive_flux"] && (
668+
!isnothing(parsed_args["smagorinsky_lilly"]) || parsed_args["amd_les"]
669+
)
670+
error(
671+
"`edmfx_sgs_horizontal_diffusive_flux` cannot be combined with \
672+
`smagorinsky_lilly` or `amd_les`, which already apply horizontal \
673+
SGS diffusion to the same fields",
674+
)
675+
end
676+
667677
# ISDAC consistency: when initial_condition is ISDAC, surface/rad/external
668678
# forcing must all be set to the matching ISDAC variants. Subsidence,
669679
# scm_coriolis, and ls_adv are owned by the setup, not the YAML schema.
@@ -769,6 +779,7 @@ function AtmosTurbconv(config::AtmosConfig, params, ::Type{FT}) where {FT}
769779
detr_model = get_detrainment_model(pa),
770780
sgs_mass_flux = pa["edmfx_sgs_mass_flux"],
771781
sgs_diffusive_flux = pa["edmfx_sgs_diffusive_flux"],
782+
sgs_diffusive_flux_horizontal = pa["edmfx_sgs_horizontal_diffusive_flux"],
772783
nh_pressure = pa["edmfx_nh_pressure"],
773784
vertical_diffusion = pa["edmfx_vertical_diffusion"],
774785
filter = pa["edmfx_filter"],

src/diagnostics/edmfx_diagnostics.jl

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,87 @@ add_diagnostic_variable!(short_name = "evu", units = "m^2 s^-1",
851851
compute = compute_evu,
852852
)
853853

854+
###
855+
# Horizontal mixing length (3d)
856+
###
857+
compute_lmixh(state, cache, time) =
858+
compute_lmixh(state, cache, time, cache.atmos.turbconv_model)
859+
compute_lmixh(_, _, _, turbconv_model) =
860+
error_diagnostic_variable("lmixh", turbconv_model)
861+
862+
function compute_lmixh(state, cache, _, ::Union{EDOnlyEDMFX, PrognosticEDMFX})
863+
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(state.c)))
864+
return ᶜmixing_length(
865+
state, cache;
866+
grid_scale = Δx, buoyancy_gradient = cache.precomputed.ᶜlinear_buoygrad,
867+
)
868+
end
869+
870+
add_diagnostic_variable!(short_name = "lmixh", units = "m",
871+
long_name = "Environment Horizontal Mixing Length",
872+
comments = "Mixing length with the grid-scale limit set by the horizontal \
873+
node spacing rather than the vertical cell thickness",
874+
compute = compute_lmixh,
875+
)
876+
877+
###
878+
# Horizontal diffusivity of heat (3d)
879+
###
880+
compute_edth(state, cache, time) =
881+
compute_edth(state, cache, time, cache.atmos.turbconv_model)
882+
compute_edth(_, _, _, turbconv_model) =
883+
error_diagnostic_variable("edth", turbconv_model)
884+
885+
function compute_edth(state, cache, _, ::Union{EDOnlyEDMFX, PrognosticEDMFX})
886+
turbconv_params = CAP.turbconv_params(cache.params)
887+
(; ᶜlinear_buoygrad, ᶜstrain_rate_norm) = cache.precomputed
888+
(; params) = cache
889+
890+
ᶜtke = @. lazy(specific(state.c.ρtke, state.c.ρ))
891+
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(state.c)))
892+
ᶜmixing_length_field = ᶜmixing_length(
893+
state, cache; grid_scale = Δx, buoyancy_gradient = ᶜlinear_buoygrad,
894+
)
895+
ᶜK_u = @. lazy(eddy_viscosity(turbconv_params, ᶜtke, ᶜmixing_length_field))
896+
ᶜprandtl_nvec =
897+
@. lazy(turbulent_prandtl_number(params, ᶜlinear_buoygrad, ᶜstrain_rate_norm))
898+
return @. lazy(eddy_diffusivity(ᶜK_u, ᶜprandtl_nvec))
899+
end
900+
901+
add_diagnostic_variable!(short_name = "edth", units = "m^2 s^-1",
902+
long_name = "Horizontal Eddy Diffusivity Coefficient for Temperature",
903+
comments = "Horizontal diffusion coefficient for scalars in the EDMFX \
904+
horizontal SGS diffusive flux",
905+
compute = compute_edth,
906+
)
907+
908+
###
909+
# Horizontal diffusivity of momentum (3d)
910+
###
911+
compute_evuh(state, cache, time) =
912+
compute_evuh(state, cache, time, cache.atmos.turbconv_model)
913+
compute_evuh(_, _, _, turbconv_model) =
914+
error_diagnostic_variable("evuh", turbconv_model)
915+
916+
function compute_evuh(state, cache, _, ::Union{EDOnlyEDMFX, PrognosticEDMFX})
917+
turbconv_params = CAP.turbconv_params(cache.params)
918+
ᶜtke = @. lazy(specific(state.c.ρtke, state.c.ρ))
919+
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(state.c)))
920+
ᶜmixing_length_field = ᶜmixing_length(
921+
state, cache;
922+
grid_scale = Δx, buoyancy_gradient = cache.precomputed.ᶜlinear_buoygrad,
923+
)
924+
return @. lazy(eddy_viscosity(turbconv_params, ᶜtke, ᶜmixing_length_field))
925+
end
926+
927+
add_diagnostic_variable!(short_name = "evuh", units = "m^2 s^-1",
928+
long_name = "Horizontal Eddy Viscosity Coefficient for Momentum",
929+
comments = "Horizontal eddy viscosity from the TKE-based closure with the \
930+
mixing length limited by the horizontal node spacing; used to \
931+
diffuse TKE in the EDMFX horizontal SGS diffusive flux",
932+
compute = compute_evuh,
933+
)
934+
854935
###
855936
# Updraft passive gas tracer A (3d)
856937
###

src/prognostic_equations/eddy_diffusion_closures.jl

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ end
256256
ustar,
257257
ᶜz,
258258
z_sfc,
259-
ᶜdz,
259+
grid_scale,
260260
sfc_tke,
261261
ᶜN²_eff,
262262
ᶜtke,
@@ -273,7 +273,9 @@ where:
273273
- `ustar`: Friction velocity [m/s].
274274
- `ᶜz`: Cell center height [m].
275275
- `z_sfc`: Surface elevation [m].
276-
- `ᶜdz`: Cell vertical thickness [m].
276+
- `grid_scale`: Grid length scale used as the upper bound on the mixing length [m].
277+
The vertical cell thickness for vertical diffusion, the horizontal node spacing for
278+
horizontal diffusion.
277279
- `sfc_tke`: TKE near the surface (e.g., first cell center) [m^2/s^2].
278280
- `ᶜN²_eff`: Effective squared Brunt-Väisälä frequency [1/s^2].
279281
- `ᶜtke`: Turbulent kinetic energy at cell center [m^2/s^2].
@@ -299,7 +301,7 @@ function mixing_length_lopez_gomez_2020(
299301
ustar,
300302
ᶜz,
301303
z_sfc,
302-
ᶜdz,
304+
grid_scale,
303305
sfc_tke,
304306
ᶜN²_eff,
305307
ᶜtke,
@@ -400,7 +402,7 @@ function mixing_length_lopez_gomez_2020(
400402
l_limited_phys_wall = min(l_smin, l_z)
401403

402404
# 2. Impose the grid-scale limit (TODO: replace by volumetric grid scale)
403-
l_grid = ᶜdz # TODO include costant rescaling factor
405+
l_grid = grid_scale # TODO include constant rescaling factor
404406
l_final = min(l_limited_phys_wall, l_grid)
405407

406408
# Final check: guarantee that the mixing length is at least a small positive
@@ -421,20 +423,23 @@ end
421423
@inline get_mixing_length_field(ml::MixingLength, ::Val{:buoy}) = ml.buoy
422424
@inline get_mixing_length_field(ml::MixingLength, ::Val{:l_grid}) = ml.l_grid
423425

424-
function ᶜmixing_length(Y, p, property::Val{P} = Val{:master}()) where {P}
426+
function ᶜmixing_length(
427+
Y, p, property::Val{P} = Val{:master}();
428+
grid_scale = Fields.Δz_field(axes(Y.c)),
429+
buoyancy_gradient = p.precomputed.ᶜlinear_buoygrad,
430+
) where {P}
425431
(; params) = p
426432
(; ustar, obukhov_length) = p.precomputed.sfc_conditions
427-
(; ᶜlinear_buoygrad, ᶜstrain_rate_norm) = p.precomputed
433+
(; ᶜstrain_rate_norm) = p.precomputed
428434
ᶜz = Fields.coordinate_field(Y.c).z
429435
z_sfc = Fields.level(Fields.coordinate_field(Y.f).z, Fields.half)
430-
ᶜdz = Fields.Δz_field(axes(Y.c))
431436

432437
ᶜtke = @. lazy(specific(Y.c.ρtke, Y.c.ρ))
433438
sfc_tke = Fields.level(ᶜtke, 1)
434439

435440
ᶜprandtl_nvec = p.scratch.ᶜtemp_scalar_5
436441
@. ᶜprandtl_nvec =
437-
turbulent_prandtl_number(params, ᶜlinear_buoygrad, ᶜstrain_rate_norm)
442+
turbulent_prandtl_number(params, buoyancy_gradient, ᶜstrain_rate_norm)
438443

439444
# Extract sub-parameters before the lazy broadcast to avoid capturing
440445
# the full ClimaAtmosParameters struct (~4 KiB) in GPU kernel parameters.
@@ -450,9 +455,9 @@ function ᶜmixing_length(Y, p, property::Val{P} = Val{:master}()) where {P}
450455
ustar,
451456
ᶜz,
452457
z_sfc,
453-
ᶜdz,
458+
grid_scale,
454459
sfc_tke,
455-
ᶜlinear_buoygrad,
460+
buoyancy_gradient,
456461
ᶜtke,
457462
obukhov_length,
458463
ᶜstrain_rate_norm,

0 commit comments

Comments
 (0)