From d16278603a6be9ee966683f5b98fc98a585b5751 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 20 Jul 2016 14:59:14 -0700 Subject: [PATCH 1/4] add optional ivcurve calculation to singlediode --- docs/sphinx/source/whatsnew/v0.4.0.txt | 2 + pvlib/pvsystem.py | 88 +++++++++++++----------- pvlib/test/test_pvsystem.py | 94 ++++++++++++++++++-------- 3 files changed, 113 insertions(+), 71 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.4.0.txt b/docs/sphinx/source/whatsnew/v0.4.0.txt index b1d572d039..ec00fe5b81 100644 --- a/docs/sphinx/source/whatsnew/v0.4.0.txt +++ b/docs/sphinx/source/whatsnew/v0.4.0.txt @@ -51,6 +51,8 @@ Enhancements * Add solarposition.nrel_earthsun_distance function and option to calculate extraterrestrial radiation using the NREL solar position algorithm. (:issue:`211`, :issue:`215`) +* pvsystem.singlediode can now calculate IV curves if a user supplies + an ivcurve_pnts keyword argument. (:issue:`83`) * Includes SAM data files in the distribution. (:issue:`52`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0e669421d0..c229cae468 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -413,7 +413,8 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, self.module_parameters, reference_irradiance=reference_irradiance) def singlediode(self, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth): + resistance_series, resistance_shunt, nNsVth, + ivcurve_pnts=None): """Wrapper around the :py:func:`singlediode` function. Parameters @@ -425,7 +426,8 @@ def singlediode(self, photocurrent, saturation_current, See pvsystem.singlediode for details """ return singlediode(photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + resistance_series, resistance_shunt, nNsVth, + ivcurve_pnts=ivcurve_pnts) def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent): @@ -1533,7 +1535,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): + resistance_shunt, nNsVth, ivcurve_pnts=None): r''' Solve the single-diode model to obtain a photovoltaic IV curve. @@ -1580,13 +1582,18 @@ def singlediode(photocurrent, saturation_current, resistance_series, temp_cell is the temperature of the p-n junction in Kelvin, and q is the charge of an electron (coulombs). + ivcurve_pnts : None or int + Number of points in the desired IV curve. If None or 0, no + IV curves will be produced. + Returns ------- - If ``photocurrent`` is a Series, a DataFrame with the following - columns. All columns have the same number of rows as the largest - input DataFrame. + If photocurrent is a Series and ivcurve_pnts is None, a DataFrame + with the columns described below. All columns have the same number + of rows as the largest input DataFrame. - If ``photocurrent`` is a scalar, a dict with the following keys. + If photocurrent is a scalar or ivcurve_pnts is not None, an + OrderedDict with the following keys. * i_sc - short circuit current in amperes. * v_oc - open circuit voltage in volts. @@ -1595,6 +1602,8 @@ def singlediode(photocurrent, saturation_current, resistance_series, * p_mp - power at maximum power point in watts. * i_x - current, in amperes, at ``v = 0.5*v_oc``. * i_xx - current, in amperes, at ``V = 0.5*(v_oc+v_mp)``. + * i - None or iv curve current. + * v - None or iv curve voltage. Notes ----- @@ -1648,37 +1657,32 @@ def singlediode(photocurrent, saturation_current, resistance_series, i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5*(v_oc+v_mp), saturation_current, photocurrent) - # @wholmgren: need to move this stuff to a different function -# If the user says they want a curve of with number of points equal to -# NumPoints (must be >=2), then create a voltage array where voltage is -# zero in the first column, and Voc in the last column. Number of columns -# must equal NumPoints. Each row represents the voltage for one IV curve. -# Then create a current array where current is Isc in the first column, and -# zero in the last column, and each row represents the current in one IV -# curve. Thus the nth (V,I) point of curve m would be found as follows: -# (Result.V(m,n),Result.I(m,n)). -# if NumPoints >= 2 -# s = ones(1,NumPoints); # shaping DataFrame to shape the column -# # DataFrame parameters into 2-D matrices -# Result.V = (Voc)*(0:1/(NumPoints-1):1); -# Result.I = I_from_V(Rsh*s, Rs*s, nNsVth*s, Result.V, I0*s, IL*s); -# end - - dfout = {} - dfout['i_sc'] = i_sc - dfout['i_mp'] = i_mp - dfout['v_oc'] = v_oc - dfout['v_mp'] = v_mp - dfout['p_mp'] = p_mp - dfout['i_x'] = i_x - dfout['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 + else: + ivcurve_v = None + ivcurve_i = None - try: - dfout = pd.DataFrame(dfout, index=photocurrent.index) - except AttributeError: - pass + out = OrderedDict() + out['i_sc'] = i_sc + out['i_mp'] = i_mp + out['v_oc'] = v_oc + out['v_mp'] = v_mp + out['p_mp'] = p_mp + out['i_x'] = i_x + out['i_xx'] = i_xx + out['i'] = ivcurve_i + out['v'] = ivcurve_v + + if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: + out = pd.DataFrame(out, index=photocurrent.index) - return dfout + return out # Created April,2014 @@ -1879,11 +1883,13 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, except ImportError: raise ImportError('This function requires scipy') - Rsh = resistance_shunt - Rs = resistance_series - I0 = saturation_current - IL = photocurrent - V = voltage + # asarray turns Series into arrays so that we don't have to worry + # about multidimensional broadcasting failing + Rsh = np.asarray(resistance_shunt) + Rs = np.asarray(resistance_series) + I0 = np.asarray(saturation_current) + IL = np.asarray(photocurrent) + V = np.asarray(voltage) argW = (Rs*I0*Rsh * np.exp(Rsh*(Rs*(IL+I0)+V) / (nNsVth*(Rs+Rsh))) / diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 5807de5855..724ce860d6 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -4,7 +4,7 @@ from collections import OrderedDict import numpy as np -from numpy import nan +from numpy import nan, array import pandas as pd import pytest @@ -150,6 +150,14 @@ def sapm_module_params(sam_data): return module_parameters +@pytest.fixture(scope="session") +def cec_module_params(sam_data): + modules = sam_data['cecmod'] + module = 'Example_Module' + module_parameters = modules[module] + return module_parameters + + def test_sapm(sapm_module_params): times = pd.DatetimeIndex(start='2015-01-01', periods=5, freq='12H') @@ -307,17 +315,15 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params): aoi, reference_irradiance=reference_irradiance) -def test_calcparams_desoto(sam_data): - module = 'Example_Module' - module_parameters = sam_data['cecmod'][module] +def test_calcparams_desoto(cec_module_params): times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') poa_data = pd.Series([0, 800], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( poa_data, temp_cell=25, - alpha_isc=module_parameters['alpha_sc'], - module_parameters=module_parameters, + alpha_isc=cec_module_params['alpha_sc'], + module_parameters=cec_module_params, EgRef=1.121, dEgdT=-0.0002677) @@ -328,13 +334,11 @@ def test_calcparams_desoto(sam_data): assert_allclose(nNsVth, 0.473) -def test_PVSystem_calcparams_desoto(sam_data): - module = 'Example_Module' - module_parameters = sam_data['cecmod'][module].copy() +def test_PVSystem_calcparams_desoto(cec_module_params): + module_parameters = cec_module_params.copy() module_parameters['EgRef'] = 1.121 module_parameters['dEgdT'] = -0.0002677 - system = pvsystem.PVSystem(module=module, - module_parameters=module_parameters) + system = pvsystem.PVSystem(module_parameters=module_parameters) times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') poa_data = pd.Series([0, 800], index=times) temp_cell = 25 @@ -367,26 +371,21 @@ def test_i_from_v(): @requires_scipy -def test_PVSystem_i_from_v(sam_data): - module = 'Example_Module' - module_parameters = sam_data['cecmod'][module] - system = pvsystem.PVSystem(module=module, - module_parameters=module_parameters) +def test_PVSystem_i_from_v(): + system = pvsystem.PVSystem() output = system.i_from_v(20, .1, .5, 40, 6e-7, 7) assert_allclose(-299.746389916, output, 5) @requires_scipy -def test_singlediode_series(sam_data): - module = 'Example_Module' - module_parameters = sam_data['cecmod'][module] +def test_singlediode_series(cec_module_params): times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') poa_data = pd.Series([0, 800], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( poa_data, temp_cell=25, - alpha_isc=module_parameters['alpha_sc'], - module_parameters=module_parameters, + alpha_isc=cec_module_params['alpha_sc'], + module_parameters=cec_module_params, EgRef=1.121, dEgdT=-0.0002677) out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth) @@ -424,31 +423,66 @@ def test_singlediode_floats(sam_data): 'p_mp': 38.194165464983037, 'i_x': 6.7556075876880621, 'i_sc': 6.9646747613963198, - 'v_mp': 6.221535886625464} + 'v_mp': 6.221535886625464, + 'i': None, + 'v': None} assert isinstance(out, dict) for k, v in out.items(): - assert_allclose(expected[k], v, atol=3) + if k in ['i', 'v']: + assert v is None + else: + assert_allclose(expected[k], v, atol=3) @requires_scipy -def test_PVSystem_singlediode_floats(sam_data): - module = 'Example_Module' - module_parameters = sam_data['cecmod'][module] - system = pvsystem.PVSystem(module=module, - module_parameters=module_parameters) - out = system.singlediode(7, 6e-7, .1, 20, .5) +def test_singlediode_floats_ivcurve(): + out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3) expected = {'i_xx': 4.2685798754011426, 'i_mp': 6.1390251797935704, 'v_oc': 8.1063001465863085, 'p_mp': 38.194165464983037, 'i_x': 6.7556075876880621, 'i_sc': 6.9646747613963198, - 'v_mp': 6.221535886625464} + 'v_mp': 6.221535886625464, + 'i': np.array([6.965172e+00, 6.755882e+00, 2.575717e-14]), + 'v': np.array([0. , 4.05315, 8.1063])} assert isinstance(out, dict) for k, v in out.items(): assert_allclose(expected[k], v, atol=3) +@requires_scipy +def test_singlediode_series_ivcurve(cec_module_params): + times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') + poa_data = pd.Series([0, 800], index=times) + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( + poa_data, + temp_cell=25, + alpha_isc=cec_module_params['alpha_sc'], + module_parameters=cec_module_params, + EgRef=1.121, + dEgdT=-0.0002677) + + out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) + + expected = OrderedDict([('i_sc', array([ nan, 6.00675648])), + ('i_mp', array([ nan, 5.6129056])), + ('v_oc', array([ nan, 10.29530483])), + ('v_mp', array([ nan, 7.25364707])), + ('p_mp', array([ nan, 40.71403625])), + ('i_x', array([ nan, 5.74622046])), + ('i_xx', array([ nan, 4.97138154])), + ('i', + array([[ nan, nan, nan], + [ 6.00726296, 5.74622046, 0. ]])), + ('v', + array([[ nan, nan, nan], + [ 0. , 5.14765242, 10.29530483]]))]) + + for k, v in out.items(): + assert_allclose(expected[k], v, atol=3) + + def test_scale_voltage_current_power(sam_data): data = pd.DataFrame( np.array([[2, 1.5, 10, 8, 12, 0.5, 1.5]]), From 37c4785f53a2c88dfc21c32149119bda46a410cc Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 21 Jul 2016 09:43:00 -0700 Subject: [PATCH 2/4] only include i v keys if needed --- pvlib/pvsystem.py | 74 +++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c229cae468..989748f072 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1557,23 +1557,23 @@ def singlediode(photocurrent, saturation_current, resistance_series, Parameters ---------- - photocurrent : float or Series + photocurrent : numeric Light-generated current (photocurrent) in amperes under desired IV curve conditions. Often abbreviated ``I_L``. - saturation_current : float or Series + saturation_current : numeric Diode saturation current in amperes under desired IV curve conditions. Often abbreviated ``I_0``. - resistance_series : float or Series + resistance_series : numeric Series resistance in ohms under desired IV curve conditions. Often abbreviated ``Rs``. - resistance_shunt : float or Series + resistance_shunt : numeric Shunt resistance in ohms under desired IV curve conditions. Often abbreviated ``Rsh``. - nNsVth : float or Series + nNsVth : numeric The product of three components. 1) The usual diode ideal factor (n), 2) the number of cells in series (Ns), and 3) the cell thermal voltage under the desired IV curve conditions (Vth). The @@ -1588,22 +1588,29 @@ def singlediode(photocurrent, saturation_current, resistance_series, Returns ------- - If photocurrent is a Series and ivcurve_pnts is None, a DataFrame - with the columns described below. All columns have the same number - of rows as the largest input DataFrame. - - If photocurrent is a scalar or ivcurve_pnts is not None, an - OrderedDict with the following keys. - - * i_sc - short circuit current in amperes. - * v_oc - open circuit voltage in volts. - * i_mp - current at maximum power point in amperes. - * v_mp - voltage at maximum power point in volts. - * p_mp - power at maximum power point in watts. - * i_x - current, in amperes, at ``v = 0.5*v_oc``. - * i_xx - current, in amperes, at ``V = 0.5*(v_oc+v_mp)``. - * i - None or iv curve current. - * v - None or iv curve voltage. + OrderedDict or DataFrame + + The returned dict-like object always contains the keys/columns: + + * i_sc - short circuit current in amperes. + * v_oc - open circuit voltage in volts. + * i_mp - current at maximum power point in amperes. + * v_mp - voltage at maximum power point in volts. + * p_mp - power at maximum power point in watts. + * i_x - current, in amperes, at ``v = 0.5*v_oc``. + * i_xx - current, in amperes, at ``V = 0.5*(v_oc+v_mp)``. + + If ivcurve_pnts is greater than 0, the output dictionary will also + include the keys: + + * i - IV curve current in amperes. + * v - IV curve voltage in volts. + + The output will be an OrderedDict if photocurrent is a scalar, + array, or ivcurve_pnts is not None. + + The output will be a DataFrame if photocurrent is a Series and + ivcurve_pnts is None. Notes ----- @@ -1657,27 +1664,24 @@ def singlediode(photocurrent, saturation_current, resistance_series, i_xx = i_from_v(resistance_shunt, resistance_series, nNsVth, 0.5*(v_oc+v_mp), saturation_current, photocurrent) - # 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 - else: - ivcurve_v = None - ivcurve_i = None - out = OrderedDict() out['i_sc'] = i_sc - out['i_mp'] = i_mp 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['i'] = ivcurve_i - out['v'] = ivcurve_v + + # 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 if isinstance(photocurrent, pd.Series) and not ivcurve_pnts: out = pd.DataFrame(out, index=photocurrent.index) From a6db2b80b37f00fae2d18e2309cd67871e1b3ee0 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 22 Jul 2016 08:50:06 -0700 Subject: [PATCH 3/4] update test for singlediode p_mp fix --- pvlib/test/test_pvsystem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 724ce860d6..305175e7b3 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -466,12 +466,12 @@ def test_singlediode_series_ivcurve(cec_module_params): out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) expected = OrderedDict([('i_sc', array([ nan, 6.00675648])), - ('i_mp', array([ nan, 5.6129056])), + ('i_mp', array([ nan, 5.285947])), ('v_oc', array([ nan, 10.29530483])), - ('v_mp', array([ nan, 7.25364707])), - ('p_mp', array([ nan, 40.71403625])), + ('v_mp', array([ nan, 8.415971])), + ('p_mp', array([ nan, 44.486373])), ('i_x', array([ nan, 5.74622046])), - ('i_xx', array([ nan, 4.97138154])), + ('i_xx', array([ nan, 3.90008])), ('i', array([[ nan, nan, nan], [ 6.00726296, 5.74622046, 0. ]])), @@ -480,7 +480,7 @@ def test_singlediode_series_ivcurve(cec_module_params): [ 0. , 5.14765242, 10.29530483]]))]) for k, v in out.items(): - assert_allclose(expected[k], v, atol=3) + assert_allclose(expected[k], v, atol=1e-2) def test_scale_voltage_current_power(sam_data): From 9cf9f6c2998114c4105e9edeb0ae8ab9d8deb10a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Fri, 22 Jul 2016 08:56:49 -0700 Subject: [PATCH 4/4] add a moderate point test sd series --- pvlib/test/test_pvsystem.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 305175e7b3..3d8607db43 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -453,8 +453,8 @@ def test_singlediode_floats_ivcurve(): @requires_scipy def test_singlediode_series_ivcurve(cec_module_params): - times = pd.DatetimeIndex(start='2015-01-01', periods=2, freq='12H') - poa_data = pd.Series([0, 800], index=times) + times = pd.DatetimeIndex(start='2015-06-01', periods=3, freq='6H') + poa_data = pd.Series([0, 400, 800], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( poa_data, temp_cell=25, @@ -465,19 +465,21 @@ def test_singlediode_series_ivcurve(cec_module_params): out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3) - expected = OrderedDict([('i_sc', array([ nan, 6.00675648])), - ('i_mp', array([ nan, 5.285947])), - ('v_oc', array([ nan, 10.29530483])), - ('v_mp', array([ nan, 8.415971])), - ('p_mp', array([ nan, 44.486373])), - ('i_x', array([ nan, 5.74622046])), - ('i_xx', array([ nan, 3.90008])), - ('i', - array([[ nan, nan, nan], - [ 6.00726296, 5.74622046, 0. ]])), + expected = OrderedDict([('i_sc', array([ nan, 3.01054475, 6.00675648])), + ('v_oc', array([ nan, 9.96886962, 10.29530483])), + ('i_mp', array([ nan, 2.65191983, 5.28594672])), + ('v_mp', array([ nan, 8.33392491, 8.4159707 ])), + ('p_mp', array([ nan, 22.10090078, 44.48637274])), + ('i_x', array([ nan, 2.88414114, 5.74622046])), + ('i_xx', array([ nan, 2.04340914, 3.90007956])), ('v', array([[ nan, nan, nan], - [ 0. , 5.14765242, 10.29530483]]))]) + [ 0. , 4.98443481, 9.96886962], + [ 0. , 5.14765242, 10.29530483]])), + ('i', + array([[ nan, nan, nan], + [ 3.01079860e+00, 2.88414114e+00, 3.10862447e-14], + [ 6.00726296e+00, 5.74622046e+00, 0.00000000e+00]]))]) for k, v in out.items(): assert_allclose(expected[k], v, atol=1e-2)