Skip to content

Commit 6f94408

Browse files
committed
tests for the two additional functions
1 parent 994a853 commit 6f94408

8 files changed

Lines changed: 173 additions & 85 deletions

File tree

pvlib/ivtools/sdm/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44
fitting method.
55
"""
66

7-
from pvlib.ivtools.sdm.batzelis import ( # noqa: F401
8-
fit_desoto_batzelis,
9-
)
10-
117
from pvlib.ivtools.sdm.cec import ( # noqa: F401
128
fit_cec_sam,
139
)
1410

1511
from pvlib.ivtools.sdm.desoto import ( # noqa: F401
1612
fit_desoto,
17-
fit_desoto_sandia
13+
fit_desoto_batzelis,
14+
fit_desoto_sandia,
1815
)
1916

2017
from pvlib.ivtools.sdm.pvsyst import ( # noqa: F401

pvlib/ivtools/sdm/batzelis.py

Lines changed: 0 additions & 68 deletions
This file was deleted.

pvlib/ivtools/sdm/desoto.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from scipy import constants
44
from scipy import optimize
5+
from scipy.special import lambertw
56

67
from pvlib.ivtools.utils import rectify_iv_curve
78
from pvlib.ivtools.sde import _fit_sandia_cocontent
@@ -399,3 +400,64 @@ def _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const):
399400
new_x = sm.add_constant(x)
400401
res = sm.RLM(y, new_x).fit()
401402
return np.array(res.params)[1]
403+
404+
405+
def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc):
406+
"""
407+
Determine De Soto single-diode model parameters from datasheet values
408+
using Batzelis's method.
409+
410+
This method is described in Section II.C of [1]_.
411+
412+
Parameters
413+
----------
414+
isc0 : float
415+
Short-circuit current at STC. [A]
416+
voc0 : float
417+
Open-circuit voltage at STC. [V]
418+
imp0 : float
419+
Maximum power point current at STC. [A]
420+
vmp0 : float
421+
Maximum power point voltage at STC. [V]
422+
alpha_sc : float
423+
Short-circuit current temperature coefficient at STC. [1/K]
424+
beta_voc : float
425+
Open-circuit voltage temperature coefficient at STC. [1/K]
426+
427+
Returns
428+
-------
429+
dict
430+
The returned dict contains the keys:
431+
432+
* ``alpha_sc`` [A/K]
433+
* ``a_ref`` [V]
434+
* ``I_L_ref`` [A]
435+
* ``I_o_ref`` [A]
436+
* ``R_sh_ref`` [Ohm]
437+
* ``R_s`` [Ohm]
438+
439+
References
440+
----------
441+
.. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well
442+
Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7,
443+
no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431`
444+
"""
445+
t0 = 298.15 # K
446+
del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9
447+
w0 = np.real(lambertw(np.exp(1/del0 + 1)))
448+
449+
# Eqs 11-15
450+
a0 = del0 * voc0
451+
Rs0 = (a0 * (w0 - 1) - vmp0) / imp0
452+
Rsh0 = a0 * (w0 - 1) / (isc0 * (1 - 1/w0) - imp0)
453+
Iph0 = (1 + Rs0 / Rsh0) * isc0
454+
Isat0 = Iph0 * np.exp(-1/del0)
455+
456+
return {
457+
'alpha_sc': alpha_sc * isc0, # convert 1/K to A/K
458+
'a_ref': a0,
459+
'I_L_ref': Iph0,
460+
'I_o_ref': Isat0,
461+
'R_sh_ref': Rsh0,
462+
'R_s': Rs0,
463+
}

pvlib/pvarray.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
import numpy as np
12+
import pandas as pd
1213
from scipy.optimize import curve_fit
1314
from scipy.special import exp10, lambertw
1415

@@ -482,7 +483,7 @@ def batzelis(effective_irradiance, temp_cell,
482483

483484
# Eq 9-10
484485
del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0)
485-
w0 = lambertw(np.exp(1/del0 + 1)).real
486+
w0 = np.real(lambertw(np.exp(1/del0 + 1)))
486487

487488
# Eqs 27-28
488489
alpha_imp = alpha_sc + (beta_voc - 1/t0) / (w0 - 1)
@@ -505,11 +506,20 @@ def batzelis(effective_irradiance, temp_cell,
505506
vmp = np.clip(vmp, a_min=0, a_max=None)
506507
voc = np.clip(voc, a_min=0, a_max=None)
507508

508-
# TODO return dataframe for is_pandas
509-
return {
509+
out = {
510510
'p_mp': vmp * imp,
511511
'i_mp': imp,
512512
'v_mp': vmp,
513513
'i_sc': isc,
514514
'v_oc': voc,
515515
}
516+
517+
# if pandas in, ensure pandas out
518+
pandas_inputs = [
519+
x for x in [effective_irradiance, temp_cell]
520+
if isinstance(x, pd.Series)
521+
]
522+
if pandas_inputs:
523+
out = pd.DataFrame(out, index=pandas_inputs[0].index)
524+
525+
return out

pvlib/singlediode.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import numpy as np
6+
import pandas as pd
67
from pvlib.tools import _golden_sect_DataFrame
78

89
from scipy.optimize import brentq, newton
@@ -866,8 +867,8 @@ def _pwr_optfcn(df, loc):
866867
def batzelis_keypoints(photocurrent, saturation_current, resistance_series,
867868
resistance_shunt, nNsVth):
868869
"""
869-
Estimate maximum power, open-circuit, and short-circuit values
870-
using Batzelis's method.
870+
Estimate maximum power, open-circuit, and short-circuit points from
871+
single-diode equation parameters using Batzelis's method.
871872
872873
This method is described in Section II.B of [1]_.
873874
@@ -916,18 +917,34 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series,
916917

917918
# Eqs 3-4
918919
isc = Iph * Rsh / (Rs + Rsh)
919-
voc = a * np.log(Iph / Is)
920+
with np.errstate(divide='ignore'): # zero Iph
921+
voc = a * np.log(Iph / Is)
920922

921923
# Eqs 5-8
922-
w = lambertw(np.e * Iph / Is).real
924+
w = np.real(lambertw(np.e * Iph / Is))
923925
#vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed
924-
imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh
926+
with np.errstate(divide='ignore', invalid='ignore'): # zero Iph -> zero w
927+
imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh
928+
925929
vmp = a * (w - 1) - Rs * imp
926930

927-
return {
931+
vmp = np.where(Iph > 0, vmp, 0)
932+
voc = np.where(Iph > 0, voc, 0)
933+
imp = np.where(Iph > 0, imp, 0)
934+
isc = np.where(Iph > 0, isc, 0)
935+
936+
out = {
928937
'p_mp': imp * vmp,
929938
'i_mp': imp,
930939
'v_mp': vmp,
931940
'i_sc': isc,
932941
'v_oc': voc,
933942
}
943+
944+
# if pandas in, ensure pandas out
945+
pandas_inputs = [
946+
x for x in [Iph, Is, Rsh, Rs, a] if isinstance(x, pd.Series)]
947+
if pandas_inputs:
948+
out = pd.DataFrame(out, index=pandas_inputs[0].index)
949+
950+
return out

tests/ivtools/sdm/test_desoto.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,28 @@ def test_fit_desoto_sandia(cec_params_cansol_cs5p_220p):
9292
assert_allclose(result['dEgdT'], -0.0002677)
9393
assert_allclose(result['EgRef'], 1.3112547292120638)
9494
assert_allclose(result['cells_in_series'], specs['cells_in_series'])
95+
96+
97+
def test_fit_desoto_batzelis():
98+
params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57,
99+
'alpha_sc': 0.00046, 'beta_voc': -0.0024}
100+
expected = { # calculated with the function itself
101+
'alpha_sc': 0.0073508,
102+
'a_ref': 1.7257631194825132,
103+
'I_L_ref': 15.985408869737098,
104+
'I_o_ref': 3.594300567343102e-12,
105+
'R_sh_ref': 389.4378389153357,
106+
'R_s': 0.1318159287478395
107+
}
108+
out = sdm.fit_desoto_batzelis(**params)
109+
for k in expected:
110+
assert out[k] == pytest.approx(expected[k])
111+
112+
# ensure the STC values are reproduced
113+
iv = pvsystem.singlediode(out['I_L_ref'], out['I_o_ref'], out['R_s'],
114+
out['R_sh_ref'], out['a_ref'])
115+
assert iv['i_sc'] == pytest.approx(params['isc0'])
116+
assert iv['i_mp'] == pytest.approx(params['imp0'], rel=3e-3)
117+
assert iv['v_oc'] == pytest.approx(params['voc0'], rel=3e-4)
118+
assert iv['v_mp'] == pytest.approx(params['vmp0'], rel=4e-3)
119+

tests/test_pvarray.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ def test_batzelis():
137137

138138
# pandas series
139139
actual = pvarray.batzelis(pd.Series(g), pd.Series(t), **params)
140+
assert isinstance(actual, pd.DataFrame)
140141
for key, exp in expected.items():
141-
pd.testing.assert_series_equal(actual[key], pd.Series(exp), atol=1e-3)
142+
np.testing.assert_allclose(actual[key], pd.Series(exp), atol=1e-3)
142143

143144
# scalar
144145
actual = pvarray.batzelis(g[1], t[1], **params)

tests/test_singlediode.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import scipy
88
from pvlib import pvsystem
99
from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN,
10-
bishop88, bishop88_i_from_v, bishop88_v_from_i)
10+
bishop88, bishop88_i_from_v, bishop88_v_from_i,
11+
batzelis_keypoints)
1112
import pytest
1213
from numpy.testing import assert_array_equal
1314
from .conftest import TESTS_DATA_DIR
@@ -600,9 +601,52 @@ def test_bishop88_init_cond(method):
600601
NsVbi=NsVbi))
601602
bad_results = np.isnan(vmp2) | (vmp2 < 0) | (err > 0.00001)
602603
assert not bad_results.any()
603-
# test v_from_i
604+
# test i_from_v
604605
imp2 = bishop88_i_from_v(vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi)
605606
err = np.abs(_sde_check_solution(imp2, vmp, *sde_params, d2mutau=d2mutau,
606607
NsVbi=NsVbi))
607608
bad_results = np.isnan(imp2) | (imp2 < 0) | (err > 0.00001)
608609
assert not bad_results.any()
610+
611+
612+
def test_batzelis_keypoints():
613+
params = {'photocurrent': 10, 'saturation_current': 1e-10,
614+
'resistance_series': 0.2, 'resistance_shunt': 3000,
615+
'nNsVth': 1.7}
616+
617+
exact_values = { # calculated using pvlib.pvsystem.singlediode
618+
'i_sc': 9.999333377550565,
619+
'v_oc': 43.05589965219406,
620+
'i_mp': 9.513255314772051,
621+
'v_mp': 35.97259289596944,
622+
'p_mp': 342.21646055371264,
623+
}
624+
rtol = 5e-3 # accurate to within half a percent in this case
625+
626+
output = batzelis_keypoints(**params)
627+
for key in exact_values:
628+
assert output[key] == pytest.approx(exact_values[key], rel=rtol)
629+
630+
# numpy arrays
631+
params2 = {k: np.array([v] * 2) for k, v in params.items()}
632+
output2 = batzelis_keypoints(**params2)
633+
for key in exact_values:
634+
exp = np.array([exact_values[key]] * 2)
635+
np.testing.assert_allclose(output2[key], exp, rtol=rtol)
636+
637+
# pandas
638+
params3 = {k: pd.Series(v) for k, v in params2.items()}
639+
output3 = batzelis_keypoints(**params3)
640+
assert isinstance(output3, pd.DataFrame)
641+
for key in exact_values:
642+
exp = pd.Series([exact_values[key]] * 2)
643+
np.testing.assert_allclose(output3[key], exp, rtol=rtol)
644+
645+
646+
def test_batzelis_keypoints_night():
647+
# SDMs produce photocurrent=0 and resistance_shunt=inf at night
648+
out = batzelis_keypoints(photocurrent=0, saturation_current=1e-10,
649+
resistance_series=0.2, resistance_shunt=np.inf,
650+
nNsVth=1.7)
651+
for k, v in out.items():
652+
assert v == 0, k # ensure all outputs are zero (not nan, etc)

0 commit comments

Comments
 (0)