Repository: pvlib/pvlib-python

Issue:
Improve docstring or behavior for irradiance.get_total_irradiance and irradiance.get_sky_diffuse
`pvlib.irradiance.get_total_irradiance` accepts kwargs `dni_extra` and `airmass`, both default to `None`. However, values for these kwargs are required for several of the irradiance transposition models. 

See discussion [here](https://groups.google.com/d/msg/pvlib-python/ZPMdpQOD6F4/cs1t23w8AwAJ)

Docstring should specify when `dni_extra` and `airmass` are required, and which airmass is appropriate for each model.

Could also test for kwarg values if e.g. `model=='perez'`


Discussion:
Consider also specifying it is relative airmass.
This came up again in @mikofski's thread [here](https://groups.google.com/g/pvlib-python/c/AUQ_hsbL_B8/m/0y83YIRrBgAJ). 

@mikofski proposed calculating values if needed and not provided.

@kanderso-nrel proposed a couple of solutions to provide more informative error messages: 1. hard coding the failure modes in `get_sky_diffuse` and 2. using a decorator to communicate the failure modes.

And repeating my take from the thread:

I'd rather not introduce the complexity of decorators to the lower level pvlib functions to solve this problem.

Ideas that I support:
* calculate if not provided (Mark's original idea)
* reraising a more informative message.
* require all of the arguments in get_sky_diffuse and get_total_irradiance regardless of whether or not they're used.
* remove the arguments from get_sky_diffuse and get_total_irradiance and do the calculation if it's needed
* ~remove get_sky_diffuse and get_total_irradiance. I'm not convinced they're a net positive for the library.~ (let's try to fix it before throwing up our hands)

Does someone want to tackle this in 0.8.0 or should we kick it down the road?
I think it's tempting to add it in v0.8, but I'm in favor of freezing features now and pushing out the release sooner with the features we already have queued. It's been a while, and I think we should deploy more often with less features per release. I believe this will make it easier to blame issues and get more testing done on new features and fixes faster.
I'll volunteer to take this up for v0.8.1, since I was the complainer.
`PVSystem.get_irradiance` has some relevant shim code:

https://github.com/pvlib/pvlib-python/blob/04a523fafbd61bc2e49420963b84ed8e2bd1b3cf/pvlib/pvsystem.py#L289-L294

Patch:
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
--- 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


Test patch:
diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py
--- 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'])