From 4a8c74567d74636627b68d98581cb7993fd7a117 Mon Sep 17 00:00:00 2001 From: Drew Camron Date: Mon, 23 Dec 2024 11:39:36 -0700 Subject: [PATCH] Add Rm, Cm, Lv, Lf, Lm Calculate gas constant and specific heat capacity of moist air. Add temperature-dependent latent heat quantity calculations. --- src/metpy/calc/thermo.py | 197 ++++++++++++++++++++++++++++++++++++++ tests/calc/test_thermo.py | 54 ++++++++++- 2 files changed, 249 insertions(+), 2 deletions(-) diff --git a/src/metpy/calc/thermo.py b/src/metpy/calc/thermo.py index 983e131547..edeb22d825 100644 --- a/src/metpy/calc/thermo.py +++ b/src/metpy/calc/thermo.py @@ -29,6 +29,203 @@ exporter = Exporter(globals()) +@exporter.export +@preprocess_and_wrap(wrap_like='specific_humidity') +@check_units('[dimensionless]') +def moist_air_gas_constant(specific_humidity): + r"""Calculate R_m, the specific gas constant for a parcel of moist air. + + Parameters + ---------- + specific_humidity : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Specific gas constant + + Examples + -------- + >>> from metpy.calc import moist_air_gas_constant + >>> from metpy.units import units + >>> moist_air_gas_constant(11 * units('g/kg')) + + + See Also + -------- + moist_air_specific_heat_pressure + + Notes + ----- + .. math:: R_m = (1 - q_v) R_a + q_v R_v + + Eq 16, [Romps2017]_ using MetPy-defined constants in place of cited values. + + """ + return ((1 - specific_humidity) * mpconsts.dry_air_gas_constant + + specific_humidity * mpconsts.water_gas_constant) + + +@exporter.export +@preprocess_and_wrap(wrap_like='specific_humidity') +@check_units('[dimensionless]') +def moist_air_specific_heat_pressure(specific_humidity): + r"""Calculate C_pm, the specific heat at constant pressure for a moist air parcel. + + Parameters + ---------- + specific_humidity : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Specific heat capacity of air at constant pressure + + Examples + -------- + >>> from metpy.calc import moist_air_specific_heat_pressure + >>> from metpy.units import units + >>> moist_air_specific_heat_pressure(11 * units('g/kg')) + + + See Also + -------- + moist_air_gas_constant + + Notes + ----- + .. math:: c_{pm} = (1 - q_v) c_{pa} + q_v c_{pv} + + Eq 17, [Romps2017]_ using MetPy-defined constants in place of cited values. + + """ + return ((1 - specific_humidity) * mpconsts.dry_air_spec_heat_press + + specific_humidity * mpconsts.wv_specific_heat_press) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@check_units('[temperature]') +def water_latent_heat_vaporization(temperature): + r"""Calculate the latent heat of vaporization for water. + + Accounts for variations in latent heat across valid temperature range. + + Parameters + ---------- + temperature : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Latent heat of vaporization + + Examples + -------- + >>> from metpy.calc import water_latent_heat_vaporization + >>> from metpy.units import units + >>> water_latent_heat_vaporization(20 * units.degC) + + + See Also + -------- + water_latent_heat_sublimation, water_latent_heat_melting + + Notes + ----- + Assumption of constant :math:`C_pv` limits validity to :math:`0 \deg \en 100 \deg C` range. + + .. math:: L = L_0 - (c_{pl} - c_{pv}) (T - T_0) + + Eq 15, [Ambaum2020]_, using MetPy-defined constants in place of cited values. + + """ + return (mpconsts.water_heat_vaporization + - (mpconsts.water_specific_heat - mpconsts.wv_specific_heat_press) + * (temperature - mpconsts.water_triple_point_temperature)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@check_units('[temperature]') +def water_latent_heat_sublimation(temperature): + r"""Calculate the latent heat of sublimation for water. + + Accounts for variations in latent heat across valid temperature range. + + Parameters + ---------- + temperature : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Latent heat of vaporization + + Examples + -------- + >>> from metpy.calc import water_latent_heat_sublimation + >>> from metpy.units import units + >>> water_latent_heat_sublimation(-15 * units.degC) + + + See Also + -------- + water_latent_heat_vaporization, water_latent_heat_melting + + Notes + ----- + .. math:: L_s = L_{s0} - (c_{pl} - c_{pv}) (T - T_0) + + Eq 18, [Ambaum2020]_, using MetPy-defined constants in place of cited values. + + """ + return (mpconsts.water_heat_sublimation + - (mpconsts.ice_specific_heat - mpconsts.wv_specific_heat_press) + * (temperature - mpconsts.water_triple_point_temperature)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@check_units('[temperature]') +def water_latent_heat_melting(temperature): + r"""Calculate the latent heat of melting for water. + + Accounts for variations in latent heat across valid temperature range. + + Parameters + ---------- + temperature : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Latent heat of vaporization + + Examples + -------- + >>> from metpy.calc import water_latent_heat_melting + >>> from metpy.units import units + >>> water_latent_heat_melting(-15 * units.degC) + + + See Also + -------- + water_latent_heat_vaporization, water_latent_heat_sublimation + + Notes + ----- + .. math:: L_m = L_{m0} + (c_{pl} - c_{pi}) (T - T_0) + + Body text below Eq 20, [Ambaum2020]_, derived from Eq 15, Eq 18. + Uses MetPy-defined constants in place of cited values. + + """ + return (mpconsts.water_heat_fusion + - (mpconsts.water_specific_heat - mpconsts.ice_specific_heat) + * (temperature - mpconsts.water_triple_point_temperature)) + + @exporter.export @preprocess_and_wrap(wrap_like='temperature', broadcast=('temperature', 'dewpoint')) @check_units('[temperature]', '[temperature]') diff --git a/tests/calc/test_thermo.py b/tests/calc/test_thermo.py index 05add1f17a..e6d56b0805 100644 --- a/tests/calc/test_thermo.py +++ b/tests/calc/test_thermo.py @@ -20,7 +20,8 @@ isentropic_interpolation, isentropic_interpolation_as_dataset, k_index, lcl, lfc, lifted_index, mixed_layer, mixed_layer_cape_cin, mixed_parcel, mixing_ratio, mixing_ratio_from_relative_humidity, - mixing_ratio_from_specific_humidity, moist_lapse, moist_static_energy, + mixing_ratio_from_specific_humidity, moist_air_gas_constant, + moist_air_specific_heat_pressure, moist_lapse, moist_static_energy, most_unstable_cape_cin, most_unstable_parcel, parcel_profile, parcel_profile_with_lcl, parcel_profile_with_lcl_as_dataset, potential_temperature, psychrometric_vapor_pressure_wet, @@ -36,13 +37,62 @@ vapor_pressure, vertical_totals, vertical_velocity, vertical_velocity_pressure, virtual_potential_temperature, virtual_temperature, virtual_temperature_from_dewpoint, - wet_bulb_potential_temperature, wet_bulb_temperature) + water_latent_heat_melting, water_latent_heat_sublimation, + water_latent_heat_vaporization, wet_bulb_potential_temperature, + wet_bulb_temperature) from metpy.calc.thermo import _find_append_zero_crossings, galvez_davison_index +from metpy.constants import (dry_air_gas_constant, dry_air_spec_heat_press, water_heat_fusion, + water_heat_sublimation, water_heat_vaporization, + water_triple_point_temperature) from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_nan, version_check) from metpy.units import is_quantity, masked_array, units +def test_moist_air_gas_constant(): + """Test calculation of gas constant for moist air.""" + q = 9 * units('g/kg') + assert_almost_equal(moist_air_gas_constant(q), 288.62 * units('J / kg / K'), 2) + assert_almost_equal(moist_air_gas_constant(0), dry_air_gas_constant) + + +def test_moist_air_specific_heat_pressure(): + """Test calculation of specific heat for moist air.""" + q = 9 * units('g/kg') + assert_almost_equal(moist_air_specific_heat_pressure(q), 1012.36 * units('J / kg /K'), 2) + assert_almost_equal(moist_air_specific_heat_pressure(0), dry_air_spec_heat_press) + + +def test_water_latent_heat_vaporization(): + """Test temperature-dependent calculation of latent heat of vaporization for water.""" + temperature = 300 * units.K + # Divide out sig figs in results for decimal comparison + assert_almost_equal(water_latent_heat_vaporization(temperature) / 10**6, + 2.4375 * units('J / kg'), 4) + assert_almost_equal(water_latent_heat_vaporization(water_triple_point_temperature), + water_heat_vaporization) + + +def test_water_latent_heat_sublimation(): + """Test temperature-dependent calculation of latent heat of sublimation for water.""" + temperature = 233 * units.K + # Divide out sig figs in results for decimal comparison + assert_almost_equal(water_latent_heat_sublimation(temperature) / 10**6, + 2.8438 * units('J / kg'), 4) + assert_almost_equal(water_latent_heat_sublimation(water_triple_point_temperature), + water_heat_sublimation) + + +def test_water_latent_heat_melting(): + """Test temperature-dependent calculation of latent heat of melting for water.""" + temperature = 233 * units.K + # Divide out sig figs in results for decimal comparison + assert_almost_equal(water_latent_heat_melting(temperature) / 10**6, + 0.4192 * units('J / kg'), 4) + assert_almost_equal(water_latent_heat_melting(water_triple_point_temperature), + water_heat_fusion) + + def test_relative_humidity_from_dewpoint(): """Test Relative Humidity calculation.""" assert_almost_equal(relative_humidity_from_dewpoint(25. * units.degC, 15. * units.degC),