Skip to content

Commit 300d1ab

Browse files
mikofskiwholmgren
authored andcommitted
Linke turbidity factor fixes (pvlib#264)
* fix pvlib#263 and pvlib#262 * set minimum index as 0 not 1 * adjust _linearly_scale so that is increments are 5 minutes or 1/12 of an arcdegree from the centers of neighboring indices * this fixes the out of bounds error for latitudes or longitudes at the limits of the range * move _linearly_scale to be right after lookup_linke_turbidity * add link to pdf on AOD and LT from MeteoTest * raise IndexError if outputmatrix is more than half an index outside of the limits Signed-off-by: Mark Mikofski <[email protected]> * unset debug logging Signed-off-by: Mark Mikofski <[email protected]> * remove logging and use pytest with raises context Signed-off-by: Mark Mikofski <[email protected]> * all test passing now Signed-off-by: Mark Mikofski <[email protected]> * fixes pvlib#265 * use calendar month days instead of approximation of monthly middles * also account for leap years * if all years are leap years, or not any are leap years, use fast interp, otherwise loop and interpolate each timestamp * add _leapyear tests and update expected values for new interpolated Linke turbidity factors Signed-off-by: Mark Mikofski <[email protected]> * update expected values for test_location.test_get_clearsky Signed-off-by: Mark Mikofski <[email protected]> * update test_modelchain.py with new expected values for PR pvlib#264 Signed-off-by: Mark Mikofski <[email protected]> * add (at)requires_scipy and update whatsnew v0.4.2 Signed-off-by: Mark Mikofski <[email protected]>
1 parent 5ddd9b1 commit 300d1ab

File tree

5 files changed

+230
-73
lines changed

5 files changed

+230
-73
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Bug fixes
1313
* PVSystem.pvwatts_ac could not use the eta_inv_ref kwarg and
1414
PVSystem.pvwatts_dc could not use the temp_ref kwarg. Fixed. (:issue:`252`)
1515
* Fixed typo in ModelChain.infer_spectral_model error message. (:issue:`251`)
16+
* Fixed Linke turbdity factor out of bounds error at 90-degree latitude or at
17+
180-degree longitude (:issue:`262`)
18+
* Fixed Linke turbidity factor grid spacing and centers (:issue:`263`)
1619

1720

1821
API Changes
@@ -41,6 +44,14 @@ Enhancements
4144
* Added name attribute to ModelChain and PVSystem. (:issue:`254`)
4245
* Restructured API section of the documentation so that there are
4346
separate pages for each function, class, or method. (:issue:`258`)
47+
* Improved Linke turbidity factor time interpolation with Python `calendar`
48+
month days and leap years (:issue:`265`)
49+
50+
51+
Other
52+
~~~~~
53+
* Typical modeling results could change by ~1%, depending on location, if they
54+
depend on the turbidity table
4455

4556

4657
Code Contributors
@@ -49,3 +60,4 @@ Code Contributors
4960
* Uwe Krien
5061
* Will Holmgren
5162
* Volker Beutner
63+
* Mark Mikofski

pvlib/clearsky.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import os
99
from collections import OrderedDict
10+
import calendar
1011

1112
import numpy as np
1213
import pandas as pd
@@ -186,6 +187,12 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None,
186187
# so divide the number from the file by 20 to get the
187188
# turbidity.
188189

190+
# The nodes of the grid are 5' (1/12=0.0833[arcdeg]) apart.
191+
# From Section 8 of Aerosol optical depth and Linke turbidity climatology
192+
# http://www.meteonorm.com/images/uploads/downloads/ieashc36_report_TL_AOD_climatologies.pdf
193+
# 1st row: 89.9583 S, 2nd row: 89.875 S
194+
# 1st column: 179.9583 W, 2nd column: 179.875 W
195+
189196
try:
190197
import scipy.io
191198
except ImportError:
@@ -201,27 +208,37 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None,
201208
linke_turbidity_table = mat['LinkeTurbidity']
202209

203210
latitude_index = (
204-
np.around(_linearly_scale(latitude, 90, -90, 1, 2160))
211+
np.around(_linearly_scale(latitude, 90, -90, 0, 2160))
205212
.astype(np.int64))
206213
longitude_index = (
207-
np.around(_linearly_scale(longitude, -180, 180, 1, 4320))
214+
np.around(_linearly_scale(longitude, -180, 180, 0, 4320))
208215
.astype(np.int64))
209216

210217
g = linke_turbidity_table[latitude_index][longitude_index]
211218

212219
if interp_turbidity:
213-
# Data covers 1 year.
214-
# Assume that data corresponds to the value at
215-
# the middle of each month.
216-
# This means that we need to add previous Dec and next Jan
217-
# to the array so that the interpolation will work for
220+
# Data covers 1 year. Assume that data corresponds to the value at the
221+
# middle of each month. This means that we need to add previous Dec and
222+
# next Jan to the array so that the interpolation will work for
218223
# Jan 1 - Jan 15 and Dec 16 - Dec 31.
219-
# Then we map the month value to the day of year value.
220-
# This is approximate and could be made more accurate.
221224
g2 = np.concatenate([[g[-1]], g, [g[0]]])
222-
days = np.linspace(-15, 380, num=14)
223-
linke_turbidity = pd.Series(np.interp(time.dayofyear, days, g2),
224-
index=time)
225+
# Then we map the month value to the day of year value.
226+
isleap = [calendar.isleap(t.year) for t in time]
227+
if all(isleap):
228+
days = _calendar_month_middles(2016) # all years are leap
229+
elif not any(isleap):
230+
days = _calendar_month_middles(2015) # none of the years are leap
231+
else:
232+
days = None # some of the years are leap years and some are not
233+
if days is None:
234+
# Loop over different years, might be slow for large timeserires
235+
linke_turbidity = pd.Series([
236+
np.interp(t.dayofyear, _calendar_month_middles(t.year), g2)
237+
for t in time
238+
], index=time)
239+
else:
240+
linke_turbidity = pd.Series(np.interp(time.dayofyear, days, g2),
241+
index=time)
225242
else:
226243
linke_turbidity = pd.DataFrame(time.month, index=time)
227244
# apply monthly data
@@ -232,6 +249,45 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None,
232249
return linke_turbidity
233250

234251

252+
def _calendar_month_middles(year):
253+
"""list of middle day of each month, used by Linke turbidity lookup"""
254+
# remove mdays[0] since January starts at mdays[1]
255+
# make local copy of mdays since we need to change February for leap years
256+
mdays = np.array(calendar.mdays[1:])
257+
ydays = 365
258+
# handle leap years
259+
if calendar.isleap(year):
260+
mdays[1] = mdays[1] + 1
261+
ydays = 366
262+
return np.concatenate([[-calendar.mdays[-1] / 2.0], # Dec last year
263+
np.cumsum(mdays) - np.array(mdays) / 2., # this year
264+
[ydays + calendar.mdays[1] / 2.0]]) # Jan next year
265+
266+
267+
def _linearly_scale(inputmatrix, inputmin, inputmax, outputmin, outputmax):
268+
"""linearly scale input to output, used by Linke turbidity lookup"""
269+
inputrange = inputmax - inputmin
270+
outputrange = outputmax - outputmin
271+
delta = outputrange/inputrange # number of indices per input unit
272+
inputmin = inputmin + 1.0 / delta / 2.0 # shift to center of index
273+
outputmax = outputmax - 1 # shift index to zero indexing
274+
outputmatrix = (inputmatrix - inputmin) * delta + outputmin
275+
err = IndexError('Input, %g, is out of range (%g, %g).' %
276+
(inputmatrix, inputmax - inputrange, inputmax))
277+
# round down if input is within half an index or else raise index error
278+
if outputmatrix > outputmax:
279+
if np.around(outputmatrix - outputmax, 1) <= 0.5:
280+
outputmatrix = outputmax
281+
else:
282+
raise err
283+
elif outputmatrix < outputmin:
284+
if np.around(outputmin - outputmatrix, 1) <= 0.5:
285+
outputmatrix = outputmin
286+
else:
287+
raise err
288+
return outputmatrix
289+
290+
235291
def haurwitz(apparent_zenith):
236292
'''
237293
Determine clear sky GHI from Haurwitz model.
@@ -281,15 +337,6 @@ def haurwitz(apparent_zenith):
281337
return df_out
282338

283339

284-
def _linearly_scale(inputmatrix, inputmin, inputmax, outputmin, outputmax):
285-
""" used by linke turbidity lookup function """
286-
287-
inputrange = inputmax - inputmin
288-
outputrange = outputmax - outputmin
289-
outputmatrix = (inputmatrix-inputmin) * outputrange/inputrange + outputmin
290-
return outputmatrix
291-
292-
293340
def simplified_solis(apparent_elevation, aod700=0.1, precipitable_water=1.,
294341
pressure=101325., dni_extra=1364.):
295342
"""

pvlib/test/test_clearsky.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,23 @@ def test_lookup_linke_turbidity():
160160
freq='12h', tz='America/Phoenix')
161161
# expect same value on 2014-06-24 0000 and 1200, and
162162
# diff value on 2014-06-25
163-
expected = pd.Series(np.array([3.10126582, 3.10126582, 3.11443038]),
164-
index=times)
165-
out = clearsky.lookup_linke_turbidity(times, 32.2, -111)
163+
expected = pd.Series(
164+
np.array([3.11803278689, 3.11803278689, 3.13114754098]), index=times
165+
)
166+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875)
167+
assert_series_equal(expected, out)
168+
169+
170+
@requires_scipy
171+
def test_lookup_linke_turbidity_leapyear():
172+
times = pd.date_range(start='2016-06-24', end='2016-06-25',
173+
freq='12h', tz='America/Phoenix')
174+
# expect same value on 2016-06-24 0000 and 1200, and
175+
# diff value on 2016-06-25
176+
expected = pd.Series(
177+
np.array([3.11803278689, 3.11803278689, 3.13114754098]), index=times
178+
)
179+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875)
166180
assert_series_equal(expected, out)
167181

168182

@@ -172,7 +186,7 @@ def test_lookup_linke_turbidity_nointerp():
172186
freq='12h', tz='America/Phoenix')
173187
# expect same value for all days
174188
expected = pd.Series(np.array([3., 3., 3.]), index=times)
175-
out = clearsky.lookup_linke_turbidity(times, 32.2, -111,
189+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875,
176190
interp_turbidity=False)
177191
assert_series_equal(expected, out)
178192

@@ -181,9 +195,21 @@ def test_lookup_linke_turbidity_nointerp():
181195
def test_lookup_linke_turbidity_months():
182196
times = pd.date_range(start='2014-04-01', end='2014-07-01',
183197
freq='1M', tz='America/Phoenix')
184-
expected = pd.Series(np.array([2.8943038, 2.97316456, 3.18025316]),
185-
index=times)
186-
out = clearsky.lookup_linke_turbidity(times, 32.2, -111)
198+
expected = pd.Series(
199+
np.array([2.89918032787, 2.97540983607, 3.19672131148]), index=times
200+
)
201+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875)
202+
assert_series_equal(expected, out)
203+
204+
205+
@requires_scipy
206+
def test_lookup_linke_turbidity_months_leapyear():
207+
times = pd.date_range(start='2016-04-01', end='2016-07-01',
208+
freq='1M', tz='America/Phoenix')
209+
expected = pd.Series(
210+
np.array([2.89918032787, 2.97540983607, 3.19672131148]), index=times
211+
)
212+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875)
187213
assert_series_equal(expected, out)
188214

189215

@@ -192,13 +218,13 @@ def test_lookup_linke_turbidity_nointerp_months():
192218
times = pd.date_range(start='2014-04-10', end='2014-07-10',
193219
freq='1M', tz='America/Phoenix')
194220
expected = pd.Series(np.array([2.85, 2.95, 3.]), index=times)
195-
out = clearsky.lookup_linke_turbidity(times, 32.2, -111,
221+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875,
196222
interp_turbidity=False)
197223
assert_series_equal(expected, out)
198224
# changing the dates shouldn't matter if interp=False
199225
times = pd.date_range(start='2014-04-05', end='2014-07-05',
200226
freq='1M', tz='America/Phoenix')
201-
out = clearsky.lookup_linke_turbidity(times, 32.2, -111,
227+
out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875,
202228
interp_turbidity=False)
203229
assert_series_equal(expected, out)
204230

@@ -440,3 +466,41 @@ def test_simplified_solis_nans_series():
440466
precipitable_water, pressure, dni_extra)
441467

442468
assert_frame_equal(expected, out)
469+
470+
471+
@requires_scipy
472+
def test_linke_turbidity_corners():
473+
"""Test Linke turbidity corners out of bounds."""
474+
months = pd.DatetimeIndex('%d/1/2016' % (m + 1) for m in range(12))
475+
476+
def monthly_lt_nointerp(lat, lon, time=months):
477+
"""monthly Linke turbidity factor without time interpolation"""
478+
return clearsky.lookup_linke_turbidity(
479+
time, lat, lon, interp_turbidity=False
480+
)
481+
482+
# Northwest
483+
assert np.allclose(
484+
monthly_lt_nointerp(90, -180),
485+
[1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9])
486+
# Southwest
487+
assert np.allclose(
488+
monthly_lt_nointerp(-90, -180),
489+
[1.35, 1.3, 1.45, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.4, 1.4, 1.3])
490+
# Northeast
491+
assert np.allclose(
492+
monthly_lt_nointerp(90, 180),
493+
[1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9])
494+
# Southeast
495+
assert np.allclose(
496+
monthly_lt_nointerp(-90, 180),
497+
[1.35, 1.7, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.7])
498+
# test out of range exceptions at corners
499+
with pytest.raises(IndexError):
500+
monthly_lt_nointerp(91, -122) # exceeds max latitude
501+
with pytest.raises(IndexError):
502+
monthly_lt_nointerp(38.2, 181) # exceeds max longitude
503+
with pytest.raises(IndexError):
504+
monthly_lt_nointerp(-91, -122) # exceeds min latitude
505+
with pytest.raises(IndexError):
506+
monthly_lt_nointerp(38.2, -181) # exceeds min longitude

pvlib/test/test_location.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,26 @@ def test_location_invalid_tz_type():
4242

4343
def test_location_print_all():
4444
tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')
45-
expected_str = 'Location: \n name: Tucson\n latitude: 32.2\n longitude: -111\n altitude: 700\n tz: US/Arizona'
45+
expected_str = '\n'.join([
46+
'Location: ',
47+
' name: Tucson',
48+
' latitude: 32.2',
49+
' longitude: -111',
50+
' altitude: 700',
51+
' tz: US/Arizona'
52+
])
4653
assert tus.__str__() == expected_str
4754

4855
def test_location_print_pytz():
4956
tus = Location(32.2, -111, aztz, 700, 'Tucson')
50-
expected_str = 'Location: \n name: Tucson\n latitude: 32.2\n longitude: -111\n altitude: 700\n tz: US/Arizona'
57+
expected_str = '\n'.join([
58+
'Location: ',
59+
' name: Tucson',
60+
' latitude: 32.2',
61+
' longitude: -111',
62+
' altitude: 700',
63+
' tz: US/Arizona'
64+
])
5165
assert tus.__str__() == expected_str
5266

5367

@@ -58,14 +72,13 @@ def test_get_clearsky():
5872
end='20160101T1800-0700',
5973
freq='3H')
6074
clearsky = tus.get_clearsky(times)
61-
expected = pd.DataFrame(data=np.
62-
array([[ 0. , 0. , 0. ],
63-
[ 258.60422702, 761.57329257, 50.1235982 ],
64-
[ 611.96347869, 956.95353414, 70.8232806 ],
65-
[ 415.10904044, 878.52649603, 59.07820922],
66-
[ 0. , 0. , 0. ]]),
67-
columns=['ghi', 'dni', 'dhi'],
68-
index=times)
75+
expected = pd.DataFrame(data=np.array([
76+
( 0.0, 0.0, 0.0),
77+
(262.77734276159333, 791.1972825869296, 46.18714900637892),
78+
(616.764693938387, 974.9610353623959, 65.44157429054201),
79+
(419.6512657626518, 901.6234995035793, 54.26016437839348),
80+
( 0.0, 0.0, 0.0)],
81+
dtype=[('ghi', '<f8'), ('dni', '<f8'), ('dhi', '<f8')]), index=times)
6982
assert_frame_equal(expected, clearsky, check_less_precise=2)
7083

7184

@@ -284,5 +297,12 @@ def test_get_airmass_valueerror():
284297
def test_Location___repr__():
285298
tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')
286299

287-
expected = 'Location: \n name: Tucson\n latitude: 32.2\n longitude: -111\n altitude: 700\n tz: US/Arizona'
300+
expected = '\n'.join([
301+
'Location: ',
302+
' name: Tucson',
303+
' latitude: 32.2',
304+
' longitude: -111',
305+
' altitude: 700',
306+
' tz: US/Arizona'
307+
])
288308
assert tus.__repr__() == expected

0 commit comments

Comments
 (0)