Skip to content
Open
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
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
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.15.3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Enhancements
* Add ``return_components`` kwarg to :py:func:`pvlib.irradiance.reindl` to
support returning the components of sky diffuse irradiance.
(:issue:`2750`, :pull:`2775`)
* Add ``return_components`` kwarg to :py:func:`pvlib.irradiance.get_sky_diffuse`
and ``diffuse_components`` kwarg to :py:func:`pvlib.irradiance.get_total_irradiance`
to support returning the components of sky diffuse irradiance.
(:issue:`2750`, :pull:`2800`)
* Add iotools functions to retrieve irradiance and weather data from NSRDB PSM4 Polar,
which provides satellite-derived irradiance data above 60 degree latitude.
:py:func:`~pvlib.iotools.get_nsrdb_psm4_polar` and
Expand Down
99 changes: 73 additions & 26 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
dni, ghi, dhi, dni_extra=None, airmass=None,
albedo=0.25, surface_type=None,
model='isotropic',
model_perez='allsitescomposite1990'):
model_perez='allsitescomposite1990',
diffuse_components=False):
r"""
Determine total in-plane irradiance and its beam, sky diffuse and ground
reflected components, using the specified sky diffuse irradiance model.
Expand Down Expand Up @@ -332,12 +333,22 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
``'perez-driesse'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
diffuse_components : bool, default False
If `True`, returns values for the different diffuse irradiance
components available from the selected model
(e.g., isotropic, circumsolar, horizon brightening).
If `False`, only the total diffuse irradiance is returned.
This option is not available for the ``'klucher'`` and
``'king'`` models.

Returns
-------
total_irrad : OrderedDict or DataFrame
total_irrad : dict or DataFrame
Contains keys/columns ``'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'``. [Wm⁻²]
If ``diffuse_components`` is `True`, additional keys/columns are
returned for each of the sky diffuse components returned by the
selected model.

Notes
-----
Expand All @@ -353,7 +364,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
poa_sky_diffuse = get_sky_diffuse(
surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, model=model,
model_perez=model_perez)
model_perez=model_perez, return_components=diffuse_components)

poa_ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo,
surface_type)
Expand All @@ -366,7 +377,8 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
solar_zenith, solar_azimuth,
dni, ghi, dhi, dni_extra=None, airmass=None,
model='isotropic',
model_perez='allsitescomposite1990'):
model_perez='allsitescomposite1990',
return_components=False):
r"""
Determine in-plane sky diffuse irradiance component
using the specified sky diffuse irradiance model.
Expand Down Expand Up @@ -408,11 +420,22 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
``'perez-driesse'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.
return_components : bool, default False
If `True`, returns values for the different diffuse irradiance
components available from the selected model
(e.g., isotropic, circumsolar, horizon brightening).
If `False`, only the total diffuse irradiance is returned.
This option is not available for the ``'klucher'`` and
``'king'`` models.

Returns
-------
poa_sky_diffuse : numeric
Sky diffuse irradiance in the plane of array. [Wm⁻²]
numeric, dict, or DataFrame
Return type controlled by ``return_components`` argument.
If `False`, total sky diffuse irradiance in the plane of array
is returned (numeric). [Wm⁻²]
If `True`, the different diffuse components are returned
(dict or DataFrame). [Wm⁻²]

Raises
------
Expand All @@ -438,33 +461,40 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,

model = model.lower()

if return_components and model in {'klucher', 'king'}:
raise ValueError('return_components is not supported for'
f' model {model}')

if dni_extra is None and model in {'haydavies', 'reindl',
'perez', 'perez-driesse'}:
raise ValueError(f'dni_extra is required for model {model}')

if model == 'isotropic':
sky = isotropic(surface_tilt, dhi)
sky = isotropic(surface_tilt, dhi, return_components=return_components)
elif model == 'klucher':
sky = klucher(surface_tilt, surface_azimuth, dhi, ghi,
solar_zenith, solar_azimuth)
elif model == 'haydavies':
sky = haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth)
solar_zenith, solar_azimuth,
return_components=return_components)
elif model == 'reindl':
sky = reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra,
solar_zenith, solar_azimuth)
solar_zenith, solar_azimuth,
return_components=return_components)
elif model == 'king':
sky = king(surface_tilt, dhi, ghi, solar_zenith)
elif model == 'perez':
if airmass is None:
airmass = atmosphere.get_relative_airmass(solar_zenith)
sky = perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth, airmass,
model=model_perez)
model=model_perez, return_components=return_components)
elif model == 'perez-driesse':
# perez_driesse will calculate its own airmass if needed
sky = perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
solar_zenith, solar_azimuth, airmass)
solar_zenith, solar_azimuth, airmass,
return_components=return_components)
else:
raise ValueError(f'invalid model selection {model}')

Expand All @@ -488,7 +518,7 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):
Direct normal irradiance, as measured from a TMY file or
calculated with a clearsky model. See :term:`dni`. [Wm⁻²]

poa_sky_diffuse : numeric
poa_sky_diffuse : numeric, dict or DataFrame
Diffuse irradiance in the plane of the modules, as
calculated by a diffuse irradiance translation function. [Wm⁻²]

Expand All @@ -499,31 +529,48 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):

Returns
-------
irrads : OrderedDict or DataFrame
irrads : dict or DataFrame
Contains the following keys:

* ``poa_global`` : Total in-plane irradiance. [Wm⁻²]
* ``poa_direct`` : Total in-plane beam irradiance. [Wm⁻²]
* ``poa_diffuse`` : Total in-plane diffuse irradiance. [Wm⁻²]
* ``poa_sky_diffuse`` : In-plane diffuse irradiance from sky. [Wm⁻²]
* ``poa_ground_diffuse`` : In-plane diffuse irradiance from ground.
[Wm⁻²]
* ``poa_global`` : Total irradiance on a tilted plane. [Wm⁻²]
* ``poa_direct`` : Direct irradiance on a tilted plane. [Wm⁻²]
* ``poa_diffuse`` : Diffuse irradiance on a tilted plane. [Wm⁻²]
* ``poa_sky_diffuse`` : The sky diffuse component of irradiance on a
tilted plane. [Wm⁻²]
* ``poa_ground_diffuse`` : The ground diffuse component of irradiance
on a tilted plane. [Wm⁻²]

If ``poa_sky_diffuse`` is a dict or DataFrame, ``irrads`` will
contain additional keys for each of the diffuse components returned by
the selected diffuse irradiance model.

Notes
------
Negative beam irradiation due to AOI > 90° or AOI < 0° is set to zero.
'''

if isinstance(poa_sky_diffuse, dict):
sky_components = poa_sky_diffuse.copy()
total_poa_sky_diffuse = sky_components.pop('poa_sky_diffuse')
elif isinstance(poa_sky_diffuse, pd.DataFrame):
sky_components = poa_sky_diffuse.to_dict(orient='series')
total_poa_sky_diffuse = sky_components.pop('poa_sky_diffuse')
else:
sky_components = {}
total_poa_sky_diffuse = poa_sky_diffuse

poa_direct = np.maximum(dni * np.cos(np.radians(aoi)), 0)
poa_diffuse = poa_sky_diffuse + poa_ground_diffuse
poa_diffuse = total_poa_sky_diffuse + poa_ground_diffuse
poa_global = poa_direct + poa_diffuse

irrads = OrderedDict()
irrads['poa_global'] = poa_global
irrads['poa_direct'] = poa_direct
irrads['poa_diffuse'] = poa_diffuse
irrads['poa_sky_diffuse'] = poa_sky_diffuse
irrads['poa_ground_diffuse'] = poa_ground_diffuse
irrads = {
'poa_global': poa_global,
'poa_direct': poa_direct,
'poa_diffuse': poa_diffuse,
'poa_sky_diffuse': total_poa_sky_diffuse,
'poa_ground_diffuse': poa_ground_diffuse,
**sky_components
}

if isinstance(poa_direct, pd.Series):
irrads = pd.DataFrame(irrads)
Expand Down
100 changes: 100 additions & 0 deletions tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,13 @@ def test_get_sky_diffuse_model_invalid():
model='invalid')


def test_get_sky_diffuse_components_model_not_supported():
with pytest.raises(ValueError):
irradiance.get_sky_diffuse(
30, 180, 0, 180, 1000, 1100, 100, dni_extra=1360, airmass=1,
model='klucher', return_components=True)


def test_get_sky_diffuse_missing_dni_extra():
msg = 'dni_extra is required'
with pytest.raises(ValueError, match=msg):
Expand Down Expand Up @@ -573,6 +580,46 @@ def test_get_total_irradiance(irrad_data, ephem_data, dni_et,
'poa_ground_diffuse']


def test_get_total_irradiance_diffuse_components(irrad_data, ephem_data,
dni_et, relative_airmass):
models = ['reindl', 'perez', 'perez-driesse']

for model in models:
total = irradiance.get_total_irradiance(
32, 180,
ephem_data['apparent_zenith'], ephem_data['azimuth'],
dni=irrad_data['dni'], ghi=irrad_data['ghi'],
dhi=irrad_data['dhi'],
dni_extra=dni_et, airmass=relative_airmass,
model=model,
surface_type='urban',
diffuse_components=True)

assert total.columns.tolist() == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse',
'poa_isotropic', 'poa_circumsolar',
'poa_horizon']

for model in models:
total = irradiance.get_total_irradiance(
32, 180,
ephem_data['apparent_zenith'].to_numpy(),
ephem_data['azimuth'].to_numpy(),
dni=irrad_data['dni'].to_numpy(), ghi=irrad_data['ghi'].to_numpy(),
dhi=irrad_data['dhi'].to_numpy(),
dni_extra=dni_et, airmass=relative_airmass,
model=model,
surface_type='urban',
diffuse_components=True)

assert list(total.keys()) == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse',
'poa_isotropic', 'poa_circumsolar',
'poa_horizon']


@pytest.mark.parametrize('model', ['isotropic', 'klucher',
'haydavies', 'reindl', 'king',
'perez', 'perez-driesse'])
Expand Down Expand Up @@ -662,6 +709,59 @@ def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass):
assert_frame_equal(out, expected)


def test_poa_components_diffuse_components_perez(irrad_data, ephem_data,
dni_et, relative_airmass):
aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'],
ephem_data['azimuth'])
gr_sand = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
surface_type='sand')
diff_perez = irradiance.perez(
40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et,
ephem_data['apparent_zenith'], ephem_data['azimuth'], relative_airmass,
return_components=True)
out = irradiance.poa_components(
aoi, irrad_data['dni'], diff_perez, gr_sand)
expected = pd.DataFrame(np.array(
[[0., -0., 0., 0.,
0., 0., 0., 0.],
[35.19456561, 0., 35.19456561, 31.4635077,
3.73105791, 26.841386, 0.000000, 4.622122],
[956.18253696, 798.31939281, 157.86314414, 109.08433162,
48.77881252, 41.621826, 61.619987, 5.842518],
[90.99624896, 33.50143401, 57.49481495, 45.45978964,
12.03502531, 31.726961, 4.479664, 9.253165]]),
columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse', 'poa_isotropic', 'poa_circumsolar',
'poa_horizon'],
index=irrad_data.index)
assert_frame_equal(out, expected)


def test_poa_components_diffuse_components_isotropic(irrad_data, ephem_data,
dni_et, relative_airmass):
aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'],
ephem_data['azimuth'])
gr_sand = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
surface_type='sand')
diff_isotropic = irradiance.isotropic(
40, irrad_data['dhi'], return_components=True)
out = irradiance.poa_components(
aoi, irrad_data['dni'], diff_isotropic, gr_sand)
expected = pd.DataFrame(np.array(
[[0., -0., 0., 0.,
0., 0.],
[39.459460, 0.000000, 39.459460, 35.728402,
3.731058, 35.728402],
[951.699533, 798.319393, 153.380140, 104.601328,
48.778813, 104.601328],
[100.313650, 33.501434, 66.812216, 54.777191,
12.035025, 54.777191]]),
columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse', 'poa_isotropic'],
index=irrad_data.index)
assert_frame_equal(out, expected)


@pytest.mark.parametrize('pressure,expected', [
(93193, [[830.46567, 0.79742, 0.93505],
[676.18340, 0.63782, 3.02102]]),
Expand Down
Loading