Skip to content

Commit

Permalink
Merge branch 'main' into coerce-and-rotate-pvgis
Browse files Browse the repository at this point in the history
  • Loading branch information
mikofski committed Aug 16, 2024
2 parents ac9dbb0 + 0428fbe commit 9875521
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 0 deletions.
126 changes: 126 additions & 0 deletions docs/examples/irradiance-transposition/use_perez_modelchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Use different Perez coefficients with the ModelChain
====================================================
This example demonstrates how to customize the ModelChain
to use site-specific Perez transposition coefficients.
"""

# %%
# The :py:class:`pvlib.modelchain.ModelChain` object provides a useful method
# for easily constructing a PV system model with a simple, unified interface.
# However, a user may want to customize the steps
# in the system model in various ways.
# One such example is during the irradiance transposition step.
# The Perez model perform very well on field data, but
# it requires a set of fitted coefficients from various sites.
# It has been noted that these coefficients can be specific to
# various climates, so users may see improved model accuracy
# when using a site-specific set of coefficients.
# However, the base :py:class:`~pvlib.modelchain.ModelChain`
# only supports the default coefficients.
# This example shows how the :py:class:`~pvlib.modelchain.ModelChain` can
# be adjusted to use a different set of Perez coefficients.

import pandas as pd
from pvlib.pvsystem import PVSystem
from pvlib.modelchain import ModelChain
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
from pvlib import iotools, location, irradiance
import pvlib
import os
import matplotlib.pyplot as plt

# load in TMY weather data from North Carolina included with pvlib
PVLIB_DIR = pvlib.__path__[0]
DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV')

tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990,
map_variables=True)

weather_data = tmy[['ghi', 'dhi', 'dni', 'temp_air', 'wind_speed']]

loc = location.Location.from_tmy(metadata)

#%%
# Now, let's set up a standard PV model using the ``ModelChain``

surface_tilt = metadata['latitude']
surface_azimuth = 180

# define an example module and inverter
sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')
cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']

temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']

# define the system and ModelChain
system = PVSystem(arrays=None,
surface_tilt=surface_tilt,
surface_azimuth=surface_azimuth,
module_parameters=sandia_module,
inverter_parameters=cec_inverter,
temperature_model_parameters=temp_params)

mc = ModelChain(system, location=loc)

# %%
# Now, let's calculate POA irradiance values outside of the ``ModelChain``.
# We do this for both the default Perez coefficients and the desired
# alternative Perez coefficients. This enables comparison at the end.

# Cape Canaveral seems like the most likely match for climate
model_perez = 'capecanaveral1988'

solar_position = loc.get_solarposition(times=weather_data.index)
dni_extra = irradiance.get_extra_radiation(weather_data.index)

POA_irradiance = irradiance.get_total_irradiance(
surface_tilt=surface_tilt,
surface_azimuth=surface_azimuth,
dni=weather_data['dni'],
ghi=weather_data['ghi'],
dhi=weather_data['dhi'],
solar_zenith=solar_position['apparent_zenith'],
solar_azimuth=solar_position['azimuth'],
model='perez',
dni_extra=dni_extra)

POA_irradiance_new_perez = irradiance.get_total_irradiance(
surface_tilt=surface_tilt,
surface_azimuth=surface_azimuth,
dni=weather_data['dni'],
ghi=weather_data['ghi'],
dhi=weather_data['dhi'],
solar_zenith=solar_position['apparent_zenith'],
solar_azimuth=solar_position['azimuth'],
model='perez',
model_perez=model_perez,
dni_extra=dni_extra)

# %%
# Now, run the ``ModelChain`` with both sets of irradiance data and compare
# (note that to use POA irradiance as input to the ModelChain the method
# `.run_model_from_poa` is used):

mc.run_model_from_poa(POA_irradiance)
ac_power_default = mc.results.ac

mc.run_model_from_poa(POA_irradiance_new_perez)
ac_power_new_perez = mc.results.ac

start, stop = '1990-05-05 06:00:00', '1990-05-05 19:00:00'
plt.plot(ac_power_default.loc[start:stop],
label="Default Composite Perez Model")
plt.plot(ac_power_new_perez.loc[start:stop],
label="Cape Canaveral Perez Model")
plt.xticks(rotation=90)
plt.ylabel("AC Power ($W$)")
plt.legend()
plt.tight_layout()
plt.show()
# %%
# Note that there is a small, but noticeable difference from the default
# coefficients that may add up over longer periods of time.
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ Spectrum
spectrum.spectral_factor_jrc
spectrum.sr_to_qe
spectrum.qe_to_sr
spectrum.average_photon_energy
8 changes: 8 additions & 0 deletions docs/sphinx/source/whatsnew/v0.11.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Deprecations

Enhancements
~~~~~~~~~~~~
* Add new function to calculate the average photon energy,
:py:func:`pvlib.spectrum.average_photon_energy`.
(:issue:`2135`, :pull:`2140`)
* Add new losses function that accounts for non-uniform irradiance on bifacial
modules, :py:func:`pvlib.bifacial.power_mismatch_deline`.
(:issue:`2045`, :pull:`2046`)
Expand Down Expand Up @@ -45,6 +48,10 @@ Documentation
* Added gallery example on calculating cell temperature for
floating PV. (:pull:`2110`)

* Added gallery example demonstrating how to use
different Perez coefficients in a ModelChain.
(:issue:`2127`, :pull:`2148`)

* Removed unused "times" input from dni_et() function (:issue:`2105`)

Requirements
Expand All @@ -60,4 +67,5 @@ Contributors
* Echedey Luis (:ghuser:`echedey-ls`)
* Rajiv Daxini (:ghuser:`RDaxini`)
* Mark A. Mikofski (:ghuser:`mikofski`)
* Ben Pierce (:ghuser:`bgpierc`)
* Jose Meza (:ghuser:`JoseMezaMendieta`)
1 change: 1 addition & 0 deletions pvlib/spectrum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pvlib.spectrum.irradiance import ( # noqa: F401
get_am15g,
get_reference_spectra,
average_photon_energy,
)
from pvlib.spectrum.response import ( # noqa: F401
get_example_spectral_response,
Expand Down
94 changes: 94 additions & 0 deletions pvlib/spectrum/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import pandas as pd
from pathlib import Path
from functools import partial
from scipy import constants
from scipy.integrate import trapezoid


@deprecated(
Expand Down Expand Up @@ -176,3 +178,95 @@ def get_reference_spectra(wavelengths=None, standard="ASTM G173-03"):
)

return standard


def average_photon_energy(spectra):
r"""
Calculate the average photon energy of one or more spectral irradiance
distributions.
Parameters
----------
spectra : pandas.Series or pandas.DataFrame
Spectral irradiance, must be positive. [Wm⁻²nm⁻¹]
A single spectrum must be a :py:class:`pandas.Series` with wavelength
[nm] as the index, while multiple spectra must be rows in a
:py:class:`pandas.DataFrame` with column headers as wavelength [nm].
Returns
-------
ape : numeric or pandas.Series
Average Photon Energy [eV].
Note: returns ``np.nan`` in the case of all-zero spectral irradiance
input.
Notes
-----
The average photon energy (APE) is an index used to characterise the solar
spectrum. It has been used widely in the physics literature since the
1900s, but its application for solar spectral irradiance characterisation
in the context of PV performance modelling was proposed in 2002 [1]_. The
APE is calculated based on the principle that a photon's energy is
inversely proportional to its wavelength:
.. math::
E_\gamma = \frac{hc}{\lambda},
where :math:`E_\gamma` is the energy of a photon with wavelength
:math:`\lambda`, :math:`h` is the Planck constant, and :math:`c` is the
speed of light. Therefore, the average energy of all photons within a
single spectral irradiance distribution provides an indication of the
general shape of the spectrum. A higher average photon energy
(shorter wavelength) indicates a blue-shifted spectrum, while a lower
average photon energy (longer wavelength) would indicate a red-shifted
spectrum. This value of the average photon energy can be calculated by
dividing the total energy in the spectrum by the total number of photons
in the spectrum as follows [1]_:
.. math::
\overline{E_\gamma} = \frac{1}{q} \cdot \frac{\int G(\lambda) \,
d\lambda}
{\int \Phi(\lambda) \, d\lambda}.
:math:`\Phi(\lambda)` is the photon flux density as a function of
wavelength, :math:`G(\lambda)` is the spectral irradiance, :math:`q` is the
elementary charge used here so that the average photon energy,
:math:`\overline{E_\gamma}`, is expressed in electronvolts (eV). The
integrals are computed over the full wavelength range of the ``spectra``
parameter.
References
----------
.. [1] Jardine, C., et al., 2002, January. Influence of spectral effects on
the performance of multijunction amorphous silicon cells. In Proc.
Photovoltaic in Europe Conference (pp. 1756-1759).
"""

if not isinstance(spectra, (pd.Series, pd.DataFrame)):
raise TypeError('`spectra` must be either a'
' pandas Series or DataFrame')

if (spectra < 0).any().any():
raise ValueError('Spectral irradiance data must be positive')

hclambda = pd.Series((constants.h*constants.c)/(spectra.T.index*1e-9))
hclambda.index = spectra.T.index
pfd = spectra.div(hclambda)

def integrate(e):
return trapezoid(e, x=e.T.index, axis=-1)

int_spectra = integrate(spectra)
int_pfd = integrate(pfd)

with np.errstate(invalid='ignore'):
ape = (1/constants.elementary_charge)*int_spectra/int_pfd

if isinstance(spectra, pd.DataFrame):
ape = pd.Series(ape, index=spectra.index)

return ape
1 change: 1 addition & 0 deletions pvlib/spectrum/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
import pandas as pd
from scipy.integrate import trapezoid

from warnings import warn


Expand Down
64 changes: 64 additions & 0 deletions pvlib/tests/spectrum/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,67 @@ def test_get_reference_spectra_invalid_reference():
# test that an invalid reference identifier raises a ValueError
with pytest.raises(ValueError, match="Invalid standard identifier"):
spectrum.get_reference_spectra(standard="invalid")


def test_average_photon_energy_series():
# test that the APE is calculated correctly with single spectrum
# series input

spectra = spectrum.get_reference_spectra()
spectra = spectra['global']
ape = spectrum.average_photon_energy(spectra)
expected = 1.45017
assert_allclose(ape, expected, rtol=1e-4)


def test_average_photon_energy_dataframe():
# test that the APE is calculated correctly with multiple spectra
# dataframe input and that the output is a series

spectra = spectrum.get_reference_spectra().T
ape = spectrum.average_photon_energy(spectra)
expected = pd.Series([1.36848, 1.45017, 1.40885])
expected.index = spectra.index
assert_series_equal(ape, expected, rtol=1e-4)


def test_average_photon_energy_invalid_type():
# test that spectrum argument is either a pandas Series or dataframe
spectra = 5
with pytest.raises(TypeError, match='must be either a pandas Series or'
' DataFrame'):
spectrum.average_photon_energy(spectra)


def test_average_photon_energy_neg_irr_series():
# test for handling of negative spectral irradiance values with a
# pandas Series input

spectra = spectrum.get_reference_spectra()['global']*-1
with pytest.raises(ValueError, match='must be positive'):
spectrum.average_photon_energy(spectra)


def test_average_photon_energy_neg_irr_dataframe():
# test for handling of negative spectral irradiance values with a
# pandas DataFrame input

spectra = spectrum.get_reference_spectra().T*-1

with pytest.raises(ValueError, match='must be positive'):
spectrum.average_photon_energy(spectra)


def test_average_photon_energy_zero_irr():
# test for handling of zero spectral irradiance values with
# pandas DataFrame and pandas Series input

spectra_df_zero = spectrum.get_reference_spectra().T
spectra_df_zero.iloc[1] = 0
spectra_series_zero = spectrum.get_reference_spectra()['global']*0
out_1 = spectrum.average_photon_energy(spectra_df_zero)
out_2 = spectrum.average_photon_energy(spectra_series_zero)
expected_1 = np.array([1.36848, np.nan, 1.40885])
expected_2 = np.nan
assert_allclose(out_1, expected_1, atol=1e-3)
assert_allclose(out_2, expected_2, atol=1e-3)

0 comments on commit 9875521

Please sign in to comment.