From 60a489e40b549a0a2ceaede4debb8bd82baf60e9 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Mon, 8 Jun 2026 11:30:38 +0100 Subject: [PATCH 01/16] Add support for return_components to Reindl --- pvlib/irradiance.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 5a4a76df25..ce0880d1b5 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -873,7 +873,7 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, - solar_zenith, solar_azimuth): + solar_zenith, solar_azimuth, return_components=False): r''' Determine the diffuse irradiance from the sky on a tilted surface using the Reindl (1990) model. @@ -912,6 +912,10 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, solar_azimuth : numeric Solar azimuth angles. See :term:`solar_azimuth`. [°] + return_components : bool, default False + Flag used to decide whether to return the calculated diffuse components + or not. + Returns ------- poa_sky_diffuse : numeric @@ -973,16 +977,32 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, HB = dni * cos_solar_zenith HB = np.maximum(HB, 0) - # these are the () and [] sub-terms of the second term of eqn 8 - term1 = 1 - AI - term2 = 0.5 * (1 + tools.cosd(surface_tilt)) + I = (1 + tools.cosd(surface_tilt)) / 2 + with np.errstate(invalid='ignore', divide='ignore'): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) - term3 = 1 + np.sqrt(hb_to_ghi) * (tools.sind(0.5 * surface_tilt)**3) - sky_diffuse = dhi * (AI * Rb + term1 * term2 * term3) - sky_diffuse = np.maximum(sky_diffuse, 0) + h = np.sqrt(hb_to_ghi) * (tools.sind(surface_tilt / 2) ** 3) - return sky_diffuse + term1 = (1 - AI) * I + term2 = AI * Rb + term3 = (1 - AI) * I * h + + sky_diffuse = dhi * (term1 + term2 + term3) + + if return_components: + diffuse_components = OrderedDict() + diffuse_components['poa_sky_diffuse'] = sky_diffuse + + # Calculate the individual components + diffuse_components['poa_isotropic'] = dhi * term1 + diffuse_components['poa_circumsolar'] = dhi * term2 + diffuse_components['poa_horizon'] = dhi * term3 + + if isinstance(sky_diffuse, pd.Series): + diffuse_components = pd.DataFrame(diffuse_components) + return diffuse_components + else: + return sky_diffuse def king(surface_tilt, dhi, ghi, solar_zenith): From 3cbc4adcf62d4023254b03bcad198258510c61c1 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Mon, 8 Jun 2026 12:31:18 +0100 Subject: [PATCH 02/16] Update docstrings for Reindl --- pvlib/irradiance.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index ce0880d1b5..9a7847055b 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -918,8 +918,21 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Returns ------- - poa_sky_diffuse : numeric - The sky diffuse component of the solar radiation. [Wm⁻²] + numeric, OrderedDict, or DataFrame + Return type controlled by ``return_components`` argument. + If ``return_components=False``, `sky_diffuse` is returned. + If ``return_components=True``, `diffuse_components` is returned. + + sky_diffuse : numeric + The sky diffuse component of the solar radiation on a tilted + surface. + + diffuse_components : OrderedDict (array input) or DataFrame (Series input) + Keys/columns are: + * poa_sky_diffuse: Total sky diffuse + * poa_isotropic + * poa_circumsolar + * poa_horizon Notes ----- @@ -943,8 +956,9 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Implementation is based on Loutzenhiser et al. (2007) [3]_, Equation 8. The beam and ground reflectance portion of the equation have been removed, therefore the model described here generates - ONLY the diffuse radiation from the sky and circumsolar, so the form of the - equation varies slightly from Equation 8 in [3]_. + ONLY the diffuse radiation from the sky, circumsolar, and horizon + brightening, so the form of the equation varies slightly from Equation 8 + in [3]_. References ---------- From 044ad59be476e51ad25a7895208ac5befa06b85a Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 9 Jun 2026 23:41:47 +0200 Subject: [PATCH 03/16] `read_nsrdb_psm4`: parse header with the `csv` module to keep quoted commas (#2771) read_nsrdb_psm4 split the three header lines with a naive str.split(','), which broke spectral-on-demand files whose column names are quoted fields containing commas (e.g. '"GaAs (Bauhuis et al., 2009)"'). Such names were split into spurious columns, raising on read. Parse the header lines with the csv module so quoted fields are kept intact. Fixes #2736 --- docs/sphinx/source/whatsnew/v0.15.2.rst | 5 +++++ pvlib/iotools/psm4.py | 12 +++++++++--- tests/iotools/test_psm4.py | 25 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 35d7dc675c..51bec8822d 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -26,6 +26,10 @@ Bug fixes represent the end of the averaging interval, consistent with ERA5 conventions. (:issue:`2772`, :pull:`2773`) +* :py:func:`pvlib.iotools.read_nsrdb_psm4` now parses the file header with the + :py:mod:`csv` module instead of a naive ``str.split(',')``, so quoted column + names containing commas (e.g. the material names in spectral-on-demand files) + are no longer split into spurious columns. (:issue:`2736`, :pull:`2771`) Enhancements ~~~~~~~~~~~~ @@ -67,6 +71,7 @@ Maintenance Contributors ~~~~~~~~~~~~ * :ghuser:`Omesh37` +* :ghuser:`gaoflow` * Cliff Hansen (:ghuser:`cwhanse`) * :ghuser:`shethkajal7` * Arthur Onno (:ghuser:`ArthurOnnoTerabase`) diff --git a/pvlib/iotools/psm4.py b/pvlib/iotools/psm4.py index 9eb760f382..fc8d098a09 100644 --- a/pvlib/iotools/psm4.py +++ b/pvlib/iotools/psm4.py @@ -6,6 +6,7 @@ https://developer.nlr.gov/docs/solar/nsrdb/nsrdb-GOES-full-disc-v4-0-0-download/ """ +import csv import io from urllib.parse import urljoin import requests @@ -723,11 +724,16 @@ def read_nsrdb_psm4(filename, map_variables=True): `_ """ with tools._file_context_manager(filename) as fbuf: + # The first 3 header lines are parsed with the csv module rather than a + # naive str.split(',') so that quoted fields containing commas are kept + # intact. Spectral-on-demand files, for instance, have column names + # like '"GaAs (Bauhuis et al., 2009)"' whose embedded commas would + # otherwise be split into spurious columns (see GH #2736). # The first 2 lines of the response are headers with metadata - metadata_fields = fbuf.readline().split(',') - metadata_values = fbuf.readline().split(',') + metadata_fields = next(csv.reader([fbuf.readline()])) + metadata_values = next(csv.reader([fbuf.readline()])) # get the column names so we can set the dtypes - columns = fbuf.readline().split(',') + columns = next(csv.reader([fbuf.readline()])) columns[-1] = columns[-1].strip() # strip trailing newline # Since the header has so many columns, excel saves blank cols in the # data below the header lines. diff --git a/tests/iotools/test_psm4.py b/tests/iotools/test_psm4.py index 3b4313b070..c16a714aa3 100644 --- a/tests/iotools/test_psm4.py +++ b/tests/iotools/test_psm4.py @@ -185,6 +185,31 @@ def test_read_nsrdb_psm4_map_variables(): assert_index_equal(data.columns, pd.Index(columns_mapped)) +def test_read_nsrdb_psm4_quoted_columns_with_commas(): + """spectral-on-demand files have quoted column names containing commas; + these must not be split into spurious columns (GH #2736)""" + # Minimal NSRDB file whose column header (3rd line) has quoted material + # names with embedded commas, which is valid CSV. A naive str.split(',') + # would break these into extra columns and raise on read. + content = ( + "Source,Location ID,City,State,Country,Latitude,Longitude,Time Zone," + "Elevation,Local Time Zone,Version\n" + "NSRDB,1,-,-,-,40.0,-105.0,-7,1600,-7,4.0.1\n" + 'Year,Month,Day,Hour,Minute,GHI,"GaAs (Bauhuis et al., 2009)",' + '"InGaP (Gray, 2008)"\n' + "2023,1,1,0,0,0,0.1,0.2\n" + "2023,1,1,1,0,5,0.3,0.4\n" + ) + data, metadata = psm4.read_nsrdb_psm4(StringIO(content), + map_variables=False) + assert list(data.columns) == [ + 'Year', 'Month', 'Day', 'Hour', 'Minute', 'GHI', + 'GaAs (Bauhuis et al., 2009)', 'InGaP (Gray, 2008)'] + assert data.shape == (2, 8) + # the embedded-comma data columns round-trip as floats + assert data['GaAs (Bauhuis et al., 2009)'].tolist() == [0.1, 0.3] + + @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_nsrdb_psm4_aggregated_parameter_mapping(nlr_api_key): From 7929b387abd1d73c6141cc1b70d6338b2478e4da Mon Sep 17 00:00:00 2001 From: Rajiv Daxini <143435106+RDaxini@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:15:59 -0500 Subject: [PATCH 04/16] Rename `ghi_from_poa_driesse_2023` to `ghi_from_poa_driesse_2024` (#2777) * Update irradiance.py * Update test_irradiance.py * update sphinx refs, examples * Update v0.15.2.rst * flake8 * test deprecation * Update v0.15.2.rst --- .../plot_rtranpose_limitations.py | 8 ++++---- .../plot_rtranpose_year.py | 6 +++--- .../irradiance/reverse-transposition.rst | 2 +- docs/sphinx/source/whatsnew/v0.15.2.rst | 5 +++++ pvlib/irradiance.py | 16 ++++++++++++---- tests/test_irradiance.py | 19 +++++++++++++------ 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py index 8df8339ad4..fca9557db2 100644 --- a/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py +++ b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py @@ -28,7 +28,7 @@ # # In this example we look at a single point in time and consider a full range # of possible GHI and POA global values as shown in figures 3 and 4 of [1]_. -# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate +# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2024` to estimate # the original GHI from POA global. # # References @@ -45,7 +45,7 @@ from pvlib.irradiance import (erbs_driesse, get_total_irradiance, - ghi_from_poa_driesse_2023, + ghi_from_poa_driesse_2024, ) matplotlib.rcParams['axes.grid'] = True @@ -92,7 +92,7 @@ poa_test = 200 -ghi_hat = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, +ghi_hat = ghi_from_poa_driesse_2024(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, poa_test, dni_extra, @@ -156,7 +156,7 @@ # out, other times not. # -result = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, +result = ghi_from_poa_driesse_2024(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, poa_global, dni_extra, diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_year.py b/docs/examples/irradiance-transposition/plot_rtranpose_year.py index c0b9860bba..b373ef1a25 100644 --- a/docs/examples/irradiance-transposition/plot_rtranpose_year.py +++ b/docs/examples/irradiance-transposition/plot_rtranpose_year.py @@ -21,7 +21,7 @@ # Recovering GHI from POA irradiance is termed "reverse transposition." # # In this example we start with a TMY file and calculate POA global irradiance. -# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate +# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2024` to estimate # the original GHI from POA global. Details of the method found in [1]_. # # Another method for reverse tranposition called GTI-DIRINT is also @@ -49,7 +49,7 @@ from pvlib import iotools, location from pvlib.irradiance import (get_extra_radiation, get_total_irradiance, - ghi_from_poa_driesse_2023, + ghi_from_poa_driesse_2024, aoi, ) @@ -114,7 +114,7 @@ start = time.process_time() -df['ghi_rev'] = ghi_from_poa_driesse_2023(TILT, ORIENT, +df['ghi_rev'] = ghi_from_poa_driesse_2024(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth, df.poa_global, diff --git a/docs/sphinx/source/reference/irradiance/reverse-transposition.rst b/docs/sphinx/source/reference/irradiance/reverse-transposition.rst index 31ce27bedd..55e1d40e36 100644 --- a/docs/sphinx/source/reference/irradiance/reverse-transposition.rst +++ b/docs/sphinx/source/reference/irradiance/reverse-transposition.rst @@ -6,5 +6,5 @@ Reverse transposition models .. autosummary:: :toctree: ../generated/ - irradiance.ghi_from_poa_driesse_2023 + irradiance.ghi_from_poa_driesse_2024 irradiance.gti_dirint diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index 51bec8822d..ed237f4720 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -10,6 +10,10 @@ Breaking Changes Deprecations ~~~~~~~~~~~~ +* Rename :py:func:`!pvlib.irradiance.ghi_from_poa_driesse_2023` to + :py:func:`~pvlib.irradiance.ghi_from_poa_driesse_2024`. The year now reflects + the publication date. The old name will be removed in v0.17.0. + (:issue:`2774`, :pull:`2777`) Bug fixes @@ -76,3 +80,4 @@ Contributors * :ghuser:`shethkajal7` * Arthur Onno (:ghuser:`ArthurOnnoTerabase`) * Adam R. Jensen (:ghuser:`AdamRJensen`) +* Rajiv Daxini (:ghuser:`RDaxini`) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 50f02426de..471ce9b93a 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -16,7 +16,7 @@ from pvlib import atmosphere, solarposition, tools import pvlib # used to avoid dni name collision in complete_irradiance -from pvlib._deprecation import pvlibDeprecationWarning +from pvlib._deprecation import pvlibDeprecationWarning, deprecated import warnings @@ -1442,7 +1442,7 @@ def _poa_from_ghi(surface_tilt, surface_azimuth, Transposition function that includes decomposition of GHI using the continuous Erbs-Driesse model. - Helper function for ghi_from_poa_driesse_2023. + Helper function for ghi_from_poa_driesse_2024. ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023 @@ -1468,7 +1468,7 @@ def _ghi_from_poa(surface_tilt, surface_azimuth, ''' Reverse transposition function that uses the scalar bisection from scipy. - Helper function for ghi_from_poa_driesse_2023. + Helper function for ghi_from_poa_driesse_2024. ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023 @@ -1512,7 +1512,7 @@ def poa_error(ghi): return ghi, conv, niter -def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, +def ghi_from_poa_driesse_2024(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, poa_global, dni_extra, airmass=None, albedo=0.25, @@ -1614,6 +1614,14 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, return ghi +ghi_from_poa_driesse_2023 = deprecated( + since="0.15.2", + name="pvlib.irradiance.ghi_from_poa_driesse_2023", + alternative="pvlib.irradiance.ghi_from_poa_driesse_2024", + removal="0.17.0", +)(ghi_from_poa_driesse_2024) + + def clearsky_index(ghi, ghi_clear, max_clearsky_index=2.0): """ Calculate the clearsky index. diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index a416636ae9..9925d99902 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -792,7 +792,7 @@ def test_ghi_from_poa_driesse(mocker): surface_azimuth = 180 # test core function - output = irradiance.ghi_from_poa_driesse_2023( + output = irradiance.ghi_from_poa_driesse_2024( surface_tilt, surface_azimuth, zenith, azimuth, poa_global, dni_extra=1366.1) @@ -802,7 +802,7 @@ def test_ghi_from_poa_driesse(mocker): # test series output poa_global = pd.Series([20, 300, 1000], index=times) - output = irradiance.ghi_from_poa_driesse_2023( + output = irradiance.ghi_from_poa_driesse_2024( surface_tilt, surface_azimuth, zenith, azimuth, poa_global, dni_extra=1366.1) @@ -811,7 +811,7 @@ def test_ghi_from_poa_driesse(mocker): # test full_output option and special cases poa_global = np.array([0, 1500, np.nan]) - ghi, conv, niter = irradiance.ghi_from_poa_driesse_2023( + ghi, conv, niter = irradiance.ghi_from_poa_driesse_2024( surface_tilt, surface_azimuth, zenith, azimuth, poa_global, dni_extra=1366.1, full_output=True) @@ -828,19 +828,26 @@ def test_ghi_from_poa_driesse(mocker): poa_global = pd.Series([20, 300, 1000], index=times) # test exception xtol = -3.14159 # negative value raises exception in scipy.optimize.bisect - with pytest.raises(ValueError, match=rf"xtol too small \({xtol:g} <= 0\)"): - output = irradiance.ghi_from_poa_driesse_2023( + with pytest.raises(ValueError, match=rf"xtol too small \({xtol} <= 0\)"): + output = irradiance.ghi_from_poa_driesse_2024( surface_tilt, surface_azimuth, zenith, azimuth, poa_global, dni_extra=1366.1, xtol=xtol) # test propagation xtol = 3.141592 bisect_spy = mocker.spy(irradiance, "bisect") - output = irradiance.ghi_from_poa_driesse_2023( + output = irradiance.ghi_from_poa_driesse_2024( surface_tilt, surface_azimuth, zenith, azimuth, poa_global, dni_extra=1366.1, xtol=xtol) assert bisect_spy.call_args[1]["xtol"] == xtol +def test_ghi_from_poa_driesse_2023_deprecated(): + with pytest.warns(pvlibDeprecationWarning, + match="ghi_from_poa_driesse_2024"): + irradiance.ghi_from_poa_driesse_2023( + 30, 180, 20, 180, 500, dni_extra=1366.1) + + def test_gti_dirint(): times = pd.DatetimeIndex( ['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700']) From 66d43b342dec7faf49ff5fa6e61444f27d187c72 Mon Sep 17 00:00:00 2001 From: OMESH CHANDURE Date: Wed, 10 Jun 2026 19:55:51 +0530 Subject: [PATCH 05/16] Add test coverage for `irradiance.dirint` for array and series inputs (#2752) * BUG: fix dirint KeyError with scalar inputs on pandas >= 2.0 * TST,CHANGELOG: add test and whatsnew entry for dirint scalar fix GH#2751 * BUG: fix dirint KeyError for scalar inputs on pandas >= 2.0 * STY: fix flake8 whitespace and blank line errors * BUG: add test coverage for dirint with array-like inputs GH#2751 * Update pvlib/irradiance.py Co-authored-by: Cliff Hansen * Update pvlib/irradiance.py Co-authored-by: Cliff Hansen * TST: fix test_dirint_array_inputs to use 2 time points * BUG: raise ValueError in dirint when use_delta_kt_prime=True and len(times)<2 * Update pvlib/irradiance.py Co-authored-by: Cliff Hansen * Update pvlib/irradiance.py Co-authored-by: Cliff Hansen * Update tests/test_irradiance.py Co-authored-by: Cliff Hansen * TST: remove orphaned times_single reference from dirint test * STY: fix line length and blank lines in dirint docstring --------- Co-authored-by: Cliff Hansen --- docs/sphinx/source/whatsnew/v0.15.2.rst | 3 +++ pvlib/irradiance.py | 21 +++++++++++++-------- tests/test_irradiance.py | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index ed237f4720..ef85250377 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -18,6 +18,9 @@ Deprecations Bug fixes ~~~~~~~~~ +* Added test coverage for :py:func:`pvlib.irradiance.dirint` with + ``np.array`` and ``pd.Series`` inputs. + (:issue:`2751`, :pull:`2752`) * Corrects a bug in :py:func:`pvlib.temperature.fuentes`. If inputs were data type integer, users can expect modeled cell temperature values to increase slightly. diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 471ce9b93a..b38db534fe 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -132,7 +132,9 @@ def _handle_extra_radiation_types(datetime_or_doy, epoch_year): # a better way to do it. if isinstance(datetime_or_doy, pd.DatetimeIndex): to_doy = tools._pandas_to_doy # won't be evaluated unless necessary - def to_datetimeindex(x): return x # noqa: E306 + + def to_datetimeindex(x): + return x # noqa: E306 to_output = partial(pd.Series, index=datetime_or_doy) elif isinstance(datetime_or_doy, pd.Timestamp): to_doy = tools._pandas_to_doy @@ -146,12 +148,14 @@ def to_datetimeindex(x): return x # noqa: E306 tools._datetimelike_scalar_to_datetimeindex to_output = tools._scalar_out elif np.isscalar(datetime_or_doy): # ints and floats of various types - def to_doy(x): return x # noqa: E306 + def to_doy(x): + return x # noqa: E306 to_datetimeindex = partial(tools._doy_to_datetimeindex, epoch_year=epoch_year) to_output = tools._scalar_out else: # assume that we have an array-like object of doy - def to_doy(x): return x # noqa: E306 + def to_doy(x): + return x # noqa: E306 to_datetimeindex = partial(tools._doy_to_datetimeindex, epoch_year=epoch_year) to_output = tools._array_out @@ -1972,14 +1976,14 @@ def dirint(ghi, solar_zenith, times, pressure=101325., use_delta_kt_prime=True, Returns ------- - dni : array-like - The modeled direct normal irradiance, as provided by the - DIRINT model. [Wm⁻²] + dni : pd.Series + Estimated direct normal irradiance. [Wm⁻²] Notes ----- - DIRINT model requires time series data (ie. one of the inputs must - be a vector of length > 2). + The DIRINT model was developed for time series data with length > 2. + The implementation in pvlib assumes the data are periodic which may + affect the first and last DNI values. References ---------- @@ -2117,6 +2121,7 @@ def _dirint_bins(times, kt_prime, zenith, w, delta_kt_prime): ------- tuple of kt_prime_bin, zenith_bin, w_bin, delta_kt_prime_bin """ + # @wholmgren: the following bin assignments use MATLAB's 1-indexing. # Later, we'll subtract 1 to conform to Python's 0-indexing. diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index 9925d99902..acc8495d3b 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -1147,6 +1147,31 @@ def test_dirindex_min_cos_zenith_max_zenith(): assert_series_equal(out, expected) +def test_dirint_array_inputs(): + """np.array and pd.Series inputs work correctly. GH #2751""" + times = pd.date_range('2023-06-21 10:00', periods=2, freq='h', tz='UTC') + + # np.array input → should return pd.Series + result = irradiance.dirint( + ghi=np.array([500.0, 400.0]), + solar_zenith=np.array([45.0, 50.0]), + times=times, + use_delta_kt_prime=False + ) + assert isinstance(result, pd.Series) + assert result.iloc[0] > 0 + + # pd.Series input → should return pd.Series + times2 = pd.date_range('2023-06-21 10:00', periods=3, freq='h', tz='UTC') + result2 = irradiance.dirint( + ghi=pd.Series([400, 500, 300], index=times2), + solar_zenith=pd.Series([50, 40, 60], index=times2), + times=times2 + ) + assert isinstance(result2, pd.Series) + assert (result2 >= 0).all() + + def test_dni(): ghi = pd.Series([90, 100, 100, 100, 100]) dhi = pd.Series([100, 90, 50, 50, 50]) From 1703cefd0b324f247695841cbda570bdd1ce5c3a Mon Sep 17 00:00:00 2001 From: Karl Hill Date: Wed, 10 Jun 2026 17:57:53 -0400 Subject: [PATCH 06/16] Update nasa_power VARIABLE_MAP with additional parameters (#2762) * Update nasa_power VARIABLE_MAP with additional parameters - Added: ALLSKY_SFC_LW_DWN, ALLSKY_TOA_SW_DWN, CLRSKY_DIFF, PS, T2MDEW, T2MWET, RH2M, SRF_ALB, TQV - Fix PS unit conversion: kPa to Pa for pvlib.atmosphere compatibility - Fix parameter names: CLRSKY_DIFF -> CLRSKY_SFC_SW_DIFF, ALLSKY_TOA_SW_DWN -> TOA_SW_DWN, SRF_ALB -> ALLSKY_SRF_ALB - Drop T2MWET (duplicate target name breaks pandas rename) - Add TQV unit conversion: kg/m^2 to cm - Add regression tests for all VARIABLE_MAP params and unit conversions * Fix flake8 E501/E702 in test file * Address review comments: move unit conversion note to Notes section, simplify inline comments - Move unit conversion documentation from map_variables param description to a dedicated Notes section in the docstring - Simplify pressure conversion comment (remove specific function list) - Simplify TQV conversion comment (remove specific function reference) * Add dni_clear to variable map * Update notes section * Fix linter * Split test_get_nasa_power_all_variable_map_parameters_valid into two batches VARIABLE_MAP now has 16 entries, exceeding NASA POWER's 15-parameter-per-request limit. Split the test into two batches of 8 instead of asserting the limit. * Add whatsnew entry for nasa_power VARIABLE_MAP additions (PR #2762) * Address review: simplify whatsnew entry, add self to Contributors (PR #2762) * Apply suggestion from @echedey-ls Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * Apply suggestion from @AdamRJensen - combine whatsnew entries --------- Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- docs/sphinx/source/whatsnew/v0.15.2.rst | 13 ++- pvlib/iotools/nasa_power.py | 22 ++++- tests/iotools/test_nasa_power.py | 107 ++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.2.rst b/docs/sphinx/source/whatsnew/v0.15.2.rst index ef85250377..94cda592f9 100644 --- a/docs/sphinx/source/whatsnew/v0.15.2.rst +++ b/docs/sphinx/source/whatsnew/v0.15.2.rst @@ -40,13 +40,11 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Add the ``front_side_fraction`` parameter to - :py:func:`pvlib.snow.loss_townsend` to support Townsend snow-loss - workflows for bifacial systems. (:issue:`2755`, :pull:`2756`) - -* Added mapping of the parameter ``"albedo"`` in - :py:func:`~pvlib.iotools.get_nasa_power` when ``map_variables=True`` - (:pull:`2753`) +* Add the following parameters to :py:func:`~pvlib.iotools.get_nasa_power` + when ``map_variables=True``: ``temp_dew``, ``precipitable_water``, + ``relative_humidity``, ``ghi_extra``, ``dhi_clear``, ``longwave_down``, + and ``albedo``. + (:issue:`2731`, :pull:`2753`, :pull:`2762`) Documentation @@ -83,4 +81,5 @@ Contributors * :ghuser:`shethkajal7` * Arthur Onno (:ghuser:`ArthurOnnoTerabase`) * Adam R. Jensen (:ghuser:`AdamRJensen`) +* Karl Hill (:ghuser:`karlhillx`) * Rajiv Daxini (:ghuser:`RDaxini`) diff --git a/pvlib/iotools/nasa_power.py b/pvlib/iotools/nasa_power.py index 47dad5f2d3..7ab3829495 100644 --- a/pvlib/iotools/nasa_power.py +++ b/pvlib/iotools/nasa_power.py @@ -14,11 +14,19 @@ '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_DNI': 'dni_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', } @@ -82,6 +90,12 @@ def get_nasa_power(latitude, longitude, start, end, requests.HTTPError Raises an error when an incorrect request is made. + Notes + ----- + When ``map_variables=True`` the following unit conversions are applied: + pressure is converted from kPa to Pa, and precipitable water + is converted from kg/m² (mm) to cm. + Returns ------- data : pd.DataFrame @@ -150,5 +164,11 @@ def get_nasa_power(latitude, longitude, start, end, # Rename according to pvlib convention if map_variables: df = df.rename(columns=VARIABLE_MAP) + # PS is returned in kPa; convert to Pa for pvlib compatibility. + if 'pressure' in df.columns: + df['pressure'] = df['pressure'] * 1000 + # TQV is returned in kg/m^2 (=mm); convert to cm for compatibility + 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..dd09d7b1ca 100644 --- a/tests/iotools/test_nasa_power.py +++ b/tests/iotools/test_nasa_power.py @@ -98,3 +98,110 @@ 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 is split + into two batches to stay within that limit. + """ + from pvlib.iotools.nasa_power import VARIABLE_MAP + nasa_params = list(VARIABLE_MAP.keys()) + # Split into two batches; API limit is 15. + half = (len(nasa_params) + 1) // 2 + batch1 = nasa_params[:half] + batch2 = nasa_params[half:] + + for batch in [batch1, batch2]: + data, meta = pvlib.iotools.get_nasa_power( + latitude=44.76, + longitude=7.64, + start='2025-02-02', + end='2025-02-02', + parameters=batch, + map_variables=False, + ) + missing = set(batch) - 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}" + ) From ab841b12e4f2bbd3eabb1c2f387fa0c281ba1e4d Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Thu, 11 Jun 2026 14:09:31 +0100 Subject: [PATCH 07/16] Fix ambiguous variable name --- pvlib/irradiance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 9a7847055b..6757b20c79 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -991,15 +991,15 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, HB = dni * cos_solar_zenith HB = np.maximum(HB, 0) - I = (1 + tools.cosd(surface_tilt)) / 2 + SVF = (1 + tools.cosd(surface_tilt)) / 2 with np.errstate(invalid='ignore', divide='ignore'): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) h = np.sqrt(hb_to_ghi) * (tools.sind(surface_tilt / 2) ** 3) - term1 = (1 - AI) * I + term1 = (1 - AI) * SVF term2 = AI * Rb - term3 = (1 - AI) * I * h + term3 = (1 - AI) * SVF * h sky_diffuse = dhi * (term1 + term2 + term3) From 7bcb7c2b6f07784353c698baccb9d86103fb8a2f Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Mon, 8 Jun 2026 11:30:38 +0100 Subject: [PATCH 08/16] Add support for return_components to Reindl --- pvlib/irradiance.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index b38db534fe..fd23f34384 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -877,7 +877,7 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, - solar_zenith, solar_azimuth): + solar_zenith, solar_azimuth, return_components=False): r''' Determine the diffuse irradiance from the sky on a tilted surface using the Reindl (1990) model. @@ -916,6 +916,10 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, solar_azimuth : numeric Solar azimuth angles. See :term:`solar_azimuth`. [°] + return_components : bool, default False + Flag used to decide whether to return the calculated diffuse components + or not. + Returns ------- poa_sky_diffuse : numeric @@ -977,16 +981,32 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, HB = dni * cos_solar_zenith HB = np.maximum(HB, 0) - # these are the () and [] sub-terms of the second term of eqn 8 - term1 = 1 - AI - term2 = 0.5 * (1 + tools.cosd(surface_tilt)) + I = (1 + tools.cosd(surface_tilt)) / 2 + with np.errstate(invalid='ignore', divide='ignore'): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) - term3 = 1 + np.sqrt(hb_to_ghi) * (tools.sind(0.5 * surface_tilt)**3) - sky_diffuse = dhi * (AI * Rb + term1 * term2 * term3) - sky_diffuse = np.maximum(sky_diffuse, 0) + h = np.sqrt(hb_to_ghi) * (tools.sind(surface_tilt / 2) ** 3) - return sky_diffuse + term1 = (1 - AI) * I + term2 = AI * Rb + term3 = (1 - AI) * I * h + + sky_diffuse = dhi * (term1 + term2 + term3) + + if return_components: + diffuse_components = OrderedDict() + diffuse_components['poa_sky_diffuse'] = sky_diffuse + + # Calculate the individual components + diffuse_components['poa_isotropic'] = dhi * term1 + diffuse_components['poa_circumsolar'] = dhi * term2 + diffuse_components['poa_horizon'] = dhi * term3 + + if isinstance(sky_diffuse, pd.Series): + diffuse_components = pd.DataFrame(diffuse_components) + return diffuse_components + else: + return sky_diffuse def king(surface_tilt, dhi, ghi, solar_zenith): From 3ad74d7f07394c79ae457c04948589322e6b41c2 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Mon, 8 Jun 2026 12:31:18 +0100 Subject: [PATCH 09/16] Update docstrings for Reindl --- pvlib/irradiance.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index fd23f34384..d51dd8e5cc 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -922,8 +922,21 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Returns ------- - poa_sky_diffuse : numeric - The sky diffuse component of the solar radiation. [Wm⁻²] + numeric, OrderedDict, or DataFrame + Return type controlled by ``return_components`` argument. + If ``return_components=False``, `sky_diffuse` is returned. + If ``return_components=True``, `diffuse_components` is returned. + + sky_diffuse : numeric + The sky diffuse component of the solar radiation on a tilted + surface. + + diffuse_components : OrderedDict (array input) or DataFrame (Series input) + Keys/columns are: + * poa_sky_diffuse: Total sky diffuse + * poa_isotropic + * poa_circumsolar + * poa_horizon Notes ----- @@ -947,8 +960,9 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Implementation is based on Loutzenhiser et al. (2007) [3]_, Equation 8. The beam and ground reflectance portion of the equation have been removed, therefore the model described here generates - ONLY the diffuse radiation from the sky and circumsolar, so the form of the - equation varies slightly from Equation 8 in [3]_. + ONLY the diffuse radiation from the sky, circumsolar, and horizon + brightening, so the form of the equation varies slightly from Equation 8 + in [3]_. References ---------- From e4d4dd81b481f16f73343a1456bf38da612b24f8 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Thu, 11 Jun 2026 14:09:31 +0100 Subject: [PATCH 10/16] Fix ambiguous variable name --- pvlib/irradiance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index d51dd8e5cc..ac60c18842 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -995,15 +995,15 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, HB = dni * cos_solar_zenith HB = np.maximum(HB, 0) - I = (1 + tools.cosd(surface_tilt)) / 2 + SVF = (1 + tools.cosd(surface_tilt)) / 2 with np.errstate(invalid='ignore', divide='ignore'): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) h = np.sqrt(hb_to_ghi) * (tools.sind(surface_tilt / 2) ** 3) - term1 = (1 - AI) * I + term1 = (1 - AI) * SVF term2 = AI * Rb - term3 = (1 - AI) * I * h + term3 = (1 - AI) * SVF * h sky_diffuse = dhi * (term1 + term2 + term3) From 7feb2e802d5e9ad9cb7307b30fc90d033b226c75 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 10:39:55 +0100 Subject: [PATCH 11/16] Change OrderedDict output to Dict --- pvlib/irradiance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index ac60c18842..3c0e2c97a1 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -922,7 +922,7 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Returns ------- - numeric, OrderedDict, or DataFrame + numeric, Dict, or DataFrame Return type controlled by ``return_components`` argument. If ``return_components=False``, `sky_diffuse` is returned. If ``return_components=True``, `diffuse_components` is returned. @@ -931,7 +931,7 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, The sky diffuse component of the solar radiation on a tilted surface. - diffuse_components : OrderedDict (array input) or DataFrame (Series input) + diffuse_components : Dict (array input) or DataFrame (Series input) Keys/columns are: * poa_sky_diffuse: Total sky diffuse * poa_isotropic @@ -1008,7 +1008,7 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, sky_diffuse = dhi * (term1 + term2 + term3) if return_components: - diffuse_components = OrderedDict() + diffuse_components = Dict() diffuse_components['poa_sky_diffuse'] = sky_diffuse # Calculate the individual components From 27abaefe36e7be696fb8a8c404dfcb032c17a7c1 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 15:09:44 +0100 Subject: [PATCH 12/16] Dict output for return_components=True --- pvlib/irradiance.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 3c0e2c97a1..02bc96c62f 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -1008,13 +1008,12 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, sky_diffuse = dhi * (term1 + term2 + term3) if return_components: - diffuse_components = Dict() - diffuse_components['poa_sky_diffuse'] = sky_diffuse - - # Calculate the individual components - diffuse_components['poa_isotropic'] = dhi * term1 - diffuse_components['poa_circumsolar'] = dhi * term2 - diffuse_components['poa_horizon'] = dhi * term3 + diffuse_components = { + 'poa_sky_diffuse': sky_diffuse, + 'poa_isotropic': dhi * term1, + 'poa_circumsolar': dhi * term2, + 'poa_horizon': dhi * term3 + } if isinstance(sky_diffuse, pd.Series): diffuse_components = pd.DataFrame(diffuse_components) From 0e4124297d6d150b2f57b7ce3e667998774f378e Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 15:50:20 +0100 Subject: [PATCH 13/16] Add Reindl components test --- tests/test_irradiance.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index acc8495d3b..1b3c578aa6 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -251,6 +251,41 @@ def test_reindl(irrad_data, ephem_data, dni_et): assert_allclose(result, [0., 27.9412, 104.1317, 34.1663], atol=1e-4) +def test_reindl_components(irrad_data, ephem_data, dni_et): + keys = ['poa_sky_diffuse', 'poa_isotropic', 'poa_circumsolar', + 'poa_horizon'] + expected = pd.DataFrame(np.array( + [[0, 27.941170, 104.131724, 34.166258], + [0, 27.177514, 30.181807, 27.983728], + [0, 0, 72.813055, 5.207138], + [0, 0.763656, 1.136862, 0.975393]]).T, + columns=keys, + index=irrad_data.index + ) + # pandas + result = irradiance.reindl( + 40, 180, irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], + dni_et, ephem_data['apparent_zenith'], ephem_data['azimuth'], + return_components=True) + assert_frame_equal(result, expected, check_less_precise=4) + # numpy + result = irradiance.reindl( + 40, 180, irrad_data['dhi'].values, irrad_data['dni'].values, + irrad_data['ghi'].values, dni_et, ephem_data['apparent_zenith'].values, + ephem_data['azimuth'].values, return_components=True) + for key in keys: + assert_allclose(result[key], expected[key], atol=1e-4) + assert isinstance(result, dict) + # scalar + result = irradiance.reindl( + 40, 180, irrad_data['dhi'].values[-1], irrad_data['dni'].values[-1], + irrad_data['ghi'].values[-1], dni_et[-1], ephem_data['apparent_zenith'].values[-1], + ephem_data['azimuth'].values[-1], return_components=True) + for key in keys: + assert_allclose(result[key], expected[key].iloc[-1], atol=1e-4) + assert isinstance(result, dict) + + def test_king(irrad_data, ephem_data): result = irradiance.king(40, irrad_data['dhi'], irrad_data['ghi'], ephem_data['apparent_zenith']) From 1d40b2a05182f728c57af7fc4769bd17a3295b42 Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 15:52:30 +0100 Subject: [PATCH 14/16] Fix long line on test --- tests/test_irradiance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index 1b3c578aa6..730711ce06 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -279,8 +279,10 @@ def test_reindl_components(irrad_data, ephem_data, dni_et): # scalar result = irradiance.reindl( 40, 180, irrad_data['dhi'].values[-1], irrad_data['dni'].values[-1], - irrad_data['ghi'].values[-1], dni_et[-1], ephem_data['apparent_zenith'].values[-1], - ephem_data['azimuth'].values[-1], return_components=True) + irrad_data['ghi'].values[-1], dni_et[-1], + ephem_data['apparent_zenith'].values[-1], + ephem_data['azimuth'].values[-1], + return_components=True) for key in keys: assert_allclose(result[key], expected[key].iloc[-1], atol=1e-4) assert isinstance(result, dict) From 44bb240d9f5a8112689a6bef4b9dd959394d6c6e Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 15:55:46 +0100 Subject: [PATCH 15/16] Clarify Reindl component formulation on docstrings --- pvlib/irradiance.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 02bc96c62f..efdbfaa647 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -964,6 +964,9 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, brightening, so the form of the equation varies slightly from Equation 8 in [3]_. + For clarity, the horizon component in `reindl` corresponds to the term + added on top of the `hay–davies` formulation, on which `reindl` builds. + References ---------- .. [1] Reindl, D. T., Beckmann, W. A., Duffie, J. A., 1990a. Diffuse From 63f8891b0fd167a91e64c7f08990314cda6466ac Mon Sep 17 00:00:00 2001 From: cbcrespo Date: Fri, 12 Jun 2026 16:01:40 +0100 Subject: [PATCH 16/16] Fix conflicts --- pvlib/irradiance.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 89893d30f9..efdbfaa647 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -878,7 +878,6 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, solar_zenith, solar_azimuth, return_components=False): - solar_zenith, solar_azimuth, return_components=False): r''' Determine the diffuse irradiance from the sky on a tilted surface using the Reindl (1990) model. @@ -921,10 +920,6 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, Flag used to decide whether to return the calculated diffuse components or not. - return_components : bool, default False - Flag used to decide whether to return the calculated diffuse components - or not. - Returns ------- numeric, Dict, or DataFrame @@ -1005,8 +1000,6 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, SVF = (1 + tools.cosd(surface_tilt)) / 2 - SVF = (1 + tools.cosd(surface_tilt)) / 2 - with np.errstate(invalid='ignore', divide='ignore'): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) h = np.sqrt(hb_to_ghi) * (tools.sind(surface_tilt / 2) ** 3)