From 73f463820348563b491cf42f17518e5104ac6bd0 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 10:06:20 -0600 Subject: [PATCH 01/34] move iam functions to iam.py --- pvlib/iam.py | 336 ++++++++++++++++++++++++++++++++++++++++++++++ pvlib/pvsystem.py | 327 -------------------------------------------- 2 files changed, 336 insertions(+), 327 deletions(-) create mode 100644 pvlib/iam.py diff --git a/pvlib/iam.py b/pvlib/iam.py new file mode 100644 index 0000000000..8c06af363b --- /dev/null +++ b/pvlib/iam.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Oct 2 09:04:01 2019 + +@author: cwhanse +""" +import numpy as np +import pandas as pd +from tools import cosd, sind, tand, asind + + +def ashrae(aoi, b=0.05): + ''' + Determine the incidence angle modifier using the ASHRAE transmission + model. + + The ASHRAE (American Society of Heating, Refrigeration, and Air + Conditioning Engineers) transmission model is developed in + [1], and in [2]. The model has been used in software such as PVSyst [3]. + + Parameters + ---------- + aoi : numeric + The angle of incidence (AOI) between the module normal vector and the + sun-beam vector in degrees. Angles of nan will result in nan. + + b : float, default 0.05 + A parameter to adjust the incidence angle modifier as a function of + angle of incidence. Typical values are on the order of 0.05 [3]. + + Returns + ------- + iam : numeric + The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90 + and for all ``iam`` values that would be less than 0. + + Notes + ----- + The incidence angle modifier is calculated as + + .. math:: + + IAM = 1 - b \times (\sec(aoi) - 1) + + As AOI approaches 90 degrees, the model yields negative values for IAM. + Negative IAM values are set to zero in this implementation. + + References + ---------- + [1] Souka A.F., Safwat H.H., "Determination of the optimum + orientations for the double exposure flat-plate collector and its + reflections". Solar Energy vol .10, pp 170-174. 1966. + + [2] ASHRAE standard 93-77 + + [3] PVsyst Contextual Help. + http://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on + September 10, 2012 + + See Also + -------- + iam.physical + iam.martin_ruiz + iam.interp + ''' + + iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1)) + aoi_gte_90 = np.full_like(aoi, False, dtype='bool') + np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90) + iam = np.where(aoi_gte_90, 0, iam) + iam = np.maximum(0, iam) + + if isinstance(aoi, pd.Series): + iam = pd.Series(iam, index=aoi.index) + + return iam + + +def physical(aoi, n=1.526, K=4., L=0.002): + ''' + Determine the incidence angle modifier using refractive index ``n``, + extinction coefficient ``K``, and glazing thickness ``L``. + + ``physical`` calculates the incidence angle modifier as described in + [1], Section 3. The calculation is based on a physical model of absorbtion + and transmission through a transparent cover. + + Parameters + ---------- + aoi : numeric + The angle of incidence between the module normal vector and the + sun-beam vector in degrees. Angles of 0 are replaced with 1e-06 + to ensure non-nan results. Angles of nan will result in nan. + + n : numeric, default 1.526 + The effective index of refraction (unitless). Reference [1] + indicates that a value of 1.526 is acceptable for glass. + + K : numeric, default 4.0 + The glazing extinction coefficient in units of 1/meters. + Reference [1] indicates that a value of 4 is reasonable for + "water white" glass. + + L : numeric, default 0.002 + The glazing thickness in units of meters. Reference [1] + indicates that 0.002 meters (2 mm) is reasonable for most + glass-covered PV panels. + + Returns + ------- + iam : numeric + The incident angle modifier + + Notes + ----- + The authors of this function believe that Eqn. 14 in [1] is + incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`. + Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI)) + + 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] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering + of Thermal Processes, third edition. [Books24x7 version] Available + from http://common.books24x7.com/toc.aspx?bookid=17160. + + See Also + -------- + iam.martin_ruiz + iam.ashrae + iam.interp + ''' + zeroang = 1e-06 + + # hold a new reference to the input aoi object since we're going to + # overwrite the aoi reference below, but we'll need it for the + # series check at the end of the function + aoi_input = aoi + + aoi = np.where(aoi == 0, zeroang, aoi) + + # angle of reflection + thetar_deg = asind(1.0 / n * (sind(aoi))) + + # reflectance and transmittance for normal incidence light + rho_zero = ((1-n) / (1+n)) ** 2 + tau_zero = np.exp(-K*L) + + # reflectance for parallel and perpendicular polarized light + rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2 + rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2 + + # transmittance for non-normal light + tau = np.exp(-K * L / cosd(thetar_deg)) + + # iam is ratio of non-normal to normal incidence transmitted light + # after deducting the reflected portion of each + iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero) + + with np.errstate(invalid='ignore'): + # angles near zero produce nan, but iam is defined as one + small_angle = 1e-06 + iam = np.where(np.abs(aoi) < small_angle, 1.0, iam) + + # angles at 90 degrees can produce tiny negative values, + # which should be zero. this is a result of calculation precision + # rather than the physical model + iam = np.where(iam < 0, 0, iam) + + # for light coming from behind the plane, none can enter the module + iam = np.where(aoi > 90, 0, iam) + + if isinstance(aoi_input, pd.Series): + iam = pd.Series(iam, index=aoi_input.index) + + return iam + + +def martin_ruiz(aoi, a_r=0.16): + ''' + Determine the incidence angle modifier (IAM) using the Martin + and Ruiz incident angle model. + + Parameters + ---------- + aoi : numeric, degrees + The angle of incidence between the module normal vector and the + sun-beam vector in degrees. + + 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. + + Returns + ------- + iam : numeric + The incident angle modifier(s) + + Notes + ----- + `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. + + The incident angle modifier is defined as + + ..math:: + + IAM = \frac{1 - \exp(-\cos(\frac{aoi}{a_r}))} + {1 - \exp(\frac{-1}{a_r}} + + which is presented as AL(alpha) = 1 - IAM in equation 4 of [1], with alpha + representing the angle of incidence AOI. Thus IAM = 1 at AOI = 0, and + IAM = 0 at AOI = 90. This equation is only valid for -90 <= aoi <= 90, + therefore iam is constrained to 0.0 beyond this range. + + 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. + + See Also + -------- + iam.physical + iam.ashrae + iam.interp + ''' + # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 + + aoi_input = aoi + + aoi = np.asanyarray(aoi) + 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.") + + with np.errstate(invalid='ignore'): + iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r)) + iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam) + + if isinstance(aoi_input, pd.Series): + iam = pd.Series(iam, index=aoi_input.index) + + return iam + + +def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): + ''' + Determine the incidence angle modifier (IAM) by interpolating a set of + reference values, which are usually measured values. + + Parameters + ---------- + aoi : numeric, degrees + The angle of incidence between the module normal vector and the + sun-beam vector in degrees. + + theta_ref : numeric, degrees + Vector of angles at which the IAM is known. + + iam_ref : numeric, unitless + IAM values for each angle in ``theta_ref``. + + method : str, default 'linear' + Specifies the interpolation method. + Useful options are: 'linear', 'quadratic','cubic'. + See scipy.interpolate.interp1d for more options. + + normalize : boolean + When true, the interpolated values are divided by the interpolated + value at zero degrees. This ensures that the iam at normal + incidence is equal to 1.0. + + Returns + ------- + iam : numeric + The incident angle modifier(s) + + Notes: + ------ + ``theta_ref`` must have two or more points and may span any range of + angles. Typically there will be a dozen or more points in the range 0-90 + degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated, + but constrained to be non-negative. + + The sign of ``aoi`` is ignored; only the magnitude is used. + + See Also + -------- + iam.physical + iam.ashrae + iam.martin_ruiz + ''' + # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 + + from scipy.interpolate import interp1d + + # Scipy doesn't give the clearest feedback, so check number of points here. + MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4} + + if len(theta_ref) < MIN_REF_VALS.get(method, 2): + raise ValueError("Too few reference points defined " + "for interpolation method '%s'." % method) + + if np.any(np.less(iam_ref, 0)): + raise ValueError("Negative value(s) found in 'iam_ref'. " + "This is not physically possible.") + + interpolator = interp1d(theta_ref, iam_ref, kind=method, + fill_value='extrapolate') + aoi_input = aoi + + aoi = np.asanyarray(aoi) + aoi = np.abs(aoi) + iam = interpolator(aoi) + iam = np.clip(iam, 0, None) + + if normalize: + iam /= interpolator(0) + + if isinstance(aoi_input, pd.Series): + iam = pd.Series(iam, index=aoi_input.index) + + return iam diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bebe2f1263..b66d290a31 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -955,333 +955,6 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, modules_per_string, return system -def ashraeiam(aoi, b=0.05): - ''' - Determine the incidence angle modifier using the ASHRAE transmission - model. - - ashraeiam calculates the incidence angle modifier as developed in - [1], and adopted by ASHRAE (American Society of Heating, - Refrigeration, and Air Conditioning Engineers) [2]. The model has - been used by model programs such as PVSyst [3]. - - Note: For incident angles near 90 degrees, this model has a - discontinuity which has been addressed in this function. - - Parameters - ---------- - aoi : numeric - The angle of incidence between the module normal vector and the - sun-beam vector in degrees. Angles of nan will result in nan. - - b : float, default 0.05 - A parameter to adjust the modifier as a function of angle of - incidence. Typical values are on the order of 0.05 [3]. - - Returns - ------- - IAM : numeric - The incident angle modifier calculated as 1-b*(sec(aoi)-1) as - described in [2,3]. - - Returns zeros for all abs(aoi) >= 90 and for all IAM values that - would be less than 0. - - References - ---------- - [1] Souka A.F., Safwat H.H., "Determination of the optimum - orientations for the double exposure flat-plate collector and its - reflections". Solar Energy vol .10, pp 170-174. 1966. - - [2] ASHRAE standard 93-77 - - [3] PVsyst Contextual Help. - http://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on - September 10, 2012 - - See Also - -------- - irradiance.aoi - physicaliam - ''' - - iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1)) - aoi_gte_90 = np.full_like(aoi, False, dtype='bool') - np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90) - iam = np.where(aoi_gte_90, 0, iam) - iam = np.maximum(0, iam) - - if isinstance(aoi, pd.Series): - iam = pd.Series(iam, index=aoi.index) - - return iam - - -def physicaliam(aoi, n=1.526, K=4., L=0.002): - ''' - Determine the incidence angle modifier using refractive index, - extinction coefficient, and glazing thickness. - - physicaliam calculates the incidence angle modifier as described in - De Soto et al. "Improvement and validation of a model for - photovoltaic array performance", section 3. The calculation is based - on a physical model of absorbtion and transmission through a - cover. - - Note: The authors of this function believe that eqn. 14 in [1] is - incorrect. This function uses the following equation in its place: - theta_r = arcsin(1/n * sin(aoi)) - - Parameters - ---------- - aoi : numeric - The angle of incidence between the module normal vector and the - sun-beam vector in degrees. Angles of 0 are replaced with 1e-06 - to ensure non-nan results. Angles of nan will result in nan. - - n : numeric, default 1.526 - The effective index of refraction (unitless). Reference [1] - indicates that a value of 1.526 is acceptable for glass. n must - be a numeric scalar or vector with all values >=0. If n is a - vector, it must be the same size as all other input vectors. - - K : numeric, default 4.0 - The glazing extinction coefficient in units of 1/meters. - Reference [1] indicates that a value of 4 is reasonable for - "water white" glass. K must be a numeric scalar or vector with - all values >=0. If K is a vector, it must be the same size as - all other input vectors. - - L : numeric, default 0.002 - The glazing thickness in units of meters. Reference [1] - indicates that 0.002 meters (2 mm) is reasonable for most - glass-covered PV panels. L must be a numeric scalar or vector - with all values >=0. If L is a vector, it must be the same size - as all other input vectors. - - Returns - ------- - iam : numeric - The incident angle modifier - - 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] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering - of Thermal Processes, third edition. [Books24x7 version] Available - from http://common.books24x7.com/toc.aspx?bookid=17160. - - See Also - -------- - getaoi - ephemeris - spa - ashraeiam - ''' - zeroang = 1e-06 - - # hold a new reference to the input aoi object since we're going to - # overwrite the aoi reference below, but we'll need it for the - # series check at the end of the function - aoi_input = aoi - - aoi = np.where(aoi == 0, zeroang, aoi) - - # angle of reflection - thetar_deg = asind(1.0 / n * (sind(aoi))) - - # reflectance and transmittance for normal incidence light - rho_zero = ((1-n) / (1+n)) ** 2 - tau_zero = np.exp(-K*L) - - # reflectance for parallel and perpendicular polarized light - rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2 - rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2 - - # transmittance for non-normal light - tau = np.exp(-K * L / cosd(thetar_deg)) - - # iam is ratio of non-normal to normal incidence transmitted light - # after deducting the reflected portion of each - iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero) - - with np.errstate(invalid='ignore'): - # angles near zero produce nan, but iam is defined as one - small_angle = 1e-06 - iam = np.where(np.abs(aoi) < small_angle, 1.0, iam) - - # angles at 90 degrees can produce tiny negative values, - # which should be zero. this is a result of calculation precision - # rather than the physical model - iam = np.where(iam < 0, 0, iam) - - # for light coming from behind the plane, none can enter the module - iam = np.where(aoi > 90, 0, iam) - - if isinstance(aoi_input, pd.Series): - iam = pd.Series(iam, index=aoi_input.index) - - return iam - - -def iam_martin_ruiz(aoi, a_r=0.16): - ''' - Determine the incidence angle modifier (iam) using the Martin - and Ruiz incident angle model. - - Parameters - ---------- - aoi : numeric, degrees - The angle of incidence between the module normal vector and the - sun-beam vector in degrees. Theta must be a numeric scalar or vector. - iam is 0 where |aoi| > 90. - - 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 a positive numeric scalar or vector (same length as aoi). - - Returns - ------- - iam : numeric - The incident angle modifier(s) - - Notes - ----- - iam_martin_ruiz calculates the incidence angle modifier (iam) - as described by Martin and Ruiz in [1]. The information - required is the incident angle (aoi) and the angular losses - coefficient (a_r). Please note that [1] has a corrigendum which makes - the document much simpler to understand. - - The incident angle modifier is defined as - [1-exp(-cos(aoi/ar))] / [1-exp(-1/ar)], which is - presented as AL(alpha) = 1 - IAM in equation 4 of [1]. Thus IAM is - equal to 1 at aoi = 0, and equal to 0 at aoi = 90. This equation is only - valid for -90 <= aoi <= 90, therefore iam must be constrained to 0.0 - beyond this range. - - 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. - - See Also - -------- - physicaliam - ashraeiam - iam_interp - ''' - # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 - - aoi_input = aoi - - aoi = np.asanyarray(aoi) - 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.") - - with np.errstate(invalid='ignore'): - iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r)) - iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam) - - if isinstance(aoi_input, pd.Series): - iam = pd.Series(iam, index=aoi_input.index) - - return iam - - -def iam_interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): - ''' - Determine the incidence angle modifier (iam) by interpolating a set of - reference values, which are usually measured values. - - Parameters - ---------- - aoi : numeric, degrees - The angle of incidence between the module normal vector and the - sun-beam vector in degrees. - - theta_ref : numeric, degrees - Vector of angles at which the iam is known. - - iam_ref : numeric, unitless - iam values for each angle in theta_ref. - - method : str, default 'linear' - Specifies the interpolation method. - Useful options are: 'linear', 'quadratic','cubic'. - See scipy.interpolate.interp1d for more options. - - normalize : boolean - When true, the interpolated values are divided by the interpolated - value at zero degrees. This ensures that the iam at normal - incidence is equal to 1.0. - - Returns - ------- - iam : numeric - The incident angle modifier(s) - - Notes: - ------ - theta_ref must have two or more points and may span any range of angles. - Typically there will be a dozen or more points in the range 0-90 degrees. - iam beyond the range of theta_ref are extrapolated, but constrained to be - non-negative. - - The sign of aoi is ignored; only the magnitude is used. - - See Also - -------- - physicaliam - ashraeiam - iam_martin_ruiz - ''' - # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 - - from scipy.interpolate import interp1d - - # Scipy doesn't give the clearest feedback, so check number of points here. - MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4} - - if len(theta_ref) < MIN_REF_VALS.get(method, 2): - raise ValueError("Too few reference points defined " - "for interpolation method '%s'." % method) - - if np.any(np.less(iam_ref, 0)): - raise ValueError("Negative value(s) found in 'iam_ref'. " - "This is not physically possible.") - - interpolator = interp1d(theta_ref, iam_ref, kind=method, - fill_value='extrapolate') - aoi_input = aoi - - aoi = np.asanyarray(aoi) - aoi = np.abs(aoi) - iam = interpolator(aoi) - iam = np.clip(iam, 0, None) - - if normalize: - iam /= interpolator(0) - - if isinstance(aoi_input, pd.Series): - iam = pd.Series(iam, index=aoi_input.index) - - return iam - - def calcparams_desoto(effective_irradiance, temp_cell, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, EgRef=1.121, dEgdT=-0.0002677, From a89cc019b1f1f3ebc8376d573b9bf20d7cf423bb Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 11:12:13 -0600 Subject: [PATCH 02/34] move function tests to test_iam.py --- pvlib/test/test_iam.py | 150 ++++++++++++++++++++++++++++++++++++ pvlib/test/test_pvsystem.py | 130 ------------------------------- 2 files changed, 150 insertions(+), 130 deletions(-) create mode 100644 pvlib/test/test_iam.py diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py new file mode 100644 index 0000000000..7b68b3b08b --- /dev/null +++ b/pvlib/test/test_iam.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Oct 2 10:14:16 2019 + +@author: cwhanse +""" + +import numpy as np +import pandas as pd + +import pytest +from pandas.util.testing import assert_series_equal +from numpy.testing import assert_allclose + +from pvlib import iam as _iam +from conftest import needs_numpy_1_10, requires_scipy + + +@needs_numpy_1_10 +def test_ashrae(): + thetas = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 89., 90., + np.nan]) + expected = np.array([0, 0.9193437, 0.97928932, 0.99588039, 1., 0.99588039, + 0.97928932, 0.9193437, 0, 0, np.nan]) + iam = _iam.ashrae(thetas, .05) + assert_allclose(iam, expected, equal_nan=True) + iam_series = _iam.ashrae(pd.Series(thetas)) + assert_series_equal(iam_series, pd.Series(expected)) + + +@needs_numpy_1_10 +def test_ashrae_scalar(): + thetas = -45. + iam = _iam.ashrae(thetas, .05) + expected = 0.97928932 + assert_allclose(iam, expected, equal_nan=True) + thetas = np.nan + iam = _iam.ashrae(thetas, .05) + expected = np.nan + assert_allclose(iam, expected, equal_nan=True) + + +@needs_numpy_1_10 +def test_physical(): + aoi = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 90., + np.nan]) + expected = np.array([0, 0.8893998, 0.98797788, 0.99926198, 1, 0.99926198, + 0.98797788, 0.8893998, 0, np.nan]) + iam = _iam.physical(aoi, 1.526, 0.002, 4) + assert_allclose(iam, expected, equal_nan=True) + + # GitHub issue 397 + aoi = pd.Series(aoi) + iam = _iam.physical(aoi, 1.526, 0.002, 4) + expected = pd.Series(expected) + assert_series_equal(iam, expected) + + +@needs_numpy_1_10 +def test_physical_scalar(): + aoi = -45. + iam = _iam.physical(aoi, 1.526, 0.002, 4) + expected = 0.98797788 + assert_allclose(iam, expected, equal_nan=True) + aoi = np.nan + iam = _iam.physical(aoi, 1.526, 0.002, 4) + expected = np.nan + assert_allclose(iam, expected, equal_nan=True) + + +def test_martin_ruiz(): + + aoi = 45. + a_r = 0.16 + expected = 0.98986965 + + # 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) + + a_r = 0.18 + aoi = [-100, -60, 0, 60, 100, np.nan, np.inf] + expected = [0.0, 0.9414631, 1.0, 0.9414631, 0.0, np.nan, 0.0] + + # check out of range of inputs as list + iam = _iam.martin_ruiz(aoi, a_r) + assert_allclose(iam, expected, equal_nan=True) + + # check out of range of inputs as array + iam = _iam.martin_ruiz(np.array(aoi), a_r) + assert_allclose(iam, expected, equal_nan=True) + + # check out of range of inputs as Series + aoi = pd.Series(aoi) + expected = pd.Series(expected) + iam = _iam.martin_ruiz(aoi, a_r) + assert_series_equal(iam, expected) + + # check exception clause + with pytest.raises(RuntimeError): + _iam.martin_ruiz(0.0, a_r=0.0) + + +@requires_scipy +def test_iam_interp(): + + aoi_meas = [0.0, 45.0, 65.0, 75.0] + iam_meas = [1.0, 0.9, 0.8, 0.6] + + # simple default linear method + aoi = 55.0 + expected = 0.85 + iam = _iam.interp(aoi, aoi_meas, iam_meas) + assert_allclose(iam, expected) + + # simple non-default method + aoi = 55.0 + expected = 0.8878062 + iam = _iam.interp(aoi, aoi_meas, iam_meas, method='cubic') + assert_allclose(iam, expected) + + # check with all reference values + aoi = aoi_meas + expected = iam_meas + iam = _iam.interp(aoi, aoi_meas, iam_meas) + assert_allclose(iam, expected) + + # check normalization and Series + aoi = pd.Series(aoi) + expected = pd.Series(expected) + iam_mult = np.multiply(0.9, iam_meas) + iam = _iam.interp(aoi, aoi_meas, iam_mult, normalize=True) + assert_series_equal(iam, expected) + + # check beyond reference values + aoi = [-45, 0, 45, 85, 90, 95, 100, 105, 110] + expected = [0.9, 1.0, 0.9, 0.4, 0.3, 0.2, 0.1, 0.0, 0.0] + iam = _iam.interp(aoi, aoi_meas, iam_meas) + assert_allclose(iam, expected) + + # check exception clause + with pytest.raises(ValueError): + _iam.interp(0.0, [0], [1]) + + # check exception clause + with pytest.raises(ValueError): + _iam.interp(0.0, [0, 90], [1, -1]) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 117b416915..e00566af0f 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -82,27 +82,6 @@ def test_systemdef_dict(): assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5) -@needs_numpy_1_10 -def test_ashraeiam(): - thetas = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 89., 90. , np.nan]) - iam = pvsystem.ashraeiam(thetas, .05) - expected = np.array([ 0, 0.9193437 , 0.97928932, 0.99588039, 1. , - 0.99588039, 0.97928932, 0.9193437 , 0, 0, np.nan]) - assert_allclose(iam, expected, equal_nan=True) - - -@needs_numpy_1_10 -def test_ashraeiam_scalar(): - thetas = -45. - iam = pvsystem.ashraeiam(thetas, .05) - expected = 0.97928932 - assert_allclose(iam, expected, equal_nan=True) - thetas = np.nan - iam = pvsystem.ashraeiam(thetas, .05) - expected = np.nan - assert_allclose(iam, expected, equal_nan=True) - - def test_PVSystem_ashraeiam(mocker): mocker.spy(pvsystem, 'ashraeiam') module_parameters = pd.Series({'b': 0.05}) @@ -113,33 +92,6 @@ def test_PVSystem_ashraeiam(mocker): assert iam < 1. -@needs_numpy_1_10 -def test_physicaliam(): - aoi = np.array([-90. , -67.5, -45. , -22.5, 0. , 22.5, 45. , 67.5, 90. , np.nan]) - iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4) - expected = np.array([ 0, 0.8893998, 0.98797788, 0.99926198, 1, - 0.99926198, 0.98797788, 0.8893998, 0, np.nan]) - assert_allclose(iam, expected, equal_nan=True) - - # GitHub issue 397 - aoi = pd.Series(aoi) - iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4) - expected = pd.Series(expected) - assert_series_equal(iam, expected) - - -@needs_numpy_1_10 -def test_physicaliam_scalar(): - aoi = -45. - iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4) - expected = 0.98797788 - assert_allclose(iam, expected, equal_nan=True) - aoi = np.nan - iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4) - expected = np.nan - assert_allclose(iam, expected, equal_nan=True) - - def test_PVSystem_physicaliam(mocker): module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526}) system = pvsystem.PVSystem(module_parameters=module_parameters) @@ -150,88 +102,6 @@ def test_PVSystem_physicaliam(mocker): assert iam < 1. -def test_iam_martin_ruiz(): - - aoi = 45. - a_r = 0.16 - expected = 0.98986965 - - # will fail of default values change - iam = pvsystem.iam_martin_ruiz(aoi) - assert_allclose(iam, expected) - # will fail of parameter names change - iam = pvsystem.iam_martin_ruiz(aoi=aoi, a_r=a_r) - assert_allclose(iam, expected) - - a_r = 0.18 - aoi = [-100, -60, 0, 60, 100, np.nan, np.inf] - expected = [0.0, 0.9414631, 1.0, 0.9414631, 0.0, np.nan, 0.0] - - # check out of range of inputs as list - iam = pvsystem.iam_martin_ruiz(aoi, a_r) - assert_allclose(iam, expected, equal_nan=True) - - # check out of range of inputs as array - iam = pvsystem.iam_martin_ruiz(np.array(aoi), a_r) - assert_allclose(iam, expected, equal_nan=True) - - # check out of range of inputs as Series - aoi = pd.Series(aoi) - expected = pd.Series(expected) - iam = pvsystem.iam_martin_ruiz(aoi, a_r) - assert_series_equal(iam, expected) - - # check exception clause - with pytest.raises(RuntimeError): - pvsystem.iam_martin_ruiz(0.0, a_r=0.0) - - -@requires_scipy -def test_iam_interp(): - - aoi_meas = [0.0, 45.0, 65.0, 75.0] - iam_meas = [1.0, 0.9, 0.8, 0.6] - - # simple default linear method - aoi = 55.0 - expected = 0.85 - iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas) - assert_allclose(iam, expected) - - # simple non-default method - aoi = 55.0 - expected = 0.8878062 - iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas, method='cubic') - assert_allclose(iam, expected) - - # check with all reference values - aoi = aoi_meas - expected = iam_meas - iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas) - assert_allclose(iam, expected) - - # check normalization and Series - aoi = pd.Series(aoi) - expected = pd.Series(expected) - iam_mult = np.multiply(0.9, iam_meas) - iam = pvsystem.iam_interp(aoi, aoi_meas, iam_mult, normalize=True) - assert_series_equal(iam, expected) - - # check beyond reference values - aoi = [-45, 0, 45, 85, 90, 95, 100, 105, 110] - expected = [0.9, 1.0, 0.9, 0.4, 0.3, 0.2, 0.1, 0.0, 0.0] - iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas) - assert_allclose(iam, expected) - - # check exception clause - with pytest.raises(ValueError): - pvsystem.iam_interp(0.0, [0], [1]) - - # check exception clause - with pytest.raises(ValueError): - pvsystem.iam_interp(0.0, [0, 90], [1, -1]) - - @pytest.fixture(scope="session") def sapm_module_params(sam_data): modules = sam_data['sandiamod'] From 0f1c90c802a0b74b27fc13442d9808815c204448 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 11:22:13 -0600 Subject: [PATCH 03/34] adjust PVSystem methods, add deprecation for functions and PVSystem methods --- pvlib/pvsystem.py | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index b66d290a31..3d9a660850 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -13,9 +13,9 @@ from pvlib._deprecation import deprecated -from pvlib import (atmosphere, irradiance, singlediode as _singlediode, +from pvlib import (atmosphere, iam, irradiance, singlediode as _singlediode, temperature) -from pvlib.tools import _build_kwargs, cosd, asind, sind, tand +from pvlib.tools import _build_kwargs from pvlib.location import Location from pvlib._deprecation import pvlibDeprecationWarning @@ -316,11 +316,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, albedo=self.albedo, **kwargs) - def ashraeiam(self, aoi): + def iam_ashrae(self, aoi): """ Determine the incidence angle modifier using ``self.module_parameters['b']``, ``aoi``, - and the :py:func:`ashraeiam` function. + and the :py:func:`iam.ashrae` function. Uses default arguments if keys not in module_parameters. @@ -336,16 +336,26 @@ def ashraeiam(self, aoi): """ kwargs = _build_kwargs(['b'], self.module_parameters) - return ashraeiam(aoi, **kwargs) + return iam.ashrae(aoi, **kwargs) - def physicaliam(self, aoi): + def ashraeiam(self, aoi): + """ + Deprecated. Use ``PVSystem.iam_ashrae`` instead. + """ + import warnings + warnings.warn( + 'PVSystem.ashraeiam is deprecated and will be removed in v0.8,' + ' use PVSystem.iam_ashrae instead', pvlibDeprecationWarning) + return PVSystem.iam_ashrae(self, aoi) + + def iam_physical(self, aoi): """ Determine the incidence angle modifier using ``aoi``, ``self.module_parameters['K']``, ``self.module_parameters['L']``, ``self.module_parameters['n']``, and the - :py:func:`physicaliam` function. + :py:func:`iam.physical` function. Uses default arguments if keys not in module_parameters. @@ -361,7 +371,17 @@ def physicaliam(self, aoi): """ kwargs = _build_kwargs(['K', 'L', 'n'], self.module_parameters) - return physicaliam(aoi, **kwargs) + return iam.physical(aoi, **kwargs) + + def physicaliam(self, aoi): + """ + Deprecated. Use ``PVSystem.iam_physical`` instead. + """ + import warnings + warnings.warn( + 'PVSystem.physicaliam is deprecated and will be removed in v0.8,' + ' use PVSystem.iam_physical instead', pvlibDeprecationWarning) + return PVSystem.iam_physical(self, aoi) def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ @@ -2800,3 +2820,13 @@ def pvwatts_ac(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): pac = np.maximum(0, pac) # GH 541 return pac + + +ashraeiam = deprecated('0.7', alternative='iam.ashrae', + name='ashraeiam', removal='0.8', + )(iam.ashrae) + + +physicaliam = deprecated('0.7', alternative='iam.physical', + name='physicaliam', removal='0.8', + )(iam.physical) From 0a55b5452b6f363e32f86a8674a05b77fd690afe Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 11:28:44 -0600 Subject: [PATCH 04/34] adjust PVSystem tests, test for function deprecation --- pvlib/test/test_pvsystem.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index e00566af0f..24000b3cec 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -1,6 +1,5 @@ import inspect import os -import datetime from collections import OrderedDict import numpy as np @@ -12,10 +11,8 @@ from numpy.testing import assert_allclose from pvlib import pvsystem -from pvlib import clearsky -from pvlib import irradiance from pvlib import atmosphere -from pvlib import solarposition +from pvlib import iam as _iam from pvlib.location import Location from pvlib import temperature from pvlib._deprecation import pvlibDeprecationWarning @@ -82,23 +79,23 @@ def test_systemdef_dict(): assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5) -def test_PVSystem_ashraeiam(mocker): - mocker.spy(pvsystem, 'ashraeiam') +def test_PVSystem_iam_ashrae(mocker): + mocker.spy(_iam, 'ashrae') module_parameters = pd.Series({'b': 0.05}) system = pvsystem.PVSystem(module_parameters=module_parameters) thetas = 1 - iam = system.ashraeiam(thetas) - pvsystem.ashraeiam.assert_called_once_with(thetas, b=0.05) + iam = system.iam_ashrae(thetas) + _iam.ashrae.assert_called_once_with(thetas, b=0.05) assert iam < 1. -def test_PVSystem_physicaliam(mocker): +def test_PVSystem_iam_physical(mocker): module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526}) system = pvsystem.PVSystem(module_parameters=module_parameters) - mocker.spy(pvsystem, 'physicaliam') + mocker.spy(_iam, 'physical') thetas = 1 - iam = system.physicaliam(thetas) - pvsystem.physicaliam.assert_called_once_with(thetas, **module_parameters) + iam = system.iam_physical(thetas) + _iam.physical.assert_called_once_with(thetas, **module_parameters) assert iam < 1. @@ -1501,6 +1498,10 @@ def test_deprecated_08(): with pytest.warns(pvlibDeprecationWarning): pvsystem.PVSystem(module_parameters=module_parameters, racking_model='open', module_type='glass_glass') + with pytest.warns(pvlibDeprecationWarning): + pvsystem.ashraeiam(45) + with pytest.warns(pvlibDeprecationWarning): + pvsystem.physicaliam(45) @fail_on_pvlib_version('0.8') From 8505a0d0076647161cc561288b7a463ab1d7637d Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 14:13:31 -0600 Subject: [PATCH 05/34] move sapm aoi function, adjust ModelChain methods --- pvlib/iam.py | 61 +++++++++++++++++++++++++++ pvlib/modelchain.py | 6 +-- pvlib/pvsystem.py | 83 +++++++------------------------------ pvlib/test/test_iam.py | 21 ++++++++++ pvlib/test/test_pvsystem.py | 27 ++---------- 5 files changed, 105 insertions(+), 93 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 8c06af363b..ba8f437333 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -334,3 +334,64 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): iam = pd.Series(iam, index=aoi_input.index) return iam + + +def sapm(aoi, module, upper=None): + """ + Determine the incidence angle modifier (IAM) using the SAPM model. + + Parameters + ---------- + aoi : numeric + Angle of incidence in degrees. Negative input angles will return + zeros. + + module : dict-like + A dict, Series, or DataFrame defining the SAPM performance + parameters. See the :py:func:`sapm` notes section for more + details. + + upper : None or float, default None + Upper limit on the results. + + Returns + ------- + iam : numeric + The SAPM angle of incidence loss coefficient F2. + + Notes + ----- + The SAPM [1] traditionally does not define an upper limit on the AOI + loss function and values slightly exceeding 1 may exist for moderate + angles of incidence (15-40 degrees). However, users may consider + imposing an upper limit of 1. + + References + ---------- + [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance + Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, + NM. + + [2] B.H. King et al, "Procedure to Determine Coefficients for the + Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia + National Laboratories (2016). + + [3] B.H. King et al, "Recent Advancements in Outdoor Measurement + Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015). + DOI: 10.1109/PVSC.2015.7355849 + """ + + aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'], + module['B1'], module['B0']] + + iam = np.polyval(aoi_coeff, aoi) + iam = np.clip(iam, 0, upper) + # nan tolerant masking + aoi_lt_0 = np.full_like(aoi, False, dtype='bool') + np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0) + iam = np.where(aoi_lt_0, 0, iam) + + if isinstance(aoi, pd.Series): + iam = pd.Series(iam, aoi.index) + + return iam diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 92a92e2abc..9892edbd30 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -590,15 +590,15 @@ def infer_aoi_model(self): 'aoi_model="no_loss".') def ashrae_aoi_loss(self): - self.aoi_modifier = self.system.ashraeiam(self.aoi) + self.aoi_modifier = self.system.iam_ashrae(self.aoi) return self def physical_aoi_loss(self): - self.aoi_modifier = self.system.physicaliam(self.aoi) + self.aoi_modifier = self.system.iam_physical(self.aoi) return self def sapm_aoi_loss(self): - self.aoi_modifier = self.system.sapm_aoi_loss(self.aoi) + self.aoi_modifier = self.system.iam_sapm(self.aoi) return self def no_aoi_loss(self): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3d9a660850..eafebdda11 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -550,8 +550,18 @@ def sapm_spectral_loss(self, airmass_absolute): def sapm_aoi_loss(self, aoi): """ - Use the :py:func:`sapm_aoi_loss` function, the input parameters, - and ``self.module_parameters`` to calculate F2. + Deprecated. Use ``PVSystem.iam_sapm`` instead. + """ + import warnings + warnings.warn( + 'PVSystem.sapm_aoi_loss is deprecated and will be removed in v0.8,' + ' use PVSystem.iam_sapm instead', pvlibDeprecationWarning) + return PVSystem.iam_sapm(self, aoi) + + def iam_sapm(self, aoi): + """ + Use the :py:func:`iam.sapm` function, the input parameters, + and ``self.module_parameters`` to calculate iam. Parameters ---------- @@ -560,10 +570,10 @@ def sapm_aoi_loss(self, aoi): Returns ------- - F2 : numeric - The SAPM angle of incidence loss coefficient. + iam : numeric + The SAPM angle of incidence loss coefficient F2. """ - return sapm_aoi_loss(aoi, self.module_parameters) + return iam.sapm(aoi, self.module_parameters) def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -1846,67 +1856,6 @@ def sapm_spectral_loss(airmass_absolute, module): return spectral_loss -def sapm_aoi_loss(aoi, module, upper=None): - """ - Calculates the SAPM angle of incidence loss coefficient, F2. - - Parameters - ---------- - aoi : numeric - Angle of incidence in degrees. Negative input angles will return - zeros. - - module : dict-like - A dict, Series, or DataFrame defining the SAPM performance - parameters. See the :py:func:`sapm` notes section for more - details. - - upper : None or float, default None - Upper limit on the results. - - Returns - ------- - F2 : numeric - The SAPM angle of incidence loss coefficient. - - Notes - ----- - The SAPM traditionally does not define an upper limit on the AOI - loss function and values slightly exceeding 1 may exist for moderate - angles of incidence (15-40 degrees). However, users may consider - imposing an upper limit of 1. - - References - ---------- - [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance - Model", SAND Report 3535, Sandia National Laboratories, Albuquerque, - NM. - - [2] B.H. King et al, "Procedure to Determine Coefficients for the - Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia - National Laboratories (2016). - - [3] B.H. King et al, "Recent Advancements in Outdoor Measurement - Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015). - DOI: 10.1109/PVSC.2015.7355849 - """ - - aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'], - module['B1'], module['B0']] - - aoi_loss = np.polyval(aoi_coeff, aoi) - aoi_loss = np.clip(aoi_loss, 0, upper) - # nan tolerant masking - aoi_lt_0 = np.full_like(aoi, False, dtype='bool') - np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0) - aoi_loss = np.where(aoi_lt_0, 0, aoi_loss) - - if isinstance(aoi, pd.Series): - aoi_loss = pd.Series(aoi_loss, aoi.index) - - return aoi_loss - - def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, module, reference_irradiance=1000): """ @@ -1942,7 +1891,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, """ F1 = sapm_spectral_loss(airmass_absolute, module) - F2 = sapm_aoi_loss(aoi, module) + F2 = _iam.sapm(aoi, module) E0 = reference_irradiance diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py index 7b68b3b08b..89317e3ea5 100644 --- a/pvlib/test/test_iam.py +++ b/pvlib/test/test_iam.py @@ -148,3 +148,24 @@ def test_iam_interp(): # check exception clause with pytest.raises(ValueError): _iam.interp(0.0, [0, 90], [1, -1]) + + +def test_sapm(sapm_module_params, aoi, expected): + + out = _iam.sapm(aoi, sapm_module_params) + + if isinstance(aoi, pd.Series): + assert_series_equal(out, expected, check_less_precise=4) + else: + assert_allclose(out, expected, atol=1e-4) + + +def test_sapm_limits(): + module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + assert _iam.sapm_aoi_loss(1, module_parameters) == 5 + + module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + assert _iam.sapm_aoi_loss(1, module_parameters, upper=1) == 1 + + module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + assert _iam.sapm_aoi_loss(1, module_parameters) == 0 diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 24000b3cec..13d1b8ffa5 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -293,33 +293,14 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, np.array([[0, 1.007572, 0, np.nan]])), (pd.Series([80]), pd.Series([0.597472])) ]) -def test_sapm_aoi_loss(sapm_module_params, aoi, expected): - out = pvsystem.sapm_aoi_loss(aoi, sapm_module_params) - if isinstance(aoi, pd.Series): - assert_series_equal(out, expected, check_less_precise=4) - else: - assert_allclose(out, expected, atol=1e-4) - - -def test_sapm_aoi_loss_limits(): - module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert pvsystem.sapm_aoi_loss(1, module_parameters) == 5 - - module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert pvsystem.sapm_aoi_loss(1, module_parameters, upper=1) == 1 - - module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert pvsystem.sapm_aoi_loss(1, module_parameters) == 0 - - -def test_PVSystem_sapm_aoi_loss(sapm_module_params, mocker): +def test_PVSystem_aoi_sapm(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) - mocker.spy(pvsystem, 'sapm_aoi_loss') + mocker.spy(_iam, 'sapm') aoi = 0 - out = system.sapm_aoi_loss(aoi) - pvsystem.sapm_aoi_loss.assert_called_once_with(aoi, sapm_module_params) + out = system.aoi_sapm(aoi) + _iam.sapm.assert_called_once_with(aoi, sapm_module_params) assert_allclose(out, 1.0, atol=0.01) From f48742cca69462ba4d99a2a7e0accc43514b96fd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 14:22:08 -0600 Subject: [PATCH 06/34] remove _ typo --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index eafebdda11..fdb9facaaa 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1891,7 +1891,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, """ F1 = sapm_spectral_loss(airmass_absolute, module) - F2 = _iam.sapm(aoi, module) + F2 = iam.sapm(aoi, module) E0 = reference_irradiance From 1bcde31be48196996661cf1a20f9ac49a49178fd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 14:50:49 -0600 Subject: [PATCH 07/34] lint fixes --- pvlib/iam.py | 18 +++++++++--------- pvlib/pvsystem.py | 28 +++++++++++++--------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index ba8f437333..24fb1c8589 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -6,11 +6,11 @@ """ import numpy as np import pandas as pd -from tools import cosd, sind, tand, asind +from pvlib.tools import cosd, sind, tand, asind def ashrae(aoi, b=0.05): - ''' + r""" Determine the incidence angle modifier using the ASHRAE transmission model. @@ -40,7 +40,7 @@ def ashrae(aoi, b=0.05): .. math:: - IAM = 1 - b \times (\sec(aoi) - 1) + IAM = 1 - b x (\sec(aoi) - 1) As AOI approaches 90 degrees, the model yields negative values for IAM. Negative IAM values are set to zero in this implementation. @@ -62,7 +62,7 @@ def ashrae(aoi, b=0.05): iam.physical iam.martin_ruiz iam.interp - ''' + """ iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1)) aoi_gte_90 = np.full_like(aoi, False, dtype='bool') @@ -77,7 +77,7 @@ def ashrae(aoi, b=0.05): def physical(aoi, n=1.526, K=4., L=0.002): - ''' + r""" Determine the incidence angle modifier using refractive index ``n``, extinction coefficient ``K``, and glazing thickness ``L``. @@ -132,7 +132,7 @@ def physical(aoi, n=1.526, K=4., L=0.002): iam.martin_ruiz iam.ashrae iam.interp - ''' + """ zeroang = 1e-06 # hold a new reference to the input aoi object since we're going to @@ -180,7 +180,7 @@ def physical(aoi, n=1.526, K=4., L=0.002): def martin_ruiz(aoi, a_r=0.16): - ''' + r''' Determine the incidence angle modifier (IAM) using the Martin and Ruiz incident angle model. @@ -257,7 +257,7 @@ def martin_ruiz(aoi, a_r=0.16): def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): - ''' + r''' Determine the incidence angle modifier (IAM) by interpolating a set of reference values, which are usually measured values. @@ -337,7 +337,7 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): def sapm(aoi, module, upper=None): - """ + r""" Determine the incidence angle modifier (IAM) using the SAPM model. Parameters diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fdb9facaaa..dd1bbdb988 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -343,9 +343,9 @@ def ashraeiam(self, aoi): Deprecated. Use ``PVSystem.iam_ashrae`` instead. """ import warnings - warnings.warn( - 'PVSystem.ashraeiam is deprecated and will be removed in v0.8,' - ' use PVSystem.iam_ashrae instead', pvlibDeprecationWarning) + warnings.warn('PVSystem.ashraeiam is deprecated and will be removed in' + 'v0.8, use PVSystem.iam_ashrae instead', + pvlibDeprecationWarning) return PVSystem.iam_ashrae(self, aoi) def iam_physical(self, aoi): @@ -378,9 +378,9 @@ def physicaliam(self, aoi): Deprecated. Use ``PVSystem.iam_physical`` instead. """ import warnings - warnings.warn( - 'PVSystem.physicaliam is deprecated and will be removed in v0.8,' - ' use PVSystem.iam_physical instead', pvlibDeprecationWarning) + warnings.warn('PVSystem.physicaliam is deprecated and will be removed' + ' in v0.8, use PVSystem.iam_physical instead', + pvlibDeprecationWarning) return PVSystem.iam_physical(self, aoi) def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): @@ -553,9 +553,9 @@ def sapm_aoi_loss(self, aoi): Deprecated. Use ``PVSystem.iam_sapm`` instead. """ import warnings - warnings.warn( - 'PVSystem.sapm_aoi_loss is deprecated and will be removed in v0.8,' - ' use PVSystem.iam_sapm instead', pvlibDeprecationWarning) + warnings.warn('PVSystem.sapm_aoi_loss is deprecated and will be' + ' removed in v0.8, use PVSystem.iam_sapm instead', + pvlibDeprecationWarning) return PVSystem.iam_sapm(self, aoi) def iam_sapm(self, aoi): @@ -2771,11 +2771,9 @@ def pvwatts_ac(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): return pac -ashraeiam = deprecated('0.7', alternative='iam.ashrae', - name='ashraeiam', removal='0.8', - )(iam.ashrae) +ashraeiam = deprecated('0.7', alternative='iam.ashrae', name='ashraeiam', + removal='0.8')(iam.ashrae) -physicaliam = deprecated('0.7', alternative='iam.physical', - name='physicaliam', removal='0.8', - )(iam.physical) +physicaliam = deprecated('0.7', alternative='iam.physical', name='physicaliam', + removal='0.8')(iam.physical) From c51bc08cc4f7a0ac0f65e965f813b568dda17458 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 15:00:02 -0600 Subject: [PATCH 08/34] move fixture to correct place --- pvlib/test/test_iam.py | 6 ++++++ pvlib/test/test_pvsystem.py | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py index 89317e3ea5..41e3e0d380 100644 --- a/pvlib/test/test_iam.py +++ b/pvlib/test/test_iam.py @@ -150,6 +150,12 @@ def test_iam_interp(): _iam.interp(0.0, [0, 90], [1, -1]) +@pytest.mark.parametrize('aoi,expected', [ + (45, 0.9975036250000002), + (np.array([[-30, 30, 100, np.nan]]), + np.array([[0, 1.007572, 0, np.nan]])), + (pd.Series([80]), pd.Series([0.597472])) +]) def test_sapm(sapm_module_params, aoi, expected): out = _iam.sapm(aoi, sapm_module_params) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 13d1b8ffa5..548f934b44 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -287,19 +287,12 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, assert_allclose(out, 1, atol=0.5) -@pytest.mark.parametrize('aoi,expected', [ - (45, 0.9975036250000002), - (np.array([[-30, 30, 100, np.nan]]), - np.array([[0, 1.007572, 0, np.nan]])), - (pd.Series([80]), pd.Series([0.597472])) -]) - -def test_PVSystem_aoi_sapm(sapm_module_params, mocker): +def test_PVSystem_iam_sapm(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) mocker.spy(_iam, 'sapm') aoi = 0 - out = system.aoi_sapm(aoi) + out = system.iam_sapm(aoi) _iam.sapm.assert_called_once_with(aoi, sapm_module_params) assert_allclose(out, 1.0, atol=0.01) From 5e01712216b8b98079d023a7431f6ac7b0a491bc Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 2 Oct 2019 15:30:24 -0600 Subject: [PATCH 09/34] move sapm_module_params fixture to conftest.py --- pvlib/test/conftest.py | 43 +++++++++++++++++++++++++++++++++++++ pvlib/test/test_pvsystem.py | 8 ------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index 8789bf99f4..c68750c634 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -365,3 +365,46 @@ def sapm_temperature_cs5p_220m(): # SAPM temperature model parameters for Canadian_Solar_CS5P_220M # (glass/polymer) in open rack return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3} + + +@pytest.fixture(scope='function') +def sapm_module_params(): + """ + Define SAPM model parameters for Canadian Solar CS5P 220M module. + + The scope of the fixture is set to ``'function'`` to allow tests to modify + parameters if required without affecting other tests. + """ + parameters = {'A0': 0.928385, + 'A1': 0.068093, + 'A2': -0.0157738, + 'A3': 0.0016606, + 'A4': -6.93E-05, + 'B0': 1, + 'B1': 002438, + 'B2': 003103, + 'B3': 00001246, + 'B4': 1E-07, + 'B5': -1.36E-09, + 'C0': 1.01284, + 'C1':-0.0128398, + 'C2' : 0.279317, + 'C3': -7.24463, + 'C3': 0.996446, + 'C4': 0.003554, + 'C6': 1.15535, + 'C7': -0.155353, + 'Isco': 5.09115, + 'Impo': 4.54629, + 'Aisc': 0.000397, + 'Aimp': 0.000181, + 'Bvoco': -0.21696, + 'Mbvoc': 0.0, + 'Bvmpo': -0.235488, + 'Mbvmp': 0.0, + 'N': 1.4032, + 'Cells_in_Series': 96, + 'IXO': 4.97599, + 'IXXO': 3.18803, + 'FD': 1} + return parameters diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 548f934b44..467fe599a1 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -99,14 +99,6 @@ def test_PVSystem_iam_physical(mocker): assert iam < 1. -@pytest.fixture(scope="session") -def sapm_module_params(sam_data): - modules = sam_data['sandiamod'] - module = 'Canadian_Solar_CS5P_220M___2009_' - module_parameters = modules[module] - return module_parameters - - def test_retrieve_sam_raise_no_parameters(): """ Raise an exception if no parameters are provided to `retrieve_sam()`. From e5bebe11a9570041ea58253c70162930edf1340b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 07:57:49 -0600 Subject: [PATCH 10/34] fix cut/paste errors --- pvlib/test/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index c68750c634..b81e19064f 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -381,17 +381,17 @@ def sapm_module_params(): 'A3': 0.0016606, 'A4': -6.93E-05, 'B0': 1, - 'B1': 002438, - 'B2': 003103, - 'B3': 00001246, + 'B1': -0.002438, + 'B2': 0.0003103, + 'B3': 0.00001246, 'B4': 1E-07, 'B5': -1.36E-09, 'C0': 1.01284, 'C1':-0.0128398, 'C2' : 0.279317, 'C3': -7.24463, - 'C3': 0.996446, - 'C4': 0.003554, + 'C4': 0.996446, + 'C5': 0.003554, 'C6': 1.15535, 'C7': -0.155353, 'Isco': 5.09115, From 549fc2c8de47b613932eb01e9f3cc765431108e7 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 08:23:17 -0600 Subject: [PATCH 11/34] add missing keys to fixture, add missing text to pvsystem.sapm docstring --- pvlib/pvsystem.py | 4 +++- pvlib/test/conftest.py | 6 ++++-- pvlib/test/test_iam.py | 6 +++--- pvlib/test/test_modelchain.py | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index dd1bbdb988..f77bc933ae 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -25,7 +25,7 @@ 'sapm': set([ 'A0', 'A1', 'A2', 'A3', 'A4', 'B0', 'B1', 'B2', 'B3', 'B4', 'B5', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', - 'C7', 'Isco', 'Impo', 'Aisc', 'Aimp', 'Bvoco', + 'C7', 'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco', 'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series', 'IXO', 'IXXO', 'FD']), 'desoto': set([ @@ -1643,6 +1643,8 @@ def sapm(effective_irradiance, temp_cell, module): Imp, Vmp, Ix, and Ixx to effective irradiance Isco Short circuit current at reference condition (amps) Impo Maximum power current at reference condition (amps) + Voco Open circuit voltage at reference condition (amps) + Vmpo Maximum power voltage at reference condition (amps) Aisc Short circuit current temperature coefficient at reference condition (1/C) Aimp Maximum power current temperature coefficient at diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index b81e19064f..ce35c7b4e5 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -383,8 +383,8 @@ def sapm_module_params(): 'B0': 1, 'B1': -0.002438, 'B2': 0.0003103, - 'B3': 0.00001246, - 'B4': 1E-07, + 'B3': -0.00001246, + 'B4': 2.11E-07, 'B5': -1.36E-09, 'C0': 1.01284, 'C1':-0.0128398, @@ -396,6 +396,8 @@ def sapm_module_params(): 'C7': -0.155353, 'Isco': 5.09115, 'Impo': 4.54629, + 'Voco': 59.2608, + 'Vmpo': 48.3156, 'Aisc': 0.000397, 'Aimp': 0.000181, 'Bvoco': -0.21696, diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py index 41e3e0d380..0c51b005a1 100644 --- a/pvlib/test/test_iam.py +++ b/pvlib/test/test_iam.py @@ -168,10 +168,10 @@ def test_sapm(sapm_module_params, aoi, expected): def test_sapm_limits(): module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert _iam.sapm_aoi_loss(1, module_parameters) == 5 + assert _iam.sapm(1, module_parameters) == 5 module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert _iam.sapm_aoi_loss(1, module_parameters, upper=1) == 1 + assert _iam.sapm(1, module_parameters, upper=1) == 1 module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} - assert _iam.sapm_aoi_loss(1, module_parameters) == 0 + assert _iam.sapm(1, module_parameters) == 0 diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 20a8a956bc..94fd0ec44d 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -385,8 +385,8 @@ def constant_aoi_loss(mc): @pytest.mark.parametrize('aoi_model, method', [ - ('sapm', 'sapm_aoi_loss'), ('ashrae', 'ashraeiam'), - ('physical', 'physicaliam')]) + ('sapm', 'iam_sapm'), ('ashrae', 'iam_ashrae'), + ('physical', 'iam_physical')]) def test_aoi_models(system, location, aoi_model, method, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') From 42210e49b71fcb085b338e976764d70c3708dcb2 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 08:41:24 -0600 Subject: [PATCH 12/34] fix and update pvsystem.sapm tests --- pvlib/test/test_pvsystem.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 467fe599a1..fccb46a538 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -215,9 +215,13 @@ def test_sapm(sapm_module_params): for k, v in expected.items(): assert_allclose(out[k], v, atol=1e-4) - # just make sure it works with a dict input + # just make sure it works with Series input pvsystem.sapm(effective_irradiance, temp_cell, - sapm_module_params.to_dict()) + pd.Series(sapm_module_params)) + # just make sure it works with DataFrame input + pvsystem.sapm(effective_irradiance, temp_cell, + pd.DataFrame(data=sapm_module_params, index=[0], + columns=sapm_module_params.keys())) def test_PVSystem_sapm(sapm_module_params, mocker): From 1475db6ab76fdf6a53340cd5c8b7b6f1b328c616 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 10:38:42 -0600 Subject: [PATCH 13/34] remove DataFrame test for sapm, lint fixes --- pvlib/test/conftest.py | 4 ++-- pvlib/test/test_pvsystem.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index ce35c7b4e5..07d84eaed9 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -387,8 +387,8 @@ def sapm_module_params(): 'B4': 2.11E-07, 'B5': -1.36E-09, 'C0': 1.01284, - 'C1':-0.0128398, - 'C2' : 0.279317, + 'C1': -0.0128398, + 'C2': 0.279317, 'C3': -7.24463, 'C4': 0.996446, 'C5': 0.003554, diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index fccb46a538..488b940957 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -218,10 +218,6 @@ def test_sapm(sapm_module_params): # just make sure it works with Series input pvsystem.sapm(effective_irradiance, temp_cell, pd.Series(sapm_module_params)) - # just make sure it works with DataFrame input - pvsystem.sapm(effective_irradiance, temp_cell, - pd.DataFrame(data=sapm_module_params, index=[0], - columns=sapm_module_params.keys())) def test_PVSystem_sapm(sapm_module_params, mocker): @@ -283,7 +279,6 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, assert_allclose(out, 1, atol=0.5) - def test_PVSystem_iam_sapm(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) mocker.spy(_iam, 'sapm') From 006768d4e1c91a9f50f95a9d4c2404878636e70f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 14:56:40 -0600 Subject: [PATCH 14/34] implement PVSystem.get_iam, add deprecation test for pvsystem.sapm_aoi_loss --- pvlib/iam.py | 13 ++++- pvlib/modelchain.py | 6 +-- pvlib/pvsystem.py | 98 ++++++++++++++--------------------- pvlib/test/test_modelchain.py | 13 +++-- pvlib/test/test_pvsystem.py | 43 +++++++-------- 5 files changed, 80 insertions(+), 93 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 24fb1c8589..226350d844 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -9,6 +9,17 @@ 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 = { + 'ashrae': set(['b']), + 'physical': set(['n', 'K', 'L']), + 'martin_ruiz': set(['a_r']), + 'sapm': set(['B0', 'B1', 'B2', 'B3', 'B4', 'B5']), + 'interp': set([]) +} + + def ashrae(aoi, b=0.05): r""" Determine the incidence angle modifier using the ASHRAE transmission @@ -347,7 +358,7 @@ def sapm(aoi, module, upper=None): zeros. module : dict-like - A dict, Series, or DataFrame defining the SAPM performance + A dict or Series with the SAPM IAM model parameters. parameters. See the :py:func:`sapm` notes section for more details. diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 9892edbd30..5360f4fabe 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -590,15 +590,15 @@ def infer_aoi_model(self): 'aoi_model="no_loss".') def ashrae_aoi_loss(self): - self.aoi_modifier = self.system.iam_ashrae(self.aoi) + self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='ashrae') return self def physical_aoi_loss(self): - self.aoi_modifier = self.system.iam_physical(self.aoi) + self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='physical') return self def sapm_aoi_loss(self): - self.aoi_modifier = self.system.iam_sapm(self.aoi) + self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='sapm') return self def no_aoi_loss(self): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f77bc933ae..db22d3fefc 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -316,62 +316,55 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, albedo=self.albedo, **kwargs) - def iam_ashrae(self, aoi): + def get_iam(self, aoi, iam_model='physical'): """ - Determine the incidence angle modifier using - ``self.module_parameters['b']``, ``aoi``, - and the :py:func:`iam.ashrae` function. + Determine the incidence angle modifier using the method specified by + ``iam_model``. - Uses default arguments if keys not in module_parameters. + Parameters for the selected IAM model are expected to be in + ``PVSystem.module_parameters``. Parameters ---------- aoi : numeric The angle of incidence in degrees. + aoi_model : string, default 'physical' + The IAM model to be used. Valid strings are 'physical', 'ashrae', + 'martin_ruiz' and 'sapm'. + Returns ------- - modifier : numeric + iam : numeric The AOI modifier. - """ - kwargs = _build_kwargs(['b'], self.module_parameters) - return iam.ashrae(aoi, **kwargs) + Raises + ------ + ValueError if `iam_model` is not a valid model name. + """ + model = iam_model.lower() + if model in ['ashrae', 'physical', 'martin_ruiz']: + param_names = iam.IAM_MODEL_PARAMS[model] + kwargs = _build_kwargs(param_names, self.module_parameters) + func = iam.__getattribute__(model) + return func(aoi, **kwargs) + elif model=='sapm': + return iam.sapm(aoi, self.module_parameters) + elif model=='interp': + raise ValueError(model + ' is not implemented as an IAM model' + 'option for PVSystem') + else: + raise ValueError(model + ' is not a valid IAM model') def ashraeiam(self, aoi): """ - Deprecated. Use ``PVSystem.iam_ashrae`` instead. + Deprecated. Use ``PVSystem.iam`` instead. """ import warnings warnings.warn('PVSystem.ashraeiam is deprecated and will be removed in' - 'v0.8, use PVSystem.iam_ashrae instead', + 'v0.8, use PVSystem.get_iam instead', pvlibDeprecationWarning) - return PVSystem.iam_ashrae(self, aoi) - - def iam_physical(self, aoi): - """ - Determine the incidence angle modifier using ``aoi``, - ``self.module_parameters['K']``, - ``self.module_parameters['L']``, - ``self.module_parameters['n']``, - and the - :py:func:`iam.physical` function. - - Uses default arguments if keys not in module_parameters. - - Parameters - ---------- - aoi : numeric - The angle of incidence in degrees. - - Returns - ------- - modifier : numeric - The AOI modifier. - """ - kwargs = _build_kwargs(['K', 'L', 'n'], self.module_parameters) - - return iam.physical(aoi, **kwargs) + return PVSystem.get_iam(self, aoi, iam_model='ashrae') def physicaliam(self, aoi): """ @@ -379,9 +372,9 @@ def physicaliam(self, aoi): """ import warnings warnings.warn('PVSystem.physicaliam is deprecated and will be removed' - ' in v0.8, use PVSystem.iam_physical instead', + ' in v0.8, use PVSystem.get_iam instead', pvlibDeprecationWarning) - return PVSystem.iam_physical(self, aoi) + return PVSystem.get_iam(self, aoi, iam_model='physical') def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs): """ @@ -550,30 +543,13 @@ def sapm_spectral_loss(self, airmass_absolute): def sapm_aoi_loss(self, aoi): """ - Deprecated. Use ``PVSystem.iam_sapm`` instead. + Deprecated. Use ``PVSystem.iam`` instead. """ import warnings warnings.warn('PVSystem.sapm_aoi_loss is deprecated and will be' - ' removed in v0.8, use PVSystem.iam_sapm instead', + ' removed in v0.8, use PVSystem.get_iam instead', pvlibDeprecationWarning) - return PVSystem.iam_sapm(self, aoi) - - def iam_sapm(self, aoi): - """ - Use the :py:func:`iam.sapm` function, the input parameters, - and ``self.module_parameters`` to calculate iam. - - Parameters - ---------- - aoi : numeric - Angle of incidence in degrees. - - Returns - ------- - iam : numeric - The SAPM angle of incidence loss coefficient F2. - """ - return iam.sapm(aoi, self.module_parameters) + return PVSystem.get_iam(self, aoi, iam_model='sapm') def sapm_effective_irradiance(self, poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -2779,3 +2755,7 @@ def pvwatts_ac(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): physicaliam = deprecated('0.7', alternative='iam.physical', name='physicaliam', removal='0.8')(iam.physical) + + +sapm_aoi_loss = deprecated('0.7', alternative='iam.sapm', name='sapm_aoi_loss', + removal='0.8')(iam.sapm) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 94fd0ec44d..daaf6eb656 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -17,10 +17,9 @@ @pytest.fixture -def system(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m): - modules = sam_data['sandiamod'] +def system(sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m): module = 'Canadian_Solar_CS5P_220M___2009_' - module_parameters = modules[module].copy() + module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() system = PVSystem(surface_tilt=32.2, surface_azimuth=180, module=module, @@ -384,13 +383,13 @@ def constant_aoi_loss(mc): mc.aoi_modifier = 0.9 -@pytest.mark.parametrize('aoi_model, method', [ - ('sapm', 'iam_sapm'), ('ashrae', 'iam_ashrae'), - ('physical', 'iam_physical')]) +@pytest.mark.parametrize('aoi_model', [ + ('sapm', 'ashrae', 'physical', 'martin_ruiz') + ]) def test_aoi_models(system, location, aoi_model, method, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') - m = mocker.spy(system, method) + m = mocker.spy(system, 'get_iam') mc.run_model(weather.index, weather=weather) assert m.call_count == 1 assert isinstance(mc.ac, pd.Series) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 488b940957..36fbe289e2 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -79,24 +79,28 @@ def test_systemdef_dict(): assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5) -def test_PVSystem_iam_ashrae(mocker): - mocker.spy(_iam, 'ashrae') - module_parameters = pd.Series({'b': 0.05}) +@pytest.mark.parametrize('iam_model','model_params', [ + ('ashrae', {'b': 0.05}), + ('physical', {'K': 4, 'L': 0.002, 'n': 1.526}), + ('martin_ruiz', {'a_r': 0.16}), + ]) +def test_PVSystem_get_iam(mocker, iam_model, model_params): + mocker.spy(_iam, iam_model) + module_parameters = pd.Series(model_params) system = pvsystem.PVSystem(module_parameters=module_parameters) thetas = 1 - iam = system.iam_ashrae(thetas) - _iam.ashrae.assert_called_once_with(thetas, b=0.05) + iam = system.get_iam(thetas, iam_model=iam_model) + _iam.ashrae.assert_called_once_with(thetas, **module_parameters) assert iam < 1. -def test_PVSystem_iam_physical(mocker): - module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526}) - system = pvsystem.PVSystem(module_parameters=module_parameters) - mocker.spy(_iam, 'physical') - thetas = 1 - iam = system.iam_physical(thetas) - _iam.physical.assert_called_once_with(thetas, **module_parameters) - assert iam < 1. +def test_PVSystem_get_iam_sapm(sapm_module_params, mocker): + system = pvsystem.PVSystem(module_parameters=sapm_module_params) + mocker.spy(_iam, 'sapm') + aoi = 0 + out = system.get_iam(aoi, 'sapm') + _iam.sapm.assert_called_once_with(aoi, sapm_module_params) + assert_allclose(out, 1.0, atol=0.01) def test_retrieve_sam_raise_no_parameters(): @@ -279,15 +283,6 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, assert_allclose(out, 1, atol=0.5) -def test_PVSystem_iam_sapm(sapm_module_params, mocker): - system = pvsystem.PVSystem(module_parameters=sapm_module_params) - mocker.spy(_iam, 'sapm') - aoi = 0 - out = system.iam_sapm(aoi) - _iam.sapm.assert_called_once_with(aoi, sapm_module_params) - assert_allclose(out, 1.0, atol=0.01) - - @pytest.mark.parametrize('test_input,expected', [ ([1000, 100, 5, 45, 1000], 1.1400510967821877), ([np.array([np.nan, 1000, 1000]), @@ -1453,7 +1448,7 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker): @fail_on_pvlib_version('0.8') -def test_deprecated_08(): +def test_deprecated_08(sapm_module_params): with pytest.warns(pvlibDeprecationWarning): pvsystem.sapm_celltemp(1000, 25, 1) with pytest.warns(pvlibDeprecationWarning): @@ -1467,6 +1462,8 @@ def test_deprecated_08(): pvsystem.ashraeiam(45) with pytest.warns(pvlibDeprecationWarning): pvsystem.physicaliam(45) + with pytest.warns(pvlibDeprecationWarning): + pvsystem.sapm_aoi_loss(45, sapm_module_params) @fail_on_pvlib_version('0.8') From c10af4c69a99f70379d261876299b553c125b988 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 15:02:37 -0600 Subject: [PATCH 15/34] lint --- pvlib/pvsystem.py | 4 ++-- pvlib/test/test_modelchain.py | 5 +++-- pvlib/test/test_pvsystem.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index db22d3fefc..adfcaa5dbe 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -348,9 +348,9 @@ def get_iam(self, aoi, iam_model='physical'): kwargs = _build_kwargs(param_names, self.module_parameters) func = iam.__getattribute__(model) return func(aoi, **kwargs) - elif model=='sapm': + elif model == 'sapm': return iam.sapm(aoi, self.module_parameters) - elif model=='interp': + elif model == 'interp': raise ValueError(model + ' is not implemented as an IAM model' 'option for PVSystem') else: diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index daaf6eb656..1d6b5ede30 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -17,7 +17,8 @@ @pytest.fixture -def system(sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m): +def system(sapm_module_params, cec_inverter_parameters, + sapm_temperature_cs5p_220m): module = 'Canadian_Solar_CS5P_220M___2009_' module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() @@ -385,7 +386,7 @@ def constant_aoi_loss(mc): @pytest.mark.parametrize('aoi_model', [ ('sapm', 'ashrae', 'physical', 'martin_ruiz') - ]) +]) def test_aoi_models(system, location, aoi_model, method, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 36fbe289e2..e119bca9c2 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -79,11 +79,11 @@ def test_systemdef_dict(): assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5) -@pytest.mark.parametrize('iam_model','model_params', [ +@pytest.mark.parametrize('iam_model,model_params', [ ('ashrae', {'b': 0.05}), ('physical', {'K': 4, 'L': 0.002, 'n': 1.526}), ('martin_ruiz', {'a_r': 0.16}), - ]) +]) def test_PVSystem_get_iam(mocker, iam_model, model_params): mocker.spy(_iam, iam_model) module_parameters = pd.Series(model_params) From ab1fbbc8f1c8e0acae2aebe3046e7203d36460de Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 3 Oct 2019 15:59:16 -0600 Subject: [PATCH 16/34] test fixes, add Material to sapm_module_params fixture --- pvlib/test/conftest.py | 6 ++++-- pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_pvsystem.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py index 07d84eaed9..9c838249a9 100644 --- a/pvlib/test/conftest.py +++ b/pvlib/test/conftest.py @@ -375,7 +375,10 @@ def sapm_module_params(): The scope of the fixture is set to ``'function'`` to allow tests to modify parameters if required without affecting other tests. """ - parameters = {'A0': 0.928385, + parameters = {'Material': 'c-Si', + 'Cells_in_Series': 96, + 'Parallel_Strings': 1, + 'A0': 0.928385, 'A1': 0.068093, 'A2': -0.0157738, 'A3': 0.0016606, @@ -405,7 +408,6 @@ def sapm_module_params(): 'Bvmpo': -0.235488, 'Mbvmp': 0.0, 'N': 1.4032, - 'Cells_in_Series': 96, 'IXO': 4.97599, 'IXXO': 3.18803, 'FD': 1} diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 8ac5a23135..5c00e798c4 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -408,7 +408,7 @@ def constant_aoi_loss(mc): @pytest.mark.parametrize('aoi_model', [ ('sapm', 'ashrae', 'physical', 'martin_ruiz') ]) -def test_aoi_models(system, location, aoi_model, method, weather, mocker): +def test_aoi_models(system, location, aoi_model, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') m = mocker.spy(system, 'get_iam') diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index e119bca9c2..d7b4f603cc 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -85,12 +85,12 @@ def test_systemdef_dict(): ('martin_ruiz', {'a_r': 0.16}), ]) def test_PVSystem_get_iam(mocker, iam_model, model_params): - mocker.spy(_iam, iam_model) + m = mocker.spy(_iam, iam_model) module_parameters = pd.Series(model_params) system = pvsystem.PVSystem(module_parameters=module_parameters) thetas = 1 iam = system.get_iam(thetas, iam_model=iam_model) - _iam.ashrae.assert_called_once_with(thetas, **module_parameters) + assert m.call_count == 1 assert iam < 1. From 4dadd4b52ee2f243c2d0c065b7b0b9e9a8dc50a7 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 09:08:19 -0600 Subject: [PATCH 17/34] test fixes --- pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_pvsystem.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 5c00e798c4..a36918543d 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -406,7 +406,7 @@ def constant_aoi_loss(mc): @pytest.mark.parametrize('aoi_model', [ - ('sapm', 'ashrae', 'physical', 'martin_ruiz') + 'sapm', 'ashrae', 'physical', 'martin_ruiz' ]) def test_aoi_models(system, location, aoi_model, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index d7b4f603cc..4c43bf3fae 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -1448,7 +1448,7 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker): @fail_on_pvlib_version('0.8') -def test_deprecated_08(sapm_module_params): +def test_deprecated_08(): with pytest.warns(pvlibDeprecationWarning): pvsystem.sapm_celltemp(1000, 25, 1) with pytest.warns(pvlibDeprecationWarning): @@ -1463,7 +1463,7 @@ def test_deprecated_08(sapm_module_params): with pytest.warns(pvlibDeprecationWarning): pvsystem.physicaliam(45) with pytest.warns(pvlibDeprecationWarning): - pvsystem.sapm_aoi_loss(45, sapm_module_params) + pvsystem.sapm_aoi_loss(45, {}) @fail_on_pvlib_version('0.8') From 7f14c95a36fc30cf429ef3f8e9435b592e61d7cb Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 09:29:11 -0600 Subject: [PATCH 18/34] add martin_ruiz to modelchain --- pvlib/modelchain.py | 5 +++++ pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_pvsystem.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index df3fb84051..6bb7843b26 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -575,6 +575,11 @@ def sapm_aoi_loss(self): self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='sapm') return self + def martin_ruiz_aoi_loss(self): + self.aoi_modifier = self.system.get_iam(self.aoi, + iam_model='martin_ruiz') + return self + def no_aoi_loss(self): self.aoi_modifier = 1.0 return self diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index a36918543d..b42b520f04 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -412,7 +412,7 @@ def test_aoi_models(system, location, aoi_model, weather, mocker): mc = ModelChain(system, location, dc_model='sapm', aoi_model=aoi_model, spectral_model='no_loss') m = mocker.spy(system, 'get_iam') - mc.run_model(weather.index, weather=weather) + mc.run_model(weather=weather) assert m.call_count == 1 assert isinstance(mc.ac, pd.Series) assert not mc.ac.empty diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 4c43bf3fae..570db97124 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -1463,7 +1463,8 @@ def test_deprecated_08(): with pytest.warns(pvlibDeprecationWarning): pvsystem.physicaliam(45) with pytest.warns(pvlibDeprecationWarning): - pvsystem.sapm_aoi_loss(45, {}) + pvsystem.sapm_aoi_loss(45, {'B5': 0.0, 'B4': 0.0, 'B3': 0.0, 'B2': 0.0, + 'B1': 0.0, 'B0': 1.0}) @fail_on_pvlib_version('0.8') From 91645929f48286e53dc51b9c603d30790976bf94 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 10:07:25 -0600 Subject: [PATCH 19/34] finish adding martin_ruiz to modelchain --- pvlib/modelchain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 6bb7843b26..e5312953a4 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -540,6 +540,8 @@ def aoi_model(self, model): self._aoi_model = self.physical_aoi_loss elif model == 'sapm': self._aoi_model = self.sapm_aoi_loss + elif model == 'martin_ruiz': + self._aoi_model = self.martin_ruiz_aoi_loss elif model == 'no_loss': self._aoi_model = self.no_aoi_loss else: @@ -555,6 +557,8 @@ def infer_aoi_model(self): return self.sapm_aoi_loss elif set(['b']) <= params: return self.ashrae_aoi_loss + elif set(['a_r']) <= params: + return self.martin_ruiz_aoi_loss else: raise ValueError('could not infer AOI model from ' 'system.module_parameters. Check that the ' From 2fe7396a458dada43afa297275eb94ec2236d2be Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 11:18:33 -0600 Subject: [PATCH 20/34] add test for ModelChain.infer_aoi_model, improve coverage --- pvlib/modelchain.py | 10 +++++----- pvlib/test/test_modelchain.py | 29 ++++++++++++++++++++++++---- pvlib/test/test_pvsystem.py | 36 ++++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index e5312953a4..16bd7ba426 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -271,8 +271,8 @@ class ModelChain(object): aoi_model: None, str, or function, default None If None, the model will be inferred from the contents of system.module_parameters. Valid strings are 'physical', - 'ashrae', 'sapm', 'no_loss'. The ModelChain instance will be - passed as the first argument to a user-defined function. + 'ashrae', 'sapm', 'martin_ruiz', 'no_loss'. The ModelChain instance + will be passed as the first argument to a user-defined function. spectral_model: None, str, or function, default None If None, the model will be inferred from the contents of @@ -563,9 +563,9 @@ def infer_aoi_model(self): raise ValueError('could not infer AOI model from ' 'system.module_parameters. Check that the ' 'system.module_parameters contain parameters for ' - 'the physical, aoi, or ashrae model; explicitly ' - 'set model with aoi_model kwarg; or set ' - 'aoi_model="no_loss".') + 'the physical, aoi, ashrae or martin_ruiz model; ' + 'explicitly set the model with the aoi_model ' + 'kwarg; or set aoi_model="no_loss".') def ashrae_aoi_loss(self): self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='ashrae') diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index b42b520f04..cb60285cbe 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from pvlib import modelchain, pvsystem, temperature +from pvlib import iam, modelchain, pvsystem, temperature from pvlib.modelchain import ModelChain from pvlib.pvsystem import PVSystem from pvlib.tracking import SingleAxisTracker @@ -16,7 +16,7 @@ from conftest import fail_on_pvlib_version, requires_scipy, requires_tables -@pytest.fixture +@pytest.fixture(scope='function') def system(sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m): module = 'Canadian_Solar_CS5P_220M___2009_' @@ -442,8 +442,29 @@ def test_aoi_model_user_func(system, location, weather, mocker): assert mc.ac[1] < 1 -def constant_spectral_loss(mc): - mc.spectral_modifier = 0.9 +@pytest.mark.parametrize('aoi_model', [ + 'sapm', 'ashrae', 'physical', 'martin_ruiz' +]) +def test_infer_aoi_model(location, pvwatts_dc_pvwatts_ac_system, aoi_model): + # use pvwatts/pvwatts fixture because it has no AOI model parameters + temp = pvwatts_dc_pvwatts_ac_system.copy() + for k in iam.IAM_MODEL_PARAMS[aoi_model]: + temp.module_parameters.update({k: 1.0}) + mc = ModelChain(system, location, + orientation_strategy='None', + spectral_model='no_loss') + assert isinstance(mc, ModelChain) + # remove added parameters + for k in iam.IAM_MODEL_PARAMS[aoi_model]: + system.module_parameters.pop(k) + + +@requires_scipy +def test_infer_temp_model_invalid(location, system): + system.temperature_model_parameters.pop('a') + with pytest.raises(ValueError): + ModelChain(system, location, orientation_strategy='None', + aoi_model='physical', spectral_model='no_loss') @requires_scipy diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 570db97124..c333cc93f9 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -103,6 +103,18 @@ def test_PVSystem_get_iam_sapm(sapm_module_params, mocker): assert_allclose(out, 1.0, atol=0.01) +def test_PVSystem_get_iam_interp(sapm_module_params, mocker): + system = pvsystem.PVSystem(module_parameters=sapm_module_params) + with pytest.raises(ValueError): + system.get_iam(45, iam_model='interp') + + +def test_PVSystem_get_iam_invalid(sapm_module_params, mocker): + system = pvsystem.PVSystem(module_parameters=sapm_module_params) + with pytest.raises(ValueError): + system.get_iam(45, iam_model='not_a_model') + + def test_retrieve_sam_raise_no_parameters(): """ Raise an exception if no parameters are provided to `retrieve_sam()`. @@ -1454,14 +1466,36 @@ def test_deprecated_08(): with pytest.warns(pvlibDeprecationWarning): pvsystem.pvsyst_celltemp(1000, 25) module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1, - 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1} + 'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1, + 'B5': 0.0, 'B4': 0.0, 'B3': 0.0, 'B2': 0.0, + 'B1': 0.0, 'B0': 1.0, + 'b': 0.05, 'K': 4, 'L': 0.002, 'n': 1.526, + 'a_r': 0.16} + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] + # for missing temperature_model_parameters with pytest.warns(pvlibDeprecationWarning): pvsystem.PVSystem(module_parameters=module_parameters, racking_model='open', module_type='glass_glass') + pv = pvsystem.PVSystem(module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + racking_model='open', module_type='glass_glass') + # deprecated method PVSystem.ashraeiam + with pytest.warns(pvlibDeprecationWarning): + pv.ashraeiam(45) + # deprecated function ashraeiam with pytest.warns(pvlibDeprecationWarning): pvsystem.ashraeiam(45) + # deprecated method PVSystem.physicaliam + with pytest.warns(pvlibDeprecationWarning): + pv.physicaliam(45) + # deprecated function physicaliam with pytest.warns(pvlibDeprecationWarning): pvsystem.physicaliam(45) + # deprecated method PVSystem.sapm_aoi_loss + with pytest.warns(pvlibDeprecationWarning): + pv.sapm_aoi_loss(45) + # deprecated function sapm_aoi_loss with pytest.warns(pvlibDeprecationWarning): pvsystem.sapm_aoi_loss(45, {'B5': 0.0, 'B4': 0.0, 'B3': 0.0, 'B2': 0.0, 'B1': 0.0, 'B0': 1.0}) From 3d6bcf54ad553df352610ed069cdea130b5aab36 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 11:25:20 -0600 Subject: [PATCH 21/34] repair delete mistake --- pvlib/test/test_modelchain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index cb60285cbe..4b5c1621b7 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -467,6 +467,10 @@ def test_infer_temp_model_invalid(location, system): aoi_model='physical', spectral_model='no_loss') +def constant_spectral_loss(mc): + mc.spectral_modifier = 0.9 + + @requires_scipy @pytest.mark.parametrize('spectral_model', [ 'sapm', 'first_solar', 'no_loss', constant_spectral_loss From 64a6b3c1b76a60960ab12da3f0ea8e25fb6bd100 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 11:35:55 -0600 Subject: [PATCH 22/34] test for invalid aoi model parameters in ModelChain --- pvlib/test/test_modelchain.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 4b5c1621b7..9bc443a4db 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -459,12 +459,10 @@ def test_infer_aoi_model(location, pvwatts_dc_pvwatts_ac_system, aoi_model): system.module_parameters.pop(k) -@requires_scipy -def test_infer_temp_model_invalid(location, system): - system.temperature_model_parameters.pop('a') +def test_infer_aoi_model_invalid(location, pvwatts_dc_pvwatts_ac_system): with pytest.raises(ValueError): ModelChain(system, location, orientation_strategy='None', - aoi_model='physical', spectral_model='no_loss') + spectral_model='no_loss') def constant_spectral_loss(mc): From 152ef7a96261b549e3077d1ce07bfe813500ab8e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 11:49:05 -0600 Subject: [PATCH 23/34] update api, whatsnew --- docs/sphinx/source/api.rst | 12 +++++++----- docs/sphinx/source/whatsnew/v0.7.0.rst | 27 +++++++++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 6e93dc5807..c37bedf7df 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -211,15 +211,17 @@ wrap the functions listed below. See its documentation for details. pvsystem.PVSystem pvsystem.LocalizedPVSystem -AOI modifiers -------------- +Incident angle modifiers +------------------------ .. autosummary:: :toctree: generated/ - pvsystem.physicaliam - pvsystem.ashraeiam - pvsystem.sapm_aoi_loss + iam.physical + iam.ashrae + iam.martin_ruiz + iam.sapm + iam.interp PV temperature models --------------------- diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst index d358c88249..c2b2c34533 100644 --- a/docs/sphinx/source/whatsnew/v0.7.0.rst +++ b/docs/sphinx/source/whatsnew/v0.7.0.rst @@ -69,6 +69,20 @@ API Changes - `modelchain.basic_chain` has a new required argument `temperature_model_parameters`. +* Changes related to IAM (AOI loss) functions (:issue:`680`): + * Changes to functions + - Moved functions from `pvsystem.py` to `iam.py`. `pvsystem` IAM + functions are deprecated and will be removed in v0.8. + - Functions are renamed to a consistent pattern: + - `pvsystem.physicaliam` is `iam.physical` + - `pvsystem.ashraeiam` is `iam.ashrae` + - `pvsystem.sapm_aoi_loss` is `iam.sapm` + - Created dict `iam.IAM_MODEL_PARAMS` to aid in identifying IAM models + * Changes to `PVSystem` class + - IAM models are provided by `PVSystem.get_iam` with kwarg `iam_model`. + - Methods `PVSystem.ashraeiam`, `PVSystem.physicaliam` and + `PVSystem.sapm_aoi_loss` are deprecated and will be removed in v0.8. + * Calling :py:func:`pvlib.pvsystem.retrieve_sam` with no parameters will raise an exception instead of displaying a dialog. * The `times` keyword argument has been deprecated in the @@ -83,8 +97,9 @@ API Changes Enhancements ~~~~~~~~~~~~ -* Created two new incidence angle modifier functions: :py:func:`pvlib.pvsystem.iam_martin_ruiz` - and :py:func:`pvlib.pvsystem.iam_interp`. (:issue:`751`) +* Created two new incidence angle modifier (IAM) functions: + :py:func:`pvlib.iam.martin_ruiz` and :py:func:`pvlib.iam.interp`. (:issue:`751`) +* Add the `martin_ruiz` IAM function as an option for `ModelChain.aoi_model`. * Updated the file for module parameters for the CEC model, from the SAM file dated 2017-6-5 to the SAM file dated 2019-03-05. (:issue:`761`) * Updated the file for inverter parameters for the CEC model, from the SAM file @@ -96,6 +111,7 @@ 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 `timeout` to :py:func:`pvlib.iotools.get_psm3`. Bug fixes ~~~~~~~~~ @@ -113,6 +129,8 @@ Testing in NSRDB (:issue:`733`) * Added tests for methods in bifacial.py. * Added tests for changes to cell temperature models. +* Added tests for changes to IAM models. +* Added test for `ModelChain.infer_aoi_model`. Documentation ~~~~~~~~~~~~~ @@ -132,11 +150,6 @@ Removal of prior version deprecations * Removed `ModelChain.prepare_inputs` clearsky assumption when no irradiance data was provided. -Enhancements -~~~~~~~~~~~~ -* Add `timeout` to :py:func:`pvlib.iotools.get_psm3`. - - Contributors ~~~~~~~~~~~~ * Mark Campanellli (:ghuser:`markcampanelli`) From 46db9d7aad9fbd07f4c51aedd5494a38ac6cdd6d Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 11:52:39 -0600 Subject: [PATCH 24/34] test fix --- pvlib/test/test_modelchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 9bc443a4db..8d273c4add 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -101,7 +101,7 @@ def pvwatts_dc_snl_ac_system(cec_inverter_parameters): return system -@pytest.fixture +@pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} temp_model_params = sapm_temperature_cs5p_220m.copy() @@ -447,7 +447,7 @@ def test_aoi_model_user_func(system, location, weather, mocker): ]) def test_infer_aoi_model(location, pvwatts_dc_pvwatts_ac_system, aoi_model): # use pvwatts/pvwatts fixture because it has no AOI model parameters - temp = pvwatts_dc_pvwatts_ac_system.copy() + temp = pvwatts_dc_pvwatts_ac_system for k in iam.IAM_MODEL_PARAMS[aoi_model]: temp.module_parameters.update({k: 1.0}) mc = ModelChain(system, location, From ea6fe818edaccbd32460c48f3e3033326559faf4 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 13:09:11 -0600 Subject: [PATCH 25/34] fixture for aoi_model tests --- pvlib/test/test_modelchain.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 8d273c4add..70b9f58d03 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -113,7 +113,23 @@ def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): return system -@pytest.fixture + +@pytest.fixture(scope="function") +def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m, + cec_inverter_parameters): + module_parameters = cec_module_cs5p_220m.copy() + module_parameters['EgRef'] = 1.121 + module_parameters['dEgdT'] = -0.0002677 + temp_model_params = sapm_temperature_cs5p_220m.copy() + inverter_parameters = cec_inverter_parameters.copy() + system = PVSystem(surface_tilt=32.2, surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters) + return system + + + @pytest.fixture def location(): return Location(32.2, -111, altitude=700) @@ -445,23 +461,21 @@ def test_aoi_model_user_func(system, location, weather, mocker): @pytest.mark.parametrize('aoi_model', [ 'sapm', 'ashrae', 'physical', 'martin_ruiz' ]) -def test_infer_aoi_model(location, pvwatts_dc_pvwatts_ac_system, aoi_model): - # use pvwatts/pvwatts fixture because it has no AOI model parameters - temp = pvwatts_dc_pvwatts_ac_system +def test_infer_aoi_model(location, system_no_aoi, aoi_model): for k in iam.IAM_MODEL_PARAMS[aoi_model]: - temp.module_parameters.update({k: 1.0}) - mc = ModelChain(system, location, + system_no_aoi.module_parameters.update({k: 1.0}) + mc = ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') assert isinstance(mc, ModelChain) # remove added parameters for k in iam.IAM_MODEL_PARAMS[aoi_model]: - system.module_parameters.pop(k) + system_no_aoi.module_parameters.pop(k) -def test_infer_aoi_model_invalid(location, pvwatts_dc_pvwatts_ac_system): +def test_infer_aoi_model_invalid(location, system_no_aoi): with pytest.raises(ValueError): - ModelChain(system, location, orientation_strategy='None', + ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') From 25293513e74a10afbe045535e5f17c17617ca2e3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 13:16:55 -0600 Subject: [PATCH 26/34] bad indent --- pvlib/test/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 70b9f58d03..74e93c82cc 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -129,7 +129,7 @@ def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m, return system - @pytest.fixture +@pytest.fixture def location(): return Location(32.2, -111, altitude=700) From f641cd0895b81e7a16c0a66cdcb5e184dd49190b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 13:41:33 -0600 Subject: [PATCH 27/34] docstring and lint --- pvlib/iam.py | 56 +++++++++++++++++------------------ pvlib/test/test_modelchain.py | 1 - 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 226350d844..ba6c427ad8 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -51,10 +51,10 @@ def ashrae(aoi, b=0.05): .. math:: - IAM = 1 - b x (\sec(aoi) - 1) + IAM = 1 - b (\sec(aoi) - 1) - As AOI approaches 90 degrees, the model yields negative values for IAM. - Negative IAM values are set to zero in this implementation. + As AOI approaches 90 degrees, the model yields negative values for IAM; + negative IAM values are set to zero in this implementation. References ---------- @@ -92,7 +92,7 @@ def physical(aoi, n=1.526, K=4., L=0.002): Determine the incidence angle modifier using refractive index ``n``, extinction coefficient ``K``, and glazing thickness ``L``. - ``physical`` calculates the incidence angle modifier as described in + ``iam.physical`` calculates the incidence angle modifier as described in [1], Section 3. The calculation is based on a physical model of absorbtion and transmission through a transparent cover. @@ -124,9 +124,9 @@ def physical(aoi, n=1.526, K=4., L=0.002): Notes ----- - The authors of this function believe that Eqn. 14 in [1] is + The pvlib python authors believe that Eqn. 14 in [1] is incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`. - Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI)) + Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI))` References ---------- @@ -220,15 +220,16 @@ def martin_ruiz(aoi, a_r=0.16): The incident angle modifier is defined as - ..math:: + .. math:: - IAM = \frac{1 - \exp(-\cos(\frac{aoi}{a_r}))} - {1 - \exp(\frac{-1}{a_r}} + IAM = \frac{1 - \exp(-\cos(\frac{aoi}{a_r}))} + {1 - \exp(\frac{-1}{a_r}} - which is presented as AL(alpha) = 1 - IAM in equation 4 of [1], with alpha - representing the angle of incidence AOI. Thus IAM = 1 at AOI = 0, and - IAM = 0 at AOI = 90. This equation is only valid for -90 <= aoi <= 90, - therefore iam is constrained to 0.0 beyond this range. + which is presented as :math:`AL(\alpha) = 1 - IAM` in equation 4 of [1], + with :math:`\alpha` representing the angle of incidence AOI. Thus IAM = 1 + at AOI = 0, and IAM = 0 at AOI = 90. This equation is only valid for + -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this + interval. References ---------- @@ -274,33 +275,33 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): Parameters ---------- - aoi : numeric, degrees + aoi : numeric The angle of incidence between the module normal vector and the - sun-beam vector in degrees. + sun-beam vector [degrees]. - theta_ref : numeric, degrees - Vector of angles at which the IAM is known. + theta_ref : numeric + Vector of angles at which the IAM is known [degrees]. - iam_ref : numeric, unitless - IAM values for each angle in ``theta_ref``. + iam_ref : numeric + IAM values for each angle in ``theta_ref`` [unitless]. method : str, default 'linear' Specifies the interpolation method. Useful options are: 'linear', 'quadratic','cubic'. See scipy.interpolate.interp1d for more options. - normalize : boolean + normalize : boolean, default True When true, the interpolated values are divided by the interpolated - value at zero degrees. This ensures that the iam at normal - incidence is equal to 1.0. + value at zero degrees. This ensures that ``iam``=1.0 at normal + incidence. Returns ------- iam : numeric - The incident angle modifier(s) + The incident angle modifier(s) [unitless] - Notes: - ------ + Notes + ----- ``theta_ref`` must have two or more points and may span any range of angles. Typically there will be a dozen or more points in the range 0-90 degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated, @@ -359,8 +360,7 @@ def sapm(aoi, module, upper=None): module : dict-like A dict or Series with the SAPM IAM model parameters. - parameters. See the :py:func:`sapm` notes section for more - details. + See the :py:func:`sapm` notes section for more details. upper : None or float, default None Upper limit on the results. @@ -368,7 +368,7 @@ def sapm(aoi, module, upper=None): Returns ------- iam : numeric - The SAPM angle of incidence loss coefficient F2. + The SAPM angle of incidence loss coefficient, termed F2 in [1]. Notes ----- diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 74e93c82cc..dd44c7490f 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -113,7 +113,6 @@ def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): return system - @pytest.fixture(scope="function") def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m, cec_inverter_parameters): From f33194932ec851058f6c8bb19f39876f8243d8b5 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Oct 2019 13:43:23 -0600 Subject: [PATCH 28/34] lint --- pvlib/iam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index ba6c427ad8..713e4849ef 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -227,7 +227,7 @@ def martin_ruiz(aoi, a_r=0.16): which is presented as :math:`AL(\alpha) = 1 - IAM` in equation 4 of [1], with :math:`\alpha` representing the angle of incidence AOI. Thus IAM = 1 - at AOI = 0, and IAM = 0 at AOI = 90. This equation is only valid for + at AOI = 0, and IAM = 0 at AOI = 90. This equation is only valid for -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this interval. From ec723c656c7df51695c809eb8dd86bd71826070f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 15 Oct 2019 10:26:58 -0600 Subject: [PATCH 29/34] module docstring, changes to tests --- pvlib/iam.py | 26 ++++++++++++++++++++------ pvlib/pvsystem.py | 11 ++++++----- pvlib/test/test_modelchain.py | 4 +--- pvlib/test/test_pvsystem.py | 5 ++--- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pvlib/iam.py b/pvlib/iam.py index 713e4849ef..3d869f298d 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -1,9 +1,13 @@ -# -*- coding: utf-8 -*- +r""" +The ``iam`` module contains functions that implement models for the incidence +angle modifier (IAM). The IAM quantifies the fraction of direct irradiance on +a module's front surface that is transmitted through the module materials to +the cells. Stated differently, the quantity 1 - IAM is the fraction of direct +irradiance that is reflected away or absorbed by the module's front materials. +IAM is typically a function of the angle of incidence (AOI) of the direct +irradiance to the module's surface. """ -Created on Wed Oct 2 09:04:01 2019 -@author: cwhanse -""" import numpy as np import pandas as pd from pvlib.tools import cosd, sind, tand, asind @@ -65,8 +69,8 @@ def ashrae(aoi, b=0.05): [2] ASHRAE standard 93-77 [3] PVsyst Contextual Help. - http://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on - September 10, 2012 + https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on + October 14, 2019 See Also -------- @@ -143,6 +147,7 @@ def physical(aoi, n=1.526, K=4., L=0.002): iam.martin_ruiz iam.ashrae iam.interp + iam.sapm """ zeroang = 1e-06 @@ -247,6 +252,7 @@ def martin_ruiz(aoi, a_r=0.16): iam.physical iam.ashrae iam.interp + iam.sapm ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 @@ -314,6 +320,7 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): iam.physical iam.ashrae iam.martin_ruiz + iam.sapm ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 @@ -390,6 +397,13 @@ def sapm(aoi, module, upper=None): [3] B.H. King et al, "Recent Advancements in Outdoor Measurement Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015). DOI: 10.1109/PVSC.2015.7355849 + + See Also + -------- + iam.physical + iam.ashrae + iam.martin_ruiz + iam.interp """ aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'], diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index adfcaa5dbe..5eb66b34f5 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -322,7 +322,8 @@ def get_iam(self, aoi, iam_model='physical'): ``iam_model``. Parameters for the selected IAM model are expected to be in - ``PVSystem.module_parameters``. + ``PVSystem.module_parameters``. Default parameters are available for + the 'physical', 'ashrae' and 'martin_ruiz' models. Parameters ---------- @@ -346,7 +347,7 @@ def get_iam(self, aoi, iam_model='physical'): if model in ['ashrae', 'physical', 'martin_ruiz']: param_names = iam.IAM_MODEL_PARAMS[model] kwargs = _build_kwargs(param_names, self.module_parameters) - func = iam.__getattribute__(model) + func = getattr(iam, model) return func(aoi, **kwargs) elif model == 'sapm': return iam.sapm(aoi, self.module_parameters) @@ -358,7 +359,7 @@ def get_iam(self, aoi, iam_model='physical'): def ashraeiam(self, aoi): """ - Deprecated. Use ``PVSystem.iam`` instead. + Deprecated. Use ``PVSystem.get_iam`` instead. """ import warnings warnings.warn('PVSystem.ashraeiam is deprecated and will be removed in' @@ -368,7 +369,7 @@ def ashraeiam(self, aoi): def physicaliam(self, aoi): """ - Deprecated. Use ``PVSystem.iam_physical`` instead. + Deprecated. Use ``PVSystem.get_iam`` instead. """ import warnings warnings.warn('PVSystem.physicaliam is deprecated and will be removed' @@ -543,7 +544,7 @@ def sapm_spectral_loss(self, airmass_absolute): def sapm_aoi_loss(self, aoi): """ - Deprecated. Use ``PVSystem.iam`` instead. + Deprecated. Use ``PVSystem.get_iam`` instead. """ import warnings warnings.warn('PVSystem.sapm_aoi_loss is deprecated and will be' diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index dd44c7490f..88cf89ae5a 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -467,15 +467,13 @@ def test_infer_aoi_model(location, system_no_aoi, aoi_model): orientation_strategy='None', spectral_model='no_loss') assert isinstance(mc, ModelChain) - # remove added parameters - for k in iam.IAM_MODEL_PARAMS[aoi_model]: - system_no_aoi.module_parameters.pop(k) def test_infer_aoi_model_invalid(location, system_no_aoi): with pytest.raises(ValueError): ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') + assert 'could not infer AOI model' in str(sys.exc_info()) def constant_spectral_loss(mc): diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index c333cc93f9..8a80701ccf 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -86,11 +86,10 @@ def test_systemdef_dict(): ]) def test_PVSystem_get_iam(mocker, iam_model, model_params): m = mocker.spy(_iam, iam_model) - module_parameters = pd.Series(model_params) - system = pvsystem.PVSystem(module_parameters=module_parameters) + system = pvsystem.PVSystem(module_parameters=model_params) thetas = 1 iam = system.get_iam(thetas, iam_model=iam_model) - assert m.call_count == 1 + assert m.assert_called_once_with(thetas, model_params) assert iam < 1. From cf998ea4357fb08012fc196bc088f6cd215d42c4 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 15 Oct 2019 10:58:37 -0600 Subject: [PATCH 30/34] test fixes --- pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_pvsystem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 88cf89ae5a..38f17bec6a 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -473,7 +473,7 @@ def test_infer_aoi_model_invalid(location, system_no_aoi): with pytest.raises(ValueError): ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') - assert 'could not infer AOI model' in str(sys.exc_info()) + assert 'could not infer AOI model' in str(sys.exc_info()) def constant_spectral_loss(mc): diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index 8a80701ccf..aa0145562e 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -89,7 +89,7 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params): system = pvsystem.PVSystem(module_parameters=model_params) thetas = 1 iam = system.get_iam(thetas, iam_model=iam_model) - assert m.assert_called_once_with(thetas, model_params) + assert m.assert_called_once_with(thetas, **model_params) assert iam < 1. From 23f2677ace8db33f479b7438fb28422818840072 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Oct 2019 09:05:35 -0600 Subject: [PATCH 31/34] another test fix --- pvlib/test/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py index aa0145562e..8157c5704c 100644 --- a/pvlib/test/test_pvsystem.py +++ b/pvlib/test/test_pvsystem.py @@ -89,7 +89,7 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params): system = pvsystem.PVSystem(module_parameters=model_params) thetas = 1 iam = system.get_iam(thetas, iam_model=iam_model) - assert m.assert_called_once_with(thetas, **model_params) + m.assert_called_with(thetas, **model_params) assert iam < 1. From 1e63f04d5206bfe72e66ec2c0f2f0073997a6bed Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Oct 2019 11:56:54 -0600 Subject: [PATCH 32/34] improve coverage --- pvlib/test/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 38f17bec6a..88cf89ae5a 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -473,7 +473,7 @@ def test_infer_aoi_model_invalid(location, system_no_aoi): with pytest.raises(ValueError): ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') - assert 'could not infer AOI model' in str(sys.exc_info()) + assert 'could not infer AOI model' in str(sys.exc_info()) def constant_spectral_loss(mc): From 5fb7cc834fd5779662efb131ebcf7b70fdda22a0 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Oct 2019 12:33:11 -0600 Subject: [PATCH 33/34] fix exception test --- pvlib/test/test_modelchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 88cf89ae5a..de894fb8c6 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): + with pytest.raises(ValueError) as excinfo: ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') - assert 'could not infer AOI model' in str(sys.exc_info()) + assert 'could not infer AOI model' in str(excinfo) def constant_spectral_loss(mc): From b43b28c8d52fdd0deb4259dfd2ae6f744dddba8f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Oct 2019 12:57:13 -0600 Subject: [PATCH 34/34] fix the fix --- pvlib/test/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index de894fb8c6..c4d7c7aabe 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -473,7 +473,7 @@ def test_infer_aoi_model_invalid(location, system_no_aoi): with pytest.raises(ValueError) as excinfo: ModelChain(system_no_aoi, location, orientation_strategy='None', spectral_model='no_loss') - assert 'could not infer AOI model' in str(excinfo) + assert 'could not infer AOI model' in str(excinfo.value) def constant_spectral_loss(mc):