Skip to content

Commit cd9cb9a

Browse files
RDaxinikandersolarcwhanseAdamRJensen
authored
Create function to calculate average photon energy (#2140)
* create * Update spectrum.rst * Update test_spectrum.py * Update mismatch.py update docstring * Update mismatch.py * Update mismatch.py fix docs typesetting * Update mismatch.py * docs, tests * docs, data checks, tests add checks in function for negative irradiance and invalid data type add tests for these checks update docs maths typesetting * Update mismatch.py still trying to typeset the units without using / / between multiple length units * Update mismatch.py trying unicode superscript * Apply suggestions from code review Co-authored-by: Kevin Anderson <[email protected]> * remove comments, inverse fraction * remove comment * Update irradiance.py * Update test_irradiance.py * Update test_irradiance.py * Update irradiance.py update returns statement, change symbols, suppress runtimewarning for division by zero Co-Authored-By: Kevin Anderson <[email protected]> * mixed up my lambdas and gammas:( * change variable name spectral_irr -> spectrum * update variable name in docstring + error messages * Update irradiance.py * Update test_irradiance.py add test for return np.nan in case of zero si input * Apply suggestions from code review Co-authored-by: Cliff Hansen <[email protected]> * Update irradiance.py * Update v0.11.1.rst * Update irradiance.py apple suggestions from code review Co-Authored-By: Adam R. Jensen <[email protected]> * Update irradiance.py "spectrum" variable --> "spectra" (see #2150) * Update test_irradiance.py "spectrum" variable --> "spectra" (see #2150) * Update test_mismatch.py remove unintentional change * Update v0.11.1.rst apply suggestion from code review Co-Authored-By: Kevin Anderson <[email protected]> * Update irradiance.py sentence correction Co-Authored-By: Kevin Anderson <[email protected]> * update output datatype and associated tests return series in case of dataframe input, add test to verify. Remove unnecessary spaces from tests * Update test_irradiance.py update test for datatype according to suggestion from code review * Update docs/sphinx/source/whatsnew/v0.11.1.rst Co-authored-by: Kevin Anderson <[email protected]> --------- Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: Adam R. Jensen <[email protected]>
1 parent 5e43be7 commit cd9cb9a

File tree

6 files changed

+164
-0
lines changed

6 files changed

+164
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ Spectrum
1818
spectrum.spectral_factor_jrc
1919
spectrum.sr_to_qe
2020
spectrum.qe_to_sr
21+
spectrum.average_photon_energy

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Deprecations
1010

1111
Enhancements
1212
~~~~~~~~~~~~
13+
* Add new function to calculate the average photon energy,
14+
:py:func:`pvlib.spectrum.average_photon_energy`.
15+
(:issue:`2135`, :pull:`2140`)
1316
* Add new losses function that accounts for non-uniform irradiance on bifacial
1417
modules, :py:func:`pvlib.bifacial.power_mismatch_deline`.
1518
(:issue:`2045`, :pull:`2046`)

pvlib/spectrum/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pvlib.spectrum.irradiance import ( # noqa: F401
1111
get_am15g,
1212
get_reference_spectra,
13+
average_photon_energy,
1314
)
1415
from pvlib.spectrum.response import ( # noqa: F401
1516
get_example_spectral_response,

pvlib/spectrum/irradiance.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import pandas as pd
1010
from pathlib import Path
1111
from functools import partial
12+
from scipy import constants
13+
from scipy.integrate import trapezoid
1214

1315

1416
@deprecated(
@@ -176,3 +178,95 @@ def get_reference_spectra(wavelengths=None, standard="ASTM G173-03"):
176178
)
177179

178180
return standard
181+
182+
183+
def average_photon_energy(spectra):
184+
r"""
185+
Calculate the average photon energy of one or more spectral irradiance
186+
distributions.
187+
188+
Parameters
189+
----------
190+
spectra : pandas.Series or pandas.DataFrame
191+
192+
Spectral irradiance, must be positive. [Wm⁻²nm⁻¹]
193+
194+
A single spectrum must be a :py:class:`pandas.Series` with wavelength
195+
[nm] as the index, while multiple spectra must be rows in a
196+
:py:class:`pandas.DataFrame` with column headers as wavelength [nm].
197+
198+
Returns
199+
-------
200+
ape : numeric or pandas.Series
201+
Average Photon Energy [eV].
202+
Note: returns ``np.nan`` in the case of all-zero spectral irradiance
203+
input.
204+
205+
Notes
206+
-----
207+
The average photon energy (APE) is an index used to characterise the solar
208+
spectrum. It has been used widely in the physics literature since the
209+
1900s, but its application for solar spectral irradiance characterisation
210+
in the context of PV performance modelling was proposed in 2002 [1]_. The
211+
APE is calculated based on the principle that a photon's energy is
212+
inversely proportional to its wavelength:
213+
214+
.. math::
215+
216+
E_\gamma = \frac{hc}{\lambda},
217+
218+
where :math:`E_\gamma` is the energy of a photon with wavelength
219+
:math:`\lambda`, :math:`h` is the Planck constant, and :math:`c` is the
220+
speed of light. Therefore, the average energy of all photons within a
221+
single spectral irradiance distribution provides an indication of the
222+
general shape of the spectrum. A higher average photon energy
223+
(shorter wavelength) indicates a blue-shifted spectrum, while a lower
224+
average photon energy (longer wavelength) would indicate a red-shifted
225+
spectrum. This value of the average photon energy can be calculated by
226+
dividing the total energy in the spectrum by the total number of photons
227+
in the spectrum as follows [1]_:
228+
229+
.. math::
230+
231+
\overline{E_\gamma} = \frac{1}{q} \cdot \frac{\int G(\lambda) \,
232+
d\lambda}
233+
{\int \Phi(\lambda) \, d\lambda}.
234+
235+
:math:`\Phi(\lambda)` is the photon flux density as a function of
236+
wavelength, :math:`G(\lambda)` is the spectral irradiance, :math:`q` is the
237+
elementary charge used here so that the average photon energy,
238+
:math:`\overline{E_\gamma}`, is expressed in electronvolts (eV). The
239+
integrals are computed over the full wavelength range of the ``spectra``
240+
parameter.
241+
242+
References
243+
----------
244+
.. [1] Jardine, C., et al., 2002, January. Influence of spectral effects on
245+
the performance of multijunction amorphous silicon cells. In Proc.
246+
Photovoltaic in Europe Conference (pp. 1756-1759).
247+
"""
248+
249+
if not isinstance(spectra, (pd.Series, pd.DataFrame)):
250+
raise TypeError('`spectra` must be either a'
251+
' pandas Series or DataFrame')
252+
253+
if (spectra < 0).any().any():
254+
raise ValueError('Spectral irradiance data must be positive')
255+
256+
hclambda = pd.Series((constants.h*constants.c)/(spectra.T.index*1e-9))
257+
hclambda.index = spectra.T.index
258+
pfd = spectra.div(hclambda)
259+
260+
def integrate(e):
261+
return trapezoid(e, x=e.T.index, axis=-1)
262+
263+
int_spectra = integrate(spectra)
264+
int_pfd = integrate(pfd)
265+
266+
with np.errstate(invalid='ignore'):
267+
ape = (1/constants.elementary_charge)*int_spectra/int_pfd
268+
269+
if isinstance(spectra, pd.DataFrame):
270+
ape = pd.Series(ape, index=spectra.index)
271+
272+
return ape

pvlib/spectrum/mismatch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy as np
99
import pandas as pd
1010
from scipy.integrate import trapezoid
11+
1112
from warnings import warn
1213

1314

pvlib/tests/spectrum/test_irradiance.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,67 @@ def test_get_reference_spectra_invalid_reference():
7272
# test that an invalid reference identifier raises a ValueError
7373
with pytest.raises(ValueError, match="Invalid standard identifier"):
7474
spectrum.get_reference_spectra(standard="invalid")
75+
76+
77+
def test_average_photon_energy_series():
78+
# test that the APE is calculated correctly with single spectrum
79+
# series input
80+
81+
spectra = spectrum.get_reference_spectra()
82+
spectra = spectra['global']
83+
ape = spectrum.average_photon_energy(spectra)
84+
expected = 1.45017
85+
assert_allclose(ape, expected, rtol=1e-4)
86+
87+
88+
def test_average_photon_energy_dataframe():
89+
# test that the APE is calculated correctly with multiple spectra
90+
# dataframe input and that the output is a series
91+
92+
spectra = spectrum.get_reference_spectra().T
93+
ape = spectrum.average_photon_energy(spectra)
94+
expected = pd.Series([1.36848, 1.45017, 1.40885])
95+
expected.index = spectra.index
96+
assert_series_equal(ape, expected, rtol=1e-4)
97+
98+
99+
def test_average_photon_energy_invalid_type():
100+
# test that spectrum argument is either a pandas Series or dataframe
101+
spectra = 5
102+
with pytest.raises(TypeError, match='must be either a pandas Series or'
103+
' DataFrame'):
104+
spectrum.average_photon_energy(spectra)
105+
106+
107+
def test_average_photon_energy_neg_irr_series():
108+
# test for handling of negative spectral irradiance values with a
109+
# pandas Series input
110+
111+
spectra = spectrum.get_reference_spectra()['global']*-1
112+
with pytest.raises(ValueError, match='must be positive'):
113+
spectrum.average_photon_energy(spectra)
114+
115+
116+
def test_average_photon_energy_neg_irr_dataframe():
117+
# test for handling of negative spectral irradiance values with a
118+
# pandas DataFrame input
119+
120+
spectra = spectrum.get_reference_spectra().T*-1
121+
122+
with pytest.raises(ValueError, match='must be positive'):
123+
spectrum.average_photon_energy(spectra)
124+
125+
126+
def test_average_photon_energy_zero_irr():
127+
# test for handling of zero spectral irradiance values with
128+
# pandas DataFrame and pandas Series input
129+
130+
spectra_df_zero = spectrum.get_reference_spectra().T
131+
spectra_df_zero.iloc[1] = 0
132+
spectra_series_zero = spectrum.get_reference_spectra()['global']*0
133+
out_1 = spectrum.average_photon_energy(spectra_df_zero)
134+
out_2 = spectrum.average_photon_energy(spectra_series_zero)
135+
expected_1 = np.array([1.36848, np.nan, 1.40885])
136+
expected_2 = np.nan
137+
assert_allclose(out_1, expected_1, atol=1e-3)
138+
assert_allclose(out_2, expected_2, atol=1e-3)

0 commit comments

Comments
 (0)