Skip to content

Commit 7ec4570

Browse files
committed
Add Ambaum method option to dewpoint function (#3976)
Add 'method' parameter to dewpoint() with options: - 'bolton' (default): Original analytical formula, preserves backward compatibility - 'ambaum': Numerical inversion of Ambaum (2020), consistent with saturation_vapor_pressure() This allows users who need thermodynamic consistency to use: dewpoint(e, method='ambaum') which satisfies: dewpoint(saturation_vapor_pressure(T), method='ambaum') == T - Add _dewpoint_ambaum() helper using scipy.optimize.brentq - Add test_dewpoint_ambaum_roundtrip() verifying consistency - Document both methods in docstring with examples Addresses #3976
1 parent 9a4901c commit 7ec4570

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed

src/metpy/calc/thermo.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,17 +1711,51 @@ def dewpoint_from_relative_humidity(temperature, relative_humidity):
17111711
return dewpoint(relative_humidity * saturation_vapor_pressure(temperature))
17121712

17131713

1714+
def _dewpoint_ambaum(vapor_pressure):
1715+
"""Calculate dewpoint by numerically inverting Ambaum (2020) formula."""
1716+
def objective(temp_kelvin, target_pressure):
1717+
"""Calculate difference between saturation pressure at temp and target."""
1718+
latent_heat = (mpconsts.nounit.Lv
1719+
- (mpconsts.nounit.Cp_l - mpconsts.nounit.Cp_v)
1720+
* (temp_kelvin - mpconsts.nounit.T0))
1721+
heat_power = (mpconsts.nounit.Cp_l - mpconsts.nounit.Cp_v) / mpconsts.nounit.Rv
1722+
exp_term = ((mpconsts.nounit.Lv / mpconsts.nounit.T0 - latent_heat / temp_kelvin)
1723+
/ mpconsts.nounit.Rv)
1724+
e_sat = (mpconsts.nounit.sat_pressure_0c
1725+
* (mpconsts.nounit.T0 / temp_kelvin) ** heat_power
1726+
* np.exp(exp_term))
1727+
return e_sat - target_pressure
1728+
1729+
t_min = 182.65 # -90.5°C
1730+
t_max = 373.15 # 100°C
1731+
1732+
if np.isscalar(vapor_pressure) or vapor_pressure.size == 1:
1733+
return so.brentq(objective, t_min, t_max, args=(vapor_pressure,))
1734+
else:
1735+
vapor_pressure_flat = vapor_pressure.flatten()
1736+
result_flat = np.empty_like(vapor_pressure_flat)
1737+
for i, vp in enumerate(vapor_pressure_flat):
1738+
result_flat[i] = so.brentq(objective, t_min, t_max, args=(vp,))
1739+
return result_flat.reshape(vapor_pressure.shape)
1740+
1741+
17141742
@exporter.export
17151743
@preprocess_and_wrap(wrap_like='vapor_pressure')
17161744
@process_units({'vapor_pressure': '[pressure]'}, '[temperature]', output_to=units.degC)
1717-
def dewpoint(vapor_pressure):
1745+
def dewpoint(vapor_pressure, method='bolton'):
17181746
r"""Calculate the ambient dewpoint given the vapor pressure.
17191747
17201748
Parameters
17211749
----------
17221750
vapor_pressure : `pint.Quantity`
17231751
Water vapor partial pressure
17241752
1753+
method : str, optional
1754+
Formula to use for calculation. Options are:
1755+
- 'bolton' (default): Analytical inversion of Bolton (1980) formula
1756+
- 'ambaum': Numerical inversion of Ambaum (2020) formula, consistent
1757+
with :func:`saturation_vapor_pressure`
1758+
17251759
Returns
17261760
-------
17271761
`pint.Quantity`
@@ -1734,24 +1768,41 @@ def dewpoint(vapor_pressure):
17341768
>>> dewpoint(22 * units.hPa)
17351769
<Quantity(19.0291018, 'degree_Celsius')>
17361770
1771+
Using the Ambaum method for consistency with saturation_vapor_pressure:
1772+
1773+
>>> dewpoint(22 * units.hPa, method='ambaum')
1774+
<Quantity(19.0426679, 'degree_Celsius')>
1775+
17371776
See Also
17381777
--------
17391778
dewpoint_from_relative_humidity, saturation_vapor_pressure, vapor_pressure
17401779
17411780
Notes
17421781
-----
1743-
This function inverts the [Bolton1980]_ formula for saturation vapor
1744-
pressure to instead calculate the temperature. This yields the following formula for
1745-
dewpoint in degrees Celsius, where :math:`e` is the ambient vapor pressure in millibars:
1782+
The default 'bolton' method inverts the [Bolton1980]_ formula:
17461783
17471784
.. math:: T = \frac{243.5 \log(e / 6.112)}{17.67 - \log(e / 6.112)}
17481785
1786+
The 'ambaum' method numerically inverts the [Ambaum2020]_ formula used by
1787+
:func:`saturation_vapor_pressure`, ensuring thermodynamic consistency
1788+
where ``dewpoint(saturation_vapor_pressure(T), method='ambaum') == T``.
1789+
17491790
.. versionchanged:: 1.0
17501791
Renamed ``e`` parameter to ``vapor_pressure``
17511792
1793+
.. versionchanged:: 1.8
1794+
Added ``method`` parameter with 'ambaum' option for consistency with
1795+
:func:`saturation_vapor_pressure`.
1796+
17521797
"""
1753-
val = np.log(vapor_pressure / mpconsts.nounit.sat_pressure_0c)
1754-
return mpconsts.nounit.zero_degc + 243.5 * val / (17.67 - val)
1798+
if method == 'ambaum':
1799+
return _dewpoint_ambaum(vapor_pressure)
1800+
elif method == 'bolton':
1801+
# Original Bolton (1980) analytical formula
1802+
val = np.log(vapor_pressure / mpconsts.nounit.sat_pressure_0c)
1803+
return mpconsts.nounit.zero_degc + 243.5 * val / (17.67 - val)
1804+
else:
1805+
raise ValueError(f"Unknown method '{method}'. Must be 'bolton' or 'ambaum'.")
17551806

17561807

17571808
@exporter.export

tests/calc/test_thermo.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,20 @@ def test_dewpoint_weird_units():
436436
13.8564 * units.degC, 4)
437437

438438

439+
def test_dewpoint_ambaum_roundtrip():
440+
"""Test that dewpoint with ambaum method round-trips with saturation_vapor_pressure."""
441+
temperatures = np.array([-40, -20, 0, 20, 40]) * units.degC
442+
443+
# Calculate saturation vapor pressure
444+
e_sat = saturation_vapor_pressure(temperatures)
445+
446+
# Calculate dewpoint using ambaum method
447+
td = dewpoint(e_sat, method='ambaum')
448+
449+
# Should round-trip with very small error
450+
assert_array_almost_equal(td, temperatures, 10)
451+
452+
439453
def test_mixing_ratio():
440454
"""Test mixing ratio calculation."""
441455
p = 998. * units.mbar

0 commit comments

Comments
 (0)