Repository: pvlib/pvlib-python

Issue:
make Array play nicely with fixed tilt systems and trackers
#1076 is adding an `Array` class that largely describes a fixed-tilt array. However, the composition logic of `PVSystem: def __init__(arrays,...)` combined with the inheritance logic of `SingleAxisTracker(PVSystem)` makes for an odd combination of `Array` objects within `SingleAxisTrackers`. See, for example, https://github.com/pvlib/pvlib-python/pull/1076#discussion_r539704316. 

In https://github.com/pvlib/pvlib-python/pull/1076#discussion_r539686448 I proposed roughly:

Split the `Array` into `BaseArray`, `FixedTiltArray(BaseArray)`, `SingleAxisTrackingArray(BaseArray)`? Basic idea:

```python
class FixedTiltArray(BaseArray)
    """
    Parameters
    ----------
    surface_tilt: float or array-like, default 0
        Surface tilt angles in decimal degrees.
        The tilt angle is defined as degrees from horizontal
        (e.g. surface facing up = 0, surface facing horizon = 90)

    surface_azimuth: float or array-like, default 180
        Azimuth angle of the module surface.
        North=0, East=90, South=180, West=270.

    **kwargs
        Passed to Array. Or copy remainder of Array doc string to be explicit.
    """


# could be in pvsystem.py (module is gradually becoming just the objects) or could be in tracking.py
class SingleAxisTrackerArray(BaseArray)
    """
    Parameters
    ----------
    axis_tilt : float, default 0
        The tilt of the axis of rotation (i.e, the y-axis defined by
        axis_azimuth) with respect to horizontal, in decimal degrees.

    etc.

    **kwargs
        Passed to Array. Or copy remainder of Array doc string to be explicit.
    """
```

I believe the only major challenge is that the `get_aoi` and `get_irradiance` methods would either need to differ in signature (as they do now, and thus present a challenge to a `PVSystem` wrapper) or in implementation (tracker methods would include a call to `singleaxis`, and thus would be less efficient in some workflows). @wfvining suggests that the consistent signature is more important and I'm inclined to agree.

We'd also deprecate the old `SingleAxisTracking` class.

We should resolve this issue before releasing the new Array code into the wild in 0.9.


Discussion:
I like the idea of depreciating the `SingleAxisTracking` class and wrapping tracking functionality more directly into `PVSystem` and `Array`. I don't quite picture yet how it would work on the user side. They can directly create a `SingleAxisTrackerArray` and then pass that to a `PVSystem`? Or create a `FixedTiltArray` and pass that? 

I think we should keep in mind though that when you have a tracking system you are probably very likely going to have a uniform system and a single `Array` per `PVsystem`. So if I am going to create a tracking PVSystem, I am likely going to want to create it straight from a `PVSystem` as the most direct route rather than having to create the array first. (Unless the intent is to depreciate that functionality eventually and push always creating an `Array` first).  In that sense, keeping `SingleAxisTracker` as a `PVSystem` class and just having it create a `SingleAxisTrackingArray` instead of a `Array` may be more user friendly. But I do think there is opportunity to come up with a system to wrap everything together better. 

I also like the simplicity of `Array` and `PVsystem`, and worry about now adding different types of `Array`. 

Just throwing this out there, what if `Array` had a `tracking_model` attribute that right now could be either `fixed` or `single_axis`? Depending on what is passed it sets the appropriate `get_iam` and `get_irradiance` methods, and initiates the appropriate default attributes (`surface_tilt`, `surface_azimuth`, `axis_angle`, `max_angle` etc)? 
> They can directly create a SingleAxisTrackerArray and then pass that to a PVSystem? Or create a FixedTiltArray and pass that?

Yes.

> I think we should keep in mind though that when you have a tracking system you are probably very likely going to have a uniform system and a single Array per PVsystem. 

True. The main application that I can see for mixing a `SingleAxisTrackerArray` with something else is for modeling systems with a mix of broken and working trackers, so it would look something like:

```python
other_params = {}  # module_parameters, etc
PVSystem([
    Array(surface_tilt=45, surface_azimuth=90, **other_params), 
    SingleAxisTrackerArray(**other_params)
])
```

> So if I am going to create a tracking PVSystem, I am likely going to want to create it straight from a PVSystem as the most direct route rather than having to create the array first. (Unless the intent is to depreciate that functionality eventually and push always creating an Array first). In that sense, keeping SingleAxisTracker as a PVSystem class and just having it create a SingleAxisTrackingArray instead of a Array may be more user friendly. 

We discussed deprecating that functionality but haven't committed to it. 

> Just throwing this out there, what if Array had a tracking_model attribute that right now could be either fixed or single_axis? Depending on what is passed it sets the appropriate get_iam and get_irradiance methods, and initiates the appropriate default attributes (surface_tilt, surface_azimuth, axis_angle, max_angle etc)?

Interesting idea. I could see this working at the `PVSystem` level so that you can retain the ability to create the system with a single function call despite the removal of `SingleAxisTracker`.

Patch:
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -11,6 +11,9 @@
 from urllib.request import urlopen
 import numpy as np
 import pandas as pd
+from dataclasses import dataclass
+from abc import ABC, abstractmethod
+from typing import Optional
 
 from pvlib._deprecation import deprecated
 
@@ -234,8 +237,7 @@ def __init__(self,
                 array_losses_parameters = _build_kwargs(['dc_ohmic_percent'],
                                                         losses_parameters)
             self.arrays = (Array(
-                surface_tilt,
-                surface_azimuth,
+                FixedMount(surface_tilt, surface_azimuth, racking_model),
                 albedo,
                 surface_type,
                 module,
@@ -244,7 +246,6 @@ def __init__(self,
                 temperature_model_parameters,
                 modules_per_string,
                 strings_per_inverter,
-                racking_model,
                 array_losses_parameters,
             ),)
         elif len(arrays) == 0:
@@ -299,7 +300,6 @@ def _validate_per_array(self, values, system_wide=False):
 
     @_unwrap_single_value
     def _infer_cell_type(self):
-
         """
         Examines module_parameters and maps the Technology key for the CEC
         database and the Material key for the Sandia database to a common
@@ -813,11 +813,13 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed):
         The Fuentes thermal model uses the module surface tilt for convection
         modeling. The SAM implementation of PVWatts hardcodes the surface tilt
         value at 30 degrees, ignoring whatever value is used for irradiance
-        transposition. This method defaults to using ``self.surface_tilt``, but
-        if you want to match the PVWatts behavior, you can override it by
-        including a ``surface_tilt`` value in ``temperature_model_parameters``.
+        transposition.  If you want to match the PVWatts behavior you can
+        either leave ``surface_tilt`` unspecified to use the PVWatts default
+        of 30, or specify a ``surface_tilt`` value in the Array's
+        ``temperature_model_parameters``.
 
-        The `temp_air` and `wind_speed` parameters may be passed as tuples
+        The `temp_air`, `wind_speed`, and `surface_tilt` parameters may be
+        passed as tuples
         to provide different values for each Array in the system. If not
         passed as a tuple then the same value is used for input to each Array.
         If passed as a tuple the length must be the same as the number of
@@ -867,7 +869,6 @@ def noct_sam_celltemp(self, poa_global, temp_air, wind_speed,
 
     @_unwrap_single_value
     def first_solar_spectral_loss(self, pw, airmass_absolute):
-
         """
         Use the :py:func:`first_solar_spectral_correction` function to
         calculate the spectral loss modifier. The model coefficients are
@@ -1165,25 +1166,25 @@ def temperature_model_parameters(self, value):
     @_unwrap_single_value
     @_check_deprecated_passthrough
     def surface_tilt(self):
-        return tuple(array.surface_tilt for array in self.arrays)
+        return tuple(array.mount.surface_tilt for array in self.arrays)
 
     @surface_tilt.setter
     @_check_deprecated_passthrough
     def surface_tilt(self, value):
         for array in self.arrays:
-            array.surface_tilt = value
+            array.mount.surface_tilt = value
 
     @property
     @_unwrap_single_value
     @_check_deprecated_passthrough
     def surface_azimuth(self):
-        return tuple(array.surface_azimuth for array in self.arrays)
+        return tuple(array.mount.surface_azimuth for array in self.arrays)
 
     @surface_azimuth.setter
     @_check_deprecated_passthrough
     def surface_azimuth(self, value):
         for array in self.arrays:
-            array.surface_azimuth = value
+            array.mount.surface_azimuth = value
 
     @property
     @_unwrap_single_value
@@ -1201,13 +1202,13 @@ def albedo(self, value):
     @_unwrap_single_value
     @_check_deprecated_passthrough
     def racking_model(self):
-        return tuple(array.racking_model for array in self.arrays)
+        return tuple(array.mount.racking_model for array in self.arrays)
 
     @racking_model.setter
     @_check_deprecated_passthrough
     def racking_model(self, value):
         for array in self.arrays:
-            array.racking_model = value
+            array.mount.racking_model = value
 
     @property
     @_unwrap_single_value
@@ -1243,20 +1244,16 @@ class Array:
     """
     An Array is a set of of modules at the same orientation.
 
-    Specifically, an array is defined by tilt, azimuth, the
+    Specifically, an array is defined by its mount, the
     module parameters, the number of parallel strings of modules
     and the number of modules on each string.
 
     Parameters
     ----------
-    surface_tilt: float or array-like, default 0
-        Surface tilt angles in decimal degrees.
-        The tilt angle is defined as degrees from horizontal
-        (e.g. surface facing up = 0, surface facing horizon = 90)
-
-    surface_azimuth: float or array-like, default 180
-        Azimuth angle of the module surface.
-        North=0, East=90, South=180, West=270.
+    mount: FixedMount, SingleAxisTrackerMount, or other
+        Mounting for the array, either on fixed-tilt racking or horizontal
+        single axis tracker. Mounting is used to determine module orientation.
+        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
@@ -1290,26 +1287,22 @@ class Array:
     strings: int, default 1
         Number of parallel strings in the array.
 
-    racking_model : None or string, default None
-        Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
-        Used to identify a parameter set for the SAPM cell temperature model.
-
     array_losses_parameters: None, dict or Series, default None.
         Supported keys are 'dc_ohmic_percent'.
 
+    name: None or str, default None
+        Name of Array instance.
     """
 
-    def __init__(self,
-                 surface_tilt=0, surface_azimuth=180,
+    def __init__(self, mount,
                  albedo=None, surface_type=None,
                  module=None, module_type=None,
                  module_parameters=None,
                  temperature_model_parameters=None,
                  modules_per_string=1, strings=1,
-                 racking_model=None, array_losses_parameters=None,
+                 array_losses_parameters=None,
                  name=None):
-        self.surface_tilt = surface_tilt
-        self.surface_azimuth = surface_azimuth
+        self.mount = mount
 
         self.surface_type = surface_type
         if albedo is None:
@@ -1324,7 +1317,6 @@ def __init__(self,
             self.module_parameters = module_parameters
 
         self.module_type = module_type
-        self.racking_model = racking_model
 
         self.strings = strings
         self.modules_per_string = modules_per_string
@@ -1343,10 +1335,11 @@ def __init__(self,
         self.name = name
 
     def __repr__(self):
-        attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module',
-                 'albedo', 'racking_model', 'module_type',
+        attrs = ['name', 'mount', 'module',
+                 'albedo', 'module_type',
                  'temperature_model_parameters',
                  'strings', 'modules_per_string']
+
         return 'Array:\n  ' + '\n  '.join(
             f'{attr}: {getattr(self, attr)}' for attr in attrs
         )
@@ -1354,7 +1347,7 @@ def __repr__(self):
     def _infer_temperature_model_params(self):
         # try to infer temperature model parameters from from racking_model
         # and module_type
-        param_set = f'{self.racking_model}_{self.module_type}'
+        param_set = f'{self.mount.racking_model}_{self.module_type}'
         if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']:
             return temperature._temperature_model_params('sapm', param_set)
         elif 'freestanding' in param_set:
@@ -1424,7 +1417,9 @@ def get_aoi(self, solar_zenith, solar_azimuth):
         aoi : Series
             Then angle of incidence.
         """
-        return irradiance.aoi(self.surface_tilt, self.surface_azimuth,
+        orientation = self.mount.get_orientation(solar_zenith, solar_azimuth)
+        return irradiance.aoi(orientation['surface_tilt'],
+                              orientation['surface_azimuth'],
                               solar_zenith, solar_azimuth)
 
     def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
@@ -1473,8 +1468,9 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
         if airmass is None:
             airmass = atmosphere.get_relative_airmass(solar_zenith)
 
-        return irradiance.get_total_irradiance(self.surface_tilt,
-                                               self.surface_azimuth,
+        orientation = self.mount.get_orientation(solar_zenith, solar_azimuth)
+        return irradiance.get_total_irradiance(orientation['surface_tilt'],
+                                               orientation['surface_azimuth'],
                                                solar_zenith, solar_azimuth,
                                                dni, ghi, dhi,
                                                dni_extra=dni_extra,
@@ -1595,13 +1591,11 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model,
             func = temperature.fuentes
             required = _build_tcell_args(['noct_installed'])
             optional = _build_kwargs([
-                'module_height', 'wind_height', 'emissivity', 'absorption',
+                'wind_height', 'emissivity', 'absorption',
                 'surface_tilt', 'module_width', 'module_length'],
                 self.temperature_model_parameters)
-            # default to using the Array attribute, but allow user to override
-            # with a custom surface_tilt value in temperature_model_parameters
-            if 'surface_tilt' not in optional:
-                optional['surface_tilt'] = self.surface_tilt
+            if self.mount.module_height is not None:
+                optional['module_height'] = self.mount.module_height
         elif model == 'noct_sam':
             func = functools.partial(temperature.noct_sam,
                                      effective_irradiance=effective_irradiance)
@@ -1683,6 +1677,145 @@ def dc_ohms_from_percent(self):
             self.strings)
 
 
+@dataclass
+class AbstractMount(ABC):
+    """
+    A base class for Mount classes to extend. It is not intended to be
+    instantiated directly.
+    """
+
+    @abstractmethod
+    def get_orientation(self, solar_zenith, solar_azimuth):
+        """
+        Determine module orientation.
+
+        Parameters
+        ----------
+        solar_zenith : numeric
+            Solar apparent zenith angle [degrees]
+        solar_azimuth : numeric
+            Solar azimuth angle [degrees]
+
+        Returns
+        -------
+        orientation : dict-like
+            A dict-like object with keys `'surface_tilt', 'surface_azimuth'`
+            (typically a dict or pandas.DataFrame)
+        """
+
+
+@dataclass
+class FixedMount(AbstractMount):
+    """
+    Racking at fixed (static) orientation.
+
+    Parameters
+    ----------
+    surface_tilt : float, default 0
+        Surface tilt angle. The tilt angle is defined as angle from horizontal
+        (e.g. surface facing up = 0, surface facing horizon = 90) [degrees]
+
+    surface_azimuth : float, default 180
+        Azimuth angle of the module surface. North=0, East=90, South=180,
+        West=270. [degrees]
+
+    racking_model : str, optional
+        Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
+        Used to identify a parameter set for the SAPM cell temperature model.
+
+    module_height : float, optional
+       The height above ground of the center of the module [m]. Used for
+       the Fuentes cell temperature model.
+    """
+
+    surface_tilt: float = 0.0
+    surface_azimuth: float = 180.0
+    racking_model: Optional[str] = None
+    module_height: Optional[float] = None
+
+    def get_orientation(self, solar_zenith, solar_azimuth):
+        # note -- docstring is automatically inherited from AbstractMount
+        return {
+            'surface_tilt': self.surface_tilt,
+            'surface_azimuth': self.surface_azimuth,
+        }
+
+
+@dataclass
+class SingleAxisTrackerMount(AbstractMount):
+    """
+    Single-axis tracker racking for dynamic solar tracking.
+
+    Parameters
+    ----------
+    axis_tilt : float, default 0
+        The tilt of the axis of rotation (i.e, the y-axis defined by
+        axis_azimuth) with respect to horizontal. [degrees]
+
+    axis_azimuth : float, default 180
+        A value denoting the compass direction along which the axis of
+        rotation lies, measured east of north. [degrees]
+
+    max_angle : float, default 90
+        A value denoting the maximum rotation angle
+        of the one-axis tracker from its horizontal position (horizontal
+        if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
+        to rotate to a vertical position to point the panel towards a
+        horizon. max_angle of 180 degrees allows for full rotation. [degrees]
+
+    backtrack : bool, default True
+        Controls whether the tracker has the capability to "backtrack"
+        to avoid row-to-row shading. False denotes no backtrack
+        capability. True denotes backtrack capability.
+
+    gcr : float, default 2.0/7.0
+        A value denoting the ground coverage ratio of a tracker system
+        which utilizes backtracking; i.e. the ratio between the PV array
+        surface area to total ground area. A tracker system with modules
+        2 meters wide, centered on the tracking axis, with 6 meters
+        between the tracking axes has a gcr of 2/6=0.333. If gcr is not
+        provided, a gcr of 2/7 is default. gcr must be <=1. [unitless]
+
+    cross_axis_tilt : float, default 0.0
+        The angle, relative to horizontal, of the line formed by the
+        intersection between the slope containing the tracker axes and a plane
+        perpendicular to the tracker axes. Cross-axis tilt should be specified
+        using a right-handed convention. For example, trackers with axis
+        azimuth of 180 degrees (heading south) will have a negative cross-axis
+        tilt if the tracker axes plane slopes down to the east and positive
+        cross-axis tilt if the tracker axes plane slopes up to the east. Use
+        :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
+        `cross_axis_tilt`. [degrees]
+
+    racking_model : str, optional
+        Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
+        Used to identify a parameter set for the SAPM cell temperature model.
+
+    module_height : float, optional
+       The height above ground of the center of the module [m]. Used for
+       the Fuentes cell temperature model.
+    """
+    axis_tilt: float = 0.0
+    axis_azimuth: float = 0.0
+    max_angle: float = 90.0
+    backtrack: bool = True
+    gcr: float = 2.0/7.0
+    cross_axis_tilt: float = 0.0
+    racking_model: Optional[str] = None
+    module_height: Optional[float] = None
+
+    def get_orientation(self, solar_zenith, solar_azimuth):
+        # note -- docstring is automatically inherited from AbstractMount
+        from pvlib import tracking  # avoid circular import issue
+        tracking_data = tracking.singleaxis(
+            solar_zenith, solar_azimuth,
+            self.axis_tilt, self.axis_azimuth,
+            self.max_angle, self.backtrack,
+            self.gcr, self.cross_axis_tilt
+        )
+        return tracking_data
+
+
 def calcparams_desoto(effective_irradiance, temp_cell,
                       alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
                       EgRef=1.121, dEgdT=-0.0002677,
diff --git a/pvlib/tracking.py b/pvlib/tracking.py
--- a/pvlib/tracking.py
+++ b/pvlib/tracking.py
@@ -2,10 +2,14 @@
 import pandas as pd
 
 from pvlib.tools import cosd, sind, tand
-from pvlib.pvsystem import PVSystem, _unwrap_single_value
+from pvlib.pvsystem import (
+    PVSystem, Array, SingleAxisTrackerMount, _unwrap_single_value
+)
 from pvlib import irradiance, atmosphere
+from pvlib._deprecation import deprecated
 
 
+@deprecated('0.9.0', alternative='PVSystem with SingleAxisTrackerMount')
 class SingleAxisTracker(PVSystem):
     """
     A class for single-axis trackers that inherits the PV modeling methods from
@@ -76,20 +80,31 @@ class SingleAxisTracker(PVSystem):
     def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90,
                  backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs):
 
-        arrays = kwargs.get('arrays', [])
-        if len(arrays) > 1:
-            raise ValueError("SingleAxisTracker does not support "
-                             "multiple arrays.")
-        elif len(arrays) == 1:
-            surface_tilt = arrays[0].surface_tilt
-            surface_azimuth = arrays[0].surface_azimuth
-            if surface_tilt is not None or surface_azimuth is not None:
-                raise ValueError(
-                    "Array must not have surface_tilt or "
-                    "surface_azimuth assigned. You must pass an "
-                    "Array with these fields set to None."
-                )
-
+        mount_kwargs = {
+            k: kwargs.pop(k) for k in ['racking_model', 'module_height']
+            if k in kwargs
+        }
+        mount = SingleAxisTrackerMount(axis_tilt, axis_azimuth, max_angle,
+                                       backtrack, gcr, cross_axis_tilt,
+                                       **mount_kwargs)
+
+        array_defaults = {
+            'albedo': None, 'surface_type': None, 'module': None,
+            'module_type': None, 'module_parameters': None,
+            'temperature_model_parameters': None,
+            'modules_per_string': 1,
+        }
+        array_kwargs = {
+            key: kwargs.get(key, array_defaults[key]) for key in array_defaults
+        }
+        # strings/strings_per_inverter is a special case
+        array_kwargs['strings'] = kwargs.get('strings_per_inverter', 1)
+
+        array = Array(mount=mount, **array_kwargs)
+        pass_through_kwargs = {  # other args to pass to PVSystem()
+            k: v for k, v in kwargs.items() if k not in array_defaults
+        }
+        # leave these in case someone is using them
         self.axis_tilt = axis_tilt
         self.axis_azimuth = axis_azimuth
         self.max_angle = max_angle
@@ -97,10 +112,10 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90,
         self.gcr = gcr
         self.cross_axis_tilt = cross_axis_tilt
 
-        kwargs['surface_tilt'] = None
-        kwargs['surface_azimuth'] = None
+        pass_through_kwargs['surface_tilt'] = None
+        pass_through_kwargs['surface_azimuth'] = None
 
-        super().__init__(**kwargs)
+        super().__init__(arrays=[array], **pass_through_kwargs)
 
     def __repr__(self):
         attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr',


Test patch:
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
@@ -55,13 +55,13 @@ def cec_dc_snl_ac_arrays(cec_module_cs5p_220m, cec_inverter_parameters,
     module_parameters['dEgdT'] = -0.0002677
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     array_one = pvsystem.Array(
-        surface_tilt=32.2, surface_azimuth=180,
+        mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180),
         module=module_parameters['Name'],
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
     )
     array_two = pvsystem.Array(
-        surface_tilt=42.2, surface_azimuth=220,
+        mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220),
         module=module_parameters['Name'],
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
@@ -109,13 +109,13 @@ def pvsyst_dc_snl_ac_arrays(pvsyst_module_params, cec_inverter_parameters,
     module_parameters['b'] = 0.05
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     array_one = pvsystem.Array(
-        surface_tilt=32.2, surface_azimuth=180,
+        mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180),
         module=module,
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
     )
     array_two = pvsystem.Array(
-        surface_tilt=42.2, surface_azimuth=220,
+        mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220),
         module=module,
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
@@ -172,12 +172,12 @@ def pvwatts_dc_pvwatts_ac_system_arrays(sapm_temperature_cs5p_220m):
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95}
     array_one = pvsystem.Array(
-        surface_tilt=32.2, surface_azimuth=180,
+        mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180),
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
     )
     array_two = pvsystem.Array(
-        surface_tilt=42.2, surface_azimuth=220,
+        mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220),
         module_parameters=module_parameters.copy(),
         temperature_model_parameters=temp_model_params.copy()
     )
@@ -288,13 +288,15 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters,
     module = 'Canadian_Solar_CS5P_220M___2009_'
     module_parameters = sapm_module_params.copy()
     temp_model_params = sapm_temperature_cs5p_220m.copy()
-    array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180,
+    array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32,
+                                                         surface_azimuth=180),
                                albedo=0.2, module=module,
                                module_parameters=module_parameters,
                                temperature_model_parameters=temp_model_params,
                                modules_per_string=1,
                                strings=1)
-    array_two = pvsystem.Array(surface_tilt=15, surface_azimuth=180,
+    array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=15,
+                                                         surface_azimuth=180),
                                albedo=0.2, module=module,
                                module_parameters=module_parameters,
                                temperature_model_parameters=temp_model_params,
@@ -312,13 +314,15 @@ def sapm_dc_snl_ac_system_same_arrays(sapm_module_params,
     module = 'Canadian_Solar_CS5P_220M___2009_'
     module_parameters = sapm_module_params.copy()
     temp_model_params = sapm_temperature_cs5p_220m.copy()
-    array_one = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180,
+    array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2,
+                                                         surface_azimuth=180),
                                module=module,
                                module_parameters=module_parameters,
                                temperature_model_parameters=temp_model_params,
                                modules_per_string=1,
                                strings=1)
-    array_two = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180,
+    array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2,
+                                                         surface_azimuth=180),
                                module=module,
                                module_parameters=module_parameters,
                                temperature_model_parameters=temp_model_params,
@@ -365,12 +369,12 @@ def multi_array_sapm_dc_snl_ac_system(
     temp_model_parameters = sapm_temperature_cs5p_220m.copy()
     inverter_parameters = cec_inverter_parameters
     array_one = pvsystem.Array(
-        surface_tilt=32.2, surface_azimuth=180,
+        mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180),
         module_parameters=module_parameters,
         temperature_model_parameters=temp_model_parameters
     )
     array_two = pvsystem.Array(
-        surface_tilt=32.2, surface_azimuth=220,
+        mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=220),
         module_parameters=module_parameters,
         temperature_model_parameters=temp_model_parameters
     )
@@ -696,7 +700,7 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location,
     weather['wind_speed'] = 5
     weather['temp_air'] = 10
     sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = {
-        'noct_installed': 45
+        'noct_installed': 45, 'surface_tilt': 30,
     }
     mc = ModelChain(sapm_dc_snl_ac_system, location)
     mc.temperature_model = 'fuentes'
@@ -730,12 +734,13 @@ def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location,
 
 
 def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker):
-    system = SingleAxisTracker(
-        module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,
-        temperature_model_parameters=(
-            sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
-        ),
-        inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = SingleAxisTracker(
+            module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,  # noqa: E501
+            temperature_model_parameters=(
+                sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
+            ),
+            inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
     mocker.spy(system, 'singleaxis')
     mc = ModelChain(system, location)
     mc.run_model(weather)
@@ -751,12 +756,13 @@ def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker):
 
 def test_run_model_tracker_list(
         sapm_dc_snl_ac_system, location, weather, mocker):
-    system = SingleAxisTracker(
-        module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,
-        temperature_model_parameters=(
-            sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
-        ),
-        inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = SingleAxisTracker(
+            module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,  # noqa: E501
+            temperature_model_parameters=(
+                sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
+            ),
+            inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
     mocker.spy(system, 'singleaxis')
     mc = ModelChain(system, location)
     mc.run_model([weather])
@@ -1023,12 +1029,13 @@ def test_run_model_from_poa_arrays_solar_position_weather(
 
 def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location,
                                      total_irrad):
-    system = SingleAxisTracker(
-        module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,
-        temperature_model_parameters=(
-            sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
-        ),
-        inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = SingleAxisTracker(
+            module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters,  # noqa: E501
+            temperature_model_parameters=(
+                sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters
+            ),
+            inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters)
     mc = ModelChain(system, location, aoi_model='no_loss',
                     spectral_model='no_loss')
     ac = mc.run_model_from_poa(total_irrad).results.ac
@@ -1439,6 +1446,7 @@ def test_infer_ac_model_invalid_params(location):
     module_parameters = {'pdc0': 1, 'gamma_pdc': 1}
     system = pvsystem.PVSystem(
         arrays=[pvsystem.Array(
+            mount=pvsystem.FixedMount(0, 180),
             module_parameters=module_parameters
         )],
         inverter_parameters={'foo': 1, 'bar': 2}
@@ -1953,10 +1961,13 @@ def test_inconsistent_array_params(location,
     different_module_system = pvsystem.PVSystem(
         arrays=[
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=sapm_module_params),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=cec_module_params),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=cec_module_params)]
     )
     with pytest.raises(ValueError, match=module_error):
@@ -1964,16 +1975,19 @@ def test_inconsistent_array_params(location,
     different_temp_system = pvsystem.PVSystem(
         arrays=[
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=cec_module_params,
                 temperature_model_parameters={'a': 1,
                                               'b': 1,
                                               'deltaT': 1}),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=cec_module_params,
                 temperature_model_parameters={'a': 2,
                                               'b': 2,
                                               'deltaT': 2}),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters=cec_module_params,
                 temperature_model_parameters={'b': 3, 'deltaT': 3})]
     )
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
@@ -15,6 +15,7 @@
 from pvlib import iam as _iam
 from pvlib import irradiance
 from pvlib.location import Location
+from pvlib.pvsystem import FixedMount
 from pvlib import temperature
 from pvlib._deprecation import pvlibDeprecationWarning
 
@@ -36,8 +37,10 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params):
 def test_PVSystem_multi_array_get_iam():
     model_params = {'b': 0.05}
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(module_parameters=model_params),
-                pvsystem.Array(module_parameters=model_params)]
+        arrays=[pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
+                               module_parameters=model_params),
+                pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
+                               module_parameters=model_params)]
     )
     iam = system.get_iam((1, 5), iam_model='ashrae')
     assert len(iam) == 2
@@ -226,8 +229,10 @@ def test_PVSystem_sapm(sapm_module_params, mocker):
 
 def test_PVSystem_multi_array_sapm(sapm_module_params):
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(module_parameters=sapm_module_params),
-                pvsystem.Array(module_parameters=sapm_module_params)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params),
+                pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params)]
     )
     effective_irradiance = (100, 500)
     temp_cell = (15, 25)
@@ -274,8 +279,10 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params, mocker):
 
 def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params):
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(module_parameters=sapm_module_params),
-                pvsystem.Array(module_parameters=sapm_module_params)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params),
+                pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params)]
     )
     loss_one, loss_two = system.sapm_spectral_loss(2)
     assert loss_one == loss_two
@@ -308,10 +315,12 @@ def test_PVSystem_multi_array_first_solar_spectral_loss():
     system = pvsystem.PVSystem(
         arrays=[
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters={'Technology': 'mc-Si'},
                 module_type='multisi'
             ),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 module_parameters={'Technology': 'mc-Si'},
                 module_type='multisi'
             )
@@ -363,8 +372,10 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
 
 def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params):
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(module_parameters=sapm_module_params),
-                pvsystem.Array(module_parameters=sapm_module_params)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params),
+                pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               module_parameters=sapm_module_params)]
     )
     poa_direct = (500, 900)
     poa_diffuse = (50, 100)
@@ -397,10 +408,12 @@ def two_array_system(pvsyst_module_params, cec_module_params):
     return pvsystem.PVSystem(
         arrays=[
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 temperature_model_parameters=temperature_model,
                 module_parameters=module_params
             ),
             pvsystem.Array(
+                mount=pvsystem.FixedMount(0, 180),
                 temperature_model_parameters=temperature_model,
                 module_parameters=module_params
             )
@@ -457,8 +470,10 @@ def test_PVSystem_multi_array_sapm_celltemp_different_arrays():
     temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
         'close_mount_glass_glass']
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(temperature_model_parameters=temp_model_one),
-                pvsystem.Array(temperature_model_parameters=temp_model_two)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               temperature_model_parameters=temp_model_one),
+                pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               temperature_model_parameters=temp_model_two)]
     )
     temp_one, temp_two = system.get_cell_temperature(
         (1000, 1000), 25, 1, model='sapm'
@@ -555,6 +570,7 @@ def test_PVSystem_multi_array_celltemp_functions(model, two_array_system):
     irrad_two = pd.Series(500, index=times)
     temp_air = pd.Series(25, index=times)
     wind_speed = pd.Series(1, index=times)
+
     temp_one, temp_two = two_array_system.get_cell_temperature(
         (irrad_one, irrad_two), temp_air, wind_speed, model=model)
     assert (temp_one != temp_two).all()
@@ -667,7 +683,7 @@ def test_PVSystem_multi_array_celltemp_poa_length_mismatch(
 
 def test_PVSystem_fuentes_celltemp(mocker):
     noct_installed = 45
-    temp_model_params = {'noct_installed': noct_installed}
+    temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 0}
     system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
     spy = mocker.spy(temperature, 'fuentes')
     index = pd.date_range('2019-01-01 11:00', freq='h', periods=3)
@@ -683,47 +699,38 @@ def test_PVSystem_fuentes_celltemp(mocker):
                                        name='tmod'))
 
 
-def test_PVSystem_fuentes_celltemp_override(mocker):
-    # test that the surface_tilt value in the cell temp calculation can be
-    # overridden but defaults to the surface_tilt attribute of the PVSystem
+def test_PVSystem_fuentes_module_height(mocker):
+    # check that fuentes picks up Array.mount.module_height correctly
+    # (temperature.fuentes defaults to 5 for module_height)
+    array = pvsystem.Array(mount=FixedMount(module_height=3),
+                           temperature_model_parameters={'noct_installed': 45})
     spy = mocker.spy(temperature, 'fuentes')
-
-    noct_installed = 45
     index = pd.date_range('2019-01-01 11:00', freq='h', periods=3)
     temps = pd.Series(25, index)
     irrads = pd.Series(1000, index)
     winds = pd.Series(1, index)
-
-    # uses default value
-    temp_model_params = {'noct_installed': noct_installed}
-    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params,
-                               surface_tilt=20)
-    system.get_cell_temperature(irrads, temps, winds, model='fuentes')
-    assert spy.call_args[1]['surface_tilt'] == 20
-
-    # can be overridden
-    temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 30}
-    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params,
-                               surface_tilt=20)
-    system.get_cell_temperature(irrads, temps, winds, model='fuentes')
-    assert spy.call_args[1]['surface_tilt'] == 30
+    _ = array.get_cell_temperature(irrads, temps, winds, model='fuentes')
+    assert spy.call_args[1]['module_height'] == 3
 
 
 def test_Array__infer_temperature_model_params():
-    array = pvsystem.Array(module_parameters={},
-                           racking_model='open_rack',
+    array = pvsystem.Array(mount=FixedMount(0, 180,
+                                            racking_model='open_rack'),
+                           module_parameters={},
                            module_type='glass_polymer')
     expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
         'sapm']['open_rack_glass_polymer']
     assert expected == array._infer_temperature_model_params()
-    array = pvsystem.Array(module_parameters={},
-                           racking_model='freestanding',
+    array = pvsystem.Array(mount=FixedMount(0, 180,
+                                            racking_model='freestanding'),
+                           module_parameters={},
                            module_type='glass_polymer')
     expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
         'pvsyst']['freestanding']
     assert expected == array._infer_temperature_model_params()
-    array = pvsystem.Array(module_parameters={},
-                           racking_model='insulated',
+    array = pvsystem.Array(mount=FixedMount(0, 180,
+                                            racking_model='insulated'),
+                           module_parameters={},
                            module_type=None)
     expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
         'pvsyst']['insulated']
@@ -731,7 +738,8 @@ def test_Array__infer_temperature_model_params():
 
 
 def test_Array__infer_cell_type():
-    array = pvsystem.Array(module_parameters={})
+    array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180),
+                           module_parameters={})
     assert array._infer_cell_type() is None
 
 
@@ -1434,8 +1442,10 @@ def test_PVSystem_scale_voltage_current_power(mocker):
 def test_PVSystem_multi_scale_voltage_current_power(mocker):
     data = (1, 2)
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(modules_per_string=2, strings=3),
-                pvsystem.Array(modules_per_string=3, strings=5)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               modules_per_string=2, strings=3),
+                pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               modules_per_string=3, strings=5)]
     )
     m = mocker.patch(
         'pvlib.pvsystem.scale_voltage_current_power', autospec=True
@@ -1482,7 +1492,8 @@ def test_PVSystem_snlinverter(cec_inverter_parameters):
 def test_PVSystem_get_ac_sandia_multi(cec_inverter_parameters, mocker):
     inv_fun = mocker.spy(inverter, 'sandia_multi')
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(), pvsystem.Array()],
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)),
+                pvsystem.Array(pvsystem.FixedMount(0, 180))],
         inverter=cec_inverter_parameters['Name'],
         inverter_parameters=cec_inverter_parameters,
     )
@@ -1529,7 +1540,8 @@ def test_PVSystem_get_ac_pvwatts_multi(
     systems = [pvwatts_system_defaults, pvwatts_system_kwargs]
     for base_sys, exp in zip(systems, expected):
         system = pvsystem.PVSystem(
-            arrays=[pvsystem.Array(), pvsystem.Array()],
+            arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)),
+                    pvsystem.Array(pvsystem.FixedMount(0, 180),)],
             inverter_parameters=base_sys.inverter_parameters,
         )
         pdcs = pd.Series([0., 25., 50.])
@@ -1571,7 +1583,7 @@ def test_PVSystem_get_ac_single_array_tuple_input(
         'sandia': pd.Series([-0.020000, 132.004308, 250.000000])
     }
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array()],
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180))],
         inverter_parameters=inverter_parameters[model]
     )
     ac = system.get_ac(p_dc=(pdcs[model],), v_dc=(vdcs[model],), model=model)
@@ -1597,7 +1609,8 @@ def test_PVSystem_get_ac_adr(adr_inverter_parameters, mocker):
 
 def test_PVSystem_get_ac_adr_multi(adr_inverter_parameters):
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(), pvsystem.Array()],
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)),
+                pvsystem.Array(pvsystem.FixedMount(0, 180))],
         inverter_parameters=adr_inverter_parameters,
     )
     pdcs = pd.Series([135, 1232, 1170, 420, 551])
@@ -1625,9 +1638,12 @@ def test_PVSystem_creation():
 
 
 def test_PVSystem_multiple_array_creation():
-    array_one = pvsystem.Array(surface_tilt=32)
-    array_two = pvsystem.Array(surface_tilt=15, module_parameters={'pdc0': 1})
+    array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32))
+    array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=15),
+                               module_parameters={'pdc0': 1})
     pv_system = pvsystem.PVSystem(arrays=[array_one, array_two])
+    assert pv_system.arrays[0].module_parameters == {}
+    assert pv_system.arrays[1].module_parameters == {'pdc0': 1}
     assert pv_system.arrays == (array_one, array_two)
     with pytest.raises(TypeError):
         pvsystem.PVSystem(arrays=array_one)
@@ -1641,8 +1657,10 @@ def test_PVSystem_get_aoi():
 
 def test_PVSystem_multiple_array_get_aoi():
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(surface_tilt=15, surface_azimuth=135),
-                pvsystem.Array(surface_tilt=32, surface_azimuth=135)]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=15,
+                                                   surface_azimuth=135)),
+                pvsystem.Array(pvsystem.FixedMount(surface_tilt=32,
+                                                   surface_azimuth=135))]
     )
     aoi_one, aoi_two = system.get_aoi(30, 225)
     assert np.round(aoi_two, 4) == 42.7408
@@ -1703,8 +1721,10 @@ def test_PVSystem_get_irradiance_model(mocker):
 
 
 def test_PVSystem_multi_array_get_irradiance():
-    array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=135)
-    array_two = pvsystem.Array(surface_tilt=5, surface_azimuth=150)
+    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',
@@ -1743,8 +1763,8 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad():
     for each array when different GHI/DHI/DNI input is given. For the later
     case we verify that the correct irradiance data is passed to each array.
     """
-    array_one = pvsystem.Array()
-    array_two = pvsystem.Array()
+    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',
@@ -1804,40 +1824,6 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad():
     assert not array_irrad[0].equals(array_irrad[1])
 
 
-def test_PVSystem_change_surface_azimuth():
-    system = pvsystem.PVSystem(surface_azimuth=180)
-    with pytest.warns(pvlibDeprecationWarning):
-        assert system.surface_azimuth == 180
-    with pytest.warns(pvlibDeprecationWarning):
-        system.surface_azimuth = 90
-    with pytest.warns(pvlibDeprecationWarning):
-        assert system.surface_azimuth == 90
-
-
-def test_PVSystem_get_albedo():
-    system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(albedo=0.5)]
-    )
-    with pytest.warns(pvlibDeprecationWarning):
-        assert system.albedo == 0.5
-
-
-def test_PVSystem_modules_per_string():
-    system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(modules_per_string=5)]
-    )
-    with pytest.warns(pvlibDeprecationWarning):
-        assert system.modules_per_string == 5
-
-
-def test_PVSystem_strings_per_inverter():
-    system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(strings=5)]
-    )
-    with pytest.warns(pvlibDeprecationWarning):
-        assert system.strings_per_inverter == 5
-
-
 @fail_on_pvlib_version('0.10')
 @pytest.mark.parametrize('attr', ['module_parameters', 'module', 'module_type',
                                   'temperature_model_parameters', 'albedo',
@@ -1845,8 +1831,8 @@ def test_PVSystem_strings_per_inverter():
                                   'racking_model', 'modules_per_string',
                                   'strings_per_inverter'])
 def test_PVSystem_multi_array_attributes(attr):
-    array_one = pvsystem.Array()
-    array_two = pvsystem.Array()
+    array_one = pvsystem.Array(pvsystem.FixedMount())
+    array_two = pvsystem.Array(pvsystem.FixedMount())
     system = pvsystem.PVSystem(arrays=[array_one, array_two])
     with pytest.raises(AttributeError):
         getattr(system, attr)
@@ -1871,23 +1857,23 @@ def test_PVSystem___repr__():
   name: pv ftw
   Array:
     name: None
-    surface_tilt: 0
-    surface_azimuth: 180
+    mount: FixedMount(surface_tilt=0, surface_azimuth=180, racking_model=None, module_height=None)
     module: blah
     albedo: 0.25
-    racking_model: None
     module_type: None
     temperature_model_parameters: {'a': -3.56}
     strings: 1
     modules_per_string: 1
-  inverter: blarg"""
+  inverter: blarg"""  # noqa: E501
     assert system.__repr__() == expected
 
 
 def test_PVSystem_multi_array___repr__():
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(surface_tilt=30, surface_azimuth=100),
-                pvsystem.Array(surface_tilt=20, surface_azimuth=220,
+        arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=30,
+                                                   surface_azimuth=100)),
+                pvsystem.Array(pvsystem.FixedMount(surface_tilt=20,
+                                                   surface_azimuth=220),
                                name='foo')],
         inverter='blarg',
     )
@@ -1895,36 +1881,32 @@ def test_PVSystem_multi_array___repr__():
   name: None
   Array:
     name: None
-    surface_tilt: 30
-    surface_azimuth: 100
+    mount: FixedMount(surface_tilt=30, surface_azimuth=100, racking_model=None, module_height=None)
     module: None
     albedo: 0.25
-    racking_model: None
     module_type: None
     temperature_model_parameters: {}
     strings: 1
     modules_per_string: 1
   Array:
     name: foo
-    surface_tilt: 20
-    surface_azimuth: 220
+    mount: FixedMount(surface_tilt=20, surface_azimuth=220, racking_model=None, module_height=None)
     module: None
     albedo: 0.25
-    racking_model: None
     module_type: None
     temperature_model_parameters: {}
     strings: 1
     modules_per_string: 1
-  inverter: blarg"""
+  inverter: blarg"""  # noqa: E501
     assert expected == system.__repr__()
 
 
 def test_Array___repr__():
     array = pvsystem.Array(
-        surface_tilt=10, surface_azimuth=100,
+        mount=pvsystem.FixedMount(surface_tilt=10, surface_azimuth=100,
+                                  racking_model='close_mount'),
         albedo=0.15, module_type='glass_glass',
         temperature_model_parameters={'a': -3.56},
-        racking_model='close_mount',
         module_parameters={'foo': 'bar'},
         modules_per_string=100,
         strings=10, module='baz',
@@ -1932,15 +1914,13 @@ def test_Array___repr__():
     )
     expected = """Array:
   name: biz
-  surface_tilt: 10
-  surface_azimuth: 100
+  mount: FixedMount(surface_tilt=10, surface_azimuth=100, racking_model='close_mount', module_height=None)
   module: baz
   albedo: 0.15
-  racking_model: close_mount
   module_type: glass_glass
   temperature_model_parameters: {'a': -3.56}
   strings: 10
-  modules_per_string: 100"""
+  modules_per_string: 100"""  # noqa: E501
     assert array.__repr__() == expected
 
 
@@ -2035,12 +2015,14 @@ def test_PVSystem_multiple_array_pvwatts_dc():
         'pdc0': 100, 'gamma_pdc': -0.003, 'temp_ref': 20
     }
     array_one = pvsystem.Array(
+        pvsystem.FixedMount(0, 180),
         module_parameters=array_one_module_parameters
     )
     array_two_module_parameters = {
         'pdc0': 150, 'gamma_pdc': -0.002, 'temp_ref': 25
     }
     array_two = pvsystem.Array(
+        pvsystem.FixedMount(0, 180),
         module_parameters=array_two_module_parameters
     )
     system = pvsystem.PVSystem(arrays=[array_one, array_two])
@@ -2060,7 +2042,9 @@ def test_PVSystem_multiple_array_pvwatts_dc():
 
 def test_PVSystem_multiple_array_pvwatts_dc_value_error():
     system = pvsystem.PVSystem(
-        arrays=[pvsystem.Array(), pvsystem.Array(), pvsystem.Array()]
+        arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)),
+                pvsystem.Array(pvsystem.FixedMount(0, 180)),
+                pvsystem.Array(pvsystem.FixedMount(0, 180))]
     )
     error_message = 'Length mismatch for per-array parameter'
     with pytest.raises(ValueError, match=error_message):
@@ -2118,7 +2102,9 @@ def test_PVSystem_pvwatts_ac_kwargs(pvwatts_system_kwargs, mocker):
 
 def test_PVSystem_num_arrays():
     system_one = pvsystem.PVSystem()
-    system_two = pvsystem.PVSystem(arrays=[pvsystem.Array(), pvsystem.Array()])
+    system_two = pvsystem.PVSystem(arrays=[
+        pvsystem.Array(pvsystem.FixedMount(0, 180)),
+        pvsystem.Array(pvsystem.FixedMount(0, 180))])
     assert system_one.num_arrays == 1
     assert system_two.num_arrays == 2
 
@@ -2146,6 +2132,49 @@ def test_no_extra_kwargs():
         pvsystem.PVSystem(arbitrary_kwarg='value')
 
 
+def test_AbstractMount_constructor():
+    match = "Can't instantiate abstract class AbstractMount"
+    with pytest.raises(TypeError, match=match):
+        _ = pvsystem.AbstractMount()
+
+
+@pytest.fixture
+def fixed_mount():
+    return pvsystem.FixedMount(20, 180)
+
+
+@pytest.fixture
+def single_axis_tracker_mount():
+    return pvsystem.SingleAxisTrackerMount(axis_tilt=10, axis_azimuth=170,
+                                           max_angle=45, backtrack=False,
+                                           gcr=0.4, cross_axis_tilt=-5)
+
+
+def test_FixedMount_constructor(fixed_mount):
+    assert fixed_mount.surface_tilt == 20
+    assert fixed_mount.surface_azimuth == 180
+
+
+def test_FixedMount_get_orientation(fixed_mount):
+    expected = {'surface_tilt': 20, 'surface_azimuth': 180}
+    assert fixed_mount.get_orientation(45, 130) == expected
+
+
+def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount):
+    expected = dict(axis_tilt=10, axis_azimuth=170, max_angle=45,
+                    backtrack=False, gcr=0.4, cross_axis_tilt=-5)
+    for attr_name, expected_value in expected.items():
+        assert getattr(single_axis_tracker_mount, attr_name) == expected_value
+
+
+def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount):
+    expected = {'surface_tilt': 19.29835284, 'surface_azimuth': 229.7643755}
+    actual = single_axis_tracker_mount.get_orientation(45, 190)
+    for key, expected_value in expected.items():
+        err_msg = f"{key} value incorrect"
+        assert actual[key] == pytest.approx(expected_value), err_msg
+
+
 def test_dc_ohms_from_percent():
     expected = .1425
     out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1)
@@ -2183,7 +2212,8 @@ def test_Array_dc_ohms_from_percent(mocker):
 
     expected = .1425
 
-    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+    array = pvsystem.Array(pvsystem.FixedMount(0, 180),
+                           array_losses_parameters={'dc_ohmic_percent': 3},
                            module_parameters={'I_mp_ref': 8,
                                               'V_mp_ref': 38})
     out = array.dc_ohms_from_percent()
@@ -2196,7 +2226,8 @@ def test_Array_dc_ohms_from_percent(mocker):
     )
     assert_allclose(out, expected)
 
-    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+    array = pvsystem.Array(pvsystem.FixedMount(0, 180),
+                           array_losses_parameters={'dc_ohmic_percent': 3},
                            module_parameters={'Impo': 8,
                                               'Vmpo': 38})
     out = array.dc_ohms_from_percent()
@@ -2209,7 +2240,8 @@ def test_Array_dc_ohms_from_percent(mocker):
     )
     assert_allclose(out, expected)
 
-    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+    array = pvsystem.Array(pvsystem.FixedMount(0, 180),
+                           array_losses_parameters={'dc_ohmic_percent': 3},
                            module_parameters={'Impp': 8,
                                               'Vmpp': 38})
     out = array.dc_ohms_from_percent()
@@ -2230,7 +2262,8 @@ def test_Array_dc_ohms_from_percent(mocker):
                               '{"V_mp_ref", "I_mp_Ref"}, '
                               '{"Vmpo", "Impo"}, or '
                               '{"Vmpp", "Impp"}.')):
-        array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3})
+        array = pvsystem.Array(pvsystem.FixedMount(0, 180),
+                               array_losses_parameters={'dc_ohmic_percent': 3})
         out = array.dc_ohms_from_percent()
 
 
@@ -2261,7 +2294,7 @@ def test_PVSystem_temperature_deprecated(funcname):
 ])
 def test_Array_temperature_missing_parameters(model, keys):
     # test that a nice error is raised when required temp params are missing
-    array = pvsystem.Array()
+    array = pvsystem.Array(pvsystem.FixedMount(0, 180))
     index = pd.date_range('2019-01-01', freq='h', periods=5)
     temps = pd.Series(25, index)
     irrads = pd.Series(1000, index)
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
@@ -6,8 +6,9 @@
 from numpy.testing import assert_allclose
 
 import pvlib
-from pvlib import tracking, pvsystem
+from pvlib import tracking
 from .conftest import DATA_DIR, assert_frame_equal
+from pvlib._deprecation import pvlibDeprecationWarning
 
 SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi',
                         'surface_azimuth', 'surface_tilt']
@@ -289,51 +290,11 @@ def test_low_sun_angles():
         assert_allclose(expected[k], v)
 
 
-def test_SingleAxisTracker_creation():
-    system = tracking.SingleAxisTracker(max_angle=45,
-                                        gcr=.25,
-                                        module='blah',
-                                        inverter='blarg')
-
-    assert system.max_angle == 45
-    assert system.gcr == .25
-    assert system.arrays[0].module == 'blah'
-    assert system.inverter == 'blarg'
-
-
-def test_SingleAxisTracker_one_array_only():
-    system = tracking.SingleAxisTracker(
-        arrays=[pvsystem.Array(
-            module='foo',
-            surface_tilt=None,
-            surface_azimuth=None
-        )]
-    )
-    assert system.arrays[0].module == 'foo'
-    with pytest.raises(ValueError,
-                       match="SingleAxisTracker does not support "
-                             r"multiple arrays\."):
-        tracking.SingleAxisTracker(
-            arrays=[pvsystem.Array(module='foo'),
-                    pvsystem.Array(module='bar')]
-        )
-    with pytest.raises(ValueError,
-                       match="Array must not have surface_tilt "):
-        tracking.SingleAxisTracker(arrays=[pvsystem.Array(module='foo')])
-    with pytest.raises(ValueError,
-                       match="Array must not have surface_tilt "):
-        tracking.SingleAxisTracker(
-            arrays=[pvsystem.Array(surface_azimuth=None)])
-    with pytest.raises(ValueError,
-                       match="Array must not have surface_tilt "):
-        tracking.SingleAxisTracker(
-            arrays=[pvsystem.Array(surface_tilt=None)])
-
-
 def test_SingleAxisTracker_tracking():
-    system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
-                                        axis_azimuth=180, gcr=2.0/7.0,
-                                        backtrack=True)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
+                                            axis_azimuth=180, gcr=2.0/7.0,
+                                            backtrack=True)
 
     apparent_zenith = pd.Series([30])
     apparent_azimuth = pd.Series([135])
@@ -353,9 +314,10 @@ def test_SingleAxisTracker_tracking():
     pvsyst_solar_height = 27.315
     pvsyst_axis_tilt = 20.
     pvsyst_axis_azimuth = 20.
-    pvsyst_system = tracking.SingleAxisTracker(
-        max_angle=60., axis_tilt=pvsyst_axis_tilt,
-        axis_azimuth=180+pvsyst_axis_azimuth, backtrack=False)
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsyst_system = tracking.SingleAxisTracker(
+            max_angle=60., axis_tilt=pvsyst_axis_tilt,
+            axis_azimuth=180+pvsyst_axis_azimuth, backtrack=False)
     # the definition of azimuth is different from PYsyst
     apparent_azimuth = pd.Series([180+pvsyst_solar_azimuth])
     apparent_zenith = pd.Series([90-pvsyst_solar_height])
@@ -371,9 +333,10 @@ def test_SingleAxisTracker_tracking():
 
 # see test_irradiance for more thorough testing
 def test_get_aoi():
-    system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
-                                        axis_azimuth=180, gcr=2.0/7.0,
-                                        backtrack=True)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
+                                            axis_azimuth=180, gcr=2.0/7.0,
+                                            backtrack=True)
     surface_tilt = np.array([30, 0])
     surface_azimuth = np.array([90, 270])
     solar_zenith = np.array([70, 10])
@@ -385,9 +348,10 @@ def test_get_aoi():
 
 
 def test_get_irradiance():
-    system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
-                                        axis_azimuth=180, gcr=2.0/7.0,
-                                        backtrack=True)
+    with pytest.warns(pvlibDeprecationWarning):
+        system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30,
+                                            axis_azimuth=180, gcr=2.0/7.0,
+                                            backtrack=True)
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     # latitude=32, longitude=-111
@@ -431,9 +395,10 @@ def test_get_irradiance():
 
 
 def test_SingleAxisTracker___repr__():
-    system = tracking.SingleAxisTracker(
-        max_angle=45, gcr=.25, module='blah', inverter='blarg',
-        temperature_model_parameters={'a': -3.56})
+    with pytest.warns(pvlibDeprecationWarning):
+        system = tracking.SingleAxisTracker(
+            max_angle=45, gcr=.25, module='blah', inverter='blarg',
+            temperature_model_parameters={'a': -3.56})
     expected = """SingleAxisTracker:
   axis_tilt: 0
   axis_azimuth: 0
@@ -444,16 +409,14 @@ def test_SingleAxisTracker___repr__():
   name: None
   Array:
     name: None
-    surface_tilt: None
-    surface_azimuth: None
+    mount: SingleAxisTrackerMount(axis_tilt=0, axis_azimuth=0, max_angle=45, backtrack=True, gcr=0.25, cross_axis_tilt=0.0, racking_model=None, module_height=None)
     module: blah
     albedo: 0.25
-    racking_model: None
     module_type: None
     temperature_model_parameters: {'a': -3.56}
     strings: 1
     modules_per_string: 1
-  inverter: blarg"""
+  inverter: blarg"""  # noqa: E501
     assert system.__repr__() == expected