diff --git a/.gitignore b/.gitignore index 245dc283..ea7b7ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ docs/site/ # Deps Manifest.toml + +*.jld2 diff --git a/docs/src/Formulation.md b/docs/src/Formulation.md index a2cbe96f..58ae099a 100644 --- a/docs/src/Formulation.md +++ b/docs/src/Formulation.md @@ -420,6 +420,7 @@ TODO: there was a commented equation here, do we need it? This expression for temperature as a function of liquid-ice potential temperature is obtained from \eqref{e:TempFromThetaLiGivenP} by substituting for pressure in the Exner function ``\Pi`` from the ideal gas law, ``p=ρ R_m T``, solving for temperature using a second-order Taylor expansion around ``T_u`` for small condensate specific humidities, and using the relation ``1-κ = c_{vm}/c_{pm}``, which follows from ``c_{pm} - R_m = c_{vm}``. The relation for temperature \eqref{e:TempFromThetaLiGivenRho} holds to second order in condensate specific humidities ``q_l`` and ``q_i``. That is, the inversion relation \eqref{e:TempFromThetaLiGivenRho} holds to one higher order of accuracy than the definition of the liquid-ice potential temperature \eqref{e:liquid_ice_pottemp} itself, which is only first-order accurate in the condensate specific humidities ``q_l`` and ``q_i``. ### Speed of Sound + The speed of sound in (moist) unstratified air is ```math \begin{equation} diff --git a/examples/JRA55_atmospheric_state_Jan_1_1991.jld2 b/examples/JRA55_atmospheric_state_Jan_1_1991.jld2 new file mode 100644 index 00000000..48c22f78 Binary files /dev/null and b/examples/JRA55_atmospheric_state_Jan_1_1991.jld2 differ diff --git a/examples/density_from_temperature_pressure_humidity.jl b/examples/density_from_temperature_pressure_humidity.jl new file mode 100644 index 00000000..9544d68e --- /dev/null +++ b/examples/density_from_temperature_pressure_humidity.jl @@ -0,0 +1,186 @@ +# # Defining a simple parameter set and using it to compute density +# +# This script shows how to define a simple parameter set, and then using it to +# compute density as a function of pressure, temperature, and humidity. + +# # Define parameters for computing the density of air +# +# First, we build a parameter set suitable for computing the density of +# _moist_ air --- that is, a mixture of dry air, water vapor, liquid droplets, +# and ice crystals (the latter two are called "condensates"). + +using Thermodynamics +using Thermodynamics.Parameters: AbstractThermodynamicsParameters + +struct ConstitutiveParameters{FT} <: AbstractThermodynamicsParameters{FT} + gas_constant::FT + dry_air_molar_mass::FT + water_molar_mass::FT +end + +""" + ConstitutiveParameters(FT; gas_constant = 8.3144598, + dry_air_molar_mass = 0.02897, + water_molar_mass = 0.018015) + +Construct a set of parameters that define the density of moist air, + +```math +ρ = p / Rᵐ(q) T, +``` + +where ``p`` is pressure, ``T`` is temperature, ``q`` defines the partition +of total mass into vapor, liqiud, and ice mass fractions, and +``Rᵐ`` is the effective specific gas constant for the mixture, + +```math +Rᵐ(q) = +``` + +where + +For more information see [reference docs]. +""" +function ConstitutiveParameters( + FT = Float64; + gas_constant = 8.3144598, + dry_air_molar_mass = 0.02897, + water_molar_mass = 0.018015, +) + + return ConstitutiveParameters( + convert(FT, gas_constant), + convert(FT, dry_air_molar_mass), + convert(FT, water_molar_mass), + ) +end + +# Next, we define functions that return: +# 1. The specific gas constant for dry air +# 2. The specific gas constant for water vapor +# 3. The ratio between the dry air molar mass and water molar mass + +const VTP = ConstitutiveParameters +using Thermodynamics: Parameters + +Parameters.R_d(p::VTP) = p.gas_constant / p.dry_air_molar_mass +Parameters.R_v(p::VTP) = p.gas_constant / p.water_molar_mass +Parameters.molmass_ratio(p::VTP) = p.dry_air_molar_mass / p.water_molar_mass + +# # The density of dry air + +import Thermodynamics as AtmosphericThermodynamics + +# To compute the density of dry air, we first build a default +# parameter set, + +parameters = ConstitutiveParameters() + +# Next, we construct a phase partitioning representing dry air --- the trivial +# case where the all mass ratios are 0. +# +# Note that the syntax for `PhasePartition` is +# +# ``` +# PhasePartition(q_total, q_liquid, q_ice) +# ``` +# where the q's are mass fractions of total water components, +# liquid droplet condensate, and ice crystal condensate. + +q_dry = AtmosphericThermodynamics.PhasePartition(0.0) + +# Finally, we define the pressure and temperature, which constitute +# the "state" of our atmosphere, + +p₀ = 101325.0 # sea level pressure in Pascals (here taken to be mean sea level pressure) +T₀ = 273.15 # temperature in Kelvin + +# Note that the above must be defined with the same float point precision as +# used for the parameters and PhasePartition. +# We're now ready to compute the density of dry air, + +ρ = air_density(parameters, T₀, p₀, q_dry) + +@show ρ + +# Note that the above must be defined with the same float point precision as +# used for the parameters and PhasePartition. +# We're now ready to compute the density of dry air, + +using JLD2 + +# Next, we load an atmospheric state correpsonding to atmospheric surface +# variables substampled from the JRA55 dataset, from the date Jan 1, 1991: + +@load "JRA55_atmospheric_state_Jan_1_1991.jld2" q T p +nothing + +# The variables q, T and p correspond to the total specific humidity (a mass fraction), +# temperature (Kelvin), and sea level pressure (Pa) +# +# We use q to build a vector of PhasePartition, + +qp = PhasePartition.(q) + +# And then compute the density using the same parameters as before: + +ρ = air_density.(parameters, T, p, qp) + +# Finally, we plot the density as a function of temperature and specific humidity, + +using CairoMakie + +## Pressure range, centered around the mean sea level pressure defined above +pmax = maximum(abs, p) +dp = 3 / 4 * (pmax - p₀) +prange = (p₀ - dp, p₀ + dp) +pmap = :balance + +## Compute temperature range +Tmin = minimum(T) +Tmax = maximum(T) +Trange = (Tmin, Tmax) +Tmap = :viridis + +fig = Figure(size = (1200, 500)) + +axρ = Axis(fig[2, 1], xlabel = "Temperature (K) ", ylabel = "Density (kg m⁻³)") +axq = Axis(fig[2, 2], xlabel = "Specific humidity", ylabel = "Density (kg m⁻³)") + +scatter!( + axρ, + T[:], + ρ[:], + color = p[:], + colorrange = prange, + colormap = pmap, + alpha = 0.1, +) +scatter!( + axq, + q[:], + ρ[:], + color = T[:], + colorrange = Trange, + colormap = Tmap, + alpha = 0.1, +) + +Colorbar( + fig[1, 1], + label = "Pressure (Pa)", + vertical = false, + colorrange = prange, + colormap = pmap, +) +Colorbar( + fig[1, 2], + label = "Temperature (K)", + vertical = false, + colorrange = Trange, + colormap = Tmap, +) + +save(fig, "density_versus_temperature.png") + +display(fig) diff --git a/src/Parameters.jl b/src/Parameters.jl index fd51b602..18c96a52 100644 --- a/src/Parameters.jl +++ b/src/Parameters.jl @@ -2,6 +2,9 @@ module Parameters export ThermodynamicsParameters +abstract type AbstractThermodynamicsParameters{FT} end +const ATP = AbstractThermodynamicsParameters + """ ThermodynamicsParameters @@ -21,7 +24,8 @@ param_set = TP.ThermodynamicsParameters(toml_dict) ``` """ -Base.@kwdef struct ThermodynamicsParameters{FT} +Base.@kwdef struct ThermodynamicsParameters{FT} <: + AbstractThermodynamicsParameters{FT} T_0::FT MSLP::FT p_ref_theta::FT @@ -50,14 +54,12 @@ Base.@kwdef struct ThermodynamicsParameters{FT} pow_icenuc::FT end -const ATP = ThermodynamicsParameters - Base.broadcastable(ps::ATP) = tuple(ps) Base.eltype(::ThermodynamicsParameters{FT}) where {FT} = FT # wrappers -for fn in fieldnames(ATP) - @eval @inline $(fn)(ps::ATP) = ps.$(fn) +for fn in fieldnames(ThermodynamicsParameters) + @eval @inline $(fn)(ps::ThermodynamicsParameters) = ps.$(fn) end # Derived parameters diff --git a/src/TemperatureProfiles.jl b/src/TemperatureProfiles.jl index dbe4ba8b..7dd1a889 100644 --- a/src/TemperatureProfiles.jl +++ b/src/TemperatureProfiles.jl @@ -8,7 +8,7 @@ export TemperatureProfile, import ..Parameters const TP = Parameters -const APS = TP.ThermodynamicsParameters +const APS = TP.AbstractThermodynamicsParameters """ TemperatureProfile diff --git a/src/Thermodynamics.jl b/src/Thermodynamics.jl index 4cef0f87..fd2c6b07 100644 --- a/src/Thermodynamics.jl +++ b/src/Thermodynamics.jl @@ -54,7 +54,7 @@ const KA = KernelAbstractions include("Parameters.jl") import .Parameters const TP = Parameters -const APS = TP.ThermodynamicsParameters +const APS = TP.AbstractThermodynamicsParameters # Allow users to skip error on non-convergence # by importing: diff --git a/src/relations.jl b/src/relations.jl index 8d68762d..13ea98c2 100644 --- a/src/relations.jl +++ b/src/relations.jl @@ -1022,12 +1022,11 @@ end Compute the saturation specific humidity over a plane surface of condensate, given - - `param_set` an `AbstractParameterSet`, see the [`Thermodynamics`](@ref) for more details - - `T` temperature - - `ρ` (moist-)air density -and, optionally, - - `Liquid()` indicating condensate is liquid - - `Ice()` indicating condensate is ice + - `param_set`: an `AbstractParameterSet`, see the [`Thermodynamics`](@ref) for more details + - `T`: temperature + - `ρ`: air density + - (optional) `Liquid()`: indicating condensate is liquid + - (optional) `Ice()`: indicating condensate is ice """ @inline function q_vap_saturation_generic( param_set::APS, @@ -1046,12 +1045,11 @@ q_vap_saturation_generic(param_set::APS, T, ρ, phase::Phase) = Compute the saturation specific humidity, given - - `param_set` an `AbstractParameterSet`, see the [`Thermodynamics`](@ref) for more details - - `T` temperature - - `ρ` (moist-)air density - - `phase_type` a thermodynamic state type -and, optionally, - - `q` [`PhasePartition`](@ref) + - `param_set`: an `AbstractParameterSet`, see the [`Thermodynamics`](@ref) for more details + - `T`: temperature + - `ρ`: air density + - `phase_type`: a thermodynamic state type + - (optional) `q`: [`PhasePartition`](@ref) If the `PhasePartition` `q` is given, the saturation specific humidity is that of a mixture of liquid and ice, computed in a thermodynamically consistent way from the