Skip to content

Calculate Relative Humidity via Magnus Tetens Equation #2286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
70e0156
changed default types to float in solarposition.py
Oct 29, 2024
0ec0cbf
switched to floats in the docstrings and removed type hints
Oct 30, 2024
5eb9d00
added magnus_tetens equations
Oct 30, 2024
ab2de3a
line too long in solarposition.py
Oct 30, 2024
f62bd8e
revert solarposition.py to pre-PR state
Oct 30, 2024
77025c7
set default magnus coefficients per conversation in #1744
Nov 5, 2024
b52f9a9
moved equations to atmosphere.py, updated function names to suggested…
Nov 11, 2024
58b246e
moved tests to atmosphere.py tests
Nov 11, 2024
a06871a
changed reference to WMO
Nov 11, 2024
caa4417
dry-bult temperature in the docstring
Nov 11, 2024
edd84b1
revert pyproject.toml
Nov 11, 2024
9c32f6a
remove uv.lock
Nov 11, 2024
63ed5c7
Update pvlib/atmosphere.py
kurt-rhee Nov 18, 2024
6312f92
fixing flake8 errors for tdew/rh functions
Nov 18, 2024
410e95f
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
d94df47
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
e6b5908
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
63a9226
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
459d5a6
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
05b846f
Update pvlib/atmosphere.py
kurt-rhee Nov 25, 2024
0ae31e1
added some unit tests for different coefficients and input types
kurt-rhee Dec 1, 2024
337f723
refactored tests, removed magnus_tetens, updated whatsnew
kurt-rhee Dec 5, 2024
8bc9b86
reference and whatsnew
kurt-rhee Dec 5, 2024
9651c3b
revert line 36 (linter)
kurt-rhee Dec 5, 2024
b860ca5
Update pvlib/atmosphere.py
kurt-rhee Dec 7, 2024
2bdc1dc
Update docs/sphinx/source/whatsnew/v0.11.2.rst
kurt-rhee Dec 7, 2024
c39c449
Update pvlib/atmosphere.py
kurt-rhee Dec 7, 2024
b5a6f09
Update pvlib/tests/test_atmosphere.py
kurt-rhee Dec 7, 2024
aedc835
Update pyproject.toml
kurt-rhee Dec 7, 2024
4fcb9e3
Merge branch 'main' into relative-humidity
AdamRJensen Dec 9, 2024
c956c91
Update pvlib/atmosphere.py
kurt-rhee Dec 10, 2024
f682815
Update pvlib/atmosphere.py
kurt-rhee Dec 10, 2024
0bfc6cb
Update pvlib/atmosphere.py
kurt-rhee Dec 10, 2024
71a165e
Update pvlib/atmosphere.py
kurt-rhee Dec 10, 2024
25527d6
Update pvlib/atmosphere.py
kurt-rhee Dec 10, 2024
1eff53f
added a round-trip test for magnus tetens
kurt-rhee Dec 11, 2024
df33a31
temperature -> temp_air; dewpoint -> temp_dew
kandersolar Dec 12, 2024
b4802a3
miscellaneous other cleanup
kandersolar Dec 12, 2024
3893692
tests: put assertions next to calculations
kandersolar Dec 12, 2024
11d81c3
linter
kandersolar Dec 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions pvlib/atmosphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,82 @@
return pw


def rh_from_tdew(temperature, dewpoint, coeff=(6.112, 17.62, 243.12)):
Copy link
Member

@kandersolar kandersolar Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed earlier but forgot to comment: we should rename this parameter to temp_dew, the name used by other pvlib models and iotools functions. I'll push a commit for that to give @kurt-rhee a break from this PR :P

edit: and same for temp_air of course

"""
Calculate relative humidity from dewpoint temperature using the Magnus equation.

Check failure on line 342 in pvlib/atmosphere.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E501 line too long (84 > 79 characters)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Calculate relative humidity from dewpoint temperature using the Magnus equation.
Calculate relative humidity from dew-point temperature using the Magnus equation.

Copy link
Member

@cwhanse cwhanse Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(inner grammarian emerges) the term is either "dewpoint" or "dew point": NOAA glossary. The AMS glossary prefers "dewpoint".

I can't find any instances of "dew-point" as a hyphenated adjective, although I understand that's what grammar rules suggest it should be, rather than "dew point".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admittedly I didn't search beyond the WMO document we referenced, so it could be an old-world / new-world thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to use any term that you all prefer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like EPW and TMY3 files use the hyphen. Most of the pvlib docs use "dew point", with the exception of the Nomenclature page which says "Dewpoint".

I suggest we leave this for a potential package-wide cleanup and not worry about it here.


Parameters
----------
temperature : numeric
Air temperature (dry-bulb temperature) in degrees Celsius
dewpoint : numeric
Dewpoint temperature in degrees Celsius
coeff: tuple
Magnus equation coefficient (A, B, C)

Returns
-------
numeric
Relative humidity as percentage (0.0-100.0)

References
----------
.. [1] "Guide to Instruments and Methods of Observation",
World Meteorological Organization, WMO-No. 8, 2023.
https://library.wmo.int/idurl/4/68695
"""

# Calculate vapor pressure (e) and saturation vapor pressure (es)
e = coeff[0] * np.exp((coeff[1] * temperature) / (coeff[2] + temperature))
es = coeff[0] * np.exp((coeff[1] * dewpoint) / (coeff[2] + dewpoint))

# Calculate relative humidity as percentage
relative_humidity = 100 * (es / e)

return relative_humidity


def tdew_from_rh(
temperature, relative_humidity, coeff=(6.112, 17.62, 243.12)
):
"""
Calculate dewpoint temperature using Magnus equation.
This is a reversal of the calculation in :py:func:`rh_from_tdew`.

Parameters
----------
temperature : numeric
Air temperature (dry-bulb temperature) in degrees Celsius
relative_humidity : numeric
Relative humidity as percentage (0-100)

Returns
-------
numeric
Dewpoint temperature in degrees Celsius
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Dewpoint temperature in degrees Celsius
Dew-point temperature in degrees Celsius


References
----------
.. [1] "Guide to Instruments and Methods of Observation",
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)
ln_term = (
(coeff[1] * temperature) / (coeff[2] + temperature)
+ np.log(relative_humidity/100)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like RH zero might throw an error or warning here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a check for zero values or leave the original message in the stack trace?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary, RH=0% is practically impossible (source).

)

# Then solve for dewpoint
dewpoint = coeff[2] * ln_term / (coeff[1] - ln_term)

return dewpoint


first_solar_spectral_correction = deprecated(
since='0.10.0',
alternative='pvlib.spectrum.spectral_factor_firstsolar'
Expand Down
108 changes: 108 additions & 0 deletions pvlib/spectrum/magnus_tetens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import numpy as np

Check warning on line 1 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L1

Added line #L1 was not covered by tests


def magnus_tetens_aekr(temperature, dewpoint, A=6.112, B=17.62, C=243.12):

Check warning on line 4 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L4

Added line #L4 was not covered by tests
"""
Calculate relative humidity using Magnus equation with AEKR coefficients.
This function was used by First Solar in creating their spectral model
and is therefore relevant to the first solar spectral model in pvlib.
Default magnus equation coefficients are from [2].

Parameters
----------
temperature : pd.Series
Air temperature in degrees Celsius
dewpoint : pd.Series
Dewpoint temperature in degrees Celsius
A: float
Magnus equation coefficient A
B: float
Magnus equation coefficient B
C: float
Magnus equation coefficient C

Returns
-------
pd.Series
Relative humidity as percentage (0-100)

Notes
-----
Uses the AEKR coefficients which minimize errors between -40 and
50 degrees C according to reference [1].

References
----------
.. [1] https://www.osti.gov/servlets/purl/548871-PjpxAP/webviewable/
.. [2] https://www.schweizerbart.de//papers/metz/detail/3/89544/Advancements_in_the_field_of_hygrometry?af=crossref

Check failure on line 37 in pvlib/spectrum/magnus_tetens.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E501 line too long (119 > 79 characters)
"""

# Calculate vapor pressure (e) and saturation vapor pressure (es)
e = A * np.exp((B * temperature) / (C + temperature))
es = A * np.exp((B * dewpoint) / (C + dewpoint))

Check warning on line 42 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L41-L42

Added lines #L41 - L42 were not covered by tests

# Calculate relative humidity as percentage
relative_humidity = 100 * (es / e)

Check warning on line 45 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L45

Added line #L45 was not covered by tests

return relative_humidity

Check warning on line 47 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L47

Added line #L47 was not covered by tests


def reverse_magnus_tetens_aekr(

Check warning on line 50 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L50

Added line #L50 was not covered by tests
temperature, relative_humidity, B=17.62, C=243.12
):
"""
Calculate dewpoint temperature using Magnus equation with
AEKR coefficients. This is just a reversal of the calculation
in calculate_relative_humidity.

Parameters
----------
temperature : pd.Series
Air temperature in degrees Celsius
relative_humidity : pd.Series
Relative humidity as percentage (0-100)

Returns
-------
pd.Series
Dewpoint temperature in degrees Celsius

Notes
-----
Derived by solving the Magnus equation for dewpoint given
relative humidity.
Valid for temperatures between -40 and 50 degrees C.

References
----------
.. [1] https://www.osti.gov/servlets/purl/548871-PjpxAP/webviewable/
"""
# 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)
ln_term = (

Check warning on line 85 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L85

Added line #L85 was not covered by tests
(B * temperature) / (C + temperature)
+ np.log(relative_humidity/100)
)

# Then solve for dewpoint
dewpoint = C * ln_term / (B - ln_term)

Check warning on line 91 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L91

Added line #L91 was not covered by tests

return dewpoint

Check warning on line 93 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L93

Added line #L93 was not covered by tests


if __name__ == "__main__":
import pandas as pd
rh = magnus_tetens_aekr(

Check warning on line 98 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L96-L98

Added lines #L96 - L98 were not covered by tests
temperature=pd.Series([20.0, 25.0, 30.0, 15.0, 10.0]),
dewpoint=pd.Series([15.0, 20.0, 25.0, 12.0, 8.0])
)

dewpoint = reverse_magnus_tetens_aekr(

Check warning on line 103 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L103

Added line #L103 was not covered by tests
temperature=pd.Series([20.0, 25.0, 30.0, 15.0, 10.0]),
relative_humidity=rh
)
print(rh)
print(dewpoint)

Check warning on line 108 in pvlib/spectrum/magnus_tetens.py

View check run for this annotation

Codecov / codecov/patch

pvlib/spectrum/magnus_tetens.py#L107-L108

Added lines #L107 - L108 were not covered by tests
36 changes: 36 additions & 0 deletions pvlib/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,39 @@
'IXXO': 3.18803,
'FD': 1}
return parameters


@pytest.fixture(scope='function')
def tdew_from_rh_tamb():
temperature = pd.Series([20.0, 25.0, 30.0, 15.0, 10.0])
return temperature


@pytest.fixture(scope='function')
def tdew_from_rh_tdew():
dewpoint = pd.Series([15.0, 20.0, 25.0, 12.0, 8.0])
return dewpoint

@pytest.fixture(scope='function')

Check failure on line 490 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E302 expected 2 blank lines, found 1
def tdew_from_rh_tdew_aekr():
dewpoint = pd.Series([
15.002788636614607, 20.00273293813965, 25.002679259728964,
12.001714029144065, 8.0011690397714
])
return dewpoint

@pytest.fixture(scope='function')

Check failure on line 498 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E302 expected 2 blank lines, found 1
def tdew_from_rh_rh():
relative_humidity = pd.Series([
72.95185312581116, 73.81500029087906, 74.6401272083123,
82.27063889868842, 87.39018119185337
])
return relative_humidity

@pytest.fixture(scope='function')

Check failure on line 506 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E302 expected 2 blank lines, found 1
def tdew_from_rh_rh_aekr():
relative_humidity = pd.Series([

Check failure on line 508 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E111 indentation is not a multiple of 4
72.93876680928582, 73.8025121880607, 74.62820502423823,
82.26135295757305, 87.38323744820416

Check failure on line 510 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E131 continuation line unaligned for hanging indent
])
return relative_humidity

Check failure on line 512 in pvlib/tests/conftest.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E111 indentation is not a multiple of 4
114 changes: 113 additions & 1 deletion pvlib/tests/test_atmosphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
[['simple', [nan, 572.958, 5.759, 1.000]],
['kasten1966', [nan, 35.365, 5.580, 0.999]],
['youngirvine1967', [
nan, -2.251358367165932e+05, 5.5365, 1.0000]],
nan, -2.251358367165932e+05, 5.5365, 1.0000]],

Check failure on line 36 in pvlib/tests/test_atmosphere.py

View workflow job for this annotation

GitHub Actions / flake8-linter

E121 continuation line under-indented for hanging indent
['kastenyoung1989', [nan, 36.467, 5.586, 1.000]],
['gueymard1993', [nan, 36.431, 5.581, 1.000]],
['young1994', [nan, 30.733, 5.541, 1.000]],
Expand Down Expand Up @@ -88,6 +88,118 @@
assert_allclose(pws, expected, atol=0.01)


# Unit tests
def test_rh_from_tdew(
tdew_from_rh_tamb, tdew_from_rh_tdew,
tdew_from_rh_tdew_aekr, tdew_from_rh_rh,
tdew_from_rh_rh_aekr
):

# Calculate relative humidity using pandas series as input
rh_series = atmosphere.rh_from_tdew(
temperature=tdew_from_rh_tamb,
dewpoint=tdew_from_rh_tdew
)

# Calulate relative humidity using pandas series as input
# with AEKR coefficients
rh_series_aekr = atmosphere.rh_from_tdew(
temperature=tdew_from_rh_tamb,
dewpoint=tdew_from_rh_tdew,
coeff=(6.1094, 17.625, 243.04)
)

# Calculate relative humidity using array as input
rh_array = atmosphere.rh_from_tdew(
temperature=tdew_from_rh_tamb.to_numpy(),
dewpoint=tdew_from_rh_tdew.to_numpy()
)

# Calculate relative humidity using float as input
rh_float = atmosphere.rh_from_tdew(
temperature=tdew_from_rh_tamb.iloc[0],
dewpoint=tdew_from_rh_tdew.iloc[0]
)

# test
pd.testing.assert_series_equal(
rh_series,
tdew_from_rh_rh,
check_names=False
)

pd.testing.assert_series_equal(
rh_series_aekr,
tdew_from_rh_rh_aekr,
check_names=False
)

np.testing.assert_allclose(
rh_array,
tdew_from_rh_rh.to_numpy(),
atol=0.001
)

assert np.isclose(
rh_float,
tdew_from_rh_rh.iloc[0]
)


# Unit tests
def test_tdew_from_rh(
tdew_from_rh_tamb, tdew_from_rh_tdew,
tdew_from_rh_rh, tdew_from_rh_rh_aekr,
tdew_from_rh_tdew_aekr
):

# test as series
dewpoint_series = atmosphere.tdew_from_rh(
temperature=tdew_from_rh_tamb,
relative_humidity=tdew_from_rh_rh
)

# test as series with AEKR coefficients
dewpoint_series_aekr = atmosphere.tdew_from_rh(
temperature=tdew_from_rh_tamb,
relative_humidity=tdew_from_rh_rh,
coeff=(6.1094, 17.625, 243.04)
)

# test as numpy array
dewpoint_array = atmosphere.tdew_from_rh(
temperature=tdew_from_rh_tamb.to_numpy(),
relative_humidity=tdew_from_rh_rh.to_numpy()
)

# test as float
dewpoint_float = atmosphere.tdew_from_rh(
temperature=tdew_from_rh_tamb.iloc[0],
relative_humidity=tdew_from_rh_rh.iloc[0]
)

# test
pd.testing.assert_series_equal(
dewpoint_series, tdew_from_rh_tdew, check_names=False
)

pd.testing.assert_series_equal(
dewpoint_series_aekr, tdew_from_rh_tdew_aekr,
check_names=False
)

np.testing.assert_allclose(
dewpoint_array,
tdew_from_rh_tdew.to_numpy(),
atol=0.001
)

assert np.isclose(
dewpoint_float,
tdew_from_rh_tdew.iloc[0]
)


def test_first_solar_spectral_correction_deprecated():
with pytest.warns(pvlibDeprecationWarning,
match='Use pvlib.spectrum.spectral_factor_firstsolar'):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ optional = [
]
doc = [
'ipython',
'pickleshare', # required by ipython
'pickleshare', # required by ipython
'matplotlib',
'sphinx == 7.3.7',
'pydata-sphinx-theme == 0.15.4',
Expand Down
Loading