Skip to content

Commit d1764d5

Browse files
committed
feat(P3): add more P3 ice tendencies
new tendencies: - ice self-collection - sublimation/deposition - heterogeneous ice nucleation (Frostenberg mean) - deposition nucleation numerics: - add numeric epsilon for rime volume - squash a few bugs - improve docs
1 parent a111414 commit d1764d5

File tree

7 files changed

+320
-48
lines changed

7 files changed

+320
-48
lines changed

src/BulkMicrophysicsTendencies.jl

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import ..Microphysics1M as CM1
3232
import ..Microphysics2M as CM2
3333
import ..MicrophysicsNonEq as CMNonEq
3434
import ..P3Scheme as CMP3
35+
import ..HetIceNucleation as CM_HetIce
3536
import ...ThermodynamicsInterface as TDI
37+
import ..Common as CO
3638

3739
export MicrophysicsScheme,
3840
Microphysics0Moment,
@@ -804,6 +806,9 @@ to be non-Nothing, eliminating runtime type checks and dynamic dispatch.
804806
ρ, T, q_tot, q_lcl, n_lcl, q_rai, n_rai,
805807
q_ice = zero(ρ), n_ice = zero(ρ), q_rim = zero(ρ), b_rim = zero(ρ), logλ = zero(ρ),
806808
) where {WR, ICE <: CMP.P3IceParams}
809+
ϵₘ = UT.ϵ_numerics_2M_M(FT)
810+
ϵₙ = UT.ϵ_numerics_2M_M(FT)
811+
ϵB = UT.ϵ_numerics_P3_B(FT)
807812
# Clamp negative inputs to zero (robustness against numerical errors)
808813
ρ = UT.clamp_to_nonneg(ρ)
809814
q_tot = UT.clamp_to_nonneg(q_tot)
@@ -816,13 +821,26 @@ to be non-Nothing, eliminating runtime type checks and dynamic dispatch.
816821
q_rim = UT.clamp_to_nonneg(q_rim)
817822
b_rim = UT.clamp_to_nonneg(b_rim)
818823

824+
# Convert to volumetric quantities for P3 functions
825+
L_lcl = q_lcl * ρ # [kg lcl / m³ air]
826+
L_rai = q_rai * ρ # [kg rai / m³ air]
827+
N_lcl = n_lcl * ρ # [1 / m³ air]
828+
N_rai = n_rai * ρ # [1 / m³ air]
829+
L_ice = q_ice * ρ # [kg ice / m³ air]
830+
N_ice = n_ice * ρ # [1 / m³ air]
831+
L_rim = q_rim * ρ # [kg rim / m³ air]
832+
B_rim = b_rim * ρ # [m³ rim / m³ air]
833+
# Compute rime fraction and density
834+
F_rim = ifelse(q_ice > ϵₘ, L_rim / L_ice, zero(L_rim))
835+
ρ_rim = ifelse(b_rim > ϵB, L_rim / B_rim, zero(L_rim)) # [kg rim / m³ rim]
836+
819837
# Unpack warm rain parameters (always present)
820838
aps = mp.warm_rain.air_properties
839+
subdep = mp.warm_rain.subdep
821840

822841
# Initialize ice-related tendencies
823842
dq_ice_dt = zero(ρ)
824-
# TODO: When ice number concentration becomes prognostic, add:
825-
# dn_ice_dt = zero(ρ) # Ice number tendency (changes due to melting, aggregation)
843+
dn_ice_dt = zero(ρ) # Ice number tendency (changes due to melting, aggregation)
826844
dq_rim_dt = zero(ρ)
827845
db_rim_dt = zero(ρ)
828846

@@ -833,51 +851,24 @@ to be non-Nothing, eliminating runtime type checks and dynamic dispatch.
833851
dq_rai_dt = warm.dq_rai_dt
834852
dn_rai_dt = warm.dn_rai_dt
835853

836-
# Convert to number densities for remaining functions
837-
N_lcl = ρ * n_lcl
838-
N_rai = ρ * n_rai
839-
840854
# --- P3 Ice Processes ---
841855
p3 = mp.ice.scheme
842856
vel = mp.ice.terminal_velocity
843857
pdf_c = mp.ice.cloud_pdf
844858
pdf_r = mp.ice.rain_pdf
859+
heterogeneous = mp.ice.heterogeneous
860+
deposition_condfreeze = mp.ice.deposition_condfreeze
861+
845862

846863
# Only compute ice processes if there is ice mass/number present
847-
if (q_ice > zero(q_ice) || n_ice > zero(n_ice))
848-
# Convert to volumetric quantities for P3 functions
849-
L_ice = q_ice * ρ # [kg/m³]
850-
N_ice = n_ice * ρ # [1/m³]
851-
L_lcl = q_lcl * ρ # [kg/m³]
852-
N_lcl = n_lcl * ρ # [1/m³]
853-
L_rai = q_rai * ρ # [kg/m³]
854-
N_rai = n_rai * ρ # [1/m³]
855-
856-
# Compute rime fraction and density
857-
F_rim = ifelse(q_ice > zero(q_ice), q_rim / q_ice, zero(q_rim))
858-
ρ_rim = ifelse(b_rim > zero(b_rim), q_rim * ρ / (b_rim * ρ), ρ) # [kg/m³]
864+
if q_ice > ϵₘ && n_ice > ϵₙ
859865

860866
# Liquid-ice collision sources (core P3 process)
861867
# Only compute if there is ice present
862868
if L_ice > zero(L_ice) && N_ice > zero(N_ice)
863869
coll = CMP3.bulk_liquid_ice_collision_sources(
864-
p3,
865-
logλ,
866-
L_ice,
867-
N_ice,
868-
F_rim,
869-
ρ_rim,
870-
pdf_c,
871-
pdf_r,
872-
L_lcl,
873-
N_lcl,
874-
L_rai,
875-
N_rai,
876-
aps,
877-
tps,
878-
vel,
879-
ρ,
880-
T,
870+
p3, logλ, L_ice, N_ice, F_rim, ρ_rim, pdf_c, pdf_r,
871+
L_lcl, N_lcl, L_rai, N_rai, aps, tps, vel, ρ, T,
881872
)
882873

883874
# Add collision tendencies
@@ -907,9 +898,69 @@ to be non-Nothing, eliminating runtime type checks and dynamic dispatch.
907898
end
908899
end
909900

910-
# TODO: When ice number concentration is tracked, add dn_ice_dt to return tuple:
911-
# return (; dq_lcl_dt, dn_lcl_dt, dq_rai_dt, dn_rai_dt, dq_ice_dt, dn_ice_dt, dq_rim_dt, db_rim_dt)
912-
return (; dq_lcl_dt, dn_lcl_dt, dq_rai_dt, dn_rai_dt, dq_ice_dt, dq_rim_dt, db_rim_dt)
901+
# --- -------------------- ---
902+
# --- Ice Nucleation Modes ---
903+
# --- -------------------- ---
904+
905+
# --- Ice Nucleation (Deposition) ---
906+
# Assume nucleated particles have diameter 1μm --> nucleated mass (per particle)
907+
D_nuc_ice = FT(1e-6) # 1μm
908+
m_nuc = p3.ρ_i * CO.volume_sphere_D(D_nuc_ice) # [kg]
909+
910+
# TODO: The parameterixation should return a rate, `∂N/∂t`, not number changes `ΔN`
911+
n_dep = CM_HetIce.P3_deposition_N_i(deposition_condfreeze, T) / ρ # [particles / kg air]
912+
m_dep = n_dep * m_nuc # [kg ice / kg air]
913+
dn_ice_dt += n_dep
914+
dq_ice_dt += m_dep
915+
dq_rim_dt += m_dep
916+
db_rim_dt += m_dep / 900
917+
918+
# --- Heterogeneous Ice Nucleation (Immersion freezing) ---
919+
# Assume mass loss is mean condensate mass
920+
m_lcl = ifelse(n_lcl > ϵₙ, q_lcl / n_lcl, zero(q_lcl)) # mean liquid mass
921+
922+
# TODO: The parameterixation should return a rate, `∂N/∂t`, not number changes `ΔN`
923+
inpc = CM_HetIce.INP_concentration_mean(heterogeneous, T) / ρ # [particles / kg air]
924+
n_het = max(0, inpc - n_ice) # [particles / kg air]
925+
q_het = n_het * m_lcl # [kg ice / kg air]
926+
927+
dn_ice_dt += n_het
928+
dq_ice_dt += q_het
929+
dq_rim_dt += q_het
930+
db_rim_dt += q_het / 900 # = ρ^* TODO: make this a parameter?
931+
dn_lcl_dt -= n_het
932+
dq_lcl_dt -= q_het
933+
934+
# --- Cloud Droplet Condensation Freezing ---
935+
# ref: `homogeneous_freezing` in `parcel/ParcelTendencies.jl`
936+
# get mean diameter of cloud droplets, then convert to volume
937+
938+
# --- Ice Sublimation / Deposition ---
939+
# Deposition/sublimation of cloud ice
940+
∂ₜq_ice_dep = CMNonEq.conv_q_vap_to_q_lcl_icl_MM2015(subdep, tps, q_tot, q_lcl, q_ice, q_rai, zero(q_ice), ρ, T)
941+
# No ice deposition above freezing (lack of INPs)
942+
∂ₜq_ice_dep = ifelse(T > tps.T_freeze, min(∂ₜq_ice_dep, zero(T)), ∂ₜq_ice_dep)
943+
# During sublimation, the number of ice particles decreases in proportion to the mean ice mass
944+
# During deposition, the number of ice particles remain unchanged
945+
∂ₜn_ice_dep = ifelse(∂ₜq_ice_dep < 0, (n_ice / q_ice) * ∂ₜq_ice_dep, zero(∂ₜq_ice_dep))
946+
dq_ice_dt += ∂ₜq_ice_dep
947+
dn_ice_dt += ∂ₜn_ice_dep
948+
949+
# --- Ice Self-collection (Aggregation) ---
950+
if q_ice > ϵₘ && n_ice > ϵₙ
951+
state = CMP3.P3State(p3, L_ice, N_ice, F_rim, ρ_rim)
952+
S_ice_agg = CMP3.ice_self_collection(state, logλ, aps, tps, vel, ρ, T)
953+
dn_ice_dt -= S_ice_agg.dNdt / ρ
954+
end
955+
956+
# --- Rain Heterogeneous Freezing ---
957+
# TODO: Implement heterogeneous freezing of rain
958+
# This process is currently missing in P3_processes.jl
959+
# S_rai_frz = ...
960+
# dq_rai_dt -= S_rai_frz
961+
# dq_ice_dt += S_rai_frz
962+
963+
return (; dq_lcl_dt, dn_lcl_dt, dq_rai_dt, dn_rai_dt, dq_ice_dt, dn_ice_dt, dq_rim_dt, db_rim_dt)
913964
end
914965

915966
end # module BulkMicrophysicsTendencies

src/Common.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,22 @@ See e.g., Seifert and Beheng (2006), https://doi.org/10.1007/s00703-005-0112-4,
463463
464464
# Returns
465465
- `F_v(D)`: ventilation factor function (dimensionless)
466+
467+
# Extended help
468+
469+
The ventilation factor is given by
470+
471+
```math
472+
F_v(D) = a_v + b_v ⋅ ∛Sc ⋅ √Re(D)
473+
```
474+
475+
where
476+
477+
```math
478+
Sc = ν_{air} / D_{vapor}
479+
Re(D) = D * v_{term}(D) / ν_{air}
480+
```
481+
466482
"""
467483
@inline function ventilation_factor(vent, aps, v_term)
468484
(; aᵥ, bᵥ) = vent

src/P3_processes.jl

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ function ∫liquid_ice_collisions(
382382
# Initialize integration buffers by evaluating a representative integral
383383
p = FT(0.00001)
384384
ice_bounds = integral_bounds(state, logλ; p)
385-
bounds_c = CM2.get_size_distribution_bounds(psd_c, L_c, ρₐ, N_c, p)
386-
bounds_r = CM2.get_size_distribution_bounds(psd_r, L_r, ρₐ, N_r, p)
385+
bounds_c = CM2.get_size_distribution_bounds(psd_c, L_c / ρₐ, ρₐ, N_c, p)
386+
bounds_r = CM2.get_size_distribution_bounds(psd_r, L_r / ρₐ, ρₐ, N_r, p)
387387

388388
# Integrand components
389389
# NOTE: We assume collision efficiency, shape (spherical), and terminal velocity is the
@@ -488,3 +488,75 @@ function bulk_liquid_ice_collision_sources(
488488
(∂ₜq_c, ∂ₜq_r, ∂ₜN_c, ∂ₜN_r, ∂ₜL_rim, ∂ₜL_ice, ∂ₜB_rim)
489489
)
490490
end
491+
492+
function collision_cross_section_ice_ice(state, D_1, D_2)
493+
r_eff(D) = (ice_area(state, D) / π)
494+
return π * (r_eff(D_1) + r_eff(D_2))^2 # collision cross section
495+
end
496+
497+
"""
498+
volumetric_ice_ice_collision_rate_integrand(state, velocity_params, ρₐ)
499+
500+
Returns a function that computes the volumetric collision rate integrand for ice-ice collisions [m³/s].
501+
502+
# Arguments
503+
- `state`: [`P3State`](@ref)
504+
- `velocity_params`: velocity parameterization, e.g. [`CMP.Chen2022VelType`](@ref)
505+
- `ρₐ`: air density
506+
507+
# Returns
508+
A function `(D_1, D_2) -> E * K * |vᵢ(D_1) - vᵢ(D_2)|` where:
509+
- `D_1` and `D_2` are the (maximum) diameters of the ice particles
510+
- `E` is the collision efficiency
511+
- `K` is the collision cross section
512+
- `vᵢ` is the terminal velocity of ice particles
513+
"""
514+
function volumetric_ice_ice_collision_rate_integrand(velocity_params, ρₐ, state)
515+
v_ice = ice_particle_terminal_velocity(velocity_params, ρₐ, state)
516+
function integrand(D_1::FT, D_2::FT) where {FT}
517+
E = FT(1) # Collision efficiency
518+
K = collision_cross_section_ice_ice(state, D_1, D_2)
519+
return E * K * abs(v_ice(D_1) - v_ice(D_2))
520+
end
521+
return integrand
522+
end
523+
524+
"""
525+
ice_self_collection(state, logλ, aps, tps, vel, ρₐ, T; ∫kwargs...)
526+
527+
Computes the ice self-collection (aggregation) rate, which decreases the ice number concentration
528+
while leaving mass, rime mass, and rime volume unchanged.
529+
530+
# Arguments
531+
- `state`: [`P3State`](@ref)
532+
- `logλ`: the log of the slope parameter [log(1/m)]
533+
- `aps`: [`CMP.AirProperties`](@ref)
534+
- `tps`: thermodynamics parameters
535+
- `vel`: the velocity parameterization, e.g. [`CMP.Chen2022VelType`](@ref)
536+
- `ρₐ`: air density [kg/m³]
537+
- `T`: temperature [K]
538+
539+
# Returns
540+
A `NamedTuple` of `(; dNdt)`, where:
541+
1. `dNdt`: ice number concentration tendency due to self-collection [1/m³/s] (always positive or zero, represents a loss rate)
542+
"""
543+
function ice_self_collection(state, logλ, aps, tps, vel, ρₐ, T; ∫kwargs...)
544+
n_i = DT.size_distribution(state, logλ)
545+
∂ₜV = volumetric_ice_ice_collision_rate_integrand(vel, ρₐ, state)
546+
547+
p = eps(one(ρₐ))
548+
ice_bounds = integral_bounds(state, logλ; p)
549+
550+
function inner_integral(D_1)
551+
(rate_at_D1,) = integrate(ice_bounds...; ∫kwargs...) do D_2
552+
return SA.SVector(∂ₜV(D_1, D_2) * n_i(D_2))
553+
end
554+
return rate_at_D1 * n_i(D_1)
555+
end
556+
557+
(total_rate,) = integrate(inner_integral, ice_bounds...; ∫kwargs...)
558+
559+
# The 0.5 factor accounts for double-counting in self-collection
560+
dNdt = (1 // 2) * total_rate
561+
return (; dNdt)
562+
end

src/Utilities.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Contains pure numerical operations with no physics dependencies.
66
"""
77
module Utilities
88

9-
export clamp_to_nonneg, ϵ_numerics, ϵ_numerics_2M_M, ϵ_numerics_2M_N
9+
export clamp_to_nonneg, ϵ_numerics, ϵ_numerics_2M_M, ϵ_numerics_2M_N, ϵ_numerics_P3_B
1010

1111
"""
1212
clamp_to_nonneg(x)
@@ -44,4 +44,12 @@ Numerical epsilon for 2-moment number calculations.
4444
"""
4545
@inline ϵ_numerics_2M_N(FT) = eps(FT)
4646

47+
"""
48+
ϵ_numerics_P3_B(FT)
49+
50+
Numerical epsilon for P3 bulk microphysics mass calculations
51+
relating to rim volume, B_rim.
52+
"""
53+
@inline ϵ_numerics_P3_B(FT) = eps(FT)
54+
4755
end # module

src/parameters/Microphysics2MParams.jl

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ Parameters for 2-moment warm rain processes (Seifert-Beheng 2006).
99
- `seifert_beheng::SB`: SB2006 — all warm rain parameters (autoconversion, accretion, etc.)
1010
- `air_properties::AP`: AirProperties — air properties for evaporation
1111
"""
12-
@kwdef struct WarmRainParams2M{SB, AP, CE} <: ParametersType
12+
@kwdef struct WarmRainParams2M{SB, AP, CE, SD} <: ParametersType
1313
seifert_beheng::SB
1414
air_properties::AP
1515
condevap::CE
16+
subdep::SD
1617
end
1718
# Construct WarmRainParams2M from a ClimaParams TOML dictionary
1819
WarmRainParams2M(toml_dict::CP.ParamDict; is_limited = true) =
1920
WarmRainParams2M(;
2021
seifert_beheng = SB2006(toml_dict; is_limited),
2122
air_properties = AirProperties(toml_dict),
2223
condevap = CondEvap2M(toml_dict),
24+
subdep = SubDep2M(toml_dict),
2325
)
2426

2527
Base.show(io::IO, mime::MIME"text/plain", x::WarmRainParams2M) =
@@ -28,19 +30,39 @@ Base.show(io::IO, mime::MIME"text/plain", x::WarmRainParams2M) =
2830
"""
2931
P3IceParams
3032
31-
Parameters for P3 ice-phase processes (optional).
33+
Parameters for P3 ice-phase processes.
3234
3335
# Fields
34-
- `scheme::P3`: ParametersP3 — P3 scheme parameters
35-
- `terminal_velocity::VL`: Chen2022VelType — terminal velocity for ice
36-
- `cloud_pdf::PDc`: CloudParticlePDF_SB2006 — cloud droplet size distribution
37-
- `rain_pdf::PDr`: RainParticlePDF_SB2006 — rain drop size distribution
36+
$(DocStringExtensions.FIELDS)
37+
38+
# Constructor
39+
40+
The main constructor is
41+
```
42+
P3IceParams(toml_dict::CP.ParamDict; is_limited = true)
43+
```
44+
which constructs the parameterization with components:
45+
- `scheme` = [`ParametersP3`](@ref)
46+
- `terminal_velocity` = [`Chen2022VelType`](@ref)
47+
- `cloud_pdf` = [`CloudParticlePDF_SB2006`](@ref)
48+
- `rain_pdf` = [`RainParticlePDF_SB2006`](@ref)
49+
- `heterogeneous` = [`Frostenberg2023`](@ref)
50+
- `deposition_condfreeze` = [`MorrisonMilbrandt2014`](@ref)
51+
3852
"""
39-
@kwdef struct P3IceParams{P3, VL, PDc, PDr} <: ParametersType
53+
@kwdef struct P3IceParams{P3, VL, PDc, PDr, HET, DEP} <: ParametersType
54+
"The core P3 scheme parameters"
4055
scheme::P3
56+
"The terminal velocity parameterization"
4157
terminal_velocity::VL
58+
"The cloud droplet size distribution"
4259
cloud_pdf::PDc
60+
"The rain drop size distribution"
4361
rain_pdf::PDr
62+
"The heterogeneous ice nucleation parameters"
63+
heterogeneous::HET
64+
"The deposition ice nucleation and condensation freezing parameters"
65+
deposition_condfreeze::DEP # TODO: Combine or split these structs?
4466
end
4567
Base.show(io::IO, mime::MIME"text/plain", x::P3IceParams) =
4668
ShowMethods.verbose_show_type_and_fields(io, mime, x)
@@ -51,6 +73,8 @@ P3IceParams(toml_dict::CP.ParamDict; is_limited = true) =
5173
terminal_velocity = Chen2022VelType(toml_dict),
5274
cloud_pdf = CloudParticlePDF_SB2006(toml_dict),
5375
rain_pdf = RainParticlePDF_SB2006(toml_dict; is_limited),
76+
heterogeneous = Frostenberg2023(toml_dict),
77+
deposition_condfreeze = MorrisonMilbrandt2014(toml_dict),
5478
)
5579

5680
"""

0 commit comments

Comments
 (0)