Skip to content

Commit 0faccdc

Browse files
authored
Merge branch 'main' into add-era5-land
2 parents 0355369 + 1703cef commit 0faccdc

5 files changed

Lines changed: 175 additions & 12 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Deprecations
1818

1919
Bug fixes
2020
~~~~~~~~~
21+
* Added test coverage for :py:func:`pvlib.irradiance.dirint` with
22+
``np.array`` and ``pd.Series`` inputs.
23+
(:issue:`2751`, :pull:`2752`)
2124
* Corrects a bug in :py:func:`pvlib.temperature.fuentes`. If inputs were
2225
data type integer, users can expect modeled cell temperature values to
2326
increase slightly.
@@ -40,12 +43,14 @@ Enhancements
4043
* Add the ``front_side_fraction`` parameter to
4144
:py:func:`pvlib.snow.loss_townsend` to support Townsend snow-loss
4245
workflows for bifacial systems. (:issue:`2755`, :pull:`2756`)
43-
* Add mapping of the parameter ``"albedo"`` in
44-
:py:func:`~pvlib.iotools.get_nasa_power` when ``map_variables=True``
45-
(:pull:`2753`)
4646
* Add ``dataset`` parameter to :py:func:`~pvlib.iotools.get_era5`
4747
allowing for choosing between ERA5 and ERA5-Land datasets.
4848
(pull:`2780`)
49+
* Add the following parameters to :py:func:`~pvlib.iotools.get_nasa_power`
50+
when ``map_variables=True``: ``temp_dew``, ``precipitable_water``,
51+
``relative_humidity``, ``ghi_extra``, ``dhi_clear``, ``longwave_down``,
52+
and ``albedo``.
53+
(:issue:`2731`, :pull:`2753`, :pull:`2762`)
4954

5055
Documentation
5156
~~~~~~~~~~~~~
@@ -81,4 +86,5 @@ Contributors
8186
* :ghuser:`shethkajal7`
8287
* Arthur Onno (:ghuser:`ArthurOnnoTerabase`)
8388
* Adam R. Jensen (:ghuser:`AdamRJensen`)
89+
* Karl Hill (:ghuser:`karlhillx`)
8490
* Rajiv Daxini (:ghuser:`RDaxini`)

pvlib/iotools/nasa_power.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,19 @@
1414
'ALLSKY_SFC_SW_DWN': 'ghi',
1515
'ALLSKY_SFC_SW_DIFF': 'dhi',
1616
'ALLSKY_SFC_SW_DNI': 'dni',
17+
'ALLSKY_SRF_ALB': 'albedo',
18+
'ALLSKY_SFC_LW_DWN': 'longwave_down',
19+
'CLRSKY_SFC_SW_DIFF': 'dhi_clear',
20+
'CLRSKY_SFC_SW_DNI': 'dni_clear',
1721
'CLRSKY_SFC_SW_DWN': 'ghi_clear',
22+
'PS': 'pressure',
23+
'RH2M': 'relative_humidity',
1824
'T2M': 'temp_air',
25+
'T2MDEW': 'temp_dew',
26+
'TQV': 'precipitable_water',
27+
'TOA_SW_DWN': 'ghi_extra',
1928
'WS2M': 'wind_speed_2m',
2029
'WS10M': 'wind_speed',
21-
'ALLSKY_SRF_ALB': 'albedo',
2230
}
2331

2432

@@ -82,6 +90,12 @@ def get_nasa_power(latitude, longitude, start, end,
8290
requests.HTTPError
8391
Raises an error when an incorrect request is made.
8492
93+
Notes
94+
-----
95+
When ``map_variables=True`` the following unit conversions are applied:
96+
pressure is converted from kPa to Pa, and precipitable water
97+
is converted from kg/m² (mm) to cm.
98+
8599
Returns
86100
-------
87101
data : pd.DataFrame
@@ -150,5 +164,11 @@ def get_nasa_power(latitude, longitude, start, end,
150164
# Rename according to pvlib convention
151165
if map_variables:
152166
df = df.rename(columns=VARIABLE_MAP)
167+
# PS is returned in kPa; convert to Pa for pvlib compatibility.
168+
if 'pressure' in df.columns:
169+
df['pressure'] = df['pressure'] * 1000
170+
# TQV is returned in kg/m^2 (=mm); convert to cm for compatibility
171+
if 'precipitable_water' in df.columns:
172+
df['precipitable_water'] = df['precipitable_water'] / 10
153173

154174
return df, meta

pvlib/irradiance.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ def _handle_extra_radiation_types(datetime_or_doy, epoch_year):
132132
# a better way to do it.
133133
if isinstance(datetime_or_doy, pd.DatetimeIndex):
134134
to_doy = tools._pandas_to_doy # won't be evaluated unless necessary
135-
def to_datetimeindex(x): return x # noqa: E306
135+
136+
def to_datetimeindex(x):
137+
return x # noqa: E306
136138
to_output = partial(pd.Series, index=datetime_or_doy)
137139
elif isinstance(datetime_or_doy, pd.Timestamp):
138140
to_doy = tools._pandas_to_doy
@@ -146,12 +148,14 @@ def to_datetimeindex(x): return x # noqa: E306
146148
tools._datetimelike_scalar_to_datetimeindex
147149
to_output = tools._scalar_out
148150
elif np.isscalar(datetime_or_doy): # ints and floats of various types
149-
def to_doy(x): return x # noqa: E306
151+
def to_doy(x):
152+
return x # noqa: E306
150153
to_datetimeindex = partial(tools._doy_to_datetimeindex,
151154
epoch_year=epoch_year)
152155
to_output = tools._scalar_out
153156
else: # assume that we have an array-like object of doy
154-
def to_doy(x): return x # noqa: E306
157+
def to_doy(x):
158+
return x # noqa: E306
155159
to_datetimeindex = partial(tools._doy_to_datetimeindex,
156160
epoch_year=epoch_year)
157161
to_output = tools._array_out
@@ -1972,14 +1976,14 @@ def dirint(ghi, solar_zenith, times, pressure=101325., use_delta_kt_prime=True,
19721976
19731977
Returns
19741978
-------
1975-
dni : array-like
1976-
The modeled direct normal irradiance, as provided by the
1977-
DIRINT model. [Wm⁻²]
1979+
dni : pd.Series
1980+
Estimated direct normal irradiance. [Wm⁻²]
19781981
19791982
Notes
19801983
-----
1981-
DIRINT model requires time series data (ie. one of the inputs must
1982-
be a vector of length > 2).
1984+
The DIRINT model was developed for time series data with length > 2.
1985+
The implementation in pvlib assumes the data are periodic which may
1986+
affect the first and last DNI values.
19831987
19841988
References
19851989
----------
@@ -2117,6 +2121,7 @@ def _dirint_bins(times, kt_prime, zenith, w, delta_kt_prime):
21172121
-------
21182122
tuple of kt_prime_bin, zenith_bin, w_bin, delta_kt_prime_bin
21192123
"""
2124+
21202125
# @wholmgren: the following bin assignments use MATLAB's 1-indexing.
21212126
# Later, we'll subtract 1 to conform to Python's 0-indexing.
21222127

tests/iotools/test_nasa_power.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,110 @@ def test_get_nasa_power_duplicate_parameter_name(data_index):
9898
start=data_index[0],
9999
end=data_index[-1],
100100
parameters=2*['ALLSKY_SFC_SW_DWN'])
101+
102+
103+
@pytest.mark.remote_data
104+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
105+
def test_get_nasa_power_all_variable_map_parameters_valid():
106+
"""
107+
Every NASA POWER parameter name in VARIABLE_MAP must be accepted by the
108+
live API. A typo or stale name (e.g. CLRSKY_DIFF vs CLRSKY_SFC_SW_DIFF)
109+
causes the API to return an HTTPError, which would fail this test.
110+
111+
NASA POWER allows max 15 parameters per request; VARIABLE_MAP is split
112+
into two batches to stay within that limit.
113+
"""
114+
from pvlib.iotools.nasa_power import VARIABLE_MAP
115+
nasa_params = list(VARIABLE_MAP.keys())
116+
# Split into two batches; API limit is 15.
117+
half = (len(nasa_params) + 1) // 2
118+
batch1 = nasa_params[:half]
119+
batch2 = nasa_params[half:]
120+
121+
for batch in [batch1, batch2]:
122+
data, meta = pvlib.iotools.get_nasa_power(
123+
latitude=44.76,
124+
longitude=7.64,
125+
start='2025-02-02',
126+
end='2025-02-02',
127+
parameters=batch,
128+
map_variables=False,
129+
)
130+
missing = set(batch) - set(data.columns)
131+
assert not missing, f"NASA POWER did not return: {sorted(missing)}"
132+
133+
134+
@pytest.mark.remote_data
135+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
136+
def test_get_nasa_power_all_variable_map_renamed():
137+
"""
138+
With map_variables=True every NASA name must be renamed to its pvlib
139+
equivalent. Catches duplicate-target collisions (e.g. two entries both
140+
mapping to 'temp_dew', which silently drops a column under pandas rename).
141+
"""
142+
from pvlib.iotools.nasa_power import VARIABLE_MAP
143+
nasa_params = list(VARIABLE_MAP.keys())
144+
pvlib_names = set(VARIABLE_MAP.values())
145+
146+
data, _ = pvlib.iotools.get_nasa_power(
147+
latitude=44.76,
148+
longitude=7.64,
149+
start='2025-02-02',
150+
end='2025-02-02',
151+
parameters=nasa_params,
152+
map_variables=True,
153+
)
154+
155+
missing = pvlib_names - set(data.columns)
156+
assert not missing, (
157+
f"map_variables=True dropped pvlib columns: {sorted(missing)}. "
158+
"Likely cause: duplicate target names in VARIABLE_MAP."
159+
)
160+
161+
162+
@pytest.mark.remote_data
163+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
164+
def test_get_nasa_power_pressure_unit_conversion():
165+
"""
166+
NASA POWER returns PS in kPa; pvlib convention is Pa.
167+
Sea-level surface pressure should be ~101 kPa = ~101325 Pa, not ~101.
168+
"""
169+
data, _ = pvlib.iotools.get_nasa_power(
170+
latitude=0.0, # sea-level equatorial point
171+
longitude=-30.0,
172+
start='2025-02-02',
173+
end='2025-02-02',
174+
parameters=['PS'],
175+
map_variables=True,
176+
)
177+
mean_pressure = data['pressure'].mean()
178+
# Anywhere on earth, surface pressure in Pa is between ~50k and ~110k.
179+
# If the conversion is missing, value will be ~100 (kPa) and fail this.
180+
assert 50_000 < mean_pressure < 110_000, (
181+
f"PS not converted from kPa to Pa. Got mean={mean_pressure}"
182+
)
183+
184+
185+
@pytest.mark.remote_data
186+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
187+
def test_get_nasa_power_precipitable_water_unit_conversion():
188+
"""
189+
NASA POWER returns TQV in kg/m^2 (= mm of water column);
190+
pvlib convention is cm. Typical atmospheric column is 1-5 cm,
191+
never above ~7 cm. If the /10 conversion is missing, values will
192+
be 10-50 and fail this bound.
193+
"""
194+
data, _ = pvlib.iotools.get_nasa_power(
195+
latitude=0.0, # tropics: high water vapor, worst case for bounds
196+
longitude=-60.0,
197+
start='2025-02-02',
198+
end='2025-02-02',
199+
parameters=['TQV'],
200+
map_variables=True,
201+
)
202+
mean_pw = data['precipitable_water'].mean()
203+
# pvlib precipitable_water is in cm. Tropical column is <~7 cm.
204+
# Missing /10 conversion would give 10-70 (kg/m^2 = mm).
205+
assert 0 < mean_pw < 10, (
206+
f"TQV not converted from kg/m^2 to cm. Got mean={mean_pw}"
207+
)

tests/test_irradiance.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,31 @@ def test_dirindex_min_cos_zenith_max_zenith():
11471147
assert_series_equal(out, expected)
11481148

11491149

1150+
def test_dirint_array_inputs():
1151+
"""np.array and pd.Series inputs work correctly. GH #2751"""
1152+
times = pd.date_range('2023-06-21 10:00', periods=2, freq='h', tz='UTC')
1153+
1154+
# np.array input → should return pd.Series
1155+
result = irradiance.dirint(
1156+
ghi=np.array([500.0, 400.0]),
1157+
solar_zenith=np.array([45.0, 50.0]),
1158+
times=times,
1159+
use_delta_kt_prime=False
1160+
)
1161+
assert isinstance(result, pd.Series)
1162+
assert result.iloc[0] > 0
1163+
1164+
# pd.Series input → should return pd.Series
1165+
times2 = pd.date_range('2023-06-21 10:00', periods=3, freq='h', tz='UTC')
1166+
result2 = irradiance.dirint(
1167+
ghi=pd.Series([400, 500, 300], index=times2),
1168+
solar_zenith=pd.Series([50, 40, 60], index=times2),
1169+
times=times2
1170+
)
1171+
assert isinstance(result2, pd.Series)
1172+
assert (result2 >= 0).all()
1173+
1174+
11501175
def test_dni():
11511176
ghi = pd.Series([90, 100, 100, 100, 100])
11521177
dhi = pd.Series([100, 90, 50, 50, 50])

0 commit comments

Comments
 (0)