Skip to content

Better handling of missing inputs in get_total_irradiance, get_sky_diffuse #1225

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
merged 7 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ Enhancements
* Added :py:func:`~pvlib.ivtools.sdm.pvsyst_temperature_coeff` to calculate
the temperature coefficient of power for the pvsyst module model.
(:pull:`1190`)
* :py:func:`~pvlib.irradiance.get_total_irradiance` and
:py:func:`~pvlib.irradiance.get_sky_diffuse` now fill in ``airmass``
if required and not provided. These functions now raise a ``ValueError``
if ``dni_extra`` is required and not provided. (:issue:`949`, :pull:`1225`)

Bug fixes
~~~~~~~~~
Expand Down
103 changes: 70 additions & 33 deletions pvlib/irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pvlib import atmosphere, solarposition, tools


# see References section of grounddiffuse function
# see References section of get_ground_diffuse function
SURFACE_ALBEDOS = {'urban': 0.18,
'grass': 0.20,
'fresh grass': 0.26,
Expand Down Expand Up @@ -323,38 +323,51 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
Parameters
----------
surface_tilt : numeric
Panel tilt from horizontal.
Panel tilt from horizontal. [degree]
surface_azimuth : numeric
Panel azimuth from north.
Panel azimuth from north. [degree]
solar_zenith : numeric
Solar zenith angle.
Solar zenith angle. [degree]
solar_azimuth : numeric
Solar azimuth angle.
Solar azimuth angle. [degree]
dni : numeric
Direct Normal Irradiance
Direct Normal Irradiance. [W/m2]
ghi : numeric
Global horizontal irradiance
Global horizontal irradiance. [W/m2]
dhi : numeric
Diffuse horizontal irradiance
Diffuse horizontal irradiance. [W/m2]
dni_extra : None or numeric, default None
Extraterrestrial direct normal irradiance
Extraterrestrial direct normal irradiance. [W/m2]
airmass : None or numeric, default None
Airmass
Relative airmass (not adjusted for pressure). [unitless]
albedo : numeric, default 0.25
Surface albedo
surface_type : None or String, default None
Surface type. See grounddiffuse.
model : String, default 'isotropic'
Irradiance model.
model_perez : String, default 'allsitescomposite1990'
Used only if model='perez'. See :py:func:`perez`.
Surface albedo. [unitless]
surface_type : None or str, default None
Surface type. See :py:func:`~pvlib.irradiance.get_ground_diffuse` for
the list of accepted values.
model : str, default 'isotropic'
Irradiance model. Can be one of ``'isotropic'``, ``'klucher'``,
``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.

Returns
-------
total_irrad : OrderedDict or DataFrame
Contains keys/columns ``'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'``.

Notes
-----
Models ``'haydavies'``, ``'reindl'``, or ``'perez'`` require
``'dni_extra'``. Values can be calculated using
:py:func:`~pvlib.irradiance.get_extra_radiation`.

The ``'perez'`` model requires relative airmass (``airmass``) as input. If
``airmass`` is not provided, it is calculated using the defaults in
:py:func:`~pvlib.atmosphere.get_relative_airmass`.
"""

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,
Expand Down Expand Up @@ -387,34 +400,56 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
Parameters
----------
surface_tilt : numeric
Panel tilt from horizontal.
Panel tilt from horizontal. [degree]
surface_azimuth : numeric
Panel azimuth from north.
Panel azimuth from north. [degree]
solar_zenith : numeric
Solar zenith angle.
Solar zenith angle. [degree]
solar_azimuth : numeric
Solar azimuth angle.
Solar azimuth angle. [degree]
dni : numeric
Direct Normal Irradiance
Direct Normal Irradiance. [W/m2]
ghi : numeric
Global horizontal irradiance
Global horizontal irradiance. [W/m2]
dhi : numeric
Diffuse horizontal irradiance
Diffuse horizontal irradiance. [W/m2]
dni_extra : None or numeric, default None
Extraterrestrial direct normal irradiance
Extraterrestrial direct normal irradiance. [W/m2]
airmass : None or numeric, default None
Airmass
model : String, default 'isotropic'
Irradiance model.
model_perez : String, default 'allsitescomposite1990'
See perez.
Relative airmass (not adjusted for pressure). [unitless]
model : str, default 'isotropic'
Irradiance model. Can be one of ``'isotropic'``, ``'klucher'``,
``'haydavies'``, ``'reindl'``, ``'king'``, ``'perez'``.
model_perez : str, default 'allsitescomposite1990'
Used only if ``model='perez'``. See :py:func:`~pvlib.irradiance.perez`.

Returns
-------
poa_sky_diffuse : numeric
Sky diffuse irradiance in the plane of array. [W/m2]

Raises
------
ValueError
If model is one of ``'haydavies'``, ``'reindl'``, or ``'perez'`` and
``dni_extra`` is ``None``.

Notes
-----
Models ``'haydavies'``, ``'reindl'``, and ``'perez``` require 'dni_extra'.
Values can be calculated using
:py:func:`~pvlib.irradiance.get_extra_radiation`.

The ``'perez'`` model requires relative airmass (``airmass``) as input. If
``airmass`` is not provided, it is calculated using the defaults in
:py:func:`~pvlib.atmosphere.get_relative_airmass`.
"""

model = model.lower()

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

if model == 'isotropic':
sky = isotropic(surface_tilt, dhi)
elif model == 'klucher':
Expand All @@ -429,6 +464,8 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
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)
Expand Down Expand Up @@ -501,7 +538,7 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse):
def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
'''
Estimate diffuse irradiance from ground reflections given
irradiance, albedo, and surface tilt
irradiance, albedo, and surface tilt.

Function to determine the portion of irradiance on a tilted surface
due to ground reflections. Any of the inputs may be DataFrames or
Expand All @@ -515,7 +552,7 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
(e.g. surface facing up = 0, surface facing horizon = 90).

ghi : numeric
Global horizontal irradiance in W/m^2.
Global horizontal irradiance. [W/m^2]

albedo : numeric, default 0.25
Ground reflectance, typically 0.1-0.4 for surfaces on Earth
Expand All @@ -531,7 +568,7 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
Returns
-------
grounddiffuse : numeric
Ground reflected irradiances in W/m^2.
Ground reflected irradiance. [W/m^2]


References
Expand Down
51 changes: 49 additions & 2 deletions pvlib/tests/test_irradiance.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,35 @@ def test_sky_diffuse_zenith_close_to_90(model):
assert sky_diffuse < 100


def test_get_sky_diffuse_invalid():
def test_get_sky_diffuse_model_invalid():
with pytest.raises(ValueError):
irradiance.get_sky_diffuse(
30, 180, 0, 180, 1000, 1100, 100, dni_extra=1360, airmass=1,
model='invalid')


def test_get_sky_diffuse_missing_dni_extra():
msg = 'dni_extra is required'
with pytest.raises(ValueError, match=msg):
irradiance.get_sky_diffuse(
30, 180, 0, 180, 1000, 1100, 100, airmass=1,
model='haydavies')


def test_get_sky_diffuse_missing_airmass(irrad_data, ephem_data, dni_et):
# test assumes location is Tucson, AZ
# calculated airmass should be the equivalent to fixture airmass
dni = irrad_data['dni'].copy()
dni.iloc[2] = np.nan
out = irradiance.get_sky_diffuse(
40, 180, ephem_data['apparent_zenith'], ephem_data['azimuth'], dni,
irrad_data['ghi'], irrad_data['dhi'], dni_et, model='perez')
expected = pd.Series(np.array(
[0., 31.46046871, np.nan, 45.45539877]),
index=irrad_data.index)
assert_series_equal(out, expected, check_less_precise=2)


def test_campbell_norman():
expected = pd.DataFrame(np.array(
[[863.859736967, 653.123094076, 220.65905025]]),
Expand All @@ -299,7 +321,8 @@ def test_campbell_norman():
assert_frame_equal(out, expected)


def test_get_total_irradiance(irrad_data, ephem_data, dni_et, relative_airmass):
def test_get_total_irradiance(irrad_data, ephem_data, dni_et,
relative_airmass):
models = ['isotropic', 'klucher',
'haydavies', 'reindl', 'king', 'perez']

Expand Down Expand Up @@ -337,6 +360,30 @@ def test_get_total_irradiance_scalars(model):
assert np.isnan(np.array(list(total.values()))).sum() == 0


def test_get_total_irradiance_missing_dni_extra():
msg = 'dni_extra is required'
with pytest.raises(ValueError, match=msg):
irradiance.get_total_irradiance(
32, 180,
10, 180,
dni=1000, ghi=1100,
dhi=100,
model='haydavies')


def test_get_total_irradiance_missing_airmass():
total = irradiance.get_total_irradiance(
32, 180,
10, 180,
dni=1000, ghi=1100,
dhi=100,
dni_extra=1400,
model='perez')
assert list(total.keys()) == ['poa_global', 'poa_direct',
'poa_diffuse', 'poa_sky_diffuse',
'poa_ground_diffuse']


def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass):
aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'],
ephem_data['azimuth'])
Expand Down