Repository: pvlib/pvlib-python

Issue:
CEC 6-parameter coefficient generation
SAM is able to extract the CEC parameters required for calcparams_desoto.  This is done through the 'CEC Performance Model with User Entered Specifications' module model, and coefficients are automatically extracted given nameplate parameters Voc, Isc, Imp, Vmp and TempCoeff.  The method is based on Aron Dobos' "An Improved Coefficient Calculator for the California Energy Commission 6 Parameter Photovoltaic Module Model ", 2012

Ideally we should be able to work with the SAM open source code, extract the bit that does the coefficient generation, and put it into a PVLib function that would allow users to run calcparams_desoto with any arbitrary module type.  At the moment we are dependent on PV modules loaded into the SAM or CEC database.

Thank you!



Discussion:
SAM solution routine is located at https://github.com/NREL/ssc/blob/develop/shared/6par_solve.h , function "solve_with_sanity_and_heuristics" . Additional dependencies on other files 6par_newton.h, 6par_search.h, 6par_jacobian.h, 6par_lu.h, 6par_gamma.h, all located in https://github.com/NREL/ssc/tree/develop/shared . 
I'd like to try to take this up sometime soon, maybe by the end of the year? Is anyone else working on it? I haven't seen a PR. Does anyone have any strong opinions? Is seems like there's a SAM implementation, is there a PVLIB-MATLAB version already? thx
I'm not working on it. If it is practical, it would be great to wrap the SAM method. NREL put a lot of effort into the heuristics for initial values and updating.  We have a function in PVLib for MATLAB but it's a very different algorithm. For consistency with the parameter databases it would be better to use the SAM algorithm.
Do I read between the lines that the two algorithms do not converge on the same parameters?  Then it would be of great interest to have python implementations of each!  Or perhaps even more important: documents describing the full algorithms along with every heuristic, assumption and constraint they implement...
@adriesse yes. Each model fitting algorithm I'm aware of, will produce different parameter values from the same data although the predicted IV curves may be similar.
FWIW I am trying to establish a common/fair metric to compare different model fits in PVfit. In addition to choosing, say, which residual(s) to analyze, the effect of model discrepancy significantly affects such comparisons. PVfit’s orthogonal distance regression (ODR) fits to the single-diode model (SDM) often are worse than other algorithms’ fits in terms of terminal current residuals, but I have observed consistently that this is likely due to model discrepancy. One might claim that ODR is being more “honest” here, but I think it really means that the model should be improved. Note that PVfit’s ODR alternatively optimizes the residual of the sum of currents at the high-voltage diode node in the equivalent-circuit model, and it doesn’t make unfounded assumptions about error-free measurements in all data channels except the terminal current (which also has implications for parameter non-identifiability). I have seen plenty of evidence that ODR fits are “good” in the absence of significant model discrepancy, such as for some (but not all!) double-diode model (DDM) fits across various material systems. 
> If it is practical, it would be great to wrap the SAM method. 

Two kinds of practicalities here:

1. Technical. I don't know how to do it but I am sure it's possible. I am ok with it so long as pvlib remains straightforward to install from source.
2. Legal. We would need assurance from NREL that it's ok for pvlib to distribute this SAM code under the terms of a MIT license rather than SAM's mixed MIT/GPL license.

In the next 6-8 months, the NREL team will be updating our python wrapper for the SAM software development kit so that you can pip install the sam-sdk and call its routines in a much more native pythonic fashion. That might be a quick way to implement the CEC parameter generation to solve both the technical and legal practicalities Will mentions above, as well as the many file dependencies involved with that routine. The SDK is licensed separately from the SAM open source code under an MIT type license, so that distribution with pvlib would be ok, and once we have it set up as a python package, that should make it a non-issue to include with pvlib installation from source. 

To @adriesse 's point, the publication associated with the method implemented in SAM is here: http://solarenergyengineering.asmedigitalcollection.asme.org/article.aspx?articleid=1458865 
Thanks @janinefreeman that is great news!
@janinefreeman Does NREL's web site no longer provide free preprints of Aron's paper?
@thunderfish24  I'm not finding a copy of it in NREL's pubs database, but it looks like you may be able to get it here (didn't create an account to give it a try): https://www.osti.gov/biblio/1043759 
@thunderfish24 I sent you a preprint.

Patch:
diff --git a/pvlib/__init__.py b/pvlib/__init__.py
--- a/pvlib/__init__.py
+++ b/pvlib/__init__.py
@@ -7,6 +7,7 @@
 from pvlib import location
 from pvlib import solarposition
 from pvlib import iotools
+from pvlib import ivtools
 from pvlib import tracking
 from pvlib import pvsystem
 from pvlib import spa
diff --git a/pvlib/ivtools.py b/pvlib/ivtools.py
new file mode 100644
--- /dev/null
+++ b/pvlib/ivtools.py
@@ -0,0 +1,350 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 29 10:34:10 2019
+
+@author: cwhanse
+"""
+
+import numpy as np
+
+
+def fit_sdm_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc,
+                    gamma_pmp, cells_in_series, temp_ref=25):
+    """
+    Estimates parameters for the CEC single diode model (SDM) using the SAM
+    SDK.
+
+    Parameters
+    ----------
+    celltype : str
+        Value is one of 'monoSi', 'multiSi', 'polySi', 'cis', 'cigs', 'cdte',
+        'amorphous'
+    v_mp : float
+        Voltage at maximum power point [V]
+    i_mp : float
+        Current at maximum power point [A]
+    v_oc : float
+        Open circuit voltage [V]
+    i_sc : float
+        Short circuit current [A]
+    alpha_sc : float
+        Temperature coefficient of short circuit current [A/C]
+    beta_voc : float
+        Temperature coefficient of open circuit voltage [V/C]
+    gamma_pmp : float
+        Temperature coefficient of power at maximum point point [%/C]
+    cells_in_series : int
+        Number of cells in series
+    temp_ref : float, default 25
+        Reference temperature condition [C]
+
+    Returns
+    -------
+    tuple of the following elements:
+
+        * I_L_ref : float
+            The light-generated current (or photocurrent) at reference
+            conditions [A]
+
+        * I_o_ref : float
+            The dark or diode reverse saturation current at reference
+            conditions [A]
+
+        * R_sh_ref : float
+            The shunt resistance at reference conditions, in ohms.
+
+        * R_s : float
+            The series resistance at reference conditions, in ohms.
+
+        * a_ref : float
+            The product of the usual diode ideality factor ``n`` (unitless),
+            number of cells in series ``Ns``, and cell thermal voltage at
+            reference conditions [V]
+
+        * Adjust : float
+            The adjustment to the temperature coefficient for short circuit
+            current, in percent.
+
+    Raises
+    ------
+        ImportError if NREL-PySAM is not installed.
+
+        RuntimeError if parameter extraction is not successful.
+
+    Notes
+    -----
+    Inputs ``v_mp``, ``v_oc``, ``i_mp`` and ``i_sc`` are assumed to be from a
+    single IV curve at constant irradiance and cell temperature. Irradiance is
+    not explicitly used by the fitting procedure. The irradiance level at which
+    the input IV curve is determined and the specified cell temperature
+    ``temp_ref`` are the reference conditions for the output parameters
+    ``I_L_ref``, ``I_o_ref``, ``R_sh_ref``, ``R_s``, ``a_ref`` and ``Adjust``.
+
+    References
+    ----------
+    [1] A. Dobos, "An Improved Coefficient Calculator for the California
+    Energy Commission 6 Parameter Photovoltaic Module Model", Journal of
+    Solar Energy Engineering, vol 134, 2012.
+    """
+
+    try:
+        from PySAM import PySSC
+    except ImportError:
+        raise ImportError("Requires NREL's PySAM package at "
+                          "https://pypi.org/project/NREL-PySAM/.")
+
+    datadict = {'tech_model': '6parsolve', 'financial_model': 'none',
+                'celltype': celltype, 'Vmp': v_mp,
+                'Imp': i_mp, 'Voc': v_oc, 'Isc': i_sc, 'alpha_isc': alpha_sc,
+                'beta_voc': beta_voc, 'gamma_pmp': gamma_pmp,
+                'Nser': cells_in_series, 'Tref': temp_ref}
+
+    result = PySSC.ssc_sim_from_dict(datadict)
+    if result['cmod_success'] == 1:
+        return tuple([result[k] for k in ['Il', 'Io', 'Rsh', 'Rs', 'a',
+                      'Adj']])
+    else:
+        raise RuntimeError('Parameter estimation failed')
+
+
+def fit_sde_sandia(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
+                   vlim=0.2, ilim=0.1):
+    r"""
+    Fits the single diode equation (SDE) to an IV curve.
+
+    Parameters
+    ----------
+    voltage : ndarray
+        1D array of `float` type containing voltage at each point on the IV
+        curve, increasing from 0 to ``v_oc`` inclusive [V]
+
+    current : ndarray
+        1D array of `float` type containing current at each point on the IV
+        curve, from ``i_sc`` to 0 inclusive [A]
+
+    v_oc : float, default None
+        Open circuit voltage [V]. If not provided, ``v_oc`` is taken as the
+        last point in the ``voltage`` array.
+
+    i_sc : float, default None
+        Short circuit current [A]. If not provided, ``i_sc`` is taken as the
+        first point in the ``current`` array.
+
+    v_mp_i_mp : tuple of float, default None
+        Voltage, current at maximum power point in units of [V], [A].
+        If not provided, the maximum power point is found at the maximum of
+        ``voltage`` \times ``current``.
+
+    vlim : float, default 0.2
+        Defines portion of IV curve where the exponential term in the single
+        diode equation can be neglected, i.e.
+        ``voltage`` <= ``vlim`` x ``v_oc`` [V]
+
+    ilim : float, default 0.1
+        Defines portion of the IV curve where the exponential term in the
+        single diode equation is signficant, approximately defined by
+        ``current`` < (1 - ``ilim``) x ``i_sc`` [A]
+
+    Returns
+    -------
+    tuple of the following elements:
+
+        * photocurrent : float
+            photocurrent [A]
+        * saturation_current : float
+            dark (saturation) current [A]
+        * resistance_shunt : float
+            shunt (parallel) resistance, in ohms
+        * resistance_series : float
+            series resistance, in ohms
+        * nNsVth : float
+            product of thermal voltage ``Vth`` [V], diode ideality factor
+            ``n``, and number of series cells ``Ns``
+
+    Raises
+    ------
+    RuntimeError if parameter extraction is not successful.
+
+    Notes
+    -----
+    Inputs ``voltage``, ``current``, ``v_oc``, ``i_sc`` and ``v_mp_i_mp`` are
+    assumed to be from a single IV curve at constant irradiance and cell
+    temperature.
+
+    :py:func:`fit_single_diode_sandia` obtains values for the five parameters
+    for the single diode equation [1]:
+
+    .. math::
+
+        I = I_{L} - I_{0} (\exp \frac{V + I R_{s}}{nNsVth} - 1)
+        - \frac{V + I R_{s}}{R_{sh}}
+
+    See :py:func:`pvsystem.singlediode` for definition of the parameters.
+
+    The extraction method [2] proceeds in six steps.
+
+    1. In the single diode equation, replace :math:`R_{sh} = 1/G_{p}` and
+       re-arrange
+
+    .. math::
+
+        I = \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p} R_{s}}
+        - \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nNsVth}) - 1)
+
+    2. The linear portion of the IV curve is defined as
+       :math:`V \le vlim \times v_oc`. Over this portion of the IV curve,
+
+    .. math::
+
+        \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nNsVth}) - 1)
+        \approx 0
+
+    3. Fit the linear portion of the IV curve with a line.
+
+    .. math::
+
+        I &\approx \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p}
+        R_{s}} \\
+        &= \beta_{0} + \beta_{1} V
+
+    4. The exponential portion of the IV curve is defined by
+       :math:`\beta_{0} + \beta_{1} \times V - I > ilim \times i_sc`.
+       Over this portion of the curve, :math:`exp((V + IRs)/nNsVth) >> 1`
+       so that
+
+    .. math::
+
+        \exp(\frac{V + I R_{s}}{nNsVth}) - 1 \approx
+        \exp(\frac{V + I R_{s}}{nNsVth})
+
+    5. Fit the exponential portion of the IV curve.
+
+    .. math::
+
+        \log(\beta_{0} - \beta_{1} V - I)
+        &\approx \log(\frac{I_{0}}{1 + G_{p} R_{s}} + \frac{V}{nNsVth}
+        + \frac{I R_{s}}{nNsVth} \\
+        &= \beta_{2} + beta_{3} V + \beta_{4} I
+
+    6. Calculate values for ``IL, I0, Rs, Rsh,`` and ``nNsVth`` from the
+       regression coefficents :math:`\beta_{0}, \beta_{1}, \beta_{3}` and
+       :math:`\beta_{4}`.
+
+
+    References
+    ----------
+    [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
+    0 86758 909 4
+    [2] C. B. Jones, C. W. Hansen, Single Diode Parameter Extraction from
+    In-Field Photovoltaic I-V Curves on a Single Board Computer, 46th IEEE
+    Photovoltaic Specialist Conference, Chicago, IL, 2019
+    """
+
+    # If not provided, extract v_oc, i_sc, v_mp and i_mp from the IV curve data
+    if v_oc is None:
+        v_oc = voltage[-1]
+    if i_sc is None:
+        i_sc = current[0]
+    if v_mp_i_mp is not None:
+        v_mp, i_mp = v_mp_i_mp
+    else:
+        v_mp, i_mp = _find_mp(voltage, current)
+
+    # Find beta0 and beta1 from linear portion of the IV curve
+    beta0, beta1 = _find_beta0_beta1(voltage, current, vlim, v_oc)
+
+    # Find beta3 and beta4 from the exponential portion of the IV curve
+    beta3, beta4 = _find_beta3_beta4(voltage, current, beta0, beta1, ilim,
+                                     i_sc)
+
+    # calculate single diode parameters from regression coefficients
+    return _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp,
+                                     v_oc)
+
+
+def _find_mp(voltage, current):
+    """
+    Finds voltage and current at maximum power point.
+
+    Parameters
+    ----------
+    voltage : ndarray
+        1D array containing voltage at each point on the IV curve, increasing
+        from 0 to v_oc inclusive, of `float` type [V]
+
+    current : ndarray
+        1D array containing current at each point on the IV curve, decreasing
+        from i_sc to 0 inclusive, of `float` type [A]
+
+    Returns
+    -------
+    v_mp, i_mp : tuple
+        voltage ``v_mp`` and current ``i_mp`` at the maximum power point [V],
+        [A]
+    """
+    p = voltage * current
+    idx = np.argmax(p)
+    return voltage[idx], current[idx]
+
+
+def _calc_I0(IL, I, V, Gp, Rs, nNsVth):
+    return (IL - I - Gp * V - Gp * Rs * I) / np.exp((V + Rs * I) / nNsVth)
+
+
+def _find_beta0_beta1(v, i, vlim, v_oc):
+    # Get intercept and slope of linear portion of IV curve.
+    # Start with V =< vlim * v_oc, extend by adding points until slope is
+    # negative (downward).
+    beta0 = np.nan
+    beta1 = np.nan
+    first_idx = np.searchsorted(v, vlim * v_oc)
+    for idx in range(first_idx, len(v)):
+        coef = np.polyfit(v[:idx], i[:idx], deg=1)
+        if coef[0] < 0:
+            # intercept term
+            beta0 = coef[1].item()
+            # sign change of slope to get positive parameter value
+            beta1 = -coef[0].item()
+            break
+    if any(np.isnan([beta0, beta1])):
+        raise RuntimeError("Parameter extraction failed: beta0={}, beta1={}"
+                           .format(beta0, beta1))
+    else:
+        return beta0, beta1
+
+
+def _find_beta3_beta4(voltage, current, beta0, beta1, ilim, i_sc):
+    # Subtract the IV curve from the linear fit.
+    y = beta0 - beta1 * voltage - current
+    x = np.array([np.ones_like(voltage), voltage, current]).T
+    # Select points where y > ilim * i_sc to regress log(y) onto x
+    idx = (y > ilim * i_sc)
+    result = np.linalg.lstsq(x[idx], np.log(y[idx]), rcond=None)
+    coef = result[0]
+    beta3 = coef[1].item()
+    beta4 = coef[2].item()
+    if any(np.isnan([beta3, beta4])):
+        raise RuntimeError("Parameter extraction failed: beta3={}, beta4={}"
+                           .format(beta3, beta4))
+    else:
+        return beta3, beta4
+
+
+def _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc):
+    nNsVth = 1.0 / beta3
+    Rs = beta4 / beta3
+    Gp = beta1 / (1.0 - Rs * beta1)
+    Rsh = 1.0 / Gp
+    IL = (1 + Gp * Rs) * beta0
+    # calculate I0
+    I0_vmp = _calc_I0(IL, i_mp, v_mp, Gp, Rs, nNsVth)
+    I0_voc = _calc_I0(IL, 0, v_oc, Gp, Rs, nNsVth)
+    if any(np.isnan([I0_vmp, I0_voc])) or ((I0_vmp <= 0) and (I0_voc <= 0)):
+        raise RuntimeError("Parameter extraction failed: I0 is undetermined.")
+    elif (I0_vmp > 0) and (I0_voc > 0):
+        I0 = 0.5 * (I0_vmp + I0_voc)
+    elif (I0_vmp > 0):
+        I0 = I0_vmp
+    else:  # I0_voc > 0
+        I0 = I0_voc
+    return (IL, I0, Rsh, Rs, nNsVth)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -44,8 +44,8 @@
 TESTS_REQUIRE = ['nose', 'pytest', 'pytest-cov', 'pytest-mock',
                  'pytest-timeout']
 EXTRAS_REQUIRE = {
-    'optional': ['ephem', 'cython', 'netcdf4', 'numba', 'pvfactors', 'scipy',
-                 'siphon', 'tables'],
+    'optional': ['ephem', 'cython', 'netcdf4', 'nrel-pysam', 'numba',
+                 'pvfactors', 'scipy', 'siphon', 'tables'],
     'doc': ['ipython', 'matplotlib', 'sphinx', 'sphinx_rtd_theme'],
     'test': TESTS_REQUIRE
 }


Test patch:
diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py
--- a/pvlib/test/conftest.py
+++ b/pvlib/test/conftest.py
@@ -151,6 +151,15 @@ def has_numba():
                                         reason='requires pvfactors')
 
 
+try:
+    import PySAM  # noqa: F401
+    has_pysam = True
+except ImportError:
+    has_pysam = False
+
+requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")
+
+
 @pytest.fixture(scope="session")
 def sam_data():
     data = {}
diff --git a/pvlib/test/test_ivtools.py b/pvlib/test/test_ivtools.py
new file mode 100644
--- /dev/null
+++ b/pvlib/test/test_ivtools.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu May  9 10:51:15 2019
+
+@author: cwhanse
+"""
+
+import numpy as np
+import pandas as pd
+import pytest
+from pvlib import pvsystem
+from pvlib import ivtools
+from pvlib.test.conftest import requires_scipy, requires_pysam
+
+
+@pytest.fixture
+def get_test_iv_params():
+    return {'IL': 8.0, 'I0': 5e-10, 'Rsh': 1000, 'Rs': 0.2, 'nNsVth': 1.61864}
+
+
+@pytest.fixture
+def get_cec_params_cansol_cs5p_220p():
+    return {'input': {'V_mp_ref': 46.6, 'I_mp_ref': 4.73, 'V_oc_ref': 58.3,
+                      'I_sc_ref': 5.05, 'alpha_sc': 0.0025,
+                      'beta_voc': -0.19659, 'gamma_pmp': -0.43,
+                      'cells_in_series': 96},
+            'output': {'a_ref': 2.3674, 'I_L_ref': 5.056, 'I_o_ref': 1.01e-10,
+                       'R_sh_ref': 837.51, 'R_s': 1.004, 'Adjust': 2.3}}
+
+
+@requires_scipy
+def test_fit_sde_sandia(get_test_iv_params, get_bad_iv_curves):
+    test_params = get_test_iv_params
+    testcurve = pvsystem.singlediode(photocurrent=test_params['IL'],
+                                     saturation_current=test_params['I0'],
+                                     resistance_shunt=test_params['Rsh'],
+                                     resistance_series=test_params['Rs'],
+                                     nNsVth=test_params['nNsVth'],
+                                     ivcurve_pnts=300)
+    expected = tuple(test_params[k] for k in ['IL', 'I0', 'Rsh', 'Rs',
+                     'nNsVth'])
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'])
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'],
+                                    v_oc=testcurve['v_oc'],
+                                    i_sc=testcurve['i_sc'])
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'],
+                                    v_oc=testcurve['v_oc'],
+                                    i_sc=testcurve['i_sc'],
+                                    v_mp_i_mp=(testcurve['v_mp'],
+                                    testcurve['i_mp']))
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'], vlim=0.1)
+    assert np.allclose(result, expected, rtol=5e-5)
+
+
+@requires_scipy
+def test_fit_sde_sandia_bad_iv(get_bad_iv_curves):
+    # bad IV curves for coverage of if/then in _calculate_sde_parameters
+    v1, i1, v2, i2 = get_bad_iv_curves
+    result = ivtools.fit_sde_sandia(voltage=v1, current=i1)
+    assert np.allclose(result, (-2.4322856072799985, 8.854688976836396,
+                                -63.56227601452038, 111.18558915546389,
+                                -137.9965046659527))
+    result = ivtools.fit_sde_sandia(voltage=v2, current=i2)
+    assert np.allclose(result, (2.62405311949227, 1.8657963912925288,
+                                110.35202827739991, -65.652554411442,
+                                174.49362093001415))
+
+
+@requires_pysam
+def test_fit_sdm_cec_sam(get_cec_params_cansol_cs5p_220p):
+    input_data = get_cec_params_cansol_cs5p_220p['input']
+    I_L_ref, I_o_ref, R_sh_ref, R_s, a_ref, Adjust = \
+        ivtools.fit_sdm_cec_sam(
+            celltype='polySi', v_mp=input_data['V_mp_ref'],
+            i_mp=input_data['I_mp_ref'], v_oc=input_data['V_oc_ref'],
+            i_sc=input_data['I_sc_ref'], alpha_sc=input_data['alpha_sc'],
+            beta_voc=input_data['beta_voc'],
+            gamma_pmp=input_data['gamma_pmp'],
+            cells_in_series=input_data['cells_in_series'])
+    expected = pd.Series(get_cec_params_cansol_cs5p_220p['output'])
+    modeled = pd.Series(index=expected.index, data=np.nan)
+    modeled['a_ref'] = a_ref
+    modeled['I_L_ref'] = I_L_ref
+    modeled['I_o_ref'] = I_o_ref
+    modeled['R_sh_ref'] = R_sh_ref
+    modeled['R_s'] = R_s
+    modeled['Adjust'] = Adjust
+    assert np.allclose(modeled.values, expected.values, rtol=5e-2)
+    # test for fitting failure
+    with pytest.raises(RuntimeError):
+        I_L_ref, I_o_ref, R_sh_ref, R_s, a_ref, Adjust = \
+            ivtools.fit_sdm_cec_sam(
+                celltype='polySi', v_mp=0.45, i_mp=5.25, v_oc=0.55, i_sc=5.5,
+                alpha_sc=0.00275, beta_voc=0.00275, gamma_pmp=0.0055,
+                cells_in_series=1, temp_ref=25)
+
+
+@pytest.fixture
+def get_bad_iv_curves():
+    # v1, i1 produces a bad value for I0_voc
+    v1 = np.array([0, 0.338798867469060, 0.677597734938121, 1.01639660240718,
+                   1.35519546987624, 1.69399433734530, 2.03279320481436,
+                   2.37159207228342, 2.71039093975248, 3.04918980722154,
+                   3.38798867469060, 3.72678754215966, 4.06558640962873,
+                   4.40438527709779, 4.74318414456685, 5.08198301203591,
+                   5.42078187950497, 5.75958074697403, 6.09837961444309,
+                   6.43717848191215, 6.77597734938121, 7.11477621685027,
+                   7.45357508431933, 7.79237395178839, 8.13117281925745,
+                   8.46997168672651, 8.80877055419557, 9.14756942166463,
+                   9.48636828913369, 9.82516715660275, 10.1639660240718,
+                   10.5027648915409, 10.8415637590099, 11.1803626264790,
+                   11.5191614939481, 11.8579603614171, 12.1967592288862,
+                   12.5355580963552, 12.8743569638243, 13.2131558312934,
+                   13.5519546987624, 13.8907535662315, 14.2295524337005,
+                   14.5683513011696, 14.9071501686387, 15.2459490361077,
+                   15.5847479035768, 15.9235467710458, 16.2623456385149,
+                   16.6011445059840, 16.9399433734530, 17.2787422409221,
+                   17.6175411083911, 17.9563399758602, 18.2951388433293,
+                   18.6339377107983, 18.9727365782674, 19.3115354457364,
+                   19.6503343132055, 19.9891331806746, 20.3279320481436,
+                   20.6667309156127, 21.0055297830817, 21.3443286505508,
+                   21.6831275180199, 22.0219263854889, 22.3607252529580,
+                   22.6995241204270, 23.0383229878961, 23.3771218553652,
+                   23.7159207228342, 24.0547195903033, 24.3935184577724,
+                   24.7323173252414, 25.0711161927105, 25.4099150601795,
+                   25.7487139276486, 26.0875127951177, 26.4263116625867,
+                   26.7651105300558, 27.1039093975248, 27.4427082649939,
+                   27.7815071324630, 28.1203059999320, 28.4591048674011,
+                   28.7979037348701, 29.1367026023392, 29.4755014698083,
+                   29.8143003372773, 30.1530992047464, 30.4918980722154,
+                   30.8306969396845, 31.1694958071536, 31.5082946746226,
+                   31.8470935420917, 32.1858924095607, 32.5246912770298,
+                   32.8634901444989, 33.2022890119679, 33.5410878794370])
+    i1 = np.array([3.39430882774470, 2.80864492110761, 3.28358165429196,
+                   3.41191190551673, 3.11975662808148, 3.35436585834612,
+                   3.23953272899809, 3.60307083325333, 2.80478101508277,
+                   2.80505102853845, 3.16918996870373, 3.21088388439857,
+                   3.46332865310431, 3.09224155015883, 3.17541550741062,
+                   3.32470179290389, 3.33224664316240, 3.07709000050741,
+                   2.89141245343405, 3.01365768561537, 3.23265176770231,
+                   3.32253647634228, 2.97900657569736, 3.31959549243966,
+                   3.03375461550111, 2.97579298978937, 3.25432831375159,
+                   2.89178382564454, 3.00341909207567, 3.72637492250097,
+                   3.28379856976360, 2.96516169245835, 3.25658381110230,
+                   3.41655911533139, 3.02718097944604, 3.11458376760376,
+                   3.24617304369762, 3.45935502367636, 3.21557333256913,
+                   3.27611176482650, 2.86954135732485, 3.32416319254657,
+                   3.15277467598732, 3.08272557013770, 3.15602202666259,
+                   3.49432799877150, 3.53863997177632, 3.10602611478455,
+                   3.05373911151821, 3.09876772570781, 2.97417228624287,
+                   2.84573593699237, 3.16288578405195, 3.06533173612783,
+                   3.02118336639575, 3.34374977225502, 2.97255164138821,
+                   3.19286135682863, 3.10999753817133, 3.26925354620079,
+                   3.11957809501529, 3.20155017481720, 3.31724984405837,
+                   3.42879043512927, 3.17933067619240, 3.47777362613969,
+                   3.20708912539777, 3.48205761174907, 3.16804363684327,
+                   3.14055472378230, 3.13445657434470, 2.91152696252998,
+                   3.10984113847427, 2.80443349399489, 3.23146278164875,
+                   2.94521083406108, 3.17388903141715, 3.05930294897030,
+                   3.18985234673287, 3.27946609274898, 3.33717523113602,
+                   2.76394303462702, 3.19375132937510, 2.82628616689450,
+                   2.85238527394143, 2.82975892599489, 2.79196912313914,
+                   2.72860792049395, 2.75585977414140, 2.44280222448805,
+                   2.36052347370628, 2.26785071765738, 2.10868255743462,
+                   2.06165739407987, 1.90047259509385, 1.39925575828709,
+                   1.24749015957606, 0.867823806536762, 0.432752457749993, 0])
+    # v2, i2 produces a bad value for I0_vmp
+    v2 = np.array([0, 0.365686097622586, 0.731372195245173, 1.09705829286776,
+                   1.46274439049035, 1.82843048811293, 2.19411658573552,
+                   2.55980268335810, 2.92548878098069, 3.29117487860328,
+                   3.65686097622586, 4.02254707384845, 4.38823317147104,
+                   4.75391926909362, 5.11960536671621, 5.48529146433880,
+                   5.85097756196138, 6.21666365958397, 6.58234975720655,
+                   6.94803585482914, 7.31372195245173, 7.67940805007431,
+                   8.04509414769690, 8.41078024531949, 8.77646634294207,
+                   9.14215244056466, 9.50783853818725, 9.87352463580983,
+                   10.2392107334324, 10.6048968310550, 10.9705829286776,
+                   11.3362690263002, 11.7019551239228, 12.0676412215454,
+                   12.4333273191679, 12.7990134167905, 13.1646995144131,
+                   13.5303856120357, 13.8960717096583, 14.2617578072809,
+                   14.6274439049035, 14.9931300025260, 15.3588161001486,
+                   15.7245021977712, 16.0901882953938, 16.4558743930164,
+                   16.8215604906390, 17.1872465882616, 17.5529326858841,
+                   17.9186187835067, 18.2843048811293, 18.6499909787519,
+                   19.0156770763745, 19.3813631739971, 19.7470492716197,
+                   20.1127353692422, 20.4784214668648, 20.8441075644874,
+                   21.2097936621100, 21.5754797597326, 21.9411658573552,
+                   22.3068519549778, 22.6725380526004, 23.0382241502229,
+                   23.4039102478455, 23.7695963454681, 24.1352824430907,
+                   24.5009685407133, 24.8666546383359, 25.2323407359585,
+                   25.5980268335810, 25.9637129312036, 26.3293990288262,
+                   26.6950851264488, 27.0607712240714, 27.4264573216940,
+                   27.7921434193166, 28.1578295169392, 28.5235156145617,
+                   28.8892017121843, 29.2548878098069, 29.6205739074295,
+                   29.9862600050521, 30.3519461026747, 30.7176322002973,
+                   31.0833182979198, 31.4490043955424, 31.8146904931650,
+                   32.1803765907876, 32.5460626884102, 32.9117487860328,
+                   33.2774348836554, 33.6431209812779, 34.0088070789005,
+                   34.3744931765231, 34.7401792741457, 35.1058653717683,
+                   35.4715514693909, 35.8372375670135, 36.2029236646360])
+    i2 = np.array([6.49218806928330, 6.49139336899548, 6.17810697175204,
+                   6.75197816263663, 6.59529074137515, 6.18164578868300,
+                   6.38709397931910, 6.30685422248427, 6.44640615548925,
+                   6.88727230397772, 6.42074852785591, 6.46348580823746,
+                   6.38642309763941, 5.66356277572311, 6.61010381702082,
+                   6.33288284311125, 6.22475343933610, 6.30651399433833,
+                   6.44435022944051, 6.43741711131908, 6.03536180208946,
+                   6.23814639328170, 5.97229140403242, 6.20790000748341,
+                   6.22933550182341, 6.22992127804882, 6.13400871899299,
+                   6.83491312449950, 6.07952797245846, 6.35837746415450,
+                   6.41972128662324, 6.85256717258275, 6.25807797296759,
+                   6.25124948151766, 6.22229212812413, 6.72249444167406,
+                   6.41085549981649, 6.75792874870056, 6.22096181559171,
+                   6.47839564388996, 6.56010208597432, 6.63300966556949,
+                   6.34617546039339, 6.79812221146153, 6.14486056194136,
+                   6.14979256889311, 6.16883037644880, 6.57309183229605,
+                   6.40064681038509, 6.18861448239873, 6.91340138179698,
+                   5.94164388433788, 6.23638991745862, 6.31898940411710,
+                   6.45247884556830, 6.58081455524297, 6.64915284801713,
+                   6.07122119270245, 6.41398258148256, 6.62144271089614,
+                   6.36377197712687, 6.51487678829345, 6.53418950147730,
+                   6.18886469125371, 6.26341063475750, 6.83488211680259,
+                   6.62699397226695, 6.41286837534735, 6.44060085001851,
+                   6.48114130629288, 6.18607038456406, 6.16923370572396,
+                   6.64223126283631, 6.07231852289266, 5.79043710204375,
+                   6.48463886529882, 6.36263392044401, 6.11212476454494,
+                   6.14573900812925, 6.12568047243240, 6.43836230231577,
+                   6.02505694060219, 6.13819468942244, 6.22100593815064,
+                   6.02394682666345, 5.89016573063789, 5.74448527739202,
+                   5.50415294280017, 5.31883018164157, 4.87476769510305,
+                   4.74386713755523, 4.60638346931628, 4.06177345572680,
+                   3.73334482123538, 3.13848311672243, 2.71638862600768,
+                   2.02963773590165, 1.49291145092070, 0.818343889647352, 0])
+
+    return v1, i1, v2, i2