Skip to content

Improve extraradiation input/output types #219

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 6 commits into from
Jul 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.4.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ API Changes
in addition to arrays and Series. Furthermore, these functions no
longer promote scalar or array input to Series output.
Also applies to atmosphere.relativeairmass. (:issue:`201`, :issue:`214`)
* The irradiance.extraradiation function input/output type consistency
across different methods has been dramatically improved.
(:issue:`217`, :issue:`219`)
* Updated to pvsystem.sapm to be consistent with the PVLIB MATLAB API.
pvsystem.sapm now takes an effective irradiance argument instead of
POA irradiances, airmass, and AOI. Implements closely related
Expand Down Expand Up @@ -60,6 +63,7 @@ Documentation
* Added new terms to the variables documentation. (:issue:`195`)
* Added clear sky documentation page.
* Fix documentation build warnings. (:issue:`210`)
* Removed an unneeded note in irradiance.extraradiation. (:issue:`216`)


Other
Expand Down
198 changes: 144 additions & 54 deletions docs/tutorials/irradiance.ipynb

Large diffs are not rendered by default.

148 changes: 55 additions & 93 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import datetime
from collections import OrderedDict
from functools import partial

import numpy as np
import pandas as pd
Expand All @@ -36,15 +37,15 @@


def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer',
**kwargs):
epoch_year=2014, **kwargs):
"""
Determine extraterrestrial radiation from day of year.

Parameters
----------
datetime_or_doy : int, float, array, pd.DatetimeIndex
Day of year, array of days of year e.g.
pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
datetime_or_doy : int, float, array, date, datetime, datetime64,
Timestamp, DatetimeIndex
Day of year, array of days of year, or datetime-like object

solar_constant : float
The solar constant.
Expand All @@ -53,132 +54,93 @@ def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer',
The method by which the ET radiation should be calculated.
Options include ``'pyephem', 'spencer', 'asce', 'nrel'``.

epoch_year : int
The year in which a day of year input will be calculated. Only
applies to day of year input used with the pyephem or nrel
methods.

kwargs :
Passed to solarposition.nrel_earthsun_distance

Returns
-------
dni_extra : float, array, or Series
The extraterrestrial radiation present in watts per square meter
on a surface which is normal to the sun. Ea is of the same size
as the input doy.

'pyephem' and 'nrel' always return a Series.

Notes
-----
The Spencer method contains a minus sign discrepancy between
equation 12 of [1]. It's unclear what the correct formula is.
on a surface which is normal to the sun. Pandas Timestamp and
DatetimeIndex inputs will yield a Pandas TimeSeries. All other
inputs will yield a float or an array of floats.

References
----------
[1] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance
Clear Sky Models: Implementation and Analysis", Sandia National
Laboratories, SAND2012-2389, 2012.

[2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>,
Eqs. SR1 and SR2

[3] Partridge, G. W. and Platt, C. M. R. 1976.
Radiative Processes in Meteorology and Climatology.
[2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>, Eqs.
SR1 and SR2

[4] Duffie, J. A. and Beckman, W. A. 1991.
Solar Engineering of Thermal Processes,
2nd edn. J. Wiley and Sons, New York.
[3] Partridge, G. W. and Platt, C. M. R. 1976. Radiative Processes
in Meteorology and Climatology.

See Also
--------
pvlib.clearsky.disc
[4] Duffie, J. A. and Beckman, W. A. 1991. Solar Engineering of
Thermal Processes, 2nd edn. J. Wiley and Sons, New York.
"""

pvl_logger.debug('irradiance.extraradiation()')

method = method.lower()

# This block will set the functions that can be used to convert the
# inputs to either day of year or pandas DatetimeIndex, and the
# functions that will yield the appropriate output type. It's
# complicated because there are many day-of-year-like input types,
# and the different algorithms need different types. Maybe you have
# a better way to do it.
if isinstance(datetime_or_doy, pd.DatetimeIndex):
doy = datetime_or_doy.dayofyear
input_to_datetimeindex = lambda x: datetime_or_doy
elif isinstance(datetime_or_doy, (int, float)):
doy = datetime_or_doy
input_to_datetimeindex = _scalar_to_datetimeindex
else: # assume that we have an array-like object of doy. danger?
doy = datetime_or_doy
input_to_datetimeindex = _array_to_datetimeindex

B = (2. * np.pi / 365.) * (doy - 1)
to_doy = tools._pandas_to_doy # won't be evaluated unless necessary
to_datetimeindex = lambda x: datetime_or_doy
to_output = partial(pd.Series, index=datetime_or_doy)
elif isinstance(datetime_or_doy, pd.Timestamp):
to_doy = tools._pandas_to_doy
to_datetimeindex = \
tools._datetimelike_scalar_to_datetimeindex
to_output = tools._scalar_out
elif isinstance(datetime_or_doy,
(datetime.date, datetime.datetime, np.datetime64)):
to_doy = tools._datetimelike_scalar_to_doy
to_datetimeindex = \
tools._datetimelike_scalar_to_datetimeindex
to_output = tools._scalar_out
elif np.isscalar(datetime_or_doy): # ints and floats of various types
to_doy = lambda x: datetime_or_doy
to_datetimeindex = partial(tools._doy_to_datetimeindex,
epoch_year=epoch_year)
to_output = tools._scalar_out
else: # assume that we have an array-like object of doy
to_doy = lambda x: datetime_or_doy
to_datetimeindex = partial(tools._doy_to_datetimeindex,
epoch_year=epoch_year)
to_output = tools._array_out

method = method.lower()
if method == 'asce':
pvl_logger.debug('Calculating ET rad using ASCE method')
B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy))
RoverR0sqrd = 1 + 0.033 * np.cos(B)
elif method == 'spencer':
pvl_logger.debug('Calculating ET rad using Spencer method')
B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy))
RoverR0sqrd = (1.00011 + 0.034221 * np.cos(B) + 0.00128 * np.sin(B) +
0.000719 * np.cos(2 * B) + 7.7e-05 * np.sin(2 * B))
elif method == 'pyephem':
pvl_logger.debug('Calculating ET rad using pyephem method')
times = input_to_datetimeindex(datetime_or_doy)
times = to_datetimeindex(datetime_or_doy)
RoverR0sqrd = solarposition.pyephem_earthsun_distance(times) ** (-2)
elif method == 'nrel':
times = input_to_datetimeindex(datetime_or_doy)
times = to_datetimeindex(datetime_or_doy)
RoverR0sqrd = \
solarposition.nrel_earthsun_distance(times, **kwargs) ** (-2)
else:
raise ValueError('Invalid method: %s', method)

Ea = solar_constant * RoverR0sqrd

return Ea


def _scalar_to_datetimeindex(doy_scalar):
"""
Convert a scalar day of year number to a pd.DatetimeIndex.
Ea = to_output(Ea)

Parameters
----------
doy_array : int or float
Contains days of the year

Returns
-------
pd.DatetimeIndex
"""
return pd.DatetimeIndex([_doy_to_timestamp(doy_scalar)])


def _array_to_datetimeindex(doy_array):
"""
Convert an array of day of year numbers to a pd.DatetimeIndex.

Parameters
----------
doy_array : Iterable
Contains days of the year

Returns
-------
pd.DatetimeIndex
"""
return pd.DatetimeIndex(list(map(_doy_to_timestamp, doy_array)))


def _doy_to_timestamp(doy, epoch='2013-12-31'):
"""
Convert a numeric day of the year to a pd.Timestamp.

Parameters
----------
doy : int or float.
Numeric day of year.
epoch : pd.Timestamp compatible object.
Date to which to add the day of year to.

Returns
-------
pd.Timestamp
"""
return pd.Timestamp('2013-12-31') + datetime.timedelta(days=float(doy))
return Ea


def aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth):
Expand Down
15 changes: 15 additions & 0 deletions pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,3 +822,18 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=None, numthreads=4):
R = pd.Series(R, index=time)

return R


def _calculate_simple_day_angle(dayofyear):
"""
Calculates the day angle for the Earth's orbit around the Sun.

Parameters
----------
dayofyear : numeric

Returns
-------
day_angle : numeric
"""
return (2. * np.pi / 365.) * (dayofyear - 1)
87 changes: 32 additions & 55 deletions pvlib/test/test_irradiance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from collections import OrderedDict

import numpy as np
Expand Down Expand Up @@ -32,70 +33,46 @@
ghi = irrad_data['ghi']


# the test functions. these are almost all functional tests.
# need to add physical tests.

def test_extraradiation():
assert_allclose(1382, irradiance.extraradiation(300), atol=10)


def test_extraradiation_dtindex():
irradiance.extraradiation(times)


def test_extraradiation_doyarray():
irradiance.extraradiation(times.dayofyear)


def test_extraradiation_asce():
assert_allclose(
1382, irradiance.extraradiation(300, method='asce'), atol=10)


def test_extraradiation_spencer():
assert_allclose(
1382, irradiance.extraradiation(300, method='spencer'), atol=10)


@requires_ephem
def test_extraradiation_ephem_dtindex():
irradiance.extraradiation(times, method='pyephem')


@requires_ephem
def test_extraradiation_ephem_scalar():
assert_allclose(
1382, irradiance.extraradiation(300, method='pyephem').values[0],
atol=10)


@requires_ephem
def test_extraradiation_ephem_doyarray():
irradiance.extraradiation(times.dayofyear, method='pyephem')


def test_extraradiation_nrel_dtindex():
irradiance.extraradiation(times, method='nrel')


def test_extraradiation_nrel_scalar():
assert_allclose(
1382, irradiance.extraradiation(300, method='nrel').values[0],
atol=10)


def test_extraradiation_nrel_doyarray():
irradiance.extraradiation(times.dayofyear, method='nrel')
# setup for et rad test. put it here for readability
timestamp = pd.Timestamp('20161026')
dt_index = pd.DatetimeIndex([timestamp])
doy = timestamp.dayofyear
dt_date = timestamp.date()
dt_datetime = datetime.datetime.combine(dt_date, datetime.time(0))
dt_np64 = np.datetime64(dt_datetime)
value = 1383.636203

@pytest.mark.parametrize('input, expected', [
(doy, value),
(np.float64(doy), value),
(dt_date, value),
(dt_datetime, value),
(dt_np64, value),
(np.array([doy]), np.array([value])),
(pd.Series([doy]), np.array([value])),
(dt_index, pd.Series([value], index=dt_index)),
(timestamp, value)
])
@pytest.mark.parametrize('method', [
'asce', 'spencer', 'nrel', requires_ephem('pyephem')])
def test_extraradiation(input, expected, method):
out = irradiance.extraradiation(input)
assert_allclose(out, expected, atol=1)


@requires_numba
def test_extraradiation_nrel_numba():
irradiance.extraradiation(times, method='nrel', how='numba', numthreads=8)


def test_extraradiation_epoch_year():
out = irradiance.extraradiation(doy, method='nrel', epoch_year=2012)
assert_allclose(out, 1382.4926804890767, atol=0.1)


def test_extraradiation_invalid():
with pytest.raises(ValueError):
irradiance.extraradiation(times.dayofyear, method='invalid')
irradiance.extraradiation(300, method='invalid')


def test_grounddiffuse_simple_float():
Expand Down
Loading