diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 3197882d93..a143c1ad11 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -16,7 +16,10 @@ Enhancements * Add new parameters for min/max absolute air mass to :py:func:`pvlib.spectrum.spectral_factor_firstsolar`. (:issue:`2086`, :pull:`2100`) - +* Restructured the pvlib/spectrum folder by breaking up the contents of + pvlib/spectrum/mismatch.py into pvlib/spectrum/mismatch.py, + pvlib/spectrum/irradiance.py, and + pvlib/spectrum/response.py. (:issue:`2125`, :pull:`2136`) * Added function for calculating wind speed at different heights, :py:func:`pvlib.atmosphere.windspeed_powerlaw`. (:issue:`2118`, :pull:`2124`) diff --git a/pvlib/spectrum/__init__.py b/pvlib/spectrum/__init__.py index b95f221066..87deb86018 100644 --- a/pvlib/spectrum/__init__.py +++ b/pvlib/spectrum/__init__.py @@ -1,14 +1,18 @@ from pvlib.spectrum.spectrl2 import spectrl2 # noqa: F401 from pvlib.spectrum.mismatch import ( # noqa: F401 calc_spectral_mismatch_field, - get_am15g, - get_reference_spectra, - get_example_spectral_response, spectral_factor_caballero, spectral_factor_firstsolar, spectral_factor_sapm, spectral_factor_pvspec, spectral_factor_jrc, +) +from pvlib.spectrum.irradiance import ( # noqa: F401 + get_am15g, + get_reference_spectra, +) +from pvlib.spectrum.response import ( # noqa: F401 + get_example_spectral_response, sr_to_qe, - qe_to_sr + qe_to_sr, ) diff --git a/pvlib/spectrum/irradiance.py b/pvlib/spectrum/irradiance.py new file mode 100644 index 0000000000..45846a0046 --- /dev/null +++ b/pvlib/spectrum/irradiance.py @@ -0,0 +1,178 @@ +""" +The ``irradiance`` module in the ``spectrum`` package provides functions for +calculations related to spectral irradiance data. +""" + +import pvlib +from pvlib._deprecation import deprecated +import numpy as np +import pandas as pd +from pathlib import Path +from functools import partial + + +@deprecated( + since="0.11", + removal="0.12", + name="pvlib.spectrum.get_am15g", + alternative="pvlib.spectrum.get_reference_spectra", + addendum=( + "The new function reads more data. Use it with " + + "standard='ASTM G173-03' and extract the 'global' column." + ), +) +def get_am15g(wavelength=None): + r""" + Read the ASTM G173-03 AM1.5 global spectrum on a 37-degree tilted surface, + optionally interpolated to the specified wavelength(s). + + Global (tilted) irradiance includes direct and diffuse irradiance from sky + and ground reflections, and is more formally called hemispherical + irradiance (on a tilted surface). In the context of photovoltaic systems + the irradiance on a flat receiver is frequently called plane-of-array (POA) + irradiance. + + Parameters + ---------- + wavelength: 1-D sequence of numeric, optional + Wavelengths at which the spectrum is interpolated. + By default the 2002 wavelengths of the standard are returned. [nm]. + + Returns + ------- + am15g: pandas.Series + The AM1.5g standard spectrum indexed by ``wavelength``. [W/(m²nm)]. + + Notes + ----- + If ``wavelength`` is specified this function uses linear interpolation. + + If the values in ``wavelength`` are too widely spaced, the integral of the + spectrum may deviate from the standard value of 1000.37 W/m². + + The values in the data file provided with pvlib-python are copied from an + Excel file distributed by NREL, which is found here: + https://www.nrel.gov/grid/solar-resource/assets/data/astmg173.xls + + More information about reference spectra is found here: + https://www.nrel.gov/grid/solar-resource/spectra-am1.5.html + + See Also + -------- + pvlib.spectrum.get_reference_spectra : reads also the direct and + extraterrestrial components of the spectrum. + + References + ---------- + .. [1] ASTM "G173-03 Standard Tables for Reference Solar Spectral + Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface." + """ # noqa: E501 + # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 + # modified by @echedey-ls, as a wrapper of spectrum.get_reference_spectra + standard = get_reference_spectra(wavelength, standard="ASTM G173-03") + return standard["global"] + + +def get_reference_spectra(wavelengths=None, standard="ASTM G173-03"): + r""" + Read a standard spectrum specified by ``standard``, optionally + interpolated to the specified wavelength(s). + + Defaults to ``ASTM G173-03`` AM1.5 standard [1]_, which returns + ``extraterrestrial``, ``global`` and ``direct`` spectrum on a 37-degree + tilted surface, optionally interpolated to the specified wavelength(s). + + Parameters + ---------- + wavelengths : numeric, optional + Wavelengths at which the spectrum is interpolated. [nm]. + If not provided, the original wavelengths from the specified standard + are used. Values outside that range are filled with zeros. + + standard : str, default "ASTM G173-03" + The reference standard to be read. Only the reference + ``"ASTM G173-03"`` is available at the moment. + + Returns + ------- + standard_spectra : pandas.DataFrame + The standard spectrum by ``wavelength [nm]``. [W/(m²nm)]. + Column names are ``extraterrestrial``, ``direct`` and ``global``. + + Notes + ----- + If ``wavelength`` is specified, linear interpolation is used. + + If the values in ``wavelength`` are too widely spaced, the integral of each + spectrum may deviate from its standard value. + For global spectra, it is about 1000.37 W/m². + + The values of the ASTM G173-03 provided with pvlib-python are copied from + an Excel file distributed by NREL, which is found here [2]_: + https://www.nrel.gov/grid/solar-resource/assets/data/astmg173.xls + + Examples + -------- + >>> from pvlib import spectrum + >>> am15 = spectrum.get_reference_spectra() + >>> am15_extraterrestrial, am15_global, am15_direct = \ + >>> am15['extraterrestrial'], am15['global'], am15['direct'] + >>> print(am15.head()) + extraterrestrial global direct + wavelength + 280.0 0.082 4.730900e-23 2.536100e-26 + 280.5 0.099 1.230700e-21 1.091700e-24 + 281.0 0.150 5.689500e-21 6.125300e-24 + 281.5 0.212 1.566200e-19 2.747900e-22 + 282.0 0.267 1.194600e-18 2.834600e-21 + + >>> am15 = spectrum.get_reference_spectra([300, 500, 800, 1100]) + >>> print(am15) + extraterrestrial global direct + wavelength + 300 0.45794 0.00102 0.000456 + 500 1.91600 1.54510 1.339100 + 800 1.12480 1.07250 0.988590 + 1100 0.60000 0.48577 0.461130 + + References + ---------- + .. [1] ASTM "G173-03 Standard Tables for Reference Solar Spectral + Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface." + .. [2] “Reference Air Mass 1.5 Spectra,” www.nrel.gov. + https://www.nrel.gov/grid/solar-resource/spectra-am1.5.html + """ # Contributed by Echedey Luis, inspired by Anton Driesse (get_am15g) + SPECTRA_FILES = { + "ASTM G173-03": "ASTMG173.csv", + } + pvlib_datapath = Path(pvlib.__path__[0]) / "data" + + try: + filepath = pvlib_datapath / SPECTRA_FILES[standard] + except KeyError: + raise ValueError( + f"Invalid standard identifier '{standard}'. Available " + + "identifiers are: " + + ", ".join(SPECTRA_FILES.keys()) + ) + + standard = pd.read_csv( + filepath, + header=1, # expect first line of description, then column names + index_col=0, # first column is "wavelength" + dtype=float, + ) + + if wavelengths is not None: + interpolator = partial( + np.interp, xp=standard.index, left=0.0, right=0.0 + ) + standard = pd.DataFrame( + index=wavelengths, + data={ + col: interpolator(x=wavelengths, fp=standard[col]) + for col in standard.columns + }, + ) + + return standard diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index cab6084cac..ab805130d0 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -1,259 +1,14 @@ """ -The ``mismatch`` module provides functions for spectral mismatch calculations. +The ``mismatch`` module in the ``spectrum`` package provides functions for +spectral mismatch calculations. Spectral mismatch models quantify the effect on +a device's photocurrent (or its short-circuit current) of changes in the solar +spectrum due to the atmosphere. """ - import pvlib -from pvlib._deprecation import deprecated -from pvlib.tools import normalize_max2one import numpy as np import pandas as pd -import scipy.constants from scipy.integrate import trapezoid -from scipy.interpolate import interp1d - -from pathlib import Path from warnings import warn -from functools import partial - - -_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. - - Parameters - ---------- - wavelength: 1-D sequence of numeric, optional - Wavelengths at which spectral response values are generated. - By default ``wavelength`` is from 280 to 1200 in 5 nm intervals. [nm] - - Returns - ------- - spectral_response : pandas.Series - The relative spectral response indexed by ``wavelength`` in nm. [-] - - Notes - ----- - This spectral response is based on measurements taken on a c-Si cell. - A small number of points near the measured curve are used to define - a cubic spline having no undue oscillations, as shown in [1]_. The spline - can be interpolated at arbitrary wavelengths to produce a continuous, - smooth curve , which makes it suitable for experimenting with spectral - data of different resolutions. - - References - ---------- - .. [1] Driesse, Anton, and Stein, Joshua. "Global Normal Spectral - Irradiance in Albuquerque: a One-Year Open Dataset for PV Research". - United States 2020. :doi:`10.2172/1814068`. - ''' - # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 - - SR_DATA = np.array([[290, 0.00], - [350, 0.27], - [400, 0.37], - [500, 0.52], - [650, 0.71], - [800, 0.88], - [900, 0.97], - [950, 1.00], - [1000, 0.93], - [1050, 0.58], - [1100, 0.21], - [1150, 0.05], - [1190, 0.00]]).transpose() - - if wavelength is None: - resolution = 5.0 - wavelength = np.arange(280, 1200 + resolution, resolution) - - interpolator = interp1d(SR_DATA[0], SR_DATA[1], - kind='cubic', - bounds_error=False, - fill_value=0.0, - copy=False, - assume_sorted=True) - - sr = pd.Series(data=interpolator(wavelength), index=wavelength) - - sr.index.name = 'wavelength' - sr.name = 'spectral_response' - - return sr - - -@deprecated( - since="0.11", - removal="0.12", - name="pvlib.spectrum.get_am15g", - alternative="pvlib.spectrum.get_reference_spectra", - addendum=( - "The new function reads more data. Use it with " - + "standard='ASTM G173-03' and extract the 'global' column." - ), -) -def get_am15g(wavelength=None): - r""" - Read the ASTM G173-03 AM1.5 global spectrum on a 37-degree tilted surface, - optionally interpolated to the specified wavelength(s). - - Global (tilted) irradiance includes direct and diffuse irradiance from sky - and ground reflections, and is more formally called hemispherical - irradiance (on a tilted surface). In the context of photovoltaic systems - the irradiance on a flat receiver is frequently called plane-of-array (POA) - irradiance. - - Parameters - ---------- - wavelength: 1-D sequence of numeric, optional - Wavelengths at which the spectrum is interpolated. - By default the 2002 wavelengths of the standard are returned. [nm]. - - Returns - ------- - am15g: pandas.Series - The AM1.5g standard spectrum indexed by ``wavelength``. [W/(m²nm)]. - - Notes - ----- - If ``wavelength`` is specified this function uses linear interpolation. - - If the values in ``wavelength`` are too widely spaced, the integral of the - spectrum may deviate from the standard value of 1000.37 W/m². - - The values in the data file provided with pvlib-python are copied from an - Excel file distributed by NREL, which is found here: - https://www.nrel.gov/grid/solar-resource/assets/data/astmg173.xls - - More information about reference spectra is found here: - https://www.nrel.gov/grid/solar-resource/spectra-am1.5.html - - See Also - -------- - pvlib.spectrum.get_reference_spectra : reads also the direct and - extraterrestrial components of the spectrum. - - References - ---------- - .. [1] ASTM "G173-03 Standard Tables for Reference Solar Spectral - Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface." - """ # noqa: E501 - # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 - # modified by @echedey-ls, as a wrapper of spectrum.get_reference_spectra - standard = get_reference_spectra(wavelength, standard="ASTM G173-03") - return standard["global"] - - -def get_reference_spectra(wavelengths=None, standard="ASTM G173-03"): - r""" - Read a standard spectrum specified by ``standard``, optionally - interpolated to the specified wavelength(s). - - Defaults to ``ASTM G173-03`` AM1.5 standard [1]_, which returns - ``extraterrestrial``, ``global`` and ``direct`` spectrum on a 37-degree - tilted surface, optionally interpolated to the specified wavelength(s). - - Parameters - ---------- - wavelengths : numeric, optional - Wavelengths at which the spectrum is interpolated. [nm]. - If not provided, the original wavelengths from the specified standard - are used. Values outside that range are filled with zeros. - - standard : str, default "ASTM G173-03" - The reference standard to be read. Only the reference - ``"ASTM G173-03"`` is available at the moment. - - Returns - ------- - standard_spectra : pandas.DataFrame - The standard spectrum by ``wavelength [nm]``. [W/(m²nm)]. - Column names are ``extraterrestrial``, ``direct`` and ``global``. - - Notes - ----- - If ``wavelength`` is specified, linear interpolation is used. - - If the values in ``wavelength`` are too widely spaced, the integral of each - spectrum may deviate from its standard value. - For global spectra, it is about 1000.37 W/m². - - The values of the ASTM G173-03 provided with pvlib-python are copied from - an Excel file distributed by NREL, which is found here [2]_: - https://www.nrel.gov/grid/solar-resource/assets/data/astmg173.xls - - Examples - -------- - >>> from pvlib import spectrum - >>> am15 = spectrum.get_reference_spectra() - >>> am15_extraterrestrial, am15_global, am15_direct = \ - >>> am15['extraterrestrial'], am15['global'], am15['direct'] - >>> print(am15.head()) - extraterrestrial global direct - wavelength - 280.0 0.082 4.730900e-23 2.536100e-26 - 280.5 0.099 1.230700e-21 1.091700e-24 - 281.0 0.150 5.689500e-21 6.125300e-24 - 281.5 0.212 1.566200e-19 2.747900e-22 - 282.0 0.267 1.194600e-18 2.834600e-21 - - >>> am15 = spectrum.get_reference_spectra([300, 500, 800, 1100]) - >>> print(am15) - extraterrestrial global direct - wavelength - 300 0.45794 0.00102 0.000456 - 500 1.91600 1.54510 1.339100 - 800 1.12480 1.07250 0.988590 - 1100 0.60000 0.48577 0.461130 - - References - ---------- - .. [1] ASTM "G173-03 Standard Tables for Reference Solar Spectral - Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface." - .. [2] “Reference Air Mass 1.5 Spectra,” www.nrel.gov. - https://www.nrel.gov/grid/solar-resource/spectra-am1.5.html - """ # Contributed by Echedey Luis, inspired by Anton Driesse (get_am15g) - SPECTRA_FILES = { - "ASTM G173-03": "ASTMG173.csv", - } - pvlib_datapath = Path(pvlib.__path__[0]) / "data" - - try: - filepath = pvlib_datapath / SPECTRA_FILES[standard] - except KeyError: - raise ValueError( - f"Invalid standard identifier '{standard}'. Available " - + "identifiers are: " - + ", ".join(SPECTRA_FILES.keys()) - ) - - standard = pd.read_csv( - filepath, - header=1, # expect first line of description, then column names - index_col=0, # first column is "wavelength" - dtype=float, - ) - - if wavelengths is not None: - interpolator = partial( - np.interp, xp=standard.index, left=0.0, right=0.0 - ) - standard = pd.DataFrame( - index=wavelengths, - data={ - col: interpolator(x=wavelengths, fp=standard[col]) - for col in standard.columns - }, - ) - - return standard def calc_spectral_mismatch_field(sr, e_sun, e_ref=None): @@ -331,7 +86,8 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None): # get the reference spectrum at wavelengths matching the measured spectra if e_ref is None: - e_ref = get_reference_spectra(wavelengths=e_sun.T.index)["global"] + e_ref = pvlib.spectrum.get_reference_spectra( + wavelengths=e_sun.T.index)["global"] # interpolate the sr at the wavelengths of the spectra # reference spectrum wavelengths may differ if e_ref is from caller @@ -403,7 +159,7 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, * ``'cdte'`` - First Solar Series 4-2 CdTe module. * ``'monosi'``, ``'xsi'`` - First Solar TetraSun module. * ``'multisi'``, ``'polysi'`` - anonymous multi-crystalline silicon - module. + module. * ``'cigs'`` - anonymous copper indium gallium selenide module. * ``'asi'`` - anonymous amorphous silicon module. @@ -953,201 +709,3 @@ def spectral_factor_jrc(airmass, clearsky_index, module_type=None, + coeff[2] * (airmass - 1.5) ) return mismatch - - -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 diff --git a/pvlib/spectrum/response.py b/pvlib/spectrum/response.py new file mode 100644 index 0000000000..4da92bb32a --- /dev/null +++ b/pvlib/spectrum/response.py @@ -0,0 +1,280 @@ +""" +The ``response`` module in the ``spectrum`` package provides functions for +spectral response and quantum efficiency calculations. +""" +from pvlib.tools import normalize_max2one +import numpy as np +import pandas as pd +import scipy.constants +from scipy.interpolate import interp1d + + +_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. + + Parameters + ---------- + wavelength: 1-D sequence of numeric, optional + Wavelengths at which spectral response values are generated. + By default ``wavelength`` is from 280 to 1200 in 5 nm intervals. [nm] + + Returns + ------- + spectral_response : pandas.Series + The relative spectral response indexed by ``wavelength`` in nm. [-] + + Notes + ----- + This spectral response is based on measurements taken on a c-Si cell. + A small number of points near the measured curve are used to define + a cubic spline having no undue oscillations, as shown in [1]_. The spline + can be interpolated at arbitrary wavelengths to produce a continuous, + smooth curve , which makes it suitable for experimenting with spectral + data of different resolutions. + + References + ---------- + .. [1] Driesse, Anton, and Stein, Joshua. "Global Normal Spectral + Irradiance in Albuquerque: a One-Year Open Dataset for PV Research". + United States 2020. :doi:`10.2172/1814068`. + ''' + # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 + + SR_DATA = np.array([[290, 0.00], + [350, 0.27], + [400, 0.37], + [500, 0.52], + [650, 0.71], + [800, 0.88], + [900, 0.97], + [950, 1.00], + [1000, 0.93], + [1050, 0.58], + [1100, 0.21], + [1150, 0.05], + [1190, 0.00]]).transpose() + + if wavelength is None: + resolution = 5.0 + wavelength = np.arange(280, 1200 + resolution, resolution) + + interpolator = interp1d(SR_DATA[0], SR_DATA[1], + kind='cubic', + bounds_error=False, + fill_value=0.0, + copy=False, + assume_sorted=True) + + sr = pd.Series(data=interpolator(wavelength), index=wavelength) + + sr.index.name = 'wavelength' + sr.name = 'spectral_response' + + return sr + + +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