Skip to content

Commit f644840

Browse files
committed
Add average bulk microphyiscs 1m tendency based on linearized microphysics with optional sub-stepping: donor-based linearization (sinks as D*q, vapor sources constant); add operator construction and average tendency solve.
1 parent 91a04fe commit f644840

File tree

3 files changed

+764
-1
lines changed

3 files changed

+764
-1
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "CloudMicrophysics"
22
uuid = "6a9e3e04-43cd-43ba-94b9-e8782df3c71b"
3-
version = "0.34"
3+
version = "0.35"
44
authors = ["Climate Modeling Alliance"]
55

66
[deps]

src/BulkMicrophysicsTendencies.jl

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export MicrophysicsScheme,
3939
Microphysics1Moment,
4040
Microphysics2Moment,
4141
bulk_microphysics_tendencies,
42+
average_bulk_microphysics_tendencies,
4243
bulk_microphysics_derivatives,
4344
bulk_microphysics_cloud_derivatives
4445

@@ -279,6 +280,340 @@ This is a pure function of local thermodynamic state, suitable for:
279280
return (; dq_lcl_dt, dq_icl_dt, dq_rai_dt, dq_sno_dt)
280281
end
281282

283+
"""
284+
Construct a local linear approximation of 1-moment microphysics tendencies:
285+
286+
dq/dt ≈ M * q + e
287+
288+
using a donor-based linearization:
289+
- donor → receiver transfers are represented as `D * q_donor`
290+
- vapor → condensate sources are treated as constants (`e`)
291+
- condensate sinks are treated as linear sinks (`-D * q`)
292+
293+
All coefficients use `D = S / max(q_min, q_donor)` for robustness.
294+
295+
With this formulation, sink terms behave like exponential decays over a timestep
296+
in the implicit solve. The resulting operator is sparse and only stores the
297+
nonzero entries needed by the specialized solver.
298+
299+
Returns a `NamedTuple` containing the nonzero entries of `M` and `e`.
300+
"""
301+
@inline function _bulk_microphysics_linearized_operator(
302+
::Microphysics1Moment, mp::CMP.Microphysics1MParams, tps,
303+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, N_lcl = zero(ρ),
304+
)
305+
ρ = UT.clamp_to_nonneg(ρ)
306+
q_tot = UT.clamp_to_nonneg(q_tot)
307+
q_lcl = UT.clamp_to_nonneg(q_lcl)
308+
q_icl = UT.clamp_to_nonneg(q_icl)
309+
q_rai = UT.clamp_to_nonneg(q_rai)
310+
q_sno = UT.clamp_to_nonneg(q_sno)
311+
312+
lcl = mp.cloud.liquid
313+
icl = mp.cloud.ice
314+
rai = mp.precip.rain
315+
sno = mp.precip.snow
316+
ce = mp.collision
317+
aps = mp.air_properties
318+
vel = mp.terminal_velocity
319+
var = mp.autoconv_2M
320+
321+
FT = promote_type(
322+
typeof(ρ), typeof(T), typeof(q_tot), typeof(q_lcl),
323+
typeof(q_icl), typeof(q_rai), typeof(q_sno),
324+
)
325+
326+
M11 = zero(FT)
327+
M22 = zero(FT)
328+
M31 = zero(FT)
329+
M33 = zero(FT)
330+
M34 = zero(FT)
331+
M41 = zero(FT)
332+
M42 = zero(FT)
333+
M43 = zero(FT)
334+
M44 = zero(FT)
335+
e1 = zero(FT)
336+
e2 = zero(FT)
337+
e4 = zero(FT)
338+
339+
# minimum specific humidity threshold for regularizing S/q
340+
q_min = TDI.TD.Parameters.q_min(tps)
341+
342+
# ------------------------------------------------------------
343+
# Vapor -> cloud condensate: constant sources
344+
# ------------------------------------------------------------
345+
S_lcl_cond = CMNonEq.conv_q_vap_to_q_lcl_icl_MM2015(
346+
lcl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
347+
)
348+
D = S_lcl_cond / max(q_min, q_lcl)
349+
is_source = S_lcl_cond >= zero(FT)
350+
e1 += ifelse(is_source, S_lcl_cond, zero(FT))
351+
M11 += ifelse(is_source, zero(FT), D)
352+
353+
S_icl_dep = CMNonEq.conv_q_vap_to_q_lcl_icl_MM2015(
354+
icl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
355+
)
356+
D = S_icl_dep / max(q_min, q_icl)
357+
is_source = S_icl_dep >= zero(FT)
358+
e2 += ifelse(is_source, S_icl_dep, zero(FT))
359+
M22 += ifelse(is_source, zero(FT), D)
360+
361+
# ------------------------------------------------------------
362+
# Autoconversion: donor-based transfer
363+
# ------------------------------------------------------------
364+
S_acnv_1M = CM1.conv_q_lcl_to_q_rai(rai.acnv1M, q_lcl, true)
365+
S_acnv_2M = isnothing(var) ? S_acnv_1M : CM2.conv_q_lcl_to_q_rai(var, q_lcl, ρ, N_lcl)
366+
S_acnv_lcl = ifelse(N_lcl > zero(N_lcl), S_acnv_2M, S_acnv_1M)
367+
D = S_acnv_lcl / max(q_min, q_lcl)
368+
M11 -= D
369+
M31 += D
370+
371+
S_acnv_icl = CM1.conv_q_icl_to_q_sno_no_supersat(sno.acnv1M, q_icl, true)
372+
D = S_acnv_icl / max(q_min, q_icl)
373+
M22 -= D
374+
M42 += D
375+
376+
# ------------------------------------------------------------
377+
# Accretion: donor-based transfer
378+
# ------------------------------------------------------------
379+
S_accr_lcl_rai = CM1.accretion(lcl, rai, vel.rain, ce, q_lcl, q_rai, ρ)
380+
D = S_accr_lcl_rai / max(q_min, q_lcl)
381+
M11 -= D
382+
M31 += D
383+
384+
S_accr_lcl_sno = CM1.accretion(lcl, sno, vel.snow, ce, q_lcl, q_sno, ρ)
385+
D = S_accr_lcl_sno / max(q_min, q_lcl)
386+
M11 -= D
387+
is_warm = T >= sno.T_freeze
388+
M31 += ifelse(is_warm, D, zero(FT))
389+
M41 += ifelse(is_warm, zero(FT), D)
390+
391+
α = warm_accretion_melt_factor(tps, sno, T)
392+
S_accr_melt = α * S_accr_lcl_sno
393+
# this is snow -> rain melt induced by warm collected liquid
394+
# donor is snow
395+
D = S_accr_melt / max(q_min, q_sno)
396+
M44 -= D
397+
M34 += D
398+
399+
S_accr_icl_rai = CM1.accretion(icl, rai, vel.rain, ce, q_icl, q_rai, ρ)
400+
D = S_accr_icl_rai / max(q_min, q_icl)
401+
M22 -= D
402+
M42 += D
403+
404+
S_accr_icl_sno = CM1.accretion(icl, sno, vel.snow, ce, q_icl, q_sno, ρ)
405+
D = S_accr_icl_sno / max(q_min, q_icl)
406+
M22 -= D
407+
M42 += D
408+
409+
S_accr_rai_icl = CM1.accretion_rain_sink(rai, icl, vel.rain, ce, q_icl, q_rai, ρ)
410+
D = S_accr_rai_icl / max(q_min, q_rai)
411+
M33 -= D
412+
M43 += D
413+
414+
S_accr_rai_sno = CM1.accretion_snow_rain(sno, rai, vel.snow, vel.rain, ce, q_sno, q_rai, ρ)
415+
S_accr_sno_rai = CM1.accretion_snow_rain(rai, sno, vel.rain, vel.snow, ce, q_rai, q_sno, ρ)
416+
417+
# snow -> rain
418+
D = S_accr_sno_rai / max(q_min, q_sno)
419+
M44 -= ifelse(is_warm, D, zero(FT))
420+
M34 += ifelse(is_warm, D, zero(FT))
421+
422+
S_accr_melt2 = α * S_accr_rai_sno
423+
D = S_accr_melt2 / max(q_min, q_sno)
424+
M44 -= ifelse(is_warm, D, zero(FT))
425+
M34 += ifelse(is_warm, D, zero(FT))
426+
427+
# rain -> snow
428+
D = S_accr_rai_sno / max(q_min, q_rai)
429+
M33 -= ifelse(is_warm, zero(FT), D)
430+
M43 += ifelse(is_warm, zero(FT), D)
431+
432+
# ------------------------------------------------------------
433+
# Rain evaporation: sink to vapor (always zero or negative)
434+
# ------------------------------------------------------------
435+
S_evap_rai = CM1.evaporation_sublimation(
436+
rai, vel.rain, aps, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
437+
)
438+
D = (-S_evap_rai) / max(q_min, q_rai)
439+
M33 -= D
440+
441+
# ------------------------------------------------------------
442+
# Snow sublimation/deposition
443+
# ------------------------------------------------------------
444+
S_subl_sno = CM1.evaporation_sublimation(
445+
sno, vel.snow, aps, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
446+
)
447+
D = S_subl_sno / max(q_min, q_sno)
448+
is_source = S_subl_sno >= zero(FT)
449+
e4 += ifelse(is_source, S_subl_sno, zero(FT))
450+
M44 += ifelse(is_source, zero(FT), D)
451+
452+
# ------------------------------------------------------------
453+
# Snow melt: snow -> rain
454+
# ------------------------------------------------------------
455+
S_melt_sno = CM1.snow_melt(sno, vel.snow, aps, tps, q_sno, ρ, T)
456+
D = S_melt_sno / max(q_min, q_sno)
457+
M44 -= D
458+
M34 += D
459+
460+
return (
461+
M11 = M11, M22 = M22,
462+
M31 = M31, M33 = M33, M34 = M34,
463+
M41 = M41, M42 = M42, M43 = M43, M44 = M44,
464+
e1 = e1, e2 = e2, e4 = e4,
465+
)
466+
end
467+
468+
"""
469+
Compute time-averaged 1-moment microphysics tendencies over a single linearized substep.
470+
471+
Solves the linearized implicit system
472+
473+
(q* - q⁰) / Δt = M q* + e
474+
475+
and returns the average tendency
476+
477+
dq/dt = (q* - q⁰) / Δt.
478+
479+
The system uses a sparse structure specific to the 1-moment microphysics model:
480+
`q_lcl` and `q_icl` are solved independently, while `q_rai` and `q_sno` are
481+
solved from a coupled 2×2 system.
482+
483+
Because sinks are linearized as `-D q`, they are effectively integrated as
484+
exponential decays over the substep.
485+
"""
486+
@inline function _average_bulk_microphysics_tendencies(
487+
::Microphysics1Moment, mp::CMP.Microphysics1MParams, tps,
488+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, Δt, N_lcl = zero(ρ),
489+
)
490+
491+
FT = typeof(q_tot)
492+
493+
lin = _bulk_microphysics_linearized_operator(
494+
Microphysics1Moment(), mp, tps,
495+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, N_lcl,
496+
)
497+
498+
invΔt = one(FT) / Δt
499+
500+
# A = I/Δt - M
501+
a11 = invΔt - lin.M11
502+
a22 = invΔt - lin.M22
503+
a31 = -lin.M31
504+
a33 = invΔt - lin.M33
505+
a34 = -lin.M34
506+
a41 = -lin.M41
507+
a42 = -lin.M42
508+
a43 = -lin.M43
509+
a44 = invΔt - lin.M44
510+
511+
# rhs = e + q_0/Δt
512+
# e3 = 0 by the 1m model
513+
b1 = lin.e1 + invΔt * q_lcl
514+
b2 = lin.e2 + invΔt * q_icl
515+
b3 = invΔt * q_rai
516+
b4 = lin.e4 + invΔt * q_sno
517+
518+
q_lcl_new = b1 / a11
519+
q_icl_new = b2 / a22
520+
521+
# Reduced 2x2 system for q_rai_new, q_sno_new
522+
r3 = muladd(-a31, q_lcl_new, b3)
523+
r4 = muladd(-a41, q_lcl_new, muladd(-a42, q_icl_new, b4))
524+
525+
det = muladd(-a34, a43, a33 * a44)
526+
# det is a positive number because a44 and a33 are positive (greater than invΔt)
527+
# and a34 and a43 are non-positive so we don't need to safeguard division by det.
528+
q_rai_new = (r3 * a44 - a34 * r4) / det
529+
q_sno_new = (a33 * r4 - r3 * a43) / det
530+
531+
dq_lcl_dt = (q_lcl_new - q_lcl) * invΔt
532+
dq_icl_dt = (q_icl_new - q_icl) * invΔt
533+
dq_rai_dt = (q_rai_new - q_rai) * invΔt
534+
dq_sno_dt = (q_sno_new - q_sno) * invΔt
535+
536+
return (; dq_lcl_dt, dq_icl_dt, dq_rai_dt, dq_sno_dt)
537+
end
538+
539+
"""
540+
Compute average 1-moment microphysics tendencies over `Δt` using repeated
541+
linearized implicit substeps.
542+
543+
The interval `Δt` is divided into `nsub` equal substeps. At each substep, a local
544+
linearized microphysics system is rebuilt from the current state and solved
545+
implicitly for cloud liquid, cloud ice, rain, and snow. Temperature is then
546+
updated from the latent heating implied by the substep tendencies.
547+
548+
The returned tendencies are the net change in the hydrometeor species over the
549+
full interval divided by `Δt`.
550+
551+
Increasing `nsub` improves how well the method captures nonlinear changes in the
552+
active microphysical processes, including regime changes near freezing.
553+
"""
554+
@inline function average_bulk_microphysics_tendencies(
555+
cm::Microphysics1Moment,
556+
mp::CMP.Microphysics1MParams,
557+
tps,
558+
ρ,
559+
T,
560+
q_tot,
561+
q_lcl,
562+
q_icl,
563+
q_rai,
564+
q_sno,
565+
Δt,
566+
nsub = 1,
567+
N_lcl = zero(ρ),
568+
)
569+
FT = typeof(q_tot)
570+
571+
q_lcl_0 = q_lcl
572+
q_icl_0 = q_icl
573+
q_rai_0 = q_rai
574+
q_sno_0 = q_sno
575+
576+
Δt_sub = Δt / FT(nsub)
577+
578+
Lv_over_cp = TDI.TD.Parameters.LH_v0(tps) / TDI.TD.Parameters.cp_d(tps)
579+
Ls_over_cp = TDI.TD.Parameters.LH_s0(tps) / TDI.TD.Parameters.cp_d(tps)
580+
581+
for _ in 1:nsub
582+
rates = _average_bulk_microphysics_tendencies(
583+
cm,
584+
mp,
585+
tps,
586+
ρ,
587+
T,
588+
q_tot,
589+
q_lcl,
590+
q_icl,
591+
q_rai,
592+
q_sno,
593+
Δt_sub,
594+
N_lcl,
595+
)
596+
597+
q_lcl += rates.dq_lcl_dt * Δt_sub
598+
q_icl += rates.dq_icl_dt * Δt_sub
599+
q_rai += rates.dq_rai_dt * Δt_sub
600+
q_sno += rates.dq_sno_dt * Δt_sub
601+
602+
T +=
603+
(
604+
Lv_over_cp * (rates.dq_lcl_dt + rates.dq_rai_dt) +
605+
Ls_over_cp * (rates.dq_icl_dt + rates.dq_sno_dt)
606+
) * Δt_sub
607+
end
608+
609+
dq_lcl_dt = (q_lcl - q_lcl_0) / Δt
610+
dq_icl_dt = (q_icl - q_icl_0) / Δt
611+
dq_rai_dt = (q_rai - q_rai_0) / Δt
612+
dq_sno_dt = (q_sno - q_sno_0) / Δt
613+
614+
return (; dq_lcl_dt, dq_icl_dt, dq_rai_dt, dq_sno_dt)
615+
end
616+
282617
"""
283618
bulk_microphysics_derivatives(
284619
::Microphysics1Moment,

0 commit comments

Comments
 (0)