diff --git a/docs/sphinx/source/whatsnew/v0.15.1.rst b/docs/sphinx/source/whatsnew/v0.15.1.rst index 09bb0d01dd..fe2578d25a 100644 --- a/docs/sphinx/source/whatsnew/v0.15.1.rst +++ b/docs/sphinx/source/whatsnew/v0.15.1.rst @@ -14,6 +14,12 @@ Deprecations Bug fixes ~~~~~~~~~ +* Fix incorrect ``delta_kt_prime`` calculation in + :py:func:`pvlib.irradiance.dirint` for series containing internal NaN + values (e.g. multi-day data with nighttime gaps). Edge positions with + only one valid neighbor now correctly use that single delta value + (Perez eqn 3) instead of halving it. + (:issue:`1847`, :pull:`2698`) * Fix a division-by-zero condition in :py:func:`pvlib.transformer.simple_efficiency` when ``load_loss = 0``. (:issue:`2645`, :pull:`2646`) @@ -57,6 +63,7 @@ Maintenance Contributors ~~~~~~~~~~~~ +* Ishaan Arora (:ghuser:`ishaan-arora-1`) * Aman Srivastava (:ghuser:`aman-coder03`) * Rajiv Daxini (:ghuser:`RDaxini`) * Echedey Luis (:ghuser:`echedey-ls`) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index c67c1ca251..8399f53dbb 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -2026,16 +2026,15 @@ def _delta_kt_prime_dirint(kt_prime, use_delta_kt_prime, times): for use with :py:func:`_dirint_bins`. """ if use_delta_kt_prime: - # Perez eqn 2 + # Perez eqn 2 (both neighbors) and eqn 3 (one neighbor). + # mean(axis=1) skips NaN so that edge positions with only one + # valid neighbor return that single delta instead of halving it. kt_next = kt_prime.shift(-1) kt_previous = kt_prime.shift(1) - # replace nan with values that implement Perez Eq 3 for first and last - # positions. Use kt_previous and kt_next to handle series of length 1 - kt_next.iloc[-1] = kt_previous.iloc[-1] - kt_previous.iloc[0] = kt_next.iloc[0] - delta_kt_prime = 0.5 * ((kt_prime - kt_next).abs().add( - (kt_prime - kt_previous).abs(), - fill_value=0)) + delta_kt_prime = pd.DataFrame({ + 'next': (kt_prime - kt_next).abs(), + 'prev': (kt_prime - kt_previous).abs(), + }).mean(axis=1) else: # do not change unless also modifying _dirint_bins delta_kt_prime = pd.Series(-1, index=times) diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index a416636ae9..9b02594256 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -743,6 +743,38 @@ def test_dirint_no_delta_kt(): np.array([861.9, 670.4]), 1) +def test_delta_kt_prime_dirint_multiday(): + # GH 1847: _delta_kt_prime_dirint mishandled NaN boundaries between + # days, halving the delta at edge positions instead of using the + # single available neighbor value (Perez eqn 3). + times = pd.date_range('2014-01-01T05', periods=15, freq='1h', + tz='Etc/GMT+0') + kt_prime = pd.Series( + [np.nan, np.nan, np.nan, + 0.29458475, 0.21863506, 0.37650014, + 0.41238529, 0.23375275, 0.23363453, + 0.26348652, 0.25412631, 0.43794681, + np.nan, np.nan, np.nan], + index=times) + result = irradiance._delta_kt_prime_dirint(kt_prime, True, times) + # first valid value (index 3): only forward neighbor exists, + # delta_kt_prime == |kt[3] - kt[4]| (Perez eqn 3) + expected_first = abs(kt_prime.iloc[3] - kt_prime.iloc[4]) + assert_almost_equal(result.iloc[3], expected_first) + # last valid value (index 11): only backward neighbor exists, + # delta_kt_prime == |kt[11] - kt[10]| (Perez eqn 3) + expected_last = abs(kt_prime.iloc[11] - kt_prime.iloc[10]) + assert_almost_equal(result.iloc[11], expected_last) + # interior valid value (index 6): both neighbors exist, + # delta_kt_prime == mean of both deltas (Perez eqn 2) + expected_mid = 0.5 * (abs(kt_prime.iloc[6] - kt_prime.iloc[7]) + + abs(kt_prime.iloc[6] - kt_prime.iloc[5])) + assert_almost_equal(result.iloc[6], expected_mid) + # NaN positions should remain NaN + assert np.isnan(result.iloc[0]) + assert np.isnan(result.iloc[-1]) + + def test_dirint_coeffs(): coeffs = irradiance._get_dirint_coeffs() assert coeffs[0, 0, 0, 0] == 0.385230