Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 0 additions & 1 deletion docs/sphinx/source/reference/iotools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ of sources and file formats relevant to solar energy modeling.
iotools.get_nsrdb_psm4_conus
iotools.get_nsrdb_psm4_full_disc
iotools.read_nsrdb_psm4
iotools.parse_nsrdb_psm4
iotools.get_psm3
iotools.read_psm3
iotools.parse_psm3
Expand Down
5 changes: 2 additions & 3 deletions docs/sphinx/source/whatsnew/v0.12.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ Enhancements
:py:func:`~pvlib.iotools.get_nsrdb_psm4_aggregated`,
:py:func:`~pvlib.iotools.get_nsrdb_psm4_tmy`,
:py:func:`~pvlib.iotools.get_nsrdb_psm4_conus`,
:py:func:`~pvlib.iotools.get_nsrdb_psm4_full_disc`,
:py:func:`~pvlib.iotools.read_nsrdb_psm4`, and
:py:func:`~pvlib.iotools.parse_nsrdb_psm4`. (:issue:`2326`, :pull:`2378`)
:py:func:`~pvlib.iotools.get_nsrdb_psm4_full_disc`, and
:py:func:`~pvlib.iotools.read_nsrdb_psm4`. (:issue:`2326`, :pull:`2378`)

Documentation
~~~~~~~~~~~~~
Expand Down
1 change: 0 additions & 1 deletion pvlib/iotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from pvlib.iotools.psm4 import get_nsrdb_psm4_conus # noqa: F401
from pvlib.iotools.psm4 import get_nsrdb_psm4_full_disc # noqa: F401
from pvlib.iotools.psm4 import read_nsrdb_psm4 # noqa: F401
from pvlib.iotools.psm4 import parse_nsrdb_psm4 # noqa: F401
from pvlib.iotools.pvgis import get_pvgis_tmy, read_pvgis_tmy # noqa: F401
from pvlib.iotools.pvgis import read_pvgis_hourly # noqa: F401
from pvlib.iotools.pvgis import get_pvgis_hourly # noqa: F401
Expand Down
169 changes: 65 additions & 104 deletions pvlib/iotools/psm4.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests
import pandas as pd
from json import JSONDecodeError
import contextlib

NSRDB_API_BASE = "https://developer.nrel.gov/api/nsrdb/v2/solar/"
PSM4_AGG_ENDPOINT = "nsrdb-GOES-aggregated-v4-0-0-download.csv"
Expand Down Expand Up @@ -82,7 +83,7 @@ def get_nsrdb_psm4_aggregated(latitude, longitude, api_key, email,
Aggregated v4 API.

The NSRDB is described in [1]_ and the PSM4 NSRDB GOES Aggregated v4 API is
described in [2]_,.
described in [2]_.

Parameters
----------
Expand Down Expand Up @@ -132,7 +133,7 @@ def get_nsrdb_psm4_aggregated(latitude, longitude, api_key, email,
timeseries data from NREL PSM4
metadata : dict
metadata from NREL PSM4 about the record, see
:func:`pvlib.iotools.parse_nsrdb_psm4` for fields
:func:`pvlib.iotools.read_nsrdb_psm4` for fields

Raises
------
Expand All @@ -151,19 +152,15 @@ def get_nsrdb_psm4_aggregated(latitude, longitude, api_key, email,
result in rejected requests.

.. warning:: PSM4 is limited to data found in the NSRDB, please consult
the references below for locations with available data. Additionally,
querying data with < 30-minute resolution uses a different API endpoint
with fewer available fields (see [4]_).
the references below for locations with available data.

See Also
--------
pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_conus,
pvlib.iotools.get_nsrdb_psm4_full_disc, pvlib.iotools.read_nsrdb_psm4,
pvlib.iotools.parse_nsrdb_psm4
pvlib.iotools.get_nsrdb_psm4_full_disc, pvlib.iotools.read_nsrdb_psm4

References
----------

.. [1] `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_
.. [2] `NSRDB GOES Aggregated V4.0.0
Expand Down Expand Up @@ -213,7 +210,7 @@ def get_nsrdb_psm4_aggregated(latitude, longitude, api_key, email,
# 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'))
return parse_nsrdb_psm4(fbuf, map_variables)
return read_nsrdb_psm4(fbuf, map_variables)


def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
Expand All @@ -225,7 +222,7 @@ def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
TMY v4 API.

The NSRDB is described in [1]_ and the PSM4 NSRDB GOES TMY v4 API is
described in [2]_,.
described in [2]_.

Parameters
----------
Expand Down Expand Up @@ -276,7 +273,7 @@ def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
timeseries data from NREL PSM4
metadata : dict
metadata from NREL PSM4 about the record, see
:func:`pvlib.iotools.parse_nsrdb_psm4` for fields
:func:`pvlib.iotools.read_nsrdb_psm4` for fields

Raises
------
Expand All @@ -295,19 +292,16 @@ def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
result in rejected requests.

.. warning:: PSM4 is limited to data found in the NSRDB, please consult
the references below for locations with available data. Additionally,
querying data with < 30-minute resolution uses a different API endpoint
with fewer available fields (see [4]_).
the references below for locations with available data.

See Also
--------
pvlib.iotools.get_nsrdb_psm4_aggregated,
pvlib.iotools.get_nsrdb_psm4_conus, pvlib.iotools.get_nsrdb_psm4_full_disc,
pvlib.iotools.read_nsrdb_psm4,pvlib.iotools.parse_nsrdb_psm4
pvlib.iotools.read_nsrdb_psm4

References
----------

.. [1] `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_
.. [2] `NSRDB GOES Tmy V4.0.0
Expand Down Expand Up @@ -357,7 +351,7 @@ def get_nsrdb_psm4_tmy(latitude, longitude, api_key, email, year='tmy',
# 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'))
return parse_nsrdb_psm4(fbuf, map_variables)
return read_nsrdb_psm4(fbuf, map_variables)


def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
Expand All @@ -369,7 +363,7 @@ def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
v4 API.

The NSRDB is described in [1]_ and the PSM4 NSRDB GOES CONUS v4 API is
described in [2]_,.
described in [2]_.

Parameters
----------
Expand Down Expand Up @@ -418,7 +412,7 @@ def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
timeseries data from NREL PSM4
metadata : dict
metadata from NREL PSM4 about the record, see
:func:`pvlib.iotools.parse_nsrdb_psm4` for fields
:func:`pvlib.iotools.read_nsrdb_psm4` for fields

Raises
------
Expand All @@ -437,19 +431,16 @@ def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
result in rejected requests.

.. warning:: PSM4 is limited to data found in the NSRDB, please consult
the references below for locations with available data. Additionally,
querying data with < 30-minute resolution uses a different API endpoint
with fewer available fields (see [4]_).
the references below for locations with available data.

See Also
--------
pvlib.iotools.get_nsrdb_psm4_aggregated,
pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_full_disc,
pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.parse_nsrdb_psm4
pvlib.iotools.read_nsrdb_psm4

References
----------

.. [1] `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_
.. [2] `NSRDB GOES Conus V4.0.0
Expand Down Expand Up @@ -499,7 +490,7 @@ def get_nsrdb_psm4_conus(latitude, longitude, api_key, email, year='2023',
# 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'))
return parse_nsrdb_psm4(fbuf, map_variables)
return read_nsrdb_psm4(fbuf, map_variables)


def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
Expand All @@ -513,7 +504,7 @@ def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
Disc v4 API.

The NSRDB is described in [1]_ and the PSM4 NSRDB GOES Full Disc v4 API is
described in [2]_,.
described in [2]_.

Parameters
----------
Expand Down Expand Up @@ -563,7 +554,7 @@ def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
timeseries data from NREL PSM4
metadata : dict
metadata from NREL PSM4 about the record, see
:func:`pvlib.iotools.parse_nsrdb_psm4` for fields
:func:`pvlib.iotools.read_nsrdb_psm4` for fields

Raises
------
Expand All @@ -582,19 +573,16 @@ def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
result in rejected requests.

.. warning:: PSM4 is limited to data found in the NSRDB, please consult
the references below for locations with available data. Additionally,
querying data with < 30-minute resolution uses a different API endpoint
with fewer available fields (see [4]_).
the references below for locations with available data.

See Also
--------
pvlib.iotools.get_nsrdb_psm4_aggregated,
pvlib.iotools.get_nsrdb_psm4_tmy, pvlib.iotools.get_nsrdb_psm4_conus,
pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.parse_nsrdb_psm4
pvlib.iotools.read_nsrdb_psm4

References
----------

.. [1] `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_
.. [2] `NSRDB GOES Full Disc V4.0.0
Expand Down Expand Up @@ -644,19 +632,19 @@ def get_nsrdb_psm4_full_disc(latitude, longitude, api_key, email,
# 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'))
return parse_nsrdb_psm4(fbuf, map_variables)
return read_nsrdb_psm4(fbuf, map_variables)


def parse_nsrdb_psm4(fbuf, map_variables=True):
def read_nsrdb_psm4(filename, map_variables=True):
"""
Parse an NSRDB PSM4 weather file (formatted as SAM CSV).
Read an NSRDB PSM4 weather file (formatted as SAM CSV).

The NSRDB is described in [1]_ and the SAM CSV format is described in [2]_.

Parameters
----------
fbuf: file-like object
File-like object containing data to read.
filename: str, path-like, or buffer
Filename or in-memory buffer of a file containing data to read.
map_variables: bool, default True
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
Expand Down Expand Up @@ -726,12 +714,15 @@ def parse_nsrdb_psm4(fbuf, map_variables=True):
Examples
--------
>>> # Read a local PSM4 file:
>>> df, metadata = iotools.read_nsrdb_psm4("data.csv") # doctest: +SKIP

>>> # Read a file object or an in-memory buffer:
>>> with open(filename, 'r') as f: # doctest: +SKIP
... df, metadata = iotools.parse_nsrdb_psm4(f) # doctest: +SKIP
... df, metadata = iotools.read_nsrdb_psm4(f) # doctest: +SKIP

See Also
--------
pvlib.iotools.read_nsrdb_psm4, pvlib.iotools.get_psm4
pvlib.iotools.get_psm4, pvlib.iotools.read_psm3

References
----------
Expand All @@ -740,34 +731,42 @@ def parse_nsrdb_psm4(fbuf, map_variables=True):
.. [2] `Standard Time Series Data File Format
<https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
"""
# The first 2 lines of the response are headers with metadata
metadata_fields = fbuf.readline().split(',')
metadata_fields[-1] = metadata_fields[-1].strip() # strip trailing newline
metadata_values = fbuf.readline().split(',')
metadata_values[-1] = metadata_values[-1].strip() # strip trailing newline
metadata = dict(zip(metadata_fields, metadata_values))
# the response is all strings, so set some metadata types to numbers
metadata['Local Time Zone'] = int(metadata['Local Time Zone'])
metadata['Time Zone'] = int(metadata['Time Zone'])
metadata['Latitude'] = float(metadata['Latitude'])
metadata['Longitude'] = float(metadata['Longitude'])
metadata['Elevation'] = int(metadata['Elevation'])
# get the column names so we can set the dtypes
columns = fbuf.readline().split(',')
columns[-1] = columns[-1].strip() # strip trailing newline
# Since the header has so many columns, excel saves blank cols in the
# data below the header lines.
columns = [col for col in columns if col != '']
dtypes = dict.fromkeys(columns, float) # all floats except datevec
dtypes.update(Year=int, Month=int, Day=int, Hour=int, Minute=int)
dtypes['Cloud Type'] = int
dtypes['Fill Flag'] = int
data = pd.read_csv(
fbuf, header=None, names=columns, usecols=columns, dtype=dtypes,
delimiter=',', lineterminator='\n') # skip carriage returns \r
if hasattr(filename, "read"):
# already a file-like object
context = contextlib.nullcontext(filename)
else:
# otherwise, assume a filename or path
context = open(str(filename), 'r')

with context as fbuf:
# The first 2 lines of the response are headers with metadata
metadata_fields = fbuf.readline().split(',')
metadata_fields[-1] = metadata_fields[-1].strip() # trailing newline
metadata_values = fbuf.readline().split(',')
metadata_values[-1] = metadata_values[-1].strip() # trailing newline
metadata = dict(zip(metadata_fields, metadata_values))
# the response is all strings, so set some metadata types to numbers
metadata['Local Time Zone'] = int(metadata['Local Time Zone'])
metadata['Time Zone'] = int(metadata['Time Zone'])
metadata['Latitude'] = float(metadata['Latitude'])
metadata['Longitude'] = float(metadata['Longitude'])
metadata['Elevation'] = int(metadata['Elevation'])
# get the column names so we can set the dtypes
columns = fbuf.readline().split(',')
columns[-1] = columns[-1].strip() # strip trailing newline
# Since the header has so many columns, excel saves blank cols in the
# data below the header lines.
columns = [col for col in columns if col != '']
dtypes = dict.fromkeys(columns, float) # all floats except datevec
dtypes.update(Year=int, Month=int, Day=int, Hour=int, Minute=int)
dtypes['Cloud Type'] = int
dtypes['Fill Flag'] = int
data = pd.read_csv(
fbuf, header=None, names=columns, usecols=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']])
dtidx = pd.to_datetime(data[['Year', 'Month', 'Day', 'Hour', 'Minute']])
# in USA all timezones are integers
tz = 'Etc/GMT%+d' % -metadata['Time Zone']
data.index = pd.DatetimeIndex(dtidx).tz_localize(tz)
Expand All @@ -779,41 +778,3 @@ def parse_nsrdb_psm4(fbuf, map_variables=True):
metadata['altitude'] = metadata.pop('Elevation')

return data, metadata


def read_nsrdb_psm4(filename, map_variables=True):
"""
Read an NSRDB PSM4 weather file (formatted as SAM CSV).

The NSRDB is described in [1]_ and the SAM CSV format is described in [2]_.

Parameters
----------
filename: str or path-like
Filename of a file containing data to read.
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 NREL PSM4
metadata : dict
metadata from NREL PSM4 about the record, see
:func:`pvlib.iotools.parse_nsrdb_psm4` for fields

See Also
--------
pvlib.iotools.parse_nsrdb_psm4, pvlib.iotools.get_psm4

References
----------
.. [1] `NREL National Solar Radiation Database (NSRDB)
<https://nsrdb.nrel.gov/>`_
.. [2] `Standard Time Series Data File Format
<https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
"""
with open(str(filename), 'r') as fbuf:
content = parse_nsrdb_psm4(fbuf, map_variables)
return content
Loading
Loading