Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "CloudMicrophysics"
uuid = "6a9e3e04-43cd-43ba-94b9-e8782df3c71b"
authors = ["Climate Modeling Alliance"]
version = "0.31.11"
version = "0.31.12"

[deps]
ClimaParams = "5c42b081-d73a-476f-9059-fd94b934656c"
Expand Down
1 change: 1 addition & 0 deletions docs/src/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ BulkMicrophysicsTendencies.Microphysics1Moment
BulkMicrophysicsTendencies.Microphysics2Moment
BulkMicrophysicsTendencies.bulk_microphysics_tendencies
BulkMicrophysicsTendencies.bulk_microphysics_derivatives
BulkMicrophysicsTendencies.bulk_microphysics_cloud_derivatives
```

# P3 scheme
Expand Down
122 changes: 94 additions & 28 deletions src/BulkMicrophysicsTendencies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export MicrophysicsScheme,
Microphysics1Moment,
Microphysics2Moment,
bulk_microphysics_tendencies,
bulk_microphysics_derivatives
bulk_microphysics_derivatives,
bulk_microphysics_cloud_derivatives

#####
##### Singleton types for dispatch
Expand Down Expand Up @@ -317,10 +318,10 @@ as `bulk_microphysics_tendencies`.

# Returns
`NamedTuple` with fields:
- `∂dq_lcl`: Derivative of cloud liquid tendency w.r.t. q_lcl [1/s]
- `∂dq_icl`: Derivative of cloud ice tendency w.r.t. q_icl [1/s]
- `∂dq_rai`: Derivative of rain tendency w.r.t. q_rai [1/s]
- `∂dq_sno`: Derivative of snow tendency w.r.t. q_sno [1/s]
- `∂tendency_∂q_lcl`: Derivative of cloud liquid tendency w.r.t. q_lcl [1/s]
- `∂tendency_∂q_icl`: Derivative of cloud ice tendency w.r.t. q_icl [1/s]
- `∂tendency_∂q_rai`: Derivative of rain tendency w.r.t. q_rai [1/s]
- `∂tendency_∂q_sno`: Derivative of snow tendency w.r.t. q_sno [1/s]
"""
@inline function bulk_microphysics_derivatives(
::Microphysics1Moment,
Expand Down Expand Up @@ -352,29 +353,10 @@ as `bulk_microphysics_tendencies`.
aps = mp.air_properties
vel = mp.terminal_velocity

# --- Cloud liquid: condensation + sink self-derivatives ---
∂tendency_∂q_lcl = CMNonEq.∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cld(lcl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T)
# Autoconversion q_lcl → q_rai (sink, derivative ∂/∂q_lcl is conservative and safe for linear-above-threshold)
S_acnv_lcl = CM1.conv_q_lcl_to_q_rai(rai.acnv1M, q_lcl, true)
# Accretion q_lcl + q_rai → q_rai (rate is exactly linear in q_lcl)
S_accr_lcl_rai = CM1.accretion(lcl, rai, vel.rain, ce, q_lcl, q_rai, ρ)
# Accretion q_lcl + q_sno → q_sno (rate is exactly linear in q_lcl)
S_accr_lcl_sno = CM1.accretion(lcl, sno, vel.snow, ce, q_lcl, q_sno, ρ)
∂tendency_∂q_lcl -=
(S_acnv_lcl + S_accr_lcl_rai + S_accr_lcl_sno) /
max(q_lcl, UT.ϵ_numerics(typeof(q_lcl)))

# --- Cloud ice: deposition + sink self-derivatives ---
∂tendency_∂q_icl = CMNonEq.∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cld(icl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T)
# Autoconversion q_icl → q_sno
S_acnv_icl = CM1.conv_q_icl_to_q_sno_no_supersat(sno.acnv1M, q_icl, true)
# Accretion q_icl + q_rai → q_sno (rate is exactly linear in q_icl)
S_accr_icl_rai = CM1.accretion(icl, rai, vel.rain, ce, q_icl, q_rai, ρ)
# Accretion q_icl + q_sno → q_sno (rate is exactly linear in q_icl)
S_accr_icl_sno = CM1.accretion(icl, sno, vel.snow, ce, q_icl, q_sno, ρ)
∂tendency_∂q_icl -=
(S_acnv_icl + S_accr_icl_rai + S_accr_icl_sno) /
max(q_icl, UT.ϵ_numerics(typeof(q_icl)))
# --- Cloud condensate derivatives (reuse dedicated function) ---
(; ∂tendency_∂q_lcl, ∂tendency_∂q_icl) = bulk_microphysics_cloud_derivatives(
Microphysics1Moment(), mp, tps, ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno,
)

# --- Rain: evaporation + sink self-derivatives ---
∂tendency_∂q_rai =
Expand Down Expand Up @@ -411,6 +393,90 @@ as `bulk_microphysics_tendencies`.
return (; ∂tendency_∂q_lcl, ∂tendency_∂q_icl, ∂tendency_∂q_rai, ∂tendency_∂q_sno)
end

"""
bulk_microphysics_cloud_derivatives(
::Microphysics1Moment,
mp,
tps,
ρ,
T,
q_tot,
q_lcl,
q_icl,
q_rai,
q_sno,
)

Compute 1-moment cloud condensate Jacobian derivatives only.

Returns a NamedTuple with the leading-order derivative of each cloud species
tendency w.r.t. its own specific content. Precipitation derivatives are omitted
(the Jacobian uses quadrature-consistent S/q for precipitation instead).

This is cheaper than `bulk_microphysics_derivatives` because it skips rain
evaporation, snow sublimation/melting, and precipitation accretion derivatives
and avoids quadratures over the derivatives.

# Returns
`NamedTuple` with fields:
- `∂tendency_∂q_lcl`: Derivative of cloud liquid tendency w.r.t. q_lcl [1/s]
- `∂tendency_∂q_icl`: Derivative of cloud ice tendency w.r.t. q_icl [1/s]
"""
@inline function bulk_microphysics_cloud_derivatives(
::Microphysics1Moment,
mp::CMP.Microphysics1MParams,
tps,
ρ,
T,
q_tot,
q_lcl,
q_icl,
q_rai,
q_sno,
)
# Clamp negative inputs to zero (robustness against numerical errors)
ρ = UT.clamp_to_nonneg(ρ)
q_tot = UT.clamp_to_nonneg(q_tot)
q_lcl = UT.clamp_to_nonneg(q_lcl)
q_icl = UT.clamp_to_nonneg(q_icl)
q_rai = UT.clamp_to_nonneg(q_rai)
q_sno = UT.clamp_to_nonneg(q_sno)

# Unpack microphysics parameter container
lcl = mp.cloud.liquid
icl = mp.cloud.ice
rai = mp.precip.rain
sno = mp.precip.snow
ce = mp.collision
vel = mp.terminal_velocity

# --- Cloud liquid: condensation + sink self-derivatives ---
∂tendency_∂q_lcl = CMNonEq.∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cld(lcl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T)
# Autoconversion q_lcl → q_rai (sink)
S_acnv_lcl = CM1.conv_q_lcl_to_q_rai(rai.acnv1M, q_lcl, true)
# Accretion q_lcl + q_rai → q_rai (rate is exactly linear in q_lcl)
S_accr_lcl_rai = CM1.accretion(lcl, rai, vel.rain, ce, q_lcl, q_rai, ρ)
# Accretion q_lcl + q_sno → q_sno (rate is exactly linear in q_lcl)
S_accr_lcl_sno = CM1.accretion(lcl, sno, vel.snow, ce, q_lcl, q_sno, ρ)
∂tendency_∂q_lcl -=
(S_acnv_lcl + S_accr_lcl_rai + S_accr_lcl_sno) /
max(q_lcl, UT.ϵ_numerics(typeof(q_lcl)))

# --- Cloud ice: deposition + sink self-derivatives ---
∂tendency_∂q_icl = CMNonEq.∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cld(icl, tps, q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T)
# Autoconversion q_icl → q_sno
S_acnv_icl = CM1.conv_q_icl_to_q_sno_no_supersat(sno.acnv1M, q_icl, true)
# Accretion q_icl + q_rai → q_sno (rate is exactly linear in q_icl)
S_accr_icl_rai = CM1.accretion(icl, rai, vel.rain, ce, q_icl, q_rai, ρ)
# Accretion q_icl + q_sno → q_sno (rate is exactly linear in q_icl)
S_accr_icl_sno = CM1.accretion(icl, sno, vel.snow, ce, q_icl, q_sno, ρ)
∂tendency_∂q_icl -=
(S_acnv_icl + S_accr_icl_rai + S_accr_icl_sno) /
max(q_icl, UT.ϵ_numerics(typeof(q_icl)))

return (; ∂tendency_∂q_lcl, ∂tendency_∂q_icl)
end

# --- 2-Moment Microphysics derivatives ---

"""
Expand Down
127 changes: 127 additions & 0 deletions test/bulk_tendencies_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,132 @@ function test_bulk_microphysics_1m_derivatives(FT)
end
end

function test_bulk_microphysics_1m_cloud_derivatives(FT)
tps = TDI.TD.Parameters.ThermodynamicsParameters(FT)
mp = CMP.Microphysics1MParams(FT)
T_freeze = TDI.T_freeze(tps)

# Helper: call both derivative functions and assert cloud entries match
function assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
full = BMT.bulk_microphysics_derivatives(
BMT.Microphysics1Moment(),
mp, tps, ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno,
)
cloud = BMT.bulk_microphysics_cloud_derivatives(
BMT.Microphysics1Moment(),
mp, tps, ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno,
)
@test cloud.∂tendency_∂q_lcl == full.∂tendency_∂q_lcl
@test cloud.∂tendency_∂q_icl == full.∂tendency_∂q_icl
end

@testset "CloudDerivatives 1M - Return structure" begin
ρ = FT(1.2)
T = T_freeze + FT(5)
q_tot = FT(0.01)
q_lcl = FT(1e-3)
q_icl = FT(0)
q_rai = FT(1e-4)
q_sno = FT(0)

cloud = @inferred BMT.bulk_microphysics_cloud_derivatives(
BMT.Microphysics1Moment(),
mp, tps, ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno,
)
@test cloud isa @NamedTuple{∂tendency_∂q_lcl::FT, ∂tendency_∂q_icl::FT}
end

@testset "CloudDerivatives 1M - Match full derivs (warm)" begin
ρ = FT(1.2)
T = T_freeze + FT(10)
q_lcl = FT(2e-3)
q_icl = FT(0)
q_rai = FT(5e-4)
q_sno = FT(0)
q_tot = get_saturated_q_tot(tps, T, ρ, q_lcl, q_icl, q_rai, q_sno)

assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
end

@testset "CloudDerivatives 1M - Match full derivs (cold)" begin
ρ = FT(1.2)
T = T_freeze - FT(20)
q_lcl = FT(0)
q_icl = FT(1e-3)
q_rai = FT(0)
q_sno = FT(5e-4)
q_tot = FT(0.012)

assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
end

@testset "CloudDerivatives 1M - Match full derivs (mixed phase)" begin
ρ = FT(1.2)
T = T_freeze - FT(5)
q_lcl = FT(5e-4)
q_icl = FT(5e-4)
q_rai = FT(5e-4)
q_sno = FT(5e-4)
q_tot = FT(0.015)

assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
end

@testset "CloudDerivatives 1M - Match full derivs (tiny q)" begin
ρ = FT(1.2)
T = T_freeze + FT(3)
q_lcl = FT(1e-10)
q_icl = FT(1e-10)
q_rai = FT(1e-10)
q_sno = FT(1e-10)
q_tot = FT(0.01)

assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
end

@testset "CloudDerivatives 1M - Match full derivs (zero condensate)" begin
ρ = FT(1.2)
T = T_freeze + FT(7)
q_lcl = FT(0)
q_icl = FT(0)
q_rai = FT(0)
q_sno = FT(0)
q_tot = FT(0.01)

assert_cloud_derivs_match(ρ, T, q_tot, q_lcl, q_icl, q_rai, q_sno)
end

@testset "CloudDerivatives 1M - Physical sign checks" begin
# With cloud condensate present and precipitation sinks active, the net
# self-derivative of each cloud species should be negative (stable for
# implicit time-stepping): sinks (autoconversion, accretion) plus the
# negative condensation feedback (more cloud → less supersaturation)
# all drive ∂tendency/∂q < 0.
ρ = FT(1.2)

# Warm case: cloud liquid with rain
T_warm = T_freeze + FT(10)
q_lcl = FT(2e-3)
q_rai = FT(5e-4)
q_tot_warm = get_saturated_q_tot(tps, T_warm, ρ, q_lcl, FT(0), q_rai, FT(0))
cloud_warm = BMT.bulk_microphysics_cloud_derivatives(
BMT.Microphysics1Moment(),
mp, tps, ρ, T_warm, q_tot_warm, q_lcl, FT(0), q_rai, FT(0),
)
@test cloud_warm.∂tendency_∂q_lcl < FT(0)

# Cold case: cloud ice with snow
T_cold = T_freeze - FT(20)
q_icl = FT(1e-3)
q_sno = FT(5e-4)
cloud_cold = BMT.bulk_microphysics_cloud_derivatives(
BMT.Microphysics1Moment(),
mp, tps, ρ, T_cold, FT(0.012), FT(0), q_icl, FT(0), q_sno,
)
@test cloud_cold.∂tendency_∂q_icl < FT(0)
end
end

###
### 2M tendencies and derivatives tests
###
Expand Down Expand Up @@ -1253,6 +1379,7 @@ end
test_bulk_microphysics_0m_derivatives(FT)
test_bulk_microphysics_1m_tendencies(FT)
test_bulk_microphysics_1m_derivatives(FT)
test_bulk_microphysics_1m_cloud_derivatives(FT)
test_bulk_microphysics_2m_tendencies(FT)
test_bulk_microphysics_2m_derivatives(FT)
test_bulk_microphysics_p3_tendencies(FT)
Expand Down
Loading