Skip to content

Commit 3a93f4b

Browse files
glwagnernavidcygiordano
authored
Build abstraction for computing saturation vapor pressure over mixed phase surfaces (#142)
* Build abstraction for mixed phase surfaces * add tests * doctest * add zero * update clausuis claperyon docs * Update src/Thermodynamics/vapor_saturation.jl Co-authored-by: Mosè Giordano <[email protected]> * Update src/Thermodynamics/vapor_saturation.jl --------- Co-authored-by: Navid C. Constantinou <[email protected]> Co-authored-by: Mosè Giordano <[email protected]>
1 parent 9bc5911 commit 3a93f4b

File tree

4 files changed

+339
-51
lines changed

4 files changed

+339
-51
lines changed

docs/src/thermodynamics.md

Lines changed: 164 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -392,52 +392,134 @@ cᵖᵐ - cᵖᵈ
392392
```
393393

394394

395-
## The Clausius--Clapeyron relation and saturation specific humidity
395+
## The Clausius--Clapeyron relation and saturation vapor pressure
396396

397397
The [Clausius--Clapeyron relation](https://en.wikipedia.org/wiki/Clausius%E2%80%93Clapeyron_relation)
398-
for an ideal gas
398+
for an ideal gas describes how saturation vapor pressure changes with temperature:
399399

400400
```math
401401
\frac{\mathrm{d} pᵛ⁺}{\mathrm{d} T} = \frac{pᵛ⁺ ℒ^β(T)}{Rᵛ T^2} ,
402402
```
403403

404-
where ``pᵛ⁺`` is saturation vapor pressure, ``T`` is temperature, ``Rᵛ`` is the specific
405-
gas constant for vapor, ``ℒ^β(T)`` is the latent heat of the transition from vapor to the
406-
``β`` phase (e.g., ``β = l`` for vapor → liquid and ``β = i`` for vapor to ice).
404+
where ``pᵛ⁺`` is saturation vapor pressure over a surface of condensed phase ``β``,
405+
``T`` is temperature, ``Rᵛ`` is the specific gas constant for vapor, and
406+
``ℒ^β(T)`` is the latent heat of the phase transition from vapor to phase ``β``.
407+
For atmospheric moist air, the relevant condensed phases are liquid water (``β = l``)
408+
and ice (``β = i``).
409+
410+
### Temperature-dependent latent heat
407411

408412
For a thermodynamic formulation that uses constant (i.e. temperature-independent) specific
409-
heats, the latent heat of a phase transition is linear in temperature.
410-
For example, for phase change from vapor to liquid,
413+
heats, the latent heat of a phase transition is linear in temperature:
414+
415+
```math
416+
ℒ^β(T) = ℒ^β_0 + \Delta c^β \, T ,
417+
```
418+
419+
where ``ℒ^β_0 ≡ ℒ^β(T=0)`` is the latent heat at absolute zero and
420+
``\Delta c^β ≡ c_p^v - c^β`` is the constant difference between the vapor specific heat
421+
capacity at constant pressure and the specific heat capacity of the condensed phase ``β``.
422+
423+
Note that we typically parameterize the latent heat in terms of a reference
424+
temperature ``T_r`` that is well above absolute zero. In that case,
425+
the latent heat is written
426+
427+
```math
428+
ℒ^β(T) = ℒ^β_r + \Delta c^β (T - T_r), \qquad \text{and} \qquad
429+
ℒ^β_0 = ℒ^β_r - \Delta c^β T_r ,
430+
```
431+
432+
where ``ℒ^β_r`` is the latent heat at the reference temperature ``T_r``.
433+
434+
### Integration of the Clausius-Clapeyron relation
435+
436+
To find the saturation vapor pressure as a function of temperature, we integrate
437+
the Clausius-Clapeyron relation with the temperature-linear latent heat model
438+
from the triple point pressure and temperature ``(p^{tr}, T^{tr})`` to a generic
439+
pressure ``pᵛ⁺`` and temperature ``T``:
440+
441+
```math
442+
\int_{p^{tr}}^{pᵛ⁺} \frac{\mathrm{d} p}{p} = \int_{T^{tr}}^{T} \frac{ℒ^β_0 + \Delta c^β T'}{Rᵛ T'^2} \, \mathrm{d} T' .
443+
```
444+
445+
Evaluating the integrals yields
411446

412447
```math
413-
ℒˡ(T) = ℒˡ(T=0) + \big ( \underbrace{cᵖᵛ - cˡ}_{≡Δcˡ} \big ) T ,
448+
\log\left(\frac{pᵛ⁺}{p^{tr}}\right) = -\frac{ℒ^β_0}{Rᵛ T} + \frac{ℒ^β_0}{Rᵛ T^{tr}} + \frac{\Delta c^β}{Rᵛ} \log\left(\frac{T}{T^{tr}}\right) .
414449
```
415450

416-
where ``ℒˡ(T=0)`` is the latent heat at absolute zero, ``T = 0 \; \mathrm{K}``.
417-
By integrating from the triple-point temperature ``Tᵗʳ`` for which ``p(Tᵗʳ) = pᵗʳ``, we get
451+
Exponentiating both sides gives the closed-form solution:
418452

419453
```math
420-
pᵛ⁺(T) = pᵗʳ \left ( \frac{T}{Tᵗʳ} \right )^{Δcˡ / Rᵛ} \exp \left [ \frac{ℒˡ(T=0)}{Rᵛ} \left (\frac{1}{Tᵗʳ} - \frac{1}{T} \right ) \right ] .
454+
pᵛ⁺(T) = p^{tr} \left ( \frac{T}{T^{tr}} \right )^{\Delta c^β / Rᵛ} \exp \left [ \frac{ℒ^β_0}{Rᵛ} \left (\frac{1}{T^{tr}} - \frac{1}{T} \right ) \right ] .
421455
```
422456

457+
### Example: liquid water and ice parameters
458+
423459
Consider parameters for liquid water,
424460

425461
```@example thermo
426462
using Breeze.Thermodynamics: CondensedPhase
427463
liquid_water = CondensedPhase(reference_latent_heat=2500800, heat_capacity=4181)
428464
```
429465

430-
or water ice,
466+
and water ice,
431467

432468
```@example thermo
433469
water_ice = CondensedPhase(reference_latent_heat=2834000, heat_capacity=2108)
434470
```
435471

436-
The saturation vapor pressure is
472+
These represent the latent heat of vaporization at the reference temperature and
473+
the specific heat capacity of each condensed phase. We can compute the specific heat
474+
difference ``\Delta c^β`` for liquid water:
475+
476+
```@example thermo
477+
using Breeze.Thermodynamics: vapor_gas_constant
478+
cᵖᵛ = thermo.vapor.heat_capacity
479+
cˡ = thermo.liquid.heat_capacity
480+
Δcˡ = cᵖᵛ - cˡ
481+
```
482+
483+
This difference ``\Delta c^l ≈`` $(round(1885 - 4181, digits=1)) J/(kg⋅K) is negative because
484+
water vapor has a lower heat capacity than liquid water.
485+
486+
### Mixed-phase saturation vapor pressure
487+
488+
In atmospheric conditions near the freezing point, condensate may exist as a mixture of
489+
liquid and ice. Following [Pressel2015](@citet), we model the saturation vapor pressure
490+
over a mixed-phase surface using a liquid fraction ``λ`` that varies smoothly between
491+
0 (pure ice) and 1 (pure liquid). The effective latent heat and specific heat difference
492+
for the mixture are computed as weighted averages:
493+
494+
```math
495+
ℒ^{li}_0 = λ \, ℒ^l_0 + (1 - λ) \, ℒ^i_0 ,
496+
```
497+
498+
```math
499+
\Delta c^{li} = λ \, \Delta c^l + (1 - λ) \, \Delta c^i .
500+
```
501+
502+
These effective properties are then used in the Clausius-Clapeyron formula to compute
503+
the saturation vapor pressure over the mixed-phase surface. This approach ensures
504+
thermodynamic consistency and smooth transitions between pure liquid and pure ice states.
505+
506+
We can illustrate this by computing the mixed-phase specific heat difference for a
507+
50/50 mixture:
508+
509+
```@example thermo
510+
Δcⁱ = thermo.vapor.heat_capacity - thermo.ice.heat_capacity
511+
λ = 0.5
512+
Δcˡⁱ = λ * Δcˡ + (1 - λ) * Δcⁱ
513+
```
514+
515+
### Visualizing saturation vapor pressure
516+
517+
The saturation vapor pressure over liquid, ice, and mixed-phase surfaces can be computed
518+
and visualized:
437519

438520
```@example
439521
using Breeze
440-
using Breeze.Thermodynamics: saturation_vapor_pressure
522+
using Breeze.Thermodynamics: saturation_vapor_pressure, PlanarMixedPhaseSurface
441523
442524
thermo = ThermodynamicConstants()
443525
@@ -446,48 +528,99 @@ pᵛˡ⁺ = [saturation_vapor_pressure(Tⁱ, thermo, thermo.liquid) for Tⁱ in
446528
pᵛⁱ⁺ = [saturation_vapor_pressure(Tⁱ, thermo, thermo.ice) for Tⁱ in T]
447529
pᵛⁱ⁺[T .> thermo.triple_point_temperature] .= NaN
448530
531+
# Mixed-phase surface with 50% liquid, 50% ice
532+
mixed_surface = PlanarMixedPhaseSurface(0.5)
533+
pᵛᵐ⁺ = [saturation_vapor_pressure(Tⁱ, thermo, mixed_surface) for Tⁱ in T]
534+
449535
using CairoMakie
450536
451537
fig = Figure()
452-
ax = Axis(fig[1, 1], xlabel="Temperature (ᵒK)", ylabel="Saturation vapor pressure pᵛ⁺ (Pa)", yscale = log10, xticks=200:20:320)
453-
lines!(ax, T, pᵛˡ⁺, label="vapor pressure over liquid")
454-
lines!(ax, T, pᵛⁱ⁺, linestyle=:dash, label="vapor pressure over ice")
538+
ax = Axis(fig[1, 1], xlabel="Temperature (ᵒK)", ylabel="Saturation vapor pressure pᵛ⁺ (Pa)",
539+
yscale = log10, xticks=200:20:320)
540+
lines!(ax, T, pᵛˡ⁺, label="liquid", linewidth=2)
541+
lines!(ax, T, pᵛⁱ⁺, label="ice", linestyle=:dash, linewidth=2)
542+
lines!(ax, T, pᵛᵐ⁺, label="mixed (λ=0.5)", linestyle=:dot, linewidth=2, color=:purple)
455543
axislegend(ax, position=:rb)
456544
fig
457545
```
458546

459-
The saturation specific humidity is
547+
The mixed-phase saturation vapor pressure lies between the liquid and ice curves,
548+
providing a smooth interpolation between the two pure phases.
549+
550+
## Saturation specific humidity
551+
552+
The saturation specific humidity ``qᵛ⁺`` is the maximum amount of water vapor that
553+
can exist in equilibrium with a condensed phase at a given temperature and density.
554+
It is related to the saturation vapor pressure by:
460555

461556
```math
462-
qᵛ⁺ ≡ \frac{ρᵛ⁺}{ρ} = \frac{pᵛ⁺}{ρ Rᵐ T} .
557+
qᵛ⁺ ≡ \frac{ρᵛ⁺}{ρ} = \frac{pᵛ⁺}{ρ Rᵛ T} ,
463558
```
464559

465-
and this is what it looks like:
560+
where ``ρᵛ⁺`` is the vapor density at saturation, ``ρ`` is the total air density,
561+
and ``Rᵛ`` is the specific gas constant for water vapor.
562+
563+
### Visualizing saturation vapor pressure and specific humidity
564+
565+
We can visualize how both saturation vapor pressure and saturation specific humidity
566+
vary with temperature for different liquid fractions, demonstrating the smooth
567+
interpolation provided by the mixed-phase model:
466568

467569
```@example
468570
using Breeze
469-
using Breeze.Thermodynamics: saturation_specific_humidity
571+
using Breeze.Thermodynamics: saturation_vapor_pressure, saturation_specific_humidity, PlanarMixedPhaseSurface
470572
471573
thermo = ThermodynamicConstants()
472574
473-
p₀ = 101325
575+
# Temperature range covering typical atmospheric conditions
576+
T = collect(250:0.1:320)
577+
p₀ = 101325 # Surface pressure (Pa)
474578
Rᵈ = Breeze.Thermodynamics.dry_air_gas_constant(thermo)
475-
T = collect(273.2:0.1:313.2)
476-
qᵛ⁺ = zeros(length(T))
477579
478-
for i = 1:length(T)
479-
ρ = p₀ / (Rᵈ * T[i])
480-
qᵛ⁺[i] = saturation_specific_humidity(T[i], ρ, thermo, thermo.liquid)
481-
end
580+
# Liquid fractions to visualize
581+
λ_values = [0.0, 0.25, 0.5, 0.75, 1.0]
582+
labels = ["ice (λ=0)", "λ=0.25", "λ=0.5", "λ=0.75", "liquid (λ=1)"]
583+
colors = [:blue, :cyan, :purple, :orange, :red]
584+
linestyles = [:solid, :dash, :dot, :dashdot, :solid]
482585
483586
using CairoMakie
484587
485-
fig = Figure()
486-
ax = Axis(fig[1, 1], xlabel="Temperature (ᵒK)", ylabel="Saturation specific humidity qᵛ⁺ (kg kg⁻¹)")
487-
lines!(ax, T, qᵛ⁺)
588+
fig = Figure(size=(1000, 400))
589+
590+
# Panel 1: Saturation vapor pressure
591+
ax1 = Axis(fig[1, 1], xlabel="Temperature (K)", ylabel="Saturation vapor pressure (Pa)",
592+
yscale=log10, title="Saturation vapor pressure")
593+
594+
for (i, λ) in enumerate(λ_values)
595+
surface = PlanarMixedPhaseSurface(λ)
596+
pᵛ⁺ = [saturation_vapor_pressure(Tⁱ, thermo, surface) for Tⁱ in T]
597+
lines!(ax1, T, pᵛ⁺, label=labels[i], color=colors[i], linestyle=linestyles[i], linewidth=2)
598+
end
599+
600+
axislegend(ax1, position=:lt)
601+
602+
# Panel 2: Saturation specific humidity
603+
ax2 = Axis(fig[1, 2], xlabel="Temperature (K)", ylabel="Saturation specific humidity (kg/kg)",
604+
title="Saturation specific humidity")
605+
606+
for (i, λ) in enumerate(λ_values)
607+
surface = PlanarMixedPhaseSurface(λ)
608+
qᵛ⁺ = zeros(length(T))
609+
for (j, Tⁱ) in enumerate(T)
610+
ρ = p₀ / (Rᵈ * Tⁱ) # Approximate density using dry air
611+
qᵛ⁺[j] = saturation_specific_humidity(Tⁱ, ρ, thermo, surface)
612+
end
613+
lines!(ax2, T, qᵛ⁺, label=labels[i], color=colors[i], linestyle=linestyles[i], linewidth=2)
614+
end
615+
488616
fig
489617
```
490618

619+
This figure shows how the liquid fraction ``λ`` smoothly interpolates between pure ice
620+
(``λ = 0``) and pure liquid (``λ = 1``). At lower temperatures, the differences between
621+
phases are more pronounced. The mixed-phase model allows for realistic representation of
622+
conditions near the freezing point where both liquid and ice may coexist.
623+
491624
## Moist static energy
492625

493626
For moist air, a convenient thermodynamic invariant that couples temperature, composition, and height is the moist static energy (MSE),

src/Thermodynamics/thermodynamics_constants.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ struct MoistureMassFractions{FT}
251251
ice :: FT
252252
end
253253

254+
Base.zero(::Type{MoistureMassFractions{FT}}) where FT = MoistureMassFractions(zero(FT), zero(FT), zero(FT))
255+
254256
function Base.summary(q::MoistureMassFractions{FT}) where FT
255257
return string("MoistureMassFractions{$FT}(vapor=", prettysummary(q.vapor),
256258
", liquid=", prettysummary(q.liquid), ", ice=", prettysummary(q.ice), ")")

0 commit comments

Comments
 (0)