diff --git a/pvlib/location.py b/pvlib/location.py index 0fb3b42a04..7cef02bec7 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -4,7 +4,7 @@ # Will Holmgren, University of Arizona, 2014-2016. -import os +import pathlib import datetime import warnings @@ -45,8 +45,10 @@ class Location: pytz.timezone objects will be converted to strings. ints and floats must be in hours from UTC. - altitude : float, default 0. + altitude : None or float, default None Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. name : None or string, default None. Sets the name attribute of the Location object. @@ -56,7 +58,8 @@ class Location: pvlib.pvsystem.PVSystem """ - def __init__(self, latitude, longitude, tz='UTC', altitude=0, name=None): + def __init__(self, latitude, longitude, tz='UTC', altitude=None, + name=None): self.latitude = latitude self.longitude = longitude @@ -76,6 +79,9 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, name=None): else: raise TypeError('Invalid tz specification') + if altitude is None: + altitude = lookup_altitude(latitude, longitude) + self.altitude = altitude self.name = name @@ -427,8 +433,8 @@ def lookup_altitude(latitude, longitude): """ - pvlib_path = os.path.dirname(os.path.abspath(__file__)) - filepath = os.path.join(pvlib_path, 'data', 'Altitude.h5') + pvlib_path = pathlib.Path(__file__).parent + filepath = pvlib_path / 'data' / 'Altitude.h5' latitude_index = _degrees_to_index(latitude, coordinate='latitude') longitude_index = _degrees_to_index(longitude, coordinate='longitude') diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index cdcacd7ec6..1aa3f10442 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -24,7 +24,7 @@ import warnings import datetime -from pvlib import atmosphere +from pvlib import atmosphere, location from pvlib.tools import datetime_to_djd, djd_to_datetime @@ -52,12 +52,13 @@ def get_solarposition(time, latitude, longitude, negative to west. altitude : None or float, default None - If None, computed from pressure. Assumed to be 0 m - if pressure is also None. + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. pressure : None or float, default None - If None, computed from altitude. Assumed to be 101325 Pa - if altitude is also None. + Air pressure in Pascals. + If None, computed from altitude. method : string, default 'nrel_numpy' 'nrel_numpy' uses an implementation of the NREL SPA algorithm @@ -92,12 +93,10 @@ def get_solarposition(time, latitude, longitude, .. [3] NREL SPA code: http://rredc.nrel.gov/solar/codesandalgorithms/spa/ """ - if altitude is None and pressure is None: - altitude = 0. - pressure = 101325. - elif altitude is None: - altitude = atmosphere.pres2alt(pressure) - elif pressure is None: + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + + if pressure is None: pressure = atmosphere.alt2pres(altitude) method = method.lower() @@ -129,7 +128,7 @@ def get_solarposition(time, latitude, longitude, return ephem_df -def spa_c(time, latitude, longitude, pressure=101325, altitude=0, +def spa_c(time, latitude, longitude, pressure=None, altitude=None, temperature=12, delta_t=67.0, raw_spa_output=False): """ @@ -153,10 +152,13 @@ def spa_c(time, latitude, longitude, pressure=101325, altitude=0, longitude : float Longitude in decimal degrees. Positive east of prime meridian, negative to west. - pressure : float, default 101325 - Pressure in Pascals - altitude : float, default 0 - Height above sea level. [m] + pressure : None or float, default None + Air pressure in Pascals. + If None, computed from altitude. + altitude : None or float, default None + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. temperature : float, default 12 Temperature in C delta_t : float, default 67.0 @@ -194,6 +196,11 @@ def spa_c(time, latitude, longitude, pressure=101325, altitude=0, pyephem, spa_python, ephemeris """ + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + if pressure is None: + pressure = atmosphere.alt2pres(altitude) + # Added by Rob Andrews (@Calama-Consulting), Calama Consulting, 2014 # Edited by Will Holmgren (@wholmgren), University of Arizona, 2014 # Edited by Tony Lorenzo (@alorenzo175), University of Arizona, 2015 @@ -275,7 +282,7 @@ def _spa_python_import(how): def spa_python(time, latitude, longitude, - altitude=0, pressure=101325, temperature=12, delta_t=67.0, + altitude=None, pressure=None, temperature=12, delta_t=67.0, atmos_refract=None, how='numpy', numthreads=4): """ Calculate the solar position using a python implementation of the @@ -298,10 +305,13 @@ def spa_python(time, latitude, longitude, longitude : float Longitude in decimal degrees. Positive east of prime meridian, negative to west. - altitude : float, default 0 - Distance above sea level. - pressure : int or float, optional, default 101325 + altitude : None or float, default None + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. + pressure : int or float, optional, default None avg. yearly air pressure in Pascals. + If None, computed from altitude. temperature : int or float, optional, default 12 avg. yearly air temperature in degrees C. delta_t : float, optional, default 67.0 @@ -351,6 +361,11 @@ def spa_python(time, latitude, longitude, pyephem, spa_c, ephemeris """ + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + if pressure is None: + pressure = atmosphere.alt2pres(altitude) + # Added by Tony Lorenzo (@alorenzo175), University of Arizona, 2015 lat = latitude @@ -504,8 +519,8 @@ def _ephem_setup(latitude, longitude, altitude, pressure, temperature, def sun_rise_set_transit_ephem(times, latitude, longitude, next_or_previous='next', - altitude=0, - pressure=101325, + altitude=None, + pressure=None, temperature=12, horizon='0:00'): """ Calculate the next sunrise and sunset times using the PyEphem package. @@ -520,10 +535,13 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, Longitude in degrees, positive east of prime meridian, negative to west next_or_previous : str 'next' or 'previous' sunrise and sunset relative to time - altitude : float, default 0 - distance above sea level in meters. - pressure : int or float, optional, default 101325 - air pressure in Pascals. + altitude : None or float, default None + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. + pressure : None or float, default None + Air pressure in Pascals. + If None, computed from altitude. temperature : int or float, optional, default 12 air temperature in degrees C. horizon : string, format +/-X:YY @@ -555,6 +573,11 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, else: raise ValueError('times must be localized') + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + if pressure is None: + pressure = atmosphere.alt2pres(altitude) + obs, sun = _ephem_setup(latitude, longitude, altitude, pressure, temperature, horizon) # create lists of sunrise and sunset time localized to time.tz @@ -588,7 +611,7 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, 'transit': trans}) -def pyephem(time, latitude, longitude, altitude=0, pressure=101325, +def pyephem(time, latitude, longitude, altitude=None, pressure=None, temperature=12, horizon='+0:00'): """ Calculate the solar position using the PyEphem package. @@ -603,10 +626,13 @@ def pyephem(time, latitude, longitude, altitude=0, pressure=101325, longitude : float Longitude in decimal degrees. Positive east of prime meridian, negative to west. - altitude : float, default 0 - Height above sea level in meters. [m] - pressure : int or float, optional, default 101325 - air pressure in Pascals. + altitude : None or float, default None + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. + pressure : None or float, default None + Air pressure in Pascals. + If None, computed from altitude. temperature : int or float, optional, default 12 air temperature in degrees C. horizon : string, optional, default '+0:00' @@ -642,6 +668,11 @@ def pyephem(time, latitude, longitude, altitude=0, pressure=101325, except TypeError: time_utc = time + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + if pressure is None: + pressure = atmosphere.alt2pres(altitude) + sun_coords = pd.DataFrame(index=time) obs, sun = _ephem_setup(latitude, longitude, altitude, @@ -862,7 +893,7 @@ def ephemeris(time, latitude, longitude, pressure=101325, temperature=12): def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, - altitude=0, pressure=101325, temperature=12, horizon='+0:00', + altitude=None, pressure=None, temperature=12, horizon='+0:00', xtol=1.0e-12): """ Calculate the time between lower_bound and upper_bound @@ -885,11 +916,14 @@ def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, and 'az' (which must be given in radians). value : int or float The value of the attribute to solve for - altitude : float, default 0 - Distance above sea level. - pressure : int or float, optional, default 101325 + altitude : None or float, default None + Altitude from sea level in meters. + If None, the altitude will be fetched from + :py:func:`pvlib.location.lookup_altitude`. + pressure : int or float, optional, default None Air pressure in Pascals. Set to 0 for no atmospheric correction. + If None, computed from altitude. temperature : int or float, optional, default 12 Air temperature in degrees C. horizon : string, optional, default '+0:00' @@ -913,6 +947,12 @@ def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, If the given attribute is not an attribute of a PyEphem.Sun object. """ + + if altitude is None: + altitude = location.lookup_altitude(latitude, longitude) + if pressure is None: + pressure = atmosphere.alt2pres(altitude) + obs, sun = _ephem_setup(latitude, longitude, altitude, pressure, temperature, horizon) diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index b3c1576bdf..8d52f65a54 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -12,6 +12,7 @@ from pytz.exceptions import UnknownTimeZoneError import pvlib +from pvlib import location from pvlib.location import Location, lookup_altitude from pvlib.solarposition import declination_spencer71 from pvlib.solarposition import equation_of_time_spencer71 @@ -328,21 +329,28 @@ def test_extra_kwargs(): Location(32.2, -111, arbitrary_kwarg='value') -def test_lookup_altitude(): - max_alt_error = 125 - # location name, latitude, longitude, altitude - test_locations = [ - ('Tucson, USA', 32.2540, -110.9742, 724), - ('Lusaka, Zambia', -15.3875, 28.3228, 1253), - ('Tokio, Japan', 35.6762, 139.6503, 40), - ('Canberra, Australia', -35.2802, 149.1310, 566), - ('Bogota, Colombia', 4.7110, -74.0721, 2555), - ('Dead Sea, West Bank', 31.525849, 35.449214, -415), - ('New Delhi, India', 28.6139, 77.2090, 214), - ('Null Island, Atlantic Ocean', 0, 0, 0), - ] - - for name, lat, lon, expected_alt in test_locations: - alt_found = lookup_altitude(lat, lon) - assert abs(alt_found - expected_alt) < max_alt_error, \ - f'Max error exceded for {name} - e: {expected_alt} f: {alt_found}' +@pytest.mark.parametrize('lat,lon,expected_alt', [ + pytest.param(32.2540, -110.9742, 724, id='Tucson, USA'), + pytest.param(-15.3875, 28.3228, 1253, id='Lusaka, Zambia'), + pytest.param(35.6762, 139.6503, 40, id='Tokyo, Japan'), + pytest.param(-35.2802, 149.1310, 566, id='Canberra, Australia'), + pytest.param(4.7110, -74.0721, 2555, id='Bogota, Colombia'), + pytest.param(31.525849, 35.449214, -415, id='Dead Sea, West Bank'), + pytest.param(28.6139, 77.2090, 214, id='New Delhi, India'), + pytest.param(0, 0, 0, id='Null Island, Atlantic Ocean'), +]) +def test_lookup_altitude(lat, lon, expected_alt): + alt_found = lookup_altitude(lat, lon) + assert alt_found == pytest.approx(expected_alt, abs=125) + + +def test_location_lookup_altitude(mocker): + mocker.spy(location, 'lookup_altitude') + tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + location.lookup_altitude.assert_not_called() + assert tus.altitude == 700 + location.lookup_altitude.reset_mock() + + tus = Location(32.2, -111, 'US/Arizona') + location.lookup_altitude.assert_called_once_with(32.2, -111) + assert tus.altitude == location.lookup_altitude(32.2, -111) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 7fa013d0dc..5c78d95d8a 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1678,7 +1678,7 @@ def test_PVSystem_multiple_array_get_aoi(): def solar_pos(): times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') - location = Location(latitude=32, longitude=-111) + location = Location(latitude=32, longitude=-111, altitude=0) return location.get_solarposition(times) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 7578506561..2d9f1762a4 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -10,7 +10,7 @@ import pytest from pvlib.location import Location -from pvlib import solarposition, spa +from pvlib import solarposition, spa, location, atmosphere from .conftest import requires_ephem, requires_spa_c, requires_numba @@ -769,3 +769,37 @@ def test_spa_python_numba_physical_dst(expected_solpos, golden): temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy', numthreads=1) + + +@pytest.mark.parametrize( + 'method', + [ + solarposition.get_solarposition, + pytest.param(solarposition.spa_c, marks=requires_spa_c), + pytest.param(solarposition.spa_python, marks=requires_numba), + pytest.param(solarposition.pyephem, marks=requires_ephem), + ] +) +def test_solarposition_lookup_altitude(mocker, method): + lat, lon = 32.2, -111 + times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, freq='D') + mocker.spy(location, 'lookup_altitude') + mocker.spy(atmosphere, 'alt2pres') + + method(times, lat, lon, altitude=0, pressure=0) + location.lookup_altitude.assert_not_called() + atmosphere.alt2pres.assert_not_called() + location.lookup_altitude.reset_mock() + atmosphere.alt2pres.reset_mock() + + method(times, lat, lon, altitude=0) + location.lookup_altitude.assert_not_called() + atmosphere.alt2pres.assert_called_once_with(0) + location.lookup_altitude.reset_mock() + atmosphere.alt2pres.reset_mock() + + method(times, lat, lon) + location.lookup_altitude.assert_called_once_with(lat, lon) + atmosphere.alt2pres.assert_called_once_with( + location.lookup_altitude(lat, lon)) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 87452939f5..7987dacf8a 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -450,7 +450,7 @@ def test_calc_axis_tilt(): stoptime = '2017-12-31T23:59:59-0300' lat, lon = -27.597300, -48.549610 times = pd.DatetimeIndex(pd.date_range(starttime, stoptime, freq='H')) - solpos = pvlib.solarposition.get_solarposition(times, lat, lon) + solpos = pvlib.solarposition.get_solarposition(times, lat, lon, altitude=0) # singleaxis tracker w/slope data slope_azimuth, slope_tilt = 77.34, 10.1149 axis_azimuth = 0.0