Skip to content

Quantum efficiency & spectral response conversion funcs #2041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fdc65f0
Add quantum_efficiency_to_spectral_responsivity and spectral_responsi…
echedey-ls Mar 24, 2024
7c45240
Merge branch 'main' into quantum-efficiency-&-spectral-response-conve…
echedey-ls Mar 24, 2024
52a4752
Update v0.10.5.rst
echedey-ls Mar 24, 2024
8fcabde
Add docstring examples
echedey-ls Apr 11, 2024
69f5e0d
Merge branch 'main' into quantum-efficiency-&-spectral-response-conve…
echedey-ls May 3, 2024
7865c03
Merge branch 'main' into quantum-efficiency-&-spectral-response-conve…
echedey-ls May 6, 2024
1b5be03
Update v0.11.0.rst
echedey-ls May 6, 2024
3631830
Linter
echedey-ls May 6, 2024
75a27d6
More linter xD
echedey-ls May 6, 2024
b057d28
Remove reference links from first sentence
echedey-ls May 6, 2024
27f160e
remove old whatsme entries
echedey-ls May 10, 2024
d7d9b81
rename funcs and change versionadded's
echedey-ls May 10, 2024
0b5ad4b
Update v0.10.5.rst
echedey-ls May 10, 2024
28ebce8
This is test driven development
echedey-ls May 21, 2024
9bfa003
apply normalization to functions
echedey-ls May 21, 2024
7b254a4
Merge branch 'main' into quantum-efficiency-&-spectral-response-conve…
echedey-ls May 23, 2024
78cbc4c
Merge branch 'main' into quantum-efficiency-&-spectral-response-conve…
echedey-ls May 24, 2024
4d6b440
Update test_spectrum.py
echedey-ls May 24, 2024
0b00fb7
Update test_tools.py
echedey-ls May 24, 2024
8c30356
Units formatting
echedey-ls May 24, 2024
606e9b6
Links
echedey-ls May 24, 2024
83d875e
I'm obsessed with math mode
echedey-ls May 25, 2024
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 @@ -13,3 +13,5 @@ Spectrum
spectrum.spectral_factor_caballero
spectrum.spectral_factor_firstsolar
spectrum.spectral_factor_sapm
spectrum.sr_to_qe
spectrum.qe_to_sr
5 changes: 5 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Enhancements
shade perpendicular to ``axis_azimuth``. The function is applicable to both
fixed-tilt and one-axis tracking systems.
(:issue:`1689`, :pull:`1725`, :pull:`1962`)
* Added conversion functions from spectral response ([A/W]) to quantum
efficiency ([unitless]) and vice versa. The conversion functions are
:py:func:`pvlib.spectrum.sr_to_qe` and :py:func:`pvlib.spectrum.qe_to_sr`
respectively. (:issue:`2040`, :pull:`2041`)


Bug fixes
Expand All @@ -42,3 +46,4 @@ Contributors
* Cliff Hansen (:ghuser:`cwhanse`)
* Mark Mikofski (:ghuser:`mikofski`)
* Siddharth Kaul (:ghuser:`k10blogger`)
* Mark Campanelli (:ghuser:`markcampanelli`)
2 changes: 2 additions & 0 deletions pvlib/spectrum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@
spectral_factor_caballero,
spectral_factor_firstsolar,
spectral_factor_sapm,
sr_to_qe,
qe_to_sr,
)
212 changes: 210 additions & 2 deletions pvlib/spectrum/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
"""

import pvlib
from pvlib.tools import normalize_max2one
import numpy as np
import pandas as pd
from scipy.interpolate import interp1d
import scipy.constants
from scipy.integrate import trapezoid
from scipy.interpolate import interp1d
import os

from warnings import warn


_PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION = (
scipy.constants.speed_of_light
* scipy.constants.Planck
/ scipy.constants.elementary_charge
* 1e9
)


def get_example_spectral_response(wavelength=None):
'''
Generate a generic smooth spectral response (SR) for tests and experiments.
Expand Down Expand Up @@ -154,7 +164,7 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None):

e_sun: pandas.DataFrame or pandas.Series
One or more measured solar irradiance spectra in a pandas.DataFrame
having wavelength in nm as column index. A single spectrum may be
having wavelength in nm as column index. A single spectrum may be
be given as a pandas.Series having wavelength in nm as index.
[(W/m^2)/nm]

Expand Down Expand Up @@ -571,3 +581,201 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500,
)
modifier = f_AM + f_AOD + f_PW # Eq 5
return modifier


def sr_to_qe(sr, wavelength=None, normalize=False):
"""
Convert spectral responsivities to quantum efficiencies.
If ``wavelength`` is not provided, the spectral responsivity ``sr`` must be
a :py:class:`pandas.Series` or :py:class:`pandas.DataFrame`, with the
wavelengths in the index.

Provide wavelengths in nanometers, [nm].

Conversion is described in [1]_.

.. versionadded:: 0.11.0

Parameters
----------
sr : numeric, pandas.Series or pandas.DataFrame
Spectral response, [A/W].
Index must be the wavelength in nanometers, [nm].

wavelength : numeric, optional
Points where spectral response is measured, in nanometers, [nm].

normalize : bool, default False
If True, the quantum efficiency is normalized so that the maximum value
is 1.
For ``pandas.DataFrame``, normalization is done for each column.
For 2D arrays, normalization is done for each sub-array.

Returns
-------
quantum_efficiency : numeric, same type as ``sr``
Quantum efficiency, in the interval [0, 1].

Notes
-----
- If ``sr`` is of type ``pandas.Series`` or ``pandas.DataFrame``,
column names will remain unchanged in the returned object.
- If ``wavelength`` is provided it will be used independently of the
datatype of ``sr``.

Examples
--------
>>> import numpy as np
>>> import pandas as pd
>>> from pvlib import spectrum
>>> wavelengths = np.array([350, 550, 750])
>>> spectral_response = np.array([0.25, 0.40, 0.57])
>>> quantum_efficiency = spectrum.sr_to_qe(spectral_response, wavelengths)
>>> print(quantum_efficiency)
array([0.88560142, 0.90170326, 0.94227991])

>>> spectral_response_series = pd.Series(spectral_response, index=wavelengths, name="dataset")
>>> qe = spectrum.sr_to_qe(spectral_response_series)
>>> print(qe)
350 0.885601
550 0.901703
750 0.942280
Name: dataset, dtype: float64

>>> qe = spectrum.sr_to_qe(spectral_response_series, normalize=True)
>>> print(qe)
350 0.939850
550 0.956938
750 1.000000
Name: dataset, dtype: float64

References
----------
.. [1] “Spectral Response,” PV Performance Modeling Collaborative (PVPMC).
https://pvpmc.sandia.gov/modeling-guide/2-dc-module-iv/effective-irradiance/spectral-response/
.. [2] “Spectral Response | PVEducation,” www.pveducation.org.
https://www.pveducation.org/pvcdrom/solar-cell-operation/spectral-response

See Also
--------
pvlib.spectrum.qe_to_sr
""" # noqa: E501
if wavelength is None:
if hasattr(sr, "index"): # true for pandas objects
# use reference to index values instead of index alone so
# sr / wavelength returns a series with the same name
wavelength = sr.index.array
else:
raise TypeError(
"'sr' must have an '.index' attribute"
+ " or 'wavelength' must be provided"
)
quantum_efficiency = (
sr
/ wavelength
* _PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION
)

if normalize:
quantum_efficiency = normalize_max2one(quantum_efficiency)

return quantum_efficiency


def qe_to_sr(qe, wavelength=None, normalize=False):
"""
Convert quantum efficiencies to spectral responsivities.
If ``wavelength`` is not provided, the quantum efficiency ``qe`` must be
a :py:class:`pandas.Series` or :py:class:`pandas.DataFrame`, with the
wavelengths in the index.

Provide wavelengths in nanometers, [nm].

Conversion is described in [1]_.

.. versionadded:: 0.11.0

Parameters
----------
qe : numeric, pandas.Series or pandas.DataFrame
Quantum efficiency.
If pandas subtype, index must be the wavelength in nanometers, [nm].

wavelength : numeric, optional
Points where quantum efficiency is measured, in nanometers, [nm].

normalize : bool, default False
If True, the spectral response is normalized so that the maximum value
is 1.
For ``pandas.DataFrame``, normalization is done for each column.
For 2D arrays, normalization is done for each sub-array.

Returns
-------
spectral_response : numeric, same type as ``qe``
Spectral response, [A/W].

Notes
-----
- If ``qe`` is of type ``pandas.Series`` or ``pandas.DataFrame``,
column names will remain unchanged in the returned object.
- If ``wavelength`` is provided it will be used independently of the
datatype of ``qe``.

Examples
--------
>>> import numpy as np
>>> import pandas as pd
>>> from pvlib import spectrum
>>> wavelengths = np.array([350, 550, 750])
>>> quantum_efficiency = np.array([0.86, 0.90, 0.94])
>>> spectral_response = spectrum.qe_to_sr(quantum_efficiency, wavelengths)
>>> print(spectral_response)
array([0.24277287, 0.39924442, 0.56862085])

>>> quantum_efficiency_series = pd.Series(quantum_efficiency, index=wavelengths, name="dataset")
>>> sr = spectrum.qe_to_sr(quantum_efficiency_series)
>>> print(sr)
350 0.242773
550 0.399244
750 0.568621
Name: dataset, dtype: float64

>>> sr = spectrum.qe_to_sr(quantum_efficiency_series, normalize=True)
>>> print(sr)
350 0.426950
550 0.702128
750 1.000000
Name: dataset, dtype: float64

References
----------
.. [1] “Spectral Response,” PV Performance Modeling Collaborative (PVPMC).
https://pvpmc.sandia.gov/modeling-guide/2-dc-module-iv/effective-irradiance/spectral-response/
.. [2] “Spectral Response | PVEducation,” www.pveducation.org.
https://www.pveducation.org/pvcdrom/solar-cell-operation/spectral-response

See Also
--------
pvlib.spectrum.sr_to_qe
""" # noqa: E501
if wavelength is None:
if hasattr(qe, "index"): # true for pandas objects
# use reference to index values instead of index alone so
# sr / wavelength returns a series with the same name
wavelength = qe.index.array
else:
raise TypeError(
"'qe' must have an '.index' attribute"
+ " or 'wavelength' must be provided"
)
spectral_responsivity = (
qe
* wavelength
/ _PLANCK_BY_LIGHT_SPEED_OVER_ELEMENTAL_CHARGE_BY_BILLION
)

if normalize:
spectral_responsivity = normalize_max2one(spectral_responsivity)

return spectral_responsivity
106 changes: 105 additions & 1 deletion pvlib/tests/test_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def test_get_am15g():

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

# sample data
Expand Down Expand Up @@ -315,3 +315,107 @@ def test_spectral_factor_caballero_supplied_ambiguous():
with pytest.raises(ValueError):
spectrum.spectral_factor_caballero(1, 1, 1, module_type=None,
coefficients=None)


@pytest.fixture
def sr_and_eqe_fixture():
# Just some arbitrary data for testing the conversion functions
df = pd.DataFrame(
columns=("wavelength", "quantum_efficiency", "spectral_response"),
data=[
# nm, [0,1], A/W
[300, 0.85, 0.205671370402405],
[350, 0.86, 0.242772872514211],
[400, 0.87, 0.280680929019753],
[450, 0.88, 0.319395539919029],
[500, 0.89, 0.358916705212040],
[550, 0.90, 0.399244424898786],
[600, 0.91, 0.440378698979267],
[650, 0.92, 0.482319527453483],
[700, 0.93, 0.525066910321434],
[750, 0.94, 0.568620847583119],
[800, 0.95, 0.612981339238540],
[850, 0.90, 0.617014111207215],
[900, 0.80, 0.580719163489143],
[950, 0.70, 0.536358671833723],
[1000, 0.6, 0.483932636240953],
[1050, 0.4, 0.338752845368667],
],
)
df.set_index("wavelength", inplace=True)
return df


def test_sr_to_qe(sr_and_eqe_fixture):
# vector type
qe = spectrum.sr_to_qe(
sr_and_eqe_fixture["spectral_response"].values,
sr_and_eqe_fixture.index.values, # wavelength, nm
)
assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])
# pandas series type
# note: output Series' name should match the input
qe = spectrum.sr_to_qe(
sr_and_eqe_fixture["spectral_response"]
)
pd.testing.assert_series_equal(
qe, sr_and_eqe_fixture["quantum_efficiency"],
check_names=False
)
assert qe.name == "spectral_response"
# series normalization
qe = spectrum.sr_to_qe(
sr_and_eqe_fixture["spectral_response"] * 10, normalize=True
)
pd.testing.assert_series_equal(
qe,
sr_and_eqe_fixture["quantum_efficiency"]
/ max(sr_and_eqe_fixture["quantum_efficiency"]),
check_names=False,
)
# error on lack of wavelength parameter if no pandas object is provided
with pytest.raises(TypeError, match="must have an '.index' attribute"):
_ = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"].values)


def test_qe_to_sr(sr_and_eqe_fixture):
# vector type
sr = spectrum.qe_to_sr(
sr_and_eqe_fixture["quantum_efficiency"].values,
sr_and_eqe_fixture.index.values, # wavelength, nm
)
assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
# pandas series type
# note: output Series' name should match the input
sr = spectrum.qe_to_sr(
sr_and_eqe_fixture["quantum_efficiency"]
)
pd.testing.assert_series_equal(
sr, sr_and_eqe_fixture["spectral_response"],
check_names=False
)
assert sr.name == "quantum_efficiency"
# series normalization
sr = spectrum.qe_to_sr(
sr_and_eqe_fixture["quantum_efficiency"] * 10, normalize=True
)
pd.testing.assert_series_equal(
sr,
sr_and_eqe_fixture["spectral_response"]
/ max(sr_and_eqe_fixture["spectral_response"]),
check_names=False,
)
# error on lack of wavelength parameter if no pandas object is provided
with pytest.raises(TypeError, match="must have an '.index' attribute"):
_ = spectrum.qe_to_sr(
sr_and_eqe_fixture["quantum_efficiency"].values
)


def test_qe_and_sr_reciprocal_conversion(sr_and_eqe_fixture):
# test that the conversion functions are reciprocal
qe = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"])
sr = spectrum.qe_to_sr(qe)
assert_allclose(sr, sr_and_eqe_fixture["spectral_response"])
qe = spectrum.sr_to_qe(sr)
assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"])
Loading
Loading