Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #682 +/- ##
==========================================
+ Coverage 94.74% 94.80% +0.05%
==========================================
Files 53 53
Lines 2246 2270 +24
==========================================
+ Hits 2128 2152 +24
Misses 118 118
🚀 New features to boost your workflow:
|
2c09d30 to
d7f354a
Compare
|
I still need to check some signs. But please lmk if this is what we need. |
tapios
left a comment
There was a problem hiding this comment.
Looks like a great start! In the tests, we need to be careful to evaluate the derivatives at fixed e_tot and q_tot (and also make clear in the docs that this is what it is meant to be).
| T_freeze = TDI.TD.Parameters.T_freeze(tps) | ||
| T, p = T_freeze + FT(15), FT(90000) | ||
| ϵ = TDI.Rd_over_Rv(tps) | ||
| p_sat = TDI.saturation_vapor_pressure_over_liquid(tps, T) |
There was a problem hiding this comment.
You could use TD function for saturation specific humidity directly.
| ) | ||
| fd_deriv = (rate_p - rate) / Δq | ||
| TT.@test sign(drate) == sign(fd_deriv) | ||
| TT.@test drate ≈ fd_deriv rtol = FT(1.0) |
| ) | ||
| fd_deriv = (rate_p - rate) / Δq | ||
| TT.@test sign(drate) == sign(fd_deriv) | ||
| TT.@test drate ≈ fd_deriv rtol = FT(1.0) |
There was a problem hiding this comment.
Same here: Do tighter bounds work?
| # so we only check same sign and same order of magnitude | ||
| Δq = FT(1e-8) | ||
| (rate_p, _) = CM1.evaporation_sublimation( | ||
| rain, blk1mvel.rain, aps, tps, q_tot, q_lcl, q_icl, q_rai + Δq, q_sno, ρ, T, |
There was a problem hiding this comment.
This isn't testing quite the right derivative. We need to get the derivative at fixed q_tot (which you do here) and e_tot (which is not done here, instead T is fixed).
For the correct test, we'd need to wrap this in a function that keeps e_tot fixed but updates T.
| # so we only check same sign and same order of magnitude | ||
| Δq = FT(1e-8) | ||
| (rate_p, _) = CM1.evaporation_sublimation( | ||
| snow, blk1mvel.snow, aps, tps, q_tot, q_lcl, q_icl, q_rai, q_sno + Δq, ρ, T, |
There was a problem hiding this comment.
Same as above: we need to test at fixed e_tot, not fixed temperature.
|
|
||
| # Finite difference check | ||
| Δq = FT(1e-8) | ||
| (rate_p, _) = CM1.snow_melt(snow, blk1mvel.snow, aps, tps, q_sno + Δq, ρ, T) |
| dT_dq_liq = Lᵥ / cₚ | ||
|
|
||
| (S_plus, _, _) = CMNe.conv_q_vap_to_q_lcl_icl_MM2015( | ||
| liquid, tps, q_tot, q_lcl + ε, q_icl, q_rai, q_sno, ρ, T + ε * dT_dq_liq, |
There was a problem hiding this comment.
That's a better, taking first-order temperature change into account. Would be better to hold e_tot exactly fixed in a wrapper.
There was a problem hiding this comment.
Thanks for putting this PR together! Writing down these derivatives is a good step toward a reasonable implicit solver.
I do have some concerns with this design, in which all the process tendencies return a tuple:
- With the method names as is, I don't think it's intuitive that they now also return derivatives
- Different methods return different-length tuples
- it's not necessarily intuitive with respect to which variable(s) the derivatives are taken (unless you inspect the source code / docstring)
I would much prefer keeping the tendency methods as-is, and define new methods for the derivatives. For example:
# src/Microphysics1M.jl
function ∂accretion_∂q_lcl(::CMP.Acnv1M{FT}), q_lcl, smooth_transition = false) where {FT}
return FT(0)
end
# ...
function ∂evaporation_sublimation_∂q_rai(
::CMP.Rain{FT},
::CMP.Blk1MVelTypeRain{FT},
q_rai,
tendency,
) where {FT}
# derivative approximation: ∂(tendency)/∂q_rai ≈ tendency / q_rai
return q_rai > UT.ϵ_numerics(FT) ? tendency / q_rai : zero(tendency)
end# src/MicrophysicsNonEq.jl
∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cond_simple(p::CMP.CloudCondensateType) = -1 / τ_relax(p)
saturation_vapor_pressure(::CMP.CloudLiquid, tps::TDI.PS, T) =
TDI.saturation_vapor_pressure_over_liquid(tps, T)
saturation_vapor_pressure(::CMP.CloudIce, tps::TDI.PS, T) =
TDI.saturation_vapor_pressure_over_ice(tps, T)
saturation_specific_humidity(p::CMP.CloudCondensateType, tps::TDI.PS, ρ, T) =
TDI.p2q(tps, T, ρ, saturation_vapor_pressure(p, tps, T))
function ∂²qᵥ_sat_∂T²(tps::TDI.PS, p::CMP.CloudCondensateType, ρ, T)
Rᵥ = TDI.Rᵥ(tps)
Lᵥ = TDI.Lᵥ(tps, T)
qᵥ_sat = saturation_specific_humidity(p, tps, ρ, T)
return qᵥ_sat * ((Lᵥ / Rᵥ / T^2 - 1/T)^2 + (1/T^2 - 2 * Lᵥ / Rᵥ / T^3))
end
function ∂qᵥ_sat_∂T(tps::TDI.PS, p::CMP.CloudCondensateType, ρ, T)
Rᵥ = TDI.Rᵥ(tps)
Lᵥ = TDI.Lᵥ(tps, T)
qᵥ_sat = saturation_specific_humidity(p, tps, ρ, T)
return qᵥ_sat * (Lᵥ / (Rᵥ * T^2) - 1 / T)
end
function ∂conv_q_vap_to_q_lcl_icl_MM2015_∂q_cond(
p::CMP.CloudCondensateType, tps::TDI.PS,
q_tot, q_lcl, q_icl, q_rai, q_sno, ρ, T,
tendency,
)
∂qᵥs_dT = ∂qᵥ_sat_∂T(tps, p, ρ, T)
∂²qᵥs_∂T² = ∂²qᵥ_sat_∂T²(tps, p, ρ, T)
cₚ_air = TDI.cpₘ(tps, q_tot, q_lcl + q_rai, q_icl + q_sno)
Γ = 1 + (Lᵥ / cₚ_air) * ∂qᵥs_dT
return - 1 / τ_relax(p) - tendency / Γ * (Lᵥ / cₚ_air)^2 * ∂²qᵥs_∂T²
end
Along the same lines: We will need the derivatives at different call sites (Jacobian) than the tendencies themselves. So it's probably better, and better to avoid duplicate computations, to have separate functions for the derivatives. Externally, we will only need the fused tendencies and their approximate derivatives, which it would be good to have as separate functions. |
This PR adds tendency derivatives for the 1-moment microphysics and non-equilibrium cloud formation. They are to be used in the implicit solver in the Atmos model.
For each of the above processes the function now returns the rate and rate derivative. The Bulk microphysics tendency also returns the sum of tendencies and the sum of derivatives.
I have added to the documentation pages and included some plots. I have also added to the tests, including some tests based on finite-difference "manual" calculation of the derivatives.
Going forward, it would not be hard to add more analytical terms for the derivatives. Or, in case we want to make the derivative free of having to redo the math if we change the parameterization functions, I could code all of those as a finite difference numerical computation (something like
deriv = (micro_func(q+eps) - micro_func(q)) / eps)