Skip to content

Commit 59d2e8e

Browse files
IoannisSifnaiosAdamRJensenkandersolarechedey-ls
authored
Add NASA POWER to iotools (#2500)
* Create nasa_power.py * Updated API * Update __init__.py * fixed minor typos * Implement Adam's feedback * update metadata * Add tests * fix lint * 2nd try * update whatsnew * Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * suggestions from code review vol.2 * Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * lint vol.2 * Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * Adam's corrections * Update tests according to new code * come on now linter! * minor change * Apply suggestions from code review Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> * Kevin's suggestions (function) * Kevin's suggestions (tests) * Apply suggestions from code review Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> * Apply suggestions from code review Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * Put URL at the end of the function --------- Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com>
1 parent 62eed1f commit 59d2e8e

File tree

5 files changed

+269
-2
lines changed

5 files changed

+269
-2
lines changed

docs/sphinx/source/reference/iotools.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ clear-sky irradiance globally.
5757
iotools.parse_cams
5858

5959

60+
NASA POWER
61+
**********
62+
63+
Satellite-derived irradiance and weather data with global coverage.
64+
65+
.. autosummary::
66+
:toctree: generated/
67+
68+
iotools.get_nasa_power
69+
70+
6071
NSRDB
6172
*****
6273

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Bug fixes
1919

2020
Enhancements
2121
~~~~~~~~~~~~
22-
22+
* Add :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER free API.
23+
(:pull:`2500`)
2324

2425
Documentation
2526
~~~~~~~~~~~~~
@@ -47,5 +48,6 @@ Maintenance
4748
Contributors
4849
~~~~~~~~~~~~
4950
* Elijah Passmore (:ghuser:`eljpsm`)
51+
* Ioannis Sifnaios (:ghuser:`IoannisSifnaios`)
5052
* Rajiv Daxini (:ghuser:`RDaxini`)
51-
* Omar Bahamida (:ghuser:`OmarBahamida`)
53+
* Omar Bahamida (:ghuser:`OmarBahamida`)

pvlib/iotools/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
from pvlib.iotools.solcast import get_solcast_historic # noqa: F401
4040
from pvlib.iotools.solcast import get_solcast_tmy # noqa: F401
4141
from pvlib.iotools.solargis import get_solargis # noqa: F401
42+
from pvlib.iotools.nasa_power import get_nasa_power # noqa: F401

pvlib/iotools/nasa_power.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Functions for reading and retrieving data from NASA POWER."""
2+
3+
import pandas as pd
4+
import requests
5+
import numpy as np
6+
7+
URL = 'https://power.larc.nasa.gov/api/temporal/hourly/point'
8+
9+
DEFAULT_PARAMETERS = [
10+
'dni', 'dhi', 'ghi', 'temp_air', 'wind_speed'
11+
]
12+
13+
VARIABLE_MAP = {
14+
'ALLSKY_SFC_SW_DWN': 'ghi',
15+
'ALLSKY_SFC_SW_DIFF': 'dhi',
16+
'ALLSKY_SFC_SW_DNI': 'dni',
17+
'CLRSKY_SFC_SW_DWN': 'ghi_clear',
18+
'T2M': 'temp_air',
19+
'WS2M': 'wind_speed_2m',
20+
'WS10M': 'wind_speed',
21+
}
22+
23+
24+
def get_nasa_power(latitude, longitude, start, end,
25+
parameters=DEFAULT_PARAMETERS, *, community='re',
26+
elevation=None, wind_height=None, wind_surface=None,
27+
map_variables=True, url=URL):
28+
"""
29+
Retrieve irradiance and weather data from NASA POWER.
30+
31+
A general description of NASA POWER is given in [1]_ and the API is
32+
described in [2]_. A detailed list of the available parameters can be
33+
found in [3]_.
34+
35+
Parameters
36+
----------
37+
latitude: float
38+
In decimal degrees, north is positive (ISO 19115).
39+
longitude: float
40+
In decimal degrees, east is positive (ISO 19115).
41+
start: datetime like
42+
First timestamp of the requested period.
43+
end: datetime like
44+
Last timestamp of the requested period.
45+
parameters: str, list
46+
List of parameters. The default parameters are mentioned below; for the
47+
full list see [3]_. Note that the pvlib naming conventions can also be
48+
used.
49+
50+
* Global Horizontal Irradiance (GHI) [Wm⁻²]
51+
* Diffuse Horizontal Irradiance (DHI) [Wm⁻²]
52+
* Direct Normal Irradiance (DNI) [Wm⁻²]
53+
* Air temperature at 2 m [C]
54+
* Wind speed at 10 m [m/s]
55+
56+
community: str, default 're'
57+
Can be one of the following depending on which parameters are of
58+
interest. Note that in many cases this choice
59+
might affect the units of the parameter.
60+
61+
* ``'re'``: renewable energy
62+
* ``'sb'``: sustainable buildings
63+
* ``'ag'``: agroclimatology
64+
65+
elevation: float, optional
66+
The custom site elevation in meters to produce the corrected
67+
atmospheric pressure adjusted for elevation.
68+
wind_height: float, optional
69+
The custom wind height in meters to produce the wind speed adjusted
70+
for height. Has to be between 10 and 300 m; see [4]_.
71+
wind_surface: str, optional
72+
The definable surface type to adjust the wind speed. For a list of the
73+
surface types see [4]_. If you provide a wind surface alias please
74+
include a site elevation with the request.
75+
map_variables: bool, default True
76+
When true, renames columns of the Dataframe to pvlib variable names
77+
where applicable. See variable :const:`VARIABLE_MAP`.
78+
79+
Raises
80+
------
81+
requests.HTTPError
82+
Raises an error when an incorrect request is made.
83+
84+
Returns
85+
-------
86+
data : pd.DataFrame
87+
Time series data. The index corresponds to the start (left) of the
88+
interval.
89+
meta : dict
90+
Metadata.
91+
92+
References
93+
----------
94+
.. [1] `NASA Prediction Of Worldwide Energy Resources (POWER)
95+
<https://power.larc.nasa.gov/>`_
96+
.. [2] `NASA POWER API
97+
<https://power.larc.nasa.gov/api/pages/>`_
98+
.. [3] `NASA POWER API parameters
99+
<https://power.larc.nasa.gov/parameters/>`_
100+
.. [4] `NASA POWER corrected wind speed parameters
101+
<https://power.larc.nasa.gov/docs/methodology/meteorology/wind/>`_
102+
"""
103+
start = pd.Timestamp(start)
104+
end = pd.Timestamp(end)
105+
106+
# allow the use of pvlib parameter names
107+
parameter_dict = {v: k for k, v in VARIABLE_MAP.items()}
108+
parameters = [parameter_dict.get(p, p) for p in parameters]
109+
110+
params = {
111+
'latitude': latitude,
112+
'longitude': longitude,
113+
'start': start.strftime('%Y%m%d'),
114+
'end': end.strftime('%Y%m%d'),
115+
'community': community,
116+
'parameters': ','.join(parameters), # make parameters in a string
117+
'format': 'json',
118+
'user': None,
119+
'header': True,
120+
'time-standard': 'utc',
121+
'site-elevation': elevation,
122+
'wind-elevation': wind_height,
123+
'wind-surface': wind_surface,
124+
}
125+
126+
response = requests.get(url, params=params)
127+
if not response.ok:
128+
# response.raise_for_status() does not give a useful error message
129+
raise requests.HTTPError(response.json())
130+
131+
# Parse the data to dataframe
132+
data = response.json()
133+
hourly_data = data['properties']['parameter']
134+
df = pd.DataFrame(hourly_data)
135+
df.index = pd.to_datetime(df.index, format='%Y%m%d%H').tz_localize('UTC')
136+
137+
# Create metadata dictionary
138+
meta = data['header']
139+
meta['times'] = data['times']
140+
meta['parameters'] = data['parameters']
141+
142+
meta['longitude'] = data['geometry']['coordinates'][0]
143+
meta['latitude'] = data['geometry']['coordinates'][1]
144+
meta['altitude'] = data['geometry']['coordinates'][2]
145+
146+
# Replace NaN values
147+
df = df.replace(meta['fill_value'], np.nan)
148+
149+
# Rename according to pvlib convention
150+
if map_variables:
151+
df = df.rename(columns=VARIABLE_MAP)
152+
153+
return df, meta

tests/iotools/test_nasa_power.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import pandas as pd
2+
import pytest
3+
import pvlib
4+
from requests.exceptions import HTTPError
5+
from tests.conftest import RERUNS, RERUNS_DELAY
6+
7+
8+
@pytest.fixture
9+
def data_index():
10+
index = pd.date_range(start='2025-02-02 00:00+00:00',
11+
end='2025-02-02 23:00+00:00', freq='1h')
12+
return index
13+
14+
15+
@pytest.fixture
16+
def ghi_series(data_index):
17+
ghi = [
18+
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.25, 184.2, 281.55, 368.3, 406.48,
19+
386.45, 316.05, 210.1, 109.05, 12.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
20+
]
21+
return pd.Series(data=ghi, index=data_index, name='ghi')
22+
23+
24+
@pytest.mark.remote_data
25+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
26+
def test_get_nasa_power(data_index, ghi_series):
27+
data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
28+
longitude=7.64,
29+
start=data_index[0],
30+
end=data_index[-1],
31+
parameters=['ALLSKY_SFC_SW_DWN'],
32+
map_variables=False)
33+
# Check that metadata is correct
34+
assert meta['latitude'] == 44.76
35+
assert meta['longitude'] == 7.64
36+
assert meta['altitude'] == 705.88
37+
assert meta['start'] == '20250202'
38+
assert meta['end'] == '20250202'
39+
assert meta['time_standard'] == 'UTC'
40+
assert meta['title'] == 'NASA/POWER Source Native Resolution Hourly Data'
41+
# Assert that the index is parsed correctly
42+
pd.testing.assert_index_equal(data.index, data_index)
43+
# Test one column
44+
pd.testing.assert_series_equal(data['ALLSKY_SFC_SW_DWN'], ghi_series,
45+
check_freq=False, check_names=False)
46+
47+
48+
@pytest.mark.remote_data
49+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
50+
def test_get_nasa_power_pvlib_params_naming(data_index, ghi_series):
51+
data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
52+
longitude=7.64,
53+
start=data_index[0],
54+
end=data_index[-1],
55+
parameters=['ghi'])
56+
# Assert that the index is parsed correctly
57+
pd.testing.assert_index_equal(data.index, data_index)
58+
# Test one column
59+
pd.testing.assert_series_equal(data['ghi'], ghi_series,
60+
check_freq=False)
61+
62+
63+
@pytest.mark.remote_data
64+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
65+
def test_get_nasa_power_map_variables(data_index):
66+
# Check that variables are mapped by default to pvlib names
67+
data, meta = pvlib.iotools.get_nasa_power(latitude=44.76,
68+
longitude=7.64,
69+
start=data_index[0],
70+
end=data_index[-1])
71+
mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed']
72+
for c in mapped_column_names:
73+
assert c in data.columns
74+
assert meta['latitude'] == 44.76
75+
assert meta['longitude'] == 7.64
76+
assert meta['altitude'] == 705.88
77+
78+
79+
@pytest.mark.remote_data
80+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
81+
def test_get_nasa_power_wrong_parameter_name(data_index):
82+
# Test if HTTPError is raised if a wrong parameter name is asked
83+
with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DLN"):
84+
pvlib.iotools.get_nasa_power(latitude=44.76,
85+
longitude=7.64,
86+
start=data_index[0],
87+
end=data_index[-1],
88+
parameters=['ALLSKY_SFC_SW_DLN'])
89+
90+
91+
@pytest.mark.remote_data
92+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
93+
def test_get_nasa_power_duplicate_parameter_name(data_index):
94+
# Test if HTTPError is raised if a duplicate parameter is asked
95+
with pytest.raises(HTTPError, match=r"ALLSKY_SFC_SW_DWN"):
96+
pvlib.iotools.get_nasa_power(latitude=44.76,
97+
longitude=7.64,
98+
start=data_index[0],
99+
end=data_index[-1],
100+
parameters=2*['ALLSKY_SFC_SW_DWN'])

0 commit comments

Comments
 (0)