Skip to content

Commit 6d5a9fe

Browse files
committed
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.
1 parent a53e022 commit 6d5a9fe

3 files changed

Lines changed: 44 additions & 13 deletions

File tree

docs/sphinx/source/whatsnew/v0.15.2.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ Documentation
4242
* Clarifies how Linke turbidity values can be provided to
4343
:py:func:`pvlib.clearsky.ineichen` via
4444
:py:func:`pvlib.clearsky.lookup_linke_turbidity` (:issue:`2598`, :pull:`2746`)
45+
* Clarifies the variable naming, comments and references in
46+
:py:func:`pvlib.atmosphere.rh_from_tdew` (and the related comments in
47+
:py:func:`pvlib.atmosphere.tdew_from_rh`) so the actual and saturation vapor
48+
pressures are no longer swapped in the source. The returned values are
49+
unchanged. (:issue:`2734`, :pull:`2782`)
4550

4651

4752
Testing
@@ -67,3 +72,4 @@ Contributors
6772
* :ghuser:`shethkajal7`
6873
* Arthur Onno (:ghuser:`ArthurOnnoTerabase`)
6974
* Adam R. Jensen (:ghuser:`AdamRJensen`)
75+
* :ghuser:`gaoflow`

pvlib/atmosphere.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,34 @@ def rh_from_tdew(temp_air, temp_dew, coeff=(6.112, 17.62, 243.12)):
363363
numeric
364364
Relative humidity (0.0-100.0). [%]
365365
366+
Notes
367+
-----
368+
Relative humidity is computed as ``100 * e / es``, where the actual vapor
369+
pressure ``e`` is the saturation vapor pressure at the dew point and the
370+
saturation vapor pressure ``es`` is evaluated at the air temperature, both
371+
from the Magnus equation ``A * exp(B * T / (C + T))``. The default
372+
coefficients ``(A, B, C) = (6.112, 17.62, 243.12)`` are the WMO-recommended
373+
Magnus form [1]_, valid for saturation over liquid water (see [2]_ for the
374+
approximation and its temperature range).
375+
366376
References
367377
----------
368378
.. [1] "Guide to Instruments and Methods of Observation",
369379
World Meteorological Organization, WMO-No. 8, 2023.
370380
https://library.wmo.int/idurl/4/68695
381+
.. [2] O. A. Alduchov and R. E. Eskridge, "Improved Magnus Form
382+
Approximation of Saturation Vapor Pressure", Journal of Applied
383+
Meteorology, 35(4), pp. 601-609, 1996.
371384
"""
372385

373-
# Calculate vapor pressure (e) and saturation vapor pressure (es)
374-
e = coeff[0] * np.exp((coeff[1] * temp_air) / (coeff[2] + temp_air))
375-
es = coeff[0] * np.exp((coeff[1] * temp_dew) / (coeff[2] + temp_dew))
386+
# Actual vapor pressure ``e`` is the saturation vapor pressure at the dew
387+
# point; the saturation vapor pressure ``es`` is taken at the air
388+
# temperature. Both come from the Magnus equation.
389+
e = coeff[0] * np.exp((coeff[1] * temp_dew) / (coeff[2] + temp_dew))
390+
es = coeff[0] * np.exp((coeff[1] * temp_air) / (coeff[2] + temp_air))
376391

377-
# Calculate relative humidity as percentage
378-
relative_humidity = 100 * (es / e)
392+
# Relative humidity is their ratio, as a percentage.
393+
relative_humidity = 100 * (e / es)
379394

380395
return relative_humidity
381396

@@ -406,17 +421,14 @@ def tdew_from_rh(temp_air, relative_humidity, coeff=(6.112, 17.62, 243.12)):
406421
World Meteorological Organization, WMO-No. 8, 2023.
407422
https://library.wmo.int/idurl/4/68695
408423
"""
409-
# Calculate the term inside the log
410-
# From RH = 100 * (es/e), we get es = (RH/100) * e
411-
# Substituting the Magnus equation and solving for dewpoint
412-
413-
# First calculate ln(es/A)
424+
# Invert RH = 100 * (e / es): the actual vapor pressure is
425+
# e = (RH / 100) * es, and the dew point is the temperature at which the
426+
# saturation vapor pressure equals e. Substituting the Magnus equation for
427+
# both and solving for the dew point gives the expression below.
414428
ln_term = (
415429
(coeff[1] * temp_air) / (coeff[2] + temp_air)
416-
+ np.log(relative_humidity/100)
430+
+ np.log(relative_humidity / 100)
417431
)
418-
419-
# Then solve for dewpoint
420432
dewpoint = coeff[2] * ln_term / (coeff[1] - ln_term)
421433

422434
return dewpoint

tests/test_atmosphere.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ def test_rh_from_tdew():
175175
assert np.isclose(rh_float, relative_humidity_wmo.iloc[0])
176176

177177

178+
def test_rh_from_tdew_physical_bounds():
179+
# The dew point cannot exceed the air temperature: equal values mean the
180+
# air is saturated (100% RH), and a lower dew point gives a lower RH. This
181+
# pins the direction of the calculation so it cannot silently invert.
182+
assert atmosphere.rh_from_tdew(
183+
temp_air=20.0, temp_dew=20.0
184+
) == pytest.approx(100.0)
185+
assert atmosphere.rh_from_tdew(temp_air=20.0, temp_dew=10.0) < 100.0
186+
assert atmosphere.rh_from_tdew(
187+
temp_air=20.0, temp_dew=5.0
188+
) < atmosphere.rh_from_tdew(temp_air=20.0, temp_dew=15.0)
189+
190+
178191
# Unit tests
179192
def test_tdew_from_rh():
180193

0 commit comments

Comments
 (0)