Skip to content

Make i_x and i_xx SAPM parameters optional #2433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/sphinx/source/whatsnew/v0.12.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ Bug fixes
Enhancements
~~~~~~~~~~~~
* ``pvlib.ivtools.sdm`` is now a subpackage. (:issue:`2252`, :pull:`2256`)

* The parameters for the Ix and Ixx points are now optional when using
:py:func:`pvlib.pvsystem.sapm` directly and through
:py:class:`~pvlib.pvsystem.PVSystem` and :py:class:`~pvlib.modelchain.ModelChain`.
Comment on lines +21 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

Not super important, but is there a reason for ~ on the class reference but not the function reference?

Copy link
Member Author

Choose a reason for hiding this comment

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

The thought was to make it clear that the change applies to both the function and class layers. If it rendered as just sapm, it would be visually ambiguous whether it was pvlib.pvsystem.sapm or pvlib.pvsystem.PVSystem.sapm.

(:issue:`2402`, :pull:`2433`)

Documentation
~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ def infer_dc_model(self):
"""Infer DC power model from Array module parameters."""
params = _common_keys(
tuple(array.module_parameters for array in self.system.arrays))
if {'A0', 'A1', 'C7'} <= params:
if {'A0', 'A1', 'C3'} <= params:
return self.sapm, 'sapm'
elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s',
'Adjust'} <= params:
Expand Down
24 changes: 13 additions & 11 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
from abc import ABC, abstractmethod
from typing import Optional, Union

from pvlib._deprecation import deprecated

import pvlib # used to avoid albedo name collision in the Array class
from pvlib import (atmosphere, iam, inverter, irradiance,
singlediode as _singlediode, spectrum, temperature)
Expand All @@ -29,7 +27,7 @@
# a dict of required parameter names for each DC power model
_DC_MODEL_PARAMS = {
'sapm': {
'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7',
'C0', 'C1', 'C2', 'C3', # C4-C7 (i_x and i_xx params) not required
'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco',
'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series',
'IXO', 'IXXO'},
Expand Down Expand Up @@ -2224,9 +2222,11 @@ def sapm(effective_irradiance, temp_cell, module):
* v_mp : Voltage at maximum-power point (V)
* p_mp : Power at maximum-power point (W)
* i_x : Current at module V = 0.5Voc, defines 4th point on I-V
curve for modeling curve shape
curve for modeling curve shape. Omitted if ``C4`` and ``C5``
parameters are not supplied.
* i_xx : Current at module V = 0.5(Voc+Vmp), defines 5th point on
I-V curve for modeling curve shape
I-V curve for modeling curve shape. Omitted if ``C6`` and ``C7``
parameters are not supplied.

Notes
-----
Expand Down Expand Up @@ -2334,13 +2334,15 @@ def sapm(effective_irradiance, temp_cell, module):

out['p_mp'] = out['i_mp'] * out['v_mp']

out['i_x'] = (
module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - temp_ref)))
if 'C4' in module and 'C5' in module:
out['i_x'] = (
module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - temp_ref)))

out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temp_ref)))
if 'C6' in module and 'C7' in module:
out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temp_ref)))

if isinstance(out['i_sc'], pd.Series):
out = pd.DataFrame(out)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,16 @@ def test_invalid_dc_model_params(sapm_dc_snl_ac_system, cec_dc_snl_ac_system,
ModelChain(pvwatts_dc_pvwatts_ac_system, location, **kwargs)


def test_sapm_optional_params(sapm_dc_snl_ac_system, location):
# inference works when the optional (i_x, i_xx) SAPM parameters are missing
for array in sapm_dc_snl_ac_system.arrays:
for key in ['C4', 'C5', 'C6', 'C7']:
array.module_parameters.pop(key)

# no error:
ModelChain(sapm_dc_snl_ac_system, location)


@pytest.mark.parametrize('model', [
'dc_model', 'ac_model', 'aoi_model', 'spectral_model',
'temperature_model', 'losses_model'
Expand Down
10 changes: 9 additions & 1 deletion tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from pvlib.location import Location
from pvlib.pvsystem import FixedMount
from pvlib import temperature
from pvlib._deprecation import pvlibDeprecationWarning
from pvlib.tools import cosd
from pvlib.singlediode import VOLTAGE_BUILTIN

Expand Down Expand Up @@ -197,6 +196,15 @@ def test_sapm(sapm_module_params):
pvsystem.sapm(effective_irradiance, temp_cell,
pd.Series(sapm_module_params))

# ensure C4-C7 are optional
optional_keys = ['C4', 'C5', 'C6', 'C7']
params_no_c4c7 = {
k: v for k, v in sapm_module_params.items() if k not in optional_keys
}
out = pvsystem.sapm(effective_irradiance, temp_cell, params_no_c4c7)
assert 'i_x' not in out.keys()
assert 'i_xx' not in out.keys()


def test_PVSystem_sapm(sapm_module_params, mocker):
mocker.spy(pvsystem, 'sapm')
Expand Down