Skip to content

Commit 5f7ca9d

Browse files
committed
unify SGS vertical diffusion with grid mean
1 parent ff91caf commit 5f7ca9d

5 files changed

Lines changed: 85 additions & 103 deletions

File tree

reproducibility_tests/ref_counter.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
387
1+
388
22

33
# **README**
44
#
@@ -32,6 +32,11 @@
3232
# 3) (optional) leave a link to the buildkite run that prompted this ref counter bump.
3333

3434
#=
35+
388
36+
- Unify SGS vertical diffusion with grid mean. Each PrognosticEDMFX
37+
subdomain inherits the grid-mean specific tendency for mseⱼ, q_totⱼ,
38+
and matching auto-discovered SGS tracers.
39+
3540
387
3641
- Change some EDMF parameters
3742

src/prognostic_equations/edmfx_sgs_flux.jl

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ function edmfx_sgs_diffusive_flux_tendency!(
226226
turbconv_params = CAP.turbconv_params(params)
227227
(; ᶜu) = p.precomputed
228228
ᶠgradᵥ = Operators.GradientC2F()
229+
n = n_mass_flux_subdomains(turbconv_model)
230+
# The SGS-updraft branches below apply the same specific tendency the grid
231+
# mean receives to each subdomain scalar (uniform vertical diffusion across
232+
# the grid box). Gated on `edmfx_model.vertical_diffusion` so configs can
233+
# opt in/out just like the old subdomain-native diffusion did.
234+
apply_sgs_updraft =
235+
turbconv_model isa PrognosticEDMFX &&
236+
p.atmos.edmfx_model.vertical_diffusion isa Val{true}
229237

230238
if p.atmos.edmfx_model.sgs_diffusive_flux isa Val{true}
231239

@@ -272,7 +280,13 @@ function edmfx_sgs_diffusive_flux_tendency!(
272280
ᶜq_vap = @. lazy(
273281
TD.vapor_specific_humidity(ᶜq_tot_nonneg, ᶜq_liq, ᶜq_ice),
274282
)
275-
@. Yₜ.c.ρe_tot -= ᶜdivᵥ_ρe_tot(
283+
# Materialize the total-enthalpy flux divergence once so it can be
284+
# applied to the grid-mean ρe_tot and to each subdomain mse using
285+
# the same specific-enthalpy tendency dh_gm/dt = dρe_tot/dt / ρ.
286+
# Uniform vertical diffusion in the grid box: every subdomain feels
287+
# the grid-mean specific tendency.
288+
ᶜρe_totₜ_diffusion = p.scratch.ᶜtemp_scalar_2
289+
@. ᶜρe_totₜ_diffusion = ᶜdivᵥ_ρe_tot(
276290
-(
277291
ᶠρaK_h * (
278292
ᶠgradᵥ(TD.dry_static_energy(thermo_params, ᶜT, ᶜΦ)) +
@@ -285,6 +299,12 @@ function edmfx_sgs_diffusive_flux_tendency!(
285299
)
286300
),
287301
)
302+
@. Yₜ.c.ρe_tot -= ᶜρe_totₜ_diffusion
303+
if apply_sgs_updraft
304+
for j in 1:n
305+
@. Yₜ.c.sgsʲs.:($$j).mse -= ᶜρe_totₜ_diffusion / Y.c.ρ
306+
end
307+
end
288308

289309
if use_prognostic_tke(turbconv_model)
290310
(; ρtke_flux) = p.precomputed
@@ -320,6 +340,12 @@ function edmfx_sgs_diffusive_flux_tendency!(
320340
ᶜdivᵥ_ρq_tot(-(ᶠρaK_h * ᶠgradᵥ(specific(Y.c.ρq_tot, Y.c.ρ))))
321341
@. Yₜ.c.ρq_tot -= ᶜρχₜ_diffusion
322342
@. Yₜ.c.ρ -= ᶜρχₜ_diffusion # Effect of moisture diffusion on (moist) air mass
343+
if apply_sgs_updraft
344+
for j in 1:n
345+
@. Yₜ.c.sgsʲs.:($$j).q_tot -= ᶜρχₜ_diffusion / Y.c.ρ
346+
end
347+
# The corresponding ρaⱼ dry-mass correction is deliberately neglected.
348+
end
323349
end
324350

325351
α_vert_diff_microphysics = CAP.α_vert_diff_tracer(params)
@@ -356,6 +382,18 @@ function edmfx_sgs_diffusive_flux_tendency!(
356382
),
357383
)
358384
@. ᶜρχₜ -= ᶜρχₜ_diffusion
385+
# Uniform vertical diffusion: apply the same specific tendency to
386+
# the matching subdomain field (e.g. ρq_lcl → q_lcl in updrafts).
387+
if apply_sgs_updraft
388+
χ_name = specific_tracer_name(ρχ_name)
389+
for j in 1:n
390+
if MatrixFields.has_field(Y.c.sgsʲs.:($j), χ_name)
391+
ᶜχⱼₜ =
392+
MatrixFields.get_field(Yₜ.c.sgsʲs.:($j), χ_name)
393+
@. ᶜχⱼₜ -= ᶜρχₜ_diffusion / Y.c.ρ
394+
end
395+
end
396+
end
359397
end
360398

361399
# Momentum diffusion

src/prognostic_equations/implicit/implicit_tendency.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ NVTX.@annotate function implicit_tendency!(Yₜ, Y, p, t)
5454

5555
edmfx_sgs_mass_flux_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
5656

57-
edmfx_vertical_diffusion_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
58-
5957
edmfx_boundary_condition_tendency!(Yₜ, Y, p, t, p.atmos.turbconv_model)
6058

6159
# NOTE: All ρa tendencies should be applied before calling this function

src/prognostic_equations/implicit/manual_sparse_jacobian.jl

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -794,9 +794,9 @@ function update_sedimentation_jacobian!(matrix, Y, p, dtγ)
794794
end
795795

796796
# Center eddy diffusivity and viscosity for the non-EDMF implicit diffusion
797-
# Jacobians. May write to ᶜtemp_scalar_3. AbstractEDMF configurations return
798-
# nothing: their grid-mean and updraft diffusion Jacobians both use the
799-
# face-native ᶠK_h/ᶠK_u/ᶠK_entr from set_face_diffusivities! (see
797+
# Jacobian. May write to ᶜtemp_scalar_3. AbstractEDMF configurations return
798+
# nothing: their grid-mean diffusion Jacobian uses the face-native
799+
# ᶠK_h/ᶠK_u/ᶠK_entr from set_face_diffusivities! (see
800800
# update_diffusion_jacobian! and update_sgs_diffusion_jacobian!).
801801
function eddy_diffusivity_coefficients!(Y, p)
802802
(; vertical_diffusion, smagorinsky_lilly) = p.atmos
@@ -1236,63 +1236,75 @@ end
12361236
update_sgs_diffusion_jacobian!(matrix, Y, p, dtγ, diffusion_flag)
12371237
12381238
Updates the Jacobian blocks for implicit vertical diffusion of the updraft
1239-
scalars. No-op when diffusion is treated explicitly.
1240-
1241-
Reuses `ᶜdiffusion_h_matrix` as scratch space, so it must run after
1242-
`update_diffusion_jacobian!`.
1239+
scalars under the unified grid-mean tendency. Under the "uniform diffusion
1240+
in the grid box" tendency in `edmfx_sgs_diffusive_flux_tendency!`, each
1241+
subdomain scalar receives the grid-mean specific tendency which strictly
1242+
depends on grid-mean state, not on χⱼ. The exact linearization would
1243+
put off-diagonal (subdomain-column, grid-mean-column) entries into the
1244+
Jacobian, which the current triangular structure of the ClimaAtmos Jacobian
1245+
does not accommodate. We instead use a `χ ≈ χⱼ` self-linearization (frozen-
1246+
coefficient / diagonal approximation): pretend `ᶜρχₜ_diffusion` depends on χⱼ
1247+
via the same divergence-of-K-gradient operator applied to χⱼ. The residual
1248+
still uses the true (grid-mean-derived) tendency; only the Jacobian is
1249+
approximated. This gives implicit stability for the subdomain scalar
1250+
diffusion without introducing off-triangular Jacobian entries.
12431251
"""
12441252
function update_sgs_diffusion_jacobian!(matrix, Y, p, dtγ, diffusion_flag)
12451253
p.atmos.turbconv_model isa PrognosticEDMFX || return nothing
12461254
use_derivative(diffusion_flag) || return nothing
12471255
# Mirror the gate of the tendency this linearizes
1248-
# (edmfx_vertical_diffusion_tendency!): without it, the updraft scalar
1249-
# diagonals would carry diffusion terms that have no tendency counterpart.
1250-
p.atmos.edmfx_model.vertical_diffusion isa Val{true} || return nothing
1256+
# (`edmfx_sgs_diffusive_flux_tendency!` inside the sgs_diffusive_flux
1257+
# branch): without it, the updraft scalar diagonals would carry
1258+
# diffusion terms that have no tendency counterpart.
1259+
p.atmos.edmfx_model.sgs_diffusive_flux isa Val{true} || return nothing
12511260
(; params) = p
1252-
(; ᶜρʲs) = p.precomputed
12531261
(; ᶜdiffusion_h_matrix) = p.scratch
1262+
ᶜρ = Y.c.ρ
12541263

1255-
α_vert_diff_microphysics = CAP.α_vert_diff_tracer(params)
1256-
# Face-native ᶠK_h, consistent with edmfx_vertical_diffusion_tendency!
1257-
# (ᶠK_entr is deliberately excluded there; see that function).
1258-
(; ᶠK_h) = p.precomputed
1259-
@. ᶜdiffusion_h_matrix =
1260-
ᶜadvdivᵥ_matrix() DiagonalMatrixRow(ᶠinterp(ᶜρʲs.:(1)) * ᶠK_h)
1261-
ᶠgradᵥ_matrix()
1262-
1264+
# mseⱼ and q_totⱼ diagonals: same operator, no tracer factor. Uses the
1265+
# full ρ(K_h + K_entr) diffusion matrix.
12631266
∂ᶜmseʲ_err_∂ᶜmseʲ =
12641267
matrix[@name(c.sgsʲs.:(1).mse), @name(c.sgsʲs.:(1).mse)]
12651268
∂ᶜq_totʲ_err_∂ᶜq_totʲ =
12661269
matrix[@name(c.sgsʲs.:(1).q_tot), @name(c.sgsʲs.:(1).q_tot)]
12671270
@. ∂ᶜmseʲ_err_∂ᶜmseʲ +=
1268-
dtγ * DiagonalMatrixRow(1 / ᶜρʲs.:(1)) ᶜdiffusion_h_matrix
1271+
dtγ * DiagonalMatrixRow(1 / ᶜρ) ᶜdiffusion_h_matrix
12691272
@. ∂ᶜq_totʲ_err_∂ᶜq_totʲ +=
1270-
dtγ * DiagonalMatrixRow(1 / ᶜρʲs.:(1)) ᶜdiffusion_h_matrix
1273+
dtγ * DiagonalMatrixRow(1 / ᶜρ) ᶜdiffusion_h_matrix
12711274

1275+
# Auto-discovered SGS tracers. Sedimenting microphysics species are
1276+
# diffused with `α·K_h + K_entr` (matching the tendency); passive
1277+
# tracers keep the full `K_h + K_entr` — so the passive branch reuses
1278+
# `ᶜdiffusion_h_matrix` directly and the sedimenting branch rebuilds the
1279+
# α-scaled matrix in scratch.
12721280
if p.atmos.microphysics_model isa Union{
12731281
NonEquilibriumMicrophysics1M,
12741282
NonEquilibriumMicrophysics2M,
12751283
}
1284+
α_vert_diff_microphysics = CAP.α_vert_diff_tracer(params)
1285+
(; ᶠK_h, ᶠK_entr) = p.precomputed
1286+
ᶜsgs_tracer_diffusion_matrix = p.scratch.ᶜtridiagonal_matrix_scalar
1287+
@. ᶜsgs_tracer_diffusion_matrix =
1288+
ᶜadvdivᵥ_matrix() DiagonalMatrixRow(
1289+
ᶠinterp(ᶜρ) * (α_vert_diff_microphysics * ᶠK_h + ᶠK_entr),
1290+
) ᶠgradᵥ_matrix()
12761291
MatrixFields.unrolled_foreach(
12771292
sedimenting_sgs_tracer_names(Y),
12781293
) do χ_name
12791294
χ_state_name = sgs_state_name(χ_name)
12801295
∂ᶜχʲ_err_∂ᶜχʲ = matrix[χ_state_name, χ_state_name]
12811296
@. ∂ᶜχʲ_err_∂ᶜχʲ +=
1282-
dtγ * α_vert_diff_microphysics *
1283-
DiagonalMatrixRow(1 / ᶜρʲs.:(1))
1284-
ᶜdiffusion_h_matrix
1297+
dtγ * DiagonalMatrixRow(1 / ᶜρ)
1298+
ᶜsgs_tracer_diffusion_matrix
12851299
end
12861300
end
12871301

1288-
# Passive SGS tracers are diffused with the unscaled K_h (see
1289-
# edmfx_vertical_diffusion_tendency!); their diagonals are initialized by
1290-
# update_sgs_advection_jacobian!, so the diffusion term is accumulated.
1302+
# Passive SGS tracers: unscaled ρ(K_h + K_entr), same as mseⱼ / q_totⱼ.
12911303
MatrixFields.unrolled_foreach(passive_sgs_tracer_names(Y)) do χ_name
12921304
χ_state_name = sgs_state_name(χ_name)
12931305
∂ᶜχʲ_err_∂ᶜχʲ = matrix[χ_state_name, χ_state_name]
12941306
@. ∂ᶜχʲ_err_∂ᶜχʲ +=
1295-
dtγ * DiagonalMatrixRow(1 / ᶜρʲs.:(1)) ᶜdiffusion_h_matrix
1307+
dtγ * DiagonalMatrixRow(1 / ᶜρ) ᶜdiffusion_h_matrix
12961308
end
12971309
return nothing
12981310
end

src/prognostic_equations/mass_flux_closures.jl

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -162,77 +162,6 @@ layers (`⟨w'b'⟩_s ≤ 0`).
162162
return a_s * ρ * w_star
163163
end
164164

165-
edmfx_vertical_diffusion_tendency!(Yₜ, Y, p, t, turbconv_model) = nothing
166-
167-
function edmfx_vertical_diffusion_tendency!(
168-
Yₜ,
169-
Y,
170-
p,
171-
t,
172-
turbconv_model::PrognosticEDMFX,
173-
)
174-
if p.atmos.edmfx_model.vertical_diffusion isa Val{true}
175-
(; params) = p
176-
(; ᶜρʲs) = p.precomputed
177-
FT = eltype(p.params)
178-
n = n_mass_flux_subdomains(turbconv_model)
179-
ᶜdivᵥ_mse = Operators.DivergenceF2C(
180-
top = Operators.SetValue(C3(0)),
181-
bottom = Operators.SetValue(C3(0)),
182-
)
183-
ᶜdivᵥ_q_tot = Operators.DivergenceF2C(
184-
top = Operators.SetValue(C3(0)),
185-
bottom = Operators.SetValue(C3(0)),
186-
)
187-
188-
# Updraft internal diffusion uses the same face-native environment
189-
# diffusivity ᶠK_h as the grid-mean diffusion (see
190-
# set_face_diffusivities! and edmfx_sgs_diffusive_flux_tendency!):
191-
# the SGS turbulence that stirs updraft interiors is the same
192-
# environment turbulence, and the face-native, interface-aware
193-
# evaluation collapses the flux at faces bordering quiescent,
194-
# strongly stratified air without interpolation. ᶠK_entr is
195-
# deliberately not added here: it represents grid-mean interfacial
196-
# entrainment, which for the updrafts is carried by the
197-
# entrainment/detrainment closures.
198-
(; ᶠK_h) = p.precomputed
199-
for j in 1:n
200-
ᶜρʲ = ᶜρʲs.:($j)
201-
ᶜmseʲ = Y.c.sgsʲs.:($j).mse
202-
ᶜq_totʲ = Y.c.sgsʲs.:($j).q_tot
203-
# Note: For this and other diffusive tendencies, we should use ρaʲ instead of ρʲ,
204-
# but it causes stability issues when ρaʲ is small
205-
@. Yₜ.c.sgsʲs.:($$j).mse -=
206-
ᶜdivᵥ_mse(-(ᶠinterp(ᶜρʲ) * ᶠK_h * ᶠgradᵥ(ᶜmseʲ))) / ᶜρʲ
207-
@. Yₜ.c.sgsʲs.:($$j).q_tot -=
208-
ᶜdivᵥ_q_tot(-(ᶠinterp(ᶜρʲ) * ᶠK_h * ᶠgradᵥ(ᶜq_totʲ))) / ᶜρʲ
209-
end
210-
211-
if !isempty(sgs_tracer_names(Y))
212-
α_vert_diff_microphysics = CAP.α_vert_diff_tracer(params)
213-
ᶜρʲ = ᶜρʲs.:(1)
214-
ᶜdivᵥ_q = Operators.DivergenceF2C(
215-
top = Operators.SetValue(C3(FT(0))),
216-
bottom = Operators.SetValue(C3(FT(0))),
217-
)
218-
# Sedimenting microphysics species are diffused with
219-
# α_vert_diff_tracer * K_h, passive tracers with the unscaled K_h,
220-
# matching the grid-mean tracer diffusion and the implicit
221-
# Jacobian (update_sgs_diffusion_jacobian!).
222-
for χ_name in sgs_tracer_names(Y)
223-
α =
224-
χ_name in sgs_sedimenting_tracer_candidates ?
225-
α_vert_diff_microphysics :
226-
one(α_vert_diff_microphysics)
227-
ᶜχʲ = MatrixFields.get_field(Y.c.sgsʲs.:(1), χ_name)
228-
ᶜχʲₜ = MatrixFields.get_field(Yₜ.c.sgsʲs.:(1), χ_name)
229-
@. ᶜχʲₜ -=
230-
ᶜdivᵥ_q(-(ᶠinterp(ᶜρʲ) * ᶠK_h * α * ᶠgradᵥ(ᶜχʲ))) / ᶜρʲ
231-
end
232-
end
233-
end
234-
end
235-
236165
# Private helper: clips grid-mean condensate tracers to non-negative values and
237166
# rescales the condensate sum so it cannot exceed the available total moisture.
238167
function enforce_grid_mean_microphysics_constraints!(Y, p, t)

0 commit comments

Comments
 (0)