diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 31cfabdb02..735b89ff10 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -213,11 +213,11 @@ Low-level functions for solving the single diode equation. .. autosummary:: :toctree: generated/ - singlediode_methods.estimate_voc - singlediode_methods.bishop88 - singlediode_methods.bishop88_i_from_v - singlediode_methods.bishop88_v_from_i - singlediode_methods.bishop88_mpp + singlediode.estimate_voc + singlediode.bishop88 + singlediode.bishop88_i_from_v + singlediode.bishop88_v_from_i + singlediode.bishop88_mpp SAPM model ---------- diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index a5ad0d3e4e..f1106b151a 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -81,6 +81,7 @@ Contents api comparison_pvlib_matlab variables_style_rules + singlediode Indices and tables diff --git a/docs/sphinx/source/singlediode.rst b/docs/sphinx/source/singlediode.rst new file mode 100644 index 0000000000..2fafa7d9f5 --- /dev/null +++ b/docs/sphinx/source/singlediode.rst @@ -0,0 +1,114 @@ +.. _singlediode: + +Single Diode Equation +===================== + +This section reviews the solutions to the single diode equation used in +pvlib-python to generate an IV curve of a PV module. + +pvlib-python supports two ways to solve the single diode equation: + +1. Lambert W-Function +2. Bishop's Algorithm + +The :func:`pvlib.pvsystem.singlediode` function allows the user to choose the +method using the ``method`` keyword. + +Lambert W-Function +------------------ +When ``method='lambertw'``, the Lambert W-function is used as previously shown +by Jain, Kapoor [1, 2] and Hansen [3]. The following algorithm can be found on +`Wikipedia: Theory of Solar Cells +`_, given the basic single +diode model equation. + +.. math:: + + I = I_L - I_0 \left(\exp \left(\frac{V + I R_s}{n Ns V_{th}} \right) - 1 \right) + - \frac{V + I R_s}{R_{sh}} + +Lambert W-function is the inverse of the function +:math:`f \left( w \right) = w \exp \left( w \right)` or +:math:`w = f^{-1} \left( w \exp \left( w \right) \right)` also given as +:math:`w = W \left( w \exp \left( w \right) \right)`. Defining the following +parameter, :math:`z`, is necessary to transform the single diode equation into +a form that can be expressed as a Lambert W-function. + +.. math:: + + z = \frac{R_s I_0}{n Ns V_{th} \left(1 + \frac{R_s}{R_{sh}} \right)} \exp \left( + \frac{R_s \left( I_L + I_0 \right) + V}{n Ns V_{th} \left(1 + \frac{R_s}{R_{sh}}\right)} + \right) + +Then the module current can be solved using the Lambert W-function, +:math:`W \left(z \right)`. + +.. math:: + + I = \frac{I_L + I_0 - \frac{V}{R_{sh}}}{1 + \frac{R_s}{R_{sh}}} + - \frac{n Ns V_{th}}{R_s} W \left(z \right) + + +Bishop's Algorithm +------------------ +The function :func:`pvlib.singlediode.bishop88` uses an explicit solution [4] +that finds points on the IV curve by first solving for pairs :math:`(V_d, I)` +where :math:`V_d` is the diode voltage :math:`V_d = V + I*Rs`. Then the voltage +is backed out from :math:`V_d`. Points with specific voltage, such as open +circuit, are located using the bisection search method, ``brentq``, bounded +by a zero diode voltage and an estimate of open circuit voltage given by + +.. math:: + + V_{oc, est} = n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right) + +We know that :math:`V_d = 0` corresponds to a voltage less than zero, and +we can also show that when :math:`V_d = V_{oc, est}`, the resulting +current is also negative, meaning that the corresponding voltage must be +in the 4th quadrant and therefore greater than the open circuit voltage +(see proof below). Therefore the entire forward-bias 1st quadrant IV-curve +is bounded because :math:`V_{oc} < V_{oc, est}`, and so a bisection search +between 0 and :math:`V_{oc, est}` will always find any desired condition in the +1st quadrant including :math:`V_{oc}`. + +.. math:: + + I = I_L - I_0 \left(\exp \left(\frac{V_{oc, est}}{n Ns V_{th}} \right) - 1 \right) + - \frac{V_{oc, est}}{R_{sh}} \newline + + I = I_L - I_0 \left(\exp \left(\frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{n Ns V_{th}} \right) - 1 \right) + - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline + + I = I_L - I_0 \left(\exp \left(\log \left(\frac{I_L}{I_0} + 1 \right) \right) - 1 \right) + - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline + + I = I_L - I_0 \left(\frac{I_L}{I_0} + 1 - 1 \right) + - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline + + I = I_L - I_0 \left(\frac{I_L}{I_0} \right) + - \frac{n Ns V_{th} \log \left(\frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline + + I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right)}{R_{sh}} \newline + + I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right)}{R_{sh}} + +References +---------- +[1] "Exact analytical solutions of the parameters of real solar cells using +Lambert W-function," A. Jain, A. Kapoor, Solar Energy Materials and Solar Cells, +81, (2004) pp 269-277. +:doi:`10.1016/j.solmat.2003.11.018` + +[2] "A new method to determine the diode ideality factor of real solar cell +using Lambert W-function," A. Jain, A. Kapoor, Solar Energy Materials and Solar +Cells, 85, (2005) 391-396. +:doi:`10.1016/j.solmat.2004.05.022` + +[3] "Parameter Estimation for Single Diode Models of Photovoltaic Modules," +Clifford W. Hansen, Sandia `Report SAND2015-2065 +`_, +2015 :doi:`10.13140/RG.2.1.4336.7842` + +[4] "Computer simulation of the effects of electrical mismatches in +photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) +:doi:`10.1016/0379-6787(88)90059-2` \ No newline at end of file diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index b60a683c5f..1db4dc91a5 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -32,7 +32,7 @@ Enhancements ``method`` in ``('lambertw', 'newton', 'brentq')``, default is ``'lambertw'``, to select a method to solve the single diode equation for points on the IV curve. Selecting either ``'brentq'`` or ``'newton'`` as the method uses - :func:`~pvlib.singlediode_methods.bishop88` with the corresponding method. + :func:`~pvlib.singlediode.bishop88` with the corresponding method. (:issue:`410`) * Implement new methods ``'brentq'`` and ``'newton'`` for solving the single diode equation for points on the IV curve. ``'brentq'`` uses a bisection @@ -40,24 +40,24 @@ Enhancements uses the Newton-Raphson method and may be faster but is not guaranteed to converge. However, ``'newton'`` should be safe for well-behaved IV curves. (:issue:`408`) -* Implement :func:`~pvlib.singlediode_methods.bishop88` for explicit calculation +* Implement :func:`~pvlib.singlediode.bishop88` for explicit calculation of arbitrary IV curve points using diode voltage instead of cell voltage. If ``method`` is either ``'newton'`` or ``'brentq'`` and ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` is provided, the IV curve points will be log spaced instead of linear. -* Implement :func:`~pvlib.singlediode_methods.estimate_voc` to estimate open +* Implement :func:`~pvlib.singlediode.estimate_voc` to estimate open circuit voltage by assuming :math:`R_{sh} \to \infty` and :math:`R_s=0` as an upper bound in bisection method for :func:`~pvlib.pvsystem.singlediode` when method is either ``'newton'`` or ``'brentq'``. * Add :func:`~pvlib.pvsystem.max_power_point` method to compute the max power point using the new ``'brentq'`` method. -* Add new module ``pvlib.singlediode_methods`` with low-level functions for +* Add new module ``pvlib.singlediode`` with low-level functions for solving the single diode equation such as: - :func:`~pvlib.singlediode_methods.bishop88`, - :func:`~pvlib.singlediode_methods.estimate_voc`, - :func:`~pvlib.singlediode_methods.bishop88_i_from_v`, - :func:`~pvlib.singlediode_methods.bishop88_v_from_i`, and - :func:`~pvlib.singlediode_methods.bishop88_mpp`. + :func:`~pvlib.singlediode.bishop88`, + :func:`~pvlib.singlediode.estimate_voc`, + :func:`~pvlib.singlediode.bishop88_i_from_v`, + :func:`~pvlib.singlediode.bishop88_v_from_i`, and + :func:`~pvlib.singlediode.bishop88_mpp`. * Add PVSyst thin-film recombination losses for CdTe and a:Si (:issue:`163`) * Python 3.7 officially supported. (:issue:`496`) @@ -84,6 +84,8 @@ Documentation * Updated several incorrect statements in ModelChain documentation regarding implementation status and default values. (:issue:`480`) * Expanded general contributing and pull request guidelines. +* Added section on single diode equation with some detail on solutions used in + pvlib-python (:issue:`518`) Testing diff --git a/pvlib/__init__.py b/pvlib/__init__.py index 3a231da728..0606737ee5 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -11,4 +11,4 @@ from pvlib import pvsystem from pvlib import spa from pvlib import modelchain -from pvlib import singlediode_methods +from pvlib import singlediode diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e44617f36a..a89da7dd48 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -20,7 +20,7 @@ from pvlib.tools import _build_kwargs from pvlib.location import Location from pvlib import irradiance, atmosphere -from pvlib import singlediode_methods +import pvlib # use pvlib.singlediode to avoid clash with local method # not sure if this belongs in the pvsystem module. @@ -1963,52 +1963,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, open-circuit. If the method is either ``'newton'`` or ``'brentq'`` and ``ivcurve_pnts`` - are indicated, then :func:`pvlib.singlediode_methods.bishop88` is used to + are indicated, then :func:`pvlib.singlediode.bishop88` [4] is used to calculate the points on the IV curve points at diode voltages from zero to open-circuit voltage with a log spacing that gets closer as voltage increases. If the method is ``'lambertw'`` then the calculated points on the IV curve are linearly spaced. - The ``bishop88`` method uses an explicit solution from [4] that finds - points on the IV curve by first solving for pairs :math:`(V_d, I)` where - :math:`V_d` is the diode voltage :math:`V_d = V + I*Rs`. Then the voltage - is backed out from :math:`V_d`. Points with specific voltage, such as open - circuit, are located using the bisection search method, ``brentq``, bounded - by a zero diode voltage and an estimate of open circuit voltage given by - - .. math:: - - V_{oc, est} = n Ns V_{th} \\log \\left( \\frac{I_L}{I_0} + 1 \\right) - - We know that :math:`V_d = 0` corresponds to a voltage less than zero, and - we can also show that when :math:`V_d = V_{oc, est}`, the resulting - current is also negative, meaning that the corresponding voltage must be - in the 4th quadrant and therefore greater than the open circuit voltage - (see proof below). Therefore the entire forward-bias 1st quadrant IV-curve - is bounded, and a bisection search within these points will always find - desired condition. - - .. math:: - - I = I_L - I_0 \\left(\\exp \\left(\\frac{V_{oc, est}}{n Ns V_{th}} \\right) - 1 \\right) - - \\frac{V_{oc, est}}{R_{sh}} \\newline - - I = I_L - I_0 \\left(\\exp \\left(\\frac{n Ns V_{th} \\log \\left(\\frac{I_L}{I_0} + 1 \\right)}{n Ns V_{th}} \\right) - 1 \\right) - - \\frac{n Ns V_{th} \\log \\left(\\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} \\newline - - I = I_L - I_0 \\left(\\exp \\left(\\log \\left(\\frac{I_L}{I_0} + 1 \\right) \\right) - 1 \\right) - - \\frac{n Ns V_{th} \\log \\left(\\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} \\newline - - I = I_L - I_0 \\left(\\frac{I_L}{I_0} + 1 - 1 \\right) - - \\frac{n Ns V_{th} \\log \\left(\\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} \\newline - - I = I_L - I_0 \\left(\\frac{I_L}{I_0} \\right) - - \\frac{n Ns V_{th} \\log \\left(\\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} \\newline - - I = I_L - I_L - \\frac{n Ns V_{th} \log \\left( \\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} \\newline - - I = - \\frac{n Ns V_{th} \\log \\left( \\frac{I_L}{I_0} + 1 \\right)}{R_{sh}} - References ----------- [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN @@ -2029,12 +1989,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, -------- sapm calcparams_desoto - pvlib.singlediode_methods.bishop88 + pvlib.singlediode.bishop88 """ # Calculate points on the IV curve using the LambertW solution to the # single diode equation if method.lower() == 'lambertw': - out = singlediode_methods._lambertw( + out = pvlib.singlediode._lambertw( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts ) @@ -2047,19 +2007,19 @@ def singlediode(photocurrent, saturation_current, resistance_series, # equation for the diode voltage V_d then backing out voltage args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args - v_oc = singlediode_methods.bishop88_v_from_i( + v_oc = pvlib.singlediode.bishop88_v_from_i( 0.0, *args, method=method.lower() ) - i_mp, v_mp, p_mp = singlediode_methods.bishop88_mpp( + i_mp, v_mp, p_mp = pvlib.singlediode.bishop88_mpp( *args, method=method.lower() ) - i_sc = singlediode_methods.bishop88_i_from_v( + i_sc = pvlib.singlediode.bishop88_i_from_v( 0.0, *args, method=method.lower() ) - i_x = singlediode_methods.bishop88_i_from_v( + i_x = pvlib.singlediode.bishop88_i_from_v( v_oc / 2.0, *args, method=method.lower() ) - i_xx = singlediode_methods.bishop88_i_from_v( + i_xx = pvlib.singlediode.bishop88_i_from_v( (v_oc + v_mp) / 2.0, *args, method=method.lower() ) @@ -2069,7 +2029,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - ivcurve_i, ivcurve_v, _ = singlediode_methods.bishop88(vd, *args) + ivcurve_i, ivcurve_v, _ = pvlib.singlediode.bishop88(vd, *args) out = OrderedDict() out['i_sc'] = i_sc @@ -2125,7 +2085,7 @@ def max_power_point(photocurrent, saturation_current, resistance_series, curve. This function uses Brent's method by default because it is guaranteed to converge. """ - i_mp, v_mp, p_mp = singlediode_methods.bishop88_mpp( + i_mp, v_mp, p_mp = pvlib.singlediode.bishop88_mpp( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, method=method.lower() ) @@ -2205,7 +2165,7 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, Energy Materials and Solar Cells, 81 (2004) 269-277. ''' if method.lower() == 'lambertw': - return singlediode_methods._lambertw_v_from_i( + return pvlib.singlediode._lambertw_v_from_i( resistance_shunt, resistance_series, nNsVth, current, saturation_current, photocurrent ) @@ -2215,9 +2175,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, # equation for the diode voltage V_d then backing out voltage args = (current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - V = singlediode_methods.bishop88_v_from_i(*args, method=method.lower()) + V = pvlib.singlediode.bishop88_v_from_i(*args, method=method.lower()) # find the right size and shape for returns - size, shape = singlediode_methods._get_size_and_shape(args) + size, shape = pvlib.singlediode._get_size_and_shape(args) if size <= 1: if shape is not None: V = np.tile(V, shape) @@ -2293,7 +2253,7 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, Energy Materials and Solar Cells, 81 (2004) 269-277. ''' if method.lower() == 'lambertw': - return singlediode_methods._lambertw_i_from_v( + return pvlib.singlediode._lambertw_i_from_v( resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent ) @@ -2303,9 +2263,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, # equation for the diode voltage V_d then backing out voltage args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - I = singlediode_methods.bishop88_i_from_v(*args, method=method.lower()) + I = pvlib.singlediode.bishop88_i_from_v(*args, method=method.lower()) # find the right size and shape for returns - size, shape = singlediode_methods._get_size_and_shape(args) + size, shape = pvlib.singlediode._get_size_and_shape(args) if size <= 1: if shape is not None: I = np.tile(I, shape) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode.py similarity index 100% rename from pvlib/singlediode_methods.py rename to pvlib/singlediode.py diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 8d4ebd3d7d..50b86c6e3a 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -6,13 +6,13 @@ This module can be executed from the command line to generate a high precision dataset of I-V curve points to test the explicit single diode calculations -:func:`pvlib.singlediode_methods.bishop88`:: +:func:`pvlib.singlediode.bishop88`:: $ python test_numeric_precision.py This generates a file in the pvlib data folder, which is specified by the constant ``DATA_PATH``. When the test is run using ``pytest`` it will compare -the values calculated by :func:`pvlib.singlediode_methods.bishop88` with the +the values calculated by :func:`pvlib.singlediode.bishop88` with the high-precision values generated with SymPy. """ @@ -21,7 +21,7 @@ import numpy as np import pandas as pd from pvlib import pvsystem -from pvlib.singlediode_methods import bishop88, estimate_voc +from pvlib.singlediode import bishop88, estimate_voc logging.basicConfig() LOGGER = logging.getLogger(__name__) diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode.py similarity index 99% rename from pvlib/test/test_singlediode_methods.py rename to pvlib/test/test_singlediode.py index 27deaf4276..3d659eb450 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode.py @@ -4,7 +4,7 @@ import numpy as np from pvlib import pvsystem -from pvlib.singlediode_methods import bishop88, estimate_voc, VOLTAGE_BUILTIN +from pvlib.singlediode import bishop88, estimate_voc, VOLTAGE_BUILTIN import pytest from conftest import requires_scipy diff --git a/pvlib/tools.py b/pvlib/tools.py index 16596af885..72835e8803 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -255,8 +255,8 @@ def _build_kwargs(keys, input_dict): # FIXME: remove _array_newton when SciPy-1.2.0 is released -# pvlib.singlediode_methods.bishop88_i_from_v(..., method='newton') and other -# functions in singlediode_methods call scipy.optimize.newton with a vector +# pvlib.singlediode.bishop88_i_from_v(..., method='newton') and other +# functions in singlediode call scipy.optimize.newton with a vector # unfortunately wrapping the functions with np.vectorize() was too slow # a vectorized newton method was merged into SciPy but isn't released yet, so # in the meantime, we just copied the relevant code: "_array_newton" for more