Skip to content

Commit e39d4b2

Browse files
authored
Improve extraradiation input/output types (#219)
* remove unneeded extrarad note * make a delicious plate of et rad in/out spaghetti * change timestamp input to float output. update tutorial * dump helper functions in different modules * simplify doy to datetime handling * updated whatsnew
1 parent 5218c9d commit e39d4b2

File tree

6 files changed

+334
-240
lines changed

6 files changed

+334
-240
lines changed

docs/sphinx/source/whatsnew/v0.4.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ API Changes
1919
in addition to arrays and Series. Furthermore, these functions no
2020
longer promote scalar or array input to Series output.
2121
Also applies to atmosphere.relativeairmass. (:issue:`201`, :issue:`214`)
22+
* The irradiance.extraradiation function input/output type consistency
23+
across different methods has been dramatically improved.
24+
(:issue:`217`, :issue:`219`)
2225
* Updated to pvsystem.sapm to be consistent with the PVLIB MATLAB API.
2326
pvsystem.sapm now takes an effective irradiance argument instead of
2427
POA irradiances, airmass, and AOI. Implements closely related
@@ -60,6 +63,7 @@ Documentation
6063
* Added new terms to the variables documentation. (:issue:`195`)
6164
* Added clear sky documentation page.
6265
* Fix documentation build warnings. (:issue:`210`)
66+
* Removed an unneeded note in irradiance.extraradiation. (:issue:`216`)
6367

6468

6569
Other

docs/tutorials/irradiance.ipynb

Lines changed: 144 additions & 54 deletions
Large diffs are not rendered by default.

pvlib/irradiance.py

Lines changed: 55 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import datetime
1212
from collections import OrderedDict
13+
from functools import partial
1314

1415
import numpy as np
1516
import pandas as pd
@@ -36,15 +37,15 @@
3637

3738

3839
def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer',
39-
**kwargs):
40+
epoch_year=2014, **kwargs):
4041
"""
4142
Determine extraterrestrial radiation from day of year.
4243
4344
Parameters
4445
----------
45-
datetime_or_doy : int, float, array, pd.DatetimeIndex
46-
Day of year, array of days of year e.g.
47-
pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
46+
datetime_or_doy : int, float, array, date, datetime, datetime64,
47+
Timestamp, DatetimeIndex
48+
Day of year, array of days of year, or datetime-like object
4849
4950
solar_constant : float
5051
The solar constant.
@@ -53,132 +54,93 @@ def extraradiation(datetime_or_doy, solar_constant=1366.1, method='spencer',
5354
The method by which the ET radiation should be calculated.
5455
Options include ``'pyephem', 'spencer', 'asce', 'nrel'``.
5556
57+
epoch_year : int
58+
The year in which a day of year input will be calculated. Only
59+
applies to day of year input used with the pyephem or nrel
60+
methods.
61+
5662
kwargs :
5763
Passed to solarposition.nrel_earthsun_distance
5864
5965
Returns
6066
-------
6167
dni_extra : float, array, or Series
6268
The extraterrestrial radiation present in watts per square meter
63-
on a surface which is normal to the sun. Ea is of the same size
64-
as the input doy.
65-
66-
'pyephem' and 'nrel' always return a Series.
67-
68-
Notes
69-
-----
70-
The Spencer method contains a minus sign discrepancy between
71-
equation 12 of [1]. It's unclear what the correct formula is.
69+
on a surface which is normal to the sun. Pandas Timestamp and
70+
DatetimeIndex inputs will yield a Pandas TimeSeries. All other
71+
inputs will yield a float or an array of floats.
7272
7373
References
7474
----------
7575
[1] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance
7676
Clear Sky Models: Implementation and Analysis", Sandia National
7777
Laboratories, SAND2012-2389, 2012.
7878
79-
[2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>,
80-
Eqs. SR1 and SR2
81-
82-
[3] Partridge, G. W. and Platt, C. M. R. 1976.
83-
Radiative Processes in Meteorology and Climatology.
79+
[2] <http://solardat.uoregon.edu/SolarRadiationBasics.html>, Eqs.
80+
SR1 and SR2
8481
85-
[4] Duffie, J. A. and Beckman, W. A. 1991.
86-
Solar Engineering of Thermal Processes,
87-
2nd edn. J. Wiley and Sons, New York.
82+
[3] Partridge, G. W. and Platt, C. M. R. 1976. Radiative Processes
83+
in Meteorology and Climatology.
8884
89-
See Also
90-
--------
91-
pvlib.clearsky.disc
85+
[4] Duffie, J. A. and Beckman, W. A. 1991. Solar Engineering of
86+
Thermal Processes, 2nd edn. J. Wiley and Sons, New York.
9287
"""
9388

94-
pvl_logger.debug('irradiance.extraradiation()')
95-
96-
method = method.lower()
97-
89+
# This block will set the functions that can be used to convert the
90+
# inputs to either day of year or pandas DatetimeIndex, and the
91+
# functions that will yield the appropriate output type. It's
92+
# complicated because there are many day-of-year-like input types,
93+
# and the different algorithms need different types. Maybe you have
94+
# a better way to do it.
9895
if isinstance(datetime_or_doy, pd.DatetimeIndex):
99-
doy = datetime_or_doy.dayofyear
100-
input_to_datetimeindex = lambda x: datetime_or_doy
101-
elif isinstance(datetime_or_doy, (int, float)):
102-
doy = datetime_or_doy
103-
input_to_datetimeindex = _scalar_to_datetimeindex
104-
else: # assume that we have an array-like object of doy. danger?
105-
doy = datetime_or_doy
106-
input_to_datetimeindex = _array_to_datetimeindex
107-
108-
B = (2. * np.pi / 365.) * (doy - 1)
96+
to_doy = tools._pandas_to_doy # won't be evaluated unless necessary
97+
to_datetimeindex = lambda x: datetime_or_doy
98+
to_output = partial(pd.Series, index=datetime_or_doy)
99+
elif isinstance(datetime_or_doy, pd.Timestamp):
100+
to_doy = tools._pandas_to_doy
101+
to_datetimeindex = \
102+
tools._datetimelike_scalar_to_datetimeindex
103+
to_output = tools._scalar_out
104+
elif isinstance(datetime_or_doy,
105+
(datetime.date, datetime.datetime, np.datetime64)):
106+
to_doy = tools._datetimelike_scalar_to_doy
107+
to_datetimeindex = \
108+
tools._datetimelike_scalar_to_datetimeindex
109+
to_output = tools._scalar_out
110+
elif np.isscalar(datetime_or_doy): # ints and floats of various types
111+
to_doy = lambda x: datetime_or_doy
112+
to_datetimeindex = partial(tools._doy_to_datetimeindex,
113+
epoch_year=epoch_year)
114+
to_output = tools._scalar_out
115+
else: # assume that we have an array-like object of doy
116+
to_doy = lambda x: datetime_or_doy
117+
to_datetimeindex = partial(tools._doy_to_datetimeindex,
118+
epoch_year=epoch_year)
119+
to_output = tools._array_out
109120

110121
method = method.lower()
111122
if method == 'asce':
112-
pvl_logger.debug('Calculating ET rad using ASCE method')
123+
B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy))
113124
RoverR0sqrd = 1 + 0.033 * np.cos(B)
114125
elif method == 'spencer':
115-
pvl_logger.debug('Calculating ET rad using Spencer method')
126+
B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy))
116127
RoverR0sqrd = (1.00011 + 0.034221 * np.cos(B) + 0.00128 * np.sin(B) +
117128
0.000719 * np.cos(2 * B) + 7.7e-05 * np.sin(2 * B))
118129
elif method == 'pyephem':
119-
pvl_logger.debug('Calculating ET rad using pyephem method')
120-
times = input_to_datetimeindex(datetime_or_doy)
130+
times = to_datetimeindex(datetime_or_doy)
121131
RoverR0sqrd = solarposition.pyephem_earthsun_distance(times) ** (-2)
122132
elif method == 'nrel':
123-
times = input_to_datetimeindex(datetime_or_doy)
133+
times = to_datetimeindex(datetime_or_doy)
124134
RoverR0sqrd = \
125135
solarposition.nrel_earthsun_distance(times, **kwargs) ** (-2)
126136
else:
127137
raise ValueError('Invalid method: %s', method)
128138

129139
Ea = solar_constant * RoverR0sqrd
130140

131-
return Ea
132-
133-
134-
def _scalar_to_datetimeindex(doy_scalar):
135-
"""
136-
Convert a scalar day of year number to a pd.DatetimeIndex.
141+
Ea = to_output(Ea)
137142

138-
Parameters
139-
----------
140-
doy_array : int or float
141-
Contains days of the year
142-
143-
Returns
144-
-------
145-
pd.DatetimeIndex
146-
"""
147-
return pd.DatetimeIndex([_doy_to_timestamp(doy_scalar)])
148-
149-
150-
def _array_to_datetimeindex(doy_array):
151-
"""
152-
Convert an array of day of year numbers to a pd.DatetimeIndex.
153-
154-
Parameters
155-
----------
156-
doy_array : Iterable
157-
Contains days of the year
158-
159-
Returns
160-
-------
161-
pd.DatetimeIndex
162-
"""
163-
return pd.DatetimeIndex(list(map(_doy_to_timestamp, doy_array)))
164-
165-
166-
def _doy_to_timestamp(doy, epoch='2013-12-31'):
167-
"""
168-
Convert a numeric day of the year to a pd.Timestamp.
169-
170-
Parameters
171-
----------
172-
doy : int or float.
173-
Numeric day of year.
174-
epoch : pd.Timestamp compatible object.
175-
Date to which to add the day of year to.
176-
177-
Returns
178-
-------
179-
pd.Timestamp
180-
"""
181-
return pd.Timestamp('2013-12-31') + datetime.timedelta(days=float(doy))
143+
return Ea
182144

183145

184146
def aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth):

pvlib/solarposition.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,18 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=None, numthreads=4):
822822
R = pd.Series(R, index=time)
823823

824824
return R
825+
826+
827+
def _calculate_simple_day_angle(dayofyear):
828+
"""
829+
Calculates the day angle for the Earth's orbit around the Sun.
830+
831+
Parameters
832+
----------
833+
dayofyear : numeric
834+
835+
Returns
836+
-------
837+
day_angle : numeric
838+
"""
839+
return (2. * np.pi / 365.) * (dayofyear - 1)

pvlib/test/test_irradiance.py

Lines changed: 32 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from collections import OrderedDict
23

34
import numpy as np
@@ -32,70 +33,46 @@
3233
ghi = irrad_data['ghi']
3334

3435

35-
# the test functions. these are almost all functional tests.
36-
# need to add physical tests.
37-
38-
def test_extraradiation():
39-
assert_allclose(1382, irradiance.extraradiation(300), atol=10)
40-
41-
42-
def test_extraradiation_dtindex():
43-
irradiance.extraradiation(times)
44-
45-
46-
def test_extraradiation_doyarray():
47-
irradiance.extraradiation(times.dayofyear)
48-
49-
50-
def test_extraradiation_asce():
51-
assert_allclose(
52-
1382, irradiance.extraradiation(300, method='asce'), atol=10)
53-
54-
55-
def test_extraradiation_spencer():
56-
assert_allclose(
57-
1382, irradiance.extraradiation(300, method='spencer'), atol=10)
58-
59-
60-
@requires_ephem
61-
def test_extraradiation_ephem_dtindex():
62-
irradiance.extraradiation(times, method='pyephem')
63-
64-
65-
@requires_ephem
66-
def test_extraradiation_ephem_scalar():
67-
assert_allclose(
68-
1382, irradiance.extraradiation(300, method='pyephem').values[0],
69-
atol=10)
70-
71-
72-
@requires_ephem
73-
def test_extraradiation_ephem_doyarray():
74-
irradiance.extraradiation(times.dayofyear, method='pyephem')
75-
76-
77-
def test_extraradiation_nrel_dtindex():
78-
irradiance.extraradiation(times, method='nrel')
79-
80-
81-
def test_extraradiation_nrel_scalar():
82-
assert_allclose(
83-
1382, irradiance.extraradiation(300, method='nrel').values[0],
84-
atol=10)
85-
86-
87-
def test_extraradiation_nrel_doyarray():
88-
irradiance.extraradiation(times.dayofyear, method='nrel')
36+
# setup for et rad test. put it here for readability
37+
timestamp = pd.Timestamp('20161026')
38+
dt_index = pd.DatetimeIndex([timestamp])
39+
doy = timestamp.dayofyear
40+
dt_date = timestamp.date()
41+
dt_datetime = datetime.datetime.combine(dt_date, datetime.time(0))
42+
dt_np64 = np.datetime64(dt_datetime)
43+
value = 1383.636203
44+
45+
@pytest.mark.parametrize('input, expected', [
46+
(doy, value),
47+
(np.float64(doy), value),
48+
(dt_date, value),
49+
(dt_datetime, value),
50+
(dt_np64, value),
51+
(np.array([doy]), np.array([value])),
52+
(pd.Series([doy]), np.array([value])),
53+
(dt_index, pd.Series([value], index=dt_index)),
54+
(timestamp, value)
55+
])
56+
@pytest.mark.parametrize('method', [
57+
'asce', 'spencer', 'nrel', requires_ephem('pyephem')])
58+
def test_extraradiation(input, expected, method):
59+
out = irradiance.extraradiation(input)
60+
assert_allclose(out, expected, atol=1)
8961

9062

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

9567

68+
def test_extraradiation_epoch_year():
69+
out = irradiance.extraradiation(doy, method='nrel', epoch_year=2012)
70+
assert_allclose(out, 1382.4926804890767, atol=0.1)
71+
72+
9673
def test_extraradiation_invalid():
9774
with pytest.raises(ValueError):
98-
irradiance.extraradiation(times.dayofyear, method='invalid')
75+
irradiance.extraradiation(300, method='invalid')
9976

10077

10178
def test_grounddiffuse_simple_float():

0 commit comments

Comments
 (0)