Skip to content

Commit 986667f

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

File tree

3 files changed

+531
-1
lines changed

3 files changed

+531
-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: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,279 @@ This is a pure function of local thermodynamic state, suitable for:
279279
return (; dq_lcl_dt, dq_icl_dt, dq_rai_dt, dq_sno_dt)
280280
end
281281

282+
"""
283+
Construct a local linear approximation of 1-moment microphysics tendencies:
284+
285+
dq/dt ≈ M * q + e
286+
287+
using a donor-based linearization:
288+
289+
- Donor → receiver transfers are linearized as `D * q_donor`
290+
- Vapor → condensate sources are treated as constants (`e`)
291+
- Condensate → vapor sinks are treated as linear sinks (`-D * q`)
292+
293+
All coefficients use `D = S / max(eps(T), q_donor)` for robustness.
294+
295+
With this formulation, sink terms take the form
296+
297+
dq/dt = -D q
298+
299+
which corresponds to exponential decay over a timestep in the implicit solve.
300+
301+
The resulting operator is sparse with nonzeros only in:
302+
- Diagonal: `M11`, `M22`, `M33`, `M44`
303+
- Couplings: `M31`, `M34`, `M41`, `M42`, `M43`
304+
- Sources: `e1`, `e2`, `e4` (rain has no constant source)
305+
306+
Returns a `NamedTuple` with these entries.
307+
"""
308+
@inline function bulk_microphysics_linearized_operator(
309+
::Microphysics1Moment, mp::CMP.Microphysics1MParams, tps,
310+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, N_lcl = zero(ρ),
311+
)
312+
ρ = UT.clamp_to_nonneg(ρ)
313+
q_tot = UT.clamp_to_nonneg(q_tot)
314+
q_lcl = UT.clamp_to_nonneg(q_lcl)
315+
q_icl = UT.clamp_to_nonneg(q_icl)
316+
q_rai = UT.clamp_to_nonneg(q_rai)
317+
q_sno = UT.clamp_to_nonneg(q_sno)
318+
319+
lcl = mp.cloud.liquid
320+
icl = mp.cloud.ice
321+
rai = mp.precip.rain
322+
sno = mp.precip.snow
323+
ce = mp.collision
324+
aps = mp.air_properties
325+
vel = mp.terminal_velocity
326+
var = mp.autoconv_2M
327+
328+
FT = promote_type(
329+
typeof(ρ), typeof(T), typeof(q_tot), typeof(q_lcl),
330+
typeof(q_icl), typeof(q_rai), typeof(q_sno),
331+
)
332+
333+
M11 = zero(FT)
334+
M22 = zero(FT)
335+
M31 = zero(FT)
336+
M33 = zero(FT)
337+
M34 = zero(FT)
338+
M41 = zero(FT)
339+
M42 = zero(FT)
340+
M43 = zero(FT)
341+
M44 = zero(FT)
342+
e1 = zero(FT)
343+
e2 = zero(FT)
344+
e4 = zero(FT)
345+
346+
# ------------------------------------------------------------
347+
# Vapor -> cloud condensate: constant sources
348+
# ------------------------------------------------------------
349+
S_lcl_cond = CMNonEq.conv_q_vap_to_q_lcl_icl_MM2015(
350+
lcl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
351+
)
352+
if S_lcl_cond >= zero(FT)
353+
e1 += S_lcl_cond
354+
else
355+
D = (-S_lcl_cond) / max(eps(FT), q_lcl)
356+
M11 -= D
357+
end
358+
359+
S_icl_dep = CMNonEq.conv_q_vap_to_q_lcl_icl_MM2015(
360+
icl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
361+
)
362+
if S_icl_dep >= zero(FT)
363+
e2 += S_icl_dep
364+
else
365+
D = (-S_icl_dep) / max(eps(FT), q_icl)
366+
M22 -= D
367+
end
368+
369+
# ------------------------------------------------------------
370+
# Autoconversion: donor-based transfer
371+
# ------------------------------------------------------------
372+
S_acnv_1M = CM1.conv_q_lcl_to_q_rai(rai.acnv1M, q_lcl, true)
373+
S_acnv_2M = isnothing(var) ? S_acnv_1M : CM2.conv_q_lcl_to_q_rai(var, q_lcl, ρ, N_lcl)
374+
S_acnv_lcl = ifelse(N_lcl > zero(N_lcl), S_acnv_2M, S_acnv_1M)
375+
D = S_acnv_lcl / max(eps(FT), q_lcl)
376+
M11 -= D
377+
M31 += D
378+
379+
S_acnv_icl = CM1.conv_q_icl_to_q_sno_no_supersat(sno.acnv1M, q_icl, true)
380+
D = S_acnv_icl / max(eps(FT), q_icl)
381+
M22 -= D
382+
M42 += D
383+
384+
# ------------------------------------------------------------
385+
# Accretion: donor-based transfer
386+
# ------------------------------------------------------------
387+
S_accr_lcl_rai = CM1.accretion(lcl, rai, vel.rain, ce, q_lcl, q_rai, ρ)
388+
D = S_accr_lcl_rai / max(eps(FT), q_lcl)
389+
M11 -= D
390+
M31 += D
391+
392+
S_accr_lcl_sno = CM1.accretion(lcl, sno, vel.snow, ce, q_lcl, q_sno, ρ)
393+
D = S_accr_lcl_sno / max(eps(FT), q_lcl)
394+
M11 -= D
395+
is_warm = T >= sno.T_freeze
396+
if is_warm
397+
M31 += D
398+
else
399+
M41 += D
400+
end
401+
402+
α = warm_accretion_melt_factor(tps, sno, T)
403+
S_accr_melt = α * S_accr_lcl_sno
404+
# this is snow -> rain melt induced by warm collected liquid
405+
# donor is snow
406+
D = S_accr_melt / max(eps(FT), q_sno)
407+
M44 -= D
408+
M34 += D
409+
410+
S_accr_icl_rai = CM1.accretion(icl, rai, vel.rain, ce, q_icl, q_rai, ρ)
411+
D = S_accr_icl_rai / max(eps(FT), q_icl)
412+
M22 -= D
413+
M42 += D
414+
415+
S_accr_icl_sno = CM1.accretion(icl, sno, vel.snow, ce, q_icl, q_sno, ρ)
416+
D = S_accr_icl_sno / max(eps(FT), q_icl)
417+
M22 -= D
418+
M42 += D
419+
420+
S_accr_rai_icl = CM1.accretion_rain_sink(rai, icl, vel.rain, ce, q_icl, q_rai, ρ)
421+
D = S_accr_rai_icl / max(eps(FT), q_rai)
422+
M33 -= D
423+
M43 += D
424+
425+
S_accr_rai_sno = CM1.accretion_snow_rain(sno, rai, vel.snow, vel.rain, ce, q_sno, q_rai, ρ)
426+
S_accr_sno_rai = CM1.accretion_snow_rain(rai, sno, vel.rain, vel.snow, ce, q_rai, q_sno, ρ)
427+
428+
if is_warm
429+
# snow -> rain
430+
D = S_accr_sno_rai / max(eps(FT), q_sno)
431+
M44 -= D
432+
M34 += D
433+
434+
S_accr_melt2 = α * S_accr_rai_sno
435+
D = S_accr_melt2 / max(eps(FT), q_sno)
436+
M44 -= D
437+
M34 += D
438+
else
439+
# rain -> snow
440+
D = S_accr_rai_sno / max(eps(FT), q_rai)
441+
M33 -= D
442+
M43 += D
443+
end
444+
445+
# ------------------------------------------------------------
446+
# Rain evaporation: sink to vapor if negative, else constant source
447+
# ------------------------------------------------------------
448+
S_evap_rai = CM1.evaporation_sublimation(
449+
rai, vel.rain, aps, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
450+
)
451+
D = (-S_evap_rai) / max(eps(FT), q_rai)
452+
M33 -= D
453+
454+
# ------------------------------------------------------------
455+
# Snow sublimation/deposition
456+
# ------------------------------------------------------------
457+
S_subl_sno = CM1.evaporation_sublimation(
458+
sno, vel.snow, aps, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
459+
)
460+
if S_subl_sno < zero(FT)
461+
D = (-S_subl_sno) / max(eps(FT), q_sno)
462+
M44 -= D
463+
else
464+
e4 += S_subl_sno
465+
end
466+
467+
# ------------------------------------------------------------
468+
# Snow melt: snow -> rain
469+
# ------------------------------------------------------------
470+
S_melt_sno = CM1.snow_melt(sno, vel.snow, aps, tps, q_sno, ρ, T)
471+
D = S_melt_sno / max(eps(FT), q_sno)
472+
M44 -= D
473+
M34 += D
474+
475+
return (
476+
M11 = M11, M22 = M22,
477+
M31 = M31, M33 = M33, M34 = M34,
478+
M41 = M41, M42 = M42, M43 = M43, M44 = M44,
479+
e1 = e1, e2 = e2, e4 = e4,
480+
)
481+
end
482+
483+
"""
484+
Compute time-averaged microphysics tendencies over a period of time Δt.
485+
486+
Solves the linearized implicit system:
487+
488+
(q* - q⁰) / Δt = M q* + e
489+
490+
and returns the average tendency
491+
492+
dq/dt = (q* - q⁰) / Δt
493+
494+
The system is solved using a specialized sparse structure (specific to the microphysics 1M model):
495+
- `q_lcl`, `q_icl`: independent scalar solves
496+
- `(q_rai, q_sno)`: 2×2 coupled solve
497+
498+
Because sinks are linearized as `-D q`, this method effectively integrates
499+
them as exponential decays over the timestep, improving stability for stiff processes.
500+
501+
Returns a `NamedTuple` of averaged tendencies for all species.
502+
"""
503+
@inline function average_bulk_microphysics_tendencies(
504+
::Microphysics1Moment, mp::CMP.Microphysics1MParams, tps,
505+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, Δt;
506+
N_lcl = zero(ρ),
507+
)
508+
509+
FT = typeof(q_tot)
510+
511+
lin = bulk_microphysics_linearized_operator(
512+
Microphysics1Moment(), mp, tps,
513+
ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno, N_lcl,
514+
)
515+
516+
invΔt = one(FT) / Δt
517+
518+
# A = I/Δt - M
519+
a11 = invΔt - lin.M11
520+
a22 = invΔt - lin.M22
521+
a31 = -lin.M31
522+
a33 = invΔt - lin.M33
523+
a34 = -lin.M34
524+
a41 = -lin.M41
525+
a42 = -lin.M42
526+
a43 = -lin.M43
527+
a44 = invΔt - lin.M44
528+
529+
# rhs = e + q_0/Δt
530+
# e3 = 0 by the 1m model
531+
b1 = lin.e1 + invΔt * q_lcl
532+
b2 = lin.e2 + invΔt * q_icl
533+
b3 = invΔt * q_rai
534+
b4 = lin.e4 + invΔt * q_sno
535+
536+
q_lcl_new = b1 / a11
537+
q_icl_new = b2 / a22
538+
539+
# Reduced 2x2 system for q_rai_new, q_sno_new
540+
r3 = muladd(-a31, q_lcl_new, b3)
541+
r4 = muladd(-a41, q_lcl_new, muladd(-a42, q_icl_new, b4))
542+
543+
det = muladd(-a34, a43, a33 * a44)
544+
q_rai_new = (r3 * a44 - a34 * r4) / det
545+
q_sno_new = (a33 * r4 - r3 * a43) / det
546+
547+
dq_lcl_dt = (q_lcl_new - q_lcl) * invΔt
548+
dq_icl_dt = (q_icl_new - q_icl) * invΔt
549+
dq_rai_dt = (q_rai_new - q_rai) * invΔt
550+
dq_sno_dt = (q_sno_new - q_sno) * invΔt
551+
552+
return (; dq_lcl_dt, dq_icl_dt, dq_rai_dt, dq_sno_dt)
553+
end
554+
282555
"""
283556
bulk_microphysics_derivatives(
284557
::Microphysics1Moment,

0 commit comments

Comments
 (0)