From 794cc5d423f9f457a465c58acc8ef79369121093 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Tue, 15 Jul 2025 14:46:05 +0200 Subject: [PATCH 01/22] Create nasa_power.py --- pvlib/iotools/nasa_power.py | 145 ++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 pvlib/iotools/nasa_power.py diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py new file mode 100644 index 0000000000..5688bbbc02 --- /dev/null +++ b/pvlib/iotools/nasa_power.py @@ -0,0 +1,145 @@ +"""Function for reading and retrieving data from NASA POWER.""" + +import pandas as pd +import requests + + +URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point' + +VARIABLE_MAP = { + 'ALLSKY_SFC_SW_DWN': 'ghi', + 'ALLSKY_SFC_SW_DIFF': 'dhi', + 'ALLSKY_SFC_SW_DNI': 'dni', + 'CLRSKY_SFC_SW_DWN': 'ghi_clear', + 'T2M': 'temp_air_2m', + 'WS2M': 'wind_speed_2m', + 'WS10M': 'wind_speed_10m', +} + + +def get_nasa_power(latitude, longitude, start_date, end_date, + parameters, time_standard='utc', community='re', + site_elevation=None, wind_elevation=None, + wind_surface=None, map_variables=True): + """ + Retrieve irradiance and weather data from NASA POWER. + + A general description of NASA POWER is given in [1]_ and the API is + described in [2]_. A detailed list of the available parameters can be + found in [3]_. + + Parameters + ---------- + latitude: float + In decimal degrees, north is positive (ISO 19115). + longitude: float + In decimal degrees, east is positive (ISO 19115). + start_date: datetime like + First timestamp of the requested period. If a timezone is not + specified, UTC is assumed. + end_date: datetime like + Last timestamp of the requested period. If a timezone is not + specified, UTC is assumed. + parameters: str, list + List of parameters. Some common parameters are mentioned below; for the + full list see [3]_: + * ALLSKY_SFC_SW_DWN: Global Horizontal Irradiance (GHI) [Wm⁻²] + * ALLSKY_SFC_SW_DIFF: Diffuse Horizonatl Irradiance (DHI) [Wm⁻²] + * ALLSKY_SFC_SW_DNI: Direct Normal Irradiance (DNI) [Wm⁻²] + * CLRSKY_SFC_SW_DWN: Clear-sky GHI [Wm⁻²] + * T2M : Air temperature at 2 m [C] + * WS2M: Wind speed at 2 m [m/s] + * WS10M: Wind speed at 10 m [m/s] + community: str, default: 're' + Can be one of the following depending on which parameters are of + interest. Note that in many cases this choice might affect the units + of the parameter: + * 're': renewable energy + * 'sb': sustainable buildings + * 'ag': agroclimatology + time_standard: str, default:'utc' + Can be either: + * Universal Time Coordinated (utc): is the standard time measure + used by the world. + * Local Solar Time (lst): A 15 Degrees swath that represents solar + noon at the middle longitude of the swath. + site_elevation: float, optional + The custom site elevation in meters to produce the corrected + atmospheric pressure adjusted for elevation. + wind_elevation: float, optional + The custom wind elevation in meters to produce the wind speed adjusted + for elevation. Has to be between 10 and 300 m; see [4]_. + wind_surface: str, optional + The definable surface type to adjust the wind speed. For a list of the + surface types see [4]_. + map_variables: bool, default: True + When true, renames columns of the Dataframe to pvlib variable names + where applicable. The default is True. See variable + :const:`VARIABLE_MAP`. + + Raises + ------ + requests.HTTPError + Raises an error when an incorrect request is made. + ValueError + Raises an error if more than 15 parameters are requested in one call. + + Returns + ------- + data : pd.DataFrame + Time series data. The index corresponds to the start (left) of the + interval. + + References + ---------- + .. [1] `NASA Prediction Of Worldwide Energy Resources (POWER) + `_ + .. [2] `NASA POWER API + `_ + .. [3] `NASA POWER API parameters + `_ + .. [4] `NASA POWER corrected wind speed parameters + _ + """ + if len(parameters) > 15: + raise ValueError("A maximum of 15 parameters can currently be " + "requested in one submission.") + + start = pd.Timestamp(start_date) + end = pd.Timestamp(end_date) + start = start.tz_localize('UTC') if start.tzinfo is None else start + end = end.tz_localize('UTC') if end.tzinfo is None else end + + params = { + 'latitude': latitude, + 'longitude': longitude, + 'start': start.strftime('%Y%m%d'), + 'end': end.strftime('%Y%m%d'), + 'community': community, + 'parameters': ','.join(parameters), # make parameters in a string + 'format': 'json', + 'user': None, + 'header': True, + 'time-standard': time_standard, + 'site-elevation': site_elevation, + 'wind-elevation': wind_elevation, + 'wind-surface': wind_surface, + } + + response = requests.get(URL, params=params) + if not response.ok: + # response.raise_for_status() does not give a useful error message + raise requests.HTTPError(response.json()) + + # Parse the data to dataframe + data = response.json() + hourly_data = data['properties']['parameter'] + df = pd.DataFrame(hourly_data) + df.index = pd.to_datetime(df.index, format='%Y%m%d%H') + df = df.sort_index() + + # Rename according to pvlib convention + if map_variables: + df = df.rename(columns=VARIABLE_MAP) + + return df From ff8d010725220ae794522bf9c1ce34a72038452b Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Tue, 15 Jul 2025 14:55:22 +0200 Subject: [PATCH 02/22] Updated API --- docs/sphinx/source/reference/iotools.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/sphinx/source/reference/iotools.rst b/docs/sphinx/source/reference/iotools.rst index cbf89c71a7..bcc9f4e2a6 100644 --- a/docs/sphinx/source/reference/iotools.rst +++ b/docs/sphinx/source/reference/iotools.rst @@ -57,6 +57,17 @@ clear-sky irradiance globally. iotools.parse_cams +NASA POWER +********** + +Satellite-derived irradiance and weather data with global coverage. + +.. autosummary:: + :toctree: generated/ + + iotools.get_nasa_power + + NSRDB ***** From 9a99395359d8f9811c9963cbd94309e361760a02 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Tue, 15 Jul 2025 15:10:03 +0200 Subject: [PATCH 03/22] Update __init__.py --- pvlib/iotools/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/iotools/__init__.py b/pvlib/iotools/__init__.py index 352044e5cd..51136ce9bc 100644 --- a/pvlib/iotools/__init__.py +++ b/pvlib/iotools/__init__.py @@ -39,3 +39,4 @@ from pvlib.iotools.solcast import get_solcast_historic # noqa: F401 from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401 from pvlib.iotools.solargis import get_solargis # noqa: F401 +from pvlib.iotools.nasa_power import get_nasa_power # noqa: F401 From 88c2efaa5802ff7be210e92a3f4e4b2aa0ac839d Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Tue, 15 Jul 2025 15:20:15 +0200 Subject: [PATCH 04/22] fixed minor typos --- pvlib/iotools/nasa_power.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 5688bbbc02..da4471dad0 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -57,12 +57,11 @@ def get_nasa_power(latitude, longitude, start_date, end_date, * 're': renewable energy * 'sb': sustainable buildings * 'ag': agroclimatology - time_standard: str, default:'utc' - Can be either: - * Universal Time Coordinated (utc): is the standard time measure - used by the world. + time_standard: str, default: 'utc' + Can be either 'utc' or 'lst': + * Universal Time Coordinated (utc) * Local Solar Time (lst): A 15 Degrees swath that represents solar - noon at the middle longitude of the swath. + noon at the middle longitude of the swath site_elevation: float, optional The custom site elevation in meters to produce the corrected atmospheric pressure adjusted for elevation. @@ -72,7 +71,7 @@ def get_nasa_power(latitude, longitude, start_date, end_date, wind_surface: str, optional The definable surface type to adjust the wind speed. For a list of the surface types see [4]_. - map_variables: bool, default: True + map_variables: bool, optional, default: True When true, renames columns of the Dataframe to pvlib variable names where applicable. The default is True. See variable :const:`VARIABLE_MAP`. @@ -99,7 +98,7 @@ def get_nasa_power(latitude, longitude, start_date, end_date, .. [3] `NASA POWER API parameters `_ .. [4] `NASA POWER corrected wind speed parameters - _ + `_ """ if len(parameters) > 15: raise ValueError("A maximum of 15 parameters can currently be " From 72228829c1b812d6fc57b345661facf12d2185ef Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 11:50:15 +0200 Subject: [PATCH 05/22] Implement Adam's feedback --- pvlib/iotools/nasa_power.py | 80 +++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index da4471dad0..86d370c449 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -17,10 +17,9 @@ } -def get_nasa_power(latitude, longitude, start_date, end_date, - parameters, time_standard='utc', community='re', - site_elevation=None, wind_elevation=None, - wind_surface=None, map_variables=True): +def get_nasa_power(latitude, longitude, start, end, parameters, + time_standard='utc', community='re', elevation=None, + wind_height=None, wind_surface=None, map_variables=True): """ Retrieve irradiance and weather data from NASA POWER. @@ -34,40 +33,46 @@ def get_nasa_power(latitude, longitude, start_date, end_date, In decimal degrees, north is positive (ISO 19115). longitude: float In decimal degrees, east is positive (ISO 19115). - start_date: datetime like + start: datetime like First timestamp of the requested period. If a timezone is not specified, UTC is assumed. - end_date: datetime like + end: datetime like Last timestamp of the requested period. If a timezone is not specified, UTC is assumed. parameters: str, list List of parameters. Some common parameters are mentioned below; for the full list see [3]_: - * ALLSKY_SFC_SW_DWN: Global Horizontal Irradiance (GHI) [Wm⁻²] - * ALLSKY_SFC_SW_DIFF: Diffuse Horizonatl Irradiance (DHI) [Wm⁻²] - * ALLSKY_SFC_SW_DNI: Direct Normal Irradiance (DNI) [Wm⁻²] - * CLRSKY_SFC_SW_DWN: Clear-sky GHI [Wm⁻²] - * T2M : Air temperature at 2 m [C] - * WS2M: Wind speed at 2 m [m/s] - * WS10M: Wind speed at 10 m [m/s] - community: str, default: 're' + + * ``ALLSKY_SFC_SW_DWN``: Global Horizontal Irradiance (GHI) [Wm⁻²] + * ``ALLSKY_SFC_SW_DIFF``: Diffuse Horizontal Irradiance (DHI) [Wm⁻²] + * ``ALLSKY_SFC_SW_DNI``: Direct Normal Irradiance (DNI) [Wm⁻²] + * ``CLRSKY_SFC_SW_DWN``: Clear-sky GHI [Wm⁻²] + * ``T2M``: Air temperature at 2 m [C] + * ``WS2M``: Wind speed at 2 m [m/s] + * ``WS10M``: Wind speed at 10 m [m/s] + + community: str, default: ``'re'`` Can be one of the following depending on which parameters are of interest. Note that in many cases this choice might affect the units of the parameter: - * 're': renewable energy - * 'sb': sustainable buildings - * 'ag': agroclimatology - time_standard: str, default: 'utc' - Can be either 'utc' or 'lst': - * Universal Time Coordinated (utc) - * Local Solar Time (lst): A 15 Degrees swath that represents solar - noon at the middle longitude of the swath - site_elevation: float, optional + + * ``'re'``: renewable energy + * ``'sb'``: sustainable buildings + * ``'ag'``: agroclimatology + + time_standard: str, default: ``'utc'`` + Can be either ``'utc'`` or ``'lst'``: + + * Universal Time Coordinated (utc) + * Local Solar Time (lst): A 15 Degrees swath that represents solar + noon at the middle longitude of the swath + + elevation: float, optional The custom site elevation in meters to produce the corrected atmospheric pressure adjusted for elevation. - wind_elevation: float, optional - The custom wind elevation in meters to produce the wind speed adjusted - for elevation. Has to be between 10 and 300 m; see [4]_. + wind_height: float, optional + The custom wind height in meters to produce the wind speed adjusted + for height. Has to be between 10 and 300 m; see [4]_. wind_surface: str, optional The definable surface type to adjust the wind speed. For a list of the surface types see [4]_. @@ -80,8 +85,6 @@ def get_nasa_power(latitude, longitude, start_date, end_date, ------ requests.HTTPError Raises an error when an incorrect request is made. - ValueError - Raises an error if more than 15 parameters are requested in one call. Returns ------- @@ -100,12 +103,8 @@ def get_nasa_power(latitude, longitude, start_date, end_date, .. [4] `NASA POWER corrected wind speed parameters `_ """ - if len(parameters) > 15: - raise ValueError("A maximum of 15 parameters can currently be " - "requested in one submission.") - - start = pd.Timestamp(start_date) - end = pd.Timestamp(end_date) + start = pd.Timestamp(start) + end = pd.Timestamp(end) start = start.tz_localize('UTC') if start.tzinfo is None else start end = end.tz_localize('UTC') if end.tzinfo is None else end @@ -120,8 +119,8 @@ def get_nasa_power(latitude, longitude, start_date, end_date, 'user': None, 'header': True, 'time-standard': time_standard, - 'site-elevation': site_elevation, - 'wind-elevation': wind_elevation, + 'site-elevation': elevation, + 'wind-elevation': wind_height, 'wind-surface': wind_surface, } @@ -134,11 +133,14 @@ def get_nasa_power(latitude, longitude, start_date, end_date, data = response.json() hourly_data = data['properties']['parameter'] df = pd.DataFrame(hourly_data) - df.index = pd.to_datetime(df.index, format='%Y%m%d%H') - df = df.sort_index() + df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC') + + # Make metadata dictionary + meta = {key: data[key] for key in ['geometry', 'header', 'messages', + 'parameters', 'times', 'type']} # Rename according to pvlib convention if map_variables: df = df.rename(columns=VARIABLE_MAP) - return df + return df, meta From 773dfab73cf8ce85fefa352b9ed7ebbf5cfe6d36 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 12:50:46 +0200 Subject: [PATCH 06/22] update metadata --- pvlib/iotools/nasa_power.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 86d370c449..0fe679f2fb 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -136,8 +136,12 @@ def get_nasa_power(latitude, longitude, start, end, parameters, df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC') # Make metadata dictionary - meta = {key: data[key] for key in ['geometry', 'header', 'messages', - 'parameters', 'times', 'type']} + meta = {key: data[key] for key in ['header', 'messages', 'parameters', + 'times', 'type']} + + meta['longitude'] = data['geometry']['coordinates'][0] + meta['latitude'] = data['geometry']['coordinates'][1] + meta['altitude'] = data['geometry']['coordinates'][2] # Rename according to pvlib convention if map_variables: From 85cc8ded471c054da7bece0c541995077ab7d9b1 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 13:58:57 +0200 Subject: [PATCH 07/22] Add tests --- tests/iotools/test_nasa_power.py | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/iotools/test_nasa_power.py diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py new file mode 100644 index 0000000000..3e929b3519 --- /dev/null +++ b/tests/iotools/test_nasa_power.py @@ -0,0 +1,87 @@ +import pandas as pd +import pytest +import pvlib +from requests.exceptions import HTTPError + + +@pytest.fixture +def data_index(): + index = pd.date_range(start='2025-02-02 00:00+00:00', + end='2025-02-02 23:00+00:00', freq='1h') + return index + + +@pytest.fixture +def ghi_series(data_index): + ghi = [ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.25, 184.2, 281.55, 368.3, 406.48, + 386.45, 316.05, 210.1, 109.05, 12.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 + ] + return pd.Series(data=ghi, index=data_index, name='ghi') + + +def test_get_nasa_power(data_index, ghi_series): + data, meta = pvlib.iotools.get_nasa_power(latitude=44.76, + longitude=7.64, + start=data_index[0], + end=data_index[-1], + parameters=['ALLSKY_SFC_SW_DWN'], + map_variables=False) + # Check that metadata is correct + assert meta['latitude'] == 44.76 + assert meta['longitude'] == 7.64 + assert meta['altitude'] == 705.88 + assert meta['header']['start'] == '20250202' + assert meta['header']['end'] == '20250202' + assert meta['header']['time_standard'] == 'UTC' + assert meta['header']['title'] == 'NASA/POWER Source Native Resolution Hourly Data' # noqa: E501 + assert meta['header']['sources'][0] == 'SYN1DEG' + # Check that columns are parsed correctly + assert 'ALLSKY_SFC_SW_DWN' in data.columns + # Assert that the index is parsed correctly + pd.testing.assert_index_equal(data.index, data_index) + # Test one column + pd.testing.assert_series_equal(data['ALLSKY_SFC_SW_DWN'], ghi_series, + check_freq=False, check_names=False) + + +def test_get_nasa_power_map_variables(data_index): + # Check that variables are mapped by default to pvlib names + data, meta = pvlib.iotools.get_nasa_power(latitude=44.76, + longitude=7.64, + start=data_index[0], + end=data_index[-1], + parameters=['ALLSKY_SFC_SW_DWN', + 'ALLSKY_SFC_SW_DIFF', + 'ALLSKY_SFC_SW_DNI', + 'CLRSKY_SFC_SW_DWN', + 'T2M', 'WS2M', + 'WS10M' + ]) + mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air_2m', 'wind_speed_2m', + 'wind_speed_10m', 'ghi_clear'] + for c in mapped_column_names: + assert c in data.columns + assert meta['latitude'] == 44.76 + assert meta['longitude'] == 7.64 + assert meta['altitude'] == 705.88 + + +def test_get_nasa_power_wrong_parameter_name(data_index): + # Test if HTTPError is raised if a wrong parameter name is asked + with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DLN"): + pvlib.iotools.get_nasa_power(latitude=44.76, + longitude=7.64, + start=data_index[0], + end=data_index[-1], + parameters=['ALLSKY_SFC_SW_DLN']) + + +def test_get_nasa_power_duplicate_parameter_name(data_index): + # Test if HTTPError is raised if a duplicate parameter is asked + with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DWN"): + pvlib.iotools.get_nasa_power(latitude=44.76, + longitude=7.64, + start=data_index[0], + end=data_index[-1], + parameters=2*['ALLSKY_SFC_SW_DWN']) From fc3f520cd73245ba8e3a0b66eba469676635ca21 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 14:04:37 +0200 Subject: [PATCH 08/22] fix lint --- tests/iotools/test_nasa_power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index 3e929b3519..02bb90a866 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -16,7 +16,7 @@ def ghi_series(data_index): ghi = [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.25, 184.2, 281.55, 368.3, 406.48, 386.45, 316.05, 210.1, 109.05, 12.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 - ] + ] return pd.Series(data=ghi, index=data_index, name='ghi') From 2775387b6d69d639e4c1af4080938b12b84b9f95 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 14:07:04 +0200 Subject: [PATCH 09/22] 2nd try --- tests/iotools/test_nasa_power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index 02bb90a866..8179ce71ff 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -16,7 +16,7 @@ def ghi_series(data_index): ghi = [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.25, 184.2, 281.55, 368.3, 406.48, 386.45, 316.05, 210.1, 109.05, 12.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 - ] + ] return pd.Series(data=ghi, index=data_index, name='ghi') From 2f127e4780627906ea2a81bf5e4d3366f766b756 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 14:12:57 +0200 Subject: [PATCH 10/22] update whatsnew --- docs/sphinx/source/whatsnew/v0.13.1.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 9c50d00bbb..4a53c10cce 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -19,7 +19,7 @@ Bug fixes Enhancements ~~~~~~~~~~~~ - +* Added function :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER. (:pull:`2500`) Documentation ~~~~~~~~~~~~~ @@ -45,3 +45,5 @@ Maintenance Contributors ~~~~~~~~~~~~ * Elijah Passmore (:ghuser:`eljpsm`) +* Ioannis Sifnaios (:ghuser:`IoannisSifnaios`) + From 1b1793df423503d15e6cd896154bab95be42510a Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:28:59 +0200 Subject: [PATCH 11/22] Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/nasa_power.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 0fe679f2fb..64ce9bae97 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -34,11 +34,9 @@ def get_nasa_power(latitude, longitude, start, end, parameters, longitude: float In decimal degrees, east is positive (ISO 19115). start: datetime like - First timestamp of the requested period. If a timezone is not - specified, UTC is assumed. + First timestamp of the requested period. end: datetime like - Last timestamp of the requested period. If a timezone is not - specified, UTC is assumed. + Last timestamp of the requested period. parameters: str, list List of parameters. Some common parameters are mentioned below; for the full list see [3]_: @@ -60,11 +58,11 @@ def get_nasa_power(latitude, longitude, start, end, parameters, * ``'sb'``: sustainable buildings * ``'ag'``: agroclimatology - time_standard: str, default: ``'utc'`` - Can be either ``'utc'`` or ``'lst'``: + time_standard: str, optional + Must be one of the below options. The default is ``'utc'``. - * Universal Time Coordinated (utc) - * Local Solar Time (lst): A 15 Degrees swath that represents solar + * ``'utc'`` universal time coordinated + * ``'lst'``: local solar time. 15 degree swath that represents solar noon at the middle longitude of the swath elevation: float, optional @@ -76,10 +74,10 @@ def get_nasa_power(latitude, longitude, start, end, parameters, wind_surface: str, optional The definable surface type to adjust the wind speed. For a list of the surface types see [4]_. - map_variables: bool, optional, default: True + map_variables: bool, optional When true, renames columns of the Dataframe to pvlib variable names - where applicable. The default is True. See variable - :const:`VARIABLE_MAP`. + where applicable. See variable :const:`VARIABLE_MAP`. + The default is True. Raises ------ @@ -105,8 +103,6 @@ def get_nasa_power(latitude, longitude, start, end, parameters, """ start = pd.Timestamp(start) end = pd.Timestamp(end) - start = start.tz_localize('UTC') if start.tzinfo is None else start - end = end.tz_localize('UTC') if end.tzinfo is None else end params = { 'latitude': latitude, From 0ae734688c14935285be95472b8aa4bb39164863 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 14:39:39 +0200 Subject: [PATCH 12/22] suggestions from code review vol.2 --- pvlib/iotools/nasa_power.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 64ce9bae97..a83c3f5272 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -58,13 +58,6 @@ def get_nasa_power(latitude, longitude, start, end, parameters, * ``'sb'``: sustainable buildings * ``'ag'``: agroclimatology - time_standard: str, optional - Must be one of the below options. The default is ``'utc'``. - - * ``'utc'`` universal time coordinated - * ``'lst'``: local solar time. 15 degree swath that represents solar - noon at the middle longitude of the swath - elevation: float, optional The custom site elevation in meters to produce the corrected atmospheric pressure adjusted for elevation. @@ -73,7 +66,8 @@ def get_nasa_power(latitude, longitude, start, end, parameters, for height. Has to be between 10 and 300 m; see [4]_. wind_surface: str, optional The definable surface type to adjust the wind speed. For a list of the - surface types see [4]_. + surface types see [4]_. If you provide a wind surface alias please + include a site elevation with the request. map_variables: bool, optional When true, renames columns of the Dataframe to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. @@ -89,6 +83,8 @@ def get_nasa_power(latitude, longitude, start, end, parameters, data : pd.DataFrame Time series data. The index corresponds to the start (left) of the interval. + meta : dict + Metadata. References ---------- @@ -114,7 +110,7 @@ def get_nasa_power(latitude, longitude, start, end, parameters, 'format': 'json', 'user': None, 'header': True, - 'time-standard': time_standard, + 'time-standard': 'utc', 'site-elevation': elevation, 'wind-elevation': wind_height, 'wind-surface': wind_surface, From 095f311b045b8e515562453bbf165855672a327e Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:39:15 +0200 Subject: [PATCH 13/22] Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- docs/sphinx/source/whatsnew/v0.13.1.rst | 3 ++- pvlib/iotools/nasa_power.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 4a53c10cce..ad798edc0f 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -19,7 +19,8 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Added function :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER. (:pull:`2500`) +* Added function :py:func:`~pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER. + (:pull:`2500`) Documentation ~~~~~~~~~~~~~ diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index a83c3f5272..9a22956a94 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -1,9 +1,8 @@ -"""Function for reading and retrieving data from NASA POWER.""" +"""Functions for reading and retrieving data from NASA POWER.""" import pandas as pd import requests - URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point' VARIABLE_MAP = { @@ -18,7 +17,7 @@ def get_nasa_power(latitude, longitude, start, end, parameters, - time_standard='utc', community='re', elevation=None, + community='re', elevation=None, wind_height=None, wind_surface=None, map_variables=True): """ Retrieve irradiance and weather data from NASA POWER. From d72659124d837ddf6de7e1b896bfdd9633be56db Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Wed, 16 Jul 2025 15:43:10 +0200 Subject: [PATCH 14/22] lint vol.2 --- pvlib/iotools/nasa_power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 9a22956a94..43697de024 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -83,7 +83,7 @@ def get_nasa_power(latitude, longitude, start, end, parameters, Time series data. The index corresponds to the start (left) of the interval. meta : dict - Metadata. + Metadata. References ---------- From 11eedcc5cbdf05db131d0dfb8a33ea88b9cf9223 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:24:26 +0200 Subject: [PATCH 15/22] Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> --- pvlib/iotools/nasa_power.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 43697de024..42a26ac64d 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -10,7 +10,7 @@ 'ALLSKY_SFC_SW_DIFF': 'dhi', 'ALLSKY_SFC_SW_DNI': 'dni', 'CLRSKY_SFC_SW_DWN': 'ghi_clear', - 'T2M': 'temp_air_2m', + 'T2M': 'temp_air', 'WS2M': 'wind_speed_2m', 'WS10M': 'wind_speed_10m', } @@ -99,6 +99,10 @@ def get_nasa_power(latitude, longitude, start, end, parameters, start = pd.Timestamp(start) end = pd.Timestamp(end) + # allow the use of pvlib parameter names + parameter_dict = {v: k for k, v in VARIABLE_MAP.items()} + parameters = [parameter_dict.get(p, p) for p in parameters] + params = { 'latitude': latitude, 'longitude': longitude, @@ -126,9 +130,10 @@ def get_nasa_power(latitude, longitude, start, end, parameters, df = pd.DataFrame(hourly_data) df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC') - # Make metadata dictionary - meta = {key: data[key] for key in ['header', 'messages', 'parameters', - 'times', 'type']} + # Create metadata dictionary + meta = data['header'] + meta['times'] = data['times'] + meta['parameters'] = data['parameters'] meta['longitude'] = data['geometry']['coordinates'][0] meta['latitude'] = data['geometry']['coordinates'][1] From 724119625843b56761db44d35f9fb3110afcc1dc Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Thu, 17 Jul 2025 10:53:26 +0200 Subject: [PATCH 16/22] Adam's corrections --- pvlib/iotools/nasa_power.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 42a26ac64d..e024959a4c 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -2,9 +2,15 @@ import pandas as pd import requests +import numpy as np URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point' +DEFAULT_PARAMETERS = [ + 'ALLSKY_SFC_SW_DNI', 'ALLSKY_SFC_SW_DIFF', 'ALLSKY_SFC_SW_DWN', + 'T2M', 'WS10M' + ] + VARIABLE_MAP = { 'ALLSKY_SFC_SW_DWN': 'ghi', 'ALLSKY_SFC_SW_DIFF': 'dhi', @@ -12,13 +18,14 @@ 'CLRSKY_SFC_SW_DWN': 'ghi_clear', 'T2M': 'temp_air', 'WS2M': 'wind_speed_2m', - 'WS10M': 'wind_speed_10m', + 'WS10M': 'wind_speed', } -def get_nasa_power(latitude, longitude, start, end, parameters, - community='re', elevation=None, - wind_height=None, wind_surface=None, map_variables=True): +def get_nasa_power(latitude, longitude, start, end, + parameters=DEFAULT_PARAMETERS, community='re', url=URL, + elevation=None, wind_height=None, wind_surface=None, + map_variables=True): """ Retrieve irradiance and weather data from NASA POWER. @@ -37,15 +44,14 @@ def get_nasa_power(latitude, longitude, start, end, parameters, end: datetime like Last timestamp of the requested period. parameters: str, list - List of parameters. Some common parameters are mentioned below; for the - full list see [3]_: + List of parameters. The default parameters are mentioned below; for the + full list see [3]_. Note that the pvlib naming conventions can also be + used. * ``ALLSKY_SFC_SW_DWN``: Global Horizontal Irradiance (GHI) [Wm⁻²] * ``ALLSKY_SFC_SW_DIFF``: Diffuse Horizontal Irradiance (DHI) [Wm⁻²] * ``ALLSKY_SFC_SW_DNI``: Direct Normal Irradiance (DNI) [Wm⁻²] - * ``CLRSKY_SFC_SW_DWN``: Clear-sky GHI [Wm⁻²] * ``T2M``: Air temperature at 2 m [C] - * ``WS2M``: Wind speed at 2 m [m/s] * ``WS10M``: Wind speed at 10 m [m/s] community: str, default: ``'re'`` @@ -119,7 +125,7 @@ def get_nasa_power(latitude, longitude, start, end, parameters, 'wind-surface': wind_surface, } - response = requests.get(URL, params=params) + response = requests.get(url, params=params) if not response.ok: # response.raise_for_status() does not give a useful error message raise requests.HTTPError(response.json()) @@ -129,6 +135,7 @@ def get_nasa_power(latitude, longitude, start, end, parameters, hourly_data = data['properties']['parameter'] df = pd.DataFrame(hourly_data) df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC') + df = df.replace(-999, np.nan) # Create metadata dictionary meta = data['header'] From 3720b85901a0e4185721b8d70d5266c231aaaf55 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Thu, 17 Jul 2025 10:53:40 +0200 Subject: [PATCH 17/22] Update tests according to new code --- tests/iotools/test_nasa_power.py | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index 8179ce71ff..ef175c7e37 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -25,19 +25,15 @@ def test_get_nasa_power(data_index, ghi_series): longitude=7.64, start=data_index[0], end=data_index[-1], - parameters=['ALLSKY_SFC_SW_DWN'], map_variables=False) # Check that metadata is correct assert meta['latitude'] == 44.76 assert meta['longitude'] == 7.64 assert meta['altitude'] == 705.88 - assert meta['header']['start'] == '20250202' - assert meta['header']['end'] == '20250202' - assert meta['header']['time_standard'] == 'UTC' - assert meta['header']['title'] == 'NASA/POWER Source Native Resolution Hourly Data' # noqa: E501 - assert meta['header']['sources'][0] == 'SYN1DEG' - # Check that columns are parsed correctly - assert 'ALLSKY_SFC_SW_DWN' in data.columns + assert meta['start'] == '20250202' + assert meta['end'] == '20250202' + assert meta['time_standard'] == 'UTC' + assert meta['title'] == 'NASA/POWER Source Native Resolution Hourly Data' # Assert that the index is parsed correctly pd.testing.assert_index_equal(data.index, data_index) # Test one column @@ -45,21 +41,26 @@ def test_get_nasa_power(data_index, ghi_series): check_freq=False, check_names=False) +def test_get_nasa_power_pvlib_params_naming(data_index, ghi_series): + data, meta = pvlib.iotools.get_nasa_power(latitude=44.76, + longitude=7.64, + start=data_index[0], + end=data_index[-1], + parameters=['ghi']) + # Assert that the index is parsed correctly + pd.testing.assert_index_equal(data.index, data_index) + # Test one column + pd.testing.assert_series_equal(data['ghi'], ghi_series, + check_freq=False) + + def test_get_nasa_power_map_variables(data_index): # Check that variables are mapped by default to pvlib names data, meta = pvlib.iotools.get_nasa_power(latitude=44.76, longitude=7.64, start=data_index[0], - end=data_index[-1], - parameters=['ALLSKY_SFC_SW_DWN', - 'ALLSKY_SFC_SW_DIFF', - 'ALLSKY_SFC_SW_DNI', - 'CLRSKY_SFC_SW_DWN', - 'T2M', 'WS2M', - 'WS10M' - ]) - mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air_2m', 'wind_speed_2m', - 'wind_speed_10m', 'ghi_clear'] + end=data_index[-1]) + mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed'] for c in mapped_column_names: assert c in data.columns assert meta['latitude'] == 44.76 From c508058462e9a088428f2116a677c06ef62ceaa9 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Thu, 17 Jul 2025 10:54:56 +0200 Subject: [PATCH 18/22] come on now linter! --- pvlib/iotools/nasa_power.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index e024959a4c..3c8ab63e3e 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -9,7 +9,7 @@ DEFAULT_PARAMETERS = [ 'ALLSKY_SFC_SW_DNI', 'ALLSKY_SFC_SW_DIFF', 'ALLSKY_SFC_SW_DWN', 'T2M', 'WS10M' - ] +] VARIABLE_MAP = { 'ALLSKY_SFC_SW_DWN': 'ghi', From a2c9a44d618791d81861d6ac960a3fc3491a8a13 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Thu, 17 Jul 2025 11:13:27 +0200 Subject: [PATCH 19/22] minor change --- pvlib/iotools/nasa_power.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 3c8ab63e3e..6a9b01f10c 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -54,10 +54,10 @@ def get_nasa_power(latitude, longitude, start, end, * ``T2M``: Air temperature at 2 m [C] * ``WS10M``: Wind speed at 10 m [m/s] - community: str, default: ``'re'`` + community: str Can be one of the following depending on which parameters are of - interest. Note that in many cases this choice might affect the units - of the parameter: + interest. The default is ``'re'``. Note that in many cases this choice + might affect the units of the parameter. * ``'re'``: renewable energy * ``'sb'``: sustainable buildings @@ -76,7 +76,7 @@ def get_nasa_power(latitude, longitude, start, end, map_variables: bool, optional When true, renames columns of the Dataframe to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - The default is True. + The default is `True`. Raises ------ From 36a337f122aaa72e563a02614e982d3d2798930c Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios <88548539+IoannisSifnaios@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:59:39 +0200 Subject: [PATCH 20/22] Apply suggestions from code review Co-authored-by: Kevin Anderson --- pvlib/iotools/nasa_power.py | 7 +++---- tests/iotools/test_nasa_power.py | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 6a9b01f10c..5587498e8d 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -54,9 +54,9 @@ def get_nasa_power(latitude, longitude, start, end, * ``T2M``: Air temperature at 2 m [C] * ``WS10M``: Wind speed at 10 m [m/s] - community: str + community: str, default 're' Can be one of the following depending on which parameters are of - interest. The default is ``'re'``. Note that in many cases this choice + interest. Note that in many cases this choice might affect the units of the parameter. * ``'re'``: renewable energy @@ -73,10 +73,9 @@ def get_nasa_power(latitude, longitude, start, end, The definable surface type to adjust the wind speed. For a list of the surface types see [4]_. If you provide a wind surface alias please include a site elevation with the request. - map_variables: bool, optional + map_variables: bool, default True When true, renames columns of the Dataframe to pvlib variable names where applicable. See variable :const:`VARIABLE_MAP`. - The default is `True`. Raises ------ diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index ef175c7e37..88b38bfe68 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -20,6 +20,8 @@ def ghi_series(data_index): return pd.Series(data=ghi, index=data_index, name='ghi') +@pytest.mark.remote_data +@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_nasa_power(data_index, ghi_series): data, meta = pvlib.iotools.get_nasa_power(latitude=44.76, longitude=7.64, From 9c0d58f39c65414f65cdeefce642c9656f6cc806 Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Mon, 21 Jul 2025 12:29:53 +0200 Subject: [PATCH 21/22] Kevin's suggestions (function) --- pvlib/iotools/nasa_power.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 5587498e8d..5acef7e0dd 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -7,8 +7,7 @@ URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point' DEFAULT_PARAMETERS = [ - 'ALLSKY_SFC_SW_DNI', 'ALLSKY_SFC_SW_DIFF', 'ALLSKY_SFC_SW_DWN', - 'T2M', 'WS10M' + 'dni', 'dhi', 'ghi', 'temp_air', 'wind_speed' ] VARIABLE_MAP = { @@ -48,11 +47,11 @@ def get_nasa_power(latitude, longitude, start, end, full list see [3]_. Note that the pvlib naming conventions can also be used. - * ``ALLSKY_SFC_SW_DWN``: Global Horizontal Irradiance (GHI) [Wm⁻²] - * ``ALLSKY_SFC_SW_DIFF``: Diffuse Horizontal Irradiance (DHI) [Wm⁻²] - * ``ALLSKY_SFC_SW_DNI``: Direct Normal Irradiance (DNI) [Wm⁻²] - * ``T2M``: Air temperature at 2 m [C] - * ``WS10M``: Wind speed at 10 m [m/s] + * Global Horizontal Irradiance (GHI) [Wm⁻²] + * Diffuse Horizontal Irradiance (DHI) [Wm⁻²] + * Direct Normal Irradiance (DNI) [Wm⁻²] + * Air temperature at 2 m [C] + * Wind speed at 10 m [m/s] community: str, default 're' Can be one of the following depending on which parameters are of @@ -134,7 +133,6 @@ def get_nasa_power(latitude, longitude, start, end, hourly_data = data['properties']['parameter'] df = pd.DataFrame(hourly_data) df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC') - df = df.replace(-999, np.nan) # Create metadata dictionary meta = data['header'] @@ -145,6 +143,9 @@ def get_nasa_power(latitude, longitude, start, end, meta['latitude'] = data['geometry']['coordinates'][1] meta['altitude'] = data['geometry']['coordinates'][2] + # Replace NaN values + df = df.replace(meta['fill_value'], np.nan) + # Rename according to pvlib convention if map_variables: df = df.rename(columns=VARIABLE_MAP) From b8ff9ebf0090bea4637551e3fafc4380905142ff Mon Sep 17 00:00:00 2001 From: Ioannis Sifnaios Date: Mon, 21 Jul 2025 12:30:05 +0200 Subject: [PATCH 22/22] Kevin's suggestions (tests) --- tests/iotools/test_nasa_power.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index 88b38bfe68..96e0c66cbc 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -2,6 +2,7 @@ import pytest import pvlib from requests.exceptions import HTTPError +from tests.conftest import RERUNS, RERUNS_DELAY @pytest.fixture @@ -27,6 +28,7 @@ def test_get_nasa_power(data_index, ghi_series): longitude=7.64, start=data_index[0], end=data_index[-1], + parameters=['ALLSKY_SFC_SW_DWN'], map_variables=False) # Check that metadata is correct assert meta['latitude'] == 44.76