diff --git a/docs/examples/bifacial/plot_irradiance_nonuniformity_loss.py b/docs/examples/bifacial/plot_irradiance_nonuniformity_loss.py new file mode 100644 index 0000000000..8b19ebeb79 --- /dev/null +++ b/docs/examples/bifacial/plot_irradiance_nonuniformity_loss.py @@ -0,0 +1,129 @@ +""" +Plot Irradiance Non-uniformity Loss +=================================== + +Calculate the DC power lost to irradiance non-uniformity in a bifacial PV +array. +""" + +# %% +# The incident irradiance on the backside of a bifacial PV module is +# not uniform due to neighboring rows, the ground albedo, and site conditions. +# When each cell works at different irradiance levels, the power produced by +# the module is less than the sum of the power produced by each cell since the +# maximum power point (MPP) of each cell is different, but cells connected in +# series will operate at the same current. +# This is known as irradiance non-uniformity loss. +# +# Calculating the IV curve of each cell and then matching the working point of +# the whole module is computationally expensive, so a simple model to account +# for this loss is of interest. Deline et al. [1]_ proposed a model based on +# the Relative Mean Absolute Difference (RMAD) of the irradiance of each cell. +# They considered the standard deviation of the cells' irradiances, but they +# found that the RMAD was a better predictor of the mismatch loss. +# +# This example demonstrates how to model the irradiance non-uniformity loss +# from the irradiance levels of each cell in a PV module. +# +# The function +# :py:func:`pvlib.bifacial.power_mismatch_deline` is +# used to transform the Relative Mean Absolute Difference (RMAD) of the +# irradiance into a power loss mismatch. Down below you will find a +# numpy-based implementation of the RMAD function. +# +# References +# ---------- +# .. [1] C. Deline, S. Ayala Pelaez, S. MacAlpine, and C. Olalla, 'Estimating +# and parameterizing mismatch power loss in bifacial photovoltaic +# systems', Progress in Photovoltaics: Research and Applications, vol. 28, +# no. 7, pp. 691-703, 2020, :doi:`10.1002/pip.3259`. +# +# .. sectionauthor:: Echedey Luis + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.cm import ScalarMappable +from matplotlib.colors import Normalize + +from pvlib.bifacial import power_mismatch_deline + +# %% +# Problem description +# ------------------- +# Let's set a fixed irradiance to each cell row of the PV array with the values +# described in Figure 1 (A), [1]_. We will cover this case for educational +# purposes, although it can be achieved with the packages +# :ref:`solarfactors ` and +# :ref:`bifacial_radiance `. +# +# Here we set and plot the global irradiance level of each cell. + +x = np.arange(12, 0, -1) +y = np.arange(6, 0, -1) +cells_irrad = np.repeat([1059, 976, 967, 986, 1034, 1128], len(x)).reshape( + len(y), len(x) +) + +color_map = "gray" +color_norm = Normalize(930, 1150) + +fig, ax = plt.subplots() +fig.suptitle("Global Irradiance Levels of Each Cell") +fig.colorbar( + ScalarMappable(cmap=color_map, norm=color_norm), + ax=ax, + orientation="vertical", + label="$[W/m^2]$", +) +ax.set_aspect("equal") +ax.pcolormesh( + x, + y, + cells_irrad, + shading="nearest", + edgecolors="black", + cmap=color_map, + norm=color_norm, +) + +# %% +# Relative Mean Absolute Difference +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# Calculate the Relative Mean Absolute Difference (RMAD) of the cells' +# irradiances with the following function, Eq. (4) of [1]_: +# +# .. math:: +# +# \Delta \left[ unitless \right] = \frac{1}{n^2 \bar{G}_{total}} +# \sum_{i=1}^{n} \sum_{j=1}^{n} \lvert G_{total,i} - G_{total,j} \rvert +# + + +def rmad(data, axis=None): + """ + Relative Mean Absolute Difference. Output is [Unitless]. + https://stackoverflow.com/a/19472336/19371110 + """ + mean = np.mean(data, axis) + mad = np.mean(np.absolute(data - mean), axis) + return mad / mean + + +rmad_cells = rmad(cells_irrad) + +# this is the same as a column's RMAD! +print(rmad_cells == rmad(cells_irrad[:, 0])) + +# %% +# Mismatch Loss +# ^^^^^^^^^^^^^ +# Calculate the power loss ratio due to the irradiance non-uniformity +# with :py:func:`pvlib.bifacial.power_mismatch_deline`. + +mismatch_loss = power_mismatch_deline(rmad_cells) + +print(f"RMAD of the cells' irradiance: {rmad_cells:.3} [unitless]") +print( + "Power loss due to the irradiance non-uniformity: " + + f"{mismatch_loss:.3} [unitless]" +) diff --git a/docs/sphinx/source/reference/bifacial.rst b/docs/sphinx/source/reference/bifacial.rst index 6405fe4afc..a107062fd2 100644 --- a/docs/sphinx/source/reference/bifacial.rst +++ b/docs/sphinx/source/reference/bifacial.rst @@ -12,6 +12,13 @@ Functions for calculating front and back surface irradiance bifacial.infinite_sheds.get_irradiance bifacial.infinite_sheds.get_irradiance_poa +Loss models that are specific to bifacial PV systems + +.. autosummary:: + :toctree: generated/ + + bifacial.power_mismatch_deline + Utility functions for bifacial modeling .. autosummary:: diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index aa2205bb43..1e839c596c 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -10,6 +10,9 @@ Deprecations 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`) Bug fixes @@ -30,4 +33,4 @@ Requirements Contributors ~~~~~~~~~~~~ - +* Echedey Luis (:ghuser:`echedey-ls`) diff --git a/pvlib/bifacial/__init__.py b/pvlib/bifacial/__init__.py index 0a6b98d4f5..e166c55108 100644 --- a/pvlib/bifacial/__init__.py +++ b/pvlib/bifacial/__init__.py @@ -1,10 +1,10 @@ """ -The ``bifacial`` module contains functions to model irradiance for bifacial -modules. - +The ``bifacial`` submodule contains functions to model bifacial modules. """ + from pvlib._deprecation import deprecated -from pvlib.bifacial import pvfactors, infinite_sheds, utils # noqa: F401 +from pvlib.bifacial import pvfactors, infinite_sheds, utils # noqa: F401 +from .loss_models import power_mismatch_deline # noqa: F401 pvfactors_timeseries = deprecated( since='0.9.1', diff --git a/pvlib/bifacial/loss_models.py b/pvlib/bifacial/loss_models.py new file mode 100644 index 0000000000..cbcb2ba4d1 --- /dev/null +++ b/pvlib/bifacial/loss_models.py @@ -0,0 +1,155 @@ +import numpy as np +import pandas as pd + + +def power_mismatch_deline( + rmad, + coefficients=(0, 0.142, 0.032 * 100), + fill_factor: float = None, + fill_factor_reference: float = 0.79, +): + r""" + Estimate DC power loss due to irradiance non-uniformity. + + This model is described for bifacial modules in [1]_, where the backside + irradiance is less uniform due to mounting and site conditions. + + The power loss is estimated by a polynomial model of the Relative Mean + Absolute Difference (RMAD) of the cell-by-cell total irradiance. + + Use ``fill_factor`` to account for different fill factors between the + data used to fit the model and the module of interest. Specify the model's fill factor with + ``fill_factor_reference``. + + .. versionadded:: 0.11.1 + + Parameters + ---------- + rmad : numeric + The Relative Mean Absolute Difference of the cell-by-cell total + irradiance. [Unitless] + + See the *Notes* section for the equation to calculate ``rmad`` from the + bifaciality and the front and back irradiances. + + coefficients : float collection or numpy.polynomial.polynomial.Polynomial, default ``(0, 0.142, 0.032 * 100)`` + The polynomial coefficients to use. + + If a :external:class:`numpy.polynomial.polynomial.Polynomial`, + it is evaluated as is. If not a ``Polynomial``, it must be the + coefficients of a polynomial in ``rmad``, where the first element is + the constant term and the last element is the highest order term. A + :external:class:`~numpy.polynomial.polynomial.Polynomial` + will be created internally. + + fill_factor : float, optional + Fill factor at standard test condition (STC) of the module. + Accounts for different fill factors between the trained model and the + module under non-uniform irradiance. + If not provided, the default ``fill_factor_reference`` of 0.79 is used. + + fill_factor_reference : float, default 0.79 + Fill factor at STC of the module used to train the model. + + Returns + ------- + loss : numeric + The fractional power loss. [Unitless] + + Output will be a ``pandas.Series`` if ``rmad`` is a ``pandas.Series``. + + Notes + ----- + The default model implemented is equation (11) [1]_: + + .. math:: + + M[\%] &= 0.142 \Delta[\%] + 0.032 \Delta^2[\%] \qquad \text{(11)} + + M[-] &= 0.142 \Delta[-] + 0.032 \times 100 \Delta^2[-] + + where the upper equation is in percentage (same as paper) and the lower + one is unitless. The implementation uses the unitless version, where + :math:`M[-]` is the mismatch power loss [unitless] and + :math:`\Delta[-]` is the Relative Mean Absolute Difference [unitless] + of the global irradiance, Eq. (4) of [1]_ and [2]_. + Note that the n-th power coefficient is multiplied by :math:`100^{n-1}` + to convert the percentage to unitless. + + The losses definition is Eq. (1) of [1]_, and it's defined as a loss of the + output power: + + .. math:: + + M = 1 - \frac{P_{Array}}{\sum P_{Cells}} \qquad \text{(1)} + + To account for a module with a fill factor distinct from the one used to + train the model (``0.79`` by default), the output of the model can be + modified with Eq. (7): + + .. math:: + + M_{FF_1} = M_{FF_0} \frac{FF_1}{FF_0} \qquad \text{(7)} + + where parameter ``fill_factor`` is :math:`FF_1` and + ``fill_factor_reference`` is :math:`FF_0`. + + In the section *See Also*, you will find two packages that can be used to + calculate the irradiance at different points of the module. + + .. note:: + The global irradiance RMAD is different from the backside irradiance + RMAD. + + In case the RMAD of the backside irradiance is known, the global RMAD can + be calculated as follows, assuming the front irradiance RMAD is + negligible [2]_: + + .. math:: + + RMAD(k \cdot X + c) = RMAD(X) \cdot k \frac{k \bar{X}}{k \bar{X} + c} + = RMAD(X) \cdot \frac{k}{1 + \frac{c}{k \bar{X}}} + + by similarity with equation (2) of [1]_: + + .. math:: + + G_{total\,i} = G_{front\,i} + \phi_{Bifi} G_{rear\,i} \qquad \text{(2)} + + which yields: + + .. math:: + + RMAD_{total} = RMAD_{rear} \frac{\phi_{Bifi}} + {1 + \frac{G_{front}}{\phi_{Bifi} \bar{G}_{rear}}} + + See Also + -------- + `solarfactors `_ + Calculate the irradiance at different points of the module. + `bifacial_radiance `_ + Calculate the irradiance at different points of the module. + + References + ---------- + .. [1] C. Deline, S. Ayala Pelaez, S. MacAlpine, and C. Olalla, 'Estimating + and parameterizing mismatch power loss in bifacial photovoltaic + systems', Progress in Photovoltaics: Research and Applications, vol. 28, + no. 7, pp. 691-703, 2020, :doi:`10.1002/pip.3259`. + .. [2] “Mean absolute difference,” Wikipedia, Sep. 05, 2023. + https://en.wikipedia.org/wiki/Mean_absolute_difference#Relative_mean_absolute_difference + (accessed 2024-04-14). + """ # noqa: E501 + if isinstance(coefficients, np.polynomial.Polynomial): + model_polynom = coefficients + else: # expect an iterable + model_polynom = np.polynomial.Polynomial(coef=coefficients) + + if fill_factor: # Eq. (7), [1] + # Scale output of trained model to account for different fill factors + model_polynom = model_polynom * fill_factor / fill_factor_reference + + mismatch = model_polynom(rmad) + if isinstance(rmad, pd.Series): + mismatch = pd.Series(mismatch, index=rmad.index) + return mismatch diff --git a/pvlib/tests/bifacial/test_losses_models.py b/pvlib/tests/bifacial/test_losses_models.py new file mode 100644 index 0000000000..72dd050928 --- /dev/null +++ b/pvlib/tests/bifacial/test_losses_models.py @@ -0,0 +1,54 @@ +from pvlib import bifacial + +import pandas as pd +import numpy as np +from numpy.testing import assert_allclose + + +def test_power_mismatch_deline(): + """tests bifacial.power_mismatch_deline""" + premise_rmads = np.array([0.0, 0.05, 0.1, 0.15, 0.2, 0.25]) + # test default model is for fixed tilt + expected_ft_mms = np.array([0.0, 0.0151, 0.0462, 0.0933, 0.1564, 0.2355]) + result_def_mms = bifacial.power_mismatch_deline(premise_rmads) + assert_allclose(result_def_mms, expected_ft_mms, atol=1e-5) + assert np.all(np.diff(result_def_mms) > 0) # higher RMADs => higher losses + + # test custom coefficients, set model to 1+1*RMAD + # as Polynomial class + polynomial = np.polynomial.Polynomial([1, 1, 0]) + result_custom_mms = bifacial.power_mismatch_deline( + premise_rmads, coefficients=polynomial + ) + assert_allclose(result_custom_mms, 1 + premise_rmads) + # as list + result_custom_mms = bifacial.power_mismatch_deline( + premise_rmads, coefficients=[1, 1, 0] + ) + assert_allclose(result_custom_mms, 1 + premise_rmads) + + # test datatypes IO with Series + result_mms = bifacial.power_mismatch_deline(pd.Series(premise_rmads)) + assert isinstance(result_mms, pd.Series) + + # test fill_factor, fill_factor_reference + # default model + default fill_factor_reference + ff_ref_default = 0.79 + ff_of_interest = 0.65 + result_mms = bifacial.power_mismatch_deline( + premise_rmads, fill_factor=ff_of_interest + ) + assert_allclose( + result_mms, + expected_ft_mms * ff_of_interest / ff_ref_default, + atol=1e-5, + ) + # default model + custom fill_factor_reference + ff_of_interest = 0.65 + ff_ref = 0.75 + result_mms = bifacial.power_mismatch_deline( + premise_rmads, fill_factor=ff_of_interest, fill_factor_reference=ff_ref + ) + assert_allclose( + result_mms, expected_ft_mms * ff_of_interest / ff_ref, atol=1e-5 + )