Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3363ace
Add polo-smm
jesuspolo Jun 25, 2025
ceba758
Apply suggestions from code review
jesuspolo Jun 25, 2025
36c9b43
Update mismatch.py
jesuspolo Jun 25, 2025
eaa90b4
Update mismatch.py
jesuspolo Jun 25, 2025
794ed98
Update mismatch.py
jesuspolo Jun 25, 2025
7926a3d
Update mismatch.py
jesuspolo Jun 25, 2025
988557c
Update mismatch.py
jesuspolo Jul 11, 2025
45e768f
Update mismatch.py
jesuspolo Jul 28, 2025
38e5694
Update pvlib/spectrum/mismatch.py
jesuspolo Jul 28, 2025
c24e013
Update pvlib/spectrum/mismatch.py
jesuspolo Jul 28, 2025
0a00f1b
Changes from code review
AdamRJensen Oct 25, 2025
2a9d0db
Add function to spectrum/__init__.py
AdamRJensen Oct 25, 2025
f296c7b
Update test_mismatch.py
jesuspolo Oct 27, 2025
ce862d8
Update test_mismatch.py
jesuspolo Oct 27, 2025
11336ab
Fix module import in tests
AdamRJensen Oct 27, 2025
e418218
Update test_mismatch.py
jesuspolo Oct 27, 2025
fb0ad0e
Merge branch 'polo-smm' of https://github.com/jesuspolo/pvlib-python …
jesuspolo Oct 27, 2025
450dd37
Fix linter
AdamRJensen Oct 27, 2025
1eb3293
Remove tab character
AdamRJensen Oct 27, 2025
4892e8d
Update test_mismatch.py
jesuspolo Oct 27, 2025
7513f3e
Update test_mismatch.py
jesuspolo Oct 27, 2025
3e4dd56
Update test_mismatch.py
jesuspolo Oct 27, 2025
b9d21ac
fix line legth linter error
AdamRJensen Oct 28, 2025
49c8c91
Update test_mismatch.py
jesuspolo Oct 28, 2025
fa3ff32
Merge branch 'polo-smm' of https://github.com/jesuspolo/pvlib-python …
jesuspolo Oct 28, 2025
31fe508
Update mismatch.py
jesuspolo Oct 29, 2025
c958abf
Update mismatch.py
jesuspolo Nov 14, 2025
bd1ff4f
Apply suggestions from code review
kandersolar Nov 19, 2025
f0a4f34
replace `altitude` parameter with `pressure`
kandersolar Nov 19, 2025
ee7f13f
`albedo : numeric`
kandersolar Nov 19, 2025
82598ec
make albedo coefficients user-specified as well
kandersolar Nov 19, 2025
8b2b7ca
complete tests
kandersolar Nov 19, 2025
31f26b5
docstring cleanup
kandersolar Nov 19, 2025
f4e07de
Merge remote-tracking branch 'upstream/main' into pr/2491
kandersolar Nov 19, 2025
5ec4bee
whatsnew note and contributors
kandersolar Nov 19, 2025
d0efc37
add to user guide SMM model comparison table
kandersolar Nov 19, 2025
297f160
lint
kandersolar Nov 19, 2025
c9b9816
minor doc fixes
kandersolar Nov 19, 2025
3df13ae
Update mismatch.py
jesuspolo Nov 21, 2025
3015b3a
Update mismatch.py
jesuspolo Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 20 additions & 7 deletions docs/sphinx/source/user_guide/modeling_topics/spectrum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,21 @@ Reference [2]_.
| +-----------------------------+ | ✓ | ✓ | | | + [4]_ |
| | clearsky_index | | | | | | | |
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
| :py:func:`Polo <spectral_factor_polo>` | :term:`precipitable_water`, | | | | | | | |
| +-----------------------------+ ✓ | | ✓ | ✓ | ✓ | + [5]_ |
| | :term:`airmass_absolute`, | | | | | | | |
| +-----------------------------+ | | | | | | |
| | aod500, | | | | | | | |
| +-----------------------------+ | | | | | | |
| | :term:`aoi`, | | | | | | | |
| +-----------------------------+ | | | | | | |
| | :term:`pressure` | | | | | | | |
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
| :py:func:`PVSPEC <spectral_factor_pvspec>` | :term:`airmass_absolute`, | | | | | | | |
| +-----------------------------+ ✓ | ✓ | ✓ | ✓ | ✓ | | [5]_ |
| +-----------------------------+ ✓ | ✓ | ✓ | ✓ | ✓ | | [6]_ |
| | clearsky_index | | | | | | | |
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+
| :py:func:`SAPM <spectral_factor_sapm>` | :term:`airmass_absolute` | | | | | | | [6]_ |
| :py:func:`SAPM <spectral_factor_sapm>` | :term:`airmass_absolute` | | | | | | | [7]_ |
+-----------------------------------------------------+-----------------------------+---------+---------+------+------+------+------------+-----------+


Expand All @@ -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.
8 changes: 8 additions & 0 deletions docs/sphinx/source/whatsnew/v0.13.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~
Expand Down Expand Up @@ -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`)
5 changes: 3 additions & 2 deletions pvlib/spectrum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
92 changes: 92 additions & 0 deletions pvlib/spectrum/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the paper I read:

"...being the angle of incidence corresponding to 90° of tilt angle and the surface azimuth."

Does that really mean the same as this comment in the code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the same to me. What difference do you see @adriesse?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an awkward wording so I was just looking for confirmation from @jesuspolo

[degrees]
pressure : numeric
Atmospheric pressure. See :term:`pressure`. [Pa]
module_type : str, optional
One of the following PV technology strings from [1]_:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the SR curves used to develop this model open data?


* ``'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
85 changes: 85 additions & 0 deletions tests/spectrum/test_mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading