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 all 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
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.12.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Enhancements
* :py:mod:`pvlib.ivtools.sdm` is now a subpackage. (:issue:`2252`, :pull:`2256`)
* Add a function for estimating PVsyst SDM parameters from IEC 61853-1 matrix
data (:py:func:`~pvlib.ivtools.sdm.fit_pvsyst_iec61853_sandia_2025`). (:issue:`2185`, :pull:`2429`)
* 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`)
* Add optional arguments ``temperature_ref`` and ``irradiance_ref`` to
:py:func:`~pvlib.pvsystem.sapm`(:issue:`2432`, :pull:`2434`)

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
26 changes: 15 additions & 11 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
# a dict of required parameter names for each DC power model
_DC_MODEL_PARAMS = {
'sapm': {
'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7',
# i_x and i_xx params (IXO, IXXO, C4-C7) not required
'C0', 'C1', 'C2', 'C3',
'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco',
'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series',
'IXO', 'IXXO'},
'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series'},
'desoto': {
'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',
'R_sh_ref', 'R_s'},
Expand Down Expand Up @@ -2229,9 +2229,11 @@ def sapm(effective_irradiance, temp_cell, module, *, temperature_ref=25,
* 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 ``IXO``, ``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 ``IXXO``, ``C6``,
and ``C7`` parameters are not supplied.

Notes
-----
Expand Down Expand Up @@ -2335,13 +2337,15 @@ def sapm(effective_irradiance, temp_cell, module, *, temperature_ref=25,

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 - temperature_ref)))
if 'IXO' in module and 'C4' in module and 'C5' in module:
out['i_x'] = (
module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
(1 + module['Aisc']*(temp_cell - temperature_ref)))

out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temperature_ref)))
if 'IXXO' in module and 'C6' in module and 'C7' in module:
out['i_xx'] = (
module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
(1 + module['Aimp']*(temp_cell - temperature_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 ['IXO', 'IXXO', '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 = ['IXO', 'IXXO', '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
Loading