Repository: pvlib/pvlib-python

Issue:
remove ModelChain.orientation_strategy
I don't like that `ModelChain(system, location, orientation_strategy='flat`|`south_at_latitude_tilt`) modifies the `system` object. It's not something we do anywhere else in pvlib. `orientation_strategy` only supports flat and south_at_latitude_tilt, neither of which are commonly used in the real world in 2020. 

I think we should remove it, maybe even without deprecation, in 0.8.

I'm ok with keeping the `modelchain.get_orientation` function for now.


Discussion:

Patch:
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -63,11 +63,10 @@
 
 
 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.
@@ -332,12 +314,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.
 
@@ -395,7 +371,6 @@ 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',
@@ -421,7 +396,6 @@ def __init__(self, system, location,
         self.temperature_model = temperature_model
 
         self.losses_model = losses_model
-        self.orientation_strategy = orientation_strategy
 
         self.weather = None
         self.times = None
@@ -451,7 +425,6 @@ def __setattr__(self, key, value):
 
     @classmethod
     def with_pvwatts(cls, system, location,
-                     orientation_strategy=None,
                      clearsky_model='ineichen',
                      airmass_model='kastenyoung1989',
                      name=None,
@@ -469,12 +442,6 @@ def with_pvwatts(cls, system, location,
             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 +469,6 @@ def with_pvwatts(cls, system, location,
         >>> ModelChain.with_pvwatts(system, location)
         ModelChain:
           name: None
-          orientation_strategy: None
           clearsky_model: ineichen
           transposition_model: perez
           solar_position_method: nrel_numpy
@@ -518,7 +484,6 @@ def with_pvwatts(cls, system, location,
         config.update(kwargs)
         return ModelChain(
             system, location,
-            orientation_strategy=orientation_strategy,
             clearsky_model=clearsky_model,
             airmass_model=airmass_model,
             name=name,
@@ -527,7 +492,6 @@ def with_pvwatts(cls, system, location,
 
     @classmethod
     def with_sapm(cls, system, location,
-                  orientation_strategy=None,
                   clearsky_model='ineichen',
                   transposition_model='haydavies',
                   solar_position_method='nrel_numpy',
@@ -548,12 +512,6 @@ def with_sapm(cls, system, location,
             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 +547,6 @@ def with_sapm(cls, system, location,
         >>> ModelChain.with_sapm(system, location)
         ModelChain:
           name: None
-          orientation_strategy: None
           clearsky_model: ineichen
           transposition_model: haydavies
           solar_position_method: nrel_numpy
@@ -605,7 +562,6 @@ def with_sapm(cls, system, location,
         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 +572,7 @@ def with_sapm(cls, system, location,
 
     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 +590,6 @@ def getmcattr(self, attr):
         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


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
@@ -333,22 +333,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')
@@ -1235,8 +1219,7 @@ 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)
 
 
@@ -1252,8 +1235,7 @@ def test_infer_temp_model(location, sapm_dc_snl_ac_system,
                   'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system,
                   'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_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 +1245,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 +1421,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):
@@ -1623,23 +1600,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 +1613,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 +1636,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 +1646,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 +1656,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)