forked from pvlib/pvlib-python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsodapro.py
371 lines (311 loc) · 15 KB
/
sodapro.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
"""Functions to access data from Copernicus Atmosphere Monitoring Service
(CAMS) radiation service.
.. codeauthor:: Adam R. Jensen<adam-r-j@hotmail.com>
"""
import pandas as pd
import requests
import io
import warnings
URL = 'api.soda-solardata.com'
CAMS_INTEGRATED_COLUMNS = [
'TOA', 'Clear sky GHI', 'Clear sky BHI', 'Clear sky DHI', 'Clear sky BNI',
'GHI', 'BHI', 'DHI', 'BNI',
'GHI no corr', 'BHI no corr', 'DHI no corr', 'BNI no corr']
# Dictionary mapping CAMS Radiation and McClear variables to pvlib names
VARIABLE_MAP = {
'TOA': 'ghi_extra',
'Clear sky GHI': 'ghi_clear',
'Clear sky BHI': 'bhi_clear',
'Clear sky DHI': 'dhi_clear',
'Clear sky BNI': 'dni_clear',
'GHI': 'ghi',
'BHI': 'bhi',
'DHI': 'dhi',
'BNI': 'dni',
'sza': 'solar_zenith',
}
# Dictionary mapping time steps to CAMS time step format
TIME_STEPS_MAP = {'1min': 'PT01M', '15min': 'PT15M', '1h': 'PT01H',
'1d': 'P01D', '1M': 'P01M'}
TIME_STEPS_IN_HOURS = {'1min': 1/60, '15min': 15/60, '1h': 1, '1d': 24}
SUMMATION_PERIOD_TO_TIME_STEP = {'0 year 0 month 0 day 0 h 1 min 0 s': '1min',
'0 year 0 month 0 day 0 h 15 min 0 s': '15min', # noqa
'0 year 0 month 0 day 1 h 0 min 0 s': '1h',
'0 year 0 month 1 day 0 h 0 min 0 s': '1d',
'0 year 1 month 0 day 0 h 0 min 0 s': '1M'}
def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
altitude=None, time_step='1h', time_ref='UT', verbose=False,
integrated=False, label=None, map_variables=True,
server=URL, timeout=30):
"""Retrieve irradiance and clear-sky time series from CAMS.
Time-series of radiation and/or clear-sky global, beam, and
diffuse radiation from CAMS (see [1]_). Data is retrieved from SoDa [2]_.
Time coverage: 2004-01-01 to two days ago
Access: free, but requires registration, see [2]_
Requests: max. 100 per day
Geographical coverage: worldwide for CAMS McClear and approximately -66° to
66° in both latitude and longitude for CAMS Radiation.
Parameters
----------
latitude: float
in decimal degrees, between -90 and 90, north is positive (ISO 19115)
longitude : float
in decimal degrees, between -180 and 180, east is positive (ISO 19115)
start: datetime-like
First day of the requested period
end: datetime-like
Last day of the requested period
email: str
Email address linked to a SoDa account
identifier: {'mcclear', 'cams_radiation'}
Specify whether to retrieve CAMS Radiation or McClear parameters
altitude: float, optional
Altitude in meters. If not specified, then the altitude is determined
from the NASA SRTM database
time_step: str, {'1min', '15min', '1h', '1d', '1M'}, default: '1h'
Time step of the time series, either 1 minute, 15 minute, hourly,
daily, or monthly.
time_ref: str, {'UT', 'TST'}, default: 'UT'
'UT' (universal time) or 'TST' (True Solar Time)
verbose: boolean, default: False
Verbose mode outputs additional parameters (aerosols). Only available
for 1 minute and universal time. See [1]_ for parameter description.
integrated: boolean, default False
Whether to return radiation parameters as integrated values (Wh/m^2)
or as average irradiance values (W/m^2) (pvlib preferred units)
label : {'right', 'left'}, optional
Which bin edge label to label time-step with. The default is 'left' for
all time steps except for '1M' which has a default of 'right'.
map_variables: bool, default: True
When true, renames columns of the DataFrame to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
server: str, default: :const:`pvlib.iotools.sodapro.URL`
Base url of the SoDa Pro CAMS Radiation API.
timeout : int, default: 30
Time in seconds to wait for server response before timeout
Returns
-------
data: pandas.DataFrame
Timeseries data, see Notes for columns
metadata: dict
Metadata of the requested time-series
Notes
-----
In order to use the CAMS services, users must register for a free SoDa
account using an email address [2]_.
The returned data DataFrame includes the following fields:
======================== ====== =========================================
Key, mapped key Format Description
======================== ====== =========================================
**Mapped field names are returned when the map_variables argument is True**
---------------------------------------------------------------------------
Observation period str Beginning/end of time period
TOA, ghi_extra float Horizontal radiation at top of atmosphere
Clear sky GHI, ghi_clear float Clear sky global radiation on horizontal
Clear sky BHI, bhi_clear float Clear sky beam radiation on horizontal
Clear sky DHI, dhi_clear float Clear sky diffuse radiation on horizontal
Clear sky BNI, dni_clear float Clear sky beam radiation normal to sun
GHI, ghi† float Global horizontal radiation
BHI, bhi† float Beam (direct) radiation on horizontal
DHI, dhi† float Diffuse horizontal radiation
BNI, dni† float Beam (direct) radiation normal to the sun
Reliability† float Reliable data fraction in summarization
======================== ====== =========================================
†Parameters only returned if identifier='cams_radiation'. For description
of additional output parameters in verbose mode, see [1]_.
Note that it is recommended to specify the latitude and longitude to at
least the fourth decimal place.
Variables corresponding to standard pvlib variables are renamed,
e.g. `sza` becomes `solar_zenith`. See variable :const:`VARIABLE_MAP` for
the complete mapping.
See Also
--------
pvlib.iotools.read_cams, pvlib.iotools.parse_cams
Raises
------
requests.HTTPError
If the request is invalid, then an XML file is returned by the CAMS
service and the error message will be raised as an exception.
References
----------
.. [1] `CAMS solar radiation documentation
<https://atmosphere.copernicus.eu/solar-radiation>`_
.. [2] `CAMS Radiation Automatic Access (SoDa)
<https://www.soda-pro.com/help/cams-services/cams-radiation-service/automatic-access>`_
"""
try:
time_step_str = TIME_STEPS_MAP[time_step]
except KeyError:
raise ValueError(f'Time step not recognized. Must be one of '
f'{list(TIME_STEPS_MAP.keys())}')
if (verbose) and ((time_step != '1min') or (time_ref != 'UT')):
verbose = False
warnings.warn("Verbose mode only supports 1 min. UT time series!")
if identifier not in ['mcclear', 'cams_radiation']:
raise ValueError('Identifier must be either mcclear or cams_radiation')
# Format verbose variable to the required format: {'true', 'false'}
verbose = str(verbose).lower()
if altitude is None: # Let SoDa get elevation from the NASA SRTM database
altitude = -999
# Start and end date should be in the format: yyyy-mm-dd
start = pd.to_datetime(start).strftime('%Y-%m-%d')
end = pd.to_datetime(end).strftime('%Y-%m-%d')
email = email.replace('@', '%2540') # Format email address
identifier = 'get_{}'.format(identifier.lower()) # Format identifier str
base_url = f"https://{server}/service/wps"
data_inputs_dict = {
'latitude': latitude,
'longitude': longitude,
'altitude': altitude,
'date_begin': start,
'date_end': end,
'time_ref': time_ref,
'summarization': time_step_str,
'username': email,
'verbose': verbose}
# Manual formatting of the input parameters seperating each by a semicolon
data_inputs = ";".join([f"{key}={value}" for key, value in
data_inputs_dict.items()])
params = {'Service': 'WPS',
'Request': 'Execute',
'Identifier': identifier,
'version': '1.0.0',
'RawDataOutput': 'irradiation',
}
# The DataInputs parameter of the URL has to be manually formatted and
# added to the base URL as it contains sub-parameters seperated by
# semi-colons, which gets incorrectly formatted by the requests function
# if passed using the params argument.
res = requests.get(base_url + '?DataInputs=' + data_inputs, params=params,
timeout=timeout)
# Invalid requests returns an XML error message and the HTTP staus code 200
# as if the request was successful. Therefore, errors cannot be handled
# automatic (e.g. res.raise_for_status()) and errors are handled manually
if res.headers['Content-Type'] == 'application/xml':
errors = res.text.split('ows:ExceptionText')[1][1:-2]
raise requests.HTTPError(errors, response=res)
# Successful requests returns a csv data file
elif res.headers['Content-Type'] == 'application/csv':
fbuf = io.StringIO(res.content.decode('utf-8'))
data, metadata = parse_cams(fbuf, integrated=integrated, label=label,
map_variables=map_variables)
return data, metadata
def parse_cams(fbuf, integrated=False, label=None, map_variables=True):
"""
Parse a file-like buffer with data in the format of a CAMS Radiation or
McClear file. The CAMS solar radiation services are described in [1]_.
Parameters
----------
fbuf: file-like object
File-like object containing data to read.
integrated: boolean, default False
Whether to return radiation parameters as integrated values (Wh/m^2)
or as average irradiance values (W/m^2) (pvlib preferred units)
label : {'right', 'left'}, optional
Which bin edge label to label time-step with. The default is 'left' for
all time steps except for '1M' which has a default of 'right'.
map_variables: bool, default: True
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
Returns
-------
data: pandas.DataFrame
Timeseries data from CAMS Radiation or McClear
metadata: dict
Metadata available in the file.
See Also
--------
pvlib.iotools.read_cams, pvlib.iotools.get_cams
References
----------
.. [1] `CAMS solar radiation documentation
<https://atmosphere.copernicus.eu/solar-radiation>`_
"""
metadata = {}
# Initial lines starting with # contain metadata
while True:
line = fbuf.readline().rstrip('\n')
if line.startswith('# Observation period'):
# The last line of the metadata section contains the column names
names = line.lstrip('# ').split(';')
break # End of metadata section has been reached
elif ': ' in line:
metadata[line.split(': ')[0].lstrip('# ')] = line.split(': ')[1]
# Convert latitude, longitude, and altitude values from strings to floats
for k_old in list(metadata.keys()):
k_new = k_old.lstrip().split(' ')[0].lower()
if k_new in ['latitude', 'longitude', 'altitude']:
metadata[k_new] = float(metadata.pop(k_old))
metadata['radiation_unit'] = \
{True: 'Wh/m^2', False: 'W/m^2'}[integrated]
# Determine the time_step from the metadata dictionary
time_step = SUMMATION_PERIOD_TO_TIME_STEP[
metadata['Summarization (integration) period']]
metadata['time_step'] = time_step
data = pd.read_csv(fbuf, sep=';', comment='#', header=None, names=names)
obs_period = data['Observation period'].str.split('/')
# Set index as the start observation time (left) and localize to UTC
if (label == 'left') | ((label is None) & (time_step != '1M')):
data.index = pd.to_datetime(obs_period.str[0], utc=True)
# Set index as the stop observation time (right) and localize to UTC
# default label for monthly data is 'right' following Pandas' convention
elif (label == 'right') | ((label is None) & (time_step == '1M')):
data.index = pd.to_datetime(obs_period.str[1], utc=True)
# For time_steps '1d' and '1M', drop timezone and round to nearest midnight
if (time_step == '1d') | (time_step == '1M'):
data.index = pd.DatetimeIndex(data.index.date)
# For monthly data with 'right' label, the index should be the last
# date of the month and not the first date of the following month
if (time_step == '1M') & (label != 'left'):
data.index = data.index - pd.Timedelta(days=1)
if not integrated: # Convert radiation values from Wh/m2 to W/m2
integrated_cols = [c for c in CAMS_INTEGRATED_COLUMNS
if c in data.columns]
if time_step == '1M':
time_delta = (pd.to_datetime(obs_period.str[1])
- pd.to_datetime(obs_period.str[0]))
hours = time_delta.dt.total_seconds()/60/60
data[integrated_cols] = data[integrated_cols].\
divide(hours.tolist(), axis='rows')
else:
data[integrated_cols] = (data[integrated_cols] /
TIME_STEPS_IN_HOURS[time_step])
data.index.name = None # Set index name to None
if map_variables:
data = data.rename(columns=VARIABLE_MAP)
return data, metadata
def read_cams(filename, integrated=False, label=None, map_variables=True):
"""
Read a CAMS Radiation or McClear file into a pandas DataFrame.
CAMS Radiation and McClear are described in [1]_.
Parameters
----------
filename: str
Filename of a file containing data to read.
integrated: boolean, default False
Whether to return radiation parameters as integrated values (Wh/m^2)
or as average irradiance values (W/m^2) (pvlib preferred units)
label : {'right', 'left}, optional
Which bin edge label to label time-step with. The default is 'left' for
all time steps except for '1M' which has a default of 'right'.
map_variables: bool, default: True
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
Returns
-------
data: pandas.DataFrame
Timeseries data from CAMS Radiation or McClear.
See :func:`pvlib.iotools.get_cams` for fields.
metadata: dict
Metadata available in the file.
See Also
--------
pvlib.iotools.parse_cams, pvlib.iotools.get_cams
References
----------
.. [1] `CAMS solar radiation documentation
<https://atmosphere.copernicus.eu/solar-radiation>`_
"""
with open(str(filename), 'r') as fbuf:
content = parse_cams(fbuf, integrated, label, map_variables)
return content