-
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 5 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,84 @@ 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): | ||
"""helper that converts hour angles in degrees into hours""" | ||
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): | ||
"""helper converts hours from an array of floats to localized times""" | ||
tz_info = times.tz | ||
try: | ||
hours = hours.values # make a new reference | ||
except AttributeError: | ||
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 understand the point of this try/except. Is hours = # your eqn
# enforce Series
hours = pd.Series(hours, index=times)
# OR enforce array
hours = np.array(hours)
return hours Then we can remove this try except. 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. no apparently depending on the pandas version it changes, sometimes it's a numpy array of floats and sometimes it's a pandas series of 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 it would simplify things to enforce it elsewhere. I am sorry for being slow/stubborn but these functions are hard for me to follow. I assume it will be even harder for future me to understand them when something inevitably breaks. |
||
pass | ||
try: | ||
# OLD pandas, "TypeError: Already tz-aware, use tz_convert to convert." | ||
times.tz = None | ||
except AttributeError: | ||
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 also don't understand this try/except. Your API declaration requires a localized DatetimeIndex, so 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.
But that's not what we want. We just want to make the tz-aware time a local naive time, so we want to effectively replace Again in some older version of pandas, you could just do this by setting EG: 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 understand now, thanks.
Is this a documented behavior of 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 believe it is documented, I've used this before, I don't recall where, but I will look for it. @cwhanse and @wholmgren thanks so much for being patient and providing really great feedback, most of which I believe I have addressed and incorporated.
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. @wholmgren it's in the pandas documentation for
It's and also here:
and there
@cwhanse
|
||
# NEW pandas, "AttributeError: Cannot directly set timezone. Use | ||
# tz_localize() or tz_convert() as appropriate" | ||
times = times.tz_localize(None) | ||
return pd.DatetimeIndex( | ||
times.values.astype('datetime64[D]').astype('datetime64[ns]') + | ||
(hours * 3600. * 1.e9).astype('timedelta64[ns]'), | ||
tz=tz_info) | ||
|
||
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 |
---|---|---|
|
@@ -15,7 +15,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 +498,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
.