pvlib 0.12.0__py3-none-any.whl → 0.12.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pvlib/iotools/psm4.py ADDED
@@ -0,0 +1,819 @@
1
+ """
2
+ Functions for reading and retrieving data from NSRDB PSM4. See:
3
+ https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-aggregated-v4-0-0-download/
4
+ https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-tmy-v4-0-0-download/
5
+ https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-conus-v4-0-0-download/
6
+ https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-full-disc-v4-0-0-download/
7
+ """
8
+
9
+ import io
10
+ from urllib.parse import urljoin
11
+ import requests
12
+ import pandas as pd
13
+ from json import JSONDecodeError
14
+
15
+ NSRDB_API_BASE = "https://developer.nrel.gov/api/nsrdb/v2/solar/"
16
+ PSM4_AGG_ENDPOINT = "nsrdb-GOES-aggregated-v4-0-0-download.csv"
17
+ PSM4_TMY_ENDPOINT = "nsrdb-GOES-tmy-v4-0-0-download.csv"
18
+ PSM4_CON_ENDPOINT = "nsrdb-GOES-conus-v4-0-0-download.csv"
19
+ PSM4_FUL_ENDPOINT = "nsrdb-GOES-full-disc-v4-0-0-download.csv"
20
+ PSM4_AGG_URL = urljoin(NSRDB_API_BASE, PSM4_AGG_ENDPOINT)
21
+ PSM4_TMY_URL = urljoin(NSRDB_API_BASE, PSM4_TMY_ENDPOINT)
22
+ PSM4_CON_URL = urljoin(NSRDB_API_BASE, PSM4_CON_ENDPOINT)
23
+ PSM4_FUL_URL = urljoin(NSRDB_API_BASE, PSM4_FUL_ENDPOINT)
24
+
25
+ PARAMETERS = (
26
+ 'air_temperature', 'dew_point', 'dhi', 'dni', 'ghi', 'surface_albedo',
27
+ 'surface_pressure', 'wind_direction', 'wind_speed')
28
+ PVLIB_PYTHON = 'pvlib python'
29
+
30
+ # Dictionary mapping PSM4 response names to pvlib names
31
+ VARIABLE_MAP = {
32
+ 'GHI': 'ghi',
33
+ 'DHI': 'dhi',
34
+ 'DNI': 'dni',
35
+ 'Clearsky GHI': 'ghi_clear',
36
+ 'Clearsky DHI': 'dhi_clear',
37
+ 'Clearsky DNI': 'dni_clear',
38
+ 'Solar Zenith Angle': 'solar_zenith',
39
+ 'Temperature': 'temp_air',
40
+ 'Dew Point': 'temp_dew',
41
+ 'Relative Humidity': 'relative_humidity',
42
+ 'Pressure': 'pressure',
43
+ 'Wind Speed': 'wind_speed',
44
+ 'Wind Direction': 'wind_direction',
45
+ 'Surface Albedo': 'albedo',
46
+ 'Precipitable Water': 'precipitable_water',
47
+ 'AOD': 'aod',
48
+ }
49
+
50
+ # Dictionary mapping pvlib names to PSM4 request names
51
+ # Note, PSM4 uses different names for the same variables in the
52
+ # response and the request
53
+ REQUEST_VARIABLE_MAP = {
54
+ 'ghi': 'ghi',
55
+ 'dhi': 'dhi',
56
+ 'dni': 'dni',
57
+ 'ghi_clear': 'clearsky_ghi',
58
+ 'dhi_clear': 'clearsky_dhi',
59
+ 'dni_clear': 'clearsky_dni',
60
+ 'solar_zenith': 'solar_zenith_angle',
61
+ 'temp_air': 'air_temperature',
62
+ 'temp_dew': 'dew_point',
63
+ 'relative_humidity': 'relative_humidity',
64
+ 'pressure': 'surface_pressure',
65
+ 'wind_speed': 'wind_speed',
66
+ 'wind_direction': 'wind_direction',
67
+ 'albedo': 'surface_albedo',
68
+ 'precipitable_water': 'total_precipitable_water',
69
+ 'aod': 'aod',
70
+ }
71
+
72
+
73
+ def get_nsrdb_psm4_aggregated(latitude, longitude, api_key, email,
74
+ year='2023', time_step=60,
75
+ parameters=PARAMETERS, leap_day=True,
76
+ full_name=PVLIB_PYTHON,
77
+ affiliation=PVLIB_PYTHON,
78
+ utc=False, map_variables=True, url=None,
79
+ timeout=30):
80
+ """
81
+ Retrieve NSRDB PSM4 timeseries weather data from the PSM4 NSRDB GOES
82
+ Aggregated v4 API.
83
+
84
+ The NSRDB is described in [1]_ and the PSM4 NSRDB GOES Aggregated v4 API is
85
+ described in [2]_,.
86
+
87
+ Parameters
88
+ ----------
89
+ latitude : float or int
90
+ in decimal degrees, between -90 and 90, north is positive
91
+ longitude : float or int
92
+ in decimal degrees, between -180 and 180, east is positive
93
+ api_key : str
94
+ NREL Developer Network API key
95
+ email : str
96
+ NREL API uses this to automatically communicate messages back
97
+ to the user only if necessary
98
+ year : str, default '2023'
99
+ PSM4 API parameter specifing year (e.g. ``2023``) to download. The
100
+ allowed values update periodically, so consult the NSRDB reference
101
+ below for the current set of options. Called ``names`` in NSRDB API.
102
+ time_step : int, {60, 30}
103
+ time step in minutes, must be 60 or 30 for PSM4 Aggregated. Called
104
+ ``interval`` in NSRDB API.
105
+ parameters : list of str, optional
106
+ meteorological fields to fetch. If not specified, defaults to
107
+ ``pvlib.iotools.psm4.PARAMETERS``. See reference [2]_ for a list of
108
+ available fields. Alternatively, pvlib names may also be used (e.g.
109
+ 'ghi' rather than 'GHI'); see :const:`REQUEST_VARIABLE_MAP`. To
110
+ retrieve all available fields, set ``parameters=[]``.
111
+ leap_day : bool, default : True
112
+ include leap day in the results
113
+ full_name : str, default 'pvlib python'
114
+ optional
115
+ affiliation : str, default 'pvlib python'
116
+ optional
117
+ utc: bool, default : False
118
+ retrieve data with timestamps converted to UTC. False returns
119
+ timestamps in local standard time of the selected location
120
+ map_variables : bool, default True
121
+ When true, renames columns of the Dataframe to pvlib variable names
122
+ where applicable. See variable :const:`VARIABLE_MAP`.
123
+ url : str, optional
124
+ Full API endpoint URL. If not specified, the PSM4 GOES Aggregated v4
125
+ URL is used.
126
+ timeout : int, default 30
127
+ time in seconds to wait for server response before timeout
128
+
129
+ Returns
130
+ -------
131
+ data : pandas.DataFrame
132
+ timeseries data from NREL PSM4
133
+ metadata : dict
134
+ metadata from NREL PSM4 about the record, see
135
+ :func:`pvlib.iotools.parse_nsrdb_psm4` for fields
136
+
137
+ Raises
138
+ ------
139
+ requests.HTTPError
140
+ if the request response status is not ok, then the ``'errors'`` field
141
+ from the JSON response or any error message in the content will be
142
+ raised as an exception, for example if the `api_key` was rejected or if
143
+ the coordinates were not found in the NSRDB
144
+
145
+ Notes
146
+ -----
147
+ The required NREL developer key, `api_key`, is available for free by
148
+ registering at the `NREL Developer Network <https://developer.nrel.gov/>`_.
149
+
150
+ .. warning:: The "DEMO_KEY" `api_key` is severely rate limited and may
151
+ result in rejected requests.
152
+
153
+ .. warning:: PSM4 is limited to data found in the NSRDB, please consult
154
+ the references below for locations with available data. Additionally,
155
+ querying data with < 30-minute resolution uses a different API endpoint
156
+ with fewer available fields (see [4]_).
157
+
158
+ See Also
159
+ --------
160
+ pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_conus,
161
+ pvlib.iotools.get_nsrdb_psm4_full_disc, pvlib.iotools.read_nsrdb_psm4,
162
+ pvlib.iotools.parse_nsrdb_psm4
163
+
164
+ References
165
+ ----------
166
+
167
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
168
+ <https://nsrdb.nrel.gov/>`_
169
+ .. [2] `NSRDB GOES Aggregated V4.0.0
170
+ <https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-aggregated-v4-0-0-download/>`_
171
+ """
172
+ # The well know text (WKT) representation of geometry notation is strict.
173
+ # A POINT object is a string with longitude first, then the latitude, with
174
+ # four decimals each, and exactly one space between them.
175
+ longitude = ('%9.4f' % longitude).strip()
176
+ latitude = ('%8.4f' % latitude).strip()
177
+ # TODO: make format_WKT(object_type, *args) in tools.py
178
+
179
+ # convert to string to accomodate integer years being passed in
180
+ year = str(year)
181
+
182
+ # convert pvlib names in parameters to PSM4 convention
183
+ parameters = [REQUEST_VARIABLE_MAP.get(a, a) for a in parameters]
184
+
185
+ # required query-string parameters for request to PSM4 API
186
+ params = {
187
+ 'api_key': api_key,
188
+ 'full_name': full_name,
189
+ 'email': email,
190
+ 'affiliation': affiliation,
191
+ 'reason': PVLIB_PYTHON,
192
+ 'mailing_list': 'false',
193
+ 'wkt': 'POINT(%s %s)' % (longitude, latitude),
194
+ 'names': year,
195
+ 'attributes': ','.join(parameters),
196
+ 'leap_day': str(leap_day).lower(),
197
+ 'utc': str(utc).lower(),
198
+ 'interval': time_step
199
+ }
200
+ # request CSV download from NREL PSM4
201
+ if url is None:
202
+ url = PSM4_AGG_URL
203
+
204
+ response = requests.get(url, params=params, timeout=timeout)
205
+ if not response.ok:
206
+ # if the API key is rejected, then the response status will be 403
207
+ # Forbidden, and then the error is in the content and there is no JSON
208
+ try:
209
+ errors = response.json()['errors']
210
+ except JSONDecodeError:
211
+ errors = response.content.decode('utf-8')
212
+ raise requests.HTTPError(errors, response=response)
213
+ # the CSV is in the response content as a UTF-8 bytestring
214
+ # to use pandas we need to create a file buffer from the response
215
+ fbuf = io.StringIO(response.content.decode('utf-8'))
216
+ return parse_nsrdb_psm4(fbuf, map_variables)
217
+
218
+
219
+ def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
220
+ time_step=60, parameters=PARAMETERS, leap_day=False,
221
+ full_name=PVLIB_PYTHON, affiliation=PVLIB_PYTHON,
222
+ utc=False, map_variables=True, url=None, timeout=30):
223
+ """
224
+ Retrieve NSRDB PSM4 timeseries weather data from the PSM4 NSRDB GOES
225
+ TMY v4 API.
226
+
227
+ The NSRDB is described in [1]_ and the PSM4 NSRDB GOES TMY v4 API is
228
+ described in [2]_,.
229
+
230
+ Parameters
231
+ ----------
232
+ latitude : float or int
233
+ in decimal degrees, between -90 and 90, north is positive
234
+ longitude : float or int
235
+ in decimal degrees, between -180 and 180, east is positive
236
+ api_key : str
237
+ NREL Developer Network API key
238
+ email : str
239
+ NREL API uses this to automatically communicate messages back
240
+ to the user only if necessary
241
+ year : str, default 'tmy'
242
+ PSM4 API parameter specifing TMY variant to download (e.g. ``'tmy'``
243
+ or ``'tgy-2022'``). The allowed values update periodically, so
244
+ consult the NSRDB references below for the current set of options.
245
+ Called ``names`` in NSRDB API.
246
+ time_step : int, {60}
247
+ time step in minutes. Must be 60 for typical year requests. Called
248
+ ``interval`` in NSRDB API.
249
+ parameters : list of str, optional
250
+ meteorological fields to fetch. If not specified, defaults to
251
+ ``pvlib.iotools.psm4.PARAMETERS``. See reference [2]_ for a list of
252
+ available fields. Alternatively, pvlib names may also be used (e.g.
253
+ 'ghi' rather than 'GHI'); see :const:`REQUEST_VARIABLE_MAP`. To
254
+ retrieve all available fields, set ``parameters=[]``.
255
+ leap_day : bool, default : False
256
+ Include leap day in the results. Ignored for tmy/tgy/tdy requests.
257
+ full_name : str, default 'pvlib python'
258
+ optional
259
+ affiliation : str, default 'pvlib python'
260
+ optional
261
+ utc: bool, default : False
262
+ retrieve data with timestamps converted to UTC. False returns
263
+ timestamps in local standard time of the selected location
264
+ map_variables : bool, default True
265
+ When true, renames columns of the Dataframe to pvlib variable names
266
+ where applicable. See variable :const:`VARIABLE_MAP`.
267
+ url : str, optional
268
+ Full API endpoint URL. If not specified, the PSM4 GOES TMY v4 URL is
269
+ used.
270
+ timeout : int, default 30
271
+ time in seconds to wait for server response before timeout
272
+
273
+ Returns
274
+ -------
275
+ data : pandas.DataFrame
276
+ timeseries data from NREL PSM4
277
+ metadata : dict
278
+ metadata from NREL PSM4 about the record, see
279
+ :func:`pvlib.iotools.parse_nsrdb_psm4` for fields
280
+
281
+ Raises
282
+ ------
283
+ requests.HTTPError
284
+ if the request response status is not ok, then the ``'errors'`` field
285
+ from the JSON response or any error message in the content will be
286
+ raised as an exception, for example if the `api_key` was rejected or if
287
+ the coordinates were not found in the NSRDB
288
+
289
+ Notes
290
+ -----
291
+ The required NREL developer key, `api_key`, is available for free by
292
+ registering at the `NREL Developer Network <https://developer.nrel.gov/>`_.
293
+
294
+ .. warning:: The "DEMO_KEY" `api_key` is severely rate limited and may
295
+ result in rejected requests.
296
+
297
+ .. warning:: PSM4 is limited to data found in the NSRDB, please consult
298
+ the references below for locations with available data. Additionally,
299
+ querying data with < 30-minute resolution uses a different API endpoint
300
+ with fewer available fields (see [4]_).
301
+
302
+ See Also
303
+ --------
304
+ pvlib.iotools.get_nsrdb_psm4_aggregated,
305
+ pvlib.iotools.get_nsrdb_psm4_conus, pvlib.iotools.get_nsrdb_psm4_full_disc,
306
+ pvlib.iotools.read_nsrdb_psm4,pvlib.iotools.parse_nsrdb_psm4
307
+
308
+ References
309
+ ----------
310
+
311
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
312
+ <https://nsrdb.nrel.gov/>`_
313
+ .. [2] `NSRDB GOES Tmy V4.0.0
314
+ <https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-tmy-v4-0-0-download/>`_
315
+ """
316
+ # The well know text (WKT) representation of geometry notation is strict.
317
+ # A POINT object is a string with longitude first, then the latitude, with
318
+ # four decimals each, and exactly one space between them.
319
+ longitude = ('%9.4f' % longitude).strip()
320
+ latitude = ('%8.4f' % latitude).strip()
321
+ # TODO: make format_WKT(object_type, *args) in tools.py
322
+
323
+ # convert to string to accomodate integer years being passed in
324
+ year = str(year)
325
+
326
+ # convert pvlib names in parameters to PSM4 convention
327
+ parameters = [REQUEST_VARIABLE_MAP.get(a, a) for a in parameters]
328
+
329
+ # required query-string parameters for request to PSM4 API
330
+ params = {
331
+ 'api_key': api_key,
332
+ 'full_name': full_name,
333
+ 'email': email,
334
+ 'affiliation': affiliation,
335
+ 'reason': PVLIB_PYTHON,
336
+ 'mailing_list': 'false',
337
+ 'wkt': 'POINT(%s %s)' % (longitude, latitude),
338
+ 'names': year,
339
+ 'attributes': ','.join(parameters),
340
+ 'leap_day': str(leap_day).lower(),
341
+ 'utc': str(utc).lower(),
342
+ 'interval': time_step
343
+ }
344
+ # request CSV download from NREL PSM4
345
+ if url is None:
346
+ url = PSM4_TMY_URL
347
+
348
+ response = requests.get(url, params=params, timeout=timeout)
349
+ if not response.ok:
350
+ # if the API key is rejected, then the response status will be 403
351
+ # Forbidden, and then the error is in the content and there is no JSON
352
+ try:
353
+ errors = response.json()['errors']
354
+ except JSONDecodeError:
355
+ errors = response.content.decode('utf-8')
356
+ raise requests.HTTPError(errors, response=response)
357
+ # the CSV is in the response content as a UTF-8 bytestring
358
+ # to use pandas we need to create a file buffer from the response
359
+ fbuf = io.StringIO(response.content.decode('utf-8'))
360
+ return parse_nsrdb_psm4(fbuf, map_variables)
361
+
362
+
363
+ def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
364
+ time_step=60, parameters=PARAMETERS, leap_day=True,
365
+ full_name=PVLIB_PYTHON, affiliation=PVLIB_PYTHON,
366
+ utc=False, map_variables=True, url=None, timeout=30):
367
+ """
368
+ Retrieve NSRDB PSM4 timeseries weather data from the PSM4 NSRDB GOES CONUS
369
+ v4 API.
370
+
371
+ The NSRDB is described in [1]_ and the PSM4 NSRDB GOES CONUS v4 API is
372
+ described in [2]_,.
373
+
374
+ Parameters
375
+ ----------
376
+ latitude : float or int
377
+ in decimal degrees, between -90 and 90, north is positive
378
+ longitude : float or int
379
+ in decimal degrees, between -180 and 180, east is positive
380
+ api_key : str
381
+ NREL Developer Network API key
382
+ email : str
383
+ NREL API uses this to automatically communicate messages back
384
+ to the user only if necessary
385
+ year : str, default '2023'
386
+ PSM4 API parameter specifing year (e.g. ``2023``) to download. The
387
+ allowed values update periodically, so consult the NSRDB reference
388
+ below for the current set of options. Called ``names`` in NSRDB API.
389
+ time_step : int, {60, 5, 15, 30}
390
+ time step in minutes. Called ``interval`` in NSRDB API.
391
+ parameters : list of str, optional
392
+ meteorological fields to fetch. If not specified, defaults to
393
+ ``pvlib.iotools.psm4.PARAMETERS``. See reference [2]_ for a list of
394
+ available fields. Alternatively, pvlib names may also be used (e.g.
395
+ 'ghi' rather than 'GHI'); see :const:`REQUEST_VARIABLE_MAP`. To
396
+ retrieve all available fields, set ``parameters=[]``.
397
+ leap_day : bool, default : True
398
+ include leap day in the results
399
+ full_name : str, default 'pvlib python'
400
+ optional
401
+ affiliation : str, default 'pvlib python'
402
+ optional
403
+ utc: bool, default : False
404
+ retrieve data with timestamps converted to UTC. False returns
405
+ timestamps in local standard time of the selected location
406
+ map_variables : bool, default True
407
+ When true, renames columns of the Dataframe to pvlib variable names
408
+ where applicable. See variable :const:`VARIABLE_MAP`.
409
+ url : str, optional
410
+ Full API endpoint URL. If not specified, the PSM4 GOES CONUS v4 URL is
411
+ used.
412
+ timeout : int, default 30
413
+ time in seconds to wait for server response before timeout
414
+
415
+ Returns
416
+ -------
417
+ data : pandas.DataFrame
418
+ timeseries data from NREL PSM4
419
+ metadata : dict
420
+ metadata from NREL PSM4 about the record, see
421
+ :func:`pvlib.iotools.parse_nsrdb_psm4` for fields
422
+
423
+ Raises
424
+ ------
425
+ requests.HTTPError
426
+ if the request response status is not ok, then the ``'errors'`` field
427
+ from the JSON response or any error message in the content will be
428
+ raised as an exception, for example if the `api_key` was rejected or if
429
+ the coordinates were not found in the NSRDB
430
+
431
+ Notes
432
+ -----
433
+ The required NREL developer key, `api_key`, is available for free by
434
+ registering at the `NREL Developer Network <https://developer.nrel.gov/>`_.
435
+
436
+ .. warning:: The "DEMO_KEY" `api_key` is severely rate limited and may
437
+ result in rejected requests.
438
+
439
+ .. warning:: PSM4 is limited to data found in the NSRDB, please consult
440
+ the references below for locations with available data. Additionally,
441
+ querying data with < 30-minute resolution uses a different API endpoint
442
+ with fewer available fields (see [4]_).
443
+
444
+ See Also
445
+ --------
446
+ pvlib.iotools.get_nsrdb_psm4_aggregated,
447
+ pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_full_disc,
448
+ pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.parse_nsrdb_psm4
449
+
450
+ References
451
+ ----------
452
+
453
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
454
+ <https://nsrdb.nrel.gov/>`_
455
+ .. [2] `NSRDB GOES Conus V4.0.0
456
+ <https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-conus-v4-0-0-download/>`_
457
+ """
458
+ # The well know text (WKT) representation of geometry notation is strict.
459
+ # A POINT object is a string with longitude first, then the latitude, with
460
+ # four decimals each, and exactly one space between them.
461
+ longitude = ('%9.4f' % longitude).strip()
462
+ latitude = ('%8.4f' % latitude).strip()
463
+ # TODO: make format_WKT(object_type, *args) in tools.py
464
+
465
+ # convert to string to accomodate integer years being passed in
466
+ year = str(year)
467
+
468
+ # convert pvlib names in parameters to PSM4 convention
469
+ parameters = [REQUEST_VARIABLE_MAP.get(a, a) for a in parameters]
470
+
471
+ # required query-string parameters for request to PSM4 API
472
+ params = {
473
+ 'api_key': api_key,
474
+ 'full_name': full_name,
475
+ 'email': email,
476
+ 'affiliation': affiliation,
477
+ 'reason': PVLIB_PYTHON,
478
+ 'mailing_list': 'false',
479
+ 'wkt': 'POINT(%s %s)' % (longitude, latitude),
480
+ 'names': year,
481
+ 'attributes': ','.join(parameters),
482
+ 'leap_day': str(leap_day).lower(),
483
+ 'utc': str(utc).lower(),
484
+ 'interval': time_step
485
+ }
486
+ # request CSV download from NREL PSM4
487
+ if url is None:
488
+ url = PSM4_CON_URL
489
+
490
+ response = requests.get(url, params=params, timeout=timeout)
491
+ if not response.ok:
492
+ # if the API key is rejected, then the response status will be 403
493
+ # Forbidden, and then the error is in the content and there is no JSON
494
+ try:
495
+ errors = response.json()['errors']
496
+ except JSONDecodeError:
497
+ errors = response.content.decode('utf-8')
498
+ raise requests.HTTPError(errors, response=response)
499
+ # the CSV is in the response content as a UTF-8 bytestring
500
+ # to use pandas we need to create a file buffer from the response
501
+ fbuf = io.StringIO(response.content.decode('utf-8'))
502
+ return parse_nsrdb_psm4(fbuf, map_variables)
503
+
504
+
505
+ def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
506
+ year='2023', time_step=60,
507
+ parameters=PARAMETERS, leap_day=True,
508
+ full_name=PVLIB_PYTHON,
509
+ affiliation=PVLIB_PYTHON, utc=False,
510
+ map_variables=True, url=None, timeout=30):
511
+ """
512
+ Retrieve NSRDB PSM4 timeseries weather data from the PSM4 NSRDB GOES Full
513
+ Disc v4 API.
514
+
515
+ The NSRDB is described in [1]_ and the PSM4 NSRDB GOES Full Disc v4 API is
516
+ described in [2]_,.
517
+
518
+ Parameters
519
+ ----------
520
+ latitude : float or int
521
+ in decimal degrees, between -90 and 90, north is positive
522
+ longitude : float or int
523
+ in decimal degrees, between -180 and 180, east is positive
524
+ api_key : str
525
+ NREL Developer Network API key
526
+ email : str
527
+ NREL API uses this to automatically communicate messages back
528
+ to the user only if necessary
529
+ year : str, default '2023'
530
+ PSM4 API parameter specifing year (e.g. ``2023``) to download. The
531
+ allowed values update periodically, so consult the NSRDB reference
532
+ below for the current set of options. Called ``names`` in NSRDB API.
533
+ time_step : int, {60, 10, 30}
534
+ time step in minutes, must be 10, 30 or 60. Called ``interval`` in
535
+ NSRDB API.
536
+ parameters : list of str, optional
537
+ meteorological fields to fetch. If not specified, defaults to
538
+ ``pvlib.iotools.psm4.PARAMETERS``. See reference [2]_ for a list of
539
+ available fields. Alternatively, pvlib names may also be used (e.g.
540
+ 'ghi' rather than 'GHI'); see :const:`REQUEST_VARIABLE_MAP`. To
541
+ retrieve all available fields, set ``parameters=[]``.
542
+ leap_day : bool, default : True
543
+ include leap day in the results
544
+ full_name : str, default 'pvlib python'
545
+ optional
546
+ affiliation : str, default 'pvlib python'
547
+ optional
548
+ utc: bool, default : False
549
+ retrieve data with timestamps converted to UTC. False returns
550
+ timestamps in local standard time of the selected location
551
+ map_variables : bool, default True
552
+ When true, renames columns of the Dataframe to pvlib variable names
553
+ where applicable. See variable :const:`VARIABLE_MAP`.
554
+ url : str, optional
555
+ Full API endpoint URL. If not specified, the PSM4 GOES Full Disc v4
556
+ URL is used.
557
+ timeout : int, default 30
558
+ time in seconds to wait for server response before timeout
559
+
560
+ Returns
561
+ -------
562
+ data : pandas.DataFrame
563
+ timeseries data from NREL PSM4
564
+ metadata : dict
565
+ metadata from NREL PSM4 about the record, see
566
+ :func:`pvlib.iotools.parse_nsrdb_psm4` for fields
567
+
568
+ Raises
569
+ ------
570
+ requests.HTTPError
571
+ if the request response status is not ok, then the ``'errors'`` field
572
+ from the JSON response or any error message in the content will be
573
+ raised as an exception, for example if the `api_key` was rejected or if
574
+ the coordinates were not found in the NSRDB
575
+
576
+ Notes
577
+ -----
578
+ The required NREL developer key, `api_key`, is available for free by
579
+ registering at the `NREL Developer Network <https://developer.nrel.gov/>`_.
580
+
581
+ .. warning:: The "DEMO_KEY" `api_key` is severely rate limited and may
582
+ result in rejected requests.
583
+
584
+ .. warning:: PSM4 is limited to data found in the NSRDB, please consult
585
+ the references below for locations with available data. Additionally,
586
+ querying data with < 30-minute resolution uses a different API endpoint
587
+ with fewer available fields (see [4]_).
588
+
589
+ See Also
590
+ --------
591
+ pvlib.iotools.get_nsrdb_psm4_aggregated,
592
+ pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_conus,
593
+ pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.parse_nsrdb_psm4
594
+
595
+ References
596
+ ----------
597
+
598
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
599
+ <https://nsrdb.nrel.gov/>`_
600
+ .. [2] `NSRDB GOES Full Disc V4.0.0
601
+ <https://developer.nrel.gov/docs/solar/nsrdb/nsrdb-GOES-full-disc-v4-0-0-download/>`_
602
+ """
603
+ # The well know text (WKT) representation of geometry notation is strict.
604
+ # A POINT object is a string with longitude first, then the latitude, with
605
+ # four decimals each, and exactly one space between them.
606
+ longitude = ('%9.4f' % longitude).strip()
607
+ latitude = ('%8.4f' % latitude).strip()
608
+ # TODO: make format_WKT(object_type, *args) in tools.py
609
+
610
+ # convert to string to accomodate integer years being passed in
611
+ year = str(year)
612
+
613
+ # convert pvlib names in parameters to PSM4 convention
614
+ parameters = [REQUEST_VARIABLE_MAP.get(a, a) for a in parameters]
615
+
616
+ # required query-string parameters for request to PSM4 API
617
+ params = {
618
+ 'api_key': api_key,
619
+ 'full_name': full_name,
620
+ 'email': email,
621
+ 'affiliation': affiliation,
622
+ 'reason': PVLIB_PYTHON,
623
+ 'mailing_list': 'false',
624
+ 'wkt': 'POINT(%s %s)' % (longitude, latitude),
625
+ 'names': year,
626
+ 'attributes': ','.join(parameters),
627
+ 'leap_day': str(leap_day).lower(),
628
+ 'utc': str(utc).lower(),
629
+ 'interval': time_step
630
+ }
631
+ # request CSV download from NREL PSM4
632
+ if url is None:
633
+ url = PSM4_FUL_URL
634
+
635
+ response = requests.get(url, params=params, timeout=timeout)
636
+ if not response.ok:
637
+ # if the API key is rejected, then the response status will be 403
638
+ # Forbidden, and then the error is in the content and there is no JSON
639
+ try:
640
+ errors = response.json()['errors']
641
+ except JSONDecodeError:
642
+ errors = response.content.decode('utf-8')
643
+ raise requests.HTTPError(errors, response=response)
644
+ # the CSV is in the response content as a UTF-8 bytestring
645
+ # to use pandas we need to create a file buffer from the response
646
+ fbuf = io.StringIO(response.content.decode('utf-8'))
647
+ return parse_nsrdb_psm4(fbuf, map_variables)
648
+
649
+
650
+ def parse_nsrdb_psm4(fbuf, map_variables=True):
651
+ """
652
+ Parse an NSRDB PSM4 weather file (formatted as SAM CSV).
653
+
654
+ The NSRDB is described in [1]_ and the SAM CSV format is described in [2]_.
655
+
656
+ Parameters
657
+ ----------
658
+ fbuf: file-like object
659
+ File-like object containing data to read.
660
+ map_variables: bool, default True
661
+ When true, renames columns of the Dataframe to pvlib variable names
662
+ where applicable. See variable :const:`VARIABLE_MAP`.
663
+
664
+ Returns
665
+ -------
666
+ data : pandas.DataFrame
667
+ timeseries data from NREL PSM4
668
+ metadata : dict
669
+ metadata from NREL PSM4 about the record, see notes for fields
670
+
671
+ Notes
672
+ -----
673
+ The return is a tuple with two items. The first item is a dataframe with
674
+ the PSM4 timeseries data.
675
+
676
+ The second item is a dictionary with metadata from NREL PSM4 about the
677
+ record containing the following fields:
678
+
679
+ * Source
680
+ * Location ID
681
+ * City
682
+ * State
683
+ * Country
684
+ * Latitude
685
+ * Longitude
686
+ * Time Zone
687
+ * Elevation
688
+ * Local Time Zone
689
+ * Clearsky DHI Units
690
+ * Clearsky DNI Units
691
+ * Clearsky GHI Units
692
+ * Dew Point Units
693
+ * DHI Units
694
+ * DNI Units
695
+ * GHI Units
696
+ * Solar Zenith Angle Units
697
+ * Temperature Units
698
+ * Pressure Units
699
+ * Relative Humidity Units
700
+ * Precipitable Water Units
701
+ * Wind Direction Units
702
+ * Wind Speed Units
703
+ * Cloud Type -15
704
+ * Cloud Type 0
705
+ * Cloud Type 1
706
+ * Cloud Type 2
707
+ * Cloud Type 3
708
+ * Cloud Type 4
709
+ * Cloud Type 5
710
+ * Cloud Type 6
711
+ * Cloud Type 7
712
+ * Cloud Type 8
713
+ * Cloud Type 9
714
+ * Cloud Type 10
715
+ * Cloud Type 11
716
+ * Cloud Type 12
717
+ * Fill Flag 0
718
+ * Fill Flag 1
719
+ * Fill Flag 2
720
+ * Fill Flag 3
721
+ * Fill Flag 4
722
+ * Fill Flag 5
723
+ * Surface Albedo Units
724
+ * Version
725
+
726
+ Examples
727
+ --------
728
+ >>> # Read a local PSM4 file:
729
+ >>> with open(filename, 'r') as f: # doctest: +SKIP
730
+ ... df, metadata = iotools.parse_nsrdb_psm4(f) # doctest: +SKIP
731
+
732
+ See Also
733
+ --------
734
+ pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.get_psm4
735
+
736
+ References
737
+ ----------
738
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
739
+ <https://nsrdb.nrel.gov/>`_
740
+ .. [2] `Standard Time Series Data File Format
741
+ <https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
742
+ """
743
+ # The first 2 lines of the response are headers with metadata
744
+ metadata_fields = fbuf.readline().split(',')
745
+ metadata_fields[-1] = metadata_fields[-1].strip() # strip trailing newline
746
+ metadata_values = fbuf.readline().split(',')
747
+ metadata_values[-1] = metadata_values[-1].strip() # strip trailing newline
748
+ metadata = dict(zip(metadata_fields, metadata_values))
749
+ # the response is all strings, so set some metadata types to numbers
750
+ metadata['Local Time Zone'] = int(metadata['Local Time Zone'])
751
+ metadata['Time Zone'] = int(metadata['Time Zone'])
752
+ metadata['Latitude'] = float(metadata['Latitude'])
753
+ metadata['Longitude'] = float(metadata['Longitude'])
754
+ metadata['Elevation'] = int(metadata['Elevation'])
755
+ # get the column names so we can set the dtypes
756
+ columns = fbuf.readline().split(',')
757
+ columns[-1] = columns[-1].strip() # strip trailing newline
758
+ # Since the header has so many columns, excel saves blank cols in the
759
+ # data below the header lines.
760
+ columns = [col for col in columns if col != '']
761
+ dtypes = dict.fromkeys(columns, float) # all floats except datevec
762
+ dtypes.update(Year=int, Month=int, Day=int, Hour=int, Minute=int)
763
+ dtypes['Cloud Type'] = int
764
+ dtypes['Fill Flag'] = int
765
+ data = pd.read_csv(
766
+ fbuf, header=None, names=columns, usecols=columns, dtype=dtypes,
767
+ delimiter=',', lineterminator='\n') # skip carriage returns \r
768
+ # the response 1st 5 columns are a date vector, convert to datetime
769
+ dtidx = pd.to_datetime(
770
+ data[['Year', 'Month', 'Day', 'Hour', 'Minute']])
771
+ # in USA all timezones are integers
772
+ tz = 'Etc/GMT%+d' % -metadata['Time Zone']
773
+ data.index = pd.DatetimeIndex(dtidx).tz_localize(tz)
774
+
775
+ if map_variables:
776
+ data = data.rename(columns=VARIABLE_MAP)
777
+ metadata['latitude'] = metadata.pop('Latitude')
778
+ metadata['longitude'] = metadata.pop('Longitude')
779
+ metadata['altitude'] = metadata.pop('Elevation')
780
+
781
+ return data, metadata
782
+
783
+
784
+ def read_nsrdb_psm4(filename, map_variables=True):
785
+ """
786
+ Read an NSRDB PSM4 weather file (formatted as SAM CSV).
787
+
788
+ The NSRDB is described in [1]_ and the SAM CSV format is described in [2]_.
789
+
790
+ Parameters
791
+ ----------
792
+ filename: str or path-like
793
+ Filename of a file containing data to read.
794
+ map_variables: bool, default True
795
+ When true, renames columns of the Dataframe to pvlib variable names
796
+ where applicable. See variable :const:`VARIABLE_MAP`.
797
+
798
+ Returns
799
+ -------
800
+ data : pandas.DataFrame
801
+ timeseries data from NREL PSM4
802
+ metadata : dict
803
+ metadata from NREL PSM4 about the record, see
804
+ :func:`pvlib.iotools.parse_nsrdb_psm4` for fields
805
+
806
+ See Also
807
+ --------
808
+ pvlib.iotools.parse_nsrdb_psm4, pvlib.iotools.get_psm4
809
+
810
+ References
811
+ ----------
812
+ .. [1] `NREL National Solar Radiation Database (NSRDB)
813
+ <https://nsrdb.nrel.gov/>`_
814
+ .. [2] `Standard Time Series Data File Format
815
+ <https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
816
+ """
817
+ with open(str(filename), 'r') as fbuf:
818
+ content = parse_nsrdb_psm4(fbuf, map_variables)
819
+ return content