Skip to content

Commit ac2cb4b

Browse files
nicomtcwhanse
andauthored
Lookup altitude (#1518)
* Lookup altitude implementation * Adding test for lookup altitude * Updating reference for lookup altitude * Adding whatsnew documentation * Adding myself as a contribuitor * slight documentation improvemnts * Switching back to original _degrees_to_index. There is a bug in the new implementation when numbers hit the boundaries * Fix style issue Removing extra empty line at the end of the file * Updating altitude lookup map Using new map so is reproducible with script https://gist.github.com/nicomt/d2c5f08a8ee3500550be42d8dbee6c1d * Update pvlib/location.py Small documentation change Co-authored-by: Cliff Hansen <[email protected]> * Moving _degrees_to_index test to test_tools * Update lookup_location * Retuning float altitude instead of int * Removing h5 file input * Moving whatsnew to next release Co-authored-by: Cliff Hansen <[email protected]>
1 parent 6a94e35 commit ac2cb4b

File tree

10 files changed

+192
-71
lines changed

10 files changed

+192
-71
lines changed

docs/sphinx/source/reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ API reference
2020
modelchain
2121
bifacial
2222
scaling
23+
location
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. currentmodule:: pvlib
2+
3+
Location
4+
========
5+
6+
Methods for information about locations.
7+
8+
.. autosummary::
9+
:toctree: generated/
10+
11+
location.lookup_altitude

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ Deprecations
99

1010
Enhancements
1111
~~~~~~~~~~~~
12-
12+
* Low resolution altitude lookup map
13+
:py:func:`~pvlib.location.lookup_altitude`
14+
(:issue:`1516`, :pull:`1518`)
1315

1416
Bug fixes
1517
~~~~~~~~~
@@ -33,4 +35,5 @@ Requirements
3335

3436
Contributors
3537
~~~~~~~~~~~~
36-
João Guilherme (:ghuser:`joaoguilhermeS`)
38+
* João Guilherme (:ghuser:`joaoguilhermeS`)
39+
* Nicolas Martinez (:ghuser:`nicomt`)

pvlib/clearsky.py

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import h5py
1515

1616
from pvlib import atmosphere, tools
17+
from pvlib.tools import _degrees_to_index
1718

1819

1920
def ineichen(apparent_zenith, airmass_absolute, linke_turbidity,
@@ -286,67 +287,6 @@ def _calendar_month_middles(year):
286287
return middles
287288

288289

289-
def _degrees_to_index(degrees, coordinate):
290-
"""Transform input degrees to an output index integer. The Linke
291-
turbidity lookup tables have three dimensions, latitude, longitude, and
292-
month. Specify a degree value and either 'latitude' or 'longitude' to get
293-
the appropriate index number for the first two of these index numbers.
294-
295-
Parameters
296-
----------
297-
degrees : float or int
298-
Degrees of either latitude or longitude.
299-
coordinate : string
300-
Specify whether degrees arg is latitude or longitude. Must be set to
301-
either 'latitude' or 'longitude' or an error will be raised.
302-
303-
Returns
304-
-------
305-
index : np.int16
306-
The latitude or longitude index number to use when looking up values
307-
in the Linke turbidity lookup table.
308-
"""
309-
# Assign inputmin, inputmax, and outputmax based on degree type.
310-
if coordinate == 'latitude':
311-
inputmin = 90
312-
inputmax = -90
313-
outputmax = 2160
314-
elif coordinate == 'longitude':
315-
inputmin = -180
316-
inputmax = 180
317-
outputmax = 4320
318-
else:
319-
raise IndexError("coordinate must be 'latitude' or 'longitude'.")
320-
321-
inputrange = inputmax - inputmin
322-
scale = outputmax/inputrange # number of indices per degree
323-
center = inputmin + 1 / scale / 2 # shift to center of index
324-
outputmax -= 1 # shift index to zero indexing
325-
index = (degrees - center) * scale
326-
err = IndexError('Input, %g, is out of range (%g, %g).' %
327-
(degrees, inputmin, inputmax))
328-
329-
# If the index is still out of bounds after rounding, raise an error.
330-
# 0.500001 is used in comparisons instead of 0.5 to allow for a small
331-
# margin of error which can occur when dealing with floating point numbers.
332-
if index > outputmax:
333-
if index - outputmax <= 0.500001:
334-
index = outputmax
335-
else:
336-
raise err
337-
elif index < 0:
338-
if -index <= 0.500001:
339-
index = 0
340-
else:
341-
raise err
342-
# If the index wasn't set to outputmax or 0, round it and cast it as an
343-
# integer so it can be used in integer-based indexing.
344-
else:
345-
index = int(np.around(index))
346-
347-
return index
348-
349-
350290
def haurwitz(apparent_zenith):
351291
'''
352292
Determine clear sky GHI using the Haurwitz model.

pvlib/data/Altitude.h5

1.3 MB
Binary file not shown.

pvlib/location.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

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

7+
import os
78
import datetime
89
import warnings
910

1011
import pandas as pd
1112
import pytz
13+
import h5py
1214

1315
from pvlib import solarposition, clearsky, atmosphere, irradiance
16+
from pvlib.tools import _degrees_to_index
1417

1518
class Location:
1619
"""
@@ -356,3 +359,88 @@ def get_sun_rise_set_transit(self, times, method='pyephem', **kwargs):
356359
'one of pyephem, spa, geometric'
357360
.format(method))
358361
return result
362+
363+
364+
def lookup_altitude(latitude, longitude):
365+
"""
366+
Look up location altitude from low-resolution altitude map
367+
supplied with pvlib. The data for this map comes from multiple open data
368+
sources with varying resolutions aggregated by Mapzen.
369+
370+
More details can be found here
371+
https://github.com/tilezen/joerd/blob/master/docs/data-sources.md
372+
373+
Altitudes from this map are a coarse approximation and can have
374+
significant errors (100+ meters) introduced by downsampling and
375+
source data resolution.
376+
377+
Parameters
378+
----------
379+
latitude : float.
380+
Positive is north of the equator.
381+
Use decimal degrees notation.
382+
383+
longitude : float.
384+
Positive is east of the prime meridian.
385+
Use decimal degrees notation.
386+
387+
Returns
388+
-------
389+
altitude : float
390+
The altitude of the location in meters.
391+
392+
Notes
393+
-----------
394+
Attributions:
395+
396+
* ArcticDEM terrain data DEM(s) were created from DigitalGlobe, Inc.,
397+
imagery and funded under National Science Foundation awards 1043681,
398+
1559691, and 1542736;
399+
* Australia terrain data © Commonwealth of Australia
400+
(Geoscience Australia) 2017;
401+
* Austria terrain data © offene Daten Österreichs - Digitales
402+
Geländemodell (DGM) Österreich;
403+
* Canada terrain data contains information licensed under the Open
404+
Government Licence - Canada;
405+
* Europe terrain data produced using Copernicus data and information
406+
funded by the European Union - EU-DEM layers;
407+
* Global ETOPO1 terrain data U.S. National Oceanic and Atmospheric
408+
Administration
409+
* Mexico terrain data source: INEGI, Continental relief, 2016;
410+
* New Zealand terrain data Copyright 2011 Crown copyright (c) Land
411+
Information New Zealand and the New Zealand Government
412+
(All rights reserved);
413+
* Norway terrain data © Kartverket;
414+
* United Kingdom terrain data © Environment Agency copyright and/or
415+
database right 2015. All rights reserved;
416+
* United States 3DEP (formerly NED) and global GMTED2010 and SRTM
417+
terrain data courtesy of the U.S. Geological Survey.
418+
419+
References
420+
----------
421+
.. [1] `Mapzen, Linux foundation project for open data maps
422+
<https://www.mapzen.com/>`_
423+
.. [2] `Joerd, tool for downloading and processing DEMs, Used by Mapzen
424+
<https://github.com/tilezen/joerd/>`_
425+
.. [3] `AWS, Open Data Registry Terrain Tiles
426+
<https://registry.opendata.aws/terrain-tiles/>`_
427+
428+
"""
429+
430+
pvlib_path = os.path.dirname(os.path.abspath(__file__))
431+
filepath = os.path.join(pvlib_path, 'data', 'Altitude.h5')
432+
433+
latitude_index = _degrees_to_index(latitude, coordinate='latitude')
434+
longitude_index = _degrees_to_index(longitude, coordinate='longitude')
435+
436+
with h5py.File(filepath, 'r') as alt_h5_file:
437+
alt = alt_h5_file['Altitude'][latitude_index, longitude_index]
438+
439+
# 255 is a special value that means nodata. Fallback to 0 if nodata.
440+
if alt == 255:
441+
return 0
442+
# Altitude is encoded in 28 meter steps from -450 meters to 6561 meters
443+
# There are 0-254 possible altitudes, with 255 reserved for nodata.
444+
alt *= 28
445+
alt -= 450
446+
return float(alt)

pvlib/tests/test_clearsky.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -511,13 +511,6 @@ def monthly_lt_nointerp(lat, lon, time=months):
511511
monthly_lt_nointerp(38.2, -181) # exceeds min longitude
512512

513513

514-
def test_degrees_to_index_1():
515-
"""Test that _degrees_to_index raises an error when something other than
516-
'latitude' or 'longitude' is passed."""
517-
with pytest.raises(IndexError): # invalid value for coordinate argument
518-
clearsky._degrees_to_index(degrees=22.0, coordinate='width')
519-
520-
521514
@pytest.fixture
522515
def detect_clearsky_data():
523516
data_file = DATA_DIR / 'detect_clearsky_data.csv'

pvlib/tests/test_location.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pytz.exceptions import UnknownTimeZoneError
1313

1414
import pvlib
15-
from pvlib.location import Location
15+
from pvlib.location import Location, lookup_altitude
1616
from pvlib.solarposition import declination_spencer71
1717
from pvlib.solarposition import equation_of_time_spencer71
1818
from .conftest import requires_ephem
@@ -326,3 +326,23 @@ def test_get_sun_rise_set_transit_valueerror(golden):
326326
def test_extra_kwargs():
327327
with pytest.raises(TypeError, match='arbitrary_kwarg'):
328328
Location(32.2, -111, arbitrary_kwarg='value')
329+
330+
331+
def test_lookup_altitude():
332+
max_alt_error = 125
333+
# location name, latitude, longitude, altitude
334+
test_locations = [
335+
('Tucson, USA', 32.2540, -110.9742, 724),
336+
('Lusaka, Zambia', -15.3875, 28.3228, 1253),
337+
('Tokio, Japan', 35.6762, 139.6503, 40),
338+
('Canberra, Australia', -35.2802, 149.1310, 566),
339+
('Bogota, Colombia', 4.7110, -74.0721, 2555),
340+
('Dead Sea, West Bank', 31.525849, 35.449214, -415),
341+
('New Delhi, India', 28.6139, 77.2090, 214),
342+
('Null Island, Atlantic Ocean', 0, 0, 0),
343+
]
344+
345+
for name, lat, lon, expected_alt in test_locations:
346+
alt_found = lookup_altitude(lat, lon)
347+
assert abs(alt_found - expected_alt) < max_alt_error, \
348+
f'Max error exceded for {name} - e: {expected_alt} f: {alt_found}'

pvlib/tests/test_tools.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,10 @@ def test__golden_sect_DataFrame_nans():
7272
v, x = tools._golden_sect_DataFrame(params, lower, upper,
7373
_obj_test_golden_sect)
7474
assert np.allclose(x, expected, atol=1e-8, equal_nan=True)
75+
76+
77+
def test_degrees_to_index_1():
78+
"""Test that _degrees_to_index raises an error when something other than
79+
'latitude' or 'longitude' is passed."""
80+
with pytest.raises(IndexError): # invalid value for coordinate argument
81+
tools._degrees_to_index(degrees=22.0, coordinate='width')

pvlib/tools.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,61 @@ def _get_sample_intervals(times, win_length):
412412
'periods, leap days, etc.'
413413
)
414414
raise NotImplementedError(message)
415+
416+
417+
def _degrees_to_index(degrees, coordinate):
418+
"""Transform input degrees to an output index integer.
419+
Specify a degree value and either 'latitude' or 'longitude' to get
420+
the appropriate index number for these two index numbers.
421+
Parameters
422+
----------
423+
degrees : float or int
424+
Degrees of either latitude or longitude.
425+
coordinate : string
426+
Specify whether degrees arg is latitude or longitude. Must be set to
427+
either 'latitude' or 'longitude' or an error will be raised.
428+
Returns
429+
-------
430+
index : np.int16
431+
The latitude or longitude index number to use when looking up values
432+
in the Linke turbidity lookup table.
433+
"""
434+
# Assign inputmin, inputmax, and outputmax based on degree type.
435+
if coordinate == 'latitude':
436+
inputmin = 90
437+
inputmax = -90
438+
outputmax = 2160
439+
elif coordinate == 'longitude':
440+
inputmin = -180
441+
inputmax = 180
442+
outputmax = 4320
443+
else:
444+
raise IndexError("coordinate must be 'latitude' or 'longitude'.")
445+
446+
inputrange = inputmax - inputmin
447+
scale = outputmax/inputrange # number of indices per degree
448+
center = inputmin + 1 / scale / 2 # shift to center of index
449+
outputmax -= 1 # shift index to zero indexing
450+
index = (degrees - center) * scale
451+
err = IndexError('Input, %g, is out of range (%g, %g).' %
452+
(degrees, inputmin, inputmax))
453+
454+
# If the index is still out of bounds after rounding, raise an error.
455+
# 0.500001 is used in comparisons instead of 0.5 to allow for a small
456+
# margin of error which can occur when dealing with floating point numbers.
457+
if index > outputmax:
458+
if index - outputmax <= 0.500001:
459+
index = outputmax
460+
else:
461+
raise err
462+
elif index < 0:
463+
if -index <= 0.500001:
464+
index = 0
465+
else:
466+
raise err
467+
# If the index wasn't set to outputmax or 0, round it and cast it as an
468+
# integer so it can be used in integer-based indexing.
469+
else:
470+
index = int(np.around(index))
471+
472+
return index

0 commit comments

Comments
 (0)