diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 47dad5f2d3..28dec39504 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -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', + 'TOA_SW_DWN': 'ghi_extra', 'WS2M': 'wind_speed_2m', 'WS10M': 'wind_speed', - 'ALLSKY_SRF_ALB': 'albedo', } @@ -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 ------ @@ -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 diff --git a/tests/iotools/test_nasa_power.py b/tests/iotools/test_nasa_power.py index 4a0afe3587..0846fcb3b6 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -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}" + )