Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
41 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
52255f2
Fix Array.get_iam_diffuse kwargs
cbcrespo Sep 3, 2026
e93b7c1
Implement suggestions by cwhanse
cbcrespo Sep 9, 2026
1fc0e1d
Apply batched suggestions from code review
cbcrespo Sep 9, 2026
4935587
Implement suggestions by kandersolar
cbcrespo Sep 14, 2026
508139b
Merge branch 'array-iam' of https://github.com/cbcrespo/pvlib-python …
cbcrespo Sep 14, 2026
478f02c
Remove marion_diffuse as default
cbcrespo Sep 15, 2026
cae647e
Fix backticks
cbcrespo Sep 16, 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
Comment thread
cbcrespo marked this conversation as resolved.
Outdated

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).
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
The tilt angle of the surface in degrees.
iam_model : string, default 'marion_diffuse'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that having marion_diffuse as the default is not wise, so long as it may cause users to run into #1402.

Maybe switch to martin-ruiz, which is the more common choice anyway? Maybe leave it with no default? Not sure...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I went with marion_diffuse is that it is the only one which provides an IAM for all three components (sky, horizon, and ground). But #1402 is indeed a problem. If switching, I would maybe prefer to leave it with no default, but I'm curious to let others weigh in.

@ramaroesilva ramaroesilva Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe have no default.

Nonetheless, #2824 would address this risk. So maybe this can be revisited at a later point in time 👀

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with default iam_model=None. The current settings, iam_model='marion' and marion_model=None, will error. That shouldn't be the default behavior. Since a user has to choose at least marion_model, might as well choose both kwargs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we have a certain quorum here. The only downside I see is that it requires users to be aware of this new functionality, but I have been thinking that maybe a small technical note could be made to showcase this.

A side topic to be discussed by e-mail maybe :-)

Comment thread
cbcrespo marked this conversation as resolved.
Outdated
The IAM model to be used. Valid strings are 'marion_diffuse',
'martin_ruiz_diffuse', and 'schlick_diffuse'.
marion_model : string, default None
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
components available from the selected model
(e.g., isotropic, circumsolar, horizon brightening).
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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.
Comment thread
cbcrespo marked this conversation as resolved.
Outdated

Parameters
----------
surface_tilt : float or Series
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
The tilt angle of the surface in degrees.
iam_model : string, default 'marion_diffuse'
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
The IAM model to be used. Valid strings are 'marion_diffuse',
'martin_ruiz_diffuse' and 'schlick_diffuse'.
marion_model : string, default None
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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.
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
"""
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')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see that this is copied over from get_iam, but we have this dict so that we don't have to hack around with inspect.signature:

_IAM_MODEL_PARAMS = {

I suggest we go ahead with this as-is, but file an issue to eventually come back and switch to _IAM_MODEL_PARAMS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and changed this line to use _IAM_MODEL_PARAMS as it's a straightforward change, but get_iam should probably be changed separately. inspect.signature is also still currently being used within get_iam_diffuse for martin_ruiz_diffuse.

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):
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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):
Comment thread
cbcrespo marked this conversation as resolved.
Outdated
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