From ace0a171fd6fefb284394b911cf4144f8904ef4b Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 01:12:01 -0700 Subject: [PATCH 1/7] add test for hour_angle, vectorize * closes #598 vectorize to make it more efficient * closes #597 add test Signed-off-by: Mark Mikofski --- pvlib/solarposition.py | 7 +++---- pvlib/test/test_solarposition.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index b8b2e20274..372a84b77b 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1327,8 +1327,7 @@ def hour_angle(times, longitude, equation_of_time): equation_of_time_Spencer71 equation_of_time_pvcdrom """ - hours = np.array([(t - t.tz.localize( - dt.datetime(t.year, t.month, t.day) - )).total_seconds() / 3600. for t in times]) - timezone = times.tz.utcoffset(times).total_seconds() / 3600. + tz_info = times.tz + timezone = tz_info.utcoffset(times).total_seconds() / 3600. + hours = (times - times.normalize()).astype(int) / 3600. / 1.e9 return 15. * (hours - 12. - timezone) + longitude + equation_of_time / 4. diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index 6c407a7a9b..37c90dd112 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -694,3 +694,25 @@ def test_analytical_azimuth(): azimuths = solarposition.solar_azimuth_analytical(*test_angles.T, zenith=zeniths) assert not np.isnan(azimuths).any() + + +def test_hour_angle(): + """ + Test conversion from hours to hour angles in degrees given the following + inputs from NREL SPA calculator at Golden, CO + date,times,eot,sunrise,sunset + 1/2/2015,7:21:55,-3.935172,-70.699400,70.512721 + 1/2/2015,16:47:43,-4.117227,-70.699400,70.512721 + 1/2/2015,12:04:45,-4.026295,-70.699400,70.512721 + """ + longitude = -105.1786 # degrees + times = pd.DatetimeIndex([ + '2015-01-02 07:21:55.2132', + '2015-01-02 16:47:42.9828', + '2015-01-02 12:04:44.6340' + ]).tz_localize('Etc/GMT+7') + eot = np.array([-3.935172, -4.117227, -4.026295]) + hours = solarposition.hour_angle(times, longitude, eot) + expected = (-70.682338, 70.72118825000001, 0.000801250) + #expected = (-70.699400,70.512721, 0.0) # can't quite get these, why? + assert np.allclose(hours, expected) From 433175d04a5715a402bb73d880c3b24d4be981b9 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 10:33:11 -0700 Subject: [PATCH 2/7] BUG: use np.int64 works better for older numpy version than python int * also converting times to int before subtracting works better for older pandas versions which were not calculating the timedeltas correctly * remove comment with missing space after hash, add FIXME that explains why the expected values are slightly different than the SPA calculator output --- pvlib/solarposition.py | 3 ++- pvlib/test/test_solarposition.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 372a84b77b..dbcc0006a7 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1329,5 +1329,6 @@ def hour_angle(times, longitude, equation_of_time): """ tz_info = times.tz timezone = tz_info.utcoffset(times).total_seconds() / 3600. - hours = (times - times.normalize()).astype(int) / 3600. / 1.e9 + hours = (times.astype(np.int64) - times.normalize().astype(np.int64)) / ( + 3600. * 1.e9) return 15. * (hours - 12. - timezone) + longitude + equation_of_time / 4. diff --git a/pvlib/test/test_solarposition.py b/pvlib/test/test_solarposition.py index 37c90dd112..efc424e8a1 100644 --- a/pvlib/test/test_solarposition.py +++ b/pvlib/test/test_solarposition.py @@ -714,5 +714,7 @@ def test_hour_angle(): eot = np.array([-3.935172, -4.117227, -4.026295]) hours = solarposition.hour_angle(times, longitude, eot) expected = (-70.682338, 70.72118825000001, 0.000801250) - #expected = (-70.699400,70.512721, 0.0) # can't quite get these, why? + # FIXME: there are differences from expected NREL SPA calculator values + # sunrise: 4 seconds, sunset: 48 seconds, transit: 0.2 seconds + # but the differences may be due to other SPA input parameters assert np.allclose(hours, expected) From 2908875712c49f09990187754eced62b01c594fa Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 10:38:23 -0700 Subject: [PATCH 3/7] fix hanging indent --- pvlib/solarposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index dbcc0006a7..1b3a715846 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1329,6 +1329,6 @@ def hour_angle(times, longitude, equation_of_time): """ tz_info = times.tz timezone = tz_info.utcoffset(times).total_seconds() / 3600. - hours = (times.astype(np.int64) - times.normalize().astype(np.int64)) / ( - 3600. * 1.e9) + hours = 1 / (3600. * 1.e9) * ( + times.astype(np.int64) - times.normalize().astype(np.int64)) return 15. * (hours - 12. - timezone) + longitude + equation_of_time / 4. From 7369f3f0eb9bcfd4cd5d44d829ea75b58fdbeef9 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 13:56:24 -0700 Subject: [PATCH 4/7] BUG: replace utcoffset() with reliable, efficient approach ... * ... suggested by @wholmgren (thx!) * utcoffset() is unpredictable when used with pandas datetime indices * only predictable with Python datetime objects or pandas Timestamps * instead replace tzinfo with None to get naive local times, and calculate difference from tz-aware times to get timezones --- pvlib/solarposition.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 1b3a715846..5545493b31 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1327,8 +1327,17 @@ def hour_angle(times, longitude, equation_of_time): equation_of_time_Spencer71 equation_of_time_pvcdrom """ - tz_info = times.tz - timezone = tz_info.utcoffset(times).total_seconds() / 3600. - hours = 1 / (3600. * 1.e9) * ( - times.astype(np.int64) - times.normalize().astype(np.int64)) - return 15. * (hours - 12. - timezone) + longitude + equation_of_time / 4. + # utcoffset() has unpredictable outcome when used with pandas DatetimeIndex + # change the localize tz-aware times to local, naive without converting tz + # by either replacing tz with None or using tz_localize(None) + try: + naive_times = times.tz_localize(None) + except TypeError: + naive_times = times.copy() + naive_times.tz = None + ns2hr = 1 / (3600. * 1.e9) + timezones = np.array(ns2hr * ( + naive_times.astype(np.int64) - times.astype(np.int64))) + hours = ns2hr * ( + times.astype(np.int64) - times.normalize().astype(np.int64)) + return 15. * (hours - 12. - timezones) + longitude + equation_of_time / 4. From 273106c7b03c5787d6eb2eaff2592d90dba7f269 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 14:11:39 -0700 Subject: [PATCH 5/7] BUG: combine arithmetic to make calculation more efficient * also use asarray wrapper at return to ensure consistency * add comments to explain to future maintainers --- pvlib/solarposition.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 5545493b31..c2c1ede4db 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1333,11 +1333,14 @@ def hour_angle(times, longitude, equation_of_time): try: naive_times = times.tz_localize(None) except TypeError: + # pandas <0.15 doesn't allow localization of tz-aware datetime index naive_times = times.copy() naive_times.tz = None - ns2hr = 1 / (3600. * 1.e9) - timezones = np.array(ns2hr * ( - naive_times.astype(np.int64) - times.astype(np.int64))) - hours = ns2hr * ( - times.astype(np.int64) - times.normalize().astype(np.int64)) - return 15. * (hours - 12. - timezones) + longitude + equation_of_time / 4. + # combine some arithmetic to make calculation more efficient: + # hours - timezone = times - times.normalized - (naive_times - times) + hrs_minus_tzs = 1 / (3600. * 1.e9) * ( + 2 * times.astype(np.int64) - times.normalize().astype(np.int64) + - naive_times.astype(np.int64)) + # ensure array return instead of a version-dependent pandas Index + return np.asarray( + 15. * (hrs_minus_tzs - 12.) + longitude + equation_of_time / 4.) From d89e725a3c20744684d066858a969645c73b42f7 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 14:14:30 -0700 Subject: [PATCH 6/7] stickler --- pvlib/solarposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index c2c1ede4db..480a72d1bb 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1339,8 +1339,8 @@ def hour_angle(times, longitude, equation_of_time): # combine some arithmetic to make calculation more efficient: # hours - timezone = times - times.normalized - (naive_times - times) hrs_minus_tzs = 1 / (3600. * 1.e9) * ( - 2 * times.astype(np.int64) - times.normalize().astype(np.int64) - - naive_times.astype(np.int64)) + 2 * times.astype(np.int64) - times.normalize().astype(np.int64) - + naive_times.astype(np.int64)) # ensure array return instead of a version-dependent pandas Index return np.asarray( 15. * (hrs_minus_tzs - 12.) + longitude + equation_of_time / 4.) From 9622661b13c4260870585c843c93dee5f43d34e1 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 5 Oct 2018 15:09:27 -0700 Subject: [PATCH 7/7] remove try-except, doesn't work anyway, wait for pandas >=0.15.0 --- pvlib/solarposition.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 480a72d1bb..7d3db9d949 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -1327,17 +1327,8 @@ def hour_angle(times, longitude, equation_of_time): equation_of_time_Spencer71 equation_of_time_pvcdrom """ - # utcoffset() has unpredictable outcome when used with pandas DatetimeIndex - # change the localize tz-aware times to local, naive without converting tz - # by either replacing tz with None or using tz_localize(None) - try: - naive_times = times.tz_localize(None) - except TypeError: - # pandas <0.15 doesn't allow localization of tz-aware datetime index - naive_times = times.copy() - naive_times.tz = None - # combine some arithmetic to make calculation more efficient: - # hours - timezone = times - times.normalized - (naive_times - times) + naive_times = times.tz_localize(None) # naive but still localized + # hours - timezone = (times - normalized_times) - (naive_times - times) hrs_minus_tzs = 1 / (3600. * 1.e9) * ( 2 * times.astype(np.int64) - times.normalize().astype(np.int64) - naive_times.astype(np.int64))