Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c459d3
Add return_components to isotropic
cbcrespo Jun 9, 2026
4148c53
Change OrderedDict to dict
cbcrespo Jun 17, 2026
f3e69b3
Add tests for isotropic with return_components=True
cbcrespo Jun 17, 2026
fdde55c
Merge branch 'main' of https://github.com/pvlib/pvlib-python into iso…
cbcrespo Jun 22, 2026
97a34f6
Add what's new entry
cbcrespo Jun 22, 2026
dcd202c
Add support for 'return_components'
cbcrespo Jun 24, 2026
722f6fd
Add tests
cbcrespo Jun 29, 2026
2eb64ca
Raise error if return_components=True used with king or klucher
cbcrespo Jul 1, 2026
8d698e3
Update pvlib/irradiance.py
cbcrespo Jul 8, 2026
fbf1e30
Merge branch 'main' into diffuse-components-support
cbcrespo Jul 8, 2026
4be6430
Adjust docstrings
cbcrespo Jul 17, 2026
41c3ff0
Fix list indentation
cbcrespo Jul 23, 2026
a3b9391
Merge branch 'main' into diffuse-components-support
cbcrespo Jul 23, 2026
9455e83
Merge branch 'main' into pr/2800
AdamRJensen Jul 30, 2026
51c08b3
Merge branch 'diffuse-components-support' of https://github.com/cbcre…
AdamRJensen Jul 30, 2026
6eebc16
Merge branch 'main' into pr/2800
AdamRJensen Jul 30, 2026
d360853
Add more tests
cbcrespo Jul 30, 2026
eddb1c3
Minor fix
cbcrespo Jul 30, 2026
1795658
Add whatsnew entry
cbcrespo Jul 30, 2026
0c5552d
Merge branch 'main' into diffuse-components-support
cbcrespo Aug 4, 2026
2cc548e
Apply suggestions from code review
cbcrespo Aug 5, 2026
9182928
Remove duplicated lines
cbcrespo Aug 5, 2026
4ffd8f7
Adjust tests
cbcrespo Aug 7, 2026
8b5667c
Add breaking change to whatsnew
cbcrespo Aug 7, 2026
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
7 changes: 7 additions & 0 deletions docs/sphinx/source/whatsnew/v0.15.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ v0.15.3 (Anticipated September, 2026)

Breaking Changes
~~~~~~~~~~~~~~~~
* Changed output type of :py:func:`pvlib.irradiance.get_total_irradiance`,
:py:func:`pvlib.irradiance.get_sky_diffuse`, and :py:func:`pvlib.irradiance.poa_components`
from ``OrderedDict`` to ``dict``. (:issue:`2750`, :pull:`2800`)


Deprecations
Expand All @@ -28,6 +31,10 @@ Enhancements
* Add ``return_components`` kwarg to :py:func:`pvlib.irradiance.reindl` to
support returning the components of sky diffuse irradiance.
(:issue:`2750`, :pull:`2775`)
* Add ``return_components`` kwarg to :py:func:`pvlib.irradiance.get_sky_diffuse`
and ``diffuse_components`` kwarg to :py:func:`pvlib.irradiance.get_total_irradiance`
to support returning the components of sky diffuse irradiance.
(:issue:`2750`, :pull:`2800`)
* Add iotools functions to retrieve irradiance and weather data from NSRDB PSM4 Polar,
which provides satellite-derived irradiance data above 60 degree latitude.
:py:func:`~pvlib.iotools.get_nsrdb_psm4_polar` and
Expand Down
99 changes: 73 additions & 26 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
dni, ghi, dhi, dni_extra=None, airmass=None,
albedo=0.25, surface_type=None,
model='isotropic',
model_perez='allsitescomposite1990'):
model_perez='allsitescomposite1990',
diffuse_components=False):
r"""
Determine total in-plane irradiance and its beam, sky diffuse and ground
reflected components, using the specified sky diffuse irradiance model.
Expand Down Expand Up @@ -332,12 +333,22 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
``'perez-driesse'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
diffuse_components : bool, default False
If `True`, returns values for the different diffuse irradiance
components available from the selected model
(e.g., isotropic, circumsolar, horizon brightening).
If `False`, only the total diffuse irradiance is returned.
This option is not available for the ``'klucher'`` and
``'king'`` models.

Returns
-------
total_irrad : OrderedDict or DataFrame
total_irrad : dict or DataFrame
Contains keys/columns ``'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'``. [Wm⁻²]
If ``diffuse_components`` is `True`, additional keys/columns are
returned for each of the sky diffuse components returned by the
selected model.

Notes
-----
Expand All @@ -353,7 +364,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
poa_sky_diffuse = get_sky_diffuse(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, model=model,
model_perez=model_perez)
model_perez=model_perez, return_components=diffuse_components)

poa_ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo,
surface_type)
Expand All @@ -366,7 +377,8 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
dni, ghi, dhi, dni_extra=None, airmass=None,
model='isotropic',
model_perez='allsitescomposite1990'):
model_perez='allsitescomposite1990',
return_components=False):
r"""
Determine in-plane sky diffuse irradiance component
using the specified sky diffuse irradiance model.
Expand Down Expand Up @@ -408,11 +420,22 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
``'perez-driesse'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
return_components : bool, default False
If `True`, returns values for the different diffuse irradiance
components available from the selected model
(e.g., isotropic, circumsolar, horizon brightening).
If `False`, only the total diffuse irradiance is returned.
This option is not available for the ``'klucher'`` and
``'king'`` models.

Returns
-------
poa_sky_diffuse : numeric
Sky diffuse irradiance in the plane of array. [Wm⁻²]
numeric, dict, or DataFrame
Return type controlled by ``return_components`` argument.
If `False`, total sky diffuse irradiance in the plane of array
is returned (numeric). [Wm⁻²]
If `True`, the different diffuse components are returned
(dict or DataFrame). [Wm⁻²]

Raises
------
Expand All @@ -438,33 +461,40 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,

model = model.lower()

if return_components and model in {'klucher', 'king'}:
raise ValueError('return_components is not supported for'
f' model {model}')

if dni_extra is None and model in {'haydavies', 'reindl',
'perez', 'perez-driesse'}:
raise ValueError(f'dni_extra is required for model {model}')

if model == 'isotropic':
sky = isotropic(surface_tilt, dhi)
sky = isotropic(surface_tilt, dhi, return_components=return_components)
elif model == 'klucher':
sky = klucher(surface_tilt, surface_azimuth, dhi, ghi,
solar_zenith, solar_azimuth)
elif model == 'haydavies':
sky = haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth)
solar_zenith, solar_azimuth,
return_components=return_components)
elif model == 'reindl':
sky = reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra,
solar_zenith, solar_azimuth)
solar_zenith, solar_azimuth,
return_components=return_components)
elif model == 'king':
sky = king(surface_tilt, dhi, ghi, solar_zenith)
elif model == 'perez':
if airmass is None:
airmass = atmosphere.get_relative_airmass(solar_zenith)
sky = perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth, airmass,
model=model_perez)
model=model_perez, return_components=return_components)
elif model == 'perez-driesse':
# perez_driesse will calculate its own airmass if needed
sky = perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth, airmass)
solar_zenith, solar_azimuth, airmass,
return_components=return_components)
else:
raise ValueError(f'invalid model selection {model}')

Expand All @@ -488,7 +518,7 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):
Direct normal irradiance, as measured from a TMY file or
calculated with a clearsky model. See :term:`dni`. [Wm⁻²]

poa_sky_diffuse : numeric
poa_sky_diffuse : numeric, dict or DataFrame
Diffuse irradiance in the plane of the modules, as
calculated by a diffuse irradiance translation function. [Wm⁻²]

Expand All @@ -499,31 +529,48 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):

Returns
-------
irrads : OrderedDict or DataFrame
irrads : dict or DataFrame
Contains the following keys:

* ``poa_global`` : Total in-plane irradiance. [Wm⁻²]
* ``poa_direct`` : Total in-plane beam irradiance. [Wm⁻²]
* ``poa_diffuse`` : Total in-plane diffuse irradiance. [Wm⁻²]
* ``poa_sky_diffuse`` : In-plane diffuse irradiance from sky. [Wm⁻²]
* ``poa_ground_diffuse`` : In-plane diffuse irradiance from ground.
[Wm⁻²]
* ``poa_global`` : Total irradiance on a tilted plane. [Wm⁻²]
* ``poa_direct`` : Direct irradiance on a tilted plane. [Wm⁻²]
* ``poa_diffuse`` : Diffuse irradiance on a tilted plane. [Wm⁻²]
* ``poa_sky_diffuse`` : The sky diffuse component of irradiance on a
tilted plane. [Wm⁻²]
* ``poa_ground_diffuse`` : The ground diffuse component of irradiance
on a tilted plane. [Wm⁻²]

If ``poa_sky_diffuse`` is a dict or DataFrame, ``irrads`` will
contain additional keys for each of the diffuse components returned by
the selected diffuse irradiance model.

Notes
------
Negative beam irradiation due to AOI > 90° or AOI < 0° is set to zero.
'''

if isinstance(poa_sky_diffuse, dict):
sky_components = poa_sky_diffuse.copy()
total_poa_sky_diffuse = sky_components.pop('poa_sky_diffuse')
elif isinstance(poa_sky_diffuse, pd.DataFrame):
sky_components = poa_sky_diffuse.to_dict(orient='series')
total_poa_sky_diffuse = sky_components.pop('poa_sky_diffuse')
else:
sky_components = {}
total_poa_sky_diffuse = poa_sky_diffuse

poa_direct = np.maximum(dni * np.cos(np.radians(aoi)), 0)
poa_diffuse = poa_sky_diffuse + poa_ground_diffuse
poa_diffuse = total_poa_sky_diffuse + poa_ground_diffuse
poa_global = poa_direct + poa_diffuse

irrads = OrderedDict()
irrads['poa_global'] = poa_global
irrads['poa_direct'] = poa_direct
irrads['poa_diffuse'] = poa_diffuse
irrads['poa_sky_diffuse'] = poa_sky_diffuse
irrads['poa_ground_diffuse'] = poa_ground_diffuse
irrads = {
'poa_global': poa_global,
'poa_direct': poa_direct,
'poa_diffuse': poa_diffuse,
'poa_sky_diffuse': total_poa_sky_diffuse,
'poa_ground_diffuse': poa_ground_diffuse,
**sky_components
}

if isinstance(poa_direct, pd.Series):
irrads = pd.DataFrame(irrads)
Expand Down
99 changes: 99 additions & 0 deletions tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,14 @@ def test_get_sky_diffuse_model_invalid():
model='invalid')


def test_get_sky_diffuse_components_model_not_supported():
msg = 'return_components is not supported'
with pytest.raises(ValueError, match=msg):
irradiance.get_sky_diffuse(
30, 180, 0, 180, 1000, 1100, 100, dni_extra=1360, airmass=1,
model='klucher', return_components=True)


def test_get_sky_diffuse_missing_dni_extra():
msg = 'dni_extra is required'
with pytest.raises(ValueError, match=msg):
Expand Down Expand Up @@ -573,6 +581,44 @@ def test_get_total_irradiance(irrad_data, ephem_data, dni_et,
'poa_ground_diffuse']


@pytest.mark.parametrize('model', ['reindl', 'perez', 'perez-driesse'])
def test_get_total_irradiance_diffuse_components(irrad_data, ephem_data,
dni_et, relative_airmass,
model):
total = irradiance.get_total_irradiance(
32, 180,
ephem_data['apparent_zenith'], ephem_data['azimuth'],
dni=irrad_data['dni'], ghi=irrad_data['ghi'],
dhi=irrad_data['dhi'],
dni_extra=dni_et, airmass=relative_airmass,
model=model,
surface_type='urban',
diffuse_components=True)

assert total.columns.tolist() == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse',
'poa_isotropic', 'poa_circumsolar',
'poa_horizon']

total = irradiance.get_total_irradiance(
32, 180,
ephem_data['apparent_zenith'].to_numpy(),
ephem_data['azimuth'].to_numpy(),
dni=irrad_data['dni'].to_numpy(), ghi=irrad_data['ghi'].to_numpy(),
dhi=irrad_data['dhi'].to_numpy(),
dni_extra=dni_et, airmass=relative_airmass,
model=model,
surface_type='urban',
diffuse_components=True)

assert list(total.keys()) == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse',
'poa_isotropic', 'poa_circumsolar',
'poa_horizon']


@pytest.mark.parametrize('model', ['isotropic', 'klucher',
'haydavies', 'reindl', 'king',
'perez', 'perez-driesse'])
Expand Down Expand Up @@ -662,6 +708,59 @@ def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass):
assert_frame_equal(out, expected)


def test_poa_components_diffuse_components_perez(irrad_data, ephem_data,
dni_et, relative_airmass):
aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'],
ephem_data['azimuth'])
gr_sand = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
surface_type='sand')
diff_perez = irradiance.perez(
40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et,
ephem_data['apparent_zenith'], ephem_data['azimuth'], relative_airmass,
return_components=True)
out = irradiance.poa_components(
aoi, irrad_data['dni'], diff_perez, gr_sand)
expected = pd.DataFrame(np.array(
[[0., -0., 0., 0.,
0., 0., 0., 0.],
[35.19456561, 0., 35.19456561, 31.4635077,
3.73105791, 26.841386, 0.000000, 4.622122],
[956.18253696, 798.31939281, 157.86314414, 109.08433162,
48.77881252, 41.621826, 61.619987, 5.842518],
[90.99624896, 33.50143401, 57.49481495, 45.45978964,
12.03502531, 31.726961, 4.479664, 9.253165]]),
columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse', 'poa_isotropic', 'poa_circumsolar',
'poa_horizon'],
index=irrad_data.index)
assert_frame_equal(out, expected)


def test_poa_components_diffuse_components_isotropic(irrad_data, ephem_data,
dni_et, relative_airmass):
aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'],
ephem_data['azimuth'])
gr_sand = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
surface_type='sand')
diff_isotropic = irradiance.isotropic(
40, irrad_data['dhi'], return_components=True)
out = irradiance.poa_components(
aoi, irrad_data['dni'], diff_isotropic, gr_sand)
expected = pd.DataFrame(np.array(
[[0., -0., 0., 0.,
0., 0.],
[39.459460, 0.000000, 39.459460, 35.728402,
3.731058, 35.728402],
[951.699533, 798.319393, 153.380140, 104.601328,
48.778813, 104.601328],
[100.313650, 33.501434, 66.812216, 54.777191,
12.035025, 54.777191]]),
columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse', 'poa_isotropic'],
index=irrad_data.index)
assert_frame_equal(out, expected)


@pytest.mark.parametrize('pressure,expected', [
(93193, [[830.46567, 0.79742, 0.93505],
[676.18340, 0.63782, 3.02102]]),
Expand Down
Loading