Repository navigation
Add the largest P3 mean ice mass and the Frostenberg 2023 deposition timescale - #336
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #336 +/- ##
=======================================
Coverage 97.26% 97.26%
=======================================
Files 1 1
Lines 146 146
=======================================
Hits 142 142
Misses 4 4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
haakon-e
force-pushed
the
he/p3-microphysics-constants
branch
from
October 19, 2026 01:29
93f8353 to
0165b57
Compare
…timescale Add the largest mean ice particle mass of the P3 size distribution, which CloudMicrophysics hard-codes in its P3 ice scheme. Add the relaxation timescale of Frostenberg 2023 deposition nucleation, so that CloudMicrophysics can read it from the parameter file instead of its source. The activation conditions of that nucleation are the existing P3 deposition keys P3_cooper_deposition_temperature_threshold and P3_cooper_deposition_ice_supersaturation_threshold. Bump the version to 1.3.4.
haakon-e
force-pushed
the
he/p3-microphysics-constants
branch
from
October 21, 2026 21:45
0165b57 to
20649fc
Compare
haakon-e
added a commit
to CliMA/CloudMicrophysics.jl
that referenced
this pull request
Oct 21, 2026
The P3 ice in one cell is split by what each part depends on. `P3Particles(params, F_rim, ρ_rim)` holds the properties of a particle of size `D`: the parameters, the rime mass fraction, the rime density, the graupel density, and the size thresholds of the mass relation. These depend on the two intensive rime quantities alone, not on the amount of ice, so table generation can build them from table coordinates. `P3Distribution(ρn, μ, logλ, logN₀)` describes the population. `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` is the single constructor from the prognostic variables. It builds the particles, then the distribution with `P3Distribution(particles, integration, ρq_ice, ρn_ice)`, then the state, each once. The slope parameter is a function of the ice moments and the rime state, so the distribution derives it instead of every caller carrying it. The construction bounds the ice number so that the mean particle mass lies between the mass of a newly nucleated crystal and a largest mean mass, which guarantees a root inside the search interval of the slope solve. The solve integrates in closed form with a fixed iteration budget and no warm start, uses primal values, and stores a primal slope, so the slope is constant under automatic differentiation with respect to the state. The bounded number shapes the size distribution only; the state keeps the prognostic number, and the number tendencies of melting and of self-collection are fractional rates of the distribution times the prognostic number, so that a step removes at most the number that exists. `integration` says how the size-distribution integrals are evaluated: a quadrature rule now, and a lookup table later, whose `P3Distribution` method returns no distribution. In a simulation it is fixed for the run, and the state and its distribution are built once per cell and used together. The functions that integrate over the size distribution therefore take the state and read the particles, the integration method and the distribution from it, so a call cannot pair a state with a distribution it was not built with. The functions of a single particle, such as `ice_mass`, `ice_area`, `ϕᵢ` and `ice_particle_terminal_velocity`, take the particles. The integration method has no default, because a default hides which rule a caller integrates with, and the defaults differed between functions. The 2M+P3 tendencies build the state with `mp.ice.integration`. The arguments of these functions follow one order: the parameters, the particles or the state, the other species, and the air state. Type annotations remain where they select a method. `ice_terminal_velocities(velocity_params, state, ρₐ; p)` returns the number- and mass-weighted fall speeds `(; v_n, v_m)` from one quadrature pass. Each is the ratio of two integrals on the same nodes, `∫ n v / ∫ n` and `∫ n m v / ∫ n m`, so it is a weighted mean of the particle fall speed, and the errors of the rule and of the truncated bounds cancel to first order. Against an adaptive reference, the mass-weighted fall speed with 12 Gauss-Legendre nodes is about three times more accurate than with the closed-form normalization. The ice density of the particle fall speed is `params.ρ_i`. `P3State` stores the prognostic rime volume `ρb_rim`, clamped to be nonnegative, because the densification of rime relaxes the prognostic variable; the moved, admissible value enters only `F_rim` and `ρ_rim`. `D_m(particles, dist)` is the ratio of the first to the zeroth mass moment of the distribution, or zero without ice number. Each derived particle quantity has one method, from the parameters and the intensive rime quantities. The largest mean ice mass is the ClimaParams key `P3_ice_largest_mean_particle_mass`, read into the new field `m_max` of `ParametersP3`. `ice_moments_are_admissible` and `rime_density_bounds` expose the limits that the state construction applies, so that a host checking its prognostic ice state uses the same limits as the scheme. The P3 section of the API page lists the functions a model calls, and the new P3 internals page lists their building blocks, which can change without notice. The plot script `RainEvapoartionSB2006.jl` is renamed to `RainEvaporationSB2006.jl`. Known limitations: - `P3_ice_largest_mean_particle_mass` is not in a ClimaParams release yet (CliMA/ClimaParams.jl#336), so CI needs that release; the compat entry is unchanged until then. - The Jacobian of the 2M+P3 tendencies holds the slope fixed in the ice columns, so the finite-difference check of the Jacobian covers the liquid columns only. Breaking changes: - `ParametersP3` has the field `m_max`. - `state_from_prognostic` is removed; `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` replaces it and the former `P3State(params, ρq_ice, ρn_ice, F_rim, ρ_rim)`. - `P3State` has the fields `particles`, `integration`, `ρq_ice`, `ρn_ice`, `ρb_rim` and `dist`; `F_rim`, `ρ_rim`, `ρ_g`, `D_th`, `D_gr`, `D_cr` and `params` move to `state.particles`. - `P3IceParams` and `Microphysics2MParams` take the keyword `integration` in place of `quad`, and the field `mp.ice.quad` is `mp.ice.integration`. - The number tendencies of `ice_melt` and `ice_self_collection` scale with the prognostic number. - `isunrimed`, `segment_boundaries`, `regime_value`, `ice_mass_coeffs`, `ice_mass`, `ice_density`, `ice_area`, `ϕ_material_density`, `ϕᵢ`, `logmass_gamma_moment` and `logLdivN` take a `P3Particles` in place of a `P3State`. - `ice_particle_terminal_velocity(velocity_params, particles, ρₐ)`, `volumetric_collision_rate_integrand(velocity_params, particles, ρₐ)`, `compute_max_freeze_rate(aps, tps, velocity_params, particles, ρₐ, Tₐ)` and `compute_local_rime_density(velocity_params, particles, ρₐ, T)` take the particles before the air state. - `get_ρ_d(mass, F_rim, ρ_rim)`, `get_ρ_g(mass, F_rim, ρ_rim)`, `get_D_th(mass, ρ_i)` and `get_μ(slope, logλ)` are the only methods; the methods of a state or of the parameters, and `get_ρ_g(F_rim, ρ_rim, ρ_d)`, are removed. - `get_distribution_logλ(state, [logλ_guess])` becomes `get_distribution_logλ(particles, ρq_ice, ρn_ice)`, without a guess, and `get_distribution_logλ_from_prognostic` is removed; read `state.dist.logλ` instead. - `logN′ice(dist)` and `size_distribution(dist)` take the distribution; `integral_bounds(particles, dist; p)`, `velocity_integral_bounds(particles, dist, v_term; p)` and `D_m(particles, dist)` take the particles and the distribution, without a `logλ` argument. - `ice_terminal_velocity_number_weighted`, `ice_terminal_velocity_mass_weighted` and their `*_from_prognostic` wrappers are removed; `ice_terminal_velocities` replaces them. - The functions that integrate over the size distribution take the state, without a `logλ` argument and without a rule or `quad` keyword: - `ice_melt(velocity_params, aps, tps, state, ρₐ, Tₐ)` - `bulk_liquid_ice_collision_sources(psd_c, psd_r, aps, tps, vel, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `∫liquid_ice_collisions(psd_c, psd_r, aps, tps, vel, m_liq, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `ice_self_collection(vel, state, ρₐ)` - The integration helpers take the quadrature rule as their last positional argument, without a default: - `∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds, quad)` - `get_liquid_integrals(n, ∂ₜV, m_liq, ρ′_rim, liq_bounds, quad)` - `get_liquid_integrals_rain_closed(psd_r, vel, n_r, ρₐ, L_r, N_r, particles, ∂ₜV, m_liq, ρ′_rim, bounds_r, quad)` - `∂ice_mass_∂D` and `get_∂mass_∂D_coeffs` are removed. - `bulk_microphysics_tendencies(::Microphysics2Moment, ...)` loses the positional `logλ` argument after `b_rim`, in both the P3 ice method and the warm-rain-only method. The optional arguments that follow it move one position forward, so a caller that still passes `logλ` passes it as `inpc_log_shift`.
haakon-e
added a commit
to CliMA/CloudMicrophysics.jl
that referenced
this pull request
Oct 21, 2026
The P3 ice in one cell is split by what each part depends on. `P3Particles(params, F_rim, ρ_rim)` holds the properties of a particle of size `D`: the parameters, the rime mass fraction, the rime density, the graupel density, and the size thresholds of the mass relation. These depend on the two intensive rime quantities alone, not on the amount of ice, so table generation can build them from table coordinates. `P3Distribution(ρn, μ, logλ, logN₀)` describes the population. `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` is the single constructor from the prognostic variables. It builds the particles, then the distribution with `P3Distribution(particles, integration, ρq_ice, ρn_ice)`, then the state, each once. The slope parameter is a function of the ice moments and the rime state, so the distribution derives it instead of every caller carrying it. The construction bounds the ice number so that the mean particle mass lies between the mass of a newly nucleated crystal and a largest mean mass, which guarantees a root inside the search interval of the slope solve. The solve integrates in closed form with a fixed iteration budget and no warm start, uses primal values, and stores a primal slope, so the slope is constant under automatic differentiation with respect to the state. The bounded number shapes the size distribution only; the state keeps the prognostic number, and the number tendencies of melting and of self-collection are fractional rates of the distribution times the prognostic number, so that a step removes at most the number that exists. `integration` says how the size-distribution integrals are evaluated: a quadrature rule now, and a lookup table later, whose `P3Distribution` method returns no distribution. In a simulation it is fixed for the run, and the state and its distribution are built once per cell and used together. The functions that integrate over the size distribution therefore take the state and read the particles, the integration method and the distribution from it, so a call cannot pair a state with a distribution it was not built with. The functions of a single particle, such as `ice_mass`, `ice_area`, `ϕᵢ` and `ice_particle_terminal_velocity`, take the particles. The integration method has no default, because a default hides which rule a caller integrates with, and the defaults differed between functions. The 2M+P3 tendencies build the state with `mp.ice.integration`. The quadrature rules carry their float type: `ChebyshevGauss(FT, n)` like `GaussLegendre(FT, n)`, both `Float64` by default, and `node(quad, i)` and `weight(quad, i)` take the integer index. The Gauss-Legendre lookup therefore has no float-to-integer conversion and no error path, which a GPU kernel cannot compile in Float32. The arguments of these functions follow one order: the parameters, the particles or the state, the other species, and the air state. Type annotations remain where they select a method. `ice_terminal_velocities(velocity_params, state, ρₐ; p)` returns the number- and mass-weighted fall speeds `(; v_n, v_m)` from one quadrature pass. Each is the ratio of two integrals on the same nodes, `∫ n v / ∫ n` and `∫ n m v / ∫ n m`, so it is a weighted mean of the particle fall speed, and the errors of the rule and of the truncated bounds cancel to first order. Against an adaptive reference, the mass-weighted fall speed with 12 Gauss-Legendre nodes is about three times more accurate than with the closed-form normalization. The ice density of the particle fall speed is `params.ρ_i`. `P3State` stores the prognostic rime volume `ρb_rim`, clamped to be nonnegative, because the densification of rime relaxes the prognostic variable; the moved, admissible value enters only `F_rim` and `ρ_rim`. `D_m(particles, dist)` is the ratio of the first to the zeroth mass moment of the distribution, or zero without ice number. Each derived particle quantity has one method, from the parameters and the intensive rime quantities. The largest mean ice mass is the ClimaParams key `P3_ice_largest_mean_particle_mass`, read into the new field `m_max` of `ParametersP3`. `ice_moments_are_admissible` and `rime_density_bounds` expose the limits that the state construction applies, so that a host checking its prognostic ice state uses the same limits as the scheme. The P3 section of the API page lists the functions a model calls, and the new P3 internals page lists their building blocks, which can change without notice. The plot script `RainEvapoartionSB2006.jl` is renamed to `RainEvaporationSB2006.jl`. Known limitations: - `P3_ice_largest_mean_particle_mass` is not in a ClimaParams release yet (CliMA/ClimaParams.jl#336), so CI needs that release; the compat entry is unchanged until then. - The Jacobian of the 2M+P3 tendencies holds the slope fixed in the ice columns, so the finite-difference check of the Jacobian covers the liquid columns only. Breaking changes: - `ParametersP3` has the field `m_max`. - `state_from_prognostic` is removed; `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` replaces it and the former `P3State(params, ρq_ice, ρn_ice, F_rim, ρ_rim)`. - `P3State` has the fields `particles`, `integration`, `ρq_ice`, `ρn_ice`, `ρb_rim` and `dist`; `F_rim`, `ρ_rim`, `ρ_g`, `D_th`, `D_gr`, `D_cr` and `params` move to `state.particles`. - `P3IceParams` and `Microphysics2MParams` take the keyword `integration` in place of `quad`, and the field `mp.ice.quad` is `mp.ice.integration`. - The number tendencies of `ice_melt` and `ice_self_collection` scale with the prognostic number. - `isunrimed`, `segment_boundaries`, `regime_value`, `ice_mass_coeffs`, `ice_mass`, `ice_density`, `ice_area`, `ϕ_material_density`, `ϕᵢ`, `logmass_gamma_moment` and `logLdivN` take a `P3Particles` in place of a `P3State`. - `ice_particle_terminal_velocity(velocity_params, particles, ρₐ)`, `volumetric_collision_rate_integrand(velocity_params, particles, ρₐ)`, `compute_max_freeze_rate(aps, tps, velocity_params, particles, ρₐ, Tₐ)` and `compute_local_rime_density(velocity_params, particles, ρₐ, T)` take the particles before the air state. - `get_ρ_d(mass, F_rim, ρ_rim)`, `get_ρ_g(mass, F_rim, ρ_rim)`, `get_D_th(mass, ρ_i)` and `get_μ(slope, logλ)` are the only methods; the methods of a state or of the parameters, and `get_ρ_g(F_rim, ρ_rim, ρ_d)`, are removed. - `get_distribution_logλ(state, [logλ_guess])` becomes `get_distribution_logλ(particles, ρq_ice, ρn_ice)`, without a guess, and `get_distribution_logλ_from_prognostic` is removed; read `state.dist.logλ` instead. - `logN′ice(dist)` and `size_distribution(dist)` take the distribution; `integral_bounds(particles, dist; p)`, `velocity_integral_bounds(particles, dist, v_term; p)` and `D_m(particles, dist)` take the particles and the distribution, without a `logλ` argument. - `ice_terminal_velocity_number_weighted`, `ice_terminal_velocity_mass_weighted` and their `*_from_prognostic` wrappers are removed; `ice_terminal_velocities` replaces them. - The functions that integrate over the size distribution take the state, without a `logλ` argument and without a rule or `quad` keyword: - `ice_melt(velocity_params, aps, tps, state, ρₐ, Tₐ)` - `bulk_liquid_ice_collision_sources(psd_c, psd_r, aps, tps, vel, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `∫liquid_ice_collisions(psd_c, psd_r, aps, tps, vel, m_liq, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `ice_self_collection(vel, state, ρₐ)` - The integration helpers take the quadrature rule as their last positional argument, without a default: - `∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds, quad)` - `get_liquid_integrals(n, ∂ₜV, m_liq, ρ′_rim, liq_bounds, quad)` - `get_liquid_integrals_rain_closed(psd_r, vel, n_r, ρₐ, L_r, N_r, particles, ∂ₜV, m_liq, ρ′_rim, bounds_r, quad)` - `∂ice_mass_∂D` and `get_∂mass_∂D_coeffs` are removed. - `ChebyshevGauss` has the type parameter `FT`, and `node(quad, i)` and `weight(quad, i)` take the integer index in place of `(quad, FT(i), n)`. - `bulk_microphysics_tendencies(::Microphysics2Moment, ...)` loses the positional `logλ` argument after `b_rim`, in both the P3 ice method and the warm-rain-only method. The optional arguments that follow it move one position forward, so a caller that still passes `logλ` passes it as `inpc_log_shift`.
haakon-e
added a commit
to CliMA/CloudMicrophysics.jl
that referenced
this pull request
Oct 21, 2026
The P3 ice in one cell is split by what each part depends on. `P3Particles(params, F_rim, ρ_rim)` holds the properties of a particle of size `D`: the parameters, the rime mass fraction, the rime density, the graupel density, and the size thresholds of the mass relation. These depend on the two intensive rime quantities alone, not on the amount of ice, so table generation can build them from table coordinates. `P3Distribution(ρn, μ, logλ, logN₀)` describes the population. `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` is the single constructor from the prognostic variables. It builds the particles, then the distribution with `P3Distribution(particles, integration, ρq_ice, ρn_ice)`, then the state, each once. The slope parameter is a function of the ice moments and the rime state, so the distribution derives it instead of every caller carrying it. The construction bounds the ice number so that the mean particle mass lies between the mass of a newly nucleated crystal and a largest mean mass, which guarantees a root inside the search interval of the slope solve. The solve integrates in closed form with a fixed iteration budget and no warm start, uses primal values, and stores a primal slope, so the slope is constant under automatic differentiation with respect to the state. The bounded number shapes the size distribution only; the state keeps the prognostic number, and the number tendencies of melting and of self-collection are fractional rates of the distribution times the prognostic number, so that a step removes at most the number that exists. `integration` says how the size-distribution integrals are evaluated: a quadrature rule now, and a lookup table later, whose `P3Distribution` method returns no distribution. In a simulation it is fixed for the run, and the state and its distribution are built once per cell and used together. The functions that integrate over the size distribution therefore take the state and read the particles, the integration method and the distribution from it, so a call cannot pair a state with a distribution it was not built with. The functions of a single particle, such as `ice_mass`, `ice_area`, `ϕᵢ` and `ice_particle_terminal_velocity`, take the particles. The integration method has no default, because a default hides which rule a caller integrates with, and the defaults differed between functions. The 2M+P3 tendencies build the state with `mp.ice.integration`. The quadrature rules carry their float type: `ChebyshevGauss(FT, n)` like `GaussLegendre(FT, n)`, both `Float64` by default, and `node(quad, i)` and `weight(quad, i)` take the integer index. The Gauss-Legendre lookup therefore has no float-to-integer conversion and no error path, which a GPU kernel cannot compile in Float32. The arguments of these functions follow one order: the parameters, the particles or the state, the other species, and the air state. Type annotations remain where they select a method. `ice_terminal_velocities(velocity_params, state, ρₐ; p)` returns the number- and mass-weighted fall speeds `(; v_n, v_m)` from one quadrature pass. Each is the ratio of two integrals on the same nodes, `∫ n v / ∫ n` and `∫ n m v / ∫ n m`, so it is a weighted mean of the particle fall speed, and the errors of the rule and of the truncated bounds cancel to first order. Against an adaptive reference, the mass-weighted fall speed with 12 Gauss-Legendre nodes is about three times more accurate than with the closed-form normalization. The ice density of the particle fall speed is `params.ρ_i`. `P3State` stores the prognostic rime volume `ρb_rim`, clamped to be nonnegative, because the densification of rime relaxes the prognostic variable; the moved, admissible value enters only `F_rim` and `ρ_rim`. `D_m(particles, dist)` is the ratio of the first to the zeroth mass moment of the distribution, or zero without ice number. Each derived particle quantity has one method, from the parameters and the intensive rime quantities. The largest mean ice mass is the ClimaParams key `P3_ice_largest_mean_particle_mass`, read into the new field `m_max` of `ParametersP3`. `ice_moments_are_admissible` and `rime_density_bounds` expose the limits that the state construction applies, so that a host checking its prognostic ice state uses the same limits as the scheme. The P3 section of the API page lists the functions a model calls, and the new P3 internals page lists their building blocks, which can change without notice. The plot script `RainEvapoartionSB2006.jl` is renamed to `RainEvaporationSB2006.jl`. Known limitations: - `P3_ice_largest_mean_particle_mass` is not in a ClimaParams release yet (CliMA/ClimaParams.jl#336), so CI needs that release; the compat entry is unchanged until then. - The Jacobian of the 2M+P3 tendencies holds the slope fixed in the ice columns, so the finite-difference check of the Jacobian covers the liquid columns only. Breaking changes: - `ParametersP3` has the field `m_max`. - `state_from_prognostic` is removed; `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` replaces it and the former `P3State(params, ρq_ice, ρn_ice, F_rim, ρ_rim)`. - `P3State` has the fields `particles`, `integration`, `ρq_ice`, `ρn_ice`, `ρb_rim` and `dist`; `F_rim`, `ρ_rim`, `ρ_g`, `D_th`, `D_gr`, `D_cr` and `params` move to `state.particles`. - `P3IceParams` and `Microphysics2MParams` take the keyword `integration` in place of `quad`, and the field `mp.ice.quad` is `mp.ice.integration`. - The number tendencies of `ice_melt` and `ice_self_collection` scale with the prognostic number. - `isunrimed`, `segment_boundaries`, `regime_value`, `ice_mass_coeffs`, `ice_mass`, `ice_density`, `ice_area`, `ϕ_material_density`, `ϕᵢ`, `logmass_gamma_moment` and `logLdivN` take a `P3Particles` in place of a `P3State`. - `ice_particle_terminal_velocity(velocity_params, particles, ρₐ)`, `volumetric_collision_rate_integrand(velocity_params, particles, ρₐ)`, `compute_max_freeze_rate(aps, tps, velocity_params, particles, ρₐ, Tₐ)` and `compute_local_rime_density(velocity_params, particles, ρₐ, T)` take the particles before the air state. - `get_ρ_d(mass, F_rim, ρ_rim)`, `get_ρ_g(mass, F_rim, ρ_rim)`, `get_D_th(mass, ρ_i)` and `get_μ(slope, logλ)` are the only methods; the methods of a state or of the parameters, and `get_ρ_g(F_rim, ρ_rim, ρ_d)`, are removed. - `get_distribution_logλ(state, [logλ_guess])` becomes `get_distribution_logλ(particles, ρq_ice, ρn_ice)`, without a guess, and `get_distribution_logλ_from_prognostic` is removed; read `state.dist.logλ` instead. - `logN′ice(dist)` and `size_distribution(dist)` take the distribution; `integral_bounds(particles, dist; p)`, `velocity_integral_bounds(particles, dist, v_term; p)` and `D_m(particles, dist)` take the particles and the distribution, without a `logλ` argument. - `ice_terminal_velocity_number_weighted`, `ice_terminal_velocity_mass_weighted` and their `*_from_prognostic` wrappers are removed; `ice_terminal_velocities` replaces them. - The functions that integrate over the size distribution take the state, without a `logλ` argument and without a rule or `quad` keyword: - `ice_melt(velocity_params, aps, tps, state, ρₐ, Tₐ)` - `bulk_liquid_ice_collision_sources(psd_c, psd_r, aps, tps, vel, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `∫liquid_ice_collisions(psd_c, psd_r, aps, tps, vel, m_liq, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `ice_self_collection(vel, state, ρₐ)` - The integration helpers take the quadrature rule as their last positional argument, without a default: - `∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds, quad)` - `get_liquid_integrals(n, ∂ₜV, m_liq, ρ′_rim, liq_bounds, quad)` - `get_liquid_integrals_rain_closed(psd_r, vel, n_r, ρₐ, L_r, N_r, particles, ∂ₜV, m_liq, ρ′_rim, bounds_r, quad)` - `∂ice_mass_∂D` and `get_∂mass_∂D_coeffs` are removed. - `ChebyshevGauss` has the type parameter `FT`, and `node(quad, i)` and `weight(quad, i)` take the integer index in place of `(quad, FT(i), n)`. - `bulk_microphysics_tendencies(::Microphysics2Moment, ...)` loses the positional `logλ` argument after `b_rim`, in both the P3 ice method and the warm-rain-only method. The optional arguments that follow it move one position forward, so a caller that still passes `logλ` passes it as `inpc_log_shift`.
sajjadazimi
approved these changes
Oct 22, 2026
haakon-e
added a commit
to CliMA/CloudMicrophysics.jl
that referenced
this pull request
Oct 22, 2026
The P3 ice in one cell is split by what each part depends on. `P3Particles(params, F_rim, ρ_rim)` holds the properties of a particle of size `D`: the parameters, the rime mass fraction, the rime density, the graupel density, and the size thresholds of the mass relation. These depend on the two intensive rime quantities alone, not on the amount of ice, so table generation can build them from table coordinates. `P3Distribution(ρn, μ, logλ, logN₀)` describes the population. `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` is the single constructor from the prognostic variables. It builds the particles, then the distribution with `P3Distribution(particles, integration, ρq_ice, ρn_ice)`, then the state, each once. The slope parameter is a function of the ice moments and the rime state, so the distribution derives it instead of every caller carrying it. The construction bounds the ice number so that the mean particle mass lies between the mass of a newly nucleated crystal and a largest mean mass, which guarantees a root inside the search interval of the slope solve. The solve integrates in closed form with a fixed iteration budget and no warm start, uses primal values, and stores a primal slope, so the slope is constant under automatic differentiation with respect to the state. The bounded number shapes the size distribution only; the state keeps the prognostic number, and the number tendencies of melting and of self-collection are fractional rates of the distribution times the prognostic number, so that a step removes at most the number that exists. `integration` says how the size-distribution integrals are evaluated: a quadrature rule now, and a lookup table later, whose `P3Distribution` method returns no distribution. In a simulation it is fixed for the run, and the state and its distribution are built once per cell and used together. The functions that integrate over the size distribution therefore take the state and read the particles, the integration method and the distribution from it, so a call cannot pair a state with a distribution it was not built with. The functions of a single particle, such as `ice_mass`, `ice_area`, `ϕᵢ` and `ice_particle_terminal_velocity`, take the particles. The integration method has no default, because a default hides which rule a caller integrates with, and the defaults differed between functions. The 2M+P3 tendencies build the state with `mp.ice.integration`. The quadrature rules carry their float type: `ChebyshevGauss(FT, n)` like `GaussLegendre(FT, n)`, both `Float64` by default, and `node(quad, i)` and `weight(quad, i)` take the integer index. The Gauss-Legendre lookup therefore has no float-to-integer conversion and no error path, which a GPU kernel cannot compile in Float32. The arguments of these functions follow one order: the parameters, the particles or the state, the other species, and the air state. Type annotations remain where they select a method. `ice_terminal_velocities(velocity_params, state, ρₐ; p)` returns the number- and mass-weighted fall speeds `(; v_n, v_m)` from one quadrature pass. Each is the ratio of two integrals on the same nodes, `∫ n v / ∫ n` and `∫ n m v / ∫ n m`, so it is a weighted mean of the particle fall speed, and the errors of the rule and of the truncated bounds cancel to first order. Against an adaptive reference, the mass-weighted fall speed with 12 Gauss-Legendre nodes is about three times more accurate than with the closed-form normalization. The ice density of the particle fall speed is `params.ρ_i`. `P3State` stores the prognostic rime volume `ρb_rim`, clamped to be nonnegative, because the densification of rime relaxes the prognostic variable; the moved, admissible value enters only `F_rim` and `ρ_rim`. `D_m(particles, dist)` is the ratio of the first to the zeroth mass moment of the distribution, or zero without ice number. Each derived particle quantity has one method, from the parameters and the intensive rime quantities. The largest mean ice mass is the ClimaParams key `P3_ice_largest_mean_particle_mass`, read into the new field `m_max` of `ParametersP3`. `ice_moments_are_admissible` and `rime_density_bounds` expose the limits that the state construction applies, so that a host checking its prognostic ice state uses the same limits as the scheme. The P3 section of the API page lists the functions a model calls, and the new P3 internals page lists their building blocks, which can change without notice. The plot script `RainEvapoartionSB2006.jl` is renamed to `RainEvaporationSB2006.jl`. Known limitations: - `P3_ice_largest_mean_particle_mass` is not in a ClimaParams release yet (CliMA/ClimaParams.jl#336), so CI needs that release; the compat entry is unchanged until then. - The Jacobian of the 2M+P3 tendencies holds the slope fixed in the ice columns, so the finite-difference check of the Jacobian covers the liquid columns only. Breaking changes: - `ParametersP3` has the field `m_max`. - `state_from_prognostic` is removed; `P3State(params, integration, ρq_ice, ρn_ice, ρq_rim, ρb_rim)` replaces it and the former `P3State(params, ρq_ice, ρn_ice, F_rim, ρ_rim)`. - `P3State` has the fields `particles`, `integration`, `ρq_ice`, `ρn_ice`, `ρb_rim` and `dist`; `F_rim`, `ρ_rim`, `ρ_g`, `D_th`, `D_gr`, `D_cr` and `params` move to `state.particles`. - `P3IceParams` and `Microphysics2MParams` take the keyword `integration` in place of `quad`, and the field `mp.ice.quad` is `mp.ice.integration`. - The number tendencies of `ice_melt` and `ice_self_collection` scale with the prognostic number. - `isunrimed`, `segment_boundaries`, `regime_value`, `ice_mass_coeffs`, `ice_mass`, `ice_density`, `ice_area`, `ϕ_material_density`, `ϕᵢ`, `logmass_gamma_moment` and `logLdivN` take a `P3Particles` in place of a `P3State`. - `ice_particle_terminal_velocity(velocity_params, particles, ρₐ)`, `volumetric_collision_rate_integrand(velocity_params, particles, ρₐ)`, `compute_max_freeze_rate(aps, tps, velocity_params, particles, ρₐ, Tₐ)` and `compute_local_rime_density(velocity_params, particles, ρₐ, T)` take the particles before the air state. - `get_ρ_d(mass, F_rim, ρ_rim)`, `get_ρ_g(mass, F_rim, ρ_rim)`, `get_D_th(mass, ρ_i)` and `get_μ(slope, logλ)` are the only methods; the methods of a state or of the parameters, and `get_ρ_g(F_rim, ρ_rim, ρ_d)`, are removed. - `get_distribution_logλ(state, [logλ_guess])` becomes `get_distribution_logλ(particles, ρq_ice, ρn_ice)`, without a guess, and `get_distribution_logλ_from_prognostic` is removed; read `state.dist.logλ` instead. - `logN′ice(dist)` and `size_distribution(dist)` take the distribution; `integral_bounds(particles, dist; p)`, `velocity_integral_bounds(particles, dist, v_term; p)` and `D_m(particles, dist)` take the particles and the distribution, without a `logλ` argument. - `ice_terminal_velocity_number_weighted`, `ice_terminal_velocity_mass_weighted` and their `*_from_prognostic` wrappers are removed; `ice_terminal_velocities` replaces them. - The functions that integrate over the size distribution take the state, without a `logλ` argument and without a rule or `quad` keyword: - `ice_melt(velocity_params, aps, tps, state, ρₐ, Tₐ)` - `bulk_liquid_ice_collision_sources(psd_c, psd_r, aps, tps, vel, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `∫liquid_ice_collisions(psd_c, psd_r, aps, tps, vel, m_liq, state, L_c, N_c, L_r, N_r, ρₐ, T)` - `ice_self_collection(vel, state, ρₐ)` - The integration helpers take the quadrature rule as their last positional argument, without a default: - `∫liquid_ice_collisions(n_i, ∂ₜM_max, cloud_integrals, rain_integrals, ice_bounds, quad)` - `get_liquid_integrals(n, ∂ₜV, m_liq, ρ′_rim, liq_bounds, quad)` - `get_liquid_integrals_rain_closed(psd_r, vel, n_r, ρₐ, L_r, N_r, particles, ∂ₜV, m_liq, ρ′_rim, bounds_r, quad)` - `∂ice_mass_∂D` and `get_∂mass_∂D_coeffs` are removed. - `ChebyshevGauss` has the type parameter `FT`, and `node(quad, i)` and `weight(quad, i)` take the integer index in place of `(quad, FT(i), n)`. - `bulk_microphysics_tendencies(::Microphysics2Moment, ...)` loses the positional `logλ` argument after `b_rim`, in both the P3 ice method and the warm-rain-only method. The optional arguments that follow it move one position forward, so a caller that still passes `logλ` passes it as `inpc_log_shift`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The P3 implementation in development in CloudMicrophysics hard-codes the largest mean ice particle mass of its size distribution and the relaxation timescale of Frostenberg (2023) deposition nucleation. This PR adds both to
parameters.toml, so that CloudMicrophysics can read them through ClimaParams, and bumps the version to 1.3.4.New keys:
P3_ice_largest_mean_particle_mass(1e-4 kg): the largest mean ice particle mass of the P3 size distribution.Frostenberg2023_deposition_relaxation_timescale(300 s): the timescale of the relaxation of deposition nucleation toward the Frostenberg et al. (2023) target concentration.The activation conditions of that deposition nucleation are the existing keys
P3_cooper_deposition_temperature_thresholdandP3_cooper_deposition_ice_supersaturation_threshold. Both new keys are marked experimental. No existing key changes.