Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability for user-defined parameter sets, with an example #172

Closed
wants to merge 11 commits into from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ docs/site/

# Deps
Manifest.toml

*.jld2
1 change: 1 addition & 0 deletions docs/src/Formulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Binary file added examples/JRA55_atmospheric_state_Jan_1_1991.jld2
Binary file not shown.
155 changes: 155 additions & 0 deletions examples/density_from_temperature_pressure_humidity.jl
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we literate it and add it in the docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I asked about in the original post

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# # 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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some discussions a while back with SEs asking if the parameters constructors should come with default values. The feedback I got was leaning towards not having the defaults, unless it is really beneficial to the structure of the code.

From what I understood the defaults should live in a toml file. - I don't have any opinions on this personally. More of a question of convention and how much we want to stick to it in different examples

Copy link
Member Author

@glwagner glwagner Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a "classroom example" that illustrates some thermodynamic calculations. I think we want to include examples like this in the docs as well to form a link between theromdynamic theory explained in the docs and functions that one calls to make thermodynamic calculations.

This example is not intended to illustrate how to use this package within an Earth system model. So, I understand the validity of the argument to avoid defaults in parameter structs that are designed for use in ClimaAtmos.

However, this parameter struct is not for ClimaAtmos or an Earth system model --- it's just for this small example. For this use case, it's nice to have the defaults here so that we can read this file and understand what the code is doing. The same applies for code snippets embedded in the documentation, I think.

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)

11 changes: 6 additions & 5 deletions src/Parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module Parameters

export ThermodynamicsParameters

abstract type AbstractThermodynamicsParameters{FT} end
const ATP = AbstractThermodynamicsParameters

"""
ThermodynamicsParameters

Expand All @@ -21,7 +24,7 @@ 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
Expand Down Expand Up @@ -49,14 +52,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
Expand Down
2 changes: 1 addition & 1 deletion src/TemperatureProfiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export TemperatureProfile,

import ..Parameters
const TP = Parameters
const APS = TP.ThermodynamicsParameters
const APS = TP.AbstractThermodynamicsParameters

"""
TemperatureProfile
Expand Down
2 changes: 1 addition & 1 deletion src/Thermodynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 10 additions & 12 deletions src/relations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1020,12 +1020,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,
Expand All @@ -1042,12 +1041,11 @@ end

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
Expand Down
Loading