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 fde3b170a9..56d7a6df15 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 @@ -13,5 +13,6 @@ Spectrum spectrum.spectral_factor_caballero spectrum.spectral_factor_firstsolar spectrum.spectral_factor_sapm + spectrum.spectral_factor_pvspec spectrum.sr_to_qe spectrum.qe_to_sr diff --git a/docs/sphinx/source/whatsnew/v0.11.0.rst b/docs/sphinx/source/whatsnew/v0.11.0.rst index 14694d0fc4..9d36af22e3 100644 --- a/docs/sphinx/source/whatsnew/v0.11.0.rst +++ b/docs/sphinx/source/whatsnew/v0.11.0.rst @@ -32,7 +32,9 @@ Enhancements efficiency ([unitless]) and vice versa. The conversion functions are :py:func:`pvlib.spectrum.sr_to_qe` and :py:func:`pvlib.spectrum.qe_to_sr` respectively. (:issue:`2040`, :pull:`2041`) - +* Add function :py:func:`pvlib.spectrum.spectral_factor_pvspec`, which calculates the + spectral mismatch factor as a function of absolute airmass and clearsky index + using the PVSPEC model. (:issue:`1950`, :issue:`2065`, :pull:`2072`) Bug fixes ~~~~~~~~~ @@ -56,3 +58,4 @@ Contributors * Mark Mikofski (:ghuser:`mikofski`) * Siddharth Kaul (:ghuser:`k10blogger`) * Mark Campanelli (:ghuser:`markcampanelli`) +* Rajiv Daxini (:ghuser:`RDaxini`) diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py index 0b9f7b03e9..cda7202689 100644 --- a/pvlib/spectrum/__init__.py +++ b/pvlib/spectrum/__init__.py @@ -6,6 +6,7 @@ spectral_factor_caballero, spectral_factor_firstsolar, spectral_factor_sapm, + spectral_factor_pvspec, sr_to_qe, - qe_to_sr, + qe_to_sr ) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index e3f434f99c..7c70a35360 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -54,14 +54,14 @@ def get_example_spectral_response(wavelength=None): ''' # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 - SR_DATA = np.array([[ 290, 0.00], - [ 350, 0.27], - [ 400, 0.37], - [ 500, 0.52], - [ 650, 0.71], - [ 800, 0.88], - [ 900, 0.97], - [ 950, 1.00], + SR_DATA = np.array([[290, 0.00], + [350, 0.27], + [400, 0.37], + [500, 0.52], + [650, 0.71], + [800, 0.88], + [900, 0.97], + [950, 1.00], [1000, 0.93], [1050, 0.58], [1100, 0.21], @@ -256,7 +256,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, max_precipitable_water=8): r""" Spectral mismatch modifier based on precipitable water and absolute - (pressure-adjusted) airmass. + (pressure-adjusted) air mass. Estimates a spectral mismatch modifier :math:`M` representing the effect on module short circuit current of variation in the spectral @@ -294,7 +294,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, atmospheric precipitable water. [cm] airmass_absolute : numeric - absolute (pressure-adjusted) airmass. [unitless] + absolute (pressure-adjusted) air mass. [unitless] module_type : str, optional a string specifying a cell type. Values of 'cdte', 'monosi', 'xsi', @@ -583,6 +583,112 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, return modifier +def spectral_factor_pvspec(airmass_absolute, clearsky_index, + module_type=None, coefficients=None): + r""" + Estimate a technology-specific spectral mismatch modifier from absolute + airmass and clear sky index using the PVSPEC model. + + The PVSPEC spectral mismatch model includes the effects of cloud cover on + the irradiance spectrum. Model coefficients are derived using spectral + irradiance and other meteorological data from eight locations. Coefficients + for six module types are available via the ``module_type`` parameter. + More details on the model can be found in [1]_. + + Parameters + ---------- + airmass_absolute : numeric + absolute (pressure-adjusted) airmass. [unitless] + + clearsky_index: numeric + clear sky index. [unitless] + + module_type : str, optional + One of the following PV technology strings from [1]_: + + * ``'fs4-1'`` - First Solar series 4-1 and earlier CdTe module. + * ``'fs4-2'`` - First Solar 4-2 and later CdTe module. + * ``'monosi'``, - anonymous monocrystalline Si module. + * ``'multisi'``, - anonymous multicrystalline Si module. + * ``'cigs'`` - anonymous copper indium gallium selenide module. + * ``'asi'`` - anonymous amorphous silicon module. + + coefficients : array-like, optional + user-defined coefficients, if not using one of the default coefficient + sets via the ``module_type`` parameter. + + Returns + ------- + mismatch: numeric + spectral mismatch factor (unitless) which is multiplied + with broadband irradiance reaching a module's cells to estimate + effective irradiance, i.e., the irradiance that is converted to + electrical current. + + Notes + ----- + The PVSPEC model parameterises the spectral mismatch factor as a function + of absolute air mass and the clear sky index as follows: + + .. math:: + + M = a_1 k_c^{a_2} AM_a^{a_3}, + + where :math:`M` is the spectral mismatch factor, :math:`k_c` is the clear + sky index, :math:`AM_a` is the absolute air mass, and :math:`a_1, a_2, a_3` + are module-specific coefficients. In the PVSPEC model publication, absolute + air mass (denoted as :math:`AM`) is estimated starting from the Kasten and + Young relative air mass [2]_. The clear sky index, which is the ratio of + GHI to clear sky GHI, uses the ESRA model [3]_ to estimate the clear sky + GHI with monthly Linke turbidity values from [4]_ as inputs. + + References + ---------- + .. [1] Pelland, S., Beswick, C., Thevenard, D., Côté, A., Pai, A. and + Poissant, Y., 2020. Development and testing of the PVSPEC model of + photovoltaic spectral mismatch factor. In 2020 47th IEEE Photovoltaic + Specialists Conference (PVSC) (pp. 1258-1264). IEEE. + :doi:`10.1109/PVSC45281.2020.9300932` + .. [2] Kasten, F. and Young, A.T., 1989. Revised optical air mass tables + and approximation formula. Applied Optics, 28(22), pp.4735-4738. + :doi:`10.1364/AO.28.004735` + .. [3] Rigollier, C., Bauer, O. and Wald, L., 2000. On the clear sky model + of the ESRA—European Solar Radiation Atlas—with respect to the Heliosat + method. Solar energy, 68(1), pp.33-48. + :doi:`10.1016/S0038-092X(99)00055-9` + .. [4] SoDa website monthly Linke turbidity values: + http://www.soda-pro.com/ + """ + + _coefficients = {} + _coefficients['multisi'] = (0.9847, -0.05237, 0.03034) + _coefficients['monosi'] = (0.9845, -0.05169, 0.03034) + _coefficients['fs-2'] = (1.002, -0.07108, 0.02465) + _coefficients['fs-4'] = (0.9981, -0.05776, 0.02336) + _coefficients['cigs'] = (0.9791, -0.03904, 0.03096) + _coefficients['asi'] = (1.051, -0.1033, 0.009838) + + 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 ValueError('No valid input provided, both module_type and ' + + 'coefficients are None. module_type can be one of ' + + ", ".join(_coefficients.keys())) + else: + raise ValueError('Cannot resolve input, must supply only one of ' + + 'module_type and coefficients. module_type can be ' + + 'one of' ", ".join(_coefficients.keys())) + + coeff = coefficients + ama = airmass_absolute + kc = clearsky_index + mismatch = coeff[0]*np.power(kc, coeff[1])*np.power(ama, coeff[2]) + + return mismatch + + def sr_to_qe(sr, wavelength=None, normalize=False): """ Convert spectral responsivities to quantum efficiencies. diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 7b86cb713e..6b1dcd4506 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -8,6 +8,7 @@ SPECTRL2_TEST_DATA = DATA_DIR / 'spectrl2_example_spectra.csv' + @pytest.fixture def spectrl2_data(): # reference spectra generated with solar_utils==0.3 @@ -175,25 +176,25 @@ def test_calc_spectral_mismatch_field(spectrl2_data): @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]])), + [[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]])), + [[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]])), + [[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]])), + [[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]])) + [[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]) @@ -317,6 +318,62 @@ def test_spectral_factor_caballero_supplied_ambiguous(): coefficients=None) +@pytest.mark.parametrize("module_type,expected", [ + ('asi', np.array([1.15534029, 1.1123772, 1.08286684, 1.01915462])), + ('fs-2', np.array([1.0694323, 1.04948777, 1.03556288, 0.9881471])), + ('fs-4', np.array([1.05234725, 1.037771, 1.0275516, 0.98820533])), + ('multisi', np.array([1.03310403, 1.02391703, 1.01744833, 0.97947605])), + ('monosi', np.array([1.03225083, 1.02335353, 1.01708734, 0.97950110])), + ('cigs', np.array([1.01475834, 1.01143927, 1.00909094, 0.97852966])), +]) +def test_spectral_factor_pvspec(module_type, expected): + ams = np.array([1.0, 1.5, 2.0, 1.5]) + kcs = np.array([0.4, 0.6, 0.8, 1.4]) + out = spectrum.spectral_factor_pvspec(ams, kcs, + module_type=module_type) + assert np.allclose(expected, out, atol=1e-8) + + +@pytest.mark.parametrize("module_type,expected", [ + ('asi', pd.Series([1.15534029, 1.1123772, 1.08286684, 1.01915462])), + ('fs-2', pd.Series([1.0694323, 1.04948777, 1.03556288, 0.9881471])), + ('fs-4', pd.Series([1.05234725, 1.037771, 1.0275516, 0.98820533])), + ('multisi', pd.Series([1.03310403, 1.02391703, 1.01744833, 0.97947605])), + ('monosi', pd.Series([1.03225083, 1.02335353, 1.01708734, 0.97950110])), + ('cigs', pd.Series([1.01475834, 1.01143927, 1.00909094, 0.97852966])), +]) +def test_spectral_factor_pvspec_series(module_type, expected): + ams = pd.Series([1.0, 1.5, 2.0, 1.5]) + kcs = pd.Series([0.4, 0.6, 0.8, 1.4]) + out = spectrum.spectral_factor_pvspec(ams, kcs, + module_type=module_type) + assert isinstance(out, pd.Series) + assert np.allclose(expected, out, atol=1e-8) + + +def test_spectral_factor_pvspec_supplied(): + # use the multisi coeffs + coeffs = (0.9847, -0.05237, 0.03034) + out = spectrum.spectral_factor_pvspec(1.5, 0.8, coefficients=coeffs) + expected = 1.00860641 + assert_allclose(out, expected, atol=1e-8) + + +def test_spectral_factor_pvspec_supplied_redundant(): + # Error when specifying both module_type and coefficients + coeffs = (0.9847, -0.05237, 0.03034) + with pytest.raises(ValueError, match='supply only one of'): + spectrum.spectral_factor_pvspec(1.5, 0.8, module_type='multisi', + coefficients=coeffs) + + +def test_spectral_factor_pvspec_supplied_ambiguous(): + # Error when specifying neither module_type nor coefficients + with pytest.raises(ValueError, match='No valid input provided'): + spectrum.spectral_factor_pvspec(1.5, 0.8, module_type=None, + coefficients=None) + + @pytest.fixture def sr_and_eqe_fixture(): # Just some arbitrary data for testing the conversion functions