Skip to content

Commit 3c7b5da

Browse files
authored
Add get_solargis iotools function (#1969)
* Initial commit * Linting * Include v0.10.4 whatsnew * Add example * Update raises message * Update doc section order * Set parser to etree * Add tests * Add test skip for pandas<1.3.0 * Fix nan replacement and add variable conversion * Add time_resolution conversion * Fix precipitable_water conversion in solcast * Address reviewer comments * Fix test issues * Update pandas version in ci/requirements*.yml * Update test_solcast.py * Advance numpy requirement
1 parent c4a2b4b commit 3c7b5da

16 files changed

+309
-21
lines changed

ci/requirements-py3.10.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

ci/requirements-py3.11.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

ci/requirements-py3.12.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

ci/requirements-py3.7-min.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ dependencies:
1414
- pip:
1515
- dataclasses
1616
- h5py==3.1.0
17-
- numpy==1.16.0
18-
- pandas==0.25.0
17+
- numpy==1.17.3
18+
- pandas==1.3.0
1919
- scipy==1.5.0
2020
- pytest-rerunfailures # conda version is >3.6
2121
- pytest-remotedata # conda package is 0.3.0, needs > 0.3.1

ci/requirements-py3.7.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

ci/requirements-py3.8.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

ci/requirements-py3.9.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ dependencies:
88
- ephem
99
- h5py
1010
- numba
11-
- numpy >= 1.16.0
12-
- pandas >= 0.25.0
11+
- numpy >= 1.17.3
12+
- pandas >= 1.3.0
1313
- pip
1414
- pytest
1515
- pytest-cov

docs/sphinx/source/reference/iotools.rst

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ of sources and file formats relevant to solar energy modeling.
5353
iotools.get_solcast_historic
5454
iotools.get_solcast_forecast
5555
iotools.get_solcast_live
56+
iotools.get_solargis
5657

5758

5859
A :py:class:`~pvlib.location.Location` object may be created from metadata

docs/sphinx/source/whatsnew.rst

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ What's New
66

77
These are new features and improvements of note in each release.
88

9+
.. include:: whatsnew/v0.10.4.rst
910
.. include:: whatsnew/v0.10.3.rst
1011
.. include:: whatsnew/v0.10.2.rst
1112
.. include:: whatsnew/v0.10.1.rst

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ v0.10.4 (Anticipated March, 2024)
88
Enhancements
99
~~~~~~~~~~~~
1010
* Added the Huld PV model used by PVGIS (:pull:`1940`)
11+
* Add :py:func:`pvlib.iotools.get_solargis` for retrieving Solargis
12+
irradiance data. (:pull:`1969`)
1113
* Added function :py:func:`pvlib.shading.projected_solar_zenith_angle`,
1214
a common calculation in shading and tracking. (:issue:`1734`, :pull:`1904`)
1315
* Added :py:func:`~pvlib.iotools.get_solrad` for fetching irradiance data from
1416
the SOLRAD ground station network. (:pull:`1967`)
1517
* Added metadata parsing to :py:func:`~pvlib.iotools.read_solrad` to follow the standard iotools
1618
convention of returning a tuple of (data, meta). Previously the function only returned a dataframe. (:pull:`1968`)
1719

18-
1920
Bug fixes
2021
~~~~~~~~~
2122
* Fixed an error in solar position calculations when using
@@ -33,6 +34,7 @@ Bug fixes
3334
``temperature_model_parameters`` are specified on the passed ``system`` instead of on its ``arrays``. (:issue:`1759`).
3435
* :py:func:`pvlib.irradiance.ghi_from_poa_driesse_2023` now correctly makes use
3536
of the ``xtol`` argument. Previously, it was ignored. (:issue:`1970`, :pull:`1971`)
37+
* Fixed incorrect unit conversion of precipitable water used for the Solcast iotools functions.
3638
* :py:class:`~pvlib.modelchain.ModelChain.infer_temperature_model` now raises a more useful error when
3739
the temperature model cannot be inferred (:issue:`1946`)
3840

@@ -49,6 +51,8 @@ Documentation
4951

5052
Requirements
5153
~~~~~~~~~~~~
54+
* Minimum version of pandas advanced from 0.25.0 to 1.3.0. (:pull:`1969`)
55+
* Minimum version of numpy advanced from 1.16.0 to 1.17.3. (:pull:`1969`)
5256

5357

5458
Contributors
@@ -59,5 +63,4 @@ Contributors
5963
* Cliff Hansen (:ghuser:`cwhanse`)
6064
* Roma Koulikov (:ghuser:`matsuobasho`)
6165
* Adam R. Jensen (:ghuser:`AdamRJensen`)
62-
* Kevin Anderson (:ghuser:`kandersolar`)
6366
* Peter Dudfield (:ghuser:`peterdudfield`)

pvlib/iotools/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@
3434
from pvlib.iotools.solcast import get_solcast_live # noqa: F401
3535
from pvlib.iotools.solcast import get_solcast_historic # noqa: F401
3636
from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401
37+
from pvlib.iotools.solargis import get_solargis # noqa: F401

pvlib/iotools/solargis.py

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Functions to retrieve and parse irradiance data from Solargis."""
2+
3+
import pandas as pd
4+
import requests
5+
from dataclasses import dataclass
6+
import io
7+
8+
URL = 'https://solargis.info/ws/rest/datadelivery/request'
9+
10+
11+
TIME_RESOLUTION_MAP = {
12+
5: 'MIN_5', 10: 'MIN_10', 15: 'MIN_15', 30: 'MIN_30', 60: 'HOURLY',
13+
'PT05M': 'MIN_5', 'PT5M': 'MIN_5', 'PT10M': 'MIN_10', 'PT15M': 'MIN_15',
14+
'PT30': 'MIN_30', 'PT60M': 'HOURLY', 'PT1H': 'HOURLY', 'P1D': 'DAILY',
15+
'P1M': 'MONTHLY', 'P1Y': 'YEARLY'}
16+
17+
18+
@dataclass
19+
class ParameterMap:
20+
solargis_name: str
21+
pvlib_name: str
22+
conversion: callable = lambda x: x
23+
24+
25+
# define the conventions between Solargis and pvlib nomenclature and units
26+
VARIABLE_MAP = [
27+
# Irradiance (unit varies based on time resolution)
28+
ParameterMap('GHI', 'ghi'),
29+
ParameterMap('GHI_C', 'ghi_clear'), # this is stated in documentation
30+
ParameterMap('GHIc', 'ghi_clear'), # this is used in practice
31+
ParameterMap('DNI', 'dni'),
32+
ParameterMap('DNI_C', 'dni_clear'),
33+
ParameterMap('DNIc', 'dni_clear'),
34+
ParameterMap('DIF', 'dhi'),
35+
ParameterMap('GTI', 'poa_global'),
36+
ParameterMap('GTI_C', 'poa_global_clear'),
37+
ParameterMap('GTIc', 'poa_global_clear'),
38+
# Solar position
39+
ParameterMap('SE', 'solar_elevation'),
40+
# SA -> solar_azimuth (degrees) (different convention)
41+
ParameterMap("SA", "solar_azimuth", lambda x: x + 180),
42+
# Weather / atmospheric parameters
43+
ParameterMap('TEMP', 'temp_air'),
44+
ParameterMap('TD', 'temp_dew'),
45+
# surface_pressure (hPa) -> pressure (Pa)
46+
ParameterMap('AP', 'pressure', lambda x: x*100),
47+
ParameterMap('RH', 'relative_humidity'),
48+
ParameterMap('WS', 'wind_speed'),
49+
ParameterMap('WD', 'wind_direction'),
50+
ParameterMap('INC', 'aoi'), # angle of incidence of direct irradiance
51+
# precipitable_water (kg/m2) -> precipitable_water (cm)
52+
ParameterMap('PWAT', 'precipitable_water', lambda x: x/10),
53+
]
54+
55+
METADATA_FIELDS = [
56+
'issued', 'site name', 'latitude', 'longitude', 'elevation',
57+
'summarization type', 'summarization period'
58+
]
59+
60+
61+
# Variables that use "-9" as nan values
62+
NA_9_COLUMNS = ['GHI', 'GHIc', 'DNI', 'DNIc', 'DIF', 'GTI', 'GIc', 'KT', 'PAR',
63+
'PREC', 'PWAT', 'SDWE', 'SFWE']
64+
65+
66+
def get_solargis(latitude, longitude, start, end, variables, api_key,
67+
time_resolution, timestamp_type='center', tz='GMT+00',
68+
terrain_shading=True, url=URL, map_variables=True,
69+
timeout=30):
70+
"""
71+
Retrieve irradiance time series data from Solargis.
72+
73+
The Solargis [1]_ API is described in [2]_.
74+
75+
Parameters
76+
----------
77+
latitude: float
78+
In decimal degrees, between -90 and 90, north is positive (ISO 19115)
79+
longitude: float
80+
In decimal degrees, between -180 and 180, east is positive (ISO 19115)
81+
start : datetime-like
82+
Start date of time series.
83+
end : datetime-like
84+
End date of time series.
85+
variables : list
86+
List of variables to request, see [2]_ for options.
87+
api_key : str
88+
API key.
89+
time_resolution : str, {'PT05M', 'PT10M', 'PT15M', 'PT30', 'PT1H', 'P1D', 'P1M', 'P1Y'}
90+
Time resolution as an integer number of minutes (e.g. 5, 60)
91+
or an ISO 8601 duration string (e.g. "PT05M", "PT60M", "P1M").
92+
timestamp_type : {'start', 'center', 'end'}, default: 'center'
93+
Labeling of time stamps of the return data.
94+
tz : str, default : 'GMT+00'
95+
Timezone of `start` and `end` in the format "GMT+hh" or "GMT-hh".
96+
terrain_shading : boolean, default: True
97+
Whether to account for horizon shading.
98+
url : str, default : :const:`pvlib.iotools.solargis.URL`
99+
Base url of Solargis API.
100+
map_variables : boolean, default: True
101+
When true, renames columns of the Dataframe to pvlib variable names
102+
where applicable. See variable :const:`VARIABLE_MAP`.
103+
timeout : int or float, default: 30
104+
Time in seconds to wait for server response before timeout
105+
106+
Returns
107+
-------
108+
data : DataFrame
109+
DataFrame containing time series data.
110+
meta : dict
111+
Dictionary containing metadata.
112+
113+
Raises
114+
------
115+
requests.HTTPError
116+
A message from the Solargis server if the request is rejected
117+
118+
Notes
119+
-----
120+
Each XML request is limited to retrieving 31 days of data.
121+
122+
The variable units depends on the time frequency, e.g., the unit for
123+
sub-hourly irradiance data is :math:`W/m^2`, for hourly data it is
124+
:math:`Wh/m^2`, and for daily data it is :math:`kWh/m^2`.
125+
126+
References
127+
----------
128+
.. [1] `Solargis <https://solargis.com>`_
129+
.. [2] `Solargis API User Guide
130+
<https://solargis.atlassian.net/wiki/spaces/public/pages/7602367/Solargis+API+User+Guide>`_
131+
132+
Examples
133+
--------
134+
>>> # Retrieve two days of irradiance data from Solargis
135+
>>> data, meta = response = pvlib.iotools.get_solargis(
136+
>>> latitude=48.61259, longitude=20.827079,
137+
>>> start='2022-01-01', end='2022-01-02',
138+
>>> variables=['GHI', 'DNI'], time_resolution='PT05M', api_key='demo')
139+
""" # noqa: E501
140+
# Use pd.to_datetime so that strings (e.g. '2021-01-01') are accepted
141+
start = pd.to_datetime(start)
142+
end = pd.to_datetime(end)
143+
144+
headers = {'Content-Type': 'application/xml'}
145+
146+
# Solargis recommends creating a unique site_id for each location request.
147+
# The site_id does not impact the data retrieval and is used for debugging.
148+
site_id = f"latitude_{latitude}_longitude_{longitude}"
149+
150+
request_xml = f'''<ws:dataDeliveryRequest
151+
dateFrom="{start.strftime('%Y-%m-%d')}"
152+
dateTo="{end.strftime('%Y-%m-%d')}"
153+
xmlns="http://geomodel.eu/schema/data/request"
154+
xmlns:ws="http://geomodel.eu/schema/ws/data"
155+
xmlns:geo="http://geomodel.eu/schema/common/geo"
156+
xmlns:pv="http://geomodel.eu/schema/common/pv"
157+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
158+
<site id="{site_id}" name="" lat="{latitude}" lng="{longitude}">
159+
</site>
160+
<processing key="{' '.join(variables)}"
161+
summarization="{TIME_RESOLUTION_MAP.get(time_resolution, time_resolution).upper()}"
162+
terrainShading="{str(terrain_shading).lower()}">
163+
<timestampType>{timestamp_type.upper()}</timestampType>
164+
<timeZone>{tz}</timeZone>
165+
</processing>
166+
</ws:dataDeliveryRequest>''' # noqa: E501
167+
168+
response = requests.post(url + "?key=" + api_key, headers=headers,
169+
data=request_xml.encode('utf8'), timeout=timeout)
170+
171+
if response.ok is False:
172+
raise requests.HTTPError(response.json())
173+
174+
# Parse metadata
175+
header = pd.read_xml(io.StringIO(response.text), parser='etree')
176+
meta_lines = header['metadata'].iloc[0].split('#')
177+
meta_lines = [line.strip() for line in meta_lines]
178+
meta = {}
179+
for line in meta_lines:
180+
if ':' in line:
181+
key = line.split(':')[0].lower()
182+
if key in METADATA_FIELDS:
183+
meta[key] = ':'.join(line.split(':')[1:])
184+
meta['latitude'] = float(meta['latitude'])
185+
meta['longitude'] = float(meta['longitude'])
186+
meta['altitude'] = float(meta.pop('elevation').replace('m a.s.l.', ''))
187+
188+
# Parse data
189+
data = pd.read_xml(io.StringIO(response.text), xpath='.//doc:row',
190+
namespaces={'doc': 'http://geomodel.eu/schema/ws/data'},
191+
parser='etree')
192+
data.index = pd.to_datetime(data['dateTime'])
193+
# when requesting one variable, it is necessary to convert dataframe to str
194+
data = data['values'].astype(str).str.split(' ', expand=True)
195+
data = data.astype(float)
196+
data.columns = header['columns'].iloc[0].split()
197+
198+
# Replace "-9" with nan values for specific columns
199+
for variable in data.columns:
200+
if variable in NA_9_COLUMNS:
201+
data[variable] = data[variable].replace(-9, pd.NA)
202+
203+
# rename and convert variables
204+
if map_variables:
205+
for variable in VARIABLE_MAP:
206+
if variable.solargis_name in data.columns:
207+
data.rename(
208+
columns={variable.solargis_name: variable.pvlib_name},
209+
inplace=True
210+
)
211+
data[variable.pvlib_name] = data[
212+
variable.pvlib_name].apply(variable.conversion)
213+
214+
return data, meta

pvlib/iotools/solcast.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ParameterMap:
3535
"azimuth", "solar_azimuth", lambda x: -x % 360
3636
),
3737
# precipitable_water (kg/m2) -> precipitable_water (cm)
38-
ParameterMap("precipitable_water", "precipitable_water", lambda x: x*10),
38+
ParameterMap("precipitable_water", "precipitable_water", lambda x: x/10),
3939
# zenith -> solar_zenith
4040
ParameterMap("zenith", "solar_zenith"),
4141
# clearsky

0 commit comments

Comments
 (0)