Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 61 additions & 52 deletions pvlib/spectrum/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
----------
Expand All @@ -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
Expand All @@ -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
-------
Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
44 changes: 29 additions & 15 deletions pvlib/tests/test_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
Expand Down