|
| 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 |
0 commit comments