Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
4871948
Add get_iam_diffuse to the Array and PVSystem classes
cbcrespo Aug 5, 2026
6cbb1aa
Set all IAM outputs to dict
cbcrespo Aug 11, 2026
8772a7c
Add whatsnew entry
cbcrespo Aug 11, 2026
720c8ea
Minor change to docstrings
cbcrespo Aug 11, 2026
b4270a2
Docstring changes
cbcrespo Aug 12, 2026
f09a971
Add schlick_diffuse
cbcrespo Aug 12, 2026
e3a5a29
Merge branch 'main' of https://github.com/pvlib/pvlib-python
cbcrespo Aug 14, 2026
11375de
Merge branch 'iam-dict-output' into array-iam
cbcrespo Aug 14, 2026
7911c17
Add error test
cbcrespo Aug 14, 2026
d9bf100
Add Dataframe support
cbcrespo Aug 14, 2026
ae7d017
Add whatsnew entry
cbcrespo Aug 14, 2026
515ff98
Merge branch 'main' into array-iam
cbcrespo Aug 20, 2026
a52ff3f
Fix diffuse IAM kwargs
cbcrespo Aug 20, 2026
3f76a90
Add tests to fix codecov
cbcrespo Aug 20, 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
3 changes: 3 additions & 0 deletions docs/sphinx/source/whatsnew/v0.16.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Enhancements
(:issue:`2828`, :pull:`2832`)
* Allow variables from multiple datasets to be requested at once in
:py:func:`~pvlib.iotools.get_merra2`. (:pull:`2839`)
* Add support for diffuse IAM in the :py:class:`pvlib.pvsystem.Array` and
:py:class:`pvlib.pvsystem.PVSystem` classes (see `pvlib.pvsystem.Array.get_iam_diffuse`
and `pvlib.pvsystem.PVSystem.get_iam_diffuse`). (:issue:`2812`, :pull:`2845`)


Documentation
Expand Down
5 changes: 5 additions & 0 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,11 @@ def isotropic(surface_tilt, dhi, return_components=False):
* poa_isotropic: The portion of sky diffuse irradiance on a tilted
plane from the isotropic sky dome. [Wm⁻²]

diffuse_components : Dict (array input) or DataFrame (Series input)
Keys/columns are:
* poa_sky_diffuse: Total sky diffuse
* poa_isotropic

References
----------
.. [1] Loutzenhiser P.G. et al. "Empirical validation of models to
Expand Down
162 changes: 146 additions & 16 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def get_aoi(self, solar_zenith, solar_azimuth):
@_unwrap_single_value
def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
dni_extra=None, airmass=None, albedo=None,
model='haydavies', **kwargs):
model='haydavies', diffuse_components=False, **kwargs):
"""
Uses :py:func:`pvlib.irradiance.get_total_irradiance` to
calculate the plane of array irradiance components on the tilted
Expand All @@ -334,6 +334,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
Ground surface albedo. [unitless]
model : String, default 'haydavies'
Irradiance model.
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.

kwargs
Extra parameters passed to
Expand Down Expand Up @@ -373,7 +378,9 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
array.get_irradiance(solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra=dni_extra, airmass=airmass,
albedo=albedo, model=model, **kwargs)
albedo=albedo, model=model,
diffuse_components=diffuse_components,
**kwargs)
for array, dni, ghi, dhi, albedo in zip(
self.arrays, dni, ghi, dhi, albedo
)
Expand All @@ -382,8 +389,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
@_unwrap_single_value
def get_iam(self, aoi, iam_model='physical'):
"""
Determine the incidence angle modifier using the method specified by
``iam_model``.
Determine the incidence angle modifier for direct irradiance
using the method specified by ``iam_model``.

Parameters for the selected IAM model are expected to be in
``PVSystem.module_parameters``. Default parameters are available for
Expand Down Expand Up @@ -411,6 +418,48 @@ def get_iam(self, aoi, iam_model='physical'):
return tuple(array.get_iam(aoi, iam_model)
for array, aoi in zip(self.arrays, aoi))

@_unwrap_single_value
def get_iam_diffuse(self, surface_tilt, iam_model='marion_diffuse',
marion_model=None, **kwargs):
"""
Determine the incidence angle modifier for diffuse irradiance using the
method specified by ``iam_model``.

Parameters for the selected IAM model are expected to be in
``Array.module_parameters``. Default parameters are available for
the 'marion_diffuse' and 'martin_ruiz_diffuse' models.

Parameters
----------
surface_tilt : float or Series
The tilt angle of the surface in degrees.
iam_model : string, default 'marion_diffuse'
The IAM model to be used. Valid strings are 'marion_diffuse',
'martin_ruiz_diffuse', and 'schlick_diffuse'.
marion_model : string, default None
The IAM function to evaluate across a solid angle. Only used when
``iam_model='marion_diffuse'``. Must be one of 'ashrae',
'physical', 'martin_ruiz', 'sapm', and 'schlick'.

kwargs : dict, optional
Additional keyword arguments passed to the IAM model function.

Returns
-------
iam_diffuse : dict or DataFrame
The AOI modifiers for different diffuse irradiance components.
Included components depend on the selected ``iam_model``.

Raises
------
ValueError
if `iam_model` is not a valid model name.
"""
surface_tilt = self._validate_per_array(surface_tilt)
return tuple(array.get_iam_diffuse(tilt, iam_model=iam_model,
marion_model=marion_model, **kwargs)
for array, tilt in zip(self.arrays, surface_tilt))

@_unwrap_single_value
def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
effective_irradiance=None, longwave_down=None):
Expand Down Expand Up @@ -1093,7 +1142,7 @@ def get_aoi(self, solar_zenith, solar_azimuth):

def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
dni_extra=None, airmass=None, albedo=None,
model='haydavies', **kwargs):
model='haydavies', diffuse_components=False, **kwargs):
"""
Get plane of array irradiance components.

Expand Down Expand Up @@ -1121,6 +1170,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
Ground surface albedo. [unitless]
model : String, default 'haydavies'
Irradiance model.
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.

kwargs
Extra parameters passed to
Expand Down Expand Up @@ -1161,20 +1215,23 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
airmass = atmosphere.get_relative_airmass(solar_zenith)

orientation = self.mount.get_orientation(solar_zenith, solar_azimuth)
return irradiance.get_total_irradiance(orientation['surface_tilt'],
orientation['surface_azimuth'],
solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
model=model,
**kwargs)
return irradiance.get_total_irradiance(
orientation['surface_tilt'],
orientation['surface_azimuth'],
solar_zenith, solar_azimuth,
dni, ghi, dhi,
dni_extra=dni_extra,
airmass=airmass,
albedo=albedo,
model=model,
diffuse_components=diffuse_components,
**kwargs
)

def get_iam(self, aoi, iam_model='physical'):
"""
Determine the incidence angle modifier using the method specified by
``iam_model``.
Determine the incidence angle modifier for direct irradiance
using the method specified by ``iam_model``.

Parameters for the selected IAM model are expected to be in
``Array.module_parameters``. Default parameters are available for
Expand Down Expand Up @@ -1213,6 +1270,79 @@ def get_iam(self, aoi, iam_model='physical'):
else:
raise ValueError(model + ' is not a valid IAM model')

def get_iam_diffuse(self, surface_tilt, iam_model='marion_diffuse',
marion_model=None):
"""
Determine the incidence angle modifier for various diffuse irradiance
components using the method specified by ``iam_model``.

Parameters for the selected IAM model are expected to be in
``Array.module_parameters``. Default parameters are available for
the 'marion_diffuse' and 'martin_ruiz_diffuse' models.

Parameters
----------
surface_tilt : float or Series
The tilt angle of the surface in degrees.
iam_model : string, default 'marion_diffuse'
The IAM model to be used. Valid strings are 'marion_diffuse',
'martin_ruiz_diffuse' and 'schlick_diffuse'.
marion_model : string, default None
The IAM function to evaluate across a solid angle. Only used when
``iam_model='marion_diffuse'``. Must be one of 'ashrae',
'physical', 'martin_ruiz', 'sapm', and 'schlick'.

Returns
-------
iam_diffuse : dict or DataFrame
The AOI modifiers for different diffuse irradiance components.
Included components depend on the selected ``iam_model``.

Raises
------
ValueError
if `iam_model` is not a valid model name.
ValueError
if `iam_model` is 'marion_diffuse' and `marion_model` is None.
"""
model = iam_model.lower()
if model == 'marion_diffuse' and marion_model is None:
raise ValueError('marion_model must be specified when '
'iam_model="marion_diffuse"')
if model == 'marion_diffuse':
if marion_model in ['ashrae', 'physical', 'martin_ruiz',
'schlick']:
func = getattr(iam, marion_model)
params = set(inspect.signature(func).parameters.keys())
params.discard('aoi')
kwargs = _build_kwargs(params, self.module_parameters)
iams = iam.marion_diffuse(model=marion_model,
surface_tilt=surface_tilt,
**kwargs)
elif marion_model == 'sapm':
iams = iam.marion_diffuse(model='sapm',
surface_tilt=surface_tilt,
module=self.module_parameters)
else:
raise ValueError(marion_model + ' is not a valid IAM model')
elif model == 'martin_ruiz_diffuse':
func = getattr(iam, model) # get function at pvlib.iam
# get all parameters from function signature to retrieve them from
# module_parameters if present
params = set(inspect.signature(func).parameters.keys())
params.discard('aoi')
kwargs = _build_kwargs(params, self.module_parameters)
iams = iam.martin_ruiz_diffuse(surface_tilt=surface_tilt, **kwargs)
elif model == 'schlick_diffuse':
iams = iam.schlick_diffuse(surface_tilt=surface_tilt)
else:
raise ValueError(model + ' is not a valid diffuse IAM model')

if isinstance(surface_tilt, pd.Series):
iams = pd.DataFrame(iams, index=surface_tilt.index)

return iams

def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
effective_irradiance=None, longwave_down=None):
"""
Expand Down
68 changes: 68 additions & 0 deletions tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,74 @@ def test_PVSystem_get_iam_invalid(sapm_module_params, mocker):
system.get_iam(45, iam_model='not_a_model')


def test_PVSystem_get_iam_diffuse_marion(sapm_module_params, mocker):
model_params = {'b': 0.05}
m = mocker.spy(_iam, 'marion_diffuse')
system = pvsystem.PVSystem(module_parameters=model_params)
tilt = 30
iam = system.get_iam_diffuse(tilt, iam_model='marion_diffuse',
marion_model='ashrae')
m.assert_called_with(model='ashrae', surface_tilt=tilt,
**model_params)
assert isinstance(iam, dict)
assert set(iam.keys()) == {'sky', 'ground', 'horizon'}

system = pvsystem.PVSystem(module_parameters=sapm_module_params)
tilt = pd.Series([30, 60])
iam = system.get_iam_diffuse(tilt, iam_model='marion_diffuse',
marion_model='sapm')
assert isinstance(iam, pd.DataFrame)


@pytest.mark.parametrize('iam_model', ['martin_ruiz_diffuse',
'schlick_diffuse'])
def test_PVSystem_get_iam_diffuse(iam_model, mocker):
model_params = {'a_r': 0.16} if iam_model == 'martin_ruiz_diffuse' else {}
m = mocker.spy(_iam, iam_model)
system = pvsystem.PVSystem(module_parameters=model_params)
tilt = 30
iam = system.get_iam_diffuse(tilt, iam_model=iam_model)
m.assert_called_with(surface_tilt=tilt, **model_params)
assert isinstance(iam, dict)


def test_PVSystem_multi_array_get_iam_diffuse():
model_params = {'b': 0.05}
system = pvsystem.PVSystem(
arrays=[pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
module_parameters=model_params),
pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
module_parameters=model_params)]
)
iam = system.get_iam_diffuse((30, 60), iam_model='marion_diffuse',
marion_model='ashrae')
assert len(iam) == 2
assert iam[0] != iam[1]
with pytest.raises(ValueError,
match="Length mismatch for per-array parameter"):
system.get_iam_diffuse((30,), iam_model='marion_diffuse',
marion_model='ashrae')


def test_PVSystem_get_iam_diffuse_invalid(sapm_module_params):
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
with pytest.raises(ValueError):
system.get_iam_diffuse(45, iam_model='not_a_model')


def test_PVSystem_get_iam_diffuse_marion_invalid(sapm_module_params):
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
with pytest.raises(ValueError):
system.get_iam_diffuse(45, iam_model='marion_diffuse',
marion_model='not_a_model')


def test_PVSystem_get_iam_diffuse_marion_missing_model(sapm_module_params):
system = pvsystem.PVSystem(module_parameters=sapm_module_params)
with pytest.raises(ValueError, match="marion_model must be specified"):
system.get_iam_diffuse(45, iam_model='marion_diffuse')


def test_retrieve_sam_raises_exceptions():
"""
Raise an exception if an invalid parameter is provided to `retrieve_sam()`.
Expand Down
Loading