From 2bfba2318d36c2e357957248d19f60d115328f33 Mon Sep 17 00:00:00 2001 From: gaoflow Date: Thu, 11 Jun 2026 00:09:43 +0200 Subject: [PATCH 1/2] Clarify rh_from_tdew variable naming, comments and references In rh_from_tdew the intermediate named `e` was computed from the air temperature (so it was actually the saturation vapor pressure) and `es` from the dew point (the actual vapor pressure) -- swapped relative to the usual convention, which led #2734 to read it as an inverted formula. The returned values are correct and unchanged (100 * es/e with the swapped names equals 100 * e/es with conventional ones); this computes `e` from the dew point and `es` from the air temperature so the source matches the convention, and fixes the matching derivation comment in tdew_from_rh. Also adds a Notes section and the Alduchov & Eskridge (1996) reference for the Magnus form, plus a physical-bounds regression test (saturation when dew point == air temperature, RH below 100% and monotonic in the dew point otherwise) so the direction cannot silently invert. Addresses #2734. --- docs/sphinx/source/whatsnew/v0.15.2.rst | 8 ++++++ pvlib/atmosphere.py | 38 ++++++++++++++++--------- tests/test_atmosphere.py | 13 +++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 97383a5723..0732bb803d 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -53,6 +53,14 @@ Documentation * Clarify 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 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(): From 19ae5f11ae89590b10164497176cdf4192f6ac12 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sun, 14 Jun 2026 16:04:50 +0200 Subject: [PATCH 2/2] Add explicit WMO temperature range to rh_from_tdew notes State the -45 to +60 C validity range of the Magnus form directly in the Notes, citing WMO-No. 8 [1], instead of deferring to the reference. --- pvlib/atmosphere.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 4322ced22b..7e826c3828 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -370,8 +370,8 @@ def rh_from_tdew(temp_air, temp_dew, coeff=(6.112, 17.62, 243.12)): 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). + Magnus form for saturation over liquid water, valid for temperatures from + -45 to +60 °C [1]_; see [2]_ for the approximation and its accuracy. References ----------