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