diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index b953cee238..17f568b5d3 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -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 ~~~~~~~~~ diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 3ec6b213f9..b218c06ae4 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -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, @@ -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, @@ -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': @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py index f11897d8e6..c3061d2479 100644 --- a/pvlib/tests/test_irradiance.py +++ b/pvlib/tests/test_irradiance.py @@ -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]]), @@ -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'] @@ -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'])