Skip to content

Add albedo function for inland water bodies #2079

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 33 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0e87325
Albedo function
IoannisSifnaios Jun 6, 2024
163cf82
fixed test tolerance
IoannisSifnaios Jun 6, 2024
47efb47
fix linter
IoannisSifnaios Jun 6, 2024
7e44696
changed format of function description
IoannisSifnaios Jun 6, 2024
c3c2fc1
update watsnew
IoannisSifnaios Jun 6, 2024
357cb38
Update test_albedo.py
IoannisSifnaios Jun 6, 2024
55b9271
Update test_albedo.py
IoannisSifnaios Jun 6, 2024
187b6f6
Update pvlib/albedo.py
IoannisSifnaios Jun 10, 2024
2a20e8c
Update pvlib/albedo.py
IoannisSifnaios Jun 10, 2024
75ae142
Update pvlib/albedo.py
IoannisSifnaios Jun 10, 2024
5c93bfe
Reviewers' comments
IoannisSifnaios Jun 10, 2024
e487893
Reviewer's comments vol.2
IoannisSifnaios Jun 10, 2024
b5cef90
Update v0.11.0.rst
IoannisSifnaios Jun 10, 2024
d8b214d
Merge branch 'main' into water_albedo_function
IoannisSifnaios Jun 10, 2024
0e5b464
fixed linter
IoannisSifnaios Jun 10, 2024
79406e2
Merge branch 'water_albedo_function' of https://github.com/IoannisSif…
IoannisSifnaios Jun 10, 2024
0340389
fixed linter vol.2
IoannisSifnaios Jun 10, 2024
f469db3
Added test raising a ValueError
IoannisSifnaios Jun 10, 2024
d2e83f3
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
c9ef664
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
4a089ab
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
7595216
More review comments
IoannisSifnaios Jun 11, 2024
93b6ff8
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
7c99fd2
Reviewers' comments vol.3
IoannisSifnaios Jun 11, 2024
2fce253
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
98a5006
Update test_albedo.py
IoannisSifnaios Jun 11, 2024
94b3f03
Merge branch 'water_albedo_function' of https://github.com/IoannisSif…
IoannisSifnaios Jun 11, 2024
7738020
fixed linter
IoannisSifnaios Jun 11, 2024
7d55e30
now I actually fixed linter
IoannisSifnaios Jun 11, 2024
4bfc6d1
Reviewers' comments vol. 4
IoannisSifnaios Jun 11, 2024
eaaced0
renaming tests
IoannisSifnaios Jun 11, 2024
7776c33
Update pvlib/albedo.py
IoannisSifnaios Jun 11, 2024
0645463
Update albedo.py
IoannisSifnaios Jun 11, 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
9 changes: 9 additions & 0 deletions docs/sphinx/source/reference/irradiance/albedo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. currentmodule:: pvlib

Albedo
------

.. autosummary::
:toctree: ../generated/

albedo.inland_water_dvoracek
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/irradiance/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Irradiance
transposition
decomposition
clearness-index
albedo
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Enhancements
shade perpendicular to ``axis_azimuth``. The function is applicable to both
fixed-tilt and one-axis tracking systems.
(:issue:`1689`, :pull:`1725`, :pull:`1962`)
* Add function :py:func:`pvlib.albedo.inland_water_dvoracek`, to calculate the
albedo for inland water bodies.
(:pull:`2079`)
* Added conversion functions from spectral response ([A/W]) to quantum
efficiency ([unitless]) and vice versa. The conversion functions are
:py:func:`pvlib.spectrum.sr_to_qe` and :py:func:`pvlib.spectrum.qe_to_sr`
Expand Down Expand Up @@ -55,4 +58,5 @@ Contributors
* Cliff Hansen (:ghuser:`cwhanse`)
* Mark Mikofski (:ghuser:`mikofski`)
* Siddharth Kaul (:ghuser:`k10blogger`)
* Ioannis Sifnaios (:ghuser:`IoannisSifnaios`)
* Mark Campanelli (:ghuser:`markcampanelli`)
1 change: 1 addition & 0 deletions pvlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# list spectrum first so it's available for atmosphere & pvsystem (GH 1628)
spectrum,

albedo,
atmosphere,
bifacial,
clearsky,
Expand Down
149 changes: 149 additions & 0 deletions pvlib/albedo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
The ``albedo`` module contains functions for modeling albedo.
"""

from pvlib.tools import sind
import numpy as np
import pandas as pd


WATER_COLOR_COEFFS = {
'clear_water_no_waves': 0.13,
'clear_water_ripples_up_to_2.5cm': 0.16,
'clear_water_ripples_larger_than_2.5cm_occasional_whitecaps': 0.23,
'clear_water_frequent_whitecaps': 0.3,
'green_water_ripples_up_to_2.5cm': 0.22,
'muddy_water_no_waves': 0.19
}

WATER_ROUGHNESS_COEFFS = {
'clear_water_no_waves': 0.29,
'clear_water_ripples_up_to_2.5cm': 0.7,
'clear_water_ripples_larger_than_2.5cm_occasional_whitecaps': 1.25,
'clear_water_frequent_whitecaps': 2,
'green_water_ripples_up_to_2.5cm': 0.7,
'muddy_water_no_waves': 0.29
}


def inland_water_dvoracek(solar_elevation, surface_condition=None,
color_coeff=None, wave_roughness_coeff=None):
r"""
Estimation of albedo for inland water bodies.

The available surface conditions are for inland water bodies, e.g., lakes
and ponds. For ocean/open sea, see
:const:`pvlib.irradiance.SURFACE_ALBEDOS`.

Parameters
----------
solar_elevation : numeric
Sun elevation angle. [degrees]

surface_condition : string, optional
If supplied, overrides ``color_coeff`` and ``wave_roughness_coeff``.
``surface_condition`` can be one of the following:

* ``'clear_water_no_waves'``
* ``'clear_water_ripples_up_to_2.5cm'``
* ``'clear_water_ripples_larger_than_2.5cm_occasional_whitecaps'``
* ``'clear_water_frequent_whitecaps'``
* ``'green_water_ripples_up_to_2.5cm'``
* ``'muddy_water_no_waves'``

color_coeff : float, optional
Water color coefficient. [-]

wave_roughness_coeff : float, optional
Water wave roughness coefficient. [-]

Returns
-------
albedo : numeric
Albedo for inland water bodies. [-]

Raises
------
ValueError
If neither of ``surface_condition`` nor a combination of
``color_coeff`` and ``wave_roughness_coeff`` are given.
If ``surface_condition`` and any of ``color_coeff`` or
``wave_roughness_coeff`` are given. These parameters are
mutually exclusive.

KeyError
If ``surface_condition`` is invalid.

Notes
-----
The equation for calculating the albedo :math:`\rho` is given by

.. math::
:label: albedo

\rho = c^{(r \cdot \sin(\alpha) + 1)}

Inputs to the model are the water color coefficient :math:`c` [-], the
water wave roughness coefficient :math:`r` [-] and the solar elevation
:math:`\alpha` [degrees]. Parameters are provided in [1]_ , and are coded
for convenience in :data:`~pvlib.albedo.WATER_COLOR_COEFFS` and
:data:`~pvlib.albedo.WATER_ROUGHNESS_COEFFS`. The values of these
coefficients are experimentally determined.

+------------------------+-------------------+-------------------------+
| Surface and condition | Color coefficient | Wave roughness |
| | (:math:`c`) | coefficient (:math:`r`) |
+========================+===================+=========================+
| Clear water, no waves | 0.13 | 0.29 |
+------------------------+-------------------+-------------------------+
| Clear water, ripples | 0.16 | 0.70 |
| up to 2.5 cm | | |
+------------------------+-------------------+-------------------------+
| Clear water, ripples | 0.23 | 1.25 |
| larger than 2.5 cm | | |
| (occasional whitecaps) | | |
+------------------------+-------------------+-------------------------+
| Clear water, | 0.30 | 2.00 |
| frequent whitecaps | | |
+------------------------+-------------------+-------------------------+
| Green water, ripples | 0.22 | 0.70 |
| up to 2.5cm | | |
+------------------------+-------------------+-------------------------+
| Muddy water, no waves | 0.19 | 0.29 |
+------------------------+-------------------+-------------------------+

References
----------
.. [1] Dvoracek M.J., Hannabas B. (1990). "Prediction of albedo for use in
evapotranspiration and irrigation scheduling." IN: Visions of the Future
American Society of Agricultural Engineers 04-90: 692-699.
"""

if surface_condition is not None and (
color_coeff is None and wave_roughness_coeff is None
):
# use surface_condition
color_coeff = WATER_COLOR_COEFFS[surface_condition]
wave_roughness_coeff = WATER_ROUGHNESS_COEFFS[surface_condition]

elif surface_condition is None and not (
color_coeff is None or wave_roughness_coeff is None
):
# use provided color_coeff and wave_roughness_coeff
pass
else:
raise ValueError(
"Either a `surface_condition` has to be chosen or"
" a combination of `color_coeff` and"
" `wave_roughness_coeff`.")

solar_elevation_positive = np.where(solar_elevation < 0, 0,
solar_elevation)

albedo = color_coeff ** (wave_roughness_coeff *
sind(solar_elevation_positive) + 1)

if isinstance(solar_elevation, pd.Series):
albedo = pd.Series(albedo, index=solar_elevation.index)

return albedo
Copy link
Member

Choose a reason for hiding this comment

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

It's possible to get an array out despite putting a Series in:

In [399]: inland_water_dvoracek(pd.Series([45]), "muddy_water_no_waves")
Out[399]: array([0.13516185])

This is not an uncommon problem when using numpy tricks like np.where. Our usual workaround is to fix up the types at the end of the function like this:

    if isinstance(solar_elevation, pd.Series):
        albedo = pd.Series(albedo, index=solar_elevation.index)

    return albedo

A little inelegant, but it gets the job done.

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't the type check of int/floats I suggested earlier do the job?

Copy link
Member

Choose a reason for hiding this comment

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

Type checking has some tricky edge cases (e.g. isinstance(np.array([45.])[0], (int, float)) is False because it is an np.float64, not a float). A test based on np.isscalar might be ok.

But I think it is generally better to fix up types at the end instead of following different calculation paths based on type. Less chance of accidentally calculating different output values for different types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kandersolar the problem is that np.where returns an array (even if you pass a pd.Series for the solar_elevation) - so the if statement is never true. I did a fix, but it might not be the prettiest one... can you approve it (or suggest an alternative)?

84 changes: 84 additions & 0 deletions pvlib/tests/test_albedo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import numpy as np
import pandas as pd
import pytest
from pvlib import albedo

from .conftest import assert_series_equal
from numpy.testing import assert_allclose


def test_inland_water_dvoracek_default():
result = albedo.inland_water_dvoracek(solar_elevation=90,
color_coeff=0.13,
wave_roughness_coeff=0.29)
assert_allclose(result, 0.072, 0.001)


def test_inland_water_dvoracek_negative_elevation():
result = albedo.inland_water_dvoracek(solar_elevation=-60,
color_coeff=0.13,
wave_roughness_coeff=0.29)
assert_allclose(result, 0.13, 0.01)


def test_inland_water_dvoracek_string_surface_condition():
result = albedo.inland_water_dvoracek(solar_elevation=90,
surface_condition='clear_water_no_waves') # noqa: E501
assert_allclose(result, 0.072, 0.001)


def test_inland_water_dvoracek_ndarray():
solar_elevs = np.array([-50, 0, 20, 60, 90])
color_coeffs = np.array([0.1, 0.1, 0.2, 0.3, 0.4])
roughness_coeffs = np.array([0.3, 0.3, 0.8, 1.5, 2])
result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs,
color_coeff=color_coeffs,
wave_roughness_coeff=roughness_coeffs) # noqa: E501
expected = np.array([0.1, 0.1, 0.12875, 0.06278, 0.064])
assert_allclose(expected, result, atol=1e-5)


def test_inland_water_dvoracek_series():
times = pd.date_range(start="2015-01-01 00:00", end="2015-01-02 00:00",
freq="6h")
solar_elevs = pd.Series([-50, 0, 20, 60, 90], index=times)
color_coeffs = pd.Series([0.1, 0.1, 0.2, 0.3, 0.4], index=times)
roughness_coeffs = pd.Series([0.1, 0.3, 0.8, 1.5, 2], index=times)
result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs,
color_coeff=color_coeffs,
wave_roughness_coeff=roughness_coeffs) # noqa: E501
expected = pd.Series([0.1, 0.1, 0.12875, 0.06278, 0.064], index=times)
assert_series_equal(expected, result, atol=1e-5)


def test_inland_water_dvoracek_series_mix_with_array():
times = pd.date_range(start="2015-01-01 00:00", end="2015-01-01 06:00",
freq="6h")
solar_elevs = pd.Series([45, 60], index=times)
color_coeffs = 0.13
roughness_coeffs = 0.29
result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs,
color_coeff=color_coeffs,
wave_roughness_coeff=roughness_coeffs) # noqa: E501
expected = pd.Series([0.08555, 0.07787], index=times)
assert_series_equal(expected, result, atol=1e-5)


def test_inland_water_dvoracek_invalid():
with pytest.raises(ValueError, match='Either a `surface_condition` has to '
'be chosen or a combination of `color_coeff` and'
' `wave_roughness_coeff`.'): # no surface info given
albedo.inland_water_dvoracek(solar_elevation=45)
with pytest.raises(KeyError, match='not_a_surface_type'): # invalid type
albedo.inland_water_dvoracek(solar_elevation=45,
surface_condition='not_a_surface_type')
with pytest.raises(ValueError, match='Either a `surface_condition` has to '
'be chosen or a combination of `color_coeff` and'
' `wave_roughness_coeff`.'): # only one coeff given
albedo.inland_water_dvoracek(solar_elevation=45,
color_coeff=0.13)
with pytest.raises(ValueError, match='Either a `surface_condition` has to '
'be chosen or a combination of `color_coeff` and'
' `wave_roughness_coeff`.'): # only one coeff given
albedo.inland_water_dvoracek(solar_elevation=45,
wave_roughness_coeff=0.29)
Loading