Repository: TimeEval/TimeEval (language: python)

Issue:
Docs: Improve inline documentation (Pydoc)
Add Python docstrings to all user-facing APIs. We use the [numpy-format](https://numpydoc.readthedocs.io/en/latest/format.html) for the docstrings.

Modules and classes requiring documentation because they are user-facing:

- [x] `timeeval.TimeEval`
- [x] `timeeval.Algorithm`
- [x] `timeeval.Remoteconfiguration`
- [x] `timeeval.utils.Metric`
- [x] `timeeval.constants.HPI_CLUSTER`
- [x] `timeeval.data_types`
- [x] `timeeval.datasets` (when !162 is merged)
- [x] `timeeval.params`
- [x] `timeeval.adapters`
- [x] `timeeval.heuristics`

Modules and classes not using the numpy-format:

- [x] `timeeval.ResourceConstraints`


Discussion / hints:

Fix patch:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index cd1b3a7a..cb098b2f 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -4,6 +4,6 @@ API Reference
 This section documents the public API of TimeEval.
 
 .. toctree::
-   :maxdepth: 3
+   :maxdepth: 4
 
    timeeval
diff --git a/docs/api/timeeval.core.rst b/docs/api/timeeval.core.rst
new file mode 100644
index 00000000..76728e08
--- /dev/null
+++ b/docs/api/timeeval.core.rst
@@ -0,0 +1,37 @@
+timeeval.core package
+=====================
+
+.. warning::
+    This package contains TimeEval-internal classes and functions.
+
+timeeval.core.experiments.Experiment
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: timeeval.core.experiments.Experiment
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+timeeval.core.experiments.Experiments
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: timeeval.core.experiments.Experiments
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+timeeval.core.remote.Remote
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: timeeval.core.remote.Remote
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+timeeval.core.times.Times
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: timeeval.core.times.Times
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/api/timeeval.rst b/docs/api/timeeval.rst
index b344c83e..5049b3ca 100644
--- a/docs/api/timeeval.rst
+++ b/docs/api/timeeval.rst
@@ -6,106 +6,91 @@ timeeval package
    :undoc-members:
    :show-inheritance:
 
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 1
-
-   timeeval.adapters
-   timeeval.datasets
-   timeeval.heuristics
-   timeeval.params
-   timeeval.utils
-
-
-timeeval.constants
-------------------
+timeeval.TimeEval
+-----------------
 
-.. automodule:: timeeval.constants
+.. autoclass:: timeeval.TimeEval
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.data\_types
---------------------
+timeeval.Status
+---------------
 
-.. automodule:: timeeval.data_types
+.. autoclass:: timeeval.Status
    :members:
    :undoc-members:
    :show-inheritance:
 
-Module contents
----------------
-
-timeeval.algorithm.Algorithm
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.Algorithm
+------------------
 
-.. autoclass:: timeeval.algorithm.Algorithm
+.. autoclass:: timeeval.Algorithm
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.experiments.Experiment
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.InputDimensionality
+----------------------------
 
-.. autoclass:: timeeval.experiments.Experiment
+.. autoclass:: timeeval.InputDimensionality
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.experiments.Experiments
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.TrainingType
+---------------------
 
-.. autoclass:: timeeval.experiments.Experiments
+.. autoclass:: timeeval.TrainingType
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.remote.Remote
-^^^^^^^^^^^^^^^^^^^^^^
+timeeval.RemoteConfiguration
+----------------------------
 
-.. autoclass:: timeeval.remote.Remote
+.. autoclass:: timeeval.RemoteConfiguration
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.remote_configuration.RemoteConfiguration
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.ResourceConstraints
+----------------------------
 
-.. autoclass:: timeeval.remote_configuration.RemoteConfiguration
+.. autoclass:: timeeval.ResourceConstraints
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.resource_constraints.ResourceConstraints
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: timeeval.resource_constraints.ResourceConstraints
-   :members:
+.. automodule:: timeeval.resource_constraints
+   :members: MB, GB
    :undoc-members:
-   :show-inheritance:
 
-timeeval.timeeval.Status
-^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.constants
+------------------
 
-.. autoclass:: timeeval.timeeval.Status
+.. automodule:: timeeval.constants
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.timeeval.TimeEval
-^^^^^^^^^^^^^^^^^^^^^^^^^^
+timeeval.data\_types.ExecutionType
+----------------------------------
 
-.. autoclass:: timeeval.timeeval.TimeEval
+.. autoclass:: timeeval.data_types.ExecutionType
    :members:
    :undoc-members:
    :show-inheritance:
 
-timeeval.times.Times
-^^^^^^^^^^^^^^^^^^^^
+Subpackages
+-----------
 
-.. autoclass:: timeeval.times.Times
-   :members:
-   :undoc-members:
-   :show-inheritance:
+.. toctree::
+   :maxdepth: 1
+
+   timeeval.adapters
+   timeeval.datasets
+   timeeval.heuristics
+   timeeval.params
+   timeeval.utils
+   timeeval.core
diff --git a/docs/concepts/distributed.md b/docs/concepts/distributed.md
index 64ff1336..b01961a2 100644
--- a/docs/concepts/distributed.md
+++ b/docs/concepts/distributed.md
@@ -1,6 +1,6 @@
 # Distributed TimeEval
 
 TimeEval is able to run multiple tests in parallel on a cluster.
-It uses [Dask's SSHCluster](https://docs.dask.org/en/latest/setup/ssh.html#distributed.deploy.ssh.SSHCluster) to distribute tasks.
+It uses {obj}`Dask's SSHCluster <dask.distributed.SSHCluster>` to distribute tasks.
 In order to use this feature, the `TimeEval` class accepts a `distributed: bool` flag
-and  additional configuration options with `ssh_cluster_kwargs: dict` to setup the [SSHCluster](https://docs.dask.org/en/latest/setup/ssh.html#distributed.deploy.ssh.SSHCluster).
+and  additional configuration options with `ssh_cluster_kwargs: dict` to setup the {obj}`~dask.distributed.SSHCluster`.
diff --git a/docs/conf.py b/docs/conf.py
index e57ef678..f5974345 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -34,8 +34,10 @@
 extensions = [
     'sphinx.ext.napoleon',
     'sphinx.ext.autodoc',
+    'sphinx.ext.intersphinx',
     'myst_parser',
-    'sphinx_rtd_dark_mode'
+    'sphinx_rtd_dark_mode',
+    'sphinx.ext.mathjax'
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -72,3 +74,13 @@
 
 # -- myst_parser settings ----------------------------------------------------
 myst_heading_anchors = 3
+
+# -- intersphinx settings ----------------------------------------------------
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3', None),
+    'dask': ('https://docs.dask.org/en/stable/', None),
+    'numpy': ('https://numpy.org/doc/1.21/', None),
+    'pandas': ('https://pandas.pydata.org/pandas-docs/version/1.3/', None),
+    'sklearn': ('https://scikit-learn.org/0.24/', None),
+    'statsmodels': ('https://www.statsmodels.org/v0.12.0/', None),
+}
diff --git a/scripts/create_summary.py b/scripts/create_summary.py
index 5ce4bd23..0ceb8be4 100644
--- a/scripts/create_summary.py
+++ b/scripts/create_summary.py
@@ -12,7 +12,7 @@
 from timeeval.adapters.docker import SCORES_FILE_NAME as DOCKER_SCORES_FILE_NAME
 from timeeval.constants import RESULTS_CSV, HYPER_PARAMETERS, METRICS_CSV, EXECUTION_LOG, ANOMALY_SCORES_TS
 from timeeval.data_types import ExecutionType
-from timeeval.experiments import Experiment as TimeEvalExperiment
+from timeeval.core.experiments import Experiment as TimeEvalExperiment
 from timeeval.heuristics import inject_heuristic_values
 from timeeval.utils.datasets import load_labels_only
 from timeeval_experiments.algorithm_configurator import AlgorithmConfigurator
diff --git a/scripts/reevaluate.py b/scripts/reevaluate.py
index 256db3da..fd4b0317 100644
--- a/scripts/reevaluate.py
+++ b/scripts/reevaluate.py
@@ -12,7 +12,7 @@
 from timeeval.adapters.docker import SCORES_FILE_NAME as DOCKER_SCORES_FILE_NAME
 from timeeval.constants import RESULTS_CSV, HYPER_PARAMETERS, METRICS_CSV, ANOMALY_SCORES_TS
 from timeeval.data_types import ExecutionType
-from timeeval.experiments import Experiment as TimeEvalExperiment
+from timeeval.core.experiments import Experiment as TimeEvalExperiment
 from timeeval.utils.datasets import load_labels_only
 from timeeval.utils.metrics import PrecisionAtK, FScoreAtK
 
diff --git a/timeeval/__init__.py b/timeeval/__init__.py
index bbe5fb61..933217e0 100644
--- a/timeeval/__init__.py
+++ b/timeeval/__init__.py
@@ -1,13 +1,10 @@
 """
 An Evaluation Tool for Anomaly Detection Algorithms on Time Series Data.
 
-Provides:
-  1. tbd
-
-How to use the documentation:
+**How to use the documentation:**
 
 Documentation is available in two forms: docstrings provided with the code, and a loose standing reference guide,
-available on `timeeval.readthedocs.io <timeeval.readthedocs.io>`_.
+available on `timeeval.readthedocs.io <https://timeeval.readthedocs.io>`_.
 
 Code snippets in the docstring examples are indicated by three greater-than signs::
 
diff --git a/timeeval/_version.py b/timeeval/_version.py
index edcea843..d4c4a089 100644
--- a/timeeval/_version.py
+++ b/timeeval/_version.py
@@ -1,1 +1,2 @@
 __version__: str = "1.2.6rc1"
+"""Version of this TimeEval installation"""
diff --git a/timeeval/algorithm.py b/timeeval/algorithm.py
index 6ba40bec..0e0b6717 100644
--- a/timeeval/algorithm.py
+++ b/timeeval/algorithm.py
@@ -21,16 +21,17 @@ class Algorithm:
     ----------
     name : str
         The name of the Algorithm shown in the results.
-    main : Adapter
-        Adapter The :py:class:`Adapter<timeeval.adapters.base.BaseAdapter>` that contains the algorithm to evaluate.
+    main : timeeval.adapters.base.Adapter
+        The adapter implementation that contains the algorithm to evaluate.
     preprocess : Optional[TSFunction]
-        Optional function to perform before `main` to modify input data.
+        Optional function to perform before ``main`` to modify input data.
     postprocess : Optional[TSFunctionPost]
-        Optional function to perform after `main` to modify output data.
+        Optional function to perform after ``main`` to modify output data.
     data_as_file : bool
-        Whether the data is input as `Path` or as `numpy.ndarray`.
+        Whether the data input is a ``Path`` or a ``numpy.ndarray``.
     param_schema : Dict[str, Dict[str, Any]]
-        Optional schema of the algorithm's input parameters needed by AlgorithmConfigurator. Schema definition::
+        Optional schema of the algorithm's input parameters needed by :class:`timeeval_experiments.algorithm_configurator.AlgorithmConfigurator`.
+        Schema definition::
 
             [
                 "param_name": {
@@ -41,15 +42,24 @@ class Algorithm:
                 },
             ]
 
-    param_config : ParameterConfig
+    param_config : timeeval.params.search.ParameterConfig
         Optional object of type ParameterConfig to define a search grid or fixed parameters.
-    training_type : TrainingType
+    training_type : timeeval.data_types.TrainingType
         Definition of training type to receive the correct dataset formats (needed if TimeEval is run with
-        `force_training_type_match` config).
-    input_dimensionality : InputDimensionality
+        ``force_training_type_match`` config).
+    input_dimensionality : timeeval.data_types.InputDimensionality
         Definition of training type to receive the correct dataset formats (needed if TimeEval is run with
-        `force_dimensionality_match` config).
+        ``force_dimensionality_match`` config option).
 
+    Examples
+    --------
+    Create a baseline algorithm that always assigns a normal anomaly score:
+
+    >>> import numpy as np
+    >>> from timeeval import Algorithm
+    >>> from timeeval.adapters import FunctionAdapter
+    >>> my_fn = lambda X, args: np.zeros(len(X))
+    >>> Algorithm(name="Test Algorithm", main=FunctionAdapter(my_fn), data_as_file=False)
     """
 
     name: str
@@ -63,6 +73,39 @@ class Algorithm:
     input_dimensionality: InputDimensionality = InputDimensionality.UNIVARIATE
 
     def train(self, dataset: AlgorithmParameter, args: Optional[dict] = None) -> AlgorithmParameter:
+        """Execute this algorithm's training procedure.
+
+        .. warning::
+            Internal API!
+
+        This method sets the algorithms :class:`~timeeval.ExecutionType` to ``TRAIN`` and then calls the
+        adapter implementation.
+        Calling ``train()`` on an unsupervised algorithm is not supported and will raise a :class:`ValueError`.
+
+        Parameters
+        ----------
+        dataset : timeeval.data_types.AlgorithmParameter
+            Either a numpy-array containing the training time series data or a path to the training time series file.
+        args : dict
+            TimeEval arguments that get passed to the :class:`~timeeval.adapters.Adapter`-implementation. Example::
+
+                {
+                    "hyper_params": {},
+                    "results_path": Path("results"),
+                    "resource_constraints": ResourceConstraints(),
+                    "dataset_details": ...
+                }
+
+        Returns
+        -------
+        results : timeeval.data_types.AlgorithmParameter
+            Optionally returns the training results or a path to the model-file.
+
+            .. note::
+                Currently not used by TimeEval!
+
+        :meta private:
+        """
         if self.training_type == TrainingType.UNSUPERVISED:
             raise ValueError("Calling 'train()' on an unsupervised algorithm is not supported! "
                              f"Algorithm '{self.name}' has training type '{self.training_type.value}', but you tried "
@@ -72,22 +115,81 @@ def train(self, dataset: AlgorithmParameter, args: Optional[dict] = None) -> Alg
         return self.main(dataset, args)
 
     def execute(self, dataset: AlgorithmParameter, args: Optional[dict] = None) -> AlgorithmParameter:
+        """Execute this algorithm's test/execute procedure.
+
+        .. warning::
+            Internal API!
+
+        This method sets the algorithms :class:`~timeeval.data_types.ExecutionType` to ``EXECUTE`` and then calls the
+        adapter implementation.
+
+        Parameters
+        ----------
+        dataset : timeeval.data_types.AlgorithmParameter
+            Either a numpy-array containing the test time series data or a path to the test time series file.
+        args : dict
+            TimeEval arguments that get passed to the :class:`~timeeval.adapters.Adapter`-implementation. Example::
+
+                {
+                    "hyper_params": {},
+                    "results_path": Path("results"),
+                    "resource_constraints": ResourceConstraints(),
+                    "dataset_details": ...
+                }
+
+        Returns
+        -------
+        scores : timeeval.data_types.AlgorithmParameter
+            Should return the anomaly scores for the test time series, but can also return something else.
+            The implementation of the `postprocess`-function must take care of transforming the result to valid
+            anomaly scores.
+
+        :meta private:
+        """
         args = args or {}
         args["executionType"] = ExecutionType.EXECUTE
         return self.main(dataset, args)
 
     def prepare_fn(self) -> Optional[Callable[[], None]]:
+        """Returns the prepare-step-function of this algorithm.
+
+        .. warning::
+            Internal API!
+
+        :meta private:
+        """
         return self.main.get_prepare_fn()
 
     def prepare(self) -> None:
+        """Executes the prepare-step of this algorithm.
+
+        .. warning::
+            Internal API!
+
+        :meta private:
+        """
         fn = self.prepare_fn()
         if fn:
             fn()
 
     def finalize_fn(self) -> Optional[Callable[[], None]]:
+        """Returns the finalize-step-function of this algorithm.
+
+        .. warning::
+            Internal API!
+
+        :meta private:
+        """
         return self.main.get_finalize_fn()
 
     def finalize(self) -> None:
+        """Executes the finalize-step of this algorithm.
+
+        .. warning::
+            Internal API!
+
+        :meta private:
+        """
         fn = self.finalize_fn()
         if fn:
             fn()
diff --git a/timeeval/constants.py b/timeeval/constants.py
index 76cac8cc..97eece3b 100644
--- a/timeeval/constants.py
+++ b/timeeval/constants.py
@@ -10,6 +10,10 @@
 
 
 class HPI_CLUSTER:
+    """Cluster constant for the HPI cluster.
+
+    These constants are applicable only for the HPI infrastructure and might not be useful for you.
+    """
     odin01: str = "odin01"
     odin02: str = "odin02"
     odin03: str = "odin03"
@@ -43,11 +47,13 @@ class HPI_CLUSTER:
     nodes: List[str] = [
         odin01, odin02, odin03, odin04, odin05, odin06, odin07, odin08, odin09, odin10, odin11, odin12, odin13, odin14
     ]
+    """All nodes of the homogenous HPI cluster."""
 
     nodes_ip: List[str] = [
         odin01_ip, odin02_ip, odin03_ip, odin04_ip, odin05_ip, odin06_ip, odin07_ip, odin08_ip, odin09_ip, odin10_ip,
         odin11_ip, odin12_ip, odin13_ip, odin14_ip
     ]
+    """All IP addresses of the nodes in the homogenous HPI cluster."""
 
     BENCHMARK = "benchmark"
     CORRELATION_ANOMALIES = "correlation-anomalies"
@@ -64,3 +70,4 @@ class HPI_CLUSTER:
         MULTIVARIATE_ANOMALY_TEST_CASES: Path("/home/projects/akita/data/multivariate-anomaly-test-cases"),
         MULTIVARIATE_TEST_CASES: Path("/home/projects/akita/data/multivariate-test-cases")
     }
+    """This dictionary contains the paths to the dataset collection folders."""
diff --git a/timeeval/core/__init__.py b/timeeval/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/timeeval/experiments.py b/timeeval/core/experiments.py
similarity index 94%
rename from timeeval/experiments.py
rename to timeeval/core/experiments.py
index 73c4c12b..d83a8788 100644
--- a/timeeval/experiments.py
+++ b/timeeval/core/experiments.py
@@ -7,18 +7,18 @@
 import pandas as pd
 from sklearn.preprocessing import MinMaxScaler
 
-from timeeval.algorithm import Algorithm
-from timeeval.constants import EXECUTION_LOG, ANOMALY_SCORES_TS, METRICS_CSV, HYPER_PARAMETERS
-from timeeval.data_types import AlgorithmParameter, TrainingType, InputDimensionality
-from timeeval.datasets import Datasets, Dataset
-from timeeval.heuristics import inject_heuristic_values
-from timeeval.resource_constraints import ResourceConstraints
-from timeeval.times import Times
-from timeeval.utils.datasets import extract_features, load_dataset, load_labels_only
-from timeeval.utils.encode_params import dump_params
-from timeeval.utils.hash_dict import hash_dict
-from timeeval.utils.metrics import Metric, DefaultMetrics
-from timeeval.utils.results_path import generate_experiment_path
+from ..algorithm import Algorithm
+from ..constants import EXECUTION_LOG, ANOMALY_SCORES_TS, METRICS_CSV, HYPER_PARAMETERS
+from ..data_types import AlgorithmParameter, TrainingType, InputDimensionality
+from ..datasets import Datasets, Dataset
+from ..heuristics import inject_heuristic_values
+from ..resource_constraints import ResourceConstraints
+from ..core.times import Times
+from ..utils.datasets import extract_features, load_dataset, load_labels_only
+from ..utils.encode_params import dump_params
+from ..utils.hash_dict import hash_dict
+from ..utils.metrics import Metric, DefaultMetrics
+from ..utils.results_path import generate_experiment_path
 
 
 @dataclass
diff --git a/timeeval/remote.py b/timeeval/core/remote.py
similarity index 97%
rename from timeeval/remote.py
rename to timeeval/core/remote.py
index 448808e4..03f6fe77 100644
--- a/timeeval/remote.py
+++ b/timeeval/core/remote.py
@@ -9,8 +9,8 @@
 from dask import config as dask_config
 from dask.distributed import Client, SSHCluster
 
-from timeeval.remote_configuration import RemoteConfiguration
-from timeeval.resource_constraints import ResourceConstraints
+from ..remote_configuration import RemoteConfiguration
+from ..resource_constraints import ResourceConstraints
 
 
 class Remote:
diff --git a/timeeval/times.py b/timeeval/core/times.py
similarity index 94%
rename from timeeval/times.py
rename to timeeval/core/times.py
index 63b84753..0dc5477a 100644
--- a/timeeval/times.py
+++ b/timeeval/core/times.py
@@ -3,8 +3,8 @@
 from typing import Optional, Dict, Callable, Tuple, Any, List
 import numpy as np
 
-from .algorithm import Algorithm
-from .data_types import AlgorithmParameter, ExecutionType
+from ..algorithm import Algorithm
+from ..data_types import AlgorithmParameter, ExecutionType
 
 
 @dataclass
diff --git a/timeeval/data_types.py b/timeeval/data_types.py
index 8161d6c1..064f90c7 100644
--- a/timeeval/data_types.py
+++ b/timeeval/data_types.py
@@ -6,12 +6,30 @@
 
 
 class TrainingType(Enum):
+    """Training type of algorithm or dataset.
+
+    TimeEval distinguishes between unsupervised, semi-supervised, and supervised algorithms.
+    """
     UNSUPERVISED = "unsupervised"
+    """An unsupervised algorithm does not require any training data.
+
+    An unsupervised dataset consists only of a single test time series.
+    """
     SEMI_SUPERVISED = "semi-supervised"
+    """A semi-supervised algorithm requires normal data for training.
+
+    A semi-supervised dataset consists of a training time series with normal data (no anomalies; all labels are 0) and
+    a test time series.
+    """
     SUPERVISED = "supervised"
+    """A supervised algorithm requires training data with anomalies.
+
+    A supervised dataset consists of a training time series with anomalies and a test time series.
+    """
 
     @staticmethod
     def from_text(name: str) -> 'TrainingType':
+        """Converts the string-representation to an Enum-object."""
         if name.lower() == TrainingType.SUPERVISED.value:
             return TrainingType.SUPERVISED
         elif name.lower() == TrainingType.SEMI_SUPERVISED.value:
@@ -21,11 +39,23 @@ def from_text(name: str) -> 'TrainingType':
 
 
 class InputDimensionality(Enum):
+    """Input dimensionality supported by an algorithm or of a dataset.
+
+    TimeEval distinguishes between univariate and multivariate datasets / time series."""
     UNIVARIATE = "univariate"
+    """Univariate datasets consist of a single feature/dimension/channel.
+
+    An univariate algorithm can process only a dataset with a single feature/dimension/channel.
+    """
     MULTIVARIATE = "multivariate"
+    """Multivariate datasets have 2 or more features/dimensions/channels.
+
+    A multivariate algorithm can process univariate or multivariate datasets.
+    """
 
     @staticmethod
     def from_dimensions(n: int) -> 'InputDimensionality':
+        """Converts the feature/dimension/channel count to an Enum-object."""
         if n < 1:
             raise ValueError(f"Zero dimensional dataset is not supported!")
         elif n == 1:
@@ -35,6 +65,15 @@ def from_dimensions(n: int) -> 'InputDimensionality':
 
 
 class ExecutionType(Enum):
+    """Enum used to indicate the execution type of algorithms.
+
+    TimeEval calls each algorithm up to two times with two different execution types and passes the current execution
+    type as an object of this class to the algorithm adapter implementation.
+
+    Depending on the algorithm's :class:`timeeval.TrainingType`, it requires a training step.
+    TimeEval will call these algorithms first with the execution type set to ``TRAIN``.
+    Then, for all algorithms, the algorithm is called with execution type ``EXECUTE``.
+    """
     TRAIN = "train"
     EXECUTE = "execute"
 
diff --git a/timeeval/datasets/analyzer.py b/timeeval/datasets/analyzer.py
index b8c62cf9..47940476 100755
--- a/timeeval/datasets/analyzer.py
+++ b/timeeval/datasets/analyzer.py
@@ -57,8 +57,8 @@ class DatasetAnalyzer:
 
     See Also
     --------
-    :class:`statsmodels.tsa.stattools.adfuller`
-    :class:`statsmodels.tsa.stattools.kpss`
+    :obj:`statsmodels.tsa.stattools.adfuller`
+    :obj:`statsmodels.tsa.stattools.kpss`
     """
 
     def __init__(self, dataset_id: DatasetId, is_train: bool,
@@ -157,7 +157,7 @@ def load_from_json(filename: Union[str, Path], train: bool = False) -> DatasetMe
         ----------
         filename: path
             Path to the JSON-file containing the dataset metadata. Can be written using
-            :func:`timeeval.datasets.DatasetAnalyzer.save_to_json`.
+            :func:`timeeval.datasets.analyzer.DatasetAnalyzer.save_to_json`.
         train: bool
             Whether the training or testing time series' metadata should be loaded from the file.
 
diff --git a/timeeval/datasets/custom.py b/timeeval/datasets/custom.py
index 485678c1..4bd1deb3 100644
--- a/timeeval/datasets/custom.py
+++ b/timeeval/datasets/custom.py
@@ -45,7 +45,7 @@ class CustomDatasets(CustomDatasetsBase):
 
     Internal API! You should **not need to use or modify** this class.
 
-    This class behaves similar to the :class:`timeeval.datasets.Datasets`-API while using a different internal
+    This class behaves similar to the :class:`timeeval.datasets.datasets.Datasets`-API while using a different internal
     representation for the dataset index.
     """
 
diff --git a/timeeval/datasets/custom_noop.py b/timeeval/datasets/custom_noop.py
index 833e88da..00d2e4f8 100644
--- a/timeeval/datasets/custom_noop.py
+++ b/timeeval/datasets/custom_noop.py
@@ -13,7 +13,7 @@ class NoOpCustomDatasets(CustomDatasetsBase):
     Internal API! You should **not need to use or modify** this class.
 
     This dummy implementation does nothing and improves readability of the
-    :class:`timeeval.datasets.Datasets`-implementation by removing the need for None-checks.
+    :class:`timeeval.datasets.datasets.Datasets`-implementation by removing the need for None-checks.
     """
 
     def __init__(self) -> None:
diff --git a/timeeval/datasets/dataset_manager.py b/timeeval/datasets/dataset_manager.py
index b27a2abf..ff7888d5 100644
--- a/timeeval/datasets/dataset_manager.py
+++ b/timeeval/datasets/dataset_manager.py
@@ -52,22 +52,22 @@ class DatasetManager(ContextManager['DatasetManager'], Datasets):
     ----------
     data_folder : path
         Path to the folder, where the benchmark data is stored. This folder consists of the file *datasets.csv* and
-        the datasets in an hierarchical storage layout.
+        the datasets in a hierarchical storage layout.
     custom_datasets_file : path
         Path to a file listing additional custom datasets.
     create_if_missing : bool
-        Create an index-file in the `data_folder` if none could be found. Set this to ``False`` if an exception should
+        Create an index-file in the ``data_folder`` if none could be found. Set this to ``False`` if an exception should
         be raised if the folder is wrong or does not exist.
 
     Raises
     ------
     FileNotFoundError
-        If `create_if_missing` is set to ``False`` and no *datasets.csv*-file was found in the `data_folder`.
+        If ``create_if_missing`` is set to ``False`` and no *datasets.csv*-file was found in the ``data_folder``.
 
     See Also
     --------
-    :class:`timeeval.datasets.Datasets`
-    :class:`timeeval.datasets.MultiDatasetManager`
+    :class:`timeeval.datasets.datasets.Datasets`
+    :class:`timeeval.datasets.multi_dataset_manager.MultiDatasetManager`
     """
 
     def __init__(self, data_folder: Union[str, Path], custom_datasets_file: Optional[Union[str, Path]] = None,
@@ -169,7 +169,7 @@ def add_datasets(self, datasets: List[DatasetRecord]) -> None:
 
         See Also
         --------
-        :func:`timeeval.datasets.DatasetManager.add_dataset`
+        :func:`timeeval.datasets.dataset_manager.DatasetManager.add_dataset`
         """
         df_new = pd.DataFrame(datasets)
         df_new.set_index(["collection_name", "dataset_name"], inplace=True)
@@ -181,9 +181,10 @@ def add_datasets(self, datasets: List[DatasetRecord]) -> None:
     def save(self) -> None:
         """Saves the in-memory dataset index to disk.
 
-        Persists newly added benchmark datasets from memory to the benchmark dataset collection file `datasets.csv`.
+        Persists newly added benchmark datasets from memory to the benchmark dataset collection file ``datasets.csv``.
         Custom datasets are excluded from persistence and cannot be saved to disk; use
-        :func:`timeeval.datasets.DatasetManager.add_dataset` or :func:`timeeval.datasets.DatasetManager.add_datasets`
+        :func:`~timeeval.datasets.dataset_manager.DatasetManager.add_dataset` or
+        :func:`~timeeval.datasets.dataset_manager.DatasetManager.add_datasets`
         to add datasets to the benchmark dataset collection.
         """
         self._df.to_csv(self._filepath)
diff --git a/timeeval/datasets/datasets.py b/timeeval/datasets/datasets.py
index 8bd53777..152eafa1 100644
--- a/timeeval/datasets/datasets.py
+++ b/timeeval/datasets/datasets.py
@@ -19,9 +19,9 @@
 class Datasets(abc.ABC):
     """Provides read-only access to benchmark datasets and their metadata.
 
-    This is an abstract class (interface). Please use :class:`timeeval.datasets.DatasetManager` or
-    :class:`timeeval.datasets.MultiDatasetManager` instead. The constructor arguments are filled in by the
-    respective implementation.
+    This is an abstract class (interface). Please use :class:`timeeval.datasets.dataset_manager.DatasetManager` or
+    :class:`timeeval.datasets.multi_dataset_manager.MultiDatasetManager` instead. The constructor arguments are filled
+    in by the respective implementation.
 
     Parameters
     ----------
@@ -146,17 +146,17 @@ def select(self,
             restrict dataset type (e.g. *real* or *synthetic*)
         datetime_index : bool
             only select datasets for which a datetime index exists;
-            if ``true``: "timestamp"-column has datetime values;
-            if ``false``: "timestamp"-column has monotonically increasing integer values;
+            if ``True``: "timestamp"-column has datetime values;
+            if ``False``: "timestamp"-column has monotonically increasing integer values;
             this condition is ignored by custom datasets.
         training_type : timeeval.TrainingType
             select datasets for specific training needs:
-            * *supervised* (`TrainingType.SUPERVISED`),
-            * *semi-supervised* (`TrainingType.SEMI_SUPERVISED`), or
-            * *unsupervised* (`TrainingType.UNSUPERVISED`)
+            * :const:`~timeeval.TrainingType.SUPERVISED`,
+            * :const:`~timeeval.TrainingType.SEMI_SUPERVISED`, or
+            * :const:`~timeeval.TrainingType.UNSUPERVISED`
         train_is_normal : bool
-            if ``true``: only return datasets for which the training dataset does not contain anomalies;
-            if ``false``: only return datasets for which the training dataset contains anomalies
+            if ``True``: only return datasets for which the training dataset does not contain anomalies;
+            if ``False``: only return datasets for which the training dataset contains anomalies
         input_dimensionality : timeeval.InputDimensionality
             restrict dataset to input type, either univariate or multivariate
         min_anomalies : int
@@ -168,7 +168,7 @@ def select(self,
 
         Returns
         -------
-        dataset_names : list of str-str-tuples
+        dataset_names : List[Tuple[str,str]]
             A list of dataset identifiers (tuple of collection name and dataset name).
         """
         custom_datasets = self._custom_datasets.select(collection, dataset, dataset_type, datetime_index,
diff --git a/timeeval/remote_configuration.py b/timeeval/remote_configuration.py
index 33b7dafb..140b08ba 100644
--- a/timeeval/remote_configuration.py
+++ b/timeeval/remote_configuration.py
@@ -13,24 +13,84 @@
 
 @dataclass
 class RemoteConfiguration:
+    """This class holds the configuration for distributed TimeEval.
+
+    TimeEval uses a :obj:`dask.distributed.SSHCluster` to distribute the evaluation tasks to multiple compute nodes.
+    Please read the Dask documentation carefully and then use the constructor arguments to setup a TimeEval cluster.
+
+    Parameters
+    ----------
+    scheduler_host : str
+        IP address or hostname for the :obj:`distributed.Scheduler`.
+        This node will be responsible to coordinate the cluster.
+        The scheduler does not perform any evaluations.
+    scheduler_port : int
+        Port for the scheduler.
+    worker_hosts  : List[str]
+        List of IP address or hostnames for the :obj:`distributed.Worker`.
+        These nodes will execute the evaluation tasks.
+    remote_python : str
+        Path to the Python-executable.
+        If you set up all your nodes in the same way, the default is fine.
+    kwargs_overwrites : dict
+        Use this option to overwrite any configuration options of the :obj:`~dask.distributed.SSHCluster`.
+
+        .. warning::
+            Only use if you know what you are doing!
+
+    dask_logging_file_level : str
+        Logging level for the file-based Dask logger.
+    dask_logging_console_level : str
+        Logging level for the console-based Dask logger.
+    dask_logging_filename : str
+        Name of the Dask logging file without any parent paths.
+        Each node will write its own logging file and TimeEval will automatically postfix the filenames with the hostname and
+        place the Dask logging files into the ``results_path``.
+
+    Examples
+    --------
+    Two-node cluster where the first node hosts the scheduler but also takes part in the evaluation:
+
+    >>> from timeeval import TimeEval, RemoteConfiguration
+    >>> config = RemoteConfiguration(scheduler_host="192.168.1.1", worker_hosts=["192.168.1.1", "192.168.1.2"])
+    >>> TimeEval(dm=..., datasets=[], algorithms=[], distributed=True, remote_config=config)
+    """
     scheduler_host: str = DEFAULT_SCHEDULER_HOST
     scheduler_port: int = DEFAULT_DASK_PORT
     worker_hosts: List[str] = field(default_factory=lambda: [])
-    remote_python: str = sys.executable
+    remote_python: str = field(default_factory=lambda: sys.executable)
     kwargs_overwrites: dict = field(default_factory=lambda: {})
     dask_logging_file_level: str = "INFO"
     dask_logging_console_level: str = "INFO"
     dask_logging_filename: str = DEFAULT_DASK_LOG_FILENAME
 
     def update_logging_path(self, results_path: Path) -> None:
+        """Updates the path to the log filename.
+
+        .. warning::
+            Internal API!
+
+        .. attention::
+            Always call this function before calling
+            :func:`~timeeval.RemoteConfiguration.get_remote_logging_config`!
+
+        :meta private:
+        """
         name = self.dask_logging_filename
         path = results_path / name
         self.dask_logging_filename = str(path.resolve())
 
     def to_ssh_cluster_kwargs(self, limits: ResourceConstraints) -> Dict[str, Any]:
-        """
-        Creates the kwargs for the Dask SSHCluster-constructor:
-        https://docs.dask.org/en/latest/setup/ssh.html#distributed.deploy.ssh.SSHCluster
+        """Creates the kwargs for the Dask SSHCluster-constructor.
+
+        .. warning::
+            Internal API!
+
+        See Also
+        --------
+        dask.distributed.SSHCluster : Object that receives these options.
+
+        :meta private:
         """
         mem_limit = limits.task_memory_limit or "auto"
         config = {
@@ -53,6 +113,13 @@ def to_ssh_cluster_kwargs(self, limits: ResourceConstraints) -> Dict[str, Any]:
         return config
 
     def get_remote_logging_config(self) -> Dict[str, Any]:
+        """Creates the logging configuration for Dask based on the remote configuration.
+
+        .. warning::
+            Internal API!
+
+        :meta private:
+        """
         return {
             "version": 1,
             "disable_existing_loggers": False,
diff --git a/timeeval/resource_constraints.py b/timeeval/resource_constraints.py
index c4f66c75..c2b18bc8 100644
--- a/timeeval/resource_constraints.py
+++ b/timeeval/resource_constraints.py
@@ -5,18 +5,75 @@
 from durations import Duration
 
 MB = 1024 ** 2
+""":math:`1 MB = 2^{20} \\text{Bytes}`
+
+Can be used to set the memory limit.
+
+Examples
+--------
+
+>>> from timeeval.resource_constraints import ResourceConstraints, MB
+>>> ResourceConstraints(task_memory_limit=500 * MB)
+"""
 GB = 1024 ** 3
+""":math:`1 GB = 2^{30} \\text{Bytes}`
+
+Can be used to set the memory limit.
+
+Examples
+--------
+
+>>> from timeeval.resource_constraints import ResourceConstraints, GB
+>>> ResourceConstraints(task_memory_limit=1 * GB)
+"""
 DEFAULT_TASKS_PER_HOST = 1
 DEFAULT_TIMEOUT = Duration("8 hours")
 
 
 @dataclass
 class ResourceConstraints:
-    """
-    **Resource constraints are not supported by all algorithm Adapters!**
+    """Use this class to configure resource constraints and how TimeEval deals with preliminary results.
+
+    .. warning::
+        Resource constraints are just supported by the :class:`~timeeval.adapters.docker.DockerAdapter`!
 
     For docker: Swap is always disabled. Resource constraints are enforced using explicit resource limits on the Docker
     container.
+
+    Parameters
+    ----------
+    tasks_per_host : int
+        Specify, how many evaluation tasks are executed on each host. This setting influences the default memory and
+        CPU limits if :attr:`~timeeval.ResourceConstraints.task_memory_limit` and
+        :attr:`~timeeval.ResourceConstraints.task_cpu_limit` are ``None``: the available
+        resources of the node are shared equally between the tasks.
+
+        Because each tasks, in effect, trains or executes a time series anomaly detection algorithm, the tasks are
+        resource-intensive, which means that over-provisioning is not useful and could decrease overall performance. If
+        runtime measurements are taken, **make sure that no resources are shared between the tasks**!
+    task_memory_limit : Optional[int]
+        Specify the maximum allowed memory in Bytes. You can use :const:`~timeeval.resource_constraints.MB` and
+        :const:`~timeeval.resource_constraints.GB` for better readability. This setting limits the available main
+        memory per task to a fixed value.
+    task_cpu_limit : Optional[float]
+        Specify the maximum allowed CPU usage in fractions of CPUs (e.g. 0.25 means: only use 1/4 of a single
+        CPU core). Usually, it is advisable to use whole CPU cores (e.g. 1.0 for 1 CPU core, 2.0 for 2 CPU cores, etc.).
+    train_timeout : Duration
+        Default timeout for training an algorithm. This value can be overridden for each algorithm in its
+        :class:`~timeeval.adapters.docker.DockerAdapter`.
+    execute_timeout : Duration
+        Default timeout for executing an algorithm. This value can be overridden for each algorithm in its
+        :class:`~timeeval.adapters.docker.DockerAdapter`.
+    use_preliminary_model_on_train_timeout : bool
+        If this option is enabled (default), then algorithms can save preliminary models (model checkpoints) to disk and
+        TimeEval will use the last preliminary model if the training step runs into the training timeout. This is
+        especially useful for machine learning algorithms that use an iterative training process (e.g. using SGD). As
+        long as the algorithm implementation stores the best-so-far model after each training epoch, the training must
+        not be limited by the number of epochs but just by the training time.
+    use_preliminary_scores_on_execute_timeout : bool
+        If this option is enabled (default) and an algorithm exceeds the execution timeout, TimeEval will look for any
+        preliminary result. This allows the evaluation of progressive algorithms that output a rough result, refine it
+        over time, and would otherwise run into the execution timeout.
     """
 
     tasks_per_host: int = DEFAULT_TASKS_PER_HOST
@@ -30,9 +87,7 @@ class ResourceConstraints:
     def get_compute_resource_limits(self,
                                     memory_overwrite: Optional[int] = None,
                                     cpu_overwrite: Optional[float] = None) -> Tuple[int, float]:
-        """
-        Calculates the resource constraints for a single task. **Must be called on the node that will execute the
-        task!**
+        """Calculates the resource constraints for a single task.
 
         There are three sources for resource limits (in decreasing priority):
 
@@ -48,9 +103,20 @@ def get_compute_resource_limits(self,
         that CPU limit is set based on node CPU count divided by the number of tasks and memory limit is set based on
         total memory of node minus 1 GB (for OS) divided by the number of tasks.
 
-        :param memory_overwrite: if this is set, it will overwrite the memory limit
-        :param cpu_overwrite: if this is set, it will overwrite the CPU limit
-        :return: Tuple of memory and CPU limit.
+        .. attention::
+            Must be called on the node that will execute the task!
+
+        Parameters
+        ----------
+        memory_overwrite: int
+            If this is set, it will overwrite the memory limit.
+        cpu_overwrite : float
+            If this is set, it will overwrite the CPU limit.
+
+        Returns
+        -------
+        memory_limit, cpu_limit : Tuple[int,float]
+            Tuple of memory and CPU limit.
             Memory limit is expressed in Bytes and CPU limit is expressed in fractions of CPUs (e.g. 0.25 means: only
             use 1/4 of a single CPU core).
         """
@@ -75,14 +141,30 @@ def get_compute_resource_limits(self,
     def get_train_timeout(self, timeout_overwrite: Optional[Duration] = None) -> Duration:
         """Returns the maximum runtime of a training task in seconds.
 
-        :param timeout_overwrite: if this is set, it will overwrite the global timeout
+        Parameters
+        ----------
+        timeout_overwrite : Duration
+            If this is set, it will overwrite the global timeout.
+
+        Returns
+        -------
+        train_timeout : Duration
+            The training timeout with the highest precedence (method overwrite then global configuration).
         """
         return self._get_timeout_with_overwrite(self.train_timeout, timeout_overwrite)
 
     def get_execute_timeout(self, timeout_overwrite: Optional[Duration] = None) -> Duration:
         """Returns the maximum runtime of an execution task in seconds.
 
-        :param timeout_overwrite: if this is set, it will overwrite the global timeout
+        Parameters
+        ----------
+        timeout_overwrite : Duration
+            If this is set, it will overwrite the global timeout.
+
+        Returns
+        -------
+        execute_timeout : Duration
+            The execution timeout with the highest precedence (method overwrite then global configuration).
         """
         return self._get_timeout_with_overwrite(self.execute_timeout, timeout_overwrite)
 
@@ -94,4 +176,5 @@ def _get_timeout_with_overwrite(timeout: Duration, overwrite: Optional[Duration]
 
     @staticmethod
     def default_constraints() -> 'ResourceConstraints':
+        """Creates a configuration object with the default resource constraints."""
         return ResourceConstraints()
diff --git a/timeeval/timeeval.py b/timeeval/timeeval.py
index 78901271..a0ab58ac 100644
--- a/timeeval/timeeval.py
+++ b/timeeval/timeeval.py
@@ -15,27 +15,156 @@
 from distributed.client import Future
 from joblib import Parallel, delayed
 
-from .adapters.docker import DockerTimeoutError
+from .adapters.docker import DockerTimeoutError, DockerAdapter
 from .algorithm import Algorithm
 from .constants import RESULTS_CSV
+from .core.experiments import Experiments, Experiment
+from .core.remote import Remote, RemoteConfiguration
+from .core.times import Times
 from .data_types import TrainingType, InputDimensionality
 from .datasets import Datasets
-from .experiments import Experiments, Experiment
-from .remote import Remote, RemoteConfiguration
 from .resource_constraints import ResourceConstraints
-from .times import Times
 from .utils.encode_params import dumps_params
 from .utils.metrics import Metric, DefaultMetrics
 from .utils.tqdm_joblib import tqdm_joblib
 
 
 class Status(Enum):
+    """Status of an experiment.
+
+    The status of each evaluation experiment can have one of three states: ok, error, or timeout.
+    """
     OK = 0
     ERROR = 1
     TIMEOUT = 2
 
 
 class TimeEval:
+    """Main class of TimeEval.
+
+    This class is the main utility to configure and execute evaluation experiments.
+    First select your algorithms and datasets and, then, pass them to TimeEval and use its constructor arguments to
+    configure your evaluation run.
+    Per default, TimeEval evaluates all algorithms on all datasets (cross product).
+    You can use the parameters ``skip_invalid_combinations``, ``force_training_type_match``,
+    ``force_dimensionality_match``, and ``experiment_combinations_file`` to control which algorithm runs on which dataset.
+    See the description of the other arguments for further configuration details.
+
+    After you have created your TimeEval object, holding the experiment run configuration, you can execute the
+    experiments by calling :func:`~timeeval.TimeEval.run`.
+    Afterwards, the evaluation summary results are accessible in the ``results_path`` and from
+    :func:`~timeeval.TimeEval.get_results`.
+
+    Examples
+    --------
+    Simple example experiment evaluating a single algorithm on the test datasets using the default metrics (just
+    :attr:`~timeeval.utils.metrics.DefaultMetrics.ROC_AUC`):
+
+    >>> from timeeval import TimeEval, DefaultMetrics, Algorithm, TrainingType, InputDimensionality, DatasetManager
+    >>> from timeeval.adapters import DockerAdapter
+    >>> from timeeval.params import FixedParameters
+    >>>
+    >>> dm = DatasetManager(Path("tests/example_data"))
+    >>> datasets = dm.select()
+    >>>
+    >>> algorithms = [
+    >>>     Algorithm(
+    >>>         name="COF",
+    >>>         main=DockerAdapter(image_name="registry.gitlab.hpi.de/akita/i/cof"),
+    >>>         param_config=FixedParameters({"n_neighbors": 20, "random_state": 42}),
+    >>>         data_as_file=True,
+    >>>         training_type=TrainingType.UNSUPERVISED,
+    >>>         input_dimensionality=InputDimensionality.MULTIVARIATE
+    >>>     ),
+    >>> ]
+    >>>
+    >>> timeeval = TimeEval(dm, datasets, algorithms, metrics=DefaultMetrics.default_list())
+    >>> timeeval.run()
+    >>> results = timeeval.get_results(aggregated=False)
+    >>> print(results)
+
+    Parameters
+    ----------
+    dataset_mgr : ~timeeval.datasets.datasets.Datasets
+        The dataset manager provides the metadata about the datasets. You can either use a
+        :class:`~timeeval.datasets.dataset_manager.DatasetManager` or a
+        :class:`~timeeval.datasets.multi_dataset_manager.MultiDatasetManager`.
+    datasets : List[Tuple[str, str]]
+        List of dataset IDs consisting of collection name and dataset name to uniquely identify each dataset. The
+        datasets must be known by the ``dataset_mgr``. You can call :func:`~timeeval.datasets.datasets.Datasets.select`
+        on the ``dataset_mgr`` to get a list of dataset IDs.
+    algorithms : List[Algorithm]
+        List of algorithms to evaluate on the datasets.
+        The algorithm specification also contains the hyperparameter configurations that TimeEval will test.
+    results_path : Path
+        Use this parameter to change the path where all evaluation results are stored.
+        If TimeEval is used in distributed mode, this path will be created on **all** nodes!
+    repetitions : int
+        Execute each unique combination of dataset, algorithm, and hyperparameter-setting multiple times.
+        This allows you to use TimeEval to measure runtimes more precisely by aggregating the runtime measurements over
+        multiple repetitions.
+    distributed : bool
+        Run TimeEval in distributed mode.
+        In this case, you **should** also supply a ``remote_config``.
+    remote_config : Optional[RemoteConfiguration]
+        Configuration of the Dask cluster used for distributed execution of TimeEval.
+        See :class:`~timeeval.RemoteConfiguration` for details.
+    resource_constraints : Optional[ResourceConstraints]
+        You can supply a :class:`~timeeval.ResourceConstraints`-object to limit the amount of (CPU, memory, or runtime)
+        resources available to each experiment.
+        These options apply to each experiment to ensure a fair comparison.
+
+        .. warning::
+            Resource constraints are currently only implemented by the :class:`~timeeval.adapters.docker.DockerAdapter`.
+            If you rely on resource constraints, please make sure that all algorithms use the ``DockerAdapter``-implementation.
+
+    disable_progress_bar : bool
+        Enable / disable showing the `tqdm <https://tqdm.github.io>`_ progress bars.
+    metrics : Optional[List[Metric]]
+        Supply a list of :class:`~timeeval.utils.metrics.Metric` to evaluate the algorithms with.
+        TimeEval computes all supplied metrics over all experiments.
+        If you don't specify any metric (``None``), the default metric list
+        :func:`~timeeval.utils.metrics.DefaultMetrics.default_list` is used instead.
+    skip_invalid_combinations : bool
+        Not all algorithms can be executed on all datasets.
+        If this flag is set to ``True``, TimeEval will skip all invalid combinations of algorithms and datasets based on
+        their input dimensionality and training type.
+        It is automatically enabled if either ``force_training_type_match`` or ``force_dimensionality_match`` is set to
+        ``True``.
+        Per default (``force_training_type_match == force_dimensionality_match == False``), the following combinations
+        are not executed:
+
+        - supervised algorithms on semi-supervised or unsupervised datasets (datasets cannot be used to train the algorithm)
+        - semi-supervised algorithm on supervised or unsupervised datasets (datasets cannot be used to train the algorithm)
+        - univariate algorithms on multivariate datasets (algorithm cannot process the dataset)
+
+    force_training_type_match : bool
+        Narrow down the algorithm-dataset combinations further by executing an algorithm only on datasets with **the same**
+        training type, e.g. unsupervised algorithms only on unsupervised datasets.
+        This flag implies ``skip_invalid_combinations==True``.
+    force_dimensionality_match : bool
+        Narrow down the algorithm-dataset combinations furthter by executing an algorithm only on datasets with **the same**
+        input dimensionality, e.g. multivariate algorithms only on multivariate datasets.
+        This flag implies ``skip_invalid_combinations==True``.
+    n_jobs : int
+        Set the number of jobs / processes used to fetch the results from the remote machine.
+        This setting is used only in distributed mode.
+        ``-1`` instructs TimeEval to use all locally available cores.
+    experiment_combinations_file : Optional[Path]
+        Supply a path to an experiment combinations CSV-File.
+        Using this file, you can specify explicitly which combinations of algorithms, datasts, and hyperparameters
+        should be executed.
+        The file should contain CSV data with a single header line and four columns with the following names:
+
+        1. `algorithm` - name of the algorithm
+        2. `collection` - name of the dataset collection
+        3. `dataset` - name of the dataset
+        4. `hyper_params_id` - ID of the hyperparameter configuration
+
+        Only experiments that are present in the TimeEval configuration **and** this file are scheduled and executed.
+        This allows you to circumvent the cross-product that TimeEval will perform in its default configuration.
+    """
+
     RESULT_KEYS = ["algorithm",
                    "collection",
                    "dataset",
@@ -53,8 +182,25 @@ class TimeEval:
                    "repetition",
                    "hyper_params",
                    "hyper_params_id"]
+    """This list contains all the _fixed_ result data frame's column headers.
+    TimeEval dynamically adds the metrics and execution times depending on its configuration.
+    
+    For metrics, their :func:`~timeeval.utils.metrics.Metric.name` will be used as column header, and TimeEval will add
+    the following runtime measurements depending on whether they are applicable to the algorithms in the run or not:
+    
+    - train_preprocess_time: if :func:`~timeeval.Algorithm.preprocess` is defined
+    - train_main_time: if the algorithm is semi-supervised or supervised
+    - execute_preprocess_time: if :func:`~timeeval.Algorithm.preprocess` is defined
+    - execute_main_time: always
+    - execute_postprocess_time: if :func:`~timeeval.Algorithm.postprocess` is defined
+    """
 
     DEFAULT_RESULT_PATH = Path("./results")
+    """Default path for the results.
+    
+    If you don't specify the ``results_path``, TimeEval will store the evaluation results in the folder ``results``
+    within the current working directory.
+    """
 
     def __init__(self,
                  dataset_mgr: Datasets,
@@ -86,15 +232,21 @@ def __init__(self,
                 dataset_details.append(dataset_mgr.get(d))
             except KeyError:
                 not_found_datasets.append(repr(d))
-        assert len(
-            not_found_datasets) == 0, "Some datasets could not be found in DatasetManager!\n  " \
-                                      f"{', '.join(not_found_datasets)}"
+        assert len(not_found_datasets) == 0, "Some datasets could not be found in DatasetManager!\n  " \
+                                             f"{', '.join(not_found_datasets)}"
+
+        limits = resource_constraints or ResourceConstraints.default_constraints()
+        if limits != ResourceConstraints.default_constraints():
+            incompatible_algos = [a.name for a in algorithms if not isinstance(a.main, DockerAdapter)]
+            assert len(incompatible_algos) == 0, "The following algorithms won't satisfy the specified resource " \
+                                                 f"constraints: {', '.join(incompatible_algos)}. Either drop the " \
+                                                 "resource constraints or use the DockerAdapter for all algorithms!"
 
         self.log = logging.getLogger(self.__class__.__name__)
         start_date: str = dt.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
         self.results_path = results_path.resolve() / start_date
         self.disable_progress_bar = disable_progress_bar
-        self.metrics = metrics or DefaultMetrics.default_list()
+        self.metrics: List[Metric] = metrics or DefaultMetrics.default_list()
         self.metric_names = [m.name for m in self.metrics]
         self.results = pd.DataFrame(columns=TimeEval.RESULT_KEYS + self.metric_names)
         self.distributed = distributed
@@ -103,18 +255,17 @@ def __init__(self,
         self.log.info(f"Results are recorded in the directory {self.results_path}")
         self.results_path.mkdir(parents=True, exist_ok=True)
 
-        resource_constraints = resource_constraints or ResourceConstraints()
-        if not distributed and resource_constraints.tasks_per_host > 1:
+        if not distributed and limits.tasks_per_host > 1:
             self.log.warning(
-                f"`tasks_per_host` was set to {resource_constraints.tasks_per_host}. However, non-distributed "
+                f"`tasks_per_host` was set to {limits.tasks_per_host}. However, non-distributed "
                 "execution of TimeEval does currently not support parallelism! Reducing `tasks_per_host` to 1. "
                 "The automatic resource limitation will reflect this by increasing the limits for the single task. "
                 "Explicitly set constraints to limit the resources for local executions of TimeEval."
             )
-            resource_constraints.tasks_per_host = 1
+            limits.tasks_per_host = 1
 
         self.exps = Experiments(dataset_mgr, dataset_details, algorithms, self.results_path,
-                                resource_constraints=resource_constraints,
+                                resource_constraints=limits,
                                 repetitions=repetitions,
                                 skip_invalid_combinations=skip_invalid_combinations,
                                 force_training_type_match=force_training_type_match,
@@ -122,14 +273,14 @@ def __init__(self,
                                 metrics=self.metrics,
                                 experiment_combinations_file=experiment_combinations_file)
 
-        self.remote_config = remote_config or RemoteConfiguration()
+        self.remote_config: RemoteConfiguration = remote_config or RemoteConfiguration()
         self.remote_config.update_logging_path(self.results_path)
         self.log.debug(f"Updated dask logging filepath to {self.remote_config.dask_logging_filename}")
 
         if self.distributed:
             self.log.info("TimeEval is running in distributed environment, setting up remoting ...")
             self.remote = Remote(disable_progress_bar=self.disable_progress_bar, remote_config=self.remote_config,
-                                 resource_constraints=resource_constraints)
+                                 resource_constraints=limits)
             self.results["future_result"] = np.nan
 
             self.log.info("... registering signal handlers ...")
@@ -237,6 +388,31 @@ def get_future_result(f: Future) -> Tuple[Any, ...]:
         self.results = self.results.drop(['future_result'], axis=1)
 
     def get_results(self, aggregated: bool = True, short: bool = True) -> pd.DataFrame:
+        """Return the (aggregated) evaluation results of a previous evaluation run.
+
+        The results are returned in a Pandas :obj:`~pandas.DataFrame` and contain the mean runtime and metrics of the
+        algorithms for each dataset.
+        You can tweak the output using the parameters.
+
+        .. note::
+            Must be called after :func:`~timeeval.TimeEval.run`, otherwise the returned DataFrame is empty.
+
+        Parameters
+        ----------
+        aggregated : bool
+            If ``True``, returns the aggregated results (controled by parameter ``short``), otherwise all collected
+            information is returned.
+        short : bool
+            This parameter is used only in aggregation mode and controls the aggregation level and functions.
+            If ``True``, the aggregation is over algorithms and datasets, and the mean of the metrics, training time, and
+            execution time is returned.
+            If ``False``, the aggregation is over algorithms, datasets, and parameter combinations, and the mean and
+            standard deviation of all runtime measurements and metrics are computed.
+
+        Returns
+        -------
+        :obj:`~pandas.DataFrame` containing the evaluation results.
+        """
         if not aggregated:
             return self.results
 
@@ -266,11 +442,38 @@ def get_results(self, aggregated: bool = True, short: bool = True) -> pd.DataFra
         return results
 
     def save_results(self, results_path: Optional[Path] = None) -> None:
+        """Store the evaluation results to a CSV-file in the provided `results_path`.
+        This method is automatically executed by TimeEval at the end of an evaluation run when calling
+        :func:`~timeeval.TimeEval.run`.
+
+        Parameters
+        ----------
+        results_path: Optional[Path]
+            Path, where the results should be stored at.
+            If it is not supplied, the results path of the current TimeEval run (:attr:`timeeval.TimeEval.results_path`)
+            is used.
+        """
         path = results_path or (self.results_path / RESULTS_CSV)
         self.results.to_csv(path.resolve(), index=False)
 
     @staticmethod
     def rsync_results_from(results_path: Path, hosts: List[str], disable_progress_bar: bool = False, n_jobs: int = -1) -> None:
+        """Fetches evaluation results of an independent TimeEval run from remote machines merging the temporary data
+        and results together on the local host.
+
+        Parameters
+        ----------
+        results_path : Path
+            Path to the evaluation results.
+            Must be the same for all hosts.
+        hosts : List[str]
+            List of hostnames or IP addresses that took part in the evaluation run.
+        disable_progress_bar : bool
+            If a progress bar should be displayed or not.
+        n_jobs : int
+            Number of parallel processes used to fetch the results.
+            The parallelism is limited by the number of external hosts and the maximum number of available CPU cores.
+        """
         results_path = results_path.resolve()
         hostname = socket.gethostname()
         excluded_aliases = [
@@ -288,6 +491,15 @@ def rsync_results_from(results_path: Path, hosts: List[str], disable_progress_ba
             Parallel(n_jobs)(jobs)
 
     def rsync_results(self) -> None:
+        """Fetches the evaluation results of the current evaluation run from all remote machines merging the temporary
+        data and results together on the local host.
+        This method is automatically executed by TimeEval at the end of an evaluation run started by calling
+        :func:`~timeeval.TimeEval.run`.
+
+        See Also
+        --------
+        timeeval.TimeEval.rsync_results_from
+        """
         TimeEval.rsync_results_from(
             self.results_path,
             self.remote_config.worker_hosts,
@@ -341,6 +553,24 @@ def _distributed_finalize(self) -> None:
         self.rsync_results()
 
     def run(self) -> None:
+        """Starts the configured evaluation run.
+
+        Each TimeEval run consists of a number of experiments that are executed independently of each other.
+        There are three phases: PREPARE, EVALUATION, FINALIZE.
+
+        1. _PREPARE_ phase: In the first phase, the execution environment is prepared, the result folder is created, and
+           algorithm adapter-dependent preparation steps, such as pulling Docker images for the
+           :class:`~timeeval.adapters.docker.DockerAdapter`, are executed.
+        2. _EVALUATION_ phase: In the evaluation phase, the experiments are executed and the results are recorded and
+           stored to disk.
+        3. _FINALIZE_ phase: In the last phase, the execution environment is cleaned up, and algorithm adapter-dependent
+           finalization steps, such as removing the temporary Docker containers for the
+           :class:`~timeeval.adapters.docker.DockerAdapter`, are executed.
+
+        This method executes all three phases after each other and returns after they are finished.
+        You can access the evaluation results either using :func:`~timeeval.TimeEval.get_results` programmatically or
+        in the results folder from the file system.
+        """
         print("Running PREPARE phase")
         self.log.info("Running PREPARE phase")
         t0 = time()


Test patch:
diff --git a/tests/test_algorithm_timer.py b/tests/test_algorithm_timer.py
index c82c1062..b854e7f5 100644
--- a/tests/test_algorithm_timer.py
+++ b/tests/test_algorithm_timer.py
@@ -6,7 +6,7 @@
 from timeeval import Algorithm, AlgorithmParameter, TrainingType
 from timeeval.adapters import FunctionAdapter
 from timeeval.data_types import ExecutionType
-from timeeval.times import Times
+from timeeval.core.times import Times
 
 
 def pre(x: AlgorithmParameter, args) -> AlgorithmParameter:
diff --git a/tests/test_dataset_and_algorithm_match.py b/tests/test_dataset_and_algorithm_match.py
index 3411ad22..22c9e9ac 100644
--- a/tests/test_dataset_and_algorithm_match.py
+++ b/tests/test_dataset_and_algorithm_match.py
@@ -13,13 +13,12 @@
     TrainingType,
     InputDimensionality,
     Status,
-    Metric,
     DefaultMetrics,
     ResourceConstraints,
     DatasetManager
 )
 from timeeval.datasets import Dataset, DatasetRecord
-from timeeval.experiments import Experiment, Experiments
+from timeeval.core.experiments import Experiment, Experiments
 from timeeval.utils.hash_dict import hash_dict
 
 
diff --git a/tests/test_distributed_timeeval.py b/tests/test_distributed_timeeval.py
index 19a427ba..65d17b65 100644
--- a/tests/test_distributed_timeeval.py
+++ b/tests/test_distributed_timeeval.py
@@ -73,11 +73,11 @@ def test_distributed_results_and_shutdown_cluster(self):
                     f"{[p.cmdline() for p in processes]}"
             )
 
-    @patch("timeeval.remote.Popen")
+    @patch("timeeval.core.remote.Popen")
     @patch("timeeval.timeeval.subprocess.call")
     @patch("timeeval.adapters.docker.docker.from_env")
-    @patch("timeeval.remote.Client")
-    @patch("timeeval.remote.SSHCluster")
+    @patch("timeeval.core.remote.Client")
+    @patch("timeeval.core.remote.SSHCluster")
     def test_distributed_phases(self, mock_cluster, mock_client, mock_docker, mock_call, mock_popen):
         mock_client.return_value = MockDaskClient()
         mock_cluster.return_value = MockDaskSSHCluster(workers=2)
@@ -124,11 +124,11 @@ def test_phases(self, mock_docker):
                 (timeeval.results_path / "docker" / hash_dict({}) / "custom" / "dataset.1" / "1").exists()
             )
 
-    @patch("timeeval.remote.Popen")
+    @patch("timeeval.core.remote.Popen")
     @patch("timeeval.timeeval.subprocess.call")
     @patch("timeeval.adapters.docker.docker.from_env")
-    @patch("timeeval.remote.Client")
-    @patch("timeeval.remote.SSHCluster")
+    @patch("timeeval.core.remote.Client")
+    @patch("timeeval.core.remote.SSHCluster")
     def test_aliases_excluded(self, mock_cluster, mock_client, mock_docker, mock_call, mock_popen):
         mock_client.return_value = MockDaskClient()
         mock_cluster.return_value = MockDaskSSHCluster(workers=2)
@@ -151,11 +151,11 @@ def test_aliases_excluded(self, mock_cluster, mock_client, mock_docker, mock_cal
             self.assertEqual(len(rsync.params), 1)
             self.assertTrue(rsync.params[0], ["rsync", "-a", f"test-host:{tmp_path}/", tmp_path])
 
-    @patch("timeeval.remote.Popen")
+    @patch("timeeval.core.remote.Popen")
     @patch("timeeval.timeeval.subprocess.call")
     @patch("timeeval.adapters.docker.docker.from_env")
-    @patch("timeeval.remote.Client")
-    @patch("timeeval.remote.SSHCluster")
+    @patch("timeeval.core.remote.Client")
+    @patch("timeeval.core.remote.SSHCluster")
     def test_catches_future_exception(self, mock_cluster, mock_client, mock_docker, mock_call, mock_popen):
         mock_client.return_value = MockDaskExceptionClient()
         mock_cluster.return_value = MockDaskSSHCluster(workers=2)
@@ -175,11 +175,11 @@ def test_catches_future_exception(self, mock_cluster, mock_client, mock_docker,
             timeeval.run()
             self.assertListEqual(timeeval.results[["status", "error_message"]].values[0].tolist(), [Status.ERROR, "ExceptionForTest('test-exception')"])
 
-    @patch("timeeval.remote.Popen")
+    @patch("timeeval.core.remote.Popen")
     @patch("timeeval.timeeval.subprocess.call")
     @patch("timeeval.adapters.docker.docker.from_env")
-    @patch("timeeval.remote.Client")
-    @patch("timeeval.remote.SSHCluster")
+    @patch("timeeval.core.remote.Client")
+    @patch("timeeval.core.remote.SSHCluster")
     def test_catches_future_timeout_exception(self, mock_cluster, mock_client, mock_docker, mock_call, mock_popen):
         mock_client.return_value = MockDaskDockerTimeoutExceptionClient()
         mock_cluster.return_value = MockDaskSSHCluster(workers=2)
diff --git a/tests/test_experiments.py b/tests/test_experiments.py
index d6357601..c920e766 100755
--- a/tests/test_experiments.py
+++ b/tests/test_experiments.py
@@ -3,8 +3,8 @@
 from pathlib import Path
 
 from tests.fixtures.algorithms import SupervisedDeviatingFromMean
-from timeeval import Algorithm, TrainingType, InputDimensionality, Metric, DefaultMetrics, DatasetManager
-from timeeval.experiments import Experiments
+from timeeval import Algorithm, TrainingType, InputDimensionality, DefaultMetrics, DatasetManager
+from timeeval.core.experiments import Experiments
 from timeeval.params import FullParameterGrid
 
 
diff --git a/tests/test_remote.py b/tests/test_remote.py
index e850ef57..1c296938 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -8,7 +8,7 @@
 import pytest
 from dask.distributed import SSHCluster
 
-from timeeval.remote import Remote, RemoteConfiguration
+from timeeval.core.remote import Remote, RemoteConfiguration
 
 
 class TestRemote(unittest.TestCase):
diff --git a/tests/test_resource_constraints.py b/tests/test_resource_constraints.py
index 6842b4a6..cdf043bf 100644
--- a/tests/test_resource_constraints.py
+++ b/tests/test_resource_constraints.py
@@ -5,6 +5,7 @@
 from tests.fixtures.algorithms import DeviatingFromMean
 
 from timeeval import TimeEval, DatasetManager, ResourceConstraints, Algorithm
+from timeeval.adapters import DockerAdapter
 from timeeval.resource_constraints import GB, DEFAULT_TIMEOUT
 
 
@@ -59,7 +60,7 @@ def test_overwrites(self):
 
     def test_tasks_per_node_overwrite_when_non_distributed(self):
         limits = ResourceConstraints(tasks_per_host=4)
-        algorithm = Algorithm(name="dummy", main=DeviatingFromMean)
+        algorithm = Algorithm(name="dummy", main=DockerAdapter(image_name="dummy", skip_pull=True))
 
         timeeval = TimeEval(DatasetManager("./tests/example_data"), [("test", "dataset-int")], [algorithm],
                             distributed=False,
diff --git a/tests/test_timeeval_exceptions.py b/tests/test_timeeval_exceptions.py
index bfe1b50d..653c8db9 100644
--- a/tests/test_timeeval_exceptions.py
+++ b/tests/test_timeeval_exceptions.py
@@ -7,7 +7,7 @@
 import pandas as pd
 
 from tests.fixtures.algorithms import ErroneousAlgorithm
-from timeeval import TimeEval, Algorithm, Datasets, DatasetManager, Status
+from timeeval import TimeEval, Algorithm, Datasets, DatasetManager, Status, ResourceConstraints
 from timeeval.adapters import FunctionAdapter
 
 
@@ -17,7 +17,7 @@ def setUp(self) -> None:
         self.datasets: Datasets = DatasetManager("./tests/example_data", custom_datasets_file=self.datasets_config)
         self.identity_algorithm = Algorithm(name="test", main=FunctionAdapter.identity(), data_as_file=False)
 
-    @patch("timeeval.experiments.load_dataset")
+    @patch("timeeval.core.experiments.load_dataset")
     def test_wrong_df_shape(self, mock_load):
         df = pd.DataFrame(np.random.rand(10, 2))
         mock_load.side_effect = [df]
@@ -26,11 +26,45 @@ def test_wrong_df_shape(self, mock_load):
             timeeval.run()
         self.assertTrue("has a shape that was not expected" in timeeval.results.iloc[0].error_message)
 
+    def test_no_datasets(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [], [self.identity_algorithm])
+        self.assertIn("No datasets given", str(ex.exception))
+
     def test_no_algorithms(self):
-        with tempfile.TemporaryDirectory() as tmp_path:
-            with self.assertRaises(AssertionError):
-                timeeval = TimeEval(self.datasets, [("custom", "dataset.1")], [], results_path=Path(tmp_path))
-                timeeval.run()
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1")], [])
+        self.assertIn("No algorithms given", str(ex.exception))
+
+    def test_wrong_repetition_no(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1")], [self.identity_algorithm],
+                     repetitions=0)
+        self.assertIn("repetitions are not supported", str(ex.exception))
+
+    def test_wrong_n_jobs(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1")], [self.identity_algorithm],
+                     n_jobs=-2)
+        self.assertIn("n_jobs", str(ex.exception))
+        self.assertIn("not supported", str(ex.exception))
+
+    def test_missing_experiment_combinations_file(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1")], [self.identity_algorithm],
+                     experiment_combinations_file=Path("nonexistent-file.txt"))
+        self.assertIn("file not found", str(ex.exception))
+
+    def test_unknown_dataset(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1"), ("does", "not exist")], [self.identity_algorithm])
+        self.assertIn("could not be found in DatasetManager", str(ex.exception))
+
+    def test_nonenforceable_resource_constraints(self):
+        with self.assertRaises(AssertionError) as ex:
+            TimeEval(self.datasets, [("custom", "dataset.1")], [self.identity_algorithm],
+                     resource_constraints=ResourceConstraints(task_cpu_limit=1.0))
+        self.assertIn("won't satisfy the specified resource constraints", str(ex.exception))
 
     def test_evaluation_continues_after_exception_in_algorithm(self):
         ERROR_MESSAGE = "error message test"