diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 94cda592f9..e9c2ffb62f 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -55,6 +55,11 @@ Documentation * Clarifies how Linke turbidity values can be provided to :py:func:`pvlib.clearsky.ineichen` via :py:func:`pvlib.clearsky.lookup_linke_turbidity` (:issue:`2598`, :pull:`2746`) +* Clarifies the variable naming, comments and references in + :py:func:`pvlib.atmosphere.rh_from_tdew` (and the related comments in + :py:func:`pvlib.atmosphere.tdew_from_rh`) so the actual and saturation vapor + pressures are no longer swapped in the source. The returned values are + unchanged. (:issue:`2734`, :pull:`2782`) Testing diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index f17a8e5866..4322ced22b 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -363,19 +363,34 @@ def rh_from_tdew(temp_air, temp_dew, coeff=(6.112, 17.62, 243.12)): numeric Relative humidity (0.0-100.0). [%] + Notes + ----- + Relative humidity is computed as ``100 * e / es``, where the actual vapor + pressure ``e`` is the saturation vapor pressure at the dew point and the + saturation vapor pressure ``es`` is evaluated at the air temperature, both + from the Magnus equation ``A * exp(B * T / (C + T))``. The default + coefficients ``(A, B, C) = (6.112, 17.62, 243.12)`` are the WMO-recommended + Magnus form [1]_, valid for saturation over liquid water (see [2]_ for the + approximation and its temperature range). + References ---------- .. [1] "Guide to Instruments and Methods of Observation", World Meteorological Organization, WMO-No. 8, 2023. https://library.wmo.int/idurl/4/68695 + .. [2] O. A. Alduchov and R. E. Eskridge, "Improved Magnus Form + Approximation of Saturation Vapor Pressure", Journal of Applied + Meteorology, 35(4), pp. 601-609, 1996. """ - # Calculate vapor pressure (e) and saturation vapor pressure (es) - e = coeff[0] * np.exp((coeff[1] * temp_air) / (coeff[2] + temp_air)) - es = coeff[0] * np.exp((coeff[1] * temp_dew) / (coeff[2] + temp_dew)) + # Actual vapor pressure ``e`` is the saturation vapor pressure at the dew + # point; the saturation vapor pressure ``es`` is taken at the air + # temperature. Both come from the Magnus equation. + e = coeff[0] * np.exp((coeff[1] * temp_dew) / (coeff[2] + temp_dew)) + es = coeff[0] * np.exp((coeff[1] * temp_air) / (coeff[2] + temp_air)) - # Calculate relative humidity as percentage - relative_humidity = 100 * (es / e) + # Relative humidity is their ratio, as a percentage. + relative_humidity = 100 * (e / es) return relative_humidity @@ -406,17 +421,14 @@ def tdew_from_rh(temp_air, relative_humidity, coeff=(6.112, 17.62, 243.12)): World Meteorological Organization, WMO-No. 8, 2023. https://library.wmo.int/idurl/4/68695 """ - # Calculate the term inside the log - # From RH = 100 * (es/e), we get es = (RH/100) * e - # Substituting the Magnus equation and solving for dewpoint - - # First calculate ln(es/A) + # Invert RH = 100 * (e / es): the actual vapor pressure is + # e = (RH / 100) * es, and the dew point is the temperature at which the + # saturation vapor pressure equals e. Substituting the Magnus equation for + # both and solving for the dew point gives the expression below. ln_term = ( (coeff[1] * temp_air) / (coeff[2] + temp_air) - + np.log(relative_humidity/100) + + np.log(relative_humidity / 100) ) - - # Then solve for dewpoint dewpoint = coeff[2] * ln_term / (coeff[1] - ln_term) return dewpoint diff --git a/tests/test_atmosphere.py b/tests/test_atmosphere.py index 7c3f56051c..6236fe5ab0 100644 --- a/tests/test_atmosphere.py +++ b/tests/test_atmosphere.py @@ -175,6 +175,19 @@ def test_rh_from_tdew(): assert np.isclose(rh_float, relative_humidity_wmo.iloc[0]) +def test_rh_from_tdew_physical_bounds(): + # The dew point cannot exceed the air temperature: equal values mean the + # air is saturated (100% RH), and a lower dew point gives a lower RH. This + # pins the direction of the calculation so it cannot silently invert. + assert atmosphere.rh_from_tdew( + temp_air=20.0, temp_dew=20.0 + ) == pytest.approx(100.0) + assert atmosphere.rh_from_tdew(temp_air=20.0, temp_dew=10.0) < 100.0 + assert atmosphere.rh_from_tdew( + temp_air=20.0, temp_dew=5.0 + ) < atmosphere.rh_from_tdew(temp_air=20.0, temp_dew=15.0) + + # Unit tests def test_tdew_from_rh():