diff --git a/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst b/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst index b6fe7f4684..ed6cdc0b51 100644 --- a/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst +++ b/docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst @@ -10,3 +10,5 @@ Spectrum spectrum.get_example_spectral_response spectrum.get_am15g spectrum.calc_spectral_mismatch_field + spectrum.spectral_factor_firstsolar + spectrum.spectral_factor_sapm diff --git a/docs/sphinx/source/whatsnew/v0.9.6.rst b/docs/sphinx/source/whatsnew/v0.9.6.rst index c731eba2eb..729de3e457 100644 --- a/docs/sphinx/source/whatsnew/v0.9.6.rst +++ b/docs/sphinx/source/whatsnew/v0.9.6.rst @@ -15,6 +15,11 @@ Breaking Changes Deprecations ~~~~~~~~~~~~ +* Functions for calculating spectral modifiers have been moved to :py:mod:`pvlib.spectrum`: + :py:func:`pvlib.atmosphere.first_solar_spectral_correction` is deprecated and + replaced by :py:func:`~pvlib.spectrum.spectral_factor_firstsolar`, and + :py:func:`pvlib.pvsystem.sapm_spectral_loss` is deprecated and replaced by + :py:func:`~pvlib.spectrum.spectral_factor_sapm`. (:pull:`1628`) * Removed the ``get_ecmwf_macc`` and ``read_ecmwf_macc`` iotools functions as the MACC dataset has been `removed by ECMWF `_ (data period 2003-2012). Instead, ECMWF recommends to use CAMS global diff --git a/pvlib/__init__.py b/pvlib/__init__.py index ff6b375017..dfd5f1dc2a 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -1,6 +1,9 @@ from pvlib.version import __version__ # noqa: F401 from pvlib import ( # noqa: F401 + # list spectrum first so it's available for atmosphere & pvsystem (GH 1628) + spectrum, + atmosphere, bifacial, clearsky, @@ -20,7 +23,6 @@ soiling, solarposition, spa, - spectrum, temperature, tools, tracking, diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index bc00269ea0..08e40b9fc2 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -3,11 +3,11 @@ absolute airmass and to determine pressure from altitude or vice versa. """ -from warnings import warn - import numpy as np import pandas as pd +import pvlib +from pvlib._deprecation import deprecated APPARENT_ZENITH_MODELS = ('simple', 'kasten1966', 'kastenyoung1989', 'gueymard1993', 'pickering2002') @@ -336,175 +336,10 @@ def gueymard94_pw(temp_air, relative_humidity): return pw -def first_solar_spectral_correction(pw, airmass_absolute, - module_type=None, coefficients=None, - min_pw=0.1, max_pw=8): - r""" - Spectral mismatch modifier based on precipitable water and absolute - (pressure-adjusted) airmass. - - Estimates a spectral mismatch modifier :math:`M` representing the effect on - module short circuit current of variation in the spectral - irradiance. :math:`M` is estimated from absolute (pressure currected) air - mass, :math:`AM_a`, and precipitable water, :math:`Pw`, using the following - function: - - .. math:: - - M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5} - + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}} - - Default coefficients are determined for several cell types with - known quantum efficiency curves, by using the Simple Model of the - Atmospheric Radiative Transfer of Sunshine (SMARTS) [1]_. Using - SMARTS, spectrums are simulated with all combinations of AMa and - Pw where: - - * :math:`0.5 \textrm{cm} <= Pw <= 5 \textrm{cm}` - * :math:`1.0 <= AM_a <= 5.0` - * Spectral range is limited to that of CMP11 (280 nm to 2800 nm) - * spectrum simulated on a plane normal to the sun - * All other parameters fixed at G173 standard - - From these simulated spectra, M is calculated using the known - quantum efficiency curves. Multiple linear regression is then - applied to fit Eq. 1 to determine the coefficients for each module. - - Based on the PVLIB Matlab function ``pvl_FSspeccorr`` by Mitchell - Lee and Alex Panchula of First Solar, 2016 [2]_. - - Parameters - ---------- - pw : array-like - atmospheric precipitable water. [cm] - - airmass_absolute : array-like - absolute (pressure-adjusted) airmass. [unitless] - - min_pw : float, default 0.1 - minimum atmospheric precipitable water. Any pw value lower than min_pw - is set to min_pw to avoid model divergence. [cm] - - max_pw : float, default 8 - maximum atmospheric precipitable water. Any pw value higher than max_pw - is set to NaN to avoid model divergence. [cm] - - module_type : None or string, default None - a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi', - 'multisi', and 'polysi' (can be lower or upper case). If provided, - module_type selects default coefficients for the following modules: - - * 'cdte' - First Solar Series 4-2 CdTe module. - * 'monosi', 'xsi' - First Solar TetraSun module. - * 'multisi', 'polysi' - anonymous multi-crystalline silicon module. - * 'cigs' - anonymous copper indium gallium selenide module. - * 'asi' - anonymous amorphous silicon module. - - The module used to calculate the spectral correction - coefficients corresponds to the Multi-crystalline silicon - Manufacturer 2 Model C from [3]_. The spectral response (SR) of CIGS - and a-Si modules used to derive coefficients can be found in [4]_ - - coefficients : None or array-like, default None - Allows for entry of user-defined spectral correction - coefficients. Coefficients must be of length 6. Derivation of - coefficients requires use of SMARTS and PV module quantum - efficiency curve. Useful for modeling PV module types which are - not included as defaults, or to fine tune the spectral - correction to a particular PV module. Note that the parameters for - modules with very similar quantum efficiency should be similar, - in most cases limiting the need for module specific coefficients. - - Returns - ------- - modifier: array-like - spectral mismatch factor (unitless) which is can be multiplied - with broadband irradiance reaching a module's cells to estimate - effective irradiance, i.e., the irradiance that is converted to - electrical current. - - References - ---------- - .. [1] Gueymard, Christian. SMARTS2: a simple model of the atmospheric - radiative transfer of sunshine: algorithms and performance - assessment. Cocoa, FL: Florida Solar Energy Center, 1995. - .. [2] Lee, Mitchell, and Panchula, Alex. "Spectral Correction for - Photovoltaic Module Performance Based on Air Mass and Precipitable - Water." IEEE Photovoltaic Specialists Conference, Portland, 2016 - .. [3] Marion, William F., et al. User's Manual for Data for Validating - Models for PV Module Performance. National Renewable Energy - Laboratory, 2014. http://www.nrel.gov/docs/fy14osti/61610.pdf - .. [4] Schweiger, M. and Hermann, W, Influence of Spectral Effects - on Energy Yield of Different PV Modules: Comparison of Pwat and - MMF Approach, TUV Rheinland Energy GmbH report 21237296.003, - January 2017 - """ - - # --- Screen Input Data --- - - # *** Pw *** - # Replace Pw Values below 0.1 cm with 0.1 cm to prevent model from - # diverging" - pw = np.atleast_1d(pw) - pw = pw.astype('float64') - if np.min(pw) < min_pw: - pw = np.maximum(pw, min_pw) - warn(f'Exceptionally low pw values replaced with {min_pw} cm to ' - 'prevent model divergence') - - # Warn user about Pw data that is exceptionally high - if np.max(pw) > max_pw: - pw[pw > max_pw] = np.nan - warn('Exceptionally high pw values replaced by np.nan: ' - 'check input data.') - - # *** AMa *** - # Replace Extremely High AM with AM 10 to prevent model divergence - # AM > 10 will only occur very close to sunset - if np.max(airmass_absolute) > 10: - airmass_absolute = np.minimum(airmass_absolute, 10) - - # Warn user about AMa data that is exceptionally low - if np.min(airmass_absolute) < 0.58: - warn('Exceptionally low air mass: ' + - 'model not intended for extra-terrestrial use') - # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of - # Mina Pirquita, Argentian = 4340 m. Highest elevation city with - # population over 50,000. - - _coefficients = {} - _coefficients['cdte'] = ( - 0.86273, -0.038948, -0.012506, 0.098871, 0.084658, -0.0042948) - _coefficients['monosi'] = ( - 0.85914, -0.020880, -0.0058853, 0.12029, 0.026814, -0.0017810) - _coefficients['xsi'] = _coefficients['monosi'] - _coefficients['polysi'] = ( - 0.84090, -0.027539, -0.0079224, 0.13570, 0.038024, -0.0021218) - _coefficients['multisi'] = _coefficients['polysi'] - _coefficients['cigs'] = ( - 0.85252, -0.022314, -0.0047216, 0.13666, 0.013342, -0.0008945) - _coefficients['asi'] = ( - 1.12094, -0.047620, -0.0083627, -0.10443, 0.098382, -0.0033818) - - if module_type is not None and coefficients is None: - coefficients = _coefficients[module_type.lower()] - elif module_type is None and coefficients is not None: - pass - elif module_type is None and coefficients is None: - raise TypeError('No valid input provided, both module_type and ' + - 'coefficients are None') - else: - raise TypeError('Cannot resolve input, must supply only one of ' + - 'module_type and coefficients') - - # Evaluate Spectral Shift - coeff = coefficients - ama = airmass_absolute - modifier = ( - coeff[0] + coeff[1]*ama + coeff[2]*pw + coeff[3]*np.sqrt(ama) + - coeff[4]*np.sqrt(pw) + coeff[5]*ama/np.sqrt(pw)) - - return modifier +first_solar_spectral_correction = deprecated( + since='0.10.0', + alternative='pvlib.spectrum.spectral_factor_firstsolar' +)(pvlib.spectrum.spectral_factor_firstsolar) def bird_hulstrom80_aod_bb(aod380, aod500): diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index d136089b10..6d5a5b2ef4 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -19,7 +19,7 @@ from pvlib._deprecation import deprecated from pvlib import (atmosphere, iam, inverter, irradiance, - singlediode as _singlediode, temperature) + singlediode as _singlediode, spectrum, temperature) from pvlib.tools import _build_kwargs, _build_args @@ -672,8 +672,8 @@ def sapm_celltemp(self, poa_global, temp_air, wind_speed): @_unwrap_single_value def sapm_spectral_loss(self, airmass_absolute): """ - Use the :py:func:`sapm_spectral_loss` function, the input - parameters, and ``self.module_parameters`` to calculate F1. + Use the :py:func:`pvlib.spectrum.spectral_factor_sapm` function, + the input parameters, and ``self.module_parameters`` to calculate F1. Parameters ---------- @@ -686,7 +686,8 @@ def sapm_spectral_loss(self, airmass_absolute): The SAPM spectral loss coefficient. """ return tuple( - sapm_spectral_loss(airmass_absolute, array.module_parameters) + spectrum.spectral_factor_sapm(airmass_absolute, + array.module_parameters) for array in self.arrays ) @@ -884,7 +885,7 @@ def noct_sam_celltemp(self, poa_global, temp_air, wind_speed, @_unwrap_single_value def first_solar_spectral_loss(self, pw, airmass_absolute): """ - Use :py:func:`pvlib.atmosphere.first_solar_spectral_correction` to + Use :py:func:`pvlib.spectrum.spectral_factor_firstsolar` to calculate the spectral loss modifier. The model coefficients are specific to the module's cell type, and are determined by searching for one of the following keys in self.module_parameters (in order): @@ -925,9 +926,8 @@ def _spectral_correction(array, pw): module_type = array._infer_cell_type() coefficients = None - return atmosphere.first_solar_spectral_correction( - pw, airmass_absolute, - module_type, coefficients + return spectrum.spectral_factor_firstsolar( + pw, airmass_absolute, module_type, coefficients ) return tuple( itertools.starmap(_spectral_correction, zip(self.arrays, pw)) @@ -2602,43 +2602,10 @@ def sapm(effective_irradiance, temp_cell, module): return out -def sapm_spectral_loss(airmass_absolute, module): - """ - Calculates the SAPM spectral loss coefficient, F1. - - Parameters - ---------- - airmass_absolute : numeric - Absolute airmass - - module : dict-like - A dict, Series, or DataFrame defining the SAPM performance - parameters. See the :py:func:`sapm` notes section for more - details. - - Returns - ------- - F1 : numeric - The SAPM spectral loss coefficient. - - Notes - ----- - nan airmass values will result in 0 output. - """ - - am_coeff = [module['A4'], module['A3'], module['A2'], module['A1'], - module['A0']] - - spectral_loss = np.polyval(am_coeff, airmass_absolute) - - spectral_loss = np.where(np.isnan(spectral_loss), 0, spectral_loss) - - spectral_loss = np.maximum(0, spectral_loss) - - if isinstance(airmass_absolute, pd.Series): - spectral_loss = pd.Series(spectral_loss, airmass_absolute.index) - - return spectral_loss +sapm_spectral_loss = deprecated( + since='0.10.0', + alternative='pvlib.spectrum.spectral_factor_sapm' +)(spectrum.spectral_factor_sapm) def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, @@ -2698,11 +2665,11 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, See also -------- pvlib.iam.sapm - pvlib.pvsystem.sapm_spectral_loss + pvlib.spectrum.spectral_factor_sapm pvlib.pvsystem.sapm """ - F1 = sapm_spectral_loss(airmass_absolute, module) + F1 = spectrum.spectral_factor_sapm(airmass_absolute, module) F2 = iam.sapm(aoi, module) Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse) diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py index b3d838acfe..70d3918b49 100644 --- a/pvlib/spectrum/__init__.py +++ b/pvlib/spectrum/__init__.py @@ -1,3 +1,8 @@ from pvlib.spectrum.spectrl2 import spectrl2 # noqa: F401 -from pvlib.spectrum.mismatch import (get_example_spectral_response, get_am15g, - calc_spectral_mismatch_field) +from pvlib.spectrum.mismatch import ( # noqa: F401 + calc_spectral_mismatch_field, + get_am15g, + get_example_spectral_response, + spectral_factor_firstsolar, + spectral_factor_sapm, +) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 5db4649ddd..a7e7cf0851 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -8,6 +8,8 @@ from scipy.interpolate import interp1d import os +from warnings import warn + def get_example_spectral_response(wavelength=None): ''' @@ -235,3 +237,212 @@ def integrate(e): smm = pd.Series(smm, index=e_sun.index) return smm + + +def spectral_factor_firstsolar(pw, airmass_absolute, module_type=None, + coefficients=None, min_pw=0.1, max_pw=8): + r""" + Spectral mismatch modifier based on precipitable water and absolute + (pressure-adjusted) airmass. + + Estimates a spectral mismatch modifier :math:`M` representing the effect on + module short circuit current of variation in the spectral + irradiance. :math:`M` is estimated from absolute (pressure currected) air + mass, :math:`AM_a`, and precipitable water, :math:`Pw`, using the following + function: + + .. math:: + + M = c_1 + c_2 AM_a + c_3 Pw + c_4 AM_a^{0.5} + + c_5 Pw^{0.5} + c_6 \frac{AM_a} {Pw^{0.5}} + + Default coefficients are determined for several cell types with + known quantum efficiency curves, by using the Simple Model of the + Atmospheric Radiative Transfer of Sunshine (SMARTS) [1]_. Using + SMARTS, spectrums are simulated with all combinations of AMa and + Pw where: + + * :math:`0.5 \textrm{cm} <= Pw <= 5 \textrm{cm}` + * :math:`1.0 <= AM_a <= 5.0` + * Spectral range is limited to that of CMP11 (280 nm to 2800 nm) + * spectrum simulated on a plane normal to the sun + * All other parameters fixed at G173 standard + + From these simulated spectra, M is calculated using the known + quantum efficiency curves. Multiple linear regression is then + applied to fit Eq. 1 to determine the coefficients for each module. + + Based on the PVLIB Matlab function ``pvl_FSspeccorr`` by Mitchell + Lee and Alex Panchula of First Solar, 2016 [2]_. + + Parameters + ---------- + pw : array-like + atmospheric precipitable water. [cm] + + airmass_absolute : array-like + absolute (pressure-adjusted) airmass. [unitless] + + min_pw : float, default 0.1 + minimum atmospheric precipitable water. Any pw value lower than min_pw + is set to min_pw to avoid model divergence. [cm] + + max_pw : float, default 8 + maximum atmospheric precipitable water. Any pw value higher than max_pw + is set to NaN to avoid model divergence. [cm] + + module_type : None or string, default None + a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi', + 'multisi', and 'polysi' (can be lower or upper case). If provided, + module_type selects default coefficients for the following modules: + + * 'cdte' - First Solar Series 4-2 CdTe module. + * 'monosi', 'xsi' - First Solar TetraSun module. + * 'multisi', 'polysi' - anonymous multi-crystalline silicon module. + * 'cigs' - anonymous copper indium gallium selenide module. + * 'asi' - anonymous amorphous silicon module. + + The module used to calculate the spectral correction + coefficients corresponds to the Multi-crystalline silicon + Manufacturer 2 Model C from [3]_. The spectral response (SR) of CIGS + and a-Si modules used to derive coefficients can be found in [4]_ + + coefficients : None or array-like, default None + Allows for entry of user-defined spectral correction + coefficients. Coefficients must be of length 6. Derivation of + coefficients requires use of SMARTS and PV module quantum + efficiency curve. Useful for modeling PV module types which are + not included as defaults, or to fine tune the spectral + correction to a particular PV module. Note that the parameters for + modules with very similar quantum efficiency should be similar, + in most cases limiting the need for module specific coefficients. + + Returns + ------- + modifier: array-like + spectral mismatch factor (unitless) which is can be multiplied + with broadband irradiance reaching a module's cells to estimate + effective irradiance, i.e., the irradiance that is converted to + electrical current. + + References + ---------- + .. [1] Gueymard, Christian. SMARTS2: a simple model of the atmospheric + radiative transfer of sunshine: algorithms and performance + assessment. Cocoa, FL: Florida Solar Energy Center, 1995. + .. [2] Lee, Mitchell, and Panchula, Alex. "Spectral Correction for + Photovoltaic Module Performance Based on Air Mass and Precipitable + Water." IEEE Photovoltaic Specialists Conference, Portland, 2016 + .. [3] Marion, William F., et al. User's Manual for Data for Validating + Models for PV Module Performance. National Renewable Energy + Laboratory, 2014. http://www.nrel.gov/docs/fy14osti/61610.pdf + .. [4] Schweiger, M. and Hermann, W, Influence of Spectral Effects + on Energy Yield of Different PV Modules: Comparison of Pwat and + MMF Approach, TUV Rheinland Energy GmbH report 21237296.003, + January 2017 + """ + + # --- Screen Input Data --- + + # *** Pw *** + # Replace Pw Values below 0.1 cm with 0.1 cm to prevent model from + # diverging" + pw = np.atleast_1d(pw) + pw = pw.astype('float64') + if np.min(pw) < min_pw: + pw = np.maximum(pw, min_pw) + warn(f'Exceptionally low pw values replaced with {min_pw} cm to ' + 'prevent model divergence') + + # Warn user about Pw data that is exceptionally high + if np.max(pw) > max_pw: + pw[pw > max_pw] = np.nan + warn('Exceptionally high pw values replaced by np.nan: ' + 'check input data.') + + # *** AMa *** + # Replace Extremely High AM with AM 10 to prevent model divergence + # AM > 10 will only occur very close to sunset + if np.max(airmass_absolute) > 10: + airmass_absolute = np.minimum(airmass_absolute, 10) + + # Warn user about AMa data that is exceptionally low + if np.min(airmass_absolute) < 0.58: + warn('Exceptionally low air mass: ' + + 'model not intended for extra-terrestrial use') + # pvl_absoluteairmass(1,pvl_alt2pres(4340)) = 0.58 Elevation of + # Mina Pirquita, Argentian = 4340 m. Highest elevation city with + # population over 50,000. + + _coefficients = {} + _coefficients['cdte'] = ( + 0.86273, -0.038948, -0.012506, 0.098871, 0.084658, -0.0042948) + _coefficients['monosi'] = ( + 0.85914, -0.020880, -0.0058853, 0.12029, 0.026814, -0.0017810) + _coefficients['xsi'] = _coefficients['monosi'] + _coefficients['polysi'] = ( + 0.84090, -0.027539, -0.0079224, 0.13570, 0.038024, -0.0021218) + _coefficients['multisi'] = _coefficients['polysi'] + _coefficients['cigs'] = ( + 0.85252, -0.022314, -0.0047216, 0.13666, 0.013342, -0.0008945) + _coefficients['asi'] = ( + 1.12094, -0.047620, -0.0083627, -0.10443, 0.098382, -0.0033818) + + if module_type is not None and coefficients is None: + coefficients = _coefficients[module_type.lower()] + elif module_type is None and coefficients is not None: + pass + elif module_type is None and coefficients is None: + raise TypeError('No valid input provided, both module_type and ' + + 'coefficients are None') + else: + raise TypeError('Cannot resolve input, must supply only one of ' + + 'module_type and coefficients') + + # Evaluate Spectral Shift + coeff = coefficients + ama = airmass_absolute + modifier = ( + coeff[0] + coeff[1]*ama + coeff[2]*pw + coeff[3]*np.sqrt(ama) + + coeff[4]*np.sqrt(pw) + coeff[5]*ama/np.sqrt(pw)) + + return modifier + + +def spectral_factor_sapm(airmass_absolute, module): + """ + Calculates the SAPM spectral loss coefficient, F1. + + Parameters + ---------- + airmass_absolute : numeric + Absolute airmass + + module : dict-like + A dict, Series, or DataFrame defining the SAPM performance + parameters. See the :py:func:`sapm` notes section for more + details. + + Returns + ------- + F1 : numeric + The SAPM spectral loss coefficient. + + Notes + ----- + nan airmass values will result in 0 output. + """ + + am_coeff = [module['A4'], module['A3'], module['A2'], module['A1'], + module['A0']] + + spectral_loss = np.polyval(am_coeff, airmass_absolute) + + spectral_loss = np.where(np.isnan(spectral_loss), 0, spectral_loss) + + spectral_loss = np.maximum(0, spectral_loss) + + if isinstance(airmass_absolute, pd.Series): + spectral_loss = pd.Series(spectral_loss, airmass_absolute.index) + + return spectral_loss diff --git a/pvlib/tests/test_atmosphere.py b/pvlib/tests/test_atmosphere.py index 3518ccb7d6..46db622ee5 100644 --- a/pvlib/tests/test_atmosphere.py +++ b/pvlib/tests/test_atmosphere.py @@ -9,6 +9,8 @@ from pvlib import atmosphere +from pvlib._deprecation import pvlibDeprecationWarning + def test_pres2alt(): out = atmosphere.pres2alt(np.array([10000, 90000, 101325])) @@ -86,68 +88,10 @@ def test_gueymard94_pw(): assert_allclose(pws, expected, atol=0.01) -@pytest.mark.parametrize("module_type,expect", [ - ('cdte', np.array( - [[ 0.9905102 , 0.9764032 , 0.93975028], - [ 1.02928735, 1.01881074, 0.98578821], - [ 1.04750335, 1.03814456, 1.00623986]])), - ('monosi', np.array( - [[ 0.9776977 , 1.02043409, 1.03574032], - [ 0.98630905, 1.03055092, 1.04736262], - [ 0.98828494, 1.03299036, 1.05026561]])), - ('polysi', np.array( - [[ 0.9770408 , 1.01705849, 1.02613202], - [ 0.98992828, 1.03173953, 1.04260662], - [ 0.99352435, 1.03588785, 1.04730718]])), - ('cigs', np.array( - [[ 0.9745919 , 1.02821696, 1.05067895], - [ 0.97529378, 1.02967497, 1.05289307], - [ 0.97269159, 1.02730558, 1.05075651]])), - ('asi', np.array( - [[ 1.0555275 , 0.87707583, 0.72243772], - [ 1.11225204, 0.93665901, 0.78487953], - [ 1.14555295, 0.97084011, 0.81994083]])) -]) -def test_first_solar_spectral_correction(module_type, expect): - ams = np.array([1, 3, 5]) - pws = np.array([1, 3, 5]) - ams, pws = np.meshgrid(ams, pws) - out = atmosphere.first_solar_spectral_correction(pws, ams, module_type) - assert_allclose(out, expect, atol=0.001) - - -def test_first_solar_spectral_correction_supplied(): - # use the cdte coeffs - coeffs = (0.87102, -0.040543, -0.00929202, 0.10052, 0.073062, -0.0034187) - out = atmosphere.first_solar_spectral_correction(1, 1, coefficients=coeffs) - expected = 0.99134828 - assert_allclose(out, expected, atol=1e-3) - - -def test_first_solar_spectral_correction_ambiguous(): - with pytest.raises(TypeError): - atmosphere.first_solar_spectral_correction(1, 1) - - -def test_first_solar_spectral_correction_range(): - with pytest.warns(UserWarning, match='Exceptionally high pw values'): - out = atmosphere.first_solar_spectral_correction(np.array([.1, 3, 10]), - np.array([1, 3, 5]), - module_type='monosi') - expected = np.array([0.96080878, 1.03055092, nan]) - assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Exceptionally high pw values'): - out = atmosphere.first_solar_spectral_correction(6, 1.5, max_pw=5, - module_type='monosi') - with pytest.warns(UserWarning, match='Exceptionally low pw values'): - out = atmosphere.first_solar_spectral_correction(np.array([0, 3, 8]), - np.array([1, 3, 5]), - module_type='monosi') - expected = np.array([0.96080878, 1.03055092, 1.04932727]) - assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Exceptionally low pw values'): - out = atmosphere.first_solar_spectral_correction(0.2, 1.5, min_pw=1, - module_type='monosi') +def test_first_solar_spectral_correction_deprecated(): + with pytest.warns(pvlibDeprecationWarning, + match='Use pvlib.spectrum.spectral_factor_firstsolar'): + atmosphere.first_solar_spectral_correction(1, 1, 'cdte') def test_kasten96_lt(): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2966aa55d6..8d3caca477 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -14,6 +14,7 @@ from pvlib import atmosphere from pvlib import iam as _iam from pvlib import irradiance +from pvlib import spectrum from pvlib.location import Location from pvlib.pvsystem import FixedMount from pvlib import temperature @@ -253,28 +254,19 @@ def test_PVSystem_multi_array_sapm(sapm_module_params): system.sapm(500, temp_cell) -@pytest.mark.parametrize('airmass,expected', [ - (1.5, 1.00028714375), - (np.array([[10, np.nan]]), np.array([[0.999535, 0]])), - (pd.Series([5]), pd.Series([1.0387675])) -]) -def test_sapm_spectral_loss(sapm_module_params, airmass, expected): - - out = pvsystem.sapm_spectral_loss(airmass, sapm_module_params) - - if isinstance(airmass, pd.Series): - assert_series_equal(out, expected, check_less_precise=4) - else: - assert_allclose(out, expected, atol=1e-4) +def test_sapm_spectral_loss_deprecated(sapm_module_params): + with pytest.warns(pvlibDeprecationWarning, + match='Use pvlib.spectrum.spectral_factor_sapm'): + pvsystem.sapm_spectral_loss(1, sapm_module_params) def test_PVSystem_sapm_spectral_loss(sapm_module_params, mocker): - mocker.spy(pvsystem, 'sapm_spectral_loss') + mocker.spy(spectrum, 'spectral_factor_sapm') system = pvsystem.PVSystem(module_parameters=sapm_module_params) airmass = 2 out = system.sapm_spectral_loss(airmass) - pvsystem.sapm_spectral_loss.assert_called_once_with(airmass, - sapm_module_params) + spectrum.spectral_factor_sapm.assert_called_once_with(airmass, + sapm_module_params) assert_allclose(out, 1, atol=0.5) @@ -302,12 +294,12 @@ def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params): ]) def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, coefficients, mocker): - mocker.spy(atmosphere, 'first_solar_spectral_correction') + mocker.spy(spectrum, 'spectral_factor_firstsolar') system = pvsystem.PVSystem(module_parameters=module_parameters) pw = 3 airmass_absolute = 3 out = system.first_solar_spectral_loss(pw, airmass_absolute) - atmosphere.first_solar_spectral_correction.assert_called_once_with( + spectrum.spectral_factor_firstsolar.assert_called_once_with( pw, airmass_absolute, module_type, coefficients) assert_allclose(out, 1, atol=0.5) diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 80b53d2af3..444e45733c 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -4,7 +4,7 @@ import numpy as np from pvlib import spectrum -from .conftest import DATA_DIR +from .conftest import DATA_DIR, assert_series_equal SPECTRL2_TEST_DATA = DATA_DIR / 'spectrl2_example_spectra.csv' @@ -171,3 +171,101 @@ def test_calc_spectral_mismatch_field(spectrl2_data): mm = spectrum.calc_spectral_mismatch_field(sr, e_sun=e_sun) assert mm.index is e_sun.index assert_allclose(mm, expected, rtol=1e-6) + + +@pytest.mark.parametrize("module_type,expect", [ + ('cdte', np.array( + [[ 0.99051020, 0.97640320, 0.93975028], + [ 1.02928735, 1.01881074, 0.98578821], + [ 1.04750335, 1.03814456, 1.00623986]])), + ('monosi', np.array( + [[ 0.97769770, 1.02043409, 1.03574032], + [ 0.98630905, 1.03055092, 1.04736262], + [ 0.98828494, 1.03299036, 1.05026561]])), + ('polysi', np.array( + [[ 0.97704080, 1.01705849, 1.02613202], + [ 0.98992828, 1.03173953, 1.04260662], + [ 0.99352435, 1.03588785, 1.04730718]])), + ('cigs', np.array( + [[ 0.97459190, 1.02821696, 1.05067895], + [ 0.97529378, 1.02967497, 1.05289307], + [ 0.97269159, 1.02730558, 1.05075651]])), + ('asi', np.array( + [[ 1.05552750, 0.87707583, 0.72243772], + [ 1.11225204, 0.93665901, 0.78487953], + [ 1.14555295, 0.97084011, 0.81994083]])) +]) +def test_spectral_factor_firstsolar(module_type, expect): + ams = np.array([1, 3, 5]) + pws = np.array([1, 3, 5]) + ams, pws = np.meshgrid(ams, pws) + out = spectrum.spectral_factor_firstsolar(pws, ams, module_type) + assert_allclose(out, expect, atol=0.001) + + +def test_spectral_factor_firstsolar_supplied(): + # use the cdte coeffs + coeffs = (0.87102, -0.040543, -0.00929202, 0.10052, 0.073062, -0.0034187) + out = spectrum.spectral_factor_firstsolar(1, 1, coefficients=coeffs) + expected = 0.99134828 + assert_allclose(out, expected, atol=1e-3) + + +def test_spectral_factor_firstsolar_ambiguous(): + with pytest.raises(TypeError): + spectrum.spectral_factor_firstsolar(1, 1) + + +def test_spectral_factor_firstsolar_ambiguous_both(): + # use the cdte coeffs + coeffs = (0.87102, -0.040543, -0.00929202, 0.10052, 0.073062, -0.0034187) + with pytest.raises(TypeError): + spectrum.spectral_factor_firstsolar(1, 1, 'cdte', coefficients=coeffs) + + +def test_spectral_factor_firstsolar_large_airmass(): + # test that airmass > 10 is treated same as airmass==10 + m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, 'monosi') + m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') + assert_allclose(m_eq10, m_gt10) + + +def test_spectral_factor_firstsolar_low_airmass(): + with pytest.warns(UserWarning, match='Exceptionally low air mass'): + _ = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') + + +def test_spectral_factor_firstsolar_range(): + with pytest.warns(UserWarning, match='Exceptionally high pw values'): + out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), + np.array([1, 3, 5]), + module_type='monosi') + expected = np.array([0.96080878, 1.03055092, np.nan]) + assert_allclose(out, expected, atol=1e-3) + with pytest.warns(UserWarning, match='Exceptionally high pw values'): + out = spectrum.spectral_factor_firstsolar(6, 1.5, max_pw=5, + module_type='monosi') + with pytest.warns(UserWarning, match='Exceptionally low pw values'): + out = spectrum.spectral_factor_firstsolar(np.array([0, 3, 8]), + np.array([1, 3, 5]), + module_type='monosi') + expected = np.array([0.96080878, 1.03055092, 1.04932727]) + assert_allclose(out, expected, atol=1e-3) + with pytest.warns(UserWarning, match='Exceptionally low pw values'): + out = spectrum.spectral_factor_firstsolar(0.2, 1.5, min_pw=1, + module_type='monosi') + + +@pytest.mark.parametrize('airmass,expected', [ + (1.5, 1.00028714375), + (np.array([[10, np.nan]]), np.array([[0.999535, 0]])), + (pd.Series([5]), pd.Series([1.0387675])) +]) +def test_spectral_factor_sapm(sapm_module_params, airmass, expected): + + out = spectrum.spectral_factor_sapm(airmass, sapm_module_params) + + if isinstance(airmass, pd.Series): + assert_series_equal(out, expected, check_less_precise=4) + else: + assert_allclose(out, expected, atol=1e-4)