Skip to content

Commit 8d172c4

Browse files
echedey-lskandersolarcesarddcwhanseRDaxini
authored
NREL non-uniform irradiance mismatch loss for bifacial modules (#2046)
* Preliminar implementation, tests, docs and gallery example I expect to extend the gallery example for a day's RMAD profiling. * Update versionadded * Add example * docstring update * Update v0.11.0.rst * who would have guessed it's always the linter? * Update plot_irradiance_nonuniformity_loss.py * equation fixes * RMAD properties application typo * does this fix the link? * yet another link * These links will never work, I suppose * Move some "Notes" equations to example and rearrange things there * give this equation it's space * eq rendering fixes? * maths are not mathing today * ??? * ??? x2 * ??? x3 * i feel stupid for this * Change preprint ref to published one Co-Authored-By: Kevin Anderson <[email protected]> * Published paper coeffs Co-Authored-By: Kevin Anderson <[email protected]> * Lots of improvements Pending second section of example, and checking docs Co-Authored-By: César Domínguez <[email protected]> Co-Authored-By: Kevin Anderson <[email protected]> * Linter - also missing Eq (7) Co-Authored-By: César Domínguez <[email protected]> Co-Authored-By: Kevin Anderson <[email protected]> * Test polynomial input Co-Authored-By: César Domínguez <[email protected]> * Math * Test exception * Update pvsystem.py * Update plot_irradiance_nonuniformity_loss.py * Add fill factor ratio Co-Authored-By: Kevin Anderson <[email protected]> * ª * Trying new things * Revert "Trying new things" This reverts commit 8a19a4d. * Update pvsystem.py * :( * :(( * I hope we dont miss that reference * Update pvsystem.py * Remove second section of the example * Minor text upgrade * Update pvsystem.py * Port to namespace `pvlib.bifacial` Co-Authored-By: Cliff Hansen <[email protected]> * Rename to ``power_mismatch_deline``, document in ``bifacial.rst`` Co-Authored-By: Cliff Hansen <[email protected]> * fillfactor only for predefined models Co-Authored-By: Cliff Hansen <[email protected]> * Docstring rewording Co-Authored-By: Cliff Hansen <[email protected]> * Linter * More docs rewording Co-Authored-By: Cliff Hansen <[email protected]> * Apply suggestions from Dax Co-authored-by: RDaxini <[email protected]> * Unneded image Dominguez_et_al_PVSC2023.png * Rename file * Fix equations * yup it didnt save * lintaaaaaaarrrr * Eq 7 numbering * Revert unintended changes * Adam code review Co-Authored-By: Adam R. Jensen <[email protected]> * Links? Co-Authored-By: Adam R. Jensen <[email protected]> * Referencing equations manually Co-Authored-By: Adam R. Jensen <[email protected]> * Polynomial links Co-Authored-By: Adam R. Jensen <[email protected]> * Dont abuse math mode * Update loss_models.py * I forgot * Adam Code Review Co-Authored-By: Adam R. Jensen <[email protected]> * whatsnews * Update v0.11.0.rst * Weird that some tests pass and others dont * Update loss_models.py * document * Update loss_models.py * Kevin's review (I) Co-Authored-By: Kevin Anderson <[email protected]> * Kevin's review (II) Co-Authored-By: Kevin Anderson <[email protected]> * Kevin's review (III) Co-Authored-By: Kevin Anderson <[email protected]> * Update v0.11.1.rst Co-Authored-By: Kevin Anderson <[email protected]> * minor changes * Forgot to update tests Co-Authored-By: Kevin Anderson <[email protected]> * Accordingly modify model, IO, docs to unitless model Co-Authored-By: Kevin Anderson <[email protected]> * Update loss_models.py * Update loss_models.py Co-Authored-By: Kevin Anderson <[email protected]> * Apply suggestions from code review Co-authored-by: Cliff Hansen <[email protected]> * Code review from Cliff Co-Authored-By: Cliff Hansen <[email protected]> * Rendering issues, code review from Kevin Co-Authored-By: Kevin Anderson <[email protected]> * Little missing newline --------- Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: César Domínguez <[email protected]> Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: RDaxini <[email protected]> Co-authored-by: Adam R. Jensen <[email protected]> Co-authored-by: Cliff Hansen <[email protected]>
1 parent c7b8b00 commit 8d172c4

File tree

6 files changed

+353
-5
lines changed

6 files changed

+353
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Plot Irradiance Non-uniformity Loss
3+
===================================
4+
5+
Calculate the DC power lost to irradiance non-uniformity in a bifacial PV
6+
array.
7+
"""
8+
9+
# %%
10+
# The incident irradiance on the backside of a bifacial PV module is
11+
# not uniform due to neighboring rows, the ground albedo, and site conditions.
12+
# When each cell works at different irradiance levels, the power produced by
13+
# the module is less than the sum of the power produced by each cell since the
14+
# maximum power point (MPP) of each cell is different, but cells connected in
15+
# series will operate at the same current.
16+
# This is known as irradiance non-uniformity loss.
17+
#
18+
# Calculating the IV curve of each cell and then matching the working point of
19+
# the whole module is computationally expensive, so a simple model to account
20+
# for this loss is of interest. Deline et al. [1]_ proposed a model based on
21+
# the Relative Mean Absolute Difference (RMAD) of the irradiance of each cell.
22+
# They considered the standard deviation of the cells' irradiances, but they
23+
# found that the RMAD was a better predictor of the mismatch loss.
24+
#
25+
# This example demonstrates how to model the irradiance non-uniformity loss
26+
# from the irradiance levels of each cell in a PV module.
27+
#
28+
# The function
29+
# :py:func:`pvlib.bifacial.power_mismatch_deline` is
30+
# used to transform the Relative Mean Absolute Difference (RMAD) of the
31+
# irradiance into a power loss mismatch. Down below you will find a
32+
# numpy-based implementation of the RMAD function.
33+
#
34+
# References
35+
# ----------
36+
# .. [1] C. Deline, S. Ayala Pelaez, S. MacAlpine, and C. Olalla, 'Estimating
37+
# and parameterizing mismatch power loss in bifacial photovoltaic
38+
# systems', Progress in Photovoltaics: Research and Applications, vol. 28,
39+
# no. 7, pp. 691-703, 2020, :doi:`10.1002/pip.3259`.
40+
#
41+
# .. sectionauthor:: Echedey Luis <echelual (at) gmail.com>
42+
43+
import numpy as np
44+
import matplotlib.pyplot as plt
45+
from matplotlib.cm import ScalarMappable
46+
from matplotlib.colors import Normalize
47+
48+
from pvlib.bifacial import power_mismatch_deline
49+
50+
# %%
51+
# Problem description
52+
# -------------------
53+
# Let's set a fixed irradiance to each cell row of the PV array with the values
54+
# described in Figure 1 (A), [1]_. We will cover this case for educational
55+
# purposes, although it can be achieved with the packages
56+
# :ref:`solarfactors <https://github.com/pvlib/solarfactors/>` and
57+
# :ref:`bifacial_radiance <https://github.com/NREL/bifacial_radiance>`.
58+
#
59+
# Here we set and plot the global irradiance level of each cell.
60+
61+
x = np.arange(12, 0, -1)
62+
y = np.arange(6, 0, -1)
63+
cells_irrad = np.repeat([1059, 976, 967, 986, 1034, 1128], len(x)).reshape(
64+
len(y), len(x)
65+
)
66+
67+
color_map = "gray"
68+
color_norm = Normalize(930, 1150)
69+
70+
fig, ax = plt.subplots()
71+
fig.suptitle("Global Irradiance Levels of Each Cell")
72+
fig.colorbar(
73+
ScalarMappable(cmap=color_map, norm=color_norm),
74+
ax=ax,
75+
orientation="vertical",
76+
label="$[W/m^2]$",
77+
)
78+
ax.set_aspect("equal")
79+
ax.pcolormesh(
80+
x,
81+
y,
82+
cells_irrad,
83+
shading="nearest",
84+
edgecolors="black",
85+
cmap=color_map,
86+
norm=color_norm,
87+
)
88+
89+
# %%
90+
# Relative Mean Absolute Difference
91+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92+
# Calculate the Relative Mean Absolute Difference (RMAD) of the cells'
93+
# irradiances with the following function, Eq. (4) of [1]_:
94+
#
95+
# .. math::
96+
#
97+
# \Delta \left[ unitless \right] = \frac{1}{n^2 \bar{G}_{total}}
98+
# \sum_{i=1}^{n} \sum_{j=1}^{n} \lvert G_{total,i} - G_{total,j} \rvert
99+
#
100+
101+
102+
def rmad(data, axis=None):
103+
"""
104+
Relative Mean Absolute Difference. Output is [Unitless].
105+
https://stackoverflow.com/a/19472336/19371110
106+
"""
107+
mean = np.mean(data, axis)
108+
mad = np.mean(np.absolute(data - mean), axis)
109+
return mad / mean
110+
111+
112+
rmad_cells = rmad(cells_irrad)
113+
114+
# this is the same as a column's RMAD!
115+
print(rmad_cells == rmad(cells_irrad[:, 0]))
116+
117+
# %%
118+
# Mismatch Loss
119+
# ^^^^^^^^^^^^^
120+
# Calculate the power loss ratio due to the irradiance non-uniformity
121+
# with :py:func:`pvlib.bifacial.power_mismatch_deline`.
122+
123+
mismatch_loss = power_mismatch_deline(rmad_cells)
124+
125+
print(f"RMAD of the cells' irradiance: {rmad_cells:.3} [unitless]")
126+
print(
127+
"Power loss due to the irradiance non-uniformity: "
128+
+ f"{mismatch_loss:.3} [unitless]"
129+
)

docs/sphinx/source/reference/bifacial.rst

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ Functions for calculating front and back surface irradiance
1212
bifacial.infinite_sheds.get_irradiance
1313
bifacial.infinite_sheds.get_irradiance_poa
1414

15+
Loss models that are specific to bifacial PV systems
16+
17+
.. autosummary::
18+
:toctree: generated/
19+
20+
bifacial.power_mismatch_deline
21+
1522
Utility functions for bifacial modeling
1623

1724
.. autosummary::

docs/sphinx/source/whatsnew/v0.11.1.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Deprecations
1010

1111
Enhancements
1212
~~~~~~~~~~~~
13+
* Add new losses function that accounts for non-uniform irradiance on bifacial
14+
modules, :py:func:`pvlib.bifacial.power_mismatch_deline`.
15+
(:issue:`2045`, :pull:`2046`)
1316

1417

1518
Bug fixes
@@ -30,4 +33,4 @@ Requirements
3033

3134
Contributors
3235
~~~~~~~~~~~~
33-
36+
* Echedey Luis (:ghuser:`echedey-ls`)

pvlib/bifacial/__init__.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""
2-
The ``bifacial`` module contains functions to model irradiance for bifacial
3-
modules.
4-
2+
The ``bifacial`` submodule contains functions to model bifacial modules.
53
"""
4+
65
from pvlib._deprecation import deprecated
7-
from pvlib.bifacial import pvfactors, infinite_sheds, utils # noqa: F401
6+
from pvlib.bifacial import pvfactors, infinite_sheds, utils # noqa: F401
7+
from .loss_models import power_mismatch_deline # noqa: F401
88

99
pvfactors_timeseries = deprecated(
1010
since='0.9.1',

pvlib/bifacial/loss_models.py

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import numpy as np
2+
import pandas as pd
3+
4+
5+
def power_mismatch_deline(
6+
rmad,
7+
coefficients=(0, 0.142, 0.032 * 100),
8+
fill_factor: float = None,
9+
fill_factor_reference: float = 0.79,
10+
):
11+
r"""
12+
Estimate DC power loss due to irradiance non-uniformity.
13+
14+
This model is described for bifacial modules in [1]_, where the backside
15+
irradiance is less uniform due to mounting and site conditions.
16+
17+
The power loss is estimated by a polynomial model of the Relative Mean
18+
Absolute Difference (RMAD) of the cell-by-cell total irradiance.
19+
20+
Use ``fill_factor`` to account for different fill factors between the
21+
data used to fit the model and the module of interest. Specify the model's fill factor with
22+
``fill_factor_reference``.
23+
24+
.. versionadded:: 0.11.1
25+
26+
Parameters
27+
----------
28+
rmad : numeric
29+
The Relative Mean Absolute Difference of the cell-by-cell total
30+
irradiance. [Unitless]
31+
32+
See the *Notes* section for the equation to calculate ``rmad`` from the
33+
bifaciality and the front and back irradiances.
34+
35+
coefficients : float collection or numpy.polynomial.polynomial.Polynomial, default ``(0, 0.142, 0.032 * 100)``
36+
The polynomial coefficients to use.
37+
38+
If a :external:class:`numpy.polynomial.polynomial.Polynomial`,
39+
it is evaluated as is. If not a ``Polynomial``, it must be the
40+
coefficients of a polynomial in ``rmad``, where the first element is
41+
the constant term and the last element is the highest order term. A
42+
:external:class:`~numpy.polynomial.polynomial.Polynomial`
43+
will be created internally.
44+
45+
fill_factor : float, optional
46+
Fill factor at standard test condition (STC) of the module.
47+
Accounts for different fill factors between the trained model and the
48+
module under non-uniform irradiance.
49+
If not provided, the default ``fill_factor_reference`` of 0.79 is used.
50+
51+
fill_factor_reference : float, default 0.79
52+
Fill factor at STC of the module used to train the model.
53+
54+
Returns
55+
-------
56+
loss : numeric
57+
The fractional power loss. [Unitless]
58+
59+
Output will be a ``pandas.Series`` if ``rmad`` is a ``pandas.Series``.
60+
61+
Notes
62+
-----
63+
The default model implemented is equation (11) [1]_:
64+
65+
.. math::
66+
67+
M[\%] &= 0.142 \Delta[\%] + 0.032 \Delta^2[\%] \qquad \text{(11)}
68+
69+
M[-] &= 0.142 \Delta[-] + 0.032 \times 100 \Delta^2[-]
70+
71+
where the upper equation is in percentage (same as paper) and the lower
72+
one is unitless. The implementation uses the unitless version, where
73+
:math:`M[-]` is the mismatch power loss [unitless] and
74+
:math:`\Delta[-]` is the Relative Mean Absolute Difference [unitless]
75+
of the global irradiance, Eq. (4) of [1]_ and [2]_.
76+
Note that the n-th power coefficient is multiplied by :math:`100^{n-1}`
77+
to convert the percentage to unitless.
78+
79+
The losses definition is Eq. (1) of [1]_, and it's defined as a loss of the
80+
output power:
81+
82+
.. math::
83+
84+
M = 1 - \frac{P_{Array}}{\sum P_{Cells}} \qquad \text{(1)}
85+
86+
To account for a module with a fill factor distinct from the one used to
87+
train the model (``0.79`` by default), the output of the model can be
88+
modified with Eq. (7):
89+
90+
.. math::
91+
92+
M_{FF_1} = M_{FF_0} \frac{FF_1}{FF_0} \qquad \text{(7)}
93+
94+
where parameter ``fill_factor`` is :math:`FF_1` and
95+
``fill_factor_reference`` is :math:`FF_0`.
96+
97+
In the section *See Also*, you will find two packages that can be used to
98+
calculate the irradiance at different points of the module.
99+
100+
.. note::
101+
The global irradiance RMAD is different from the backside irradiance
102+
RMAD.
103+
104+
In case the RMAD of the backside irradiance is known, the global RMAD can
105+
be calculated as follows, assuming the front irradiance RMAD is
106+
negligible [2]_:
107+
108+
.. math::
109+
110+
RMAD(k \cdot X + c) = RMAD(X) \cdot k \frac{k \bar{X}}{k \bar{X} + c}
111+
= RMAD(X) \cdot \frac{k}{1 + \frac{c}{k \bar{X}}}
112+
113+
by similarity with equation (2) of [1]_:
114+
115+
.. math::
116+
117+
G_{total\,i} = G_{front\,i} + \phi_{Bifi} G_{rear\,i} \qquad \text{(2)}
118+
119+
which yields:
120+
121+
.. math::
122+
123+
RMAD_{total} = RMAD_{rear} \frac{\phi_{Bifi}}
124+
{1 + \frac{G_{front}}{\phi_{Bifi} \bar{G}_{rear}}}
125+
126+
See Also
127+
--------
128+
`solarfactors <https://github.com/pvlib/solarfactors/>`_
129+
Calculate the irradiance at different points of the module.
130+
`bifacial_radiance <https://github.com/NREL/bifacial_radiance>`_
131+
Calculate the irradiance at different points of the module.
132+
133+
References
134+
----------
135+
.. [1] C. Deline, S. Ayala Pelaez, S. MacAlpine, and C. Olalla, 'Estimating
136+
and parameterizing mismatch power loss in bifacial photovoltaic
137+
systems', Progress in Photovoltaics: Research and Applications, vol. 28,
138+
no. 7, pp. 691-703, 2020, :doi:`10.1002/pip.3259`.
139+
.. [2] “Mean absolute difference,” Wikipedia, Sep. 05, 2023.
140+
https://en.wikipedia.org/wiki/Mean_absolute_difference#Relative_mean_absolute_difference
141+
(accessed 2024-04-14).
142+
""" # noqa: E501
143+
if isinstance(coefficients, np.polynomial.Polynomial):
144+
model_polynom = coefficients
145+
else: # expect an iterable
146+
model_polynom = np.polynomial.Polynomial(coef=coefficients)
147+
148+
if fill_factor: # Eq. (7), [1]
149+
# Scale output of trained model to account for different fill factors
150+
model_polynom = model_polynom * fill_factor / fill_factor_reference
151+
152+
mismatch = model_polynom(rmad)
153+
if isinstance(rmad, pd.Series):
154+
mismatch = pd.Series(mismatch, index=rmad.index)
155+
return mismatch
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pvlib import bifacial
2+
3+
import pandas as pd
4+
import numpy as np
5+
from numpy.testing import assert_allclose
6+
7+
8+
def test_power_mismatch_deline():
9+
"""tests bifacial.power_mismatch_deline"""
10+
premise_rmads = np.array([0.0, 0.05, 0.1, 0.15, 0.2, 0.25])
11+
# test default model is for fixed tilt
12+
expected_ft_mms = np.array([0.0, 0.0151, 0.0462, 0.0933, 0.1564, 0.2355])
13+
result_def_mms = bifacial.power_mismatch_deline(premise_rmads)
14+
assert_allclose(result_def_mms, expected_ft_mms, atol=1e-5)
15+
assert np.all(np.diff(result_def_mms) > 0) # higher RMADs => higher losses
16+
17+
# test custom coefficients, set model to 1+1*RMAD
18+
# as Polynomial class
19+
polynomial = np.polynomial.Polynomial([1, 1, 0])
20+
result_custom_mms = bifacial.power_mismatch_deline(
21+
premise_rmads, coefficients=polynomial
22+
)
23+
assert_allclose(result_custom_mms, 1 + premise_rmads)
24+
# as list
25+
result_custom_mms = bifacial.power_mismatch_deline(
26+
premise_rmads, coefficients=[1, 1, 0]
27+
)
28+
assert_allclose(result_custom_mms, 1 + premise_rmads)
29+
30+
# test datatypes IO with Series
31+
result_mms = bifacial.power_mismatch_deline(pd.Series(premise_rmads))
32+
assert isinstance(result_mms, pd.Series)
33+
34+
# test fill_factor, fill_factor_reference
35+
# default model + default fill_factor_reference
36+
ff_ref_default = 0.79
37+
ff_of_interest = 0.65
38+
result_mms = bifacial.power_mismatch_deline(
39+
premise_rmads, fill_factor=ff_of_interest
40+
)
41+
assert_allclose(
42+
result_mms,
43+
expected_ft_mms * ff_of_interest / ff_ref_default,
44+
atol=1e-5,
45+
)
46+
# default model + custom fill_factor_reference
47+
ff_of_interest = 0.65
48+
ff_ref = 0.75
49+
result_mms = bifacial.power_mismatch_deline(
50+
premise_rmads, fill_factor=ff_of_interest, fill_factor_reference=ff_ref
51+
)
52+
assert_allclose(
53+
result_mms, expected_ft_mms * ff_of_interest / ff_ref, atol=1e-5
54+
)

0 commit comments

Comments
 (0)