Repository: KIT-IAI/pyWATTS

Issue:
Setting Run Parameters to the steps
**What is your feature request related to?**
* Problem related

**Please describe the module/problem**

There exists some parameters like computation_mode or in feature #146 the summary_formatter which are also needed by some steps for working correctly. For solving this there currently exist the method set_computation_mode. However, I think it would be useful if we make this more generic e.g. by a  new method set_run_settings.

**What type of Feature Request**
* Core enhancement


Discussion:

Patch:
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index e7e9d11..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Default ignored files
-/workspace.xml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index efc2746..abe9eea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@
 * Add summary parameter to train and test to ensure backward compatibility. ([#127](https://github.com/KIT-IAI/pyWATTS/issues/127))
 * Add Min amd Max as new summaries ([#105](https://github.com/KIT-IAI/pyWATTS/issues/105))
 * Add MAPE as new summary ([#104](https://github.com/KIT-IAI/pyWATTS/issues/104))
-
+* Add a RunSetting for setting run specific settings in the steps ([#150](https://github.com/KIT-IAI/pyWATTS/issues/150))
 
 ### Changed
 * Remove parameter step.stop. Instead we call the method _should_stop on the previous steps. ([#25](https://github.com/KIT-IAI/pyWATTS/issues/25))
diff --git a/docs/source/api/pywatts.core.rst b/docs/source/api/pywatts.core.rst
index 9f49435..1c82279 100644
--- a/docs/source/api/pywatts.core.rst
+++ b/docs/source/api/pywatts.core.rst
@@ -100,6 +100,14 @@ pywatts.core.result\_step module
    :undoc-members:
    :show-inheritance:
 
+pywatts.core.run\_setting module
+--------------------------------
+
+.. automodule:: pywatts.core.run_setting
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 pywatts.core.start\_step module
 -------------------------------
 
diff --git a/pywatts/core/base_step.py b/pywatts/core/base_step.py
index e35be31..6fa8f60 100644
--- a/pywatts/core/base_step.py
+++ b/pywatts/core/base_step.py
@@ -8,6 +8,7 @@ import xarray as xr
 
 from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.filemanager import FileManager
+from pywatts.core.run_setting import RunSetting
 from pywatts.utils._xarray_time_series_utils import _get_time_indeces
 
 logger = logging.getLogger(__name__)
@@ -29,8 +30,9 @@ class BaseStep(ABC):
     def __init__(self, input_steps: Optional[Dict[str, "BaseStep"]] = None,
                  targets: Optional[Dict[str, "BaseStep"]] = None, condition=None,
                  computation_mode=ComputationMode.Default):
-        self._original_compuation_mode = computation_mode
-        self.computation_mode = computation_mode
+        self.default_run_setting = RunSetting(computation_mode=computation_mode)
+        self.current_run_setting = self.default_run_setting.clone()
+
         self.input_steps: Dict[str, "BaseStep"] = dict() if input_steps is None else input_steps
         self.targets: Dict[str, "BaseStep"] = dict() if targets is None else targets
         self.condition = condition
@@ -88,8 +90,8 @@ class BaseStep(ABC):
         if self.cached_result["cached"] is not None and self.cached_result["start"] == start and self.cached_result[
             "end"] == end:
             return copy.deepcopy(self.cached_result["cached"]) if return_all else copy.deepcopy(self.cached_result["cached"][
-                buffer_element]) if buffer_element is not None else copy.deepcopy(list(self.cached_result["cached"].values())[
-                0])
+                    buffer_element]) if buffer_element is not None else copy.deepcopy(list(self.cached_result["cached"].values())[
+                    0])
         return self._pack_data(start, end, buffer_element, return_all=return_all)
 
     def _compute(self, start, end) -> Dict[str, xr.DataArray]:
@@ -181,7 +183,7 @@ class BaseStep(ABC):
             "class": self.__class__.__name__,
             "name": self.name,
             "last": self.last,
-            "computation_mode": int(self._original_compuation_mode)
+            "default_run_setting": self.default_run_setting.save()
         }
 
     @classmethod
@@ -223,9 +225,9 @@ class BaseStep(ABC):
         """
         self.buffer = {}
         self.finished = False
-        self.computation_mode = self._original_compuation_mode
+        self.current_run_setting = self.default_run_setting.clone()
 
-    def set_computation_mode(self, computation_mode: ComputationMode):
+    def set_run_setting(self, run_setting: RunSetting):
         """
         Sets the computation mode of the step for the current run. Note that after reset the all mode is restored.
         Moreover, setting the computation_mode is only possible if the computation_mode is not set explicitly while
@@ -234,5 +236,4 @@ class BaseStep(ABC):
         :param computation_mode: The computation mode which should be set.
         :type computation_mode: ComputationMode
         """
-        if self._original_compuation_mode == computation_mode.Default:
-            self.computation_mode = computation_mode
+        self.current_run_setting = self.default_run_setting.update(run_setting)
diff --git a/pywatts/core/pipeline.py b/pywatts/core/pipeline.py
index 72844cd..d4c36b6 100644
--- a/pywatts/core/pipeline.py
+++ b/pywatts/core/pipeline.py
@@ -14,6 +14,7 @@ import xarray as xr
 
 from pywatts.core.base import BaseTransformer
 from pywatts.core.base_step import BaseStep
+from pywatts.core.run_setting import RunSetting
 from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.exceptions.io_exceptions import IOException
 from pywatts.core.filemanager import FileManager
@@ -179,7 +180,7 @@ class Pipeline(BaseTransformer):
 
         for step in self.id_to_step.values():
             step.reset()
-            step.set_computation_mode(mode)
+            step.set_run_setting(RunSetting(computation_mode=mode))
 
         if isinstance(data, pd.DataFrame):
             data = data.to_xarray()
diff --git a/pywatts/core/pipeline_step.py b/pywatts/core/pipeline_step.py
index 9e20d2d..77d83e1 100644
--- a/pywatts/core/pipeline_step.py
+++ b/pywatts/core/pipeline_step.py
@@ -1,9 +1,9 @@
+from pywatts.core.run_setting import RunSetting
 from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.pipeline import Pipeline
 from pywatts.core.step import Step
 
 
-
 class PipelineStep(Step):
     """
     This step is necessary for subpipelining. Since it contains functionality for adding a pipeline as a
@@ -26,9 +26,9 @@ class PipelineStep(Step):
     """
     module: Pipeline
 
-    def set_computation_mode(self, computation_mode: ComputationMode):
+    def set_run_setting(self, run_setting: RunSetting):
         """
-        Sets the computation mode of the step for the current run. Note that after reset the all mode is restored.
+        Sets the run settings of the step for the current run. Note that after reset old setting is restored.
         Moreover, setting the computation_mode is only possible if the computation_mode is not set explicitly while
         adding the corresponding module to the pipeline.
         Moreover, it sets also the computation_mode of all steps in the subpipeline.
@@ -36,10 +36,10 @@ class PipelineStep(Step):
         :param computation_mode: The computation mode which should be set.
         :type computation_mode: ComputationMode
         """
-        if self._original_compuation_mode == computation_mode.Default:
-            self.computation_mode = computation_mode
-            for step in self.module.id_to_step.values():
-                step.set_computation_mode(computation_mode)
+        self.current_run_setting = self.default_run_setting.update(run_setting=run_setting)
+        for step in self.module.id_to_step.values():
+            step.set_run_setting(run_setting)
+
 
     def reset(self):
         """
diff --git a/pywatts/core/run_setting.py b/pywatts/core/run_setting.py
new file mode 100644
index 0000000..bcca787
--- /dev/null
+++ b/pywatts/core/run_setting.py
@@ -0,0 +1,58 @@
+from typing import Dict
+
+from pywatts.core.computation_mode import ComputationMode
+
+
+class RunSetting:
+    """
+    The RunSetting contains the setting which is specific to one run.
+
+    :param computation_mode: The computation mode for the specific run.
+    :type computation_mode: ComputationMode
+    """
+
+    def __init__(self, computation_mode: ComputationMode):
+        self.computation_mode = computation_mode
+
+    def update(self, run_setting: 'RunSetting') -> 'RunSetting':
+        """
+        Updates and returns a new run_setting. **Note** The existing run_settings stay unchanged.
+
+        :param run_setting: The run_setting which should be combined with this RunSetting.
+        :type run_setting: RunSetting
+        :return: The new RunSetting
+        :rtype: RunSetting
+        """
+        setting = self.clone()
+        if setting.computation_mode == ComputationMode.Default:
+            setting.computation_mode = run_setting.computation_mode
+        return setting
+
+    def clone(self) -> 'RunSetting':
+        """
+        Clones the current RunSetting.
+        :return: The cloned RunSetting.
+        :rtype: RunSetting
+        """
+        return RunSetting(
+            computation_mode=self.computation_mode
+        )
+
+    def save(self) -> Dict:
+        """
+        Saves the RunSetting as JSON.
+        :return: A dict which contains all information needed for restoring the RunSetting
+        :rtype: Dict
+        """
+        return {
+            "computation_mode": int(self.computation_mode)
+        }
+
+    @staticmethod
+    def load(load_information: Dict):
+        """
+        Create a RunSetting from a Dict.
+        :return: The loaded RunSetting
+        :rtype: RunSetting
+        """
+        return RunSetting(**load_information)
diff --git a/pywatts/core/step.py b/pywatts/core/step.py
index 278c6d0..797e9b6 100644
--- a/pywatts/core/step.py
+++ b/pywatts/core/step.py
@@ -8,6 +8,7 @@ import xarray as xr
 from pywatts.callbacks import BaseCallback
 from pywatts.core.base import Base, BaseEstimator
 from pywatts.core.base_step import BaseStep
+from pywatts.core.run_setting import RunSetting
 from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.exceptions.not_fitted_exception import NotFittedException
 from pywatts.core.filemanager import FileManager
@@ -108,9 +109,10 @@ class Step(BaseStep):
             callback.set_filemanager(file_manager)
             callbacks.append(callback)
 
-        step = cls(module, inputs, targets=targets, file_manager=file_manager,
-                   computation_mode=ComputationMode(stored_step["computation_mode"]), condition=condition,
+        step = cls(module, inputs, targets=targets, file_manager=file_manager, condition=condition,
                    train_if=train_if, callbacks=callbacks, batch_size=stored_step["batch_size"])
+        step.default_run_setting = RunSetting.load(stored_step["default_run_setting"])
+        step.current_run_setting = step.default_run_setting.clone()
         step.id = stored_step["id"]
         step.name = stored_step["name"]
         step.last = stored_step["last"]
@@ -120,7 +122,8 @@ class Step(BaseStep):
     def _compute(self, start, end):
         input_data = self._get_input(start, end)
         target = self._get_target(start, end)
-        if self.computation_mode in [ComputationMode.Default, ComputationMode.FitTransform, ComputationMode.Train] and (
+        if self.current_run_setting.computation_mode in [ComputationMode.Default, ComputationMode.FitTransform,
+                                                         ComputationMode.Train] and (
                 not self.train_if or self.train_if(input_data, target)):
             # Fetch input_data and target data
             start_time = time.time()


Test patch:
diff --git a/tests/unit/core/test_either_or_step.py b/tests/unit/core/test_either_or_step.py
index 3ba225d..350463a 100644
--- a/tests/unit/core/test_either_or_step.py
+++ b/tests/unit/core/test_either_or_step.py
@@ -37,13 +37,13 @@ class TestEitherOrStep(unittest.TestCase):
         params = {
             "target_ids": {},
             "input_ids": {2: "stepTwo", 1: "stepOne"},
-            'computation_mode': 4,
+            'default_run_setting': {'computation_mode': 4},
             "id": -1,
             "module": "pywatts.core.either_or_step",
             "class": "EitherOrStep",
             "name": "EitherOrStep",
             "last": False
         }
-        step = EitherOrStep.load(params, {"stepOne": self.step_one, "stepTwo":self.step_two}, None, None, None)
+        step = EitherOrStep.load(params, {"stepOne": self.step_one, "stepTwo": self.step_two}, None, None, None)
         json = step.get_json("file")
         self.assertEqual(params, json)
diff --git a/tests/unit/core/test_pipeline.py b/tests/unit/core/test_pipeline.py
index 5f9fbf4..87c8a1e 100644
--- a/tests/unit/core/test_pipeline.py
+++ b/tests/unit/core/test_pipeline.py
@@ -11,6 +11,7 @@ from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.pipeline import Pipeline
 from pywatts.core.start_step import StartStep
 from pywatts.core.step import Step
+from pywatts.core.run_setting import RunSetting
 from pywatts.modules.missing_value_detection import MissingValueDetector
 from pywatts.modules.root_mean_squared_error import RmseCalculator
 from pywatts.summaries import RMSE
@@ -35,7 +36,7 @@ pipeline_json = {'id': 1,
                                          'normalize': False},
                               'sklearn_module': os.path.join('test_pipeline', 'LinearRegression.pickle')}],
                  'steps': [{'class': 'StartStep',
-                            'computation_mode': 4,
+                            'default_run_setting': {'computation_mode': 4},
                             'id': 1,
                             'index': 'input',
                             'input_ids': {},
@@ -46,7 +47,7 @@ pipeline_json = {'id': 1,
                            {'batch_size': None,
                             'callbacks': [],
                             'class': 'Step',
-                            'computation_mode': 4,
+                            'default_run_setting': {'computation_mode': 4},
                             'condition': None,
                             'id': 2,
                             'input_ids': {1: 'input'},
@@ -59,7 +60,7 @@ pipeline_json = {'id': 1,
                            {'batch_size': None,
                             'callbacks': [],
                             'class': 'Step',
-                            'computation_mode': 4,
+                            'default_run_setting': {'computation_mode': 4},
                             'condition': None,
                             'id': 3,
                             'input_ids': {2: 'x'},
@@ -221,7 +222,7 @@ class TestPipeline(unittest.TestCase):
             ['2015-06-03 00:00:00', '2015-06-03 01:00:00'])))
 
         for step in self.pipeline.id_to_step.values():
-            assert step.computation_mode == ComputationMode.FitTransform
+            assert step.current_run_setting.computation_mode == ComputationMode.FitTransform
 
     @patch('pywatts.core.pipeline.FileManager')
     def test_add_pipeline_to_pipeline_and_test(self, fm_mock):
@@ -338,7 +339,7 @@ class TestPipeline(unittest.TestCase):
 
         # Assert that the computation is set to fit_transform if the ComputationMode was default
         first_step = MagicMock()
-        first_step.computation_mode = ComputationMode.Default
+        first_step.run_setting = RunSetting(ComputationMode.Default)
         first_step.finished = False
         first_step.further_elements.side_effect = [True, True, True, True, False]
 
@@ -350,7 +351,8 @@ class TestPipeline(unittest.TestCase):
                             index=pd.DatetimeIndex(pd.date_range('2000-01-01', freq='24H', periods=4)))
         self.pipeline.test(data)
 
-        first_step.set_computation_mode.assert_called_once_with(ComputationMode.Transform)
+        first_step.set_run_setting.assert_called_once()
+        self.assertEqual(first_step.set_run_setting.call_args[0][0].computation_mode, ComputationMode.Transform)
         calls = [
             call(pd.Timestamp('2000-01-01 00:00:00', freq='24H'), pd.Timestamp('2000-01-02 00:00:00', freq='24H'),
                  return_all=True),
@@ -490,8 +492,10 @@ class TestPipeline(unittest.TestCase):
         second_step.get_result.assert_called_once_with(pd.Timestamp('2000-01-01 00:00:00', freq='24H'), None,
                                                        return_all=True)
 
-        first_step.set_computation_mode.assert_called_once_with(ComputationMode.Transform)
-        second_step.set_computation_mode.assert_called_once_with(ComputationMode.Transform)
+        first_step.set_run_setting.assert_called_once()
+        self.assertEqual(first_step.set_run_setting.call_args[0][0].computation_mode, ComputationMode.Transform)
+        second_step.set_run_setting.assert_called_once()
+        self.assertEqual(second_step.set_run_setting.call_args[0][0].computation_mode, ComputationMode.Transform)
 
         first_step.reset.assert_called_once()
         second_step.reset.assert_called_once()
@@ -521,8 +525,11 @@ class TestPipeline(unittest.TestCase):
                             index=pd.DatetimeIndex(pd.date_range('2000-01-01', freq='24H', periods=5)))
         result, summary = self.pipeline.train(data, summary=True)
 
-        first_step.set_computation_mode.assert_called_once_with(ComputationMode.FitTransform)
-        second_step.set_computation_mode.assert_called_once_with(ComputationMode.FitTransform)
+        first_step.set_run_setting.assert_called_once()
+        self.assertEqual(first_step.set_run_setting.call_args[0][0].computation_mode, ComputationMode.FitTransform)
+        second_step.set_run_setting.assert_called_once()
+        self.assertEqual(second_step.set_run_setting.call_args[0][0].computation_mode, ComputationMode.FitTransform)
+
         first_step.get_result.assert_called_once_with(pd.Timestamp('2000-01-01 00:00:00', freq='24H'), None,
                                                       return_all=True)
         second_step.get_result.assert_called_once_with(pd.Timestamp('2000-01-01 00:00:00', freq='24H'), None,
@@ -537,7 +544,6 @@ class TestPipeline(unittest.TestCase):
         lin_reg = LinearRegression()
         self.fm_mock.get_path.return_value = "summary_path"
 
-
         multi_regressor = SKLearnWrapper(lin_reg)(foo=self.pipeline["foo"], target=self.pipeline["target"],
                                                   target2=self.pipeline["target2"])
         RMSE()(y=self.pipeline["target"], prediction=multi_regressor["target"])
diff --git a/tests/unit/core/test_run_setting.py b/tests/unit/core/test_run_setting.py
new file mode 100644
index 0000000..d90838a
--- /dev/null
+++ b/tests/unit/core/test_run_setting.py
@@ -0,0 +1,44 @@
+import unittest
+
+from pywatts.core.computation_mode import ComputationMode
+from pywatts.core.run_setting import RunSetting
+
+
+class TestRunSetting(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.run_setting = RunSetting(computation_mode=ComputationMode.Default)
+
+    def tearDown(self) -> None:
+        self.run_setting = None
+
+    def test_update(self):
+        run_setting = self.run_setting.update(RunSetting(computation_mode=ComputationMode.Train))
+        self.assertEqual(run_setting.computation_mode, ComputationMode.Train)
+
+    def test_update_computation_mode_not_updatable(self):
+        run_setting = RunSetting(computation_mode=ComputationMode.Train)
+        run_setting.update(RunSetting(computation_mode=ComputationMode.Transform))
+        self.assertEqual(run_setting.computation_mode, ComputationMode.Train)
+
+        run_setting = RunSetting(computation_mode=ComputationMode.Transform)
+        run_setting.update(RunSetting(computation_mode=ComputationMode.Train))
+        self.assertEqual(run_setting.computation_mode, ComputationMode.Transform)
+
+    def test_clone(self):
+        run_setting = self.run_setting.clone()
+
+        self.assertEqual(run_setting.computation_mode, self.run_setting.computation_mode)
+
+    def test_save(self):
+        json = self.run_setting.save()
+
+        self.assertEqual(json, {
+            "computation_mode": 4
+        })
+
+    def test_load(self):
+        run_setting = RunSetting.load({
+            "computation_mode": 4
+        })
+        self.assertEqual(run_setting.computation_mode, ComputationMode.Default)
diff --git a/tests/unit/core/test_start_step.py b/tests/unit/core/test_start_step.py
index a44813f..aacc593 100644
--- a/tests/unit/core/test_start_step.py
+++ b/tests/unit/core/test_start_step.py
@@ -15,7 +15,7 @@ class TestStartStep(unittest.TestCase):
             "target_ids": {},
             "input_ids": {},
             "id": -1,
-            'computation_mode': 4,
+            'default_run_setting': {'computation_mode': 4},
             "module": "pywatts.core.start_step",
             "class": "StartStep",
             "name": "StartStep",
diff --git a/tests/unit/core/test_step.py b/tests/unit/core/test_step.py
index cf46004..f0474f7 100644
--- a/tests/unit/core/test_step.py
+++ b/tests/unit/core/test_step.py
@@ -8,6 +8,7 @@ import xarray as xr
 
 from pywatts.core.computation_mode import ComputationMode
 from pywatts.core.step import Step
+from pywatts.core.run_setting import RunSetting
 
 
 class TestStep(unittest.TestCase):
@@ -48,7 +49,7 @@ class TestStep(unittest.TestCase):
             'batch_size': None,
             "input_ids": {},
             "id": -1,
-            'computation_mode': 4,
+            'default_run_setting': {'computation_mode': 4},
             "train_if": None,
             "module": "pywatts.core.step",
             "class": "Step",
@@ -84,7 +85,7 @@ class TestStep(unittest.TestCase):
             "input_ids": {},
             "id": -1,
             'batch_size': None,
-            'computation_mode': 4,
+            'default_run_setting': {'computation_mode': 4},
             "train_if": os.path.join("folder", "test_train_if.pickle"),
             "module": "pywatts.core.step",
             "class": "Step",
@@ -209,7 +210,7 @@ class TestStep(unittest.TestCase):
             'batch_size': None,
             "target_ids": {},
             "input_ids": {2: 'x'},
-            'computation_mode': 3,
+            'default_run_setting': {'computation_mode': 3},
             "id": -1,
             "module": "pywatts.core.step",
             "class": "Step",
@@ -232,7 +233,7 @@ class TestStep(unittest.TestCase):
         self.assertEqual({'batch_size': None,
                           'callbacks': [],
                           'class': 'Step',
-                          'computation_mode': 4,
+                          'default_run_setting': {'computation_mode': 4},
                           'condition': None,
                           'id': -1,
                           'input_ids': {},
@@ -242,30 +243,30 @@ class TestStep(unittest.TestCase):
                           'target_ids': {},
                           'train_if': None}, json)
 
-    def test_set_computation_mode(self):
+    def test_set_run_setting(self):
         step = Step(MagicMock(), MagicMock(), MagicMock())
-        step.set_computation_mode(ComputationMode.FitTransform)
+        step.set_run_setting(RunSetting(ComputationMode.FitTransform))
 
-        assert step.computation_mode == ComputationMode.FitTransform
+        assert step.current_run_setting.computation_mode == ComputationMode.FitTransform
 
-        step.set_computation_mode(ComputationMode.Transform)
-        assert step.computation_mode == ComputationMode.Transform
+        step.set_run_setting(RunSetting(ComputationMode.Transform))
+        assert step.current_run_setting.computation_mode == ComputationMode.Transform
 
     def test_set_computation_mode_specified(self):
         step = Step(MagicMock(), MagicMock(), MagicMock(), computation_mode=ComputationMode.FitTransform)
-        assert step.computation_mode == ComputationMode.FitTransform
+        assert step.current_run_setting.computation_mode == ComputationMode.FitTransform
 
-        step.set_computation_mode(ComputationMode.Transform)
-        assert step.computation_mode == ComputationMode.FitTransform
+        step.set_run_setting(RunSetting(computation_mode=ComputationMode.Transform))
+        assert step.current_run_setting.computation_mode == ComputationMode.FitTransform
 
     def test_reset(self):
         step = Step(MagicMock(), MagicMock(), MagicMock())
         step.buffer = MagicMock()
-        step.computation_mode = ComputationMode.Transform
+        step.current_run_setting = RunSetting(computation_mode=ComputationMode.Transform)
         step.finished = True
         step.reset()
 
         self.assertIsNone(None)
-        assert step.computation_mode == ComputationMode.Default
+        assert step.current_run_setting.computation_mode == ComputationMode.Default
         assert step._should_stop(None, None) == False
         assert step.finished == False