Skip to content

feat(edmfx): add horizontal component to the EDMFX SGS diffusive flux#4657

Open
haakon-e wants to merge 1 commit into
mainfrom
he/edmf-3d-diffusion
Open

feat(edmfx): add horizontal component to the EDMFX SGS diffusive flux#4657
haakon-e wants to merge 1 commit into
mainfrom
he/edmf-3d-diffusion

Conversation

@haakon-e

@haakon-e haakon-e commented Jul 9, 2026

Copy link
Copy Markdown
Member

Add the horizontal component of the previously vertical-only EDMFX environment SGS diffusive flux, enabled by the opt-in option edmfx_sgs_horizontal_diffusive_flux (default false). It is intended for high-resolution configurations (the gray-zone to LES regime), where the horizontal grid spacing is comparable to or smaller than the environment mixing length; on single columns the tendency is identically zero, so column results are unchanged.

The horizontal fluxes mirror the vertical EDMFX diffusive flux term by term: the same variables (total enthalpy with the dry-static-energy + water-enthalpy decomposition, total specific humidity with its moist-air-mass counterpart, grid-mean tracers with the α_vert_diff_tracer weighting, and TKE), the same TKE-based eddy diffusivity, and horizontal rather than vertical operators. The momentum tendency is the horizontal weak divergence of the SGS stress τ = -2 K_u S with the full strain rate, applied to the horizontal and vertical wind, with the corresponding TKE shear production from horizontal gradients. Anisotropy enters only through the mixing-length grid-scale limiter: ᶜmixing_length gains a grid_scale keyword defaulting to the resolvability filter scale, and the horizontal path passes the horizontal node scale, giving l_h = min(l_phys, Δx_h). The diffusivities are cached once per explicit cache update (ᶜK_u_h/ᶜK_h_h) and exposed by the new diagnostics lmixh, edth, and evuh. All horizontal terms are always explicit and never enter the implicit Jacobian.

With prognostic updrafts, the separate opt-in edmfx_horizontal_diffusion (default false, requires the flux option) applies the grid-mean horizontal specific tendencies uniformly to the updraft scalars, matching the uniform vertical treatment of edmfx_vertical_diffusion (#4720).

Combining the flux option with Smagorinsky-Lilly or AMD, which already apply horizontal SGS diffusion to the same fields, is rejected at model construction. The closure reuses the vertical flux's stability inputs (ᶜN²_eff, vertical shear); horizontal-specific stability formulations are out of scope. The formulation and its limits are documented in the new docs/src/edmf_horizontal_diffusion.md. Neither flag is set in any bundled configuration, and the grid_scale generalization does not change results on the default path, which the EDMF reproducibility jobs verify.

Unit tests cover the null states (horizontally uniform, single column), reference-based checks of each flux term, the updraft identities, the solver wiring, and the construction-time rejections.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📖 Docs preview for this PR: https://clima.github.io/ClimaAtmos.jl/previews/PR4657/

@haakon-e

haakon-e commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 2 times, most recently from 05ec4d9 to e5787ab Compare July 9, 2026 08:24
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 4 times, most recently from ee9a351 to ded13f0 Compare July 10, 2026 19:29
@haakon-e
haakon-e requested a review from costachris July 10, 2026 19:36
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 2 times, most recently from 231327f to e9244b3 Compare July 11, 2026 01:33

- `(; ᶜK_u, ᶜK_h)`.
"""
function ᶜeddy_diffusivities!(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The center-K consolidation looks like it collides with #4647, which moved everything to face-native set_face_diffusivities! (ᶠK_h/ᶠK_u/ᶠK_entr). (

NVTX.@annotate function set_face_diffusivities!(Y, p)
on main)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The face-native vertical pipeline from #4647 (set_face_diffusivities!, ᶠK_h/ᶠK_u/ᶠK_entr) is untouched, and the horizontal path gets its own set_horizontal_diffusivities!(Y, p) that caches center-based ᶜK_u_h/ᶜK_h_h once per explicit cache update.

These coefficients are center-fields because the horizontal operators themselves (gradₕ, wdivₕ) act at cell centers. the vertical fluxes stay on faces. Both are built from the same closure functions and stability inputs, so there's no duplicated physics.

@inline get_mixing_length_field(ml::MixingLength, ::Val{:l_grid}) = ml.l_grid

function ᶜmixing_length(Y, p, property::Val{P} = Val{:master}()) where {P}
function ᶜmixing_length(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this PR and #4647 edit ᶜmixing_length, but differently. Here you add grid_scale / buoyancy_gradient so the horizontal path can pass Δx and a centered buoyancy gradient. On main, it's written around resolvability_filter_scale and a split ᶜN²_eff / ᶜbuoygrad without keywards kwargs. So this can be consolidated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is resolved now. I kept a single grid_scale kwarg, but it now defaults to resolvability_filter_scale(axes(Y.c)), so the default path is exactly the formulation on main (there's a unit test asserting the identity). The horizontal path just passes the horizontal grid spacing in set_horizontal_diffusivities!(Y, p):

ᶜl_h = ᶜmixing_length(Y, p; grid_scale = Δx_h)

The buoyancy_gradient kwarg is dropped, so the horizontal path reads the same ᶜN²_eff and strain rate from the cache as the vertical closure.

return nothing
iscolumn(axes(Y.c)) && return nothing
(; params) = p
(; ᶜlinear_buoygrad) = p.precomputed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ᶜlinear_buoygrad got renamed to ᶜbuoygrad 

# Mixing length limited by the horizontal node spacing, with the centered
# buoyancy gradient as the stability input
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(Y.c)))
ᶜK = ᶜeddy_diffusivities!(

@costachris costachris Jul 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vertical path adds ᶠK_entr into all its diffusivities now, the horizontal terms should likely not include it after rebasing .

# buoyancy gradient as the stability input
Δx = Spaces.node_horizontal_length_scale(Spaces.horizontal_space(axes(Y.c)))
ᶜK = ᶜeddy_diffusivities!(
Y, p;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recompute the horizontal diffusivities here + in the updraft tendency + a few times in these diagnostics. Since main precomputes face K's anyway, could we precompute ᶜK_u_h/ᶜK_h_h once?

@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from 173f886 to ec636d3 Compare July 16, 2026 08:21
@haakon-e
haakon-e changed the base branch from main to he/diffusion-flux-helpers July 16, 2026 08:21
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from ec636d3 to f3aca88 Compare July 16, 2026 19:16
@haakon-e
haakon-e force-pushed the he/diffusion-flux-helpers branch 2 times, most recently from 2967b19 to 077de51 Compare July 16, 2026 19:28
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 3 times, most recently from 159e1aa to 0d46663 Compare July 16, 2026 19:43
@haakon-e
haakon-e force-pushed the he/diffusion-flux-helpers branch 2 times, most recently from c39237d to e9c164e Compare July 16, 2026 19:45
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 2 times, most recently from 87b971a to 2141622 Compare July 16, 2026 19:52
@haakon-e
haakon-e force-pushed the he/diffusion-flux-helpers branch from e9c164e to 4bfcd52 Compare July 16, 2026 19:52
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from 2141622 to 5e0c98b Compare July 16, 2026 20:07
@haakon-e
haakon-e force-pushed the he/diffusion-flux-helpers branch from 4bfcd52 to f5f88e2 Compare July 16, 2026 20:07
Base automatically changed from he/diffusion-flux-helpers to main July 16, 2026 20:13
@haakon-e haakon-e added the Launch Buildkite Add to launch buildkite run label Jul 17, 2026
@haakon-e

haakon-e commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

This is implemented. The grid-mean horizontal specific tendencies are applied uniformly to the updraft scalars, behind the edmfx_horizontal_diffusion option, which requires edmfx_sgs_horizontal_diffusive_flux and is rejected at construction if set alone.

@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from 5e0c98b to cd56d43 Compare July 17, 2026 17:05
@haakon-e
haakon-e changed the base branch from main to sa/unify_sgs_vertical_diffusion July 17, 2026 17:05
@sajjadazimi
sajjadazimi force-pushed the sa/unify_sgs_vertical_diffusion branch 2 times, most recently from 844918e to 5f7ca9d Compare July 22, 2026 18:59
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from 626a24c to 0decd0d Compare July 22, 2026 19:00
@haakon-e
haakon-e changed the base branch from sa/unify_sgs_vertical_diffusion to main July 22, 2026 19:00
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch 6 times, most recently from 86f1665 to 03fe0c8 Compare July 23, 2026 21:32

@haakon-e haakon-e left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few comments to assist review

@inline get_mixing_length_field(ml::MixingLength, ::Val{:l_grid}) = ml.l_grid

function ᶜmixing_length(Y, p, property::Val{P} = Val{:master}()) where {P}
function ᶜmixing_length(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is resolved now. I kept a single grid_scale kwarg, but it now defaults to resolvability_filter_scale(axes(Y.c)), so the default path is exactly the formulation on main (there's a unit test asserting the identity). The horizontal path just passes the horizontal grid spacing in set_horizontal_diffusivities!(Y, p):

ᶜl_h = ᶜmixing_length(Y, p; grid_scale = Δx_h)

The buoyancy_gradient kwarg is dropped, so the horizontal path reads the same ᶜN²_eff and strain rate from the cache as the vertical closure.


- `(; ᶜK_u, ᶜK_h)`.
"""
function ᶜeddy_diffusivities!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The face-native vertical pipeline from #4647 (set_face_diffusivities!, ᶠK_h/ᶠK_u/ᶠK_entr) is untouched, and the horizontal path gets its own set_horizontal_diffusivities!(Y, p) that caches center-based ᶜK_u_h/ᶜK_h_h once per explicit cache update.

These coefficients are center-fields because the horizontal operators themselves (gradₕ, wdivₕ) act at cell centers. the vertical fluxes stay on faces. Both are built from the same closure functions and stability inputs, so there's no duplicated physics.

@. Yₜ.c.uₕ -= C12(wdivₕ(ᶜρ * ᶜτ_h) / ᶜρ)
ᶠτ_h = compute_strain_rate_face_full!(p.scratch.ᶠtemp_UVWxUVW, ᶜu, ᶠu)
@. ᶠτ_h = -2 * ᶠinterp(ᶜK_u_h) * ᶠτ_h
@. Yₜ.f.u₃ -= C3(wdivₕ(ᶠρ * ᶠτ_h) / ᶠρ)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I include u₃ here because the horizontal flux of vertical momentum u'w' is a covariance. The vertical counterpart w'w' is a variance and is already in TKE / mass flux / pressure, which is why the vertical pathway doesn't diffuse u₃. Some additional explanation in the new docs page.

ice. The gradient is accumulated in two broadcasts; see the horizontal EDMF
diffusion documentation page.
"""
function ᶜtotal_enthalpy_gradientₕ!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mirrors ᶠtotal_enthalpy_gradientᵥ, but I materialize into scratch in two broadcasts. A single fused broadcast with four spectral gradients exceeds GPU kernel parameter limits on extruded spaces with warped topography (the lazy form also needs CliMA/ClimaCore.jl#2551). There's a regression test that the ρe_tot tendency equals the sum of the four constituent fluxes.

`ᶜK_h_h` of the TKE-based closure, with the mixing length limited by the
horizontal node spacing, `l_h = min(l_phys, Δx_h)`.
"""
function set_horizontal_diffusivities!(Y, p)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't include ᶠK_entr here, since my understanding is that it parameterizes vertical entrainment across an unresolved inversion face (K_e = γ w_e Δz) and has no horizontal analogue.

Add the horizontal counterpart of the EDMFX environment SGS diffusive flux,
enabled by the opt-in `edmfx_sgs_horizontal_diffusive_flux` option: scalar
fluxes (total enthalpy via the dry-static-energy + water-enthalpy
decomposition, specific humidity with its moist-air mass counterpart, and
grid-scale tracers with the sedimentation α weighting), turbulent TKE
transport with the horizontal shear production, and the momentum stress
τ = -2 K_u S with the full strain rate. The eddy diffusivity uses the
TKE-based closure with the mixing length limited by the horizontal node
spacing, cached in ᶜK_u_h/ᶜK_h_h with diagnostics lmixh, edth, and evuh.

With prognostic updrafts, the separate opt-in `edmfx_horizontal_diffusion`
option applies the grid-mean horizontal specific tendencies uniformly to the
updraft scalars (mse, q_tot with the ρa dry-mass correction, and SGS
tracers). The enthalpy gradient is materialized by ᶜtotal_enthalpy_gradientₕ!
into scratch before the weak divergence, since a single fused broadcast with
four spectral gradients exceeds GPU kernel parameter limits on
topography-warped extruded spaces.
@haakon-e
haakon-e force-pushed the he/edmf-3d-diffusion branch from ecf3379 to fa5abe6 Compare July 25, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Launch Buildkite Add to launch buildkite run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants