Repository: pvlib/pvlib-python (python)

Issue:
`pvsystem.sapm` documentation: clarification of coefficients used
**Is your feature request related to a problem? Please describe.**
[`pvlib.pvsystem.sapm`](https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.pvsystem.sapm.html#pvlib.pvsystem.sapm) requires effective irradiance and SAPM coefficients as inputs.

https://github.com/pvlib/pvlib-python/blob/22364d85b2f0844624b9a8160ea38535b82d8360/pvlib/pvsystem.py#L2169-L2171
https://github.com/pvlib/pvlib-python/blob/22364d85b2f0844624b9a8160ea38535b82d8360/pvlib/pvsystem.py#L2176-L2178

`effective_irradiance` considers AOI/spectrum. The notes section explaining `module` contains a table explaining the SAPM coefficients:
https://github.com/pvlib/pvlib-python/blob/22364d85b2f0844624b9a8160ea38535b82d8360/pvlib/pvsystem.py#L2194-L2235

I'm wondering whether any clarification over which coefficients are actually used is necessary. I think it's helpful to present the full table, but could someone be misled into thinking that since `A0-A4` are required, a spectral correction is being applied (for example)

**Describe the solution you'd like**
Suggestion: add a line or two to the notes section clarifying which coefficients are used.

**Describe alternatives you've considered**
Such a revision could be redundant since `effective_irradiance` is already clearly (and correctly) defined. However, when I first glanced over these docs, I just felt like some clarification could help some users. Not a major issue; I am interested to hear whether anyone else thinks this would help or not.


Pull request:
Clarify which SAPM parameters are required by `module` in `pvsystem.sapm`
 - [X] Closes #2392
 - [X] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing/index.html)
 - [ ] ~Tests added~
 - [ ] ~Updates entries in [`docs/sphinx/source/reference`](https://github.com/pvlib/pvlib-python/blob/main/docs/sphinx/source/reference) for API changes.~
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/main/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - [X] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [X] Maintainer: Appropriate GitHub Labels (including `remote-data`) and Milestone are assigned to the Pull Request and linked Issue.

Patch:
diff --git a/docs/sphinx/source/whatsnew/v0.12.1.rst b/docs/sphinx/source/whatsnew/v0.12.1.rst
index 84f9a90..88cdfc6 100644
--- a/docs/sphinx/source/whatsnew/v0.12.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.12.1.rst
@@ -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`.
+  (:issue:`2402`, :pull:`2433`)
 * Add optional arguments ``temperature_ref`` and ``irradiance_ref`` to
   :py:func:`~pvlib.pvsystem.sapm`(:issue:`2432`, :pull:`2434`)
 * Add NREL NSRDB PSM v4 API client to :py:mod:`pvlib.iotools`. See
@@ -30,8 +34,10 @@ Enhancements
 Documentation
 ~~~~~~~~~~~~~
 * Add a supporting reference to :py:func:`pvlib.atmosphere.get_relative_airmass` (:issue:`2390`, :pull:`2424`)
-* Documented how `np.nan` values are handled by :py:func:`~pvlib.spectrum.average_photon_energy`
+* Document how ``np.nan`` values are handled by :py:func:`~pvlib.spectrum.average_photon_energy`
   (:issue:`2423`, :pull:`2426`)
+* Clarify which SAPM coefficients are required by the ``module`` parameter in
+  :py:func:`~pvlib.pvsystem.sapm` (:issue:`2392`, :pull:`2435`)
 * Update references in :py:func`~pvlib.irradiance.get_extra_radiation`
   (:issue:`2333`, :pull:`2347`)
 
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 0f58da5..0f5fc5f 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -611,7 +611,7 @@ class ModelChain:
         """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:
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index 9e96111..bba6f5a 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -27,10 +27,10 @@ import pvlib.tools as tools
 # 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'},
@@ -2229,18 +2229,33 @@ 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
     -----
-    The SAPM parameters which are required in ``module`` are
-    listed in the following table.
-
     The Sandia module database contains parameter values for a limited set
     of modules. The CEC module database does not contain these parameters.
-    Both databases can be accessed using :py:func:`retrieve_sam`.
+    Both databases can be accessed using :py:func:`retrieve_sam`. The full list
+    of SAPM parameters is presented in the table below. Those that are required
+    in the ``module`` parameter to run this model are as follows:
+
+    * ``C0``, ``C1``, ``C2``, ``C3``
+    * ``Isco``
+    * ``Impo``
+    * ``Voco``
+    * ``Vmpo``
+    * ``Aisc``
+    * ``Aimp``
+    * ``Bvoco``
+    * ``Mbvoc``
+    * ``Bvmpo``
+    * ``Mbvmp``
+    * ``N``
+    * ``Cells_in_series``
 
     ================   ========================================================
     Key                Description
@@ -2335,13 +2350,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)


Test patch:
diff --git a/tests/test_modelchain.py b/tests/test_modelchain.py
index 1bdcb87..51b4018 100644
--- a/tests/test_modelchain.py
+++ b/tests/test_modelchain.py
@@ -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'
diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py
index ebd015d..4473e6b 100644
--- a/tests/test_pvsystem.py
+++ b/tests/test_pvsystem.py
@@ -17,7 +17,6 @@ from pvlib import spectrum
 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
 
@@ -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')