Repository: pvlib/pvlib-python
Pull request #1168 (2021-02-15 17:36:52)

DC Ohmic Losses for pvsystem and modelchain
 - [x] partially addresses #1069 
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - [x] Tests added
 - [x] Updates entries to [`docs/sphinx/source/api.rst`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/api.rst) for API changes.
 - [ ] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [ ] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

Adds functionality for array level losses and adds functions for dc ohmic losses to `pvsystem` and `modelchain`. Replaces #1084 for compatibility with `Array` update.



Patch:
diff --git a/LICENSE b/LICENSE
index b4168b3..e468955 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2013-2020, Sandia National Laboratories and pvlib python Development Team
+Copyright (c) 2013-2021, Sandia National Laboratories and pvlib python Development Team
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 8805d19..373c879 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -238,9 +238,12 @@ PV temperature models
    temperature.faiman
    temperature.fuentes
    temperature.ross
+   temperature.noct_sam
    pvsystem.PVSystem.sapm_celltemp
    pvsystem.PVSystem.pvsyst_celltemp
    pvsystem.PVSystem.faiman_celltemp
+   pvsystem.PVSystem.fuentes_celltemp
+   pvsystem.PVSystem.noct_sam_celltemp
 
 Temperature Model Parameters
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -331,6 +334,8 @@ Pvsyst model
    temperature.pvsyst_cell
    pvsystem.calcparams_pvsyst
    pvsystem.singlediode
+   pvsystem.dc_ohms_from_percent
+   pvsystem.dc_ohmic_losses
 
 PVWatts model
 ^^^^^^^^^^^^^
@@ -389,6 +394,7 @@ Loss models
    :toctree: generated/
 
    pvsystem.combine_loss_factors
+   pvsystem.dc_ohms_from_percent
 
 Snow
 ----
@@ -609,12 +615,12 @@ ModelChain properties that are aliases for your specific modeling functions.
 .. autosummary::
    :toctree: generated/
 
-   modelchain.ModelChain.orientation_strategy
    modelchain.ModelChain.dc_model
    modelchain.ModelChain.ac_model
    modelchain.ModelChain.aoi_model
    modelchain.ModelChain.spectral_model
    modelchain.ModelChain.temperature_model
+   modelchain.ModelChain.dc_ohmic_model
    modelchain.ModelChain.losses_model
    modelchain.ModelChain.effective_irradiance_model
 
@@ -645,6 +651,8 @@ ModelChain model definitions.
    modelchain.ModelChain.pvsyst_temp
    modelchain.ModelChain.faiman_temp
    modelchain.ModelChain.fuentes_temp
+   modelchain.ModelChain.dc_ohmic_model
+   modelchain.ModelChain.no_dc_ohmic_loss
    modelchain.ModelChain.pvwatts_losses
    modelchain.ModelChain.no_extra_losses
 
diff --git a/docs/sphinx/source/introtutorial.rst b/docs/sphinx/source/introtutorial.rst
index c45260a..495b0c6 100644
--- a/docs/sphinx/source/introtutorial.rst
+++ b/docs/sphinx/source/introtutorial.rst
@@ -190,12 +190,6 @@ by examining the parameters defined for the module.
     from pvlib.location import Location
     from pvlib.modelchain import ModelChain
 
-    system = PVSystem(
-        module_parameters=module,
-        inverter_parameters=inverter,
-        temperature_model_parameters=temperature_model_parameters,
-    )
-
     energies = {}
     for location, weather in zip(coordinates, tmys):
         latitude, longitude, name, altitude, timezone = location
@@ -206,11 +200,15 @@ by examining the parameters defined for the module.
             altitude=altitude,
             tz=timezone,
         )
-        mc = ModelChain(
-            system,
-            location,
-            orientation_strategy='south_at_latitude_tilt',
+        system = PVSystem(
+            surface_tilt=latitude,
+            surface_azimuth=180,
+            module_parameters=module,
+            inverter_parameters=inverter,
+            temperature_model_parameters=temperature_model_parameters,
         )
+
+        mc = ModelChain(system, location)
         results = mc.run_model(weather)
         annual_energy = results.ac.sum()
         energies[name] = annual_energy
diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst
index 1377458..2c8e178 100644
--- a/docs/sphinx/source/whatsnew/v0.9.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.9.0.rst
@@ -40,6 +40,11 @@ Breaking changes
   ``ModelChain.adrinverter`` changed to ``ModelChain.adr_inverter``.
   (:pull:`1150`)
 
+* The ``orientation_strategy`` parameter has been removed from the various
+  :py:class:`pvlib.modelchain.ModelChain` constructors and ``surface_tilt``,
+  ``surface_azimuth`` are now required parameters for
+  :py:func:`pvlib.modelchain.basic_chain` (:issue:`1028`, :pull:`1181`)
+
 
 Deprecations
 ~~~~~~~~~~~~
@@ -96,14 +101,17 @@ Enhancements
 * :py:meth:`~pvlib.pvsystem.PVSystem.get_ac` is added to calculate AC power
   from DC power. Use parameter ``model`` to specify which inverter model to use.
   (:pull:`1147`, :issue:`998`, :pull:`1150`)
+* Added :py:func:`~pvlib.temperature.noct_sam`, a cell temperature model
+  implemented in SAM (:pull:`1177`, :pull:`1195`)
 
 Bug fixes
 ~~~~~~~~~
 * Pass weather data to solar position calculations in
-  :ref:meth:`~pvlib.modelchain.ModelChain.prepare_inputs_from_poa`.
+  :py:meth:`~pvlib.modelchain.ModelChain.prepare_inputs_from_poa`.
   (:issue:`1065`, :pull:`1140`)
 * Reindl model fixed to generate sky_diffuse=0 when GHI=0.
   (:issue:`1153`, :pull:`1154`)
+
 Testing
 ~~~~~~~
 
@@ -111,7 +119,8 @@ Documentation
 ~~~~~~~~~~~~~
 
 * Update intro tutorial to highlight the use of historical meteorological data
-  and to make the procedural and OO results match exactly.
+  and to make the procedural and object oriented results match exactly.
+* Update documentation links in :py:func:`pvlib.iotools.get_psm3`
 
 Requirements
 ~~~~~~~~~~~~
@@ -127,3 +136,5 @@ Contributors
 * Nate Croft (:ghuser:`ncroft-b4`)
 * Kevin Anderson (:ghuser:`kanderso-nrel`)
 * Adam R. Jensen (:ghuser:`AdamRJensen`)
+* Joshua Stein (:ghuser:`jsstein`)
+* Tony Lorenzo (:ghuser:`alorenzo175`)
diff --git a/docs/tutorials/forecast.ipynb b/docs/tutorials/forecast.ipynb
index 58c3cfb..be40e3d 100644
--- a/docs/tutorials/forecast.ipynb
+++ b/docs/tutorials/forecast.ipynb
@@ -6664,7 +6664,6 @@
       "text/plain": [
        "ModelChain: \n",
        "  name: None\n",
-       "  orientation_strategy: south_at_latitude_tilt\n",
        "  clearsky_model: ineichen\n",
        "  transposition_model: haydavies\n",
        "  solar_position_method: nrel_numpy\n",
@@ -6692,15 +6691,16 @@
     "inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']\n",
     "\n",
     "system = PVSystem(module_parameters=module,\n",
-    "                  inverter_parameters=inverter)\n",
+    "                  inverter_parameters=inverter,\n",
+    "                  surface_tilt=latitude,\n",
+    "                  surface_azimuth=180)\n",
     "\n",
     "# fx is a common abbreviation for forecast\n",
     "fx_model = GFS()\n",
     "fx_data = fx_model.get_processed_data(latitude, longitude, start, end)\n",
     "\n",
     "# use a ModelChain object to calculate modeling intermediates\n",
-    "mc = ModelChain(system, fx_model.location,\n",
-    "                orientation_strategy='south_at_latitude_tilt')\n",
+    "mc = ModelChain(system, fx_model.location)\n",
     "\n",
     "# extract relevant data for model chain\n",
     "mc.run_model(weather=fx_data)"
diff --git a/pvlib/iotools/psm3.py b/pvlib/iotools/psm3.py
index 87dd386..7588841 100644
--- a/pvlib/iotools/psm3.py
+++ b/pvlib/iotools/psm3.py
@@ -112,9 +112,9 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60,
     .. [1] `NREL National Solar Radiation Database (NSRDB)
        <https://nsrdb.nrel.gov/>`_
     .. [2] `Physical Solar Model (PSM) v3
-       <https://developer.nrel.gov/docs/solar/nsrdb/psm3_data_download/>`_
+       <https://developer.nrel.gov/docs/solar/nsrdb/psm3-download/>`_
     .. [3] `Physical Solar Model (PSM) v3 TMY
-       <https://developer.nrel.gov/docs/solar/nsrdb/psm3_tmy_data_download/>`_
+       <https://developer.nrel.gov/docs/solar/nsrdb/psm3-tmy-download/>`_
     .. [4] `Physical Solar Model (PSM) v3 - Five Minute Temporal Resolution
        <https://developer.nrel.gov/docs/solar/nsrdb/psm3-5min-download/>`_
     """
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 1c07857..aa8aac4 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -63,11 +63,10 @@ SAPM_CONFIG = dict(
 
 
 def basic_chain(times, latitude, longitude,
+                surface_tilt, surface_azimuth,
                 module_parameters, temperature_model_parameters,
                 inverter_parameters,
                 irradiance=None, weather=None,
-                surface_tilt=None, surface_azimuth=None,
-                orientation_strategy=None,
                 transposition_model='haydavies',
                 solar_position_method='nrel_numpy',
                 airmass_model='kastenyoung1989',
@@ -91,6 +90,17 @@ def basic_chain(times, latitude, longitude,
         Positive is east of the prime meridian.
         Use decimal degrees notation.
 
+    surface_tilt : numeric
+        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 : numeric
+        Surface azimuth angles in decimal degrees.
+        The azimuth convention is defined
+        as degrees east of north
+        (North=0, South=180, East=90, West=270).
+
     module_parameters : None, dict or Series
         Module parameters as defined by the SAPM. See pvsystem.sapm for
         details.
@@ -112,23 +122,6 @@ def basic_chain(times, latitude, longitude,
         wind speed is 0 m/s.
         Columns must be 'wind_speed', 'temp_air'.
 
-    surface_tilt : None, float or Series, default None
-        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 : None, float or Series, default None
-        Surface azimuth angles in decimal degrees.
-        The azimuth convention is defined
-        as degrees east of north
-        (North=0, South=180, East=90, West=270).
-
-    orientation_strategy : None or str, default None
-        The strategy for aligning the modules.
-        If not None, sets the ``surface_azimuth`` and ``surface_tilt``
-        properties of the ``system``. Allowed strategies include 'flat',
-        'south_at_latitude_tilt'. Ignored for SingleAxisTracker systems.
-
     transposition_model : str, default 'haydavies'
         Passed to system.get_irradiance.
 
@@ -157,17 +150,6 @@ def basic_chain(times, latitude, longitude,
         power (Series).
     """
 
-    # use surface_tilt and surface_azimuth if provided,
-    # otherwise set them using the orientation_strategy
-    if surface_tilt is not None and surface_azimuth is not None:
-        pass
-    elif orientation_strategy is not None:
-        surface_tilt, surface_azimuth = \
-            get_orientation(orientation_strategy, latitude=latitude)
-    else:
-        raise ValueError('orientation_strategy or surface_tilt and '
-                         'surface_azimuth must be provided')
-
     if altitude is None and pressure is None:
         altitude = 0.
         pressure = 101325.
@@ -273,7 +255,8 @@ class ModelChainResult:
     _singleton_tuples: bool = field(default=False)
     _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier',
                          'spectral_modifier', 'cell_temperature',
-                         'effective_irradiance', 'dc', 'diode_params'}
+                         'effective_irradiance', 'dc', 'diode_params',
+                         'dc_ohmic_losses'}
 
     # system-level information
     solar_position: Optional[pd.DataFrame] = field(default=None)
@@ -293,6 +276,7 @@ class ModelChainResult:
     dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \
         field(default=None)
     diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None)
+    dc_ohmic_losses: Optional[PerArray[pd.Series]] = field(default=None)
 
     def _result_type(self, value):
         """Coerce `value` to the correct type according to
@@ -332,12 +316,6 @@ class ModelChain:
         A :py:class:`~pvlib.location.Location` object that represents
         the physical location at which to evaluate the model.
 
-    orientation_strategy : None or str, default None
-        The strategy for aligning the modules. If not None, sets the
-        ``surface_azimuth`` and ``surface_tilt`` properties of the
-        ``system``. Allowed strategies include 'flat',
-        'south_at_latitude_tilt'. Ignored for SingleAxisTracker systems.
-
     clearsky_model : str, default 'ineichen'
         Passed to location.get_clearsky.
 
@@ -376,10 +354,15 @@ class ModelChain:
         as the first argument to a user-defined function.
 
     temperature_model: None, str or function, default None
-        Valid strings are 'sapm', 'pvsyst', 'faiman', and 'fuentes'.
+        Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'.
         The ModelChain instance will be passed as the first argument to a
         user-defined function.
 
+    dc_ohmic_model: str or function, default 'no_loss'
+        Valid strings are 'dc_ohms_from_percent', 'no_loss'. The ModelChain
+        instance will be passed as the first argument to a user-defined
+        function.
+
     losses_model: str or function, default 'no_loss'
         Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance
         will be passed as the first argument to a user-defined function.
@@ -395,13 +378,13 @@ class ModelChain:
                          'dc', 'ac', 'diode_params', 'tracking']
 
     def __init__(self, system, location,
-                 orientation_strategy=None,
                  clearsky_model='ineichen',
                  transposition_model='haydavies',
                  solar_position_method='nrel_numpy',
                  airmass_model='kastenyoung1989',
                  dc_model=None, ac_model=None, aoi_model=None,
                  spectral_model=None, temperature_model=None,
+                 dc_ohmic_model='no_loss',
                  losses_model='no_loss', name=None):
 
         self.name = name
@@ -420,8 +403,8 @@ class ModelChain:
         self.spectral_model = spectral_model
         self.temperature_model = temperature_model
 
+        self.dc_ohmic_model = dc_ohmic_model
         self.losses_model = losses_model
-        self.orientation_strategy = orientation_strategy
 
         self.weather = None
         self.times = None
@@ -451,7 +434,6 @@ class ModelChain:
 
     @classmethod
     def with_pvwatts(cls, system, location,
-                     orientation_strategy=None,
                      clearsky_model='ineichen',
                      airmass_model='kastenyoung1989',
                      name=None,
@@ -469,12 +451,6 @@ class ModelChain:
             A :py:class:`~pvlib.location.Location` object that represents
             the physical location at which to evaluate the model.
 
-        orientation_strategy : None or str, default None
-            The strategy for aligning the modules. If not None, sets the
-            ``surface_azimuth`` and ``surface_tilt`` properties of the
-            ``system``. Allowed strategies include 'flat',
-            'south_at_latitude_tilt'. Ignored for SingleAxisTracker systems.
-
         clearsky_model : str, default 'ineichen'
             Passed to location.get_clearsky.
 
@@ -502,7 +478,6 @@ class ModelChain:
         >>> ModelChain.with_pvwatts(system, location)
         ModelChain:
           name: None
-          orientation_strategy: None
           clearsky_model: ineichen
           transposition_model: perez
           solar_position_method: nrel_numpy
@@ -518,7 +493,6 @@ class ModelChain:
         config.update(kwargs)
         return ModelChain(
             system, location,
-            orientation_strategy=orientation_strategy,
             clearsky_model=clearsky_model,
             airmass_model=airmass_model,
             name=name,
@@ -527,7 +501,6 @@ class ModelChain:
 
     @classmethod
     def with_sapm(cls, system, location,
-                  orientation_strategy=None,
                   clearsky_model='ineichen',
                   transposition_model='haydavies',
                   solar_position_method='nrel_numpy',
@@ -548,12 +521,6 @@ class ModelChain:
             A :py:class:`~pvlib.location.Location` object that represents
             the physical location at which to evaluate the model.
 
-        orientation_strategy : None or str, default None
-            The strategy for aligning the modules. If not None, sets the
-            ``surface_azimuth`` and ``surface_tilt`` properties of the
-            ``system``. Allowed strategies include 'flat',
-            'south_at_latitude_tilt'. Ignored for SingleAxisTracker systems.
-
         clearsky_model : str, default 'ineichen'
             Passed to location.get_clearsky.
 
@@ -589,7 +556,6 @@ class ModelChain:
         >>> ModelChain.with_sapm(system, location)
         ModelChain:
           name: None
-          orientation_strategy: None
           clearsky_model: ineichen
           transposition_model: haydavies
           solar_position_method: nrel_numpy
@@ -605,7 +571,6 @@ class ModelChain:
         config.update(kwargs)
         return ModelChain(
             system, location,
-            orientation_strategy=orientation_strategy,
             clearsky_model=clearsky_model,
             transposition_model=transposition_model,
             solar_position_method=solar_position_method,
@@ -616,7 +581,7 @@ class ModelChain:
 
     def __repr__(self):
         attrs = [
-            'name', 'orientation_strategy', 'clearsky_model',
+            'name', 'clearsky_model',
             'transposition_model', 'solar_position_method',
             'airmass_model', 'dc_model', 'ac_model', 'aoi_model',
             'spectral_model', 'temperature_model', 'losses_model'
@@ -634,21 +599,6 @@ class ModelChain:
         return ('ModelChain: \n  ' + '\n  '.join(
             f'{attr}: {getmcattr(self, attr)}' for attr in attrs))
 
-    @property
-    def orientation_strategy(self):
-        return self._orientation_strategy
-
-    @orientation_strategy.setter
-    def orientation_strategy(self, strategy):
-        if strategy == 'None':
-            strategy = None
-
-        if strategy is not None:
-            self.system.surface_tilt, self.system.surface_azimuth = \
-                get_orientation(strategy, latitude=self.location.latitude)
-
-        self._orientation_strategy = strategy
-
     @property
     def dc_model(self):
         return self._dc_model
@@ -994,6 +944,8 @@ class ModelChain:
                 self._temperature_model = self.faiman_temp
             elif model == 'fuentes':
                 self._temperature_model = self.fuentes_temp
+            elif model == 'noct_sam':
+                self._temperature_model = self.noct_sam_temp
             else:
                 raise ValueError(model + ' is not a valid temperature model')
             # check system.temperature_model_parameters for consistency
@@ -1024,6 +976,8 @@ class ModelChain:
             return self.faiman_temp
         elif {'noct_installed'} <= params:
             return self.fuentes_temp
+        elif {'noct', 'eta_m_ref'} <= params:
+            return self.noct_sam_temp
         else:
             raise ValueError(f'could not infer temperature model from '
                              f'system.temperature_model_parameters. Check '
@@ -1053,7 +1007,11 @@ class ModelChain:
                                   self.results.effective_irradiance)
         temp_air = _tuple_from_dfs(self.weather, 'temp_air')
         wind_speed = _tuple_from_dfs(self.weather, 'wind_speed')
-        self.results.cell_temperature = model(poa, temp_air, wind_speed)
+        kwargs = {}
+        if model == self.system.noct_sam_celltemp:
+            kwargs['effective_irradiance'] = self.results.effective_irradiance
+        self.results.cell_temperature = model(poa, temp_air, wind_speed,
+                                              **kwargs)
         return self
 
     def sapm_temp(self):
@@ -1068,6 +1026,52 @@ class ModelChain:
     def fuentes_temp(self):
         return self._set_celltemp(self.system.fuentes_celltemp)
 
+    def noct_sam_temp(self):
+        return self._set_celltemp(self.system.noct_sam_celltemp)
+
+    @property
+    def dc_ohmic_model(self):
+        return self._dc_ohmic_model
+
+    @dc_ohmic_model.setter
+    def dc_ohmic_model(self, model):
+        if isinstance(model, str):
+            model = model.lower()
+            if model == 'dc_ohms_from_percent':
+                self._dc_ohmic_model = self.dc_ohms_from_percent
+            elif model == 'no_loss':
+                self._dc_ohmic_model = self.no_dc_ohmic_loss
+            else:
+                raise ValueError(model + ' is not a valid losses model')
+        else:
+            self._dc_ohmic_model = partial(model, self)
+
+    def dc_ohms_from_percent(self):
+        """
+        Calculate time series of ohmic losses and apply those to the mpp power
+        output of the `dc_model` based on the pvsyst equivalent resistance
+        method. Uses a `dc_ohmic_percent` parameter in the `losses_parameters`
+        of the PVsystem.
+        """
+        Rw = self.system.dc_ohms_from_percent()
+        if isinstance(self.results.dc, tuple):
+            self.results.dc_ohmic_losses = tuple(
+                pvsystem.dc_ohmic_losses(Rw, df['i_mp'])
+                for Rw, df in zip(Rw, self.results.dc)
+            )
+            for df, loss in zip(self.results.dc, self.results.dc_ohmic_losses):
+                df['p_mp'] = df['p_mp'] - loss
+        else:
+            self.results.dc_ohmic_losses = pvsystem.dc_ohmic_losses(
+                Rw, self.results.dc['i_mp']
+            )
+            self.results.dc['p_mp'] = (self.results.dc['p_mp']
+                                       - self.results.dc_ohmic_losses)
+        return self
+
+    def no_dc_ohmic_loss(self):
+        return self
+
     @property
     def losses_model(self):
         return self._losses_model
@@ -1578,10 +1582,14 @@ class ModelChain:
         if not isinstance(data, tuple):
             # broadcast data to all arrays
             data = (data,) * self.system.num_arrays
+        # data is tuple, so temperature_model_parameters must also be
+        # tuple. system.temperature_model_parameters is reduced to a dict
+        # if system.num_arrays == 1, so manually access parameters. GH 1192
+        t_mod_params = tuple(array.temperature_model_parameters
+                             for array in self.system.arrays)
         # find where cell or module temperature is specified in input data
         given_cell_temperature = tuple(itertools.starmap(
-            self._get_cell_temperature,
-            zip(data, poa, self.system.temperature_model_parameters)
+            self._get_cell_temperature, zip(data, poa, t_mod_params)
         ))
         # If cell temperature has been specified for all arrays return
         # immediately and do not try to compute it.
@@ -1737,6 +1745,7 @@ class ModelChain:
         """
         self._prepare_temperature(data)
         self.dc_model()
+        self.dc_ohmic_model()
         self.losses_model()
         self.ac_model()
 
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index a727673..f690161 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -143,7 +143,8 @@ class PVSystem:
         Module parameters as defined by the SAPM, CEC, or other.
 
     temperature_model_parameters : None, dict or Series, default None.
-        Temperature model parameters as defined by the SAPM, Pvsyst, or other.
+        Temperature model parameters as required by one of the models in
+        pvlib.temperature (excluding poa_global, temp_air and wind_speed).
 
     modules_per_string: int or float, default 1
         See system topology discussion above.
@@ -190,6 +191,11 @@ class PVSystem:
                  racking_model=None, losses_parameters=None, name=None):
 
         if arrays is None:
+            if losses_parameters is None:
+                array_losses_parameters = {}
+            else:
+                array_losses_parameters = _build_kwargs(['dc_ohmic_percent'],
+                                                        losses_parameters)
             self.arrays = (Array(
                 surface_tilt,
                 surface_azimuth,
@@ -201,7 +207,8 @@ class PVSystem:
                 temperature_model_parameters,
                 modules_per_string,
                 strings_per_inverter,
-                racking_model
+                racking_model,
+                array_losses_parameters,
             ),)
         else:
             self.arrays = tuple(arrays)
@@ -750,8 +757,6 @@ class PVSystem:
         if you want to match the PVWatts behavior, you can override it by
         including a ``surface_tilt`` value in ``temperature_model_parameters``.
 
-        Notes
-        -----
         The `temp_air` and `wind_speed` 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.
@@ -781,6 +786,82 @@ class PVSystem:
             )
         )
 
+    @_unwrap_single_value
+    def noct_sam_celltemp(self, poa_global, temp_air, wind_speed,
+                          effective_irradiance=None):
+        """
+        Use :py:func:`temperature.noct_sam` to calculate cell temperature.
+
+        Parameters
+        ----------
+        poa_global : numeric or tuple of numeric
+            Total incident irradiance in W/m^2.
+
+        temp_air : numeric or tuple of numeric
+            Ambient dry bulb temperature in degrees C.
+
+        wind_speed : numeric or tuple of numeric
+            Wind speed in m/s at a height of 10 meters.
+
+        effective_irradiance : numeric, tuple of numeric, or None.
+            The irradiance that is converted to photocurrent. If None,
+            assumed equal to ``poa_global``. [W/m^2]
+
+        Returns
+        -------
+        temperature_cell : numeric or tuple of numeric
+            The modeled cell temperature [C]
+
+        Notes
+        -----
+        The `temp_air` and `wind_speed` 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
+        Arrays.
+        """
+        # default to using the Array attribute, but allow user to
+        # override with a custom surface_tilt value
+        poa_global = self._validate_per_array(poa_global)
+        temp_air = self._validate_per_array(temp_air, system_wide=True)
+        wind_speed = self._validate_per_array(wind_speed, system_wide=True)
+
+        # need effective_irradiance to be an iterable
+        if effective_irradiance is None:
+            effective_irradiance = tuple([None] * self.num_arrays)
+        else:
+            effective_irradiance = self._validate_per_array(
+                effective_irradiance)
+
+        def _build_kwargs_noct_sam(array):
+            temp_model_kwargs = _build_kwargs([
+                'transmittance_absorptance',
+                'array_height', 'mount_standoff'],
+                array.temperature_model_parameters)
+            try:
+                # noct_sam required args
+                # bundled with kwargs for simplicity
+                temp_model_kwargs['noct'] = \
+                    array.temperature_model_parameters['noct']
+                temp_model_kwargs['eta_m_ref'] = \
+                    array.temperature_model_parameters['eta_m_ref']
+            except KeyError:
+                msg = ('Parameters noct and eta_m_ref are required.'
+                       ' Found {} in temperature_model_parameters.'
+                       .format(array.temperature_model_parameters))
+                raise KeyError(msg)
+            return temp_model_kwargs
+        return tuple(
+            temperature.noct_sam(
+                poa_global, temp_air, wind_speed,
+                effective_irradiance=eff_irrad,
+                **_build_kwargs_noct_sam(array))
+            for array, poa_global, temp_air, wind_speed, eff_irrad in zip(
+                self.arrays, poa_global, temp_air, wind_speed,
+                effective_irradiance
+            )
+        )
+
     @_unwrap_single_value
     def first_solar_spectral_loss(self, pw, airmass_absolute):
 
@@ -1017,6 +1098,17 @@ class PVSystem:
         return inverter.pvwatts(pdc, self.inverter_parameters['pdc0'],
                                 **kwargs)
 
+    @_unwrap_single_value
+    def dc_ohms_from_percent(self):
+        """
+        Calculates the equivalent resistance of the wires for each array using
+        :py:func:`pvlib.pvsystem.dc_ohms_from_percent`
+
+        See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for details.
+        """
+
+        return tuple(array.dc_ohms_from_percent() for array in self.arrays)
+
     @property
     @_unwrap_single_value
     def module_parameters(self):
@@ -1149,6 +1241,9 @@ class Array:
         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'.
+
     """
 
     def __init__(self,
@@ -1158,7 +1253,8 @@ class Array:
                  module_parameters=None,
                  temperature_model_parameters=None,
                  modules_per_string=1, strings=1,
-                 racking_model=None, name=None):
+                 racking_model=None, array_losses_parameters=None,
+                 name=None):
         self.surface_tilt = surface_tilt
         self.surface_azimuth = surface_azimuth
 
@@ -1186,6 +1282,11 @@ class Array:
         else:
             self.temperature_model_parameters = temperature_model_parameters
 
+        if array_losses_parameters is None:
+            self.array_losses_parameters = {}
+        else:
+            self.array_losses_parameters = array_losses_parameters
+
         self.name = name
 
     def __repr__(self):
@@ -1372,6 +1473,72 @@ class Array:
         else:
             raise ValueError(model + ' is not a valid IAM model')
 
+    def dc_ohms_from_percent(self):
+        """
+        Calculates the equivalent resistance of the wires using
+        :py:func:`pvlib.pvsystem.dc_ohms_from_percent`
+
+        Makes use of array module parameters according to the
+        following DC models:
+
+        CEC:
+
+            * `self.module_parameters["V_mp_ref"]`
+            * `self.module_parameters["I_mp_ref"]`
+
+        SAPM:
+
+            * `self.module_parameters["Vmpo"]`
+            * `self.module_parameters["Impo"]`
+
+        PVsyst-like or other:
+
+            * `self.module_parameters["Vmpp"]`
+            * `self.module_parameters["Impp"]`
+
+        Other array parameters that are used are:
+        `self.losses_parameters["dc_ohmic_percent"]`,
+        `self.modules_per_string`, and
+        `self.strings`.
+
+        See :py:func:`pvlib.pvsystem.dc_ohms_from_percent` for more details.
+        """
+
+        # get relevent Vmp and Imp parameters from CEC parameters
+        if all([elem in self.module_parameters
+                for elem in ['V_mp_ref', 'I_mp_ref']]):
+            vmp_ref = self.module_parameters['V_mp_ref']
+            imp_ref = self.module_parameters['I_mp_ref']
+
+        # get relevant Vmp and Imp parameters from SAPM parameters
+        elif all([elem in self.module_parameters
+                  for elem in ['Vmpo', 'Impo']]):
+            vmp_ref = self.module_parameters['Vmpo']
+            imp_ref = self.module_parameters['Impo']
+
+        # get relevant Vmp and Imp parameters if they are PVsyst-like
+        elif all([elem in self.module_parameters
+                  for elem in ['Vmpp', 'Impp']]):
+            vmp_ref = self.module_parameters['Vmpp']
+            imp_ref = self.module_parameters['Impp']
+
+        # raise error if relevant Vmp and Imp parameters are not found
+        else:
+            raise ValueError('Parameters for Vmp and Imp could not be found '
+                             'in the array module parameters. Module '
+                             'parameters must include one set of '
+                             '{"V_mp_ref", "I_mp_Ref"}, '
+                             '{"Vmpo", "Impo"}, or '
+                             '{"Vmpp", "Impp"}.'
+                             )
+
+        return dc_ohms_from_percent(
+            vmp_ref,
+            imp_ref,
+            self.array_losses_parameters['dc_ohmic_percent'],
+            self.modules_per_string,
+            self.strings)
+
 
 def calcparams_desoto(effective_irradiance, temp_cell,
                       alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
@@ -2810,6 +2977,80 @@ def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2,
     return losses
 
 
+def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent,
+                         modules_per_string=1,
+                         strings=1):
+    """
+    Calculates the equivalent resistance of the wires from a percent
+    ohmic loss at STC.
+
+    Equivalent resistance is calculated with the function:
+
+    .. math::
+        Rw = (L_{stc} / 100) * (Varray / Iarray)
+
+    :math:`Rw` is the equivalent resistance in ohms
+    :math:`Varray` is the Vmp of the modules times modules per string
+    :math:`Iarray` is the Imp of the modules times strings per array
+    :math:`L_{stc}` is the input dc loss percent
+
+    Parameters
+    ----------
+    vmp_ref: numeric
+        Voltage at maximum power in reference conditions [V]
+    imp_ref: numeric
+        Current at maximum power in reference conditions [V]
+    dc_ohmic_percent: numeric, default 0
+        input dc loss as a percent, e.g. 1.5% loss is input as 1.5
+    modules_per_string: int, default 1
+        Number of modules per string in the array.
+    strings: int, default 1
+        Number of parallel strings in the array.
+
+    Returns
+    ----------
+    Rw: numeric
+        Equivalent resistance [ohm]
+
+    References
+    ----------
+    .. [1] PVsyst 7 Help. "Array ohmic wiring loss".
+       https://www.pvsyst.com/help/ohmic_loss.htm
+    """
+    vmp = modules_per_string * vmp_ref
+
+    imp = strings * imp_ref
+
+    Rw = (dc_ohmic_percent / 100) * (vmp / imp)
+
+    return Rw
+
+
+def dc_ohmic_losses(resistance, current):
+    """
+    Returns ohmic losses in units of power from the equivalent
+    resistance of the wires and the operating current.
+
+    Parameters
+    ----------
+    resistance: numeric
+        Equivalent resistance of wires [ohm]
+    current: numeric, float or array-like
+        Operating current [A]
+
+    Returns
+    ----------
+    loss: numeric
+        Power Loss [W]
+
+    References
+    ----------
+    .. [1] PVsyst 7 Help. "Array ohmic wiring loss".
+       https://www.pvsyst.com/help/ohmic_loss.htm
+    """
+    return resistance * current * current
+
+
 def combine_loss_factors(index, *losses, fill_method='ffill'):
     r"""
     Combines Series loss fractions while setting a common index.
diff --git a/pvlib/temperature.py b/pvlib/temperature.py
index 0387114..5797eed 100644
--- a/pvlib/temperature.py
+++ b/pvlib/temperature.py
@@ -316,7 +316,8 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
         :math:`\left[ \frac{\text{W}/\text{m}^2}{\text{C}\ \left( \text{m/s} \right)} \right]`
 
     eta_m : numeric, default 0.1
-        Module external efficiency as a fraction, i.e., DC power / poa_global.
+        Module external efficiency as a fraction, i.e.,
+        :math:`DC\ power / (POA\ irradiance \times module\ area)`.
         Parameter :math:`\eta_{m}` in :eq:`pvsyst`.
 
     alpha_absorption : numeric, default 0.9
@@ -706,3 +707,109 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5,
         sun0 = sun
 
     return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod')
+
+
+def _adj_for_mounting_standoff(x):
+    # supports noct cell temperature function. Except for x > 3.5, the SAM code
+    # and documentation aren't clear on the precise intervals. The choice of
+    # < or <= here is pvlib's.
+    return np.piecewise(x, [x <= 0, (x > 0) & (x < 0.5),
+                            (x >= 0.5) & (x < 1.5), (x >= 1.5) & (x < 2.5),
+                            (x >= 2.5) & (x <= 3.5), x > 3.5],
+                        [0., 18., 11., 6., 2., 0.])
+
+
+def noct_sam(poa_global, temp_air, wind_speed, noct, eta_m_ref,
+             effective_irradiance=None, transmittance_absorptance=0.9,
+             array_height=1, mount_standoff=4):
+    r'''
+    Cell temperature model from the System Advisor Model (SAM).
+
+    The model is described in [1]_, Section 10.6.
+
+    Parameters
+    ----------
+    poa_global : numeric
+        Total incident irradiance. [W/m^2]
+
+    temp_air : numeric
+        Ambient dry bulb temperature. [C]
+
+    wind_speed : numeric
+        Wind speed in m/s measured at the same height for which the wind loss
+        factor was determined.  The default value 1.0 m/s is the wind
+        speed at module height used to determine NOCT. [m/s]
+
+    noct : float
+        Nominal operating cell temperature [C], determined at conditions of
+        800 W/m^2 irradiance, 20 C ambient air temperature and 1 m/s wind.
+
+    eta_m_ref : float
+        Module external efficiency [unitless] at reference conditions of
+        1000 W/m^2 and 20C. Calculate as
+        :math:`\eta_{m} = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2}`
+        where A is module area [m^2].
+
+    effective_irradiance : numeric, default None.
+        The irradiance that is converted to photocurrent. If None,
+        assumed equal to poa_global. [W/m^2]
+
+    transmittance_absorptance : numeric, default 0.9
+        Coefficient for combined transmittance and absorptance effects.
+        [unitless]
+
+    array_height : int, default 1
+        Height of array above ground in stories (one story is about 3m). Must
+        be either 1 or 2. For systems elevated less than one story, use 1.
+        If system is elevated more than two stories, use 2.
+
+    mount_standoff : numeric, default 4
+        Distance between array mounting and mounting surface. Use default
+        if system is ground-mounted. [inches]
+
+    Returns
+    -------
+    cell_temperature : numeric
+        Cell temperature. [C]
+
+    Raises
+    ------
+    ValueError
+        If array_height is an invalid value (must be 1 or 2).
+
+    References
+    ----------
+    .. [1] Gilman, P., Dobos, A., DiOrio, N., Freeman, J., Janzou, S.,
+           Ryberg, D., 2018, "SAM Photovoltaic Model Technical Reference
+           Update", National Renewable Energy Laboratory Report
+           NREL/TP-6A20-67399.
+    '''
+    # in [1] the denominator for irr_ratio isn't precisely clear. From
+    # reproducing output of the SAM function noct_celltemp_t, we determined
+    # that:
+    #  - G_total (SAM) is broadband plane-of-array irradiance before
+    #    reflections. Equivalent to pvlib variable poa_global
+    #  - Geff_total (SAM) is POA irradiance after reflections and
+    #    adjustment for spectrum. Equivalent to effective_irradiance
+    if effective_irradiance is None:
+        irr_ratio = 1.
+    else:
+        irr_ratio = effective_irradiance / poa_global
+
+    if array_height == 1:
+        wind_adj = 0.51 * wind_speed
+    elif array_height == 2:
+        wind_adj = 0.61 * wind_speed
+    else:
+        raise ValueError(
+            f'array_height must be 1 or 2, {array_height} was given')
+
+    noct_adj = noct + _adj_for_mounting_standoff(mount_standoff)
+    tau_alpha = transmittance_absorptance * irr_ratio
+
+    # [1] Eq. 10.37 isn't clear on exactly what "G" is. SAM SSC code uses
+    # poa_global where G appears
+    cell_temp_init = poa_global / 800. * (noct_adj - 20.)
+    heat_loss = 1 - eta_m_ref / tau_alpha
+    wind_loss = 9.5 / (5.7 + 3.8 * wind_adj)
+    return temp_air + cell_temp_init * heat_loss * wind_loss


Test patch:
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
index c45fd50..97a7fa7 100644
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -223,6 +223,18 @@ def pvwatts_dc_pvwatts_ac_fuentes_temp_system():
     return system
 
 
+@pytest.fixture(scope="function")
+def pvwatts_dc_pvwatts_ac_noct_sam_temp_system():
+    module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
+    temp_model_params = {'noct': 45, 'eta_m_ref': 0.2}
+    inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95}
+    system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
+                      module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
+                      inverter_parameters=inverter_parameters)
+    return system
+
+
 @pytest.fixture(scope="function")
 def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m,
                   cec_inverter_parameters):
@@ -333,22 +345,6 @@ def test_with_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather):
     mc.run_model(weather)
 
 
-@pytest.mark.parametrize('strategy, expected', [
-    (None, (32.2, 180)), ('None', (32.2, 180)), ('flat', (0, 180)),
-    ('south_at_latitude_tilt', (32.2, 180))
-])
-def test_orientation_strategy(strategy, expected, sapm_dc_snl_ac_system,
-                              location):
-    mc = ModelChain(sapm_dc_snl_ac_system, location,
-                    orientation_strategy=strategy)
-
-    # the || accounts for the coercion of 'None' to None
-    assert (mc.orientation_strategy == strategy or
-            mc.orientation_strategy is None)
-    assert sapm_dc_snl_ac_system.surface_tilt == expected[0]
-    assert sapm_dc_snl_ac_system.surface_azimuth == expected[1]
-
-
 def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location):
     mc = ModelChain(sapm_dc_snl_ac_system, location)
     times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
@@ -709,6 +705,25 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location,
     assert not mc.results.ac.empty
 
 
+def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location,
+                                              weather, mocker):
+    weather['wind_speed'] = 5
+    weather['temp_air'] = 10
+    sapm_dc_snl_ac_system.temperature_model_parameters = {
+        'noct': 45, 'eta_m_ref': 0.2
+    }
+    mc = ModelChain(sapm_dc_snl_ac_system, location)
+    mc.temperature_model = 'noct_sam'
+    m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'noct_sam_celltemp')
+    mc.run_model(weather)
+    assert m_noct_sam.call_count == 1
+    assert_series_equal(m_noct_sam.call_args[0][1], weather['temp_air'])
+    assert_series_equal(m_noct_sam.call_args[0][2], weather['wind_speed'])
+    # check that effective_irradiance was used
+    assert m_noct_sam.call_args[1] == {
+        'effective_irradiance': mc.results.effective_irradiance}
+
+
 def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker):
     system = SingleAxisTracker(
         module_parameters=sapm_dc_snl_ac_system.module_parameters,
@@ -846,6 +861,38 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather,
     assert_series_equal(mc.results.cell_temperature, data['cell_temperature'])
 
 
+def test__prepare_temperature_len1_weather_tuple(
+        sapm_dc_snl_ac_system, location, weather, total_irrad):
+    # GH 1192
+    weather['module_temperature'] = [40., 30.]
+    data = weather.copy()
+
+    mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss',
+                    spectral_model='no_loss')
+    mc.run_model([data])
+    expected = pd.Series([42.617244212941394, 30.0], index=data.index)
+    assert_series_equal(mc.results.cell_temperature[0], expected)
+
+    data = weather.copy().rename(
+        columns={
+            "ghi": "poa_global", "dhi": "poa_diffuse", "dni": "poa_direct"}
+    )
+    mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss',
+                    spectral_model='no_loss')
+    mc.run_model_from_poa([data])
+    expected = pd.Series([41.5, 30.0], index=data.index)
+    assert_series_equal(mc.results.cell_temperature[0], expected)
+
+    data = weather.copy()[["module_temperature", "ghi"]].rename(
+        columns={"ghi": "effective_irradiance"}
+    )
+    mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss',
+                    spectral_model='no_loss')
+    mc.run_model_from_effective_irradiance([data])
+    expected = pd.Series([41.5, 30.0], index=data.index)
+    assert_series_equal(mc.results.cell_temperature[0], expected)
+
+
 def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays,
                                              location, weather,
                                              total_irrad):
@@ -891,7 +938,9 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays,
                           ({'u0': 25.0, 'u1': 6.84},
                            ModelChain.faiman_temp),
                           ({'noct_installed': 45},
-                           ModelChain.fuentes_temp)])
+                           ModelChain.fuentes_temp),
+                          ({'noct': 45, 'eta_m_ref': 0.2},
+                           ModelChain.noct_sam_temp)])
 def test_temperature_models_arrays_multi_weather(
         temp_params, temp_model,
         sapm_dc_snl_ac_system_same_arrays,
@@ -1235,25 +1284,26 @@ def test_infer_spectral_model(location, sapm_dc_snl_ac_system,
                   'cec': cec_dc_snl_ac_system,
                   'cec_native': cec_dc_native_snl_ac_system}
     system = dc_systems[dc_model]
-    mc = ModelChain(system, location,
-                    orientation_strategy='None', aoi_model='physical')
+    mc = ModelChain(system, location, aoi_model='physical')
     assert isinstance(mc, ModelChain)
 
 
 @pytest.mark.parametrize('temp_model', [
-    'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp'])
+    'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp',
+    'noct_sam_temp'])
 def test_infer_temp_model(location, sapm_dc_snl_ac_system,
                           pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
                           pvwatts_dc_pvwatts_ac_faiman_temp_system,
                           pvwatts_dc_pvwatts_ac_fuentes_temp_system,
+                          pvwatts_dc_pvwatts_ac_noct_sam_temp_system,
                           temp_model):
     dc_systems = {'sapm_temp': sapm_dc_snl_ac_system,
                   'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system,
                   'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system,
-                  'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system}
+                  'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system,
+                  'noct_sam_temp': pvwatts_dc_pvwatts_ac_noct_sam_temp_system}
     system = dc_systems[temp_model]
-    mc = ModelChain(system, location,
-                    orientation_strategy='None', aoi_model='physical',
+    mc = ModelChain(system, location, aoi_model='physical',
                     spectral_model='no_loss')
     assert temp_model == mc.temperature_model.__name__
     assert isinstance(mc, ModelChain)
@@ -1263,14 +1313,12 @@ def test_infer_temp_model_invalid(location, sapm_dc_snl_ac_system):
     sapm_dc_snl_ac_system.temperature_model_parameters.pop('a')
     with pytest.raises(ValueError):
         ModelChain(sapm_dc_snl_ac_system, location,
-                   orientation_strategy='None', aoi_model='physical',
-                   spectral_model='no_loss')
+                   aoi_model='physical', spectral_model='no_loss')
 
 
 def test_temperature_model_inconsistent(location, sapm_dc_snl_ac_system):
     with pytest.raises(ValueError):
-        ModelChain(sapm_dc_snl_ac_system, location,
-                   orientation_strategy='None', aoi_model='physical',
+        ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical',
                    spectral_model='no_loss', temperature_model='pvsyst')
 
 
@@ -1441,17 +1489,14 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker):
 def test_infer_aoi_model(location, system_no_aoi, aoi_model):
     for k in iam._IAM_MODEL_PARAMS[aoi_model]:
         system_no_aoi.module_parameters.update({k: 1.0})
-    mc = ModelChain(system_no_aoi, location,
-                    orientation_strategy='None',
-                    spectral_model='no_loss')
+    mc = ModelChain(system_no_aoi, location, spectral_model='no_loss')
     assert isinstance(mc, ModelChain)
 
 
 def test_infer_aoi_model_invalid(location, system_no_aoi):
     exc_text = 'could not infer AOI model'
     with pytest.raises(ValueError, match=exc_text):
-        ModelChain(system_no_aoi, location, orientation_strategy='None',
-                   spectral_model='no_loss')
+        ModelChain(system_no_aoi, location, spectral_model='no_loss')
 
 
 def constant_spectral_loss(mc):
@@ -1491,6 +1536,91 @@ def constant_losses(mc):
     mc.results.dc *= mc.losses
 
 
+def dc_constant_losses(mc):
+    mc.results.dc['p_mp'] *= 0.9
+
+
+def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system,
+                                          cec_dc_snl_ac_arrays,
+                                          location,
+                                          weather,
+                                          mocker):
+
+    m = mocker.spy(pvsystem, 'dc_ohms_from_percent')
+
+    system = cec_dc_snl_ac_system
+
+    for array in system.arrays:
+        array.array_losses_parameters = dict(dc_ohmic_percent=3)
+
+    mc = ModelChain(system, location,
+                    aoi_model='no_loss',
+                    spectral_model='no_loss',
+                    dc_ohmic_model='dc_ohms_from_percent')
+    mc.run_model(weather)
+
+    assert m.call_count == 1
+
+    assert isinstance(mc.results.dc_ohmic_losses, pd.Series)
+
+    system = cec_dc_snl_ac_arrays
+
+    for array in system.arrays:
+        array.array_losses_parameters = dict(dc_ohmic_percent=3)
+
+    mc = ModelChain(system, location,
+                    aoi_model='no_loss',
+                    spectral_model='no_loss',
+                    dc_ohmic_model='dc_ohms_from_percent')
+    mc.run_model(weather)
+
+    assert m.call_count == 3
+    assert len(mc.results.dc_ohmic_losses) == len(mc.system.arrays)
+
+    assert isinstance(mc.results.dc_ohmic_losses, tuple)
+
+
+def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system,
+                                         location,
+                                         weather,
+                                         mocker):
+
+    m = mocker.spy(modelchain.ModelChain, 'no_dc_ohmic_loss')
+    mc = ModelChain(cec_dc_snl_ac_system, location,
+                    aoi_model='no_loss',
+                    spectral_model='no_loss',
+                    dc_ohmic_model='no_loss')
+    mc.run_model(weather)
+
+    assert mc.dc_ohmic_model == mc.no_dc_ohmic_loss
+    assert m.call_count == 1
+    assert mc.results.dc_ohmic_losses is None
+
+
+def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location,
+                          weather, mocker):
+    m = mocker.spy(sys.modules[__name__], 'dc_constant_losses')
+    mc = ModelChain(cec_dc_snl_ac_system, location,
+                    aoi_model='no_loss',
+                    spectral_model='no_loss',
+                    dc_ohmic_model=dc_constant_losses)
+    mc.run_model(weather)
+
+    assert m.call_count == 1
+    assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame))
+    assert not mc.results.ac.empty
+
+
+def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location,
+                              weather, mocker):
+    exc_text = 'not_a_dc_model is not a valid losses model'
+    with pytest.raises(ValueError, match=exc_text):
+        ModelChain(cec_dc_snl_ac_system, location,
+                   aoi_model='no_loss',
+                   spectral_model='no_loss',
+                   dc_ohmic_model='not_a_dc_model')
+
+
 def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather,
                                mocker):
     age = 1
@@ -1623,23 +1753,6 @@ def test_ModelChain_attributes_deprecated_10(sapm_dc_snl_ac_system, location):
         mc.aoi = 5
 
 
-def test_basic_chain_required(sam_data, cec_inverter_parameters,
-                              sapm_temperature_cs5p_220m):
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    latitude = 32
-    longitude = -111
-    altitude = 700
-    modules = sam_data['sandiamod']
-    module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
-    temp_model_params = sapm_temperature_cs5p_220m.copy()
-    with pytest.raises(ValueError):
-        dc, ac = modelchain.basic_chain(
-            times, latitude, longitude, module_parameters, temp_model_params,
-            cec_inverter_parameters, altitude=altitude
-        )
-
-
 @requires_tables
 def test_basic_chain_alt_az(sam_data, cec_inverter_parameters,
                             sapm_temperature_cs5p_220m):
@@ -1653,37 +1766,15 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters,
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    module_parameters,  temp_model_params,
-                                    cec_inverter_parameters,
-                                    surface_tilt=surface_tilt,
-                                    surface_azimuth=surface_azimuth)
+                                    surface_tilt, surface_azimuth,
+                                    module_parameters, temp_model_params,
+                                    cec_inverter_parameters)
 
     expected = pd.Series(np.array([111.621405, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected)
 
 
-@requires_tables
-def test_basic_chain_strategy(sam_data, cec_inverter_parameters,
-                              sapm_temperature_cs5p_220m):
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    latitude = 32.2
-    longitude = -111
-    altitude = 700
-    modules = sam_data['sandiamod']
-    module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
-    temp_model_params = sapm_temperature_cs5p_220m.copy()
-    dc, ac = modelchain.basic_chain(
-        times, latitude, longitude, module_parameters, temp_model_params,
-        cec_inverter_parameters, orientation_strategy='south_at_latitude_tilt',
-        altitude=altitude)
-
-    expected = pd.Series(np.array([178.382754, -2.00000000e-02]),
-                         index=times)
-    assert_series_equal(ac, expected)
-
-
 @requires_tables
 def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
                                        sapm_temperature_cs5p_220m):
@@ -1698,10 +1789,9 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
+                                    surface_tilt, surface_azimuth,
                                     module_parameters, temp_model_params,
                                     cec_inverter_parameters,
-                                    surface_tilt=surface_tilt,
-                                    surface_azimuth=surface_azimuth,
                                     pressure=93194)
 
     expected = pd.Series(np.array([113.190045, -2.00000000e-02]),
@@ -1709,10 +1799,9 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
     assert_series_equal(ac, expected)
 
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
+                                    surface_tilt, surface_azimuth,
                                     module_parameters, temp_model_params,
                                     cec_inverter_parameters,
-                                    surface_tilt=surface_tilt,
-                                    surface_azimuth=surface_azimuth,
                                     altitude=altitude)
 
     expected = pd.Series(np.array([113.189814, -2.00000000e-02]),
@@ -1720,34 +1809,6 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
     assert_series_equal(ac, expected)
 
 
-@pytest.mark.parametrize('strategy, strategy_str', [
-    ('south_at_latitude_tilt', 'south_at_latitude_tilt'),
-    (None, 'None')])  # GitHub issue 352
-def test_ModelChain___repr__(sapm_dc_snl_ac_system, location, strategy,
-                             strategy_str):
-
-    mc = ModelChain(sapm_dc_snl_ac_system, location,
-                    orientation_strategy=strategy, name='my mc')
-
-    expected = '\n'.join([
-        'ModelChain: ',
-        '  name: my mc',
-        '  orientation_strategy: ' + strategy_str,
-        '  clearsky_model: ineichen',
-        '  transposition_model: haydavies',
-        '  solar_position_method: nrel_numpy',
-        '  airmass_model: kastenyoung1989',
-        '  dc_model: sapm',
-        '  ac_model: sandia_inverter',
-        '  aoi_model: sapm_aoi_loss',
-        '  spectral_model: sapm_spectral_loss',
-        '  temperature_model: sapm_temp',
-        '  losses_model: no_extra_losses'
-    ])
-
-    assert mc.__repr__() == expected
-
-
 def test_complete_irradiance_clean_run(sapm_dc_snl_ac_system, location):
     """The DataFrame should not change if all columns are passed"""
     mc = ModelChain(sapm_dc_snl_ac_system, location)
diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py
index 5c0ef7b..cbb9899 100644
--- a/pvlib/tests/test_pvsystem.py
+++ b/pvlib/tests/test_pvsystem.py
@@ -388,7 +388,11 @@ def two_array_system(pvsyst_module_params, cec_module_params):
     # Need u_v to be non-zero so wind-speed changes cell temperature
     # under the pvsyst model.
     temperature_model['u_v'] = 1.0
+    # parameter for fuentes temperature model
     temperature_model['noct_installed'] = 45
+    # parameters for noct_sam temperature model
+    temperature_model['noct'] = 45.
+    temperature_model['eta_m_ref'] = 0.2
     module_params = {**pvsyst_module_params, **cec_module_params}
     return pvsystem.PVSystem(
         arrays=[
@@ -495,11 +499,53 @@ def test_PVSystem_faiman_celltemp(mocker):
     assert_allclose(out, 56.4, atol=1)
 
 
+def test_PVSystem_noct_celltemp(mocker):
+    poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
+                                                         0.2)
+    expected = 55.230790492
+    temp_model_params = {'noct': noct, 'eta_m_ref': eta_m_ref}
+    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
+    mocker.spy(temperature, 'noct_sam')
+    out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed)
+    temperature.noct_sam.assert_called_once_with(
+        poa_global, temp_air, wind_speed, effective_irradiance=None, noct=noct,
+        eta_m_ref=eta_m_ref)
+    assert_allclose(out, expected)
+    # dufferent types
+    out = system.noct_sam_celltemp(np.array(poa_global), np.array(temp_air),
+                                   np.array(wind_speed))
+    assert_allclose(out, expected)
+    dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00',
+                       freq='1H')
+    out = system.noct_sam_celltemp(pd.Series(index=dr, data=poa_global),
+                                   pd.Series(index=dr, data=temp_air),
+                                   pd.Series(index=dr, data=wind_speed))
+    assert_series_equal(out, pd.Series(index=dr, data=expected))
+    # now use optional arguments
+    temp_model_params.update({'transmittance_absorptance': 0.8,
+                              'array_height': 2,
+                              'mount_standoff': 2.0})
+    expected = 60.477703576
+    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
+    out = system.noct_sam_celltemp(poa_global, temp_air, wind_speed,
+                                   effective_irradiance=1100.)
+    assert_allclose(out, expected)
+
+
+def test_PVSystem_noct_celltemp_error():
+    poa_global, temp_air, wind_speed, eta_m_ref = (1000., 25., 1., 0.2)
+    temp_model_params = {'eta_m_ref': eta_m_ref}
+    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
+    with pytest.raises(KeyError):
+        system.noct_sam_celltemp(poa_global, temp_air, wind_speed)
+
+
 @pytest.mark.parametrize("celltemp",
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
     times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
     irrad_one = pd.Series(1000, index=times)
@@ -515,7 +561,8 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system):
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
     times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
     irrad = pd.Series(1000, index=times)
@@ -543,7 +590,8 @@ def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system):
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
     times = pd.date_range(start='2020-08-25 11:00', freq='H', periods=3)
     irrad = pd.Series(1000, index=times)
@@ -571,7 +619,8 @@ def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system):
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_temp_too_short(
         celltemp, two_array_system):
     with pytest.raises(ValueError,
@@ -583,7 +632,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_short(
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_temp_too_long(
         celltemp, two_array_system):
     with pytest.raises(ValueError,
@@ -595,7 +645,8 @@ def test_PVSystem_multi_array_celltemp_temp_too_long(
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_wind_too_short(
         celltemp, two_array_system):
     with pytest.raises(ValueError,
@@ -607,7 +658,8 @@ def test_PVSystem_multi_array_celltemp_wind_too_short(
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
                           pvsystem.PVSystem.sapm_celltemp,
-                          pvsystem.PVSystem.fuentes_celltemp])
+                          pvsystem.PVSystem.fuentes_celltemp,
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_wind_too_long(
         celltemp, two_array_system):
     with pytest.raises(ValueError,
@@ -618,8 +670,9 @@ def test_PVSystem_multi_array_celltemp_wind_too_long(
 @pytest.mark.parametrize("celltemp",
                          [pvsystem.PVSystem.faiman_celltemp,
                           pvsystem.PVSystem.pvsyst_celltemp,
+                          pvsystem.PVSystem.sapm_celltemp,
                           pvsystem.PVSystem.fuentes_celltemp,
-                          pvsystem.PVSystem.sapm_celltemp])
+                          pvsystem.PVSystem.noct_sam_celltemp])
 def test_PVSystem_multi_array_celltemp_poa_length_mismatch(
         celltemp, two_array_system):
     with pytest.raises(ValueError,
@@ -2044,3 +2097,91 @@ def test_combine_loss_factors():
 def test_no_extra_kwargs():
     with pytest.raises(TypeError, match="arbitrary_kwarg"):
         pvsystem.PVSystem(arbitrary_kwarg='value')
+
+
+def test_dc_ohms_from_percent():
+    expected = .1425
+    out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1)
+    assert_allclose(out, expected)
+
+
+def test_PVSystem_dc_ohms_from_percent(mocker):
+    mocker.spy(pvsystem, 'dc_ohms_from_percent')
+
+    expected = .1425
+    system = pvsystem.PVSystem(losses_parameters={'dc_ohmic_percent': 3},
+                               module_parameters={'I_mp_ref': 8,
+                                                  'V_mp_ref': 38})
+    out = system.dc_ohms_from_percent()
+
+    pvsystem.dc_ohms_from_percent.assert_called_once_with(
+        dc_ohmic_percent=3,
+        vmp_ref=38,
+        imp_ref=8,
+        modules_per_string=1,
+        strings=1
+    )
+
+    assert_allclose(out, expected)
+
+
+def test_dc_ohmic_losses():
+    expected = 9.12
+    out = pvsystem.dc_ohmic_losses(.1425, 8)
+    assert_allclose(out, expected)
+
+
+def test_Array_dc_ohms_from_percent(mocker):
+    mocker.spy(pvsystem, 'dc_ohms_from_percent')
+
+    expected = .1425
+
+    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+                           module_parameters={'I_mp_ref': 8,
+                                              'V_mp_ref': 38})
+    out = array.dc_ohms_from_percent()
+    pvsystem.dc_ohms_from_percent.assert_called_with(
+        dc_ohmic_percent=3,
+        vmp_ref=38,
+        imp_ref=8,
+        modules_per_string=1,
+        strings=1
+    )
+    assert_allclose(out, expected)
+
+    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+                           module_parameters={'Impo': 8,
+                                              'Vmpo': 38})
+    out = array.dc_ohms_from_percent()
+    pvsystem.dc_ohms_from_percent.assert_called_with(
+        dc_ohmic_percent=3,
+        vmp_ref=38,
+        imp_ref=8,
+        modules_per_string=1,
+        strings=1
+    )
+    assert_allclose(out, expected)
+
+    array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3},
+                           module_parameters={'Impp': 8,
+                                              'Vmpp': 38})
+    out = array.dc_ohms_from_percent()
+
+    pvsystem.dc_ohms_from_percent.assert_called_with(
+        dc_ohmic_percent=3,
+        vmp_ref=38,
+        imp_ref=8,
+        modules_per_string=1,
+        strings=1
+    )
+    assert_allclose(out, expected)
+
+    with pytest.raises(ValueError,
+                       match=('Parameters for Vmp and Imp could not be found '
+                              'in the array module parameters. Module '
+                              'parameters must include one set of '
+                              '{"V_mp_ref", "I_mp_Ref"}, '
+                              '{"Vmpo", "Impo"}, or '
+                              '{"Vmpp", "Impp"}.')):
+        array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3})
+        out = array.dc_ohms_from_percent()
diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py
index f8ea3a8..4c8574f 100644
--- a/pvlib/tests/test_temperature.py
+++ b/pvlib/tests/test_temperature.py
@@ -5,7 +5,7 @@ import pytest
 from conftest import DATA_DIR, assert_series_equal
 from numpy.testing import assert_allclose
 
-from pvlib import temperature
+from pvlib import temperature, tools
 
 
 @pytest.fixture
@@ -212,3 +212,76 @@ def test_fuentes_timezone(tz):
 
     assert_series_equal(out, pd.Series([47.85, 50.85, 50.85], index=index,
                                        name='tmod'))
+
+
+def test_noct_sam():
+    poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
+                                                         0.2)
+    expected = 55.230790492
+    result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
+                                  eta_m_ref)
+    assert_allclose(result, expected)
+    # test with different types
+    result = temperature.noct_sam(np.array(poa_global), np.array(temp_air),
+                                  np.array(wind_speed), np.array(noct),
+                                  np.array(eta_m_ref))
+    assert_allclose(result, expected)
+    dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00',
+                       freq='1H')
+    result = temperature.noct_sam(pd.Series(index=dr, data=poa_global),
+                                  pd.Series(index=dr, data=temp_air),
+                                  pd.Series(index=dr, data=wind_speed),
+                                  pd.Series(index=dr, data=noct),
+                                  eta_m_ref)
+    assert_series_equal(result, pd.Series(index=dr, data=expected))
+
+
+def test_noct_sam_against_sam():
+    # test is constructed to reproduce output from SAM v2020.11.29.
+    # SAM calculation is the default Detailed PV System model (CEC diode model,
+    # NOCT cell temperature model), with the only change being the soiling
+    # loss is set to 0. Weather input is TMY3 for Phoenix AZ.
+    # Values are taken from the Jan 1 12:00:00 timestamp.
+    poa_total, temp_air, wind_speed, noct, eta_m_ref = (
+        860.673, 25, 3, 46.4, 0.20551)
+    poa_total_after_refl = 851.458  # from SAM output
+    # compute effective irradiance
+    # spectral loss coefficients fixed in lib_cec6par.cpp
+    a = np.flipud([0.918093, 0.086257, -0.024459, 0.002816, -0.000126])
+    # reproduce SAM air mass calculation
+    zen = 56.4284
+    elev = 358
+    air_mass = 1. / (tools.cosd(zen) + 0.5057 * (96.080 - zen)**-1.634)
+    air_mass *= np.exp(-0.0001184 * elev)
+    f1 = np.polyval(a, air_mass)
+    effective_irradiance = f1 * poa_total_after_refl
+    transmittance_absorptance = 0.9
+    array_height = 1
+    mount_standoff = 4.0
+    result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct,
+                                  eta_m_ref, effective_irradiance,
+                                  transmittance_absorptance, array_height,
+                                  mount_standoff)
+    expected = 43.0655
+    # rtol from limited SAM output precision
+    assert_allclose(result, expected, rtol=1e-5)
+
+
+def test_noct_sam_options():
+    poa_global, temp_air, wind_speed, noct, eta_m_ref = (1000., 25., 1., 45.,
+                                                         0.2)
+    effective_irradiance = 1100.
+    transmittance_absorptance = 0.8
+    array_height = 2
+    mount_standoff = 2.0
+    result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
+                                  eta_m_ref, effective_irradiance,
+                                  transmittance_absorptance, array_height,
+                                  mount_standoff)
+    expected = 60.477703576
+    assert_allclose(result, expected)
+
+
+def test_noct_sam_errors():
+    with pytest.raises(ValueError):
+        temperature.noct_sam(1000., 25., 1., 34., 0.2, array_height=3)