Skip to content

ENH: Create psm3.py #694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ relevant to solar energy modeling.
iotools.get_ecmwf_macc
iotools.read_crn
iotools.read_solrad
iotools.get_psm3

A :py:class:`~pvlib.location.Location` object may be created from metadata
in some files.
Expand Down
1 change: 1 addition & 0 deletions docs/sphinx/source/whatsnew/v0.6.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Enhancements
* Add US CRN data reader to :ref:`iotools`.
* Add SOLRAD data reader to :ref:`iotools`.
* Add EPW data reader to :ref:`iotools`. (:issue:`591`)
* Add PSM3 reader to :ref:`iotools`. (:issue:`592`)

Bug fixes
~~~~~~~~~
Expand Down
8,761 changes: 8,761 additions & 0 deletions pvlib/data/test_psm3.csv

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pvlib/iotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from pvlib.iotools.ecmwf_macc import get_ecmwf_macc # noqa: F401
from pvlib.iotools.crn import read_crn # noqa: F401
from pvlib.iotools.solrad import read_solrad # noqa: F401
from pvlib.iotools.psm3 import get_psm3 # noqa: F401
148 changes: 148 additions & 0 deletions pvlib/iotools/psm3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""
Get PSM3 TMY
see https://developer.nrel.gov/docs/solar/nsrdb/psm3_data_download/
"""

import io
import requests
import pandas as pd

URL = "http://developer.nrel.gov/api/solar/nsrdb_psm3_download.csv"

# 'relative_humidity', 'total_precipitable_water' are not available
ATTRIBUTES = [
'air_temperature', 'dew_point', 'dhi', 'dni', 'ghi', 'surface_albedo',
'surface_pressure', 'wind_direction', 'wind_speed']


def get_psm3(latitude, longitude, names='tmy', interval=60,
api_key='DEMO_KEY'):
"""
Get PSM3 data

Parameters
----------
latitude : float or int
in decimal degrees, between -90 and 90, north is positive
longitude : float or int
in decimal degrees, between -180 and 180, east is positive
names : str
PSM3 API parameter specifing year or TMY variant to download, see notes
below for options, default: ``'tmy'``
interval : int
interval size in minutes, can be only either 30 or 60, default: 60
api_key : str
optional, supply your NREL Developer Network API key

Returns
-------
headers : dict
metadata from NREL PSM3 about the record, see notes for fields
data : pandas.DataFrame
timeseries data from NREL PSM3

Raises
------
requests.HTTPError
if the request return status is not ok then the ``'errors'`` from the
JSON response will be returned as an exception

Notes
-----
The PSM3 API `names` parameter must be a single value from the following
list::

['1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005',
'2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013',
'2014', '2015', '2016', '2017', 'tmy', 'tmy-2016', 'tmy-2017',
'tdy-2017', 'tgy-2017']

The return is a tuple with two items. The first item is a header with
metadata from NREL PSM3 about the record containing the following fields:

* Source
* Location ID
* City
* State
* Country
* Latitude
* Longitude
* Time Zone
* Elevation
* Local Time Zone
* Dew Point Units
* DHI Units
* DNI Units
* GHI Units
* Temperature Units
* Pressure Units
* Wind Direction Units
* Wind Speed
* Surface Albedo Units
* Version

The second item is a dataframe with the timeseries data downloaded.

See Also
--------
pvlib.iotools.read_tmy2, pvlib.iotools.read_tmy3

References
----------

* `NREL Developer Network - Physical Solar Model (PSM) v3
<https://developer.nrel.gov/docs/solar/nsrdb/psm3_data_download/>`_
* `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_

"""
longitude = ('%9.4f' % longitude).strip()
latitude = ('%8.4f' % latitude).strip()
params = {
'api_key': api_key,
'full_name': 'Sample User',
'email': 'sample@email.com',
'affiliation': 'Test Organization',
'reason': 'Example',
'mailing_list': 'true',
'wkt': 'POINT(%s %s)' % (longitude, latitude),
'names': names,
'attributes': ','.join(ATTRIBUTES),
'leap_day': 'false',
'utc': 'false',
'interval': interval
}
# request CSV download from NREL PSM3
response = requests.get(URL, params=params)
if not response.ok:
raise requests.HTTPError(response.json()['errors'])
# the CSV is in the response content as a UTF-8 bytestring
# to use pandas we need to create a file buffer from the response
fbuf = io.StringIO(response.content.decode('utf-8'))
# The first 2 lines of the response are headers with metadat
header_fields = fbuf.readline().split(',')
header_fields[-1] = header_fields[-1].strip() # strip trailing newline
header_values = fbuf.readline().split(',')
header_values[-1] = header_values[-1].strip() # strip trailing newline
header = dict(zip(header_fields, header_values))
# the response is all strings, so set some header types to numbers
header['Local Time Zone'] = int(header['Local Time Zone'])
header['Time Zone'] = int(header['Time Zone'])
header['Latitude'] = float(header['Latitude'])
header['Longitude'] = float(header['Longitude'])
header['Elevation'] = int(header['Elevation'])
# get the column names so we can set the dtypes
columns = fbuf.readline().split(',')
columns[-1] = columns[-1].strip() # strip trailing newline
dtypes = dict.fromkeys(columns, float) # all floats except datevec
dtypes.update(Year=int, Month=int, Day=int, Hour=int, Minute=int)
data = pd.read_csv(
fbuf, header=None, names=columns, dtype=dtypes,
delimiter=',', lineterminator='\n') # skip carriage returns \r
# the response 1st 5 columns are a date vector, convert to datetime
dtidx = pd.to_datetime(
data[['Year', 'Month', 'Day', 'Hour', 'Minute']])
# in USA all timezones are intergers
tz = 'Etc/GMT%+d' % -header['Time Zone']
data.index = pd.DatetimeIndex(dtidx).tz_localize(tz)
return header, data
4 changes: 4 additions & 0 deletions pvlib/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ def pandas_0_22():
return parse_version(pd.__version__) >= parse_version('0.22.0')


needs_pandas_0_22 = pytest.mark.skipif(
not pandas_0_22(), reason='requires pandas 0.22 or greater')


def has_spa_c():
try:
from pvlib.spa_c_files.spa_py import spa_calc
Expand Down
54 changes: 54 additions & 0 deletions pvlib/test/test_psm3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
test iotools for PSM3
"""

import os
from pvlib.iotools import psm3
from conftest import needs_pandas_0_22
import numpy as np
import pandas as pd
import pytest
from requests import HTTPError

BASEDIR = os.path.abspath(os.path.dirname(__file__))
PROJDIR = os.path.dirname(BASEDIR)
DATADIR = os.path.join(PROJDIR, 'data')
TEST_DATA = os.path.join(DATADIR, 'test_psm3.csv')
LATITUDE, LONGITUDE = 40.5137, -108.5449
HEADER_FIELDS = [
'Source', 'Location ID', 'City', 'State', 'Country', 'Latitude',
'Longitude', 'Time Zone', 'Elevation', 'Local Time Zone',
'Dew Point Units', 'DHI Units', 'DNI Units', 'GHI Units',
'Temperature Units', 'Pressure Units', 'Wind Direction Units',
'Wind Speed', 'Surface Albedo Units', 'Version']


@needs_pandas_0_22
def test_get_psm3():
"""test get_psm3"""
header, data = psm3.get_psm3(LATITUDE, LONGITUDE)
expected = pd.read_csv(TEST_DATA)
# check datevec columns
assert np.allclose(data.Year, expected.Year)
assert np.allclose(data.Month, expected.Month)
assert np.allclose(data.Day, expected.Day)
assert np.allclose(data.Hour, expected.Hour)
assert np.allclose(data.Minute, expected.Minute)
# check data columns
assert np.allclose(data.GHI, expected.GHI)
assert np.allclose(data.DNI, expected.DNI)
assert np.allclose(data.DHI, expected.DHI)
assert np.allclose(data.Temperature, expected.Temperature)
assert np.allclose(data.Pressure, expected.Pressure)
assert np.allclose(data['Dew Point'], expected['Dew Point'])
assert np.allclose(data['Surface Albedo'], expected['Surface Albedo'])
assert np.allclose(data['Wind Speed'], expected['Wind Speed'])
assert np.allclose(data['Wind Direction'], expected['Wind Direction'])
# check header
for hf in HEADER_FIELDS:
assert hf in header
# check timezone
assert (data.index.tzinfo.zone == 'Etc/GMT%+d' % -header['Time Zone'])
# check error
with pytest.raises(HTTPError):
psm3.get_psm3(LATITUDE, LONGITUDE, 'bad')