diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 5ffb0e564a..4ccc9687c3 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -13,6 +13,9 @@ Enhancements * Add new losses function that accounts for non-uniform irradiance on bifacial modules, :py:func:`pvlib.bifacial.power_mismatch_deline`. (:issue:`2045`, :pull:`2046`) +* Add new parameters for min/max absolute air mass to + :py:func:`pvlib.spectrum.spectral_factor_firstsolar`. + (:issue:`2086`, :pull:`2100`) Bug fixes diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 197c226231..cab6084cac 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -358,21 +358,17 @@ def integrate(e): def spectral_factor_firstsolar(precipitable_water, airmass_absolute, module_type=None, coefficients=None, min_precipitable_water=0.1, - max_precipitable_water=8): + max_precipitable_water=8, + min_airmass_absolute=0.58, + max_airmass_absolute=10): r""" Spectral mismatch modifier based on precipitable water and absolute (pressure-adjusted) air mass. - 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}} + Estimates the spectral mismatch modifier, :math:`M`, representing the + effect of variation in the spectral irradiance on the module short circuit + current :math:`M` is estimated from absolute (pressure-corrected) air + mass, :math:`AM_a`, and precipitable water, :math:`Pw`. Default coefficients are determined for several cell types with known quantum efficiency curves, by using the Simple Model of the @@ -383,15 +379,13 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * :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 + * Spectrum simulated on an equatorial facing surface with 37° tilt * All other parameters fixed at G173 standard - From these simulated spectra, M is calculated using the known + From these simulated spectra, :math:`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]_. + applied to fit Eq. 1 to determine the coefficients for each module. More + details on the model can be found in [2]_. Parameters ---------- @@ -406,11 +400,12 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, '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. + * ``'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 @@ -430,12 +425,20 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, min_precipitable_water : float, default 0.1 minimum atmospheric precipitable water. Any ``precipitable_water`` value lower than ``min_precipitable_water`` - is set to ``min_precipitable_water`` to avoid model divergence. [cm] + is set to ``min_precipitable_water``. [cm] max_precipitable_water : float, default 8 maximum atmospheric precipitable water. Any ``precipitable_water`` value greater than ``max_precipitable_water`` - is set to ``np.nan`` to avoid model divergence. [cm] + is set to ``np.nan``. [cm] + + min_airmass_absolute : float, default 0.58 + minimum absolute airmass. Any ``airmass_absolute`` value lower than + ``min_airmass_absolute`` is set to ``min_airmass_absolute``. [unitless] + + max_airmass_absolute : float, default 10 + minimum absolute airmass. Any ``airmass_absolute`` value greater than + ``max_airmass_absolute`` is set to ``max_airmass_absolute``. [unitless] Returns ------- @@ -445,6 +448,22 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, effective irradiance, i.e., the irradiance that is converted to electrical current. + Notes + ---- + The ``spectral_factor_firstsolar`` model takes the following form: + + .. 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}}. + + The default values for the limits applied to :math:`AM_a` and :math:`Pw` + via the ``min_precipitable_water``, ``max_precipitable_water``, + ``min_airmass_absolute``, and ``max_airmass_absolute`` are set to prevent + divergence of the model presented above. These default values were + determined by the publication authors in the original pvlib-python + implementation (:pull:`208`). + References ---------- .. [1] Gueymard, Christian. SMARTS2: a simple model of the atmospheric @@ -461,36 +480,27 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, 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(precipitable_water) pw = pw.astype('float64') if np.min(pw) < min_precipitable_water: pw = np.maximum(pw, min_precipitable_water) - warn('Exceptionally low pw values replaced with ' - f'{min_precipitable_water} cm to prevent model divergence') + warn('Low precipitable water values replaced with ' + f'{min_precipitable_water} cm in the calculation of spectral ' + 'mismatch.') - # Warn user about Pw data that is exceptionally high if np.max(pw) > max_precipitable_water: pw[pw > max_precipitable_water] = 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 + warn('High precipitable water values replaced with np.nan in ' + 'the calculation of spectral mismatch.') + + airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) + + if np.min(airmass_absolute) < min_airmass_absolute: + airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) + warn('Low airmass values replaced with 'f'{min_airmass_absolute} in ' + 'the calculation of spectral mismatch.') + # pvlib.atmosphere.get_absolute_airmass(1, + # pvlib.atmosphere.alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000. @@ -519,7 +529,6 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, raise TypeError('Cannot resolve input, must supply only one of ' + 'module_type and coefficients') - # Evaluate Spectral Shift coeff = coefficients ama = airmass_absolute modifier = ( @@ -640,8 +649,8 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, One of the following PV technology strings from [1]_: * ``'cdte'`` - anonymous CdTe module. - * ``'monosi'``, - anonymous sc-si module. - * ``'multisi'``, - anonymous mc-si- module. + * ``'monosi'`` - anonymous sc-si module. + * ``'multisi'`` - anonymous mc-si- module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. * ``'perovskite'`` - anonymous pervoskite module. @@ -755,8 +764,8 @@ def spectral_factor_pvspec(airmass_absolute, clearsky_index, * ``'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. + * ``'monosi'`` - anonymous monocrystalline Si module. + * ``'multisi'`` - anonymous multicrystalline Si module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py index 969fb819b8..09b24866ea 100644 --- a/pvlib/tests/test_spectrum.py +++ b/pvlib/tests/test_spectrum.py @@ -264,6 +264,22 @@ def test_spectral_factor_firstsolar_supplied(): assert_allclose(out, expected, atol=1e-3) +def test_spectral_factor_firstsolar_large_airmass_supplied_max(): + # test airmass > user-defined maximum is treated same as airmass=maximum + m_eq11 = spectrum.spectral_factor_firstsolar(1, 11, 'monosi', + max_airmass_absolute=11) + m_gt11 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi', + max_airmass_absolute=11) + assert_allclose(m_eq11, m_gt11) + + +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_ambiguous(): with pytest.raises(TypeError): spectrum.spectral_factor_firstsolar(1, 1) @@ -276,36 +292,34 @@ def test_spectral_factor_firstsolar_ambiguous_both(): 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'): + m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, 'monosi') + m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') + assert_allclose(m_eq58, m_lt58) + with pytest.warns(UserWarning, match='Low airmass values replaced'): _ = 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') + 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'): + with pytest.warns(UserWarning, match='High precipitable water values ' + 'replaced'): out = spectrum.spectral_factor_firstsolar(6, 1.5, max_precipitable_water=5, module_type='monosi') - with pytest.warns(UserWarning, match='Exceptionally low pw values'): + with pytest.warns(UserWarning, match='Low precipitable water values ' + 'replaced'): 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'): + with pytest.warns(UserWarning, match='Low precipitable water values ' + 'replaced'): out = spectrum.spectral_factor_firstsolar(0.2, 1.5, min_precipitable_water=1, module_type='monosi')