Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 21 additions & 2 deletions pvlib/iotools/nasa_power.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@
'ALLSKY_SFC_SW_DWN': 'ghi',
'ALLSKY_SFC_SW_DIFF': 'dhi',
'ALLSKY_SFC_SW_DNI': 'dni',
'ALLSKY_SRF_ALB': 'albedo',
'ALLSKY_SFC_LW_DWN': 'longwave_down',
'CLRSKY_SFC_SW_DIFF': 'dhi_clear',
'CLRSKY_SFC_SW_DWN': 'ghi_clear',
'PS': 'pressure',
'RH2M': 'relative_humidity',
'T2M': 'temp_air',
'T2MDEW': 'temp_dew',
'TQV': 'precipitable_water',
Comment thread
karlhillx marked this conversation as resolved.
'TOA_SW_DWN': 'ghi_extra',
'WS2M': 'wind_speed_2m',
'WS10M': 'wind_speed',
'ALLSKY_SRF_ALB': 'albedo',
}


Expand Down Expand Up @@ -75,7 +82,10 @@ def get_nasa_power(latitude, longitude, start, end,
include a site elevation with the request.
map_variables: bool, default True
When true, renames columns of the Dataframe to pvlib variable names
where applicable. See variable :const:`VARIABLE_MAP`.
where applicable. See variable :const:`VARIABLE_MAP`. Note that the
following unit conversions are applied after renaming: pressure is
converted from kPa to Pa, and precipitable water is converted from
kg/m\u00b2 (mm) to cm.

Raises
------
Expand Down Expand Up @@ -150,5 +160,14 @@ def get_nasa_power(latitude, longitude, start, end,
# Rename according to pvlib convention
if map_variables:
df = df.rename(columns=VARIABLE_MAP)
# NASA POWER returns PS in kPa; pvlib convention is Pa.
# Conversion required for pvlib.atmosphere.pres2alt, alt2pres,
# get_absolute_airmass, and other pressure-dependent functions.
if 'pressure' in df.columns:
df['pressure'] = df['pressure'] * 1000
# NASA POWER returns TQV in kg/m^2 (=mm); pvlib convention is cm.
# Required for pvlib.atmosphere.kasten96_lt and other clear-sky models.
if 'precipitable_water' in df.columns:
df['precipitable_water'] = df['precipitable_water'] / 10

return df, meta
108 changes: 108 additions & 0 deletions tests/iotools/test_nasa_power.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,111 @@ def test_get_nasa_power_duplicate_parameter_name(data_index):
start=data_index[0],
end=data_index[-1],
parameters=2*['ALLSKY_SFC_SW_DWN'])


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_nasa_power_all_variable_map_parameters_valid():
"""
Every NASA POWER parameter name in VARIABLE_MAP must be accepted by the
live API. A typo or stale name (e.g. CLRSKY_DIFF vs CLRSKY_SFC_SW_DIFF)
causes the API to return an HTTPError, which would fail this test.

NASA POWER allows max 15 parameters per request; VARIABLE_MAP fits within
that. If the map grows past 15, split this test into batches.
"""
from pvlib.iotools.nasa_power import VARIABLE_MAP
nasa_params = list(VARIABLE_MAP.keys())
assert len(nasa_params) <= 15, (
"VARIABLE_MAP exceeds NASA POWER's 15-parameter-per-request limit; "
"split this test into batches."
)

data, meta = pvlib.iotools.get_nasa_power(
latitude=44.76,
longitude=7.64,
start='2025-02-02',
end='2025-02-02',
parameters=nasa_params,
map_variables=False,
)

# Every requested NASA parameter must come back as a column.
missing = set(nasa_params) - set(data.columns)
assert not missing, f"NASA POWER did not return: {sorted(missing)}"


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_nasa_power_all_variable_map_renamed():
"""
With map_variables=True every NASA name must be renamed to its pvlib
equivalent. Catches duplicate-target collisions (e.g. two entries both
mapping to 'temp_dew', which silently drops a column under pandas rename).
"""
from pvlib.iotools.nasa_power import VARIABLE_MAP
nasa_params = list(VARIABLE_MAP.keys())
pvlib_names = set(VARIABLE_MAP.values())

data, _ = pvlib.iotools.get_nasa_power(
latitude=44.76,
longitude=7.64,
start='2025-02-02',
end='2025-02-02',
parameters=nasa_params,
map_variables=True,
)

missing = pvlib_names - set(data.columns)
assert not missing, (
f"map_variables=True dropped pvlib columns: {sorted(missing)}. "
"Likely cause: duplicate target names in VARIABLE_MAP."
)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_nasa_power_pressure_unit_conversion():
"""
NASA POWER returns PS in kPa; pvlib convention is Pa.
Sea-level surface pressure should be ~101 kPa = ~101325 Pa, not ~101.
"""
data, _ = pvlib.iotools.get_nasa_power(
latitude=0.0, # sea-level equatorial point
longitude=-30.0,
start='2025-02-02',
end='2025-02-02',
parameters=['PS'],
map_variables=True,
)
mean_pressure = data['pressure'].mean()
# Anywhere on earth, surface pressure in Pa is between ~50k and ~110k.
# If the conversion is missing, value will be ~100 (kPa) and fail this.
assert 50_000 < mean_pressure < 110_000, (
f"PS not converted from kPa to Pa. Got mean={mean_pressure}"
)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_nasa_power_precipitable_water_unit_conversion():
"""
NASA POWER returns TQV in kg/m^2 (= mm of water column);
pvlib convention is cm. Typical atmospheric column is 1-5 cm,
never above ~7 cm. If the /10 conversion is missing, values will
be 10-50 and fail this bound.
"""
data, _ = pvlib.iotools.get_nasa_power(
latitude=0.0, # tropics: high water vapor, worst case for bounds
longitude=-60.0,
start='2025-02-02',
end='2025-02-02',
parameters=['TQV'],
map_variables=True,
)
mean_pw = data['precipitable_water'].mean()
# pvlib precipitable_water is in cm. Tropical column is <~7 cm.
# Missing /10 conversion would give 10-70 (kg/m^2 = mm).
assert 0 < mean_pw < 10, (
f"TQV not converted from kg/m^2 to cm. Got mean={mean_pw}"
)