Skip to content
16 changes: 11 additions & 5 deletions pvlib/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Will Holmgren, University of Arizona, 2014-2016.

import os
import pathlib
import datetime
import warnings

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down
110 changes: 75 additions & 35 deletions pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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)

Expand Down
44 changes: 26 additions & 18 deletions pvlib/tests/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
Loading