From 22d2e5c70ad3d7a3626d464103bec3e3ff102bb5 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Sat, 27 Jan 2018 02:50:10 -0800 Subject: [PATCH 01/73] add methods and tests for a faster way to calculate single-diode model --- pvlib/test/test_way_faster.py | 73 +++++++++++++++++++ pvlib/way_faster.py | 129 ++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 pvlib/test/test_way_faster.py create mode 100644 pvlib/way_faster.py diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py new file mode 100644 index 0000000000..a87cd59ca1 --- /dev/null +++ b/pvlib/test/test_way_faster.py @@ -0,0 +1,73 @@ +""" +testing way faster single-diode methods using JW Bishop 1988 +""" + +from time import clock +import logging +import numpy as np +from pvlib import pvsystem +from pvlib.way_faster import faster_way + +logging.basicConfig() +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +POA = 888 +TCELL = 55 +CECMOD = pvsystem.retrieve_sam('cecmod') + + +def test_spr_e20_327(): + spr_e20_327 = CECMOD.SunPower_SPR_E20_327 + x = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, + EgRef=1.121, dEgdT=-0.0002677) + il, io, rs, rsh, nnsvt = x + tstart = clock() + pvs = pvsystem.singlediode(*x) + tstop = clock() + LOGGER.debug('single diode elapsed time = %g[s]', tstop - tstart) + tstart = clock() + voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) + tstop = clock() + LOGGER.debug('way faster elapsed time = %g[s]', tstop - tstart) + assert np.isclose(pvs['i_sc'], isc) + assert np.isclose(pvs['v_oc'], voc) + # the singlediode method doesn't actually get the MPP correct + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + assert np.isclose(pvs_imp, imp) + assert np.isclose(pvs_vmp, vmp) + assert np.isclose(pvs['p_mp'], pmp) + return voc, isc, imp, vmp, pmp, pvs + + +def test_fs_495(): + fs_495 = CECMOD.First_Solar_FS_495 + x = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=fs_495.alpha_sc, module_parameters=fs_495, + EgRef=1.475, dEgdT=-0.0003) + il, io, rs, rsh, nnsvt = x + tstart = clock() + pvs = pvsystem.singlediode(*x) + tstop = clock() + LOGGER.debug('single diode elapsed time = %g[s]', tstop - tstart) + tstart = clock() + voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) + tstop = clock() + LOGGER.debug('way faster elapsed time = %g[s]', tstop - tstart) + assert np.isclose(pvs['i_sc'], isc) + assert np.isclose(pvs['v_oc'], voc) + # the singlediode method doesn't actually get the MPP correct + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + assert np.isclose(pvs_imp, imp) + assert np.isclose(pvs_vmp, vmp) + assert np.isclose(pvs['p_mp'], pmp) + return voc, isc, imp, vmp, pmp, pvs + +if __name__ == '__main__': + r_spr_e20_327 = test_spr_e20_327() + r_fs_495 = test_fs_495() diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py new file mode 100644 index 0000000000..d20fa8a020 --- /dev/null +++ b/pvlib/way_faster.py @@ -0,0 +1,129 @@ +"""faster ways""" + +import logging +import numpy as np + +logging.basicConfig() +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +EPS = np.finfo(float).eps +DAMP = 1.5 +DELTA = EPS**0.33 + + +def est_voc(il, io, nnsvt): + # http://www.pveducation.org/pvcdrom/open-circuit-voltage + return nnsvt * np.log(il / io + 1.0) + + +def bishop88(vd, il, io, rs, rsh, nnsvt): + """bishop 1988""" + a = np.exp(vd / nnsvt) + b = 1.0 / rsh + i = il - io * (a - 1.0) - vd * b + v = vd - i * rs + c = io * a / nnsvt + grad_i = - c - b # di/dvd + grad_v = 1.0 - grad_i * rs # dv/dvd + # dp/dv = d(iv)/dv = v * di/dv + i + grad = grad_i / grad_v + grad_p = v * grad + i # dp/dv + grad2i = -c / nnsvt + grad2v = -grad2i * rs + grad2p = (grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + + grad_i) + return i, v, grad_i, grad_v, i*v, grad_p, grad2p + + +def newton_solver(fjx, x0, x, tol=EPS, damp=DAMP, log=False, test=True): + resnorm = np.inf # nor of residuals + while resnorm > tol: + f, j = fjx(x0, *x) + newton_step = f / j + # don't let step get crazy + if np.abs(newton_step / x0) > damp: + break + x0 -= newton_step + resnorm = f**2 + if log: + LOGGER.debug( + 'x0=%g, newton step=%g, f=%g, resnorm=%g', + x0, newton_step, f, resnorm + ) + if test: + f2, _ = fjx(x0 * (1.0 + DELTA), *x) + LOGGER.debug('test_grad=%g', (f2 - f) / x0 / DELTA) + LOGGER.debug('grad=%g', j) + return x0, f, j + + +def faster_way(il, io, rs, rsh, nnsvt, + tol=EPS, damp=DAMP, log=True, test=True): + """a faster way""" + x = (il, io, rs, rsh, nnsvt) # collect args + # first estimate Voc + voc_est = est_voc(il, io, nnsvt) + # find the real voc + resnorm = np.inf # nor of residuals + while resnorm > tol: + i_test, v_test, grad, _, _, _, _ = bishop88(voc_est, *x) + newton_step = i_test / grad + # don't let step get crazy + if np.abs(newton_step / voc_est) > damp: + break + voc_est -= newton_step + resnorm = i_test**2 + if log: + LOGGER.debug( + 'voc_est=%g, step=%g, i_test=%g, v_test=%g, resnorm=%g', + voc_est, newton_step, i_test, v_test, resnorm + ) + if test: + delta = EPS**0.3 + i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + delta), *x) + LOGGER.debug('test_grad=%g', (i_test2 - i_test) / voc_est / delta) + LOGGER.debug('grad=%g', grad) + # find isc too + vd_sc = 0.0 + resnorm = np.inf # nor of residuals + while resnorm > tol: + isc_est, v_test, _, grad, _, _, _ = bishop88(vd_sc, *x) + newton_step = v_test / grad + # don't let step get crazy + if np.abs(newton_step / voc_est) > damp: + break + vd_sc -= newton_step + resnorm = v_test**2 + if log: + LOGGER.debug( + 'vd_sc=%g, step=%g, isc_est=%g, v_test=%g, resnorm=%g', + vd_sc, newton_step, isc_est, v_test, resnorm + ) + if test: + delta = EPS**0.3 + _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + delta), *x) + LOGGER.debug('test_grad=%g', (v_test2 - v_test) / vd_sc / delta) + LOGGER.debug('grad=%g', grad) + # find the mpp + vd_mp = voc_est + resnorm = np.inf # nor of residuals + while resnorm > tol: + imp_est, vmp_est, _, _, pmp_est, grad_p, grad2p = bishop88(vd_mp, *x) + newton_step = grad_p / grad2p + # don't let step get crazy + if np.abs(newton_step / voc_est) > damp: + break + vd_mp -= newton_step + resnorm = grad_p**2 + if log: + LOGGER.debug( + 'vd_mp=%g, step=%g, pmp_est=%g, resnorm=%g', + vd_mp, newton_step, pmp_est, resnorm + ) + if test: + delta = EPS**0.3 + _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + delta), *x) + LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / delta) + LOGGER.debug('grad=%g', grad2p) + return voc_est, isc_est, imp_est, vmp_est, pmp_est From bb7c49a6a16b0c0354a9c6815625f02e049d9eff Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Sat, 27 Jan 2018 03:18:43 -0800 Subject: [PATCH 02/73] log estimate of speedup in test --- pvlib/test/test_way_faster.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index a87cd59ca1..eaa121a487 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -27,11 +27,14 @@ def test_spr_e20_327(): tstart = clock() pvs = pvsystem.singlediode(*x) tstop = clock() - LOGGER.debug('single diode elapsed time = %g[s]', tstop - tstart) + dt_slow = tstop - tstart + LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) tstop = clock() - LOGGER.debug('way faster elapsed time = %g[s]', tstop - tstart) + dt_fast = tstop - tstart + LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) + LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct @@ -53,11 +56,14 @@ def test_fs_495(): tstart = clock() pvs = pvsystem.singlediode(*x) tstop = clock() - LOGGER.debug('single diode elapsed time = %g[s]', tstop - tstart) + dt_slow = tstop - tstart + LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) tstop = clock() - LOGGER.debug('way faster elapsed time = %g[s]', tstop - tstart) + dt_fast = tstop - tstart + LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) + LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct From 36d4f78b15596ab8bcc693ab63f9aa07739353aa Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Sat, 27 Jan 2018 04:05:48 -0800 Subject: [PATCH 03/73] use ordered dict for output, match inputs and outputs, add iv-curve --- pvlib/test/test_way_faster.py | 11 +++++++---- pvlib/way_faster.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index eaa121a487..485fabb9c6 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -30,7 +30,8 @@ def test_spr_e20_327(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) + out = faster_way(*x, log=False, test=False) + isc, voc, imp, vmp, pmp, _, _ = out.values() tstop = clock() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) @@ -43,7 +44,7 @@ def test_spr_e20_327(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) - return voc, isc, imp, vmp, pmp, pvs + return isc, voc, imp, vmp, pmp, pvs def test_fs_495(): @@ -53,13 +54,15 @@ def test_fs_495(): alpha_isc=fs_495.alpha_sc, module_parameters=fs_495, EgRef=1.475, dEgdT=-0.0003) il, io, rs, rsh, nnsvt = x + x += (101, ) tstart = clock() pvs = pvsystem.singlediode(*x) tstop = clock() dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - voc, isc, imp, vmp, pmp = faster_way(*x, log=False, test=False) + out = faster_way(*x, log=False, test=False) + isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() tstop = clock() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) @@ -72,7 +75,7 @@ def test_fs_495(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) - return voc, isc, imp, vmp, pmp, pvs + return isc, voc, imp, vmp, pmp, i, v, p, pvs if __name__ == '__main__': r_spr_e20_327 = test_spr_e20_327() diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index d20fa8a020..cd9e7809b7 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -1,6 +1,7 @@ """faster ways""" import logging +from collections import OrderedDict import numpy as np logging.basicConfig() @@ -58,9 +59,16 @@ def newton_solver(fjx, x0, x, tol=EPS, damp=DAMP, log=False, test=True): return x0, f, j -def faster_way(il, io, rs, rsh, nnsvt, +def faster_way(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts=None, tol=EPS, damp=DAMP, log=True, test=True): """a faster way""" + # FIXME: everything is named the wrong thing! + il = photocurrent + io = saturation_current + rs = resistance_series + rsh = resistance_shunt + nnsvt = nNsVth x = (il, io, rs, rsh, nnsvt) # collect args # first estimate Voc voc_est = est_voc(il, io, nnsvt) @@ -126,4 +134,21 @@ def faster_way(il, io, rs, rsh, nnsvt, _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + delta), *x) LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / delta) LOGGER.debug('grad=%g', grad2p) - return voc_est, isc_est, imp_est, vmp_est, pmp_est + out = OrderedDict() + out['i_sc'] = isc_est + out['v_oc'] = voc_est + out['i_mp'] = imp_est + out['v_mp'] = vmp_est + out['p_mp'] = pmp_est + out['i_x'] = None + out['i_xx'] = None + # calculate the IV curve if requested using bishop88 + if ivcurve_pnts: + vd = voc_est * ( + (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 + ) + i, v, _, _, p, _, _ = bishop88(vd, *x) + out['i'] = i + out['v'] = v + out['p'] = p + return out From 817a30324264546bfd850ecc409a87be24738e5b Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Sun, 28 Jan 2018 12:29:39 -0800 Subject: [PATCH 04/73] add "slow" but reliable ways to calculate i_from_v and vv * add slow_i_from_v(v, *x) which uses bishop88 and the canned bisecton method from scipy.optimize.fminbound to guarantee convergence * ditto for slow_v_from_i(i, *x) * ditto for slow_mppt(*x) * add slower_way(*x) which is the equivalent of faster_way(*x) but using the canned bisection method from scipy.optimize.fminbound instead of the generic newtom step method. * also add some better docstrings and comments Signed-off-by: Mark Mikofski --- pvlib/way_faster.py | 132 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index cd9e7809b7..c177a32c8f 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -1,8 +1,12 @@ -"""faster ways""" +""" +Faster ways to calculate single diode model currents and voltages using +methods from J.W. Bishop (Solar Cells, 1988). +""" import logging from collections import OrderedDict import numpy as np +from scipy.optimize import fminbound logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -14,12 +18,40 @@ def est_voc(il, io, nnsvt): + """ + Rough estimate of open circuit voltage useful for bounding searches for + ``i`` of ``v`` when using :func:`~pvlib.way_faster`. + + :param il: photo-generated current [A] + :param io: diode one reverse saturation or "dark" current [A] + :param nnsvt" product of thermal voltage ``Vt`` [V], ideality factor ``n``, + and number of series cells ``Ns`` + :returns: rough estimate of open circuit voltage [V] + """ # http://www.pveducation.org/pvcdrom/open-circuit-voltage return nnsvt * np.log(il / io + 1.0) def bishop88(vd, il, io, rs, rsh, nnsvt): - """bishop 1988""" + """ + Explicit calculation single-diode-model (SDM) currents and voltages using + diode junction voltages [1]. + + [1] "Computer simulation of the effects of electrical mismatches in + photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) + https://doi.org/10.1016/0379-6787(88)90059-2 + + :param vd: diode voltages [V}] + :param il: photo-generated current [A] + :param io: diode one reverse saturation or "dark" current [A] + :param rs: series resitance [ohms] + :param rsh: shunt resitance [ohms] + :param nnsvt" product of thermal voltage ``Vt`` [V], ideality factor ``n``, + and number of series cells ``Ns`` + :returns: tuple containing currents [A], voltages [V], gradient ``di/dvd``, + gradient ``dv/dvd``, power [W], gradient ``dp/dv``, and gradient + ``d2p/dv/dvd`` + """ a = np.exp(vd / nnsvt) b = 1.0 / rsh i = il - io * (a - 1.0) - vd * b @@ -28,12 +60,13 @@ def bishop88(vd, il, io, rs, rsh, nnsvt): grad_i = - c - b # di/dvd grad_v = 1.0 - grad_i * rs # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i - grad = grad_i / grad_v + grad = grad_i / grad_v # di/dv grad_p = v * grad + i # dp/dv grad2i = -c / nnsvt grad2v = -grad2i * rs - grad2p = (grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) - + grad_i) + grad2p = ( + grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i + ) return i, v, grad_i, grad_v, i*v, grad_p, grad2p @@ -59,6 +92,95 @@ def newton_solver(fjx, x0, x, tol=EPS, damp=DAMP, log=False, test=True): return x0, f, j +def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a slow but reliable way to find current given any voltage. + """ + # FIXME: everything is named the wrong thing! + il = photocurrent + io = saturation_current + rs = resistance_series + rsh = resistance_shunt + nnsvt = nNsVth + x = (il, io, rs, rsh, nnsvt) # collect args + # first bound the search using voc + voc_est = est_voc(il, io, nnsvt) + vd = fminbound(lambda vd: (v - bishop88(vd, *x)[1])**2, 0.0, voc_est) + return bishop88(vd, il, io, rs, rsh, nnsvt)[0] + + +def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a slow but reliable way to find voltage given any current. + """ + # FIXME: everything is named the wrong thing! + il = photocurrent + io = saturation_current + rs = resistance_series + rsh = resistance_shunt + nnsvt = nNsVth + x = (il, io, rs, rsh, nnsvt) # collect args + # first bound the search using voc + voc_est = est_voc(il, io, nnsvt) + vd = fminbound(lambda vd: (i - bishop88(vd, *x)[0])**2, 0.0, voc_est) + return bishop88(vd, il, io, rs, rsh, nnsvt)[1] + +def slow_mppt(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a slow but reliable way to find mpp. + """ + # FIXME: everything is named the wrong thing! + il = photocurrent + io = saturation_current + rs = resistance_series + rsh = resistance_shunt + nnsvt = nNsVth + x = (il, io, rs, rsh, nnsvt) # collect args + # first bound the search using voc + voc_est = est_voc(il, io, nnsvt) + vd = fminbound(lambda vd: -(bishop88(vd, *x)[4])**2, 0.0, voc_est) + i, v, _, _, p, _, _ = bishop88(vd, il, io, rs, rsh, nnsvt) + return i, v, p + + +def slower_way(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts=None): + """ + This is the slow but reliable way. + """ + # FIXME: everything is named the wrong thing! + il = photocurrent + io = saturation_current + rs = resistance_series + rsh = resistance_shunt + nnsvt = nNsVth + x = (il, io, rs, rsh, nnsvt) # collect args + voc = slow_v_from_i(0, *x) + i_sc = slow_i_from_v(0, *x) + i_mp, v_mp, p_mp = slow_mppt(*x) + out = OrderedDict() + out['i_sc'] = i_sc + out['v_oc'] = voc + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = None + out['i_xx'] = None + # calculate the IV curve if requested using bishop88 + if ivcurve_pnts: + vd = voc * ( + (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 + ) + i, v, _, _, p, _, _ = bishop88(vd, *x) + out['i'] = i + out['v'] = v + out['p'] = p + return out + + def faster_way(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None, tol=EPS, damp=DAMP, log=True, test=True): From b17a8cd47089f54fdb0b82ca056010dacd6bafbf Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Mon, 29 Jan 2018 23:30:25 -0800 Subject: [PATCH 05/73] add check for numerical errors --- pvlib/test/test_numerical_precision.py | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pvlib/test/test_numerical_precision.py diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py new file mode 100644 index 0000000000..00049c6141 --- /dev/null +++ b/pvlib/test/test_numerical_precision.py @@ -0,0 +1,63 @@ +""" +Numerical Precision +http://docs.sympy.org/latest/modules/evalf.html#accuracy-and-error-handling +""" + +import logging +import numpy as np +from sympy import symbols, exp as sy_exp +from pvlib import pvsystem +from pvlib import way_faster + +logging.basicConfig() +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +POA = 888 +TCELL = 55 +CECMOD = pvsystem.retrieve_sam('cecmod') + + +def test_numerical_precicion(): + """ + Test that there are no numerical errors due to floating point arithmetic. + """ + il, io, rs, rsh, nnsvt, vd = symbols('il, io, rs, rsh, nnsvt, vd') + a = sy_exp(vd / nnsvt) + b = 1.0 / rsh + i = il - io * (a - 1.0) - vd * b + v = vd - i * rs + c = io * a / nnsvt + grad_i = - c - b # di/dvd + grad_v = 1.0 - grad_i * rs # dv/dvd + # dp/dv = d(iv)/dv = v * di/dv + i + grad = grad_i / grad_v # di/dv + p = i * v + grad_p = v * grad + i # dp/dv + grad2i = -c / nnsvt + grad2v = -grad2i * rs + grad2p = ( + grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i + ) + # get module from cecmod and apply temp/irrad desoto corrections + spr_e20_327 = CECMOD.SunPower_SPR_E20_327 + x = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, + EgRef=1.121, dEgdT=-0.0002677) + data = dict(zip((il, io, rs, rsh, nnsvt), x)) + vdtest = np.linspace(0, way_faster.est_voc(x[0], x[1], x[4]), 100) + results = way_faster.bishop88(vdtest, *x) + for test, expected in zip(vdtest, zip(*results)): + data[vd] = test + LOGGER.debug('expected = %r', expected) + LOGGER.debug('test data = %r', data) + assert np.isclose(np.float(i.evalf(subs=data)), expected[0]) + assert np.isclose(np.float(v.evalf(subs=data)), expected[1]) + assert np.isclose(np.float(grad_i.evalf(subs=data)), expected[2]) + assert np.isclose(np.float(grad_v.evalf(subs=data)), expected[3]) + assert np.isclose(np.float(grad.evalf(subs=data)), + expected[2] / expected[3]) + assert np.isclose(np.float(p.evalf(subs=data)), expected[4]) + assert np.isclose(np.float(grad_p.evalf(subs=data)), expected[5]) + assert np.isclose(np.float(grad2p.evalf(subs=data)), expected[6]) From 0666d29c1e9d96ce815438bf150e085026214faf Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Mon, 29 Jan 2018 23:44:38 -0800 Subject: [PATCH 06/73] use float64, output symbols for fun, make executable --- pvlib/test/test_numerical_precision.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 00049c6141..3b75bedf11 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -52,12 +52,17 @@ def test_numerical_precicion(): data[vd] = test LOGGER.debug('expected = %r', expected) LOGGER.debug('test data = %r', data) - assert np.isclose(np.float(i.evalf(subs=data)), expected[0]) - assert np.isclose(np.float(v.evalf(subs=data)), expected[1]) - assert np.isclose(np.float(grad_i.evalf(subs=data)), expected[2]) - assert np.isclose(np.float(grad_v.evalf(subs=data)), expected[3]) - assert np.isclose(np.float(grad.evalf(subs=data)), + assert np.isclose(np.float64(i.evalf(subs=data)), expected[0]) + assert np.isclose(np.float64(v.evalf(subs=data)), expected[1]) + assert np.isclose(np.float64(grad_i.evalf(subs=data)), expected[2]) + assert np.isclose(np.float64(grad_v.evalf(subs=data)), expected[3]) + assert np.isclose(np.float64(grad.evalf(subs=data)), expected[2] / expected[3]) - assert np.isclose(np.float(p.evalf(subs=data)), expected[4]) - assert np.isclose(np.float(grad_p.evalf(subs=data)), expected[5]) - assert np.isclose(np.float(grad2p.evalf(subs=data)), expected[6]) + assert np.isclose(np.float64(p.evalf(subs=data)), expected[4]) + assert np.isclose(np.float64(grad_p.evalf(subs=data)), expected[5]) + assert np.isclose(np.float64(grad2p.evalf(subs=data)), expected[6]) + return i, v, grad_i, grad_v, grad, p, grad_p, grad2p + + +if __name__ == '__main__': + syms = test_numerical_precicion() From 595e2540991700ee78100201cedb53c50fc28bbd Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 13:22:17 -0800 Subject: [PATCH 07/73] add tests for slower_way using fminbound test_way_faster --------------- * change test names to test_fast_ and test_slow to differential * move tstop before the expansion of output values way_faster ---------- * refactor to use photocurrent for il, saturation_current for io, resistance_series for rs, resistance_shunt for rsh, and nNsVth for nnsvt * remove start of custom newton solver * fix FIXME to use the correct names --- pvlib/test/test_way_faster.py | 76 ++++++++++++++-- pvlib/way_faster.py | 161 +++++++++++++--------------------- 2 files changed, 131 insertions(+), 106 deletions(-) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index 485fabb9c6..6fc76be7a3 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -6,7 +6,7 @@ import logging import numpy as np from pvlib import pvsystem -from pvlib.way_faster import faster_way +from pvlib.way_faster import faster_way, slower_way logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -17,7 +17,7 @@ CECMOD = pvsystem.retrieve_sam('cecmod') -def test_spr_e20_327(): +def test_fast_spr_e20_327(): spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( poa_global=POA, temp_cell=TCELL, @@ -31,8 +31,8 @@ def test_spr_e20_327(): LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() out = faster_way(*x, log=False, test=False) - isc, voc, imp, vmp, pmp, _, _ = out.values() tstop = clock() + isc, voc, imp, vmp, pmp, _, _ = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) @@ -47,7 +47,7 @@ def test_spr_e20_327(): return isc, voc, imp, vmp, pmp, pvs -def test_fs_495(): +def test_fast_fs_495(): fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( poa_global=POA, temp_cell=TCELL, @@ -62,8 +62,69 @@ def test_fs_495(): LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() out = faster_way(*x, log=False, test=False) + tstop = clock() isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() + dt_fast = tstop - tstart + LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) + LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) + assert np.isclose(pvs['i_sc'], isc) + assert np.isclose(pvs['v_oc'], voc) + # the singlediode method doesn't actually get the MPP correct + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + assert np.isclose(pvs_imp, imp) + assert np.isclose(pvs_vmp, vmp) + assert np.isclose(pvs['p_mp'], pmp) + return isc, voc, imp, vmp, pmp, i, v, p, pvs + + +def test_slow_spr_e20_327(): + spr_e20_327 = CECMOD.SunPower_SPR_E20_327 + x = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, + EgRef=1.121, dEgdT=-0.0002677) + il, io, rs, rsh, nnsvt = x + tstart = clock() + pvs = pvsystem.singlediode(*x) + tstop = clock() + dt_slow = tstop - tstart + LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) + tstart = clock() + out = slower_way(*x) + tstop = clock() + isc, voc, imp, vmp, pmp, _, _ = out.values() + dt_fast = tstop - tstart + LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) + LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) + assert np.isclose(pvs['i_sc'], isc) + assert np.isclose(pvs['v_oc'], voc) + # the singlediode method doesn't actually get the MPP correct + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + assert np.isclose(pvs_imp, imp) + assert np.isclose(pvs_vmp, vmp) + assert np.isclose(pvs['p_mp'], pmp) + return isc, voc, imp, vmp, pmp, pvs + + +def test_slow_fs_495(): + fs_495 = CECMOD.First_Solar_FS_495 + x = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=fs_495.alpha_sc, module_parameters=fs_495, + EgRef=1.475, dEgdT=-0.0003) + il, io, rs, rsh, nnsvt = x + x += (101, ) + tstart = clock() + pvs = pvsystem.singlediode(*x) tstop = clock() + dt_slow = tstop - tstart + LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) + tstart = clock() + out = slower_way(*x) + tstop = clock() + isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) @@ -77,6 +138,9 @@ def test_fs_495(): assert np.isclose(pvs['p_mp'], pmp) return isc, voc, imp, vmp, pmp, i, v, p, pvs + if __name__ == '__main__': - r_spr_e20_327 = test_spr_e20_327() - r_fs_495 = test_fs_495() + r_fast_spr_e20_327 = test_fast_spr_e20_327() + r_fast_fs_495 = test_fast_fs_495() + r_slow_spr_e20_327 = test_slow_spr_e20_327() + r_slow_fs_495 = test_slow_fs_495() diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index c177a32c8f..7548736b4d 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -6,7 +6,7 @@ import logging from collections import OrderedDict import numpy as np -from scipy.optimize import fminbound +from scipy.optimize import fminbound, newton logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -17,22 +17,23 @@ DELTA = EPS**0.33 -def est_voc(il, io, nnsvt): +def est_voc(photocurrent, saturation_current, nNsVth): """ Rough estimate of open circuit voltage useful for bounding searches for ``i`` of ``v`` when using :func:`~pvlib.way_faster`. - :param il: photo-generated current [A] - :param io: diode one reverse saturation or "dark" current [A] - :param nnsvt" product of thermal voltage ``Vt`` [V], ideality factor ``n``, - and number of series cells ``Ns`` + :param photocurrent: photo-generated current [A] + :param saturation_current: diode one reverse saturation current [A] + :param nNsVth: product of thermal voltage ``Vth`` [V], diode ideality + factor ``n``, and number of series cells ``Ns`` :returns: rough estimate of open circuit voltage [V] """ # http://www.pveducation.org/pvcdrom/open-circuit-voltage - return nnsvt * np.log(il / io + 1.0) + return nNsVth * np.log(photocurrent / saturation_current + 1.0) -def bishop88(vd, il, io, rs, rsh, nnsvt): +def bishop88(vd, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): """ Explicit calculation single-diode-model (SDM) currents and voltages using diode junction voltages [1]. @@ -42,72 +43,46 @@ def bishop88(vd, il, io, rs, rsh, nnsvt): https://doi.org/10.1016/0379-6787(88)90059-2 :param vd: diode voltages [V}] - :param il: photo-generated current [A] - :param io: diode one reverse saturation or "dark" current [A] - :param rs: series resitance [ohms] - :param rsh: shunt resitance [ohms] - :param nnsvt" product of thermal voltage ``Vt`` [V], ideality factor ``n``, - and number of series cells ``Ns`` + :param photocurrent: photo-generated current [A] + :param saturation_current: diode one reverse saturation current [A] + :param resistance_series: series resitance [ohms] + :param resistance_shunt: shunt resitance [ohms] + :param nNsVth" product of thermal voltage ``Vth`` [V], diode ideality + factor ``n``, and number of series cells ``Ns`` :returns: tuple containing currents [A], voltages [V], gradient ``di/dvd``, gradient ``dv/dvd``, power [W], gradient ``dp/dv``, and gradient ``d2p/dv/dvd`` """ - a = np.exp(vd / nnsvt) - b = 1.0 / rsh - i = il - io * (a - 1.0) - vd * b - v = vd - i * rs - c = io * a / nnsvt + a = np.exp(vd / nNsVth) + b = 1.0 / resistance_shunt + i = photocurrent - saturation_current * (a - 1.0) - vd * b + v = vd - i * resistance_series + c = saturation_current * a / nNsVth grad_i = - c - b # di/dvd - grad_v = 1.0 - grad_i * rs # dv/dvd + grad_v = 1.0 - grad_i * resistance_series # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i grad = grad_i / grad_v # di/dv grad_p = v * grad + i # dp/dv - grad2i = -c / nnsvt - grad2v = -grad2i * rs + grad2i = -c / nNsVth + grad2v = -grad2i * resistance_series grad2p = ( grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i ) return i, v, grad_i, grad_v, i*v, grad_p, grad2p -def newton_solver(fjx, x0, x, tol=EPS, damp=DAMP, log=False, test=True): - resnorm = np.inf # nor of residuals - while resnorm > tol: - f, j = fjx(x0, *x) - newton_step = f / j - # don't let step get crazy - if np.abs(newton_step / x0) > damp: - break - x0 -= newton_step - resnorm = f**2 - if log: - LOGGER.debug( - 'x0=%g, newton step=%g, f=%g, resnorm=%g', - x0, newton_step, f, resnorm - ) - if test: - f2, _ = fjx(x0 * (1.0 + DELTA), *x) - LOGGER.debug('test_grad=%g', (f2 - f) / x0 / DELTA) - LOGGER.debug('grad=%g', j) - return x0, f, j - - def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ This is a slow but reliable way to find current given any voltage. """ - # FIXME: everything is named the wrong thing! - il = photocurrent - io = saturation_current - rs = resistance_series - rsh = resistance_shunt - nnsvt = nNsVth - x = (il, io, rs, rsh, nnsvt) # collect args + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(il, io, nnsvt) - vd = fminbound(lambda vd: (v - bishop88(vd, *x)[1])**2, 0.0, voc_est) - return bishop88(vd, il, io, rs, rsh, nnsvt)[0] + voc_est = est_voc(photocurrent, saturation_current, nNsVth) + vd = fminbound(lambda x: (v - bishop88(x, *args)[1])**2, 0.0, voc_est) + return bishop88(vd, *args)[0] def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, @@ -115,34 +90,27 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, """ This is a slow but reliable way to find voltage given any current. """ - # FIXME: everything is named the wrong thing! - il = photocurrent - io = saturation_current - rs = resistance_series - rsh = resistance_shunt - nnsvt = nNsVth - x = (il, io, rs, rsh, nnsvt) # collect args + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(il, io, nnsvt) - vd = fminbound(lambda vd: (i - bishop88(vd, *x)[0])**2, 0.0, voc_est) - return bishop88(vd, il, io, rs, rsh, nnsvt)[1] + voc_est = est_voc(photocurrent, saturation_current, nNsVth) + vd = fminbound(lambda x: (i - bishop88(x, *args)[0])**2, 0.0, voc_est) + return bishop88(vd, *args)[1] + def slow_mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ This is a slow but reliable way to find mpp. """ - # FIXME: everything is named the wrong thing! - il = photocurrent - io = saturation_current - rs = resistance_series - rsh = resistance_shunt - nnsvt = nNsVth - x = (il, io, rs, rsh, nnsvt) # collect args + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(il, io, nnsvt) - vd = fminbound(lambda vd: -(bishop88(vd, *x)[4])**2, 0.0, voc_est) - i, v, _, _, p, _, _ = bishop88(vd, il, io, rs, rsh, nnsvt) + voc_est = est_voc(photocurrent, saturation_current, nNsVth) + vd = fminbound(lambda x: -(bishop88(x, *args)[4])**2, 0.0, voc_est) + i, v, _, _, p, _, _ = bishop88(vd, *args) return i, v, p @@ -151,16 +119,12 @@ def slower_way(photocurrent, saturation_current, resistance_series, """ This is the slow but reliable way. """ - # FIXME: everything is named the wrong thing! - il = photocurrent - io = saturation_current - rs = resistance_series - rsh = resistance_shunt - nnsvt = nNsVth - x = (il, io, rs, rsh, nnsvt) # collect args - voc = slow_v_from_i(0, *x) - i_sc = slow_i_from_v(0, *x) - i_mp, v_mp, p_mp = slow_mppt(*x) + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + voc = slow_v_from_i(0.0, *args) + i_sc = slow_i_from_v(0.0, *args) + i_mp, v_mp, p_mp = slow_mppt(*args) out = OrderedDict() out['i_sc'] = i_sc out['v_oc'] = voc @@ -174,7 +138,7 @@ def slower_way(photocurrent, saturation_current, resistance_series, vd = voc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, _, _, p, _, _ = bishop88(vd, *x) + i, v, _, _, p, _, _ = bishop88(vd, *args) out['i'] = i out['v'] = v out['p'] = p @@ -185,19 +149,14 @@ def faster_way(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None, tol=EPS, damp=DAMP, log=True, test=True): """a faster way""" - # FIXME: everything is named the wrong thing! - il = photocurrent - io = saturation_current - rs = resistance_series - rsh = resistance_shunt - nnsvt = nNsVth - x = (il, io, rs, rsh, nnsvt) # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) # collect args # first estimate Voc - voc_est = est_voc(il, io, nnsvt) + voc_est = est_voc(photocurrent, saturation_current, nNsVth) # find the real voc resnorm = np.inf # nor of residuals while resnorm > tol: - i_test, v_test, grad, _, _, _, _ = bishop88(voc_est, *x) + i_test, v_test, grad, _, _, _, _ = bishop88(voc_est, *args) newton_step = i_test / grad # don't let step get crazy if np.abs(newton_step / voc_est) > damp: @@ -211,14 +170,15 @@ def faster_way(photocurrent, saturation_current, resistance_series, ) if test: delta = EPS**0.3 - i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + delta), *x) + i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + delta), *args) LOGGER.debug('test_grad=%g', (i_test2 - i_test) / voc_est / delta) LOGGER.debug('grad=%g', grad) # find isc too + isc_est = 0.0 vd_sc = 0.0 resnorm = np.inf # nor of residuals while resnorm > tol: - isc_est, v_test, _, grad, _, _, _ = bishop88(vd_sc, *x) + isc_est, v_test, _, grad, _, _, _ = bishop88(vd_sc, *args) newton_step = v_test / grad # don't let step get crazy if np.abs(newton_step / voc_est) > damp: @@ -232,14 +192,15 @@ def faster_way(photocurrent, saturation_current, resistance_series, ) if test: delta = EPS**0.3 - _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + delta), *x) + _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + delta), *args) LOGGER.debug('test_grad=%g', (v_test2 - v_test) / vd_sc / delta) LOGGER.debug('grad=%g', grad) # find the mpp + imp_est, vmp_est, pmp_est = 0.0, 0.0, 0.0 vd_mp = voc_est resnorm = np.inf # nor of residuals while resnorm > tol: - imp_est, vmp_est, _, _, pmp_est, grad_p, grad2p = bishop88(vd_mp, *x) + imp_est, vmp_est, _, _, pmp_est, grad_p, grad2p = bishop88(vd_mp, *args) newton_step = grad_p / grad2p # don't let step get crazy if np.abs(newton_step / voc_est) > damp: @@ -253,7 +214,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, ) if test: delta = EPS**0.3 - _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + delta), *x) + _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + delta), *args) LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / delta) LOGGER.debug('grad=%g', grad2p) out = OrderedDict() @@ -269,7 +230,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, vd = voc_est * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, _, _, p, _, _ = bishop88(vd, *x) + i, v, _, _, p, _, _ = bishop88(vd, *args) out['i'] = i out['v'] = v out['p'] = p From 5ade5886fa0ad15ce3cf51402607bfcfd8acfe16 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 13:23:48 -0800 Subject: [PATCH 08/73] change voc to v_oc --- pvlib/way_faster.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 7548736b4d..9fd0adc60c 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -122,12 +122,12 @@ def slower_way(photocurrent, saturation_current, resistance_series, # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - voc = slow_v_from_i(0.0, *args) + v_oc = slow_v_from_i(0.0, *args) i_sc = slow_i_from_v(0.0, *args) i_mp, v_mp, p_mp = slow_mppt(*args) out = OrderedDict() out['i_sc'] = i_sc - out['v_oc'] = voc + out['v_oc'] = v_oc out['i_mp'] = i_mp out['v_mp'] = v_mp out['p_mp'] = p_mp @@ -135,7 +135,7 @@ def slower_way(photocurrent, saturation_current, resistance_series, out['i_xx'] = None # calculate the IV curve if requested using bishop88 if ivcurve_pnts: - vd = voc * ( + vd = v_oc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) i, v, _, _, p, _, _ = bishop88(vd, *args) From 7179096568f7601eec7cf6afae2d1229604fd277 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 13:37:01 -0800 Subject: [PATCH 09/73] add numeric type to args in docstrings * so that pycharm will stop complaining * add a bunch of todo's for #408 and #410 * use DELTA instead of recalculating each time * fix typo/bug in docstring for nNsVth arg, missing trailing colon (:) had quotes (") instead --- pvlib/way_faster.py | 47 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 9fd0adc60c..d75ef251b2 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -6,7 +6,7 @@ import logging from collections import OrderedDict import numpy as np -from scipy.optimize import fminbound, newton +from scipy.optimize import fminbound logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -16,16 +16,24 @@ DAMP = 1.5 DELTA = EPS**0.33 +# TODO: make fast_i_from_v, fast_v_from_i, fast_mppt using newton +# TODO: remove grad calcs from bishop88 +# TODO: add new residual and f_prime calcs for fast_ methods to use newton +# TODO: refactor singlediode to be a wrapper with a method argument +# TODO: update pvsystem.singlediode to use slow_ methods by default +# TODO: ditto for i_from_v and v_from_i +# TODO: add new mppt function to pvsystem + def est_voc(photocurrent, saturation_current, nNsVth): """ Rough estimate of open circuit voltage useful for bounding searches for ``i`` of ``v`` when using :func:`~pvlib.way_faster`. - :param photocurrent: photo-generated current [A] - :param saturation_current: diode one reverse saturation current [A] - :param nNsVth: product of thermal voltage ``Vth`` [V], diode ideality - factor ``n``, and number of series cells ``Ns`` + :param numeric photocurrent: photo-generated current [A] + :param numeric saturation_current: diode one reverse saturation current [A] + :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode + ideality factor ``n``, and number of series cells ``Ns`` :returns: rough estimate of open circuit voltage [V] """ # http://www.pveducation.org/pvcdrom/open-circuit-voltage @@ -42,13 +50,13 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) https://doi.org/10.1016/0379-6787(88)90059-2 - :param vd: diode voltages [V}] - :param photocurrent: photo-generated current [A] - :param saturation_current: diode one reverse saturation current [A] - :param resistance_series: series resitance [ohms] - :param resistance_shunt: shunt resitance [ohms] - :param nNsVth" product of thermal voltage ``Vth`` [V], diode ideality - factor ``n``, and number of series cells ``Ns`` + :param numeric vd: diode voltages [V] + :param numeric photocurrent: photo-generated current [A] + :param numeric saturation_current: diode one reverse saturation current [A] + :param numeric resistance_series: series resitance [ohms] + :param numeric resistance_shunt: shunt resitance [ohms] + :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode + ideality factor ``n``, and number of series cells ``Ns`` :returns: tuple containing currents [A], voltages [V], gradient ``di/dvd``, gradient ``dv/dvd``, power [W], gradient ``dp/dv``, and gradient ``d2p/dv/dvd`` @@ -169,9 +177,8 @@ def faster_way(photocurrent, saturation_current, resistance_series, voc_est, newton_step, i_test, v_test, resnorm ) if test: - delta = EPS**0.3 - i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + delta), *args) - LOGGER.debug('test_grad=%g', (i_test2 - i_test) / voc_est / delta) + i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + DELTA), *args) + LOGGER.debug('test_grad=%g', (i_test2 - i_test) / voc_est / DELTA) LOGGER.debug('grad=%g', grad) # find isc too isc_est = 0.0 @@ -191,9 +198,8 @@ def faster_way(photocurrent, saturation_current, resistance_series, vd_sc, newton_step, isc_est, v_test, resnorm ) if test: - delta = EPS**0.3 - _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + delta), *args) - LOGGER.debug('test_grad=%g', (v_test2 - v_test) / vd_sc / delta) + _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + DELTA), *args) + LOGGER.debug('test_grad=%g', (v_test2 - v_test) / vd_sc / DELTA) LOGGER.debug('grad=%g', grad) # find the mpp imp_est, vmp_est, pmp_est = 0.0, 0.0, 0.0 @@ -213,9 +219,8 @@ def faster_way(photocurrent, saturation_current, resistance_series, vd_mp, newton_step, pmp_est, resnorm ) if test: - delta = EPS**0.3 - _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + delta), *args) - LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / delta) + _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + DELTA), *args) + LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / DELTA) LOGGER.debug('grad=%g', grad2p) out = OrderedDict() out['i_sc'] = isc_est From 4b66f875198c8ac567468155cb8b8491f1901262 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:19:50 -0800 Subject: [PATCH 10/73] replace custom newton method with scipy.optimize.newton * remove tol, damp, log, & test args in from faster_way() since not used anymore * do not import logging, and remove LOGGER, etc. * also remove EPS, DAMP, DELTA all not used * import newton * add fast_i_from_v() using newton * remove the entire custom newton code in faster_way for finding the real voc and replace it with a call to newton, passing func and fprime, and return the value of v from bishop88() using optimum vd * ditto for isc and mpp --- pvlib/test/test_way_faster.py | 4 +- pvlib/way_faster.py | 101 +++++++++++----------------------- 2 files changed, 34 insertions(+), 71 deletions(-) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index 6fc76be7a3..42149f4b4f 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -30,7 +30,7 @@ def test_fast_spr_e20_327(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = faster_way(*x, log=False, test=False) + out = faster_way(*x) tstop = clock() isc, voc, imp, vmp, pmp, _, _ = out.values() dt_fast = tstop - tstart @@ -61,7 +61,7 @@ def test_fast_fs_495(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = faster_way(*x, log=False, test=False) + out = faster_way(*x) tstop = clock() isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() dt_fast = tstop - tstart diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index d75ef251b2..e80e3a2815 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -3,18 +3,10 @@ methods from J.W. Bishop (Solar Cells, 1988). """ -import logging from collections import OrderedDict import numpy as np -from scipy.optimize import fminbound +from scipy.optimize import fminbound, newton -logging.basicConfig() -LOGGER = logging.getLogger(__name__) -LOGGER.setLevel(logging.DEBUG) - -EPS = np.finfo(float).eps -DAMP = 1.5 -DELTA = EPS**0.33 # TODO: make fast_i_from_v, fast_v_from_i, fast_mppt using newton # TODO: remove grad calcs from bishop88 @@ -93,6 +85,27 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, return bishop88(vd, *args)[0] +def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a fast but unreliable way to find current given any voltage. + """ + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + + def func(x, *a): + _, vtest, _, grad_v, _, _, _ = bishop88(x, *a) + return vtest - v + + def fprime(x, *a): + _, vtest, _, grad_v, _, _, _ = bishop88(x, *a) + return grad_v + + vd = newton(func=func, x0=v, fprime=fprime, args=args) + return bishop88(vd, *args)[0] + + def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ @@ -154,74 +167,24 @@ def slower_way(photocurrent, saturation_current, resistance_series, def faster_way(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None, - tol=EPS, damp=DAMP, log=True, test=True): + resistance_shunt, nNsVth, ivcurve_pnts=None): """a faster way""" args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args # first estimate Voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) # find the real voc - resnorm = np.inf # nor of residuals - while resnorm > tol: - i_test, v_test, grad, _, _, _, _ = bishop88(voc_est, *args) - newton_step = i_test / grad - # don't let step get crazy - if np.abs(newton_step / voc_est) > damp: - break - voc_est -= newton_step - resnorm = i_test**2 - if log: - LOGGER.debug( - 'voc_est=%g, step=%g, i_test=%g, v_test=%g, resnorm=%g', - voc_est, newton_step, i_test, v_test, resnorm - ) - if test: - i_test2, _, _, _, _, _, _ = bishop88(voc_est * (1.0 + DELTA), *args) - LOGGER.debug('test_grad=%g', (i_test2 - i_test) / voc_est / DELTA) - LOGGER.debug('grad=%g', grad) + vd = newton(func=lambda x, *a: bishop88(x, *a)[0], x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a)[2], args=args) + voc_est = bishop88(vd, *args)[1] # find isc too - isc_est = 0.0 - vd_sc = 0.0 - resnorm = np.inf # nor of residuals - while resnorm > tol: - isc_est, v_test, _, grad, _, _, _ = bishop88(vd_sc, *args) - newton_step = v_test / grad - # don't let step get crazy - if np.abs(newton_step / voc_est) > damp: - break - vd_sc -= newton_step - resnorm = v_test**2 - if log: - LOGGER.debug( - 'vd_sc=%g, step=%g, isc_est=%g, v_test=%g, resnorm=%g', - vd_sc, newton_step, isc_est, v_test, resnorm - ) - if test: - _, v_test2, _, _, _, _, _ = bishop88(vd_sc * (1.0 + DELTA), *args) - LOGGER.debug('test_grad=%g', (v_test2 - v_test) / vd_sc / DELTA) - LOGGER.debug('grad=%g', grad) + vd = newton(func=lambda x, *a: bishop88(x, *a)[1], x0=0.0, + fprime=lambda x, *a: bishop88(x, *a)[3], args=args) + isc_est = bishop88(vd, *args)[0] # find the mpp - imp_est, vmp_est, pmp_est = 0.0, 0.0, 0.0 - vd_mp = voc_est - resnorm = np.inf # nor of residuals - while resnorm > tol: - imp_est, vmp_est, _, _, pmp_est, grad_p, grad2p = bishop88(vd_mp, *args) - newton_step = grad_p / grad2p - # don't let step get crazy - if np.abs(newton_step / voc_est) > damp: - break - vd_mp -= newton_step - resnorm = grad_p**2 - if log: - LOGGER.debug( - 'vd_mp=%g, step=%g, pmp_est=%g, resnorm=%g', - vd_mp, newton_step, pmp_est, resnorm - ) - if test: - _, _, _, _, _, grad_p2, _ = bishop88(vd_mp * (1.0 + DELTA), *args) - LOGGER.debug('test_grad=%g', (grad_p2 - grad_p) / vd_mp / DELTA) - LOGGER.debug('grad=%g', grad2p) + vd = newton(func=lambda x, *a: bishop88(x, *a)[5], x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a)[6], args=args) + imp_est, vmp_est, _, _, pmp_est, _, _ = bishop88(vd, *args) out = OrderedDict() out['i_sc'] = isc_est out['v_oc'] = voc_est From 664d7d888237aba6317263e91fa7a1b33b5556c8 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:29:44 -0800 Subject: [PATCH 11/73] get lambdas working in fast_i_from_v --- pvlib/way_faster.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index e80e3a2815..556e6fbc3c 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -93,16 +93,8 @@ def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - - def func(x, *a): - _, vtest, _, grad_v, _, _, _ = bishop88(x, *a) - return vtest - v - - def fprime(x, *a): - _, vtest, _, grad_v, _, _, _ = bishop88(x, *a) - return grad_v - - vd = newton(func=func, x0=v, fprime=fprime, args=args) + vd = newton(func=lambda x, *a: bishop88(x, *a)[1] - v, x0=v, + fprime=lambda x, *a: bishop88(x, *a)[3], args=args) return bishop88(vd, *args)[0] From 09a9e147564ae9bd520a26b860b6fc2dd3a6799f Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:34:40 -0800 Subject: [PATCH 12/73] add fast_v_from_i using newton --- pvlib/way_faster.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 556e6fbc3c..770a01b3b1 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -112,6 +112,21 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, return bishop88(vd, *args)[1] +def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a fast but unreliable way to find voltage given any current. + """ + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + # first bound the search using voc + voc_est = est_voc(photocurrent, saturation_current, nNsVth) + vd = newton(func=lambda x, *a: bishop88(x, *a)[0] - i, x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a)[2], args=args) + return bishop88(vd, *args)[1] + + def slow_mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ From 06be522874a5dc8146e242e16a91ec9573efb895 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:39:02 -0800 Subject: [PATCH 13/73] add fast_mppt --- pvlib/way_faster.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 770a01b3b1..a0ce0efb19 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -142,6 +142,22 @@ def slow_mppt(photocurrent, saturation_current, resistance_series, return i, v, p +def fast_mppt(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + This is a fast but unreliable way to find mpp. + """ + # collect args + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + # first bound the search using voc + voc_est = est_voc(photocurrent, saturation_current, nNsVth) + vd = newton(func=lambda x, *a: bishop88(x, *a)[5], x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a)[6], args=args) + i, v, _, _, p, _, _ = bishop88(vd, *args) + return i, v, p + + def slower_way(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None): """ From 1aba56a719c0ffd0c9ef86874e485fafa0953d0a Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:48:34 -0800 Subject: [PATCH 14/73] calculate i_x and i_xx too for both fast and slow * use fast_i_from_v, fast_v_from_i, and fast_mppt in faster_way * change names in faster_way to match the same notation eg: v_oc instead of voc_est --- pvlib/way_faster.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index a0ce0efb19..35c915b909 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -175,8 +175,8 @@ def slower_way(photocurrent, saturation_current, resistance_series, out['i_mp'] = i_mp out['v_mp'] = v_mp out['p_mp'] = p_mp - out['i_x'] = None - out['i_xx'] = None + out['i_x'] = slow_i_from_v(v_oc / 2.0, *args) + out['i_xx'] = slow_i_from_v((v_oc + v_mp) / 2.0, *args) # calculate the IV curve if requested using bishop88 if ivcurve_pnts: vd = v_oc * ( @@ -194,31 +194,20 @@ def faster_way(photocurrent, saturation_current, resistance_series, """a faster way""" args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args - # first estimate Voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) - # find the real voc - vd = newton(func=lambda x, *a: bishop88(x, *a)[0], x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a)[2], args=args) - voc_est = bishop88(vd, *args)[1] - # find isc too - vd = newton(func=lambda x, *a: bishop88(x, *a)[1], x0=0.0, - fprime=lambda x, *a: bishop88(x, *a)[3], args=args) - isc_est = bishop88(vd, *args)[0] - # find the mpp - vd = newton(func=lambda x, *a: bishop88(x, *a)[5], x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a)[6], args=args) - imp_est, vmp_est, _, _, pmp_est, _, _ = bishop88(vd, *args) + v_oc = fast_v_from_i(0.0, *args) + i_sc = fast_i_from_v(0.0, *args) + i_mp, v_mp, p_mp = fast_mppt(*args) out = OrderedDict() - out['i_sc'] = isc_est - out['v_oc'] = voc_est - out['i_mp'] = imp_est - out['v_mp'] = vmp_est - out['p_mp'] = pmp_est - out['i_x'] = None - out['i_xx'] = None + out['i_sc'] = i_sc + out['v_oc'] = v_oc + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = fast_i_from_v(v_oc / 2.0, *args) + out['i_xx'] = fast_i_from_v((v_oc + v_mp) / 2.0, *args) # calculate the IV curve if requested using bishop88 if ivcurve_pnts: - vd = voc_est * ( + vd = v_oc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) i, v, _, _, p, _, _ = bishop88(vd, *args) From f09be91573dfabcfbaa4a32a1f6f1bd7dfcf3552 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 16:57:36 -0800 Subject: [PATCH 15/73] test i_x and i_xx too * since v_mp doesn't match because of different tolerances, use pvsystem.i_from_v(v=(voc+vmp)/2) to get ixx for test * don't need to save isc as temp variable, only voc, so calculate it on assignment into "out" the ordered dict --- pvlib/test/test_way_faster.py | 20 ++++++++++++++++---- pvlib/way_faster.py | 6 ++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index 42149f4b4f..d0774e5cf0 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -32,7 +32,7 @@ def test_fast_spr_e20_327(): tstart = clock() out = faster_way(*x) tstop = clock() - isc, voc, imp, vmp, pmp, _, _ = out.values() + isc, voc, imp, vmp, pmp, ix, ixx = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) @@ -44,6 +44,9 @@ def test_fast_spr_e20_327(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) + assert np.isclose(pvs['i_x'], ix) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, pvs @@ -63,7 +66,7 @@ def test_fast_fs_495(): tstart = clock() out = faster_way(*x) tstop = clock() - isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() + isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) @@ -75,6 +78,9 @@ def test_fast_fs_495(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) + assert np.isclose(pvs['i_x'], ix) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, i, v, p, pvs @@ -93,7 +99,7 @@ def test_slow_spr_e20_327(): tstart = clock() out = slower_way(*x) tstop = clock() - isc, voc, imp, vmp, pmp, _, _ = out.values() + isc, voc, imp, vmp, pmp, ix, ixx = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) @@ -105,6 +111,9 @@ def test_slow_spr_e20_327(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) + assert np.isclose(pvs['i_x'], ix) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, pvs @@ -124,7 +133,7 @@ def test_slow_fs_495(): tstart = clock() out = slower_way(*x) tstop = clock() - isc, voc, imp, vmp, pmp, _, _, i, v, p = out.values() + isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() dt_fast = tstop - tstart LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) @@ -136,6 +145,9 @@ def test_slow_fs_495(): assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) + assert np.isclose(pvs['i_x'], ix) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, i, v, p, pvs diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 35c915b909..a1a5ffecc0 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -167,10 +167,9 @@ def slower_way(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) v_oc = slow_v_from_i(0.0, *args) - i_sc = slow_i_from_v(0.0, *args) i_mp, v_mp, p_mp = slow_mppt(*args) out = OrderedDict() - out['i_sc'] = i_sc + out['i_sc'] = slow_i_from_v(0.0, *args) out['v_oc'] = v_oc out['i_mp'] = i_mp out['v_mp'] = v_mp @@ -195,10 +194,9 @@ def faster_way(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args v_oc = fast_v_from_i(0.0, *args) - i_sc = fast_i_from_v(0.0, *args) i_mp, v_mp, p_mp = fast_mppt(*args) out = OrderedDict() - out['i_sc'] = i_sc + out['i_sc'] = fast_i_from_v(0.0, *args) out['v_oc'] = v_oc out['i_mp'] = i_mp out['v_mp'] = v_mp From 49053635cfce78fbf7e0504d871766a212f8f7a4 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 30 Jan 2018 23:22:27 -0800 Subject: [PATCH 16/73] use brentq instead of fminbound * add gradients bool arg to bishop88(), only outputs gradients if true, otherwise just i, v, and p * output di/dv too, why not? * brentq finds zeros, not minimums, like newton, so don't need to square residual in any slow_ * change lambda to pass args from brentq * grad_v is now index 4 (5th item) * grad_i is now index 3 (4th item) * grad_p is now index 6 (7th item) * grad2p is now index 7 (8th item) * since brentq finds zero, not minimum, don't use power in slow_mppt, use grad_p --- pvlib/way_faster.py | 69 +++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index a1a5ffecc0..1d3abd56e3 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -5,10 +5,9 @@ from collections import OrderedDict import numpy as np -from scipy.optimize import fminbound, newton +from scipy.optimize import brentq, newton -# TODO: make fast_i_from_v, fast_v_from_i, fast_mppt using newton # TODO: remove grad calcs from bishop88 # TODO: add new residual and f_prime calcs for fast_ methods to use newton # TODO: refactor singlediode to be a wrapper with a method argument @@ -33,7 +32,7 @@ def est_voc(photocurrent, saturation_current, nNsVth): def bishop88(vd, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): + resistance_shunt, nNsVth, gradients=False): """ Explicit calculation single-diode-model (SDM) currents and voltages using diode junction voltages [1]. @@ -49,26 +48,31 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, :param numeric resistance_shunt: shunt resitance [ohms] :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of series cells ``Ns`` - :returns: tuple containing currents [A], voltages [V], gradient ``di/dvd``, - gradient ``dv/dvd``, power [W], gradient ``dp/dv``, and gradient - ``d2p/dv/dvd`` + :param bool gradients: default returns only i, v, and p, returns gradients + if true + :returns: tuple containing currents [A], voltages [V], power [W], + gradient ``di/dvd``, gradient ``dv/dvd``, gradient ``di/dv``, + gradient ``dp/dv``, and gradient ``d2p/dv/dvd`` """ a = np.exp(vd / nNsVth) b = 1.0 / resistance_shunt i = photocurrent - saturation_current * (a - 1.0) - vd * b v = vd - i * resistance_series - c = saturation_current * a / nNsVth - grad_i = - c - b # di/dvd - grad_v = 1.0 - grad_i * resistance_series # dv/dvd - # dp/dv = d(iv)/dv = v * di/dv + i - grad = grad_i / grad_v # di/dv - grad_p = v * grad + i # dp/dv - grad2i = -c / nNsVth - grad2v = -grad2i * resistance_series - grad2p = ( - grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i - ) - return i, v, grad_i, grad_v, i*v, grad_p, grad2p + retval = (i, v, i*v) + if gradients: + c = saturation_current * a / nNsVth + grad_i = - c - b # di/dvd + grad_v = 1.0 - grad_i * resistance_series # dv/dvd + # dp/dv = d(iv)/dv = v * di/dv + i + grad = grad_i / grad_v # di/dv + grad_p = v * grad + i # dp/dv + grad2i = -c / nNsVth # d2i/dvd + grad2v = -grad2i * resistance_series # d2v/dvd + grad2p = ( + grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i + ) # d2p/dv/dvd + retval += (grad_i, grad_v, grad, grad_p, grad2p) + return retval def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, @@ -81,7 +85,7 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) - vd = fminbound(lambda x: (v - bishop88(x, *args)[1])**2, 0.0, voc_est) + vd = brentq(lambda x, *a: v - bishop88(x, *a)[1], 0.0, voc_est, args) return bishop88(vd, *args)[0] @@ -94,7 +98,8 @@ def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) vd = newton(func=lambda x, *a: bishop88(x, *a)[1] - v, x0=v, - fprime=lambda x, *a: bishop88(x, *a)[3], args=args) + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], + args=args) return bishop88(vd, *args)[0] @@ -108,7 +113,7 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) - vd = fminbound(lambda x: (i - bishop88(x, *args)[0])**2, 0.0, voc_est) + vd = brentq(lambda x, *a: i - bishop88(x, *a)[0], 0.0, voc_est, args) return bishop88(vd, *args)[1] @@ -123,7 +128,8 @@ def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, # first bound the search using voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) vd = newton(func=lambda x, *a: bishop88(x, *a)[0] - i, x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a)[2], args=args) + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], + args=args) return bishop88(vd, *args)[1] @@ -137,9 +143,9 @@ def slow_mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) - vd = fminbound(lambda x: -(bishop88(x, *args)[4])**2, 0.0, voc_est) - i, v, _, _, p, _, _ = bishop88(vd, *args) - return i, v, p + vd = brentq(lambda x, *a: bishop88(x, *a, gradients=True)[6], 0.0, voc_est, + args) + return bishop88(vd, *args) def fast_mppt(photocurrent, saturation_current, resistance_series, @@ -152,10 +158,11 @@ def fast_mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = est_voc(photocurrent, saturation_current, nNsVth) - vd = newton(func=lambda x, *a: bishop88(x, *a)[5], x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a)[6], args=args) - i, v, _, _, p, _, _ = bishop88(vd, *args) - return i, v, p + vd = newton( + func=lambda x, *a: bishop88(x, *a, gradients=True)[6], x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args + ) + return bishop88(vd, *args) def slower_way(photocurrent, saturation_current, resistance_series, @@ -181,7 +188,7 @@ def slower_way(photocurrent, saturation_current, resistance_series, vd = v_oc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, _, _, p, _, _ = bishop88(vd, *args) + i, v, p = bishop88(vd, *args) out['i'] = i out['v'] = v out['p'] = p @@ -208,7 +215,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, vd = v_oc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, _, _, p, _, _ = bishop88(vd, *args) + i, v, p = bishop88(vd, *args) out['i'] = i out['v'] = v out['p'] = p From a3d2bb49fd01353ee6e2fc01894647105ebfbe6c Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 00:25:10 -0800 Subject: [PATCH 17/73] change test for numerical errors to use data file * make old test generate data, add to data folder * make new test read data * try to figure out clever way to not need sympy --- pvlib/data/bishop88_numerical_precision.csv | 101 ++++++++++++++++++++ pvlib/test/test_numerical_precision.py | 93 ++++++++++++------ 2 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 pvlib/data/bishop88_numerical_precision.csv diff --git a/pvlib/data/bishop88_numerical_precision.csv b/pvlib/data/bishop88_numerical_precision.csv new file mode 100644 index 0000000000..94cf44adc9 --- /dev/null +++ b/pvlib/data/bishop88_numerical_precision.csv @@ -0,0 +1,101 @@ +,grad,grad2p,grad_i,grad_p,grad_v,i,p,v +0.0,-0.0029752937017359805,-0.005957140772172728,-0.0029785726893041607,5.870505570369785,1.0011020718950425,5.86405008,-12.723220836076763,-2.1696985296 +0.5845170900989292,-0.00297529507385398,-0.005957144029576585,-0.002978574064448173,5.8670235188728626,1.0011020724038457,5.862309052970334,-9.289047121136205,-1.5845372595000944 +1.1690341801978583,-0.002975296745849822,-0.005957148329744609,-0.0029785757401313675,5.863541465180093,1.0011020730238487,5.860568025051929,-5.856910966556231,-0.9993759890713553 +1.7535512702967875,-0.002975298783262126,-0.0059571539728827455,-0.002978577782036902,5.860059408597745,1.0011020737793537,5.85882699605055,-2.426812373397211,-0.41421471824191614 +2.3380683603957166,-0.002975301265953046,-0.005957161340610417,-0.0029785802702030567,5.856577348233737,1.001102074699975,5.857085965729514,1.0012486569100834,0.17094655307579656 +2.922585450494646,-0.0029753042912386935,-0.005957170917207319,-0.002978583302160564,5.8530952829440785,1.0011020758217994,5.855344933800406,4.427272122453232,0.7561078249884956 +3.507102540593575,-0.0029753079777037196,-0.005957183316258555,-0.0029785869967556004,5.849613211265367,1.0011020771887995,5.853603899911782,7.851258020695973,1.3412690976262158 +4.091619630692504,-0.0029753124698495737,-0.005957199314042896,-0.0029785914988083007,5.846131131329814,1.001102078854559,5.851862863635386,11.27320634829687,1.9264303711474113 +4.676136720791433,-0.002975317943758649,-0.005957219891338891,-0.002978596984789394,5.84264904075839,1.001102080884372,5.85012182444937,14.693117100878508,2.5115916457451664 +5.260653810890362,-0.0029753246139963283,-0.0059572462857335464,-0.002978603669737475,5.839166936526567,1.0011020833578028,5.8483807817178315,18.110990272734274,3.096752921654765 +5.845170900989292,-0.002975332742021483,-0.005957280057027362,-0.002978611815688049,5.835684814795811,1.0011020863718045,5.846639734665894,21.52682585645643,3.681914199162911 +6.429687991088221,-0.0029753426464350914,-0.005957323168961724,-0.002978621741944762,5.8322026707022365,1.0011020900445196,5.844898682349329,24.940623842465143,4.267075478618969 +7.01420508118715,-0.002975354715468701,-0.005957378091279118,-0.0029786338375954085,5.8287204980917755,1.0011020945199103,5.843157623617547,28.352384218412883,4.852236760448657 +7.598722171286079,-0.0029753694222022617,-0.005957447927099995,-0.0029786485767633332,5.825238289188552,1.0011020999734024,5.841416557068495,31.762106968432207,5.4373980451707355 +8.183239261385008,-0.0029753873431078255,-0.0059575365718074124,-0.002978666537192042,5.821756034179936,1.001102106618761,5.8396754809937015,35.16979207218701,6.022559333417338 +8.767756351483937,-0.002975409180645987,-0.005957648911127437,-0.0029786884228914967,5.818273720697683,1.0011021147164698,5.837934393311305,38.57543950367713,6.607720625958756 +9.352273441582867,-0.0029754357908008,-0.005957791067948803,-0.002978715091733781,5.81479133316957,1.0011021245839415,5.836193291484451,41.979049229733924,7.192881923733619 +9.936790531681796,-0.0029754682166324635,-0.0059579707097246176,-0.0029787475890798343,5.811308852009686,1.0011021366079595,5.834452172421854,45.38062120812886,7.778043227885708 +10.521307621780725,-0.0029755077291629788,-0.005958197431147377,-0.002978787188755338,5.8078262526078515,1.0011021512598395,5.832711032356624,48.78015538519764,8.363204539808773 +11.105824711879654,-0.0029755558771973863,-0.005958483230316493,-0.0029788354429819267,5.804343504069003,1.0011021691139033,5.830969866698612,52.177651692858575,8.948365861201166 +11.690341801978583,-0.0029756145480334634,-0.005958843100985721,-0.00297889424322092,5.800860567641557,1.0011021908699917,5.829228669854491,55.57311004487415,9.533527194132422 +12.274858892077512,-0.0029756860414395505,-0.005959295768885225,-0.0029789658943145158,5.797377394759002,1.0011022173808963,5.827487435008501,58.966530332166734,10.118688541124365 +12.859375982176442,-0.0029757731598002535,-0.00595986460680504,-0.002979053204830618,5.793893924600721,1.0011022496857873,5.825746153855294,62.35791241695444,10.703849905249983 +13.44389307227537,-0.0029758793179634957,-0.005960578771406571,-0.002979159597152625,5.790410081055457,1.0011022890509464,5.82400481627438,65.74725612541428,11.289011290253852 +14.0284101623743,-0.002976008677094632,-0.005961474614971044,-0.002979289241629458,5.786925768942823,1.001102337019403,5.822263409933423,69.13456123850973,11.874172700698933 +14.612927252473229,-0.0029761663077843346,-0.005962597437960581,-0.002979447220044218,5.783440869313562,1.0011023954714164,5.820521919804842,72.5198274805304,12.459334142145439 +15.197444342572158,-0.002976358388803591,-0.005964003663929506,-0.0029796397248090935,5.779955233606391,1.0011024666981794,5.818780327576764,75.90305450478175,13.044495621368755 +15.781961432671087,-0.002976592449296402,-0.005965763537684524,-0.002979874301694524,5.776468676386092,1.001102553491627,5.817038610935235,79.28424187572728,13.629657146625048 +16.366478522770016,-0.002976877663903328,-0.0059679644715214695,-0.0029801601456070836,5.7729809663218195,1.0011026592538745,5.815296742689563,82.66338904671491,14.214818727974878 +16.950995612868944,-0.0029772252123836693,-0.005970715193935012,-0.0029805084610099305,5.769491814983297,1.0011027881305736,5.8135546897065025,86.04049533221146,14.799980377677539 +17.535512702967875,-0.002977648717832082,-0.005974150891728205,-0.0029809329011135314,5.766000862931994,1.001102945173412,5.8118124116115,89.4155598732081,15.385142110671623 +18.120029793066806,-0.00297816478066586,-0.005978439581571335,-0.002981450103051982,5.762507662460098,1.0011031365381293,5.810069859206104,92.78858159413763,15.970303945160547 +18.704546883165733,-0.0029787936293126675,-0.005983790002784571,-0.002982080340022682,5.759011656176353,1.0011033697258085,5.808326972539485,96.15955914924689,16.555465903326123 +19.28906397326466,-0.0029795599131022697,-0.005990461391925088,-0.0029828483159518274,5.755512150447955,1.0011036538769023,5.806583678558498,99.5284908558716,17.140628012198015 +19.87358106336359,-0.0029804936684389004,-0.005998775584701608,-0.0029837841338348874,5.752008282472884,1.0011040001295188,5.804839888244158,102.89537461145103,17.72579030471325 +20.458098153462522,-0.002981631496121704,-0.006009131995583255,-0.0029849244757089214,5.748498979467168,1.0011044220560124,5.8030954931222825,106.26020779036207,18.310952821007277 +21.04261524356145,-0.002983017995955065,-0.006022026154854778,-0.0029863140405090977,5.744982908093305,1.0011049361949884,5.8013503610115436,109.62298711571498,18.896115609987177 +21.627132333660377,-0.0029847075148726693,-0.006038072642512993,-0.0029880072961702775,5.741458411813756,1.001105562699583,5.799604330842244,112.98370850009512,19.481278731248747 +22.211649423759308,-0.0029867662770834236,-0.006058033455340477,-0.002990070614652262,5.73792343330725,1.0011063261274213,5.797857206342731,116.34236684779933,20.066442257412493 +22.79616651385824,-0.002989274979714781,-0.00608285308640337,-0.002992584873577066,5.734375418411309,1.0011072564032235,5.7961087483459774,119.69895580934383,20.651606276970227 +23.380683603957166,-0.0029923319556650167,-0.00611370189577642,-0.0029956486264567862,5.730811197222261,1.001108389991789,5.794358665414746,123.05346747682691,21.23677089775371 +23.965200694056094,-0.00299605702759383,-0.006152029720641873,-0.0029993819657781226,5.727226836956887,1.0011097713273378,5.792606602417884,126.40589200602012,21.82193625116148 +24.549717784155025,-0.003000596204048713,-0.0061996321282111674,-0.003003931230368128,5.723617459912789,1.0011114545552362,5.790852126609946,129.75621714771086,22.40710249730935 +25.134234874253956,-0.0030061274017001574,-0.00625873227607497,-0.0030094747415597284,5.719977018301338,1.0011135056543772,5.789094710668513,133.1044276666803,22.992269831306608 +25.718751964352883,-0.0030128674178291435,-0.006332082036065065,-0.003016229793002312,5.716298015799175,1.001116005023411,5.7873337120242905,136.45050462158724,23.577438490903894 +26.30326905445181,-0.003021080426142027,-0.006423086889588923,-0.0030244611681028225,5.712571163286861,1.001119050632198,5.785568347673785,139.79442447271,24.16260876581251 +26.88778614455074,-0.003031088328586912,-0.006535960151633435,-0.0030344915189626518,5.708784953312269,1.0011227618620162,5.783797663487256,143.13615797669814,24.747781009060454 +27.472303234649672,-0.0030432833684303404,-0.006675913372617289,-0.0030467140136421905,5.704925134203544,1.0011272841850476,5.782020496808899,146.4756688178521,25.332955650830378 +28.0568203247486,-0.003058143498242408,-0.006849391357709309,-0.0030616077474984646,5.700974060304231,1.0011327948665745,5.780235430883248,149.81291191355504,25.9181332153218 +28.641337414847527,-0.003076251104057797,-0.007064362200442791,-0.003079756522686961,5.696909889317819,1.0011395099133942,5.77844073932143,153.14783131680173,26.503314341298598 +29.225854504946458,-0.003098315817990082,-0.007330675135155671,-0.0031018717319434906,5.692705590992404,1.0011476925408191,5.776634318430448,156.4803576206475,27.088499807127196 +29.81037159504539,-0.003125202311026061,-0.007660501973435053,-0.003128820243640859,5.688327723056219,1.0011576634901471,5.774813604752972,159.81040474704287,27.67369056128679 +30.394888685144316,-0.0031579641517379847,-0.008068881528586546,-0.0031616583811542435,5.683734920072716,1.001169813601027,5.772975474585356,163.13786597492938,28.258887759547736 +30.979405775243244,-0.0031978850526391375,-0.00857439090203128,-0.00320167332845292,5.678876028280287,1.0011846191315277,5.7711161215352025,166.46260902843693,28.844092810275217 +31.563922865342175,-0.003246529112840734,-0.009199972992622521,-0.003250433584927979,5.673687803977977,1.0012026604264233,5.769230907319024,169.78447000403867,29.42930742963414 +32.148439955441106,-0.0033058020143741994,-0.009973956320365261,-0.0033098504471761533,5.668092073956306,1.0012246446654551,5.767314179951555,173.10324586373662,30.01453370885903 +32.73295704554003,-0.003378025553070552,-0.010931311503920758,-0.0033822529276927283,5.661992233047112,1.0012514335832463,5.765359052200152,176.41868515749206,30.599774196225976 +33.31747413563896,-0.0034660283989182195,-0.012115198827249804,-0.0034704790471242375,5.655268925097611,1.001284077247436,5.763357131620177,179.73047655936463,31.185031996939493 +33.90199122573789,-0.0035732566041354074,-0.013578873671946769,-0.003577987078539832,5.647774718368853,1.0013238552190598,5.761298191589311,183.03823470472858,31.770310894849842 +34.48650831583682,-0.0037039081322002257,-0.015388031652369478,-0.0037089911042561515,5.6393275430704275,1.0013723267085748,5.75916977044608,186.34148269622136,32.35561550077176 +35.07102540593575,-0.0038630965943735403,-0.017623693632213337,-0.00386862619875006,5.629702605722588,1.0014313916935376,5.756956683019679,189.63963049849377,32.94095143321847 +35.65554249603468,-0.004057050483175599,-0.02038575308226625,-0.004063149712472153,5.61862243017849,1.0015033653936147,5.754640425404113,192.93194825988823,33.52632553863516 +36.24005958613361,-0.004293355521479788,-0.023797335221513505,-0.00430018654645125,5.605744595922764,1.001591069022187,5.752198449645108,196.21753337488724,34.11174615976492 +36.82457667623254,-0.004581249343709803,-0.02801014993255118,-0.004589028031923824,5.5906466476921715,1.0016979403718118,5.7496032799090635,199.4952698246832,34.69722346266619 +37.409093766331466,-0.004931979640265873,-0.033211059514087614,-0.004940996130423489,5.572807533010409,1.0018281685682566,5.746821435489804,202.7637779923818,35.28276983520024 +37.993610856430394,-0.005359239181470032,-0.039630128945234155,-0.005369887230186346,5.551584781783767,1.001986858275169,5.7438121184373045,206.02135272915035,35.86840037260859 +38.57812794652932,-0.005879693851065687,-0.04755048150237668,-0.005892512934731463,5.526186469961425,1.0021802297858506,5.740525614366289,209.26588692954923,36.454133469213794 +39.162645036628255,-0.006513623021513256,-0.057320347186224224,-0.00652935904135924,5.495636802151911,1.0024158628453028,5.736901343759845,212.49477723545002,37.0399915394371 +39.74716212672718,-0.007285695351029681,-0.06936776606936956,-0.007305388540099587,5.458733900288981,1.0027029937598368,5.732865487383382,215.7048077000665,37.62600189639534 +40.33167921682611,-0.008225907421738,-0.08421849332264997,-0.008251020108931858,5.413998091061136,1.0030528774403047,5.72832809273029,218.89200627168503,38.2121978225159 +40.916196306925045,-0.009370717591804082,-0.10251774615968948,-0.009403320460173364,5.359608638285938,1.003479228570264,5.723179548078225,222.05146775731706,38.7986198741361 +41.50071339702397,-0.010764412980085559,-0.12505653220817234,-0.010807457275435202,5.293326463334625,1.003998759191911,5.717286285946656,225.17713544588256,39.38531747122371 +42.0852304871229,-0.012460753541850314,-0.15280339779969054,-0.01251846968105451,5.212399935303023,1.0046318337819902,5.710485547540512,228.2615317418191,39.97235083453291 +42.66974757722183,-0.014524943507684662,-0.18694252257421318,-0.014603425662809928,5.113450295921546,1.0054032674952396,5.702579002957414,231.29542589991897,40.559793346127584 +43.254264667320754,-0.01703598663125314,-0.22891914566857377,-0.0171440509859117,4.992332723500172,1.0063432988647874,5.693324977084448,234.2674241568419,41.147734425799506 +43.83878175741969,-0.02008948701834557,-0.28049331005226186,-0.020239932668244195,4.84396846020808,1.0074887750872503,5.682428976456426,237.16346409409655,41.73628303613082 +44.423298847518616,-0.02380096064790911,-0.34380281144062375,-0.02401242257608301,4.662142873281297,1.0088845963531508,5.669532145748974,239.96619077809814,42.32557195359149 +45.00781593761754,-0.02830972227370975,-0.4214359713240549,-0.028609394154798077,4.439263870493397,1.0105854758372752,5.65419720142593,242.65418689950377,42.915762973089954 +45.59233302771648,-0.03378340555144074,-0.5165143255015552,-0.034211038748095036,4.166074866878158,1.0126580843367952,5.635891291170475,245.2010225160716,43.5070532499834 +46.176850117815405,-0.04042315708918318,-0.6327843977180755,-0.04103692870897761,3.8313166925969986,1.0151836636223217,5.613965107226749,247.57408175974606,44.099683028141506 +46.76136720791433,-0.04846951217513105,-0.7747162340807431,-0.04935462416110098,3.4213337208458725,1.0182612109396074,5.587627434940117,249.73311357612394,44.69394505698649 +47.34588429801326,-0.05820890367283452,-0.9476040789303638,-0.05949016077694885,2.9196214786872496,1.0220113594874711,5.555914138853929,251.6284406780968,45.29019606663731 +47.93040138811219,-0.06998066609906431,-1.1576612037329261,-0.07184082967105557,2.306316628998847,1.0265811069782906,5.517650370684814,253.19874470933536,45.88887075095881 +48.51491847821112,-0.08418426188957046,-1.4120961692589842,-0.0868907503524476,1.5576361908975433,1.0321495776304057,5.471404517810895,254.36832520582863,46.49049880662108 +49.09943556831005,-0.1012862621448542,-1.7191514760865074,-0.10522984716235959,0.6452820683996454,1.038935043450073,5.415432087153262,255.04370410222978,47.09572569606333 +49.68395265840898,-0.12182634556357387,-2.088077606107995,-0.12757697303254226,-0.46415966128739167,1.0472034800220407,5.347607324820204,255.10941464495394,47.705337948225505 +50.26846974850791,-0.14642122666854582,-2.5290062924988645,-0.15480808696547316,-1.8097768276699933,1.057278992177225,5.265339891152171,254.42277149133224,48.32029398878161 +50.85298683860684,-0.17576498907130353,-3.052677727102656,-0.18799058973276161,-3.436774886789651,1.0695565182011217,5.165473325009417,252.8073645834666,48.941761708353354 +51.437503928705766,-0.21062380604954392,-3.669969935228677,-0.22842516367685428,-5.396705965486928,1.084517310560436,5.044161317322018,250.04494912056487,49.57116424129662 +52.02202101880469,-0.25182254424459144,-4.391179275391126,-0.2776967566457863,-7.74735236946439,1.102747799958941,4.8967169440929945,245.8653121614329,50.21023574949029 +52.60653810890362,-0.3002203929971172,-5.225015825085368,-0.33773670852095095,-10.552107298134041,1.1249625821527518,4.717428949114363,239.93357550835128,50.861089397731305 +53.191055199002555,-0.3566726468607908,-6.177315083784205,-0.4108984555634116,-13.878683994931766,1.1520324285584622,4.4993378750758986,231.8342339859107,51.526300185224464 +53.77557228910148,-0.42197637789647496,-7.249536657710529,-0.5000497800254029,-17.797003926920357,1.185018418609399,4.2339632678902515,221.05101314891033,52.209005879982094 +54.36008937920041,-0.4967992967974098,-8.437225397893114,-0.6086852210122606,-22.376184840418,1.2252135317745365,3.910971261234011,206.94133972387158,52.91303001254382 +54.944606469299345,-0.5815938882013996,-9.728742691201678,-0.7410630528575037,-27.6806898991169,1.2741933295572765,3.5177695113374905,188.7038215862265,53.64303175010447 +55.52912355939827,-0.6765029438074703,-11.104706308706483,-0.9023722002670144,-33.76591834028289,1.3338777140987954,3.0390136043433786,165.33658856994361,54.40468852579122 +56.1136406494972,-0.7812674544568233,-12.538652143710078,-1.0989356329451032,-40.67380062540061,1.4066061841896882,2.4560055884698335,135.58358854783296,55.20491858176336 +56.69815773959613,-0.8951523346832339,-13.99937902743073,-1.3384582123317916,-48.42925380576363,1.495229538562763,1.7459610547399986,97.86487468711262,56.05215214934233 +57.282674829695054,-1.016907883547717,-15.45519893702795,-1.6303287055074418,-57.03856246502108,1.6032216210377535,0.8811160374274739,50.185428234765006,56.95666189584689 +57.86719191979398,-1.1447833032697259,-16.879887399800843,-1.9859878045565882,-66.49076342414816,1.7348154876859376,-0.17236127335316223,-9.985054995831723,57.930965590934655 diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 3b75bedf11..486acf044b 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -4,24 +4,48 @@ """ import logging +import os import numpy as np -from sympy import symbols, exp as sy_exp +import pandas as pd from pvlib import pvsystem from pvlib import way_faster logging.basicConfig() LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) - +TEST_DATA = 'bishop88_numerical_precision.csv' +TEST_PATH = os.path.dirname(os.path.abspath(__file__)) +PVLIB_PATH = os.path.dirname(TEST_PATH) +DATA_PATH = os.path.join(PVLIB_PATH, 'data', TEST_DATA) POA = 888 TCELL = 55 CECMOD = pvsystem.retrieve_sam('cecmod') +# get module from cecmod and apply temp/irrad desoto corrections +SPR_E20_327 = CECMOD.SunPower_SPR_E20_327 +ARGS = pvsystem.calcparams_desoto( + poa_global=POA, temp_cell=TCELL, + alpha_isc=SPR_E20_327.alpha_sc, module_parameters=SPR_E20_327, + EgRef=1.121, dEgdT=-0.0002677 +) +IL, I0, RS, RSH, NNSVTH = ARGS +IVCURVE_NPTS = 100 +try: + from sympy import symbols, exp as sy_exp +except ImportError as exc: + LOGGER.exception(exc) + symbols = NotImplemented + sy_exp = NotImplemented -def test_numerical_precicion(): + +def generate_numerical_precicion(): """ - Test that there are no numerical errors due to floating point arithmetic. + Generate expected data with infinite numerical precision using SymPy. + :return: dataframe of expected values """ + if symbols is NotImplemented: + LOGGER.critical("SymPy is required to generate expected data.") + raise ImportError("could not import sympy") il, io, rs, rsh, nnsvt, vd = symbols('il, io, rs, rsh, nnsvt, vd') a = sy_exp(vd / nnsvt) b = 1.0 / rsh @@ -39,30 +63,45 @@ def test_numerical_precicion(): grad2p = ( grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i ) - # get module from cecmod and apply temp/irrad desoto corrections - spr_e20_327 = CECMOD.SunPower_SPR_E20_327 - x = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, - EgRef=1.121, dEgdT=-0.0002677) - data = dict(zip((il, io, rs, rsh, nnsvt), x)) - vdtest = np.linspace(0, way_faster.est_voc(x[0], x[1], x[4]), 100) - results = way_faster.bishop88(vdtest, *x) - for test, expected in zip(vdtest, zip(*results)): + # generate exact values + data = dict(zip((il, io, rs, rsh, nnsvt), ARGS)) + vdtest = np.linspace(0, way_faster.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + expected = [] + for test in vdtest: data[vd] = test - LOGGER.debug('expected = %r', expected) - LOGGER.debug('test data = %r', data) - assert np.isclose(np.float64(i.evalf(subs=data)), expected[0]) - assert np.isclose(np.float64(v.evalf(subs=data)), expected[1]) - assert np.isclose(np.float64(grad_i.evalf(subs=data)), expected[2]) - assert np.isclose(np.float64(grad_v.evalf(subs=data)), expected[3]) - assert np.isclose(np.float64(grad.evalf(subs=data)), - expected[2] / expected[3]) - assert np.isclose(np.float64(p.evalf(subs=data)), expected[4]) - assert np.isclose(np.float64(grad_p.evalf(subs=data)), expected[5]) - assert np.isclose(np.float64(grad2p.evalf(subs=data)), expected[6]) - return i, v, grad_i, grad_v, grad, p, grad_p, grad2p + test_data = { + 'i': np.float64(i.evalf(subs=data)), + 'v': np.float64(v.evalf(subs=data)), + 'p': np.float64(p.evalf(subs=data)), + 'grad_i': np.float64(grad_i.evalf(subs=data)), + 'grad_v': np.float64(grad_v.evalf(subs=data)), + 'grad': np.float64(grad.evalf(subs=data)), + 'grad_p': np.float64(grad_p.evalf(subs=data)), + 'grad2p': np.float64(grad2p.evalf(subs=data)) + } + LOGGER.debug(test_data) + expected.append(test_data) + return pd.DataFrame(expected, index=vdtest) + + +def test_numerical_precicion(): + """ + Test that there are no numerical errors due to floating point arithmetic. + """ + expected = pd.read_csv(DATA_PATH) + vdtest = np.linspace(0, way_faster.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + results = way_faster.bishop88(vdtest, *ARGS, gradients=True) + assert np.allclose(expected['i'], results[0]) + assert np.allclose(expected['v'], results[1]) + assert np.allclose(expected['p'], results[2]) + assert np.allclose(expected['grad_i'], results[3]) + assert np.allclose(expected['grad_v'], results[4]) + assert np.allclose(expected['grad'], results[5]) + assert np.allclose(expected['grad_p'], results[6]) + assert np.allclose(expected['grad2p'], results[7]) if __name__ == '__main__': - syms = test_numerical_precicion() + expected = generate_numerical_precicion() + expected.to_csv(DATA_PATH) + test_numerical_precicion() From 8ee1b9427d8bdf1c683e1f715493dc090d00d8ce Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 01:51:46 -0800 Subject: [PATCH 18/73] working on #410 * add method arg and make slow the default * check size of photocurrent and vectorize method if necessary * reorganize out if len(photocurrent) --- pvlib/pvsystem.py | 111 +++++++++++++++++++++++------------- pvlib/test/test_pvsystem.py | 18 +++++- 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 40ef7c2f0e..5466ee1362 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -20,6 +20,7 @@ from pvlib.tools import _build_kwargs from pvlib.location import Location from pvlib import irradiance, atmosphere +from pvlib import way_faster # not sure if this belongs in the pvsystem module. @@ -1571,7 +1572,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None): + resistance_shunt, nNsVth, ivcurve_pnts=None, method=''): r''' Solve the single-diode model to obtain a photovoltaic IV curve. @@ -1627,6 +1628,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, Number of points in the desired IV curve. If None or 0, no IV curves will be produced. + method : str, default 'slow' + Either 'slow', 'fast', or 'lambertw'. Determines the method used to + calculate IV curve and points. If 'slow' then ``brentq`` is used, if + 'fast' then ``newton`` is used, and if 'lambertw' then `lambertw` is + used. + Returns ------- OrderedDict or DataFrame @@ -1677,53 +1684,75 @@ def singlediode(photocurrent, saturation_current, resistance_series, calcparams_desoto ''' - # Compute short circuit current - i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent) + if method.lower() == 'lambertw': + # Compute short circuit current + i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0., + saturation_current, photocurrent) + + # Compute open circuit voltage + v_oc = v_from_i(resistance_shunt, resistance_series, nNsVth, 0., + saturation_current, photocurrent) - # Compute open circuit voltage - v_oc = v_from_i(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent) + params = {'r_sh': resistance_shunt, + 'r_s': resistance_series, + 'nNsVth': nNsVth, + 'i_0': saturation_current, + 'i_l': photocurrent} - params = {'r_sh': resistance_shunt, - 'r_s': resistance_series, - 'nNsVth': nNsVth, - 'i_0': saturation_current, - 'i_l': photocurrent} + p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) + # Invert the Power-Current curve. Find the current where the inverted power + # is minimized. This is i_mp. Start the optimization at v_oc/2 + i_mp = i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, + saturation_current, photocurrent) - # Invert the Power-Current curve. Find the current where the inverted power - # is minimized. This is i_mp. Start the optimization at v_oc/2 - i_mp = i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, - saturation_current, photocurrent) + # Find Ix and Ixx using Lambert W + i_x = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5 * v_oc, + saturation_current, photocurrent) - # Find Ix and Ixx using Lambert W - i_x = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5 * v_oc, - saturation_current, photocurrent) + i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, + 0.5 * (v_oc + v_mp), saturation_current, photocurrent) - i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, - 0.5 * (v_oc + v_mp), saturation_current, photocurrent) + out = OrderedDict() + out['i_sc'] = i_sc + out['v_oc'] = v_oc + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = i_x + out['i_xx'] = i_xx - out = OrderedDict() - out['i_sc'] = i_sc - out['v_oc'] = v_oc - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - out['i_x'] = i_x - out['i_xx'] = i_xx - - # create ivcurve - if ivcurve_pnts: - ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * - np.linspace(0, 1, ivcurve_pnts)) - - ivcurve_i = i_from_v(resistance_shunt, resistance_series, nNsVth, - ivcurve_v.T, saturation_current, photocurrent).T - - out['v'] = ivcurve_v - out['i'] = ivcurve_i + # create ivcurve + if ivcurve_pnts: + ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * + np.linspace(0, 1, ivcurve_pnts)) + + ivcurve_i = i_from_v(resistance_shunt, resistance_series, nNsVth, + ivcurve_v.T, saturation_current, photocurrent).T + + out['v'] = ivcurve_v + out['i'] = ivcurve_i + + else: + size = 0 + try: + size = len(photocurrent) + except TypeError: + my_func = way_faster.faster_way + else: + my_func = np.vectorize(way_faster.slower_way) + out = my_func(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts) + if size: + out_array = pd.DataFrame(out.tolist()) + out = OrderedDict() + out['i_sc'] = out_array.i_sc + out['v_oc'] = out_array.v_oc + out['i_mp'] = out_array.i_mp + out['v_mp'] = out_array.v_mp + out['p_mp'] = out_array.p_mp + out['i_x'] = out_array.i_x + out['i_xx'] = out_array.i_xx if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: out = pd.DataFrame(out, index=photocurrent.index) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 53f01ea0ef..e35b9576c6 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -625,7 +625,8 @@ def test_singlediode_array(): saturation_current = 1.943e-09 sd = pvsystem.singlediode(photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + resistance_series, resistance_shunt, nNsVth, + method='lambertw') expected = np.array([ 0. , 0.54538398, 1.43273966, 2.36328163, 3.29255606, @@ -634,6 +635,14 @@ def test_singlediode_array(): assert_allclose(sd['i_mp'], expected, atol=0.01) + sd = pvsystem.singlediode(photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + + expected = pvsystem.i_from_v(resistance_shunt, resistance_series, nNsVth, + sd['v_mp'], saturation_current, photocurrent) + + assert_allclose(sd['i_mp'], expected, atol=0.01) + @requires_scipy def test_singlediode_floats(sam_data): @@ -671,6 +680,7 @@ def test_singlediode_floats_ivcurve(): 'v': np.array([0., 4.05315, 8.1063])} assert isinstance(out, dict) for k, v in out.items(): + if k == 'p': continue assert_allclose(v, expected[k], atol=3) @@ -684,7 +694,8 @@ def test_singlediode_series_ivcurve(cec_module_params): module_parameters=cec_module_params, EgRef=1.121, dEgdT=-0.0002677) - out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) + out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3, + method='lambertw') expected = OrderedDict([('i_sc', array([0., 3.01054475, 6.00675648])), ('v_oc', array([0., 9.96886962, 10.29530483])), @@ -705,6 +716,9 @@ def test_singlediode_series_ivcurve(cec_module_params): for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-2) + out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3, + method='lambertw') + def test_scale_voltage_current_power(sam_data): data = pd.DataFrame( From c9a893abafe3b79cc497a25587c3c26ef6914359 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 01:59:21 -0800 Subject: [PATCH 19/73] helping test_singlediode pass tests * in any test where mpp is tested, test lambertw first separately, then use i_from_v to calculate the corresponding value and store it in the expected values * --- pvlib/test/test_pvsystem.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index e35b9576c6..0c340f08d1 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -716,8 +716,13 @@ def test_singlediode_series_ivcurve(cec_module_params): for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-2) - out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3, - method='lambertw') + out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) + + expected['i_mp'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v_mp'], I0, IL) + expected['v_mp'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i_mp'], I0, IL) + + for k, v in out.items(): + assert_allclose(v, expected[k], atol=1e-2) def test_scale_voltage_current_power(sam_data): From c5248bb5c11839c8d8343e68b2dc6c40cc9c51b0 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 02:04:19 -0800 Subject: [PATCH 20/73] add the requires scipy decorator * still 2 tests in model chain strugling --- pvlib/test/test_way_faster.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index d0774e5cf0..02f5e5ad20 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -7,6 +7,7 @@ import numpy as np from pvlib import pvsystem from pvlib.way_faster import faster_way, slower_way +from conftest import requires_scipy logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -17,6 +18,7 @@ CECMOD = pvsystem.retrieve_sam('cecmod') +@requires_scipy def test_fast_spr_e20_327(): spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( @@ -50,6 +52,7 @@ def test_fast_spr_e20_327(): return isc, voc, imp, vmp, pmp, pvs +@requires_scipy def test_fast_fs_495(): fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( @@ -84,6 +87,7 @@ def test_fast_fs_495(): return isc, voc, imp, vmp, pmp, i, v, p, pvs +@requires_scipy def test_slow_spr_e20_327(): spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( @@ -117,6 +121,7 @@ def test_slow_spr_e20_327(): return isc, voc, imp, vmp, pmp, pvs +@requires_scipy def test_slow_fs_495(): fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( From 68c65c3d68fd67dc122879280506108b6234f57e Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 09:28:18 -0800 Subject: [PATCH 21/73] fixes #410 * test_modelchain now passes * in pvsystem.singlediode if using default "slow" then use ducktyping to see if photocurrent is a sequence, sorry other params are not checked, if not, then get out as usual, if it is, then use np.vectorize, and get a sequence of outputs, check if photocurrent is a pd.Series, and if so, then convert out to a pd.DataFrame, otherwise, transpose the sequence of outputs into a an OrderedDict of sequences (actually np.arrays) * move series check from original lambertw back up to there, because it doesn't work for the vectorized slow or fast methods, you need to call .tolist() first * also copy & paste WET code for 'fast' --- pvlib/pvsystem.py | 67 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 5466ee1362..224d5709cd 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1733,29 +1733,58 @@ def singlediode(photocurrent, saturation_current, resistance_series, out['v'] = ivcurve_v out['i'] = ivcurve_i + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out, index=photocurrent.index) + + elif method.lower() == 'fast': + try: + len(photocurrent) + except TypeError: + out = way_faster.faster_way( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts + ) + else: + vecfun = np.vectorize(way_faster.faster_way) + out = vecfun(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts) + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out.tolist(), index=photocurrent.index) + else: + out_array = pd.DataFrame(out.tolist()) + out = OrderedDict() + out['i_sc'] = out_array.i_sc + out['v_oc'] = out_array.v_oc + out['i_mp'] = out_array.i_mp + out['v_mp'] = out_array.v_mp + out['p_mp'] = out_array.p_mp + out['i_x'] = out_array.i_x + out['i_xx'] = out_array.i_xx + else: - size = 0 try: - size = len(photocurrent) + len(photocurrent) except TypeError: - my_func = way_faster.faster_way + out = way_faster.slower_way( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts + ) else: - my_func = np.vectorize(way_faster.slower_way) - out = my_func(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts) - if size: - out_array = pd.DataFrame(out.tolist()) - out = OrderedDict() - out['i_sc'] = out_array.i_sc - out['v_oc'] = out_array.v_oc - out['i_mp'] = out_array.i_mp - out['v_mp'] = out_array.v_mp - out['p_mp'] = out_array.p_mp - out['i_x'] = out_array.i_x - out['i_xx'] = out_array.i_xx - - if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: - out = pd.DataFrame(out, index=photocurrent.index) + vecfun = np.vectorize(way_faster.slower_way) + out = vecfun(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts) + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out.tolist(), index=photocurrent.index) + else: + out_array = pd.DataFrame(out.tolist()) + out = OrderedDict() + out['i_sc'] = out_array.i_sc + out['v_oc'] = out_array.v_oc + out['i_mp'] = out_array.i_mp + out['v_mp'] = out_array.v_mp + out['p_mp'] = out_array.p_mp + out['i_x'] = out_array.i_x + out['i_xx'] = out_array.i_xx return out From 41e0c83db98f94248700cb5bbc179f3039176e88 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 11:58:10 -0800 Subject: [PATCH 22/73] add i, v, and p to out * also use numpy arrays not pd.Series in OrderedDict out by using pd.Series.value * also use np.vstack to reshape array of arrays inside i, v, and p --- pvlib/pvsystem.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 224d5709cd..0c4510ef02 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1753,13 +1753,16 @@ def singlediode(photocurrent, saturation_current, resistance_series, else: out_array = pd.DataFrame(out.tolist()) out = OrderedDict() - out['i_sc'] = out_array.i_sc - out['v_oc'] = out_array.v_oc - out['i_mp'] = out_array.i_mp - out['v_mp'] = out_array.v_mp - out['p_mp'] = out_array.p_mp - out['i_x'] = out_array.i_x - out['i_xx'] = out_array.i_xx + out['i_sc'] = out_array.i_sc.values + out['v_oc'] = out_array.v_oc.values + out['i_mp'] = out_array.i_mp.values + out['v_mp'] = out_array.v_mp.values + out['p_mp'] = out_array.p_mp.values + out['i_x'] = out_array.i_x.values + out['i_xx'] = out_array.i_xx.values + out['i'] = np.vstack(out_array.i.values) + out['v'] = np.vstack(out_array.v.values) + out['p'] = np.vstack(out_array.p.values) else: try: @@ -1778,13 +1781,19 @@ def singlediode(photocurrent, saturation_current, resistance_series, else: out_array = pd.DataFrame(out.tolist()) out = OrderedDict() - out['i_sc'] = out_array.i_sc - out['v_oc'] = out_array.v_oc - out['i_mp'] = out_array.i_mp - out['v_mp'] = out_array.v_mp - out['p_mp'] = out_array.p_mp - out['i_x'] = out_array.i_x - out['i_xx'] = out_array.i_xx + out['i_sc'] = out_array.i_sc.values + out['v_oc'] = out_array.v_oc.values + out['i_mp'] = out_array.i_mp.values + out['v_mp'] = out_array.v_mp.values + out['p_mp'] = out_array.p_mp.values + out['i_x'] = out_array.i_x.values + out['i_xx'] = out_array.i_xx.values + out['i'] = np.vstack(out_array.i.values) + out['v'] = np.vstack(out_array.v.values) + out['p'] = np.vstack(out_array.p.values) + + # FIXME: WET code, remove redudancy, last conditions are identical, only + # the solver is different return out From d7b6e62c1b0c59be74baf6f391031410c2ec48ee Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 12:32:32 -0800 Subject: [PATCH 23/73] need to calculate i-v curve points to compare * using pvsystem.i_from_v and v_from_i, use transpose twice to get it in the right shape * also only output i, v, and p when there actually are ivcurve_pnts --- pvlib/pvsystem.py | 14 ++++++++------ pvlib/test/test_pvsystem.py | 8 +++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0c4510ef02..1bf514601a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1760,9 +1760,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, out['p_mp'] = out_array.p_mp.values out['i_x'] = out_array.i_x.values out['i_xx'] = out_array.i_xx.values - out['i'] = np.vstack(out_array.i.values) - out['v'] = np.vstack(out_array.v.values) - out['p'] = np.vstack(out_array.p.values) + if ivcurve_pnts: + out['i'] = np.vstack(out_array.i.values) + out['v'] = np.vstack(out_array.v.values) + out['p'] = np.vstack(out_array.p.values) else: try: @@ -1788,9 +1789,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, out['p_mp'] = out_array.p_mp.values out['i_x'] = out_array.i_x.values out['i_xx'] = out_array.i_xx.values - out['i'] = np.vstack(out_array.i.values) - out['v'] = np.vstack(out_array.v.values) - out['p'] = np.vstack(out_array.p.values) + if ivcurve_pnts: + out['i'] = np.vstack(out_array.i.values) + out['v'] = np.vstack(out_array.v.values) + out['p'] = np.vstack(out_array.p.values) # FIXME: WET code, remove redudancy, last conditions are identical, only # the solver is different diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 0c340f08d1..a17281ac33 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -680,7 +680,8 @@ def test_singlediode_floats_ivcurve(): 'v': np.array([0., 4.05315, 8.1063])} assert isinstance(out, dict) for k, v in out.items(): - if k == 'p': continue + if k == 'p': + continue assert_allclose(v, expected[k], atol=3) @@ -720,8 +721,13 @@ def test_singlediode_series_ivcurve(cec_module_params): expected['i_mp'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v_mp'], I0, IL) expected['v_mp'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i_mp'], I0, IL) + expected['i'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v'].T, I0, IL).T + expected['v'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i'].T, I0, IL).T for k, v in out.items(): + if k == 'p': + # skip power, only in way_faster output + continue assert_allclose(v, expected[k], atol=1e-2) From aa3d29a71d8db4d80c9ac05f2c2507e5f7b2cd64 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 15:44:49 -0800 Subject: [PATCH 24/73] add mppt method, update docs * add mppt method that returns an OrderedDict with `i_mp`, `v_mp`, and `p_mp` or a pandas series with the same * add `pvlib.pvsystem.mppt` to api.rst * add section to single diode that explains the different methods, proves that the problem is bounded, and that is guaranteed to converge * add reference to bishop * combine redundant code and remove FIXME * add notes to what's new * duck type scipy.optimize imports in way_faster.py and remove TODO's Signed-off-by: Mark Mikofski --- docs/sphinx/source/api.rst | 1 + docs/sphinx/source/whatsnew/v0.5.2.rst | 12 ++- pvlib/pvsystem.py | 140 +++++++++++++++++-------- pvlib/way_faster.py | 13 +-- 4 files changed, 116 insertions(+), 50 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 4070ed56c3..be86253a88 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -205,6 +205,7 @@ Functions relevant for the single diode model. pvsystem.i_from_v pvsystem.singlediode pvsystem.v_from_i + pvsystem.mppt SAPM model ---------- diff --git a/docs/sphinx/source/whatsnew/v0.5.2.rst b/docs/sphinx/source/whatsnew/v0.5.2.rst index b325b9b101..09f01c1e71 100644 --- a/docs/sphinx/source/whatsnew/v0.5.2.rst +++ b/docs/sphinx/source/whatsnew/v0.5.2.rst @@ -9,7 +9,16 @@ API Changes Enhancements ~~~~~~~~~~~~ -* +* Implement a reliable "gold" implementation of the single diode model (SDM) + using a bisection method (Brent, 1973) bounded by points known to include the + full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using + a gradient decent method (Newton-Raphson) that is not bounded, but should be + safe for well behaved IV-curves. (:issue:`408`) +* Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W + with the two new implementations to form a wrapper, that takes an additional + ``method`` argument ``('lambertw', 'fast')`` that defaults to the new "gold" + bounded bisection method. (:issue:`410`) +* Add :func:`~pvlib.pvsystem.mppt` method to compute the max power point. Bug fixes ~~~~~~~~~ @@ -31,5 +40,6 @@ Contributors * Cliff Hansen * Will Holmgren * KonstantinTr +* Mark Mikofski diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 1bf514601a..42ac05cbbb 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1628,11 +1628,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, Number of points in the desired IV curve. If None or 0, no IV curves will be produced. - method : str, default 'slow' - Either 'slow', 'fast', or 'lambertw'. Determines the method used to - calculate IV curve and points. If 'slow' then ``brentq`` is used, if - 'fast' then ``newton`` is used, and if 'lambertw' then `lambertw` is - used. + method : str, default '' + Determines the method used to calculate IV curve and points. If + 'lambertw' then '`lambertw`'' is used. If 'fast' then ``newton`` is + used. Otherwise the problem is bounded between zero and open-circuit + voltage and a bisection method, ``brentq``, is used, that guarantees + convergence. Returns ------- @@ -1662,9 +1663,46 @@ def singlediode(photocurrent, saturation_current, resistance_series, Notes ----- - The solution employed to solve the implicit diode equation utilizes - the Lambert W function to obtain an explicit function of V=f(i) and - I=f(V) as shown in [2]. + The default method employed is an explicit solution using [4] to find an + arbitrary point on the IV curve as a function of the diode voltage + :math:`V_d = V + I*Rs`. Then the voltage is backed out from :math:`V_d`. + A specific desired point, such as short circuit current or max power, is + located using the bisection search method, ``brentq``, bounded by a zero + diode voltage and an estimat of open circuit current 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}` that the resulting + current is also negative, meaning that the corresponding voltagge must be + in the 4th quadrant and therefore greater than the open circuit voltage. + + .. 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} + 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} + 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} + 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} + 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} + I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh} + I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh} + + If ``method.lower() == 'fast'`` then a gradient descent method, ``newton`` + is used to solve the implicit diode equation. It should be safe for well + behaved IV-curves, but the default method is recommended for reliability + and surprisingly it is just as fast. This method may be removed in future + versions. + + If either the "fast" or default methods are indicated, then + :func:`pvlib.way_faster.bishop88` is used to calculate the points at diode + voltages from zero to open-circuit voltage with a log spacing so that + points get closer as they approach the open-circuit voltage. + + If ``method.lower() == 'lambertw'`` then he solution employed to solve the + implicit diode equation utilizes the Lambert W function to obtain an + explicit function of V=f(i) and I=f(V) as shown in [2]. References ----------- @@ -1678,6 +1716,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, [3] D. King et al, "Sandia Photovoltaic Array Performance Model", SAND2004-3535, Sandia National Laboratories, Albuquerque, NM + [4] "Computer simulation of the effects of electrical mismatches in + photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) + https://doi.org/10.1016/0379-6787(88)90059-2 + See also -------- sapm @@ -1736,45 +1778,20 @@ def singlediode(photocurrent, saturation_current, resistance_series, if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: out = pd.DataFrame(out, index=photocurrent.index) - elif method.lower() == 'fast': - try: - len(photocurrent) - except TypeError: - out = way_faster.faster_way( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts - ) - else: - vecfun = np.vectorize(way_faster.faster_way) - out = vecfun(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts) - if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: - out = pd.DataFrame(out.tolist(), index=photocurrent.index) - else: - out_array = pd.DataFrame(out.tolist()) - out = OrderedDict() - out['i_sc'] = out_array.i_sc.values - out['v_oc'] = out_array.v_oc.values - out['i_mp'] = out_array.i_mp.values - out['v_mp'] = out_array.v_mp.values - out['p_mp'] = out_array.p_mp.values - out['i_x'] = out_array.i_x.values - out['i_xx'] = out_array.i_xx.values - if ivcurve_pnts: - out['i'] = np.vstack(out_array.i.values) - out['v'] = np.vstack(out_array.v.values) - out['p'] = np.vstack(out_array.p.values) - else: + if method.lower() == 'fast': + sdm_fun = way_faster.faster_way + else: + sdm_fun = way_faster.slower_way try: len(photocurrent) except TypeError: - out = way_faster.slower_way( + out = sdm_fun( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts ) else: - vecfun = np.vectorize(way_faster.slower_way) + vecfun = np.vectorize(sdm_fun) out = vecfun(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts) if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: @@ -1793,10 +1810,51 @@ def singlediode(photocurrent, saturation_current, resistance_series, out['i'] = np.vstack(out_array.i.values) out['v'] = np.vstack(out_array.v.values) out['p'] = np.vstack(out_array.p.values) + return out - # FIXME: WET code, remove redudancy, last conditions are identical, only - # the solver is different +def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, + nNsVth, method=''): + """ + Max power point tracker. Given the calculated DeSoto parameters calculates + the maximum power point (MPP). + + :param numeric photocurrent: photo-generated current [A] + :param numeric saturation_current: diode one reverse saturation current [A] + :param numeric resistance_series: series resitance [ohms] + :param numeric resistance_shunt: shunt resitance [ohms] + :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode + :param str method: if "fast" then use Newton, otherwise use bisection + :returns: ``OrderedDict`` or ``pandas.Datafrane`` with ``i_mp``, ``v_mp``, + and ``p_mp`` + """ + if method.lower() == 'fast': + mppt_func = way_faster.fast_mppt + else: + mppt_func = way_faster.slow_mppt + try: + len(photocurrent) + except TypeError: + i_mp, v_mp, p_mp = mppt_func( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth + ) + out = OrderedDict() + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + else: + vecfun = np.vectorize(mppt_func) + ivp = vecfun(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + if isinstance(photocurrent, pd.Series): + ivp = {k: v for k, v in zip(('i_mp', 'v_mp', 'p_mp'), ivp)} + out = pd.DataFrame(ivp, index=photocurrent.index) + else: + out = OrderedDict() + out['i_mp'] = ivp[0] + out['v_mp'] = ivp[1] + out['p_mp'] = ivp[2] return out diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 1d3abd56e3..93bb07a16e 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -5,15 +5,12 @@ from collections import OrderedDict import numpy as np -from scipy.optimize import brentq, newton +try: + from scipy.optimize import brentq, newton +except ImportError: + raise ImportError('Thes function requires scipy') - -# TODO: remove grad calcs from bishop88 -# TODO: add new residual and f_prime calcs for fast_ methods to use newton -# TODO: refactor singlediode to be a wrapper with a method argument -# TODO: update pvsystem.singlediode to use slow_ methods by default -# TODO: ditto for i_from_v and v_from_i -# TODO: add new mppt function to pvsystem +# TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default def est_voc(photocurrent, saturation_current, nNsVth): From 9fc350df40f04ba954bbb2d9ae844b210e6be47d Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 15:56:32 -0800 Subject: [PATCH 25/73] fix latex and other sphinx formatting issue sand typos --- pvlib/pvsystem.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 42ac05cbbb..244049fa30 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1630,7 +1630,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, method : str, default '' Determines the method used to calculate IV curve and points. If - 'lambertw' then '`lambertw`'' is used. If 'fast' then ``newton`` is + 'lambertw' then ``lambertw`` is used. If 'fast' then ``newton`` is used. Otherwise the problem is bounded between zero and open-circuit voltage and a bisection method, ``brentq``, is used, that guarantees convergence. @@ -1681,13 +1681,13 @@ def singlediode(photocurrent, saturation_current, resistance_series, .. 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} - 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} - 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} - 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} - 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} - I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh} - I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh} + 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}} \\ + 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}} \\ + 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}} \\ + 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}} \\ + 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}} \\ + I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} \\ + I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} If ``method.lower() == 'fast'`` then a gradient descent method, ``newton`` is used to solve the implicit diode equation. It should be safe for well @@ -1700,7 +1700,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, voltages from zero to open-circuit voltage with a log spacing so that points get closer as they approach the open-circuit voltage. - If ``method.lower() == 'lambertw'`` then he solution employed to solve the + If ``method.lower() == 'lambertw'`` then the solution employed to solve the implicit diode equation utilizes the Lambert W function to obtain an explicit function of V=f(i) and I=f(V) as shown in [2]. From 54e8d18b919438dad63e2b8e6cf4b0d54409609b Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 16:40:44 -0800 Subject: [PATCH 26/73] fix circular import issues? * add bishop88 and est_voc to pvsystem api * add 0.5.2 to what's new * literally add bishop88 and est_voc to pvsystem from way_faster * fix some typos * explicitly state that these point bound the entire forward bias 1st quadrant iv curve * remove, "we'll remove fast in the future", why? * fix link to bishop88 * fix links in est_voc and add equation and reference * add reference in bishop88 * do not import way_faster ANYWHERE! - in test_numerical_precision.py use pvsystem.est_voc and pvsystem.bishop88 - in test_way_faster.py define faster_way = pvsystem.way_faster.faster_way and ditto for slower_way Signed-off-by: Mark Mikofski --- docs/sphinx/source/api.rst | 2 ++ docs/sphinx/source/whatsnew.rst | 1 + pvlib/pvsystem.py | 23 ++++++++++++++--------- pvlib/test/test_numerical_precision.py | 7 +++---- pvlib/test/test_way_faster.py | 4 +++- pvlib/way_faster.py | 16 ++++++++++++---- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index be86253a88..bf54f35f83 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -206,6 +206,8 @@ Functions relevant for the single diode model. pvsystem.singlediode pvsystem.v_from_i pvsystem.mppt + pvsystem.bishop88 + pvsystem.est_voc SAPM model ---------- diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index 37870b40ec..78522f50d3 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New These are new features and improvements of note in each release. +.. include:: whatsnew/v0.5.2.rst .. include:: whatsnew/v0.5.1.rst .. include:: whatsnew/v0.5.0.rst .. include:: whatsnew/v0.4.5.txt diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 244049fa30..44c27daa63 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -22,6 +22,9 @@ from pvlib import irradiance, atmosphere from pvlib import way_faster +bishop88 = way_faster.bishop88 +est_voc = way_faster.est_voc + # not sure if this belongs in the pvsystem module. # maybe something more like core.py? It may eventually grow to @@ -1668,16 +1671,19 @@ def singlediode(photocurrent, saturation_current, resistance_series, :math:`V_d = V + I*Rs`. Then the voltage is backed out from :math:`V_d`. A specific desired point, such as short circuit current or max power, is located using the bisection search method, ``brentq``, bounded by a zero - diode voltage and an estimat of open circuit current given by + 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}` that the resulting - current is also negative, meaning that the corresponding voltagge must be - in the 4th quadrant and therefore greater than the open circuit voltage. + 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:: @@ -1689,14 +1695,13 @@ def singlediode(photocurrent, saturation_current, resistance_series, I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} \\ I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} - If ``method.lower() == 'fast'`` then a gradient descent method, ``newton`` + If ``method.lower() == 'fast'`` then a gradient descent method, ``newton``, is used to solve the implicit diode equation. It should be safe for well - behaved IV-curves, but the default method is recommended for reliability - and surprisingly it is just as fast. This method may be removed in future - versions. + behaved IV-curves, but the default method is recommended for reliability, + it is often just as fast. If either the "fast" or default methods are indicated, then - :func:`pvlib.way_faster.bishop88` is used to calculate the points at diode + :func:`pvlib.pvsystem.bishop88` is used to calculate the points at diode voltages from zero to open-circuit voltage with a log spacing so that points get closer as they approach the open-circuit voltage. diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 486acf044b..647bba15a6 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -8,7 +8,6 @@ import numpy as np import pandas as pd from pvlib import pvsystem -from pvlib import way_faster logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def generate_numerical_precicion(): ) # generate exact values data = dict(zip((il, io, rs, rsh, nnsvt), ARGS)) - vdtest = np.linspace(0, way_faster.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + vdtest = np.linspace(0, pvsystem.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) expected = [] for test in vdtest: data[vd] = test @@ -89,8 +88,8 @@ def test_numerical_precicion(): Test that there are no numerical errors due to floating point arithmetic. """ expected = pd.read_csv(DATA_PATH) - vdtest = np.linspace(0, way_faster.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) - results = way_faster.bishop88(vdtest, *ARGS, gradients=True) + vdtest = np.linspace(0, pvsystem.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + results = pvsystem.bishop88(vdtest, *ARGS, gradients=True) assert np.allclose(expected['i'], results[0]) assert np.allclose(expected['v'], results[1]) assert np.allclose(expected['p'], results[2]) diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_way_faster.py index 02f5e5ad20..11f3e58d11 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_way_faster.py @@ -6,9 +6,11 @@ import logging import numpy as np from pvlib import pvsystem -from pvlib.way_faster import faster_way, slower_way from conftest import requires_scipy +faster_way = pvsystem.way_faster.faster_way +slower_way = pvsystem.way_faster.slower_way + logging.basicConfig() LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 93bb07a16e..81be8c8a4d 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -16,15 +16,23 @@ def est_voc(photocurrent, saturation_current, nNsVth): """ Rough estimate of open circuit voltage useful for bounding searches for - ``i`` of ``v`` when using :func:`~pvlib.way_faster`. + ``i`` of ``v`` when using :func:`~pvlib.pvsystem.singlediode`. :param numeric photocurrent: photo-generated current [A] :param numeric saturation_current: diode one reverse saturation current [A] :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of series cells ``Ns`` :returns: rough estimate of open circuit voltage [V] + + The equation is from [1]. + + .. math:: + + V_{oc, est}=n Ns V_{th} \\log \\left( \\frac{I_L}{I_0} + 1 \\right) + + [1] http://www.pveducation.org/pvcdrom/open-circuit-voltage """ - # http://www.pveducation.org/pvcdrom/open-circuit-voltage + return nNsVth * np.log(photocurrent / saturation_current + 1.0) @@ -35,8 +43,8 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, diode junction voltages [1]. [1] "Computer simulation of the effects of electrical mismatches in - photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) - https://doi.org/10.1016/0379-6787(88)90059-2 + photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) + https://doi.org/10.1016/0379-6787(88)90059-2 :param numeric vd: diode voltages [V] :param numeric photocurrent: photo-generated current [A] From 16ad9b4f3be8e7964fd250692a2ea4acaf6429ae Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 17:03:47 -0800 Subject: [PATCH 27/73] try something remove redundant package imports --- pvlib/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pvlib/__init__.py b/pvlib/__init__.py index d8b0657822..5a182fb402 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -1,15 +1,15 @@ import logging logging.basicConfig() from pvlib.version import __version__ -from pvlib import tools -from pvlib import atmosphere -from pvlib import clearsky -# from pvlib import forecast -from pvlib import irradiance -from pvlib import location -from pvlib import solarposition -from pvlib import tmy -from pvlib import tracking -from pvlib import pvsystem -from pvlib import spa -from pvlib import modelchain +# from pvlib import tools +# from pvlib import atmosphere +# from pvlib import clearsky +# # from pvlib import forecast +# from pvlib import irradiance +# from pvlib import location +# from pvlib import solarposition +# from pvlib import tmy +# from pvlib import tracking +# from pvlib import pvsystem +# from pvlib import spa +# from pvlib import modelchain From 7aca30221927f66db6b45abd7ab9536e2fc61735 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 17:19:14 -0800 Subject: [PATCH 28/73] don't raise import error at module level if no scipy --- pvlib/way_faster.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 81be8c8a4d..8db2638830 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -8,7 +8,8 @@ try: from scipy.optimize import brentq, newton except ImportError: - raise ImportError('Thes function requires scipy') + brentq = NotImplemented + newton = NotImplemented # TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default @@ -85,6 +86,8 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, """ This is a slow but reliable way to find current given any voltage. """ + if brentq is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -99,6 +102,8 @@ def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, """ This is a fast but unreliable way to find current given any voltage. """ + if newton is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -113,6 +118,8 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, """ This is a slow but reliable way to find voltage given any current. """ + if brentq is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -127,6 +134,8 @@ def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, """ This is a fast but unreliable way to find voltage given any current. """ + if newton is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -143,6 +152,8 @@ def slow_mppt(photocurrent, saturation_current, resistance_series, """ This is a slow but reliable way to find mpp. """ + if brentq is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -158,6 +169,8 @@ def fast_mppt(photocurrent, saturation_current, resistance_series, """ This is a fast but unreliable way to find mpp. """ + if newton is NotImplemented: + raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) From 086e73f1a2f6ed4f02f670d50ceed5dae927a613 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 17:27:49 -0800 Subject: [PATCH 29/73] okay I think I fixed it, now return pvlib module objects --- pvlib/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pvlib/__init__.py b/pvlib/__init__.py index 5a182fb402..d8b0657822 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -1,15 +1,15 @@ import logging logging.basicConfig() from pvlib.version import __version__ -# from pvlib import tools -# from pvlib import atmosphere -# from pvlib import clearsky -# # from pvlib import forecast -# from pvlib import irradiance -# from pvlib import location -# from pvlib import solarposition -# from pvlib import tmy -# from pvlib import tracking -# from pvlib import pvsystem -# from pvlib import spa -# from pvlib import modelchain +from pvlib import tools +from pvlib import atmosphere +from pvlib import clearsky +# from pvlib import forecast +from pvlib import irradiance +from pvlib import location +from pvlib import solarposition +from pvlib import tmy +from pvlib import tracking +from pvlib import pvsystem +from pvlib import spa +from pvlib import modelchain From 9f2b157d90e12a8c43f11320559fa78f38272036 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 31 Jan 2018 18:00:56 -0800 Subject: [PATCH 30/73] try to modify i_from_v --- pvlib/pvsystem.py | 150 ++++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 66 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 44c27daa63..8a4a3c0602 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1734,7 +1734,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, if method.lower() == 'lambertw': # Compute short circuit current i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent) + saturation_current, photocurrent, method) # Compute open circuit voltage v_oc = v_from_i(resistance_shunt, resistance_series, nNsVth, 0., @@ -1746,19 +1746,21 @@ def singlediode(photocurrent, saturation_current, resistance_series, 'i_0': saturation_current, 'i_l': photocurrent} - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) + p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, + _pwr_optfcn) - # Invert the Power-Current curve. Find the current where the inverted power - # is minimized. This is i_mp. Start the optimization at v_oc/2 + # Invert the Power-Current curve. Find the current where the inverted + # power is minimized. This is i_mp. Start the optimization at v_oc/2 i_mp = i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, - saturation_current, photocurrent) + saturation_current, photocurrent, method) # Find Ix and Ixx using Lambert W i_x = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5 * v_oc, - saturation_current, photocurrent) + saturation_current, photocurrent, method) i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, - 0.5 * (v_oc + v_mp), saturation_current, photocurrent) + 0.5 * (v_oc + v_mp), saturation_current, photocurrent, + method) out = OrderedDict() out['i_sc'] = i_sc @@ -1775,7 +1777,8 @@ def singlediode(photocurrent, saturation_current, resistance_series, np.linspace(0, 1, ivcurve_pnts)) ivcurve_i = i_from_v(resistance_shunt, resistance_series, nNsVth, - ivcurve_v.T, saturation_current, photocurrent).T + ivcurve_v.T, saturation_current, photocurrent, + method).T out['v'] = ivcurve_v out['i'] = ivcurve_i @@ -1941,7 +1944,7 @@ def _pwr_optfcn(df, loc): ''' I = i_from_v(df['r_sh'], df['r_s'], df['nNsVth'], df[loc], df['i_0'], - df['i_l']) + df['i_l'], method='lambertw') return I * df[loc] @@ -2085,7 +2088,7 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, - saturation_current, photocurrent): + saturation_current, photocurrent, method=''): ''' Device current at the given device voltage for the single diode model. @@ -2144,65 +2147,80 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, parameters of real solar cells using Lambert W-function", Solar Energy Materials and Solar Cells, 81 (2004) 269-277. ''' - try: - from scipy.special import lambertw - except ImportError: - raise ImportError('This function requires scipy') - - # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - [resistance_shunt, resistance_series, nNsVth, - voltage, saturation_current, photocurrent])) - - # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which - # is generally more numerically stable - conductance_shunt = 1./resistance_shunt - - # Ensure that we are working with read-only views of numpy arrays - # Turns Series into arrays so that we don't have to worry about - # multidimensional broadcasting failing - Gsh, Rs, a, V, I0, IL = \ - np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, - voltage, saturation_current, photocurrent) - - # Intitalize output I (V might not be float64) - I = np.full_like(V, np.nan, dtype=np.float64) - - # Determine indices where 0 < Rs requires implicit model solution - idx_p = 0. < Rs - - # Determine indices where 0 = Rs allows explicit model solution - idx_z = 0. == Rs - - # Explicit solutions where Rs=0 - if np.any(idx_z): - I[idx_z] = IL[idx_z] - I0[idx_z]*np.expm1(V[idx_z]/a[idx_z]) - \ - Gsh[idx_z]*V[idx_z] - - # Only compute using LambertW if there are cases with Rs>0 - # Does NOT handle possibility of overflow, github issue 298 - if np.any(idx_p): - # LambertW argument, cannot be float128, may overflow to np.inf - argW = Rs[idx_p]*I0[idx_p]/(a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.)) * \ - np.exp((Rs[idx_p]*(IL[idx_p] + I0[idx_p]) + V[idx_p]) / - (a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.))) - - # lambertw typically returns complex value with zero imaginary part - # may overflow to np.inf - lambertwterm = lambertw(argW).real - - # Eqn. 2 in Jain and Kapoor, 2004 - # I = -V/(Rs + Rsh) - (a/Rs)*lambertwterm + Rsh*(IL + I0)/(Rs + Rsh) - # Recast in terms of Gsh=1/Rsh for better numerical stability. - I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p]*Gsh[idx_p]) / \ - (Rs[idx_p]*Gsh[idx_p] + 1.) - (a[idx_p]/Rs[idx_p])*lambertwterm - - if output_is_scalar: - return np.asscalar(I) + if method.lower() == 'lambertw': + try: + from scipy.special import lambertw + except ImportError: + raise ImportError('This function requires scipy') + + # Record if inputs were all scalar + output_is_scalar = all(map(np.isscalar, + [resistance_shunt, resistance_series, nNsVth, + voltage, saturation_current, photocurrent])) + + # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which + # is generally more numerically stable + conductance_shunt = 1./resistance_shunt + + # Ensure that we are working with read-only views of numpy arrays + # Turns Series into arrays so that we don't have to worry about + # multidimensional broadcasting failing + Gsh, Rs, a, V, I0, IL = \ + np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, + voltage, saturation_current, photocurrent) + + # Intitalize output I (V might not be float64) + I = np.full_like(V, np.nan, dtype=np.float64) + + # Determine indices where 0 < Rs requires implicit model solution + idx_p = 0. < Rs + + # Determine indices where 0 = Rs allows explicit model solution + idx_z = 0. == Rs + + # Explicit solutions where Rs=0 + if np.any(idx_z): + I[idx_z] = IL[idx_z] - I0[idx_z]*np.expm1(V[idx_z]/a[idx_z]) - \ + Gsh[idx_z]*V[idx_z] + + # Only compute using LambertW if there are cases with Rs>0 + # Does NOT handle possibility of overflow, github issue 298 + if np.any(idx_p): + # LambertW argument, cannot be float128, may overflow to np.inf + argW = Rs[idx_p]*I0[idx_p]/(a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.)) * \ + np.exp((Rs[idx_p]*(IL[idx_p] + I0[idx_p]) + V[idx_p]) / + (a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.))) + + # lambertw typically returns complex value with zero imaginary part + # may overflow to np.inf + lambertwterm = lambertw(argW).real + + # Eqn. 2 in Jain and Kapoor, 2004 + # I = -V/(Rs + Rsh) - (a/Rs)*lambertwterm + Rsh*(IL + I0)/(Rs + Rsh) + # Recast in terms of Gsh=1/Rsh for better numerical stability. + I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p]*Gsh[idx_p]) / \ + (Rs[idx_p]*Gsh[idx_p] + 1.) - (a[idx_p]/Rs[idx_p])*lambertwterm + + if output_is_scalar: + return np.asscalar(I) + else: + return I else: + if method.lower() == 'fast': + i_from_v_fun = way_faster.fast_i_from_v + else: + i_from_v_fun = way_faster.slow_i_from_v + try: + len(photocurrent) + except TypeError: + I = i_from_v_fun(voltage, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + else: + vecfun = np.vectorize(i_from_v_fun) + I = vecfun(voltage, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) return I - def snlinverter(v_dc, p_dc, inverter): r''' Converts DC power and voltage to AC power using Sandia's From 555e94640f612a0e6a16cfe44bbf8d0ba07c7aea Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 01:08:14 -0800 Subject: [PATCH 31/73] implement v_from_i wrapper * add new test_i_from_v_from_i which recycles the test fixture from v_from_i which has more reasonable test values that are not way down in quadrant 4, it uses the old i_from_v(method='lambertw') to recalculate I so it has the write shape, size and dtype * set method='lambertw' for old i_from_v test, because since the Voc is like 8V, and the test voltage is 40V, the expected current is -300A, which is unlikely, and I doubt very very very much that this calculated value is anywhere near the actual current. Ha, ha, ha, 300A, that is so funny! * change PVSystem_i_from_v test to use the v_from_i fixture values which actually make some sense * set v_from_i inside singlediode, to use the method='lambertw' when given * add default method='' to v_from_i, add docstring * add docstring to i_from_v * add section to size and shape * wrap i_from_v_fun with returns_nan(), defaults to ValueError and RuntimeError * add returns_nan() decorator to way_faster --- pvlib/pvsystem.py | 245 ++++++++++++++++++++++++------------ pvlib/test/test_pvsystem.py | 46 +++++-- pvlib/way_faster.py | 20 +++ 3 files changed, 222 insertions(+), 89 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8a4a3c0602..f2f6c72ee5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1738,7 +1738,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, # Compute open circuit voltage v_oc = v_from_i(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent) + saturation_current, photocurrent, method) params = {'r_sh': resistance_shunt, 'r_s': resistance_series, @@ -1950,7 +1950,7 @@ def _pwr_optfcn(df, loc): def v_from_i(resistance_shunt, resistance_series, nNsVth, current, - saturation_current, photocurrent): + saturation_current, photocurrent, method=''): ''' Device voltage at the given device current for the single diode model. @@ -1999,6 +1999,11 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, IV curve conditions. Often abbreviated ``I_L``. 0 <= photocurrent + method : str + Method to use. If 'lambertw' use ``lambertw``, if 'fast' use ``newton``, + otherwise use bisection. Note: bisection is limited to 1st quadrant + only. + Returns ------- current : np.ndarray or scalar @@ -2009,81 +2014,125 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, parameters of real solar cells using Lambert W-function", Solar Energy Materials and Solar Cells, 81 (2004) 269-277. ''' - try: - from scipy.special import lambertw - except ImportError: - raise ImportError('This function requires scipy') - - # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - [resistance_shunt, resistance_series, nNsVth, - current, saturation_current, photocurrent])) - - # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which - # is generally more numerically stable - conductance_shunt = 1./resistance_shunt - - # Ensure that we are working with read-only views of numpy arrays - # Turns Series into arrays so that we don't have to worry about - # multidimensional broadcasting failing - Gsh, Rs, a, I, I0, IL = \ - np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, - current, saturation_current, photocurrent) - - # Intitalize output V (I might not be float64) - V = np.full_like(I, np.nan, dtype=np.float64) - - # Determine indices where 0 < Gsh requires implicit model solution - idx_p = 0. < Gsh - - # Determine indices where 0 = Gsh allows explicit model solution - idx_z = 0. == Gsh - - # Explicit solutions where Gsh=0 - if np.any(idx_z): - V[idx_z] = a[idx_z]*np.log1p((IL[idx_z] - I[idx_z])/I0[idx_z]) - \ - I[idx_z]*Rs[idx_z] - - # Only compute using LambertW if there are cases with Gsh>0 - if np.any(idx_p): - # LambertW argument, cannot be float128, may overflow to np.inf - argW = I0[idx_p] / (Gsh[idx_p]*a[idx_p]) * \ - np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p]*a[idx_p])) - - # lambertw typically returns complex value with zero imaginary part - # may overflow to np.inf - lambertwterm = lambertw(argW).real - - # Record indices where lambertw input overflowed output - idx_inf = np.logical_not(np.isfinite(lambertwterm)) - - # Only re-compute LambertW if it overflowed - if np.any(idx_inf): - # Calculate using log(argW) in case argW is really big - logargW = (np.log(I0[idx_p]) - np.log(Gsh[idx_p]) - - np.log(a[idx_p]) + - (-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p] * a[idx_p]))[idx_inf] - - # Three iterations of Newton-Raphson method to solve - # w+log(w)=logargW. The initial guess is w=logargW. Where direct - # evaluation (above) results in NaN from overflow, 3 iterations - # of Newton's method gives approximately 8 digits of precision. - w = logargW - for _ in range(0, 3): - w = w * (1. - np.log(w) + logargW) / (1. + w) - lambertwterm[idx_inf] = w - - # Eqn. 3 in Jain and Kapoor, 2004 - # V = -I*(Rs + Rsh) + IL*Rsh - a*lambertwterm + I0*Rsh - # Recast in terms of Gsh=1/Rsh for better numerical stability. - V[idx_p] = (IL[idx_p] + I0[idx_p] - I[idx_p])/Gsh[idx_p] - \ - I[idx_p]*Rs[idx_p] - a[idx_p]*lambertwterm - - if output_is_scalar: - return np.asscalar(V) + if method.lower() == 'lambertw': + try: + from scipy.special import lambertw + except ImportError: + raise ImportError('This function requires scipy') + + # Record if inputs were all scalar + output_is_scalar = all(map(np.isscalar, + [resistance_shunt, resistance_series, nNsVth, + current, saturation_current, photocurrent])) + + # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which + # is generally more numerically stable + conductance_shunt = 1./resistance_shunt + + # Ensure that we are working with read-only views of numpy arrays + # Turns Series into arrays so that we don't have to worry about + # multidimensional broadcasting failing + Gsh, Rs, a, I, I0, IL = \ + np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, + current, saturation_current, photocurrent) + + # Intitalize output V (I might not be float64) + V = np.full_like(I, np.nan, dtype=np.float64) + + # Determine indices where 0 < Gsh requires implicit model solution + idx_p = 0. < Gsh + + # Determine indices where 0 = Gsh allows explicit model solution + idx_z = 0. == Gsh + + # Explicit solutions where Gsh=0 + if np.any(idx_z): + V[idx_z] = a[idx_z]*np.log1p((IL[idx_z] - I[idx_z])/I0[idx_z]) - \ + I[idx_z]*Rs[idx_z] + + # Only compute using LambertW if there are cases with Gsh>0 + if np.any(idx_p): + # LambertW argument, cannot be float128, may overflow to np.inf + argW = I0[idx_p] / (Gsh[idx_p]*a[idx_p]) * \ + np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) / + (Gsh[idx_p]*a[idx_p])) + + # lambertw typically returns complex value with zero imaginary part + # may overflow to np.inf + lambertwterm = lambertw(argW).real + + # Record indices where lambertw input overflowed output + idx_inf = np.logical_not(np.isfinite(lambertwterm)) + + # Only re-compute LambertW if it overflowed + if np.any(idx_inf): + # Calculate using log(argW) in case argW is really big + logargW = (np.log(I0[idx_p]) - np.log(Gsh[idx_p]) - + np.log(a[idx_p]) + + (-I[idx_p] + IL[idx_p] + I0[idx_p]) / + (Gsh[idx_p] * a[idx_p]))[idx_inf] + + # Three iterations of Newton-Raphson method to solve + # w+log(w)=logargW. The initial guess is w=logargW. Where direct + # evaluation (above) results in NaN from overflow, 3 iterations + # of Newton's method gives approximately 8 digits of precision. + w = logargW + for _ in range(0, 3): + w = w * (1. - np.log(w) + logargW) / (1. + w) + lambertwterm[idx_inf] = w + + # Eqn. 3 in Jain and Kapoor, 2004 + # V = -I*(Rs + Rsh) + IL*Rsh - a*lambertwterm + I0*Rsh + # Recast in terms of Gsh=1/Rsh for better numerical stability. + V[idx_p] = (IL[idx_p] + I0[idx_p] - I[idx_p])/Gsh[idx_p] - \ + I[idx_p]*Rs[idx_p] - a[idx_p]*lambertwterm + + if output_is_scalar: + return np.asscalar(V) + else: + return V else: + # use way_faster methods + if method.lower() == 'fast': + v_from_i_fun = way_faster.fast_v_from_i # fast method + else: + v_from_i_fun = way_faster.slow_v_from_i # gold method + # wrap it so it returns nan + v_from_i_fun = way_faster.returns_nan()(v_from_i_fun) + # find the right size and shape for returns + args = (current, photocurrent, resistance_shunt) + size = 0 + shape = None + for n, arg in enumerate(args): + try: + this_shape = arg.shape + except AttributeError: + this_shape = None + try: + this_size = len(arg) + except TypeError: + this_size = 0 + else: + this_size = sum(this_shape) + if shape is None: + shape = this_shape + if this_size > size and size <= 1: + size = this_size + if this_shape is not None: + shape = this_shape + if size <= 1: + V = v_from_i_fun(current, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + if shape is not None: + V = np.tile(V, shape) + else: + vecfun = np.vectorize(v_from_i_fun) + V = vecfun(current, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + if np.isnan(V).any() and size <= 1: + V = np.repeat(V, size) + if shape is not None: + V = V.reshape(shape) return V @@ -2137,6 +2186,11 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, IV curve conditions. Often abbreviated ``I_L``. 0 <= photocurrent + method : str + Method to use. If 'lambertw' use ``lambertw``, if 'fast' use ``newton``, + otherwise use bisection. Note: bisection is limited to 1st quadrant + only. + Returns ------- current : np.ndarray or scalar @@ -2206,21 +2260,50 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, else: return I else: + # use way_faster methods if method.lower() == 'fast': - i_from_v_fun = way_faster.fast_i_from_v + i_from_v_fun = way_faster.fast_i_from_v # fast method else: - i_from_v_fun = way_faster.slow_i_from_v - try: - len(photocurrent) - except TypeError: + i_from_v_fun = way_faster.slow_i_from_v # gold method + # wrap it so it returns nan + i_from_v_fun = way_faster.returns_nan()(i_from_v_fun) + # find the right size and shape for returns + args = (voltage, photocurrent, resistance_shunt) + size = 0 + shape = None + for n, arg in enumerate(args): + try: + this_shape = arg.shape + except AttributeError: + this_shape = None + try: + this_size = len(arg) + except TypeError: + this_size = 0 + else: + this_size = sum(this_shape) + if shape is None: + shape = this_shape + if this_size > size and size <= 1: + size = this_size + if this_shape is not None: + shape = this_shape + if size <= 1: I = i_from_v_fun(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) + if shape is not None: + I = np.tile(I, shape) else: vecfun = np.vectorize(i_from_v_fun) I = vecfun(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) + if np.isnan(I).any() and size <= 1: + I = np.repeat(I, size) + if shape is not None: + I = I.reshape(shape) return I + def snlinverter(v_dc, p_dc, inverter): r''' Converts DC power and voltage to AC power using Sandia's diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index a17281ac33..fbb205a685 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -498,6 +498,31 @@ def test_v_from_i(fixture_v_from_i): assert_allclose(V, V_expected, atol=atol) +@requires_scipy +def test_i_from_v_from_i(fixture_v_from_i): + # Solution set loaded from fixture + Rsh = fixture_v_from_i['Rsh'] + Rs = fixture_v_from_i['Rs'] + nNsVth = fixture_v_from_i['nNsVth'] + I = fixture_v_from_i['I'] + I0 = fixture_v_from_i['I0'] + IL = fixture_v_from_i['IL'] + V = fixture_v_from_i['V_expected'] + + # Convergence criteria + atol = 1.e-11 + + I_expected = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, + method='lambertw') + assert_allclose(I, I_expected, atol=atol) + I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL) + assert(isinstance(I, type(I_expected))) + if isinstance(I, type(np.ndarray)): + assert(isinstance(I.dtype, type(I_expected.dtype))) + assert(I.shape == I_expected.shape) + assert_allclose(I, I_expected, atol=atol) + + @pytest.fixture(params=[ { # Can handle all python scalar inputs 'Rsh': 20., @@ -585,7 +610,7 @@ def test_i_from_v(fixture_i_from_v): # Convergence criteria atol = 1.e-11 - I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL) + I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='lambertw') assert(isinstance(I, type(I_expected))) if isinstance(I, type(np.ndarray)): assert(isinstance(I.dtype, type(I_expected.dtype))) @@ -596,8 +621,8 @@ def test_i_from_v(fixture_i_from_v): @requires_scipy def test_PVSystem_i_from_v(): system = pvsystem.PVSystem() - output = system.i_from_v(20, .1, .5, 40, 6e-7, 7) - assert_allclose(output, -299.746389916, atol=1e-5) + output = system.i_from_v(20, 0.1, 0.5, 7.5049875193450521, 6.0e-7, 7.0) + assert_allclose(output, 3.0, atol=1e-5) @requires_scipy @@ -639,7 +664,8 @@ def test_singlediode_array(): resistance_series, resistance_shunt, nNsVth) expected = pvsystem.i_from_v(resistance_shunt, resistance_series, nNsVth, - sd['v_mp'], saturation_current, photocurrent) + sd['v_mp'], saturation_current, photocurrent, + method='lambertw') assert_allclose(sd['i_mp'], expected, atol=0.01) @@ -719,10 +745,14 @@ def test_singlediode_series_ivcurve(cec_module_params): out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) - expected['i_mp'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v_mp'], I0, IL) - expected['v_mp'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i_mp'], I0, IL) - expected['i'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v'].T, I0, IL).T - expected['v'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i'].T, I0, IL).T + expected['i_mp'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v_mp'], I0, IL, + method='lambertw') + expected['v_mp'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i_mp'], I0, IL, + method='lambertw') + expected['i'] = pvsystem.i_from_v(Rsh, Rs, nNsVth, out['v'].T, I0, IL, + method='lambertw').T + expected['v'] = pvsystem.v_from_i(Rsh, Rs, nNsVth, out['i'].T, I0, IL, + method='lambertw').T for k, v in out.items(): if k == 'p': diff --git a/pvlib/way_faster.py b/pvlib/way_faster.py index 8db2638830..df59c7216d 100644 --- a/pvlib/way_faster.py +++ b/pvlib/way_faster.py @@ -4,6 +4,7 @@ """ from collections import OrderedDict +from functools import wraps import numpy as np try: from scipy.optimize import brentq, newton @@ -14,6 +15,25 @@ # TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default +def returns_nan(exc=None): + """ + Decorator that changes the return to NaN if either + """ + if not exc: + exc = (ValueError, RuntimeError) + + def wrapper(f): + @wraps(f) + def wrapped_fcn(*args, **kwargs): + try: + rval = f(*args, **kwargs) + except exc: + rval = np.nan + return rval + return wrapped_fcn + return wrapper + + def est_voc(photocurrent, saturation_current, nNsVth): """ Rough estimate of open circuit voltage useful for bounding searches for From 52a8e8877001dde0f6fe12e36c3c34a0e57c6dc9 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 01:42:11 -0800 Subject: [PATCH 32/73] respond to @thunderfish24 review items: * fix spelling in what's new decent -> descent * refactor/rename way_faster.py -> singlediode_methods.py, thanks pycharm * refactor/rename test_way_faster.py -> test_singlediode_methods.py * --- docs/sphinx/source/whatsnew/v0.5.2.rst | 2 +- pvlib/pvsystem.py | 26 +++++++++---------- .../{way_faster.py => singlediode_methods.py} | 0 ..._faster.py => test_singlediode_methods.py} | 4 +-- 4 files changed, 16 insertions(+), 16 deletions(-) rename pvlib/{way_faster.py => singlediode_methods.py} (100%) rename pvlib/test/{test_way_faster.py => test_singlediode_methods.py} (98%) diff --git a/docs/sphinx/source/whatsnew/v0.5.2.rst b/docs/sphinx/source/whatsnew/v0.5.2.rst index 09f01c1e71..d3ff5a2079 100644 --- a/docs/sphinx/source/whatsnew/v0.5.2.rst +++ b/docs/sphinx/source/whatsnew/v0.5.2.rst @@ -12,7 +12,7 @@ Enhancements * Implement a reliable "gold" implementation of the single diode model (SDM) using a bisection method (Brent, 1973) bounded by points known to include the full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using - a gradient decent method (Newton-Raphson) that is not bounded, but should be + a gradient descent method (Newton-Raphson) that is not bounded, but should be safe for well behaved IV-curves. (:issue:`408`) * Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W with the two new implementations to form a wrapper, that takes an additional diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f2f6c72ee5..7e55a21071 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -20,10 +20,10 @@ from pvlib.tools import _build_kwargs from pvlib.location import Location from pvlib import irradiance, atmosphere -from pvlib import way_faster +from pvlib import singlediode_methods -bishop88 = way_faster.bishop88 -est_voc = way_faster.est_voc +bishop88 = singlediode_methods.bishop88 +est_voc = singlediode_methods.est_voc # not sure if this belongs in the pvsystem module. @@ -1788,9 +1788,9 @@ def singlediode(photocurrent, saturation_current, resistance_series, else: if method.lower() == 'fast': - sdm_fun = way_faster.faster_way + sdm_fun = singlediode_methods.faster_way else: - sdm_fun = way_faster.slower_way + sdm_fun = singlediode_methods.slower_way try: len(photocurrent) except TypeError: @@ -1837,9 +1837,9 @@ def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, and ``p_mp`` """ if method.lower() == 'fast': - mppt_func = way_faster.fast_mppt + mppt_func = singlediode_methods.fast_mppt else: - mppt_func = way_faster.slow_mppt + mppt_func = singlediode_methods.slow_mppt try: len(photocurrent) except TypeError: @@ -2094,11 +2094,11 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, else: # use way_faster methods if method.lower() == 'fast': - v_from_i_fun = way_faster.fast_v_from_i # fast method + v_from_i_fun = singlediode_methods.fast_v_from_i # fast method else: - v_from_i_fun = way_faster.slow_v_from_i # gold method + v_from_i_fun = singlediode_methods.slow_v_from_i # gold method # wrap it so it returns nan - v_from_i_fun = way_faster.returns_nan()(v_from_i_fun) + v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) # find the right size and shape for returns args = (current, photocurrent, resistance_shunt) size = 0 @@ -2262,11 +2262,11 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, else: # use way_faster methods if method.lower() == 'fast': - i_from_v_fun = way_faster.fast_i_from_v # fast method + i_from_v_fun = singlediode_methods.fast_i_from_v # fast method else: - i_from_v_fun = way_faster.slow_i_from_v # gold method + i_from_v_fun = singlediode_methods.slow_i_from_v # gold method # wrap it so it returns nan - i_from_v_fun = way_faster.returns_nan()(i_from_v_fun) + i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) # find the right size and shape for returns args = (voltage, photocurrent, resistance_shunt) size = 0 diff --git a/pvlib/way_faster.py b/pvlib/singlediode_methods.py similarity index 100% rename from pvlib/way_faster.py rename to pvlib/singlediode_methods.py diff --git a/pvlib/test/test_way_faster.py b/pvlib/test/test_singlediode_methods.py similarity index 98% rename from pvlib/test/test_way_faster.py rename to pvlib/test/test_singlediode_methods.py index 11f3e58d11..97ebc7a987 100644 --- a/pvlib/test/test_way_faster.py +++ b/pvlib/test/test_singlediode_methods.py @@ -8,8 +8,8 @@ from pvlib import pvsystem from conftest import requires_scipy -faster_way = pvsystem.way_faster.faster_way -slower_way = pvsystem.way_faster.slower_way +faster_way = pvsystem.singlediode_methods.faster_way +slower_way = pvsystem.singlediode_methods.slower_way logging.basicConfig() LOGGER = logging.getLogger(__name__) From c0e18a55e5bbc48854959a90334471b4b3768d93 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 01:52:22 -0800 Subject: [PATCH 33/73] change default argument for method to "gold" * it doesn't matter what this is called * also fix spelling precicion -> precision --- pvlib/pvsystem.py | 10 +++++----- pvlib/test/test_numerical_precision.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 7e55a21071..475c63bf02 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1575,7 +1575,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None, method=''): + resistance_shunt, nNsVth, ivcurve_pnts=None, method='gold'): r''' Solve the single-diode model to obtain a photovoltaic IV curve. @@ -1631,7 +1631,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, Number of points in the desired IV curve. If None or 0, no IV curves will be produced. - method : str, default '' + method : str, default 'gold' Determines the method used to calculate IV curve and points. If 'lambertw' then ``lambertw`` is used. If 'fast' then ``newton`` is used. Otherwise the problem is bounded between zero and open-circuit @@ -1822,7 +1822,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, - nNsVth, method=''): + nNsVth, method='gold'): """ Max power point tracker. Given the calculated DeSoto parameters calculates the maximum power point (MPP). @@ -1950,7 +1950,7 @@ def _pwr_optfcn(df, loc): def v_from_i(resistance_shunt, resistance_series, nNsVth, current, - saturation_current, photocurrent, method=''): + saturation_current, photocurrent, method='gold'): ''' Device voltage at the given device current for the single diode model. @@ -2137,7 +2137,7 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, - saturation_current, photocurrent, method=''): + saturation_current, photocurrent, method='gold'): ''' Device current at the given device voltage for the single diode model. diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 647bba15a6..674c5d6603 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -37,7 +37,7 @@ sy_exp = NotImplemented -def generate_numerical_precicion(): +def generate_numerical_precision(): """ Generate expected data with infinite numerical precision using SymPy. :return: dataframe of expected values @@ -83,7 +83,7 @@ def generate_numerical_precicion(): return pd.DataFrame(expected, index=vdtest) -def test_numerical_precicion(): +def test_numerical_precision(): """ Test that there are no numerical errors due to floating point arithmetic. """ @@ -101,6 +101,6 @@ def test_numerical_precicion(): if __name__ == '__main__': - expected = generate_numerical_precicion() + expected = generate_numerical_precision() expected.to_csv(DATA_PATH) - test_numerical_precicion() + test_numerical_precision() From 09c6a758cb8958d76f53f64b142a105b0a0849bb Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 02:10:03 -0800 Subject: [PATCH 34/73] update test_singlediode_methods to use lambertw in comparison * add comment about how Voc is estimated and that it's useful as a bound for the bisection method * remove "unreliable" from fast/newton methods --- pvlib/singlediode_methods.py | 12 ++++++--- pvlib/test/test_singlediode_methods.py | 36 ++++++++++++++------------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index df59c7216d..b5316b75f0 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -45,7 +45,11 @@ def est_voc(photocurrent, saturation_current, nNsVth): ideality factor ``n``, and number of series cells ``Ns`` :returns: rough estimate of open circuit voltage [V] - The equation is from [1]. + Calculating the open circuit voltage, :math:`V_{oc}`, of an ideal device + with infinite shunt resistance, :math:`R_{sh} \\to \\infty`, and zero series + resistance, :math:`R_s = 0`, yields the following equation [1]. As an + estimate of :math:`V_{oc}` it is useful as an upper bound for the bisection + method. .. math:: @@ -120,7 +124,7 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ - This is a fast but unreliable way to find current given any voltage. + This is a possibly faster way to find current given any voltage. """ if newton is NotImplemented: raise ImportError('This function requires scipy') @@ -152,7 +156,7 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ - This is a fast but unreliable way to find voltage given any current. + This is a possibly faster way to find voltage given any current. """ if newton is NotImplemented: raise ImportError('This function requires scipy') @@ -187,7 +191,7 @@ def slow_mppt(photocurrent, saturation_current, resistance_series, def fast_mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ - This is a fast but unreliable way to find mpp. + This is a possibly faster way to find mpp. """ if newton is NotImplemented: raise ImportError('This function requires scipy') diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index 97ebc7a987..c1700bbaf3 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -29,7 +29,7 @@ def test_fast_spr_e20_327(): EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x tstart = clock() - pvs = pvsystem.singlediode(*x) + pvs = pvsystem.singlediode(*x, method='lambertw') tstop = clock() dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) @@ -43,13 +43,14 @@ def test_fast_spr_e20_327(): assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct - pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) - pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il, method='lambertw') + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il, method='lambertw') assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) assert np.isclose(pvs['i_x'], ix) - pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, + method='lambertw') assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, pvs @@ -64,7 +65,7 @@ def test_fast_fs_495(): il, io, rs, rsh, nnsvt = x x += (101, ) tstart = clock() - pvs = pvsystem.singlediode(*x) + pvs = pvsystem.singlediode(*x, method='lambertw') tstop = clock() dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) @@ -78,13 +79,14 @@ def test_fast_fs_495(): assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct - pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) - pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il, method='lambertw') + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il, method='lambertw') assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) assert np.isclose(pvs['i_x'], ix) - pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, + method='lambertw') assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, i, v, p, pvs @@ -98,7 +100,7 @@ def test_slow_spr_e20_327(): EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x tstart = clock() - pvs = pvsystem.singlediode(*x) + pvs = pvsystem.singlediode(*x, method='lambertw') tstop = clock() dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) @@ -112,13 +114,14 @@ def test_slow_spr_e20_327(): assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct - pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) - pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il, method='lambertw') + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il, method='lambertw') assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) assert np.isclose(pvs['i_x'], ix) - pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, + method='lambertw') assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, pvs @@ -133,7 +136,7 @@ def test_slow_fs_495(): il, io, rs, rsh, nnsvt = x x += (101, ) tstart = clock() - pvs = pvsystem.singlediode(*x) + pvs = pvsystem.singlediode(*x, method='lambertw') tstop = clock() dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) @@ -147,13 +150,14 @@ def test_slow_fs_495(): assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct - pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il) - pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il) + pvs_imp = pvsystem.i_from_v(rsh, rs, nnsvt, vmp, io, il, method='lambertw') + pvs_vmp = pvsystem.v_from_i(rsh, rs, nnsvt, imp, io, il, method='lambertw') assert np.isclose(pvs_imp, imp) assert np.isclose(pvs_vmp, vmp) assert np.isclose(pvs['p_mp'], pmp) assert np.isclose(pvs['i_x'], ix) - pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il) + pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, + method='lambertw') assert np.isclose(pvs_ixx, ixx) return isc, voc, imp, vmp, pmp, i, v, p, pvs From ff8cc0b27fe75019973b868ee9a0f40b8c3e7a53 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 09:11:28 -0800 Subject: [PATCH 35/73] change name from mppt -> mpp * add mpp tests for float, array and series * --- pvlib/pvsystem.py | 16 ++++++------ pvlib/singlediode_methods.py | 12 ++++----- pvlib/test/test_pvsystem.py | 50 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 475c63bf02..97a2465e84 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1821,11 +1821,11 @@ def singlediode(photocurrent, saturation_current, resistance_series, return out -def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, - nNsVth, method='gold'): +def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, + nNsVth, method='gold'): """ - Max power point tracker. Given the calculated DeSoto parameters calculates - the maximum power point (MPP). + Given the calculated DeSoto parameters, calculates the maximum power point + (MPP). :param numeric photocurrent: photo-generated current [A] :param numeric saturation_current: diode one reverse saturation current [A] @@ -1837,13 +1837,13 @@ def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, and ``p_mp`` """ if method.lower() == 'fast': - mppt_func = singlediode_methods.fast_mppt + mpp_fun = singlediode_methods.fast_mpp else: - mppt_func = singlediode_methods.slow_mppt + mpp_fun = singlediode_methods.slow_mpp try: len(photocurrent) except TypeError: - i_mp, v_mp, p_mp = mppt_func( + i_mp, v_mp, p_mp = mpp_fun( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth ) @@ -1852,7 +1852,7 @@ def mppt(photocurrent, saturation_current, resistance_series, resistance_shunt, out['v_mp'] = v_mp out['p_mp'] = p_mp else: - vecfun = np.vectorize(mppt_func) + vecfun = np.vectorize(mpp_fun) ivp = vecfun(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) if isinstance(photocurrent, pd.Series): diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index b5316b75f0..bc616a5809 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -171,8 +171,8 @@ def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, return bishop88(vd, *args)[1] -def slow_mppt(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def slow_mpp(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): """ This is a slow but reliable way to find mpp. """ @@ -188,8 +188,8 @@ def slow_mppt(photocurrent, saturation_current, resistance_series, return bishop88(vd, *args) -def fast_mppt(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def fast_mpp(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): """ This is a possibly faster way to find mpp. """ @@ -216,7 +216,7 @@ def slower_way(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) v_oc = slow_v_from_i(0.0, *args) - i_mp, v_mp, p_mp = slow_mppt(*args) + i_mp, v_mp, p_mp = slow_mpp(*args) out = OrderedDict() out['i_sc'] = slow_i_from_v(0.0, *args) out['v_oc'] = v_oc @@ -243,7 +243,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args v_oc = fast_v_from_i(0.0, *args) - i_mp, v_mp, p_mp = fast_mppt(*args) + i_mp, v_mp, p_mp = fast_mpp(*args) out = OrderedDict() out['i_sc'] = fast_i_from_v(0.0, *args) out['v_oc'] = v_oc diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index fbb205a685..596bfe4781 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -625,6 +625,56 @@ def test_PVSystem_i_from_v(): assert_allclose(output, 3.0, atol=1e-5) +@requires_scipy +def test_mpp_floats(): + """test mpp""" + IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, .1, 20, .5) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + expected = {'i_mp': 6.1362673597376753, # 6.1390251797935704, lambertw + 'v_mp': 6.2243393757884284, # 6.221535886625464, lambertw + 'p_mp': 38.194210547580511} # 38.194165464983037} lambertw + assert isinstance(out, dict) + for k, v in out.items(): + assert np.isclose(v, expected[k]) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='fast') + for k, v in out.items(): + assert np.isclose(v, expected[k]) + + +@requires_scipy +def test_mpp_array(): + """test mpp""" + IL, I0, Rs, Rsh, nNsVth = ([7, 7], 6e-7, .1, 20, .5) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + expected = {'i_mp': [6.1362673597376753] * 2, + 'v_mp': [6.2243393757884284] * 2, + 'p_mp': [38.194210547580511] * 2} + assert isinstance(out, dict) + for k, v in out.items(): + assert np.allclose(v, expected[k]) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='fast') + for k, v in out.items(): + assert np.allclose(v, expected[k]) + + +@requires_scipy +def test_mpp_series(): + """test mpp""" + idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] + IL, I0, Rs, Rsh, nNsVth = ([7, 7], 6e-7, .1, 20, .5) + IL = pd.Series(IL, index=idx) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + expected = pd.DataFrame({'i_mp': [6.1362673597376753] * 2, + 'v_mp': [6.2243393757884284] * 2, + 'p_mp': [38.194210547580511] * 2}, + index=idx) + assert isinstance(out, pd.DataFrame) + for k, v in out.items(): + assert np.allclose(v, expected[k]) + for k, v in out.items(): + assert np.allclose(v, expected[k]) + + @requires_scipy def test_singlediode_series(cec_module_params): times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') From 446fa9e65b299ea45f22e353697b5c206aef019f Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 10:33:15 -0800 Subject: [PATCH 36/73] add test to check for verbose yet graceful failure of v_from_i * check size of all args * return value error if this_size > size and size > 1 --- pvlib/pvsystem.py | 14 +++++++++----- pvlib/test/test_pvsystem.py | 6 ++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 97a2465e84..a1ff400d6b 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2268,7 +2268,8 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, # wrap it so it returns nan i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) # find the right size and shape for returns - args = (voltage, photocurrent, resistance_shunt) + args = (voltage, photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) size = 0 shape = None for n, arg in enumerate(args): @@ -2288,15 +2289,18 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, size = this_size if this_shape is not None: shape = this_shape + else: + msg = ('Argument: "%s" is different size from other arguments' + ' (%d > %d). All arguments must be the same size or' + ' scalar.') % (arg, this_size, size) + raise ValueError(msg) if size <= 1: - I = i_from_v_fun(voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + I = i_from_v_fun(*args) if shape is not None: I = np.tile(I, shape) else: vecfun = np.vectorize(i_from_v_fun) - I = vecfun(voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + I = vecfun(*args) if np.isnan(I).any() and size <= 1: I = np.repeat(I, size) if shape is not None: diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 596bfe4781..f91491d807 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -625,6 +625,12 @@ def test_PVSystem_i_from_v(): assert_allclose(output, 3.0, atol=1e-5) +@requires_scipy +@pytest.raises(ValueError) +def test_i_from_v_size(): + pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0) + + @requires_scipy def test_mpp_floats(): """test mpp""" From f976f61421e1c0a320913c46ba0d7f433e20269e Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 10:56:51 -0800 Subject: [PATCH 37/73] fix pytest.raise context, add test_v_from_i * add all i_from_v and v_from_i args to checklist * remove enumerate, not used * add some comments to explain why we do it * combine assignment of size, shape initial values in one line * use *args in calls to i_from_v, etc. since we have it * don't need to check if size<=1 before updating size or shape, since we let np.vectorize handle broadcasting and raising ValueError --- pvlib/pvsystem.py | 48 +++++++++++++++++-------------------- pvlib/test/test_pvsystem.py | 10 ++++++-- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a1ff400d6b..64db23bf67 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2100,35 +2100,35 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, # wrap it so it returns nan v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) # find the right size and shape for returns - args = (current, photocurrent, resistance_shunt) - size = 0 - shape = None - for n, arg in enumerate(args): + args = (current, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + size, shape = 0, None # 0 or None both mean scalar + for arg in args: try: - this_shape = arg.shape + this_shape = arg.shape # try to get shape except AttributeError: this_shape = None try: - this_size = len(arg) + this_size = len(arg) # try to get the size except TypeError: this_size = 0 else: - this_size = sum(this_shape) + this_size = sum(this_shape) # calc size from shape if shape is None: - shape = this_shape - if this_size > size and size <= 1: + shape = this_shape # set the shape if None + # update size and shape + if this_size > size: size = this_size if this_shape is not None: shape = this_shape if size <= 1: - V = v_from_i_fun(current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + V = v_from_i_fun(*args) if shape is not None: V = np.tile(V, shape) else: + # np.vectorize handles broadcasting, raises ValueError vecfun = np.vectorize(v_from_i_fun) - V = vecfun(current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + V = vecfun(*args) if np.isnan(V).any() and size <= 1: V = np.repeat(V, size) if shape is not None: @@ -2270,35 +2270,31 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, # find the right size and shape for returns args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - size = 0 - shape = None - for n, arg in enumerate(args): + size, shape = 0, None # 0 or None both mean scalar + for arg in args: try: - this_shape = arg.shape + this_shape = arg.shape # try to get shape except AttributeError: this_shape = None try: - this_size = len(arg) + this_size = len(arg) # try to get the size except TypeError: this_size = 0 else: - this_size = sum(this_shape) + this_size = sum(this_shape) # calc size from shape if shape is None: - shape = this_shape - if this_size > size and size <= 1: + shape = this_shape # set the shape if None + # update size and shape + if this_size > size: size = this_size if this_shape is not None: shape = this_shape - else: - msg = ('Argument: "%s" is different size from other arguments' - ' (%d > %d). All arguments must be the same size or' - ' scalar.') % (arg, this_size, size) - raise ValueError(msg) if size <= 1: I = i_from_v_fun(*args) if shape is not None: I = np.tile(I, shape) else: + # np.vectorize handles broadcasting, raises ValueError vecfun = np.vectorize(i_from_v_fun) I = vecfun(*args) if np.isnan(I).any() and size <= 1: diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index f91491d807..b849290f8a 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -626,9 +626,15 @@ def test_PVSystem_i_from_v(): @requires_scipy -@pytest.raises(ValueError) def test_i_from_v_size(): - pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0) + with pytest.raises(ValueError): + pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0) + + +@requires_scipy +def test_v_from_i_size(): + with pytest.raises(ValueError): + pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0) @requires_scipy From db88022b92d0da515cb2f4f653fb78bf62942345 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 11:15:32 -0800 Subject: [PATCH 38/73] change mppt->mpp in api.rst docs --- docs/sphinx/source/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index bf54f35f83..c7589b0e30 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -205,7 +205,7 @@ Functions relevant for the single diode model. pvsystem.i_from_v pvsystem.singlediode pvsystem.v_from_i - pvsystem.mppt + pvsystem.mpp pvsystem.bishop88 pvsystem.est_voc From a509dd30489c350af5b5c252b9673f17576ce06b Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 1 Feb 2018 11:38:36 -0800 Subject: [PATCH 39/73] fix what's new to point to mpp in docs, also add links to bishop88 and voc_est --- docs/sphinx/source/whatsnew/v0.5.2.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.5.2.rst b/docs/sphinx/source/whatsnew/v0.5.2.rst index d3ff5a2079..04c276d108 100644 --- a/docs/sphinx/source/whatsnew/v0.5.2.rst +++ b/docs/sphinx/source/whatsnew/v0.5.2.rst @@ -14,11 +14,19 @@ Enhancements full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using a gradient descent method (Newton-Raphson) that is not bounded, but should be safe for well behaved IV-curves. (:issue:`408`) +* Implement :func:`~pvlib.pvsystem.bishop88` for explicit calculation of + arbitrary IV curve points using diode voltage instead of cell voltage. This + method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` + if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will + be log spaced instead of linear. +* Implement :func:`~pvlib.pvsystem.est_voc` to estimate open circuit voltage by + assuming :math:`R_{sh} \to \infty`. This is used as an upper bound in + bisection method for :func:`~pvlib.pvsystem.singlediode`. * Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W with the two new implementations to form a wrapper, that takes an additional - ``method`` argument ``('lambertw', 'fast')`` that defaults to the new "gold" - bounded bisection method. (:issue:`410`) -* Add :func:`~pvlib.pvsystem.mppt` method to compute the max power point. + ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new + "gold" bounded bisection method. (:issue:`410`) +* Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point. Bug fixes ~~~~~~~~~ From 5c939b8f192b863dff9379577c7668a8a351e990 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 20 Jun 2018 00:42:49 -0700 Subject: [PATCH 40/73] TST: fix precision test to use new calcparams* API --- pvlib/test/test_numerical_precision.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 674c5d6603..ea2242a405 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -22,8 +22,10 @@ # get module from cecmod and apply temp/irrad desoto corrections SPR_E20_327 = CECMOD.SunPower_SPR_E20_327 ARGS = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=SPR_E20_327.alpha_sc, module_parameters=SPR_E20_327, + effective_irradiance=POA, temp_cell=TCELL, + alpha_sc=SPR_E20_327.alpha_sc, a_ref=SPR_E20_327.a_ref, + I_L_ref=SPR_E20_327.I_L_ref, I_o_ref=SPR_E20_327.I_o_ref, + R_sh_ref=SPR_E20_327.R_sh_ref, R_s=SPR_E20_327.R_s, EgRef=1.121, dEgdT=-0.0002677 ) IL, I0, RS, RSH, NNSVTH = ARGS From 66a801d4a76f81c62f93b4a07524bb2b97034fdb Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 20 Jun 2018 01:14:00 -0700 Subject: [PATCH 41/73] TST: update singlediode test for updated atol in #415 * use `method='lambertw'` for singlediode tests * update arguments for calcparams_desoto everywhere with effective_irradiance and expand model parameters a_ref, I_L_ref, I_o_ref, R_sh_ref, and R_s --- pvlib/test/test_pvsystem.py | 4 ++-- pvlib/test/test_singlediode_methods.py | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index f3354d3e2f..424255d555 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -797,7 +797,7 @@ def test_singlediode_array(): def test_singlediode_floats(sam_data): module = 'Example_Module' module_parameters = sam_data['cecmod'][module] - out = pvsystem.singlediode(7, 6e-7, .1, 20, .5) + out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, method='lambertw') expected = {'i_xx': 4.2498, 'i_mp': 6.1275, 'v_oc': 8.1063, @@ -817,7 +817,7 @@ def test_singlediode_floats(sam_data): @requires_scipy def test_singlediode_floats_ivcurve(): - out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3) + out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3, method='lambertw') expected = {'i_xx': 4.2498, 'i_mp': 6.1275, 'v_oc': 8.1063, diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index c1700bbaf3..d4b92bfca9 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -24,8 +24,10 @@ def test_fast_spr_e20_327(): spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, + effective_irradiance=POA, temp_cell=TCELL, + alpha_sc=spr_e20_327.alpha_sc, a_ref=spr_e20_327.a_ref, + I_L_ref=spr_e20_327.I_L_ref, I_o_ref=spr_e20_327.I_o_ref, + R_sh_ref=spr_e20_327.R_sh_ref, R_s=spr_e20_327.R_s, EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x tstart = clock() @@ -59,8 +61,9 @@ def test_fast_spr_e20_327(): def test_fast_fs_495(): fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=fs_495.alpha_sc, module_parameters=fs_495, + effective_irradiance=POA, temp_cell=TCELL, + alpha_sc=fs_495.alpha_sc, a_ref=fs_495.a_ref, I_L_ref=fs_495.I_L_ref, + I_o_ref=fs_495.I_o_ref, R_sh_ref=fs_495.R_sh_ref, R_s=fs_495.R_s, EgRef=1.475, dEgdT=-0.0003) il, io, rs, rsh, nnsvt = x x += (101, ) @@ -95,8 +98,10 @@ def test_fast_fs_495(): def test_slow_spr_e20_327(): spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=spr_e20_327.alpha_sc, module_parameters=spr_e20_327, + effective_irradiance=POA, temp_cell=TCELL, + alpha_sc=spr_e20_327.alpha_sc, a_ref=spr_e20_327.a_ref, + I_L_ref=spr_e20_327.I_L_ref, I_o_ref=spr_e20_327.I_o_ref, + R_sh_ref=spr_e20_327.R_sh_ref, R_s=spr_e20_327.R_s, EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x tstart = clock() @@ -130,8 +135,9 @@ def test_slow_spr_e20_327(): def test_slow_fs_495(): fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( - poa_global=POA, temp_cell=TCELL, - alpha_isc=fs_495.alpha_sc, module_parameters=fs_495, + effective_irradiance=POA, temp_cell=TCELL, + alpha_sc=fs_495.alpha_sc, a_ref=fs_495.a_ref, I_L_ref=fs_495.I_L_ref, + I_o_ref=fs_495.I_o_ref, R_sh_ref=fs_495.R_sh_ref, R_s=fs_495.R_s, EgRef=1.475, dEgdT=-0.0003) il, io, rs, rsh, nnsvt = x x += (101, ) From df1423b3b650bcfc8f9e3ab235f30088710df87f Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 20 Jun 2018 01:22:52 -0700 Subject: [PATCH 42/73] DOC: update what's new for 0.6 with proposed explicit SDM solution --- docs/sphinx/source/whatsnew/v0.5.2.rst | 21 --------------------- docs/sphinx/source/whatsnew/v0.6.0.rst | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.5.2.rst b/docs/sphinx/source/whatsnew/v0.5.2.rst index 9e7de317e8..a48e97c691 100644 --- a/docs/sphinx/source/whatsnew/v0.5.2.rst +++ b/docs/sphinx/source/whatsnew/v0.5.2.rst @@ -12,24 +12,6 @@ API Changes Enhancements ~~~~~~~~~~~~ -* Implement a reliable "gold" implementation of the single diode model (SDM) - using a bisection method (Brent, 1973) bounded by points known to include the - full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using - a gradient descent method (Newton-Raphson) that is not bounded, but should be - safe for well behaved IV-curves. (:issue:`408`) -* Implement :func:`~pvlib.pvsystem.bishop88` for explicit calculation of - arbitrary IV curve points using diode voltage instead of cell voltage. This - method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` - if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will - be log spaced instead of linear. -* Implement :func:`~pvlib.pvsystem.est_voc` to estimate open circuit voltage by - assuming :math:`R_{sh} \to \infty`. This is used as an upper bound in - bisection method for :func:`~pvlib.pvsystem.singlediode`. -* Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W - with the two new implementations to form a wrapper, that takes an additional - ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new - "gold" bounded bisection method. (:issue:`410`) -* Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point. * Improve clearsky.lookup_linke_turbidity speed, changing .mat source file to .h5 (:issue:`437`) * Updated libraries for CEC module parameters to SAM release 2017.9.5 @@ -67,8 +49,5 @@ Contributors * Cliff Hansen * Will Holmgren * KonstantinTr -* Mark Mikofski - - * Anton Driesse * Cedric Leroy diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 2f205edf04..5e0dde2afa 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -13,6 +13,24 @@ Enhancements * Add sea surface albedo in irradiance.py (:issue:`458`) * Implement first_solar_spectral_loss in modelchain.py (:issue:`359`) * Clarify arguments Egref and dEgdT for calcparams_desoto (:issue:`462`) +* Implement a reliable "gold" implementation of the single diode model (SDM) + using a bisection method (Brent, 1973) bounded by points known to include the + full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using + a gradient descent method (Newton-Raphson) that is not bounded, but should be + safe for well behaved IV-curves. (:issue:`408`) +* Implement :func:`~pvlib.pvsystem.bishop88` for explicit calculation of + arbitrary IV curve points using diode voltage instead of cell voltage. This + method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` + if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will + be log spaced instead of linear. +* Implement :func:`~pvlib.pvsystem.est_voc` to estimate open circuit voltage by + assuming :math:`R_{sh} \to \infty`. This is used as an upper bound in + bisection method for :func:`~pvlib.pvsystem.singlediode`. +* Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W + with the two new implementations to form a wrapper, that takes an additional + ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new + "gold" bounded bisection method. (:issue:`410`) +* Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point. Bug fixes @@ -41,4 +59,4 @@ Contributors * Will Holmgren * Yu Cao * Cliff Hansen - +* Mark Mikofski From eb20cf479009881425f37228df345a1729214b5c Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 20 Jun 2018 11:56:29 -0700 Subject: [PATCH 43/73] DOC: update docstring to conform to numpydoc style in sdm methods --- pvlib/singlediode_methods.py | 55 +++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index bc616a5809..fdc257b1f5 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -39,11 +39,20 @@ def est_voc(photocurrent, saturation_current, nNsVth): Rough estimate of open circuit voltage useful for bounding searches for ``i`` of ``v`` when using :func:`~pvlib.pvsystem.singlediode`. - :param numeric photocurrent: photo-generated current [A] - :param numeric saturation_current: diode one reverse saturation current [A] - :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode - ideality factor ``n``, and number of series cells ``Ns`` - :returns: rough estimate of open circuit voltage [V] + Parameters + ---------- + photocurrent : numeric + photo-generated current [A] + saturation_current : numeric + diode one reverse saturation current [A] + nNsVth : numeric + product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, + and number of series cells ``Ns`` + + Returns + ------- + numeric + rough estimate of open circuit voltage [V] Calculating the open circuit voltage, :math:`V_{oc}`, of an ideal device with infinite shunt resistance, :math:`R_{sh} \\to \\infty`, and zero series @@ -71,18 +80,30 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) https://doi.org/10.1016/0379-6787(88)90059-2 - :param numeric vd: diode voltages [V] - :param numeric photocurrent: photo-generated current [A] - :param numeric saturation_current: diode one reverse saturation current [A] - :param numeric resistance_series: series resitance [ohms] - :param numeric resistance_shunt: shunt resitance [ohms] - :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode - ideality factor ``n``, and number of series cells ``Ns`` - :param bool gradients: default returns only i, v, and p, returns gradients - if true - :returns: tuple containing currents [A], voltages [V], power [W], - gradient ``di/dvd``, gradient ``dv/dvd``, gradient ``di/dv``, - gradient ``dp/dv``, and gradient ``d2p/dv/dvd`` + Parameters + ---------- + vd : numeric + diode voltages [V] + photocurrent : numeric + photo-generated current [A] + saturation_current : numeric + diode one reverse saturation current [A] + resistance_series : numeric + series resistance [ohms] + resistance_shunt: numeric + shunt resistance [ohms] + nNsVth : numeric + product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, + and number of series cells ``Ns`` + gradients : bool + default returns only i, v, and p, returns gradients if true + + Returns + ------- + tuple + containing currents [A], voltages [V], power [W], gradient ``di/dvd``, + gradient ``dv/dvd``, gradient ``di/dv``, gradient ``dp/dv``, and + gradient ``d2p/dv/dvd`` """ a = np.exp(vd / nNsVth) b = 1.0 / resistance_shunt From 5f895789f7a90f3bd08c500f0ad4827c1f17a7c2 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 26 Jun 2018 16:45:02 -0700 Subject: [PATCH 44/73] ENH: refactor est_voc -> estimate_voc * in what's new "Implement :func:`pvlib.pvsystem.estimate_voc` ..." * in api.rst * as an object from singlediode_methods in pvsystem, hmmm, probably need to refactor this too, right? * where defined and used in singlediode_methods 6 times --- docs/sphinx/source/api.rst | 2 +- docs/sphinx/source/whatsnew/v0.6.0.rst | 6 +++--- pvlib/pvsystem.py | 30 ++++++++++++++++++-------- pvlib/singlediode_methods.py | 12 +++++------ 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index c7589b0e30..8049d93549 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -207,7 +207,7 @@ Functions relevant for the single diode model. pvsystem.v_from_i pvsystem.mpp pvsystem.bishop88 - pvsystem.est_voc + pvsystem.estimate_voc SAPM model ---------- diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 2dedc90405..9bf6eb4826 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -24,9 +24,9 @@ Enhancements method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will be log spaced instead of linear. -* Implement :func:`~pvlib.pvsystem.est_voc` to estimate open circuit voltage by - assuming :math:`R_{sh} \to \infty`. This is used as an upper bound in - bisection method for :func:`~pvlib.pvsystem.singlediode`. +* Implement :func:`~pvlib.pvsystem.estimate_voc` to estimate open circuit + voltage by assuming :math:`R_{sh} \to \infty`. This is used as an upper bound + in bisection method for :func:`~pvlib.pvsystem.singlediode`. * Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W with the two new implementations to form a wrapper, that takes an additional ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bb866c6324..65f9d25a6d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -23,7 +23,7 @@ from pvlib import singlediode_methods bishop88 = singlediode_methods.bishop88 -est_voc = singlediode_methods.est_voc +est_voc = singlediode_methods.estimate_voc # not sure if this belongs in the pvsystem module. @@ -1950,14 +1950,26 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, Given the calculated DeSoto parameters, calculates the maximum power point (MPP). - :param numeric photocurrent: photo-generated current [A] - :param numeric saturation_current: diode one reverse saturation current [A] - :param numeric resistance_series: series resitance [ohms] - :param numeric resistance_shunt: shunt resitance [ohms] - :param numeric nNsVth: product of thermal voltage ``Vth`` [V], diode - :param str method: if "fast" then use Newton, otherwise use bisection - :returns: ``OrderedDict`` or ``pandas.Datafrane`` with ``i_mp``, ``v_mp``, - and ``p_mp`` + Parameters + ---------- + photocurrent : numeric + photo-generated current [A] + saturation_current : numeric + diode reverse saturation current [A] + resistance_series : numeric + series resitance [ohms] + resistance_shunt : numeric + shunt resitance [ohms] + nNsVth : numeric + product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and + number of serices cells ``Ns`` + method : str + if "fast" then use Newton, otherwise use bisection + + Returns + ------- + OrderedDict or pandas.Datafrane + ``(i_mp, v_mp, p_mp)`` """ if method.lower() == 'fast': mpp_fun = singlediode_methods.fast_mpp diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index fdc257b1f5..6db4e0b712 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -34,7 +34,7 @@ def wrapped_fcn(*args, **kwargs): return wrapper -def est_voc(photocurrent, saturation_current, nNsVth): +def estimate_voc(photocurrent, saturation_current, nNsVth): """ Rough estimate of open circuit voltage useful for bounding searches for ``i`` of ``v`` when using :func:`~pvlib.pvsystem.singlediode`. @@ -137,7 +137,7 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = brentq(lambda x, *a: v - bishop88(x, *a)[1], 0.0, voc_est, args) return bishop88(vd, *args)[0] @@ -169,7 +169,7 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = brentq(lambda x, *a: i - bishop88(x, *a)[0], 0.0, voc_est, args) return bishop88(vd, *args)[1] @@ -185,7 +185,7 @@ def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = newton(func=lambda x, *a: bishop88(x, *a)[0] - i, x0=voc_est, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args) @@ -203,7 +203,7 @@ def slow_mpp(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = brentq(lambda x, *a: bishop88(x, *a, gradients=True)[6], 0.0, voc_est, args) return bishop88(vd, *args) @@ -220,7 +220,7 @@ def fast_mpp(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc - voc_est = est_voc(photocurrent, saturation_current, nNsVth) + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = newton( func=lambda x, *a: bishop88(x, *a, gradients=True)[6], x0=voc_est, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args From ba84fcf504c8ab31d30cb23836fa70df79f0c5ba Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 26 Jun 2018 16:46:23 -0700 Subject: [PATCH 45/73] ENH refactor pvsystem.estimate_voc too, also update docs to numpy style --- pvlib/pvsystem.py | 2 +- pvlib/test/test_numerical_precision.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 65f9d25a6d..0fc2ced6e9 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -23,7 +23,7 @@ from pvlib import singlediode_methods bishop88 = singlediode_methods.bishop88 -est_voc = singlediode_methods.estimate_voc +estimate_voc = singlediode_methods.estimate_voc # not sure if this belongs in the pvsystem module. diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index ea2242a405..80ceb2c407 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -66,7 +66,7 @@ def generate_numerical_precision(): ) # generate exact values data = dict(zip((il, io, rs, rsh, nnsvt), ARGS)) - vdtest = np.linspace(0, pvsystem.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + vdtest = np.linspace(0, pvsystem.estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) expected = [] for test in vdtest: data[vd] = test @@ -90,7 +90,7 @@ def test_numerical_precision(): Test that there are no numerical errors due to floating point arithmetic. """ expected = pd.read_csv(DATA_PATH) - vdtest = np.linspace(0, pvsystem.est_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + vdtest = np.linspace(0, pvsystem.estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) results = pvsystem.bishop88(vdtest, *ARGS, gradients=True) assert np.allclose(expected['i'], results[0]) assert np.allclose(expected['v'], results[1]) From 0f3893c48e4eedabc04c8802c3b751306c54a814 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 26 Jun 2018 16:56:41 -0700 Subject: [PATCH 46/73] ENH refactor vd->diode_voltage in bishop88 --- pvlib/singlediode_methods.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 6db4e0b712..ae59e66312 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -70,7 +70,7 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): return nNsVth * np.log(photocurrent / saturation_current + 1.0) -def bishop88(vd, photocurrent, saturation_current, resistance_series, +def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, gradients=False): """ Explicit calculation single-diode-model (SDM) currents and voltages using @@ -82,7 +82,7 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, Parameters ---------- - vd : numeric + diode_voltage : numeric diode voltages [V] photocurrent : numeric photo-generated current [A] @@ -105,10 +105,10 @@ def bishop88(vd, photocurrent, saturation_current, resistance_series, gradient ``dv/dvd``, gradient ``di/dv``, gradient ``dp/dv``, and gradient ``d2p/dv/dvd`` """ - a = np.exp(vd / nNsVth) + a = np.exp(diode_voltage / nNsVth) b = 1.0 / resistance_shunt - i = photocurrent - saturation_current * (a - 1.0) - vd * b - v = vd - i * resistance_series + i = photocurrent - saturation_current * (a - 1.0) - diode_voltage * b + v = diode_voltage - i * resistance_series retval = (i, v, i*v) if gradients: c = saturation_current * a / nNsVth From d6023b1abe408f450007ef56c9b2392b7ec3d740 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 01:20:38 -0700 Subject: [PATCH 47/73] ENH: TST: refactor reshaping conditions for _array_newton * test for size and shape in pvsystem not necessary for _array_newton and were redundant, so move them to singlediode_method, and only need np.vectorize for brentq for now * add get_size_and_shape which combines the redundant test conditions * separte imports for brentq and newton * use partial to set tol, maxiter, etc. * add _array_newton to tools.py --- pvlib/pvsystem.py | 139 +++++------------------------------ pvlib/singlediode_methods.py | 96 +++++++++++++++++++++++- pvlib/tools.py | 99 ++++++++++++++++++++++++- 3 files changed, 210 insertions(+), 124 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0fc2ced6e9..81232259e4 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -22,6 +22,7 @@ from pvlib import irradiance, atmosphere from pvlib import singlediode_methods +# expose single diode methods to API bishop88 = singlediode_methods.bishop88 estimate_voc = singlediode_methods.estimate_voc @@ -1914,33 +1915,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, sdm_fun = singlediode_methods.faster_way else: sdm_fun = singlediode_methods.slower_way - try: - len(photocurrent) - except TypeError: - out = sdm_fun( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts - ) - else: - vecfun = np.vectorize(sdm_fun) - out = vecfun(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts) - if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: - out = pd.DataFrame(out.tolist(), index=photocurrent.index) - else: - out_array = pd.DataFrame(out.tolist()) - out = OrderedDict() - out['i_sc'] = out_array.i_sc.values - out['v_oc'] = out_array.v_oc.values - out['i_mp'] = out_array.i_mp.values - out['v_mp'] = out_array.v_mp.values - out['p_mp'] = out_array.p_mp.values - out['i_x'] = out_array.i_x.values - out['i_xx'] = out_array.i_xx.values - if ivcurve_pnts: - out['i'] = np.vstack(out_array.i.values) - out['v'] = np.vstack(out_array.v.values) - out['p'] = np.vstack(out_array.p.values) + out = sdm_fun( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts + ) return out @@ -1975,29 +1953,14 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, mpp_fun = singlediode_methods.fast_mpp else: mpp_fun = singlediode_methods.slow_mpp - try: - len(photocurrent) - except TypeError: - i_mp, v_mp, p_mp = mpp_fun( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth - ) - out = OrderedDict() - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - else: - vecfun = np.vectorize(mpp_fun) - ivp = vecfun(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - if isinstance(photocurrent, pd.Series): - ivp = {k: v for k, v in zip(('i_mp', 'v_mp', 'p_mp'), ivp)} - out = pd.DataFrame(ivp, index=photocurrent.index) - else: - out = OrderedDict() - out['i_mp'] = ivp[0] - out['v_mp'] = ivp[1] - out['p_mp'] = ivp[2] + i_mp, v_mp, p_mp = mpp_fun( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth + ) + out = OrderedDict() + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp return out @@ -2005,7 +1968,7 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, # Author: Rob Andrews, Calama Consulting def _golden_sect_DataFrame(params, VL, VH, func): - ''' + """ Vectorized golden section search for finding MPPT from a dataframe timeseries. @@ -2035,8 +1998,8 @@ def _golden_sect_DataFrame(params, VL, VH, func): Notes ----- - This funtion will find the MAXIMUM of a function - ''' + This function will find the MAXIMUM of a function + """ df = params df['VH'] = VH @@ -2068,7 +2031,7 @@ def _golden_sect_DataFrame(params, VL, VH, func): iterations += 1 if iterations > 50: - raise Exception("EXCEPTION:iterations exeeded maximum (50)") + raise Exception("EXCEPTION:iterations exceeded maximum (50)") return func(df, 'V1'), df['V1'] @@ -2236,41 +2199,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, v_from_i_fun = singlediode_methods.slow_v_from_i # gold method # wrap it so it returns nan v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) - # find the right size and shape for returns args = (current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - size, shape = 0, None # 0 or None both mean scalar - for arg in args: - try: - this_shape = arg.shape # try to get shape - except AttributeError: - this_shape = None - try: - this_size = len(arg) # try to get the size - except TypeError: - this_size = 0 - else: - this_size = sum(this_shape) # calc size from shape - if shape is None: - shape = this_shape # set the shape if None - # update size and shape - if this_size > size: - size = this_size - if this_shape is not None: - shape = this_shape - if size <= 1: - V = v_from_i_fun(*args) - if shape is not None: - V = np.tile(V, shape) - else: - # np.vectorize handles broadcasting, raises ValueError - vecfun = np.vectorize(v_from_i_fun) - V = vecfun(*args) - if np.isnan(V).any() and size <= 1: - V = np.repeat(V, size) - if shape is not None: - V = V.reshape(shape) - return V + return v_from_i_fun(*args) def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, @@ -2404,41 +2335,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, i_from_v_fun = singlediode_methods.slow_i_from_v # gold method # wrap it so it returns nan i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) - # find the right size and shape for returns args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - size, shape = 0, None # 0 or None both mean scalar - for arg in args: - try: - this_shape = arg.shape # try to get shape - except AttributeError: - this_shape = None - try: - this_size = len(arg) # try to get the size - except TypeError: - this_size = 0 - else: - this_size = sum(this_shape) # calc size from shape - if shape is None: - shape = this_shape # set the shape if None - # update size and shape - if this_size > size: - size = this_size - if this_shape is not None: - shape = this_shape - if size <= 1: - I = i_from_v_fun(*args) - if shape is not None: - I = np.tile(I, shape) - else: - # np.vectorize handles broadcasting, raises ValueError - vecfun = np.vectorize(i_from_v_fun) - I = vecfun(*args) - if np.isnan(I).any() and size <= 1: - I = np.repeat(I, size) - if shape is not None: - I = I.reshape(shape) - return I + return i_from_v_fun(*args) def snlinverter(v_dc, p_dc, inverter): diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index ae59e66312..c5a3175bd7 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -4,13 +4,21 @@ """ from collections import OrderedDict -from functools import wraps +from functools import wraps, partial import numpy as np +import pandas as pd try: - from scipy.optimize import brentq, newton + from scipy.optimize import brentq except ImportError: brentq = NotImplemented - newton = NotImplemented +# FIXME: change this to newton when scipy-1.2 is released +try: + from scipy.optimize import brentq, _array_newton as newton +except ImportError: + from pvlib import tools + from pvlib.tools import _array_newton + newton = partial(_array_newton, tol=1e-5, maxiter=50, fprime2=None, + converged=False) # TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default @@ -136,6 +144,13 @@ def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) + # find the right size and shape for returns + size, shape = get_size_and_shape((v,) + args) + # recursion + if size > 1: + # np.vectorize handles broadcasting, raises ValueError + vecfun = np.vectorize(slow_i_from_v) + return vecfun(v, *args) # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = brentq(lambda x, *a: v - bishop88(x, *a)[1], 0.0, voc_est, args) @@ -168,6 +183,13 @@ def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) + # find the right size and shape for returns + size, shape = get_size_and_shape((i,) + args) + # recursion + if size > 1: + # np.vectorize handles broadcasting, raises ValueError + vecfun = np.vectorize(slow_v_from_i) + return vecfun(i, *args) # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) vd = brentq(lambda x, *a: i - bishop88(x, *a)[0], 0.0, voc_est, args) @@ -197,6 +219,24 @@ def slow_mpp(photocurrent, saturation_current, resistance_series, """ This is a slow but reliable way to find mpp. """ + # recursion + try: + len(photocurrent) + except TypeError: + pass + else: + vecfun = np.vectorize(slow_mpp) + ivp = vecfun(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + if isinstance(photocurrent, pd.Series): + ivp = {k: v for k, v in zip(('i_mp', 'v_mp', 'p_mp'), ivp)} + out = pd.DataFrame(ivp, index=photocurrent.index) + else: + out = OrderedDict() + out['i_mp'] = ivp[0] + out['v_mp'] = ivp[1] + out['p_mp'] = ivp[2] + return out if brentq is NotImplemented: raise ImportError('This function requires scipy') # collect args @@ -233,6 +273,32 @@ def slower_way(photocurrent, saturation_current, resistance_series, """ This is the slow but reliable way. """ + # recursion + try: + len(photocurrent) + except TypeError: + pass + else: + vecfun = np.vectorize(slower_way) + out = vecfun(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts) + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out.tolist(), index=photocurrent.index) + else: + out_array = pd.DataFrame(out.tolist()) + out = OrderedDict() + out['i_sc'] = out_array.i_sc.values + out['v_oc'] = out_array.v_oc.values + out['i_mp'] = out_array.i_mp.values + out['v_mp'] = out_array.v_mp.values + out['p_mp'] = out_array.p_mp.values + out['i_x'] = out_array.i_x.values + out['i_xx'] = out_array.i_xx.values + if ivcurve_pnts: + out['i'] = np.vstack(out_array.i.values) + out['v'] = np.vstack(out_array.v.values) + out['p'] = np.vstack(out_array.p.values) + return out # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) @@ -283,3 +349,27 @@ def faster_way(photocurrent, saturation_current, resistance_series, out['v'] = v out['p'] = p return out + + +def get_size_and_shape(args): + # find the right size and shape for returns + size, shape = 0, None # 0 or None both mean scalar + for arg in args: + try: + this_shape = arg.shape # try to get shape + except AttributeError: + this_shape = None + try: + this_size = len(arg) # try to get the size + except TypeError: + this_size = 0 + else: + this_size = arg.size # if it has shape then it also has size + if shape is None: + shape = this_shape # set the shape if None + # update size and shape + if this_size > size: + size = this_size + if this_shape is not None: + shape = this_shape + return size, shape diff --git a/pvlib/tools.py b/pvlib/tools.py index a722500f2a..5fc790e32f 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -2,8 +2,9 @@ Collection of functions used in pvlib_python """ +from collections import namedtuple import datetime as dt - +import warnings import numpy as np import pandas as pd import pytz @@ -251,3 +252,99 @@ def _build_kwargs(keys, input_dict): pass return kwargs + + +def _array_newton(func, x0, fprime, args, tol, maxiter, fprime2, + converged=False): + """ + A vectorized version of Newton, Halley, and secant methods for arrays. Do + not use this method directly. This method is called from :func:`newton` + when ``np.isscalar(x0)`` is true. For docstring, see :func:`newton`. + """ + try: + p = np.asarray(x0, dtype=float) + except TypeError: # can't convert complex to float + p = np.asarray(x0) + failures = np.ones_like(p, dtype=bool) # at start, nothing converged + nz_der = np.copy(failures) + if fprime is not None: + # Newton-Raphson method + for iteration in range(maxiter): + # first evaluate fval + fval = np.asarray(func(p, *args)) + # If all fval are 0, all roots have been found, then terminate + if not fval.any(): + failures = fval.astype(bool) + break + fder = np.asarray(fprime(p, *args)) + nz_der = (fder != 0) + # stop iterating if all derivatives are zero + if not nz_der.any(): + break + # Newton step + dp = fval[nz_der] / fder[nz_der] + if fprime2 is not None: + fder2 = np.asarray(fprime2(p, *args)) + dp = dp / (1.0 - 0.5 * dp * fder2[nz_der] / fder[nz_der]) + # only update nonzero derivatives + p[nz_der] -= dp + failures[nz_der] = np.abs(dp) >= tol # items not yet converged + # stop iterating if there aren't any failures, not incl zero der + if not failures[nz_der].any(): + break + else: + # Secant method + dx = np.finfo(float).eps**0.33 + p1 = p * (1 + dx) + np.where(p >= 0, dx, -dx) + q0 = np.asarray(func(p, *args)) + q1 = np.asarray(func(p1, *args)) + active = np.ones_like(p, dtype=bool) + for iteration in range(maxiter): + nz_der = (q1 != q0) + # stop iterating if all derivatives are zero + if not nz_der.any(): + p = (p1 + p) / 2.0 + break + # Secant Step + dp = (q1 * (p1 - p))[nz_der] / (q1 - q0)[nz_der] + # only update nonzero derivatives + p[nz_der] = p1[nz_der] - dp + active_zero_der = ~nz_der & active + p[active_zero_der] = (p1 + p)[active_zero_der] / 2.0 + active &= nz_der # don't assign zero derivatives again + failures[nz_der] = np.abs(dp) >= tol # not yet converged + # stop iterating if there aren't any failures, not incl zero der + if not failures[nz_der].any(): + break + p1, p = p, p1 + q0 = q1 + q1 = np.asarray(func(p1, *args)) + zero_der = ~nz_der & failures # don't include converged with zero-ders + if zero_der.any(): + # secant warnings + if fprime is None: + nonzero_dp = (p1 != p) + # non-zero dp, but infinite newton step + zero_der_nz_dp = (zero_der & nonzero_dp) + if zero_der_nz_dp.any(): + rms = np.sqrt( + sum((p1[zero_der_nz_dp] - p[zero_der_nz_dp]) ** 2) + ) + warnings.warn('RMS of {:g} reached'.format(rms), RuntimeWarning) + # newton or halley warnings + else: + all_or_some = 'all' if zero_der.all() else 'some' + msg = '{:s} derivatives were zero'.format(all_or_some) + warnings.warn(msg, RuntimeWarning) + elif failures.any(): + all_or_some = 'all' if failures.all() else 'some' + msg = '{0:s} failed to converge after {1:d} iterations'.format( + all_or_some, maxiter + ) + if failures.all(): + raise RuntimeError(msg) + warnings.warn(msg, RuntimeWarning) + if converged: + result = namedtuple('result', ('root', 'converged', 'zero_der')) + p = result(p, ~failures, zero_der) + return p From 80b33527eaeee297d9799be9987e05ede445309e Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 10:15:57 -0700 Subject: [PATCH 48/73] ENH: TST: make brentq vectorized always * replace and combine two *_x_from_y with a single function to do both bishop88_x_from_y(..., method='newton') * move reshaping back to pvsystem * change _get_size_and_shape to private method * add numpy style docstrings to bishop_x_from_y() functions * use current instead of "i" and voltage instead of "v" for args in bishop_x_from_y() --- pvlib/pvsystem.py | 10 ++- pvlib/singlediode_methods.py | 155 ++++++++++++++++++++++------------- 2 files changed, 104 insertions(+), 61 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 81232259e4..e6594695ce 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2201,7 +2201,10 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) args = (current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - return v_from_i_fun(*args) + V = v_from_i_fun(*args) + # find the right size and shape for returns + size, shape = singlediode_methods._get_size_and_shape(args) + return V def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, @@ -2337,7 +2340,10 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - return i_from_v_fun(*args) + I = i_from_v_fun(*args) + # find the right size and shape for returns + size, shape = singlediode_methods._get_size_and_shape(args) + return I def snlinverter(v_dc, p_dc, inverter): diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index c5a3175bd7..fbba2878ec 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -11,6 +11,9 @@ from scipy.optimize import brentq except ImportError: brentq = NotImplemented +else: + # np.vectorize handles broadcasting, raises ValueError + brentq = np.vectorize(brentq) # FIXME: change this to newton when scipy-1.2 is released try: from scipy.optimize import brentq, _array_newton as newton @@ -134,83 +137,113 @@ def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, return retval -def slow_i_from_v(v, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def bishop88_i_from_v(voltage, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth, + method='newton'): """ - This is a slow but reliable way to find current given any voltage. - """ - if brentq is NotImplemented: - raise ImportError('This function requires scipy') - # collect args - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - # find the right size and shape for returns - size, shape = get_size_and_shape((v,) + args) - # recursion - if size > 1: - # np.vectorize handles broadcasting, raises ValueError - vecfun = np.vectorize(slow_i_from_v) - return vecfun(v, *args) - # first bound the search using voc - voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = brentq(lambda x, *a: v - bishop88(x, *a)[1], 0.0, voc_est, args) - return bishop88(vd, *args)[0] + Find current given any voltage. + Parameters + ---------- + voltage : numeric + voltage (V) in volts [V] + photocurrent : numeric + photogenerated current (Iph or IL) in amperes [A] + saturation_current : numeric + diode dark or saturation current (Io or Isat) in amperes [A] + resistance_series : numeric + series resistance (Rs) in ohms + resistance_shunt : numeric + shunt resistance (Rsh) in ohms + nNsVth : numeric + product of diode ideality factor (n), number of series cells (Ns), and + thermal voltage (Vth = k_b * T / q_e) in volts [V] + method : str + one of two ptional search methods: either `brentq`, a reliable and + bounded method or `newton` the default, a gradient descent method. -def fast_i_from_v(v, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): - """ - This is a possibly faster way to find current given any voltage. + Returns + ------- + current : numeric + current (I) at the specified voltage (V) in amperes [A] """ - if newton is NotImplemented: - raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - vd = newton(func=lambda x, *a: bishop88(x, *a)[1] - v, x0=v, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], - args=args) + + def fv(x, *a): + # calculate voltage + return bishop88(x, *a)[1] - voltage + + if method.lower() == 'brentq': + if brentq is NotImplemented: + raise ImportError('This function requires scipy') + # first bound the search using voc + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) + vd = brentq(fv, 0.0, voc_est, args) + elif method.lower() == 'newton': + if newton is NotImplemented: + raise ImportError('This function requires scipy') + vd = newton(func=fv, x0=voltage, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], + args=args) + else: + raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args)[0] -def slow_v_from_i(i, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def bishop88_v_from_i(current, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth, + method='newton'): """ - This is a slow but reliable way to find voltage given any current. - """ - if brentq is NotImplemented: - raise ImportError('This function requires scipy') - # collect args - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - # find the right size and shape for returns - size, shape = get_size_and_shape((i,) + args) - # recursion - if size > 1: - # np.vectorize handles broadcasting, raises ValueError - vecfun = np.vectorize(slow_v_from_i) - return vecfun(i, *args) - # first bound the search using voc - voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = brentq(lambda x, *a: i - bishop88(x, *a)[0], 0.0, voc_est, args) - return bishop88(vd, *args)[1] + Find voltage given any current. + Parameters + ---------- + current : numeric + current (I) in amperes [A] + photocurrent : numeric + photogenerated current (Iph or IL) in amperes [A] + saturation_current : numeric + diode dark or saturation current (Io or Isat) in amperes [A] + resistance_series : numeric + series resistance (Rs) in ohms + resistance_shunt : numeric + shunt resistance (Rsh) in ohms + nNsVth : numeric + product of diode ideality factor (n), number of series cells (Ns), and + thermal voltage (Vth = k_b * T / q_e) in volts [V] + method : str + one of two ptional search methods: either `brentq`, a reliable and + bounded method or `newton` the default, a gradient descent method. -def fast_v_from_i(i, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): - """ - This is a possibly faster way to find voltage given any current. + Returns + ------- + voltage : numeric + voltage (V) at the specified current (I) in volts [V] """ - if newton is NotImplemented: - raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = newton(func=lambda x, *a: bishop88(x, *a)[0] - i, x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], - args=args) + + def fi(x, *a): + # calculate current + return bishop88(x, *a)[0] - current + + if method.lower() == 'brentq': + if brentq is NotImplemented: + raise ImportError('This function requires scipy') + vd = brentq(fi, 0.0, voc_est, args) + elif method.lower() == 'newton': + if newton is NotImplemented: + raise ImportError('This function requires scipy') + vd = newton(func=fi, x0=voc_est, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], + args=args) + else: + raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args)[1] @@ -273,6 +306,8 @@ def slower_way(photocurrent, saturation_current, resistance_series, """ This is the slow but reliable way. """ + slow_v_from_i = partial(bishop88_v_from_i, method='brentq') + slow_i_from_v = partial(bishop88_i_from_v, method='brentq') # recursion try: len(photocurrent) @@ -327,6 +362,8 @@ def slower_way(photocurrent, saturation_current, resistance_series, def faster_way(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None): """a faster way""" + fast_v_from_i = partial(bishop88_v_from_i, method='newton') + fast_i_from_v = partial(bishop88_i_from_v, method='newton') args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args v_oc = fast_v_from_i(0.0, *args) @@ -351,7 +388,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, return out -def get_size_and_shape(args): +def _get_size_and_shape(args): # find the right size and shape for returns size, shape = 0, None # 0 or None both mean scalar for arg in args: From ef206766db5ac5f5b89b9bd34f2560709c719336 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 10:51:48 -0700 Subject: [PATCH 49/73] ENH: TST: can't vectorize brentq because it treats args as array * instead of each element of args as an array, ie: it loops over each item in args but objective requires _all_ of the items, we only want it to loop if each or any of the items in args are an array * fix don't import brentq twice, and don't import _array_newton as newton so it will keep working even after scipy-1.2.0 is released * need to vectorize brentq in each bishop88_x_from_y() by breaking the args out individually like they were originally --- pvlib/singlediode_methods.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index fbba2878ec..448633ca0f 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -11,17 +11,14 @@ from scipy.optimize import brentq except ImportError: brentq = NotImplemented -else: - # np.vectorize handles broadcasting, raises ValueError - brentq = np.vectorize(brentq) # FIXME: change this to newton when scipy-1.2 is released try: - from scipy.optimize import brentq, _array_newton as newton + from scipy.optimize import _array_newton except ImportError: from pvlib import tools from pvlib.tools import _array_newton - newton = partial(_array_newton, tol=1e-5, maxiter=50, fprime2=None, - converged=False) +# rename newton and set keyword arguments +newton = partial(_array_newton, tol=1e-5, maxiter=50, fprime2=None) # TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default @@ -180,10 +177,14 @@ def fv(x, *a): raise ImportError('This function requires scipy') # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = brentq(fv, 0.0, voc_est, args) + # numpy.vectorize handles broadcasting, raises ValueError + vec_fun = np.vectorize( + lambda iph, isat, rs, rsh, gamma: brentq( + fv, 0.0, voc_est, args=(iph, isat, rs, rsh, gamma) + ) + ) + vd = vec_fun(*args) elif method.lower() == 'newton': - if newton is NotImplemented: - raise ImportError('This function requires scipy') vd = newton(func=fv, x0=voltage, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args) @@ -235,10 +236,14 @@ def fi(x, *a): if method.lower() == 'brentq': if brentq is NotImplemented: raise ImportError('This function requires scipy') - vd = brentq(fi, 0.0, voc_est, args) + # numpy.vectorize handles broadcasting, raises ValueError + vec_fun = np.vectorize( + lambda iph, isat, rs, rsh, gamma: brentq( + fi, 0.0, voc_est, args=(iph, isat, rs, rsh, gamma) + ) + ) + vd = vec_fun(*args) elif method.lower() == 'newton': - if newton is NotImplemented: - raise ImportError('This function requires scipy') vd = newton(func=fi, x0=voc_est, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args) From 98d1c0153e140af4ab7dd0fc9aaf7fd854f76f60 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 13:09:02 -0700 Subject: [PATCH 50/73] BUG: TST: use "brentq" instead of "gold" * use partial to set singlediode search method to brentq, newton, ... * add shape, size reshape logic back to pvsystem * use bishop88_x_from_y methods that combine all search methods * fix vectorized brentq functions to include i or v, and voc_est, and pass i or v to the objective, don't let it be set by locals, ditto for voc_est * fix newton need to set initial guess shape, and if using broadcast_to, then copy to get a new instance, or else can't update the guess --- pvlib/pvsystem.py | 35 +++++++++++++++++++++++------------ pvlib/singlediode_methods.py | 34 +++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e6594695ce..edb46c9766 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -6,6 +6,7 @@ from __future__ import division from collections import OrderedDict +from functools import partial import os import io try: @@ -2048,7 +2049,7 @@ def _pwr_optfcn(df, loc): def v_from_i(resistance_shunt, resistance_series, nNsVth, current, - saturation_current, photocurrent, method='gold'): + saturation_current, photocurrent, method='brentq'): ''' Device voltage at the given device current for the single diode model. @@ -2192,11 +2193,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, else: return V else: - # use way_faster methods - if method.lower() == 'fast': - v_from_i_fun = singlediode_methods.fast_v_from_i # fast method - else: - v_from_i_fun = singlediode_methods.slow_v_from_i # gold method + # use singlediode methods + v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, + method=method.lower()) # wrap it so it returns nan v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) args = (current, photocurrent, saturation_current, @@ -2204,11 +2203,18 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, V = v_from_i_fun(*args) # find the right size and shape for returns size, shape = singlediode_methods._get_size_and_shape(args) + if size <= 1: + if shape is not None: + V = np.tile(V, shape) + if np.isnan(V).any() and size <= 1: + V = np.repeat(V, size) + if shape is not None: + V = V.reshape(shape) return V def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, - saturation_current, photocurrent, method='gold'): + saturation_current, photocurrent, method='brentq'): ''' Device current at the given device voltage for the single diode model. @@ -2331,11 +2337,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, else: return I else: - # use way_faster methods - if method.lower() == 'fast': - i_from_v_fun = singlediode_methods.fast_i_from_v # fast method - else: - i_from_v_fun = singlediode_methods.slow_i_from_v # gold method + # use singlediode methods + i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, + method=method.lower()) # wrap it so it returns nan i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) args = (voltage, photocurrent, saturation_current, resistance_series, @@ -2343,6 +2347,13 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, I = i_from_v_fun(*args) # find the right size and shape for returns size, shape = singlediode_methods._get_size_and_shape(args) + if size <= 1: + if shape is not None: + I = np.tile(I, shape) + if np.isnan(I).any() and size <= 1: + I = np.repeat(I, size) + if shape is not None: + I = I.reshape(shape) return I diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 448633ca0f..d5ce127553 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -18,7 +18,7 @@ from pvlib import tools from pvlib.tools import _array_newton # rename newton and set keyword arguments -newton = partial(_array_newton, tol=1e-5, maxiter=50, fprime2=None) +newton = partial(_array_newton, tol=1e-6, maxiter=100, fprime2=None) # TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default @@ -168,23 +168,25 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - def fv(x, *a): + def fv(x, v, *a): # calculate voltage - return bishop88(x, *a)[1] - voltage + return bishop88(x, *a)[1] - v if method.lower() == 'brentq': if brentq is NotImplemented: raise ImportError('This function requires scipy') # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - # numpy.vectorize handles broadcasting, raises ValueError + # break out arguments for numpy.vectorize to handle broadcasting vec_fun = np.vectorize( - lambda iph, isat, rs, rsh, gamma: brentq( - fv, 0.0, voc_est, args=(iph, isat, rs, rsh, gamma) - ) + lambda voc, v, iph, isat, rs, rsh, gamma: + brentq(fv, 0.0, voc, args=(v, iph, isat, rs, rsh, gamma)) ) - vd = vec_fun(*args) + vd = vec_fun(voc_est, voltage, *args) elif method.lower() == 'newton': + size, shape = _get_size_and_shape((voltage,) + args) + if shape is not None: + voltage = np.broadcast_to(voltage, shape).copy() vd = newton(func=fv, x0=voltage, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args) @@ -229,21 +231,23 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - def fi(x, *a): + def fi(x, i, *a): # calculate current - return bishop88(x, *a)[0] - current + return bishop88(x, *a)[0] - i if method.lower() == 'brentq': if brentq is NotImplemented: raise ImportError('This function requires scipy') - # numpy.vectorize handles broadcasting, raises ValueError + # break out arguments for numpy.vectorize to handle broadcasting vec_fun = np.vectorize( - lambda iph, isat, rs, rsh, gamma: brentq( - fi, 0.0, voc_est, args=(iph, isat, rs, rsh, gamma) - ) + lambda voc, i, iph, isat, rs, rsh, gamma: + brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma)) ) - vd = vec_fun(*args) + vd = vec_fun( voc_est, current,*args) elif method.lower() == 'newton': + size, shape = _get_size_and_shape((current,) + args) + if shape is not None: + voc_est = np.broadcast_to(voc_est, shape).copy() vd = newton(func=fi, x0=voc_est, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args) From 4ecd9134f891b377ecf8b29fd50a96f1429f549e Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 16:41:12 -0700 Subject: [PATCH 51/73] TST: change test fixtures with nonsensical values in quadrant four * several test_i_from_v_from_i and test_i_from_v test fixtures had voltages of -299.75 which would never be used in any test, and can only be solved using the lambertw maybe or the newton, but will always fail the brentq because it is bounded in quadrant 1 from zero to voc. * add tests for brentq and newton for test_v_from_i and test_i_from_v * also specify method in the test for graceful failure of mismatched arrays (ValueError: can't broadcast) to test newton and brentq explicitly * set default in pvsystem to 'lambertw' - let's take it slow * fix issue with newton i_from_v, cannot use voltage for initial guess and have it as part of the objective function because they have the same references, so must make a copy of it, call it v0 * also if max size > 1 then make sure all args are numpy arrays so that broadcasting errors are gracefully raised --- pvlib/pvsystem.py | 8 +-- pvlib/singlediode_methods.py | 32 ++++++++-- pvlib/test/test_pvsystem.py | 120 ++++++++++++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 21 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index edb46c9766..815077cc9c 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2049,7 +2049,7 @@ def _pwr_optfcn(df, loc): def v_from_i(resistance_shunt, resistance_series, nNsVth, current, - saturation_current, photocurrent, method='brentq'): + saturation_current, photocurrent, method='lambertw'): ''' Device voltage at the given device current for the single diode model. @@ -2197,7 +2197,7 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, method=method.lower()) # wrap it so it returns nan - v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) + # v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) args = (current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) V = v_from_i_fun(*args) @@ -2214,7 +2214,7 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, - saturation_current, photocurrent, method='brentq'): + saturation_current, photocurrent, method='lambertw'): ''' Device current at the given device voltage for the single diode model. @@ -2341,7 +2341,7 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, method=method.lower()) # wrap it so it returns nan - i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) + # i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) I = i_from_v_fun(*args) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index d5ce127553..ef9525a312 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -169,7 +169,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, resistance_shunt, nNsVth) def fv(x, v, *a): - # calculate voltage + # calculate voltage residual given diode voltage "x" return bishop88(x, *a)[1] - v if method.lower() == 'brentq': @@ -184,10 +184,19 @@ def fv(x, v, *a): ) vd = vec_fun(voc_est, voltage, *args) elif method.lower() == 'newton': + # make sure all args are numpy arrays if max size > 1 size, shape = _get_size_and_shape((voltage,) + args) + if size > 1: + args = [np.asarray(arg) for arg in args] + # newton uses initial guess for the output shape + # copy v0 to a new array and broadcast it to the shape of max size if shape is not None: - voltage = np.broadcast_to(voltage, shape).copy() - vd = newton(func=fv, x0=voltage, + v0 = np.broadcast_to(voltage, shape).copy() + else: + v0 = voltage + # x0 and x in func are the same reference! DO NOT set x0 to voltage! + # voltage in fv(x, voltage, *a) MUST BE CONSTANT! + vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args) else: @@ -232,7 +241,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) def fi(x, i, *a): - # calculate current + # calculate current residual given diode voltage "x" return bishop88(x, *a)[0] - i if method.lower() == 'brentq': @@ -243,12 +252,21 @@ def fi(x, i, *a): lambda voc, i, iph, isat, rs, rsh, gamma: brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma)) ) - vd = vec_fun( voc_est, current,*args) + vd = vec_fun(voc_est, current,*args) elif method.lower() == 'newton': + # make sure all args are numpy arrays if max size > 1 size, shape = _get_size_and_shape((current,) + args) + if size > 1: + args = [np.asarray(arg) for arg in args] + # newton uses initial guess for the output shape + # copy v0 to a new array and broadcast it to the shape of max size if shape is not None: - voc_est = np.broadcast_to(voc_est, shape).copy() - vd = newton(func=fi, x0=voc_est, + v0 = np.broadcast_to(voc_est, shape).copy() + else: + v0 = voc_est + # x0 and x in func are the same reference! DO NOT set x0 to current! + # voltage in fi(x, current, *a) MUST BE CONSTANT! + vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args) else: diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 424255d555..e5328c0121 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -553,6 +553,50 @@ def test_v_from_i(fixture_v_from_i): assert_allclose(V, V_expected, atol=atol) +@requires_scipy +def test_v_from_i_brentq(fixture_v_from_i): + # Solution set loaded from fixture + Rsh = fixture_v_from_i['Rsh'] + Rs = fixture_v_from_i['Rs'] + nNsVth = fixture_v_from_i['nNsVth'] + I = fixture_v_from_i['I'] + I0 = fixture_v_from_i['I0'] + IL = fixture_v_from_i['IL'] + V_expected = fixture_v_from_i['V_expected'] + + # Convergence criteria + atol = 1.e-11 + + V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL, method='brentq') + assert(isinstance(V, type(V_expected))) + if isinstance(V, type(np.ndarray)): + assert(isinstance(V.dtype, type(V_expected.dtype))) + assert(V.shape == V_expected.shape) + assert_allclose(V, V_expected, atol=atol) + + +@requires_scipy +def test_v_from_i_newton(fixture_v_from_i): + # Solution set loaded from fixture + Rsh = fixture_v_from_i['Rsh'] + Rs = fixture_v_from_i['Rs'] + nNsVth = fixture_v_from_i['nNsVth'] + I = fixture_v_from_i['I'] + I0 = fixture_v_from_i['I0'] + IL = fixture_v_from_i['IL'] + V_expected = fixture_v_from_i['V_expected'] + + # Convergence criteria + atol = 1.e-8 + + V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL, method='newton') + assert(isinstance(V, type(V_expected))) + if isinstance(V, type(np.ndarray)): + assert(isinstance(V.dtype, type(V_expected.dtype))) + assert(V.shape == V_expected.shape) + assert_allclose(V, V_expected, atol=atol) + + @requires_scipy def test_i_from_v_from_i(fixture_v_from_i): # Solution set loaded from fixture @@ -583,38 +627,38 @@ def test_i_from_v_from_i(fixture_v_from_i): 'Rsh': 20., 'Rs': 0.1, 'nNsVth': 0.5, - 'V': 40., + 'V': 7.5049875193450521, 'I0': 6.e-7, 'IL': 7., - 'I_expected': -299.746389916 + 'I_expected': 3. }, { # Can handle all rank-0 array inputs 'Rsh': np.array(20.), 'Rs': np.array(0.1), 'nNsVth': np.array(0.5), - 'V': np.array(40.), + 'V': np.array(7.5049875193450521), 'I0': np.array(6.e-7), 'IL': np.array(7.), - 'I_expected': np.array(-299.746389916) + 'I_expected': np.array(3.) }, { # Can handle all rank-1 singleton array inputs 'Rsh': np.array([20.]), 'Rs': np.array([0.1]), 'nNsVth': np.array([0.5]), - 'V': np.array([40.]), + 'V': np.array([7.5049875193450521]), 'I0': np.array([6.e-7]), 'IL': np.array([7.]), - 'I_expected': np.array([-299.746389916]) + 'I_expected': np.array([3.]) }, { # Can handle all rank-1 non-singleton array inputs with a zero # series resistance, Rs=0 gives I=IL=Isc at V=0 'Rsh': np.array([20., 20.]), 'Rs': np.array([0., 0.1]), 'nNsVth': np.array([0.5, 0.5]), - 'V': np.array([0., 40.]), + 'V': np.array([0., 7.5049875193450521]), 'I0': np.array([6.e-7, 6.e-7]), 'IL': np.array([7., 7.]), - 'I_expected': np.array([7., -299.746389916]) + 'I_expected': np.array([7., 3.]) }, { # Can handle mixed inputs with a rank-2 array with zero series # resistance, Rs=0 gives I=IL=Isc at V=0 @@ -642,10 +686,10 @@ def test_i_from_v_from_i(fixture_v_from_i): 'Rsh': np.inf, 'Rs': 0.1, 'nNsVth': 0.5, - 'V': 40., + 'V': 7.5049875193450521, 'I0': 6.e-7, 'IL': 7., - 'I_expected': -299.7383436645412 + 'I_expected': 3.2244873645510923 }]) def fixture_i_from_v(request): return request.param @@ -673,6 +717,50 @@ def test_i_from_v(fixture_i_from_v): assert_allclose(I, I_expected, atol=atol) +@requires_scipy +def test_i_from_v_brentq(fixture_i_from_v): + # Solution set loaded from fixture + Rsh = fixture_i_from_v['Rsh'] + Rs = fixture_i_from_v['Rs'] + nNsVth = fixture_i_from_v['nNsVth'] + V = fixture_i_from_v['V'] + I0 = fixture_i_from_v['I0'] + IL = fixture_i_from_v['IL'] + I_expected = fixture_i_from_v['I_expected'] + + # Convergence criteria + atol = 1.e-11 + + I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='brentq') + assert(isinstance(I, type(I_expected))) + if isinstance(I, type(np.ndarray)): + assert(isinstance(I.dtype, type(I_expected.dtype))) + assert(I.shape == I_expected.shape) + assert_allclose(I, I_expected, atol=atol) + + +@requires_scipy +def test_i_from_v_newton(fixture_i_from_v): + # Solution set loaded from fixture + Rsh = fixture_i_from_v['Rsh'] + Rs = fixture_i_from_v['Rs'] + nNsVth = fixture_i_from_v['nNsVth'] + V = fixture_i_from_v['V'] + I0 = fixture_i_from_v['I0'] + IL = fixture_i_from_v['IL'] + I_expected = fixture_i_from_v['I_expected'] + + # Convergence criteria + atol = 1.e-11 + + I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='newton') + assert(isinstance(I, type(I_expected))) + if isinstance(I, type(np.ndarray)): + assert(isinstance(I.dtype, type(I_expected.dtype))) + assert(I.shape == I_expected.shape) + assert_allclose(I, I_expected, atol=atol) + + @requires_scipy def test_PVSystem_i_from_v(mocker): system = pvsystem.PVSystem() @@ -686,12 +774,24 @@ def test_PVSystem_i_from_v(mocker): def test_i_from_v_size(): with pytest.raises(ValueError): pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0) + with pytest.raises(ValueError): + pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0, + method='brentq') + with pytest.raises(ValueError): + pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0, + method='newton') @requires_scipy def test_v_from_i_size(): with pytest.raises(ValueError): pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0) + with pytest.raises(ValueError): + pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0, + method='brentq') + with pytest.raises(ValueError): + pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0, + method='newton') @requires_scipy From bf05d2d5bdc785bb038e5b8d9ecea9288be0c983 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 17:29:37 -0700 Subject: [PATCH 52/73] TST: fix mpp tests * in floats, array, and series change default to 'brentq', and 'fast' to 'newton' * change IL list [7, 7] to an array, easy way out for now * add missing 2nd test comparing newton in test_mpp_series * fix randomly wierd indent * in pvsystem.singlediode() change default method to 'lambertw', baby steps for now, and update documentation change 'fast'->'newton' and 'gold'->'brentq' * in pvsystem.mpp change default method from 'gold'->'brentq' * use partial and make mpp_fun from bishop88_mpp and method.lower * revert changes that moved making dataframe to singlediode_methods, and wasn't working, move it back to pvsystem, and create a datafram if the photocurrent is a series * combine *_mpp() functions together to make bishop88_mpp(..., method), defaults to 'newton', and add docstring with numpy style * remove "recursive" call for vectorization, and use same pattern from other bishop88_*() methods for both brent and newton * add a stopgap *_mpp shortcut for slowway() and fastway() methods * TODO: fix slowway and fastway, and use np.asarray in voc_est --- pvlib/pvsystem.py | 27 +++++----- pvlib/singlediode_methods.py | 102 ++++++++++++++++++++--------------- pvlib/test/test_pvsystem.py | 36 +++++++------ 3 files changed, 92 insertions(+), 73 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 815077cc9c..17671d6fdf 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1700,7 +1700,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None, method='gold'): + resistance_shunt, nNsVth, ivcurve_pnts=None, method='lambertw'): r''' Solve the single-diode model to obtain a photovoltaic IV curve. @@ -1756,9 +1756,9 @@ def singlediode(photocurrent, saturation_current, resistance_series, Number of points in the desired IV curve. If None or 0, no IV curves will be produced. - method : str, default 'gold' + method : str, default 'lambertw' Determines the method used to calculate IV curve and points. If - 'lambertw' then ``lambertw`` is used. If 'fast' then ``newton`` is + 'lambertw' then ``lambertw`` is used. If 'newton' then ``newton`` is used. Otherwise the problem is bounded between zero and open-circuit voltage and a bisection method, ``brentq``, is used, that guarantees convergence. @@ -1912,7 +1912,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, out = pd.DataFrame(out, index=photocurrent.index) else: - if method.lower() == 'fast': + if method.lower() == 'newton': sdm_fun = singlediode_methods.faster_way else: sdm_fun = singlediode_methods.slower_way @@ -1924,7 +1924,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, - nNsVth, method='gold'): + nNsVth, method='brentq'): """ Given the calculated DeSoto parameters, calculates the maximum power point (MPP). @@ -1950,18 +1950,19 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, OrderedDict or pandas.Datafrane ``(i_mp, v_mp, p_mp)`` """ - if method.lower() == 'fast': - mpp_fun = singlediode_methods.fast_mpp - else: - mpp_fun = singlediode_methods.slow_mpp + mpp_fun = partial(singlediode_methods.bishop88_mpp, method=method.lower()) i_mp, v_mp, p_mp = mpp_fun( photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth ) - out = OrderedDict() - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp + if isinstance(photocurrent, pd.Series): + ivp = {'i_mp': i_mp, 'v_mp': v_mp, 'p_mp': p_mp} + out = pd.DataFrame(ivp, index=photocurrent.index) + else: + out = OrderedDict() + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp return out diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index ef9525a312..e9a68334b5 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -252,7 +252,7 @@ def fi(x, i, *a): lambda voc, i, iph, isat, rs, rsh, gamma: brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma)) ) - vd = vec_fun(voc_est, current,*args) + vd = vec_fun(voc_est, current, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 size, shape = _get_size_and_shape((current,) + args) @@ -274,57 +274,71 @@ def fi(x, i, *a): return bishop88(vd, *args)[1] -def slow_mpp(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def bishop88_mpp(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, method='newton'): """ - This is a slow but reliable way to find mpp. - """ - # recursion - try: - len(photocurrent) - except TypeError: - pass - else: - vecfun = np.vectorize(slow_mpp) - ivp = vecfun(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - if isinstance(photocurrent, pd.Series): - ivp = {k: v for k, v in zip(('i_mp', 'v_mp', 'p_mp'), ivp)} - out = pd.DataFrame(ivp, index=photocurrent.index) - else: - out = OrderedDict() - out['i_mp'] = ivp[0] - out['v_mp'] = ivp[1] - out['p_mp'] = ivp[2] - return out - if brentq is NotImplemented: - raise ImportError('This function requires scipy') - # collect args - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - # first bound the search using voc - voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = brentq(lambda x, *a: bishop88(x, *a, gradients=True)[6], 0.0, voc_est, - args) - return bishop88(vd, *args) + Find max power point. + Parameters + ---------- + photocurrent : numeric + photogenerated current (Iph or IL) in amperes [A] + saturation_current : numeric + diode dark or saturation current (Io or Isat) in amperes [A] + resistance_series : numeric + series resistance (Rs) in ohms + resistance_shunt : numeric + shunt resistance (Rsh) in ohms + nNsVth : numeric + product of diode ideality factor (n), number of series cells (Ns), and + thermal voltage (Vth = k_b * T / q_e) in volts [V] + method : str + one of two ptional search methods: either `brentq`, a reliable and + bounded method or `newton` the default, a gradient descent method. -def fast_mpp(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): - """ - This is a possibly faster way to find mpp. + Returns + ------- + OrderedDict or pandas.DataFrame + max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and + max power ``p_mp`` [W] """ - if newton is NotImplemented: - raise ImportError('This function requires scipy') # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - vd = newton( - func=lambda x, *a: bishop88(x, *a, gradients=True)[6], x0=voc_est, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args - ) + + def fmpp(x, *a): + return bishop88(x, *a, gradients=True)[6] + + if method.lower() == 'brentq': + if brentq is NotImplemented: + raise ImportError('This function requires scipy') + # break out arguments for numpy.vectorize to handle broadcasting + vec_fun = np.vectorize( + lambda voc, iph, isat, rs, rsh, gamma: + brentq(fmpp, 0.0, voc, args=(iph, isat, rs, rsh, gamma)) + ) + vd = vec_fun(voc_est, *args) + elif method.lower() == 'newton': + # make sure all args are numpy arrays if max size > 1 + size, shape = _get_size_and_shape(args) + if size > 1: + args = [np.asarray(arg) for arg in args] + # newton uses initial guess for the output shape + # copy v0 to a new array and broadcast it to the shape of max size + if shape is not None: + v0 = np.broadcast_to(voc_est, shape).copy() + else: + v0 = voc_est + # x0 and x in func are the same reference! DO NOT set x0 to current! + # voltage in fi(x, current, *a) MUST BE CONSTANT! + vd = newton( + func=fmpp, x0=v0, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args + ) + else: + raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args) @@ -335,6 +349,7 @@ def slower_way(photocurrent, saturation_current, resistance_series, """ slow_v_from_i = partial(bishop88_v_from_i, method='brentq') slow_i_from_v = partial(bishop88_i_from_v, method='brentq') + slow_mpp = partial(bishop88_mpp, method='brentq') # recursion try: len(photocurrent) @@ -391,6 +406,7 @@ def faster_way(photocurrent, saturation_current, resistance_series, """a faster way""" fast_v_from_i = partial(bishop88_v_from_i, method='newton') fast_i_from_v = partial(bishop88_i_from_v, method='newton') + fast_mpp = partial(bishop88_mpp, method='newton') args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args v_oc = fast_v_from_i(0.0, *args) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index e5328c0121..da91dd3b0f 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -798,14 +798,14 @@ def test_v_from_i_size(): def test_mpp_floats(): """test mpp""" IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, .1, 20, .5) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = {'i_mp': 6.1362673597376753, # 6.1390251797935704, lambertw 'v_mp': 6.2243393757884284, # 6.221535886625464, lambertw 'p_mp': 38.194210547580511} # 38.194165464983037} lambertw assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k]) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='fast') + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.isclose(v, expected[k]) @@ -813,15 +813,15 @@ def test_mpp_floats(): @requires_scipy def test_mpp_array(): """test mpp""" - IL, I0, Rs, Rsh, nNsVth = ([7, 7], 6e-7, .1, 20, .5) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = {'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2} assert isinstance(out, dict) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='fast') + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.allclose(v, expected[k]) @@ -830,9 +830,9 @@ def test_mpp_array(): def test_mpp_series(): """test mpp""" idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] - IL, I0, Rs, Rsh, nNsVth = ([7, 7], 6e-7, .1, 20, .5) + IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) IL = pd.Series(IL, index=idx) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = pd.DataFrame({'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2}, @@ -840,6 +840,7 @@ def test_mpp_series(): assert isinstance(out, pd.DataFrame) for k, v in out.items(): assert np.allclose(v, expected[k]) + out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.allclose(v, expected[k]) @@ -849,16 +850,17 @@ def test_singlediode_series(cec_module_params): times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') effective_irradiance = pd.Series([0.0, 800.0], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - effective_irradiance, - temp_cell=25, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - EgRef=1.121, - dEgdT=-0.0002677) + effective_irradiance, + temp_cell=25, + alpha_sc=cec_module_params['alpha_sc'], + a_ref=cec_module_params['a_ref'], + I_L_ref=cec_module_params['I_L_ref'], + I_o_ref=cec_module_params['I_o_ref'], + R_sh_ref=cec_module_params['R_sh_ref'], + R_s=cec_module_params['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 + ) out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth) assert isinstance(out, pd.DataFrame) From edc445d74375e8f68905ea60f79b125870fcf9cc Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 17:47:20 -0700 Subject: [PATCH 53/73] ENH: clean up last little bit * remove slowway and fastway --- pvlib/pvsystem.py | 37 ++++++-- pvlib/singlediode_methods.py | 174 +++++++++++++++++------------------ 2 files changed, 116 insertions(+), 95 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 17671d6fdf..919d9c4ebe 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1912,14 +1912,35 @@ def singlediode(photocurrent, saturation_current, resistance_series, out = pd.DataFrame(out, index=photocurrent.index) else: - if method.lower() == 'newton': - sdm_fun = singlediode_methods.faster_way - else: - sdm_fun = singlediode_methods.slower_way - out = sdm_fun( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts - ) + # use singlediode_methods + v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, + method=method.lower()) + i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, + method=method.lower()) + mpp_fun = partial(singlediode_methods.bishop88_mpp, + method=method.lower()) + args = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) # collect args + v_oc = v_from_i_fun(0.0, *args) + i_mp, v_mp, p_mp = mpp_fun(*args) + out = OrderedDict() + out['i_sc'] = i_from_v_fun(0.0, *args) + out['v_oc'] = v_oc + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = i_from_v_fun(v_oc / 2.0, *args) + out['i_xx'] = i_from_v_fun((v_oc + v_mp) / 2.0, *args) + # calculate the IV curve if requested using bishop88 + if ivcurve_pnts: + vd = v_oc * ( + (11.0 - np.logspace(np.log10(11.0), 0.0, + ivcurve_pnts)) / 10.0 + ) + i, v, p = bishop88(vd, *args) + out['i'] = i + out['v'] = v + out['p'] = p return out diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index e9a68334b5..c4388eb15e 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -342,93 +342,93 @@ def fmpp(x, *a): return bishop88(vd, *args) -def slower_way(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None): - """ - This is the slow but reliable way. - """ - slow_v_from_i = partial(bishop88_v_from_i, method='brentq') - slow_i_from_v = partial(bishop88_i_from_v, method='brentq') - slow_mpp = partial(bishop88_mpp, method='brentq') - # recursion - try: - len(photocurrent) - except TypeError: - pass - else: - vecfun = np.vectorize(slower_way) - out = vecfun(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts) - if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: - out = pd.DataFrame(out.tolist(), index=photocurrent.index) - else: - out_array = pd.DataFrame(out.tolist()) - out = OrderedDict() - out['i_sc'] = out_array.i_sc.values - out['v_oc'] = out_array.v_oc.values - out['i_mp'] = out_array.i_mp.values - out['v_mp'] = out_array.v_mp.values - out['p_mp'] = out_array.p_mp.values - out['i_x'] = out_array.i_x.values - out['i_xx'] = out_array.i_xx.values - if ivcurve_pnts: - out['i'] = np.vstack(out_array.i.values) - out['v'] = np.vstack(out_array.v.values) - out['p'] = np.vstack(out_array.p.values) - return out - # collect args - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - v_oc = slow_v_from_i(0.0, *args) - i_mp, v_mp, p_mp = slow_mpp(*args) - out = OrderedDict() - out['i_sc'] = slow_i_from_v(0.0, *args) - out['v_oc'] = v_oc - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - out['i_x'] = slow_i_from_v(v_oc / 2.0, *args) - out['i_xx'] = slow_i_from_v((v_oc + v_mp) / 2.0, *args) - # calculate the IV curve if requested using bishop88 - if ivcurve_pnts: - vd = v_oc * ( - (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 - ) - i, v, p = bishop88(vd, *args) - out['i'] = i - out['v'] = v - out['p'] = p - return out - - -def faster_way(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None): - """a faster way""" - fast_v_from_i = partial(bishop88_v_from_i, method='newton') - fast_i_from_v = partial(bishop88_i_from_v, method='newton') - fast_mpp = partial(bishop88_mpp, method='newton') - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) # collect args - v_oc = fast_v_from_i(0.0, *args) - i_mp, v_mp, p_mp = fast_mpp(*args) - out = OrderedDict() - out['i_sc'] = fast_i_from_v(0.0, *args) - out['v_oc'] = v_oc - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - out['i_x'] = fast_i_from_v(v_oc / 2.0, *args) - out['i_xx'] = fast_i_from_v((v_oc + v_mp) / 2.0, *args) - # calculate the IV curve if requested using bishop88 - if ivcurve_pnts: - vd = v_oc * ( - (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 - ) - i, v, p = bishop88(vd, *args) - out['i'] = i - out['v'] = v - out['p'] = p - return out +# def slower_way(photocurrent, saturation_current, resistance_series, +# resistance_shunt, nNsVth, ivcurve_pnts=None): +# """ +# This is the slow but reliable way. +# """ +# slow_v_from_i = partial(bishop88_v_from_i, method='brentq') +# slow_i_from_v = partial(bishop88_i_from_v, method='brentq') +# slow_mpp = partial(bishop88_mpp, method='brentq') +# # recursion +# try: +# len(photocurrent) +# except TypeError: +# pass +# else: +# vecfun = np.vectorize(slower_way) +# out = vecfun(photocurrent, saturation_current, resistance_series, +# resistance_shunt, nNsVth, ivcurve_pnts) +# if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: +# out = pd.DataFrame(out.tolist(), index=photocurrent.index) +# else: +# out_array = pd.DataFrame(out.tolist()) +# out = OrderedDict() +# out['i_sc'] = out_array.i_sc.values +# out['v_oc'] = out_array.v_oc.values +# out['i_mp'] = out_array.i_mp.values +# out['v_mp'] = out_array.v_mp.values +# out['p_mp'] = out_array.p_mp.values +# out['i_x'] = out_array.i_x.values +# out['i_xx'] = out_array.i_xx.values +# if ivcurve_pnts: +# out['i'] = np.vstack(out_array.i.values) +# out['v'] = np.vstack(out_array.v.values) +# out['p'] = np.vstack(out_array.p.values) +# return out +# # collect args +# args = (photocurrent, saturation_current, resistance_series, +# resistance_shunt, nNsVth) +# v_oc = slow_v_from_i(0.0, *args) +# i_mp, v_mp, p_mp = slow_mpp(*args) +# out = OrderedDict() +# out['i_sc'] = slow_i_from_v(0.0, *args) +# out['v_oc'] = v_oc +# out['i_mp'] = i_mp +# out['v_mp'] = v_mp +# out['p_mp'] = p_mp +# out['i_x'] = slow_i_from_v(v_oc / 2.0, *args) +# out['i_xx'] = slow_i_from_v((v_oc + v_mp) / 2.0, *args) +# # calculate the IV curve if requested using bishop88 +# if ivcurve_pnts: +# vd = v_oc * ( +# (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 +# ) +# i, v, p = bishop88(vd, *args) +# out['i'] = i +# out['v'] = v +# out['p'] = p +# return out +# +# +# def faster_way(photocurrent, saturation_current, resistance_series, +# resistance_shunt, nNsVth, ivcurve_pnts=None): +# """a faster way""" +# fast_v_from_i = partial(bishop88_v_from_i, method='newton') +# fast_i_from_v = partial(bishop88_i_from_v, method='newton') +# fast_mpp = partial(bishop88_mpp, method='newton') +# args = (photocurrent, saturation_current, resistance_series, +# resistance_shunt, nNsVth) # collect args +# v_oc = fast_v_from_i(0.0, *args) +# i_mp, v_mp, p_mp = fast_mpp(*args) +# out = OrderedDict() +# out['i_sc'] = fast_i_from_v(0.0, *args) +# out['v_oc'] = v_oc +# out['i_mp'] = i_mp +# out['v_mp'] = v_mp +# out['p_mp'] = p_mp +# out['i_x'] = fast_i_from_v(v_oc / 2.0, *args) +# out['i_xx'] = fast_i_from_v((v_oc + v_mp) / 2.0, *args) +# # calculate the IV curve if requested using bishop88 +# if ivcurve_pnts: +# vd = v_oc * ( +# (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 +# ) +# i, v, p = bishop88(vd, *args) +# out['i'] = i +# out['v'] = v +# out['p'] = p +# return out def _get_size_and_shape(args): From f542d152aacbd3166e8882bd439f421e8fc38b0b Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Wed, 27 Jun 2018 20:42:27 -0700 Subject: [PATCH 54/73] ENH: BUG: TST: DOC: remove all traces for "fast" or "slow" * remove "faster_way" and "slower_way" from test_singlediode_methods, and use the pvsystem.singlediode(..., method=<'newton', 'brentq', ...>) * update docstring notes in pvsystem.singlediode to refer to methods as "newton" or "brentq" instead of "fast" or "slow" * remove the returns_nan()() wrapper, do we need it? seems to pass all tests without it. * don't need partial in *_from_*, just call singlediode_method.bishop88 * remove extra imports from singlediode_methods like wraps, OrderedDict, pandas, also remove TODO and returns_nan()() * wrap a long line? * remove the old slowway and fastway functions no longer needed --- pvlib/pvsystem.py | 30 +++---- pvlib/singlediode_methods.py | 117 +------------------------ pvlib/test/test_singlediode_methods.py | 11 +-- 3 files changed, 18 insertions(+), 140 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 919d9c4ebe..8e829cb398 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1820,12 +1820,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} \\ I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} - If ``method.lower() == 'fast'`` then a gradient descent method, ``newton``, + If ``method.lower() == 'newton'`` then a gradient descent method, ``newton``, is used to solve the implicit diode equation. It should be safe for well behaved IV-curves, but the default method is recommended for reliability, it is often just as fast. - If either the "fast" or default methods are indicated, then + If either the "newton" or "brentq" methods are indicated, then :func:`pvlib.pvsystem.bishop88` is used to calculate the points at diode voltages from zero to open-circuit voltage with a log spacing so that points get closer as they approach the open-circuit voltage. @@ -1964,7 +1964,7 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of serices cells ``Ns`` method : str - if "fast" then use Newton, otherwise use bisection + if "newton" then use Newton, otherwise use bisection method, ``brentq`` Returns ------- @@ -2121,9 +2121,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, 0 <= photocurrent method : str - Method to use. If 'lambertw' use ``lambertw``, if 'fast' use ``newton``, - otherwise use bisection. Note: bisection is limited to 1st quadrant - only. + Method to use. If 'lambertw' use ``lambertw``, if 'newton' use + ``newton``, otherwise use bisection method ``brentq``. Note: bisection + is limited to 1st quadrant only. Returns ------- @@ -2216,13 +2216,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, return V else: # use singlediode methods - v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, - method=method.lower()) - # wrap it so it returns nan - # v_from_i_fun = singlediode_methods.returns_nan()(v_from_i_fun) args = (current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - V = v_from_i_fun(*args) + V = singlediode_methods.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) if size <= 1: @@ -2286,9 +2282,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, 0 <= photocurrent method : str - Method to use. If 'lambertw' use ``lambertw``, if 'fast' use ``newton``, - otherwise use bisection. Note: bisection is limited to 1st quadrant - only. + Method to use. If 'lambertw' use ``lambertw``, if 'newton' use + ``newton``, otherwise use bisection method, ``brentq``. Note: bisection + is limited to 1st quadrant only. Returns ------- @@ -2360,13 +2356,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, return I else: # use singlediode methods - i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, - method=method.lower()) - # wrap it so it returns nan - # i_from_v_fun = singlediode_methods.returns_nan()(i_from_v_fun) args = (voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - I = i_from_v_fun(*args) + I = singlediode_methods.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) if size <= 1: diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index c4388eb15e..8413d4623d 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -3,10 +3,8 @@ methods from J.W. Bishop (Solar Cells, 1988). """ -from collections import OrderedDict -from functools import wraps, partial +from functools import partial import numpy as np -import pandas as pd try: from scipy.optimize import brentq except ImportError: @@ -20,27 +18,6 @@ # rename newton and set keyword arguments newton = partial(_array_newton, tol=1e-6, maxiter=100, fprime2=None) -# TODO: update pvsystem.i_from_v and v_from_i to use "gold" method by default - - -def returns_nan(exc=None): - """ - Decorator that changes the return to NaN if either - """ - if not exc: - exc = (ValueError, RuntimeError) - - def wrapper(f): - @wraps(f) - def wrapped_fcn(*args, **kwargs): - try: - rval = f(*args, **kwargs) - except exc: - rval = np.nan - return rval - return wrapped_fcn - return wrapper - def estimate_voc(photocurrent, saturation_current, nNsVth): """ @@ -128,7 +105,8 @@ def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, grad2i = -c / nNsVth # d2i/dvd grad2v = -grad2i * resistance_series # d2v/dvd grad2p = ( - grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i + grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + + grad_i ) # d2p/dv/dvd retval += (grad_i, grad_v, grad, grad_p, grad2p) return retval @@ -342,95 +320,6 @@ def fmpp(x, *a): return bishop88(vd, *args) -# def slower_way(photocurrent, saturation_current, resistance_series, -# resistance_shunt, nNsVth, ivcurve_pnts=None): -# """ -# This is the slow but reliable way. -# """ -# slow_v_from_i = partial(bishop88_v_from_i, method='brentq') -# slow_i_from_v = partial(bishop88_i_from_v, method='brentq') -# slow_mpp = partial(bishop88_mpp, method='brentq') -# # recursion -# try: -# len(photocurrent) -# except TypeError: -# pass -# else: -# vecfun = np.vectorize(slower_way) -# out = vecfun(photocurrent, saturation_current, resistance_series, -# resistance_shunt, nNsVth, ivcurve_pnts) -# if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: -# out = pd.DataFrame(out.tolist(), index=photocurrent.index) -# else: -# out_array = pd.DataFrame(out.tolist()) -# out = OrderedDict() -# out['i_sc'] = out_array.i_sc.values -# out['v_oc'] = out_array.v_oc.values -# out['i_mp'] = out_array.i_mp.values -# out['v_mp'] = out_array.v_mp.values -# out['p_mp'] = out_array.p_mp.values -# out['i_x'] = out_array.i_x.values -# out['i_xx'] = out_array.i_xx.values -# if ivcurve_pnts: -# out['i'] = np.vstack(out_array.i.values) -# out['v'] = np.vstack(out_array.v.values) -# out['p'] = np.vstack(out_array.p.values) -# return out -# # collect args -# args = (photocurrent, saturation_current, resistance_series, -# resistance_shunt, nNsVth) -# v_oc = slow_v_from_i(0.0, *args) -# i_mp, v_mp, p_mp = slow_mpp(*args) -# out = OrderedDict() -# out['i_sc'] = slow_i_from_v(0.0, *args) -# out['v_oc'] = v_oc -# out['i_mp'] = i_mp -# out['v_mp'] = v_mp -# out['p_mp'] = p_mp -# out['i_x'] = slow_i_from_v(v_oc / 2.0, *args) -# out['i_xx'] = slow_i_from_v((v_oc + v_mp) / 2.0, *args) -# # calculate the IV curve if requested using bishop88 -# if ivcurve_pnts: -# vd = v_oc * ( -# (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 -# ) -# i, v, p = bishop88(vd, *args) -# out['i'] = i -# out['v'] = v -# out['p'] = p -# return out -# -# -# def faster_way(photocurrent, saturation_current, resistance_series, -# resistance_shunt, nNsVth, ivcurve_pnts=None): -# """a faster way""" -# fast_v_from_i = partial(bishop88_v_from_i, method='newton') -# fast_i_from_v = partial(bishop88_i_from_v, method='newton') -# fast_mpp = partial(bishop88_mpp, method='newton') -# args = (photocurrent, saturation_current, resistance_series, -# resistance_shunt, nNsVth) # collect args -# v_oc = fast_v_from_i(0.0, *args) -# i_mp, v_mp, p_mp = fast_mpp(*args) -# out = OrderedDict() -# out['i_sc'] = fast_i_from_v(0.0, *args) -# out['v_oc'] = v_oc -# out['i_mp'] = i_mp -# out['v_mp'] = v_mp -# out['p_mp'] = p_mp -# out['i_x'] = fast_i_from_v(v_oc / 2.0, *args) -# out['i_xx'] = fast_i_from_v((v_oc + v_mp) / 2.0, *args) -# # calculate the IV curve if requested using bishop88 -# if ivcurve_pnts: -# vd = v_oc * ( -# (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 -# ) -# i, v, p = bishop88(vd, *args) -# out['i'] = i -# out['v'] = v -# out['p'] = p -# return out - - def _get_size_and_shape(args): # find the right size and shape for returns size, shape = 0, None # 0 or None both mean scalar diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index d4b92bfca9..cd0f1bf305 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -8,9 +8,6 @@ from pvlib import pvsystem from conftest import requires_scipy -faster_way = pvsystem.singlediode_methods.faster_way -slower_way = pvsystem.singlediode_methods.slower_way - logging.basicConfig() LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @@ -36,7 +33,7 @@ def test_fast_spr_e20_327(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = faster_way(*x) + out = pvsystem.singlediode(*x, method='newton') tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx = out.values() dt_fast = tstop - tstart @@ -73,7 +70,7 @@ def test_fast_fs_495(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = faster_way(*x) + out = pvsystem.singlediode(*x, method='newton') tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() dt_fast = tstop - tstart @@ -110,7 +107,7 @@ def test_slow_spr_e20_327(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = slower_way(*x) + out = pvsystem.singlediode(*x, method='brentq') tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx = out.values() dt_fast = tstop - tstart @@ -147,7 +144,7 @@ def test_slow_fs_495(): dt_slow = tstop - tstart LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) tstart = clock() - out = slower_way(*x) + out = pvsystem.singlediode(*x, method='brentq') tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() dt_fast = tstop - tstart From cc6c976db5ea62c1734c27d1ef2049f0aa5a7a31 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 28 Jun 2018 00:04:34 -0700 Subject: [PATCH 55/73] TST: BUG: fix the boolean mask numpy<1.14 bug in tests * also cast photocurrent as array in estimate_voc in case it isn't and help users get away with using sequences instead of arrays --- pvlib/singlediode_methods.py | 2 +- pvlib/test/test_pvsystem.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 8413d4623d..2f32cef13e 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -52,7 +52,7 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): [1] http://www.pveducation.org/pvcdrom/open-circuit-voltage """ - return nNsVth * np.log(photocurrent / saturation_current + 1.0) + return nNsVth * np.log(np.asarray(photocurrent) / saturation_current + 1.0) def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index da91dd3b0f..eb766a6149 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -778,7 +778,7 @@ def test_i_from_v_size(): pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0, method='brentq') with pytest.raises(ValueError): - pvsystem.i_from_v(20, [0.1] * 2, 0.5, [7.5] * 3, 6.0e-7, 7.0, + pvsystem.i_from_v(20, 0.1, 0.5, [7.5] * 3, 6.0e-7, np.array([7., 7.]), method='newton') @@ -790,7 +790,7 @@ def test_v_from_i_size(): pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0, method='brentq') with pytest.raises(ValueError): - pvsystem.v_from_i(20, [0.1] * 2, 0.5, [3.0] * 3, 6.0e-7, 7.0, + pvsystem.v_from_i(20, [0.1], 0.5, [3.0] * 3, 6.0e-7, np.array([7., 7.]), method='newton') From 22c53fc9401d3f05cf14efe612cee0050b6265ac Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 28 Jun 2018 18:10:51 -0700 Subject: [PATCH 56/73] ENH: TST: ignore .pytest_cache/ folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f18eba0007..cbbc8257e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Byte-compiled / optimized / DLL files +.pytest_cache/ __pycache__/ *.py[cod] From 442a3d4f7981401bbe9523a2df64beb4a67aac56 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 28 Jun 2018 21:41:26 -0700 Subject: [PATCH 57/73] DOC: MAINT: respond to review by @cwhanse , clean up docstrings * reword methods for pvsystem.singlediode simplify, move some content to notes sections below * move descriptions of methods to top in same order as given in methods docstring, add description of brentq, move description of bishop88 last * fix typo "optional" missing "o" --- pvlib/pvsystem.py | 52 +++++++++++++++++++----------------- pvlib/singlediode_methods.py | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8e829cb398..f8443b3793 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1757,11 +1757,8 @@ def singlediode(photocurrent, saturation_current, resistance_series, IV curves will be produced. method : str, default 'lambertw' - Determines the method used to calculate IV curve and points. If - 'lambertw' then ``lambertw`` is used. If 'newton' then ``newton`` is - used. Otherwise the problem is bounded between zero and open-circuit - voltage and a bisection method, ``brentq``, is used, that guarantees - convergence. + Determines the method used to calculate points on the IV curve. The + options are 'lambertw', 'newton', or 'brentq'. Returns ------- @@ -1791,12 +1788,31 @@ def singlediode(photocurrent, saturation_current, resistance_series, Notes ----- - The default method employed is an explicit solution using [4] to find an - arbitrary point on the IV curve as a function of the diode voltage - :math:`V_d = V + I*Rs`. Then the voltage is backed out from :math:`V_d`. - A specific desired point, such as short circuit current or max power, is - located using the bisection search method, ``brentq``, bounded by a zero - diode voltage and an estimate of open circuit voltage given by + If the method is ``'lambertw'`` then the solution employed to solve the + implicit diode equation utilizes the Lambert W function to obtain an + explicit function of :math:`V=f(I)` and :math:`I=f(V)` as shown in [2]. + + If the method is ``'newton'`` then the root-finding Newton-Raphson method + is used. It should be safe for well behaved IV-curves, but the ``'brentq'`` + method is recommended for reliability. + + If the method is ``'brentq'`` then Brent's bisection search method is used + that guarantees convergence by bounding the voltage between zero and + open-circuit. + + If the method is either ``'newton'`` or ``'brentq'`` and ``ivcurve_pnts`` + are indicated, then :func:`pvlib.pvsystem.bishop88` 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 :func:`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:: @@ -1820,20 +1836,6 @@ def singlediode(photocurrent, saturation_current, resistance_series, I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} \\ I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} - If ``method.lower() == 'newton'`` then a gradient descent method, ``newton``, - is used to solve the implicit diode equation. It should be safe for well - behaved IV-curves, but the default method is recommended for reliability, - it is often just as fast. - - If either the "newton" or "brentq" methods are indicated, then - :func:`pvlib.pvsystem.bishop88` is used to calculate the points at diode - voltages from zero to open-circuit voltage with a log spacing so that - points get closer as they approach the open-circuit voltage. - - If ``method.lower() == 'lambertw'`` then the solution employed to solve the - implicit diode equation utilizes the Lambert W function to obtain an - explicit function of V=f(i) and I=f(V) as shown in [2]. - References ----------- [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 2f32cef13e..67bdd48ca9 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -271,7 +271,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two ptional search methods: either `brentq`, a reliable and + one of two optional search methods: either `brentq`, a reliable and bounded method or `newton` the default, a gradient descent method. Returns From 6bcffe8d748974cbc9e66438bfdbc962563fe65e Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 28 Jun 2018 21:50:35 -0700 Subject: [PATCH 58/73] MAINT: update comment about mpp search algorithm --- pvlib/pvsystem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f8443b3793..d2149aed81 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1873,11 +1873,12 @@ def singlediode(photocurrent, saturation_current, resistance_series, 'i_0': saturation_current, 'i_l': photocurrent} + # Find the voltage, v_mp, where the power is maximized. + # Start the golden section search at v_oc * 1.14 p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) - # Invert the Power-Current curve. Find the current where the inverted - # power is minimized. This is i_mp. Start the optimization at v_oc/2 + # Find Imp using Lambert W i_mp = i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, saturation_current, photocurrent, method) From e6b60c6e16b9931b0a48549e7af596c2c8d32331 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Thu, 28 Jun 2018 22:19:13 -0700 Subject: [PATCH 59/73] MAINT: update what's new for v0.6 * add missing R_s = 0 for estimate_voc * change docstring string literal to use triple double quotes per pep8 * add bishop88 to see also * add comment before lambertw like "calculate points on IV curve using lambertw" * change "use single diode methods" to be more expanatory, like "calculate points on the IV curve using newton or brentq" * change desoto parameters to sdm coeffs * better shorter simpler docstring for mpp() method kwarg: just "brent" or "newton" * note why is brentq default in pvsystem.mpp(), b/c it's an option to pvsystem.singlediode and brentq is guaranteed to converge * remove "Faster ways" * remove diode "one" * fix typos, "o" in optional --- docs/sphinx/source/whatsnew/v0.6.0.rst | 4 +-- pvlib/pvsystem.py | 36 ++++++++++++++++++-------- pvlib/singlediode_methods.py | 16 ++++++------ 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 9bf6eb4826..3e7a5dd83c 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -25,8 +25,8 @@ Enhancements if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will be log spaced instead of linear. * Implement :func:`~pvlib.pvsystem.estimate_voc` to estimate open circuit - voltage by assuming :math:`R_{sh} \to \infty`. This is used as an upper bound - in bisection method for :func:`~pvlib.pvsystem.singlediode`. + 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`. * Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W with the two new implementations to form a wrapper, that takes an additional ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d2149aed81..91c513ce03 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1701,7 +1701,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, ivcurve_pnts=None, method='lambertw'): - r''' + """ Solve the single-diode model to obtain a photovoltaic IV curve. Singlediode solves the single diode equation [1] @@ -1856,8 +1856,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, -------- sapm calcparams_desoto - ''' - + bishop88 + """ + # Calculate points on the IV curve using the LambertW solution to the + # single diode equation if method.lower() == 'lambertw': # Compute short circuit current i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0., @@ -1915,7 +1917,9 @@ def singlediode(photocurrent, saturation_current, resistance_series, out = pd.DataFrame(out, index=photocurrent.index) else: - # use singlediode_methods + # Calculate points on the IV curve using either 'newton' or 'brentq' + # methods. Voltages are determined by first solving the single diode + # equation for the diode voltage V_d then backing out voltage v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, method=method.lower()) i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, @@ -1950,8 +1954,8 @@ def singlediode(photocurrent, saturation_current, resistance_series, def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, method='brentq'): """ - Given the calculated DeSoto parameters, calculates the maximum power point - (MPP). + Given the single diode equation coefficients, calculates the maximum power + point (MPP). Parameters ---------- @@ -1967,12 +1971,18 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of serices cells ``Ns`` method : str - if "newton" then use Newton, otherwise use bisection method, ``brentq`` + either "newton" or "brentq" Returns ------- OrderedDict or pandas.Datafrane ``(i_mp, v_mp, p_mp)`` + + Notes + ----- + This function is an option to :func:`singlediode`. Use it when you just + want to find the max power point. It uses Brent's method by default because + it is guaranteed to converge. """ mpp_fun = partial(singlediode_methods.bishop88_mpp, method=method.lower()) i_mp, v_mp, p_mp = mpp_fun( @@ -1995,8 +2005,8 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, def _golden_sect_DataFrame(params, VL, VH, func): """ - Vectorized golden section search for finding MPPT - from a dataframe timeseries. + Vectorized golden section search for finding MPP from a dataframe + timeseries. Parameters ---------- @@ -2218,7 +2228,9 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, else: return V else: - # use singlediode methods + # Calculate points on the IV curve using either 'newton' or 'brentq' + # methods. Voltages are determined by first solving the single diode + # 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()) @@ -2358,7 +2370,9 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, else: return I else: - # use singlediode methods + # Calculate points on the IV curve using either 'newton' or 'brentq' + # methods. Voltages are determined by first solving the single diode + # 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()) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 67bdd48ca9..c8724164ff 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -1,6 +1,6 @@ """ -Faster ways to calculate single diode model currents and voltages using -methods from J.W. Bishop (Solar Cells, 1988). +Calculate single diode model currents and voltages using methods from +J.W. Bishop (Solar Cells, 1988). """ from functools import partial @@ -29,7 +29,7 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): photocurrent : numeric photo-generated current [A] saturation_current : numeric - diode one reverse saturation current [A] + diode reverse saturation current [A] nNsVth : numeric product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of series cells ``Ns`` @@ -58,8 +58,8 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, gradients=False): """ - Explicit calculation single-diode-model (SDM) currents and voltages using - diode junction voltages [1]. + Explicit calculation of points on the IV curve described by the single + diode equation [1]. [1] "Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) @@ -72,7 +72,7 @@ def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, photocurrent : numeric photo-generated current [A] saturation_current : numeric - diode one reverse saturation current [A] + diode reverse saturation current [A] resistance_series : numeric series resistance [ohms] resistance_shunt: numeric @@ -134,7 +134,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two ptional search methods: either `brentq`, a reliable and + one of two optional search methods: either `brentq`, a reliable and bounded method or `newton` the default, a gradient descent method. Returns @@ -204,7 +204,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two ptional search methods: either `brentq`, a reliable and + one of two optional search methods: either `brentq`, a reliable and bounded method or `newton` the default, a gradient descent method. Returns From 0fc9c83637520c19abef221456a06df7deb752d0 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 00:46:00 -0700 Subject: [PATCH 60/73] DOC: make sure docs render well * update what's new with formatted links to first_solar_spectral_loss and calcparams_desoto * newton is not a gradient descent method, ha! * replace fast and slow with newton and brentq * add note that new module singlediode_methods is for low-level functions * add low level singlediode_methods to api.rst with a note separating it from pvsystem * remvoe way faster from test_singlediode * backticks around method argument options in singlediode * add escapes to math directive in estimate_voc and singlediode, and wrap some long lines a little * shorten description of methods in *_from_* * use rubric to separate and title Notes in estimate_voc * use LaTeX math for gradients instead of code format --- docs/sphinx/source/api.rst | 19 ++++++++++-- docs/sphinx/source/whatsnew/v0.6.0.rst | 30 ++++++++++++------- pvlib/pvsystem.py | 41 ++++++++++++++++---------- pvlib/singlediode_methods.py | 23 ++++++++------- pvlib/test/test_singlediode_methods.py | 2 +- 5 files changed, 75 insertions(+), 40 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 8049d93549..491be36063 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -65,7 +65,6 @@ algorithm. spa - Correlations and analytical expressions for low precision solar position calculations. @@ -80,6 +79,7 @@ calculations. solarposition.equation_of_time_pvcdrom solarposition.hour_angle + Clear sky ========= @@ -206,8 +206,22 @@ Functions relevant for the single diode model. pvsystem.singlediode pvsystem.v_from_i pvsystem.mpp - pvsystem.bishop88 pvsystem.estimate_voc + pvsystem.bishop88 + +Low-level functions to support :func:`pvlib.pvsystem.bishop88`. *Note*: +:func:`pvlib.singlediode_methods.bishop88` and +:func:`pvlib.singlediode_methods.estimate_voc` are also imported into the +``pvlib.pvsystem`` module. + +.. 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 SAPM model ---------- @@ -234,7 +248,6 @@ PVWatts model pvsystem.pvwatts_ac pvsystem.pvwatts_losses - Other ----- diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 3e7a5dd83c..29fd508fc6 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -11,27 +11,37 @@ API Changes Enhancements ~~~~~~~~~~~~ -* Add sea surface albedo in irradiance.py (:issue:`458`) -* Implement first_solar_spectral_loss in modelchain.py (:issue:`359`) -* Clarify arguments Egref and dEgdT for calcparams_desoto (:issue:`462`) +* Add sea surface albedo in ``irradiance.py`` (:issue:`458`) +* Implement :meth:`~pvlib.modelchain.ModelChain.first_solar_spectral_loss` + in ``modelchain.py`` (:issue:`359`) +* Clarify arguments ``Egref`` and ``dEgdT`` for + :func:`~pvlib.pvsystem.calcparams_desoto` (:issue:`462`) * Implement a reliable "gold" implementation of the single diode model (SDM) using a bisection method (Brent, 1973) bounded by points known to include the full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using - a gradient descent method (Newton-Raphson) that is not bounded, but should be - safe for well behaved IV-curves. (:issue:`408`) + the Newton-Raphson method that is not bounded, but should be safe for well + behaved IV-curves. (:issue:`408`) * Implement :func:`~pvlib.pvsystem.bishop88` for explicit calculation of arbitrary IV curve points using diode voltage instead of cell voltage. This method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` - if the method is either ``'fast'`` or ``'gold'``, but the IV curve points will - be log spaced instead of linear. + if the method is either ``'newton'`` or ``'brentq'``, but the IV curve points + will be log spaced instead of linear. * Implement :func:`~pvlib.pvsystem.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`. * Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W with the two new implementations to form a wrapper, that takes an additional - ``method`` argument ``('lambertw', 'fast', 'gold')`` that defaults to the new - "gold" bounded bisection method. (:issue:`410`) -* Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point. + ``method`` argument ``('lambertw', 'newton', 'brentq')`` that defaults to a + Lambert-W solution. Selecting either "brentq" or "newton" as the method uses + ``bishop88`` with the corresponding search method. (:issue:`410`) +* Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point using + the new "brentq" bounded bisection method. +* Add new module ``pvlib.singlediode_methods`` which contains low-level + functions to support implementing ``bishop88`` and various other methods like + ``bishop88_i_from_v``, ``bishop88_v_from_i``, and ``bishop88_mpp``. *Note*: + :func:`pvlib.singlediode_methods.bishop88` and + :func:`pvlib.singlediode_methods.estimate_voc` are also imported into the + ``pvlib.pvsystem`` module. Bug fixes diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 91c513ce03..4ff069ca94 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1758,7 +1758,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, method : str, default 'lambertw' Determines the method used to calculate points on the IV curve. The - options are 'lambertw', 'newton', or 'brentq'. + options are ``'lambertw'``, ``'newton'``, or ``'brentq'``. Returns ------- @@ -1816,7 +1816,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, .. math:: - V_{oc, est} = n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1 \right) + 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 @@ -1828,13 +1828,24 @@ def singlediode(photocurrent, saturation_current, resistance_series, .. 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}} \\ - 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}} \\ - 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}} \\ - 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}} \\ - 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}} \\ - I = I_L - I_L - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} \\ - I = - \frac{n Ns V_{th} \log \left( \frac{I_L}{I_0} + 1\right)}{R_{sh}} + 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 ----------- @@ -1971,7 +1982,7 @@ def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of serices cells ``Ns`` method : str - either "newton" or "brentq" + either ``'newton'`` or ``'brentq'`` Returns ------- @@ -2134,9 +2145,8 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, 0 <= photocurrent method : str - Method to use. If 'lambertw' use ``lambertw``, if 'newton' use - ``newton``, otherwise use bisection method ``brentq``. Note: bisection - is limited to 1st quadrant only. + Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: + ``'brentq'`` is limited to 1st quadrant only. Returns ------- @@ -2297,9 +2307,8 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, 0 <= photocurrent method : str - Method to use. If 'lambertw' use ``lambertw``, if 'newton' use - ``newton``, otherwise use bisection method, ``brentq``. Note: bisection - is limited to 1st quadrant only. + Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: + ``'brentq'`` is limited to 1st quadrant only. Returns ------- diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index c8724164ff..9cc20e3dd1 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -39,6 +39,8 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): numeric rough estimate of open circuit voltage [V] + Notes + ----- Calculating the open circuit voltage, :math:`V_{oc}`, of an ideal device with infinite shunt resistance, :math:`R_{sh} \\to \\infty`, and zero series resistance, :math:`R_s = 0`, yields the following equation [1]. As an @@ -81,14 +83,15 @@ def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and number of series cells ``Ns`` gradients : bool - default returns only i, v, and p, returns gradients if true + False returns only I, V, and P. True also returns gradients Returns ------- tuple - containing currents [A], voltages [V], power [W], gradient ``di/dvd``, - gradient ``dv/dvd``, gradient ``di/dv``, gradient ``dp/dv``, and - gradient ``d2p/dv/dvd`` + currents [A], voltages [V], power [W], and optionally + :math:`\\frac{dI}{dV_d}`, :math:`\\frac{dV}{dV_d}`, + :math:`\\frac{dI}{dV}`, :math:`\\frac{dP}{dV}`, and + :math:`\\frac{d^2 P}{dV dV_d}` """ a = np.exp(diode_voltage / nNsVth) b = 1.0 / resistance_shunt @@ -134,8 +137,8 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two optional search methods: either `brentq`, a reliable and - bounded method or `newton` the default, a gradient descent method. + one of two optional search methods: either ``'brentq'``, a reliable and + bounded method or ``'newton'`` which is the default. Returns ------- @@ -204,8 +207,8 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two optional search methods: either `brentq`, a reliable and - bounded method or `newton` the default, a gradient descent method. + one of two optional search methods: either ``'brentq'``, a reliable and + bounded method or ``'newton'`` which is the default. Returns ------- @@ -271,8 +274,8 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, product of diode ideality factor (n), number of series cells (Ns), and thermal voltage (Vth = k_b * T / q_e) in volts [V] method : str - one of two optional search methods: either `brentq`, a reliable and - bounded method or `newton` the default, a gradient descent method. + one of two optional search methods: either ``'brentq'``, a reliable and + bounded method or ``'newton'`` which is the default. Returns ------- diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index cd0f1bf305..344551566b 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -1,5 +1,5 @@ """ -testing way faster single-diode methods using JW Bishop 1988 +testing single-diode methods using JW Bishop 1988 """ from time import clock From fc6cee1e6d0f0598b19ac5ad19c0a87c1074bfec Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 14:29:26 -0700 Subject: [PATCH 61/73] DOC: MAINT: rewording what's new with @cwhanse comments to make it clear --- docs/sphinx/source/whatsnew/v0.6.0.rst | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 29fd508fc6..aaa9db750e 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -16,24 +16,25 @@ Enhancements in ``modelchain.py`` (:issue:`359`) * Clarify arguments ``Egref`` and ``dEgdT`` for :func:`~pvlib.pvsystem.calcparams_desoto` (:issue:`462`) -* Implement a reliable "gold" implementation of the single diode model (SDM) - using a bisection method (Brent, 1973) bounded by points known to include the - full forward-bias 1st quadrant IV-curve. Also implement a "fast" method using - the Newton-Raphson method that is not bounded, but should be safe for well - behaved IV-curves. (:issue:`408`) +* Extend :func:`~pvlib.pvsystem.singlediode` with an additional keyword argument + ``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 + ``bishop88`` with the corresponding search 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 + method (Brent, 1973) that may be slow but guarantees a solution. ``'newton'`` + 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.pvsystem.bishop88` for explicit calculation of - arbitrary IV curve points using diode voltage instead of cell voltage. This - method is called for ``ivcurve_pnts`` in :func:`~pvlib.pvsystem.singlediode` - if the method is either ``'newton'`` or ``'brentq'``, but the IV curve points - will be log spaced instead of linear. + 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.pvsystem.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`. -* Combine existing :func:`~pvlib.pvsystem.singlediode` method using Lambert-W - with the two new implementations to form a wrapper, that takes an additional - ``method`` argument ``('lambertw', 'newton', 'brentq')`` that defaults to a - Lambert-W solution. Selecting either "brentq" or "newton" as the method uses - ``bishop88`` with the corresponding search method. (:issue:`410`) * Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point using the new "brentq" bounded bisection method. * Add new module ``pvlib.singlediode_methods`` which contains low-level From 28c8ffb80c6194a07c73566e67a6e10c6ecc6e0f Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 15:04:32 -0700 Subject: [PATCH 62/73] API: change bishop88 and estimate_voc to be used from singlediode_methods * update what's new and remove pvsystem.bishop88 and est_voc from api.rst * also remove note that they're listed in 2 places, just leave note about low-level functions to solve SDM * update imports in test_numerical_precision and pvsystem, remove func assignments from pvsystem, update docstrings * change module docstring in singlediode_methods to be generic --- docs/sphinx/source/api.rst | 7 +------ docs/sphinx/source/whatsnew/v0.6.0.rst | 25 ++++++++++++------------- pvlib/pvsystem.py | 12 ++++-------- pvlib/singlediode_methods.py | 3 +-- pvlib/test/test_numerical_precision.py | 7 ++++--- 5 files changed, 22 insertions(+), 32 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 491be36063..309caaa073 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -206,13 +206,8 @@ Functions relevant for the single diode model. pvsystem.singlediode pvsystem.v_from_i pvsystem.mpp - pvsystem.estimate_voc - pvsystem.bishop88 -Low-level functions to support :func:`pvlib.pvsystem.bishop88`. *Note*: -:func:`pvlib.singlediode_methods.bishop88` and -:func:`pvlib.singlediode_methods.estimate_voc` are also imported into the -``pvlib.pvsystem`` module. +Low-level functions for solving the single diode equation. .. autosummary:: :toctree: generated/ diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index aaa9db750e..58b2764936 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -20,29 +20,28 @@ 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 - ``bishop88`` with the corresponding search method. (:issue:`410`) + :func:`~pvlib.singlediode_methods.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 method (Brent, 1973) that may be slow but guarantees a solution. ``'newton'`` 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.pvsystem.bishop88` for explicit calculation of - arbitrary IV curve points using diode voltage instead of cell voltage. If +* Implement :func:`~pvlib.singlediode_methods.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.pvsystem.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`. +* Implement :func:`~pvlib.singlediode_methods.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.mpp` method to compute the max power point using - the new "brentq" bounded bisection method. -* Add new module ``pvlib.singlediode_methods`` which contains low-level - functions to support implementing ``bishop88`` and various other methods like - ``bishop88_i_from_v``, ``bishop88_v_from_i``, and ``bishop88_mpp``. *Note*: - :func:`pvlib.singlediode_methods.bishop88` and - :func:`pvlib.singlediode_methods.estimate_voc` are also imported into the - ``pvlib.pvsystem`` module. + the new ``'brentq'`` method. +* Add new module ``pvlib.singlediode_methods`` with low-level functions for + solving the single diode equation such as: ``bishop88``, ``estimate_voc``, + ``bishop88_i_from_v``, ``bishop88_v_from_i``, and ``bishop88_mpp``. Bug fixes diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4ff069ca94..bcb021d7ea 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -23,10 +23,6 @@ from pvlib import irradiance, atmosphere from pvlib import singlediode_methods -# expose single diode methods to API -bishop88 = singlediode_methods.bishop88 -estimate_voc = singlediode_methods.estimate_voc - # not sure if this belongs in the pvsystem module. # maybe something more like core.py? It may eventually grow to @@ -1801,13 +1797,13 @@ 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.pvsystem.bishop88` is used to calculate - the points on the IV curve points at diode voltages from zero to + are indicated, then :func:`pvlib.singlediode_methods.bishop88` 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 :func:`bishop88` method uses an explicit solution from [4] that finds + 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 @@ -1955,7 +1951,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, p = bishop88(vd, *args) + i, v, p = singlediode_methods.bishop88(vd, *args) out['i'] = i out['v'] = v out['p'] = p diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 9cc20e3dd1..431c2eb349 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -1,6 +1,5 @@ """ -Calculate single diode model currents and voltages using methods from -J.W. Bishop (Solar Cells, 1988). +Low-level functions for solving the single diode equation. """ from functools import partial diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 80ceb2c407..839f0f1088 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -8,6 +8,7 @@ import numpy as np import pandas as pd from pvlib import pvsystem +from pvlib.singlediode_methods import bishop88, estimate_voc logging.basicConfig() LOGGER = logging.getLogger(__name__) @@ -66,7 +67,7 @@ def generate_numerical_precision(): ) # generate exact values data = dict(zip((il, io, rs, rsh, nnsvt), ARGS)) - vdtest = np.linspace(0, pvsystem.estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + vdtest = np.linspace(0, estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) expected = [] for test in vdtest: data[vd] = test @@ -90,8 +91,8 @@ def test_numerical_precision(): Test that there are no numerical errors due to floating point arithmetic. """ expected = pd.read_csv(DATA_PATH) - vdtest = np.linspace(0, pvsystem.estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) - results = pvsystem.bishop88(vdtest, *ARGS, gradients=True) + vdtest = np.linspace(0, estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) + results = bishop88(vdtest, *ARGS, gradients=True) assert np.allclose(expected['i'], results[0]) assert np.allclose(expected['v'], results[1]) assert np.allclose(expected['p'], results[2]) From 58361c126f901e586c1ad696f557d401e2c9315a Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 15:43:45 -0700 Subject: [PATCH 63/73] DOC: link to new singlediode_methods subfunctions in what's new * in see also in pvsystem.singlediode need full path to bishop88 * in singlediode_methods, add a line before FIXME for clarity --- docs/sphinx/source/whatsnew/v0.6.0.rst | 8 ++++++-- pvlib/pvsystem.py | 2 +- pvlib/singlediode_methods.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index 58b2764936..d036fa4755 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -40,8 +40,12 @@ Enhancements * Add :func:`~pvlib.pvsystem.mpp` method to compute the max power point using the new ``'brentq'`` method. * Add new module ``pvlib.singlediode_methods`` with low-level functions for - solving the single diode equation such as: ``bishop88``, ``estimate_voc``, - ``bishop88_i_from_v``, ``bishop88_v_from_i``, and ``bishop88_mpp``. + 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`. Bug fixes diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bcb021d7ea..fd57f3ba8d 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1863,7 +1863,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, -------- sapm calcparams_desoto - bishop88 + pvlib.singlediode_methods.bishop88 """ # Calculate points on the IV curve using the LambertW solution to the # single diode equation diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 431c2eb349..1fb868d66a 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -8,6 +8,7 @@ from scipy.optimize import brentq except ImportError: brentq = NotImplemented + # FIXME: change this to newton when scipy-1.2 is released try: from scipy.optimize import _array_newton From 5f9ed41850ca6800521527797623d834aa930ce0 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 16:35:07 -0700 Subject: [PATCH 64/73] API: DOC: ENH: change pvsystem.mpp() -> max_power_point() * update api.rst and what's new, and test_pvsystem.py everywhere where it says mpp() * remove logging, LOGGER, timeing, and CLI for test_singlediode_methods * remove p from out in pvsystem.singlediode if ivcurve_npts is provided since not part of original api * so remove p from test_singlediode_methods.py too * add singlediode_methods to pvlib/__init__.py * just assign out once for all methods, outside of the if/else blocks in pvsystem.singlediode * since IDE complains, and future maintainer may not realize, initialize ivcurve_* with NotImplemented instead of None or nothing at all so this is explicit, you must calculate this or else * need to create values for i_sc, i_x, and i_xx in bishop88 methods to fill out --- docs/sphinx/source/api.rst | 2 +- docs/sphinx/source/whatsnew/v0.6.0.rst | 4 +- pvlib/__init__.py | 1 + pvlib/pvsystem.py | 58 +++++++++++++------------- pvlib/test/test_pvsystem.py | 18 ++++---- pvlib/test/test_singlediode_methods.py | 57 ++----------------------- 6 files changed, 46 insertions(+), 94 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 309caaa073..608f57937b 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -205,7 +205,7 @@ Functions relevant for the single diode model. pvsystem.i_from_v pvsystem.singlediode pvsystem.v_from_i - pvsystem.mpp + pvsystem.max_power_point Low-level functions for solving the single diode equation. diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst index d036fa4755..96378868bd 100644 --- a/docs/sphinx/source/whatsnew/v0.6.0.rst +++ b/docs/sphinx/source/whatsnew/v0.6.0.rst @@ -37,8 +37,8 @@ Enhancements 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.mpp` method to compute the max power point using - the new ``'brentq'`` method. +* 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 solving the single diode equation such as: :func:`~pvlib.singlediode_methods.bishop88`, diff --git a/pvlib/__init__.py b/pvlib/__init__.py index b756147eba..3a231da728 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -11,3 +11,4 @@ from pvlib import pvsystem from pvlib import spa from pvlib import modelchain +from pvlib import singlediode_methods diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fd57f3ba8d..59698f7399 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1865,6 +1865,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, calcparams_desoto pvlib.singlediode_methods.bishop88 """ + # make IDE happy + ivcurve_v = NotImplemented + ivcurve_i = NotImplemented + # Calculate points on the IV curve using the LambertW solution to the # single diode equation if method.lower() == 'lambertw': @@ -1899,15 +1903,6 @@ def singlediode(photocurrent, saturation_current, resistance_series, 0.5 * (v_oc + v_mp), saturation_current, photocurrent, method) - out = OrderedDict() - out['i_sc'] = i_sc - out['v_oc'] = v_oc - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - out['i_x'] = i_x - out['i_xx'] = i_xx - # create ivcurve if ivcurve_pnts: ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * @@ -1917,12 +1912,6 @@ def singlediode(photocurrent, saturation_current, resistance_series, ivcurve_v.T, saturation_current, photocurrent, method).T - out['v'] = ivcurve_v - out['i'] = ivcurve_i - - if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: - out = pd.DataFrame(out, index=photocurrent.index) - else: # Calculate points on the IV curve using either 'newton' or 'brentq' # methods. Voltages are determined by first solving the single diode @@ -1937,29 +1926,40 @@ def singlediode(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args v_oc = v_from_i_fun(0.0, *args) i_mp, v_mp, p_mp = mpp_fun(*args) - out = OrderedDict() - out['i_sc'] = i_from_v_fun(0.0, *args) - out['v_oc'] = v_oc - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp - out['i_x'] = i_from_v_fun(v_oc / 2.0, *args) - out['i_xx'] = i_from_v_fun((v_oc + v_mp) / 2.0, *args) + i_sc = i_from_v_fun(0.0, *args) + i_x = i_from_v_fun(v_oc / 2.0, *args) + i_xx = i_from_v_fun((v_oc + v_mp) / 2.0, *args) + # calculate the IV curve if requested using bishop88 if ivcurve_pnts: vd = v_oc * ( (11.0 - np.logspace(np.log10(11.0), 0.0, ivcurve_pnts)) / 10.0 ) - i, v, p = singlediode_methods.bishop88(vd, *args) - out['i'] = i - out['v'] = v - out['p'] = p + ivcurve_i, ivcurve_v, _ = singlediode_methods.bishop88(vd, *args) + + out = OrderedDict() + out['i_sc'] = i_sc + out['v_oc'] = v_oc + out['i_mp'] = i_mp + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = i_x + out['i_xx'] = i_xx + + if ivcurve_pnts: + + out['v'] = ivcurve_v + out['i'] = ivcurve_i + + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out, index=photocurrent.index) + return out -def mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, - nNsVth, method='brentq'): +def max_power_point(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, method='brentq'): """ Given the single diode equation coefficients, calculates the maximum power point (MPP). diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index eb766a6149..c32cd4018b 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -796,43 +796,43 @@ def test_v_from_i_size(): @requires_scipy def test_mpp_floats(): - """test mpp""" + """test max_power_point""" IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, .1, 20, .5) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = {'i_mp': 6.1362673597376753, # 6.1390251797935704, lambertw 'v_mp': 6.2243393757884284, # 6.221535886625464, lambertw 'p_mp': 38.194210547580511} # 38.194165464983037} lambertw assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k]) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.isclose(v, expected[k]) @requires_scipy def test_mpp_array(): - """test mpp""" + """test max_power_point""" IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = {'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2} assert isinstance(out, dict) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.allclose(v, expected[k]) @requires_scipy def test_mpp_series(): - """test mpp""" + """test max_power_point""" idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) IL = pd.Series(IL, index=idx) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') expected = pd.DataFrame({'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2}, @@ -840,7 +840,7 @@ def test_mpp_series(): assert isinstance(out, pd.DataFrame) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.mpp(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') for k, v in out.items(): assert np.allclose(v, expected[k]) diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index 344551566b..d39e533ffb 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -2,16 +2,10 @@ testing single-diode methods using JW Bishop 1988 """ -from time import clock -import logging import numpy as np from pvlib import pvsystem from conftest import requires_scipy -logging.basicConfig() -LOGGER = logging.getLogger(__name__) -LOGGER.setLevel(logging.DEBUG) - POA = 888 TCELL = 55 CECMOD = pvsystem.retrieve_sam('cecmod') @@ -27,18 +21,9 @@ def test_fast_spr_e20_327(): R_sh_ref=spr_e20_327.R_sh_ref, R_s=spr_e20_327.R_s, EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x - tstart = clock() pvs = pvsystem.singlediode(*x, method='lambertw') - tstop = clock() - dt_slow = tstop - tstart - LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) - tstart = clock() out = pvsystem.singlediode(*x, method='newton') - tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx = out.values() - dt_fast = tstop - tstart - LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) - LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct @@ -64,18 +49,9 @@ def test_fast_fs_495(): EgRef=1.475, dEgdT=-0.0003) il, io, rs, rsh, nnsvt = x x += (101, ) - tstart = clock() pvs = pvsystem.singlediode(*x, method='lambertw') - tstop = clock() - dt_slow = tstop - tstart - LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) - tstart = clock() out = pvsystem.singlediode(*x, method='newton') - tstop = clock() - isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() - dt_fast = tstop - tstart - LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) - LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) + isc, voc, imp, vmp, pmp, ix, ixx, i, v = out.values() assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct @@ -88,7 +64,7 @@ def test_fast_fs_495(): pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, method='lambertw') assert np.isclose(pvs_ixx, ixx) - return isc, voc, imp, vmp, pmp, i, v, p, pvs + return isc, voc, imp, vmp, pmp, i, v, pvs @requires_scipy @@ -101,18 +77,9 @@ def test_slow_spr_e20_327(): R_sh_ref=spr_e20_327.R_sh_ref, R_s=spr_e20_327.R_s, EgRef=1.121, dEgdT=-0.0002677) il, io, rs, rsh, nnsvt = x - tstart = clock() pvs = pvsystem.singlediode(*x, method='lambertw') - tstop = clock() - dt_slow = tstop - tstart - LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) - tstart = clock() out = pvsystem.singlediode(*x, method='brentq') - tstop = clock() isc, voc, imp, vmp, pmp, ix, ixx = out.values() - dt_fast = tstop - tstart - LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) - LOGGER.debug('spr_e20_327 speedup = %g', dt_slow / dt_fast) assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct @@ -138,18 +105,9 @@ def test_slow_fs_495(): EgRef=1.475, dEgdT=-0.0003) il, io, rs, rsh, nnsvt = x x += (101, ) - tstart = clock() pvs = pvsystem.singlediode(*x, method='lambertw') - tstop = clock() - dt_slow = tstop - tstart - LOGGER.debug('single diode elapsed time = %g[s]', dt_slow) - tstart = clock() out = pvsystem.singlediode(*x, method='brentq') - tstop = clock() - isc, voc, imp, vmp, pmp, ix, ixx, i, v, p = out.values() - dt_fast = tstop - tstart - LOGGER.debug('way faster elapsed time = %g[s]', dt_fast) - LOGGER.debug('fs_495 speedup = %g', dt_slow / dt_fast) + isc, voc, imp, vmp, pmp, ix, ixx, i, v = out.values() assert np.isclose(pvs['i_sc'], isc) assert np.isclose(pvs['v_oc'], voc) # the singlediode method doesn't actually get the MPP correct @@ -162,11 +120,4 @@ def test_slow_fs_495(): pvs_ixx = pvsystem.i_from_v(rsh, rs, nnsvt, (voc + vmp)/2, io, il, method='lambertw') assert np.isclose(pvs_ixx, ixx) - return isc, voc, imp, vmp, pmp, i, v, p, pvs - - -if __name__ == '__main__': - r_fast_spr_e20_327 = test_fast_spr_e20_327() - r_fast_fs_495 = test_fast_fs_495() - r_slow_spr_e20_327 = test_slow_spr_e20_327() - r_slow_fs_495 = test_slow_fs_495() + return isc, voc, imp, vmp, pmp, i, v, pvs From 43475cceace75303ff48c7b0ef85674728c06530 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Fri, 29 Jun 2018 17:25:42 -0700 Subject: [PATCH 65/73] MAINT: address comments by @wholmgren * remove extra 'p' from out, in test_pvsystem * make tests in test_singlediode_methods more descriptive, add docstring and change "fast"->"newton" and "slow"->"brentq" * use suggested language for notes in max_power_point() * replace confusing lambdas with real function definitions that are easier to understand in bishop88_i_from_v and v.v., also add a comment explaining why this is necessary * remove import of tools * add FIXME and comment explaining that we need to remove _array_newton at some point in the future, and the reason why we have it here --- pvlib/pvsystem.py | 7 +++--- pvlib/singlediode_methods.py | 31 +++++++++++++++----------- pvlib/test/test_pvsystem.py | 5 ----- pvlib/test/test_singlediode_methods.py | 12 ++++++---- pvlib/tools.py | 8 +++++++ 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 59698f7399..5dd204fd66 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1987,9 +1987,10 @@ def max_power_point(photocurrent, saturation_current, resistance_series, Notes ----- - This function is an option to :func:`singlediode`. Use it when you just - want to find the max power point. It uses Brent's method by default because - it is guaranteed to converge. + Use this function when you only want to find the maximum power point. Use + :func:`singlediode` when you need to find additional points on the IV curve. + This function uses Brent's method by default because it is guaranteed to + converge. """ mpp_fun = partial(singlediode_methods.bishop88_mpp, method=method.lower()) i_mp, v_mp, p_mp = mpp_fun( diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 1fb868d66a..9f5541134d 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -13,7 +13,6 @@ try: from scipy.optimize import _array_newton except ImportError: - from pvlib import tools from pvlib.tools import _array_newton # rename newton and set keyword arguments newton = partial(_array_newton, tol=1e-6, maxiter=100, fprime2=None) @@ -158,12 +157,15 @@ def fv(x, v, *a): raise ImportError('This function requires scipy') # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - # break out arguments for numpy.vectorize to handle broadcasting - vec_fun = np.vectorize( - lambda voc, v, iph, isat, rs, rsh, gamma: - brentq(fv, 0.0, voc, args=(v, iph, isat, rs, rsh, gamma)) - ) - vd = vec_fun(voc_est, voltage, *args) + + # brentq only works with scalar inputs, so we need a set up function + # and np.vectorize to repeatedly call the optimizer with the right + # arguments for possible array input + def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma): + return brentq(fv, 0.0, voc, args=(v, iph, isat, rs, rsh, gamma)) + + vd_from_brent_vectorized = np.vectorize(vd_from_brent) + vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 size, shape = _get_size_and_shape((voltage,) + args) @@ -228,12 +230,15 @@ def fi(x, i, *a): if method.lower() == 'brentq': if brentq is NotImplemented: raise ImportError('This function requires scipy') - # break out arguments for numpy.vectorize to handle broadcasting - vec_fun = np.vectorize( - lambda voc, i, iph, isat, rs, rsh, gamma: - brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma)) - ) - vd = vec_fun(voc_est, current, *args) + + # brentq only works with scalar inputs, so we need a set up function + # and np.vectorize to repeatedly call the optimizer with the right + # arguments for possible array input + def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma): + return brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma)) + + vd_from_brent_vectorized = np.vectorize(vd_from_brent) + vd = vd_from_brent_vectorized(voc_est, current, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 size, shape = _get_size_and_shape((current,) + args) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index c32cd4018b..6659c06d8b 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -931,8 +931,6 @@ def test_singlediode_floats_ivcurve(): 'v': np.array([0., 4.05315, 8.1063])} assert isinstance(out, dict) for k, v in out.items(): - if k == 'p': - continue assert_allclose(v, expected[k], atol=1e-3) @@ -986,9 +984,6 @@ def test_singlediode_series_ivcurve(cec_module_params): method='lambertw').T for k, v in out.items(): - if k == 'p': - # skip power, only in way_faster output - continue assert_allclose(v, expected[k], atol=1e-2) diff --git a/pvlib/test/test_singlediode_methods.py b/pvlib/test/test_singlediode_methods.py index d39e533ffb..93ad8020ed 100644 --- a/pvlib/test/test_singlediode_methods.py +++ b/pvlib/test/test_singlediode_methods.py @@ -12,7 +12,8 @@ @requires_scipy -def test_fast_spr_e20_327(): +def test_newton_spr_e20_327(): + """test pvsystem.singlediode with Newton method on SPR-E20-327""" spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( effective_irradiance=POA, temp_cell=TCELL, @@ -40,7 +41,8 @@ def test_fast_spr_e20_327(): @requires_scipy -def test_fast_fs_495(): +def test_newton_fs_495(): + """test pvsystem.singlediode with Newton method on FS495""" fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( effective_irradiance=POA, temp_cell=TCELL, @@ -68,7 +70,8 @@ def test_fast_fs_495(): @requires_scipy -def test_slow_spr_e20_327(): +def test_brentq_spr_e20_327(): + """test pvsystem.singlediode with Brent method on SPR-E20-327""" spr_e20_327 = CECMOD.SunPower_SPR_E20_327 x = pvsystem.calcparams_desoto( effective_irradiance=POA, temp_cell=TCELL, @@ -96,7 +99,8 @@ def test_slow_spr_e20_327(): @requires_scipy -def test_slow_fs_495(): +def test_brentq_fs_495(): + """test pvsystem.singlediode with Brent method on SPR-E20-327""" fs_495 = CECMOD.First_Solar_FS_495 x = pvsystem.calcparams_desoto( effective_irradiance=POA, temp_cell=TCELL, diff --git a/pvlib/tools.py b/pvlib/tools.py index 5fc790e32f..379bf38cab 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -254,6 +254,14 @@ def _build_kwargs(keys, input_dict): return kwargs +# 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 +# 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 +# info see: https://github.com/scipy/scipy/pull/8357 + def _array_newton(func, x0, fprime, args, tol, maxiter, fprime2, converged=False): """ From 1ad603113c5800418bcef6e59a8254cfd43e77a8 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Mon, 9 Jul 2018 21:09:55 -0700 Subject: [PATCH 66/73] MAINT: add comment to explain why we import brentq in try-except --- pvlib/singlediode_methods.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 9f5541134d..97e5e314fc 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -4,6 +4,10 @@ from functools import partial import numpy as np + +# Try to import brentq from scipy to use when specified in bishop88_i_from_v, +# bishop88_v_from_i, and bishop88_mpp methods below. If not imported, raises +# ImportError when 'brentq' method is specified for those methods. try: from scipy.optimize import brentq except ImportError: From f14ba04972cebbca8081c8ebefe32165b3da5e15 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Mon, 9 Jul 2018 23:17:42 -0700 Subject: [PATCH 67/73] MAINT: move lambertw methods to singlediode_methods.py * closes #497 * remove lines setting ivcurve points to NotImplemented, so that they raise an error if they are not set * replace nested code in if: else condition for lambertw with new private method "_lambertw" in singlediode_methods.py * need to break out returns from private methods, there are so many * move _golden_sect_DataFrame to pvlib.tools * replace nested code in if: else condition for lambertw with new private methods _lambertw_v_from_i and _lambertw_i_from_v in singlediode_methods.py * move _pwr_optfcn to singlediode_methods.py --- pvlib/pvsystem.py | 278 ++--------------------------------- pvlib/singlediode_methods.py | 206 ++++++++++++++++++++++++++ pvlib/tools.py | 72 +++++++++ 3 files changed, 294 insertions(+), 262 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 4b38795301..f5a9526bc8 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1853,7 +1853,8 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None, method='lambertw'): + resistance_shunt, nNsVth, ivcurve_pnts=None, + method='lambertw'): """ Solve the single-diode model to obtain a photovoltaic IV curve. @@ -2022,53 +2023,16 @@ def singlediode(photocurrent, saturation_current, resistance_series, calcparams_desoto pvlib.singlediode_methods.bishop88 """ - # make IDE happy - ivcurve_v = NotImplemented - ivcurve_i = NotImplemented - # Calculate points on the IV curve using the LambertW solution to the # single diode equation if method.lower() == 'lambertw': - # Compute short circuit current - i_sc = i_from_v(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent, method) - - # Compute open circuit voltage - v_oc = v_from_i(resistance_shunt, resistance_series, nNsVth, 0., - saturation_current, photocurrent, method) - - params = {'r_sh': resistance_shunt, - 'r_s': resistance_series, - 'nNsVth': nNsVth, - 'i_0': saturation_current, - 'i_l': photocurrent} - - # Find the voltage, v_mp, where the power is maximized. - # Start the golden section search at v_oc * 1.14 - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, - _pwr_optfcn) - - # Find Imp using Lambert W - i_mp = i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, - saturation_current, photocurrent, method) - - # Find Ix and Ixx using Lambert W - i_x = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5 * v_oc, - saturation_current, photocurrent, method) - - i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, - 0.5 * (v_oc + v_mp), saturation_current, photocurrent, - method) - - # create ivcurve + out = singlediode_methods._lambertw( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts + ) + i_sc, v_oc, i_mp, v_mp, p_mp, i_x, i_xx = out[:7] if ivcurve_pnts: - ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * - np.linspace(0, 1, ivcurve_pnts)) - - ivcurve_i = i_from_v(resistance_shunt, resistance_series, nNsVth, - ivcurve_v.T, saturation_current, photocurrent, - method).T - + ivcurve_i, ivcurve_v = out[7:] else: # Calculate points on the IV curve using either 'newton' or 'brentq' # methods. Voltages are determined by first solving the single diode @@ -2165,89 +2129,6 @@ def max_power_point(photocurrent, saturation_current, resistance_series, return out -# Created April,2014 -# Author: Rob Andrews, Calama Consulting - -def _golden_sect_DataFrame(params, VL, VH, func): - """ - Vectorized golden section search for finding MPP from a dataframe - timeseries. - - Parameters - ---------- - params : dict - Dictionary containing scalars or arrays - of inputs to the function to be optimized. - Each row should represent an independent optimization. - - VL: float - Lower bound of the optimization - - VH: float - Upper bound of the optimization - - func: function - Function to be optimized must be in the form f(array-like, x) - - Returns - ------- - func(df,'V1') : DataFrame - function evaluated at the optimal point - - df['V1']: Dataframe - Dataframe of optimal points - - Notes - ----- - This function will find the MAXIMUM of a function - """ - - df = params - df['VH'] = VH - df['VL'] = VL - - err = df['VH'] - df['VL'] - errflag = True - iterations = 0 - - while errflag: - - phi = (np.sqrt(5)-1)/2*(df['VH']-df['VL']) - df['V1'] = df['VL'] + phi - df['V2'] = df['VH'] - phi - - df['f1'] = func(df, 'V1') - df['f2'] = func(df, 'V2') - df['SW_Flag'] = df['f1'] > df['f2'] - - df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag']) - df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) - - err = df['V1'] - df['V2'] - try: - errflag = (abs(err) > .01).any() - except ValueError: - errflag = (abs(err) > .01) - - iterations += 1 - - if iterations > 50: - raise Exception("EXCEPTION:iterations exceeded maximum (50)") - - return func(df, 'V1'), df['V1'] - - -def _pwr_optfcn(df, loc): - ''' - Function to find power from ``i_from_v``. - ''' - - I = i_from_v(df['r_sh'], df['r_s'], df['nNsVth'], df[loc], df['i_0'], - df['i_l'], method='lambertw') - - return I * df[loc] - - def v_from_i(resistance_shunt, resistance_series, nNsVth, current, saturation_current, photocurrent, method='lambertw'): ''' @@ -2313,84 +2194,10 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current, Energy Materials and Solar Cells, 81 (2004) 269-277. ''' if method.lower() == 'lambertw': - try: - from scipy.special import lambertw - except ImportError: - raise ImportError('This function requires scipy') - - # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - [resistance_shunt, resistance_series, nNsVth, - current, saturation_current, photocurrent])) - - # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which - # is generally more numerically stable - conductance_shunt = 1./resistance_shunt - - # Ensure that we are working with read-only views of numpy arrays - # Turns Series into arrays so that we don't have to worry about - # multidimensional broadcasting failing - Gsh, Rs, a, I, I0, IL = \ - np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, - current, saturation_current, photocurrent) - - # Intitalize output V (I might not be float64) - V = np.full_like(I, np.nan, dtype=np.float64) - - # Determine indices where 0 < Gsh requires implicit model solution - idx_p = 0. < Gsh - - # Determine indices where 0 = Gsh allows explicit model solution - idx_z = 0. == Gsh - - # Explicit solutions where Gsh=0 - if np.any(idx_z): - V[idx_z] = a[idx_z]*np.log1p((IL[idx_z] - I[idx_z])/I0[idx_z]) - \ - I[idx_z]*Rs[idx_z] - - # Only compute using LambertW if there are cases with Gsh>0 - if np.any(idx_p): - # LambertW argument, cannot be float128, may overflow to np.inf - # overflow is explicitly handled below, so ignore warnings here - with np.errstate(over='ignore'): - argW = (I0[idx_p] / (Gsh[idx_p]*a[idx_p]) * - np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p]*a[idx_p]))) - - # lambertw typically returns complex value with zero imaginary part - # may overflow to np.inf - lambertwterm = lambertw(argW).real - - # Record indices where lambertw input overflowed output - idx_inf = np.logical_not(np.isfinite(lambertwterm)) - - # Only re-compute LambertW if it overflowed - if np.any(idx_inf): - # Calculate using log(argW) in case argW is really big - logargW = (np.log(I0[idx_p]) - np.log(Gsh[idx_p]) - - np.log(a[idx_p]) + - (-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p] * a[idx_p]))[idx_inf] - - # Three iterations of Newton-Raphson method to solve - # w+log(w)=logargW. The initial guess is w=logargW. Where direct - # evaluation (above) results in NaN from overflow, 3 iterations - # of Newton's method gives approximately 8 digits of precision. - w = logargW - for _ in range(0, 3): - w = w * (1. - np.log(w) + logargW) / (1. + w) - lambertwterm[idx_inf] = w - - # Eqn. 3 in Jain and Kapoor, 2004 - # V = -I*(Rs + Rsh) + IL*Rsh - a*lambertwterm + I0*Rsh - # Recast in terms of Gsh=1/Rsh for better numerical stability. - V[idx_p] = (IL[idx_p] + I0[idx_p] - I[idx_p])/Gsh[idx_p] - \ - I[idx_p]*Rs[idx_p] - a[idx_p]*lambertwterm - - if output_is_scalar: - return np.asscalar(V) - else: - return V + return singlediode_methods._lambertw_v_from_i( + resistance_shunt, resistance_series, nNsVth, current, + saturation_current, photocurrent + ) else: # Calculate points on the IV curve using either 'newton' or 'brentq' # methods. Voltages are determined by first solving the single diode @@ -2475,63 +2282,10 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, Energy Materials and Solar Cells, 81 (2004) 269-277. ''' if method.lower() == 'lambertw': - try: - from scipy.special import lambertw - except ImportError: - raise ImportError('This function requires scipy') - - # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - [resistance_shunt, resistance_series, nNsVth, - voltage, saturation_current, photocurrent])) - - # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which - # is generally more numerically stable - conductance_shunt = 1./resistance_shunt - - # Ensure that we are working with read-only views of numpy arrays - # Turns Series into arrays so that we don't have to worry about - # multidimensional broadcasting failing - Gsh, Rs, a, V, I0, IL = \ - np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, - voltage, saturation_current, photocurrent) - - # Intitalize output I (V might not be float64) - I = np.full_like(V, np.nan, dtype=np.float64) - - # Determine indices where 0 < Rs requires implicit model solution - idx_p = 0. < Rs - - # Determine indices where 0 = Rs allows explicit model solution - idx_z = 0. == Rs - - # Explicit solutions where Rs=0 - if np.any(idx_z): - I[idx_z] = IL[idx_z] - I0[idx_z]*np.expm1(V[idx_z]/a[idx_z]) - \ - Gsh[idx_z]*V[idx_z] - - # Only compute using LambertW if there are cases with Rs>0 - # Does NOT handle possibility of overflow, github issue 298 - if np.any(idx_p): - # LambertW argument, cannot be float128, may overflow to np.inf - argW = Rs[idx_p]*I0[idx_p]/(a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.)) * \ - np.exp((Rs[idx_p]*(IL[idx_p] + I0[idx_p]) + V[idx_p]) / - (a[idx_p]*(Rs[idx_p]*Gsh[idx_p] + 1.))) - - # lambertw typically returns complex value with zero imaginary part - # may overflow to np.inf - lambertwterm = lambertw(argW).real - - # Eqn. 2 in Jain and Kapoor, 2004 - # I = -V/(Rs + Rsh) - (a/Rs)*lambertwterm + Rsh*(IL + I0)/(Rs + Rsh) - # Recast in terms of Gsh=1/Rsh for better numerical stability. - I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p]*Gsh[idx_p]) / \ - (Rs[idx_p]*Gsh[idx_p] + 1.) - (a[idx_p]/Rs[idx_p])*lambertwterm - - if output_is_scalar: - return np.asscalar(I) - else: - return I + return singlediode_methods._lambertw_i_from_v( + resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent + ) else: # Calculate points on the IV curve using either 'newton' or 'brentq' # methods. Voltages are determined by first solving the single diode diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 97e5e314fc..fdcc522bec 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -4,6 +4,7 @@ from functools import partial import numpy as np +from pvlib.tools import _golden_sect_DataFrame # Try to import brentq from scipy to use when specified in bishop88_i_from_v, # bishop88_v_from_i, and bishop88_mpp methods below. If not imported, raises @@ -354,3 +355,208 @@ def _get_size_and_shape(args): if this_shape is not None: shape = this_shape return size, shape + + +def _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, current, + saturation_current, photocurrent): + try: + from scipy.special import lambertw + except ImportError: + raise ImportError('This function requires scipy') + + # Record if inputs were all scalar + output_is_scalar = all(map(np.isscalar, + [resistance_shunt, resistance_series, nNsVth, + current, saturation_current, photocurrent])) + + # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which + # is generally more numerically stable + conductance_shunt = 1. / resistance_shunt + + # Ensure that we are working with read-only views of numpy arrays + # Turns Series into arrays so that we don't have to worry about + # multidimensional broadcasting failing + Gsh, Rs, a, I, I0, IL = \ + np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, + current, saturation_current, photocurrent) + + # Intitalize output V (I might not be float64) + V = np.full_like(I, np.nan, dtype=np.float64) + + # Determine indices where 0 < Gsh requires implicit model solution + idx_p = 0. < Gsh + + # Determine indices where 0 = Gsh allows explicit model solution + idx_z = 0. == Gsh + + # Explicit solutions where Gsh=0 + if np.any(idx_z): + V[idx_z] = a[idx_z] * np.log1p((IL[idx_z] - I[idx_z]) / I0[idx_z]) - \ + I[idx_z] * Rs[idx_z] + + # Only compute using LambertW if there are cases with Gsh>0 + if np.any(idx_p): + # LambertW argument, cannot be float128, may overflow to np.inf + # overflow is explicitly handled below, so ignore warnings here + with np.errstate(over='ignore'): + argW = (I0[idx_p] / (Gsh[idx_p] * a[idx_p]) * + np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) / + (Gsh[idx_p] * a[idx_p]))) + + # lambertw typically returns complex value with zero imaginary part + # may overflow to np.inf + lambertwterm = lambertw(argW).real + + # Record indices where lambertw input overflowed output + idx_inf = np.logical_not(np.isfinite(lambertwterm)) + + # Only re-compute LambertW if it overflowed + if np.any(idx_inf): + # Calculate using log(argW) in case argW is really big + logargW = (np.log(I0[idx_p]) - np.log(Gsh[idx_p]) - + np.log(a[idx_p]) + + (-I[idx_p] + IL[idx_p] + I0[idx_p]) / + (Gsh[idx_p] * a[idx_p]))[idx_inf] + + # Three iterations of Newton-Raphson method to solve + # w+log(w)=logargW. The initial guess is w=logargW. Where direct + # evaluation (above) results in NaN from overflow, 3 iterations + # of Newton's method gives approximately 8 digits of precision. + w = logargW + for _ in range(0, 3): + w = w * (1. - np.log(w) + logargW) / (1. + w) + lambertwterm[idx_inf] = w + + # Eqn. 3 in Jain and Kapoor, 2004 + # V = -I*(Rs + Rsh) + IL*Rsh - a*lambertwterm + I0*Rsh + # Recast in terms of Gsh=1/Rsh for better numerical stability. + V[idx_p] = (IL[idx_p] + I0[idx_p] - I[idx_p]) / Gsh[idx_p] - \ + I[idx_p] * Rs[idx_p] - a[idx_p] * lambertwterm + + if output_is_scalar: + return np.asscalar(V) + else: + return V + + +def _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, + saturation_current, photocurrent): + try: + from scipy.special import lambertw + except ImportError: + raise ImportError('This function requires scipy') + + # Record if inputs were all scalar + output_is_scalar = all(map(np.isscalar, + [resistance_shunt, resistance_series, nNsVth, + voltage, saturation_current, photocurrent])) + + # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which + # is generally more numerically stable + conductance_shunt = 1. / resistance_shunt + + # Ensure that we are working with read-only views of numpy arrays + # Turns Series into arrays so that we don't have to worry about + # multidimensional broadcasting failing + Gsh, Rs, a, V, I0, IL = \ + np.broadcast_arrays(conductance_shunt, resistance_series, nNsVth, + voltage, saturation_current, photocurrent) + + # Intitalize output I (V might not be float64) + I = np.full_like(V, np.nan, dtype=np.float64) + + # Determine indices where 0 < Rs requires implicit model solution + idx_p = 0. < Rs + + # Determine indices where 0 = Rs allows explicit model solution + idx_z = 0. == Rs + + # Explicit solutions where Rs=0 + if np.any(idx_z): + I[idx_z] = IL[idx_z] - I0[idx_z] * np.expm1(V[idx_z] / a[idx_z]) - \ + Gsh[idx_z] * V[idx_z] + + # Only compute using LambertW if there are cases with Rs>0 + # Does NOT handle possibility of overflow, github issue 298 + if np.any(idx_p): + # LambertW argument, cannot be float128, may overflow to np.inf + argW = Rs[idx_p] * I0[idx_p] / ( + a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.)) * \ + np.exp((Rs[idx_p] * (IL[idx_p] + I0[idx_p]) + V[idx_p]) / + (a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.))) + + # lambertw typically returns complex value with zero imaginary part + # may overflow to np.inf + lambertwterm = lambertw(argW).real + + # Eqn. 2 in Jain and Kapoor, 2004 + # I = -V/(Rs + Rsh) - (a/Rs)*lambertwterm + Rsh*(IL + I0)/(Rs + Rsh) + # Recast in terms of Gsh=1/Rsh for better numerical stability. + I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p] * Gsh[idx_p]) / \ + (Rs[idx_p] * Gsh[idx_p] + 1.) - ( + a[idx_p] / Rs[idx_p]) * lambertwterm + + if output_is_scalar: + return np.asscalar(I) + else: + return I + + +def _lambertw(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, ivcurve_pnts=None): + # Compute short circuit current + i_sc = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, 0., + saturation_current, photocurrent) + + # Compute open circuit voltage + v_oc = _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, 0., + saturation_current, photocurrent) + + params = {'r_sh': resistance_shunt, + 'r_s': resistance_series, + 'nNsVth': nNsVth, + 'i_0': saturation_current, + 'i_l': photocurrent} + + # Find the voltage, v_mp, where the power is maximized. + # Start the golden section search at v_oc * 1.14 + p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, + _pwr_optfcn) + + # Find Imp using Lambert W + i_mp = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, + saturation_current, photocurrent) + + # Find Ix and Ixx using Lambert W + i_x = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, + 0.5 * v_oc, saturation_current, photocurrent) + + i_xx = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, + 0.5 * (v_oc + v_mp), saturation_current, + photocurrent) + + out = (i_sc, v_oc, i_mp, v_mp, p_mp, i_x, i_xx) + + # create ivcurve + if ivcurve_pnts: + ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * + np.linspace(0, 1, ivcurve_pnts)) + + ivcurve_i = _lambertw_i_from_v(resistance_shunt, resistance_series, + nNsVth, ivcurve_v.T, saturation_current, + photocurrent).T + + out += (ivcurve_i, ivcurve_v) + + return out + + +def _pwr_optfcn(df, loc): + ''' + Function to find power from ``i_from_v``. + ''' + + I = _lambertw_i_from_v(df['r_sh'], df['r_s'], df['nNsVth'], df[loc], + df['i_0'], df['i_l']) + + return I * df[loc] diff --git a/pvlib/tools.py b/pvlib/tools.py index 379bf38cab..16596af885 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -356,3 +356,75 @@ def _array_newton(func, x0, fprime, args, tol, maxiter, fprime2, result = namedtuple('result', ('root', 'converged', 'zero_der')) p = result(p, ~failures, zero_der) return p + + +# Created April,2014 +# Author: Rob Andrews, Calama Consulting + +def _golden_sect_DataFrame(params, VL, VH, func): + """ + Vectorized golden section search for finding MPP from a dataframe + timeseries. + + Parameters + ---------- + params : dict + Dictionary containing scalars or arrays + of inputs to the function to be optimized. + Each row should represent an independent optimization. + + VL: float + Lower bound of the optimization + + VH: float + Upper bound of the optimization + + func: function + Function to be optimized must be in the form f(array-like, x) + + Returns + ------- + func(df,'V1') : DataFrame + function evaluated at the optimal point + + df['V1']: Dataframe + Dataframe of optimal points + + Notes + ----- + This function will find the MAXIMUM of a function + """ + + df = params + df['VH'] = VH + df['VL'] = VL + + err = df['VH'] - df['VL'] + errflag = True + iterations = 0 + + while errflag: + + phi = (np.sqrt(5)-1)/2*(df['VH']-df['VL']) + df['V1'] = df['VL'] + phi + df['V2'] = df['VH'] - phi + + df['f1'] = func(df, 'V1') + df['f2'] = func(df, 'V2') + df['SW_Flag'] = df['f1'] > df['f2'] + + df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag']) + df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) + + err = df['V1'] - df['V2'] + try: + errflag = (abs(err) > .01).any() + except ValueError: + errflag = (abs(err) > .01) + + iterations += 1 + + if iterations > 50: + raise Exception("EXCEPTION:iterations exceeded maximum (50)") + + return func(df, 'V1'), df['V1'] From 8d865607837e3dddd2766747ccc9b12f91aa1a09 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 00:01:39 -0700 Subject: [PATCH 68/73] MAINT: add docstring elaborating the numerical precision test * explain how to generate the high-precision test data by running the file from the command line --- pvlib/test/test_numerical_precision.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pvlib/test/test_numerical_precision.py b/pvlib/test/test_numerical_precision.py index 839f0f1088..8d4ebd3d7d 100644 --- a/pvlib/test/test_numerical_precision.py +++ b/pvlib/test/test_numerical_precision.py @@ -1,6 +1,19 @@ """ -Numerical Precision +Test numerical precision of explicit single diode calculation using symbolic +mathematics. SymPy is a computer algebra system, that uses infinite precision +symbols instead of standard floating point and integer computer number types. http://docs.sympy.org/latest/modules/evalf.html#accuracy-and-error-handling + +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`:: + + $ 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 +high-precision values generated with SymPy. """ import logging From 337d7b4c9895c1a5e75b7f6b76dd4c91e1bf8b3f Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 00:21:30 -0700 Subject: [PATCH 69/73] MAINT: use np.expm1(x) for exp(x) - 1 in bishop88 single diode method * closes #500 * also add a comment that we are creating some temporary values to make calculations simpler * also use more descriptive names instead of a, b, c, use v_star, g_sh, and g_diode --- pvlib/singlediode_methods.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index fdcc522bec..956e54b86d 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -97,19 +97,21 @@ def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, :math:`\\frac{dI}{dV}`, :math:`\\frac{dP}{dV}`, and :math:`\\frac{d^2 P}{dV dV_d}` """ - a = np.exp(diode_voltage / nNsVth) - b = 1.0 / resistance_shunt - i = photocurrent - saturation_current * (a - 1.0) - diode_voltage * b + # calculate temporary values to simplify calculations + v_star = diode_voltage / nNsVth # non-dimensional diode voltage + g_sh = 1.0 / resistance_shunt # conductance + i = (photocurrent - saturation_current * np.expm1(v_star) + - diode_voltage * g_sh) v = diode_voltage - i * resistance_series retval = (i, v, i*v) if gradients: - c = saturation_current * a / nNsVth - grad_i = - c - b # di/dvd + g_diode = saturation_current * np.exp(v_star) / nNsVth # conductance + grad_i = -g_diode - g_sh # di/dvd grad_v = 1.0 - grad_i * resistance_series # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i grad = grad_i / grad_v # di/dv grad_p = v * grad + i # dp/dv - grad2i = -c / nNsVth # d2i/dvd + grad2i = -g_diode / nNsVth # d2i/dvd grad2v = -grad2i * resistance_series # d2v/dvd grad2p = ( grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) From ce5b0f554d77ffacf1543f59d76459613c693790 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 01:05:51 -0700 Subject: [PATCH 70/73] MAINT: replace boilerplate code for broadcasting newton array args * closes #498 * adds _prepare_newton_inputs(i_or_v_tup, args, v0) * also replace verbose comment with more descriptive one to copy v0 if it's an array to use for initial guess --- pvlib/singlediode_methods.py | 52 +++++++++++++----------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index 956e54b86d..aacf97e5fa 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -175,17 +175,8 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma): vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 - size, shape = _get_size_and_shape((voltage,) + args) - if size > 1: - args = [np.asarray(arg) for arg in args] - # newton uses initial guess for the output shape - # copy v0 to a new array and broadcast it to the shape of max size - if shape is not None: - v0 = np.broadcast_to(voltage, shape).copy() - else: - v0 = voltage - # x0 and x in func are the same reference! DO NOT set x0 to voltage! - # voltage in fv(x, voltage, *a) MUST BE CONSTANT! + # if voltage is an array, then make a copy to use for initial guess, v0 + args, v0 = _prepare_newton_inputs((voltage,), args, voltage) vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args) @@ -248,17 +239,8 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma): vd = vd_from_brent_vectorized(voc_est, current, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 - size, shape = _get_size_and_shape((current,) + args) - if size > 1: - args = [np.asarray(arg) for arg in args] - # newton uses initial guess for the output shape - # copy v0 to a new array and broadcast it to the shape of max size - if shape is not None: - v0 = np.broadcast_to(voc_est, shape).copy() - else: - v0 = voc_est - # x0 and x in func are the same reference! DO NOT set x0 to current! - # voltage in fi(x, current, *a) MUST BE CONSTANT! + # if voc_est is an array, then make a copy to use for initial guess, v0 + args, v0 = _prepare_newton_inputs((current,), args, voc_est) vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args) @@ -315,17 +297,8 @@ def fmpp(x, *a): vd = vec_fun(voc_est, *args) elif method.lower() == 'newton': # make sure all args are numpy arrays if max size > 1 - size, shape = _get_size_and_shape(args) - if size > 1: - args = [np.asarray(arg) for arg in args] - # newton uses initial guess for the output shape - # copy v0 to a new array and broadcast it to the shape of max size - if shape is not None: - v0 = np.broadcast_to(voc_est, shape).copy() - else: - v0 = voc_est - # x0 and x in func are the same reference! DO NOT set x0 to current! - # voltage in fi(x, current, *a) MUST BE CONSTANT! + # if voc_est is an array, then make a copy to use for initial guess, v0 + args, v0 = _prepare_newton_inputs((), args, voc_est) vd = newton( func=fmpp, x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args @@ -359,6 +332,19 @@ def _get_size_and_shape(args): return size, shape +def _prepare_newton_inputs(i_or_v_tup, args, v0): + # broadcast arguments for newton method + # the first argument should be a tuple, eg: (i,), (v,) or () + size, shape = _get_size_and_shape(i_or_v_tup + args) + if size > 1: + args = [np.asarray(arg) for arg in args] + # newton uses initial guess for the output shape + # copy v0 to a new array and broadcast it to the shape of max size + if shape is not None: + v0 = np.broadcast_to(v0, shape).copy() + return args, v0 + + def _lambertw_v_from_i(resistance_shunt, resistance_series, nNsVth, current, saturation_current, photocurrent): try: From 3e009f8c73d87471534e09d89fc973bd0d31f4d1 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 01:23:59 -0700 Subject: [PATCH 71/73] MAINT: TEST: parametrize i_from_v and v_from_i for methods, atol * closes #499 --- pvlib/test/test_pvsystem.py | 108 ++++-------------------------------- 1 file changed, 10 insertions(+), 98 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 53fcf0ca57..729a654ccc 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -608,7 +608,10 @@ def fixture_v_from_i(request): @requires_scipy -def test_v_from_i(fixture_v_from_i): +@pytest.mark.parametrize( + 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-8)] +) +def test_v_from_i(fixture_v_from_i, method, atol): # Solution set loaded from fixture Rsh = fixture_v_from_i['Rsh'] Rs = fixture_v_from_i['Rs'] @@ -618,54 +621,7 @@ def test_v_from_i(fixture_v_from_i): IL = fixture_v_from_i['IL'] V_expected = fixture_v_from_i['V_expected'] - # Convergence criteria - atol = 1.e-11 - - V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL) - assert(isinstance(V, type(V_expected))) - if isinstance(V, type(np.ndarray)): - assert(isinstance(V.dtype, type(V_expected.dtype))) - assert(V.shape == V_expected.shape) - assert_allclose(V, V_expected, atol=atol) - - -@requires_scipy -def test_v_from_i_brentq(fixture_v_from_i): - # Solution set loaded from fixture - Rsh = fixture_v_from_i['Rsh'] - Rs = fixture_v_from_i['Rs'] - nNsVth = fixture_v_from_i['nNsVth'] - I = fixture_v_from_i['I'] - I0 = fixture_v_from_i['I0'] - IL = fixture_v_from_i['IL'] - V_expected = fixture_v_from_i['V_expected'] - - # Convergence criteria - atol = 1.e-11 - - V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL, method='brentq') - assert(isinstance(V, type(V_expected))) - if isinstance(V, type(np.ndarray)): - assert(isinstance(V.dtype, type(V_expected.dtype))) - assert(V.shape == V_expected.shape) - assert_allclose(V, V_expected, atol=atol) - - -@requires_scipy -def test_v_from_i_newton(fixture_v_from_i): - # Solution set loaded from fixture - Rsh = fixture_v_from_i['Rsh'] - Rs = fixture_v_from_i['Rs'] - nNsVth = fixture_v_from_i['nNsVth'] - I = fixture_v_from_i['I'] - I0 = fixture_v_from_i['I0'] - IL = fixture_v_from_i['IL'] - V_expected = fixture_v_from_i['V_expected'] - - # Convergence criteria - atol = 1.e-8 - - V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL, method='newton') + V = pvsystem.v_from_i(Rsh, Rs, nNsVth, I, I0, IL, method=method) assert(isinstance(V, type(V_expected))) if isinstance(V, type(np.ndarray)): assert(isinstance(V.dtype, type(V_expected.dtype))) @@ -772,7 +728,10 @@ def fixture_i_from_v(request): @requires_scipy -def test_i_from_v(fixture_i_from_v): +@pytest.mark.parametrize( + 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11)] +) +def test_i_from_v(fixture_i_from_v, method, atol): # Solution set loaded from fixture Rsh = fixture_i_from_v['Rsh'] Rs = fixture_i_from_v['Rs'] @@ -782,54 +741,7 @@ def test_i_from_v(fixture_i_from_v): IL = fixture_i_from_v['IL'] I_expected = fixture_i_from_v['I_expected'] - # Convergence criteria - atol = 1.e-11 - - I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='lambertw') - assert(isinstance(I, type(I_expected))) - if isinstance(I, type(np.ndarray)): - assert(isinstance(I.dtype, type(I_expected.dtype))) - assert(I.shape == I_expected.shape) - assert_allclose(I, I_expected, atol=atol) - - -@requires_scipy -def test_i_from_v_brentq(fixture_i_from_v): - # Solution set loaded from fixture - Rsh = fixture_i_from_v['Rsh'] - Rs = fixture_i_from_v['Rs'] - nNsVth = fixture_i_from_v['nNsVth'] - V = fixture_i_from_v['V'] - I0 = fixture_i_from_v['I0'] - IL = fixture_i_from_v['IL'] - I_expected = fixture_i_from_v['I_expected'] - - # Convergence criteria - atol = 1.e-11 - - I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='brentq') - assert(isinstance(I, type(I_expected))) - if isinstance(I, type(np.ndarray)): - assert(isinstance(I.dtype, type(I_expected.dtype))) - assert(I.shape == I_expected.shape) - assert_allclose(I, I_expected, atol=atol) - - -@requires_scipy -def test_i_from_v_newton(fixture_i_from_v): - # Solution set loaded from fixture - Rsh = fixture_i_from_v['Rsh'] - Rs = fixture_i_from_v['Rs'] - nNsVth = fixture_i_from_v['nNsVth'] - V = fixture_i_from_v['V'] - I0 = fixture_i_from_v['I0'] - IL = fixture_i_from_v['IL'] - I_expected = fixture_i_from_v['I_expected'] - - # Convergence criteria - atol = 1.e-11 - - I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method='newton') + I = pvsystem.i_from_v(Rsh, Rs, nNsVth, V, I0, IL, method=method) assert(isinstance(I, type(I_expected))) if isinstance(I, type(np.ndarray)): assert(isinstance(I.dtype, type(I_expected.dtype))) From 082dfd59d0153fe7bd6eed86e7ef4e456e4a17c0 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 01:43:17 -0700 Subject: [PATCH 72/73] MAINT: remove import of functools.partial in pvsystem.py * replace partial functions v_from_i_fun, i_from_v_fun, and mpp_fun with full names, and extra keyword argument method=method.lower() everywhere --- pvlib/pvsystem.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f5a9526bc8..d4ae752f64 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -6,7 +6,6 @@ from __future__ import division from collections import OrderedDict -from functools import partial import os import io try: @@ -2037,19 +2036,23 @@ def singlediode(photocurrent, saturation_current, resistance_series, # Calculate points on the IV curve using either 'newton' or 'brentq' # methods. Voltages are determined by first solving the single diode # equation for the diode voltage V_d then backing out voltage - v_from_i_fun = partial(singlediode_methods.bishop88_v_from_i, - method=method.lower()) - i_from_v_fun = partial(singlediode_methods.bishop88_i_from_v, - method=method.lower()) - mpp_fun = partial(singlediode_methods.bishop88_mpp, - method=method.lower()) args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) # collect args - v_oc = v_from_i_fun(0.0, *args) - i_mp, v_mp, p_mp = mpp_fun(*args) - i_sc = i_from_v_fun(0.0, *args) - i_x = i_from_v_fun(v_oc / 2.0, *args) - i_xx = i_from_v_fun((v_oc + v_mp) / 2.0, *args) + v_oc = singlediode_methods.bishop88_v_from_i( + 0.0, *args, method=method.lower() + ) + i_mp, v_mp, p_mp = singlediode_methods.bishop88_mpp( + *args, method=method.lower() + ) + i_sc = singlediode_methods.bishop88_i_from_v( + 0.0, *args, method=method.lower() + ) + i_x = singlediode_methods.bishop88_i_from_v( + v_oc / 2.0, *args, method=method.lower() + ) + i_xx = singlediode_methods.bishop88_i_from_v( + (v_oc + v_mp) / 2.0, *args, method=method.lower() + ) # calculate the IV curve if requested using bishop88 if ivcurve_pnts: @@ -2113,10 +2116,9 @@ def max_power_point(photocurrent, saturation_current, resistance_series, This function uses Brent's method by default because it is guaranteed to converge. """ - mpp_fun = partial(singlediode_methods.bishop88_mpp, method=method.lower()) - i_mp, v_mp, p_mp = mpp_fun( + i_mp, v_mp, p_mp = singlediode_methods.bishop88_mpp( photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth + resistance_shunt, nNsVth, method=method.lower() ) if isinstance(photocurrent, pd.Series): ivp = {'i_mp': i_mp, 'v_mp': v_mp, 'p_mp': p_mp} From ceb69cdfcdfb88e8d6300a7a94d0ef1a4ff1c722 Mon Sep 17 00:00:00 2001 From: Mark Mikofski Date: Tue, 10 Jul 2018 01:55:56 -0700 Subject: [PATCH 73/73] MAINT: wrap lines longer than 79 characters --- pvlib/pvsystem.py | 10 +++++----- pvlib/singlediode_methods.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d4ae752f64..43c07deda1 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2099,8 +2099,8 @@ def max_power_point(photocurrent, saturation_current, resistance_series, resistance_shunt : numeric shunt resitance [ohms] nNsVth : numeric - product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, and - number of serices cells ``Ns`` + product of thermal voltage ``Vth`` [V], diode ideality factor ``n``, + and number of serices cells ``Ns`` method : str either ``'newton'`` or ``'brentq'`` @@ -2112,9 +2112,9 @@ def max_power_point(photocurrent, saturation_current, resistance_series, Notes ----- Use this function when you only want to find the maximum power point. Use - :func:`singlediode` when you need to find additional points on the IV curve. - This function uses Brent's method by default because it is guaranteed to - converge. + :func:`singlediode` when you need to find additional points on the IV + 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( photocurrent, saturation_current, resistance_series, diff --git a/pvlib/singlediode_methods.py b/pvlib/singlediode_methods.py index aacf97e5fa..048870d13a 100644 --- a/pvlib/singlediode_methods.py +++ b/pvlib/singlediode_methods.py @@ -46,10 +46,10 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): Notes ----- Calculating the open circuit voltage, :math:`V_{oc}`, of an ideal device - with infinite shunt resistance, :math:`R_{sh} \\to \\infty`, and zero series - resistance, :math:`R_s = 0`, yields the following equation [1]. As an - estimate of :math:`V_{oc}` it is useful as an upper bound for the bisection - method. + with infinite shunt resistance, :math:`R_{sh} \\to \\infty`, and zero + series resistance, :math:`R_s = 0`, yields the following equation [1]. As + an estimate of :math:`V_{oc}` it is useful as an upper bound for the + bisection method. .. math:: @@ -61,8 +61,8 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): return nNsVth * np.log(np.asarray(photocurrent) / saturation_current + 1.0) -def bishop88(diode_voltage, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, gradients=False): +def bishop88(diode_voltage, photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth, gradients=False): """ Explicit calculation of points on the IV curve described by the single diode equation [1]. @@ -512,8 +512,8 @@ def _lambertw(photocurrent, saturation_current, resistance_series, _pwr_optfcn) # Find Imp using Lambert W - i_mp = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, v_mp, - saturation_current, photocurrent) + i_mp = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth, + v_mp, saturation_current, photocurrent) # Find Ix and Ixx using Lambert W i_x = _lambertw_i_from_v(resistance_shunt, resistance_series, nNsVth,