diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index c37bedf7df..7241f78f4b 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -220,6 +220,7 @@ Incident angle modifiers iam.physical iam.ashrae iam.martin_ruiz + iam.martin_ruiz_diffuse iam.sapm iam.interp @@ -303,6 +304,7 @@ Functions for fitting PV models ivtools.fit_sde_sandia ivtools.fit_sdm_cec_sam + ivtools.fit_sdm_desoto Other ----- diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst index 2b65b17a9e..06e5130f5d 100644 --- a/docs/sphinx/source/whatsnew/v0.7.0.rst +++ b/docs/sphinx/source/whatsnew/v0.7.0.rst @@ -1,4 +1,4 @@ -.. _whatsnew_0700: +.. _whatsnew_0700: v0.7.0 (MONTH DAY, YEAR) ------------------------ @@ -111,7 +111,11 @@ Enhancements the single diode equation to an IV curve. * Add :py:func:`~pvlib.ivtools.fit_sdm_cec_sam`, a wrapper for the CEC single diode model fitting function '6parsolve' from NREL's System Advisor Model. +* Add :py:func:`~pvlib.ivtools.fit_sdm_desoto`, a method to fit the De Soto single + diode model to the typical specifications given in manufacturers datasheets. * Add `timeout` to :py:func:`pvlib.iotools.get_psm3`. +* Created one new incidence angle modifier (IAM) function for diffuse irradiance: + :py:func:`pvlib.iam.martin_ruiz_diffuse`. (:issue:`751`) Bug fixes ~~~~~~~~~ @@ -162,4 +166,5 @@ Contributors * Anton Driesse (:ghuser:`adriesse`) * Alexander Morgan (:ghuser:`alexandermorgan`) * Miguel Sánchez de León Peque (:ghuser:`Peque`) +* Tanguy Lunel (:ghuser:`tylunel`) * Veronica Guo (:ghuser:`veronicaguo`) diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 073ee7844a..4750054869 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -115,7 +115,7 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, ghi = cg1 * dni_extra * cos_zenith * tl / tl * np.fmax(ghi, 0) # BncI = "normal beam clear sky radiation" - b = 0.664 + 0.163/fh1 + b = 0.664 + 0.16268/fh1 bnci = b * np.exp(-0.09 * airmass_absolute * (tl - 1)) bnci = dni_extra * np.fmax(bnci, 0) diff --git a/pvlib/iam.py b/pvlib/iam.py index 3d869f298d..f2f968fac3 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -12,7 +12,6 @@ import pandas as pd from pvlib.tools import cosd, sind, tand, asind - # a dict of required parameter names for each IAM model # keys are the function names for the IAM models IAM_MODEL_PARAMS = { @@ -220,8 +219,8 @@ def martin_ruiz(aoi, a_r=0.16): ----- `martin_ruiz` calculates the incidence angle modifier (IAM) as described in [1]. The information required is the incident angle (AOI) and the angular - losses coefficient (a_r). Note that [1] has a corrigendum [2] which makes - the document much simpler to understand. + losses coefficient (a_r). Note that [1] has a corrigendum [2] which + clarifies a mix-up of 'alpha's and 'a's in the former. The incident angle modifier is defined as @@ -249,6 +248,7 @@ def martin_ruiz(aoi, a_r=0.16): See Also -------- + iam.martin_ruiz_diffuse iam.physical iam.ashrae iam.interp @@ -262,7 +262,7 @@ def martin_ruiz(aoi, a_r=0.16): a_r = np.asanyarray(a_r) if np.any(np.less_equal(a_r, 0)): - raise RuntimeError("The parameter 'a_r' cannot be zero or negative.") + raise ValueError("The parameter 'a_r' cannot be zero or negative.") with np.errstate(invalid='ignore'): iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r)) @@ -274,6 +274,111 @@ def martin_ruiz(aoi, a_r=0.16): return iam +def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None): + ''' + Determine the incidence angle modifiers (iam) for diffuse sky and + ground-reflected irradiance using the Martin and Ruiz incident angle model. + + Parameters + ---------- + surface_tilt: float or array-like, default 0 + Surface tilt angles in decimal degrees. + The tilt angle is defined as degrees from horizontal + (e.g. surface facing up = 0, surface facing horizon = 90) + surface_tilt must be in the range [0, 180] + + a_r : numeric + The angular losses coefficient described in equation 3 of [1]. + This is an empirical dimensionless parameter. Values of a_r are + generally on the order of 0.08 to 0.25 for flat-plate PV modules. + a_r must be greater than zero. + + c1 : float + First fitting parameter for the expressions that approximate the + integral of diffuse irradiance coming from different directions. + c1 is given as the constant 4 / 3 / pi (0.4244) in [1]. + + c2 : float + Second fitting parameter for the expressions that approximate the + integral of diffuse irradiance coming from different directions. + If c2 is None, it will be calculated according to the linear + relationship given in [3]. + + Returns + ------- + iam_sky : numeric + The incident angle modifier for sky diffuse + + iam_ground : numeric + The incident angle modifier for ground-reflected diffuse + + Notes + ----- + Sky and ground modifiers are complementary: iam_sky for tilt = 30 is + equal to iam_ground for tilt = 180 - 30. For vertical surfaces, + tilt = 90, the two factors are equal. + + References + ---------- + [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular + losses under field conditions by means of an analytical model", Solar + Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001. + + [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV + modules angular losses under field conditions by means of an + analytical model'", Solar Energy Materials & Solar Cells, vol. 110, + pp. 154, 2013. + + [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. + + See Also + -------- + iam.martin_ruiz + iam.physical + iam.ashrae + iam.interp + iam.sapm + ''' + # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019 + + if isinstance(surface_tilt, pd.Series): + out_index = surface_tilt.index + else: + out_index = None + + surface_tilt = np.asanyarray(surface_tilt) + + # avoid undefined results for horizontal or upside-down surfaces + zeroang = 1e-06 + + surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt) + surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt) + + if c2 is None: + # This equation is from [3] Sect. 7.2 + c2 = 0.5 * a_r - 0.154 + + beta = np.radians(surface_tilt) + + from numpy import pi, sin, cos, exp + + # because sin(pi) isn't exactly zero + sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta)) + + trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta)) + trig_term_gnd = sin_beta + (beta - sin_beta) / (1 - cos(beta)) # noqa: E222 E261 E501 + + iam_sky = 1 - exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r) + iam_gnd = 1 - exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r) + + if out_index is not None: + iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky') + iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground') + + return iam_sky, iam_gnd + + def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): r''' Determine the incidence angle modifier (IAM) by interpolating a set of diff --git a/pvlib/ivtools.py b/pvlib/ivtools.py index 353a0b8a9a..4806b2ed16 100644 --- a/pvlib/ivtools.py +++ b/pvlib/ivtools.py @@ -262,6 +262,147 @@ def fit_sde_sandia(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None, v_oc) +def fit_sdm_desoto(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, + cells_in_series, EgRef=1.121, dEgdT=-0.0002677, + temp_ref=25, irrad_ref=1000, root_kwargs={}): + """ + Calculates the parameters for the De Soto single diode model using the + procedure described in [1]. This procedure has the advantage of + using common specifications given by manufacturers in the + datasheets of PV modules. + + The solution is found using the scipy.optimize.root() function, + with the corresponding default solver method 'hybr'. + No restriction is put on the fit variables, i.e. series + or shunt resistance could go negative. Nevertheless, if it happens, + check carefully the inputs and their units; alpha_sc and beta_voc are + often given in %/K in manufacturers datasheets and should be given + in A/K and V/K here. + + The parameters returned by this function can be used by + pvsystem.calcparams_desoto to calculate the values at different + irradiance and cell temperature. + + Parameters + ---------- + v_mp: float + Module voltage at the maximum-power point at reference conditions [V]. + i_mp: float + Module current at the maximum-power point at reference conditions [A]. + v_oc: float + Open-circuit voltage at reference conditions [V]. + i_sc: float + Short-circuit current at reference conditions [A]. + alpha_sc: float + The short-circuit current (i_sc) temperature coefficient of the + module [A/K]. + beta_voc: float + The open-circuit voltage (v_oc) temperature coefficient of the + module [V/K]. + cells_in_series: integer + Number of cell in the module. + EgRef: float, default 1.121 eV - value for silicon + Energy of bandgap of semi-conductor used [eV] + dEgdT: float, default -0.0002677 - value for silicon + Variation of bandgap according to temperature [eV/K] + temp_ref: float, default 25 + Reference temperature condition [C] + irrad_ref: float, default 1000 + Reference irradiance condition [W/m2] + root_kwargs: dictionary, default None + Dictionary of arguments to pass onto scipy.optimize.root() + + Returns + ------- + Tuple of the following elements: + + * Dictionary with the following elements: + I_L_ref: float + Light-generated current at reference conditions [A] + I_o_ref: float + Diode saturation current at reference conditions [A] + R_s: float + Series resistance [ohms] + R_sh_ref: float + Shunt resistance at reference conditions [ohms]. + a_ref: float + Modified ideality factor at reference conditions. + The product of the usual diode ideality factor (n, unitless), + number of cells in series (Ns), and cell thermal voltage at + specified effective irradiance and cell temperature. + alpha_sc: float + The short-circuit current (i_sc) temperature coefficient of the + module [A/K]. + EgRef: float + Energy of bandgap of semi-conductor used [eV] + dEgdT: float + Variation of bandgap according to temperature [eV/K] + irrad_ref: float + Reference irradiance condition [W/m2] + temp_ref: float + Reference temperature condition [C] + * scipy.optimize.OptimizeResult + Optimization result of scipy.optimize.root(). + See scipy.optimize.OptimizeResult for more details. + + References + ---------- + [1] W. De Soto et al., "Improvement and validation of a model for + photovoltaic array performance", Solar Energy, vol 80, pp. 78-88, + 2006. + + [2] John A Duffie, William A Beckman, "Solar Engineering of Thermal + Processes", Wiley, 2013 + """ + + try: + from scipy.optimize import root + from scipy import constants + except ImportError: + raise ImportError("The fit_sdm_desoto function requires scipy.") + + # Constants + k = constants.value('Boltzmann constant in eV/K') + Tref = temp_ref + 273.15 # [K] + + # initial guesses of variables for computing convergence: + # Values are taken from [2], p753 + Rsh_0 = 100.0 + a_0 = 1.5*k*Tref*cells_in_series + IL_0 = i_sc + Io_0 = i_sc * np.exp(-v_oc/a_0) + Rs_0 = (a_0*np.log1p((IL_0-i_mp)/Io_0) - v_mp)/i_mp + # params_i : initial values vector + params_i = np.array([IL_0, Io_0, a_0, Rsh_0, Rs_0]) + + # specs of module + specs = (i_sc, v_oc, i_mp, v_mp, beta_voc, alpha_sc, EgRef, dEgdT, + Tref, k) + + # computing with system of equations described in [1] + optimize_result = root(_system_of_equations_desoto, x0=params_i, + args=(specs,), **root_kwargs) + + if optimize_result.success: + sdm_params = optimize_result.x + else: + raise RuntimeError( + 'Parameter estimation failed:\n' + optimize_result.message) + + # results + return ({'I_L_ref': sdm_params[0], + 'I_o_ref': sdm_params[1], + 'a_ref': sdm_params[2], + 'R_sh_ref': sdm_params[3], + 'R_s': sdm_params[4], + 'alpha_sc': alpha_sc, + 'EgRef': EgRef, + 'dEgdT': dEgdT, + 'irrad_ref': irrad_ref, + 'temp_ref': temp_ref}, + optimize_result) + + def _find_mp(voltage, current): """ Finds voltage and current at maximum power point. @@ -348,3 +489,69 @@ def _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc): else: # I0_voc > 0 I0 = I0_voc return (IL, I0, Rsh, Rs, nNsVth) + + +def _system_of_equations_desoto(params, specs): + """Evaluates the systems of equations used to solve for the single + diode equation parameters. Function designed to be used by + scipy.optimize.root() in fit_sdm_desoto(). + + Parameters + ---------- + params: ndarray + Array with parameters of the De Soto single diode model. Must be + given in the following order: IL, Io, a, Rsh, Rs + specs: tuple + Specifications of pv module given by manufacturer. Must be given + in the following order: Isc, Voc, Imp, Vmp, beta_oc, alpha_sc + + Returns + ------- + system of equations to solve with scipy.optimize.root(). + + + References + ---------- + [1] W. De Soto et al., "Improvement and validation of a model for + photovoltaic array performance", Solar Energy, vol 80, pp. 78-88, + 2006. + + [2] John A Duffie, William A Beckman, "Solar Engineering of Thermal + Processes", Wiley, 2013 + """ + + # six input known variables + Isc, Voc, Imp, Vmp, beta_oc, alpha_sc, EgRef, dEgdT, Tref, k = specs + + # five parameters vector to find + IL, Io, a, Rsh, Rs = params + + # five equation vector + y = [0, 0, 0, 0, 0] + + # 1st equation - short-circuit - eq(3) in [1] + y[0] = Isc - IL + Io * np.expm1(Isc * Rs / a) + Isc * Rs / Rsh + + # 2nd equation - open-circuit Tref - eq(4) in [1] + y[1] = -IL + Io * np.expm1(Voc / a) + Voc / Rsh + + # 3rd equation - Imp & Vmp - eq(5) in [1] + y[2] = Imp - IL + Io * np.expm1((Vmp + Imp * Rs) / a) \ + + (Vmp + Imp * Rs) / Rsh + + # 4th equation - Pmp derivated=0 - eq23.2.6 in [2] + # caution: eq(6) in [1] has a sign error + y[3] = Imp \ + - Vmp * ((Io / a) * np.exp((Vmp + Imp * Rs) / a) + 1.0 / Rsh) \ + / (1.0 + (Io * Rs / a) * np.exp((Vmp + Imp * Rs) / a) + Rs / Rsh) + + # 5th equation - open-circuit T2 - eq (4) at temperature T2 in [1] + T2 = Tref + 2 + Voc2 = (T2 - Tref) * beta_oc + Voc # eq (7) in [1] + a2 = a * T2 / Tref # eq (8) in [1] + IL2 = IL + alpha_sc * (T2 - Tref) # eq (11) in [1] + Eg2 = EgRef * (1 + dEgdT * (T2 - Tref)) # eq (10) in [1] + Io2 = Io * (T2 / Tref)**3 * np.exp(1 / k * (EgRef/Tref - Eg2/T2)) # eq (9) + y[4] = -IL2 + Io2 * np.expm1(Voc2 / a2) + Voc2 / Rsh # eq (4) at T2 + + return y diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 16bd7ba426..4786667503 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -324,13 +324,15 @@ def __init__(self, system, location, # TODO: deprecated kwarg temp_model. Remove use of temp_model in v0.8 temp_model = kwargs.pop('temp_model', None) if temp_model is not None: - warnings.warn('The temp_model keyword argument is deprecated. Use ' - 'temperature_model instead', pvlibDeprecationWarning) if temperature_model is None: + warnings.warn('The temp_model keyword argument is deprecated.' + ' Use temperature_model instead', + pvlibDeprecationWarning) temperature_model = temp_model elif temp_model == temperature_model: warnings.warn('Provide only one of temperature_model or ' - 'temp_model (deprecated).') + 'temp_model (deprecated).', + pvlibDeprecationWarning) else: raise ValueError( 'Conflicting temperature_model {} and temp_model {}. ' diff --git a/pvlib/test/test_clearsky.py b/pvlib/test/test_clearsky.py index 44d2e586a7..873e716465 100644 --- a/pvlib/test/test_clearsky.py +++ b/pvlib/test/test_clearsky.py @@ -34,11 +34,11 @@ def test_ineichen_series(): expected = pd.DataFrame(np. array([[ 0. , 0. , 0. ], [ 0. , 0. , 0. ], - [ 65.49426624, 321.16092181, 25.54562017], - [ 704.6968125 , 888.90147035, 87.73601277], - [1044.1230677 , 953.24925854, 107.03109696], - [ 853.02065704, 922.06124712, 96.42909484], - [ 251.99427693, 655.44925241, 53.9901349 ], + [ 65.49426624, 321.03665146, 25.56107792], + [ 704.6968125 , 888.55751839, 87.97474001], + [1044.1230677 , 952.88040806, 107.39369589], + [ 853.02065704, 921.70446345, 96.72185108], + [ 251.99427693, 655.19563211, 54.0667509 ], [ 0. , 0. , 0. ], [ 0. , 0. , 0. ]]), columns=['ghi', 'dni', 'dhi'], @@ -62,11 +62,11 @@ def test_ineichen_series_perez_enhancement(): expected = pd.DataFrame(np. array([[ 0. , 0. , 0. ], [ 0. , 0. , 0. ], - [ 91.1249279 , 321.16092171, 51.17628184], - [ 716.46580547, 888.9014706 , 99.50500553], - [1053.42066073, 953.24925905, 116.3286895 ], - [ 863.54692748, 922.06124652, 106.9553658 ], - [ 271.06382275, 655.44925213, 73.05968076], + [ 91.1249279 , 321.03665146, 51.1917396 ], + [ 716.46580547, 888.55751839, 99.7437328 ], + [1053.42066073, 952.88040806, 116.69128857], + [ 863.54692748, 921.70446345, 107.24812191], + [ 271.06382275, 655.1956321 , 73.13629663], [ 0. , 0. , 0. ], [ 0. , 0. , 0. ]]), columns=['ghi', 'dni', 'dhi'], @@ -79,8 +79,8 @@ def test_ineichen_series_perez_enhancement(): def test_ineichen_scalar_input(): expected = OrderedDict() expected['ghi'] = 1038.159219 - expected['dni'] = 942.2081860378344 - expected['dhi'] = 110.26529293612793 + expected['dni'] = 941.843607 + expected['dhi'] = 110.624333 out = clearsky.ineichen(10., 1., 3.) for k, v in expected.items(): @@ -107,8 +107,8 @@ def test_ineichen_nans(): expected['dhi'] = np.full(length, np.nan) expected['ghi'][length-1] = 1042.72590228 - expected['dni'][length-1] = 946.35279683 - expected['dhi'][length-1] = 110.75033088 + expected['dni'][length-1] = 945.986614 + expected['dhi'][length-1] = 111.11095 out = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity, dni_extra=dni_extra) @@ -134,30 +134,30 @@ def test_ineichen_arrays(): [ 94.81136442, 64.38555328, 43.72365587]]])) expected['dni'] = (np. - array([[[1014.38807396, 942.20818604, 861.11344424], - [1014.38807396, 942.20818604, 861.11344424], - [1014.38807396, 942.20818604, 861.11344424]], + array([[[1014.38807396, 941.8436073 , 860.78024436], + [1014.38807396, 941.8436073 , 860.78024436], + [1014.38807396, 941.8436073 , 860.78024436]], - [[ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235]], + [[ 687.34698591, 418.98672583, 255.40211861], + [ 687.34698591, 418.98672583, 255.40211861], + [ 687.34698591, 418.98672583, 255.40211861]], - [[ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012]]])) + [[ 458.44450061, 186.38962462, 75.78036626], + [ 458.44450061, 186.38962462, 75.78036626], + [ 458.44450061, 186.38962462, 75.78036626]]])) expected['dhi'] = (np. - array([[[ 81.38267402, 111.96631281, 153.04382915], - [ 62.3427452 , 85.77117175, 117.23837487], - [ 14.13195304, 19.44274618, 26.57578203]], + array([[[ 81.38267402, 112.33089156, 153.37702903], + [ 62.3427452 , 86.05045527, 117.49362079], + [ 14.13195304, 19.50605461, 26.63364159]], - [[ 85.87736039, 206.04588395, 249.82982258], - [ 65.78587472, 157.84030442, 191.38074731], - [ 14.91244713, 35.77949226, 43.38249342]], + [[ 86.1434259 , 206.20806974, 249.92868632], + [ 65.98969272, 157.96454595, 191.45648133], + [ 14.95864893, 35.80765553, 43.39966093]], - [[ 87.37492676, 184.31984947, 175.98479873], - [ 66.93307711, 141.19719644, 134.81217714], - [ 15.17249681, 32.00680597, 30.5594396 ]]])) + [[ 87.55238629, 184.39199913, 176.01413259], + [ 67.069019 , 141.25246629, 134.83464818], + [ 15.20331233, 32.01933462, 30.56453337]]])) apparent_zenith = np.linspace(0, 80, 3) airmass_absolute = np.linspace(1, 10, 3) @@ -169,12 +169,14 @@ def test_ineichen_arrays(): out = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity) for k, v in expected.items(): + print(expected[k]) + print(out[k]) assert_allclose(expected[k], out[k]) def test_ineichen_dni_extra(): expected = pd.DataFrame( - np.array([[1042.72590228, 946.35279683, 110.75033088]]), + np.array([[1042.72590228, 945.98661437, 111.11095021]]), columns=['ghi', 'dni', 'dhi']) out = clearsky.ineichen(10, 1, 3, dni_extra=pd.Series(1370)) @@ -183,7 +185,7 @@ def test_ineichen_dni_extra(): def test_ineichen_altitude(): expected = pd.DataFrame( - np.array([[1134.24312405, 994.95377835, 154.40492924]]), + np.array([[1134.24312405, 994.48564998, 154.86594569]]), columns=['ghi', 'dni', 'dhi']) out = clearsky.ineichen(10, 1, 3, altitude=pd.Series(2000)) diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py index 0c51b005a1..8e04ee2975 100644 --- a/pvlib/test/test_iam.py +++ b/pvlib/test/test_iam.py @@ -77,6 +77,7 @@ def test_martin_ruiz(): # will fail if default values change iam = _iam.martin_ruiz(aoi) assert_allclose(iam, expected) + # will fail if parameter names change iam = _iam.martin_ruiz(aoi=aoi, a_r=a_r) assert_allclose(iam, expected) @@ -99,11 +100,53 @@ def test_martin_ruiz(): iam = _iam.martin_ruiz(aoi, a_r) assert_series_equal(iam, expected) - # check exception clause - with pytest.raises(RuntimeError): + +def test_martin_ruiz_exception(): + + with pytest.raises(ValueError): _iam.martin_ruiz(0.0, a_r=0.0) +def test_martin_ruiz_diffuse(): + + surface_tilt = 30. + a_r = 0.16 + expected = (0.9549735, 0.7944426) + + # will fail if default values change + iam = _iam.martin_ruiz_diffuse(surface_tilt) + assert_allclose(iam, expected) + + # will fail if parameter names change + iam = _iam.martin_ruiz_diffuse(surface_tilt=surface_tilt, a_r=a_r) + assert_allclose(iam, expected) + + a_r = 0.18 + surface_tilt = [0, 30, 90, 120, 180, np.nan, np.inf] + expected_sky = [0.9407678, 0.9452250, 0.9407678, 0.9055541, 0.0000000, + np.nan, np.nan] + expected_gnd = [0.0000000, 0.7610849, 0.9407678, 0.9483508, 0.9407678, + np.nan, np.nan] + + # check various inputs as list + iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r) + assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True) + assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True) + + # check various inputs as array + iam = _iam.martin_ruiz_diffuse(np.array(surface_tilt), a_r) + assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True) + assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True) + + # check various inputs as Series + surface_tilt = pd.Series(surface_tilt) + expected_sky = pd.Series(expected_sky, name='iam_sky') + expected_gnd = pd.Series(expected_gnd, name='iam_ground') + iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r) + assert_series_equal(iam[0], expected_sky) + assert_series_equal(iam[1], expected_gnd) + + @requires_scipy def test_iam_interp(): diff --git a/pvlib/test/test_ivtools.py b/pvlib/test/test_ivtools.py index d4aca050c4..6f9fdd6fec 100644 --- a/pvlib/test/test_ivtools.py +++ b/pvlib/test/test_ivtools.py @@ -102,6 +102,35 @@ def test_fit_sdm_cec_sam(get_cec_params_cansol_cs5p_220p): cells_in_series=1, temp_ref=25) +@requires_scipy +def test_fit_sdm_desoto(): + result, _ = ivtools.fit_sdm_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, + i_sc=9.43, alpha_sc=0.005658, + beta_voc=-0.13788, + cells_in_series=60) + result_expected = {'I_L_ref': 9.45232, + 'I_o_ref': 3.22460e-10, + 'a_ref': 1.59128, + 'R_sh_ref': 125.798, + 'R_s': 0.297814, + 'alpha_sc': 0.005658, + 'EgRef': 1.121, + 'dEgdT': -0.0002677, + 'irrad_ref': 1000, + 'temp_ref': 25} + assert np.allclose(pd.Series(result), pd.Series(result_expected), + rtol=1e-4) + + +@requires_scipy +def test_fit_sdm_desoto_failure(): + with pytest.raises(RuntimeError) as exc: + ivtools.fit_sdm_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43, + alpha_sc=0.005658, beta_voc=-0.13788, + cells_in_series=10) + assert ('Parameter estimation failed') in str(exc.value) + + @pytest.fixture def get_bad_iv_curves(): # v1, i1 produces a bad value for I0_voc diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index c4d7c7aabe..cafaa81f71 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -470,10 +470,10 @@ def test_infer_aoi_model(location, system_no_aoi, aoi_model): def test_infer_aoi_model_invalid(location, system_no_aoi): - with pytest.raises(ValueError) as excinfo: + exc_text = 'could not infer AOI model' + with pytest.raises(ValueError, match=exc_text): ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') - assert 'could not infer AOI model' in str(excinfo.value) def constant_spectral_loss(mc): @@ -593,29 +593,26 @@ def test_deprecated_08(): module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1, 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1} # do not assign PVSystem.temperature_model_parameters + # leave out PVSystem.racking_model and PVSystem.module_type system = PVSystem(module_parameters=module_parameters) - with pytest.warns(pvlibDeprecationWarning): - ModelChain(system, location, - dc_model='desoto', - aoi_model='no_loss', spectral_model='no_loss', - temp_model='sapm', - ac_model='snlinverter') - system = PVSystem(module_parameters=module_parameters) - with pytest.warns(pvlibDeprecationWarning): - ModelChain(system, location, - dc_model='desoto', - aoi_model='no_loss', spectral_model='no_loss', - temperature_model='sapm', - temp_model='sapm', - ac_model='snlinverter') - system = PVSystem(module_parameters=module_parameters) - with pytest.raises(ValueError): - ModelChain(system, location, - dc_model='desoto', - aoi_model='no_loss', spectral_model='no_loss', - temperature_model='pvsyst', - temp_model='sapm', - ac_model='snlinverter') + # deprecated temp_model kwarg + warn_txt = 'temp_model keyword argument is deprecated' + with pytest.warns(pvlibDeprecationWarning, match=warn_txt): + ModelChain(system, location, dc_model='desoto', aoi_model='no_loss', + spectral_model='no_loss', ac_model='snlinverter', + temp_model='sapm') + # provide both temp_model and temperature_model kwargs + warn_txt = 'Provide only one of temperature_model' + with pytest.warns(pvlibDeprecationWarning, match=warn_txt): + ModelChain(system, location, dc_model='desoto', aoi_model='no_loss', + spectral_model='no_loss', ac_model='snlinverter', + temperature_model='sapm', temp_model='sapm') + # conflicting temp_model and temperature_model kwargs + exc_text = 'Conflicting temperature_model' + with pytest.raises(ValueError, match=exc_text): + ModelChain(system, location, dc_model='desoto', aoi_model='no_loss', + spectral_model='no_loss', ac_model='snlinverter', + temperature_model='pvsyst', temp_model='sapm') @requires_scipy diff --git a/pvlib/test/test_psm3.py b/pvlib/test/test_psm3.py index 43c4ca4c06..a594979526 100644 --- a/pvlib/test/test_psm3.py +++ b/pvlib/test/test_psm3.py @@ -25,6 +25,7 @@ DEMO_KEY = 'DEMO_KEY' +@pytest.mark.xfail(strict=True) @needs_pandas_0_22 def test_get_psm3(): """test get_psm3""" diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 8157c5704c..bc9f1426a7 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -419,16 +419,20 @@ def test__infer_temperature_model_params(): expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'sapm']['open_rack_glass_polymer'] assert expected == system._infer_temperature_model_params() - expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ - 'pvsyst']['freestanding'] system = pvsystem.PVSystem(module_parameters={}, racking_model='freestanding', module_type='glass_polymer') + expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'pvsyst']['freestanding'] assert expected == system._infer_temperature_model_params() - system = pvsystem.PVSystem(module_parameters={}, - racking_model='not_a_rack_model', - module_type='glass_polymer') - assert {} == system._infer_temperature_model_params() + + +def test__infer_temperature_model_params_deprec_warning(): + warn_txt = "Reverting to deprecated default" + with pytest.warns(pvlibDeprecationWarning, match=warn_txt): + pvsystem.PVSystem(module_parameters={}, + racking_model='not_a_rack_model', + module_type='glass_polymer') def test_calcparams_desoto(cec_module_params): diff --git a/pvlib/tools.py b/pvlib/tools.py index cbd59a8541..1f8c1b7cbb 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -206,22 +206,22 @@ def _datetimelike_scalar_to_datetimeindex(time): return pd.DatetimeIndex([pd.Timestamp(time)]) -def _scalar_out(input): - if np.isscalar(input): - output = input +def _scalar_out(arg): + if np.isscalar(arg): + output = arg else: # # works if it's a 1 length array and # will throw a ValueError otherwise - output = input.item() + output = np.asarray(arg).item() return output -def _array_out(input): - if isinstance(input, pd.Series): - output = input.values +def _array_out(arg): + if isinstance(arg, pd.Series): + output = arg.values else: - output = input + output = arg return output