Repository: pvlib/pvlib-python

Issue:
ModelChain should accept albedo in weather dataframe
**Is your feature request related to a problem? Please describe.**
Albedo is treated as a scalar constant in pvlib, but it is of course a function of the weather and changes throughout the year.  Albedo is currently set in the PVSystem or Array and cannot be altered using the ModelChain.  Albedo is provided as a timeseries from many weather data services as well as through NREL's NSRBD and it would be useful to provide this data to the ModelChain.

Additionally, treating albedo as property of the Array seems to conflict with the [PVSystem Design Philosophy](https://pvlib-python.readthedocs.io/en/stable/pvsystem.html#design-philosophy), which highlights the separation of the PV system and the exogenous variables, such as the weather.

**Describe the solution you'd like**
ModelChain.run_model() should accept albedo in the weather dataframe, like temperature and ghi.

**Describe alternatives you've considered**
An alternative we have implemented is calling ModelChain.run_model() on each row of a dataframe and manually updating the albedo of the array in each tilmestep.  This probably has some side effects that we are unaware of.



Discussion:
I think I agree that it would make more sense to pass around ground albedo with the weather data instead of treating it as a characteristic of the array.  

> Albedo is treated as a scalar constant in pvlib

> An alternative we have implemented is calling ModelChain.run_model() on each row of a dataframe and manually updating the albedo of the array in each tilmestep. 

It is true that the docs for `Array` and `PVSystem` say that `albedo` is a float, but I think it also works to set albedo to a time series that matches the weather you pass to `ModelChain.run_model()`.  At least that saves you from looping w/ scalar albedo values.  
I agree that albedo can change with e.g., precipitation or season, but it is also a property of the bare ground surface, and it is that perspective that put albedo with the system parameters. One use case for not having albedo in the weather data would be to evaluate the effect of different ground cover on array output.

I am only away of the NSRDB offering albedo with weather data; are there other sources?

I'm +1 on allowing `albedo` to be a Series. I'm neutral on bundling albedo with weather data, but I don't see a better option.  We only have two data structures that supply `ModelChain`: the `weather` DataFrame, and the `PVSystem` instance. I don't think it is practical to create a third just for `albedo`, and it isn't any more work to add or modify `albedo` to `weather` than it is to extract `albedo` from downloaded weather data and add it to `PVSystem`.


To clarify my above message, I think it *already* works to set `PVSystem.albedo` or `Array.albedo` to a Series, despite the docs saying it must be float.  

> are there other sources?

A non-exhaustive list of examples: [SolarAnywhere](https://www.solaranywhere.com/support/data-fields/albedo/), [SolarGIS](https://solargis.com/docs/getting-started/data-parameters), [MERRA2](https://disc.gsfc.nasa.gov/datasets/M2TMNXRAD_5.12.4/summary)
Does anyone know if including time-specific albedo this has been shown to be even more important with bifacials?

(I would think yes.)
> Does anyone know if including time-specific albedo this has been shown to be even more important with bifacials?
> 
> (I would think yes.)

Yes, it is more important than for single-sided modules. There are ground surfaces where the albedo depends on the solar elevation and hence time of day.

One caution about albedo from satellite-derived irradiance: those values are at least km^2 scale, and are observed from space, whereas a PV model is assuming that the albedo is localized (m^2) and has been determined from the irradiance reaching the ground. [SolarAnywhere ](https://www.solaranywhere.com/support/data-fields/albedo/)provides an informative list of caveats.

The good news is that the uncertainty in albedo is typically secondary to uncertainty in other data such as GHI, when considering uncertainty in energy production.

Which is the better course of action?

1. Leave `albedo` on `PVsystem` or `Array`. Edit the docstrings and add tests to make it explicit that `PVSystem.albedo` or `Array.albedo` can be a Series. Advantages: avoids deprecating and removing `PVSystem.albedo`. Downside: users obtaining albedo from weather data sources have an extra step to perform when using `ModelChain` methods.
2. Add `albedo` as an optional column in `weather`, and have `ModelChain` methods use `weather['albedo']` instead of `PVSystem.albedo` when `weather['albedo']` is present. Advantages: convenient for ModelChain users, and avoids deprecating `PVsystem.albedo`. Disadvatanges: potential for confusion when a user also assigns `PVSystem.albedo`. 
3. Move `albedo` from `PVSystem` to `weather`. Same advantages as 2, but requires deprecation, and adds an extra step for users who aren't getting albedo with weather data, and would probably lead to adding code to `ModelChain` similar to the code that infers albedo from `PVSystem.surface_type`.
@cwhanse I lean toward option 1. However, it seems that for option 1, a user might have to duplicate computations already done in the `ModelChain` in the preliminary step of using the weather to compute the existing `PVSystem.albedo` (as some formulaic combination of ground conditions and weather). Do you know if this is really an issue?
Thanks for the discussion around this feature.  I favor Option 2, noting that PVSystem.albedo is already an optional argument.  Option 1 is prone to bugs and cases need to be handled checking for mismatches in series/dataframe lengths or perhaps same series lengths but different indices.  I would discourage Option 3, as it seems there is both a basis for and utility in retaining albedo as a property of PVSystem.
For option 2, I would say raise an exception (or at very least a warning) if two albedos are specified. This could definitely be the source of a subtle computational bug when users don't realize a weather file they pulled is doing something they didn't intend.
+1 for option 2, and I agree with @campanelli-sunpower that an exception should be raised if two albedos are specified
+1 for option 2, but I like keeping the old method for the case where you just want to set either a single annual albedo or monthly albedos (tho not sure if this option currently exists). I agree also raise exception if both are specified - we can always change it later.

Patch:
diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py
--- a/pvlib/clearsky.py
+++ b/pvlib/clearsky.py
@@ -960,8 +960,8 @@ def bird(zenith, airmass_relative, aod380, aod500, precipitable_water,
         Extraterrestrial radiation [W/m^2], defaults to 1364[W/m^2]
     asymmetry : numeric
         Asymmetry factor, defaults to 0.85
-    albedo : numeric
-        Albedo, defaults to 0.2
+    albedo : numeric, default 0.2
+        Ground surface albedo. [unitless]
 
     Returns
     -------
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
--- a/pvlib/irradiance.py
+++ b/pvlib/irradiance.py
@@ -304,7 +304,7 @@ def beam_component(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
 def get_total_irradiance(surface_tilt, surface_azimuth,
                          solar_zenith, solar_azimuth,
                          dni, ghi, dhi, dni_extra=None, airmass=None,
-                         albedo=.25, surface_type=None,
+                         albedo=0.25, surface_type=None,
                          model='isotropic',
                          model_perez='allsitescomposite1990'):
     r"""
@@ -344,7 +344,7 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
     airmass : None or numeric, default None
         Relative airmass (not adjusted for pressure). [unitless]
     albedo : numeric, default 0.25
-        Surface albedo. [unitless]
+        Ground 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.
@@ -1872,7 +1872,7 @@ def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times,
         applied.
 
     albedo : numeric, default 0.25
-        Surface albedo
+        Ground surface albedo. [unitless]
 
     model : String, default 'perez'
         Irradiance model.  See :py:func:`get_sky_diffuse` for allowed values.
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -268,7 +268,7 @@ class ModelChainResult:
     _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier',
                          'spectral_modifier', 'cell_temperature',
                          'effective_irradiance', 'dc', 'diode_params',
-                         'dc_ohmic_losses', 'weather'}
+                         'dc_ohmic_losses', 'weather', 'albedo'}
 
     # system-level information
     solar_position: Optional[pd.DataFrame] = field(default=None)
@@ -366,6 +366,10 @@ class ModelChainResult:
     """DatetimeIndex containing a copy of the index of the input weather data.
     """
 
+    albedo: Optional[PerArray[pd.Series]] = None
+    """Series (or tuple of Series, one for each array) containing albedo.
+    """
+
     def _result_type(self, value):
         """Coerce `value` to the correct type according to
         ``self._singleton_tuples``."""
@@ -1339,6 +1343,17 @@ def _prep_inputs_solar_pos(self, weather):
             **kwargs)
         return self
 
+    def _prep_inputs_albedo(self, weather):
+        """
+        Get albedo from weather
+        """
+        try:
+            self.results.albedo = _tuple_from_dfs(weather, 'albedo')
+        except KeyError:
+            self.results.albedo = tuple([
+                a.albedo for a in self.system.arrays])
+        return self
+
     def _prep_inputs_airmass(self):
         """
         Assign airmass
@@ -1471,11 +1486,17 @@ def prepare_inputs(self, weather):
 
         Parameters
         ----------
-        weather : DataFrame, or tuple or list of DataFrame
+        weather : DataFrame, or tuple or list of DataFrames
             Required column names include ``'dni'``, ``'ghi'``, ``'dhi'``.
-            Optional column names are ``'wind_speed'``, ``'temp_air'``; if not
+            Optional column names are ``'wind_speed'``, ``'temp_air'``,
+            ``'albedo'``.
+
+            If optional columns ``'wind_speed'``, ``'temp_air'`` are not
             provided, air temperature of 20 C and wind speed
-            of 0 m/s will be added to the DataFrame.
+            of 0 m/s will be added to the ``weather`` DataFrame.
+
+            If optional column ``'albedo'`` is provided, albedo values in the
+            ModelChain's PVSystem.arrays are ignored.
 
             If `weather` is a tuple or list, it must be of the same length and
             order as the Arrays of the ModelChain's PVSystem.
@@ -1494,7 +1515,7 @@ def prepare_inputs(self, weather):
         Notes
         -----
         Assigns attributes to ``results``: ``times``, ``weather``,
-        ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``
+        ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``, ``albedo``.
 
         See also
         --------
@@ -1507,6 +1528,7 @@ def prepare_inputs(self, weather):
 
         self._prep_inputs_solar_pos(weather)
         self._prep_inputs_airmass()
+        self._prep_inputs_albedo(weather)
 
         # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance
         # and PVSystem.get_aoi and SingleAxisTracker.get_aoi
@@ -1531,6 +1553,7 @@ def prepare_inputs(self, weather):
             _tuple_from_dfs(self.results.weather, 'dni'),
             _tuple_from_dfs(self.results.weather, 'ghi'),
             _tuple_from_dfs(self.results.weather, 'dhi'),
+            albedo=self.results.albedo,
             airmass=self.results.airmass['airmass_relative'],
             model=self.transposition_model
         )
@@ -1724,16 +1747,32 @@ def run_model(self, weather):
         Parameters
         ----------
         weather : DataFrame, or tuple or list of DataFrame
-            Irradiance column names must include ``'dni'``, ``'ghi'``, and
-            ``'dhi'``. If optional columns ``'temp_air'`` and ``'wind_speed'``
+            Column names must include:
+
+            - ``'dni'``
+            - ``'ghi'``
+            - ``'dhi'``
+
+            Optional columns are:
+
+            - ``'temp_air'``
+            - ``'cell_temperature'``
+            - ``'module_temperature'``
+            - ``'wind_speed'``
+            - ``'albedo'``
+
+            If optional columns ``'temp_air'`` and ``'wind_speed'``
             are not provided, air temperature of 20 C and wind speed of 0 m/s
             are added to the DataFrame. If optional column
             ``'cell_temperature'`` is provided, these values are used instead
-            of `temperature_model`. If optional column `module_temperature`
-            is provided, `temperature_model` must be ``'sapm'``.
+            of `temperature_model`. If optional column ``'module_temperature'``
+            is provided, ``temperature_model`` must be ``'sapm'``.
 
-            If list or tuple, must be of the same length and order as the
-            Arrays of the ModelChain's PVSystem.
+            If optional column ``'albedo'`` is provided, ``'albedo'`` may not
+            be present on the ModelChain's PVSystem.Arrays.
+
+            If weather is a list or tuple, it must be of the same length and
+            order as the Arrays of the ModelChain's PVSystem.
 
         Returns
         -------
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -134,7 +134,7 @@ class PVSystem:
         a single array is created from the other parameters (e.g.
         `surface_tilt`, `surface_azimuth`). Must contain at least one Array,
         if length of arrays is 0 a ValueError is raised. If `arrays` is
-        specified the following parameters are ignored:
+        specified the following PVSystem parameters are ignored:
 
         - `surface_tilt`
         - `surface_azimuth`
@@ -157,13 +157,14 @@ class PVSystem:
         North=0, East=90, South=180, West=270.
 
     albedo : None or float, default None
-        The ground albedo. If ``None``, will attempt to use
-        ``surface_type`` and ``irradiance.SURFACE_ALBEDOS``
-        to lookup albedo.
+        Ground surface albedo. If ``None``, then ``surface_type`` is used
+        to look up a value in ``irradiance.SURFACE_ALBEDOS``.
+        If ``surface_type`` is also None then a ground surface albedo
+        of 0.25 is used.
 
     surface_type : None or string, default None
-        The ground surface type. See ``irradiance.SURFACE_ALBEDOS``
-        for valid values.
+        The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for
+        valid values.
 
     module : None or string, default None
         The model name of the modules.
@@ -333,30 +334,33 @@ def get_aoi(self, solar_zenith, solar_azimuth):
 
     @_unwrap_single_value
     def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
-                       dni_extra=None, airmass=None, model='haydavies',
-                       **kwargs):
+                       dni_extra=None, airmass=None, albedo=None,
+                       model='haydavies', **kwargs):
         """
         Uses the :py:func:`irradiance.get_total_irradiance` function to
-        calculate the plane of array irradiance components on a tilted
-        surface defined by ``self.surface_tilt``,
-        ``self.surface_azimuth``, and ``self.albedo``.
+        calculate the plane of array irradiance components on the tilted
+        surfaces defined by each array's ``surface_tilt`` and
+        ``surface_azimuth``.
 
         Parameters
         ----------
-        solar_zenith : float or Series.
+        solar_zenith : float or Series
             Solar zenith angle.
-        solar_azimuth : float or Series.
+        solar_azimuth : float or Series
             Solar azimuth angle.
         dni : float or Series or tuple of float or Series
-            Direct Normal Irradiance
+            Direct Normal Irradiance. [W/m2]
         ghi : float or Series or tuple of float or Series
-            Global horizontal irradiance
+            Global horizontal irradiance. [W/m2]
         dhi : float or Series or tuple of float or Series
-            Diffuse horizontal irradiance
-        dni_extra : None, float or Series, default None
-            Extraterrestrial direct normal irradiance
+            Diffuse horizontal irradiance. [W/m2]
+        dni_extra : None, float, Series or tuple of float or Series,\
+            default None
+            Extraterrestrial direct normal irradiance. [W/m2]
         airmass : None, float or Series, default None
-            Airmass
+            Airmass. [unitless]
+        albedo : None, float or Series, default None
+            Ground surface albedo. [unitless]
         model : String, default 'haydavies'
             Irradiance model.
 
@@ -376,17 +380,24 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
         poa_irradiance : DataFrame or tuple of DataFrame
             Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse',
             'poa_sky_diffuse', 'poa_ground_diffuse'``.
+
+        See also
+        --------
+        pvlib.irradiance.get_total_irradiance
         """
         dni = self._validate_per_array(dni, system_wide=True)
         ghi = self._validate_per_array(ghi, system_wide=True)
         dhi = self._validate_per_array(dhi, system_wide=True)
+
+        albedo = self._validate_per_array(albedo, system_wide=True)
+
         return tuple(
             array.get_irradiance(solar_zenith, solar_azimuth,
                                  dni, ghi, dhi,
-                                 dni_extra, airmass, model,
-                                 **kwargs)
-            for array, dni, ghi, dhi in zip(
-                self.arrays, dni, ghi, dhi
+                                 dni_extra=dni_extra, airmass=airmass,
+                                 albedo=albedo, model=model, **kwargs)
+            for array, dni, ghi, dhi, albedo in zip(
+                self.arrays, dni, ghi, dhi, albedo
             )
         )
 
@@ -1258,14 +1269,14 @@ class Array:
         If not provided, a FixedMount with zero tilt is used.
 
     albedo : None or float, default None
-        The ground albedo. If ``None``, will attempt to use
-        ``surface_type`` to look up an albedo value in
-        ``irradiance.SURFACE_ALBEDOS``. If a surface albedo
-        cannot be found then 0.25 is used.
+        Ground surface albedo. If ``None``, then ``surface_type`` is used
+        to look up a value in ``irradiance.SURFACE_ALBEDOS``.
+        If ``surface_type`` is also None then a ground surface albedo
+        of 0.25 is used.
 
     surface_type : None or string, default None
-        The ground surface type. See ``irradiance.SURFACE_ALBEDOS``
-        for valid values.
+        The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for valid
+        values.
 
     module : None or string, default None
         The model name of the modules.
@@ -1425,15 +1436,14 @@ def get_aoi(self, solar_zenith, solar_azimuth):
                               solar_zenith, solar_azimuth)
 
     def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
-                       dni_extra=None, airmass=None, model='haydavies',
-                       **kwargs):
+                       dni_extra=None, airmass=None, albedo=None,
+                       model='haydavies', **kwargs):
         """
         Get plane of array irradiance components.
 
         Uses the :py:func:`pvlib.irradiance.get_total_irradiance` function to
         calculate the plane of array irradiance components for a surface
-        defined by ``self.surface_tilt`` and ``self.surface_azimuth`` with
-        albedo ``self.albedo``.
+        defined by ``self.surface_tilt`` and ``self.surface_azimuth``.
 
         Parameters
         ----------
@@ -1442,15 +1452,17 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
         solar_azimuth : float or Series.
             Solar azimuth angle.
         dni : float or Series
-            Direct Normal Irradiance
-        ghi : float or Series
+            Direct normal irradiance. [W/m2]
+        ghi : float or Series. [W/m2]
             Global horizontal irradiance
         dhi : float or Series
-            Diffuse horizontal irradiance
+            Diffuse horizontal irradiance. [W/m2]
         dni_extra : None, float or Series, default None
-            Extraterrestrial direct normal irradiance
+            Extraterrestrial direct normal irradiance. [W/m2]
         airmass : None, float or Series, default None
-            Airmass
+            Airmass. [unitless]
+        albedo : None, float or Series, default None
+            Ground surface albedo. [unitless]
         model : String, default 'haydavies'
             Irradiance model.
 
@@ -1463,7 +1475,14 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
         poa_irradiance : DataFrame
             Column names are: ``'poa_global', 'poa_direct', 'poa_diffuse',
             'poa_sky_diffuse', 'poa_ground_diffuse'``.
+
+        See also
+        --------
+        :py:func:`pvlib.irradiance.get_total_irradiance`
         """
+        if albedo is None:
+            albedo = self.albedo
+
         # not needed for all models, but this is easier
         if dni_extra is None:
             dni_extra = irradiance.get_extra_radiation(solar_zenith.index)
@@ -1478,8 +1497,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
                                                dni, ghi, dhi,
                                                dni_extra=dni_extra,
                                                airmass=airmass,
+                                               albedo=albedo,
                                                model=model,
-                                               albedo=self.albedo,
                                                **kwargs)
 
     def get_iam(self, aoi, iam_model='physical'):
@@ -3293,7 +3312,7 @@ def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent,
 
     See Also
     --------
-    :py:func:`~pvlib.pvsystem.dc_ohmic_losses`
+    pvlib.pvsystem.dc_ohmic_losses
 
     References
     ----------
@@ -3328,7 +3347,7 @@ def dc_ohmic_losses(resistance, current):
 
     See Also
     --------
-    :py:func:`~pvlib.pvsystem.dc_ohms_from_percent`
+    pvlib.pvsystem.dc_ohms_from_percent
 
     References
     ----------
diff --git a/pvlib/tracking.py b/pvlib/tracking.py
--- a/pvlib/tracking.py
+++ b/pvlib/tracking.py
@@ -188,7 +188,8 @@ def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith,
     @_unwrap_single_value
     def get_irradiance(self, surface_tilt, surface_azimuth,
                        solar_zenith, solar_azimuth, dni, ghi, dhi,
-                       dni_extra=None, airmass=None, model='haydavies',
+                       albedo=None, dni_extra=None, airmass=None,
+                       model='haydavies',
                        **kwargs):
         """
         Uses the :func:`irradiance.get_total_irradiance` function to
@@ -215,6 +216,8 @@ def get_irradiance(self, surface_tilt, surface_azimuth,
             Global horizontal irradiance
         dhi : float or Series
             Diffuse horizontal irradiance
+        albedo : None, float or Series, default None
+            Ground surface albedo. [unitless]
         dni_extra : float or Series, default None
             Extraterrestrial direct normal irradiance
         airmass : float or Series, default None
@@ -245,6 +248,13 @@ def get_irradiance(self, surface_tilt, surface_azimuth,
         ghi = self._validate_per_array(ghi, system_wide=True)
         dhi = self._validate_per_array(dhi, system_wide=True)
 
+        if albedo is None:
+            # assign default albedo here because SingleAxisTracker
+            # initializes albedo to None
+            albedo = 0.25
+
+        albedo = self._validate_per_array(albedo, system_wide=True)
+
         return tuple(
             irradiance.get_total_irradiance(
                 surface_tilt,
@@ -255,10 +265,10 @@ def get_irradiance(self, surface_tilt, surface_azimuth,
                 dni_extra=dni_extra,
                 airmass=airmass,
                 model=model,
-                albedo=self.arrays[0].albedo,
+                albedo=albedo,
                 **kwargs)
-            for array, dni, ghi, dhi in zip(
-                self.arrays, dni, ghi, dhi
+            for array, dni, ghi, dhi, albedo in zip(
+                self.arrays, dni, ghi, dhi, albedo
             )
         )
 


Test patch:
diff --git a/pvlib/tests/test_clearsky.py b/pvlib/tests/test_clearsky.py
--- a/pvlib/tests/test_clearsky.py
+++ b/pvlib/tests/test_clearsky.py
@@ -756,6 +756,30 @@ def test_bird():
     assert np.allclose(
         testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3
     )
+    # repeat test with albedo as a Series
+    alb_series = pd.Series(0.2, index=times)
+    irrads = clearsky.bird(
+        zenith, airmass, aod_380nm, aod_500nm, h2o_cm, o3_cm, press_mB * 100.,
+        etr, b_a, alb_series
+    )
+    Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names)
+    direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.)
+    assert np.allclose(
+        testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:48], rtol=1e-3
+    )
+    direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.)
+    assert np.allclose(
+        testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:48], rtol=1e-3
+    )
+    global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.)
+    assert np.allclose(
+        testdata['Global Hz'].where(dusk, 0.), global_horz[1:48], rtol=1e-3
+    )
+    diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.)
+    assert np.allclose(
+        testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:48], rtol=1e-3
+    )
+
     # test keyword parameters
     irrads2 = clearsky.bird(
         zenith, airmass, aod_380nm, aod_500nm, h2o_cm, dni_extra=etr
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
@@ -120,29 +120,38 @@ def test_get_extra_radiation_invalid():
         irradiance.get_extra_radiation(300, method='invalid')
 
 
-def test_grounddiffuse_simple_float():
+def test_get_ground_diffuse_simple_float():
     result = irradiance.get_ground_diffuse(40, 900)
     assert_allclose(result, 26.32000014911496)
 
 
-def test_grounddiffuse_simple_series(irrad_data):
+def test_get_ground_diffuse_simple_series(irrad_data):
     ground_irrad = irradiance.get_ground_diffuse(40, irrad_data['ghi'])
     assert ground_irrad.name == 'diffuse_ground'
 
 
-def test_grounddiffuse_albedo_0(irrad_data):
+def test_get_ground_diffuse_albedo_0(irrad_data):
     ground_irrad = irradiance.get_ground_diffuse(
         40, irrad_data['ghi'], albedo=0)
     assert 0 == ground_irrad.all()
 
 
+def test_get_ground_diffuse_albedo_series(times):
+    albedo = pd.Series(0.2, index=times)
+    ground_irrad = irradiance.get_ground_diffuse(
+        45, pd.Series(1000, index=times), albedo)
+    expected = albedo * 0.5 * (1 - np.sqrt(2) / 2.) * 1000
+    expected.name = 'diffuse_ground'
+    assert_series_equal(ground_irrad, expected)
+
+
 def test_grounddiffuse_albedo_invalid_surface(irrad_data):
     with pytest.raises(KeyError):
         irradiance.get_ground_diffuse(
             40, irrad_data['ghi'], surface_type='invalid')
 
 
-def test_grounddiffuse_albedo_surface(irrad_data):
+def test_get_ground_diffuse_albedo_surface(irrad_data):
     result = irradiance.get_ground_diffuse(40, irrad_data['ghi'],
                                            surface_type='sand')
     assert_allclose(result, [0, 3.731058, 48.778813, 12.035025], atol=1e-4)
@@ -387,6 +396,25 @@ def test_get_total_irradiance(irrad_data, ephem_data, dni_et,
                                           'poa_ground_diffuse']
 
 
+@pytest.mark.parametrize('model', ['isotropic', 'klucher',
+                                   'haydavies', 'reindl', 'king', 'perez'])
+def test_get_total_irradiance_albedo(
+        irrad_data, ephem_data, dni_et, relative_airmass, model):
+    albedo = pd.Series(0.2, index=ephem_data.index)
+    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,
+        albedo=albedo)
+
+    assert total.columns.tolist() == ['poa_global', 'poa_direct',
+                                      'poa_diffuse', 'poa_sky_diffuse',
+                                      'poa_ground_diffuse']
+
+
 @pytest.mark.parametrize('model', ['isotropic', 'klucher',
                                    'haydavies', 'reindl', 'king', 'perez'])
 def test_get_total_irradiance_scalars(model):
@@ -698,6 +726,14 @@ def test_gti_dirint():
 
     assert_frame_equal(output, expected)
 
+    # test with albedo as a Series
+    albedo = pd.Series(0.05, index=times)
+    output = irradiance.gti_dirint(
+        poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth,
+        albedo=albedo)
+
+    assert_frame_equal(output, expected)
+
     # test temp_dew input
     temp_dew = np.array([70, 80, 20])
     output = irradiance.gti_dirint(
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -495,6 +495,26 @@ def test_prepare_inputs_multi_weather(
     mc.prepare_inputs(input_type((weather, weather)))
     num_arrays = sapm_dc_snl_ac_system_Array.num_arrays
     assert len(mc.results.total_irrad) == num_arrays
+    # check that albedo is transfered to mc.results from mc.system.arrays
+    assert mc.results.albedo == (0.2, 0.2)
+
+
+@pytest.mark.parametrize("input_type", [tuple, list])
+def test_prepare_inputs_albedo_in_weather(
+        sapm_dc_snl_ac_system_Array, location, input_type):
+    times = pd.date_range(start='20160101 1200-0700',
+                          end='20160101 1800-0700', freq='6H')
+    mc = ModelChain(sapm_dc_snl_ac_system_Array, location)
+    weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1, 'albedo': 0.5},
+                           index=times)
+    # weather as a single DataFrame
+    mc.prepare_inputs(weather)
+    num_arrays = sapm_dc_snl_ac_system_Array.num_arrays
+    assert len(mc.results.albedo) == num_arrays
+    # repeat with tuple of weather
+    mc.prepare_inputs(input_type((weather, weather)))
+    num_arrays = sapm_dc_snl_ac_system_Array.num_arrays
+    assert len(mc.results.albedo) == num_arrays
 
 
 def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location):
diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py
--- a/pvlib/tests/test_pvsystem.py
+++ b/pvlib/tests/test_pvsystem.py
@@ -18,6 +18,7 @@
 from pvlib.pvsystem import FixedMount
 from pvlib import temperature
 from pvlib._deprecation import pvlibDeprecationWarning
+from pvlib.tools import cosd
 
 
 @pytest.mark.parametrize('iam_model,model_params', [
@@ -1673,51 +1674,70 @@ def test_PVSystem_multiple_array_get_aoi():
     assert aoi_one > 0
 
 
-def test_PVSystem_get_irradiance():
-    system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
+@pytest.fixture
+def solar_pos():
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     location = Location(latitude=32, longitude=-111)
-    solar_position = location.get_solarposition(times)
+    return location.get_solarposition(times)
+
+
+def test_PVSystem_get_irradiance(solar_pos):
+    system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
     irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]},
-                          index=times)
+                          index=solar_pos.index)
 
-    irradiance = system.get_irradiance(solar_position['apparent_zenith'],
-                                       solar_position['azimuth'],
+    irradiance = system.get_irradiance(solar_pos['apparent_zenith'],
+                                       solar_pos['azimuth'],
                                        irrads['dni'],
                                        irrads['ghi'],
                                        irrads['dhi'])
 
     expected = pd.DataFrame(data=np.array(
-        [[ 883.65494055,  745.86141676,  137.79352379,  126.397131  ,
-              11.39639279],
-           [   0.        ,   -0.        ,    0.        ,    0.        ,    0.        ]]),
+        [[883.65494055, 745.86141676, 137.79352379, 126.397131, 11.39639279],
+         [0., -0., 0., 0., 0.]]),
                             columns=['poa_global', 'poa_direct',
                                      'poa_diffuse', 'poa_sky_diffuse',
                                      'poa_ground_diffuse'],
-                            index=times)
+                            index=solar_pos.index)
+    assert_frame_equal(irradiance, expected, check_less_precise=2)
+
 
+def test_PVSystem_get_irradiance_albedo(solar_pos):
+    system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
+    irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0],
+                           'albedo': [0.5, 0.5]},
+                          index=solar_pos.index)
+    # albedo as a Series
+    irradiance = system.get_irradiance(solar_pos['apparent_zenith'],
+                                       solar_pos['azimuth'],
+                                       irrads['dni'],
+                                       irrads['ghi'],
+                                       irrads['dhi'],
+                                       albedo=irrads['albedo'])
+    expected = pd.DataFrame(data=np.array(
+        [[895.05134334, 745.86141676, 149.18992658, 126.397131, 22.79279558],
+         [0., -0., 0., 0., 0.]]),
+        columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
+                 'poa_ground_diffuse'],
+        index=solar_pos.index)
     assert_frame_equal(irradiance, expected, check_less_precise=2)
 
 
-def test_PVSystem_get_irradiance_model(mocker):
+def test_PVSystem_get_irradiance_model(mocker, solar_pos):
     spy_perez = mocker.spy(irradiance, 'perez')
     spy_haydavies = mocker.spy(irradiance, 'haydavies')
     system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135)
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    location = Location(latitude=32, longitude=-111)
-    solar_position = location.get_solarposition(times)
     irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
-                          index=times)
-    system.get_irradiance(solar_position['apparent_zenith'],
-                          solar_position['azimuth'],
+                          index=solar_pos.index)
+    system.get_irradiance(solar_pos['apparent_zenith'],
+                          solar_pos['azimuth'],
                           irrads['dni'],
                           irrads['ghi'],
                           irrads['dhi'])
     spy_haydavies.assert_called_once()
-    system.get_irradiance(solar_position['apparent_zenith'],
-                          solar_position['azimuth'],
+    system.get_irradiance(solar_pos['apparent_zenith'],
+                          solar_pos['azimuth'],
                           irrads['dni'],
                           irrads['ghi'],
                           irrads['dhi'],
@@ -1725,31 +1745,28 @@ def test_PVSystem_get_irradiance_model(mocker):
     spy_perez.assert_called_once()
 
 
-def test_PVSystem_multi_array_get_irradiance():
+def test_PVSystem_multi_array_get_irradiance(solar_pos):
     array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32,
                                                    surface_azimuth=135))
     array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=5,
                                                    surface_azimuth=150))
     system = pvsystem.PVSystem(arrays=[array_one, array_two])
-    location = Location(latitude=32, longitude=-111)
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    solar_position = location.get_solarposition(times)
+
     irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
-                          index=times)
+                          index=solar_pos.index)
     array_one_expected = array_one.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         irrads['dni'], irrads['ghi'], irrads['dhi']
     )
     array_two_expected = array_two.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         irrads['dni'], irrads['ghi'], irrads['dhi']
     )
     array_one_irrad, array_two_irrad = system.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         irrads['dni'], irrads['ghi'], irrads['dhi']
     )
     assert_frame_equal(
@@ -1760,7 +1777,7 @@ def test_PVSystem_multi_array_get_irradiance():
     )
 
 
-def test_PVSystem_multi_array_get_irradiance_multi_irrad():
+def test_PVSystem_multi_array_get_irradiance_multi_irrad(solar_pos):
     """Test a system with two identical arrays but different irradiance.
 
     Because only the irradiance is different we expect the same output
@@ -1771,39 +1788,36 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad():
     array_one = pvsystem.Array(pvsystem.FixedMount(0, 180))
     array_two = pvsystem.Array(pvsystem.FixedMount(0, 180))
     system = pvsystem.PVSystem(arrays=[array_one, array_two])
-    location = Location(latitude=32, longitude=-111)
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    solar_position = location.get_solarposition(times)
+
     irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
-                          index=times)
+                          index=solar_pos.index)
     irrads_two = pd.DataFrame(
         {'dni': [0, 900], 'ghi': [0, 600], 'dhi': [0, 100]},
-        index=times
+        index=solar_pos.index
     )
     array_irrad = system.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         (irrads['dhi'], irrads['dhi']),
         (irrads['ghi'], irrads['ghi']),
         (irrads['dni'], irrads['dni'])
     )
     assert_frame_equal(array_irrad[0], array_irrad[1])
     array_irrad = system.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         (irrads['dhi'], irrads_two['dhi']),
         (irrads['ghi'], irrads_two['ghi']),
         (irrads['dni'], irrads_two['dni'])
     )
     array_one_expected = array_one.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         irrads['dhi'], irrads['ghi'], irrads['dni']
     )
     array_two_expected = array_two.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         irrads_two['dhi'], irrads_two['ghi'], irrads_two['dni']
     )
     assert not array_irrad[0].equals(array_irrad[1])
@@ -1812,15 +1826,15 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad():
     with pytest.raises(ValueError,
                        match="Length mismatch for per-array parameter"):
         system.get_irradiance(
-            solar_position['apparent_zenith'],
-            solar_position['azimuth'],
+            solar_pos['apparent_zenith'],
+            solar_pos['azimuth'],
             (irrads['dhi'], irrads_two['dhi'], irrads['dhi']),
             (irrads['ghi'], irrads_two['ghi']),
             irrads['dni']
         )
     array_irrad = system.get_irradiance(
-        solar_position['apparent_zenith'],
-        solar_position['azimuth'],
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
         (irrads['dhi'], irrads_two['dhi']),
         irrads['ghi'],
         irrads['dni']
@@ -1829,6 +1843,44 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad():
     assert not array_irrad[0].equals(array_irrad[1])
 
 
+def test_Array_get_irradiance(solar_pos):
+    array = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32,
+                                               surface_azimuth=135))
+    irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]},
+                          index=solar_pos.index)
+    # defaults for kwargs
+    modeled = array.get_irradiance(
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
+        irrads['dni'], irrads['ghi'], irrads['dhi']
+    )
+    expected = pd.DataFrame(
+        data=np.array(
+            [[883.65494055, 745.86141676, 137.79352379, 126.397131,
+              11.39639279],
+             [0., -0., 0., 0., 0.]]),
+        columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse',
+                 'poa_ground_diffuse'],
+        index=solar_pos.index
+    )
+    assert_frame_equal(modeled, expected, check_less_precise=5)
+    # with specified kwargs, use isotropic sky diffuse because it's easier
+    modeled = array.get_irradiance(
+        solar_pos['apparent_zenith'],
+        solar_pos['azimuth'],
+        irrads['dni'], irrads['ghi'], irrads['dhi'],
+        albedo=0.5, model='isotropic'
+    )
+    sky_diffuse = irradiance.isotropic(array.mount.surface_tilt, irrads['dhi'])
+    ground_diff = irradiance.get_ground_diffuse(
+        array.mount.surface_tilt, irrads['ghi'], 0.5, surface_type=None)
+    aoi = irradiance.aoi(array.mount.surface_tilt, array.mount.surface_azimuth,
+                         solar_pos['apparent_zenith'], solar_pos['azimuth'])
+    direct = irrads['dni'] * cosd(aoi)
+    expected = sky_diffuse + ground_diff + direct
+    assert_series_equal(expected, expected, check_less_precise=5)
+
+
 @fail_on_pvlib_version('0.10')
 @pytest.mark.parametrize('attr', ['module_parameters', 'module', 'module_type',
                                   'temperature_model_parameters', 'albedo',
diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py
--- a/pvlib/tests/test_tracking.py
+++ b/pvlib/tests/test_tracking.py
@@ -393,6 +393,25 @@ def test_get_irradiance():
 
     assert_frame_equal(irradiance, expected, check_less_precise=2)
 
+    # test with albedo as a Series
+    irrads['albedo'] = [0.5, 0.5]
+    with np.errstate(invalid='ignore'):
+        irradiance = system.get_irradiance(tracker_data['surface_tilt'],
+                                           tracker_data['surface_azimuth'],
+                                           solar_zenith,
+                                           solar_azimuth,
+                                           irrads['dni'],
+                                           irrads['ghi'],
+                                           irrads['dhi'],
+                                           albedo=irrads['albedo'])
+
+    expected = pd.Series(data=[21.05514984, nan], index=times,
+                         name='poa_ground_diffuse')
+
+    assert_series_equal(irradiance['poa_ground_diffuse'], expected,
+                        check_less_precise=2)
+
+
 
 def test_SingleAxisTracker___repr__():
     with pytest.warns(pvlibDeprecationWarning):