Skip to content

Commit 555b29a

Browse files
echedey-lskandersolarAdamRJensen
authored
Quantum efficiency & spectral response conversion funcs (#2041)
* Add quantum_efficiency_to_spectral_responsivity and spectral_responsivity_to_quantum_efficiency * Update v0.10.5.rst * Add docstring examples * Update v0.11.0.rst * Linter * More linter xD * Remove reference links from first sentence Since it messes the links in the toc table. * remove old whatsme entries Co-Authored-By: Kevin Anderson <[email protected]> * rename funcs and change versionadded's Co-Authored-By: Kevin Anderson <[email protected]> * Update v0.10.5.rst Co-Authored-By: Kevin Anderson <[email protected]> * This is test driven development * apply normalization to functions * Update test_spectrum.py Co-Authored-By: Kevin Anderson <[email protected]> * Update test_tools.py Co-Authored-By: Kevin Anderson <[email protected]> * Units formatting Co-Authored-By: Kevin Anderson <[email protected]> Co-Authored-By: Adam R. Jensen <[email protected]> * Links Co-Authored-By: Kevin Anderson <[email protected]> * I'm obsessed with math mode --------- Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Adam R. Jensen <[email protected]>
1 parent 17aed3a commit 555b29a

File tree

7 files changed

+375
-3
lines changed

7 files changed

+375
-3
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ Spectrum
1313
spectrum.spectral_factor_caballero
1414
spectrum.spectral_factor_firstsolar
1515
spectrum.spectral_factor_sapm
16+
spectrum.sr_to_qe
17+
spectrum.qe_to_sr

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Enhancements
2828
shade perpendicular to ``axis_azimuth``. The function is applicable to both
2929
fixed-tilt and one-axis tracking systems.
3030
(:issue:`1689`, :pull:`1725`, :pull:`1962`)
31+
* Added conversion functions from spectral response ([A/W]) to quantum
32+
efficiency ([unitless]) and vice versa. The conversion functions are
33+
:py:func:`pvlib.spectrum.sr_to_qe` and :py:func:`pvlib.spectrum.qe_to_sr`
34+
respectively. (:issue:`2040`, :pull:`2041`)
3135

3236

3337
Bug fixes
@@ -51,3 +55,4 @@ Contributors
5155
* Cliff Hansen (:ghuser:`cwhanse`)
5256
* Mark Mikofski (:ghuser:`mikofski`)
5357
* Siddharth Kaul (:ghuser:`k10blogger`)
58+
* Mark Campanelli (:ghuser:`markcampanelli`)

pvlib/spectrum/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
spectral_factor_caballero,
77
spectral_factor_firstsolar,
88
spectral_factor_sapm,
9+
sr_to_qe,
10+
qe_to_sr,
911
)

pvlib/spectrum/mismatch.py

Lines changed: 210 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@
33
"""
44

55
import pvlib
6+
from pvlib.tools import normalize_max2one
67
import numpy as np
78
import pandas as pd
8-
from scipy.interpolate import interp1d
9+
import scipy.constants
910
from scipy.integrate import trapezoid
11+
from scipy.interpolate import interp1d
1012
import os
1113

1214
from warnings import warn
1315

1416

17+
_PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION = (
18+
scipy.constants.speed_of_light
19+
* scipy.constants.Planck
20+
/ scipy.constants.elementary_charge
21+
* 1e9
22+
)
23+
24+
1525
def get_example_spectral_response(wavelength=None):
1626
'''
1727
Generate a generic smooth spectral response (SR) for tests and experiments.
@@ -154,7 +164,7 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None):
154164
155165
e_sun: pandas.DataFrame or pandas.Series
156166
One or more measured solar irradiance spectra in a pandas.DataFrame
157-
having wavelength in nm as column index. A single spectrum may be
167+
having wavelength in nm as column index. A single spectrum may be
158168
be given as a pandas.Series having wavelength in nm as index.
159169
[(W/m^2)/nm]
160170
@@ -571,3 +581,201 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500,
571581
)
572582
modifier = f_AM + f_AOD + f_PW # Eq 5
573583
return modifier
584+
585+
586+
def sr_to_qe(sr, wavelength=None, normalize=False):
587+
"""
588+
Convert spectral responsivities to quantum efficiencies.
589+
If ``wavelength`` is not provided, the spectral responsivity ``sr`` must be
590+
a :py:class:`pandas.Series` or :py:class:`pandas.DataFrame`, with the
591+
wavelengths in the index.
592+
593+
Provide wavelengths in nanometers, [nm].
594+
595+
Conversion is described in [1]_.
596+
597+
.. versionadded:: 0.11.0
598+
599+
Parameters
600+
----------
601+
sr : numeric, pandas.Series or pandas.DataFrame
602+
Spectral response, [A/W].
603+
Index must be the wavelength in nanometers, [nm].
604+
605+
wavelength : numeric, optional
606+
Points where spectral response is measured, in nanometers, [nm].
607+
608+
normalize : bool, default False
609+
If True, the quantum efficiency is normalized so that the maximum value
610+
is 1.
611+
For ``pandas.DataFrame``, normalization is done for each column.
612+
For 2D arrays, normalization is done for each sub-array.
613+
614+
Returns
615+
-------
616+
quantum_efficiency : numeric, same type as ``sr``
617+
Quantum efficiency, in the interval [0, 1].
618+
619+
Notes
620+
-----
621+
- If ``sr`` is of type ``pandas.Series`` or ``pandas.DataFrame``,
622+
column names will remain unchanged in the returned object.
623+
- If ``wavelength`` is provided it will be used independently of the
624+
datatype of ``sr``.
625+
626+
Examples
627+
--------
628+
>>> import numpy as np
629+
>>> import pandas as pd
630+
>>> from pvlib import spectrum
631+
>>> wavelengths = np.array([350, 550, 750])
632+
>>> spectral_response = np.array([0.25, 0.40, 0.57])
633+
>>> quantum_efficiency = spectrum.sr_to_qe(spectral_response, wavelengths)
634+
>>> print(quantum_efficiency)
635+
array([0.88560142, 0.90170326, 0.94227991])
636+
637+
>>> spectral_response_series = pd.Series(spectral_response, index=wavelengths, name="dataset")
638+
>>> qe = spectrum.sr_to_qe(spectral_response_series)
639+
>>> print(qe)
640+
350 0.885601
641+
550 0.901703
642+
750 0.942280
643+
Name: dataset, dtype: float64
644+
645+
>>> qe = spectrum.sr_to_qe(spectral_response_series, normalize=True)
646+
>>> print(qe)
647+
350 0.939850
648+
550 0.956938
649+
750 1.000000
650+
Name: dataset, dtype: float64
651+
652+
References
653+
----------
654+
.. [1] “Spectral Response,” PV Performance Modeling Collaborative (PVPMC).
655+
https://pvpmc.sandia.gov/modeling-guide/2-dc-module-iv/effective-irradiance/spectral-response/
656+
.. [2] “Spectral Response | PVEducation,” www.pveducation.org.
657+
https://www.pveducation.org/pvcdrom/solar-cell-operation/spectral-response
658+
659+
See Also
660+
--------
661+
pvlib.spectrum.qe_to_sr
662+
""" # noqa: E501
663+
if wavelength is None:
664+
if hasattr(sr, "index"): # true for pandas objects
665+
# use reference to index values instead of index alone so
666+
# sr / wavelength returns a series with the same name
667+
wavelength = sr.index.array
668+
else:
669+
raise TypeError(
670+
"'sr' must have an '.index' attribute"
671+
+ " or 'wavelength' must be provided"
672+
)
673+
quantum_efficiency = (
674+
sr
675+
/ wavelength
676+
* _PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION
677+
)
678+
679+
if normalize:
680+
quantum_efficiency = normalize_max2one(quantum_efficiency)
681+
682+
return quantum_efficiency
683+
684+
685+
def qe_to_sr(qe, wavelength=None, normalize=False):
686+
"""
687+
Convert quantum efficiencies to spectral responsivities.
688+
If ``wavelength`` is not provided, the quantum efficiency ``qe`` must be
689+
a :py:class:`pandas.Series` or :py:class:`pandas.DataFrame`, with the
690+
wavelengths in the index.
691+
692+
Provide wavelengths in nanometers, [nm].
693+
694+
Conversion is described in [1]_.
695+
696+
.. versionadded:: 0.11.0
697+
698+
Parameters
699+
----------
700+
qe : numeric, pandas.Series or pandas.DataFrame
701+
Quantum efficiency.
702+
If pandas subtype, index must be the wavelength in nanometers, [nm].
703+
704+
wavelength : numeric, optional
705+
Points where quantum efficiency is measured, in nanometers, [nm].
706+
707+
normalize : bool, default False
708+
If True, the spectral response is normalized so that the maximum value
709+
is 1.
710+
For ``pandas.DataFrame``, normalization is done for each column.
711+
For 2D arrays, normalization is done for each sub-array.
712+
713+
Returns
714+
-------
715+
spectral_response : numeric, same type as ``qe``
716+
Spectral response, [A/W].
717+
718+
Notes
719+
-----
720+
- If ``qe`` is of type ``pandas.Series`` or ``pandas.DataFrame``,
721+
column names will remain unchanged in the returned object.
722+
- If ``wavelength`` is provided it will be used independently of the
723+
datatype of ``qe``.
724+
725+
Examples
726+
--------
727+
>>> import numpy as np
728+
>>> import pandas as pd
729+
>>> from pvlib import spectrum
730+
>>> wavelengths = np.array([350, 550, 750])
731+
>>> quantum_efficiency = np.array([0.86, 0.90, 0.94])
732+
>>> spectral_response = spectrum.qe_to_sr(quantum_efficiency, wavelengths)
733+
>>> print(spectral_response)
734+
array([0.24277287, 0.39924442, 0.56862085])
735+
736+
>>> quantum_efficiency_series = pd.Series(quantum_efficiency, index=wavelengths, name="dataset")
737+
>>> sr = spectrum.qe_to_sr(quantum_efficiency_series)
738+
>>> print(sr)
739+
350 0.242773
740+
550 0.399244
741+
750 0.568621
742+
Name: dataset, dtype: float64
743+
744+
>>> sr = spectrum.qe_to_sr(quantum_efficiency_series, normalize=True)
745+
>>> print(sr)
746+
350 0.426950
747+
550 0.702128
748+
750 1.000000
749+
Name: dataset, dtype: float64
750+
751+
References
752+
----------
753+
.. [1] “Spectral Response,” PV Performance Modeling Collaborative (PVPMC).
754+
https://pvpmc.sandia.gov/modeling-guide/2-dc-module-iv/effective-irradiance/spectral-response/
755+
.. [2] “Spectral Response | PVEducation,” www.pveducation.org.
756+
https://www.pveducation.org/pvcdrom/solar-cell-operation/spectral-response
757+
758+
See Also
759+
--------
760+
pvlib.spectrum.sr_to_qe
761+
""" # noqa: E501
762+
if wavelength is None:
763+
if hasattr(qe, "index"): # true for pandas objects
764+
# use reference to index values instead of index alone so
765+
# sr / wavelength returns a series with the same name
766+
wavelength = qe.index.array
767+
else:
768+
raise TypeError(
769+
"'qe' must have an '.index' attribute"
770+
+ " or 'wavelength' must be provided"
771+
)
772+
spectral_responsivity = (
773+
qe
774+
* wavelength
775+
/ _PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION
776+
)
777+
778+
if normalize:
779+
spectral_responsivity = normalize_max2one(spectral_responsivity)
780+
781+
return spectral_responsivity

pvlib/tests/test_spectrum.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def test_get_am15g():
140140

141141
def test_calc_spectral_mismatch_field(spectrl2_data):
142142
# test that the mismatch is calculated correctly with
143-
# - default and custom reference sepctrum
143+
# - default and custom reference spectrum
144144
# - single or multiple sun spectra
145145

146146
# sample data
@@ -315,3 +315,107 @@ def test_spectral_factor_caballero_supplied_ambiguous():
315315
with pytest.raises(ValueError):
316316
spectrum.spectral_factor_caballero(1, 1, 1, module_type=None,
317317
coefficients=None)
318+
319+
320+
@pytest.fixture
321+
def sr_and_eqe_fixture():
322+
# Just some arbitrary data for testing the conversion functions
323+
df = pd.DataFrame(
324+
columns=("wavelength", "quantum_efficiency", "spectral_response"),
325+
data=[
326+
# nm, [0,1], A/W
327+
[300, 0.85, 0.205671370402405],
328+
[350, 0.86, 0.242772872514211],
329+
[400, 0.87, 0.280680929019753],
330+
[450, 0.88, 0.319395539919029],
331+
[500, 0.89, 0.358916705212040],
332+
[550, 0.90, 0.399244424898786],
333+
[600, 0.91, 0.440378698979267],
334+
[650, 0.92, 0.482319527453483],
335+
[700, 0.93, 0.525066910321434],
336+
[750, 0.94, 0.568620847583119],
337+
[800, 0.95, 0.612981339238540],
338+
[850, 0.90, 0.617014111207215],
339+
[900, 0.80, 0.580719163489143],
340+
[950, 0.70, 0.536358671833723],
341+
[1000, 0.6, 0.483932636240953],
342+
[1050, 0.4, 0.338752845368667],
343+
],
344+
)
345+
df.set_index("wavelength", inplace=True)
346+
return df
347+
348+
349+
def test_sr_to_qe(sr_and_eqe_fixture):
350+
# vector type
351+
qe = spectrum.sr_to_qe(
352+
sr_and_eqe_fixture["spectral_response"].values,
353+
sr_and_eqe_fixture.index.values, # wavelength, nm
354+
)
355+
assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])
356+
# pandas series type
357+
# note: output Series' name should match the input
358+
qe = spectrum.sr_to_qe(
359+
sr_and_eqe_fixture["spectral_response"]
360+
)
361+
pd.testing.assert_series_equal(
362+
qe, sr_and_eqe_fixture["quantum_efficiency"],
363+
check_names=False
364+
)
365+
assert qe.name == "spectral_response"
366+
# series normalization
367+
qe = spectrum.sr_to_qe(
368+
sr_and_eqe_fixture["spectral_response"] * 10, normalize=True
369+
)
370+
pd.testing.assert_series_equal(
371+
qe,
372+
sr_and_eqe_fixture["quantum_efficiency"]
373+
/ max(sr_and_eqe_fixture["quantum_efficiency"]),
374+
check_names=False,
375+
)
376+
# error on lack of wavelength parameter if no pandas object is provided
377+
with pytest.raises(TypeError, match="must have an '.index' attribute"):
378+
_ = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"].values)
379+
380+
381+
def test_qe_to_sr(sr_and_eqe_fixture):
382+
# vector type
383+
sr = spectrum.qe_to_sr(
384+
sr_and_eqe_fixture["quantum_efficiency"].values,
385+
sr_and_eqe_fixture.index.values, # wavelength, nm
386+
)
387+
assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
388+
# pandas series type
389+
# note: output Series' name should match the input
390+
sr = spectrum.qe_to_sr(
391+
sr_and_eqe_fixture["quantum_efficiency"]
392+
)
393+
pd.testing.assert_series_equal(
394+
sr, sr_and_eqe_fixture["spectral_response"],
395+
check_names=False
396+
)
397+
assert sr.name == "quantum_efficiency"
398+
# series normalization
399+
sr = spectrum.qe_to_sr(
400+
sr_and_eqe_fixture["quantum_efficiency"] * 10, normalize=True
401+
)
402+
pd.testing.assert_series_equal(
403+
sr,
404+
sr_and_eqe_fixture["spectral_response"]
405+
/ max(sr_and_eqe_fixture["spectral_response"]),
406+
check_names=False,
407+
)
408+
# error on lack of wavelength parameter if no pandas object is provided
409+
with pytest.raises(TypeError, match="must have an '.index' attribute"):
410+
_ = spectrum.qe_to_sr(
411+
sr_and_eqe_fixture["quantum_efficiency"].values
412+
)
413+
414+
415+
def test_qe_and_sr_reciprocal_conversion(sr_and_eqe_fixture):
416+
# test that the conversion functions are reciprocal
417+
qe = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"])
418+
sr = spectrum.qe_to_sr(qe)
419+
assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
420+
qe = spectrum.sr_to_qe(sr)
421+
assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])

0 commit comments

Comments
 (0)