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
5 changes: 5 additions & 0 deletions docs/sphinx/source/whatsnew/v0.16.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Deprecations

Bug fixes
~~~~~~~~~
* :py:func:`pvlib.irradiance.gti_dirint` no longer returns NaN
irradiances caused by the zenith-independent clearness index
exceeding the DIRINT coefficient table; the value used for the
coefficient lookup is now clipped to the table boundary.
(:issue:`1815`)
* Correct the Series name in ``ModelChain.results.dc_ohmic_losses`` from
``i_mp`` to ``p_dc_ohmic_loss`` when using ``dc_ohms_from_percent``.
(:issue:`1959`, :pull:`2912`)
Expand Down
6 changes: 6 additions & 0 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -2220,6 +2220,12 @@ def _dirint_coeffs(times, kt_prime, solar_zenith, w, delta_kt_prime):
-------
dirint_coeffs : array-like
"""
# The coefficient table only covers kt_prime up to 1, so clip larger
# values to the table boundary instead of returning NaN coefficients
# for them. Only the value used for the coefficient lookup is
# clipped; kt_prime itself is unchanged everywhere else. See GH 1815.
kt_prime = np.minimum(kt_prime, 1.)

kt_prime_bin, zenith_bin, w_bin, delta_kt_prime_bin = \
_dirint_bins(times, kt_prime, solar_zenith, w, delta_kt_prime)

Expand Down
89 changes: 89 additions & 0 deletions tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,56 @@ def test_dirint_coeffs():
assert coeffs[3, 2, 6, 3] == 1.032260


def test_dirint_coeffs_kt_prime_gt_one():
# regression test for gh-1815: the coefficient table only covers
# kt_prime up to 1, so larger values must be clipped to the table
# boundary instead of producing NaN coefficients
times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T13-0700',
'2014-06-24T14-0700', '2014-06-24T15-0700'])
kt_prime = pd.Series([0.5, 1.0, 1.2, 2.0], index=times)
zenith = pd.Series(20., index=times)
w = pd.Series(-1, index=times)
delta_kt_prime = pd.Series(-1, index=times)

coeffs = irradiance._dirint_coeffs(times, kt_prime, zenith, w,
delta_kt_prime)

assert not np.isnan(coeffs).any()
# in-range values are unaffected, out-of-range values get the
# coefficients of the boundary kt_prime = 1
assert coeffs[0] != coeffs[1]
assert coeffs[1] == coeffs[2] == coeffs[3]

# reference: looking up the manually capped kt_prime must give
# exactly the same coefficients, i.e. the out-of-range values are
# clipped to the table boundary and not just binned together
kt_prime_capped = pd.Series([0.5, 1.0, 1.0, 1.0], index=times)
coeffs_capped = irradiance._dirint_coeffs(
times, kt_prime_capped, zenith, w, delta_kt_prime)
np.testing.assert_array_equal(coeffs, coeffs_capped)

# non-sentinel w and delta_kt_prime values: the clip must not
# disturb the other binning dimensions -- the result still matches
# the capped reference, and changing either w or delta_kt_prime
# alone still changes the selected coefficients
w = pd.Series(1.5, index=times)
delta_kt_prime = pd.Series(0.05, index=times)
coeffs_nonsentinel = irradiance._dirint_coeffs(
times, kt_prime, zenith, w, delta_kt_prime)
coeffs_nonsentinel_capped = irradiance._dirint_coeffs(
times, kt_prime_capped, zenith, w, delta_kt_prime)
np.testing.assert_array_equal(coeffs_nonsentinel,
coeffs_nonsentinel_capped)
assert not (coeffs_nonsentinel == coeffs).all()
coeffs_w = irradiance._dirint_coeffs(
times, kt_prime, zenith, w, pd.Series(-1, index=times))
assert not (coeffs_w == coeffs).all()
coeffs_delta = irradiance._dirint_coeffs(
times, kt_prime, zenith, pd.Series(-1, index=times),
delta_kt_prime)
assert not (coeffs_delta == coeffs).all()


def test_dirint_min_cos_zenith_max_zenith():
# map out behavior under difficult conditions with various
# limiting kwargs settings
Expand Down Expand Up @@ -1115,6 +1165,45 @@ def test_gti_dirint():
assert_frame_equal(output, expected)


def test_gti_dirint_kt_prime_gt_one(mocker):
# regression test for gh-1815: poa_global values large enough to
# make kt_prime exceed 1 used to produce NaN dni and dhi, including
# for AOI >= 90 degrees
times = pd.DatetimeIndex(
['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700'])
poa_global = np.array([40, 600, 2000])
aoi = np.array([100, 70, 10])
zenith = np.array([80, 45, 20])
azimuth = np.array([90, 135, 180])
surface_tilt = 30
surface_azimuth = 180

coeffs_spy = mocker.spy(irradiance, '_dirint_coeffs')

output = irradiance.gti_dirint(
poa_global, aoi, zenith, azimuth, times, surface_tilt,
surface_azimuth)

assert not output.isna().any().any()

# the scenario really does exercise the kt_prime > 1 path: the
# coefficient lookup is called with unclipped kt_prime values, some
# of which exceed 1 (max seen is about 1.54)
kt_prime_seen = [call[0][1] for call in coeffs_spy.call_args_list]
assert max(np.max(kt_prime) for kt_prime in kt_prime_seen) > 1

# expected values produced by the fixed implementation; its output
# for the original (unscaled, kt_prime <= 1) test_gti_dirint inputs
# is unchanged, as verified against the pre-fix behavior
expected = pd.DataFrame(array(
[[92.98152465, 298.18105688, 41.20292751],
[524.21478016, 284.52281756, 323.02676646],
[1908.37778828, 0., 1908.37778828]]),
columns=['ghi', 'dni', 'dhi'], index=times)

assert_frame_equal(output, expected)


def test_gti_dirint_data_error():
times = pd.DatetimeIndex(
['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700'])
Expand Down
Loading