Repository: pvlib/pvlib-python

Issue:
Google Summer of Code project summary: Implementing new spectral corrections in pvlib
### **Introduction**

This issue summarises the ongoing and completed work for the [GSoC 2024 programme](https://summerofcode.withgoogle.com/) with pvlib.

The project I am undertaking relates to [an issue raised earlier this year](https://github.com/pvlib/pvlib-python/issues/1950). The official abstract for the project [can be found here](https://summerofcode.withgoogle.com/programs/2024/projects/TT5QrYqT). This project is being carried out under the supervision of @AdamRJensen and @kandersolar 

In summary, the aim of the project is to implement new spectral correction models in pvlib, as well as examples of their application and use. In addition to updating this issue as the project progresses, detailed updates on the project will also be shared in [blog posts](https://rdaxini.github.io/). Major milestone updates will be shared on [my LinkedIn page](https://www.linkedin.com/in/rajiv-daxini-ba354a237/) as well.

### **Plan**
The current overall project plan is described below.

1. Three new models are planned for development and implementation:

- [Pelland 2020 PVSPEC model](https://ieeexplore.ieee.org/document/9300932)
- [Huld 2009 JRC model](https://www.researchgate.net/profile/Tony-Sample/publication/256080247_A_simple_model_for_estimate_the_influence_of_spectrum_variations_on_PV_performance/links/56e6df1108ae77cfe4bd1b0c/A-simple-model-for-estimate-the-influence-of-spectrum-variations-on-PV-performance.pdf)
- [Daxini 2023 APE/e model](https://www.sciencedirect.com/science/article/pii/S0360544223024404?pes=vor) (stretch goal, if time permits)

2. A combined example to demonstrate the use of the new models and application in the overall modelling pipeline will be developed.

As the project develops I will link these tasks to individual issues and pull requests.
### **Issues**
Open:

Closed:
#2065 (this one)
#2135 (create average photon energy function)
#2125 (suggestion to split `mismatch.py`) 
#1950 (general issue, add new spectral factor models)
#2087 (add JRC spectral factor model)
#2115 (update SAPM spectral factor docs)
#2107 (add spectral factor example),
#2086 (update `spectral_factor_firstsolar`)

### **PRs**
- (merged) Split `test_spectrum.py` #2151
- (merged) Function to calculate APE #2140
- (merged) Restructure `pvlib/spectrum` #2136
- (merged) Update First Solar model #2100 
- (merged) Spectral mismatch factor example #2114
- (merged) Update SAPM docs #2116
- (merged) JRC model (air mass and clearsky index) #2088 
- (merged) PVSPEC model (air mass and clearsky index): #2072
- (closed) Add ape_e spectral  factor model #2126
---------------
Any feedback on the project is certainly more than welcome. Using github and contributing to open-source software is completely new to me so I am looking forward to learning about this so that I can contribute to pvlib not only through this project but also in future.

Dax
add JRC spectral factor model
**Is your feature request related to a problem? Please describe.**
The PVSPEC model for the spectral mismatch factor was recently merged (#2072) but an alternative parameterisation of the mismatch factor as a function of air mass and clear sky index is available, namely [the JRC model](https://www.researchgate.net/publication/256080247_A_simple_model_for_estimate_the_influence_of_spectrum_variations_on_PV_performance). I think providing pvlib users with a choice of model would be a valuable enhancement.

**Describe the solution you'd like**
Implement JRC model using the PVSPEC function as a template.

**Additional context**
Some questions:

1. Citation for the clear sky model in [the JRC publication](https://www.researchgate.net/publication/256080247_A_simple_model_for_estimate_the_influence_of_spectrum_variations_on_PV_performance) is:
>"K. Scharmer and J. Greif (eds.), “The European Solar Radiation Atlas, Vol 2: Database and Exploitation Software”, Presses de l’École des Mines, Paris (2000)"

and

>"M. Šúri and J. Hofierka. “A new GIS-based solar radiation model and its application to photovoltaic assessments”, Transactions in GIS, 8 (2004), pp. 175-190".

In [the PVSPEC paper](https://ieeexplore.ieee.org/document/9300932) and [the pvlib implementation](https://pvlib-python--2072.org.readthedocs.build/en/2072/reference/generated/pvlib.spectrum.spectral_factor_pvspec.html):
> "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.". 

I think these are the same ESRA model (?) ---if so, which citation should I use?

2. As far as I can tell, the model used to estimate air mass is not stated, nor whether it is relative or absolute air mass. In the docs, would it suffice just to write something like "the model used to estimate the air mass (denoted AM) is not stated." ?
----
This issue links to my GSoC project (#2065) and this issue #1950 


Discussion:
As part of the documentation of a growing number of methods, it might be helpful if a “overview” table of spectral-correction method vs. required+optional inputs were added. This way consumers would be able to get a quicker idea of what information/data is needed to apply each correction. The timescale over which the correction is applied could also be a factor (although maybe they are all the same at this point).
@markcampanelli I think that is a good suggestion. I tried to achieve something similar with Table 10 [in this study](https://www.sciencedirect.com/science/article/pii/S0360544223028554#tbl10); is that the sort of thing you had in mind? Timescale is a good point too. I have had a paper examining this issue under review for a while so maybe/hopefully something will come out of that soon that I can implement into this work.
@didierthevenard

> @markcampanelli I think that is a good suggestion. I tried to achieve something similar with Table 10 [in this study](https://www.sciencedirect.com/science/article/pii/S0360544223028554#tbl10); is that the sort of thing you had in mind? Timescale is a good point too. I have had a paper examining this issue under review for a while so maybe/hopefully something will come out of that soon that I can implement into this work.

@RDaxini Yes. Depending on your time, you could at a bare minimum reference your paper with table 10. If time permits, then you could perhaps provide a “translation” of that table into something more specific to pvlib’s variable names and functions.

BTW: Your timescale-focused paper also sounds like an interesting read 🙂. Good luck working it through the process!





Patch:
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 8dd74444a2..15fd1d09b8 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
@@ -15,5 +15,6 @@ Spectrum
    spectrum.spectral_factor_firstsolar
    spectrum.spectral_factor_sapm
    spectrum.spectral_factor_pvspec
+   spectrum.spectral_factor_jrc
    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 2fdcd1c444..fe137a92c7 100644
--- a/docs/sphinx/source/whatsnew/v0.11.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.11.0.rst
@@ -44,9 +44,14 @@ 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`)
+* 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`)
+* Add function :py:func:`pvlib.spectrum.spectral_factor_jrc`, which calculates
+  the spectral mismatch factor as a function of airmass and clearsky
+  index using the JRC model.
+  (:issue:`1950`, :issue:`2065`, :issue:`2087`, :pull:`2088`)
 * Added extraterrestrial and direct spectra of the ASTM G173-03 standard with
   the new function :py:func:`pvlib.spectrum.get_reference_spectra`.
   (:issue:`1963`, :pull:`2039`)
diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py
index 3194fd17a2..b95f221066 100644
--- a/pvlib/spectrum/__init__.py
+++ b/pvlib/spectrum/__init__.py
@@ -8,6 +8,7 @@
     spectral_factor_firstsolar,
     spectral_factor_sapm,
     spectral_factor_pvspec,
+    spectral_factor_jrc,
     sr_to_qe,
     qe_to_sr
 )
diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py
index 9b644b2ef9..d2fc4dcf1d 100644
--- a/pvlib/spectrum/mismatch.py
+++ b/pvlib/spectrum/mismatch.py
@@ -794,6 +794,116 @@ def spectral_factor_pvspec(airmass_absolute, clearsky_index,
     return mismatch
 
 
+def spectral_factor_jrc(airmass, clearsky_index, module_type=None,
+                        coefficients=None):
+    r"""
+    Estimate a technology-specific spectral mismatch modifier from
+    airmass and clear sky index using the JRC model.
+
+    The JRC spectral mismatch model includes the effects of cloud cover on
+    the irradiance spectrum. Model coefficients are derived using measurements
+    of irradiance and module performance at the Joint Research Centre (JRC) in
+    Ispra, Italy (45.80N, 8.62E). Coefficients for two module types are
+    available via the ``module_type`` parameter. More details on the model can
+    be found in [1]_.
+
+    Parameters
+    ----------
+    airmass : numeric
+        relative airmass. [unitless]
+
+    clearsky_index: numeric
+        clear sky index. [unitless]
+
+    module_type : str, optional
+        One of the following PV technology strings from [1]_:
+
+        * ``'cdte'`` - anonymous CdTe module.
+        * ``'multisi'`` - anonymous multicrystalline Si 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 JRC model parameterises the spectral mismatch factor as a function
+    of air mass and the clear sky index as follows:
+
+    .. math::
+
+        M = 1 + a_1(e^{-k_c}-e^{-1}) + a_2(k_c-1)+a_3(AM-1.5),
+
+    where :math:`M` is the spectral mismatch factor, :math:`k_c` is the clear
+    sky index, :math:`AM` is the air mass, :math:`e` is Euler's number, and
+    :math:`a_1, a_2, a_3` are module-specific coefficients. The :math:`a_n`
+    coefficients available via the ``coefficients`` parameter differ from the
+    :math:`k_n` coefficients documented in [1]_ in that they are normalised by
+    the specific short-circuit current value, :math:`I_{sc0}^*`, which is the
+    expected short-circuit current at standard test conditions indoors. The
+    model used to estimate the air mass (denoted as :math:`AM`) is not stated
+    in the original publication. The authors of [1]_ used the ESRA model [2]_
+    to estimate the clear sky GHI for the clear sky index, which is the ratio
+    of GHI to clear sky GHI. Also, prior to the calculation of :math:`k_c`, the
+    irradiance measurements were corrected for angle of incidence using the
+    Martin and Ruiz model [3]_.
+
+    References
+    ----------
+    .. [1] Huld, T., Sample, T., and Dunlop, E., 2009. A simple model
+       for estimating the influence of spectrum variations on PV performance.
+       In Proceedings of the 24th European Photovoltaic Solar Energy
+       Conference, Hamburg, Germany pp. 3385-3389. 2009. Accessed at:
+       https://www.researchgate.net/publication/256080247
+    .. [2] 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`
+    .. [3] Martin, N. and Ruiz, J. M., 2001. Calculation of the PV modules
+       angular losses under field conditions by means of an analytical model.
+       Solar Energy Materials and Solar Cells, 70(1), 25-38.
+       :doi:`10.1016/S0927-0248(00)00408-6`
+    """
+
+    _coefficients = {}
+    _coefficients['multisi'] = (0.00172, 0.000508, 0.00000357)
+    _coefficients['cdte'] = (0.000643, 0.000130, 0.0000108)
+    # normalise coefficients by I*sc0, see [1]
+    _coefficients = {
+        'multisi': tuple(x / 0.00348 for x in _coefficients['multisi']),
+        'cdte': tuple(x / 0.001150 for x in _coefficients['cdte'])
+    }
+    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
+    mismatch = (
+        1
+        + coeff[0] * (np.exp(-clearsky_index) - np.exp(-1))
+        + coeff[1] * (clearsky_index - 1)
+        + coeff[2] * (airmass - 1.5)
+    )
+    return mismatch
+
+
 def sr_to_qe(sr, wavelength=None, normalize=False):
     """
     Convert spectral responsivities to quantum efficiencies.


Test patch:
diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py
index 5c89078bc1..969fb819b8 100644
--- a/pvlib/tests/test_spectrum.py
+++ b/pvlib/tests/test_spectrum.py
@@ -426,6 +426,54 @@ def test_spectral_factor_pvspec_supplied_ambiguous():
                                         coefficients=None)
 
 
+@pytest.mark.parametrize("module_type,expected", [
+    ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])),
+    ('cdte', np.array([1.09657,  1.05594, 1.02763, 0.97740])),
+])
+def test_spectral_factor_jrc(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_jrc(ams, kcs,
+                                       module_type=module_type)
+    assert np.allclose(expected, out, atol=1e-4)
+
+
+@pytest.mark.parametrize("module_type,expected", [
+    ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])),
+    ('cdte', np.array([1.09657,  1.05594, 1.02763, 0.97740])),
+])
+def test_spectral_factor_jrc_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_jrc(ams, kcs,
+                                       module_type=module_type)
+    assert isinstance(out, pd.Series)
+    assert np.allclose(expected, out, atol=1e-4)
+
+
+def test_spectral_factor_jrc_supplied():
+    # use the multisi coeffs
+    coeffs = (0.494, 0.146, 0.00103)
+    out = spectrum.spectral_factor_jrc(1.0, 0.8, coefficients=coeffs)
+    expected = 1.01052106
+    assert_allclose(out, expected, atol=1e-4)
+
+
+def test_spectral_factor_jrc_supplied_redundant():
+    # Error when specifying both module_type and coefficients
+    coeffs = (0.494, 0.146, 0.00103)
+    with pytest.raises(ValueError, match='supply only one of'):
+        spectrum.spectral_factor_jrc(1.0, 0.8, module_type='multisi',
+                                     coefficients=coeffs)
+
+
+def test_spectral_factor_jrc_supplied_ambiguous():
+    # Error when specifying neither module_type nor coefficients
+    with pytest.raises(ValueError, match='No valid input provided'):
+        spectrum.spectral_factor_jrc(1.0, 0.8, module_type=None,
+                                     coefficients=None)
+
+
 @pytest.fixture
 def sr_and_eqe_fixture():
     # Just some arbitrary data for testing the conversion functions