Skip to content

Commit b1f48bb

Browse files
jesuspoloAdamRJensenechedey-lskandersolar
authored
Add polo spectral factor model (#2491)
* Add polo-smm Add the polo spectral model for BIPV facades * Apply suggestions from code review Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * Update mismatch.py * Update mismatch.py * Update mismatch.py * Update mismatch.py * Update mismatch.py * Update mismatch.py * Update pvlib/spectrum/mismatch.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * Update pvlib/spectrum/mismatch.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * Changes from code review * Add function to spectrum/__init__.py * Update test_mismatch.py * Update test_mismatch.py * Fix module import in tests * Update test_mismatch.py * Fix linter * Remove tab character * Update test_mismatch.py * Update test_mismatch.py * fix line legth linter error * Update test_mismatch.py * Update mismatch.py * Update mismatch.py * Apply suggestions from code review * replace `altitude` parameter with `pressure` * `albedo : numeric` * make albedo coefficients user-specified as well * complete tests * docstring cleanup * whatsnew note and contributors * add to user guide SMM model comparison table * lint * minor doc fixes * Update mismatch.py * Update mismatch.py * clip AOI to max of 90 degrees as discussed in review --------- Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com>
1 parent 9e267ca commit b1f48bb

File tree

6 files changed

+221
-11
lines changed

6 files changed

+221
-11
lines changed

docs/sphinx/source/reference/effects_on_pv_system_output/spectrum.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ Spectrum
1212
spectrum.calc_spectral_mismatch_field
1313
spectrum.spectral_factor_caballero
1414
spectrum.spectral_factor_firstsolar
15-
spectrum.spectral_factor_sapm
16-
spectrum.spectral_factor_pvspec
1715
spectrum.spectral_factor_jrc
16+
spectrum.spectral_factor_polo
17+
spectrum.spectral_factor_pvspec
18+
spectrum.spectral_factor_sapm
1819
spectrum.sr_to_qe
1920
spectrum.qe_to_sr
2021
spectrum.average_photon_energy

docs/sphinx/source/user_guide/modeling_topics/spectrum.rst

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,21 @@ Reference [2]_.
6161
| +-----------------------------+ ||| | | + [4]_ |
6262
| | clearsky_index | | | | | | | |
6363
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
64+
| :py:func:`Polo <spectral_factor_polo>` | :term:`precipitable_water`, | | | | | | | |
65+
| +-----------------------------+| |||| + [5]_ |
66+
| | :term:`airmass_absolute`, | | | | | | | |
67+
| +-----------------------------+ | | | | | | |
68+
| | aod500, | | | | | | | |
69+
| +-----------------------------+ | | | | | | |
70+
| | :term:`aoi`, | | | | | | | |
71+
| +-----------------------------+ | | | | | | |
72+
| | :term:`pressure` | | | | | | | |
73+
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
6474
| :py:func:`PVSPEC <spectral_factor_pvspec>` | :term:`airmass_absolute`, | | | | | | | |
65-
| +-----------------------------+||||| | [5]_ |
75+
| +-----------------------------+||||| | [6]_ |
6676
| | clearsky_index | | | | | | | |
6777
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
68-
| :py:func:`SAPM <spectral_factor_sapm>` | :term:`airmass_absolute` | | | | | | | [6]_ |
78+
| :py:func:`SAPM <spectral_factor_sapm>` | :term:`airmass_absolute` | | | | | | | [7]_ |
6979
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
7080

7181

@@ -88,16 +98,19 @@ References
8898
PVSPEC Model of Photovoltaic Spectral Mismatch Factor," in Proc. 2020
8999
IEEE 47th Photovoltaic Specialists Conference (PVSC), Calgary, AB,
90100
Canada, 2020, pp. 1–6. :doi:`10.1109/PVSC45281.2020.9300932`
91-
.. [5] D. L. King, W. E. Boyson, and J. A. Kratochvil, Photovoltaic Array
101+
.. [5] J. Polo and C. Sanz-Saiz, 'Development of spectral mismatch models
102+
for BIPV applications in building façades', Renewable Energy, vol. 245,
103+
p. 122820, Jun. 2025, :doi:`10.1016/j.renene.2025.122820`
104+
.. [6] D. L. King, W. E. Boyson, and J. A. Kratochvil, Photovoltaic Array
92105
Performance Model, Sandia National Laboratories, Albuquerque, NM, USA,
93106
Tech. Rep. SAND2004-3535, Aug. 2004. :doi:`10.2172/919131`
94-
.. [6] M. Lee and A. Panchula, "Spectral Correction for Photovoltaic Module
107+
.. [7] M. Lee and A. Panchula, "Spectral Correction for Photovoltaic Module
95108
Performance Based on Air Mass and Precipitable Water," 2016 IEEE 43rd
96109
Photovoltaic Specialists Conference (PVSC), Portland, OR, USA, 2016,
97110
pp. 3696-3699. :doi:`10.1109/PVSC.2016.7749836`
98-
.. [7] H. Thomas, S. Tony, and D. Ewan, “A Simple Model for Estimating the
99-
Influence of Spectrum Variations on PV Performance, pp. 3385–3389, Nov.
111+
.. [8] T. Huld, T. Sample, and E. Dunlop, "A Simple Model for Estimating the
112+
Influence of Spectrum Variations on PV Performance," pp. 3385–3389, Nov.
100113
2009, :doi:`10.4229/24THEUPVSEC2009-4AV.3.27`
101-
.. [8] IEC 60904-7:2019, Photovoltaic devices — Part 7: Computation of the
114+
.. [9] IEC 60904-7:2019, Photovoltaic devices — Part 7: Computation of the
102115
spectral mismatch correction for measurements of photovoltaic devices,
103116
International Electrotechnical Commission, Geneva, Switzerland, 2019.

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Enhancements
4444
ERA5 reanalysis data. (:pull:`2573`)
4545
* Add :py:func:`~pvlib.iotools.get_merra2`, a function for accessing
4646
MERRA-2 reanalysis data. (:pull:`2572`)
47+
* Add :py:func:`~pvlib.spectrum.spectral_factor_polo`, a function for estimating
48+
spectral mismatch factors for vertical PV façades. (:issue:`2406`, :pull:`2491`)
4749

4850
Documentation
4951
~~~~~~~~~~~~~
@@ -79,4 +81,9 @@ Contributors
7981
* Will Hobbs (:ghuser:`williamhobbs`)
8082
* Cliff Hansen (:ghuser:`cwhanse`)
8183
* Joseph Radford (:ghuser:`josephradford`)
84+
* Jesús Polo (:ghuser:`jesuspolo`)
85+
* Adam R. Jensen (:ghuser:`adamrjensen`)
86+
* Echedey Luis (:ghuser:`echedey-ls`)
87+
* Anton Driesse (:ghuser:`adriesse`)
88+
* Rajiv Daxini (:ghuser:`RDaxini`)
8289
* Kevin Anderson (:ghuser:`kandersolar`)

pvlib/spectrum/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
calc_spectral_mismatch_field,
44
spectral_factor_caballero,
55
spectral_factor_firstsolar,
6-
spectral_factor_sapm,
7-
spectral_factor_pvspec,
86
spectral_factor_jrc,
7+
spectral_factor_polo,
8+
spectral_factor_pvspec,
9+
spectral_factor_sapm,
910
)
1011
from pvlib.spectrum.irradiance import ( # noqa: F401
1112
get_reference_spectra,

pvlib/spectrum/mismatch.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,3 +698,104 @@ def spectral_factor_jrc(airmass, clearsky_index, module_type=None,
698698
+ coeff[2] * (airmass - 1.5)
699699
)
700700
return mismatch
701+
702+
703+
def spectral_factor_polo(precipitable_water, airmass_absolute, aod500, aoi,
704+
pressure, module_type=None, coefficients=None,
705+
albedo=0.2):
706+
"""
707+
Estimate the spectral mismatch for BIPV application in vertical facades.
708+
709+
The model's authors note that this model could also be applied to
710+
vertical bifacial ground-mount systems [1]_, although it has not been
711+
validated in that context.
712+
713+
Parameters
714+
----------
715+
precipitable_water : numeric
716+
Atmospheric precipitable water. [cm]
717+
airmass_absolute : numeric
718+
Absolute (pressure-adjusted) airmass. See :term:`airmass_absolute`.
719+
[unitless]
720+
aod500 : numeric
721+
Atmospheric aerosol optical depth at 500 nm. [unitless]
722+
aoi : numeric
723+
Angle of incidence on the vertical surface. See :term:`aoi`.
724+
[degrees]
725+
pressure : numeric
726+
Atmospheric pressure. See :term:`pressure`. [Pa]
727+
module_type : str, optional
728+
One of the following PV technology strings from [1]_:
729+
730+
* ``'cdte'`` - anonymous CdTe module.
731+
* ``'monosi'`` - anonymous monocrystalline silicon module.
732+
* ``'cigs'`` - anonymous copper indium gallium selenide module.
733+
* ``'asi'`` - anonymous amorphous silicon module.
734+
coefficients : array-like, optional
735+
User-defined coefficients, if not using one of the coefficient
736+
sets via the ``module_type`` parameter. Must have nine elements.
737+
The first six elements correspond to the [p1, p2, p3, p4, b, c]
738+
parameters of the SMM model. The last three elements corresponds
739+
to the [c1, c2, c3] parameters of the albedo correction factor.
740+
albedo : numeric, default 0.2
741+
Ground albedo. See :term:`albedo`. [unitless]
742+
743+
Returns
744+
-------
745+
modifier: numeric
746+
spectral mismatch factor (unitless) which is multiplied
747+
with broadband irradiance reaching a module's cells to estimate
748+
effective irradiance, i.e., the irradiance that is converted to
749+
electrical current.
750+
751+
Notes
752+
-----
753+
The Polo model was developed using only SMM values computed for scenarios
754+
when the sun is visible from the module's surface (i.e., for ``aoi<90``),
755+
and no provision was made in [1]_ for the case of ``aoi>90``. This would
756+
create issues in the air mass calculation internal to the model.
757+
Following discussion with the model's author, the pvlib implementation
758+
handles ``aoi>90`` by truncating the input ``aoi`` to a maximum of
759+
90 degrees.
760+
761+
References
762+
----------
763+
.. [1] J. Polo and C. Sanz-Saiz, 'Development of spectral mismatch models
764+
for BIPV applications in building façades', Renewable Energy, vol. 245,
765+
p. 122820, Jun. 2025, :doi:`10.1016/j.renene.2025.122820`
766+
"""
767+
if module_type is None and coefficients is None:
768+
raise ValueError('Must provide either `module_type` or `coefficients`')
769+
if module_type is not None and coefficients is not None:
770+
raise ValueError('Only one of `module_type` and `coefficients` should '
771+
'be provided')
772+
# prevent nan for aoi greater than 90; see docstring Notes
773+
aoi = np.clip(aoi, a_min=None, a_max=90)
774+
f_aoi_rel = pvlib.atmosphere.get_relative_airmass(aoi,
775+
model='kastenyoung1989')
776+
f_aoi = pvlib.atmosphere.get_absolute_airmass(f_aoi_rel, pressure)
777+
Ram = f_aoi / airmass_absolute
778+
_coefficients = {
779+
'cdte': (-0.0009, 46.80, 49.20, -0.87, 0.00041, 0.053),
780+
'monosi': (0.0027, 10.34, 9.48, 0.31, 0.00077, 0.006),
781+
'cigs': (0.0017, 2.33, 1.30, 0.11, 0.00098, -0.018),
782+
'asi': (0.0024, 7.32, 7.09, -0.72, -0.0013, 0.089),
783+
}
784+
c = {
785+
'asi': (0.0056, -0.020, 1.014),
786+
'cigs': (-0.0009, -0.0003, 1),
787+
'cdte': (0.0021, -0.01, 1.01),
788+
'monosi': (0, -0.003, 1.0),
789+
}
790+
if module_type is not None:
791+
coeff = _coefficients[module_type]
792+
c_albedo = c[module_type]
793+
else:
794+
coeff = coefficients[:6]
795+
c_albedo = coefficients[6:]
796+
smm = coeff[0] * Ram + coeff[1] / (coeff[2] + Ram**coeff[3]) \
797+
+ coeff[4] / aod500 + coeff[5]*np.sqrt(precipitable_water)
798+
# Ground albedo correction
799+
g = c_albedo[0] * (albedo/0.2)**2 \
800+
+ c_albedo[1] * (albedo/0.2) + c_albedo[2]
801+
return g*smm

tests/spectrum/test_mismatch.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,90 @@ def test_spectral_factor_jrc_supplied_ambiguous():
288288
with pytest.raises(ValueError, match='No valid input provided'):
289289
spectrum.spectral_factor_jrc(1.0, 0.8, module_type=None,
290290
coefficients=None)
291+
292+
293+
@pytest.mark.parametrize("module_type,expected", [
294+
('cdte', np.array(
295+
[0.992801, 1.00004, 1.011576, 0.995003, 0.950156, 0.975665])),
296+
('monosi', np.array(
297+
[1.000152, 0.969588, 0.984636, 1.015405, 1.024238, 1.005061])),
298+
('cigs', np.array(
299+
[1.004621, 0.956719, 0.971668, 1.0254, 1.060066, 1.020196])),
300+
('asi', np.array(
301+
[0.986968, 1.049725, 1.051978, 0.957968, 0.842258, 0.941927])),
302+
])
303+
def test_spectral_factor_polo(module_type, expected):
304+
pws = np.array([0.96, 0.96, 1.85, 1.88, 0.66, 0.66])
305+
aods = np.array([0.085, 0.085, 0.16, 0.19, 0.088, 0.088])
306+
ams = np.array([1.34, 1.34, 2.2, 2.2, 2.6, 2.6])
307+
aois = np.array([46.0, 76.0, 74.0, 28.0, 24.0, 55.0])
308+
pressure = np.array([101300, 101400, 100500, 101325, 80000, 120000])
309+
alb = np.array([0.15, 0.2, 0.3, 0.18, 0.32, 0.26])
310+
out = spectrum.spectral_factor_polo(
311+
pws, ams, aods, aois, pressure, module_type=module_type, albedo=alb)
312+
np.testing.assert_allclose(out, expected, atol=1e-6)
313+
314+
315+
@pytest.fixture
316+
def polo_inputs():
317+
return {'precipitable_water': 0.96,
318+
'airmass_absolute': 1.34,
319+
'aod500': 0.085,
320+
'aoi': 76,
321+
'pressure': 101400,
322+
'albedo': 0.2}
323+
324+
325+
def test_spectral_factor_polo_coefficients(polo_inputs):
326+
# test that supplying custom coefficients works as expected
327+
coefficients = (
328+
(0.0027, 10.34, 9.48, 0.31, 0.00077, 0.006) # base Si coeffs
329+
+ (0, -0.003, 1.0) # Si albedo correction coeffs
330+
)
331+
out = spectrum.spectral_factor_polo(**polo_inputs,
332+
coefficients=coefficients)
333+
np.testing.assert_allclose(out, 0.969588, atol=1e-6)
334+
335+
336+
def test_spectral_factor_polo_errors(polo_inputs):
337+
with pytest.raises(ValueError, match='Must provide either'):
338+
spectrum.spectral_factor_polo(**polo_inputs)
339+
with pytest.raises(ValueError, match='Only one of'):
340+
spectrum.spectral_factor_polo(**polo_inputs, module_type='CdTe',
341+
coefficients=(1, 1, 1, 1, 1, 1))
342+
343+
344+
def test_spectral_factor_polo_types(polo_inputs):
345+
# float:
346+
out = spectrum.spectral_factor_polo(**polo_inputs, module_type='monosi')
347+
assert isinstance(out, float)
348+
np.testing.assert_allclose(out, 0.969588, atol=1e-6)
349+
350+
# array:
351+
arrays = {k: np.array([v, v]) for k, v in polo_inputs.items()}
352+
out = spectrum.spectral_factor_polo(**arrays, module_type='monosi')
353+
assert isinstance(out, np.ndarray)
354+
np.testing.assert_allclose(out, [0.969588]*2, atol=1e-6)
355+
356+
# series:
357+
series = {k: pd.Series(v) for k, v in arrays.items()}
358+
out = spectrum.spectral_factor_polo(**series, module_type='monosi')
359+
assert isinstance(out, pd.Series)
360+
pd.testing.assert_series_equal(out, pd.Series([0.969588]*2), atol=1e-6)
361+
362+
363+
def test_spectral_factor_polo_NaN(polo_inputs):
364+
# nan in -> nan out
365+
for key in polo_inputs:
366+
inputs = polo_inputs.copy()
367+
inputs[key] = np.nan
368+
out = spectrum.spectral_factor_polo(**inputs, module_type='monosi')
369+
assert np.isnan(out)
370+
371+
372+
def test_spectral_factor_polo_aoi_gt_90(polo_inputs):
373+
polo_inputs['aoi'] = 95
374+
out95 = spectrum.spectral_factor_polo(**polo_inputs, module_type='monosi')
375+
polo_inputs['aoi'] = 90
376+
out90 = spectrum.spectral_factor_polo(**polo_inputs, module_type='monosi')
377+
assert out95 == out90

0 commit comments

Comments
 (0)