-
Notifications
You must be signed in to change notification settings - Fork 998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: add methods and tests for a explicit IV curve calculation of single-diode model #409
Changes from 4 commits
22d2e5c
bb7c49a
36d4f78
817a303
b17a8cd
0666d29
595e254
5ade588
7179096
4b66f87
664d7d8
09a9e14
06be522
1aba56a
f09be91
4905363
a3d2bb4
8ee1b94
c9a893a
c5248bb
68c65c3
41e0c83
d7b6e62
aa3d29a
9fc350d
54e8d18
16ad9b4
7aca302
086e73f
9f2b157
555e946
52a8e88
c0e18a5
09c6a75
ff8cc0b
446fa9e
f976f61
db88022
a509dd3
62010c2
5c939b8
66a801d
df1423b
eb20cf4
e3805ef
5f89578
ba84fcf
0f3893c
d6023b1
80b3352
ef20676
98d1c01
4ecd913
bf05d2d
edc445d
f542d15
cc6c976
22c53fc
442a3d4
6bcffe8
e6b60c6
0fc9c83
fc6cee1
28c8ffb
58361c1
5f9ed41
43475cc
c79ab97
1ad6031
f14ba04
8d86560
337d7b4
ce5b0f5
3e009f8
082dfd5
ceb69cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
""" | ||
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() | ||
dt_slow = tstop - tstart | ||
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() | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this change in this PR, because default method is no longer LambertW? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✔️ |
||
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_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 = 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) | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this kind of a bigish deal? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I calculate a ~0.02% (absolute) difference between my I_mp_A and V_mp_V values (pvfit) and the pvlib ones for the SunPower module. pvlib calculation: pvfit calculation: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this change in this PR, because default method is no longer LambertW? |
||
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 | ||
|
||
if __name__ == '__main__': | ||
r_spr_e20_327 = test_spr_e20_327() | ||
r_fs_495 = test_fs_495() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
""" | ||
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__) | ||
LOGGER.setLevel(logging.DEBUG) | ||
|
||
EPS = np.finfo(float).eps | ||
DAMP = 1.5 | ||
DELTA = EPS**0.33 | ||
|
||
|
||
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): | ||
""" | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why I vote for moving to gsh := 1/rsh for easier handling of ideal devices. |
||
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 | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change this to |
||
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 | ||
# 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the most straightforward option for current computation at specific terminal voltages might be to directly implement a vectorized, derivative-free brent algorithm with a sensible root bracket that will "guarantee" convergence. I am investigating better ways to vectorize, say, scipy's brentq for this purpose. It would be a shame to have to rewrite it from scratch just to get performant vectorization. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't reviewed all of this, but just want to suggest that simplicity should be a higher objective than speed for the reference implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe what I'm suggesting is the simplest method guaranteed to give the most accurate answer with the governing equations:
Further ideological simplifications:
And I'm working on simplifying both of those methods too. E.g. I think we can use the canned I am very open to changes. I want to make this as simple and robust as possible. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, @mikofski , I was not commenting on your code at all but rather referring to @thunderfish24 suggestion to vectorize things. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I understand now. Thanks @adriesse I'm sorry too. @thunderfish24 May I suggest we move proposals for customized improvements to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Bishop method is a sufficient and extremely simple "ground truth" algorithm at the "uncontrolled" terminal voltages that it produces. However, I am sufficiently skeptical that I would want to see a consensus of 2+ algorithms produce the same result (up to some tolerance) at any other voltage values that require an iterative solver, be it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To illustrate brentq() approach more concretely: #412 |
||
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): | ||
"""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) | ||
# 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) | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
timeit
might be better here