Repository: pvlib/pvlib-python

Issue:
Add retrieval function for horizon profile from MINES Paris Tech
<!-- Thank you for your contribution! The following items must be addressed before the code can be merged. Please don't hesitate to ask for help if you're unsure of how to accomplish any of the items. Feel free to remove checklist items that are not relevant to your change. -->

 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - [x] Tests added
 - [x] Updates entries to [`docs/sphinx/source/api.rst`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/api.rst) for API changes.
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

<!-- Brief description of the problem and proposed solution (if not already fully described in the issue linked to above): -->

The proposed function retrieves the local horizon profile for a specific location (latitude, longitude, and elevation). The returned horizon profile has a resolution of 1 degree in the azimuth direction. The service is provided by MINES ParisTech though I cannot find any official documentation for it.

The function added in this PR (``pvlib.iotools.get_mines_horizon``) is very similar to the function added in #1395 (``pvlib.iotools.get_pvgis_horizon``).


Discussion:
@mikofski @cwhanse I saw your discussions in #758 and #1290 and figured I'd share the code I had laying around for downloading the local horizon profile from SRTM. Does this have any interest to you?
I'm lovin' this! Could we also look into retrieving pvgis horizon data, how do they compare to the SRTM from MINES?

Patch:
diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py
--- a/pvlib/iotools/__init__.py
+++ b/pvlib/iotools/__init__.py
@@ -15,6 +15,7 @@
 from pvlib.iotools.pvgis import get_pvgis_tmy, read_pvgis_tmy  # noqa: F401
 from pvlib.iotools.pvgis import read_pvgis_hourly  # noqa: F401
 from pvlib.iotools.pvgis import get_pvgis_hourly  # noqa: F401
+from pvlib.iotools.pvgis import get_pvgis_horizon  # noqa: F401
 from pvlib.iotools.bsrn import get_bsrn  # noqa: F401
 from pvlib.iotools.bsrn import read_bsrn  # noqa: F401
 from pvlib.iotools.bsrn import parse_bsrn  # noqa: F401
diff --git a/pvlib/iotools/pvgis.py b/pvlib/iotools/pvgis.py
--- a/pvlib/iotools/pvgis.py
+++ b/pvlib/iotools/pvgis.py
@@ -665,3 +665,57 @@ def read_pvgis_tmy(filename, pvgis_format=None, map_variables=None):
         data = data.rename(columns=VARIABLE_MAP)
 
     return data, months_selected, inputs, meta
+
+
+def get_pvgis_horizon(latitude, longitude, url=URL, **kwargs):
+    """Get horizon data from PVGIS.
+
+    Parameters
+    ----------
+    latitude : float
+        Latitude in degrees north
+    longitude : float
+        Longitude in degrees east
+    url: str, default: :const:`pvlib.iotools.pvgis.URL`
+        Base URL for PVGIS
+    kwargs:
+        Passed to requests.get
+
+    Returns
+    -------
+    data : pd.Series
+        Pandas Series of the retrived horizon elevation angles. Index is the
+        corresponding horizon azimuth angles.
+    metadata : dict
+        Metadata returned by PVGIS.
+
+    Notes
+    -----
+    The horizon azimuths are specified clockwise from north, e.g., south=180.
+    This is the standard pvlib convention, although the PVGIS website specifies
+    south=0.
+
+    References
+    ----------
+    .. [1] `PVGIS horizon profile tool
+       <https://ec.europa.eu/jrc/en/PVGIS/tools/horizon>`_
+    """
+    params = {'lat': latitude, 'lon': longitude, 'outputformat': 'json'}
+    res = requests.get(url + 'printhorizon', params=params, **kwargs)
+    if not res.ok:
+        try:
+            err_msg = res.json()
+        except Exception:
+            res.raise_for_status()
+        else:
+            raise requests.HTTPError(err_msg['message'])
+    json_output = res.json()
+    metadata = json_output['meta']
+    data = pd.DataFrame(json_output['outputs']['horizon_profile'])
+    data.columns = ['horizon_azimuth', 'horizon_elevation']
+    # Convert azimuth to pvlib convention (north=0, south=180)
+    data['horizon_azimuth'] += 180
+    data.set_index('horizon_azimuth', inplace=True)
+    data = data['horizon_elevation']  # convert to pd.Series
+    data = data[data.index < 360]  # remove duplicate north point (0 and 360)
+    return data, metadata


Test patch:
diff --git a/pvlib/data/test_read_pvgis_horizon.csv b/pvlib/data/test_read_pvgis_horizon.csv
new file mode 100644
--- /dev/null
+++ b/pvlib/data/test_read_pvgis_horizon.csv
@@ -0,0 +1,49 @@
+horizon_azimuth,horizon_elevation
+0,9.9
+7.5,13
+15,14.5
+22.5,15.7
+30,14.9
+37.5,15.3
+45,15.7
+52.5,15.7
+60,13
+67.5,11.5
+75,11.1
+82.5,11.5
+90,10.3
+97.5,11.5
+105,10.3
+112.5,9.5
+120,10.7
+127.5,11.8
+135,11.8
+142.5,8.8
+150,8.4
+157.5,7.3
+165,5.7
+172.5,5.7
+180,4.6
+187.5,3.4
+195,0.8
+202.5,0
+210,0
+217.5,0
+225,0
+232.5,0
+240,0
+247.5,0
+255,0
+262.5,0
+270,0
+277.5,0
+285,0
+292.5,0
+300,0
+307.5,0
+315,1.1
+322.5,1.9
+330,3.8
+337.5,5
+345,6.5
+352.5,9.2
diff --git a/pvlib/tests/iotools/test_pvgis.py b/pvlib/tests/iotools/test_pvgis.py
--- a/pvlib/tests/iotools/test_pvgis.py
+++ b/pvlib/tests/iotools/test_pvgis.py
@@ -9,8 +9,9 @@
 import requests
 from pvlib.iotools import get_pvgis_tmy, read_pvgis_tmy
 from pvlib.iotools import get_pvgis_hourly, read_pvgis_hourly
+from pvlib.iotools import get_pvgis_horizon
 from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, assert_frame_equal,
-                        fail_on_pvlib_version)
+                        fail_on_pvlib_version, assert_series_equal)
 from pvlib._deprecation import pvlibDeprecationWarning
 
 
@@ -509,6 +510,23 @@ def test_get_pvgis_map_variables(pvgis_tmy_mapped_columns):
     assert all([c in pvgis_tmy_mapped_columns for c in actual.columns])
 
 
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_read_pvgis_horizon():
+    pvgis_data, _ = get_pvgis_horizon(35.171051, -106.465158)
+    horizon_data = pd.read_csv(DATA_DIR / 'test_read_pvgis_horizon.csv',
+                               index_col=0)
+    horizon_data = horizon_data['horizon_elevation']
+    assert_series_equal(pvgis_data, horizon_data)
+
+
+@pytest.mark.remote_data
+@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
+def test_read_pvgis_horizon_invalid_coords():
+    with pytest.raises(requests.HTTPError, match='lat: Incorrect value'):
+        _, _ = get_pvgis_horizon(100, 50)  # unfeasible latitude
+
+
 def test_read_pvgis_tmy_map_variables(pvgis_tmy_mapped_columns):
     fn = DATA_DIR / 'tmy_45.000_8.000_2005_2016.json'
     actual, _, _, _ = read_pvgis_tmy(fn, map_variables=True)