-
Notifications
You must be signed in to change notification settings - Fork 1.1k
ENH: geometric calculations for sunrise, sunset, and transit #583
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
Changes from 2 commits
3b0e597
af206f2
e6319fe
59323d3
44aed34
f1e8970
7fa6953
199a5b5
a8661f5
5d4012c
8b257de
c15f19b
a74f944
2ace87e
344f679
bf492cc
887fe29
7c90b9c
3077923
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1210,3 +1210,69 @@ def hour_angle(times, longitude, equation_of_time): | |
)).total_seconds() / 3600. for t in times]) | ||
timezone = times.tz.utcoffset(times).total_seconds() / 3600. | ||
return 15. * (hours - 12. - timezone) + longitude + equation_of_time / 4. | ||
|
||
|
||
def _hours(times, hour_angle, longitude, equation_of_time): | ||
tz_info = times.tz # pytz timezone info | ||
tz = tz_info.utcoffset(times).total_seconds() / 3600. | ||
return (hour_angle - longitude - equation_of_time / 4.) / 15. + 12. + tz | ||
|
||
|
||
def _times(times, hours): | ||
return np.array([(dt.timedelta(hours=h) + t.tz.localize( | ||
dt.datetime(t.year, t.month, t.day) | ||
)) for h, t in zip(hours, times)]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loop using pure python is likely to be painfully slow and defeat the purpose of an analytical approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I can use numpy arrays with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch @wholmgren , I get about a 70x speed improvement (from 43.5 ms to 605us) to by using numpy See this script that shows improvements with numpy over looping. In [1]: import pandas as pd
In [2]: import datetime as dt
In [3]: import numpy as np
In [4]: d = pd.DatetimeIndex(start='2013-01-01 00:00:00', end='2013-12-31 23:59:59', freq='H', tz='Etc/GMT+8')
In [5]: h = np.loadtxt('../sunrise_sunset_transit.txt', dtype=np.dtype([('sunrise', float), ('sunset', float), ('transit', float)]))
In [6]: sr = pd.Float64Index(h['sunrise'])
In [7]: %paste
def _times(times, hours):
return np.array([(dt.timedelta(hours=h) + t.tz.localize(
dt.datetime(t.year, t.month, t.day)
)) for h, t in zip(hours, times)])
## -- End pasted text --
In [8]: %timeit _times(d, sr)
43.5 ms ± 354 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [9]: del _times
In [10]: %paste
def _times(times, hours):
"""helper converts hours from an array of floats to localized times"""
tz_info = times.tz
return pd.DatetimeIndex(
times.tz_localize(None).values.astype('datetime64[D]')
+ (hours.values * 3600. * 1.e9).astype('timedelta64[ns]'), tz=tz_info)
## -- End pasted text --
In [11]: %timeit _times(d, sr)
605 µs ± 5.07 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) |
||
|
||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def sunrise_sunset_transit_analytical(times, latitude, longitude, declination, | ||
equation_of_time): | ||
""" | ||
Analytical calculation of solar sunrise, sunset, and transit. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't care for the term There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to clean up the naming convention in this module. We have prefixes 'spa' and 'nrel' for functions using SPA, we sometimes have 'pyephem' but not always, and use the suffix 'analytical' (I'm Ok with this) for some purely geometric methods. I prefer _, e.g., There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "geometric" might be an alternative to "analytical" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like "geometric" but would we change zenith and azimuth to match? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say yes, to follow the naming pattern, tacitly labeling the collection of spherical trigonometry methods as the 'geometric' model. Deprecate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I like it! Everyone else in agreement? It'll have to wait until after SPI, but I'll try to get back to this asap. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like "geometric". We should use "geometric" for the new function in this PR, but let's take on the deprecation of the existing functions in a separate PR. |
||
|
||
.. warning:: The analytic form neglects the effect of atmospheric | ||
refraction. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably "geometric" instead of analytic. Also guessing it neglects more than refraction, but haven't looked at the sources. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also spherical earth, sun is represented by a point at its center, ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and in the other "analytical"/"geometrical" functions should we give an estimate of the typical magnitude of the errors? Something like "The error depends on location and time of year but is of order 10 minutes." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from the old test the max differences are
this is a rough estimate, only to 2 decimals, I manually iterated the 2nd decimal to see what tolerance I could get. Also the errors are one sided, ie: for sunrise the geometric estimate is always earlier up to 0.11 hours, and down to some positive, non-zero number, so there's always some sunrise error that is a minute or so earlier than the apparent sunrise, and ie: for sunset it's always later up to 0.11 hours, and down to a positive, non-zero number, so there's always some sunset error that is a minute or so later than the apparent sunset. |
||
|
||
Parameters | ||
---------- | ||
times : pandas.DatetimeIndex | ||
Corresponding timestamps, must be timezone aware. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The description of |
||
latitude : float | ||
Latitude in degrees | ||
longitude : float | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Longitude in degrees | ||
declination : numeric | ||
declination angle in radians at ``times`` | ||
equation_of_time : numeric | ||
difference in time between solar time and mean solar time in minutes | ||
|
||
Returns | ||
------- | ||
sunrise : datetime | ||
localized sunrise time | ||
sunset : datetime | ||
localized sunset time | ||
transit : datetime | ||
localized sun transit time | ||
|
||
References | ||
---------- | ||
[2] J. A. Duffie and W. A. Beckman, "Solar Engineering of Thermal | ||
Processes, 3rd Edition" pp. 9-11, J. Wiley and Sons, New York (2006) | ||
|
||
[3] Frank Vignola et al., "Solar And Infrared Radiation Measurements", | ||
p. 13, CRC Press (2012) | ||
|
||
""" | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
latitude_rad = np.radians(latitude) # radians | ||
sunset_angle_rad = np.arccos(-np.tan(declination) * np.tan(latitude_rad)) | ||
sunset_angle = np.degrees(sunset_angle_rad) # degrees | ||
# solar noon is at hour angle zero | ||
# so sunrise is just negative of sunset | ||
sunrise_angle = -sunset_angle | ||
sunrise_hour = _hours(times, sunrise_angle, longitude, equation_of_time) | ||
sunset_hour = _hours(times, sunset_angle, longitude, equation_of_time) | ||
transit_hour = _hours(times, 0, longitude, equation_of_time) | ||
sunrise = _times(times, sunrise_hour) | ||
sunset = _times(times, sunset_hour) | ||
transit = _times(times, transit_hour) | ||
return sunrise, sunset, transit |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import calendar | ||
import datetime | ||
import os | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. F401 'os' imported but unused |
||
|
||
import numpy as np | ||
import pandas as pd | ||
|
@@ -15,7 +16,6 @@ | |
from conftest import (requires_ephem, needs_pandas_0_17, | ||
requires_spa_c, requires_numba) | ||
|
||
|
||
# setup times and locations to be tested. | ||
times = pd.date_range(start=datetime.datetime(2014,6,24), | ||
end=datetime.datetime(2014,6,26), freq='15Min') | ||
|
@@ -499,3 +499,60 @@ def test_analytical_azimuth(): | |
azimuths = solarposition.solar_azimuth_analytical(*test_angles.T, zenith=zeniths) | ||
|
||
assert not np.isnan(azimuths).any() | ||
|
||
|
||
EXPECTED_ANALYTICAL_SUNRISE_SUNSET_TRANSIT = [ | ||
(7.455277777777778, 16.691666666666666, 12.073611111111111), | ||
(7.3975, 16.98138888888889, 12.189444444444444), | ||
(7.161388888888889, 17.338333333333335, 12.25), | ||
(6.79, 17.69472222222222, 12.2425), | ||
(6.307777777777778, 18.0375, 12.1725), | ||
(5.821666666666666, 18.336111111111112, 12.078888888888889), | ||
(5.3613888888888885, 18.629722222222224, 11.995555555555555), | ||
(4.978888888888889, 18.925555555555555, 11.952222222222222), | ||
(4.709722222222222, 19.213333333333335, 11.961666666666666), | ||
(4.617222222222222, 19.405833333333334, 12.011388888888888), | ||
(4.686944444444444, 19.458333333333332, 12.072777777777778), | ||
(4.8825, 19.340555555555557, 12.111666666666666), | ||
(5.158333333333333, 19.043333333333333, 12.100833333333334), | ||
(5.433333333333334, 18.6375, 12.035277777777777), | ||
(5.704166666666667, 18.160833333333333, 11.9325), | ||
(5.9847222222222225, 17.666666666666668, 11.825555555555555), | ||
(6.315277777777778, 17.185, 11.75), | ||
(6.666944444444445, 16.82, 11.743611111111111), | ||
(7.0225, 16.593055555555555, 11.807777777777778), | ||
(7.311111111111111, 16.54277777777778, 11.926944444444445)] | ||
|
||
|
||
def test_analytical_sunrise_sunset_transit(): | ||
"""Test analytical calculations for sunrise, sunset, and transit times""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
times = pd.DatetimeIndex(start='2018-01-01 0:00:00', | ||
end='2018-12-31 23:59:59', | ||
freq='H').tz_localize('Etc/GMT+7') | ||
lat, lon = 39.743, -105.178 # degrees | ||
eot = solarposition.equation_of_time_pvcdrom(times.dayofyear) # minutes | ||
decl = solarposition.declination_spencer71(times.dayofyear) # radians | ||
sr, ss, st = solarposition.sunrise_sunset_transit_analytical( | ||
times, latitude=lat, longitude=lon, declination=decl, | ||
equation_of_time=eot) | ||
sst = list( | ||
zip(*[(sr[idx], ss[idx], st[idx]) for idx in range(0, 8760, 438)])) | ||
sunrise = (sr.time() for sr in sst[0]) | ||
sunset = (ss.time() for ss in sst[1]) | ||
transit = (tr.time() for tr in sst[2]) | ||
sunrise_hours = [sr.hour + (sr.minute + sr.second / 60.) / 60. | ||
for sr in sunrise] | ||
sunset_hours = [ss.hour + (ss.minute + ss.second / 60.) / 60. | ||
for ss in sunset] | ||
transit_hours = [tr.hour + (tr.minute + tr.second / 60.) / 60. | ||
for tr in transit] | ||
test_data_file = EXPECTED_ANALYTICAL_SUNRISE_SUNSET_TRANSIT | ||
test_data_type = np.dtype( | ||
[('sunrise', float), ('sunset', float), ('transit', float)]) | ||
test_data = np.array(test_data_file, dtype=test_data_type) | ||
# test data created using NREL SPA-C algorithm at following conditions: | ||
# year=2018, time_zone=-7, longitude=-105.178, latitude=39.743, | ||
# elevation=1830.14, pressure=820, temperature=11, delta_t=67 | ||
assert np.allclose(sunrise_hours, test_data['sunrise']) | ||
assert np.allclose(sunset_hours, test_data['sunset']) | ||
assert np.allclose(transit_hours, test_data['transit']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like a more descriptive function name or a one line documentation for
_hours
and_times
.