@@ -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)
280281end
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