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 982bc91742..b453a300ee 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 @@ -12,9 +12,10 @@ Spectrum spectrum.calc_spectral_mismatch_field spectrum.spectral_factor_caballero spectrum.spectral_factor_firstsolar - spectrum.spectral_factor_sapm - spectrum.spectral_factor_pvspec spectrum.spectral_factor_jrc + spectrum.spectral_factor_polo + spectrum.spectral_factor_pvspec + spectrum.spectral_factor_sapm spectrum.sr_to_qe spectrum.qe_to_sr spectrum.average_photon_energy diff --git a/docs/sphinx/source/user_guide/modeling_topics/spectrum.rst b/docs/sphinx/source/user_guide/modeling_topics/spectrum.rst index bd6440a47c..0747c0b295 100644 --- a/docs/sphinx/source/user_guide/modeling_topics/spectrum.rst +++ b/docs/sphinx/source/user_guide/modeling_topics/spectrum.rst @@ -61,11 +61,21 @@ Reference [2]_. | +-----------------------------+ | ✓ | ✓ | | | + [4]_ | | | clearsky_index | | | | | | | | +-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+ +| :py:func:`Polo ` | :term:`precipitable_water`, | | | | | | | | +| +-----------------------------+ ✓ | | ✓ | ✓ | ✓ | + [5]_ | +| | :term:`airmass_absolute`, | | | | | | | | +| +-----------------------------+ | | | | | | | +| | aod500, | | | | | | | | +| +-----------------------------+ | | | | | | | +| | :term:`aoi`, | | | | | | | | +| +-----------------------------+ | | | | | | | +| | :term:`pressure` | | | | | | | | ++-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+ | :py:func:`PVSPEC ` | :term:`airmass_absolute`, | | | | | | | | -| +-----------------------------+ ✓ | ✓ | ✓ | ✓ | ✓ | | [5]_ | +| +-----------------------------+ ✓ | ✓ | ✓ | ✓ | ✓ | | [6]_ | | | clearsky_index | | | | | | | | +-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+ -| :py:func:`SAPM ` | :term:`airmass_absolute` | | | | | | | [6]_ | +| :py:func:`SAPM ` | :term:`airmass_absolute` | | | | | | | [7]_ | +-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+ @@ -88,16 +98,19 @@ References PVSPEC Model of Photovoltaic Spectral Mismatch Factor," in Proc. 2020 IEEE 47th Photovoltaic Specialists Conference (PVSC), Calgary, AB, Canada, 2020, pp. 1–6. :doi:`10.1109/PVSC45281.2020.9300932` -.. [5] D. L. King, W. E. Boyson, and J. A. Kratochvil, Photovoltaic Array +.. [5] J. Polo and C. Sanz-Saiz, 'Development of spectral mismatch models + for BIPV applications in building façades', Renewable Energy, vol. 245, + p. 122820, Jun. 2025, :doi:`10.1016/j.renene.2025.122820` +.. [6] D. L. King, W. E. Boyson, and J. A. Kratochvil, Photovoltaic Array Performance Model, Sandia National Laboratories, Albuquerque, NM, USA, Tech. Rep. SAND2004-3535, Aug. 2004. :doi:`10.2172/919131` -.. [6] M. Lee and A. Panchula, "Spectral Correction for Photovoltaic Module +.. [7] M. Lee and A. Panchula, "Spectral Correction for Photovoltaic Module Performance Based on Air Mass and Precipitable Water," 2016 IEEE 43rd Photovoltaic Specialists Conference (PVSC), Portland, OR, USA, 2016, pp. 3696-3699. :doi:`10.1109/PVSC.2016.7749836` -.. [7] H. Thomas, S. Tony, and D. Ewan, “A Simple Model for Estimating the - Influence of Spectrum Variations on PV Performance,” pp. 3385–3389, Nov. +.. [8] T. Huld, T. Sample, and E. Dunlop, "A Simple Model for Estimating the + Influence of Spectrum Variations on PV Performance," pp. 3385–3389, Nov. 2009, :doi:`10.4229/24THEUPVSEC2009-4AV.3.27` -.. [8] IEC 60904-7:2019, Photovoltaic devices — Part 7: Computation of the +.. [9] IEC 60904-7:2019, Photovoltaic devices — Part 7: Computation of the spectral mismatch correction for measurements of photovoltaic devices, International Electrotechnical Commission, Geneva, Switzerland, 2019. \ No newline at end of file diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index 7d0f0e0747..663c48cae3 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -43,6 +43,8 @@ Enhancements installed. (:issue:`2497`, :pull:`2571`) * Add :py:func:`~pvlib.iotools.get_era5`, a function for accessing ERA5 reanalysis data. (:pull:`2573`) +* Add :py:func:`~pvlib.spectrum.spectral_factor_polo`, a function for estimating + spectral mismatch factors for vertical PV façades. (:issue:`2406`, :pull:`2491`) Documentation ~~~~~~~~~~~~~ @@ -75,3 +77,9 @@ Contributors * Will Hobbs (:ghuser:`williamhobbs`) * Cliff Hansen (:ghuser:`cwhanse`) * Joseph Radford (:ghuser:`josephradford`) +* Jesús Polo (:ghuser:`jesuspolo`) +* Adam R. Jensen (:ghuser:`adamrjensen`) +* Echedey Luis (:ghuser:`echedey-ls`) +* Anton Driesse (:ghuser:`adriesse`) +* Rajiv Daxini (:ghuser:`RDaxini`) +* Kevin Anderson (:ghuser:`kandersolar`) diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py index 59f9db9582..1e551c83fa 100644 --- a/pvlib/spectrum/__init__.py +++ b/pvlib/spectrum/__init__.py @@ -3,9 +3,10 @@ calc_spectral_mismatch_field, spectral_factor_caballero, spectral_factor_firstsolar, - spectral_factor_sapm, - spectral_factor_pvspec, spectral_factor_jrc, + spectral_factor_polo, + spectral_factor_pvspec, + spectral_factor_sapm, ) from pvlib.spectrum.irradiance import ( # noqa: F401 get_reference_spectra, diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 9f68d77c83..c754040213 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -698,3 +698,95 @@ def spectral_factor_jrc(airmass, clearsky_index, module_type=None, + coeff[2] * (airmass - 1.5) ) return mismatch + + +def spectral_factor_polo(precipitable_water, airmass_absolute, aod500, aoi, + pressure, module_type=None, coefficients=None, + albedo=0.2): + """ + Estimate the spectral mismatch for BIPV application in vertical facades. + + The model's authors note that this model could also be applied to + vertical bifacial ground-mount systems [1]_, although it has not been + validated in that context. + + Parameters + ---------- + precipitable_water : numeric + Atmospheric precipitable water. [cm] + airmass_absolute : numeric + Absolute (pressure-adjusted) airmass. See :term:`airmass_absolute`. + [unitless] + aod500 : numeric + Atmospheric aerosol optical depth at 500 nm. [unitless] + aoi : numeric + Angle of incidence on the vertical surface. See :term:`aoi`. + [degrees] + pressure : numeric + Atmospheric pressure. See :term:`pressure`. [Pa] + module_type : str, optional + One of the following PV technology strings from [1]_: + + * ``'cdte'`` - anonymous CdTe module. + * ``'monosi'`` - anonymous monocrystalline silicon 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 coefficient + sets via the ``module_type`` parameter. Must have nine elements. + The first six elements correspond to the [p1, p2, p3, p4, b, c] + parameters of the SMM model. The last three elements corresponds + to the [c1, c2, c3] parameters of the albedo correction factor. + albedo : numeric, default 0.2 + Ground albedo. See :term:`albedo`. [unitless] + + Returns + ------- + modifier: 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. + + References + ---------- + .. [1] J. Polo and C. Sanz-Saiz, 'Development of spectral mismatch models + for BIPV applications in building façades', Renewable Energy, vol. 245, + p. 122820, Jun. 2025, :doi:`10.1016/j.renene.2025.122820` + """ + if module_type is None and coefficients is None: + raise ValueError('Must provide either `module_type` or `coefficients`') + if module_type is not None and coefficients is not None: + raise ValueError('Only one of `module_type` and `coefficients` should ' + 'be provided') + # prevent nan for aoi greater than 90 + if aoi > 90: + aoi = 90 + f_aoi_rel = pvlib.atmosphere.get_relative_airmass(aoi, + model='kastenyoung1989') + f_aoi = pvlib.atmosphere.get_absolute_airmass(f_aoi_rel, pressure) + Ram = f_aoi / airmass_absolute + _coefficients = { + 'cdte': (-0.0009, 46.80, 49.20, -0.87, 0.00041, 0.053), + 'monosi': (0.0027, 10.34, 9.48, 0.31, 0.00077, 0.006), + 'cigs': (0.0017, 2.33, 1.30, 0.11, 0.00098, -0.018), + 'asi': (0.0024, 7.32, 7.09, -0.72, -0.0013, 0.089), + } + c = { + 'asi': (0.0056, -0.020, 1.014), + 'cigs': (-0.0009, -0.0003, 1), + 'cdte': (0.0021, -0.01, 1.01), + 'monosi': (0, -0.003, 1.0), + } + if module_type is not None: + coeff = _coefficients[module_type] + c_albedo = c[module_type] + else: + coeff = coefficients[:6] + c_albedo = coefficients[6:] + smm = coeff[0] * Ram + coeff[1] / (coeff[2] + Ram**coeff[3]) \ + + coeff[4] / aod500 + coeff[5]*np.sqrt(precipitable_water) + # Ground albedo correction + g = c_albedo[0] * (albedo/0.2)**2 \ + + c_albedo[1] * (albedo/0.2) + c_albedo[2] + return g*smm diff --git a/tests/spectrum/test_mismatch.py b/tests/spectrum/test_mismatch.py index edd780b2b1..bc7b6c8a2c 100644 --- a/tests/spectrum/test_mismatch.py +++ b/tests/spectrum/test_mismatch.py @@ -288,3 +288,88 @@ def test_spectral_factor_jrc_supplied_ambiguous(): with pytest.raises(ValueError, match='No valid input provided'): spectrum.spectral_factor_jrc(1.0, 0.8, module_type=None, coefficients=None) + + +@pytest.mark.parametrize("module_type,expected", [ + ('cdte', np.array( + [0.992801, 1.00004, 1.011576, 0.995003, 0.950156, 0.975665])), + ('monosi', np.array( + [1.000152, 0.969588, 0.984636, 1.015405, 1.024238, 1.005061])), + ('cigs', np.array( + [1.004621, 0.956719, 0.971668, 1.0254, 1.060066, 1.020196])), + ('asi', np.array( + [0.986968, 1.049725, 1.051978, 0.957968, 0.842258, 0.941927])), +]) +def test_spectral_factor_polo(module_type, expected): + pws = np.array([0.96, 0.96, 1.85, 1.88, 0.66, 0.66]) + aods = np.array([0.085, 0.085, 0.16, 0.19, 0.088, 0.088]) + ams = np.array([1.34, 1.34, 2.2, 2.2, 2.6, 2.6]) + aois = np.array([46.0, 76.0, 74.0, 28.0, 24.0, 55.0]) + pressure = np.array([101300, 101400, 100500, 101325, 80000, 120000]) + alb = np.array([0.15, 0.2, 0.3, 0.18, 0.32, 0.26]) + out = spectrum.spectral_factor_polo( + pws, ams, aods, aois, pressure, module_type=module_type, albedo=alb) + np.testing.assert_allclose(out, expected, atol=1e-6) + + +@pytest.fixture +def polo_inputs(): + return {'precipitable_water': 0.96, + 'airmass_absolute': 1.34, + 'aod500': 0.085, + 'aoi': 76, + 'pressure': 101400, + 'albedo': 0.2} + + +def test_spectral_factor_polo_coefficients(polo_inputs): + # test that supplying custom coefficients works as expected + coefficients = ( + (0.0027, 10.34, 9.48, 0.31, 0.00077, 0.006) # base Si coeffs + + (0, -0.003, 1.0) # Si albedo correction coeffs + ) + out = spectrum.spectral_factor_polo(**polo_inputs, + coefficients=coefficients) + np.testing.assert_allclose(out, 0.969588, atol=1e-6) + + +def test_spectral_factor_polo_errors(polo_inputs): + with pytest.raises(ValueError, match='Must provide either'): + spectrum.spectral_factor_polo(**polo_inputs) + with pytest.raises(ValueError, match='Only one of'): + spectrum.spectral_factor_polo(**polo_inputs, module_type='CdTe', + coefficients=(1, 1, 1, 1, 1, 1)) + + +def test_spectral_factor_polo_types(polo_inputs): + # float: + out = spectrum.spectral_factor_polo(**polo_inputs, module_type='monosi') + assert isinstance(out, float) + np.testing.assert_allclose(out, 0.969588, atol=1e-6) + + # array: + arrays = {k: np.array([v, v]) for k, v in polo_inputs.items()} + out = spectrum.spectral_factor_polo(**arrays, module_type='monosi') + assert isinstance(out, np.ndarray) + np.testing.assert_allclose(out, [0.969588]*2, atol=1e-6) + + # series: + series = {k: pd.Series(v) for k, v in arrays.items()} + out = spectrum.spectral_factor_polo(**series, module_type='monosi') + assert isinstance(out, pd.Series) + pd.testing.assert_series_equal(out, pd.Series([0.969588]*2), atol=1e-6) + + +def test_spectral_factor_polo_NaN(polo_inputs): + # nan in -> nan out + for key in polo_inputs: + inputs = polo_inputs.copy() + inputs[key] = np.nan + out = spectrum.spectral_factor_polo(**inputs, module_type='monosi') + assert np.isnan(out) + + +def test_spectral_factor_polo_aoi_gt_90(polo_inputs): + polo_inputs['aoi'] = 95 + out = spectrum.spectral_factor_polo(**polo_inputs, module_type='monosi') + assert np.isnan(out)