Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions pvlib/singlediode_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
# rename newton and set keyword arguments
newton = partial(_array_newton, tol=1e-6, maxiter=100, fprime2=None)

VOLTAGE_BUILTIN = 0.9 # (V) intrinsic voltage for a:Si, CdTe, Mertens et al.


def estimate_voc(photocurrent, saturation_current, nNsVth):
"""
Expand Down Expand Up @@ -62,7 +64,9 @@ def estimate_voc(photocurrent, saturation_current, nNsVth):


def bishop88(diode_voltage, photocurrent, saturation_current,
resistance_series, resistance_shunt, nNsVth, gradients=False):
resistance_series, resistance_shunt, nNsVth, d2mutau=0,
cells_in_series=None, voltage_builtin=VOLTAGE_BUILTIN,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit - re-order the arguments to keep the non-recombination current terms first, followed by d2mutau and voltage_builtin - just move cells_in_series up one position.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

gradients=False):
"""
Explicit calculation of points on the IV curve described by the single
diode equation [1].
Expand Down Expand Up @@ -97,21 +101,31 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
:math:`\\frac{dI}{dV}`, :math:`\\frac{dP}{dV}`, and
:math:`\\frac{d^2 P}{dV dV_d}`
"""
# check if need to calculate recombination loss current
i_recomb, v_recomb = 0, np.inf
if d2mutau > 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d2mutau : numeric implies that it can be an array (see here). This if statement will error if d2mutau is an array. Perhaps the docstring should be changed to scalar?

v_recomb = voltage_builtin * cells_in_series - diode_voltage
i_recomb = photocurrent * d2mutau / v_recomb
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go with

else:
    i_recomb, v_recomb = 0, np.inf

instead of defining them first, then potentially redefining them. Seems cleaner, no?

# 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)
- diode_voltage * g_sh - i_recomb)
v = diode_voltage - i * resistance_series
retval = (i, v, i*v)
if gradients:
# check again if need to calculate recombination loss current gradients
grad_i_recomb = grad_2i_recomb = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No offense, but I don't care for this assignment pattern. Two lines, please.

if d2mutau > 0:
grad_i_recomb = i_recomb / v_recomb
grad_2i_recomb = 2 * grad_i_recomb / v_recomb
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here, too, I prefer an else statement.

g_diode = saturation_current * np.exp(v_star) / nNsVth # conductance
grad_i = -g_diode - g_sh # di/dvd
grad_i = -g_diode - g_sh - grad_i_recomb # 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 = -g_diode / nNsVth # d2i/dvd
grad2i = -g_diode / nNsVth - grad_2i_recomb # d2i/dvd
grad2v = -grad2i * resistance_series # d2v/dvd
grad2p = (
grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2)
Expand Down
133 changes: 133 additions & 0 deletions pvlib/test/test_singlediode_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import numpy as np
from pvlib import pvsystem
from pvlib.singlediode_methods import bishop88, estimate_voc, VOLTAGE_BUILTIN
from conftest import requires_scipy

POA = 888
Expand Down Expand Up @@ -125,3 +126,135 @@ def test_brentq_fs_495():
method='lambertw')
assert np.isclose(pvs_ixx, ixx)
return isc, voc, imp, vmp, pmp, i, v, pvs


def pvsyst_fs_495():
"""
PVSyst First Solar FS-495 parameters.
Copy link
Member

@cwhanse cwhanse Aug 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calculate PVsyst parameters for First Solar FS-495 module.

Actually I would prefer to simply hard-code the dict of results, and provide a brief statement for the source of these values - where does the initial fs_495 data come from?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ the initial data comes from the PVSyst v6.7.2 database, I don't think any of the values were altered but it was vetted internally.

I've removed the method, and hard coded the values with a comment re: initial source.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having a current PVsyst license, I'm guessing that it does NOT permit uploading that database to pvlib?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not the whole database, but I'm guessing the relevant values come from pan files generated by FS.


Returns
-------
dictionary of PVSyst First Solar FS-495 parameters
"""
fs_495 = dict(d2mutau=1.31, alpha_sc=0.00039, gamma_ref=1.48,
mu_gamma=0.001, I_o_ref=0.962e-9, R_sh_ref=5000,
R_sh_0=12500, R_sh_exp=3.1, R_s=4.6, beta_oc=-0.2116,
EgRef=1.475, cells_in_series=108, cells_in_parallel=2,
I_sc_ref=1.55, V_oc_ref=86.5, I_mp_ref=1.4,
V_mp_ref=67.85,
temp_ref=25, irrad_ref=1000)
Vt = 0.025693001600485238 # thermal voltage at reference (V)
nNsVt = fs_495['cells_in_series'] * fs_495['gamma_ref'] * Vt
Vd = fs_495['I_sc_ref'] * fs_495['R_s'] # diode voltage at short circuit
Id = fs_495['I_o_ref'] * (np.exp(Vd / nNsVt) - 1) # diode current (A)
Ish = Vd / fs_495['R_sh_ref'] # shunt current (A)
# builtin potential difference (V)
dv = VOLTAGE_BUILTIN * fs_495['cells_in_series'] - Vd
# calculate photo-generated current at reference condition (A)
fs_495['I_L_ref'] = (
(fs_495['I_sc_ref'] + Id + Ish) / (1 - fs_495['d2mutau'] / dv)
)
return fs_495


PVSYST_FS_495 = pvsyst_fs_495() # PVSyst First Solar FS-495 parameters


def test_pvsyst_fs_495_recombination_loss():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test is more general than just the fs_495 module - I'd drop that nomenclature from the function name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ agreed, changed to test_pvsyst_recombination_loss

"""test pvsystem.singlediode with Brent method on SPR-E20-327"""
poa, temp_cell = 1000.0, 25.0 # test conditions

# first evaluate DeSoto model
cec_fs_495 = CECMOD.First_Solar_FS_495 # CEC parameters for
il_cec, io_cec, rs_cec, rsh_cec, nnsvt_cec = pvsystem.calcparams_desoto(
effective_irradiance=poa, temp_cell=temp_cell,
alpha_sc=cec_fs_495.alpha_sc, a_ref=cec_fs_495.a_ref,
I_L_ref=cec_fs_495.I_L_ref, I_o_ref=cec_fs_495.I_o_ref,
R_sh_ref=cec_fs_495.R_sh_ref, R_s=cec_fs_495.R_s,
EgRef=1.475, dEgdT=-0.0003
)
voc_est_cec = estimate_voc(photocurrent=il_cec, saturation_current=io_cec,
nNsVth=nnsvt_cec)
vd_cec = np.linspace(0, voc_est_cec, 1000)
desoto = bishop88(
diode_voltage=vd_cec, photocurrent=il_cec, saturation_current=io_cec,
resistance_series=rs_cec, resistance_shunt=rsh_cec, nNsVth=nnsvt_cec
)

# now evaluate PVSyst model with thin-film recombination loss current
pvsyst_fs_495 = PVSYST_FS_495
x = pvsystem.calcparams_pvsyst(
effective_irradiance=poa, temp_cell=temp_cell,
alpha_sc=pvsyst_fs_495['alpha_sc'],
gamma_ref=pvsyst_fs_495['gamma_ref'],
mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'],
I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'],
R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'],
R_s=pvsyst_fs_495['R_s'],
cells_in_series=pvsyst_fs_495['cells_in_series'],
EgRef=pvsyst_fs_495['EgRef']
)
il_pvsyst, io_pvsyst, rs_pvsyst, rsh_pvsyst, nnsvt_pvsyst = x
voc_est_pvsyst = estimate_voc(photocurrent=il_pvsyst,
saturation_current=io_pvsyst,
nNsVth=nnsvt_pvsyst)
vd_pvsyst = np.linspace(0, voc_est_pvsyst, 1000)
pvsyst = bishop88(
diode_voltage=vd_pvsyst, photocurrent=il_pvsyst,
saturation_current=io_pvsyst, resistance_series=rs_pvsyst,
resistance_shunt=rsh_pvsyst, nNsVth=nnsvt_pvsyst,
d2mutau=pvsyst_fs_495['d2mutau'],
cells_in_series=pvsyst_fs_495['cells_in_series']
)

# test expected change in max power
assert np.isclose(max(desoto[2]) - max(pvsyst[2]), 0.01949420697212645)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes if the Desoto model and the PVsyst model are 'close', meaning that the test would fail is the CEC parameter database entry for this module changes, if there is an error in the Desoto functions, or if the PVsyst function has an error. I'd change the test condition to compare the PVsyst output to known values calculated by a separate method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ I've changed the test to be independent of DeSoto, now it only tests 2 conditions:

  • reference condition, should agree with its own parameters
  • another condition, (888[W/m^2], 55[degC]), for which I calculated the expected output using calcparams_pvsyst and bishop88

But I should note that the output at (888[W/m^2], 55[degC]) doesn't agree well with DeSoto at the same conditions:

In [1]: from pvlib import pvsystem
In [2]: CECMOD = pvsystem.retrieve_sam('cecmod')
In [3]: fs_495 = CECMOD.First_Solar_FS_495
In [4]: il, io, rs, rsh, nnsvt= pvsystem.calcparams_desoto(888, 55,
   ...: 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)
In [5]: pvs = pvsystem.singlediode(il, io, rs, rsh, nnsvt, method='lambertw')
In [6]: pvs
Out[6]:
OrderedDict([('i_sc', 1.4020527838304147),  # PVSyst: 1.3868344548308347
             ('v_oc', 75.41704855182024),  # PVSyst: 79.29198489135703
             ('i_mp', 1.2599626119945482),
             ('v_mp', 57.70813302772382),
             ('p_mp', 72.71009002293975),  # PVSyst: 76.26211540956041 
             ('i_x', 1.3606253515830833),
             ('i_xx', 0.8340096657730403)])

According to @jdnewmil this is possibly due to a setting in PVSyst that adjusts d2mutau to a fixed max power temp coeff instead (or in addition???) to the adjustment on gamma. Even though I can see these check boxes in PVSyst, I'm not sure to what degree we want to copy or guess what PVSyst does here in PVLib (or in SAM either @timorichert)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwhanse if we wanted to try to emulate this 2nd order d2mutau behavior, then IMO it should go into calcparams_pvsyst, and so those values: d2mutau (and voltage_builtin if multi junction?) should be outputs from there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. I don't know what PVsyst might be doing behind the interface when d2mutau is in action, however, so I can't help clarify here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it isn't clear what PVsyst is doing, then I suggest mailing Bruno to get clarification. I think they're quite open about the calculations.

# test expected change in short circuit current
isc_desoto = np.interp(0, desoto[1], desoto[0])
isc_pvsyst = np.interp(0, pvsyst[1], pvsyst[0])
assert np.isclose(isc_desoto - isc_pvsyst, -7.955827628380874e-05)
# test expected change in open circuit current
voc_desoto = np.interp(0, desoto[0][::-1], desoto[1][::-1])
voc_pvsyst = np.interp(0, pvsyst[0][::-1], pvsyst[1][::-1])
assert np.isclose(voc_desoto - voc_pvsyst, -0.04184247739671321)
return desoto, pvsyst


if __name__ == '__main__':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although great for development and testing, I believe we don't keep if __name__ == '__main__': blocks in modules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ agreed, removed

import matplotlib.pyplot as plt
a, b = test_pvsyst_fs_495_recombination_loss()
ab0 = np.interp(a[1], b[1], b[0])
pmpa, pmpb = max(a[2]), max(b[2])
isca = np.interp(0, a[1], a[0])
iscb = np.interp(0, b[1], b[0])
voca = np.interp(0, a[0][::-1], a[1][::-1])
vocb = np.interp(0, b[0][::-1], b[1][::-1])
f1 = plt.figure('power')
plt.plot(a[1], a[2], a[1], ab0 * a[1], b[1], b[2], '--',
a[1], a[2] - ab0 * a[1])
plt.plot([a[1][0], a[1][-1]], [pmpa]*2, ':',
[b[1][0], b[1][-1]], [pmpb]*2, ':')
plt.legend(['DeSoto', 'PVSyst interpolated', 'PVSyst', '$\Delta P$',
'$P_{mp,DeSoto}=%4.1f$' % pmpa,
'$P_{mp,PVSyst}=%4.1f$' % pmpb])
plt.grid()
plt.xlabel('voltage (V)')
plt.ylabel('power (W)')
plt.title('FS-495 power, DeSoto vs. PVSyst with recombination loss')
f1.show()
f2 = plt.figure('current')
plt.plot(a[1], a[0], a[1], ab0, b[1], b[0], '--', a[1], a[0] - ab0)
plt.plot([a[1][0], a[1][-1]], [isca]*2, ':',
[b[1][0], b[1][-1]], [iscb]*2, ':')
plt.plot([voca]*2, [a[0][0], a[0][-1]], ':',
[vocb]*2, [b[0][0], b[0][-1]], ':')
plt.legend(['DeSoto', 'PVSyst interpolated', 'PVSyst', '$\Delta I$',
'$I_{sc,DeSoto}=%4.2f$' % isca,
'$I_{sc,PVSyst}=%4.2f$' % iscb,
'$V_{oc,DeSoto}=%4.1f$' % voca,
'$V_{oc,PVSyst}=%4.1f$' % vocb])
plt.grid()
plt.xlabel('voltage (V)')
plt.ylabel('current (A)')
plt.title('FS-495 IV-curve, DeSoto vs. PVSyst with recombination loss')
f2.show()