Skip to content

Commit 0c245a9

Browse files
cwhansewholmgren
authored andcommitted
sunrise and sunset from pyephem (#588)
* Initial commit for ephem_next_rise_set * Stickler issues * Change test result and rounding to nearest minute * Edit test condition, round not floor * Change explicit rounding to pandas .round('min') * More test fixes * Fix sec keyword * Add test for transit * Fix del statement * Fix column order * Change reference solution to NREL SPA website * Adjust pressure and temp for pyephem * Add horizon kwarg for pyephem * Separate rise, set test for spa and pyephem * Add horizon kwarg to pyephem, fix test condition for spa * Add horizon kwarg to calc_time * Extend tests for next_rise_set_ephem * Remove dtype from expected...spa * Add previous rise/set capability * style fixes * Fix expected result * Fix timezone test * Documentation updates * Fix spacing * Add transit, change golden to fixture, add horizon kwarg and error tests * style fixes * Fix references to fixture golden_mst, golden * Add transit output to rise_set_transit_ephem * Fix transit conflict * Fix column order * Minor test adjustments * Add handling of utcoffset * Inline comment
1 parent bc80cbf commit 0c245a9

File tree

5 files changed

+381
-63
lines changed

5 files changed

+381
-63
lines changed

docs/sphinx/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Additional functions for quantities closely related to solar position.
5555
solarposition.calc_time
5656
solarposition.pyephem_earthsun_distance
5757
solarposition.nrel_earthsun_distance
58+
solarposition.rise_set_transit_ephem
5859
spa.calculate_deltat
5960

6061
The spa module contains the implementation of the built-in NREL SPA

docs/sphinx/source/whatsnew/v0.6.1.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ API Changes
1616
* Deprecated ``tmy``, ``tmy.readtmy2`` and ``tmy.readtmy3``;
1717
they will be removed in v0.7. Use the new :py:func:`pvlib.iotools.read_tmy2`
1818
and :py:func:`pvlib.iotools.read_tmy3` instead. (:issue:`261`)
19+
* Added kwarg `horizon` to :func:`~pvlib.solarposition.pyephem` and :func:`~pvlib.solarposition.calc_time` with default value `'+0:00'`
1920

2021

2122
Enhancements
2223
~~~~~~~~~~~~
24+
* :func:`~pvlib.solarposition.rise_set_transit_ephem` returns sunrise, sunset and transit times using pyephem
2325
* Created :py:func:`pvlib.iotools.read_srml` and :py:func:`pvlib.iotools.read_srml_month_from_solardat`
2426
to read University of Oregon Solar Radiation Monitoring Laboratory data. (:issue:`589`)
27+
2528

2629
Bug fixes
2730
~~~~~~~~~
@@ -35,4 +38,5 @@ Testing
3538
Contributors
3639
~~~~~~~~~~~~
3740
* Will Holmgren (:ghuser:`wholmgren`)
41+
* Cliff Hansen (:ghuser:`cwhanse`)
3842
* Leland Boeman (:ghuser:`lboeman`)

pvlib/solarposition.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Rob Andrews (@Calama-Consulting), Calama Consulting, 2014
77
# Will Holmgren (@wholmgren), University of Arizona, 2014
88
# Tony Lorenzo (@alorenzo175), University of Arizona, 2015
9+
# Cliff hansen (@cwhanse), Sandia National Laboratories, 2018
910

1011
from __future__ import division
1112
import os
@@ -438,7 +439,26 @@ def get_sun_rise_set_transit(time, latitude, longitude, how='numpy',
438439
return result
439440

440441

441-
def _ephem_setup(latitude, longitude, altitude, pressure, temperature):
442+
def _ephem_convert_to_seconds_and_microseconds(date):
443+
# utility from unreleased PyEphem 3.6.7.1
444+
"""Converts a PyEphem date into seconds"""
445+
microseconds = int(round(24 * 60 * 60 * 1000000 * date))
446+
seconds, microseconds = divmod(microseconds, 1000000)
447+
seconds -= 2209032000 # difference between epoch 1900 and epoch 1970
448+
return seconds, microseconds
449+
450+
451+
def _ephem_to_timezone(date, tzinfo):
452+
# utility from unreleased PyEphem 3.6.7.1
453+
""""Convert a PyEphem Date into a timezone aware python datetime"""
454+
seconds, microseconds = _ephem_convert_to_seconds_and_microseconds(date)
455+
date = dt.datetime.fromtimestamp(seconds, tzinfo)
456+
date = date.replace(microsecond=microseconds)
457+
return date
458+
459+
460+
def _ephem_setup(latitude, longitude, altitude, pressure, temperature,
461+
horizon):
442462
import ephem
443463
# initialize a PyEphem observer
444464
obs = ephem.Observer()
@@ -447,14 +467,97 @@ def _ephem_setup(latitude, longitude, altitude, pressure, temperature):
447467
obs.elevation = altitude
448468
obs.pressure = pressure / 100. # convert to mBar
449469
obs.temp = temperature
470+
obs.horizon = horizon
450471

451472
# the PyEphem sun
452473
sun = ephem.Sun()
453474
return obs, sun
454475

455476

477+
def rise_set_transit_ephem(time, latitude, longitude,
478+
next_or_previous='next',
479+
altitude=0,
480+
pressure=101325, temperature=12, horizon='0:00'):
481+
"""
482+
Calculate the next sunrise and sunset times using the PyEphem package.
483+
484+
Parameters
485+
----------
486+
time : pandas.DatetimeIndex
487+
Must be localized
488+
latitude : float
489+
positive is north of 0
490+
longitude : float
491+
positive is east of 0
492+
next_or_previous : str
493+
'next' or 'previous' sunrise and sunset relative to time
494+
altitude : float, default 0
495+
distance above sea level in meters.
496+
pressure : int or float, optional, default 101325
497+
air pressure in Pascals.
498+
temperature : int or float, optional, default 12
499+
air temperature in degrees C.
500+
horizon : string, format +/-X:YY
501+
arc degrees:arc minutes from geometrical horizon for sunrise and
502+
sunset, e.g., horizon='+0:00' to use sun center crossing the
503+
geometrical horizon to define sunrise and sunset,
504+
horizon='-0:34' for when the sun's upper edge crosses the
505+
geometrical horizon
506+
507+
Returns
508+
-------
509+
pandas.DataFrame
510+
index is the same as input `time` argument
511+
columns are 'sunrise', 'sunset', and 'transit'
512+
513+
See also
514+
--------
515+
pyephem
516+
"""
517+
518+
try:
519+
import ephem
520+
except ImportError:
521+
raise ImportError('PyEphem must be installed')
522+
523+
# times must be localized
524+
if not time.tz:
525+
raise ValueError('rise_set_ephem: times must be localized')
526+
527+
obs, sun = _ephem_setup(latitude, longitude, altitude,
528+
pressure, temperature, horizon)
529+
# create lists of sunrise and sunset time localized to time.tz
530+
if next_or_previous.lower() == 'next':
531+
rising = obs.next_rising
532+
setting = obs.next_setting
533+
transit = obs.next_transit
534+
elif next_or_previous.lower() == 'previous':
535+
rising = obs.previous_rising
536+
setting = obs.previous_setting
537+
transit = obs.previous_transit
538+
else:
539+
raise ValueError("next_or_previous must be either 'next' or" +
540+
" 'previous'")
541+
542+
sunrise = []
543+
sunset = []
544+
trans = []
545+
for thetime in time:
546+
thetime = thetime.to_pydatetime()
547+
# pyephem drops timezone when converting to its internal datetime
548+
# format, so handle timezone explicitly here
549+
obs.date = ephem.Date(thetime - thetime.utcoffset())
550+
sunrise.append(_ephem_to_timezone(rising(sun), time.tz))
551+
sunset.append(_ephem_to_timezone(setting(sun), time.tz))
552+
trans.append(_ephem_to_timezone(transit(sun), time.tz))
553+
554+
return pd.DataFrame(index=time, data={'sunrise': sunrise,
555+
'sunset': sunset,
556+
'transit': trans})
557+
558+
456559
def pyephem(time, latitude, longitude, altitude=0, pressure=101325,
457-
temperature=12):
560+
temperature=12, horizon='+0:00'):
458561
"""
459562
Calculate the solar position using the PyEphem package.
460563
@@ -463,17 +566,26 @@ def pyephem(time, latitude, longitude, altitude=0, pressure=101325,
463566
time : pandas.DatetimeIndex
464567
Localized or UTC.
465568
latitude : float
569+
positive is north of 0
466570
longitude : float
571+
positive is east of 0
467572
altitude : float, default 0
468-
distance above sea level.
573+
distance above sea level in meters.
469574
pressure : int or float, optional, default 101325
470575
air pressure in Pascals.
471576
temperature : int or float, optional, default 12
472577
air temperature in degrees C.
578+
horizon : string, optional, default '+0:00'
579+
arc degrees:arc minutes from geometrical horizon for sunrise and
580+
sunset, e.g., horizon='+0:00' to use sun center crossing the
581+
geometrical horizon to define sunrise and sunset,
582+
horizon='-0:34' for when the sun's upper edge crosses the
583+
geometrical horizon
473584
474585
Returns
475586
-------
476-
DataFrame
587+
pandas.DataFrame
588+
index is the same as input `time` argument
477589
The DataFrame will have the following columns:
478590
apparent_elevation, elevation,
479591
apparent_azimuth, azimuth,
@@ -499,7 +611,7 @@ def pyephem(time, latitude, longitude, altitude=0, pressure=101325,
499611
sun_coords = pd.DataFrame(index=time)
500612

501613
obs, sun = _ephem_setup(latitude, longitude, altitude,
502-
pressure, temperature)
614+
pressure, temperature, horizon)
503615

504616
# make and fill lists of the sun's altitude and azimuth
505617
# this is the pressure and temperature corrected apparent alt/az.
@@ -711,7 +823,8 @@ def ephemeris(time, latitude, longitude, pressure=101325, temperature=12):
711823

712824

713825
def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value,
714-
altitude=0, pressure=101325, temperature=12, xtol=1.0e-12):
826+
altitude=0, pressure=101325, temperature=12, horizon='+0:00',
827+
xtol=1.0e-12):
715828
"""
716829
Calculate the time between lower_bound and upper_bound
717830
where the attribute is equal to value. Uses PyEphem for
@@ -736,6 +849,12 @@ def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value,
736849
atmospheric correction.
737850
temperature : int or float, optional, default 12
738851
Air temperature in degrees C.
852+
horizon : string, optional, default '+0:00'
853+
arc degrees:arc minutes from geometrical horizon for sunrise and
854+
sunset, e.g., horizon='+0:00' to use sun center crossing the
855+
geometrical horizon to define sunrise and sunset,
856+
horizon='-0:34' for when the sun's upper edge crosses the
857+
geometrical horizon
739858
xtol : float, optional, default 1.0e-12
740859
The allowed error in the result from value
741860
@@ -758,7 +877,7 @@ def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value,
758877
raise ImportError('The calc_time function requires scipy')
759878

760879
obs, sun = _ephem_setup(latitude, longitude, altitude,
761-
pressure, temperature)
880+
pressure, temperature, horizon)
762881

763882
def compute_attr(thetime, target, attr):
764883
obs.date = thetime

pvlib/test/test_location.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import pvlib
1919
from pvlib.location import Location
2020

21-
from test_solarposition import expected_solpos
21+
from test_solarposition import expected_solpos, golden_mst
2222

2323
from conftest import requires_scipy
2424

@@ -252,8 +252,7 @@ def test_from_tmy_2():
252252
assert_frame_equal(loc.tmy_data, data)
253253

254254

255-
def test_get_solarposition(expected_solpos):
256-
from test_solarposition import golden_mst
255+
def test_get_solarposition(expected_solpos, golden_mst):
257256
times = pd.date_range(datetime.datetime(2003,10,17,12,30,30),
258257
periods=1, freq='D', tz=golden_mst.tz)
259258
ephem_data = golden_mst.get_solarposition(times, temperature=11)

0 commit comments

Comments
 (0)