Skip to content

Commit 1893b20

Browse files
Add variable mapping of psm3 (#1374)
* Add variable mapping of psm3 * Add enhancement entry in whatsnew * Fix stickler * Map keys in metadata dict * Remove double spaces in docs * Fix stickler * Doc update Co-authored-by: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> * Reformatting - changes by kanderso-nrel * Update docstring table with 2020 * Add deprecation warning test coverage * Rename to VARIABLE_MAP * Change apparent_zenith to solar_zenith Based on the decision in #1403 * Update attributes docstring * Change elevation to altitude when mapping variables * Update psm3 variable mapping test Co-authored-by: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com>
1 parent df1c56e commit 1893b20

File tree

3 files changed

+115
-22
lines changed

3 files changed

+115
-22
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Deprecations
1919

2020
Enhancements
2121
~~~~~~~~~~~~
22+
* Added ``map_variables`` option to :py:func:`pvlib.iotools.get_psm3` and
23+
:py:func:`pvlib.iotools.read_psm3` (:pull:`1374`)
2224
* Added `pvlib.bifacial.infinite_sheds`, containing a model for irradiance
2325
on front and back surfaces of bifacial arrays. (:pull:`717`)
2426
* Added ``map_variables`` option to :func:`~pvlib.iotools.read_crn` (:pull:`1368`)

pvlib/iotools/psm3.py

+65-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
"""
32
Get PSM3 TMY
43
see https://developer.nrel.gov/docs/solar/nsrdb/psm3_data_download/
@@ -8,6 +7,8 @@
87
import requests
98
import pandas as pd
109
from json import JSONDecodeError
10+
import warnings
11+
from pvlib._deprecation import pvlibDeprecationWarning
1112

1213
NSRDB_API_BASE = "https://developer.nrel.gov"
1314
PSM_URL = NSRDB_API_BASE + "/api/nsrdb/v2/solar/psm3-download.csv"
@@ -20,12 +21,31 @@
2021
'surface_pressure', 'wind_direction', 'wind_speed')
2122
PVLIB_PYTHON = 'pvlib python'
2223

24+
# Dictionary mapping PSM3 names to pvlib names
25+
VARIABLE_MAP = {
26+
'GHI': 'ghi',
27+
'DHI': 'dhi',
28+
'DNI': 'dni',
29+
'Clearsky GHI': 'ghi_clear',
30+
'Clearsky DHI': 'dhi_clear',
31+
'Clearsky DNI': 'dni_clear',
32+
'Solar Zenith Angle': 'solar_zenith',
33+
'Temperature': 'temp_air',
34+
'Relative Humidity': 'relative_humidity',
35+
'Dew point': 'temp_dew',
36+
'Pressure': 'pressure',
37+
'Wind Direction': 'wind_direction',
38+
'Wind Speed': 'wind_speed',
39+
'Surface Albedo': 'albedo',
40+
'Precipitable Water': 'precipitable_water',
41+
}
42+
2343

2444
def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
2545
attributes=ATTRIBUTES, leap_day=False, full_name=PVLIB_PYTHON,
26-
affiliation=PVLIB_PYTHON, timeout=30):
46+
affiliation=PVLIB_PYTHON, map_variables=None, timeout=30):
2747
"""
28-
Retrieve NSRDB PSM3 timeseries weather data from the PSM3 API. The NSRDB
48+
Retrieve NSRDB PSM3 timeseries weather data from the PSM3 API. The NSRDB
2949
is described in [1]_ and the PSM3 API is described in [2]_, [3]_, and [4]_.
3050
3151
.. versionchanged:: 0.9.0
@@ -48,19 +68,23 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
4868
PSM3 API parameter specifing year or TMY variant to download, see notes
4969
below for options
5070
interval : int, {60, 5, 15, 30}
51-
interval size in minutes, must be 5, 15, 30 or 60. Only used for
71+
interval size in minutes, must be 5, 15, 30 or 60. Only used for
5272
single-year requests (i.e., it is ignored for tmy/tgy/tdy requests).
5373
attributes : list of str, optional
5474
meteorological fields to fetch. If not specified, defaults to
5575
``pvlib.iotools.psm3.ATTRIBUTES``. See references [2]_, [3]_, and [4]_
56-
for lists of available fields.
76+
for lists of available fields. Alternatively, pvlib names may also be
77+
used (e.g. 'ghi' rather than 'GHI'); see :const:`VARIABLE_MAP`.
5778
leap_day : boolean, default False
58-
include leap day in the results. Only used for single-year requests
79+
include leap day in the results. Only used for single-year requests
5980
(i.e., it is ignored for tmy/tgy/tdy requests).
6081
full_name : str, default 'pvlib python'
6182
optional
6283
affiliation : str, default 'pvlib python'
6384
optional
85+
map_variables: boolean, optional
86+
When true, renames columns of the Dataframe to pvlib variable names
87+
where applicable. See variable :const:`VARIABLE_MAP`.
6488
timeout : int, default 30
6589
time in seconds to wait for server response before timeout
6690
@@ -96,14 +120,15 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
96120
+===========+=============================================================+
97121
| Year | 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, |
98122
| | 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, |
99-
| | 2018, 2019 |
123+
| | 2018, 2019, 2020 |
100124
+-----------+-------------------------------------------------------------+
101125
| TMY | tmy, tmy-2016, tmy-2017, tdy-2017, tgy-2017, |
102126
| | tmy-2018, tdy-2018, tgy-2018, tmy-2019, tdy-2019, tgy-2019 |
127+
| | tmy-2020, tdy-2020, tgy-2020 |
103128
+-----------+-------------------------------------------------------------+
104129
105130
.. warning:: PSM3 is limited to data found in the NSRDB, please consult the
106-
references below for locations with available data. Additionally,
131+
references below for locations with available data. Additionally,
107132
querying data with < 30-minute resolution uses a different API endpoint
108133
with fewer available fields (see [4]_).
109134
@@ -133,6 +158,13 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
133158
# convert to string to accomodate integer years being passed in
134159
names = str(names)
135160

161+
# convert pvlib names in attributes to psm3 convention (reverse mapping)
162+
# unlike psm3 columns, attributes are lower case and with underscores
163+
amap = {value: key.lower().replace(' ', '_') for (key, value) in
164+
VARIABLE_MAP.items()}
165+
attributes = [amap.get(a, a) for a in attributes]
166+
attributes = list(set(attributes)) # remove duplicate values
167+
136168
# required query-string parameters for request to PSM3 API
137169
params = {
138170
'api_key': api_key,
@@ -167,12 +199,12 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
167199
# the CSV is in the response content as a UTF-8 bytestring
168200
# to use pandas we need to create a file buffer from the response
169201
fbuf = io.StringIO(response.content.decode('utf-8'))
170-
return parse_psm3(fbuf)
202+
return parse_psm3(fbuf, map_variables)
171203

172204

173-
def parse_psm3(fbuf):
205+
def parse_psm3(fbuf, map_variables=None):
174206
"""
175-
Parse an NSRDB PSM3 weather file (formatted as SAM CSV). The NSRDB
207+
Parse an NSRDB PSM3 weather file (formatted as SAM CSV). The NSRDB
176208
is described in [1]_ and the SAM CSV format is described in [2]_.
177209
178210
.. versionchanged:: 0.9.0
@@ -184,6 +216,9 @@ def parse_psm3(fbuf):
184216
----------
185217
fbuf: file-like object
186218
File-like object containing data to read.
219+
map_variables: bool
220+
When true, renames columns of the Dataframe to pvlib variable names
221+
where applicable. See variable VARIABLE_MAP.
187222
188223
Returns
189224
-------
@@ -296,12 +331,25 @@ def parse_psm3(fbuf):
296331
tz = 'Etc/GMT%+d' % -metadata['Time Zone']
297332
data.index = pd.DatetimeIndex(dtidx).tz_localize(tz)
298333

334+
if map_variables is None:
335+
warnings.warn(
336+
'PSM3 variable names will be renamed to pvlib conventions by '
337+
'default starting in pvlib 0.11.0. Specify map_variables=True '
338+
'to enable that behavior now, or specify map_variables=False '
339+
'to hide this warning.', pvlibDeprecationWarning)
340+
map_variables = False
341+
if map_variables:
342+
data = data.rename(columns=VARIABLE_MAP)
343+
metadata['latitude'] = metadata.pop('Latitude')
344+
metadata['longitude'] = metadata.pop('Longitude')
345+
metadata['altitude'] = metadata.pop('Elevation')
346+
299347
return data, metadata
300348

301349

302-
def read_psm3(filename):
350+
def read_psm3(filename, map_variables=None):
303351
"""
304-
Read an NSRDB PSM3 weather file (formatted as SAM CSV). The NSRDB
352+
Read an NSRDB PSM3 weather file (formatted as SAM CSV). The NSRDB
305353
is described in [1]_ and the SAM CSV format is described in [2]_.
306354
307355
.. versionchanged:: 0.9.0
@@ -313,6 +361,9 @@ def read_psm3(filename):
313361
----------
314362
filename: str
315363
Filename of a file containing data to read.
364+
map_variables: bool
365+
When true, renames columns of the Dataframe to pvlib variable names
366+
where applicable. See variable VARIABLE_MAP.
316367
317368
Returns
318369
-------
@@ -334,5 +385,5 @@ def read_psm3(filename):
334385
<https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
335386
"""
336387
with open(str(filename), 'r') as fbuf:
337-
content = parse_psm3(fbuf)
388+
content = parse_psm3(fbuf, map_variables)
338389
return content

pvlib/tests/iotools/test_psm3.py

+48-8
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
import os
66
from pvlib.iotools import psm3
7-
from ..conftest import DATA_DIR, RERUNS, RERUNS_DELAY
7+
from ..conftest import DATA_DIR, RERUNS, RERUNS_DELAY, assert_index_equal
88
import numpy as np
99
import pandas as pd
1010
import pytest
1111
from requests import HTTPError
1212
from io import StringIO
1313
import warnings
14+
from pvlib._deprecation import pvlibDeprecationWarning
1415

1516
TMY_TEST_DATA = DATA_DIR / 'test_psm3_tmy-2017.csv'
1617
YEAR_TEST_DATA = DATA_DIR / 'test_psm3_2017.csv'
@@ -76,7 +77,8 @@ def assert_psm3_equal(data, metadata, expected):
7677
def test_get_psm3_tmy(nrel_api_key):
7778
"""test get_psm3 with a TMY"""
7879
data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key,
79-
PVLIB_EMAIL, names='tmy-2017')
80+
PVLIB_EMAIL, names='tmy-2017',
81+
map_variables=False)
8082
expected = pd.read_csv(TMY_TEST_DATA)
8183
assert_psm3_equal(data, metadata, expected)
8284

@@ -86,7 +88,8 @@ def test_get_psm3_tmy(nrel_api_key):
8688
def test_get_psm3_singleyear(nrel_api_key):
8789
"""test get_psm3 with a single year"""
8890
data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key,
89-
PVLIB_EMAIL, names='2017', interval=30)
91+
PVLIB_EMAIL, names='2017',
92+
map_variables=False, interval=30)
9093
expected = pd.read_csv(YEAR_TEST_DATA)
9194
assert_psm3_equal(data, metadata, expected)
9295

@@ -96,7 +99,8 @@ def test_get_psm3_singleyear(nrel_api_key):
9699
def test_get_psm3_5min(nrel_api_key):
97100
"""test get_psm3 for 5-minute data"""
98101
data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key,
99-
PVLIB_EMAIL, names='2019', interval=5)
102+
PVLIB_EMAIL, names='2019', interval=5,
103+
map_variables=False)
100104
assert len(data) == 525600/5
101105
first_day = data.loc['2019-01-01']
102106
expected = pd.read_csv(YEAR_TEST_DATA_5MIN)
@@ -108,7 +112,7 @@ def test_get_psm3_5min(nrel_api_key):
108112
def test_get_psm3_check_leap_day(nrel_api_key):
109113
data_2012, _ = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key,
110114
PVLIB_EMAIL, names="2012", interval=60,
111-
leap_day=True)
115+
leap_day=True, map_variables=False)
112116
assert len(data_2012) == (8760 + 24)
113117

114118

@@ -133,7 +137,7 @@ def test_get_psm3_tmy_errors(
133137
"""
134138
with pytest.raises(HTTPError) as excinfo:
135139
psm3.get_psm3(latitude, longitude, api_key, PVLIB_EMAIL,
136-
names=names, interval=interval)
140+
names=names, interval=interval, map_variables=False)
137141
# ensure the HTTPError caught isn't due to overuse of the API key
138142
assert "OVER_RATE_LIMIT" not in str(excinfo.value)
139143

@@ -149,13 +153,49 @@ def io_input(request):
149153

150154
def test_parse_psm3(io_input):
151155
"""test parse_psm3"""
152-
data, metadata = psm3.parse_psm3(io_input)
156+
data, metadata = psm3.parse_psm3(io_input, map_variables=False)
153157
expected = pd.read_csv(YEAR_TEST_DATA)
154158
assert_psm3_equal(data, metadata, expected)
155159

156160

157161
def test_read_psm3():
158162
"""test read_psm3"""
159-
data, metadata = psm3.read_psm3(MANUAL_TEST_DATA)
163+
data, metadata = psm3.read_psm3(MANUAL_TEST_DATA, map_variables=False)
160164
expected = pd.read_csv(YEAR_TEST_DATA)
161165
assert_psm3_equal(data, metadata, expected)
166+
167+
168+
def test_read_psm3_map_variables():
169+
"""test read_psm3 map_variables=True"""
170+
data, metadata = psm3.read_psm3(MANUAL_TEST_DATA, map_variables=True)
171+
columns_mapped = ['Year', 'Month', 'Day', 'Hour', 'Minute', 'dhi', 'dni',
172+
'ghi', 'dhi_clear', 'dni_clear', 'ghi_clear',
173+
'Cloud Type', 'Dew Point', 'apparent_zenith',
174+
'Fill Flag', 'albedo', 'wind_speed',
175+
'precipitable_water', 'wind_direction',
176+
'relative_humidity', 'temp_air', 'pressure']
177+
data, metadata = psm3.read_psm3(MANUAL_TEST_DATA, map_variables=True)
178+
assert_index_equal(data.columns, pd.Index(columns_mapped))
179+
180+
181+
@pytest.mark.remote_data
182+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
183+
def test_get_psm3_attribute_mapping(nrel_api_key):
184+
"""Test that pvlib names can be passed in as attributes and get correctly
185+
reverse mapped to PSM3 names"""
186+
data, meta = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, PVLIB_EMAIL,
187+
names=2019, interval=60,
188+
attributes=['ghi', 'wind_speed'],
189+
map_variables=True)
190+
assert 'ghi' in data.columns
191+
assert 'wind_speed' in data.columns
192+
assert 'latitude' in meta.keys()
193+
assert 'longitude' in meta.keys()
194+
assert 'altitude' in meta.keys()
195+
196+
197+
@pytest.mark.remote_data
198+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
199+
def test_psm3_variable_map_deprecation_warning(nrel_api_key):
200+
with pytest.warns(pvlibDeprecationWarning, match='names will be renamed'):
201+
_ = psm3.read_psm3(MANUAL_TEST_DATA)

0 commit comments

Comments
 (0)