diff --git a/docs/_templates/overrides/metpy.calc.rst b/docs/_templates/overrides/metpy.calc.rst index e5e7a4f787f..09464d6b77f 100644 --- a/docs/_templates/overrides/metpy.calc.rst +++ b/docs/_templates/overrides/metpy.calc.rst @@ -39,6 +39,9 @@ Moist Thermodynamics mixing_ratio mixing_ratio_from_relative_humidity mixing_ratio_from_specific_humidity + moist_air_gas_constant + moist_air_poisson_exponent + moist_air_specific_heat_pressure moist_lapse moist_static_energy precipitable_water @@ -60,6 +63,9 @@ Moist Thermodynamics virtual_potential_temperature virtual_temperature virtual_temperature_from_dewpoint + water_latent_heat_melting + water_latent_heat_sublimation + water_latent_heat_vaporization wet_bulb_temperature wet_bulb_potential_temperature diff --git a/docs/api/references.rst b/docs/api/references.rst index dacd4189546..87af83af82f 100644 --- a/docs/api/references.rst +++ b/docs/api/references.rst @@ -2,6 +2,10 @@ References ========== +.. [Ambaum2020] Ambaum MHP, 2020: Accurate, simple equation for saturated vapour pressure over water and ice. + *QJR Meteorol Soc.*; **146**: 4252–4258, + doi: `10.1002/qj.3899 `_. + .. [Anderson2013] Anderson, G. B., M. L. Bell, and R. D. Peng, 2013: Methods to Calculate the Heat Index as an Exposure Metric in Environmental Health Research. *Environmental Health Perspectives*, **121**, 1111-1119, @@ -176,6 +180,10 @@ References .. [Rochette2006] Rochette, Scott M., and Patrick S. Market. "A primer on the ageostrophic wind." Natl. Weather Dig. 30 (2006): 17-28. +.. [Romps2017] Romps, D. M., 2017: Exact Expression for the Lifting Condensation Level. + *J. Atmos. Sci.*, **74**, 3891–3900, + doi: `10.1175/JAS-D-17-0102.1. `_. + .. [Rothfusz1990] Rothfusz, L.P.: *The Heat Index "Equation"*. Fort Worth, TX: Scientific Services Division, NWS Southern Region Headquarters, 1990. `SR90-23 <../_static/rothfusz-1990-heat-index-equation.pdf>`_, 2 pp. diff --git a/docs/conf.py b/docs/conf.py index f0d500c8f7f..baaf08a7645 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -455,6 +455,7 @@ # Couldn't fix these 403's with user agents r'https://doi\.org/10\.1029/2010GL045777', r'https://doi\.org/10\.1098/rspa\.2004\.1430', + r'https://doi\.org/10\.1002/qj\.3899', # Currently giving certificate errors on GitHub r'https://library.wmo.int/.*', # For some reason GHA gets a 403 from Stack Overflow diff --git a/src/metpy/calc/thermo.py b/src/metpy/calc/thermo.py index 983e1315474..48f18d4a6fa 100644 --- a/src/metpy/calc/thermo.py +++ b/src/metpy/calc/thermo.py @@ -29,6 +29,251 @@ exporter = Exporter(globals()) +@exporter.export +@preprocess_and_wrap(wrap_like='specific_humidity') +@process_units(input_dimensionalities={'specific_humidity': 'dimensionless'}, + output_dimensionalities='[specific_heat_capacity]', + output_to='J K**-1 kg**-1 ') +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, moist_air_poisson_exponent + + Notes + ----- + Adapted from + + .. 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 mpconsts.nounit.Rd + specific_humidity * (mpconsts.nounit.Rv - mpconsts.nounit.Rd) + + +@exporter.export +@preprocess_and_wrap(wrap_like='specific_humidity') +@process_units(input_dimensionalities={'specific_humidity': 'dimensionless'}, + output_dimensionalities='[specific_heat_capacity]', + output_to='J K**-1 kg**-1 ') +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, moist_air_poisson_exponent + + Notes + ----- + Adapted from + + .. 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 (mpconsts.nounit.Cp_d + + specific_humidity * (mpconsts.nounit.Cp_v - mpconsts.nounit.Cp_d)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='specific_humidity') +@process_units( + input_dimensionalities={'specific_humidity': 'dimensionless'}, + output_dimensionalities='[dimensionless]' +) +def moist_air_poisson_exponent(specific_humidity): + r"""Calculate kappa_m, the Poisson exponent for a moist air parcel. + + Parameters + ---------- + specific_humidity : `pint.Quantity` + + Returns + ------- + `pint.Quantity` + Poisson exponent of moist air parcel + + Examples + -------- + >>> from metpy.calc import moist_air_poisson_exponent + >>> from metpy.units import units + >>> moist_air_poisson_exponent(11 * units('g/kg')) + + + See Also + -------- + moist_air_gas_constant, moist_air_specific_heat_pressure + + """ + return (moist_air_gas_constant._nounit(specific_humidity) + / moist_air_specific_heat_pressure._nounit(specific_humidity)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@process_units(input_dimensionalities={'temperature': '[temperature]'}, + output_dimensionalities='[specific_enthalpy]', + output_to='J kg**-1') +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` -- :math:`100^{\circ} 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.nounit.Lv + - (mpconsts.nounit.Cp_l - mpconsts.nounit.Cp_v) + * (temperature - mpconsts.nounit.T0)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@process_units(input_dimensionalities={'temperature': '[temperature]'}, + output_dimensionalities='[specific_enthalpy]', + output_to='J kg**-1') +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.nounit.Ls + - (mpconsts.nounit.Cp_i - mpconsts.nounit.Cp_v) + * (temperature - mpconsts.nounit.T0)) + + +@exporter.export +@preprocess_and_wrap(wrap_like='temperature') +@process_units(input_dimensionalities={'temperature': '[temperature]'}, + output_dimensionalities='[specific_enthalpy]', + output_to='J kg**-1') +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.nounit.Lf + - (mpconsts.nounit.Cp_l - mpconsts.nounit.Cp_i) + * (temperature - mpconsts.nounit.T0)) + + @exporter.export @preprocess_and_wrap(wrap_like='temperature', broadcast=('temperature', 'dewpoint')) @check_units('[temperature]', '[temperature]') diff --git a/src/metpy/constants/__init__.py b/src/metpy/constants/__init__.py index 9c2aab04b8a..ee8228be7ee 100644 --- a/src/metpy/constants/__init__.py +++ b/src/metpy/constants/__init__.py @@ -22,20 +22,22 @@ Water ----- -======================= ================ ========== ============================ ==================================================== -Name Symbol Short Name Units Description ------------------------ ---------------- ---------- ---------------------------- ---------------------------------------------------- -water_molecular_weight :math:`M_w` Mw :math:`\text{g mol}^{-1}` Molecular weight of water [5]_ -water_gas_constant :math:`R_v` Rv :math:`\text{J (K kg)}^{-1}` Gas constant for water vapor [2]_ [5]_ -density_water :math:`\rho_l` rho_l :math:`\text{kg m}^{-3}` Maximum recommended density of liquid water, 0-40C [5]_ -wv_specific_heat_press :math:`C_{pv}` Cp_v :math:`\text{J (K kg)}^{-1}` Specific heat at constant pressure for water vapor -wv_specific_heat_vol :math:`C_{vv}` Cv_v :math:`\text{J (K kg)}^{-1}` Specific heat at constant volume for water vapor -water_specific_heat :math:`Cp_l` Cp_l :math:`\text{J (K kg)}^{-1}` Specific heat of liquid water at 0C [6]_ -water_heat_vaporization :math:`L_v` Lv :math:`\text{J kg}^{-1}` Latent heat of vaporization for liquid water at 0C [7]_ -water_heat_fusion :math:`L_f` Lf :math:`\text{J kg}^{-1}` Latent heat of fusion for liquid water at 0C [7]_ -ice_specific_heat :math:`C_{pi}` Cp_i :math:`\text{J (K kg)}^{-1}` Specific heat of ice at 0C [7]_ -density_ice :math:`\rho_i` rho_i :math:`\text{kg m}^{-3}` Density of ice at 0C -======================= ================ ========== ============================ ==================================================== +============================== ================ ========== ============================== ========================================================== +Name Symbol Short Name Units Description +------------------------------ ---------------- ---------- ------------------------------ ---------------------------------------------------------- +water_molecular_weight :math:`M_w` Mw :math:`\text{g mol}^{-1}` Molecular weight of water [5]_ +water_gas_constant :math:`R_v` Rv :math:`\text{J (K kg)}^{-1}` Gas constant for water vapor [2]_ [5]_ +density_water :math:`\rho_l` rho_l :math:`\text{kg m}^{-3}` Maximum recommended density of liquid water, 0-40C [5]_ +wv_specific_heat_press :math:`C_{pv}` Cp_v :math:`\text{J (K kg)}^{-1}` Specific heat at constant pressure for water vapor +wv_specific_heat_vol :math:`C_{vv}` Cv_v :math:`\text{J (K kg)}^{-1}` Specific heat at constant volume for water vapor +water_specific_heat :math:`C_{pl}` Cp_l :math:`\text{J (K kg)}^{-1}` Specific heat of liquid water at 0C [6]_ +water_heat_vaporization :math:`L_v` Lv :math:`\text{J kg}^{-1}` Latent heat of vaporization for liquid water at 0C [7]_ +water_heat_fusion :math:`L_f` Lf :math:`\text{J kg}^{-1}` Latent heat of fusion for liquid water at 0C [7]_ +water_heat_sublimation :math:`L_s` Ls :math:`\text{J kg}^{-1}` Latent heat of sublimation for water, Lv + Lf +ice_specific_heat :math:`C_{pi}` Cp_i :math:`\text{J (K kg)}^{-1}` Specific heat of ice at 0C [7]_ +density_ice :math:`\rho_i` rho_i :math:`\text{kg m}^{-3}` Density of ice at 0C +water_triple_point_temperature :math:`T_0` T0 :math:`\text{K}` Triple-point temperature of water [2]_ +============================== ================ ========== ============================== ========================================================== Dry Air ------- diff --git a/src/metpy/constants/default.py b/src/metpy/constants/default.py index 28c9558b01f..c3067d85aac 100644 --- a/src/metpy/constants/default.py +++ b/src/metpy/constants/default.py @@ -37,9 +37,11 @@ Cp_l = water_specific_heat = units.Quantity(4.2194, 'kJ / kg / K').to('J / kg / K') Lv = water_heat_vaporization = units.Quantity(2.50084e6, 'J / kg') Lf = water_heat_fusion = units.Quantity(3.337e5, 'J / kg') + Ls = water_heat_sublimation = Lv + Lf Cp_i = ice_specific_heat = units.Quantity(2090, 'J / kg / K') rho_i = density_ice = units.Quantity(917, 'kg / m^3') sat_pressure_0c = units.Quantity(6.112, 'millibar') + T0 = water_triple_point_temperature = units.Quantity(273.16, 'K') # Dry air Md = dry_air_molecular_weight = units.Quantity(28.96546e-3, 'kg / mol') diff --git a/src/metpy/constants/nounit.py b/src/metpy/constants/nounit.py index 25d9f2cd228..2fd6e8a78fa 100644 --- a/src/metpy/constants/nounit.py +++ b/src/metpy/constants/nounit.py @@ -7,10 +7,17 @@ from ..units import units Rd = default.Rd.m_as('m**2 / K / s**2') +Rv = default.Rv.m_as('m**2 / K / s**2') Lv = default.Lv.m_as('m**2 / s**2') +Lf = default.Lf.m_as('m**2 / s**2') +Ls = default.Ls.m_as('m**2 / s**2') Cp_d = default.Cp_d.m_as('m**2 / K / s**2') +Cp_l = default.Cp_l.m_as('m**2 / K / s**2') +Cp_v = default.Cp_v.m_as('m**2 / K / s**2') +Cp_i = default.Cp_i.m_as('m**2 / K / s**2') zero_degc = units.Quantity(0., 'degC').m_as('K') sat_pressure_0c = default.sat_pressure_0c.m_as('Pa') epsilon = default.epsilon.m_as('') kappa = default.kappa.m_as('') g = default.g.m_as('m / s**2') +T0 = default.T0.m_as('K') diff --git a/src/metpy/units.py b/src/metpy/units.py index 1328c772251..cc39517ac02 100644 --- a/src/metpy/units.py +++ b/src/metpy/units.py @@ -39,7 +39,9 @@ '[temperature]': 'K', '[dimensionless]': '', '[length]': 'm', - '[speed]': 'm s**-1' + '[speed]': 'm s**-1', + '[specific_enthalpy]': 'm**2 s**-2', + '[specific_heat_capacity]': 'm**2 s**-2 K-1' } @@ -83,6 +85,8 @@ def setup_registry(reg): reg.define('degrees_east = degree = degrees_E = degreesE = degree_east = degree_E ' '= degreeE') reg.define('dBz = 1e-18 m^3; logbase: 10; logfactor: 10 = dBZ') + reg.define('[specific_enthalpy] = [energy] / [mass]') + reg.define('[specific_heat_capacity] = [specific_enthalpy] / [temperature]') # Alias geopotential meters (gpm) to just meters reg.define('@alias meter = gpm') diff --git a/tests/calc/test_thermo.py b/tests/calc/test_thermo.py index 05add1f17a2..2fa8575fc9f 100644 --- a/tests/calc/test_thermo.py +++ b/tests/calc/test_thermo.py @@ -15,16 +15,18 @@ brunt_vaisala_period, cape_cin, ccl, cross_totals, density, dewpoint, dewpoint_from_relative_humidity, dewpoint_from_specific_humidity, downdraft_cape, dry_lapse, dry_static_energy, el, - equivalent_potential_temperature, exner_function, + equivalent_potential_temperature, exner_function, galvez_davison_index, gradient_richardson_number, InvalidSoundingError, 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, - 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, - relative_humidity_from_dewpoint, relative_humidity_from_mixing_ratio, + mixing_ratio_from_specific_humidity, moist_air_gas_constant, + moist_air_poisson_exponent, 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, relative_humidity_from_dewpoint, + relative_humidity_from_mixing_ratio, relative_humidity_from_specific_humidity, relative_humidity_wet_psychrometric, saturation_equivalent_potential_temperature, saturation_mixing_ratio, @@ -36,13 +38,64 @@ 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) -from metpy.calc.thermo import _find_append_zero_crossings, galvez_davison_index + 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 +from metpy.constants import Cp_d, kappa, Lf, Ls, Lv, Rd, T0 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), Rd) + + +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), Cp_d) + + +def test_moist_air_poisson_exponent(): + """Test calculation of kappa for moist air.""" + q = 9 * units('g/kg') + assert_almost_equal(moist_air_poisson_exponent(q), 0.2851, 3) + assert_almost_equal(moist_air_poisson_exponent(0), kappa) + + +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(T0), Lv) + + +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(T0), Ls) + + +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(T0), Lf) + + def test_relative_humidity_from_dewpoint(): """Test Relative Humidity calculation.""" assert_almost_equal(relative_humidity_from_dewpoint(25. * units.degC, 15. * units.degC),