Repository: extra-p/extrap (language: python)

Issue:
Adaptive modeling
Merging adaptive modeling feature into branch to prepare release of Extra-P 4.2.0.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 4d8da434..22f66c7a 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -28,6 +28,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install autopep8 pytest pytest-cov
         pip install .
+        sudo apt install libegl1
     - name: autopep8
       id: autopep8
       uses: peter-evans/autopep8@v1
diff --git a/AUTHORS.md b/AUTHORS.md
index 071cfc70..fadd927e 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,9 +1,10 @@
 # Extra-P Authors
 
-The following cumulative list contains the names (in alphabetical order) of all
+The following cumulative list contains the names of all
 individuals who have contributed code to this repository:
 
 * Alexandru Calotoiu (calotoiu@cs.tu-darmstadt.de)
+* Marcus Ritter (marcus.ritter@tu-darmstadt.de)
 * Alexander Geiß (alexander.geiss1@tu-darmstadt.de)
 * David Giessing (david.giessing93@gmail.com)
 * Marc-Andre Hermanns (m.a.hermanns@fz-juelich.de)
@@ -16,8 +17,8 @@ individuals who have contributed code to this repository:
 * Nicolas Ollagnier (ncls_olg@yahoo.fr)
 * Marius Poke (m.poke@grs-sim.de)
 * Patrick Reisert (kpreisert@gmail.com)
-* Marcus Ritter (marcus.ritter@tu-darmstadt.de)
 * Niharika Sharma (niharika.sharma1908@gmail.com)
 * Moritz Vanderheyden (moritz.vanderheyden@stud.tu-darmstadt.de)
 * Johannes Wehrstein (johannes.wehrstein@gmx.de)
 * Paul Wiedeking (p.wiedeking@grs-sim.de)
+* Luis Dreher (luis.dreher@stud.tu-darmstadt.de)
\ No newline at end of file
diff --git a/ChangeLog.md b/ChangeLog.md
index 3dbe02b8..52c60f26 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,22 @@
 
 ***
 
+## Released version 4.2
+
+### Measurement point suggestion via GPR
+* The GUI features a view for measurement point suggestions.
+* Several suggestion approaches help the user to complete or extend the set of available measurement points
+to improve model accuracy further and enhance the noise resilience of the model while optimizing modeling cost.
+
+### Segmented modeling
+* Support for segmented performance models with a single or multiple model parameters.
+* Extension of the available plotting features to support segmented models.
+
+### Adaptive performance modeler
+* DNN based modeling approach.
+
+***
+
 ## Released version 4.1
 
 ### Improved GUI
diff --git a/README.md b/README.md
index f2268d07..22566e26 100644
--- a/README.md
+++ b/README.md
@@ -11,29 +11,29 @@
 ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/extra-p/extrap/python-package.yml?style=plastic)
 
 [<img alt="Screenshot of Extra-P" src="https://github.com/extra-p/extrap/raw/master/docs/images/extra-p-2d.png" height="200" align="right" title="Screenshot of Extra-P"/>](docs/images/extra-p-2d.png)
-Extra-P is an automatic performance-modeling tool that supports the user in the identification of *scalability bugs*. 
-A scalability bug is a part of the program whose scaling behavior is unintentionally poor, 
-that is, much worse than expected. A performance model is a formula that expresses a performance metric of interest 
-such as execution time or energy consumption as a function of one or more execution parameters such as the size of the 
-input problem or the number of processors. 
-
-Extra-P uses measurements of various performance metrics at different execution configurations as input to generate 
-performance models of code regions (including their calling context) as a function of the execution parameters. 
-All it takes to search for scalability issues even in full-blown codes is to run a manageable number of small-scale 
+Extra-P is an automatic performance-modeling tool that supports the user in the identification of *scalability bugs*.
+A scalability bug is a part of the program whose scaling behavior is unintentionally poor,
+that is, much worse than expected. A performance model is a formula that expresses a performance metric of interest
+such as execution time or energy consumption as a function of one or more execution parameters such as the size of the
+input problem or the number of processors.
+
+Extra-P uses measurements of various performance metrics at different execution configurations as input to generate
+performance models of code regions (including their calling context) as a function of the execution parameters.
+All it takes to search for scalability issues even in full-blown codes is to run a manageable number of small-scale
 performance experiments, launch Extra-P, and compare the asymptotic or extrapolated performance of the worst instances
 to the expectations.
 
-Extra-P generates not only a list of potential scalability bugs but also human-readable models for all 
-performance metrics available such as floating-point operations or bytes sent by MPI calls that can be further 
+Extra-P generates not only a list of potential scalability bugs but also human-readable models for all
+performance metrics available such as floating-point operations or bytes sent by MPI calls that can be further
 analyzed and compared to identify the root causes of scalability issues.
 
 The following video on the Laboratory for Parallel Programming @ TUDa [YouTube](
 https://www.youtube.com/@parallel_tuda) channel provides a quick introduction to Extra-P.
 
-<a href="https://www.youtube.com/watch?v=Cv2YRCMWqBM"><img src="https://img.youtube.com/vi/Cv2YRCMWqBM/0.jpg" 
+<a href="https://www.youtube.com/watch?v=Cv2YRCMWqBM"><img src="https://img.youtube.com/vi/Cv2YRCMWqBM/0.jpg"
 alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>
 
-Extra-P is developed by [TU Darmstadt](https://www.parallel.informatik.tu-darmstadt.de/) – 
+Extra-P is developed by [TU Darmstadt](https://www.parallel.informatik.tu-darmstadt.de/) –
 in collaboration with [ETH Zurich](https://spcl.inf.ethz.ch/).
 
 *For questions regarding Extra-P, please send a message to <extra-p-support@lists.parallel.informatik.tu-darmstadt.de>.*
@@ -59,6 +59,7 @@ in collaboration with [ETH Zurich](https://spcl.inf.ethz.ch/).
 * marshmallow
 * packaging
 * tqdm
+* sklearn
 * PySide6 (for GUI)
 * matplotlib (for GUI)
 * pyobjc-framework-Cocoa (only for GUI on macOS)
@@ -76,13 +77,15 @@ The `--upgrade` forces the installation of a new version if a previous version i
 ### Usage
 
 Extra-P can be used in two ways, either using the command-line interface or the graphical user interface. More
-information about the usage of Extra-P with both interfaces can be found in the [quick start guide](docs/quick-start.md).
+information about the usage of Extra-P with both interfaces can be found in
+the [quick start guide](docs/quick-start.md).
 
 > **Note**  
 > Extra-P is designed for weak-scaling, therefore, directly modeling of strong-scaling behaviour is not supported.
 > Instead of modeling the runtime of your strong-scaling experiment, you can model the resource consumption, i.e.,
-> the runtime *times* the number of processors.
-
+> the runtime *times* the number of processors. Extra-P automatically offers this conversion, if it detects that
+> strong-scaling data was loaded. If you are loading files that contain per-thread/per-rank data you should select
+> the scaling-type upfront to run the conversion already during the import.
 
 #### Graphical user interface
 
@@ -92,7 +95,7 @@ The graphical user interface can be started by executing the `extrap-gui` comman
 
 The command line interface is available under the `extrap` command:
 
-`extrap` _OPTIONS_ (`--cube` | `--text` | `--talpas` | `--json` | `--extra-p-3`) _FILEPATH_
+`extrap` _OPTIONS_ (`--cube` | `--text` | `--talpas` | `--json` | `--extra-p-3` | `--expereiment`) _FILEPATH_
 
 You can use different input formats as shown in the examples below:
 
@@ -102,34 +105,8 @@ You can use different input formats as shown in the examples below:
 * Create model and save it to text file at the given
   path: `extrap --out test.txt --text test/data/text/one_parameter_1.txt`
 
-The Extra-P command line interface has the following options.
-
-| Arguments                                                            |                                              |
-|----------------------------------------------------------------------|----------------------------------------------|
-| **Positional**                                                       |                                              
-| _FILEPATH_                                                           | Specify a file path for Extra-P to work with 
-| **Optional**                                                         |                                              
-| `-h`, `--help`                                                       | Show help message and exit                   
-| `--version`                                                          | Show program's version number and exit       
-| `--log` {`debug`, `info`, `warning`, `error`, `critical`}            | Set program's log level (default: `warning`) 
-| **Input options**                                                    |                                              
-| `--cube`                                                             | Load a set of CUBE files and generate a new experiment
-| `--extra-p-3`                                                        | Load data from Extra-P 3 (legacy) experiment
-| `--json`                                                             | Load data from JSON or JSON Lines input file
-| `--talpas`                                                           | Load data from Talpas data format
-| `--text`                                                             | Load data from text input file
-| `--experiment`                                                       | Load Extra-P experiment and generate new models
-| `--scaling` {`weak`, `strong`}                                       | Set weak or strong scaling when loading data from CUBE files (default: `weak`) 
-| **Modeling options**                                                 |                                              
-| `--median`                                                           | Use median values for computation instead of mean values  
-| `--modeler` {`default`, `basic`, `refining`, `multi-parameter`}      | Selects the modeler for generating the performance models 
-| `--options` _KEY_=_VALUE_ [_KEY_=_VALUE_ ...]                        | Options for the selected modeler             
-| `--help-modeler` {`default`, `basic`, `refining`, `multi-parameter`} | Show help for modeler options and exit       
-| **Output options**                                                   |                                              
-| `--out` _OUTPUT_PATH_                                                | Specify the output path for Extra-P results  
-| `--print` {`all`, `callpaths`, `metrics`, `parameters`, `functions`, _FORMAT_STRING_} | Set which information should be displayed after modeling. Use one of the predefined values or specify a formatting string using placeholders (see [docs/output-formatting.md](docs/output-formatting.md)) (default: `all`).
-| `--save-experiment` <i>EXPERIMENT_PATH</i>                           | Saves the experiment including all models as Extra-P experiment (if no extension is specified, “.extra-p” is appended) 
-| `--model-set-name` _NAME_                                            | Set the name of the generated set of models when outputting an experiment (default: “New model”)
+  You can find an overview about all command line options
+  under [docs/command-line-options.md](docs/command-line-options.md).
 
 ### License
 
@@ -153,8 +130,17 @@ Please cite Extra-P in your publications if it helps your research:
 
 ### Publications
 
-1. Alexandru Calotoiu, David Beckingsale, Christopher W. Earl, Torsten Hoefler, Ian Karlin, Martin Schulz, Felix Wolf: Fast Multi-Parameter Performance Modeling. In Proc. of the 2016 IEEE International Conference on Cluster Computing (CLUSTER), Taipei, Taiwan, pages 172–181, IEEE, September 2016. [PDF](https://apps.fz-juelich.de/jsc-pubsystem/aigaion/attachments/fastmultiparam.pdf-f839eba376c6d61a8c4cab9860b6b3bf.pdf)
+1. Alexandru Calotoiu, David Beckingsale, Christopher W. Earl, Torsten Hoefler, Ian Karlin, Martin Schulz, Felix Wolf:
+   Fast Multi-Parameter Performance Modeling. In Proc. of the 2016 IEEE International Conference on Cluster Computing (
+   CLUSTER), Taipei, Taiwan, pages 172–181, IEEE, September
+    2016. [PDF](https://apps.fz-juelich.de/jsc-pubsystem/aigaion/attachments/fastmultiparam.pdf-f839eba376c6d61a8c4cab9860b6b3bf.pdf)
 
-2. Marcus Ritter, Alexandru Calotoiu, Sebastian Rinke, Thorsten Reimann, Torsten Hoefler, Felix Wolf: *Learning Cost-Effective Sampling Strategies for Empirical Performance Modeling.* In Proc. of the 34th IEEE International Parallel and Distributed Processing Symposium (IPDPS), New Orleans, LA, USA, pages 884–895, IEEE, May 2020. [PDF](https://apps.fz-juelich.de/jsc-pubsystem/aigaion/attachments/ritter_ea_2020_ipdps.pdf-01cbe96f7a170aba7c7ef941f966d292.pdf)
+2. Marcus Ritter, Alexandru Calotoiu, Sebastian Rinke, Thorsten Reimann, Torsten Hoefler, Felix Wolf: *Learning
+   Cost-Effective Sampling Strategies for Empirical Performance Modeling.* In Proc. of the 34th IEEE International
+   Parallel and Distributed Processing Symposium (IPDPS), New Orleans, LA, USA, pages 884–895, IEEE, May
+    2020. [PDF](https://apps.fz-juelich.de/jsc-pubsystem/aigaion/attachments/ritter_ea_2020_ipdps.pdf-01cbe96f7a170aba7c7ef941f966d292.pdf)
 
-3. Marcus Ritter, Alexander Geiß, Johannes Wehrstein, Alexandru Calotoiu, Thorsten Reimann, Torsten Hoefler, Felix Wolf: *Noise-Resilient Empirical Performance Modeling with Deep Neural Networks.* In Proc. of the 35th IEEE International Parallel and Distributed Processing Symposium (IPDPS), Portland, Oregon, USA, pages 23–34, IEEE, May 2021. [PDF](http://htor.inf.ethz.ch/publications/img/noiseresilientmodeling.pdf)
+3. Marcus Ritter, Alexander Geiß, Johannes Wehrstein, Alexandru Calotoiu, Thorsten Reimann, Torsten Hoefler, Felix Wolf:
+   *Noise-Resilient Empirical Performance Modeling with Deep Neural Networks.* In Proc. of the 35th IEEE International
+   Parallel and Distributed Processing Symposium (IPDPS), Portland, Oregon, USA, pages 23–34, IEEE, May
+    2021. [PDF](http://htor.inf.ethz.ch/publications/img/noiseresilientmodeling.pdf)
diff --git a/docs/command-line-options.md b/docs/command-line-options.md
new file mode 100644
index 00000000..11609b24
--- /dev/null
+++ b/docs/command-line-options.md
@@ -0,0 +1,34 @@
+Extra-P Command Line Options
+============================
+
+The Extra-P command line interface has the following options.
+
+| Arguments                                                                                                                                           |                                                                                                                                                                                                         |
+|-----------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Positional**                                                                                                                                      |                                                                                                                                                                                                         |
+| _FILEPATH_                                                                                                                                          | Specify a file path for Extra-P to work with                                                                                                                                                            |
+| **Optional**                                                                                                                                        |                                                                                                                                                                                                         |
+| `-h`, `--help`                                                                                                                                      | Show help message and exit                                                                                                                                                                              |
+| `--version`                                                                                                                                         | Show program's version number and exit                                                                                                                                                                  |
+| `--log` {`debug`, `info`, `warning`, `error`, `critical`}                                                                                           | Set program's log level (default: `warning`)                                                                                                                                                            |
+| **Input options**                                                                                                                                   |                                                                                                                                                                                                         |
+| `--cube`                                                                                                                                            | Load a set of CUBE files and generate a new experiment                                                                                                                                                  |
+| `--extra-p-3`                                                                                                                                       | Load data from Extra-P 3 (legacy) experiment                                                                                                                                                            |
+| `--json`                                                                                                                                            | Load data from JSON or JSON Lines input file                                                                                                                                                            |
+| `--talpas`                                                                                                                                          | Load data from Talpas data format                                                                                                                                                                       |
+| `--text`                                                                                                                                            | Load data from text input file                                                                                                                                                                          |
+| `--experiment`                                                                                                                                      | Load Extra-P experiment and generate new models                                                                                                                                                         |
+| `--scaling` {`weak`, `weak_parallel`, `strong`}                                                                                                     | Set scaling type when loading data from per-thread/per-rank files (CUBE files) (default: weak)                                                                                                          |
+| --keep-values                                                                                                                                       | Keeps the original values after import                                                                                                                                                                  |
+| **Modeling options**                                                                                                                                |                                                                                                                                                                                                         |
+| `--median`                                                                                                                                          | Use median values for computation instead of mean values                                                                                                                                                |
+| `--modeler` {`default`, `basic`, `refining`, `multi-parameter`}                                                                                     | Selects the modeler for generating the performance models                                                                                                                                               |
+| `--options` _KEY_=_VALUE_ [_KEY_=_VALUE_ ...]                                                                                                       | Options for the selected modeler                                                                                                                                                                        |
+| `--help-modeler` {`default`, `basic`, `refining`, `multi-parameter`}                                                                                | Show help for modeler options and exit                                                                                                                                                                  |
+| **Output options**                                                                                                                                  |                                                                                                                                                                                                         |
+| `--out` _OUTPUT_PATH_                                                                                                                               | Specify the output path for Extra-P results                                                                                                                                                             |
+| `--print` {`all`,`all-python`,`all-latex`, `callpaths`, `metrics`, `parameters`, `functions`,`functions-python`,`functions-latex`, _FORMAT_STRING_} | Set which information should be displayed after modeling. Use one of the presets or specify a formatting string using placeholders (see [output-formatting.md](output-formatting.md)). (default: `all`) |
+| `--save-experiment` <i>EXPERIMENT_PATH</i>                                                                                                          | Saves the experiment including all models as Extra-P experiment (if no extension is specified, “.extra-p” is appended)                                                                                  |
+| `--model-set-name` _NAME_                                                                                                                           | Set the name of the generated set of models when outputting an experiment (default: “New model”)                                                                                                        |
+| `--disable-progress`                                                                                                                                | Disables the progress bar outputs of Extra-P to stdout and stderr                                                                                                                                       |                                                                                                                             
+
diff --git a/docs/output-formatting.md b/docs/output-formatting.md
index e281bcac..fda65936 100644
--- a/docs/output-formatting.md
+++ b/docs/output-formatting.md
@@ -1,6 +1,7 @@
 Output formatting documentation
 ===============================
 
+
 It is possible to customize the output of Extra-P by using placeholders. Use placeholders enclosed in curly brackets in
 any string. You can modify the behaviour of the placeholders, by specifying additional options. Possible placeholders
 and their matching options are:
@@ -33,6 +34,7 @@ and their matching options are:
 - `{metric}`
 - `{callpath}`
 - `{model}`
+    - formatting options: _default_ (if nothing is specified),`python`,`latex`
 - `{smape}`
 - `{rrss}`
 - `{rss}`
@@ -48,6 +50,7 @@ have to be escaped by doubling them (`{{` or `}}`). Placeholders with numerical
 [python format specification mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language)
 to specify decimal places and precision. With `{metric}, {callpath}, {points}`
 and `{parameters}`, you can remove duplicates by adding `?` before the placeholder names.
+For `{model}`, you can switch between formats by adding the format option behind a colon: `{model:<formatting option>}`.
 
 **Example formatting strings:**
 
@@ -55,4 +58,5 @@ and `{parameters}`, you can remove duplicates by adding `?` before the placehold
  "{?metric}, {callpath}: errors: {rss:.2e}, {re}"
  "{{{points: sep:' \' ' ; format:'({point})'}}}"
  "{measurements: sep:', '; format:'{point: sep:';';format:'{parameter}{coordinate}'}--{mean}\n'}"
+ "{metric}, {callpath}: {rss}/{smape} {model:python}"
 ```
\ No newline at end of file
diff --git a/extrap/__init__.py b/extrap/__init__.py
index 9908d8cd..7c88f347 100644
--- a/extrap/__init__.py
+++ b/extrap/__init__.py
@@ -6,7 +6,7 @@
 # See the LICENSE file in the base directory for details.
 
 __title__ = "Extra-P"
-__version__ = "4.1.2"
+__version__ = "4.2.0"
 __description__ = "Extra-P, automated performance modeling for HPC applications"
 __copyright__ = "Copyright (c) 2020-2023 Technical University of Darmstadt, Darmstadt, Germany"
 __documentation_link__ = f"https://github.com/extra-p/extrap/tree/v{__version__}/docs"
diff --git a/extrap/entities/coordinate.py b/extrap/entities/coordinate.py
index 3a7e2fa6..315d58f6 100644
--- a/extrap/entities/coordinate.py
+++ b/extrap/entities/coordinate.py
@@ -81,7 +81,7 @@ def as_tuple(self):
 
     def as_partial_tuple(self, except_param):
         return tuple(c for p, c in enumerate(self._values) if p != except_param)
-
+    
     def is_mostly_lower(self, other: 'Coordinate', except_param):
         return all(a <= b
                    for i, (a, b) in enumerate(zip(self._values, other._values))
diff --git a/extrap/entities/experiment.py b/extrap/entities/experiment.py
index 41dee12d..daafdb7b 100644
--- a/extrap/entities/experiment.py
+++ b/extrap/entities/experiment.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2022, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -20,6 +20,7 @@
 from extrap.entities.measurement import Measurement, MeasurementSchema
 from extrap.entities.metric import Metric, MetricSchema
 from extrap.entities.parameter import Parameter, ParameterSchema
+from extrap.entities.scaling_type import ScalingType
 from extrap.fileio import io_helper
 from extrap.modelers.model_generator import ModelGenerator, ModelGeneratorSchema
 from extrap.util.deprecation import deprecated
@@ -35,8 +36,7 @@ def __init__(self):
         self.metrics: List[Metric] = UniqueList()
         self.parameters: List[Parameter] = UniqueList()
         self.coordinates: List[Coordinate] = UniqueList()
-        self.measurements: Dict[Tuple[Callpath,
-                                      Metric], List[Measurement]] = {}
+        self.measurements: Dict[Tuple[Callpath, Metric], List[Measurement]] = {}
         self.call_tree: CallTree = None
         self.modelers: List[ModelGenerator] = []
         self.scaling = None
@@ -80,6 +80,11 @@ def add_measurement(self, measurement: Measurement):
         else:
             self.measurements[key] = [measurement]
 
+    def delete_measurement(self, callpath: Callpath, metric: Metric):
+        key = (callpath,
+               metric)
+        self.measurements.pop(key)
+
     def clear_measurements(self):
         self.measurements = {}
 
@@ -108,7 +113,7 @@ def debug(self):
 
 class ExperimentSchema(BaseSchema):
     _version_ = fields.Constant(extrap.__version__, data_key=extrap.__title__)
-    scaling = fields.Str(required=False, allow_none=True, validate=validate.OneOf(['strong', 'weak']))
+    scaling = fields.Str(required=False, allow_none=True, validate=validate.OneOf([str(s) for s in ScalingType]))
     parameters = fields.List(fields.Pluck(ParameterSchema, 'name'))
     callpaths = ListToMappingField(CallpathSchema, 'name', list_type=UniqueList, dump_condition=lambda x: bool(x.tags))
     metrics = ListToMappingField(MetricSchema, 'name', list_type=UniqueList, dump_condition=lambda x: bool(x.tags))
@@ -121,6 +126,9 @@ class ExperimentSchema(BaseSchema):
     def set_progress_bar(self, pbar):
         self.context['progress_bar'] = pbar
 
+    def set_value_io(self, value_writer):
+        self.context['value_io'] = value_writer
+
     @pre_load
     def add_progress(self, data, **kwargs):
         file_version = data.get(extrap.__title__)
diff --git a/extrap/entities/functions.py b/extrap/entities/functions.py
index 35287612..dc292698 100644
--- a/extrap/entities/functions.py
+++ b/extrap/entities/functions.py
@@ -1,18 +1,26 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
+from __future__ import annotations
 
-from typing import List, Mapping, Union
+import math
+import numbers
+from itertools import chain
+from typing import List, Mapping, Union, Sequence
 
 import numpy
+import numpy as np
 from marshmallow import fields
 
 from extrap.entities.parameter import Parameter
-from extrap.entities.terms import CompoundTerm, MultiParameterTerm, CompoundTermSchema, MultiParameterTermSchema
-from extrap.util.serialization_schema import BaseSchema, NumberField
+from extrap.entities.terms import CompoundTerm, MultiParameterTerm, CompoundTermSchema, MultiParameterTermSchema, \
+    SegmentedTerm
+from extrap.util.latex_formatting import frmt_scientific_coefficient
+from extrap.util.serialization_schema import BaseSchema, NumberField, NumpyField
+from extrap.util.string_formats import FunctionFormats
 
 
 class Function:
@@ -54,16 +62,63 @@ def evaluate(self, parameter_value):
             function_value += t.evaluate(parameter_value)
         return function_value
 
-    def to_string(self, *parameters: Union[str, Parameter]):
+    def to_string(self, *parameters: Union[str, Parameter], format: FunctionFormats = None):
         """
         Return a string representation of the function.
         """
-        function_string = str(self.constant_coefficient)
-        for t in self.compound_terms:
-            function_string += ' + '
-            function_string += t.to_string(*parameters)
+        if format == FunctionFormats.LATEX:
+            return self.to_latex_string(*parameters)
+
+        term_list = (t.to_string(*parameters, format=format) for t in self.compound_terms)
+        if self.constant_coefficient != 0 or not self.compound_terms:
+            term_list = chain([str(self.constant_coefficient)], term_list)
+
+        joiner = ' + '
+        if format == FunctionFormats.PYTHON:
+            joiner = '+'
+        function_string = joiner.join(term_list)
+
+        if format == FunctionFormats.PYTHON:
+            function_string = function_string.replace('+-', '-')
         return function_string
 
+    def to_latex_string(self, *parameters: Union[str, Parameter]):
+        """
+        Return a math string (using latex encoding) representation of the function.
+        """
+        new_coefficients = []
+        new_coefficients.append(frmt_scientific_coefficient(self.constant_coefficient))
+        for t in self.compound_terms:
+            new_coefficients.append(frmt_scientific_coefficient(t.coefficient))
+        function_string = new_coefficients[0]
+        coeff_counter = 1
+        for t in self.compound_terms:
+            if isinstance(t, MultiParameterTerm) is True:
+                sub_terms = t.parameter_term_pairs
+            elif isinstance(t, CompoundTerm) is True:
+                sub_terms = t.simple_terms
+            new_term = new_coefficients[coeff_counter]
+            for sub_term in sub_terms:
+                if type(sub_term) is tuple:
+                    new_term = new_term + "*" + sub_term[1].to_string(parameter=parameters[sub_term[0]],
+                                                                      format=FunctionFormats.PYTHON)
+                    new_term = new_term.replace("log2(" + str(parameters[sub_term[0]]) + ")",
+                                                "\\log_2{" + str(parameters[sub_term[0]]) + "}")
+                else:
+                    new_term = new_term + "*" + sub_term.to_string(parameter=parameters[0],
+                                                                   format=FunctionFormats.PYTHON)
+                    new_term = new_term.replace("log2(" + str(parameters[0]) + ")",
+                                                "\\log_2{" + str(parameters[0]) + "}")
+            new_term = new_term.replace("**", "^")
+            new_term = new_term.replace("*", "\\cdot ")
+            new_term = new_term.replace("(", "{")
+            new_term = new_term.replace(")", "}")
+            if new_term[0] != "-":
+                function_string += "+"
+            function_string += new_term
+            coeff_counter += 1
+        return "$" + function_string + "$"
+
     def __repr__(self):
         return f"Function({self.to_string('p')})"
 
@@ -96,7 +151,7 @@ def __init__(self, constant_coefficient=1):
         self.add_compound_term = None
         self.__iadd__ = None
 
-    def to_string(self, *_):
+    def to_string(self, *_, format: FunctionFormats = None):
         """
         Returns a string representation of the constant function.
         """
@@ -117,9 +172,104 @@ def evaluate(self, parameter_value):
         return super().evaluate(parameter_value)
 
 
+class SegmentedFunction(SingleParameterFunction):
+    MAX_NUM_SEGMENTS = 2
+
+    def __init__(self, segments: list[SingleParameterFunction],
+                 intervals: Sequence[tuple[numbers.Number, numbers.Number]]):
+        super().__init__()
+        self.add_compound_term = None
+        self.__iadd__ = None
+        if len(segments) > self.MAX_NUM_SEGMENTS:
+            raise ValueError(f"Only {self.MAX_NUM_SEGMENTS} are allowed.")
+        if len(segments) != len(intervals):
+            raise ValueError("Number of intervals must be equal to the number of segments")
+        self.segments = segments
+        self.intervals = np.array(intervals, dtype=float)
+
+    @property
+    def constant_coefficient(self):
+        return 0
+
+    @constant_coefficient.setter
+    def constant_coefficient(self, value):
+        if value != 0:
+            raise NotImplementedError()
+
+    @property
+    def compound_terms(self):
+        return [SegmentedTerm(self.segments, self.intervals)]
+
+    @compound_terms.setter
+    def compound_terms(self, value):
+        if value:
+            raise NotImplementedError()
+
+    def reset_coefficients(self):
+        for v in self.segments:
+            v.reset_coefficients()
+
+    def evaluate(self, parameter_value):
+        if not self.segments:
+            return super().evaluate(parameter_value)
+
+        if hasattr(parameter_value, '__len__') and (len(parameter_value) == 1 or isinstance(parameter_value, Mapping)):
+            parameter_value = parameter_value[0]
+
+        if isinstance(parameter_value, np.ndarray):
+            function_value = np.ndarray(parameter_value.shape, dtype=float)
+            int_start = self.intervals[:, 0]
+            int_end = self.intervals[:, 1]
+            int_start_mask = int_start.reshape(1, -1) <= parameter_value.reshape(-1, 1)
+            int_end_mask = parameter_value.reshape(-1, 1) <= int_end.reshape(1, -1)
+            mask = int_start_mask & int_end_mask
+            match = np.argmax(mask, axis=1)
+            for i, segment in enumerate(self.segments):
+                function_value[match == i] = segment.evaluate(parameter_value[match == i])
+            function_value[~np.any(mask, axis=1)] = math.nan
+            return function_value
+        else:
+            for (int_start, int_end), segment in zip(self.intervals, self.segments):
+                if int_start <= parameter_value <= int_end:
+                    return segment.evaluate(parameter_value)
+            return math.nan
+
+    def to_string(self, *parameters: Union[str, Parameter], format: FunctionFormats = None):
+        """
+        Return a string representation of the function.
+        """
+        if format == FunctionFormats.LATEX:
+            return self.to_latex_string(*parameters)
+
+        elif format == FunctionFormats.PYTHON:
+            function_string = self.segments[0].to_string(*parameters, format=format)
+            function_string += f" if {parameters[0]}<={self.intervals[0][1]} else"
+            function_string += self.segments[1].to_string(*parameters, format=format)
+            if self.intervals[0][1] != self.intervals[1][0]:
+                function_string += f" if {parameters[0]}>={self.intervals[1][0]} else math.nan"
+
+        else:
+            function_string = self.segments[0].to_string(*parameters, format=format)
+            function_string += f" for {parameters[0]}<={self.intervals[0][1]}\n"
+            function_string += self.segments[1].to_string(*parameters, format=format)
+            function_string += f" for {parameters[0]}>={self.intervals[1][0]}"
+
+        return function_string
+
+    def to_latex_string(self, *parameters: Union[str, Parameter]):
+        """
+        Return a math string (using latex encoding) representation of the function.
+        """
+        function_string = self.segments[0].to_latex_string(*parameters)
+        function_string += f" for ${parameters[0]}<={self.intervals[0][1]}$\n"
+        function_string += self.segments[1].to_latex_string(*parameters)
+        function_string += f" for ${parameters[0]}>={self.intervals[1][0]}$"
+        return function_string
+
+
 class MultiParameterFunction(Function):
     compound_terms: List[MultiParameterTerm]
-    
+
     def __init__(self, *compound_terms: MultiParameterTerm):
         super().__init__(*compound_terms)
 
@@ -129,7 +279,8 @@ def __repr__(self):
 
 class FunctionSchema(BaseSchema):
     constant_coefficient = NumberField()
-    compound_terms: List[CompoundTerm] = fields.List(fields.Nested(CompoundTermSchema))
+    compound_terms: List[CompoundTerm] = fields.List(
+        fields.Nested(CompoundTermSchema))
 
 
 class ConstantFunctionSchema(FunctionSchema):
@@ -145,7 +296,18 @@ def create_object(self):
 
 
 class MultiParameterFunctionSchema(FunctionSchema):
-    compound_terms: List[CompoundTerm] = fields.List(fields.Nested(MultiParameterTermSchema))
+    compound_terms: List[CompoundTerm] = fields.List(
+        fields.Nested(MultiParameterTermSchema))
 
     def create_object(self):
         return MultiParameterFunction()
+
+
+class SegmentedFunctionSchema(SingleParameterFunctionSchema):
+    compound_terms = fields.Constant([], load_only=True)
+    constant_coefficient = fields.Constant(0, load_only=True)
+    segments = fields.List(fields.Nested(SingleParameterFunctionSchema))
+    intervals = NumpyField()
+
+    def create_object(self):
+        return SegmentedFunction([], [])
diff --git a/extrap/entities/hypotheses.py b/extrap/entities/hypotheses.py
index 1b8366dd..a5dcc427 100644
--- a/extrap/entities/hypotheses.py
+++ b/extrap/entities/hypotheses.py
@@ -5,6 +5,7 @@
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
+import math
 from typing import Sequence
 
 import numpy
@@ -23,6 +24,7 @@ def __init__(self, function: Function, use_median):
         self.function: Function = function
         self._RSS = 0
         self._rRSS = 0
+        self._nRSS = 0
         self._SMAPE = 0
         self._AR2 = 0
         self._RE = 0
@@ -38,6 +40,15 @@ def RSS(self):
             raise RuntimeError("Costs are not calculated.")
         return self._RSS
 
+    @property
+    def nRSS(self):
+        """
+        Return the nRSS.
+        """
+        if not self._costs_are_calculated:
+            raise RuntimeError("Costs are not calculated.")
+        return self._nRSS
+
     @property
     def rRSS(self):
         """
@@ -98,6 +109,7 @@ def clean_constant_coefficient(self, phi, training_measurements):
             minimum = min(m.median for m in training_measurements)
         else:
             minimum = min(m.mean for m in training_measurements)
+
         if minimum == 0:
             if abs(self.function.constant_coefficient - minimum) < phi:
                 self.function.constant_coefficient = 0
@@ -138,6 +150,7 @@ def __eq__(self, other):
 MAX_HYPOTHESIS = Hypothesis(Function(), False)
 MAX_HYPOTHESIS._RSS = float('inf')
 MAX_HYPOTHESIS._rRSS = float('inf')
+MAX_HYPOTHESIS._nRSS = float('inf')
 MAX_HYPOTHESIS._SMAPE = float('inf')
 MAX_HYPOTHESIS._AR2 = float('inf')
 MAX_HYPOTHESIS._RE = float('inf')
@@ -177,12 +190,14 @@ def compute_cost(self, measurements: Sequence[Measurement]):
         """
         self._AR2 = 1  # TODO: should this be calculated?
         smape = 0
+        actuals = []
         for measurement in measurements:
             predicted = self.function.constant_coefficient
             if self._use_median:
                 actual = measurement.median
             else:
                 actual = measurement.mean
+            actuals.append(actual)
 
             difference = predicted - actual
             self._RSS += difference * difference
@@ -199,6 +214,10 @@ def compute_cost(self, measurements: Sequence[Measurement]):
                 smape += abs(difference) / abssum * 2
 
         self._SMAPE = smape / len(measurements) * 100
+        if numpy.mean(actuals) != 0.0:
+            self._nRSS = math.sqrt(self._RSS) / numpy.mean(actuals)
+        else:
+            self._nRSS = math.nan
         self._costs_are_calculated = True
 
 
@@ -226,6 +245,9 @@ def compute_cost_leave_one_out(self, training_measurements: Sequence[Measurement
 
         difference = predicted - actual
         self._RSS += difference * difference
+        self._nRSS += math.sqrt(self._RSS) / numpy.mean(
+            numpy.array([m.value(self._use_median) for m in training_measurements])) / (len(training_measurements) + 1)
+
         if actual != 0:
             relative_difference = difference / actual
             self._RE += numpy.abs(relative_difference) / (len(training_measurements) + 1)
@@ -246,6 +268,7 @@ def compute_cost(self, measurements: Sequence[Measurement]):
 
         difference = predicted - actual
         self._RSS = numpy.sum(difference * difference)
+        self._nRSS = math.sqrt(self._RSS) / numpy.mean(actual)
 
         relativeDifference = difference / actual
         self._rRSS = numpy.sum(relativeDifference * relativeDifference)
@@ -279,6 +302,7 @@ def compute_coefficients(self, measurements: Sequence[Measurement]):
             b_list = numpy.array([m.median for m in measurements])
         else:
             b_list = numpy.array([m.mean for m in measurements])
+
         points = numpy.array([m.coordinate[0] for m in measurements])
 
         a_list = [numpy.ones((1, len(points)))]
@@ -404,8 +428,8 @@ def compute_coefficients(self, measurements):
         B = numpy.array(b_list)
         try:
             coeffs, residuals, rank, sing_val = numpy.linalg.lstsq(A, B, None)
-            if rank < A.shape[1]: # if rcond is to big the rank of A collapses and the coefficients are wrong
-                coeffs, residuals, rank, sing_val = numpy.linalg.lstsq(A, B, -1) # retry with rcond = machine precision
+            if rank < A.shape[1]:  # if rcond is to big the rank of A collapses and the coefficients are wrong
+                coeffs, residuals, rank, sing_val = numpy.linalg.lstsq(A, B, -1)  # retry with rcond = machine precision
         except numpy.linalg.LinAlgError as e:
             # sometimes first try does not work
             coeffs, _, rank, _ = numpy.linalg.lstsq(A, B, None)
@@ -425,6 +449,7 @@ class HypothesisSchema(BaseSchema):
     function = fields.Nested(FunctionSchema)
     _RSS = NumberField(data_key='RSS')
     _rRSS = NumberField(data_key='rRSS')
+    _nRSS = NumberField(data_key='nRSS')
     _SMAPE = NumberField(data_key='SMAPE')
     _AR2 = NumberField(data_key='AR2')
     _RE = NumberField(data_key='RE')
diff --git a/extrap/entities/measurement.py b/extrap/entities/measurement.py
index 8412dac5..9f670195 100644
--- a/extrap/entities/measurement.py
+++ b/extrap/entities/measurement.py
@@ -1,10 +1,14 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
+from __future__ import annotations
+
+from typing import Optional
+
 import numpy as np
 from marshmallow import fields, post_load
 
@@ -19,7 +23,7 @@ class Measurement:
     This class represents a measurement, i.e. the value measured for a specific metric and callpath at a coordinate.
     """
 
-    def __init__(self, coordinate: Coordinate, callpath: Callpath, metric: Metric, values):
+    def __init__(self, coordinate: Coordinate, callpath: Callpath, metric: Metric, values, *, keep_values=False):
         """
         Initialize the Measurement object.
         """
@@ -29,6 +33,11 @@ def __init__(self, coordinate: Coordinate, callpath: Callpath, metric: Metric, v
         if values is None:
             return
         values = np.array(values)
+        self.keep_values = keep_values
+        if self.keep_values:
+            self.values: Optional[np.typing.NDArray] = values
+        else:
+            self.values = None
         self.median: float = np.median(values)
         self.mean: float = np.mean(values)
         self.minimum: float = np.min(values)
@@ -38,6 +47,16 @@ def __init__(self, coordinate: Coordinate, callpath: Callpath, metric: Metric, v
     def value(self, use_median):
         return self.median if use_median else self.mean
 
+    def add_value(self, value):
+        if not self.keep_values:
+            raise RuntimeError("Cannot add value, because the list of original values does not exist.")
+        self.values = np.append(self.values, value)
+        self.median = np.median(self.values)
+        self.mean = np.mean(self.values)
+        self.minimum = np.min(self.values)
+        self.maximum = np.max(self.values)
+        self.std = np.std(self.values)
+
     def merge(self, other: 'Measurement') -> None:
         """Approximately merges the other measurement into this measurement."""
         if self.coordinate != other.coordinate:
@@ -57,11 +76,32 @@ def __eq__(self, other):
         elif self is other:
             return True
         else:
-            return self.coordinate == other.coordinate and \
-                   self.metric == other.metric and \
-                   self.callpath == other.callpath and \
-                   self.mean == other.mean and \
-                   self.median == other.median
+            return (self.coordinate == other.coordinate and
+                    self.metric == other.metric and
+                    self.callpath == other.callpath and
+                    self.mean == other.mean and
+                    self.median == other.median)
+
+    def __imul__(self, other):
+        if isinstance(other, Measurement):
+            if self.coordinate != other.coordinate:
+                raise ValueError("Coordinate does not match while merging measurements.")
+            self.median *= other.median
+            self.mean *= other.mean
+            self.minimum *= other.minimum
+            self.maximum *= other.maximum
+            # Var(XY) = E(X²Y²) - (E(XY))² = Var(X)Var(Y) + Var(X)(E(Y))² + Var(Y)(E(X))²
+            self_var, other_var = self.std ** 2, other.std ** 2
+            variance = self_var * other_var + self_var * other.mean ** 2 + other_var * self.mean ** 2
+            self.std = np.sqrt(variance)
+        else:
+
+            self.median *= other
+            self.mean *= other
+            self.minimum *= other
+            self.maximum *= other
+            self.std *= abs(other)
+        return self
 
 
 class MeasurementSchema(Schema):
@@ -73,6 +113,17 @@ class MeasurementSchema(Schema):
     minimum = NumberField()
     maximum = NumberField()
     std = NumberField()
+    values = fields.Method('_store_values', '_load_values', allow_none=True, load_default=None)
+
+    def _load_values(self, value):
+        if value is None:
+            return None
+        return self.context['value_io'].read_values(*value)
+
+    def _store_values(self, obj: Measurement):
+        if obj.values is None:
+            return None
+        return self.context['value_io'].write_values(obj.values)
 
     @post_load
     def report_progress(self, data, **kwargs):
diff --git a/extrap/entities/model.py b/extrap/entities/model.py
index 58302182..1cfb7243 100644
--- a/extrap/entities/model.py
+++ b/extrap/entities/model.py
@@ -15,10 +15,10 @@
 from extrap.entities.annotations import Annotation, AnnotationSchema
 from extrap.entities.callpath import CallpathSchema
 from extrap.entities.hypotheses import Hypothesis, HypothesisSchema
-from extrap.entities.measurement import Measurement
+from extrap.entities.measurement import Measurement, MeasurementSchema
 from extrap.entities.metric import MetricSchema
 from extrap.util.caching import cached_property
-from extrap.util.serialization_schema import Schema
+from extrap.util.serialization_schema import BaseSchema
 
 
 class Model:
@@ -42,12 +42,61 @@ def __eq__(self, other):
             return True
         else:
             return self.callpath == other.callpath and \
-                   self.metric == other.metric and \
-                   self.hypothesis == other.hypothesis and \
-                   self.measurements == other.measurements
+                self.metric == other.metric and \
+                self.hypothesis == other.hypothesis and \
+                self.measurements == other.measurements
 
 
-class ModelSchema(Schema):
+class SegmentedModel(Model):
+    def __init__(self, hypothesis, segment_models, changing_points, callpath=None, metric=None):
+        self._callpath = None
+        self._metric = None
+        self._measurements = None
+        self.changing_points: list[Measurement] = changing_points
+        self.segment_models: list[Model] = segment_models
+        super().__init__(hypothesis, callpath, metric)
+
+    @property
+    def measurements(self):
+        return self._measurements
+
+    @measurements.setter
+    def measurements(self, value):
+        self._measurements = value
+        if value is None:
+            return
+        index = value.index(self.changing_points[0])
+        self.segment_models[0].measurements = value[:index]
+        if len(self.changing_points) == 1:
+            self.segment_models[1].measurements = value[index:]
+        elif len(self.changing_points) == 2:
+            index2 = value.index(self.changing_points[1])
+            self.segment_models[1].measurements = value[index2:]
+        else:
+            raise NotImplementedError()
+
+    @property
+    def callpath(self):
+        return self._callpath
+
+    @callpath.setter
+    def callpath(self, value):
+        self._callpath = value
+        for model in self.segment_models:
+            model.callpath = value
+
+    @property
+    def metric(self):
+        return self._metric
+
+    @metric.setter
+    def metric(self, value):
+        self._metric = value
+        for model in self.segment_models:
+            model.metric = value
+
+
+class ModelSchema(BaseSchema):
     def create_object(self):
         return Model(None)
 
@@ -67,3 +116,11 @@ def report_progress(self, data, **kwargs):
     callpath = fields.Nested(CallpathSchema)
     metric = fields.Nested(MetricSchema)
     annotations = fields.List(fields.Nested(AnnotationSchema))
+
+
+class SegmentedModelSchema(ModelSchema):
+    segment_models = fields.List(fields.Nested(ModelSchema))
+    changing_points = fields.List(fields.Nested(MeasurementSchema))
+
+    def create_object(self):
+        return SegmentedModel(None, [], [])
diff --git a/extrap/entities/scaling_type.py b/extrap/entities/scaling_type.py
new file mode 100644
index 00000000..d3e92a4f
--- /dev/null
+++ b/extrap/entities/scaling_type.py
@@ -0,0 +1,17 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from enum import Enum
+
+
+class ScalingType(Enum):
+    WEAK = "weak"
+    WEAK_PARALLEL = "weak_parallel"
+    STRONG = "strong"
+
+    def __str__(self):
+        return self.value
diff --git a/extrap/entities/terms.py b/extrap/entities/terms.py
index b504b47b..57274081 100644
--- a/extrap/entities/terms.py
+++ b/extrap/entities/terms.py
@@ -1,11 +1,12 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
-
+import math
 from abc import ABC, abstractmethod
+from itertools import chain
 from numbers import Real
 from typing import Tuple, List, Union, Mapping
 
@@ -15,7 +16,8 @@
 from extrap.entities.coordinate import Coordinate
 from extrap.entities.fraction import Fraction
 from extrap.entities.parameter import Parameter
-from extrap.util.serialization_schema import Schema, NumberField
+from extrap.util.serialization_schema import Schema, NumberField, NumpyField, VariantSchemaField
+from extrap.util.string_formats import FunctionFormats
 
 DEFAULT_PARAM_NAMES = (
     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
@@ -28,7 +30,7 @@ def __init__(self):
         self.coefficient = 1
 
     @abstractmethod
-    def to_string(self):
+    def to_string(self, *, format: FunctionFormats = None):
         raise NotImplementedError
 
     def reset_coefficients(self):
@@ -55,7 +57,7 @@ def __mul__(self, other):
         return CompoundTerm(self, other)
 
     @abstractmethod
-    def to_string(self, parameter: Union[Parameter, str] = 'p'):
+    def to_string(self, parameter: Union[Parameter, str] = 'p', *, format: FunctionFormats = None):
         raise NotImplementedError
 
 
@@ -91,10 +93,18 @@ def term_type(self, val):
     def reset_coefficients(self):
         pass
 
-    def to_string(self, parameter='p'):
+    def to_string(self, parameter='p', *, format: FunctionFormats = None):
         if self._term_type == "polynomial":
+            if format == FunctionFormats.PYTHON:
+                return f"{parameter}**({self.exponent})"
+            elif format == FunctionFormats.LATEX:
+                return f"{{{parameter}}}^{{{self.exponent}}}"
             return f"{parameter}^({self.exponent})"
         elif self._term_type == "logarithm":
+            if format == FunctionFormats.PYTHON:
+                return f"log2({parameter})**({self.exponent})"
+            elif format == FunctionFormats.LATEX:
+                return f"\\log2{{{parameter}}}^{{{self.exponent}}}"
             return f"log2({parameter})^({self.exponent})"
 
     def _evaluate_polynomial(self, parameter_value):
@@ -115,8 +125,8 @@ def __eq__(self, other):
         elif self is other:
             return True
         else:
-            return self.exponent == other.exponent and \
-                   self._term_type == other._term_type
+            return (self.exponent == other.exponent and
+                    self._term_type == other._term_type)
 
 
 class CompoundTerm(SingleParameterTerm):
@@ -134,10 +144,17 @@ def evaluate(self, parameter_value):
             function_value *= t.evaluate(parameter_value)
         return function_value
 
-    def to_string(self, parameter='p'):
-        function_string = ' * '.join(t.to_string(parameter) for t in self.simple_terms)
-        if self.coefficient != 1:
-            function_string = str(self.coefficient) + ' * ' + function_string
+    def to_string(self, parameter='p', *, format: FunctionFormats = None):
+        term_list = (t.to_string(parameter, format=format) for t in self.simple_terms)
+        if self.coefficient != 1 or not self.simple_terms:
+            term_list = chain([str(self.coefficient)], term_list)
+
+        joiner = ' * '
+        if format == FunctionFormats.PYTHON:
+            joiner = '*'
+        elif format == FunctionFormats.LATEX:
+            joiner = '\\cdot '
+        function_string = joiner.join(term_list)
         return function_string
 
     def __imul__(self, term: SimpleTerm):
@@ -164,8 +181,78 @@ def __eq__(self, other):
         elif self is other:
             return True
         else:
-            return self.coefficient == other.coefficient and \
-                   self.simple_terms == other.simple_terms
+            return (self.coefficient == other.coefficient and
+                    self.simple_terms == other.simple_terms)
+
+
+class SegmentedTerm(CompoundTerm):
+
+    def __init__(self, segments, intervals):
+        super().__init__()
+        self.simple_terms: List[SimpleTerm] = []
+        self.segments = segments
+        self.intervals = intervals
+
+    def reset_coefficients(self):
+        pass
+
+    def evaluate(self, parameter_value):
+        if not self.segments:
+            return super().evaluate(parameter_value)
+
+        if hasattr(parameter_value, '__len__') and (len(parameter_value) == 1 or isinstance(parameter_value, Mapping)):
+            parameter_value = parameter_value[0]
+
+        if isinstance(parameter_value, np.ndarray):
+            function_value = np.ndarray(parameter_value.shape, dtype=float)
+            int_start = self.intervals[:, 0]
+            int_end = self.intervals[:, 1]
+            int_start_mask = int_start.reshape(1, -1) <= parameter_value.reshape(-1, 1)
+            int_end_mask = parameter_value.reshape(-1, 1) <= int_end.reshape(1, -1)
+            mask = int_start_mask & int_end_mask
+            match = np.argmax(mask, axis=1)
+            for i, segment in enumerate(self.segments):
+                function_value[match == i] = segment.evaluate(parameter_value[match == i])
+            function_value[~np.any(mask, axis=1)] = math.nan
+            return function_value
+        else:
+            for (int_start, int_end), segment in zip(self.intervals, self.segments):
+                if int_start <= parameter_value <= int_end:
+                    return segment.evaluate(parameter_value)
+            return math.nan
+
+    def to_string(self, parameter='p', *, format: FunctionFormats = None):
+        """
+        Return a string representation of the function.
+        """
+
+        if format == FunctionFormats.LATEX:
+            return self.to_latex_string(parameter)
+
+        elif format == FunctionFormats.PYTHON:
+            function_string = "(" + self.segments[0].to_string(parameter, format=format)
+            function_string += f" if {parameter}<={self.intervals[0][1]} else"
+            function_string += self.segments[1].to_string(parameter, format=format)
+            if self.intervals[0][1] != self.intervals[1][0]:
+                function_string += f" if {parameter}>={self.intervals[1][0]} else math.nan)"
+
+        else:
+            function_string = "{" + self.segments[0].to_string(parameter, format=format)
+            function_string += f" for {parameter}<={self.intervals[0][1]}; "
+            function_string += self.segments[1].to_string(parameter, format=format)
+            function_string += f" for {parameter}>={self.intervals[1][0]}}}"
+
+        return function_string
+
+    def to_latex_string(self, parameter):
+        """
+        Return a math string (using latex encoding) representation of the function.
+        """
+        function_string = "(" + self.segments[0].to_latex_string(parameter).replace("$", "")
+        function_string += f" for {parameter}<={self.intervals[0][1]}\n"
+        function_string += self.segments[1].to_latex_string(*parameter).replace("$", "")
+        function_string += f" for {parameter}>={self.intervals[1][0]})"
+        return function_string
 
 
 class MultiParameterTerm(Term):
@@ -191,15 +278,23 @@ def evaluate(self, parameter_values: Union[Tuple[float], Coordinate]):
             function_value *= term.evaluate(parameter_value)
         return function_value
 
-    def to_string(self, *parameters: Union[Parameter, str, Mapping[int, Union[Parameter, str]]]):
+    def to_string(self, *parameters: Union[Parameter, str, Mapping[int, Union[Parameter, str]]],
+                  format: FunctionFormats = None):
         if len(parameters) == 0:
             parameters = DEFAULT_PARAM_NAMES
         elif len(parameters) == 1 and not isinstance(parameters[0], str):
             parameters = parameters[0]
-        function_string = str(self.coefficient)
-        for param, term in self.parameter_term_pairs:
-            function_string += ' * '
-            function_string += term.to_string(parameters[param])
+
+        term_list = (term.to_string(parameters[param], format=format) for param, term in self.parameter_term_pairs)
+        if self.coefficient != 1 or not self.parameter_term_pairs:
+            term_list = chain([str(self.coefficient)], term_list)
+
+        joiner = ' * '
+        if format == FunctionFormats.PYTHON:
+            joiner = '*'
+        if format == FunctionFormats.LATEX:
+            joiner = '\\cdot '
+        function_string = joiner.join(term_list)
         return function_string
 
     def __imul__(self, parameter_term_pair: Tuple[int, SingleParameterTerm]):
@@ -215,8 +310,8 @@ def __eq__(self, other):
         elif self is other:
             return True
         else:
-            return self.parameter_term_pairs == other.parameter_term_pairs and \
-                   self.coefficient == other.coefficient
+            return (self.parameter_term_pairs == other.parameter_term_pairs and
+                    self.coefficient == other.coefficient)
 
 
 class TermSchema(Schema):
@@ -239,6 +334,15 @@ def create_object(self):
         return CompoundTerm()
 
 
+class SegmentedTermSchema(TermSchema):
+    simple_terms = fields.Constant([], load_only=True)
+    segments = fields.List(fields.Nested('SingleParameterFunctionSchema'))
+    intervals = NumpyField()
+
+    def create_object(self):
+        return SegmentedTerm([], [])
+
+
 class ListOfPairs(fields.Dict):
     def _serialize(self, value, attr, obj, **kwargs):
         return super(ListOfPairs, self)._serialize(dict(value), attr, obj, **kwargs)
@@ -249,7 +353,8 @@ def _deserialize(self, value, attr, data, **kwargs):
 
 
 class MultiParameterTermSchema(TermSchema):
-    parameter_term_pairs = ListOfPairs(keys=fields.Int(), values=fields.Nested(CompoundTermSchema))
+    parameter_term_pairs = ListOfPairs(keys=fields.Int(),
+                                       values=VariantSchemaField(CompoundTermSchema, SegmentedTermSchema))
 
     def create_object(self):
         return MultiParameterTerm()
diff --git a/extrap/extrap/extrapcmd.py b/extrap/extrap/extrapcmd.py
index fb77368d..b1799773 100644
--- a/extrap/extrap/extrapcmd.py
+++ b/extrap/extrap/extrapcmd.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -13,10 +13,12 @@
 from itertools import chain
 
 import extrap
+from extrap.entities.scaling_type import ScalingType
 from extrap.fileio import experiment_io
 from extrap.fileio.experiment_io import ExperimentReader
 from extrap.fileio.file_reader import all_readers
-from extrap.fileio.file_reader.cube_file_reader2 import CubeFileReader2
+from extrap.fileio.file_reader.abstract_directory_reader import AbstractScalingConversionReader
+from extrap.fileio.file_reader.file_reader_mixin import TKeepValuesReader
 from extrap.fileio.io_helper import save_output
 from extrap.fileio.output import format_output
 from extrap.modelers import multi_parameter
@@ -33,8 +35,7 @@ def main(args=None, prog=None):
     # argparse
     modelers_list = list(set(k.lower() for k in
                              chain(single_parameter.all_modelers.keys(), multi_parameter.all_modelers.keys())))
-    parser = argparse.ArgumentParser(prog=prog, description=extrap.__description__, add_help=False,
-                                     formatter_class=WideHelpFormatter)
+    parser = argparse.ArgumentParser(prog=prog, description=extrap.__description__, add_help=False)
     positional_arguments = parser.add_argument_group("Positional arguments")
     basic_arguments = parser.add_argument_group("Optional arguments")
     basic_arguments.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
@@ -44,7 +45,7 @@ def main(args=None, prog=None):
                                  help="Show program's version number and exit")
     basic_arguments.add_argument("--log", action="store", dest="log_level", type=str.lower, default='warning',
                                  choices=['debug', 'info', 'warning', 'error', 'critical'],
-                                 help="Set program's log level (default: warning)")
+                                 help="Set program's log level (default: %(default)s)")
 
     input_options = parser.add_argument_group("Input options")
     group = input_options.add_mutually_exclusive_group(required=True)
@@ -53,16 +54,24 @@ def main(args=None, prog=None):
                            help=reader.DESCRIPTION)
     group.add_argument(ExperimentReader.CMD_ARGUMENT, action="store_true", default=False, dest=ExperimentReader.NAME,
                        help='Load Extra-P experiment and generate new models')
-    input_options.add_argument("--scaling", action="store", dest="scaling_type", default="weak", type=str.lower,
-                               choices=["weak", "strong"],
-                               help="Set weak or strong scaling when loading data from CUBE files (default: weak)")
+
+    names_of_scaling_conversion_readers = ", ".join(reader.NAME + " files" for reader in all_readers.values()
+                                                    if issubclass(reader, AbstractScalingConversionReader))
+
+    input_options.add_argument("--scaling", action="store", dest="scaling_type", default=ScalingType.WEAK,
+                               type=ScalingType, choices=ScalingType,
+                               help="Set scaling type when loading data from per-thread/per-rank files (" +
+                                    names_of_scaling_conversion_readers + ") (default: %(default)s)")
+    input_options.add_argument("--keep-values", action="store_true", default=False, dest='keep_values',
+                               help="Keeps the original values after import")
 
     modeling_options = parser.add_argument_group("Modeling options")
     modeling_options.add_argument("--median", action="store_true", dest="median",
                                   help="Use median values for computation instead of mean values")
     modeling_options.add_argument("--modeler", action="store", dest="modeler", default='default', type=str.lower,
                                   choices=modelers_list,
-                                  help="Selects the modeler for generating the performance models")
+                                  help="Selects the modeler for generating the performance models "
+                                       "(default: %(default)s)")
     modeling_options.add_argument("--options", dest="modeler_options", default={}, nargs='+', metavar="KEY=VALUE",
                                   action=ModelerOptionsAction,
                                   help="Options for the selected modeler")
@@ -74,18 +83,21 @@ def main(args=None, prog=None):
     output_options.add_argument("--out", action="store", metavar="OUTPUT_PATH", dest="out",
                                 help="Specify the output path for Extra-P results")
     output_options.add_argument("--print", action="store", dest="print_type", default="all",
-                                metavar='{all,callpaths,metrics,parameters,functions,FORMAT_STRING}',
+                                metavar='{all,all-python,all-latex,callpaths,metrics,parameters,functions,'
+                                        'functions-python,functions-latex,FORMAT_STRING}',
                                 help="Set which information should be displayed after modeling. Use one of "
-                                     "{all, callpaths, metrics, parameters, functions} or specify a "
-                                     "formatting string using placeholders "
-                                     f"(see {extrap.__documentation_link__}/output-formatting.md).")
+                                     "the presets or specify a formatting string using placeholders "
+                                     f"(see {extrap.__documentation_link__}/output-formatting.md). "
+                                     f"(default: %(default)s)")
     output_options.add_argument("--save-experiment", action="store", metavar="EXPERIMENT_PATH", dest="save_experiment",
                                 help="Saves the experiment including all models as Extra-P experiment "
                                      "(if no extension is specified, '.extra-p' is appended)")
     output_options.add_argument("--model-set-name", action="store", metavar="NAME", default='New model',
                                 dest="model_name", type=str,
-                                help="Sets the name of the generated set of models when outputting an "
-                                     "experiment (default: 'New model')")
+                                help="Sets the name of the generated set of models when outputting an experiment "
+                                     '(default: "%(default)s")')
+    output_options.add_argument("--disable-progress", action="store_true", dest="disable_progress", default=False,
+                                help="Disables the progress bar outputs of Extra-P to stdout and stderr")
 
     positional_arguments.add_argument("path", metavar="FILEPATH", type=str, action="store",
                                       help="Specify a file path for Extra-P to work with")
@@ -128,14 +140,25 @@ def main(args=None, prog=None):
         print_output = False
 
     if arguments.path is not None:
-        with ProgressBar(desc='Loading file') as pbar:
+        with ProgressBar(desc='Loading file', disable=arguments.disable_progress) as pbar:
             for reader in chain(all_readers.values(), [ExperimentReader]):
                 if getattr(arguments, reader.NAME):
                     file_reader = reader()
+
+                    if issubclass(reader, AbstractScalingConversionReader):
+                        file_reader.scaling_type = arguments.scaling_type
+                    elif arguments.scaling_type != ScalingType.WEAK:
+                        warnings.warn(
+                            f"Scaling type {arguments.scaling_type} is not supported by the {reader.NAME} reader.")
+
+                    if isinstance(file_reader, TKeepValuesReader):
+                        file_reader.keep_values = arguments.keep_values
+                    elif arguments.keep_values:
+                        warnings.warn(
+                            f"Keeping values is not supported by the {reader.NAME} reader.")
+
                     if reader.LOADS_FROM_DIRECTORY:
                         if os.path.isdir(arguments.path):
-                            if reader is CubeFileReader2:
-                                file_reader.scaling_type = arguments.scaling_type
                             experiment = file_reader.read_experiment(arguments.path, pbar)
                         else:
                             logging.error("The given path is not valid. It must point to a directory.")
@@ -169,13 +192,13 @@ def main(args=None, prog=None):
             if value is not None:
                 setattr(modeler, name, value)
 
-        with ProgressBar(desc='Generating models') as pbar:
+        with ProgressBar(desc='Generating models', disable=arguments.disable_progress) as pbar:
             # create models from data
             model_generator.model_all(pbar)
 
         if arguments.save_experiment:
             try:
-                with ProgressBar(desc='Saving experiment') as pbar:
+                with ProgressBar(desc='Saving experiment', disable=arguments.disable_progress) as pbar:
                     if not os.path.splitext(arguments.save_experiment)[1]:
                         arguments.save_experiment += '.extra-p'
                     experiment_io.write_experiment(experiment, arguments.save_experiment, pbar)
@@ -198,17 +221,5 @@ def main(args=None, prog=None):
         sys.exit(1)
 
 
-class WideHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
-
-    def __init__(self, *args, **kwargs):
-        try:
-            kwargs['width'] = 100
-            super().__init__(*args, **kwargs)
-        except TypeError:
-            warnings.warn("Wide argparse help formatter failed, falling back.")
-            del kwargs['width']
-            super().__init__(*args, **kwargs)
-
-
 if __name__ == "__main__":
     main()
diff --git a/extrap/extrap/extrapgui.py b/extrap/extrap/extrapgui.py
index 38ca55b3..c0ac426a 100644
--- a/extrap/extrap/extrapgui.py
+++ b/extrap/extrap/extrapgui.py
@@ -4,6 +4,7 @@
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
+from __future__ import annotations
 
 import argparse
 import logging
@@ -18,9 +19,10 @@
 from matplotlib import font_manager
 
 import extrap
+from extrap.entities.scaling_type import ScalingType
 from extrap.fileio.experiment_io import read_experiment
 from extrap.fileio.file_reader import all_readers
-from extrap.fileio.file_reader.cube_file_reader2 import CubeFileReader2
+from extrap.fileio.file_reader.abstract_directory_reader import AbstractScalingConversionReader
 from extrap.gui.MainWidget import MainWidget
 from extrap.util.exceptions import RecoverableError, CancelProcessError
 
@@ -68,13 +70,16 @@ def main(*, args=None, test=False):
 
 
 def parse_arguments(args=None):
-    parser = argparse.ArgumentParser(description=extrap.__description__)
+    parser = argparse.ArgumentParser(description=extrap.__description__, add_help=False)
+    parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
+                        help='Show this help message and exit')
     parser.add_argument("--log", action="store", dest="log_level", type=str.lower, default='critical',
                         choices=['traceback', 'debug', 'info', 'warning', 'error', 'critical'],
-                        help="set program's log level (default: critical)")
+                        help="Set program's log level (default: %(default)s)")
     parser.add_argument("--logfile", action="store", dest="log_file",
-                        help="set path of log file")
-    parser.add_argument("--version", action="version", version=extrap.__title__ + " " + extrap.__version__)
+                        help="Set path of log file")
+    parser.add_argument("--version", action="version", version=extrap.__title__ + " " + extrap.__version__,
+                        help="Show program's version number and exit")
 
     group = parser.add_mutually_exclusive_group(required=False)
     for reader in all_readers.values():
@@ -82,11 +87,15 @@ def parse_arguments(args=None):
                            help=reader.DESCRIPTION)
 
     parser.add_argument("path", metavar="FILEPATH", type=str, action="store", nargs='?',
-                        help="specify a file path for Extra-P to work with")
+                        help="Specify a file path for Extra-P to work with")
 
-    parser.add_argument("--scaling", action="store", dest="scaling_type", default="weak", type=str.lower,
-                        choices=["weak", "strong"],
-                        help="set weak or strong scaling when loading data from cube files [weak (default), strong]")
+    names_of_scaling_conversion_readers = ", ".join(reader.NAME + " files" for reader in all_readers.values()
+                                                    if issubclass(reader, AbstractScalingConversionReader))
+
+    parser.add_argument("--scaling", action="store", dest="scaling_type", default=ScalingType.WEAK,
+                        type=ScalingType, choices=ScalingType,
+                        help="Set scaling type when loading data from per-thread/per-rank files (" +
+                             names_of_scaling_conversion_readers + ") (default: %(default)s)")
     arguments = parser.parse_args(args)
     return arguments
 
@@ -96,10 +105,13 @@ def load_from_command(arguments, window):
         for reader in all_readers.values():
             if getattr(arguments, reader.NAME):
                 file_reader = reader()
-                if file_reader is CubeFileReader2:
+                if issubclass(reader, AbstractScalingConversionReader):
                     file_reader.scaling_type = arguments.scaling_type
+                elif arguments.scaling_type != ScalingType.WEAK:
+                    warnings.warn(
+                        f"Scaling type {arguments.scaling_type} is not supported by the {reader.NAME} reader.")
                 window.import_file(file_reader.read_experiment, file_name=arguments.path,
-                                   model=file_reader.GENERATE_MODELS_AFTER_LOAD)
+                                   model=file_reader.GENERATE_MODELS_AFTER_LOAD, reader=file_reader)
                 return
         window.import_file(read_experiment, file_name=arguments.path, model=False)
 
diff --git a/extrap/fileio/experiment_io.py b/extrap/fileio/experiment_io.py
index b7194734..67e28a95 100644
--- a/extrap/fileio/experiment_io.py
+++ b/extrap/fileio/experiment_io.py
@@ -14,6 +14,7 @@
 
 from extrap.entities.experiment import ExperimentSchema, Experiment
 from extrap.fileio.file_reader import FileReader
+from extrap.fileio.values_io import ValueWriter, ValueReader
 from extrap.util.exceptions import FileFormatError, RecoverableError
 from extrap.util.progress_bar import DUMMY_PROGRESS, ProgressBar
 
@@ -30,6 +31,8 @@ def read_experiment(path, progress_bar=DUMMY_PROGRESS) -> Experiment:
             data = file.read(EXPERIMENT_DATA_FILE).decode("utf-8")
             progress_bar.update()
             try:
+                value_reader = ValueReader(file)
+                schema.set_value_io(value_reader)
                 experiment = schema.loads(data)
                 progress_bar.update()
                 return experiment
@@ -60,10 +63,13 @@ def write_experiment(experiment, path, progress_bar=DUMMY_PROGRESS):
     progress_bar.update()
     try:
         with ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=1, allowZip64=True) as file:
+            value_writer = ValueWriter(file)
+            schema.set_value_io(value_writer)
             progress_bar.update()
             try:
                 data = schema.dumps(experiment)
                 progress_bar.update()
+                value_writer.flush()
                 file.writestr(EXPERIMENT_DATA_FILE, data)
                 progress_bar.update()
             except ValidationError as v_err:
diff --git a/extrap/fileio/file_reader/abstract_directory_reader.py b/extrap/fileio/file_reader/abstract_directory_reader.py
index 70471ef4..b0b327f4 100644
--- a/extrap/fileio/file_reader/abstract_directory_reader.py
+++ b/extrap/fileio/file_reader/abstract_directory_reader.py
@@ -12,7 +12,9 @@
 from collections import defaultdict
 from pathlib import Path
 
+from extrap.entities.scaling_type import ScalingType
 from extrap.fileio.file_reader import FileReader
+from extrap.util.dynamic_options import DynamicOptions
 from extrap.util.exceptions import FileFormatError
 from extrap.util.progress_bar import DUMMY_PROGRESS
 
@@ -65,7 +67,7 @@ def _determine_parameters_from_paths(paths, progress_bar=DUMMY_PROGRESS):
                 parameter_dict[n].add(v)
             parameter_values.append(parameter_value)
 
-            if len(parameter_names)>=1 and parameter_names[0]=="scorep-":
+            if len(parameter_names) >= 1 and parameter_names[0] == "scorep-":
                 warnings.warn(
                     f"Could not detect any parameter names in the name of folder: {folder_name}. "
                     f"Please follow the usage guide under "
@@ -83,3 +85,8 @@ def _determine_parameters_from_paths(paths, progress_bar=DUMMY_PROGRESS):
         parameter_names = [p for p in parameter_names if len(parameter_dict[p]) > 1]
 
         return parameter_names, parameter_values
+
+
+class AbstractScalingConversionReader(FileReader, DynamicOptions, abc.ABC):
+    scaling_type: ScalingType = DynamicOptions.add(ScalingType.WEAK, ScalingType,
+                                                   range={i.name.lower(): i.value for i in ScalingType})
diff --git a/extrap/fileio/file_reader/cube_file_reader2.py b/extrap/fileio/file_reader/cube_file_reader2.py
index eba67b8c..729aaf3a 100644
--- a/extrap/fileio/file_reader/cube_file_reader2.py
+++ b/extrap/fileio/file_reader/cube_file_reader2.py
@@ -5,6 +5,8 @@
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
+from __future__ import annotations
+
 import logging
 import warnings
 from collections import defaultdict
@@ -13,11 +15,9 @@
 from operator import itemgetter
 from pathlib import Path
 from typing import Dict, Union, Sequence, Tuple, Optional
-import pkg_resources
 
 import numpy
-from pycubexr import CubexParser
-from pycubexr.utils.exceptions import MissingMetricError
+import pkg_resources
 from packaging.version import Version
 
 from extrap.entities.callpath import Callpath
@@ -26,9 +26,14 @@
 from extrap.entities.measurement import Measurement
 from extrap.entities.metric import Metric
 from extrap.entities.parameter import Parameter
+from extrap.entities.scaling_type import ScalingType
 from extrap.fileio import io_helper
-from extrap.fileio.file_reader.abstract_directory_reader import AbstractDirectoryReader
+from extrap.fileio.file_reader.abstract_directory_reader import AbstractDirectoryReader, \
+    AbstractScalingConversionReader
+from extrap.fileio.file_reader.file_reader_mixin import TKeepValuesReader
 from extrap.util.progress_bar import DUMMY_PROGRESS, ProgressBar
+from pycubexr import CubexParser
+from pycubexr.utils.exceptions import MissingMetricError
 
 
 @dataclass
@@ -38,14 +43,13 @@ class SmallKernelFilter:
     callpath: Optional[Callpath] = None
 
 
-class CubeFileReader2(AbstractDirectoryReader):
+class CubeFileReader2(AbstractDirectoryReader, AbstractScalingConversionReader, TKeepValuesReader):
     NAME = "cube"
     GUI_ACTION = "Open set of &CUBE files"
     DESCRIPTION = "Load a set of CUBE files and generate a new experiment"
     CMD_ARGUMENT = "--cube"
     LOADS_FROM_DIRECTORY = True
 
-    scaling_type = "weak"
     selected_metrics = None
     use_inclusive_measurements = False
     small_kernel_filter: SmallKernelFilter = None
@@ -59,7 +63,11 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
         # iterate over all folders and read the cube profiles in them
         experiment = Experiment()
         # set scaling flag for experiment
-        if self.scaling_type == "weak" or self.scaling_type == "strong":
+
+        if isinstance(self.scaling_type, str):
+            self.scaling_type = ScalingType(self.scaling_type)
+
+        if self.scaling_type in ScalingType:
             experiment.scaling = self.scaling_type
 
         progress_bar.step("Reading cube files")
@@ -69,13 +77,6 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
         for p in parameter_names:
             experiment.add_parameter(Parameter(p))
 
-        # check number of parameters, if > 1 use weak scaling instead
-        # since sum values for strong scaling does not work for more than 1 parameter
-        if self.scaling_type == 'strong' and len(experiment.parameters) > 1:
-            warnings.warn("Strong scaling only works for one parameter. Using weak scaling instead.")
-            scaling_type = 'weak'
-            experiment.scaling = scaling_type
-
         progress_bar.step("Reading cube files")
 
         show_warning_skipped_metrics = set()
@@ -99,7 +100,8 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
             # add measurements to experiment
             for (callpath, metric), values in aggregated_values.items():
                 progress_bar.update(0)
-                experiment.add_measurement(Measurement(coordinate, callpath, metric, values))
+                experiment.add_measurement(
+                    Measurement(coordinate, callpath, metric, values, keep_values=self.keep_values))
 
         progress_bar.step("Unify calltrees")
         callpaths_to_merge = self._determine_and_add_common_callpaths(experiment, num_points, progress_bar)
@@ -148,9 +150,14 @@ def _aggregate_repetitions_legacy(self, point_group, progress_bar, show_warning_
                                         and self.small_kernel_filter.callpath != callpaths[r_cnode.id]:
                                     continue
                                 cnode_values = metric_values.cnode_values(r_cnode, convert_to_inclusive=True)
-                                if self.scaling_type == "weak":
+                                if self.scaling_type == ScalingType.WEAK:
                                     total_values[callpaths[r_cnode.id]].extend(map(float, cnode_values))
-                                elif self.scaling_type == "strong":
+                                elif self.scaling_type == ScalingType.WEAK_PARALLEL:
+                                    values = [v for v in map(float, cnode_values) if v != 0]
+                                    if not values:
+                                        values = map(float, cnode_values)
+                                    total_values[callpaths[r_cnode.id]].extend(values)
+                                elif self.scaling_type == ScalingType.STRONG:
                                     total_values[callpaths[r_cnode.id]].append(float(sum(cnode_values)))
 
                         for cnode_id in metric_values.cnode_indices:
@@ -164,12 +171,16 @@ def _aggregate_repetitions_legacy(self, point_group, progress_bar, show_warning_
                                                                       convert_to_inclusive=self.use_inclusive_measurements)
 
                             # in case of weak scaling calculate mean and median over all mpi process values
-                            if self.scaling_type == "weak":
+                            if self.scaling_type == ScalingType.WEAK:
                                 # do NOT use generator it is slower
                                 aggregated_values[(callpath, metric)].extend(map(float, cnode_values))
-
+                            elif self.scaling_type == ScalingType.WEAK_PARALLEL:
+                                values = [v for v in map(float, cnode_values) if v != 0]
+                                if not values:
+                                    values = map(float, cnode_values)
+                                aggregated_values[(callpath, metric)].extend(values)
                                 # in case of strong scaling calculate the sum over all mpi process values
-                            elif self.scaling_type == "strong":
+                            elif self.scaling_type == ScalingType.STRONG:
                                 aggregated_values[(callpath, metric)].append(float(sum(cnode_values)))
 
                     # Take care of missing metrics
@@ -204,9 +215,15 @@ def _aggregate_repetitions(self, point_group, progress_bar, show_warning_skipped
                                         and self.small_kernel_filter.callpath != callpaths[r_cnode.id]:
                                     continue
                                 cnode_values = metric_values.cnode_values(r_cnode, convert_to_inclusive=True)
-                                if self.scaling_type == "weak":
+                                if self.scaling_type == ScalingType.WEAK:
                                     total_values[callpaths[r_cnode.id]].extend(cnode_values.astype(float))
-                                elif self.scaling_type == "strong":
+                                elif self.scaling_type == ScalingType.WEAK_PARALLEL:
+                                    values = cnode_values.astype(float)
+                                    non_zero_value_mask = values != 0
+                                    if numpy.any(non_zero_value_mask):
+                                        values = values[non_zero_value_mask]
+                                    total_values[callpaths[r_cnode.id]].extend(values)
+                                elif self.scaling_type == ScalingType.STRONG:
                                     total_values[callpaths[r_cnode.id]].append(cnode_values.sum().astype(float))
 
                         for cnode_id in metric_values.cnode_indices:
@@ -220,11 +237,16 @@ def _aggregate_repetitions(self, point_group, progress_bar, show_warning_skipped
                                                                       convert_to_inclusive=self.use_inclusive_measurements)
 
                             # in case of weak scaling calculate mean and median over all mpi process values
-                            if self.scaling_type == "weak":
+                            if self.scaling_type == ScalingType.WEAK:
                                 aggregated_values[(callpath, metric)].append(cnode_values.astype(float))
-
-                                # in case of strong scaling calculate the sum over all mpi process values
-                            elif self.scaling_type == "strong":
+                            elif self.scaling_type == ScalingType.WEAK_PARALLEL:
+                                values = cnode_values.astype(float)
+                                non_zero_value_mask = values != 0
+                                if numpy.any(non_zero_value_mask):
+                                    values = values[non_zero_value_mask]
+                                aggregated_values[(callpath, metric)].append(values)
+                            # in case of strong scaling calculate the sum over all mpi process values
+                            elif self.scaling_type == ScalingType.STRONG:
                                 aggregated_values[(callpath, metric)].append(cnode_values.sum().astype(float))
 
                     # Take care of missing metrics
@@ -234,8 +256,7 @@ def _aggregate_repetitions(self, point_group, progress_bar, show_warning_skipped
                             f'The cubex file {Path(*path.parts[-2:])} does not contain data for the metric "{e.metric.name}"')
         return aggregated_values, total_values
 
-    @staticmethod
-    def _make_callpath_mapping(cnodes):
+    def _make_callpath_mapping(self, cnodes):
         callpaths = {}
 
         def walk_tree(parent_cnode, parent_name):
@@ -308,8 +329,12 @@ def _delete_callpaths_from_experiment(experiment, callpaths_to_merge, progress_b
             # delete current measurements
             del experiment.measurements[key]
 
-    def read_cube_file(self, dir_name, scaling_type, pbar=DUMMY_PROGRESS, selected_metrics=None):
-        self.scaling_type = scaling_type
+    def read_cube_file(self, dir_name, scaling_type: Union[str, ScalingType], pbar=DUMMY_PROGRESS,
+                       selected_metrics=None):
+        if isinstance(scaling_type, str):
+            self.scaling_type = ScalingType(scaling_type)
+        else:
+            self.scaling_type = scaling_type
         self.selected_metrics = selected_metrics
         return self.read_experiment(dir_name, pbar)
 
diff --git a/extrap/fileio/file_reader/file_reader_mixin.py b/extrap/fileio/file_reader/file_reader_mixin.py
new file mode 100644
index 00000000..7a36ddc6
--- /dev/null
+++ b/extrap/fileio/file_reader/file_reader_mixin.py
@@ -0,0 +1,13 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from extrap.entities.scaling_type import ScalingType
+from extrap.util.dynamic_options import DynamicOptions
+
+
+class TKeepValuesReader(DynamicOptions):
+    keep_values: ScalingType = DynamicOptions.add(False, bool)
diff --git a/extrap/fileio/file_reader/json_file_reader.py b/extrap/fileio/file_reader/json_file_reader.py
index b000f9d6..e985c8c8 100644
--- a/extrap/fileio/file_reader/json_file_reader.py
+++ b/extrap/fileio/file_reader/json_file_reader.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -19,6 +19,7 @@
 from extrap.entities.parameter import Parameter
 from extrap.fileio import io_helper
 from extrap.fileio.file_reader import FileReader
+from extrap.fileio.file_reader.file_reader_mixin import TKeepValuesReader
 from extrap.fileio.file_reader.jsonlines_file_reader import read_jsonlines_file
 from extrap.fileio.io_helper import create_call_tree
 from extrap.util.exceptions import FileFormatError
@@ -27,7 +28,7 @@
 SCHEMA_URI = ""
 
 
-class JsonFileReader(FileReader):
+class JsonFileReader(FileReader, TKeepValuesReader):
     NAME = "json"
     GUI_ACTION = "Open &JSON input"
     DESCRIPTION = "Load data from JSON or JSON Lines input file"
@@ -44,7 +45,7 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
                 is_jsonlines = any(line.strip().startswith('{') for line in inputfile) and \
                                all(line.strip().startswith('{') or line.strip() == "" for line in inputfile)
                 if is_jsonlines:
-                    return read_jsonlines_file(path, progress_bar=DUMMY_PROGRESS)
+                    return read_jsonlines_file(path, progress_bar=DUMMY_PROGRESS, keep_values=self.keep_values)
                 else:
                     raise FileFormatError(str(error)) from error
 
@@ -53,12 +54,12 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
 
         if "callpaths" not in json_data:
             try:
-                self._read_new_json_file(experiment, json_data, progress_bar)
+                self._read_new_json_file(experiment, json_data, progress_bar, self.keep_values)
             except KeyError as err:
                 raise FileFormatError(str(err)) from err
         else:
             try:
-                self._read_legacy_json_file(experiment, json_data, progress_bar)
+                self._read_legacy_json_file(experiment, json_data, progress_bar, self.keep_values)
             except KeyError as err:
                 raise FileFormatError(str(err)) from err
 
@@ -70,7 +71,7 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
         return experiment
 
     @staticmethod
-    def _read_new_json_file(experiment, json_data, progress_bar):
+    def _read_new_json_file(experiment, json_data, progress_bar, keep_values):
         parameter_data = json_data["parameters"]
         for p in parameter_data:
             parameter = Parameter(p)
@@ -86,11 +87,12 @@ def _read_new_json_file(experiment, json_data, progress_bar):
                     experiment.add_callpath(callpath)
                     metric = Metric(metric_name)
                     experiment.add_metric(metric)
-                    measurement = Measurement(coordinate, callpath, metric, measurement['values'])
+                    measurement = Measurement(coordinate, callpath, metric, measurement['values'],
+                                              keep_values=keep_values)
                     experiment.add_measurement(measurement)
 
     @staticmethod
-    def _read_legacy_json_file(experiment, json_data, progress_bar):
+    def _read_legacy_json_file(experiment, json_data, progress_bar, keep_values):
         # read parameters
         parameter_data = json_data["parameters"]
         parameter_data = sorted(parameter_data, key=lambda x: x["id"])
@@ -148,5 +150,5 @@ def _read_legacy_json_file(experiment, json_data, progress_bar):
             callpath = experiment.callpaths[callpath_id]
             metric = experiment.metrics[metric_id]
             values = aggregate_data[key]
-            measurement = Measurement(coordinate, callpath, metric, values)
+            measurement = Measurement(coordinate, callpath, metric, values, keep_values=keep_values)
             experiment.add_measurement(measurement)
diff --git a/extrap/fileio/file_reader/jsonlines_file_reader.py b/extrap/fileio/file_reader/jsonlines_file_reader.py
index 09e04305..80568118 100644
--- a/extrap/fileio/file_reader/jsonlines_file_reader.py
+++ b/extrap/fileio/file_reader/jsonlines_file_reader.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -20,7 +20,7 @@
 from extrap.util.progress_bar import DUMMY_PROGRESS
 
 
-def read_jsonlines_file(path, progress_bar=DUMMY_PROGRESS):
+def read_jsonlines_file(path, progress_bar=DUMMY_PROGRESS, keep_values=False):
     # create an experiment object to save the date loaded from the text file
     experiment = Experiment()
 
diff --git a/extrap/fileio/file_reader/talpas_file_reader.py b/extrap/fileio/file_reader/talpas_file_reader.py
index a38783c3..d5c92bba 100644
--- a/extrap/fileio/file_reader/talpas_file_reader.py
+++ b/extrap/fileio/file_reader/talpas_file_reader.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -18,12 +18,13 @@
 from extrap.entities.parameter import Parameter
 from extrap.fileio import io_helper
 from extrap.fileio.file_reader import FileReader
+from extrap.fileio.file_reader.file_reader_mixin import TKeepValuesReader
 from extrap.fileio.io_helper import create_call_tree
 from extrap.util.exceptions import FileFormatError
 from extrap.util.progress_bar import DUMMY_PROGRESS, ProgressBar
 
 
-class TalpasFileReader(FileReader):
+class TalpasFileReader(FileReader, TKeepValuesReader):
     NAME = "talpas"
     GUI_ACTION = "Open Tal&pas input"
     DESCRIPTION = "Load data from Talpas data format"
@@ -63,7 +64,7 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
                     raise FileFormatError(f'Missing property in line {ln}: {str(error)}. Line: "{line}"')
 
         # create experiment
-        io_helper.repetition_dict_to_experiment(complete_data, experiment, progress_bar)
+        io_helper.repetition_dict_to_experiment(complete_data, experiment, progress_bar, keep_values=self.keep_values)
 
         for p in parameters:
             experiment.add_parameter(p)
diff --git a/extrap/fileio/file_reader/text_file_reader.py b/extrap/fileio/file_reader/text_file_reader.py
index 25838cce..af62e79a 100644
--- a/extrap/fileio/file_reader/text_file_reader.py
+++ b/extrap/fileio/file_reader/text_file_reader.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -17,6 +17,7 @@
 from extrap.entities.parameter import Parameter
 from extrap.fileio import io_helper
 from extrap.fileio.file_reader import FileReader
+from extrap.fileio.file_reader.file_reader_mixin import TKeepValuesReader
 from extrap.fileio.io_helper import create_call_tree
 from extrap.util.exceptions import FileFormatError
 from extrap.util.progress_bar import DUMMY_PROGRESS, ProgressBar
@@ -24,7 +25,7 @@
 re_whitespace = re.compile(r'\s+')
 
 
-class TextFileReader(FileReader):
+class TextFileReader(FileReader, TKeepValuesReader):
     NAME = "text"
     GUI_ACTION = "Open &text input"
     DESCRIPTION = "Load data from text input file"
@@ -105,8 +106,8 @@ def read_experiment(self, path: Union[Path, str], progress_bar: ProgressBar = DU
                         raise FileFormatError(
                             f'To many DATA lines ({coordinate_id}) for the number of POINTS '
                             f'({len(experiment.coordinates)}) in line {i}.')
-                    measurement = Measurement(
-                        experiment.coordinates[coordinate_id], last_callpath, last_metric, values)
+                    measurement = Measurement(experiment.coordinates[coordinate_id], last_callpath, last_metric, values,
+                                              keep_values=self.keep_values)
                     experiment.add_measurement(measurement)
                     coordinate_id += 1
                 elif number_parameters >= 5:
diff --git a/extrap/fileio/io_helper.py b/extrap/fileio/io_helper.py
index 17223706..52361748 100644
--- a/extrap/fileio/io_helper.py
+++ b/extrap/fileio/io_helper.py
@@ -1,21 +1,28 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
 from __future__ import annotations
 
+import logging
 import warnings
-from typing import TYPE_CHECKING, List
+from dataclasses import dataclass, field
+from numbers import Number
+from typing import TYPE_CHECKING, List, Sequence, Tuple
 
 from extrap.entities.callpath import Callpath
 from extrap.entities.calltree import CallTree
 from extrap.entities.calltree import Node
+from extrap.entities.coordinate import Coordinate
 from extrap.entities.measurement import Measurement
+from extrap.entities.metric import Metric
+from extrap.entities.model import SegmentedModel
 from extrap.util.exceptions import InvalidExperimentError
 from extrap.util.progress_bar import DUMMY_PROGRESS
+from extrap.util.string_formats import FunctionFormats
 
 if TYPE_CHECKING:
     from extrap.entities.experiment import Experiment
@@ -60,7 +67,7 @@ def format_parameters(experiment):
     return text
 
 
-def format_functions(experiment):
+def format_functions(experiment, format: FunctionFormats = None):
     """
     This method formats the output so that only the functions are shown.
     """
@@ -68,14 +75,21 @@ def format_functions(experiment):
     models = modeler.models
     text = ""
     for model in models.values():
-        hypothesis = model.hypothesis
-        function = hypothesis.function
-        function_string = function.to_string(*experiment.parameters)
-        text += function_string + "\n"
+        if isinstance(model, SegmentedModel):
+            counter = 1
+            for m in model.segment_models:
+                function_string = m.hypothesis.function.to_string(*experiment.parameters, format=format)
+                text += "Model " + str(counter) + ": " + function_string + "\n"
+                counter += 1
+        else:
+            hypothesis = model.hypothesis
+            function = hypothesis.function
+            function_string = function.to_string(*experiment.parameters, format=format)
+            text += function_string + "\n"
     return text
 
 
-def format_all(experiment):
+def format_all(experiment, format: FunctionFormats = None):
     """
     This method formats the output so that all information is shown.
     """
@@ -103,30 +117,65 @@ def format_all(experiment):
                 coordinate_text = coordinate_text[:-1]
                 coordinate_text += ")"
                 measurement = experiment.get_measurement(coordinate_id, callpath_id, metric_id)
-                if measurement == None:
+                if measurement is None:
                     value_mean = 0
                     value_median = 0
                 else:
                     value_mean = measurement.mean
                     value_median = measurement.median
                 text += f"\t\t{coordinate_text} Mean: {value_mean:.2E} Median: {value_median:.2E}\n"
-            try:
-                model = modeler.models[callpath, metric]
-            except KeyError as e:
-                model = None
-            if model != None:
-                hypothesis = model.hypothesis
-                function = hypothesis.function
-                rss = hypothesis.RSS
-                ar2 = hypothesis.AR2
-                function_string = function.to_string(*experiment.parameters)
+            model = modeler.models.get((callpath, metric))
+            if model is not None:
+                if isinstance(model, SegmentedModel):
+                    hypotheses = []
+                    function_strings = []
+                    rss_values = []
+                    ar2_values = []
+                    for m in model.segment_models:
+                        hypotheses.append(m.hypothesis)
+                        function_strings.append(
+                            m.hypothesis.function.to_string(*experiment.parameters, format=format))
+                        rss_values.append(m.hypothesis.RSS)
+                        ar2_values.append(m.hypothesis.AR2)
+
+                else:
+                    hypothesis = model.hypothesis
+                    function = hypothesis.function
+                    function_string = function.to_string(*experiment.parameters, format=format)
+                    rss = hypothesis.RSS
+                    ar2 = hypothesis.AR2
             else:
                 rss = 0
                 ar2 = 0
                 function_string = "None"
-            text += "\t\tModel: " + function_string + "\n"
-            text += "\t\tRSS: {:.2E}\n".format(rss)
-            text += "\t\tAdjusted R^2: {:.2E}\n".format(ar2)
+            if isinstance(model, SegmentedModel):
+                if len(model.changing_points) == 1:
+                    param_value = model.changing_points[0].coordinate[0]
+                    text += "\t\tModel 1: " + function_strings[0] + " for " + str(
+                        experiment.parameters[0]) + "<=" + str(param_value) + "\n"
+                    text += "\t\tModel 2: " + function_strings[1] + " for " + str(
+                        experiment.parameters[0]) + ">=" + str(param_value) + "\n"
+                    text += "\t\tRSS Model 1: {:.2E}\n".format(rss_values[0])
+                    text += "\t\tAdjusted R^2 Model 1: {:.2E}\n".format(ar2_values[0])
+                    text += "\t\tRSS Model 2: {:.2E}\n".format(rss_values[1])
+                    text += "\t\tAdjusted R^2 Model 2: {:.2E}\n".format(ar2_values[1])
+                elif len(model.changing_points) == 2:
+                    param_value_1 = model.changing_points[0].coordinate[0]
+                    param_value_2 = model.changing_points[1].coordinate[0]
+                    text += "\t\tModel 1: " + function_strings[0] + " for " + str(
+                        experiment.parameters[0]) + "<=" + str(param_value_1) + "\n"
+                    text += "\t\tModel 2: " + function_strings[1] + " for " + str(
+                        experiment.parameters[0]) + ">=" + str(param_value_2) + "\n"
+                    text += "\t\tRSS Model 1: {:.2E}\n".format(rss_values[0])
+                    text += "\t\tAdjusted R^2 Model 1: {:.2E}\n".format(ar2_values[0])
+                    text += "\t\tRSS Model 2: {:.2E}\n".format(rss_values[1])
+                    text += "\t\tAdjusted R^2 Model 2: {:.2E}\n".format(ar2_values[1])
+                else:
+                    raise NotImplementedError
+            else:
+                text += "\t\tModel: " + function_string + "\n"
+                text += "\t\tRSS: {:.2E}\n".format(rss)
+                text += "\t\tAdjusted R^2: {:.2E}\n".format(ar2)
     return text
 
 
@@ -135,8 +184,13 @@ def format_output(experiment, printtype):
     This method formats the output of the modeler to a string that can be printed in the console
     or to a file. Depending on the given options only parts of the modelers output get printed.
     """
+    printtype = printtype.upper()
     if printtype == "ALL":
         text = format_all(experiment)
+    elif printtype == "ALL-PYTHON":
+        text = format_all(experiment, FunctionFormats.PYTHON)
+    elif printtype == "ALL-LATEX":
+        text = format_all(experiment, FunctionFormats.LATEX)
     elif printtype == "CALLPATHS":
         text = format_callpaths(experiment)
     elif printtype == "METRICS":
@@ -145,8 +199,12 @@ def format_output(experiment, printtype):
         text = format_parameters(experiment)
     elif printtype == "FUNCTIONS":
         text = format_functions(experiment)
+    elif printtype == "FUNCTIONS-PYTHON":
+        text = format_functions(experiment, FunctionFormats.PYTHON)
+    elif printtype == "FUNCTIONS-LATEX":
+        text = format_functions(experiment, FunctionFormats.LATEX)
     else:
-        raise ValueError('printtype does not exist')
+        return None
     return text
 
 
@@ -154,7 +212,7 @@ def save_output(text, path):
     """
     This method saves the output of the modeler, i.e. it's results to a text file at the given path.
     """
-    with open(path, "w+") as out:
+    with open(path, "w+", encoding="utf-8") as out:
         out.write(text)
 
 
@@ -185,7 +243,7 @@ def append_to_repetition_dict(complete_data, key, coordinate, value, progress_ba
             progress_bar.total += 1
 
 
-def repetition_dict_to_experiment(complete_data, experiment, progress_bar=DUMMY_PROGRESS):
+def repetition_dict_to_experiment(complete_data, experiment, progress_bar=DUMMY_PROGRESS, keep_values=False):
     progress_bar.step('Creating experiment')
     for mi, key in enumerate(complete_data):
         progress_bar.update()
@@ -196,7 +254,7 @@ def repetition_dict_to_experiment(complete_data, experiment, progress_bar=DUMMY_
         for coordinate in measurementset:
             values = measurementset[coordinate]
             experiment.add_coordinate(coordinate)
-            experiment.add_measurement(Measurement(coordinate, callpath, metric, values))
+            experiment.add_measurement(Measurement(coordinate, callpath, metric, values, keep_values=keep_values))
 
 
 def create_call_tree(callpaths: List[Callpath], progress_bar=DUMMY_PROGRESS, progress_total_added=False,
@@ -210,7 +268,7 @@ def create_call_tree(callpaths: List[Callpath], progress_bar=DUMMY_PROGRESS, pro
     """
     tree = CallTree()
     progress_bar.step('Creating calltree')
-    # create a two dimensional array of the callpath elements as strings
+    # create a two-dimensional array of the callpath elements as strings
     callpaths2 = []
     max_length = 0
 
@@ -268,8 +326,8 @@ def create_call_tree(callpaths: List[Callpath], progress_bar=DUMMY_PROGRESS, pro
 
 def find_root_node(callpath_elements, tree, loop_id):
     """
-    This method finds the root node of a element in the callpath tree.
-    Therefore, it searches iterativels through the tree.
+    This method finds the root node of an element in the callpath tree.
+    Therefore, it searches iteratively through the tree.
     """
     level = 0
     root_element_string = callpath_elements[level]
@@ -332,3 +390,108 @@ def require(cond, message):
         require(len(m) == length_coordinates,
                 f'The number of measurements ({len(m)}) for {k} does not match the number of coordinates '
                 f'({length_coordinates}).')
+
+
+@dataclass
+class _StrongScalingCheckData:
+    measurements: dict[Coordinate, Number] = field(default_factory=dict)
+    dimension_coordinates: list[list[Coordinate]] = field(default_factory=list)
+
+
+def check_for_strong_scaling(experiment: Experiment, progress_bar=DUMMY_PROGRESS):
+    dimensions = len(experiment.parameters)
+
+    # sum per metric check direction after that
+
+    metric_list = experiment.metrics
+
+    if Metric('time') in metric_list:
+        logging.debug("Check for strong scaling: Found time metric - using only that.")
+        metric_list = [Metric('time')]
+
+    metric_measurement_agg: dict[Metric, _StrongScalingCheckData] = {m: _StrongScalingCheckData() for m in metric_list}
+
+    main_node = experiment.call_tree.childs[0]
+    for child in experiment.call_tree.childs:
+        if child.name.lower() == 'main':
+            main_node = child
+            break
+
+    # Group data for analysis
+    for metric in metric_measurement_agg:
+        if (main_node.path, metric) not in experiment.measurements:
+            continue
+
+        measurement_agg = metric_measurement_agg[metric]
+
+        if not measurement_agg.dimension_coordinates:
+            dimension_groups = [
+                {} for _ in range(dimensions)
+            ]
+            # group all measurements for each dimension, by their coordinates in the other dimensions
+            for m in experiment.measurements[main_node.path, metric]:
+                for p in range(dimensions):
+                    coordinate_p_ = m.coordinate.as_partial_tuple(p)
+                    groups_p_ = dimension_groups[p]
+                    if coordinate_p_ in groups_p_:
+                        groups_p_[coordinate_p_].append(m)
+                    else:
+                        groups_p_[coordinate_p_] = [m]
+
+            for d, dimension_group in enumerate(dimension_groups):
+                longest_group = max(dimension_group.values(), key=lambda x: (len(x), sum(e.mean for e in x)))
+                dimension_coordinates = []
+                for m in longest_group:
+                    dimension_coordinates.append(m.coordinate)
+                    measurement_agg.measurements[m.coordinate] = 0
+                dimension_coordinates.sort(key=lambda c: c[d])
+                assert len(measurement_agg.dimension_coordinates) == d
+                measurement_agg.dimension_coordinates.append(dimension_coordinates)
+
+    _aggregate_values_for_strong_scaling_check(metric_measurement_agg, experiment.call_tree, experiment.measurements,
+                                               progress_bar)
+
+    # Check for strong scaling in each selected metric
+    results = [0] * dimensions
+
+    for metric, check_data in metric_measurement_agg.items():
+        for d, coords in enumerate(check_data.dimension_coordinates):
+            if len(coords) <= 1:
+                continue
+            is_strong_scaling = True
+            prev_mean = check_data.measurements[coords[0]]
+            for c in coords[1:]:
+                if prev_mean <= check_data.measurements[c]:
+                    is_strong_scaling = False
+                    break
+                else:
+                    prev_mean = check_data.measurements[c]
+
+            if is_strong_scaling:
+                results[d] += 1
+
+    return results
+
+
+def _aggregate_values_for_strong_scaling_check(agg: dict[Metric, _StrongScalingCheckData], node: Node,
+                                               measurements: dict[Tuple[Callpath, Metric], Sequence[Measurement]],
+                                               progress_bar=DUMMY_PROGRESS):
+    callpath = node.path if node.path else Callpath.EMPTY
+
+    if callpath.lookup_tag('agg__usage_disabled', False):
+        return
+    if callpath.lookup_tag('agg__disabled', False):
+        return
+    if callpath.lookup_tag('agg__category') is not None:
+        return
+
+    for metric in agg:
+        if (callpath, metric) not in measurements:
+            continue
+        measurement_agg = agg[metric]
+        for m in measurements[callpath, metric]:
+            if m.coordinate in measurement_agg.measurements:
+                measurement_agg.measurements[m.coordinate] += m.mean
+
+    for child in node:
+        _aggregate_values_for_strong_scaling_check(agg, child, measurements, progress_bar)
diff --git a/extrap/fileio/output.py b/extrap/fileio/output.py
index 66b17937..c74ffc78 100644
--- a/extrap/fileio/output.py
+++ b/extrap/fileio/output.py
@@ -12,12 +12,14 @@
 from extrap.entities.coordinate import Coordinate
 from extrap.entities.experiment import Experiment
 from extrap.util.exceptions import RecoverableError
+from extrap.util.string_formats import FunctionFormats
 
 """
 This module provides the custom output formatting for the experiments. 
 You can find the explanation of the placeholders in docs/output-formatting.md
 """
 
+
 class OutputFormatError(RecoverableError):
     NAME = 'Output Format Error'
 
@@ -183,7 +185,6 @@ def _parse_apostrophe(input_str, remove_str=False):
 _re_point = re.compile(r"(\?)?point(\s*:\s*(.*?))?")
 _re_measurements = re.compile(r"measurements(\s*:\s*(.*?))?")
 _re_parameters = re.compile(r"(\?)?parameters(\s*:\s*(.*?))?")
-_re_legacy_options = re.compile(r"all|callpaths|metrics|parameters|functions")
 
 
 def _parse_options(options, format_remove_str=False):
@@ -210,16 +211,18 @@ def _parse_options(options, format_remove_str=False):
 
 
 def format_output(experiment: Experiment, printtype: str):
-    print_str = printtype.lower()
-
-    # backward compatibility
-    if _re_legacy_options.fullmatch(print_str):
-        return io.format_output(experiment, print_str.upper())
+    # predefined outputs
+    output = io.format_output(experiment, printtype)
+    if output is not None:
+        return output
 
     models = experiment.modelers[0].models
 
     placeholder_options, print_str = _parse_outer_brackets(printtype,
                                                            remove_str=True)  # convert from input string to list
+
+    placeholder_options = [option.replace("\\n", "\n").replace("\\t", "\t") for option in placeholder_options]
+
     text = ""
 
     print_coord = True
@@ -248,7 +251,10 @@ def format_output(experiment: Experiment, printtype: str):
                 metric_list.append(m.metric.name)
             elif o == "model":
                 temp = m.hypothesis.function.to_string(*experiment.parameters)
-
+            elif o == "model:python":
+                temp = m.hypothesis.function.to_string(*experiment.parameters, format=FunctionFormats.PYTHON)
+            elif o == "model:latex":
+                temp = m.hypothesis.function.to_string(*experiment.parameters, format=FunctionFormats.LATEX)
             elif _re_points.fullmatch(o):
                 data = _re_points.fullmatch(o)
                 coordinate_text = format_points(data.group(2), experiment)
@@ -273,8 +279,6 @@ def format_output(experiment: Experiment, printtype: str):
 
         text += (print_str.format(*placeholder_results) + "\n")
 
-    text = text.replace("\\n", "\n").replace("\\t", "\t")
-
     return text
 
 
diff --git a/extrap/fileio/values_io.py b/extrap/fileio/values_io.py
new file mode 100644
index 00000000..c7c9b58a
--- /dev/null
+++ b/extrap/fileio/values_io.py
@@ -0,0 +1,114 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from __future__ import annotations
+
+import struct
+from zipfile import ZipFile
+
+import numpy as np
+from numpy.lib import format
+
+from extrap.util.exceptions import FileFormatError
+
+
+class ValueWriter:
+
+    def __init__(self, zipfile: ZipFile):
+        self._zipfile = zipfile
+        self.max_memory_size = 1024 * 1024 * 1024
+        self.NUMPY_HEADER_SIZE = 128
+
+        self.index = 0
+        self.chunk_index = 0
+        self.values = []
+        self.byte_size = 0
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.flush()
+
+    def write_values(self, values: np.ndarray):
+        self.values.append(values)
+        self.byte_size += values.itemsize * values.size + self.NUMPY_HEADER_SIZE
+
+        current = self.index
+        self.index += 1
+        if self.byte_size >= self.max_memory_size:
+            self.flush()
+
+        return self.chunk_index, current
+
+    def write_to_file(self):
+
+        pos = 0
+        index = []
+
+        with self._zipfile.open("values/" + str(self.chunk_index) + ".vals", 'w') as file:
+            old_write = file.write
+
+            def write_with_pos_count(b: bytes):
+                nonlocal pos
+                written = old_write(b)
+                pos += written
+                return written
+
+            file.write = write_with_pos_count
+
+            file.write(b"Extra-P VALUES\0\0")
+
+            for val in self.values:
+                index.append(pos)
+                format.write_array(file, val)
+
+        with self._zipfile.open("values/" + str(self.chunk_index) + ".indx", 'w') as file:
+            old_write = file.write
+
+            def write_with_pos_count(b: bytes):
+                nonlocal pos
+                written = old_write(b)
+                pos += written
+                return written
+
+            file.write = write_with_pos_count
+
+            file.write(b"Extra-P INDEX\0\0\0")
+            file.write(struct.pack('<Q', len(index)))
+
+            for pos in index:
+                file.write(struct.pack('<Q', pos))
+
+    def flush(self):
+        if self.index == 0:
+            return
+        self.write_to_file()
+        self.index = 0
+        self.chunk_index += 1
+        self.values.clear()
+        self.byte_size = 0
+
+
+class ValueReader:
+    def __init__(self, zipfile: ZipFile):
+        self._zipfile = zipfile
+
+    def read_values(self, chunk_index: int, index: int):
+        with self._zipfile.open("values/" + str(chunk_index) + ".indx", 'r') as file:
+            magic_number = file.read(16)
+            if magic_number != b"Extra-P INDEX\0\0\0":
+                raise FileFormatError("Could not read index file to retrieve values.")
+            file.seek(struct.calcsize('<Q') * (index + 1) + 16)
+            start_position = struct.unpack_from('<Q', file.read(struct.calcsize('<Q')))[0]
+
+        with self._zipfile.open("values/" + str(chunk_index) + ".vals", 'r') as file:
+            magic_number = file.read(16)
+            if magic_number != b"Extra-P VALUES\0\0":
+                raise FileFormatError("Could not read value file to retrieve values.")
+            file.seek(start_position)
+            return format.read_array(file)
diff --git a/extrap/gpr/add_selection_strategy.py b/extrap/gpr/add_selection_strategy.py
new file mode 100644
index 00000000..e5683c74
--- /dev/null
+++ b/extrap/gpr/add_selection_strategy.py
@@ -0,0 +1,146 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+def suggest_points_add_mode(experiment, possible_points, selected_callpaths, metric, calculate_cost_manual, process_parameter_id, number_processes, budget, current_cost, model_generator):
+    
+    # sum up the values from the selected callpaths
+    if len(selected_callpaths) > 1:
+        runtimes = {}
+        for i in range(len(selected_callpaths)):
+            callpath = selected_callpaths[i]
+            modeler = None
+            for i in range(len(experiment.modelers)):
+                if experiment.modelers[i] == model_generator:
+                    modeler = experiment.modelers[i]
+                    break
+            model = modeler.models[callpath, metric]
+            hypothesis = model.hypothesis
+            function = hypothesis.function
+
+            for j in range(len(possible_points)):
+                point = possible_points[j]
+                # b.1 predict the runtime of the possible_points using the existing performance models
+                runtime = function.evaluate(point.as_tuple())
+                if i == 0:
+                    runtimes[point] = runtime
+                else:
+                    runtimes[point] += runtime
+
+        costs = {}
+        for i in range(len(possible_points)):
+            point = possible_points[i]
+            runtime = runtimes[point]
+            # b.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+            if calculate_cost_manual:
+                nr_processes = number_processes
+            else:
+                nr_processes = point[process_parameter_id]
+            cost = runtime * nr_processes
+            costs[point] = cost
+
+        # b.3 choose n points from the seach space with the lowest cost and check if they fit in the available budget
+        costs_sorted = dict(sorted(costs.items(), key=lambda item: item[1]))
+        available_budget = budget-current_cost
+        suggested_points = []
+
+        # suggest the coordinate if it fits into budget
+        for key, value in costs_sorted.items():
+            if value <= available_budget:
+                suggested_points.append(key)
+                available_budget -= value
+            else:
+                break
+
+    # use the values from the selected callpath
+    elif len(selected_callpaths) == 1:
+        callpath = selected_callpaths[0]
+        modeler = None
+        for i in range(len(experiment.modelers)):
+            if experiment.modelers[i] == model_generator:
+                modeler = experiment.modelers[i]
+                break
+        model = modeler.models[callpath, metric]
+        hypothesis = model.hypothesis
+        function = hypothesis.function
+        runtimes = {}
+        costs = {}
+        
+        for j in range(len(possible_points)):
+            point = possible_points[j]
+            # b.1 predict the runtime of the possible_points using the existing performance models
+            runtime = function.evaluate(point.as_tuple())
+            runtimes[point] = runtime
+            # b.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+            if calculate_cost_manual:
+                nr_processes = number_processes
+            else:
+                nr_processes = point[process_parameter_id]
+            cost = runtime * nr_processes
+            costs[point] = cost
+            
+        # b.3 choose n points from the seach space with the lowest cost and check if they fit in the available budget
+        costs_sorted = dict(sorted(costs.items(), key=lambda item: item[1]))
+        available_budget = budget-current_cost
+        suggested_points = []
+
+        # suggest the coordinate if it fits into budget
+        for key, value in costs_sorted.items():
+            if value <= available_budget:
+                suggested_points.append(key)
+                available_budget -= value
+            else:
+                break
+
+    # sum all callpaths runtime
+    elif len(selected_callpaths) == 0:
+        runtimes = {}
+        for i in range(len(experiment.callpaths)):
+            callpath = experiment.callpaths[i]
+            modeler = None
+            for i in range(len(experiment.modelers)):
+                if experiment.modelers[i] == model_generator:
+                    modeler = experiment.modelers[i]
+                    break
+            model = modeler.models[callpath, metric]
+            hypothesis = model.hypothesis
+            function = hypothesis.function
+
+            for j in range(len(possible_points)):
+                point = possible_points[j]
+                # b.1 predict the runtime of the possible_points using the existing performance models
+                runtime = function.evaluate(point.as_tuple())
+                if i == 0:
+                    runtimes[point] = runtime
+                else:
+                    runtimes[point] += runtime
+
+        costs = {}
+        for i in range(len(possible_points)):
+            point = possible_points[i]
+            runtime = runtimes[point]
+            # b.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+            if calculate_cost_manual:
+                nr_processes = number_processes
+            else:
+                nr_processes = point[process_parameter_id]
+            cost = runtime * nr_processes
+            costs[point] = cost
+
+        # b.3 choose n points from the seach space with the lowest cost and check if they fit in the available budget
+        costs_sorted = dict(sorted(costs.items(), key=lambda item: item[1]))
+        available_budget = budget-current_cost
+        suggested_points = []
+
+        # suggest the coordinate if it fits into budget
+        for key, value in costs_sorted.items():
+            if value <= available_budget:
+                suggested_points.append(key)
+                available_budget -= value
+            else:
+                break
+
+    return suggested_points
\ No newline at end of file
diff --git a/extrap/gpr/base_selection_strategy.py b/extrap/gpr/base_selection_strategy.py
new file mode 100644
index 00000000..321abca2
--- /dev/null
+++ b/extrap/gpr/base_selection_strategy.py
@@ -0,0 +1,390 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from extrap.entities.coordinate import Coordinate
+
+def suggest_points_base_mode(experiment, parameter_value_series):
+
+    if len(experiment.parameters) == 1:
+        possible_points = []
+        for i in range(len(parameter_value_series[0])):
+            exists = False
+            for j in range(len(experiment.coordinates)):
+                if parameter_value_series[0][i] == experiment.coordinates[j].as_tuple()[0]:
+                    exists = True
+                    break
+            if exists == False:
+                possible_points.append(parameter_value_series[0][i])
+        possible_points.sort()
+        points_needed = 5-len(experiment.coordinates)
+        cords = []
+        for i in range(points_needed):
+            cords.append(Coordinate(possible_points[i]))
+        return cords
+
+    elif len(experiment.parameters) == 2:
+        
+        x_line_lengths = {}
+        x_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y:
+                    x.append(cord_values2[0])
+            if y not in x_line_lengths:
+                x_line_lengths[y] = len(x)
+                x_lines[y] = x
+
+        max_value = 0
+        best_line_key = None
+        for key, value in x_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[0])):
+            if parameter_value_series[0][i] not in best_line:
+                potential_values.append(parameter_value_series[0][i])
+        potential_values.sort()
+        cords_x = []
+        for i in range(points_needed):
+            cords_x.append(Coordinate(potential_values[i],best_line_key))
+
+        y_line_lengths = {}
+        y_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x:
+                    y.append(cord_values2[1])
+            if x not in y_line_lengths:
+                y_line_lengths[x] = len(y)
+                y_lines[x] = y
+
+        max_value = 0
+        best_line_key = None
+        for key, value in y_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = y_lines[best_line_key]
+        x = parameter_value_series[0]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[1])):
+            if parameter_value_series[1][i] not in best_line:
+                potential_values.append(parameter_value_series[1][i])
+        potential_values.sort()
+        cords_y = []
+        for i in range(points_needed):
+            cords_y.append(Coordinate(best_line_key,potential_values[i]))
+
+        suggested_cords = []
+        for x in cords_x:
+            suggested_cords.append(x)
+        for x in cords_y:
+            suggested_cords.append(x)
+
+        return suggested_cords
+
+    elif len(experiment.parameters) == 3:
+        
+        x_lines = {}
+        x_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y and cord_values2[2] == z:
+                    x.append(cord_values2[0])
+            if (y,z) not in x_lines:
+                x_lines[(y,z)] = x
+                x_line_lengths[(y,z)] = len(x)
+
+        #print("DEBUG x_lines:",x_lines)
+        #print("DEBUG x_line_lengths:",x_line_lengths)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in x_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[0])):
+            if parameter_value_series[0][i] not in best_line:
+                potential_values.append(parameter_value_series[0][i])
+        potential_values.sort()
+        cords_x = []
+        for i in range(points_needed):
+            cords_x.append(Coordinate(potential_values[i],best_line_key[0],best_line_key[1]))
+
+        #print("DEBUG cords_x:",cords_x)
+            
+        y_lines = {}
+        y_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[2] == z:
+                    y.append(cord_values2[1])
+            if (x,z) not in y_lines:
+                y_lines[(x,z)] = y
+                y_line_lengths[(x,z)] = len(y)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in y_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = y_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[1])):
+            if parameter_value_series[1][i] not in best_line:
+                potential_values.append(parameter_value_series[1][i])
+        potential_values.sort()
+        cords_y = []
+        for i in range(points_needed):
+            cords_y.append(Coordinate(best_line_key[0],potential_values[i],best_line_key[1]))
+
+        #print("DEBUG cords_y:",cords_y)
+
+        z_lines = {}
+        z_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            z = []
+            y = cord_values[1]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[1] == y:
+                    z.append(cord_values2[2])
+            if (x,y) not in z_lines:
+                z_lines[(x,y)] = z
+                z_line_lengths[(x,y)] = len(z)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in z_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = z_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[2])):
+            if parameter_value_series[2][i] not in best_line:
+                potential_values.append(parameter_value_series[2][i])
+        potential_values.sort()
+        cords_z = []
+        for i in range(points_needed):
+            cords_z.append(Coordinate(best_line_key[0],best_line_key[1],potential_values[i]))
+
+        #print("DEBUG cords_z:",cords_z)
+
+        suggested_cords = []
+        for x in cords_x:
+            suggested_cords.append(x)
+        for x in cords_y:
+            suggested_cords.append(x)
+        for x in cords_z:
+            suggested_cords.append(x)
+
+        #print("DEBUG suggested_cords:",suggested_cords)
+
+        return suggested_cords
+
+    elif len(experiment.parameters) == 4:
+        
+        x1_lines = {}
+        x1_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = []
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == x2 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x1.append(cord_values2[0])
+            if (x2,x3,x4) not in x1_lines:
+                x1_lines[(x2,x3,x4)] = x1
+                x1_line_lengths[(x2,x3,x4)] = len(x1)
+
+        #print("DEBUG x1_lines:",x1_lines)
+        #print("DEBUG x1_line_lengths:",x1_line_lengths)
+                
+        max_value = 0
+        best_line_key = None
+        for key, value in x1_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x1_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[0])):
+            if parameter_value_series[0][i] not in best_line:
+                potential_values.append(parameter_value_series[0][i])
+        potential_values.sort()
+        cords_x1 = []
+        for i in range(points_needed):
+            cords_x1.append(Coordinate(potential_values[i],best_line_key[0],best_line_key[1],best_line_key[2]))
+
+        #print("DEBUG cords_x1:",cords_x1)
+
+        x2_lines = {}
+        x2_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = []
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x2.append(cord_values2[1])
+            if (x1,x3,x4) not in x2_lines:
+                x2_lines[(x1,x3,x4)] = x2
+                x2_line_lengths[(x1,x3,x4)] = len(x2)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in x2_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x2_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[1])):
+            if parameter_value_series[1][i] not in best_line:
+                potential_values.append(parameter_value_series[1][i])
+        potential_values.sort()
+        cords_x2 = []
+        for i in range(points_needed):
+            cords_x2.append(Coordinate(best_line_key[0],potential_values[i],best_line_key[1],best_line_key[2]))
+
+        #print("DEBUG cords_x2:",cords_x2)
+
+        x3_lines = {}
+        x3_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = []
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[3] == x4:
+                    x3.append(cord_values2[2])
+            if (x1,x2,x4) not in x3_lines:
+                x3_lines[(x1,x2,x4)] = x3
+                x3_line_lengths[(x1,x2,x4)] = len(x3)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in x3_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x3_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[2])):
+            if parameter_value_series[2][i] not in best_line:
+                potential_values.append(parameter_value_series[2][i])
+        potential_values.sort()
+        cords_x3 = []
+        for i in range(points_needed):
+            cords_x3.append(Coordinate(best_line_key[0],best_line_key[1],potential_values[i],best_line_key[2]))
+
+        #print("DEBUG cords_x3:",cords_x3)
+
+        x4_lines = {}
+        x4_line_lengths = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[2] == x3:
+                    x4.append(cord_values2[3])
+            if (x1,x2,x3) not in x4_lines:
+                x4_lines[(x1,x2,x3)] = x4
+                x4_line_lengths[(x1,x2,x3)] = len(x4)
+
+        max_value = 0
+        best_line_key = None
+        for key, value in x4_line_lengths.items():
+            if value > max_value:
+                best_line_key = key
+                max_value = value
+        best_line = x4_lines[best_line_key]
+        points_needed = 5-max_value
+
+        potential_values = []
+        for i in range(len(parameter_value_series[3])):
+            if parameter_value_series[3][i] not in best_line:
+                potential_values.append(parameter_value_series[3][i])
+        potential_values.sort()
+        cords_x4 = []
+        for i in range(points_needed):
+            cords_x4.append(Coordinate(best_line_key[0],best_line_key[1],best_line_key[2],potential_values[i]))
+
+        #print("DEBUG cords_x4:",cords_x4)
+        
+        suggested_cords = []
+        for x in cords_x1:
+            suggested_cords.append(x)
+        for x in cords_x2:
+            suggested_cords.append(x)
+        for x in cords_x3:
+            suggested_cords.append(x)
+        for x in cords_x4:
+            suggested_cords.append(x)
+
+        #print("DEBUG suggested_cords:",suggested_cords)
+
+        return suggested_cords
+
+    else:
+        return []
\ No newline at end of file
diff --git a/extrap/gpr/gpr_selection_strategy.py b/extrap/gpr/gpr_selection_strategy.py
new file mode 100644
index 00000000..4a3b0667
--- /dev/null
+++ b/extrap/gpr/gpr_selection_strategy.py
@@ -0,0 +1,983 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import numpy as np
+import copy
+from sklearn.gaussian_process import GaussianProcessRegressor
+from sklearn.gaussian_process.kernels import Matern
+from sklearn.gaussian_process.kernels import WhiteKernel
+import warnings
+from sklearn.exceptions import ConvergenceWarning
+from extrap.entities.coordinate import Coordinate
+import sys
+import math
+from math import log2
+from extrap.entities.measurement import Measurement
+from extrap.util.progress_bar import DUMMY_PROGRESS
+
+
+def suggest_points_gpr_mode(experiment, 
+                            possible_points, 
+                            selected_callpaths, 
+                            metric, 
+                            calculate_cost_manual, 
+                            process_parameter_id, 
+                            number_processes, 
+                            budget, 
+                            current_cost,
+                            model_generator,
+                            progress_bar=DUMMY_PROGRESS):
+    
+    progress_bar.total = 100
+    progress_bar.step("Suggesting additional measurement points.")
+
+    # disable warnings from sk
+    warnings.filterwarnings("ignore", category=DeprecationWarning)
+    warnings.filterwarnings("ignore", category=ConvergenceWarning)
+    warnings.filterwarnings("ignore", category=RuntimeWarning)
+
+    max_repetitions = 5
+
+    # sum up the values from the selected callpaths
+    if len(selected_callpaths) > 1:
+
+        selected_points = copy.deepcopy(experiment.coordinates)
+
+        # GPR parameter-value normalization for each measurement point
+        normalization_factors = get_normalization_factors(experiment)
+
+        remaining_points_gpr = {}
+        predicted_runtimes = {}
+        mean_noise_values = []
+
+        for l in range(len(selected_callpaths)):
+            callpath = selected_callpaths[l]
+
+            # do an noise analysis on the existing points
+            mean_noise = analyze_noise(experiment, callpath, metric)
+            mean_noise_values.append(mean_noise)
+
+            # construct a dict of potential measurement points with 
+            # the coordinates as the keys and the costs as the values
+
+            modeler = None
+            for i in range(len(experiment.modelers)):
+                if experiment.modelers[i] == model_generator:
+                    modeler = experiment.modelers[i]
+                    break
+            model = modeler.models[callpath, metric]
+            hypothesis = model.hypothesis
+            function = hypothesis.function
+            
+            # first use the points from the search space 
+            for i in range(len(possible_points)):
+                #key = possible_points[i]
+                point = possible_points[i]
+                # c.1 predict the runtime of the possible_points using the existing performance models
+                runtime = function.evaluate(point.as_tuple())
+                #runtimes[point] = runtime
+                # c.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+                if calculate_cost_manual:
+                    nr_processes = number_processes
+                else:
+                    nr_processes = point[process_parameter_id]
+                cost = runtime * nr_processes
+                values = []
+                for j in range(max_repetitions):
+                    values.append(cost)
+                runtime_values = []
+                for j in range(max_repetitions):
+                    runtime_values.append(runtime)
+                if l == 0:
+                    remaining_points_gpr[possible_points[i]] = values
+                    predicted_runtimes[possible_points[i]] = runtime_values
+                else:
+                    if possible_points[i] in remaining_points_gpr:
+                        for o in range(len(remaining_points_gpr[possible_points[i]])):
+                            remaining_points_gpr[possible_points[i]][o] = remaining_points_gpr[possible_points[i]][o] + np.mean(values)
+                    else:
+                        remaining_points_gpr[possible_points[i]] = values
+                    if possible_points[i] in predicted_runtimes:
+                        for o in range(len(predicted_runtimes[possible_points[i]])):
+                            predicted_runtimes[possible_points[i]][o] = predicted_runtimes[possible_points[i]][o] + np.mean(runtime_values)
+                    else:
+                        predicted_runtimes[possible_points[i]] = runtime_values
+
+            # extend the dict in case there are still reps missing from the base coorindates
+            repetitions = {}
+            costs = {}
+            runtimes = {}
+            for i in range(len(experiment.coordinates)):
+                coordinate = experiment.coordinates[i]
+                for j in range(len(experiment.measurements[(callpath, metric)])):
+                    if coordinate == experiment.measurements[(callpath, metric)][j].coordinate:
+                        try:
+                            reps = len(experiment.measurements[(callpath, metric)][i].values)
+                        except TypeError:
+                            reps = 1
+                        repetitions[coordinate] = reps
+                        cost_values = []
+                        runtime_values = []
+                        try:
+                            for l in range(len(experiment.measurements[(callpath, metric)][i].values)):
+                                runtime = experiment.measurements[(callpath, metric)][i].values[l]
+                                if calculate_cost_manual:
+                                    nr_processes = number_processes
+                                else:
+                                    nr_processes = point[process_parameter_id]
+                                cost = runtime * nr_processes
+                                cost_values.append(cost)
+                                runtime_values.append(runtime)
+                        except TypeError:
+                            runtime = experiment.measurements[(callpath, metric)][i].mean
+                            if calculate_cost_manual:
+                                nr_processes = number_processes
+                            else:
+                                nr_processes = point[process_parameter_id]
+                            cost = runtime * nr_processes
+                            cost_values.append(cost)
+                            runtime_values.append(runtime)
+                        costs[coordinate] = cost_values
+                        runtimes[coordinate] = runtime_values
+                        break
+
+            for key, value in repetitions.items():
+                remaining_reps = max_repetitions - value
+                if remaining_reps > 0:
+                    mean_cost = np.mean(costs[key])
+                    mean_runtime = np.mean(runtimes[key])
+                    values = []
+                    runtime_values = []
+                    for i in range(remaining_reps):
+                        values.append(mean_cost)
+                        runtime_values.append(mean_runtime)
+                    if l == 0:
+                        remaining_points_gpr[key] = values
+                        predicted_runtimes[key] = runtime_values
+                    else:
+                        if key in remaining_points_gpr:
+                            for o in range(len(remaining_points_gpr[possible_points[i]])):
+                                remaining_points_gpr[possible_points[i]][o] = remaining_points_gpr[possible_points[i]][o] + np.mean(values)
+                        else:
+                            remaining_points_gpr[key] = values
+                        if key in predicted_runtimes:
+                            for o in range(len(predicted_runtimes[possible_points[i]])):
+                                predicted_runtimes[possible_points[i]][o] = predicted_runtimes[possible_points[i]][o] + np.mean(runtime_values)
+                        else:
+                            predicted_runtimes[key] = runtime_values
+
+        # calculate mean noise from found noise values for different callpaths
+        mean_noise = np.mean(mean_noise_values)
+
+        # nu should be [0.5, 1.5, 2.5, inf], everything else has 10x overhead
+        # matern kernel + white kernel to simulate actual noise found in the measurements
+        kernel = 1.0 * Matern(length_scale=1.0, length_scale_bounds=(1e-5, 1e5), nu=1.5) + WhiteKernel(noise_level=mean_noise)
+
+        # create a gaussian process regressor
+        gaussian_process = GaussianProcessRegressor(
+            kernel=kernel, n_restarts_optimizer=20
+        )
+
+        # add all of the selected measurement points to the gaussian process
+        # as training data and train it for these points
+        temp_parameters = []
+        for parameter in experiment.parameters:
+            temp_parameters.append(str(parameter))
+        gaussian_process = add_measurements_to_gpr(gaussian_process, 
+                        selected_points, 
+                        experiment.measurements, 
+                        callpath,
+                        metric,
+                        normalization_factors,
+                        temp_parameters)
+        
+        experiment_gpr = copy.deepcopy(experiment)
+
+        suggested_points = []
+        rep_numbers = []
+
+        while True:
+            # additional limit for the loop
+            # otherwise takes too long or the app might crash if set budget is too large
+            # since performing the GPR is reall resource intensive
+            if len(suggested_points) >= 100:
+                break
+
+            # identify all possible next points that would 
+            # still fit into the modeling budget in core hours
+            fitting_measurements = []
+            for key, value in remaining_points_gpr.items():
+                
+                current_cost = calculate_selected_point_cost(experiment_gpr, callpath, metric, calculate_cost_manual, number_processes)
+                
+                # always take the first value in the list, until none left
+                new_cost = current_cost + value[0]
+
+                if new_cost <= budget:
+                    fitting_measurements.append(key)
+
+            # find the next best additional measurement point using the gpr
+            best_index = -1
+            best_rated = sys.float_info.max
+
+            for i in range(len(fitting_measurements)):
+            
+                parameter_values = fitting_measurements[i].as_tuple()
+                x = []
+                
+                for j in range(len(parameter_values)):
+                
+                    if len(normalization_factors) != 0:
+                        x.append(parameter_values[j] * normalization_factors[str(experiment.parameters[j])])
+                
+                    else:
+                        x.append(parameter_values[j])
+                        
+                # term_1 is cost(t)^2
+                term_1 = math.pow(remaining_points_gpr[fitting_measurements[i]][0], 2)
+                # predict variance of input vector x with the gaussian process
+                x = [x]
+                _, y_cov = gaussian_process.predict(x, return_cov=True)
+                y_cov = abs(y_cov)
+                # term_2 is gp_cov(t,t)^2
+                term_2 = math.pow(y_cov, 2)
+                # rated is h(t)
+                
+                rep = 1
+                rep = max_repetitions - len(predicted_runtimes[fitting_measurements[i]]) + 1
+                
+                rep_func = 2**((1/2)*rep-(1/2))
+                noise_func = -math.tanh((1/4)*mean_noise-2.5)
+                cost_multiplier = rep_func + noise_func
+                rated = (term_1 * cost_multiplier) / term_2
+                
+                if rated <= best_rated:
+                    best_rated = rated
+                    best_index = i    
+
+            # if there has been a point found that is suitable
+            if best_index != -1:
+
+                # add the identified measurement point to the selected point list
+                parameter_values = fitting_measurements[best_index].as_tuple()
+                cord = Coordinate(parameter_values)
+                
+                # only add coordinate to selected points list if not already in there (because of reps)
+                if cord not in selected_points:
+                    selected_points.append(cord)
+                
+                # add the new point to the gpr and call fit()
+                gaussian_process = add_measurement_to_gpr(gaussian_process, 
+                        cord, 
+                        predicted_runtimes,
+                        normalization_factors,
+                        experiment_gpr.parameters)
+                
+                new_value = 0
+                
+                # remove the identified measurement point from the remaining point list
+                try:
+                    new_value = predicted_runtimes[cord][0]
+                    predicted_runtimes[cord].pop(0)
+                    if len(predicted_runtimes[cord]) == 0:
+                        predicted_runtimes.pop(cord)
+                    
+                    # pop value from cord in remaining points list that has been selected as best next point
+                    remaining_points_gpr[cord].pop(0)
+                    
+                    # pop cord from remaining points when no value left anymore
+                    if len(remaining_points_gpr[cord]) == 0:
+                        remaining_points_gpr.pop(cord)
+                    
+                except KeyError:
+                    pass
+
+                # add this point to the gpr experiment
+                experiment_gpr = create_experiment(cord, experiment_gpr, new_value, callpath, metric)
+
+                # add cord and rep number to the list of suggestions
+                rep_number = 1
+                for i in range(len(experiment_gpr.measurements[(callpath, metric)])):
+                    if cord == experiment_gpr.measurements[(callpath, metric)][i].coordinate:
+                        rep_number = len(experiment_gpr.measurements[(callpath, metric)][i].values)
+                        break
+                suggested_points.append(cord)
+                rep_numbers.append(rep_number)
+
+                # update progress bar
+                progress_bar.update(1)
+
+            # if there are no suitable measurement points found
+            # break the while True loop
+            else:
+                break
+    
+    # use the values from the selected callpath
+    elif len(selected_callpaths) == 1:
+        callpath = selected_callpaths[0]
+        selected_points = copy.deepcopy(experiment.coordinates)
+
+        # GPR parameter-value normalization for each measurement point
+        normalization_factors = get_normalization_factors(experiment)
+
+        # do an noise analysis on the existing points
+        mean_noise = analyze_noise(experiment, callpath, metric)
+
+        # nu should be [0.5, 1.5, 2.5, inf], everything else has 10x overhead
+        # matern kernel + white kernel to simulate actual noise found in the measurements
+        kernel = 1.0 * Matern(length_scale=1.0, length_scale_bounds=(1e-5, 1e5), nu=1.5) + WhiteKernel(noise_level=mean_noise)
+
+        # create a gaussian process regressor
+        gaussian_process = GaussianProcessRegressor(
+            kernel=kernel, n_restarts_optimizer=20
+        )
+
+        # add all of the selected measurement points to the gaussian process
+        # as training data and train it for these points
+        temp_parameters = []
+        for parameter in experiment.parameters:
+            temp_parameters.append(str(parameter))
+        gaussian_process = add_measurements_to_gpr(gaussian_process, 
+                        selected_points, 
+                        experiment.measurements, 
+                        callpath,
+                        metric,
+                        normalization_factors,
+                        temp_parameters)
+
+        # construct a dict of potential measurement points with 
+        # the coordinates as the keys and the costs as the values
+
+        modeler = None
+        for i in range(len(experiment.modelers)):
+            if experiment.modelers[i] == model_generator:
+                modeler = experiment.modelers[i]
+                break
+        model = modeler.models[callpath, metric]
+        hypothesis = model.hypothesis
+        function = hypothesis.function
+        
+        # first use the points from the search space 
+        remaining_points_gpr = {}
+        predicted_runtimes = {}
+        for i in range(len(possible_points)):
+            #key = possible_points[i]
+            point = possible_points[i]
+            # c.1 predict the runtime of the possible_points using the existing performance models
+            runtime = function.evaluate(point.as_tuple())
+            #runtimes[point] = runtime
+            # c.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+            if calculate_cost_manual:
+                nr_processes = number_processes
+            else:
+                nr_processes = point[process_parameter_id]
+            cost = runtime * nr_processes
+            values = []
+            for j in range(max_repetitions):
+                values.append(cost)
+            runtime_values = []
+            for j in range(max_repetitions):
+                runtime_values.append(runtime)
+            remaining_points_gpr[possible_points[i]] = values
+            predicted_runtimes[possible_points[i]] = runtime_values
+
+        # extend the dict in case there are still reps missing from the base coorindates
+        repetitions = {}
+        costs = {}
+        runtimes = {}
+        for i in range(len(experiment.coordinates)):
+            coordinate = experiment.coordinates[i]
+            for j in range(len(experiment.measurements[(callpath, metric)])):
+                if coordinate == experiment.measurements[(callpath, metric)][j].coordinate:
+                    try:
+                        reps = len(experiment.measurements[(callpath, metric)][i].values)
+                    except TypeError:
+                        reps = 1
+                    repetitions[coordinate] = reps
+                    cost_values = []
+                    runtime_values = []
+                    try:
+                        for l in range(len(experiment.measurements[(callpath, metric)][i].values)):
+                            runtime = experiment.measurements[(callpath, metric)][i].values[l]
+                            if calculate_cost_manual:
+                                nr_processes = number_processes
+                            else:
+                                nr_processes = point[process_parameter_id]
+                            cost = runtime * nr_processes
+                            cost_values.append(cost)
+                            runtime_values.append(runtime)
+                    except TypeError:
+                        runtime = experiment.measurements[(callpath, metric)][i].mean
+                        if calculate_cost_manual:
+                            nr_processes = number_processes
+                        else:
+                            nr_processes = point[process_parameter_id]
+                        cost = runtime * nr_processes
+                        cost_values.append(cost)
+                        runtime_values.append(runtime)
+                    costs[coordinate] = cost_values
+                    runtimes[coordinate] = runtime_values
+                    break
+
+        for key, value in repetitions.items():
+            remaining_reps = max_repetitions - value
+            if remaining_reps > 0:
+                mean_cost = np.mean(costs[key])
+                mean_runtime = np.mean(runtimes[key])
+                values = []
+                runtime_values = []
+                for i in range(remaining_reps):
+                    values.append(mean_cost)
+                    runtime_values.append(mean_runtime)
+                remaining_points_gpr[key] = values
+                predicted_runtimes[key] = runtime_values
+
+        experiment_gpr = copy.deepcopy(experiment)
+
+        suggested_points = []
+        rep_numbers = []
+
+        while True:
+            # additional limit for the loop
+            # otherwise takes too long or the app might crash if set budget is too large
+            # since performing the GPR is reall resource intensive
+            if len(suggested_points) >= 100:
+                break
+
+            # identify all possible next points that would 
+            # still fit into the modeling budget in core hours
+            fitting_measurements = []
+            for key, value in remaining_points_gpr.items():
+                
+                current_cost = calculate_selected_point_cost(experiment_gpr, callpath, metric, calculate_cost_manual, number_processes)
+                
+                # always take the first value in the list, until none left
+                new_cost = current_cost + value[0]
+
+                if new_cost <= budget:
+                    fitting_measurements.append(key)
+
+            # find the next best additional measurement point using the gpr
+            best_index = -1
+            best_rated = sys.float_info.max
+
+            for i in range(len(fitting_measurements)):
+            
+                parameter_values = fitting_measurements[i].as_tuple()
+                x = []
+                
+                for j in range(len(parameter_values)):
+                
+                    if len(normalization_factors) != 0:
+                        x.append(parameter_values[j] * normalization_factors[str(experiment.parameters[j])])
+                
+                    else:
+                        x.append(parameter_values[j])
+                        
+                # term_1 is cost(t)^2
+                term_1 = math.pow(remaining_points_gpr[fitting_measurements[i]][0], 2)
+                # predict variance of input vector x with the gaussian process
+                x = [x]
+                _, y_cov = gaussian_process.predict(x, return_cov=True)
+                y_cov = abs(y_cov)
+                # term_2 is gp_cov(t,t)^2
+                term_2 = math.pow(y_cov, 2)
+                # rated is h(t)
+                
+                rep = 1
+                rep = max_repetitions - len(predicted_runtimes[fitting_measurements[i]]) + 1
+                
+                rep_func = 2**((1/2)*rep-(1/2))
+                noise_func = -math.tanh((1/4)*mean_noise-2.5)
+                cost_multiplier = rep_func + noise_func
+                rated = (term_1 * cost_multiplier) / term_2
+                
+                if rated <= best_rated:
+                    best_rated = rated
+                    best_index = i    
+
+            # if there has been a point found that is suitable
+            if best_index != -1:
+
+                # add the identified measurement point to the selected point list
+                parameter_values = fitting_measurements[best_index].as_tuple()
+                cord = Coordinate(parameter_values)
+                
+                # only add coordinate to selected points list if not already in there (because of reps)
+                if cord not in selected_points:
+                    selected_points.append(cord)
+                
+                # add the new point to the gpr and call fit()
+                gaussian_process = add_measurement_to_gpr(gaussian_process, 
+                        cord, 
+                        predicted_runtimes,
+                        normalization_factors,
+                        experiment_gpr.parameters)
+                
+                new_value = 0
+                
+                # remove the identified measurement point from the remaining point list
+                try:
+                    new_value = predicted_runtimes[cord][0]
+                    predicted_runtimes[cord].pop(0)
+                    if len(predicted_runtimes[cord]) == 0:
+                        predicted_runtimes.pop(cord)
+                    
+                    # pop value from cord in remaining points list that has been selected as best next point
+                    remaining_points_gpr[cord].pop(0)
+                    
+                    # pop cord from remaining points when no value left anymore
+                    if len(remaining_points_gpr[cord]) == 0:
+                        remaining_points_gpr.pop(cord)
+                    
+                except KeyError:
+                    pass
+
+                # add this point to the gpr experiment
+                experiment_gpr = create_experiment(cord, experiment_gpr, new_value, callpath, metric)
+
+                # add cord and rep number to the list of suggestions
+                rep_number = 1
+                for i in range(len(experiment_gpr.measurements[(callpath, metric)])):
+                    if cord == experiment_gpr.measurements[(callpath, metric)][i].coordinate:
+                        rep_number = len(experiment_gpr.measurements[(callpath, metric)][i].values)
+                        break
+                suggested_points.append(cord)
+                rep_numbers.append(rep_number)
+
+                # update progress bar
+                progress_bar.update(1)
+
+            # if there are no suitable measurement points found
+            # break the while True loop
+            else:
+                break
+    
+    # sum all callpaths runtime
+    elif len(selected_callpaths) == 0:
+        selected_points = copy.deepcopy(experiment.coordinates)
+
+        # GPR parameter-value normalization for each measurement point
+        normalization_factors = get_normalization_factors(experiment)
+
+        remaining_points_gpr = {}
+        predicted_runtimes = {}
+        mean_noise_values = []
+
+        for l in range(len(experiment.callpaths)):
+            callpath = experiment.callpaths[l]
+
+            # do an noise analysis on the existing points
+            mean_noise = analyze_noise(experiment, callpath, metric)
+            mean_noise_values.append(mean_noise)
+
+            # construct a dict of potential measurement points with 
+            # the coordinates as the keys and the costs as the values
+
+            modeler = None
+            for i in range(len(experiment.modelers)):
+                if experiment.modelers[i] == model_generator:
+                    modeler = experiment.modelers[i]
+                    break
+            model = modeler.models[callpath, metric]
+            hypothesis = model.hypothesis
+            function = hypothesis.function
+            
+            # first use the points from the search space 
+            for i in range(len(possible_points)):
+                #key = possible_points[i]
+                point = possible_points[i]
+                # c.1 predict the runtime of the possible_points using the existing performance models
+                runtime = function.evaluate(point.as_tuple())
+                #runtimes[point] = runtime
+                # c.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+                if calculate_cost_manual:
+                    nr_processes = number_processes
+                else:
+                    nr_processes = point[process_parameter_id]
+                cost = runtime * nr_processes
+                values = []
+                for j in range(max_repetitions):
+                    values.append(cost)
+                runtime_values = []
+                for j in range(max_repetitions):
+                    runtime_values.append(runtime)
+                if l == 0:
+                    remaining_points_gpr[possible_points[i]] = values
+                    predicted_runtimes[possible_points[i]] = runtime_values
+                else:
+                    if possible_points[i] in remaining_points_gpr:
+                        for o in range(len(remaining_points_gpr[possible_points[i]])):
+                            remaining_points_gpr[possible_points[i]][o] = remaining_points_gpr[possible_points[i]][o] + np.mean(values)
+                    else:
+                        remaining_points_gpr[possible_points[i]] = values
+                    if possible_points[i] in predicted_runtimes:
+                        for o in range(len(predicted_runtimes[possible_points[i]])):
+                            predicted_runtimes[possible_points[i]][o] = predicted_runtimes[possible_points[i]][o] + np.mean(runtime_values)
+                    else:
+                        predicted_runtimes[possible_points[i]] = runtime_values
+
+            # extend the dict in case there are still reps missing from the base coorindates
+            repetitions = {}
+            costs = {}
+            runtimes = {}
+            for i in range(len(experiment.coordinates)):
+                coordinate = experiment.coordinates[i]
+                for j in range(len(experiment.measurements[(callpath, metric)])):
+                    if coordinate == experiment.measurements[(callpath, metric)][j].coordinate:
+                        try:
+                            reps = len(experiment.measurements[(callpath, metric)][i].values)
+                        except TypeError:
+                            reps = 1
+                        repetitions[coordinate] = reps
+                        cost_values = []
+                        runtime_values = []
+                        try:
+                            for l in range(len(experiment.measurements[(callpath, metric)][i].values)):
+                                runtime = experiment.measurements[(callpath, metric)][i].values[l]
+                                if calculate_cost_manual:
+                                    nr_processes = number_processes
+                                else:
+                                    nr_processes = point[process_parameter_id]
+                                cost = runtime * nr_processes
+                                cost_values.append(cost)
+                                runtime_values.append(runtime)
+                        except TypeError:
+                            runtime = experiment.measurements[(callpath, metric)][i].mean
+                            if calculate_cost_manual:
+                                nr_processes = number_processes
+                            else:
+                                nr_processes = point[process_parameter_id]
+                            cost = runtime * nr_processes
+                            cost_values.append(cost)
+                            runtime_values.append(runtime)
+                        costs[coordinate] = cost_values
+                        runtimes[coordinate] = runtime_values
+                        break
+
+            for key, value in repetitions.items():
+                remaining_reps = max_repetitions - value
+                if remaining_reps > 0:
+                    mean_cost = np.mean(costs[key])
+                    mean_runtime = np.mean(runtimes[key])
+                    values = []
+                    runtime_values = []
+                    for i in range(remaining_reps):
+                        values.append(mean_cost)
+                        runtime_values.append(mean_runtime)
+                    if l == 0:
+                        remaining_points_gpr[key] = values
+                        predicted_runtimes[key] = runtime_values
+                    else:
+                        if key in remaining_points_gpr:
+                            for o in range(len(remaining_points_gpr[possible_points[i]])):
+                                remaining_points_gpr[possible_points[i]][o] = remaining_points_gpr[possible_points[i]][o] + np.mean(values)
+                        else:
+                            remaining_points_gpr[key] = values
+                        if key in predicted_runtimes:
+                            for o in range(len(predicted_runtimes[possible_points[i]])):
+                                predicted_runtimes[possible_points[i]][o] = predicted_runtimes[possible_points[i]][o] + np.mean(runtime_values)
+                        else:
+                            predicted_runtimes[key] = runtime_values
+
+        # calculate mean noise from found noise values for different callpaths
+        mean_noise = np.mean(mean_noise_values)
+
+        # nu should be [0.5, 1.5, 2.5, inf], everything else has 10x overhead
+        # matern kernel + white kernel to simulate actual noise found in the measurements
+        kernel = 1.0 * Matern(length_scale=1.0, length_scale_bounds=(1e-5, 1e5), nu=1.5) + WhiteKernel(noise_level=mean_noise)
+
+        # create a gaussian process regressor
+        gaussian_process = GaussianProcessRegressor(
+            kernel=kernel, n_restarts_optimizer=20
+        )
+
+        # add all of the selected measurement points to the gaussian process
+        # as training data and train it for these points
+        temp_parameters = []
+        for parameter in experiment.parameters:
+            temp_parameters.append(str(parameter))
+        gaussian_process = add_measurements_to_gpr(gaussian_process, 
+                        selected_points, 
+                        experiment.measurements, 
+                        callpath,
+                        metric,
+                        normalization_factors,
+                        temp_parameters)
+        
+        experiment_gpr = copy.deepcopy(experiment)
+
+        suggested_points = []
+        rep_numbers = []
+
+        while True:
+            # additional limit for the loop
+            # otherwise takes too long or the app might crash if set budget is too large
+            # since performing the GPR is reall resource intensive
+            if len(suggested_points) >= 100:
+                break
+
+            # identify all possible next points that would 
+            # still fit into the modeling budget in core hours
+            fitting_measurements = []
+            for key, value in remaining_points_gpr.items():
+                
+                current_cost = calculate_selected_point_cost(experiment_gpr, callpath, metric, calculate_cost_manual, number_processes)
+                
+                # always take the first value in the list, until none left
+                new_cost = current_cost + value[0]
+
+                if new_cost <= budget:
+                    fitting_measurements.append(key)
+
+            # find the next best additional measurement point using the gpr
+            best_index = -1
+            best_rated = sys.float_info.max
+
+            for i in range(len(fitting_measurements)):
+            
+                parameter_values = fitting_measurements[i].as_tuple()
+                x = []
+                
+                for j in range(len(parameter_values)):
+                
+                    if len(normalization_factors) != 0:
+                        x.append(parameter_values[j] * normalization_factors[str(experiment.parameters[j])])
+                
+                    else:
+                        x.append(parameter_values[j])
+                        
+                # term_1 is cost(t)^2
+                term_1 = math.pow(remaining_points_gpr[fitting_measurements[i]][0], 2)
+                # predict variance of input vector x with the gaussian process
+                x = [x]
+                _, y_cov = gaussian_process.predict(x, return_cov=True)
+                y_cov = abs(y_cov)
+                # term_2 is gp_cov(t,t)^2
+                term_2 = math.pow(y_cov, 2)
+                # rated is h(t)
+                
+                rep = 1
+                rep = max_repetitions - len(predicted_runtimes[fitting_measurements[i]]) + 1
+                
+                rep_func = 2**((1/2)*rep-(1/2))
+                noise_func = -math.tanh((1/4)*mean_noise-2.5)
+                cost_multiplier = rep_func + noise_func
+                rated = (term_1 * cost_multiplier) / term_2
+                
+                if rated <= best_rated:
+                    best_rated = rated
+                    best_index = i    
+
+            # if there has been a point found that is suitable
+            if best_index != -1:
+
+                # add the identified measurement point to the selected point list
+                parameter_values = fitting_measurements[best_index].as_tuple()
+                cord = Coordinate(parameter_values)
+                
+                # only add coordinate to selected points list if not already in there (because of reps)
+                if cord not in selected_points:
+                    selected_points.append(cord)
+                
+                # add the new point to the gpr and call fit()
+                gaussian_process = add_measurement_to_gpr(gaussian_process, 
+                        cord, 
+                        predicted_runtimes,
+                        normalization_factors,
+                        experiment_gpr.parameters)
+                
+                new_value = 0
+                
+                # remove the identified measurement point from the remaining point list
+                try:
+                    new_value = predicted_runtimes[cord][0]
+                    predicted_runtimes[cord].pop(0)
+                    if len(predicted_runtimes[cord]) == 0:
+                        predicted_runtimes.pop(cord)
+                    
+                    # pop value from cord in remaining points list that has been selected as best next point
+                    remaining_points_gpr[cord].pop(0)
+                    
+                    # pop cord from remaining points when no value left anymore
+                    if len(remaining_points_gpr[cord]) == 0:
+                        remaining_points_gpr.pop(cord)
+                    
+                except KeyError:
+                    pass
+
+                # add this point to the gpr experiment
+                experiment_gpr = create_experiment(cord, experiment_gpr, new_value, callpath, metric)
+
+                # add cord and rep number to the list of suggestions
+                rep_number = 1
+                for i in range(len(experiment_gpr.measurements[(callpath, metric)])):
+                    if cord == experiment_gpr.measurements[(callpath, metric)][i].coordinate:
+                        rep_number = len(experiment_gpr.measurements[(callpath, metric)][i].values)
+                        break
+                suggested_points.append(cord)
+                rep_numbers.append(rep_number)
+
+                # update progress bar
+                progress_bar.update(1)
+
+            # if there are no suitable measurement points found
+            # break the while True loop
+            else:
+                break
+
+    return suggested_points, rep_numbers
+
+
+def add_measurements_to_gpr(gaussian_process, 
+                            selected_coordinates, 
+                            measurements, 
+                            callpath, 
+                            metric,
+                            normalization_factors,
+                            parameters):
+    X = []
+    Y = []
+
+    for coordinate in selected_coordinates:
+        x = []
+        parameter_values = coordinate.as_tuple()
+
+        for j in range(len(parameter_values)):
+            temp = 0
+            if len(normalization_factors) != 0:
+                temp = parameter_values[j] * normalization_factors[parameters[j]]
+            else:
+                temp = parameter_values[j]
+                while temp < 1:
+                    temp = temp * 10
+            x.append(temp)
+
+        for measurement in measurements[(callpath, metric)]:
+            if measurement.coordinate == coordinate:
+                Y.append(measurement.mean)
+                break
+        X.append(x)
+
+    gaussian_process.fit(X, Y)
+
+    return gaussian_process
+
+
+def analyze_noise(experiment, callpath, metric):
+    # use only measurements for the noise analysis where the coordinates have been selected
+    selected_measurements = []
+    for cord in experiment.coordinates:
+        for measurement in experiment.measurements[(callpath, metric)]:
+            if measurement.coordinate == cord:
+                selected_measurements.append(measurement)
+                break
+    mean_noise_percentages = []
+    # try to calculate the noise level on the measurements
+    # if thats not possible because there are no repetitions available, assume its 1.0%
+    try:
+        for measurement in selected_measurements:
+            mean_mes = np.mean(measurement.values)
+            noise_percentages = []
+            for val in measurement.values:
+                if mean_mes == 0.0:
+                    noise_percentages.append(0)
+                else:
+                    noise_percentages.append(abs((val / (mean_mes / 100)) - 100))
+            mean_noise_percentages.append(np.mean(noise_percentages))
+        mean_noise = np.mean(mean_noise_percentages)
+    except TypeError:
+        mean_noise = 1.0
+    #print("mean_noise:",mean_noise,"%")
+    return mean_noise
+
+
+def get_normalization_factors(experiment):
+    normalization_factors = {}
+    for i in range(len(experiment.parameters)):
+        param_value_max = -1
+        for coord in experiment.coordinates:
+            selected_measurements = coord.as_tuple()[i]
+            if param_value_max < selected_measurements:
+                param_value_max = selected_measurements
+        param_value_max = 100 / param_value_max
+        normalization_factors[str(experiment.parameters[i])] = param_value_max
+    #print("normalization_factors:",normalization_factors)
+    return normalization_factors
+
+
+def create_experiment(cord, experiment, new_value, callpath, metric):
+    # only append the new measurement value to experiment
+    cord_found = False
+    for i in range(len(experiment.measurements[(callpath, metric)])):
+        if cord == experiment.measurements[(callpath, metric)][i].coordinate:
+            experiment.measurements[(callpath, metric)][i].add_value(new_value)
+            cord_found = True
+            break
+    if cord_found == False:
+        # add new coordinate to experiment and then add a new measurement object with the new value to the experiment
+        experiment.add_coordinate(cord)
+        new_measurement = Measurement(cord, callpath, metric, [new_value], keep_values=True)
+        experiment.add_measurement(new_measurement)
+    return experiment
+
+
+def calculate_selected_point_cost(experiment, callpath, metric, calculate_cost_manual, number_processes):
+        selected_cost = 0
+        for i in range(len(experiment.measurements[(callpath, metric)])):
+            x = experiment.measurements[(callpath, metric)][i]
+            coordinate_cost = 0
+            try:         
+                for k in range(len(x.values)):
+                    runtime = np.mean(x.values[k])
+                    if calculate_cost_manual:
+                        nr_processes = number_processes
+                    else:
+                        nr_processes = x.coordinate.as_tuple()[0]                
+                    core_hours = runtime * nr_processes
+                    coordinate_cost += core_hours
+            except TypeError:
+                runtime = x.mean
+                if calculate_cost_manual:
+                    nr_processes = number_processes
+                else:
+                    nr_processes = x.coordinate.as_tuple()[0]                
+                core_hours = runtime * nr_processes
+                coordinate_cost += core_hours
+            selected_cost += coordinate_cost
+        return selected_cost
+
+
+def add_measurement_to_gpr(gaussian_process, 
+                            coordinate, 
+                            measurements, 
+                            normalization_factors,
+                            parameters):
+
+    X = []
+    Y = []
+    x = []
+
+    parameter_values = coordinate.as_tuple()
+
+    for j in range(len(parameter_values)):
+        temp = 0
+        if len(normalization_factors) != 0:
+            temp = parameter_values[j] * normalization_factors[str(parameters[j])]
+        else:
+            temp = parameter_values[j]
+            while temp < 1:
+                temp = temp * 10  
+        x.append(temp)
+
+    Y.append(np.mean(measurements[coordinate][0]))
+
+    X.append(x)
+
+    gaussian_process.fit(X, Y)
+
+    return gaussian_process
\ No newline at end of file
diff --git a/extrap/gpr/measurement_point_advisor.py b/extrap/gpr/measurement_point_advisor.py
new file mode 100644
index 00000000..3bec57e2
--- /dev/null
+++ b/extrap/gpr/measurement_point_advisor.py
@@ -0,0 +1,190 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from extrap.gpr.util import identify_selection_mode, build_parameter_value_series, identify_step_factor, extend_parameter_value_series, build_search_space, identify_possible_points
+from extrap.gpr.base_selection_strategy import suggest_points_base_mode
+from extrap.gpr.add_selection_strategy import suggest_points_add_mode
+from extrap.gpr.gpr_selection_strategy import suggest_points_gpr_mode
+from extrap.gui.components.ProgressWindow import ProgressWindow
+
+
+class MeasurementPointAdvisor():
+
+    def __init__(self, 
+                 budget, 
+                 processes, 
+                 callpaths, 
+                 metric, 
+                 experiment, 
+                 current_cost, 
+                 manual_pms_selection, 
+                 manual_parameter_value_series, 
+                 calculate_cost_manual, 
+                 number_processes, 
+                 main_widget,
+                 model_generator) -> None:
+        
+        self.budget = budget
+        #print("budget:",budget)
+
+        self.processes = processes
+        #print("processes:",processes)
+
+        self.experiment = experiment
+
+        self.normalization = True
+
+        self.current_cost = current_cost
+
+        self.manual_pms_selection = manual_pms_selection
+
+        self.manual_parameter_value_series = manual_parameter_value_series
+
+        self.calculate_cost_manual = calculate_cost_manual
+
+        self.number_processes = number_processes
+
+        self.main_widget = main_widget
+
+        self.model_generator = model_generator
+
+        self.parameters = []
+        for i in range(len(self.experiment.parameters)):
+            self.parameters.append(str(self.experiment.parameters[i]))
+        #print("parameters:",self.parameters)
+
+        self.metric = metric
+        #print("metric:",self.metric)
+
+        # these are tree nodes, need to convert them to actual callpaths manually
+        self.selected_callpath_ids = []
+        self.selected_callpaths = []
+        for i in range(len(callpaths)):
+            self.selected_callpaths.append(callpaths[i].path)
+            for j in range(len(self.experiment.callpaths)):
+                if str(callpaths[i].path) == str(self.experiment.callpaths[j]):
+                    self.selected_callpath_ids.append(j)
+                    break
+        #print("selected callpaths:",self.selected_callpaths)
+        #print("selected callpath ids:",self.selected_callpath_ids)
+
+        # set the minimum number of points required for modeling with the sparse modeler
+        min_points = 0
+        if len(self.experiment.parameters) == 1:
+            min_points = 5
+        elif len(self.experiment.parameters) == 2:
+            min_points = 9
+        elif len(self.experiment.parameters) == 3:
+            min_points = 13
+        elif len(self.experiment.parameters) == 4:
+            min_points = 17
+        else:
+            min_points = 5
+
+        # identify the state of the selection process
+        # possible states are:
+        # 1. not enough points for modeling 
+        #   -> continue row of parameter values for all parameters until modeling requirements are reached
+        # 2. enough points for modeling, without an additional point not part of the lines
+        #   -> suggest an extra point not part of the lines for each parameter
+        # 3. enough points for modeling with an additional point not part of the lines
+        #   -> suggest additional points using the gpr method
+        
+        # can be: gpr, add, base
+        # gpr -> suggest additional measurement points with the gpr method
+        # add -> suggest an additional point that is not part of the lines for each parameter
+        # base -> suggest points to complete the lines of points for each parameter
+        self.selection_mode = identify_selection_mode(self.experiment, min_points)
+
+        #print("DEBUG selection_mode:",self.selection_mode)
+
+
+    def suggest_points(self):
+
+        if self.manual_pms_selection:
+            # 1.2.3. build the parameter series from manual entries in GUI
+            try:
+                parameter_value_series = []
+                for i in range(len(self.experiment.parameters)):
+                    value_series = self.manual_parameter_value_series[i].split(",")
+                    y = []
+                    for x in value_series:
+                        y.append(float(x))
+                    parameter_value_series.append(y)
+            except ValueError as e:
+                print(e)
+                return []
+
+        else:
+            # 1. build a value series for each parameter
+            parameter_value_series = build_parameter_value_series(self.experiment)
+
+            # 2. identify the step factor size for each parameter
+            mean_step_size_factors = identify_step_factor(parameter_value_series)
+
+            # 3. continue and complete these series for each parameter
+            parameter_value_series = extend_parameter_value_series(parameter_value_series, mean_step_size_factors)
+
+        # 4. create search space (1D, 2D, 3D, ND) from the series values of each parameters
+        search_space_coordinates = build_search_space(self.experiment, parameter_value_series)
+
+        # 5. remove existing points from search space to obtain only new possible points
+        possible_points = identify_possible_points(search_space_coordinates, self.experiment)
+                
+        # 6. suggest points using selected mode
+        # a. base mode
+        # a.1 choose the smallest of the values for each parameter
+        # a.2 combine these values of each parameter to a coordinate
+        # a.3 repeat until enough suggestions for cords to complete a line of 5 points for each parameter
+        if self.selection_mode == "base":
+            suggested_cords = suggest_points_base_mode(self.experiment, 
+                                                       parameter_value_series)
+            return suggested_cords, None
+        
+        # 6. suggest points using add mode
+        # b. add mode
+        # b.1 predict the runtime of the possible_points using the existing performance models
+        # b.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+        # b.3 choose the point from the seach space with the lowest cost
+        # b.4 check if that point fits into the available budget
+        # b.41 create a coordinate from it and suggest it if fits into budget
+        # b.42 if not fit then need to show message instead that available budget is not sufficient and needs to be increased...
+        elif self.selection_mode == "add":
+            suggested_cords = suggest_points_add_mode(self.experiment, 
+                                                      possible_points, 
+                                                      self.selected_callpaths, 
+                                                      self.metric, 
+                                                      self.calculate_cost_manual, 
+                                                      self.processes, 
+                                                      self.number_processes, 
+                                                      self.budget, 
+                                                      self.current_cost,
+                                                      self.model_generator)
+            return suggested_cords, None
+
+        
+        # 6. suggest points using gpr mode
+        # c. gpr mode
+        # c.1 predict the runtime of these points using the existing performance models (only possible if already enough points existing for modeling)
+        # c.2 calculate the cost of these points using the runtime (same calculation as for the current cost in the GUI)
+        # c.3 all of the data is used as input to the GPR method
+        # c.4 get the top x points suggested by the GPR method that do fit into the available budget
+        # c.5 create coordinates and suggest them
+        elif self.selection_mode == "gpr":
+            with ProgressWindow(self.main_widget, 'Generating models') as pbar:
+                suggested_cords, rep_numbers = suggest_points_gpr_mode(self.experiment,
+                                                        possible_points, 
+                                                        self.selected_callpaths, 
+                                                        self.metric, 
+                                                        self.calculate_cost_manual, 
+                                                        self.processes, 
+                                                        self.number_processes, 
+                                                        self.budget,
+                                                        self.current_cost,
+                                                        self.model_generator,
+                                                        pbar)
+                return suggested_cords, rep_numbers
diff --git a/extrap/gpr/util.py b/extrap/gpr/util.py
new file mode 100644
index 00000000..caa99348
--- /dev/null
+++ b/extrap/gpr/util.py
@@ -0,0 +1,618 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from extrap.entities.coordinate import Coordinate
+from collections import Counter
+import numpy as np
+
+
+def check_model_requirements(experiment, min_points):
+    modeling_reuqirements_satisfied = False
+    # one model parameter
+    if len(experiment.parameters) == 1:
+        if len(experiment.coordinates) >= min_points:
+            modeling_reuqirements_satisfied = True
+
+    # two model parameters
+    elif len(experiment.parameters) == 2:
+        x_requirement = False
+        y_requirement = False
+        
+        # find the cheapest line of 5 points for y
+        y_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x:
+                    y.append(cord_values2[1])
+            if len(y) == 5:
+                if x not in y_lines:
+                    y_lines[x] = y
+        if len(y_lines) >= 1:
+            y_requirement = True
+        #print("y_lines:",y_lines)
+
+        # find the cheapest line of 5 points for x
+        x_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y:
+                    x.append(cord_values2[0])
+            if len(x) == 5:
+                if y not in x_lines:
+                    x_lines[y] = x
+        if len(x_lines) >= 1:
+            x_requirement = True
+        #print("x_lines:",x_lines)
+
+        if x_requirement and y_requirement:
+            modeling_reuqirements_satisfied = True
+
+    # three model parameters
+    elif len(experiment.parameters) == 3:
+        x_requirement = False
+        y_requirement = False
+        z_requirement = False
+
+        # find the cheapest line of 5 points for y
+        y_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[2] == z:
+                    y.append(cord_values2[1])
+            if len(y) >= 5:
+                if (x,z) not in y_lines:
+                    y_lines[(x,z)] = y
+        if len(y_lines) >= 1:
+            y_requirement = True
+        #print("y_lines:",y_lines)
+
+        # find the cheapest line of 5 points for x
+        x_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y and cord_values2[2] == z:
+                    x.append(cord_values2[0])
+            if len(x) >= 5:
+                if (y,z) not in x_lines:
+                    x_lines[(y,z)] = x
+        if len(x_lines) >= 1:
+            x_requirement = True
+        #print("x_lines:",x_lines)
+
+        # find the cheapest line of 5 points for z
+        z_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            z = []
+            y = cord_values[1]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[1] == y:
+                    z.append(cord_values2[2])
+            if len(z) >= 5:
+                if (x,y) not in z_lines:
+                    z_lines[(x,y)] = z
+        if len(z_lines) >= 1:
+            z_requirement = True
+        #print("z_lines:",z_lines)
+
+        if x_requirement and y_requirement and z_requirement:
+            modeling_reuqirements_satisfied = True
+
+    # four model parameters
+    elif len(experiment.parameters) == 4:
+        x1_requirement = False
+        x2_requirement = False
+        x3_requirement = False
+        x4_requirement = False
+
+        # find the cheapest line of 5 points for x1
+        x1_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = []
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == x2 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x1.append(cord_values2[0])
+            if len(x1) >= 5:
+                if (x2,x3,x4) not in x1_lines:
+                    x1_lines[(x2,x3,x4)] = x1
+        if len(x1_lines) >= 1:
+            x1_requirement = True
+        #print("x1_lines:",x1_lines)
+
+        # find the cheapest line of 5 points for x2
+        x2_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = []
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x2.append(cord_values2[1])
+            if len(x2) >= 5:
+                if (x1,x3,x4) not in x2_lines:
+                    x2_lines[(x1,x3,x4)] = x2
+        if len(x2_lines) >= 1:
+            x2_requirement = True
+        #print("x2_lines:",x2_lines)
+
+        # find the cheapest line of 5 points for x3
+        x3_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = []
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[3] == x4:
+                    x3.append(cord_values2[2])
+            if len(x3) >= 5:
+                if (x1,x2,x4) not in x3_lines:
+                    x3_lines[(x1,x2,x4)] = x3
+        if len(x3_lines) >= 1:
+            x3_requirement = True
+        #print("x3_lines:",x3_lines)
+
+        # find the cheapest line of 5 points for x4
+        x4_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[2] == x3:
+                    x4.append(cord_values2[3])
+            if len(x4) >= 5:
+                if (x1,x2,x3) not in x4_lines:
+                    x4_lines[(x1,x2,x3)] = x4
+        if len(x4_lines) >= 1:
+            x4_requirement = True
+        #print("x4_lines:",x4_lines)
+
+        if x1_requirement and x2_requirement and x3_requirement and x4_requirement:
+            modeling_reuqirements_satisfied = True
+
+    return modeling_reuqirements_satisfied
+
+
+def identify_selection_mode(experiment, min_points):
+    selection_mode = None
+
+    # check if there are enough measurement points for Extra-P to create a model
+    modeling_reuqirements_satisfied = check_model_requirements(experiment, min_points) 
+
+    # if modeling requirements are satisfied check if an additional point that is not part of the lines is existing already (only for 2+ parameters)
+    if modeling_reuqirements_satisfied:
+        if len(experiment.parameters) > 1:
+            additional_point_exists = check_additional_point(experiment)
+            #print("DEBUG additional_point_exists:",additional_point_exists)
+            # if additional point is available suggest more points using gpr method
+            if additional_point_exists:
+                selection_mode = "gpr"
+            # if not available an additional point suggest points using add method
+            else:
+                selection_mode = "add"
+        else:
+            #TODO: or maybe later a special option if that does not work with GPR, since it was never testes before
+            selection_mode = "gpr"
+    # if modeling requirements are not satisfied suggest points using base method
+    else:
+        selection_mode = "base"
+    
+    return selection_mode
+
+
+def check_additional_point(experiment):
+    # two model parameters
+    if len(experiment.parameters) == 2:
+        # find the possible y lines
+        y_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x:
+                    y.append(cord_values2[1])
+            if len(y) == 5:
+                if x not in y_lines:
+                    y_lines[x] = y
+        base_cords = []
+        for key, value in y_lines.items():
+            for val in value:
+                base_cords.append(Coordinate((key,val)))
+        # find the possible x lines
+        x_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y:
+                    x.append(cord_values2[0])
+            if len(x) == 5:
+                if y not in x_lines:
+                    x_lines[y] = x
+        for key, value in x_lines.items():
+            for val in value:
+                if Coordinate((val,key)) not in base_cords:
+                    base_cords.append(Coordinate((val,key)))
+        additional_cord_found = False
+        for i in range(len(experiment.coordinates)):
+            if experiment.coordinates[i] not in base_cords:
+                additional_cord_found = True
+                break
+        
+        if len(x_lines) > 1 or len(y_lines) > 1:
+            additional_cord_found = True
+ 
+    # three model parameters
+    elif len(experiment.parameters) == 3:
+        # find the cheapest line of 5 points for y
+        y_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            y = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[2] == z:
+                    y.append(cord_values2[1])
+            if len(y) >= 5:
+                if (x,z) not in y_lines:
+                    y_lines[(x,z)] = y
+        base_cords = []
+        for key, value in y_lines.items():
+            for val in value:
+                base_cords.append(Coordinate((key[0],val,key[1])))
+
+        # find the cheapest line of 5 points for x
+        x_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            y = cord_values[1]
+            x = []
+            z = cord_values[2]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == y and cord_values2[2] == z:
+                    x.append(cord_values2[0])
+            if len(x) >= 5:
+                if (y,z) not in x_lines:
+                    x_lines[(y,z)] = x
+        for key, value in x_lines.items():
+            for val in value:
+                if Coordinate((val,key[0],key[1])) not in base_cords:
+                    base_cords.append(Coordinate((val,key[0],key[1])))
+
+        # find the cheapest line of 5 points for z
+        z_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x = cord_values[0]
+            z = []
+            y = cord_values[1]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x and cord_values2[1] == y:
+                    z.append(cord_values2[2])
+            if len(z) >= 5:
+                if (x,y) not in z_lines:
+                    z_lines[(x,y)] = z
+        for key, value in z_lines.items():
+            for val in value:
+                if Coordinate((key[0],key[1],val)) not in base_cords:
+                    base_cords.append(Coordinate((key[0],key[1],val)))
+
+        additional_cord_found = False
+        for i in range(len(experiment.coordinates)):
+            if experiment.coordinates[i] not in base_cords:
+                additional_cord_found = True
+                break
+        if len(y_lines) > 1 or len(x_lines) > 1 or len(z_lines) > 1:
+            additional_cord_found = True
+
+    # four model parameters
+    elif len(experiment.parameters) == 4:
+        # find the cheapest line of 5 points for x1
+        x1_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = []
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[1] == x2 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x1.append(cord_values2[0])
+            if len(x1) >= 5:
+                if (x2,x3,x4) not in x1_lines:
+                    x1_lines[(x2,x3,x4)] = x1
+        base_cords = []
+        for key, value in x1_lines.items():
+            for val in value:
+                base_cords.append(Coordinate((val,key[0],key[1],key[2])))
+
+        # find the cheapest line of 5 points for x2
+        x2_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = []
+            x3 = cord_values[2]
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[2] == x3 and cord_values2[3] == x4:
+                    x2.append(cord_values2[1])
+            if len(x2) >= 5:
+                if (x1,x3,x4) not in x2_lines:
+                    x2_lines[(x1,x3,x4)] = x2
+        for key, value in x2_lines.items():
+            for val in value:
+                if Coordinate((key[0],val,key[1],key[2])) not in base_cords:
+                    base_cords.append(Coordinate((key[0],val,key[1],key[2])))
+
+        # find the cheapest line of 5 points for x3
+        x3_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = []
+            x4 = cord_values[3]
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[3] == x4:
+                    x3.append(cord_values2[2])
+            if len(x3) >= 5:
+                if (x1,x2,x4) not in x3_lines:
+                    x3_lines[(x1,x2,x4)] = x3
+        for key, value in x3_lines.items():
+            for val in value:
+                if Coordinate((key[0],key[1],val,key[2])) not in base_cords:
+                    base_cords.append(Coordinate((key[0],key[1],val,key[2])))
+
+        # find the cheapest line of 5 points for x4
+        x4_lines = {}
+        for i in range(len(experiment.coordinates)):
+            cord_values = experiment.coordinates[i].as_tuple()
+            x1 = cord_values[0]
+            x2 = cord_values[1]
+            x3 = cord_values[2]
+            x4 = []
+            for j in range(len(experiment.coordinates)):
+                cord_values2 = experiment.coordinates[j].as_tuple()
+                if cord_values2[0] == x1 and cord_values2[1] == x2 and cord_values2[2] == x3:
+                    x4.append(cord_values2[3])
+            if len(x4) >= 5:
+                if (x1,x2,x3) not in x4_lines:
+                    x4_lines[(x1,x2,x3)] = x4
+        for key, value in x4_lines.items():
+            for val in value:
+                if Coordinate((key[0],key[1],key[2],val)) not in base_cords:
+                    base_cords.append(Coordinate((key[0],key[1],key[2],val)))
+
+        additional_cord_found = False
+        for i in range(len(experiment.coordinates)):
+            if experiment.coordinates[i] not in base_cords:
+                additional_cord_found = True
+                break
+        
+        if len(x1_lines) > 1 or len(x2_lines) > 1 or len(x3_lines) > 1 or len(x4_lines) > 1:
+            additional_cord_found = True
+
+    return additional_cord_found
+
+
+# get the parameter value series of each parameter that exist from the existing measurement points
+def build_parameter_value_series(experiment):
+    parameter_value_serieses = []
+    for i in range(len(experiment.parameters)):
+        parameter_value_serieses.append([])
+    for i in range(len(experiment.coordinates)):
+        parameter_values = experiment.coordinates[i].as_tuple()
+        for j in range(len(experiment.parameters)):
+            if parameter_values[j] not in parameter_value_serieses[j]:
+                parameter_value_serieses[j].append(parameter_values[j])
+        #print("DEBUG parameter_values:",parameter_values)
+    #print("DEBUG parameter_value_serieses:",parameter_value_serieses)
+    return parameter_value_serieses
+
+
+# get the step value factor for each parameter value series
+def identify_step_factor(parameter_value_serieses):
+    mean_step_size_factors = []
+    for i in range(len(parameter_value_serieses)):
+        if len(parameter_value_serieses[i]) == 1:
+            mean_step_size_factors.append(("*",2.0))
+        elif len(parameter_value_serieses[i]) == 0:
+            return 1
+        else:
+            factors = []
+            for j in range(len(parameter_value_serieses[i])-1):
+                factors.append(parameter_value_serieses[i][j+1]/parameter_value_serieses[i][j])
+            steps = []
+            for j in range(len(parameter_value_serieses[i])-1):
+                steps.append(parameter_value_serieses[i][j+1]-parameter_value_serieses[i][j])
+            max_value = 0
+            max_key = None
+            factors_dict = dict(Counter(factors))
+            for key, value in factors_dict.items():
+                if value > max_value:
+                    max_value = value
+                    max_key = key
+            factor_max = factors_dict[max_key]
+            max_value = 0
+            max_key = None
+            steps_dict = dict(Counter(steps))
+            for key, value in steps_dict.items():
+                if value > max_value:
+                    max_value = value
+                    max_key = key
+            steps_max = steps_dict[max_key]
+            #print("DEBUG steps_dict:",steps_dict,steps_max)
+            #print("DEBUG factor_max:",factors_dict,factor_max)
+            if factor_max > steps_max:
+                mean_step_size_factors.append(("*",np.median(factors)))
+            elif steps_max > factor_max:
+                mean_step_size_factors.append(("+",np.median(steps)))
+            else:
+                all_same = True
+                for i in range(len(steps)-1):
+                    if steps[0] != steps[i+1]:
+                        all_same = False
+                        break
+                if all_same:
+                    mean_step_size_factors.append(("+",np.median(steps)))
+                else:
+                    facts = []
+                    for i in range(len(factors)-1):
+                        if factors[i+1] % factors[0] == 0:
+                            facts.append(factors[0])
+                        else:
+                            facts.append(factors[i+1])
+                    all_same = True
+                    for i in range(len(facts)-1):
+                        if facts[0] != facts[i+1]:
+                            all_same = False
+                            break
+                    if all_same == False:
+                        mean_step_size_factors.append(("+",np.median(steps)))
+                    else:
+                        mean_step_size_factors.append(("*",np.median(facts)))
+    #print("DEBUG mean_step_size_factors:",mean_step_size_factors)
+    return mean_step_size_factors
+
+
+def extend_parameter_value_series(parameter_value_serieses, mean_step_size_factors):
+    #NOTE: this search space with 5 additional values is large enough
+    # especially for 4 model paramaters this results in thousands of possible points
+    # as soon as additional points are measured and loaded into extra-p, the search space will be extended
+    # using the new values as a baseline anyway.
+    additional_values = 5
+    for i in range(len(parameter_value_serieses)):
+        added_values = 0
+        for j in range(len(parameter_value_serieses[i])):
+            if mean_step_size_factors[i][0] == "*":
+                new_value = parameter_value_serieses[i][j]*mean_step_size_factors[i][1]
+                if new_value not in parameter_value_serieses[i]:
+                    parameter_value_serieses[i].append(new_value)
+                    added_values += 1
+            elif mean_step_size_factors[i][0] == "+":
+                new_value = parameter_value_serieses[i][j]+mean_step_size_factors[i][1]
+                if new_value not in parameter_value_serieses[i]:
+                    parameter_value_serieses[i].append(new_value)
+                    added_values += 1
+        if added_values < additional_values:
+            for j in range(additional_values-added_values+1):
+                if mean_step_size_factors[i][0] == "*":
+                    new_value = parameter_value_serieses[i][len(parameter_value_serieses[i])-1]*mean_step_size_factors[i][1]
+                    if new_value not in parameter_value_serieses[i]:
+                        parameter_value_serieses[i].append(new_value)
+                        added_values += 1
+                elif mean_step_size_factors[i][0] == "+":
+                    new_value = parameter_value_serieses[i][len(parameter_value_serieses[i])-1]+mean_step_size_factors[i][1]
+                    if new_value not in parameter_value_serieses[i]:
+                        parameter_value_serieses[i].append(new_value)
+                        added_values += 1
+        parameter_value_serieses[i].sort()
+    #print("DEBUG parameter_value_serieses:",parameter_value_serieses)
+    return parameter_value_serieses
+
+
+def build_search_space(experiment, parameter_value_serieses):
+    # build a 1D, 2D, 3D, ND search space of potential points (that does not contain already measured points)
+    if len(experiment.parameters) == 1:
+        search_space_coordinates = []
+        for i in range(len(parameter_value_serieses[0])):
+            search_space_coordinates.append(Coordinate(parameter_value_serieses[0][i]))
+        #print("DEBUG search_space_coordinates:",search_space_coordinates)
+        return search_space_coordinates
+    
+    elif len(experiment.parameters) == 2:
+        search_space_coordinates = []
+        for i in range(len(parameter_value_serieses[0])):
+            for j in range(len(parameter_value_serieses[1])):
+                search_space_coordinates.append(Coordinate(parameter_value_serieses[0][i],parameter_value_serieses[1][j]))
+        #print("DEBUG search_space_coordinates:",search_space_coordinates)
+        return search_space_coordinates
+
+    elif len(experiment.parameters) == 3:
+        search_space_coordinates = []
+        for i in range(len(parameter_value_serieses[0])):
+            for j in range(len(parameter_value_serieses[1])):
+                for g in range(len(parameter_value_serieses[2])):
+                    search_space_coordinates.append(Coordinate(parameter_value_serieses[0][i],parameter_value_serieses[1][j],parameter_value_serieses[2][g]))
+        #print("DEBUG search_space_coordinates:",search_space_coordinates)
+        return search_space_coordinates
+
+    elif len(experiment.parameters) == 4:
+        search_space_coordinates = []
+        for i in range(len(parameter_value_serieses[0])):
+            for j in range(len(parameter_value_serieses[1])):
+                for g in range(len(parameter_value_serieses[2])):
+                    for h in range(len(parameter_value_serieses[3])):
+                        search_space_coordinates.append(Coordinate(parameter_value_serieses[0][i],parameter_value_serieses[1][j],parameter_value_serieses[2][g],parameter_value_serieses[3][h]))
+        #print("DEBUG search_space_coordinates:",search_space_coordinates)
+        return search_space_coordinates
+
+    else:
+        return 1
+
+
+def identify_possible_points(search_space_coordinates, experiment):
+    possible_points = []
+    for i in range(len(search_space_coordinates)):
+        exists = False
+        for j in range(len(experiment.coordinates)):
+            if experiment.coordinates[j] == search_space_coordinates[i]:
+                exists = True
+                break
+        if exists == False:
+            possible_points.append(search_space_coordinates[i])
+    #print("DEBUG len() search_space_coordinates:",len(search_space_coordinates))
+    #print("DEBUG len() possible_points:",len(possible_points))
+    #print("DEBUG len() experiment.coordinates:",len(experiment.coordinates))
+    #print("possible_points:",possible_points)
+    return possible_points
diff --git a/extrap/gui/CubeFileReader.py b/extrap/gui/CubeFileReader.py
deleted file mode 100644
index 1c4a0431..00000000
--- a/extrap/gui/CubeFileReader.py
+++ /dev/null
@@ -1,263 +0,0 @@
-# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
-#
-# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
-#
-# This software may be modified and distributed under the terms of a BSD-style license.
-# See the LICENSE file in the base directory for details.
-
-from functools import partial
-from threading import Event
-
-from PySide6.QtCore import *  # @UnusedWildImport
-from PySide6.QtWidgets import *  # @UnusedWildImport
-
-from extrap.fileio.file_reader.cube_file_reader2 import CubeFileReader2
-from extrap.util.exceptions import CancelProcessError
-from extrap.util.progress_bar import ProgressBar
-
-
-class ParameterWidget(QWidget):
-
-    def __init__(self, parent):
-        super(ParameterWidget, self).__init__(parent)
-        self.name = "Parameter"
-        self.values = "1"
-
-    def init_UI(self):
-        layout = QFormLayout(self)
-        self.name_edit = QLineEdit(self)
-        self.name_edit.setText(self.name)
-        layout.addRow("Parameter name:", self.name_edit)
-
-        self.values_edit = QLineEdit(self)
-        self.values_edit.setText(self.values)
-        layout.addRow("Values:", self.values_edit)
-
-        self.setLayout(layout)
-
-    def onNewValues(self):
-        self.name_edit.setText(self.name)
-        self.values_edit.setText(self.values)
-
-
-class CubeFileReader(QDialog):
-
-    def __init__(self, parent, dirName):
-        super(CubeFileReader, self).__init__(parent)
-
-        self.valid = False
-        self.dir_name = dirName
-        self.num_params = 1
-        self.max_params = 3
-        self.prefix = ""
-        self.postfix = ""
-        self.filename = "profile.cubex"
-        self.repetitions = 1
-        self.parameters = list()
-
-        self._cancel_event = Event()
-
-        # for _ in range(0, self.max_params):
-        #     self.parameters.append(ParameterWidget(self))
-
-        self.init_UI()
-
-    def init_UI(self):
-        self.setWindowTitle("Import Settings")
-        self.setWindowModality(Qt.WindowModal)
-        self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
-        self.setWindowFlag(Qt.WindowCloseButtonHint, False)
-        main_layout = QFormLayout(self)
-        layout = QFormLayout()
-        self.controls_layout = layout
-
-        # self.num_params_choice = QSpinBox(self)
-        # self.num_params_choice.setMinimum(1)
-        # self.num_params_choice.setMaximum(self.max_params)
-        # self.num_params_choice.setValue(self.num_params)
-        # self.num_params_choice.valueChanged.connect(self.change_param_num)
-        #
-        # layout.addRow("Number of Parameters:", self.num_params_choice)
-        #
-        # self.prefix_edit = QLineEdit(self)
-        # self.prefix_edit.setText(self.prefix)
-        #
-        # layout.addRow("Prefix:", self.prefix_edit)
-        #
-        # self.postfix_edit = QLineEdit(self)
-        # self.postfix_edit.setText(self.postfix)
-        #
-        # layout.addRow("Postfix:", self.postfix_edit)
-        #
-        # self.filename_edit = QLineEdit(self)
-        # self.filename_edit.setText(self.filename)
-        #
-        # layout.addRow("File name:", self.filename_edit)
-        #
-        # self.parameter_tabs = QTabWidget(self)
-        # self.parameter_tabs.setMovable(False)
-        # self.parameter_tabs.setTabsClosable(False)
-        # for param in self.parameters:
-        #     param.init_UI()
-        #
-        # layout.addRow(self.parameter_tabs)
-        #
-        # self.spin_box = QSpinBox(self)
-        # self.spin_box.setMinimum(1)
-        # spin_box_max_val = 1073741824
-        # self.spin_box.setMaximum(spin_box_max_val)
-        # self.spin_box.setValue(self.repetitions)
-        #
-        # layout.addRow("Repetitions:", self.spin_box)
-        #
-        self.scaling_choice = QComboBox(self)
-        self.scaling_choice.addItem("weak")
-        self.scaling_choice.addItem("strong")
-
-        layout.addRow("Scaling type:", self.scaling_choice)
-
-        self.progress_indicator = QProgressBar(self)
-        self.progress_indicator.hide()
-        layout.addRow(self.progress_indicator)
-
-        # If the user presses the enter key on any element it activates the
-        # first button somehow. Thus, create a fake button, that does nothing
-        # To avoid that any entry in the value list activates that OK button.
-        # fake_button = QPushButton(self)
-        # fake_button.setText("OK")
-        # fake_button.move(-1120, -1220)
-        # fake_button.hide()
-
-        # self.ok_button = QPushButton(self)
-        # self.ok_button.setText("OK")
-        # self.ok_button.clicked.connect(self.ok_button.hide)
-        # self.ok_button.pressed.connect(self.ok_pressed)
-        #
-        # layout.addRow(self.ok_button)
-
-        # cancel_button = QPushButton(self)
-        # cancel_button.setText("Cancel")
-        # cancel_button.pressed.connect(self.close)
-        main_layout.addRow(layout)
-        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
-        self.buttonBox.accepted.connect(self.accept)
-        self.buttonBox.rejected.connect(self.reject)
-        main_layout.addRow(self.buttonBox)
-
-        # self.change_param_num()
-        self.setLayout(main_layout)
-
-    # def change_param_num(self):
-    #     self.num_params = self.num_params_choice.value()
-    #     self.parameter_tabs.clear()
-    #     for param in self.parameters:
-    #         param.hide()
-    #     for index in range(0, self.num_params):
-    #         self.parameter_tabs.addTab(self.parameters[index],
-    #                                    "Parameter " + str(index + 1))
-    #         self.parameters[index].show()
-    #
-    #     self.autoFillOptions()
-    #     self.prefix_edit.setText(self.prefix)
-    #     self.postfix_edit.setText(self.postfix)
-    #     self.filename_edit.setText(self.filename)
-    #     self.spin_box.setValue(self.repetitions)
-    #     for index in range(0, self.num_params):
-    #         self.parameters[index].onNewValues()
-    #
-    #     self.update()
-    #
-    # def autoFillOptions(self):
-    #
-    #     auto_fill_rule = r"^([^.]+)"
-    #     for i in range(0, self.num_params):
-    #         auto_fill_rule = auto_fill_rule + r"([_\.][^\d]+)(\d+)"
-    #     auto_fill_rule = auto_fill_rule + r"[_\.]r(\d+)$"
-    #     auto_fill_regex = re.compile(auto_fill_rule)
-    #
-    #     # meaningful defaults
-    #     self.filename = "profile.cubex"
-    #     self.postfix = ""
-    #     self.prefix = ""
-    #     self.repetitions = 1
-    #
-    #     # get list of existing directories matching the pattern
-    #     available_dirs = os.listdir(self.dir_name)
-    #     dir_matches = list()
-    #     for d in available_dirs:
-    #         m = auto_fill_regex.match(d)
-    #         if m is not None:
-    #             dir_matches.append(m)
-    #
-    #     # print("matched directoty list with given pattern: ", dir_matches)
-    #
-    #     if len(dir_matches) == 0:
-    #         return
-    #
-    #     # get prefix from first match
-    #     self.prefix = dir_matches[0].group(1)
-    #     matching_prefix = [d for d in dir_matches if d.group(1) == self.prefix]
-    #
-    #     for i in range(0, self.num_params):
-    #         # get parameter name from first match
-    #         self.parameters[i].name = dir_matches[0].group(2 + i * 2)
-    #
-    #         # extract all values for parameter p
-    #         available_p_values = sorted(
-    #             set(int(m.group(3 + i * 2)) for m in matching_prefix))
-    #         self.parameters[i].values = ','.join(
-    #             str(v) for v in available_p_values)
-    #
-    #     # get maximum repetition count
-    #     max_repetitions = max(int(m.group(2 + self.num_params * 2))
-    #                           for m in matching_prefix)
-    #     self.repetitions = max_repetitions
-    @Slot()
-    def reject(self):
-        self._cancel_event.set()
-        super().reject()
-
-    @Slot()
-    def accept(self):
-
-        # self.prefix = self.prefix_edit.text()
-        # self.postfix = self.postfix_edit.text()
-        # self.filename = self.filename_edit.text()
-        # self.repetitions = self.spin_box.value()
-        self.scaling_type = self.scaling_choice.currentText()
-
-        with ProgressBar(total=0, gui=True) as pbar:
-            self._show_progressbar()
-            pbar.display = partial(self._display_progress, pbar)
-            pbar.sp = None
-
-            # read the cube files
-            try:
-                self.experiment = CubeFileReader2().read_cube_file(self.dir_name, self.scaling_type, pbar)
-            except Exception as err:
-                self.close()
-                raise err
-
-            if not self.experiment:
-                QMessageBox.critical(self,
-                                     "Error",
-                                     "Could not read Cube Files, may be corrupt!",
-                                     QMessageBox.Ok,
-                                     QMessageBox.Ok)
-                self.close()
-                return
-            self.valid = True
-
-            super().accept()
-
-    def _show_progressbar(self):
-        self.controls_layout.setEnabled(False)
-        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
-        self.progress_indicator.show()
-
-    def _display_progress(self, pbar: ProgressBar, msg=None, pos=None):
-        if self._cancel_event.is_set():
-            raise CancelProcessError()
-        self.progress_indicator.setMaximum(pbar.total)
-        self.progress_indicator.setValue(pbar.n)
-        QApplication.processEvents()
diff --git a/extrap/gui/DataDisplay.py b/extrap/gui/DataDisplay.py
index b49519d8..3e7e652a 100644
--- a/extrap/gui/DataDisplay.py
+++ b/extrap/gui/DataDisplay.py
@@ -5,15 +5,20 @@
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
+from __future__ import annotations
+
 from collections import defaultdict
+from typing import cast
 
 from PySide6.QtCore import *  # @UnusedWildImport
 from PySide6.QtGui import *  # @UnusedWildImport
 from PySide6.QtWidgets import *  # @UnusedWildImport
 
+from extrap.entities.coordinate import Coordinate
 from extrap.entities.parameter import Parameter
 from extrap.gui.AdvancedPlotWidget import AdvancedPlotWidget
-from extrap.gui.GraphWidget import GraphWidget, GraphWrapperWidget
+from extrap.gui.GraphWidget import GraphWrapperWidget
+from extrap.gui.plots.AbstractPlotWidget import AbstractPlotWidget
 from extrap.gui.plots.AllFunctionsAsDifferentSurfacePlotWidget import AllFunctionsAsDifferentSurfacePlot
 from extrap.gui.plots.AllFunctionsAsOneSurfacePlotWidget import AllFunctionsAsOneSurfacePlot
 from extrap.gui.plots.DominatingFunctionsAsSingleScatterPlotWidget import DominatingFunctionsAsSingleScatterPlot
@@ -23,9 +28,8 @@
 from extrap.gui.plots.MaxZAsSingleSurfacePlotWidget import MaxZAsSingleSurfacePlot
 from extrap.gui.plots.MeasurementPointsPlotWidget import MeasurementPointsPlot
 
-#####################################################################
 MIN_PARAM_VALUE = 0.01
-MAX_PARAM_VALUE = 2000000000
+MAX_PARAM_VALUE = float("inf")
 
 
 class AxisSelection(QWidget):
@@ -36,7 +40,7 @@ class AxisSelection(QWidget):
     """
     #####################################################################
 
-    max_values = [10, 10, 10]
+    max_values: list[float] = [10, 10, 10]
 
     def __init__(self, manager, parent, index: int, parameters):
         super(AxisSelection, self).__init__(parent)
@@ -73,7 +77,7 @@ def initUI(self, parameters):
 
         label2 = QLabel("max.")
         label2.setMinimumWidth(40)
-        label2.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+        label2.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
         self.max_edit = QDoubleSpinBox()
         self.max_edit.setMinimum(MIN_PARAM_VALUE)
         self.max_edit.setMinimumHeight(25)
@@ -87,7 +91,7 @@ def initUI(self, parameters):
 
         self.grid.addWidget(label1, 0, 0)
         self.grid.addWidget(self.combo_box, 0, 1)
-        self.grid.addWidget(label2, 0, 2, Qt.AlignRight)
+        self.grid.addWidget(label2, 0, 2, Qt.AlignmentFlag.AlignRight)
         self.grid.addWidget(self.max_edit, 0, 3)
         self.grid.setContentsMargins(QMargins(0, 0, 0, 0))
 
@@ -157,7 +161,7 @@ def getValue(self):
         return self.max_edit.value()
 
     def switchParameter(self, newParam):
-        i = self.combo_box.findText(newParam, Qt.MatchExactly)
+        i = self.combo_box.findText(newParam, Qt.MatchFlag.MatchExactly)
         self.combo_box.setCurrentIndex(i)
 
 
@@ -192,7 +196,7 @@ def initUI(self):
         self.parameter_label.setMinimumWidth(100)
         label2 = QLabel("Value:")
         label2.setMinimumWidth(40)
-        label2.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+        label2.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
 
         self.value_edit = QDoubleSpinBox()
         self.value_edit.setMinimum(MIN_PARAM_VALUE)
@@ -203,7 +207,7 @@ def initUI(self):
 
         self.grid.addWidget(label0, 0, 0)
         self.grid.addWidget(self.parameter_label, 0, 1)
-        self.grid.addWidget(label2, 0, 2, Qt.AlignRight)
+        self.grid.addWidget(label2, 0, 2, Qt.AlignmentFlag.AlignRight)
         self.grid.addWidget(self.value_edit, 0, 3)
         self.grid.setContentsMargins(QMargins(0, 0, 0, 0))
         self.setLayout(self.grid)
@@ -262,7 +266,7 @@ class DataDisplayManager(QWidget):
     def __init__(self, main_widget, parent):
         super(DataDisplayManager, self).__init__(parent)
         self.main_widget = main_widget
-        self.axis_selections = list()
+        self.axis_selections: list[AxisSelection] = list()
         self.value_selections = list()
         self._experiment = None
         self.parameters = []
@@ -342,7 +346,7 @@ def experimentChange(self):
             return
         if self._experiment is None or self._experiment != experiment:
             self._experiment = experiment
-            max_coordinate = max(experiment.coordinates)
+            max_coordinate: Coordinate = max(experiment.coordinates)
             for i, pos in enumerate(max_coordinate):
                 if i < 3:
                     AxisSelection.max_values[i] = pos * 1.2
@@ -387,14 +391,12 @@ def parameterSelected(self, index, newName, oldName):
                 return
 
     def updateWidget(self):
-        display = self.display_widget.currentWidget()
+        display = cast(AbstractPlotWidget, self.display_widget.currentWidget())
         if not display:
             return
         display.drawGraph()
 
 
-
-
 class GraphLimitsWidget(QWidget):
     def __init__(self, parent, data_display: DataDisplayManager):
         super().__init__(parent)
@@ -423,7 +425,7 @@ def generateSelections(self, parameters):
             self._placeholder.deleteLater()
             self._placeholder = None
 
-        num_axis = self.display_widget.currentWidget().getNumAxis()
+        num_axis = cast(AbstractPlotWidget, self.display_widget.currentWidget()).getNumAxis()
         for axis in self.axis_selections:
             axis.clearAxisLayout()
         del self.axis_selections[:]
diff --git a/extrap/gui/GraphWidget.py b/extrap/gui/GraphWidget.py
index 05116b5b..6ae374e0 100644
--- a/extrap/gui/GraphWidget.py
+++ b/extrap/gui/GraphWidget.py
@@ -8,18 +8,18 @@
 from __future__ import annotations
 
 import importlib.resources
+import itertools
 import math
 import typing
 
 import numpy
-from PySide6 import QtGui
 from PySide6.QtCore import *  # @UnusedWildImport
 from PySide6.QtGui import *  # @UnusedWildImport
 from PySide6.QtWidgets import *  # @UnusedWildImport
 
-from extrap.gui.plots.AbstractPlotWidget import AbstractPlotWidget
 from extrap.gui.Utils import formatFormula
 from extrap.gui.Utils import formatNumber
+from extrap.gui.plots.AbstractPlotWidget import AbstractPlotWidget
 
 if typing.TYPE_CHECKING:
     from extrap.gui.MainWidget import MainWidget
@@ -263,9 +263,7 @@ def exportData(self):
             callpath_name = model.callpath.name
             data_points = [p for (_, p) in self.calculateDataPoints(model, True)]
             parameters = self.main_widget.getExperiment().parameters
-            model_function_text = 'Model: ' + \
-                                  formatFormula(
-                                      model.hypothesis.function.to_string(*parameters))
+            model_function_text = 'Model: ' + formatFormula(model.hypothesis.function.to_string(*parameters))
 
             data_points_text = '\n'.join(
                 ('(' + str(x) + ', ' + str(y) + ')') for (x, y) in data_points)
@@ -421,31 +419,33 @@ def drawLegend(self, paint):
     def drawModel(self, paint, model, color):
         function = model.hypothesis.function
 
-        cord_list = self.calculate_function(function, self.graph_width)
+        cord_lists = self.calculate_function(function, self.graph_width)
 
         pen = QPen(QColor(color))
         pen.setWidth(2)
         paint.setPen(pen)
 
-        points = [
-            QPointF(self.logicalXtoPixel(x), self.logicalYtoPixel(y)) for x, y in cord_list
-        ]
-        paint.drawPolyline(points)
+        for cord_list in cord_lists:
+            points = [
+                QPointF(self.logicalXtoPixel(x), self.logicalYtoPixel(y)) for x, y in cord_list
+            ]
+            paint.drawPolyline(points)
 
     def drawAggregratedModel(self, paint, model_list):
         functions = list()
         for model in model_list:
             function = model.hypothesis.function
             functions.append(function)
-        cord_list = self.calculate_aggregate_callpath_function(
+        cord_lists = self.calculate_aggregate_callpath_function(
             functions, self.graph_width)
         pen = QPen(self.AGGREGATE_MODEL_COLOR)
         pen.setWidth(2)
         paint.setPen(pen)
-        points = [
-            QPointF(self.logicalXtoPixel(x), self.logicalYtoPixel(y)) for x, y in cord_list
-        ]
-        paint.drawPolyline(points)
+        for cord_list in cord_lists:
+            points = [
+                QPointF(self.logicalXtoPixel(x), self.logicalYtoPixel(y)) for x, y in cord_list
+            ]
+            paint.drawPolyline(points)
 
     def drawAxis(self, paint, selectedMetric):
         # Determing the number of divisions to be marked on x axis such that there is a minimum distance of 100 pixels
@@ -555,7 +555,7 @@ def drawAxis(self, paint, selectedMetric):
             for x_value in range((x_origin + 7), x_other_end, +3):
                 paint.drawPoint(x_value, y)
 
-    def calculate_function(self, function, length_x_axis):
+    def calculate_function(self, function, length_x_axis, x_min=None, x_max=None):
         """
          This function calculates the x values,
          based on the range that were provided &
@@ -563,7 +563,10 @@ def calculate_function(self, function, length_x_axis):
         """
 
         # m_x_lower_bound = 1
-        number_of_x_points, x_list, x_values = self._calculate_evaluation_points(length_x_axis)
+        if x_min != None and x_max != None:
+            number_of_x_points, x_list, x_values = self._calculate_evaluation_points(length_x_axis, x_min, x_max)
+        else:
+            number_of_x_points, x_list, x_values = self._calculate_evaluation_points(length_x_axis)
         previous = numpy.seterr(invalid='ignore', divide='ignore')
         y_list = function.evaluate(x_list).reshape(-1)
         numpy.seterr(**previous)
@@ -592,15 +595,18 @@ def calculate_aggregate_callpath_function(self, functions, length_x_axis):
     def _create_drawing_iterator(self, x_values, y_list):
         y_list[y_list == math.inf] = numpy.max(y_list[y_list != math.inf])
         y_list[y_list == -math.inf] = numpy.min(y_list[y_list != -math.inf])
+
         cord_list_before_filtering = zip(x_values, y_list)
-        cord_list = ((x, y)
-                     for x, y in cord_list_before_filtering
-                     if not math.isnan(y) and 0 <= y)
+        cord_groups = itertools.groupby(cord_list_before_filtering, key=lambda k: not math.isnan(k[1]) and 0 <= k[1])
+        cord_list = (v for k, v in cord_groups if k)
         return cord_list
 
-    def _calculate_evaluation_points(self, length_x_axis):
+    def _calculate_evaluation_points(self, length_x_axis, x_min=None, x_max=None):
         number_of_x_points = int(length_x_axis / 2)
-        x_values = numpy.linspace(0, self.max_x, number_of_x_points)
+        if x_min != None and x_max != None:
+            x_values = numpy.linspace(x_min, x_max, number_of_x_points)
+        else:
+            x_values = numpy.linspace(0, self.max_x, number_of_x_points)
         x_list = numpy.ndarray((len(self.main_widget.getExperiment().parameters), number_of_x_points))
         param = self.main_widget.data_display.getAxisParameter(0).id
         parameter_value_list = self.main_widget.data_display.getValues()
@@ -672,8 +678,8 @@ def reduce_length(value):
     def calculateDataPoints(self, model, ignore_limit=False):
         """ This function calculates datapoints to be marked on the graph
         """
-
         datapoints = model.measurements
+
         parameter_datapoint = self.main_widget.data_display.getAxisParameter(0).id
         datapoint_x_absolute_pos_list = list()
         datapoint_y_absolute_pos_list = list()
@@ -777,17 +783,32 @@ def calculateMaxY(self, modelList):
         return y_max
 
     def showOutlierPoints(self, paint, selected_model):
-        datapoints = selected_model.measurements
-        parameter_datapoint = self.main_widget.data_display.getAxisParameter(0).id
-        for datapoint in datapoints:
-            x_value = datapoint.coordinate[parameter_datapoint]
-            if x_value <= self.max_x:
-                y_min_value = datapoint.minimum
-                y_max_value = datapoint.maximum
-                y_mean_value = datapoint.mean
-                y_median_value = datapoint.median
-                self.plotOutliers(paint, x_value, y_min_value,
-                                  y_mean_value, y_median_value, y_max_value)
+        if isinstance(selected_model, list):
+            parameter_datapoint = self.main_widget.data_display.getAxisParameter(0).id
+            for i in range(len(selected_model)):
+                datapoints = selected_model[i].measurements
+                for datapoint in datapoints:
+                    x_value = datapoint.coordinate[parameter_datapoint]
+                    if x_value <= self.max_x:
+                        y_min_value = datapoint.minimum
+                        y_max_value = datapoint.maximum
+                        y_mean_value = datapoint.mean
+                        y_median_value = datapoint.median
+                        self.plotOutliers(paint, x_value, y_min_value,
+                                          y_mean_value, y_median_value, y_max_value)
+
+        else:
+            datapoints = selected_model.measurements
+            parameter_datapoint = self.main_widget.data_display.getAxisParameter(0).id
+            for datapoint in datapoints:
+                x_value = datapoint.coordinate[parameter_datapoint]
+                if x_value <= self.max_x:
+                    y_min_value = datapoint.minimum
+                    y_max_value = datapoint.maximum
+                    y_mean_value = datapoint.mean
+                    y_median_value = datapoint.median
+                    self.plotOutliers(paint, x_value, y_min_value,
+                                      y_mean_value, y_median_value, y_max_value)
 
     def plotOutliers(self, paint, x_value, y_min_value, y_mean_value, y_median_value, y_max_value):
         # create cordinates and merge them to list
diff --git a/extrap/gui/ImportOptionsDialog.py b/extrap/gui/ImportOptionsDialog.py
new file mode 100644
index 00000000..1fdef3d7
--- /dev/null
+++ b/extrap/gui/ImportOptionsDialog.py
@@ -0,0 +1,91 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+from __future__ import annotations
+
+from PySide6.QtCore import *  # @UnusedWildImport
+from PySide6.QtWidgets import *  # @UnusedWildImport
+
+from extrap.fileio.file_reader import FileReader
+from extrap.gui.components.ProgressWindow import ProgressWidget
+from extrap.gui.components.dynamic_options import DynamicOptionsWidget
+from extrap.util.dynamic_options import DynamicOptions
+
+
+class ImportOptionsDialog(QDialog):
+
+    def __init__(self, parent, reader: FileReader | DynamicOptions, path):
+        super(ImportOptionsDialog, self).__init__(parent)
+
+        self.experiment = None
+        self.scaling_choice = None
+        self.reader = reader
+        self.path = path
+
+        self.init_UI()
+
+    def init_UI(self):
+        self.setWindowTitle("Import Options")
+        self.setWindowModality(Qt.WindowModality.WindowModal)
+        self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
+        self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, False)
+
+        layout = QFormLayout(self)
+        layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
+
+        self.options_widget = DynamicOptionsWidget(self, self.reader)
+        self.options_widget.layout().setContentsMargins(0, 0, 0, 0)
+        layout.addRow(self.options_widget)
+
+        self.progress_widget = ProgressWidget(self)
+        self.progress_widget.setContentsMargins(0, 10, 0, 0)
+        layout.addRow(self.progress_widget)
+
+        self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
+                                          QDialogButtonBox.StandardButton.Cancel |
+                                          QDialogButtonBox.StandardButton.RestoreDefaults)
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.RestoreDefaults).clicked.connect(
+            self.options_widget.reset_options)
+        layout.addRow(self.buttonBox)
+
+        # self.change_param_num()
+        self.setLayout(layout)
+
+    @Slot()
+    def reject(self):
+        self.progress_widget.cancel()
+        super().reject()
+
+    @Slot()
+    def accept(self):
+        with self.progress_widget.get_progress_bar() as pbar:
+            self._on_show_progressbar()
+
+            # read the experiment from the files
+            try:
+                self.experiment = self.reader.read_experiment(self.path, pbar)
+            except Exception as err:
+                self.close()
+                raise err
+
+            if not self.experiment:
+                gui_action = self.reader.GUI_ACTION.replace('&', '')
+                gui_action = gui_action[0].lower() + gui_action[1:]
+                QMessageBox.critical(self,
+                                     "Error",
+                                     f"Could not {gui_action}, files may be corrupt!",
+                                     QMessageBox.StandardButton.Ok,
+                                     QMessageBox.StandardButton.Ok)
+                self.close()
+                return
+            super().accept()
+
+    def _on_show_progressbar(self):
+        self.options_widget.setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.RestoreDefaults).setEnabled(False)
diff --git a/extrap/gui/MainWidget.py b/extrap/gui/MainWidget.py
index 0f0b57fc..91fe68d7 100644
--- a/extrap/gui/MainWidget.py
+++ b/extrap/gui/MainWidget.py
@@ -9,11 +9,12 @@
 import logging
 import signal
 import sys
-from urllib.error import URLError, HTTPError
 from enum import Enum
 from functools import partial
+from numbers import Number
 from pathlib import Path
-from typing import Optional, Sequence, Tuple
+from typing import Optional, Sequence, Tuple, Type
+from urllib.error import URLError, HTTPError
 
 from PySide6 import QtGui
 from PySide6.QtCore import *  # @UnusedWildImport
@@ -24,22 +25,28 @@
 from extrap.entities.calltree import Node
 from extrap.entities.experiment import Experiment
 from extrap.entities.model import Model
+from extrap.entities.scaling_type import ScalingType
+from extrap.fileio import io_helper
 from extrap.fileio.experiment_io import read_experiment, write_experiment
-from extrap.fileio.file_reader import all_readers
+from extrap.fileio.file_reader import all_readers, FileReader
+from extrap.fileio.file_reader.abstract_directory_reader import AbstractScalingConversionReader
 from extrap.fileio.file_reader.cube_file_reader2 import CubeFileReader2
 from extrap.gui.ColorWidget import ColorWidget
-from extrap.gui.CubeFileReader import CubeFileReader
 from extrap.gui.DataDisplay import DataDisplayManager, GraphLimitsWidget
+from extrap.gui.ImportOptionsDialog import ImportOptionsDialog
 from extrap.gui.LogWidget import LogWidget
 from extrap.gui.ModelerWidget import ModelerWidget
 from extrap.gui.PlotTypeSelector import PlotTypeSelector
-from extrap.gui.components.ProgressWindow import ProgressWindow
 from extrap.gui.SelectorWidget import SelectorWidget
+from extrap.gui.StrongScalingConversionDialog import StrongScalingConversionDialog
 from extrap.gui.components import file_dialog
+from extrap.gui.components.ProgressWindow import ProgressWindow
 from extrap.gui.components.model_color_map import ModelColorMap
 from extrap.gui.components.plot_formatting_options import PlotFormattingOptions, PlotFormattingDialog
 from extrap.modelers.model_generator import ModelGenerator
 from extrap.util.deprecation import deprecated
+from extrap.util.dynamic_options import DynamicOptions
+from extrap.gui.MeasurementWizardWidget import MeasurementWizardWidget
 
 _SETTING_CHECK_FOR_UPDATES_ON_STARTUP = 'check_for_updates_on_startup'
 
@@ -63,8 +70,6 @@ def __init__(self, *args, **kwargs):
 
         self.settings = QSettings(QSettings.Scope.UserScope, "Extra-P", "Extra-P GUI")
 
-        status = self.settings.status()
-
         self.max_value = 0
         self.min_value = 0
         self.old_x_pos = 0
@@ -72,7 +77,7 @@ def __init__(self, *args, **kwargs):
         self.model_color_map = ModelColorMap()
         self.plot_formatting_options = PlotFormattingOptions()
         self.experiment_change = True
-        self.initUI()
+        self._init_ui()
         signal.signal(signal.SIGINT, signal.SIG_DFL)
 
         # switch for using mean or median measurement values for modeling
@@ -83,7 +88,7 @@ def __init__(self, *args, **kwargs):
             self._macos_update_title_bar()
 
     # noinspection PyAttributeOutsideInit
-    def initUI(self):
+    def _init_ui(self):
         """
         Initializes the User Interface of the extrap widget. E.g. the menus.
         """
@@ -93,15 +98,15 @@ def initUI(self):
         # self.statusBar()
 
         # Main splitter
-        self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
-        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
+        self.setCorner(Qt.Corner.BottomRightCorner, Qt.DockWidgetArea.RightDockWidgetArea)
+        self.setCorner(Qt.Corner.BottomLeftCorner, Qt.DockWidgetArea.LeftDockWidgetArea)
         self.setDockNestingEnabled(True)
 
         # Left side: Callpath and metric selection
         dock = QDockWidget("Selection", self)
         self.selector_widget = SelectorWidget(self, dock)
         dock.setWidget(self.selector_widget)
-        self.addDockWidget(Qt.LeftDockWidgetArea, dock)
+        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)
 
         # middle: Graph
 
@@ -112,18 +117,24 @@ def initUI(self):
         dock = QDockWidget("Modeler", self)
         self.modeler_widget = ModelerWidget(self, dock)
         dock.setWidget(self.modeler_widget)
-        self.addDockWidget(Qt.RightDockWidgetArea, dock)
+        self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
+
+        # Right side: Measurement Wizard
+        dock = QDockWidget("Measurement Point Selection Wizard", self)
+        self.measurementWizard_widget = MeasurementWizardWidget(self, dock)
+        dock.setWidget(self.measurementWizard_widget)
+        self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
 
         # bottom widget
         dock = QDockWidget("Color Info", self)
         self.color_widget = ColorWidget()
         dock.setWidget(self.color_widget)
-        self.addDockWidget(Qt.LeftDockWidgetArea, dock)
+        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)
 
         dock = QDockWidget("Graph Limits", self)
         self.graph_limits_widget = GraphLimitsWidget(self, self.data_display)
         dock.setWidget(self.graph_limits_widget)
-        self.addDockWidget(Qt.BottomDockWidgetArea, dock, Qt.Horizontal)
+        self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock, Qt.Orientation.Horizontal)
 
         dock2 = QDockWidget("Log", self)
         self.log_widget = LogWidget(self)
@@ -139,29 +150,22 @@ def initUI(self):
         screenshot_action.triggered.connect(self.screenshot)
 
         exit_action = QAction('E&xit', self)
-        exit_action.setShortcut(QKeySequence.Quit)
+        exit_action.setShortcut(QKeySequence.StandardKey.Quit)
         exit_action.setStatusTip('Exit application')
         exit_action.triggered.connect(self.close)
 
         file_imports = []
         for reader in all_readers.values():
-            if reader is CubeFileReader2:
-                file_imports.append((reader.GUI_ACTION, reader.DESCRIPTION, self.open_cube_file))
-            else:
-                file_mode = QFileDialog.FileMode.Directory if reader.LOADS_FROM_DIRECTORY else None
-                file_imports.append((reader.GUI_ACTION, reader.DESCRIPTION,
-                                     self._make_import_func(reader.DESCRIPTION, reader().read_experiment,
-                                                            filter=reader.FILTER, file_mode=file_mode,
-                                                            model=reader.GENERATE_MODELS_AFTER_LOAD)))
+            file_imports.append((reader.GUI_ACTION, reader.DESCRIPTION, self._make_import_func(reader)))
 
         open_experiment_action = QAction('&Open experiment', self)
         open_experiment_action.setStatusTip('Opens experiment file')
-        open_experiment_action.setShortcut(QKeySequence.Open)
+        open_experiment_action.setShortcut(QKeySequence.StandardKey.Open)
         open_experiment_action.triggered.connect(self.open_experiment)
 
         save_experiment_action = QAction('&Save experiment', self)
         save_experiment_action.setStatusTip('Saves experiment file')
-        save_experiment_action.setShortcut(QKeySequence.Save)
+        save_experiment_action.setShortcut(QKeySequence.StandardKey.Save)
         save_experiment_action.triggered.connect(self.save_experiment)
         save_experiment_action.setEnabled(False)
         self.save_experiment_action = save_experiment_action
@@ -200,10 +204,7 @@ def initUI(self):
         metric_delete_action = QAction('Dele&te metrics', self)
         metric_delete_action.triggered.connect(self.selector_widget.delete_metric)
 
-        # compare menu
-        compare_action = QAction('&Compare with experiment', self)
-        compare_action.setStatusTip('Compare the current models with ')
-        compare_action.triggered.connect(self.selector_widget.model_delete)
+
 
         # Filter menu
         # filter_callpath_action = QAction('Filter Callpaths', self)
@@ -304,6 +305,7 @@ def set_experiment(self, experiment, file_name="", *, compared=False):
         self.selector_widget.on_experiment_changed()
         self.data_display.experimentChange()
         self.modeler_widget.experimentChanged()
+        self.measurementWizard_widget.experimentChanged()
         self.experiment_change = False
         self.updateMinMaxValue()
         self.update()
@@ -315,18 +317,18 @@ def on_selection_changed(self):
             self.updateMinMaxValue()
 
     def keyPressEvent(self, e):
-        if e.key() == Qt.Key_Escape:
+        if e.key() == Qt.Key.Key_Escape:
             self.close()
 
     def closeEvent(self, event):
         if not self.windowFilePath():
             event.accept()
             return
-        msg_box = QMessageBox(QMessageBox.Question, 'Quit', "Are you sure to quit?",
-                              QMessageBox.No | QMessageBox.Yes, self, Qt.Sheet)
-        msg_box.setDefaultButton(QMessageBox.No)
+        msg_box = QMessageBox(QMessageBox.Icon.Question, 'Quit', "Are you sure to quit?",
+                              QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes, self, Qt.WindowType.Sheet)
+        msg_box.setDefaultButton(QMessageBox.StandardButton.No)
 
-        if msg_box.exec() == QMessageBox.Yes:
+        if msg_box.exec() == QMessageBox.StandardButton.Yes:
             event.accept()
         else:
             event.ignore()
@@ -347,7 +349,7 @@ def get_selected_models(self) -> Tuple[Optional[Sequence[Model]], Optional[Seque
         return self.selector_widget.get_selected_models()
 
     def open_plot_format_dialog_box(self):
-        dialog = PlotFormattingDialog(self.plot_formatting_options, self, Qt.Sheet)
+        dialog = PlotFormattingDialog(self.plot_formatting_options, self, Qt.WindowType.Sheet)
         if dialog.exec() == QDialog.DialogCode.Accepted:
             self.data_display.updateWidget()
             self.update()
@@ -398,16 +400,65 @@ def model_experiment(self, experiment, file_name=""):
             model_generator.model_all(pbar)
         self.set_experiment(experiment, file_name)
 
-    def _make_import_func(self, title, reader_func, **kwargs):
-        return partial(self.import_file, reader_func, title, **kwargs)
+    def _make_import_func(self, reader_class: Type[FileReader]):
+        file_mode = QFileDialog.FileMode.Directory if reader_class.LOADS_FROM_DIRECTORY else None
+        title = reader_class.DESCRIPTION if reader_class.DESCRIPTION else 'Open File'
+
+        if issubclass(reader_class, DynamicOptions):
+            def _import_function():
+                def _process_with_settings(path):
+                    reader: FileReader = reader_class()
+                    dialog = ImportOptionsDialog(self, reader, path)
+                    dialog.setWindowFlag(Qt.WindowType.Sheet, True)
+                    dialog.setModal(True)
+                    # do not use open, wait for loading to finish
+                    if dialog.exec() == QDialog.DialogCode.Accepted:
+                        if reader_class.GENERATE_MODELS_AFTER_LOAD:
+                            experiment = self._check_and_convert_scaling(dialog.experiment, path, reader)
+                            self.model_experiment(experiment, path)
+                        else:
+                            self.set_experiment(dialog.experiment, path)
+
+                file_dialog.show(self, _process_with_settings, title, filter=reader_class.FILTER, file_mode=file_mode)
+
+            return _import_function
+
+        else:
+            reader: FileReader = reader_class()
+            return partial(self.import_file, reader.read_experiment, title, filter=reader_class.FILTER,
+                           model=reader_class.GENERATE_MODELS_AFTER_LOAD, file_mode=file_mode, reader=reader)
+
+    def _check_and_convert_scaling(self, experiment, path, reader=None):
+        check_res = io_helper.check_for_strong_scaling(experiment)
+        if not check_res or max(check_res) == 0:
+            return experiment
+        if isinstance(reader, AbstractScalingConversionReader):
+            if StrongScalingConversionDialog.pose_question_for_readers_with_scaling_conversion(
+                    self) == QMessageBox.StandardButton.Yes:
+                reader.scaling_type = ScalingType.STRONG
+                dialog = ImportOptionsDialog(self, reader, path)
+                dialog.setWindowFlag(Qt.WindowType.Sheet, True)
+                dialog.setModal(True)
+                dialog.open()
+                dialog.accept()
+
+                return dialog.experiment
+        else:
+            dialog = StrongScalingConversionDialog(experiment, check_res, self)
+            dialog.setWindowFlag(Qt.WindowType.Sheet, True)
+            dialog.setModal(True)
+            if dialog.exec() == QDialog.DialogCode.Accepted:
+                return dialog.experiment
+        return experiment
 
     def import_file(self, reader_func, title='Open File', filter='', model=True, progress_text="Loading File",
-                    file_name=None, file_mode=None):
+                    file_name=None, file_mode=None, reader=None):
         def _import_file(file_name):
             with ProgressWindow(self, progress_text) as pw:
                 experiment = reader_func(file_name, pw)
                 # call the modeler and create a function model
                 if model:
+                    experiment = self._check_and_convert_scaling(experiment, file_name, reader)
                     self.model_experiment(experiment, file_name)
                 else:
                     self.set_experiment(experiment, file_name)
@@ -440,16 +491,9 @@ def _save(file_name):
 
         file_dialog.showSave(self, _save, 'Save Experiment', filter='Experiments (*.extra-p)')
 
+    @deprecated
     def open_cube_file(self):
-        def _process_cube(dir_name):
-            dialog = CubeFileReader(self, dir_name)
-            dialog.setWindowFlag(Qt.Sheet, True)
-            dialog.setModal(True)
-            dialog.exec()  # do not use open, wait for loading to finish
-            if dialog.valid:
-                self.model_experiment(dialog.experiment, dir_name)
-
-        file_dialog.showOpenDirectory(self, _process_cube, 'Select a Directory with a Set of CUBE Files')
+        self._make_import_func(CubeFileReader2)()
 
     def updateMinMaxValue(self):
         if not self.experiment_change:
@@ -503,7 +547,7 @@ def show_about_dialog(self):
         layout.addWidget(QLabel(' — '), same_row, 2, 1, 1)
 
         check_for_updates = QCheckBox(self)
-        check_for_updates.setChecked(self.settings.value(_SETTING_CHECK_FOR_UPDATES_ON_STARTUP, True, bool))
+        check_for_updates.setChecked(bool(self.settings.value(_SETTING_CHECK_FOR_UPDATES_ON_STARTUP, True, bool)))
         check_for_updates.setText("Check for updates on start up")
         check_for_updates.toggled.connect(
             lambda status: self.settings.setValue(_SETTING_CHECK_FOR_UPDATES_ON_STARTUP, status))
diff --git a/extrap/gui/MeasurementWizardWidget.py b/extrap/gui/MeasurementWizardWidget.py
new file mode 100644
index 00000000..feacb10f
--- /dev/null
+++ b/extrap/gui/MeasurementWizardWidget.py
@@ -0,0 +1,725 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import numpy as np
+from PySide6.QtWidgets import *  # @UnusedWildImport
+from PySide6.QtGui import QIntValidator
+from extrap.gpr.measurement_point_advisor import MeasurementPointAdvisor
+
+
+class MeasurementWizardWidget(QWidget):
+    
+    def __init__(self, mainWidget, parent):
+        super(MeasurementWizardWidget, self).__init__(parent)
+        self.main_widget = mainWidget
+        self.main_layout = QHBoxLayout(self)
+        self.init_empty()
+        self.calculate_cost_manual = False
+        self.experiment = None
+        self.mean_noise_level = 0.0
+        self.initialized = False
+        self.processes_parameter_id = 0
+        self.no_model_parameters = 0
+        self.budget = 0.0
+        self.current_cost = 0.0
+        self.manual_pms_selection = False
+        self.parameter_value_series = []
+
+
+    def init_empty(self):
+        self.setLayout(self.main_layout)
+        self.empty_label = QLabel(self)
+        self.empty_label.setText("Please load a performance experiment.")
+        self.main_layout.addWidget(self.empty_label)
+
+    
+    def init_multi_parameter(self):
+        self.initialized = True
+
+        self.no_model_parameters = len(self.experiment.parameters)
+    
+        self.main_layout.removeWidget(self.empty_label)
+        if self.empty_label is not None:
+            self.empty_label.deleteLater()
+            self.empty_label = None
+
+        self.layout = QGridLayout(self)
+        self.layout.setRowStretch(99, 1)
+        self.layout.setColumnStretch(0, 0)
+        self.layout.setColumnStretch(1, 1)
+
+        self.main_layout.addLayout(self.layout)
+
+        self.checkbox = QCheckBox("No. of processes (MPI ranks)\n is one of the model parameters", self)
+        self.checkbox.setChecked(True)
+        self.checkbox.toggled.connect(self.clickCheckbox)
+        self.layout.addWidget(self.checkbox, 0, 0)
+
+        self.parameter_selector_label = QLabel(self)
+        self.parameter_selector_label.setText("Model Paramater resembling the\n number of processes (MPI ranks):")
+        self.layout.addWidget(self.parameter_selector_label, 1, 0)
+
+        self.parameter_selector = QComboBox(self)
+        for parameter in self.experiment.parameters:
+            self.parameter_selector.addItem(str(parameter))
+        self.layout.addWidget(self.parameter_selector, 1, 1)
+        for i in range(len(self.experiment.parameters)):
+            if self.parameter_selector.currentText() == str(self.experiment.parameters[i]):
+                self.processes_parameter_id = i
+                break
+        self.parameter_selector.currentIndexChanged.connect(self.parameterChanged)
+
+        self.process_label = QLabel(self)
+        self.process_label.setText("No. of processes (MPI ranks):")
+        self.layout.addWidget(self.process_label, 2, 0)
+        self.process_label.setVisible(False)
+
+        self.processes = QLineEdit(self)
+        self.processes.setValidator(QIntValidator())
+        self.layout.addWidget(self.processes, 2, 1)
+        self.processes.setVisible(False)
+        self.processes.setText("2")
+        self.processes.textChanged.connect(self.processesChanged)
+
+        metric_label = QLabel(self)
+        metric_label.setText("Metric used for cost calculation:")
+        self.layout.addWidget(metric_label, 3, 0)
+
+        self.metric_selector = QComboBox(self)
+        for metric in self.experiment.metrics:
+            self.metric_selector.addItem(str(metric))
+        self.layout.addWidget(self.metric_selector, 3, 1)
+        self.metric_selector.currentIndexChanged.connect(self.metricChanged)
+
+        budget_label = QLabel(self)
+        budget_label.setText("Modeling Budget [core hours]:")
+        self.layout.addWidget(budget_label, 4, 0)
+
+        self.modeling_budget = QDoubleSpinBox(self)
+        self.modeling_budget.setMaximum(999999999999.0)
+        self.modeling_budget.setMinimum(0.0)
+        self.layout.addWidget(self.modeling_budget, 4, 1)
+        self.modeling_budget.textChanged.connect(self.budgetChanged)
+
+        current_cost_label = QLabel(self)
+        current_cost_label.setText("Current Measurement Cost [core hours]:")
+        self.layout.addWidget(current_cost_label, 5, 0)
+
+        self.current_cost_edit = QLineEdit(self)
+        self.current_cost_edit.setReadOnly(True)
+        self.current_cost_edit.setText("0.0")
+        self.layout.addWidget(self.current_cost_edit, 5, 1)
+
+        used_budget_label = QLabel(self)
+        used_budget_label.setText("Used Modeling Budget [%]:")
+        self.layout.addWidget(used_budget_label, 6, 0)
+
+        self.used_budget_edit = QLineEdit(self)
+        self.used_budget_edit.setReadOnly(True)
+        self.used_budget_edit.setText("0.0")
+        self.layout.addWidget(self.used_budget_edit, 6, 1)
+
+        noise_level_label = QLabel(self)
+        noise_level_label.setText("Measurement Noise Level [%]:")
+        self.layout.addWidget(noise_level_label, 7, 0)
+
+        self.noise_level_edit = QLineEdit(self)
+        self.noise_level_edit.setReadOnly(True)
+        noise_level_str = "{:.2f}".format(self.mean_noise_level)
+        self.noise_level_edit.setText(noise_level_str)
+        self.layout.addWidget(self.noise_level_edit, 7, 1)
+
+        self.parameter_value_checkbox = QCheckBox("Enter parameter-value series manually", self)
+        self.parameter_value_checkbox.setChecked(False)
+        self.parameter_value_checkbox.toggled.connect(self.clickParameterValueCheckbox)
+        self.layout.addWidget(self.parameter_value_checkbox, 8, 0)
+
+        self.param_value_layout = QGridLayout(self)
+        self.param_value_layout.setRowStretch(99, 1)
+        self.param_value_layout.setColumnStretch(0, 0)
+        self.param_value_layout.setColumnStretch(1, 1)
+        self.layout.addLayout(self.param_value_layout, 9, 0)
+
+        self.advise_button = QPushButton(self)
+        self.layout.addWidget(self.advise_button, 10, 0)
+        self.advise_button.setText("Suggest Additional Measurement Points")
+        self.advise_button.clicked.connect(self.advice_button_clicked)
+
+        suggestion_label = QLabel(self)
+        suggestion_label.setText("Measurement point suggestions:")
+        self.layout.addWidget(suggestion_label, 11, 0)
+
+        self.measurement_point_suggestions_label = QPlainTextEdit(self)
+        self.measurement_point_suggestions_label.setPlainText("")
+        self.measurement_point_suggestions_label.setReadOnly(True)
+        self.layout.addWidget(self.measurement_point_suggestions_label, 11, 1)
+
+        self.calculate_current_measurement_cost()
+        self.calculate_noise_level()
+
+
+    def init_single_parameter(self):
+        self.initialized = True
+        self.no_model_parameters = 1
+        self.main_layout.removeWidget(self.empty_label)
+        if self.empty_label is not None:
+            self.empty_label.deleteLater()
+            self.empty_label = None
+
+        self.layout = QGridLayout(self)
+        self.layout.setRowStretch(99, 1)
+        self.layout.setColumnStretch(0, 0)
+        self.layout.setColumnStretch(1, 1)
+
+        self.main_layout.addLayout(self.layout)
+
+        self.checkbox = QCheckBox("Model parameters is the no.\n of processes (MPI ranks)", self)
+        self.checkbox.setChecked(True)
+        self.checkbox.toggled.connect(self.clickCheckbox)
+        self.layout.addWidget(self.checkbox, 0, 0)
+        self.processes_parameter_id = 0
+
+        self.process_label = QLabel(self)
+        self.process_label.setText("No. of processes (MPI ranks):")
+        self.layout.addWidget(self.process_label, 1, 0)
+        self.process_label.setVisible(False)
+
+        self.processes = QLineEdit(self)
+        self.processes.setValidator(QIntValidator())
+        self.layout.addWidget(self.processes, 1, 1)
+        self.processes.setVisible(False)
+        self.processes.setText("2")
+        self.processes.textChanged.connect(self.processesChanged)
+
+        metric_label = QLabel(self)
+        metric_label.setText("Metric used for cost calculation:")
+        self.layout.addWidget(metric_label, 2, 0)
+
+        self.metric_selector = QComboBox(self)
+        for metric in self.experiment.metrics:
+            self.metric_selector.addItem(str(metric))
+        self.layout.addWidget(self.metric_selector, 2, 1)
+        self.metric_selector.currentIndexChanged.connect(self.metricChanged)
+
+        budget_label = QLabel(self)
+        budget_label.setText("Modeling Budget [core hours]:")
+        self.layout.addWidget(budget_label, 3, 0)
+
+        self.modeling_budget = QDoubleSpinBox(self)
+        self.modeling_budget.setMaximum(999999999999.0)
+        self.modeling_budget.setMinimum(0.0)
+        self.layout.addWidget(self.modeling_budget, 3, 1)
+        self.modeling_budget.textChanged.connect(self.budgetChanged)
+
+        current_cost_label = QLabel(self)
+        current_cost_label.setText("Current Measurement Cost [core hours]:")
+        self.layout.addWidget(current_cost_label, 4, 0)
+
+        self.current_cost_edit = QLineEdit(self)
+        self.current_cost_edit.setReadOnly(True)
+        self.current_cost_edit.setText("0.0")
+        self.layout.addWidget(self.current_cost_edit, 4, 1)
+
+        used_budget_label = QLabel(self)
+        used_budget_label.setText("Used Modeling Budget [%]:")
+        self.layout.addWidget(used_budget_label, 5, 0)
+
+        self.used_budget_edit = QLineEdit(self)
+        self.used_budget_edit.setReadOnly(True)
+        self.used_budget_edit.setText("0.0")
+        self.layout.addWidget(self.used_budget_edit, 5, 1)
+
+        noise_level_label = QLabel(self)
+        noise_level_label.setText("Measurement Noise Level [%]:")
+        self.layout.addWidget(noise_level_label, 6, 0)
+
+        self.noise_level_edit = QLineEdit(self)
+        self.noise_level_edit.setReadOnly(True)
+        noise_level_str = "{:.2f}".format(self.mean_noise_level)
+        self.noise_level_edit.setText(noise_level_str)
+        self.layout.addWidget(self.noise_level_edit, 6, 1)
+
+        self.parameter_value_checkbox = QCheckBox("Enter parameter-value series manually", self)
+        self.parameter_value_checkbox.setChecked(False)
+        self.parameter_value_checkbox.toggled.connect(self.clickParameterValueCheckbox)
+        self.layout.addWidget(self.parameter_value_checkbox, 7, 0)
+
+        self.param_value_layout = QGridLayout(self)
+        self.param_value_layout.setRowStretch(99, 1)
+        self.param_value_layout.setColumnStretch(0, 0)
+        self.param_value_layout.setColumnStretch(1, 1)
+        self.layout.addLayout(self.param_value_layout, 8, 0)
+
+        self.advise_button= QPushButton(self)
+        self.advise_button.setText("Suggest Additional Measurement Points")
+        self.layout.addWidget(self.advise_button, 9, 0)
+        self.advise_button.clicked.connect(self.advice_button_clicked)
+
+        suggestion_label = QLabel(self)
+        suggestion_label.setText("Measurement point suggestions:")
+        self.layout.addWidget(suggestion_label, 10, 0)
+
+        self.measurement_point_suggestions_label = QPlainTextEdit(self)
+        self.measurement_point_suggestions_label.setPlainText("")
+        self.measurement_point_suggestions_label.setReadOnly(True)
+        self.layout.addWidget(self.measurement_point_suggestions_label, 10, 1)
+
+        self.calculate_current_measurement_cost()
+        self.calculate_noise_level()
+
+
+    def calculate_noise_level(self):
+        # do an noise analysis on the existing measurement points
+        mm = self.experiment.measurements
+        metric_string = self.metric_selector.currentText()
+        metrics = self.experiment.metrics
+        runtime_metric = None
+        for metric in metrics:
+            if metric_string == str(metric):
+                runtime_metric = metric
+        selected_callpath = self.main_widget.get_selected_call_tree_nodes()
+
+        # if there is one callpath selected in the tree
+        if len(selected_callpath) == 1:
+            try:
+                nns = []
+                try:
+                    for meas in mm[(selected_callpath[0].path, runtime_metric)]:
+                        pps = []
+                        for val in meas.values:
+                            if np.mean(meas.values) == 0.0:
+                                pp = 0
+                            else:
+                                pp = abs((val / (np.mean(meas.values) / 100)) - 100)
+                            pps.append(pp)
+                        nns.append(np.mean(pps))
+                except KeyError:
+                    nns = [0.0]
+                self.mean_noise_level = np.mean(nns)
+            except TypeError:
+                self.mean_noise_level = 0.0
+
+        # if there is more than one callpath selected in the tree
+        elif len(selected_callpath) > 1:
+            measurements = self.experiment.measurements
+            try:
+                callpath_noise_levels = []
+                for callpath in selected_callpath:
+                    nns = []
+                    try:
+                        for meas in measurements[(callpath.path, runtime_metric)]:
+                            pps = []
+                            for val in meas.values:
+                                if np.mean(meas.values) == 0.0:
+                                    pp = 0
+                                else:
+                                    pp = abs((val / (np.mean(meas.values) / 100)) - 100)
+                                pps.append(pp)
+                            nns.append(np.mean(pps))
+                    except KeyError:
+                        nns = [0.0]
+                    callpath_noise_levels.append(np.mean(nns))
+                self.mean_noise_level = np.mean(callpath_noise_levels)
+            except TypeError:
+                self.mean_noise_level = 0.0
+            
+        # if there is no callpath selected in the tree
+        elif len(selected_callpath) == 0:
+            measurements = self.experiment.measurements
+            try:
+                callpath_noise_levels = []
+                for callpath in self.experiment.callpaths:
+                    nns = []
+                    try:
+                        for meas in measurements[(callpath, runtime_metric)]:
+                            pps = []
+                            for val in meas.values:
+                                if np.mean(meas.values) == 0.0:
+                                    pp = 0
+                                else:
+                                    pp = abs((val / (np.mean(meas.values) / 100)) - 100)
+                                pps.append(pp)
+                            nns.append(np.mean(pps))
+                    except KeyError:
+                        nns = [0.0]
+                    callpath_noise_levels.append(np.mean(nns))
+                self.mean_noise_level = np.mean(callpath_noise_levels)
+            except TypeError:
+                self.mean_noise_level = 0.0
+        noise_level_str = "{:.2f}".format(self.mean_noise_level)
+        self.noise_level_edit.setText(noise_level_str)
+
+
+    def calculate_used_budget(self):
+        self.budget = self.modeling_budget.value()
+        if self.budget == 0.0:
+            self.budget = self.current_cost
+        used_budget_percent = self.current_cost / (self.budget / 100)
+        used_budget_percent_str = "{:.2f}".format(used_budget_percent)
+        self.used_budget_edit.setText(str(used_budget_percent_str))
+
+
+    def calculate_current_measurement_cost(self):
+        current_cost_str = "0.0"
+        metric_string = self.metric_selector.currentText()
+        metrics = self.experiment.metrics
+        runtime_metric = None
+        for metric in metrics:
+            if metric_string == str(metric):
+                runtime_metric = metric
+        selected_callpath = self.main_widget.get_selected_call_tree_nodes()
+        
+        # if the model parameter is not the number of mpi ranks or processes
+        if self.calculate_cost_manual:
+            try:
+                number_processes = int(self.processes.text())
+            except ValueError:
+                number_processes = 1.0
+
+            # if there is only one callpath selected
+            if len(selected_callpath) == 1:
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                try:
+                    for measurement in measurements[(selected_callpath[0].path, runtime_metric)]:
+                        core_hours_per_point = 0
+                        try:
+                            for value in measurement.values:
+                                core_hours_per_point += value * number_processes
+                        except TypeError:
+                            core_hours_per_point += measurement.mean * number_processes
+                        core_hours_total += core_hours_per_point
+                except KeyError:
+                    pass
+
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+
+            # if there is no callpath selected
+            elif len(selected_callpath) == 0:
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                for callpath in self.experiment.callpaths:
+                    core_hours_callpath = 0
+                    try:
+                        for measurement in measurements[(callpath, runtime_metric)]:
+                            core_hours_per_point = 0
+                            try:
+                                for value in measurement.values:
+                                    core_hours_per_point += value * number_processes
+                            except TypeError:
+                                core_hours_per_point += measurement.mean * number_processes
+                            core_hours_callpath += core_hours_per_point
+                    except KeyError:
+                        pass
+                    core_hours_total += core_hours_callpath
+    
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+            
+            # if there are several callpaths selected
+            elif len(selected_callpath) > 1:
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                for callpath in selected_callpath:
+                    core_hours_callpath = 0
+                    try:
+                        for measurement in measurements[(callpath.path, runtime_metric)]:
+                            core_hours_per_point = 0
+                            try:
+                                for value in measurement.values:
+                                    core_hours_per_point += value * number_processes
+                            except TypeError:
+                                core_hours_per_point += measurement.mean * number_processes
+                            core_hours_callpath += core_hours_per_point
+                    except KeyError:
+                        pass
+                    core_hours_total += core_hours_callpath
+    
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+
+        # if the model parameter is the number of processes or mpi ranks
+        else:
+
+            # if there is only one callpath selected
+            if len(selected_callpath) == 1:
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                try:
+                    for measurement in measurements[(selected_callpath[0].path, runtime_metric)]:
+                        core_hours_per_point = 0
+                        parameter_values = measurement.coordinate.as_tuple()
+                        try:
+                            for value in measurement.values:
+                                if self.no_model_parameters == 1:
+                                    core_hours_per_point += value * parameter_values[0]
+                                else:
+                                    core_hours_per_point += value * parameter_values[self.processes_parameter_id]
+                        except TypeError:
+                            if self.no_model_parameters == 1:
+                                core_hours_per_point += measurement.mean * parameter_values[0]
+                            else:
+                                core_hours_per_point += measurement.mean * parameter_values[self.processes_parameter_id]
+                        core_hours_total += core_hours_per_point
+                except KeyError:
+                    pass
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+
+            # if there is no callpath selected
+            elif len(selected_callpath) == 0:
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                for callpath in self.experiment.callpaths:
+                    core_hours_callpath = 0
+                    try:
+                        for measurement in measurements[(callpath, runtime_metric)]:
+                            core_hours_per_point = 0
+                            parameter_values = measurement.coordinate.as_tuple()
+                            try:
+                                for value in measurement.values:
+                                    if self.no_model_parameters == 1:
+                                        core_hours_per_point += value * parameter_values[0]
+                                    else:
+                                        core_hours_per_point += value * parameter_values[self.processes_parameter_id]
+                            except TypeError:
+                                if self.no_model_parameters == 1:
+                                    core_hours_per_point += measurement.mean * parameter_values[0]
+                                else:
+                                    core_hours_per_point += measurement.mean * parameter_values[self.processes_parameter_id]
+                            core_hours_callpath += core_hours_per_point
+                    except KeyError:
+                        pass
+                    core_hours_total += core_hours_callpath
+    
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+            
+            # if there are several callpaths selected
+            elif len(selected_callpath) > 1:         
+                measurements = self.experiment.measurements
+                core_hours_total = 0
+                for callpath in selected_callpath:
+                    core_hours_callpath = 0
+                    try:
+                        for measurement in measurements[(callpath.path, runtime_metric)]:
+                            core_hours_per_point = 0
+                            parameter_values = measurement.coordinate.as_tuple()
+                            try:
+                                for value in measurement.values:
+                                    if self.no_model_parameters == 1:
+                                        core_hours_per_point += value * parameter_values[0]
+                                    else:
+                                        core_hours_per_point += value * parameter_values[self.processes_parameter_id]
+                            except TypeError:
+                                if self.no_model_parameters == 1:
+                                    core_hours_per_point += measurement.mean * parameter_values[0]
+                                else:
+                                    core_hours_per_point += measurement.mean * parameter_values[self.processes_parameter_id]
+                            core_hours_callpath += core_hours_per_point
+                    except KeyError:
+                        pass
+                    core_hours_total += core_hours_callpath
+    
+                self.current_cost = core_hours_total
+                current_cost_str = "{:.2f}".format(self.current_cost)
+
+        if self.budget == 0.0:
+            self.budget = self.current_cost
+            self.modeling_budget.setValue(self.current_cost)
+
+        self.current_cost_edit.setText(str(current_cost_str))
+
+
+    def advice_button_clicked(self):
+
+        # get the modeling budget from the GUI
+        if int(self.budget) <= int(self.current_cost):
+            self.measurement_point_suggestions_label.setPlainText("Not enough budget available to suggest further measurement points!")
+        
+        else:
+            # get the performance metric that should be used for the analysis
+            metric_string = self.metric_selector.currentText()
+            metrics = self.experiment.metrics
+            runtime_metric = None
+            for metric in metrics:
+                if metric_string == str(metric):
+                    runtime_metric = metric
+                
+            # get the selected callpath(s) in the tree
+            selected_callpath = self.main_widget.get_selected_call_tree_nodes()
+
+            # get the currently used model from the GUI
+            model_gen = self.main_widget.get_current_model_gen()
+            
+            # get parameter value series if manual selection
+            if self.manual_pms_selection:
+                for i in range(len(self.experiment.parameters)):
+                    self.parameter_value_series.append(self.line_edits[i].text())
+
+            if self.calculate_cost_manual:
+                try:
+                    number_processes = int(self.processes.text())
+                except ValueError as e:
+                    number_processes = 1
+                    print(e)
+            else:
+                number_processes = 0
+            
+            # initialize a new measurement point advisor object
+            mpa = MeasurementPointAdvisor(budget=self.budget, 
+                                        processes=self.processes_parameter_id, 
+                                        callpaths=selected_callpath,
+                                        metric=runtime_metric,
+                                        experiment=self.experiment,
+                                        current_cost=self.current_cost,
+                                        manual_pms_selection=self.manual_pms_selection,
+                                        manual_parameter_value_series=self.parameter_value_series,
+                                        calculate_cost_manual=self.calculate_cost_manual,
+                                        number_processes=number_processes,
+                                        main_widget=self.main_widget,
+                                        model_generator=model_gen)
+            
+            point_suggestions, rep_numbers = mpa.suggest_points()
+
+            if len(point_suggestions) == 0:
+                text = "No measurement points could be found that fit into the available budget. Please consider increasing the modeling budget."
+
+            else:
+                text = ""
+                for i in range(len(point_suggestions)):
+                    temp = ""
+                    temp += str(i+1) + "."
+                    temp += " P("
+                    parameter_values = point_suggestions[i].as_tuple()
+                    for j in range(len(self.experiment.parameters)):
+                        if j != 0:
+                            temp += ","
+                        temp += str(self.experiment.parameters[j])
+                        temp += "="
+                        parameter_value = parameter_values[j]
+                        temp += str(parameter_value)
+                    if rep_numbers is not None:
+                        rep_number = rep_numbers[i]
+                    else:
+                        rep_number = 1
+                    temp += "), repetition no.="
+                    temp += str(rep_number)
+                    temp += "\n"
+                    text += temp
+
+            self.measurement_point_suggestions_label.setPlainText(text)
+
+
+    def budgetChanged(self):
+        self.calculate_used_budget()
+
+
+    def parameterChanged(self):
+        for i in range(len(self.experiment.parameters)):
+            if str(self.experiment.parameters[i]) == self.parameter_selector.currentText():
+                self.processes_parameter_id = i
+                break
+        self.calculate_current_measurement_cost()
+        self.calculate_used_budget()
+
+
+    def metricChanged(self):
+        self.calculate_current_measurement_cost()
+        self.calculate_used_budget()
+
+
+    def processesChanged(self):
+        self.calculate_current_measurement_cost()
+        self.calculate_used_budget()
+
+
+    def clickCheckbox(self):
+        cbutton = self.sender()
+        if cbutton.isChecked():
+            self.process_label.setVisible(False)
+            self.calculate_cost_manual = False
+            for i in range(len(self.experiment.parameters)):
+                if str(self.experiment.parameters[i]) == self.parameter_selector.currentText():
+                    self.processes_parameter_id = i
+                    break
+            self.processes.setVisible(False)
+            self.parameter_selector_label.setVisible(True)
+            self.parameter_selector.setVisible(True)
+            self.calculate_current_measurement_cost()
+            self.calculate_used_budget()
+        else:
+            self.process_label.setVisible(True)
+            self.calculate_cost_manual = True
+            self.processes.setVisible(True)
+            self.processes_parameter_id = -1
+            self.parameter_selector.setVisible(False)
+            self.parameter_selector_label.setVisible(False)
+            self.calculate_current_measurement_cost()
+            self.calculate_used_budget()
+        
+
+    def clickParameterValueCheckbox(self):
+        cbutton = self.sender()
+        if cbutton.isChecked():
+            self.manual_pms_selection = True
+            self.line_edits = []
+            self.labels = []
+            for i in range(len(self.experiment.parameters)):
+                line_edit = QLineEdit(self)
+                self.param_value_layout.addWidget(line_edit, i, 1)
+                self.line_edits.append(line_edit)
+                label = QLabel(self)
+                label.setText("Value series for parameter "+str(str(self.experiment.parameters[i]))+":")
+                self.labels.append(label)
+                self.param_value_layout.addWidget(label, i, 0)
+        else:
+            self.manual_pms_selection = False
+            for i in range(len(self.line_edits)):
+                self.line_edits[i].setVisible(False)
+                self.labels[i].setVisible(False)
+  
+
+    def reset(self, model_parameters=1):
+    
+        # if there is only one model parameter
+        if model_parameters == 1:
+            if self.initialized == False:
+                self.init_single_parameter()
+            self.metric_selector.clear()
+            for metric in self.experiment.metrics:
+                self.metric_selector.addItem(str(metric))
+            self.repaint()
+            self.update()
+        
+        # if there are several model parameters
+        else:
+            if self.initialized == False:
+                self.init_multi_parameter()
+            self.metric_selector.clear()
+            for metric in self.experiment.metrics:
+                self.metric_selector.addItem(str(metric))
+            self.repaint()
+            self.update()
+        
+    
+    def experimentChanged(self):
+        
+        self.experiment = self.main_widget.getExperiment()
+        
+        if self.experiment is None:
+            self.reset()
+            return
+        
+        self.reset(len(self.experiment.parameters))
+        
+
+    def callpath_selection_changed(self):    
+        self.calculate_current_measurement_cost()
+        self.calculate_used_budget()
+        self.calculate_noise_level()
diff --git a/extrap/gui/ModelerOptionsWidget.py b/extrap/gui/ModelerOptionsWidget.py
index 35a1618f..80be97fd 100644
--- a/extrap/gui/ModelerOptionsWidget.py
+++ b/extrap/gui/ModelerOptionsWidget.py
@@ -5,67 +5,31 @@
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
-from typing import Mapping, Collection, cast
-
 from PySide6.QtCore import Qt
-from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QGroupBox, \
-    QComboBox, QVBoxLayout, QLabel, QPushButton
+from PySide6.QtWidgets import QGroupBox, \
+    QComboBox, QVBoxLayout
 
+from extrap.gui.components.dynamic_options import DynamicOptionsWidget
 from extrap.modelers import single_parameter
 from extrap.modelers.abstract_modeler import AbstractModeler, MultiParameterModeler
-from extrap.modelers.modeler_options import ModelerOption, ModelerOptionsGroup, modeler_options
 
 
-class ModelerOptionsWidget(QWidget):
+class ModelerOptionsWidget(DynamicOptionsWidget):
+    object_with_options: AbstractModeler
+
     def __init__(self, parent, modeler: AbstractModeler, has_parent_options=False):
-        super().__init__(parent)
-        self._modeler = modeler
         self._single_parameter_modeler_widget = None
-        layout = QFormLayout()
-        self.initUI(layout, has_parent_options)
+        super().__init__(parent, modeler, has_parent_options, has_reset_button=True)
 
-    def initUI(self, layout, has_parent_options):
-        layout.setRowWrapPolicy(QFormLayout.WrapLongRows)
-        self.setLayout(layout)
-        self.parent().setEnabled(has_parent_options)
+    def init_ui(self, layout, has_parent_options):
+        super().init_ui(layout, has_parent_options)
 
-        if hasattr(self._modeler, 'OPTIONS'):
-            self.parent().setEnabled(True)
-            if not has_parent_options:
-                reset_button = QPushButton('Reset options to defaults', self)
-                reset_button.clicked.connect(self._reset_options)
-                layout.addRow(reset_button)
-            self._create_options(layout, getattr(self._modeler, 'OPTIONS').items())
-
-        if isinstance(self._modeler, MultiParameterModeler) and self._modeler.single_parameter_modeler is not None:
+        if isinstance(self.object_with_options,
+                      MultiParameterModeler) and self.object_with_options.single_parameter_modeler is not None:
             group = self._create_single_parameter_selection()
             layout.addRow(group)
             self.parent().setEnabled(True)
 
-    def _reset_options(self):
-        for option in modeler_options.iter(self._modeler):
-            setattr(self._modeler, option.field, option.value)
-        layout = self.layout()
-        for i in reversed(range(layout.count())):
-            layout.itemAt(i).widget().setParent(cast(QWidget, None))
-        if isinstance(self._modeler, MultiParameterModeler):
-            self._modeler.reset_single_parameter_modeler()
-        self.initUI(cast(QFormLayout, layout), False)
-        self.update()
-
-    def _create_options(self, layout, options):
-        for name, option in options:
-            if isinstance(option, ModelerOption):
-                layout.addRow(self._determine_label(option), self._determine_field(option))
-            elif isinstance(option, ModelerOptionsGroup):
-                group = QGroupBox(name)
-                group.setToolTip(option.description)
-                g_layout = QFormLayout()
-                g_layout.setRowWrapPolicy(QFormLayout.WrapLongRows)
-                self._create_options(g_layout, option.items())
-                group.setLayout(g_layout)
-                layout.addRow(group)
-
     def _create_single_parameter_selection(self):
         self._modeler: MultiParameterModeler
 
@@ -76,20 +40,22 @@ def _create_single_parameter_selection(self):
 
         def selection_changed():
             modeler = modeler_selection.currentData()
-            self._modeler.single_parameter_modeler = modeler()
-            options = ModelerOptionsWidget(self, self._modeler.single_parameter_modeler, has_parent_options=True)
+            self.object_with_options.single_parameter_modeler = modeler()
+            options = ModelerOptionsWidget(self, self.object_with_options.single_parameter_modeler,
+                                           has_parent_options=True)
             g_layout.replaceWidget(self._single_parameter_modeler_widget, options)
             self._single_parameter_modeler_widget.deleteLater()
             self._single_parameter_modeler_widget = options
 
         for i, (name, modeler) in enumerate(single_parameter.all_modelers.items()):
             modeler_selection.addItem(name, modeler)
-            modeler_selection.setItemData(i, modeler.DESCRIPTION, Qt.ToolTipRole)
-            if isinstance(self._modeler.single_parameter_modeler, modeler):
+            modeler_selection.setItemData(i, modeler.DESCRIPTION, Qt.ItemDataRole.ToolTipRole)
+            if isinstance(self.object_with_options.single_parameter_modeler, modeler):
                 modeler_selection.setCurrentText(name)
         modeler_selection.currentIndexChanged.connect(selection_changed)
 
-        self._single_parameter_modeler_widget = ModelerOptionsWidget(self, self._modeler.single_parameter_modeler,
+        self._single_parameter_modeler_widget = ModelerOptionsWidget(self,
+                                                                     self.object_with_options.single_parameter_modeler,
                                                                      has_parent_options=True)
 
         g_layout.addWidget(modeler_selection)
@@ -97,59 +63,3 @@ def selection_changed():
         g_layout.addStrut(1)
         group.setLayout(g_layout)
         return group
-
-    def check_and_apply(self, option, value):
-        value = option.type(value)
-        setattr(self._modeler, option.field, value)
-
-    @staticmethod
-    def _determine_label(option):
-        if option.name is not None:
-            name = option.name
-        else:
-            name = option.field.replace('_', ' ')
-            name = name[0].capitalize() + name[1:]
-        label = QLabel(name)
-        label.setToolTip(option.description)
-        return label
-
-    def _determine_field(self, option):
-        def slot(value):
-            self.check_and_apply(option, value)
-
-        if isinstance(option.range, Mapping):
-            field = QComboBox()
-            for name, item in option.range.items():
-                field.addItem(name, item)
-                if item == option.value:
-                    field.setCurrentText(name)
-            field.currentIndexChanged.connect(lambda: self.check_and_apply(option, field.currentData()))
-        elif option.type is str and isinstance(option.range, Collection):
-            field = QComboBox()
-            for name in option.range:
-                field.addItem(str(name))
-                if name == option.value:
-                    field.setCurrentText(name)
-            field.currentIndexChanged.connect(lambda: self.check_and_apply(option, field.currentText()))
-        elif option.type is bool:
-            field = QCheckBox()
-            field.setChecked(option.value)
-            field.stateChanged[int].connect(slot)
-        elif option.type is float:
-            field = QDoubleSpinBox()
-            field.setValue(option.value)
-            if isinstance(option.range, range):
-                field.setRange(option.range.start, option.range.stop)
-            field.valueChanged[float].connect(slot)
-        elif option.type is int:
-            field = QSpinBox()
-            field.setValue(option.value)
-            if isinstance(option.range, range):
-                field.setRange(option.range.start, option.range.stop)
-                field.setSingleStep(option.range.step)
-            field.valueChanged[int].connect(slot)
-        else:
-            field = QLineEdit(str(option.value), self)
-            field.textChanged[str].connect(slot)
-        field.setToolTip(option.description)
-        return field
diff --git a/extrap/gui/SelectorWidget.py b/extrap/gui/SelectorWidget.py
index b84b72e6..efaad9e8 100644
--- a/extrap/gui/SelectorWidget.py
+++ b/extrap/gui/SelectorWidget.py
@@ -147,6 +147,7 @@ def callpath_selection_changed(self):
                            (c.path, metric) in self.getCurrentModel().models]
         self.main_widget.model_color_map.update(call_tree_nodes)
         self.main_widget.on_selection_changed()
+        self.main_widget.measurementWizard_widget.callpath_selection_changed()
 
     def fillMetricList(self):
         self.metric_selector.clear()
diff --git a/extrap/gui/StrongScalingConversionDialog.py b/extrap/gui/StrongScalingConversionDialog.py
new file mode 100644
index 00000000..af5683e3
--- /dev/null
+++ b/extrap/gui/StrongScalingConversionDialog.py
@@ -0,0 +1,112 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from typing import Sequence, Optional
+
+from PySide6.QtCore import Qt, Slot
+from PySide6.QtWidgets import QDialog, QWidget, QFormLayout, QLabel, QDialogButtonBox, QComboBox, QMessageBox, QLayout, \
+    QStyle, QGridLayout, QSizePolicy, QSpacerItem
+
+from extrap.entities.experiment import Experiment
+from extrap.gui.components.ProgressWindow import ProgressWidget
+
+_TITLE = "Strong-Scaling Experiment Detected"
+
+_MESSAGE = "<h3>Modeling strong-scaling is not supported, <br>do you want to convert to a " \
+           "weak-scaling experiment?</h3>" \
+           "<p>Extra-P detected that these measurements might describe a strong-scaling experiment, but modeling " \
+           "is only supported for weak-scaling experiments. If you continue without conversion, the models " \
+           "might not be correct.</p>" \
+           "<p>After the conversion Extra-P will model your metrics globally, e.g., the run time will be the sum " \
+           "of the run times of all ranks/threads.</p>"
+
+
+class StrongScalingConversionDialog(QDialog):
+
+    def __init__(self, experiment: Experiment, check_result: Sequence[int], parent: QWidget = None,
+                 flags: Qt.WindowType = Qt.WindowType.Dialog):
+        super().__init__(parent, flags)
+        self.experiment = experiment
+        self._check_result = check_result
+        self._parameter_selection_cb: Optional[QComboBox] = None
+        self._init_ui()
+
+    def _init_ui(self):
+        self.setWindowTitle(_TITLE)
+
+        main_layout = QGridLayout(self)
+        main_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
+        main_layout.setColumnStretch(0, 0)
+        main_layout.setColumnStretch(1, 0)
+        main_layout.setColumnStretch(2, 1)
+        main_layout.setColumnMinimumWidth(2, 400)
+        self.setLayout(main_layout)
+
+        icon = QLabel()
+        icon.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(
+            self.style().pixelMetric(QStyle.PixelMetric.PM_MessageBoxIconSize, widget=self)))
+        main_layout.addWidget(icon, 0, 0, 1, 1, Qt.AlignTop)
+        main_layout.addItem(QSpacerItem(7, 1, QSizePolicy.Fixed, QSizePolicy.Fixed), 0, 1)
+
+        layout = QFormLayout(self)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
+        main_layout.addLayout(layout, 0, 2)
+
+        message = QLabel(_MESSAGE)
+        message.setWordWrap(True)
+        layout.addRow(message)
+
+        if len(self.experiment.parameters) >= 1:
+            self._parameter_selection_cb = QComboBox(self)
+            self._parameter_selection_cb.addItems([p.name for p in self.experiment.parameters])
+            self._parameter_selection_cb.setCurrentIndex(self._check_result.index(max(self._check_result)))
+
+            layout.addRow("<b>Strong-scaling parameter</b>", self._parameter_selection_cb)
+
+            hint = QLabel(
+                "The scaling parameter represents the computational resources (e.g., ranks, threads, processes). "
+                "If the automatic detection identified the wrong parameter, you should correct it.")
+            hint.setWordWrap(True)
+            layout.addRow(hint)
+
+        self.progress_widget = ProgressWidget(self)
+        self.progress_widget.setContentsMargins(0, 10, 0, 0)
+        main_layout.addWidget(self.progress_widget, 1, 0, 1, 3)
+
+        self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No)
+        self.buttons.button(QDialogButtonBox.StandardButton.Yes).setText("Convert")
+        self.buttons.button(QDialogButtonBox.StandardButton.No).setText("Don't convert")
+        self.buttons.accepted.connect(self.accept)
+        self.buttons.rejected.connect(self.reject)
+
+        main_layout.addWidget(self.buttons, 2, 0, 1, 3)
+
+    @Slot()
+    def accept(self) -> None:
+        with self.progress_widget.get_progress_bar() as pbar:
+
+            if self._parameter_selection_cb is None:
+                selected_dimension = 0
+            else:
+                self._parameter_selection_cb.setEnabled(False)
+                selected_dimension = self._parameter_selection_cb.currentIndex()
+
+            for measurements in pbar(self.experiment.measurements.values(), len(self.experiment.measurements)):
+                for m in measurements:
+                    m *= m.coordinate[selected_dimension]
+        super().accept()
+
+    @staticmethod
+    def pose_question_for_readers_with_scaling_conversion(parent):
+        msg_box = QMessageBox(QMessageBox.Icon.Warning, _TITLE, _MESSAGE,
+                              QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, parent)
+        msg_box.button(QMessageBox.StandardButton.Yes).setText("Convert")
+        msg_box.button(QMessageBox.StandardButton.No).setText("Don't convert")
+        msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
+
+        return msg_box.exec()
diff --git a/extrap/gui/TreeModel.py b/extrap/gui/TreeModel.py
index 7b48a451..f47a487e 100644
--- a/extrap/gui/TreeModel.py
+++ b/extrap/gui/TreeModel.py
@@ -9,14 +9,14 @@
 
 import copy
 from enum import Enum, auto
-from typing import Optional, TYPE_CHECKING, List, Callable
+from typing import Optional, TYPE_CHECKING, List, Callable, Any
 
 import numpy
 from PySide6.QtCore import *  # @UnusedWildImport
 
 from extrap.entities import calltree
 from extrap.entities.calltree import CallTree, Node
-from extrap.entities.model import Model
+from extrap.entities.model import Model, SegmentedModel
 from extrap.gui.Utils import formatFormula
 from extrap.gui.Utils import formatNumber
 
@@ -24,6 +24,13 @@
     from extrap.gui.SelectorWidget import SelectorWidget
 
 
+def _format_number_segmented_model(segmented_model: SegmentedModel, selector: Callable[[Model], Any]) -> str:
+    val = formatNumber(str(selector(segmented_model))) + "\n"
+    val += "\n".join(f"Model {i + 1}: " + formatNumber(str(selector(m)))
+                     for i, m in enumerate(segmented_model.segment_models))
+    return val
+
+
 class TreeModel(QAbstractItemModel):
     def __init__(self, selector_widget: SelectorWidget, parent=None):
         super(TreeModel, self).__init__(parent)
@@ -73,7 +80,7 @@ def data(self, index, role=None):
         getDecorationBoxes = (role == Qt.DecorationRole and index.column() == 1)
         get_tooltip_annotations = (role == Qt.ToolTipRole and index.column() == 2)
 
-        if role != Qt.DisplayRole and not (getDecorationBoxes or get_tooltip_annotations):
+        if role != Qt.DisplayRole and role != Qt.ToolTipRole and not (getDecorationBoxes or get_tooltip_annotations):
             return None
 
         call_tree_node = index.internalPointer().data()
@@ -84,7 +91,7 @@ def data(self, index, role=None):
         if callpath is None:
             return "Invalid"
 
-        model = self.getSelectedModel(callpath)
+        model, _ = self.getSelectedModel(callpath)
         if model is None and index.column() != 0:
             return None
 
@@ -131,9 +138,9 @@ def data(self, index, role=None):
                 return [ann.icon(parameters=parameters,
                                  parameter_values=parameter_values)
                         for ann in model.annotations]
-            return None
         elif index.column() == 3:
             experiment = self.main_widget.getExperiment()
+
             formula = model.hypothesis.function
             if self.selector_widget.asymptoticCheckBox.isChecked():
                 parameters = tuple(experiment.parameters)
@@ -141,17 +148,32 @@ def data(self, index, role=None):
             else:
                 parameters = self.selector_widget.getParameterValues()
                 previous = numpy.seterr(divide='ignore', invalid='ignore')
-                res = formatNumber(str(formula.evaluate(parameters)))
+                if role == Qt.ToolTipRole and isinstance(model, SegmentedModel):
+                    res = _format_number_segmented_model(model, lambda m: m.hypothesis.function.evaluate(parameters))
+                else:
+                    res = formatNumber(str(formula.evaluate(parameters)))
                 numpy.seterr(**previous)
                 return res
         elif index.column() == 4:
-            return formatNumber(str(model.hypothesis.RSS))
+            if role == Qt.ToolTipRole and isinstance(model, SegmentedModel):
+                return _format_number_segmented_model(model, lambda m: m.hypothesis.RSS)
+            else:
+                return formatNumber(str(model.hypothesis.RSS))
         elif index.column() == 5:
-            return formatNumber(str(model.hypothesis.AR2))
+            if role == Qt.ToolTipRole and isinstance(model, SegmentedModel):
+                return _format_number_segmented_model(model, lambda m: m.hypothesis.AR2)
+            else:
+                return formatNumber(str(model.hypothesis.AR2))
         elif index.column() == 6:
-            return formatNumber(str(model.hypothesis.SMAPE))
+            if role == Qt.ToolTipRole and isinstance(model, SegmentedModel):
+                return _format_number_segmented_model(model, lambda m: m.hypothesis.SMAPE)
+            else:
+                return formatNumber(str(model.hypothesis.SMAPE))
         elif index.column() == 7:
-            return formatNumber(str(model.hypothesis.RE))
+            if role == Qt.ToolTipRole and isinstance(model, SegmentedModel):
+                return _format_number_segmented_model(model, lambda m: m.hypothesis.RE)
+            else:
+                return formatNumber(str(model.hypothesis.RE))
         return None
 
     def get_comparison_value(self, model):
@@ -189,13 +211,14 @@ def _replace_braces(name, lb, rb):
         return name
 
     def getSelectedModel(self, callpath) -> Optional[Model]:
+        experiment = self.main_widget.getExperiment()
         model = self.selector_widget.getCurrentModel()
         metric = self.selector_widget.getSelectedMetric()
         if model is None or metric is None:
             return None
         key = (callpath, metric)
         if key in model.models:
-            return model.models[key]  # might be None
+            return model.models[key], experiment  # might be None
         else:
             return None
 
diff --git a/extrap/gui/TreeView.py b/extrap/gui/TreeView.py
index 324b2de4..53382321 100644
--- a/extrap/gui/TreeView.py
+++ b/extrap/gui/TreeView.py
@@ -11,6 +11,7 @@
 from PySide6.QtGui import *  # @UnusedWildImport
 from PySide6.QtWidgets import *  # @UnusedWildImport
 
+from extrap.entities.model import SegmentedModel
 from extrap.gui.TreeModel import TreeModel
 from extrap.gui.components.annotation_delegate import AnnotationDelegate
 
@@ -34,8 +35,11 @@ def collapseRecursively(self, index):
 
         child_count = index.model().rowCount(parent=index)
         for i in range(0, child_count):
-            child = index.child(i, 0)
-            self.collapseRecursively(child)
+            try:
+                child = index.child(i, 0)
+                self.collapseRecursively(child)
+            except AttributeError:
+                pass
 
     def expand_largest(self, model, index):
         root = index.internalPointer()
@@ -71,7 +75,7 @@ def max_path(self, model, node, parent_index):
     def get_value_from_node(self, tree_model, node, parent_index):
         index = tree_model.index(node.row(), 0, parent_index)
         callpath = tree_model.getValue(index).path
-        node_model = tree_model.getSelectedModel(callpath)
+        node_model, _ = tree_model.getSelectedModel(callpath)
         if node_model:
             return tree_model.get_comparison_value(node_model)
         else:
@@ -87,7 +91,7 @@ def contextMenuEvent(self, event):
                     self.selectedIndexes()[0])
                 if selectedCallpath is None:
                     return
-                selectedModel = model.getSelectedModel(selectedCallpath.path)
+                selectedModel, experiment = model.getSelectedModel(selectedCallpath.path)
 
                 expandAction = menu.addAction("Expand all")
                 expandAction.triggered.connect(self.expandAll)
@@ -175,6 +179,17 @@ def showDataPoints(model):
         else:
             msg_txt += "No data available."
 
+        if isinstance(model, SegmentedModel):
+            for i, segment_model in enumerate(model.segment_models):
+                if segment_model.predictions is not None and segment_model.measurements is not None:
+                    msg_txt += "\nModel " + str(i + 1) + ":\n\n"
+                    row_format = "{:20} {:>15} {:>15} {:>15}\n"
+                    msg_txt += row_format.format("Coordinate", "Predicted", "Actual Mean", "Actual Median")
+                    row_format = "{:20} {:>15g} {:>15g} {:>15g}\n"
+                    for pred, m in zip(segment_model.predictions, segment_model.measurements):
+                        msg_txt += row_format.format(str(m.coordinate), pred, m.mean, m.median)
+                        # print(str(ps[i])+","+str(actual_points[i]))
+
         print(msg_txt)
         print("")
         msg.setText(msg_txt)
diff --git a/extrap/gui/Utils.py b/extrap/gui/Utils.py
index 79ae9eeb..c4bef37b 100644
--- a/extrap/gui/Utils.py
+++ b/extrap/gui/Utils.py
@@ -181,3 +181,24 @@ def formatFormula(formula):
                 mode = 0
         i = i + 1
     return formula
+
+from typing import Union
+
+def tryCastTupelToInt(numbers :tuple) -> tuple:
+    roundedNumbers = list(numbers).copy()
+    for i, num in enumerate(numbers):
+        if not bool(int(num)-num):
+            roundedNumbers[i] = int(num)
+    return tuple(roundedNumbers)
+    
+def tryCastListToInt(numbers :list) -> list:
+    roundedNumbers = numbers.copy()
+    for i, num in enumerate(numbers):
+        if not bool(int(num)-num):
+            roundedNumbers[i] = int(num)
+    return roundedNumbers
+    
+def tryCastNumToInt(number :Union[int,float]) -> Union[int,float]:
+    if not bool(int(number)-number):
+        return int(number)
+    return number
diff --git a/extrap/gui/components/ExpanderWidget.py b/extrap/gui/components/ExpanderWidget.py
index 2fe1858a..e7bf3d1f 100644
--- a/extrap/gui/components/ExpanderWidget.py
+++ b/extrap/gui/components/ExpanderWidget.py
@@ -21,6 +21,7 @@ def __init__(self, parent, title, content=None):
 
     def setTitle(self, title: str):
         self._title = title
+        self._toggle.setText(self._title)
 
     def title(self) -> str:
         return self._title
diff --git a/extrap/gui/components/ProgressWindow.py b/extrap/gui/components/ProgressWindow.py
index 89cdd0d2..3347ab61 100644
--- a/extrap/gui/components/ProgressWindow.py
+++ b/extrap/gui/components/ProgressWindow.py
@@ -4,16 +4,25 @@
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
-
 from threading import Event
 
 from PySide6.QtCore import Qt, QCoreApplication, Slot
-from PySide6.QtWidgets import QProgressDialog, QLabel
+from PySide6.QtWidgets import QProgressDialog, QLabel, QWidget, QVBoxLayout, QProgressBar, QApplication
 
 from extrap.util.exceptions import CancelProcessError
 from extrap.util.progress_bar import ProgressBar
 
 
+def format_progress_time_for_gui(pbar: ProgressBar):
+    format_dict = pbar.format_dict
+    elapsed_str = pbar.format_interval(format_dict['elapsed'])
+    rate = format_dict['rate']
+    remaining = (pbar.total - pbar.n) / rate if rate and pbar.total else 0
+    remaining_str = pbar.format_interval(remaining) if rate else '??:??'
+    time_str = f'Time remaining:\t{remaining_str}\nTime elapsed:\t{elapsed_str}'
+    return time_str
+
+
 class ProgressWindow(ProgressBar):
     def __init__(self, parent, title, **kwargs):
         super().__init__(total=0, desc=title, **kwargs, gui=True)
@@ -60,14 +69,7 @@ def update(self, n=1):
         super(ProgressWindow, self).update(n)
 
     def display(self, msg=None, pos=None):
-        format_dict = self.format_dict
-        elapsed_str = self.format_interval(format_dict['elapsed'])
-
-        rate = format_dict['rate']
-        remaining = (self.total - self.n) / rate if rate and self.total else 0
-        remaining_str = self.format_interval(remaining) if rate else '??:??'
-
-        time_str = f'Time remaining:\t{remaining_str}\nTime elapsed:\t{elapsed_str}'
+        time_str = format_progress_time_for_gui(self)
 
         if not self._cancel_event.is_set():
             self.dialog.setMaximum(self.total)
@@ -77,3 +79,55 @@ def display(self, msg=None, pos=None):
                 self.dialog.setLabelText(time_str)
             self.dialog.setValue(self.n)
         QCoreApplication.processEvents()
+
+
+class ProgressWidget(QWidget):
+    def __init__(self, parent=None, flags=Qt.WindowFlags()):
+        super().__init__(parent, flags)
+        self._cancel_event = Event()
+        self.setMaximumHeight(0)
+        layout = QVBoxLayout()
+        layout.setContentsMargins(0, 0, 0, 0)
+        self.setLayout(layout)
+
+        self.progress_label = QLabel('Time remaining:\t??:??\nTime elapsed:\t00:00')
+        layout.addWidget(self.progress_label)
+
+        self.progress_indicator = QProgressBar(self)
+        layout.addWidget(self.progress_indicator)
+
+        self.progress_bar_class = self._make_progress_bar_class()
+
+    def _make_progress_bar_class(self):
+        widget = self
+
+        class _ProgressBar(ProgressBar):
+            def __init__(self, **kwargs):
+                super().__init__(**kwargs)
+                self.sp = None
+
+            def display(self: ProgressBar, msg=None, pos=None):
+                if widget._cancel_event.is_set():
+                    raise CancelProcessError()
+                time_str = format_progress_time_for_gui(self)
+
+                if self.postfix:
+                    widget.progress_label.setText(time_str + '\n' + self.postfix)
+                else:
+                    widget.progress_label.setText(time_str)
+                widget.progress_indicator.setMaximum(self.total)
+                widget.progress_indicator.setValue(self.n)
+                QApplication.processEvents()
+
+            def __enter__(self):
+                widget.setMaximumHeight(10000)
+                QApplication.processEvents()
+                return super().__enter__()
+
+        return _ProgressBar
+
+    def get_progress_bar(self) -> ProgressBar:
+        return self.progress_bar_class(total=0, gui=True)
+
+    def cancel(self):
+        self._cancel_event.set()
diff --git a/extrap/gui/components/dynamic_options.py b/extrap/gui/components/dynamic_options.py
new file mode 100644
index 00000000..c498b016
--- /dev/null
+++ b/extrap/gui/components/dynamic_options.py
@@ -0,0 +1,118 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from typing import Mapping, Collection, cast
+
+from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QSpinBox, QDoubleSpinBox, QGroupBox, \
+    QComboBox, QLabel, QPushButton
+
+from extrap.gui.components.switch_widget import SwitchWidget
+from extrap.util.dynamic_options import DynamicOptions, DynamicOptionsGroup, DynamicOption
+
+
+class DynamicOptionsWidget(QWidget):
+    def __init__(self, parent, object_with_options: DynamicOptions, has_parent_options=False, has_reset_button=False):
+        super().__init__(parent)
+        self.object_with_options = object_with_options
+        self._has_reset_button = has_reset_button
+        layout = QFormLayout()
+        self.init_ui(layout, has_parent_options)
+
+    def init_ui(self, layout, has_parent_options):
+        layout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
+        self.setLayout(layout)
+        self.parent().setEnabled(has_parent_options)
+
+        if self.object_with_options.OPTIONS:
+            self.parent().setEnabled(True)
+            if not has_parent_options and self._has_reset_button:
+                reset_button = QPushButton('Reset options to defaults', self)
+                reset_button.clicked.connect(self.reset_options)
+                layout.addRow(reset_button)
+            self._create_options(layout, self.object_with_options.OPTIONS.items())
+
+    def reset_options(self):
+        for option in self.object_with_options.options_iter():
+            setattr(self.object_with_options, option.field, option.value)
+        layout = self.layout()
+        for i in reversed(range(layout.count())):
+            layout.itemAt(i).widget().setParent(cast(QWidget, None))
+        self.init_ui(cast(QFormLayout, layout), False)
+        self.update()
+
+    def _create_options(self, layout, options):
+        for name, option in options:
+            if isinstance(option, DynamicOption):
+                if option.explanation_above:
+                    layout.addWidget(QLabel(option.explanation_above))
+                layout.addRow(self._determine_label(option), self._determine_field(option))
+            elif isinstance(option, DynamicOptionsGroup):
+                group = QGroupBox(name)
+                group.setToolTip(option.description)
+                g_layout = QFormLayout()
+                g_layout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
+                self._create_options(g_layout, option.items())
+                group.setLayout(g_layout)
+                layout.addRow(group)
+            else:
+                raise TypeError(f"Unsupported type {type(option)} for {name} in options.")
+
+    def _check_and_apply(self, option, value):
+        value = option.type(value)
+        setattr(self.object_with_options, option.field, value)
+
+    @staticmethod
+    def _determine_label(option):
+        if option.name is not None:
+            name = option.name
+        else:
+            name = option.field.replace('_', ' ')
+            name = name[0].capitalize() + name[1:]
+        label = QLabel(name)
+        label.setToolTip(option.description)
+        return label
+
+    def _determine_field(self, option):
+        def slot(value):
+            self._check_and_apply(option, value)
+
+        if isinstance(option.range, Mapping):
+            field = QComboBox()
+            for name, item in option.range.items():
+                field.addItem(name, item)
+                if item == option.value:
+                    field.setCurrentText(name)
+            field.currentIndexChanged.connect(lambda: self._check_and_apply(option, field.currentData()))
+        elif option.type is str and isinstance(option.range, Collection):
+            field = QComboBox()
+            for name in option.range:
+                field.addItem(str(name))
+                if name == option.value:
+                    field.setCurrentText(name)
+            field.currentIndexChanged.connect(lambda: self._check_and_apply(option, field.currentText()))
+        elif option.type is bool:
+            field = SwitchWidget()
+            field.setChecked(option.value)
+            field.stateChanged[int].connect(slot)
+        elif option.type is float:
+            field = QDoubleSpinBox()
+            field.setValue(option.value)
+            if isinstance(option.range, range):
+                field.setRange(option.range.start, option.range.stop)
+            field.valueChanged[float].connect(slot)
+        elif option.type is int:
+            field = QSpinBox()
+            field.setValue(option.value)
+            if isinstance(option.range, range):
+                field.setRange(option.range.start, option.range.stop)
+                field.setSingleStep(option.range.step)
+            field.valueChanged[int].connect(slot)
+        else:
+            field = QLineEdit(str(option.value), self)
+            field.textChanged[str].connect(slot)
+        field.setToolTip(option.description)
+        return field
diff --git a/extrap/gui/components/switch_widget.py b/extrap/gui/components/switch_widget.py
new file mode 100644
index 00000000..ef75d38e
--- /dev/null
+++ b/extrap/gui/components/switch_widget.py
@@ -0,0 +1,102 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from PySide6.QtCore import QPoint, QRect, QPropertyAnimation, QEasingCurve, Slot, Property, QSize
+from PySide6.QtGui import QPaintEvent, QPainter, QMouseEvent
+from PySide6.QtWidgets import QCheckBox, QStyle, QStyleOptionProgressBar, QStyleOptionButton
+
+
+class SwitchWidget(QCheckBox):
+
+    def __init__(self, parent=None):
+        self._drag_x = None
+        self._switch_position_value = 0
+        super().__init__(parent)
+        self.animation = QPropertyAnimation(self, b"_switch_position", self)
+        self.animation.setEasingCurve(QEasingCurve.InOutCubic)
+        self.animation.setDuration(100)
+
+        self.stateChanged.connect(self._on_state_change)
+
+    def sizeHint(self):
+        return QSize(40, 22)
+
+    def hitButton(self, pos: QPoint):
+        return self._my_contents_rect().contains(pos)
+
+    def paintEvent(self, arg__1: QPaintEvent) -> None:
+        cont_rect = self._my_contents_rect()
+
+        p = QPainter(self)
+        p.setRenderHint(QPainter.Antialiasing)
+
+        background_options = QStyleOptionProgressBar()
+        background_options.textVisible = False
+        background_options.rect = cont_rect
+        background_options.minimum = 0
+        background_options.maximum = 1000
+        background_options.progress = int(self._switch_position_value * background_options.maximum)
+
+        self.style().drawControl(QStyle.ControlElement.CE_ProgressBar, background_options, p, self)
+        handle_width = cont_rect.width() / 2
+        xPos = cont_rect.x() + cont_rect.width() / 2 * self._switch_position_value + 2
+
+        handle_options = QStyleOptionButton()
+        self.initStyleOption(handle_options)
+        handle_options.rect = QRect(int(xPos), cont_rect.y() + 2, handle_width - 2, cont_rect.height() - 4)
+        handle_options.state = handle_options.state & ~QStyle.StateFlag.State_On
+        self.style().drawControl(QStyle.ControlElement.CE_PushButtonBevel, handle_options, p, self)
+
+        p.end()
+
+    def _my_contents_rect(self):
+        cont_rect = self.contentsRect()
+        cont_rect.setWidth(min(cont_rect.width(), int(cont_rect.height() * 2.5)))
+        return cont_rect
+
+    @Slot(int)
+    def _on_state_change(self, value):
+        self.animation.stop()
+        if value:
+            self.animation.setEndValue(1)
+        else:
+            self.animation.setEndValue(0)
+        self.animation.start()
+
+    @Property(float)
+    def _switch_position(self):
+        return self._switch_position_value
+
+    @_switch_position.setter
+    def _switch_position(self, pos):
+        self._switch_position_value = pos
+        self.update()
+
+    def mousePressEvent(self, e: QMouseEvent) -> None:
+        super().mousePressEvent(e)
+        self._drag_x = e.x()
+
+    def mouseMoveEvent(self, e: QMouseEvent) -> None:
+        super().mouseMoveEvent(e)
+        if self._drag_x is None:
+            return
+        movement = (e.x() - self._drag_x)
+        min_movement = self._my_contents_rect().width() / 4
+        if movement > min_movement and not self.isChecked():
+            self.setChecked(True)
+        elif movement < -min_movement and self.isChecked():
+            self.setChecked(False)
+        # self._switch_position = min(max(0, movement), 1)
+
+    def mouseReleaseEvent(self, e: QMouseEvent) -> None:
+        min_movement = self._my_contents_rect().width() / 4
+        movement = abs(e.x() - self._drag_x)
+        if movement < min_movement:
+            super().mouseReleaseEvent(e)
+        else:
+            self.setDown(False)
+        self._drag_x = None
diff --git a/extrap/modelers/model_generator.py b/extrap/modelers/model_generator.py
index f5461541..eb073fdb 100644
--- a/extrap/modelers/model_generator.py
+++ b/extrap/modelers/model_generator.py
@@ -19,7 +19,6 @@
 from extrap.modelers import single_parameter
 from extrap.modelers.abstract_modeler import AbstractModeler, MultiParameterModeler, ModelerSchema
 from extrap.modelers.modeler_options import modeler_options
-from extrap.util.exceptions import RecoverableError
 from extrap.util.progress_bar import DUMMY_PROGRESS
 from extrap.util.serialization_schema import Schema, TupleKeyDict
 
@@ -85,6 +84,7 @@ def model_all(self, progress_bar=DUMMY_PROGRESS):
             model.callpath = callpath
             model.metric = metric
             model.measurements = self.experiment.measurements[(callpath, metric)]
+
         # add the modeler with the results to the experiment
         self.experiment.add_modeler(self)
 
@@ -94,10 +94,10 @@ def __eq__(self, other):
         elif self is other:
             return True
         else:
-            return self.models == other.models and \
-                   self._modeler.NAME == other._modeler.NAME and \
-                   self._modeler.use_median == other._modeler.use_median and \
-                   modeler_options.equal(self._modeler, other._modeler)
+            return (self.models == other.models and
+                    self._modeler.NAME == other._modeler.NAME and
+                    self._modeler.use_median == other._modeler.use_median and
+                    modeler_options.equal(self._modeler, other._modeler))
 
 
 class ModelGeneratorSchema(Schema):
diff --git a/extrap/modelers/modeler_options.py b/extrap/modelers/modeler_options.py
index 14234e25..4a76d310 100644
--- a/extrap/modelers/modeler_options.py
+++ b/extrap/modelers/modeler_options.py
@@ -1,39 +1,19 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
 
-from dataclasses import dataclass
-from dataclasses import field as d_field
-from typing import TypeVar, Generic, Sequence, Type, AnyStr, Mapping, Union, Callable, Any
+from typing import TypeVar, Type, AnyStr, Mapping, Union, Callable, Any
 
 from extrap.modelers.abstract_modeler import AbstractModeler
+from extrap.util.dynamic_options import DynamicOption, DynamicOptionsGroup, convert_dynamic_options
 
 T = TypeVar('T')
 
-
-@dataclass
-class ModelerOption(Generic[T]):
-    value: T
-    type: Type
-    description: str
-    name: str = None
-    range: Union[Mapping[AnyStr, T], range] = None
-    group = None
-    field: str = None
-    on_change: Callable[[Any, T], None] = d_field(default=None, compare=False, hash=False)
-
-
-@dataclass
-class ModelerOptionsGroup:
-    name: str
-    options: Sequence[ModelerOption]
-    description: str
-
-    def items(self):
-        return ((o.field, o) for o in self.options)
+ModelerOption = DynamicOption
+ModelerOptionsGroup = DynamicOptionsGroup
 
 
 class _ModelerOptionsClass:
@@ -43,33 +23,9 @@ def __call__(self, original_class: AbstractModeler) -> AbstractModeler:
         else:
             original_class.OPTIONS = {}
 
-        option_storage = []
-        for name in original_class.__dict__:
-            option: ModelerOption = original_class.__dict__[name]
-            if isinstance(option, ModelerOption):
-                if option.group is not None:
-                    original_class.OPTIONS[option.group.name] = option.group
-                else:
-                    original_class.OPTIONS[name] = option
-                option.field = name
-                if option.on_change is not None:
-                    def make_property(opt):
-                        # required for closure
-                        def getter(m_self):
-                            return getattr(m_self, '__OPTION_' + opt.field)
-
-                        def setter(m_self, v):
-                            setattr(m_self, '__OPTION_' + opt.field, v)
-                            opt.on_change(m_self, v)
-
-                        return property(getter, setter)
-
-                    setattr(original_class, name, make_property(option))
-                    option_storage.append(('__OPTION_' + option.field, option.value))
-                else:
-                    setattr(original_class, name, option.value)
-        for n, v in option_storage:
-            setattr(original_class, n, v)
+        convert_dynamic_options(original_class)
+        original_class.options_iter = self.iter
+        original_class.options_equal = self.equal
 
         return original_class
 
diff --git a/extrap/modelers/multi_parameter/multi_parameter_modeler.py b/extrap/modelers/multi_parameter/multi_parameter_modeler.py
index aeb34b8f..1e71da29 100644
--- a/extrap/modelers/multi_parameter/multi_parameter_modeler.py
+++ b/extrap/modelers/multi_parameter/multi_parameter_modeler.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2022, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
diff --git a/extrap/modelers/single_parameter/adaptive/__init__.py b/extrap/modelers/single_parameter/adaptive/__init__.py
new file mode 100644
index 00000000..61a828cd
--- /dev/null
+++ b/extrap/modelers/single_parameter/adaptive/__init__.py
@@ -0,0 +1,213 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import copy
+from bisect import bisect_left
+from collections import namedtuple
+from itertools import groupby
+from typing import Sequence
+
+from extrap.entities.functions import SingleParameterFunction, ConstantFunction
+from extrap.entities.hypotheses import SingleParameterHypothesis, ConstantHypothesis
+from extrap.entities.measurement import Measurement
+from extrap.entities.model import Model
+from extrap.entities.terms import CompoundTerm
+from extrap.modelers.modeler_options import modeler_options
+from extrap.modelers.single_parameter.abstract_base import AbstractSingleParameterModeler
+import numpy as np
+
+from extrap.util.progress_bar import DUMMY_PROGRESS
+
+from PySide6.QtCore import *  # @UnusedWildImport
+from PySide6.QtGui import *  # @UnusedWildImport
+from PySide6.QtWidgets import *  # @UnusedWildImport
+
+try:
+    import extrapadaptivemodeler
+    from extrapadaptivemodeler.modeler.load_model import get_model
+    from extrapadaptivemodeler.modeler.lazy_tensorflow import load_tensorflow 
+
+except ImportError:
+    extrapadaptivemodeler = None
+
+from ..basic import SingleParameterModeler
+
+_NOISE_CATEGORIES = [0.0125, 0.025, 0.05, .1, .2, .4, .8]
+
+# The following constants are fixed for the provided ML model
+_BUCKET_COORDINATES = [1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64]
+_TERMS = [CompoundTerm.create(*expo) for expo in
+          [(0, 1, 0), (0, 1, 1), (0, 1, 2), (1, 4, 0), (1, 3, 0),
+           (1, 4, 1), (1, 3, 1), (1, 4, 2), (1, 3, 2), (1, 2, 0),
+           (1, 2, 1), (1, 2, 2), (2, 3, 0), (3, 4, 0), (2, 3, 1),
+           (3, 4, 1), (4, 5, 0), (2, 3, 2), (3, 4, 2), (1, 1, 0),
+           (1, 1, 1), (1, 1, 2), (5, 4, 0), (5, 4, 1), (4, 3, 0),
+           (4, 3, 1), (3, 2, 0), (3, 2, 1), (3, 2, 2), (5, 3, 0),
+           (7, 4, 0), (2, 1, 0), (2, 1, 1), (2, 1, 2), (9, 4, 0),
+           (7, 3, 0), (5, 2, 0), (5, 2, 1), (5, 2, 2), (8, 3, 0),
+           (11, 4, 0), (3, 1, 0), (3, 1, 1)]]
+
+_PresetOptions = namedtuple('PresetOptions',
+                            ['threshold', 'retrain_epochs', 'retrain_examples_per_class', 'noise_aware'])
+
+
+@modeler_options
+class AdaptiveModeler(AbstractSingleParameterModeler):
+    NAME = "Adaptive"
+
+    PRESETS = {
+        -2: _PresetOptions(0.050301028, 1, 20, False),
+        -1: _PresetOptions(0.035916109, 1, 100, False),
+        0: _PresetOptions(0.031965664, 1, 200, False),
+        1: _PresetOptions(0.0255837, 1, 2000, False),
+        2: _PresetOptions(0.0255837, 1, 2000, True)
+    }
+
+    NOISE_CI = 2
+
+    _cached_mlmodels = {}
+
+    def _set_preset(self, value: int):
+        self._preset = value
+        p = self.PRESETS[value]
+        self._threshold, self.retrain_epochs, self.retrain_examples_per_class, self.noise_aware = p
+
+    preset = modeler_options.add(0, int, range=range(-2, 2), on_change=_set_preset)
+
+    def __init__(self, use_median=False):
+        super().__init__(use_median)
+        self.preset = 0
+        self.no_constants = False
+        self._basic_modeler: AbstractSingleParameterModeler = SingleParameterModeler()
+
+    def model(self, measurement_list_: Sequence[Sequence[Measurement]], progress_bar=DUMMY_PROGRESS) -> Sequence[Model]:
+        all_models = []
+        for _, measurement_group in groupby(measurement_list_, key=lambda ms: {m.coordinate[0] for m in ms}):
+            measurement_list = list(measurement_group)
+
+            positions = np.array([m.coordinate[0] for m in measurement_list[0]])
+            values = mean_values = np.array([np.array([m.mean for m in ml]) for ml in measurement_list])
+            if self.use_median:
+                values = np.array([np.array([m.median for m in ml]) for ml in measurement_list])
+
+            min_values = np.array([np.array([m.minimum for m in ml]) for ml in measurement_list])
+            max_values = np.array([np.array([m.maximum for m in ml]) for ml in measurement_list])
+
+            noise = self.get_noise(mean_values, min_values, max_values)
+
+            functions = self._predict_functions(positions, values, noise)
+            if functions is not None:
+                models = self._create_ml_models(functions, measurement_list)
+
+                self._update_with_additional_basic_models(measurement_list, models, noise)
+                all_models.extend(models)
+        return all_models
+
+    def _update_with_additional_basic_models(self, measurement_list, models, noise):
+        for i, measurements in enumerate(measurement_list):
+            if noise[i] >= self._threshold * self.NOISE_CI:
+                continue
+            basic_model = self._basic_modeler.model([measurements])[0]
+            if models[i].hypothesis.SMAPE < basic_model.hypothesis.SMAPE:
+                models[i] = basic_model
+
+    def _create_ml_models(self, functions, measurement_list):
+        models = []
+        for i, function in enumerate(functions):
+            if not function.compound_terms[0].simple_terms:
+                hypothesis = ConstantHypothesis(ConstantFunction(), self.use_median)
+            else:
+                hypothesis = SingleParameterHypothesis(function, self.use_median)
+            hypothesis.compute_coefficients(measurement_list[i])
+            hypothesis.compute_cost(measurement_list[i])
+            models.append(Model(hypothesis))
+        return models
+
+    @staticmethod
+    def _preprocess(positions, values):
+        max_p = np.max(positions)
+        max_sp = _BUCKET_COORDINATES[-1]
+        return (positions * max_sp) / max_p, values / positions
+
+    def _predict_functions(self, positions, values, noise_orig):
+        if extrapadaptivemodeler:
+            tf = load_tensorflow()
+            
+            transformed_points, transformed_values_list = self._preprocess(positions, values)
+            bucket_indices = self._bucketize([(p, i + 1) for i, p in enumerate(transformed_points)])
+            noise_category_list = self._noise_category(noise_orig)
+
+            top_k_classes = []
+            for noise_category, groups in groupby(zip(noise_category_list, transformed_values_list),
+                                                key=lambda ms: ms[0]):
+                transformed_values = np.array([v for _, v in groups])
+                ml_model, self._cached_mlmodels = get_model(bucket_indices, noise_category, positions, tf, self.retrain_epochs, self.retrain_examples_per_class, self._cached_mlmodels)
+
+                bucket_values = np.zeros((len(transformed_values), len(bucket_indices)))
+                for bi, pi in enumerate(bucket_indices):
+                    if pi > 0:
+                        bucket_values[:, bi] = transformed_values[:, pi - 1]
+
+                # do not use model.predict() to prevent memory leak
+                prediction = ml_model(bucket_values)
+                top_k_classes.extend([int(c) for c in tf.math.top_k(prediction, 1).indices])
+            terms = (_TERMS[c] for c in top_k_classes)
+            functions = (SingleParameterFunction(copy.copy(t)) for t in terms)
+        
+        else:
+            print("To use the adaptive modeler, please install Extra-P with the adaptive modeler extension.\nYou can do that using 'pip install extrap[adaptive_modeling]'.")
+            functions = None
+            
+        return functions
+
+    def get_noise(self, mean_values, min_values, max_values):
+        mean_values = mean_values.copy()
+        mean_values[mean_values == 0] = 1
+        rel_max = np.nanmax(max_values / mean_values, axis=1)
+        rel_min = np.nanmin(min_values / mean_values, axis=1)
+        return rel_max - rel_min
+
+    def _noise_category(self, noise):
+        if not self.noise_aware:
+            return np.full_like(noise, 0.2)
+        categories = _NOISE_CATEGORIES
+        thresholds = [(categories[i] + categories[i + 1]) / 2 for i in range(len(categories) - 1)]
+        idx = np.searchsorted(thresholds, noise)
+        return np.array(categories)[idx]
+
+    # def create_model_simple(self, legacy_info, is_exponential=False):
+    #     if isinstance(self._modeler, EXTRAP.SingleParameterSimpleModelGenerator):
+    #         self._modeler.generateHypothesisBuildingBlockSet([], [])
+    #         terms = self.exponential_terms if is_exponential else self.terms
+    #         for exp in terms:
+    #             term = EXTRAP.CompoundTerm.fromLegacy(*exp)
+    #             self._modeler.addHypothesisBuildingBlock(term)
+    #         return self._modeler.createModel(*legacy_info)
+    #     else:
+    #         raise TypeError()
+
+    @staticmethod
+    def _bucketize(points):
+        bucket_count = len(_BUCKET_COORDINATES)
+        point_buckets = [0] * bucket_count
+        point_buckets_delta = [float('inf')] * bucket_count
+        for p, v in points:
+            idx = bisect_left(_BUCKET_COORDINATES, p)
+            if idx >= bucket_count:
+                idx = bucket_count - 1
+            sp1 = _BUCKET_COORDINATES[idx]
+            if idx - 1 >= 0:
+                sp2 = _BUCKET_COORDINATES[idx - 1]
+            else:
+                sp2 = float('-inf')
+            if (sp1 - p <= p - sp2) and point_buckets_delta[idx] >= sp1 - p:
+                point_buckets_delta[idx] = sp1 - p
+                point_buckets[idx] = v
+            if (sp1 - p >= p - sp2) and point_buckets_delta[idx - 1] >= p - sp2:
+                point_buckets_delta[idx - 1] = p - sp2
+                point_buckets[idx - 1] = v
+        return point_buckets
diff --git a/extrap/modelers/single_parameter/segmented.py b/extrap/modelers/single_parameter/segmented.py
new file mode 100644
index 00000000..737fa80f
--- /dev/null
+++ b/extrap/modelers/single_parameter/segmented.py
@@ -0,0 +1,215 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2022, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import logging
+import math
+import warnings
+from typing import Sequence
+
+import numpy as np
+
+from extrap.entities.functions import SegmentedFunction
+from extrap.entities.hypotheses import SingleParameterHypothesis
+from extrap.entities.measurement import Measurement
+from extrap.entities.model import Model, SegmentedModel
+from extrap.modelers.modeler_options import modeler_options
+from extrap.modelers.single_parameter.basic import SingleParameterModeler
+
+
+@modeler_options
+class SegmentedModeler(SingleParameterModeler):
+    """
+    This class represents the modeler for segmented single parameter functions.
+    In order to create a model measurements at least 5 points are needed.
+    The result is either a constant function or one based on the PMNF.
+    """
+
+    NAME = 'Segmented'
+    DESCRIPTION = "Modeler for single-parameter models; traverses the search-space of all defined hypotheses. Able to detect segmented behavior in measurements. When segmented data is found the modeler will return two models."
+
+    def __init__(self):
+        """
+        Initialize SegmentedParameterModeler object.
+        """
+        super().__init__()
+
+    def create_model(self, measurements: Sequence[Measurement]):
+        """
+        Create a model for the given callpath and metric using the given data.
+        """
+
+        # check if the number of measurements satisfies the requirements of the modeler (>=5)
+        if len(measurements) < self.min_measurement_points:
+            warnings.warn(
+                "Number of measurements for a parameter needs to be at least 5 in order to create a performance model.")
+
+        # identify subsets
+        subsets = []
+        nr_subsets = len(measurements) - (self.min_measurement_points - 1)
+        # print("DEBUG nr_subsets:",nr_subsets)
+        for i in range(nr_subsets):
+            subset = []
+            for j in range(self.min_measurement_points):
+                subset.append(measurements[i + j])
+            # print("subset:",subset)
+            subsets.append(subset)
+        # print("DEBUG subsets:",subsets)
+
+        # create a model for each subset
+        models = []
+        for subset in subsets:
+
+            # create a constant model
+            constant_hypothesis, constant_cost = self.create_constant_model(subset)
+            logging.debug("Constant model: " + constant_hypothesis.function.to_string())
+            logging.debug("Constant model cost: " + str(constant_cost))
+
+            # use constant model when cost is 0
+            if constant_cost == 0:
+                logging.debug("Using constant model.")
+                models.append(Model(constant_hypothesis))
+
+            # otherwise start searching for the best hypothesis based on the PMNF
+            else:
+                logging.debug("Searching for a single-parameter model.")
+                # search for the best single parameter hypothesis
+                hypotheses_generator = self.build_hypotheses(subset)
+                best_hypothesis = self.find_best_hypothesis(hypotheses_generator, constant_cost, subset,
+                                                            constant_hypothesis)
+                models.append(best_hypothesis)
+
+        # print("DEBUG models:",models)
+        nRSS_values = []
+        for m in models:
+            # print("nRSS:",m._nRSS)
+            nRSS_values.append(abs(m._nRSS))
+            # print(str(m.function))
+
+        # print(nRSS_values)
+        theta = max(nRSS_values)
+
+        epsilon_values = []
+        for i in range(len(subsets)):
+            if i == 0:
+                epsilon_values.append(-math.inf)
+            else:
+                epsilon_values.append(nRSS_values[i] / (nRSS_values[i - 1] + 0.000000001))
+
+        dataset_segmented = False
+        if theta > 0.5:
+            dataset_segmented = True
+        if len(epsilon_values)==1 and math.isnan(epsilon_values[0]):
+            dataset_segmented = False
+        else:
+            if np.nanmax(epsilon_values) > 4:
+                dataset_segmented = True
+
+        # print("DEBUG dataset_segmented:",dataset_segmented)
+        # print("DEBUG epsilon_values:",epsilon_values)
+
+        if dataset_segmented:
+
+            pattern = ""
+            for i in range(len(nRSS_values)):
+                nRSS = nRSS_values[i]
+                epsilon = epsilon_values[i]
+                if nRSS >= 0.1:
+                    pattern += "1"
+                elif epsilon > 4:
+                    pattern += "1"
+                else:
+                    pattern += "0"
+
+            # print("DEBUG pattern:",pattern)
+
+            import re
+            index = [m.start() for m in re.finditer(r"1", pattern)][2]
+            # print("DEBUG index:",index)
+            ones = 0
+            for c in pattern:
+                if c == "1":
+                    ones += 1
+            # print("DEBUG ones:",ones)
+            change_point = None
+            if ones == 3:
+                subset = subsets[index - 1]
+                change_point = subset[2]
+            else:
+                subset = subsets[index - 1]
+                change_point = [subset[2], subset[3]]
+
+            # print("DEBUG change_point:",change_point)
+            models = []
+            # if the change point is a common point in both sets
+            if isinstance(change_point, Measurement):
+                index = measurements.index(change_point)
+                # print("DEBUG index:",index)
+                subsets = []
+                subsets.append(measurements[:index])
+                subsets.append(measurements[index:])
+
+            # if the change point is a point between to point of the measurements
+            else:
+                index_1 = measurements.index(change_point[0])
+                index_2 = measurements.index(change_point[1])
+                # print("DEBUG index:",index_1, index_2)
+                subsets = []
+                subsets.append(measurements[:index_1])
+                subsets.append(measurements[index_2:])
+                # print("DEBUG subsets:",subsets)
+
+            for subset in subsets:
+                # create a constant model
+                constant_hypothesis, constant_cost = self.create_constant_model(subset)
+                logging.debug("Constant model: " + constant_hypothesis.function.to_string())
+                logging.debug("Constant model cost: " + str(constant_cost))
+
+                # use constant model when cost is 0
+                if constant_cost == 0:
+                    logging.debug("Using constant model.")
+                    models.append(Model(constant_hypothesis))
+
+                # otherwise start searching for the best hypothesis based on the PMNF
+                else:
+                    logging.debug("Searching for a single-parameter model.")
+                    # search for the best single parameter hypothesis
+                    hypotheses_generator = self.build_hypotheses(subset)
+                    best_hypothesis = self.find_best_hypothesis(hypotheses_generator, constant_cost, subset,
+                                                                constant_hypothesis)
+                    models.append(Model(best_hypothesis))
+
+            if isinstance(change_point, Measurement):
+                intervals = [(-math.inf, change_point.coordinate[0]), (change_point.coordinate[0], math.inf)]
+                change_point = [change_point]
+            else:
+                intervals = [(-math.inf, change_point[0].coordinate[0]), (change_point[1].coordinate[0], math.inf)]
+
+            function = SegmentedFunction([m.hypothesis.function for m in models], intervals)
+            hypothesis = SingleParameterHypothesis(function, self.use_median)
+            hypothesis.compute_cost(measurements)
+            hypothesis.compute_adjusted_rsquared(self.create_constant_model(measurements)[1], measurements)
+            return SegmentedModel(hypothesis, models, change_point)
+
+        else:
+            # create a constant model
+            constant_hypothesis, constant_cost = self.create_constant_model(measurements)
+            logging.debug("Constant model: " + constant_hypothesis.function.to_string())
+            logging.debug("Constant model cost: " + str(constant_cost))
+
+            # use constant model when cost is 0
+            if constant_cost == 0:
+                logging.debug("Using constant model.")
+                return Model(constant_hypothesis)
+
+            # otherwise start searching for the best hypothesis based on the PMNF
+            else:
+                logging.debug("Searching for a single-parameter model.")
+                # search for the best single parameter hypothesis
+                hypotheses_generator = self.build_hypotheses(measurements)
+                best_hypothesis = self.find_best_hypothesis(hypotheses_generator, constant_cost, measurements,
+                                                            constant_hypothesis)
+                return Model(best_hypothesis)
diff --git a/extrap/util/dynamic_options.py b/extrap/util/dynamic_options.py
new file mode 100644
index 00000000..d7f262ed
--- /dev/null
+++ b/extrap/util/dynamic_options.py
@@ -0,0 +1,187 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+from __future__ import annotations
+
+import inspect
+import types
+from dataclasses import dataclass
+from dataclasses import field as d_field
+from typing import TypeVar, Generic, Sequence, Type, AnyStr, Mapping, Union, Callable, Any
+
+from extrap.util.unique_list import UniqueList
+
+T = TypeVar('T')
+
+
+@dataclass
+class DynamicOption(Generic[T]):
+    value: T
+    type: Type
+    description: str
+    name: str = None
+    range: Union[Mapping[AnyStr, T], range] = None
+    group = None
+    field: str = None
+    on_change: Callable[[Any, T], None] = d_field(default=None, compare=False, hash=False)
+    explanation_above: str = None
+
+
+@dataclass
+class DynamicOptionsGroup:
+    name: str
+    options: Sequence[DynamicOption]
+    description: str
+
+    def items(self):
+        return ((o.field, o) for o in self.options)
+
+
+def getmembers(object, predicate=None):
+    """Return all members of an object as (name, value) pairs sorted by name.
+    Optionally, only return members that satisfy a given predicate."""
+    if inspect.isclass(object):
+        mro = (object,) + inspect.getmro(object)
+    else:
+        mro = ()
+    results = []
+    processed = set()
+    names = dir(object)
+    # :dd any DynamicClassAttributes to the list of names if object is a class;
+    # this may result in duplicate entries if, for example, a virtual
+    # attribute with the same name as a DynamicClassAttribute exists
+    try:
+        for base in object.__bases__:
+            for k, v in base.__dict__.items():
+                if isinstance(v, types.DynamicClassAttribute):
+                    names.append(k)
+    except AttributeError:
+        pass
+    for key in names:
+        # First try to get the value via getattr.  Some descriptors don't
+        # like calling their __get__ (see bug #1785), so fall back to
+        # looking in the __dict__.
+        try:
+            value = getattr(object, key)
+            # handle the duplicate key
+            if key in processed:
+                raise AttributeError
+        except AttributeError:
+            for base in mro:
+                if key in base.__dict__:
+                    value = base.__dict__[key]
+                    break
+            else:
+                # could be a (currently) missing slot member, or a buggy
+                # __dir__; discard and move on
+                continue
+        if not predicate or predicate(value):
+            results.append((key, value))
+        processed.add(key)
+    results.sort(key=lambda pair: pair[0])
+    return results
+
+
+def convert_dynamic_options(object_with_options):
+    option_storage = []
+
+    names = UniqueList()
+    for base in object_with_options.__bases__:
+        names.extend(base.__dict__)
+        
+    names.extend(object_with_options.__dict__)
+
+    for name in names:
+        try:
+            option: DynamicOption = getattr(object_with_options, name)
+            if isinstance(option, DynamicOption):
+                if option.group is not None:
+                    object_with_options.OPTIONS[option.group.name] = option.group
+                else:
+                    object_with_options.OPTIONS[name] = option
+                option.field = name
+                if option.on_change is not None:
+                    def make_property(opt):
+                        # required for closure
+                        def getter(m_self):
+                            return getattr(m_self, '__OPTION_' + opt.field)
+
+                        def setter(m_self, v):
+                            setattr(m_self, '__OPTION_' + opt.field, v)
+                            opt.on_change(m_self, v)
+
+                        return property(getter, setter)
+
+                    setattr(object_with_options, name, make_property(option))
+                    option_storage.append(('__OPTION_' + option.field, option.value))
+                else:
+                    setattr(object_with_options, name, option.value)
+        except Exception as e:
+            pass
+    for n, v in option_storage:
+        setattr(object_with_options, n, v)
+
+
+class DynamicOptions:
+    def __new__(cls, *args, **kwargs):
+        if not hasattr(cls, 'OPTIONS'):
+            cls.OPTIONS = {}
+        convert_dynamic_options(cls)
+        return super().__new__(cls)
+
+    @staticmethod
+    def add(value: T, type: Type = str, description: str = None, name: str = None,
+            range: Union[Mapping[AnyStr, T], range] = None,
+            on_change: Callable[[Any, T], None] = None) -> T:
+        """
+        Creates a new dynamic option.
+
+        :param value:       The default value of the option
+        :param type:        The data type of the value.
+                            Must support the conversion from a string if called with a string parameter.
+        :param description: A description of the option. Is shown in the command-line help and as tooltip in the GUI.
+        :param name:        Alternative display name. Replaces the automatic conversion of the field name.
+        :param range:       A range of possible alternatives, either a list, a mapping or a range.
+        :param on_change:   Callback function, that is called when the value of the option is changed.
+                            Its arguments are the instance self and the new value.
+        :return:            A DynamicOption object, that is processed during options creation and replaced by the value.
+        """
+        return DynamicOption(value=value, type=type, description=description, name=name, range=range,
+                             on_change=on_change)
+
+    @staticmethod
+    def group(name: str, *options: DynamicOption, description: str = None) -> DynamicOptionsGroup:
+        """
+        Groups options under the given name and adds an optional description for the group.
+        :param name:        Name of the group. Is shown in the command-line help and in the GUI.
+        :param options:     One or more options to group.
+        :param description: A description of the option. Is shown as tooltip in the GUI.
+        """
+        group = DynamicOptionsGroup(name=name, description=description, options=options)
+        for o in options:
+            o.group = group
+        return group
+
+    def options_equal(self, other: DynamicOptions) -> bool:
+        if not isinstance(other, DynamicOptions):
+            return NotImplemented
+        if self is other:
+            return True
+        elif self.OPTIONS != other.OPTIONS:
+            return False
+        for o in self.options_iter():
+            if getattr(self, o.field) != getattr(other, o.field):
+                return False
+        return True
+
+    def options_iter(self):
+        for o in self.OPTIONS.values():
+            if isinstance(o, DynamicOptionsGroup):
+                for go in o.options:
+                    yield go
+            else:
+                yield o
diff --git a/extrap/util/latex_formatting.py b/extrap/util/latex_formatting.py
new file mode 100644
index 00000000..f5afd191
--- /dev/null
+++ b/extrap/util/latex_formatting.py
@@ -0,0 +1,43 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import matplotlib.ticker as mticker
+
+
+def frmt_scientific_coefficient(coefficient):
+    """
+    This method takes a coefficient and formats it into a string using scientific notation.
+    """
+    formatter = mticker.ScalarFormatter(useMathText=True)
+    formatter.set_powerlimits((-3, 3))
+    formatted_coefficients = "{}".format(
+        formatter.format_data(float(coefficient)))
+    coreff_terms = formatted_coefficients.split(" ")
+    new_coeff = ""
+    if not coreff_terms[0][:1].isnumeric():
+        coeff = coreff_terms[0][1:]
+        try:
+            coeff = "{:.3f}".format(float(coeff))
+        except ValueError:
+            pass
+        new_coeff += "-"
+        new_coeff += coeff
+        for i in range(len(coreff_terms)):
+            if i != 0:
+                new_coeff += coreff_terms[i]
+        return new_coeff
+    else:
+        coeff = coreff_terms[0]
+        try:
+            coeff = "{:.3f}".format(float(coeff))
+        except ValueError:
+            pass
+        new_coeff += coeff
+        for i in range(len(coreff_terms)):
+            if i != 0:
+                new_coeff += coreff_terms[i]
+        return new_coeff
diff --git a/extrap/util/progress_bar.py b/extrap/util/progress_bar.py
index ad5cb5c7..1a968d63 100644
--- a/extrap/util/progress_bar.py
+++ b/extrap/util/progress_bar.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
diff --git a/extrap/util/serialization_schema.py b/extrap/util/serialization_schema.py
index ba34c41c..5a2e733d 100644
--- a/extrap/util/serialization_schema.py
+++ b/extrap/util/serialization_schema.py
@@ -259,3 +259,76 @@ def _deserialize(self, value: typing.Any, attr: str, data: typing.Optional[typin
             return self.list_type(result)
         else:
             return result
+
+
+class VariantSchemaField(fields.Nested):
+    VARIANT_KEY = "$variant"
+
+    def __init__(self, default_schema, *alternatives: typing.Type[Schema], **kwargs):
+        self.alternatives = {type(a().create_object()).__name__: a() for a in alternatives}
+        super().__init__(default_schema, **kwargs)
+
+    def _serialize(self, value: typing.Any, attr: typing.Optional[str], obj: typing.Any, **kwargs):
+        schema = self.alternatives.get(type(value).__name__, None)
+        if schema:
+            result = schema.dump(value)
+            result[self.VARIANT_KEY] = type(value).__name__
+            return result
+        else:
+            return super()._serialize(value, attr, obj, **kwargs)
+
+    def _deserialize(self, value: typing.Any, attr: typing.Optional[str],
+                     data: typing.Optional[typing.Mapping[str, typing.Any]], **kwargs):
+        if self.VARIANT_KEY in value:
+            schema_name = value.pop(self.VARIANT_KEY)
+            return self.alternatives[schema_name].load(value)
+        else:
+            return super()._deserialize(value, attr, data, **kwargs)
+
+
+class NumpyField(fields.List):
+    import numpy
+
+    def __init__(self, **kwargs):
+        super().__init__(fields.Field, **kwargs)
+
+    def _deserialize(self, value, attr, obj, **kwargs):
+        stack = [value]
+
+        while stack:
+            elem = stack.pop()
+            for i, e in enumerate(elem):
+                if type(e) == list:
+                    stack.append(e)
+                elif type(e) == str:
+                    elem[i] = float(math.nan)
+                elif math.isfinite(e):
+                    continue
+                else:
+                    raise NotImplementedError()
+
+        arr = self.numpy.array(value)
+        return arr
+
+    def _serialize(self, value, attr, obj, **kwargs):
+        list_val = value.tolist()
+
+        stack = [list_val]
+
+        while stack:
+            elem = stack.pop()
+            for i, e in enumerate(elem):
+                if type(e) == list:
+                    stack.append(e)
+                elif math.isfinite(e):
+                    continue
+                elif math.isnan(e):
+                    elem[i] = 'nan'
+                elif e == math.inf:
+                    elem[i] = 'inf'
+                elif e == -math.inf:
+                    elem[i] = '-inf'
+                else:
+                    raise NotImplementedError
+
+        return super()._serialize(list_val, attr, obj)
diff --git a/extrap/util/string_formats.py b/extrap/util/string_formats.py
new file mode 100644
index 00000000..3d856c08
--- /dev/null
+++ b/extrap/util/string_formats.py
@@ -0,0 +1,14 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import enum
+
+
+class FunctionFormats(enum.Enum):
+    NONE = None,
+    PYTHON = enum.auto()
+    LATEX = enum.auto()
diff --git a/setup.py b/setup.py
index d11412ef..173587df 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2024, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -60,6 +60,10 @@
         "pycubexr>=1.1,<3",
         "marshmallow~=3.7",
         "packaging>=20.0",
-        "pyobjc-framework-Cocoa~=9.0; sys_platform == 'darwin'"
-    ]
+        "pyobjc-framework-Cocoa~=9.0; sys_platform == 'darwin'",
+        "scikit-learn>=1.2.2"
+    ],
+    extras_require={
+        "adaptive_modeling": ["extrapadaptivemodeler>=1.0.0"],
+    },
 )


Test patch:
diff --git a/tests/data/text/gpr/1_param_add.txt b/tests/data/text/gpr/1_param_add.txt
new file mode 100644
index 00000000..1d7d5c2d
--- /dev/null
+++ b/tests/data/text/gpr/1_param_add.txt
@@ -0,0 +1,16 @@
+PARAMETER p
+
+POINTS ( 32 )
+POINTS ( 64 )
+POINTS ( 128 )
+POINTS ( 256 )
+POINTS ( 512 )
+
+METRIC time
+
+REGION   main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base.txt b/tests/data/text/gpr/1_param_base.txt
new file mode 100644
index 00000000..95563d87
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base.txt
@@ -0,0 +1,12 @@
+PARAMETER p
+
+POINTS ( 32 )
+POINTS ( 64 )
+POINTS ( 256 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base2.txt b/tests/data/text/gpr/1_param_base2.txt
new file mode 100644
index 00000000..3e3258a2
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base2.txt
@@ -0,0 +1,14 @@
+PARAMETER p
+
+POINTS ( 32 )
+POINTS ( 64 )
+POINTS ( 256 )
+POINTS ( 512 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 800 800
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base3.txt b/tests/data/text/gpr/1_param_base3.txt
new file mode 100644
index 00000000..f0d3a902
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base3.txt
@@ -0,0 +1,14 @@
+PARAMETER p
+
+POINTS ( 10 )
+POINTS ( 20 )
+POINTS ( 40 )
+POINTS ( 50 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 800 800
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base4.txt b/tests/data/text/gpr/1_param_base4.txt
new file mode 100644
index 00000000..258ad5ad
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base4.txt
@@ -0,0 +1,12 @@
+PARAMETER p
+
+POINTS ( 10 )
+POINTS ( 20 )
+POINTS ( 40 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base5.txt b/tests/data/text/gpr/1_param_base5.txt
new file mode 100644
index 00000000..6be0ed29
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base5.txt
@@ -0,0 +1,12 @@
+PARAMETER p
+
+POINTS ( 8 )
+POINTS ( 27 )
+POINTS ( 64 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base6.txt b/tests/data/text/gpr/1_param_base6.txt
new file mode 100644
index 00000000..0c3797c7
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base6.txt
@@ -0,0 +1,12 @@
+PARAMETER p
+
+POINTS ( 8 )
+POINTS ( 27 )
+POINTS ( 125 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base7.txt b/tests/data/text/gpr/1_param_base7.txt
new file mode 100644
index 00000000..0aca1c10
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base7.txt
@@ -0,0 +1,14 @@
+PARAMETER p
+
+POINTS ( 8 )
+POINTS ( 64 )
+POINTS ( 4096 )
+POINTS ( 32768 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_base8.txt b/tests/data/text/gpr/1_param_base8.txt
new file mode 100644
index 00000000..f58c6b12
--- /dev/null
+++ b/tests/data/text/gpr/1_param_base8.txt
@@ -0,0 +1,14 @@
+PARAMETER p
+
+POINTS ( 8 )
+POINTS ( 64 )
+POINTS ( 512 )
+POINTS ( 4096 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 616.758 616.42
\ No newline at end of file
diff --git a/tests/data/text/gpr/1_param_gpr.txt b/tests/data/text/gpr/1_param_gpr.txt
new file mode 100644
index 00000000..f9255122
--- /dev/null
+++ b/tests/data/text/gpr/1_param_gpr.txt
@@ -0,0 +1,18 @@
+PARAMETER p
+
+POINTS ( 32 )
+POINTS ( 64 )
+POINTS ( 128 )
+POINTS ( 256 )
+POINTS ( 512 )
+POINTS ( 1024 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
diff --git a/tests/data/text/gpr/2_param_add.txt b/tests/data/text/gpr/2_param_add.txt
new file mode 100644
index 00000000..4f7ca5e9
--- /dev/null
+++ b/tests/data/text/gpr/2_param_add.txt
@@ -0,0 +1,25 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 8000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 128 5000 )
+POINTS ( 256 5000 )
+POINTS ( 512 5000 )
+
+METRIC time
+
+REGION   main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
\ No newline at end of file
diff --git a/tests/data/text/gpr/2_param_add2.txt b/tests/data/text/gpr/2_param_add2.txt
new file mode 100644
index 00000000..7ed62948
--- /dev/null
+++ b/tests/data/text/gpr/2_param_add2.txt
@@ -0,0 +1,47 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 8000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 128 5000 )
+POINTS ( 256 5000 )
+POINTS ( 512 5000 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+
+REGION sweep()
+DATA 10 10
+DATA 20 20 
+DATA 30 30
+DATA 40 40 
+DATA 50 50
+DATA 25 25
+DATA 35 35
+DATA 45 45
+DATA 55 55
+
+REGION blast()
+DATA 5 5
+DATA 10 10 
+DATA 15 15
+DATA 20 20 
+DATA 25 25
+DATA 25 25
+DATA 35 35
+DATA 45 45
+DATA 55 55
\ No newline at end of file
diff --git a/tests/data/text/gpr/2_param_base.txt b/tests/data/text/gpr/2_param_base.txt
new file mode 100644
index 00000000..faa34c39
--- /dev/null
+++ b/tests/data/text/gpr/2_param_base.txt
@@ -0,0 +1,21 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 128 5000 )
+POINTS ( 256 5000 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
\ No newline at end of file
diff --git a/tests/data/text/gpr/2_param_gpr.txt b/tests/data/text/gpr/2_param_gpr.txt
new file mode 100644
index 00000000..373e324c
--- /dev/null
+++ b/tests/data/text/gpr/2_param_gpr.txt
@@ -0,0 +1,27 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 8000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 64 6000 )
+POINTS ( 128 5000 )
+POINTS ( 256 5000 )
+POINTS ( 512 5000 )
+
+METRIC time
+
+REGION   main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1187.13 1177.51
\ No newline at end of file
diff --git a/tests/data/text/gpr/2_param_gpr2.txt b/tests/data/text/gpr/2_param_gpr2.txt
new file mode 100644
index 00000000..87e682d1
--- /dev/null
+++ b/tests/data/text/gpr/2_param_gpr2.txt
@@ -0,0 +1,51 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 8000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 64 6000 )
+POINTS ( 128 5000 )
+POINTS ( 256 5000 )
+POINTS ( 512 5000 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1187.13 1177.51
+
+REGION sweep()
+DATA 10 10 
+DATA 20 20 
+DATA 30 30 
+DATA 40 40 
+DATA 50 50 
+DATA 60 60 
+DATA 70 70 
+DATA 80 80 
+DATA 90 90 
+DATA 100 100 
+
+REGION blast()
+DATA 20 20 
+DATA 40 40 
+DATA 60 60 
+DATA 80 80 
+DATA 100 100
+DATA 120 120
+DATA 140 140
+DATA 160 160
+DATA 180 180
+DATA 200 200
\ No newline at end of file
diff --git a/tests/data/text/gpr/3_param_add.txt b/tests/data/text/gpr/3_param_add.txt
new file mode 100644
index 00000000..42f8f165
--- /dev/null
+++ b/tests/data/text/gpr/3_param_add.txt
@@ -0,0 +1,34 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+
+POINTS ( 32 5000 10 )
+POINTS ( 32 6000 10 )
+POINTS ( 32 7000 10 )
+POINTS ( 32 8000 10 )
+POINTS ( 32 9000 10 )
+POINTS ( 64 5000 10 )
+POINTS ( 128 5000 10 )
+POINTS ( 256 5000 10 )
+POINTS ( 512 5000 10 )
+POINTS ( 32 5000 20 )
+POINTS ( 32 5000 30 )
+POINTS ( 32 5000 40 )
+POINTS ( 32 5000 50 )
+
+METRIC time
+
+REGION   main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
\ No newline at end of file
diff --git a/tests/data/text/gpr/3_param_base.txt b/tests/data/text/gpr/3_param_base.txt
new file mode 100644
index 00000000..83eac0d0
--- /dev/null
+++ b/tests/data/text/gpr/3_param_base.txt
@@ -0,0 +1,28 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+
+POINTS ( 32 5000 10 )
+POINTS ( 32 6000 10 )
+POINTS ( 32 7000 10 )
+POINTS ( 32 8000 10)
+POINTS ( 64 5000 10 )
+POINTS ( 128 5000 10 )
+POINTS ( 256 5000 10 )
+POINTS ( 32 5000 20 )
+POINTS ( 32 5000 30 )
+POINTS ( 32 5000 40 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 406.498 405.58
+DATA 406.498 405.58
+DATA 406.498 405.58
\ No newline at end of file
diff --git a/tests/data/text/gpr/3_param_gpr.txt b/tests/data/text/gpr/3_param_gpr.txt
new file mode 100644
index 00000000..bcb7c7c7
--- /dev/null
+++ b/tests/data/text/gpr/3_param_gpr.txt
@@ -0,0 +1,36 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+
+POINTS ( 32 5000 100 )
+POINTS ( 32 6000 100 )
+POINTS ( 32 7000 100 )
+POINTS ( 32 8000 100 )
+POINTS ( 32 9000 100 )
+POINTS ( 64 5000 100 )
+POINTS ( 64 6000 100 )
+POINTS ( 128 5000 100 )
+POINTS ( 256 5000 100 )
+POINTS ( 512 5000 100 )
+POINTS ( 32 5000 200 )
+POINTS ( 32 5000 300 )
+POINTS ( 32 5000 400 )
+POINTS ( 32 5000 500 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
\ No newline at end of file
diff --git a/tests/data/text/gpr/4_param_add.txt b/tests/data/text/gpr/4_param_add.txt
new file mode 100644
index 00000000..282ebb55
--- /dev/null
+++ b/tests/data/text/gpr/4_param_add.txt
@@ -0,0 +1,43 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+PARAMETER o
+
+POINTS ( 32 5000 10 100 )
+POINTS ( 32 6000 10 100 )
+POINTS ( 32 7000 10 100 )
+POINTS ( 32 8000 10 100 )
+POINTS ( 32 9000 10 100 )
+POINTS ( 64 5000 10 100 )
+POINTS ( 128 5000 10 100 )
+POINTS ( 256 5000 10 100 )
+POINTS ( 512 5000 10 100 )
+POINTS ( 32 5000 20 100 )
+POINTS ( 32 5000 30 100 )
+POINTS ( 32 5000 40 100 )
+POINTS ( 32 5000 50 100 )
+POINTS ( 32 5000 10 200 )
+POINTS ( 32 5000 10 300 )
+POINTS ( 32 5000 10 400 )
+POINTS ( 32 5000 10 500 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
+DATA 1026.49 1023.95
\ No newline at end of file
diff --git a/tests/data/text/gpr/4_param_base.txt b/tests/data/text/gpr/4_param_base.txt
new file mode 100644
index 00000000..dcf44a9b
--- /dev/null
+++ b/tests/data/text/gpr/4_param_base.txt
@@ -0,0 +1,35 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+PARAMETER o
+
+POINTS ( 32 5000 10 100 )
+POINTS ( 32 6000 10 100 )
+POINTS ( 32 7000 10 100 )
+POINTS ( 32 8000 10 100 )
+POINTS ( 64 5000 10 100 )
+POINTS ( 128 5000 10 100 )
+POINTS ( 256 5000 10 100 )
+POINTS ( 32 5000 20 100 )
+POINTS ( 32 5000 30 100 )
+POINTS ( 32 5000 40 100 )
+POINTS ( 32 5000 10 200 )
+POINTS ( 32 5000 10 300 )
+POINTS ( 32 5000 10 400 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 406.498 405.58
+DATA 406.498 405.58
+DATA 406.498 405.58
+DATA 406.498 405.58
+DATA 406.498 405.58
+DATA 406.498 405.58
\ No newline at end of file
diff --git a/tests/data/text/gpr/4_param_gpr.txt b/tests/data/text/gpr/4_param_gpr.txt
new file mode 100644
index 00000000..f6684e24
--- /dev/null
+++ b/tests/data/text/gpr/4_param_gpr.txt
@@ -0,0 +1,45 @@
+PARAMETER p
+PARAMETER n
+PARAMETER s
+PARAMETER o
+
+POINTS ( 32 5000 100 10 )
+POINTS ( 32 6000 100 10 )
+POINTS ( 32 7000 100 10 )
+POINTS ( 32 8000 100 10 )
+POINTS ( 32 9000 100 10 )
+POINTS ( 64 5000 100 10 )
+POINTS ( 64 6000 100 10 )
+POINTS ( 128 5000 100 10 )
+POINTS ( 256 5000 100 10 )
+POINTS ( 512 5000 100 10 )
+POINTS ( 32 5000 200 10 )
+POINTS ( 32 5000 300 10 )
+POINTS ( 32 5000 400 10 )
+POINTS ( 32 5000 500 10 )
+POINTS ( 32 5000 100 20 )
+POINTS ( 32 5000 100 30 )
+POINTS ( 32 5000 100 40 )
+POINTS ( 32 5000 100 50 )
+
+METRIC time
+
+REGION main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
+DATA 1187.13 1177.51
\ No newline at end of file
diff --git a/tests/data/text/gpr/gpr_test_data_1.txt b/tests/data/text/gpr/gpr_test_data_1.txt
new file mode 100644
index 00000000..62b136c1
--- /dev/null
+++ b/tests/data/text/gpr/gpr_test_data_1.txt
@@ -0,0 +1,27 @@
+PARAMETER x
+
+POINTS ( 20 )
+POINTS ( 30 )
+POINTS ( 40 )
+POINTS ( 50 )
+
+REGION compute
+METRIC time
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795 82.54252892405795
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083 177.93461023001083
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126 323.2719163782126
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357 509.6665018646357
+
+REGION scatter
+METRIC time
+DATA 20 21 22 23 24
+DATA 40 44 47 41 38
+DATA 100 105 95 97 98
+DATA 160 165 167 155 170
+
+REGION scatter
+METRIC maxtime
+DATA 20 21 22 23 24
+DATA 40 44 47 41 38
+DATA 100 105 95 97 98
+DATA 160 165 167 155 170
\ No newline at end of file
diff --git a/tests/data/text/gpr/gpr_test_data_2.txt b/tests/data/text/gpr/gpr_test_data_2.txt
new file mode 100644
index 00000000..1de3c545
--- /dev/null
+++ b/tests/data/text/gpr/gpr_test_data_2.txt
@@ -0,0 +1,31 @@
+PARAMETER x
+
+POINTS ( 20 )
+POINTS ( 30 )
+POINTS ( 40 )
+POINTS ( 50 )
+POINTS ( 60 )
+
+REGION compute
+METRIC time
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795 82.54252892405795
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083 177.93461023001083
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126 323.2719163782126
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357 509.6665018646357
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007 721.0616186790007
+
+REGION scatter
+METRIC time
+DATA 20 21 22 23 24
+DATA 40 44 47 41 38
+DATA 100 105 95 97 98
+DATA 160 165 167 155 170
+DATA 240 245 244 238 250
+
+REGION scatter
+METRIC maxtime
+DATA 20 21 22 23 24
+DATA 40 44 47 41 38
+DATA 100 105 95 97 98
+DATA 160 165 167 155 170
+DATA 240 245 244 238 250
\ No newline at end of file
diff --git a/tests/data/text/gpr/relearn_data.txt b/tests/data/text/gpr/relearn_data.txt
new file mode 100644
index 00000000..b5e91543
--- /dev/null
+++ b/tests/data/text/gpr/relearn_data.txt
@@ -0,0 +1,409 @@
+PARAMETER p
+PARAMETER n
+
+POINTS ( 32 5000 )
+POINTS ( 32 6000 )
+POINTS ( 32 7000 )
+POINTS ( 32 8000 )
+POINTS ( 32 9000 )
+POINTS ( 64 5000 )
+POINTS ( 64 6000 )
+POINTS ( 64 7000 )
+POINTS ( 64 8000 )
+POINTS ( 64 9000 )
+POINTS ( 128 5000 )
+POINTS ( 128 6000 )
+POINTS ( 128 7000 )
+POINTS ( 128 8000 )
+POINTS ( 128 9000 )
+POINTS ( 256 5000 )
+POINTS ( 256 6000 )
+POINTS ( 256 7000 )
+POINTS ( 256 8000 )
+POINTS ( 256 9000 )
+POINTS ( 512 5000 )
+POINTS ( 512 6000 )
+POINTS ( 512 7000 )
+POINTS ( 512 8000 )
+POINTS ( 512 9000 )
+
+METRIC time
+
+REGION   main()
+DATA 406.498 405.58
+DATA 509.797 509.876
+DATA 616.758 616.42
+DATA 721.753 724.038
+DATA 834.664 835.892
+DATA 574.5 574.589
+DATA 714.7 719.669
+DATA 870.245 871.598
+DATA 1026.49 1023.95
+DATA 1187.13 1177.51
+DATA 939.909 940.9
+DATA 1169.72 1168.36
+DATA 1410.23 1400.14
+DATA 1641.32 1626.44
+DATA 1878.03 1878.03
+DATA 1123.27 1117.89
+DATA 1377.43 1368.09
+DATA 1624.79 1633.66
+DATA 1880.71 1880.71
+DATA 2126.56 2126.56
+DATA 1277.87 1273.82
+DATA 1558.89 1555.38
+DATA 1855.03 1855.03
+DATA 2136.72 2136.72
+DATA 2536.75 2536.75
+
+REGION   Initialization
+DATA 0.316835 0.307923
+DATA 0.308246 0.303192
+DATA 0.309324 0.315418
+DATA 0.300496 0.30809
+DATA 0.310636 0.316279
+DATA 0.40473 0.394988
+DATA 0.409321 0.403722
+DATA 0.399487 0.402047
+DATA 0.401058 0.400879
+DATA 0.404664 0.398763
+DATA 0.582721 0.578961
+DATA 0.575156 0.584032
+DATA 0.591047 0.586803
+DATA 0.587019 0.586909
+DATA 0.583842 0.583842
+DATA 0.963826 0.94425
+DATA 0.967479 0.961514
+DATA 0.960842 0.958396
+DATA 0.953943 0.953943
+DATA 0.962906 0.962906
+DATA 1.70938 1.71041
+DATA 1.7041 1.71041
+DATA 1.70882 1.70882
+DATA 1.71355 1.71355
+DATA 1.7089 1.7089
+
+REGION   Simulation loop
+DATA 406.182 405.273
+DATA 509.49 509.575
+DATA 616.449 616.11
+DATA 721.454 723.732
+DATA 834.354 835.582
+DATA 574.101 574.197
+DATA 714.298 719.267
+DATA 869.847 871.196
+DATA 1026.09 1023.55
+DATA 1186.72 1177.12
+DATA 939.328 940.324
+DATA 1169.15 1167.78
+DATA 1409.64 1399.56
+DATA 1640.74 1625.85
+DATA 1877.45 1877.45
+DATA 1122.32 1116.95
+DATA 1376.47 1367.14
+DATA 1623.84 1632.7
+DATA 1879.77 1879.77
+DATA 2125.61 2125.61
+DATA 1276.17 1272.12
+DATA 1557.19 1553.67
+DATA 1853.32 1853.32
+DATA 2135.01 2135.01
+DATA 2535.05 2535.05
+
+REGION     Update electrical activity
+DATA 4e-08 4.4e-08
+DATA 3.9e-08 4.4e-08
+DATA 4.8e-08 3.8e-08
+DATA 3.7e-08 3.8e-08
+DATA 3.7e-08 5.7e-08
+DATA 4.5e-08 4.4e-08
+DATA 4.4e-08 4.5e-08
+DATA 5e-08 6.6e-08
+DATA 4.6e-08 4.1e-08
+DATA 4.3e-08 4.5e-08
+DATA 4.2e-08 4.3e-08
+DATA 5.5e-08 4.8e-08
+DATA 4.6e-08 4.6e-08
+DATA 4.1e-08 5.8e-08
+DATA 5.8e-08 5.8e-08
+DATA 6.1e-08 5.1e-08
+DATA 5.6e-08 6.2e-08
+DATA 6.4e-08 5.9e-08
+DATA 4.7e-08 4.7e-08
+DATA 5.6e-08 5.6e-08
+DATA 5e-08 5.4e-08
+DATA 4.7e-08 5.7e-08
+DATA 1.2e-07 1.2e-07
+DATA 4.5e-08 4.5e-08
+DATA 5.6e-08 5.6e-08
+
+REGION     Update #synaptic elements delta
+DATA 4.2e-08 4.3e-08
+DATA 3.9e-08 3.7e-08
+DATA 3.7e-08 3.7e-08
+DATA 3.6e-08 3.6e-08
+DATA 3.6e-08 3.8e-08
+DATA 4e-08 4.2e-08
+DATA 3.8e-08 3.7e-08
+DATA 4.7e-08 4.2e-08
+DATA 3.8e-08 3.9e-08
+DATA 3.6e-08 4.2e-08
+DATA 4.1e-08 3.8e-08
+DATA 4.4e-08 4.8e-08
+DATA 3.9e-08 4e-08
+DATA 4.2e-08 4.5e-08
+DATA 3.7e-08 3.7e-08
+DATA 3.8e-08 4.2e-08
+DATA 4.9e-08 8.6e-08
+DATA 4.1e-08 4.7e-08
+DATA 4.2e-08 4.2e-08
+DATA 5.3e-08 5.3e-08
+DATA 4.1e-08 4.8e-08
+DATA 4.3e-08 4.7e-08
+DATA 4.2e-08 4.2e-08
+DATA 4.2e-08 4.2e-08
+DATA 4.7e-08 4.7e-08
+
+REGION     Connectivity update
+DATA 406.181 405.273
+DATA 509.49 509.575
+DATA 616.449 616.11
+DATA 721.454 723.732
+DATA 834.353 835.582
+DATA 574.101 574.197
+DATA 714.298 719.267
+DATA 869.846 871.196
+DATA 1026.09 1023.55
+DATA 1186.72 1177.12
+DATA 939.328 940.324
+DATA 1169.15 1167.78
+DATA 1409.64 1399.56
+DATA 1640.74 1625.85
+DATA 1877.45 1877.45
+DATA 1122.32 1116.95
+DATA 1376.47 1367.14
+DATA 1623.84 1632.7
+DATA 1879.76 1879.76
+DATA 2125.61 2125.61
+DATA 1276.17 1272.11
+DATA 1557.19 1553.67
+DATA 1853.32 1853.32
+DATA 2135.01 2135.01
+DATA 2535.05 2535.05
+
+REGION       Update #synaptic elements + del synapses
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+DATA 0 0
+
+REGION       Update local trees
+DATA 0.000814297 0.000897314
+DATA 0.00103593 0.000914727
+DATA 0.00122921 0.00133041
+DATA 0.00138393 0.00137638
+DATA 0.00158487 0.0015119
+DATA 0.000752606 0.000779212
+DATA 0.00107401 0.000932538
+DATA 0.00120582 0.0010727
+DATA 0.00122156 0.00122013
+DATA 0.00155615 0.00154723
+DATA 0.000847228 0.000840412
+DATA 0.00103436 0.000894916
+DATA 0.00121853 0.00119473
+DATA 0.00138409 0.00134856
+DATA 0.0014247 0.0014247
+DATA 0.00177413 0.000875752
+DATA 0.00105974 0.0020008
+DATA 0.00221994 0.00223276
+DATA 0.00233216 0.00233216
+DATA 0.00267523 0.00267523
+DATA 0.000909142 0.000904373
+DATA 0.00106434 0.00108563
+DATA 0.00152545 0.00152545
+DATA 0.00143946 0.00143946
+DATA 0.00163392 0.00163392
+
+REGION       Exchange branch nodes (w/ Allgather)
+DATA 0.00105487 0.00223022
+DATA 0.00169496 0.00348653
+DATA 0.00120282 0.00868759
+DATA 0.00262168 0.00302139
+DATA 0.00148078 0.00831226
+DATA 0.007876 0.00867181
+DATA 0.00840319 0.00311396
+DATA 0.00249023 0.00235903
+DATA 0.0024297 0.00364925
+DATA 0.00471294 0.00931053
+DATA 0.0040848 0.00457541
+DATA 0.00301762 0.00636378
+DATA 0.00372692 0.00416439
+DATA 0.0090178 0.00838518
+DATA 0.00594086 0.00594086
+DATA 0.0106557 0.00410392
+DATA 0.0114093 0.0101913
+DATA 0.00954556 0.00536897
+DATA 0.0116183 0.0116183
+DATA 0.0107017 0.0107017
+DATA 0.0091911 0.00973494
+DATA 0.00512714 0.0107239
+DATA 0.00956868 0.00956868
+DATA 0.00898468 0.00898468
+DATA 0.0090483 0.0090483
+
+REGION       Insert branch nodes into global tree
+DATA 4.244e-06 4.336e-06
+DATA 4.4e-06 4.607e-06
+DATA 4.189e-06 9.743e-06
+DATA 4.204e-06 4.409e-06
+DATA 4.499e-06 4.525e-06
+DATA 4.196e-06 1.4135e-05
+DATA 4.452e-06 4.43e-06
+DATA 4.411e-06 6.993e-06
+DATA 1.4084e-05 1.2593e-05
+DATA 4.417e-06 6.863e-06
+DATA 3.8607e-05 4.5135e-05
+DATA 4.052e-05 3.7826e-05
+DATA 4.0212e-05 4.4108e-05
+DATA 3e-05 3.7051e-05
+DATA 4.1696e-05 4.1696e-05
+DATA 6.1622e-05 6.0206e-05
+DATA 7.121e-05 9.3346e-05
+DATA 6.8312e-05 0.000625515
+DATA 6.9647e-05 6.9647e-05
+DATA 6.4682e-05 6.4682e-05
+DATA 5.3222e-05 6.2271e-05
+DATA 4.9972e-05 0.000705011
+DATA 4.2942e-05 4.2942e-05
+DATA 4.7022e-05 4.7022e-05
+DATA 4.4627e-05 4.4627e-05
+
+REGION       Update global tree
+DATA 3.016e-06 3.057e-06
+DATA 2.507e-06 2.796e-06
+DATA 2.803e-06 1.2921e-05
+DATA 3.1e-06 3.095e-06
+DATA 2.712e-06 2.64e-06
+DATA 3.361e-06 3.261e-06
+DATA 2.605e-06 2.742e-06
+DATA 3.088e-06 3.461e-06
+DATA 1.3027e-05 1.0193e-05
+DATA 2.607e-06 2.737e-06
+DATA 2.7238e-05 1.3228e-05
+DATA 2.0617e-05 3.3967e-05
+DATA 1.618e-05 2.4534e-05
+DATA 2.3544e-05 2.3852e-05
+DATA 1.3215e-05 1.3215e-05
+DATA 2.5773e-05 1.7228e-05
+DATA 2.6192e-05 2.9769e-05
+DATA 2.6531e-05 3.4259e-05
+DATA 3.0756e-05 3.0756e-05
+DATA 2.2794e-05 2.2794e-05
+DATA 2.7183e-05 3.2875e-05
+DATA 3.1835e-05 2.9973e-05
+DATA 2.1919e-05 2.1919e-05
+DATA 2.4959e-05 2.4959e-05
+DATA 2.7939e-05 2.7939e-05
+
+REGION       Find target neurons (w/ RMA)
+DATA 406.084 405.175
+DATA 509.387 509.469
+DATA 616.335 615.99
+DATA 721.333 723.611
+DATA 834.223 835.45
+DATA 573.906 574.004
+DATA 714.11 719.067
+DATA 869.641 871.006
+DATA 1025.88 1023.34
+DATA 1186.49 1176.86
+DATA 939.005 940.011
+DATA 1168.81 1167.45
+DATA 1409.3 1399.22
+DATA 1640.39 1625.5
+DATA 1877.11 1877.11
+DATA 1121.77 1116.42
+DATA 1375.92 1366.6
+DATA 1623.25 1632.1
+DATA 1879.23 1879.23
+DATA 2125.08 2125.08
+DATA 1275.42 1271.33
+DATA 1556.47 1552.94
+DATA 1852.64 1852.64
+DATA 2134.35 2134.35
+DATA 2534.31 2534.31
+
+REGION       Empty remote nodes cache
+DATA 0.0345086 0.0338296
+DATA 0.039527 0.0400697
+DATA 0.0448201 0.0451372
+DATA 0.0497958 0.0497184
+DATA 0.0555848 0.0546203
+DATA 0.0523149 0.0522834
+DATA 0.0603456 0.0605974
+DATA 0.0680668 0.0678726
+DATA 0.0745024 0.0756996
+DATA 0.0821694 0.081082
+DATA 0.0690143 0.0698715
+DATA 0.0759345 0.0754368
+DATA 0.0799289 0.0842847
+DATA 0.0965564 0.0921167
+DATA 0.0961959 0.0961959
+DATA 0.08753 0.0988927
+DATA 0.0987114 0.0979885
+DATA 0.10885 0.108069
+DATA 0.123089 0.123089
+DATA 0.12706 0.12706
+DATA 0.0997986 0.100863
+DATA 0.113147 0.110338
+DATA 0.121608 0.121608
+DATA 0.1431 0.1431
+DATA 0.150156 0.150156
+
+REGION       Create synapses (w/ Alltoall)
+DATA 352.56 351.18
+DATA 437.89 437.443
+DATA 525.724 529.394
+DATA 613.803 616.413
+DATA 702.193 703.392
+DATA 508.749 508.533
+DATA 628.283 633.245
+DATA 762.838 762.892
+DATA 893.851 891.679
+DATA 1030.85 1032.18
+DATA 869.728 870.927
+DATA 1079.66 1081.24
+DATA 1298.57 1289.97
+DATA 1507.14 1492.83
+DATA 1720.11 1720.11
+DATA 1045.46 1041.13
+DATA 1278.12 1273.74
+DATA 1502.4 1509.9
+DATA 1733.37 1733.37
+DATA 1953.21 1953.21
+DATA 1189.53 1185.89
+DATA 1446.51 1441.98
+DATA 1715.12 1715.12
+DATA 1971.08 1971.08
+DATA 2342.5 2342.5
+
diff --git a/tests/data/text/one_parameter_segmented_1.txt b/tests/data/text/one_parameter_segmented_1.txt
new file mode 100644
index 00000000..44e836f4
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_1.txt
@@ -0,0 +1,25 @@
+PARAMETER p
+
+POINTS ( 1 )
+POINTS ( 2 )
+POINTS ( 3 )
+POINTS ( 4 )
+POINTS ( 5 )
+POINTS ( 6 )
+POINTS ( 7 )
+POINTS ( 8 )
+POINTS ( 9 )
+POINTS ( 10 )
+
+REGION main
+METRIC runtime
+DATA 1.0 1.0 1.0 1.0 1.0 
+DATA 4.0 4.0 4.0 4.0 4.0 
+DATA 9.0 9.0 9.0 9.0 9.0 
+DATA 16.0 16.0 16.0 16.0 16.0 
+DATA 25.0 25.0 25.0 25.0 25.0 
+DATA 36.0 36.0 36.0 36.0 36.0 
+DATA 37.0 37.0 37.0 37.0 37.0 
+DATA 38.0 38.0 38.0 38.0 38.0 
+DATA 39.0 39.0 39.0 39.0 39.0 
+DATA 40.0 40.0 40.0 40.0 40.0 
diff --git a/tests/data/text/one_parameter_segmented_2.txt b/tests/data/text/one_parameter_segmented_2.txt
new file mode 100644
index 00000000..3b1821fa
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_2.txt
@@ -0,0 +1,25 @@
+PARAMETER p
+
+POINTS ( 4 )
+POINTS ( 8 )
+POINTS ( 12 )
+POINTS ( 16 )
+POINTS ( 20 )
+POINTS ( 24 )
+POINTS ( 28 )
+POINTS ( 32 )
+POINTS ( 36 )
+POINTS ( 40 )
+
+REGION main
+METRIC runtime
+DATA 2.0 2.0 2.0 2.0 2.0 
+DATA 3.0 3.0 3.0 3.0 3.0 
+DATA 3.584962500721156 3.584962500721156 3.584962500721156 3.584962500721156 3.584962500721156 
+DATA 4.0 4.0 4.0 4.0 4.0 
+DATA 4.321928094887363 4.321928094887363 4.321928094887363 4.321928094887363 4.321928094887363 
+DATA 576.0 576.0 576.0 576.0 576.0 
+DATA 784.0 784.0 784.0 784.0 784.0 
+DATA 1024.0 1024.0 1024.0 1024.0 1024.0 
+DATA 1296.0 1296.0 1296.0 1296.0 1296.0 
+DATA 1600.0 1600.0 1600.0 1600.0 1600.0 
diff --git a/tests/data/text/one_parameter_segmented_3.txt b/tests/data/text/one_parameter_segmented_3.txt
new file mode 100644
index 00000000..114175b2
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_3.txt
@@ -0,0 +1,25 @@
+PARAMETER p
+
+POINTS ( 10 )
+POINTS ( 20 )
+POINTS ( 30 )
+POINTS ( 40 )
+POINTS ( 50 )
+POINTS ( 60 )
+POINTS ( 70 )
+POINTS ( 80 )
+POINTS ( 90 )
+POINTS ( 100 )
+
+REGION main
+METRIC runtime
+DATA 332.1928094887362 332.1928094887362 332.1928094887362 332.1928094887362 332.1928094887362 
+DATA 1728.771237954945 1728.771237954945 1728.771237954945 1728.771237954945 1728.771237954945 
+DATA 4416.201536047667 4416.201536047667 4416.201536047667 4416.201536047667 4416.201536047667 
+DATA 8515.084951819781 8515.084951819781 8515.084951819781 8515.084951819781 8515.084951819781 
+DATA 14109.64047443681 14109.64047443681 14109.64047443681 14109.64047443681 14109.64047443681 
+DATA 21264.80614419067 21264.80614419067 21264.80614419067 21264.80614419067 21264.80614419067 
+DATA 30033.486783030334 30033.486783030334 30033.486783030334 30033.486783030334 30033.486783030334 
+DATA 40460.339807279124 40460.339807279124 40460.339807279124 40460.339807279124 40460.339807279124 
+DATA 52584.01008027037 52584.01008027037 52584.01008027037 52584.01008027037 52584.01008027037 
+DATA 66438.56189774725 66438.56189774725 66438.56189774725 66438.56189774725 66438.56189774725 
diff --git a/tests/data/text/one_parameter_segmented_4.txt b/tests/data/text/one_parameter_segmented_4.txt
new file mode 100644
index 00000000..bbad54d1
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_4.txt
@@ -0,0 +1,38 @@
+PARAMETER p
+
+POINTS ( 1 )
+POINTS ( 2 )
+POINTS ( 3 )
+POINTS ( 4 )
+POINTS ( 5 )
+POINTS ( 6 )
+POINTS ( 7 )
+POINTS ( 8 )
+POINTS ( 9 )
+POINTS ( 10 )
+
+REGION main
+METRIC runtime
+DATA 1.0 1.0 1.0 1.0 1.0 
+DATA 4.0 4.0 4.0 4.0 4.0 
+DATA 9.0 9.0 9.0 9.0 9.0 
+DATA 16.0 16.0 16.0 16.0 16.0 
+DATA 25.0 25.0 25.0 25.0 25.0 
+DATA 36.0 36.0 36.0 36.0 36.0 
+DATA 37.0 37.0 37.0 37.0 37.0 
+DATA 38.0 38.0 38.0 38.0 38.0 
+DATA 39.0 39.0 39.0 39.0 39.0 
+DATA 40.0 40.0 40.0 40.0 40.0
+
+REGION main->comp
+METRIC runtime
+DATA 1.0 1.0 1.0 1.0 1.0 
+DATA 4.0 4.0 4.0 4.0 4.0 
+DATA 9.0 9.0 9.0 9.0 9.0 
+DATA 16.0 16.0 16.0 16.0 16.0 
+DATA 25.0 25.0 25.0 25.0 25.0 
+DATA 36.0 36.0 36.0 36.0 36.0 
+DATA 37.0 37.0 37.0 37.0 37.0 
+DATA 38.0 38.0 38.0 38.0 38.0 
+DATA 39.0 39.0 39.0 39.0 39.0 
+DATA 40.0 40.0 40.0 40.0 40.0 
diff --git a/tests/data/text/one_parameter_segmented_5.txt b/tests/data/text/one_parameter_segmented_5.txt
new file mode 100644
index 00000000..80bbad1c
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_5.txt
@@ -0,0 +1,35 @@
+PARAMETER p
+POINTS (1000) (2000) (3000) (4000) (5000) (6000) (7000) (8000) (9000)
+
+REGION met1 
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.3 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.9 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.3 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.9 4.0 4.1
+
+REGION met2
+DATA 1
+DATA 2
+DATA 3
+DATA 4
+DATA 5
+DATA 6
+DATA 7
+DATA 8
+DATA 9
+
+REGION met1+2
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4
+DATA 5
+DATA 6
+DATA 7
+DATA 8
\ No newline at end of file
diff --git a/tests/data/text/one_parameter_segmented_6.txt b/tests/data/text/one_parameter_segmented_6.txt
new file mode 100644
index 00000000..88271616
--- /dev/null
+++ b/tests/data/text/one_parameter_segmented_6.txt
@@ -0,0 +1,27 @@
+PARAMETER p
+POINTS (500) (1000) (2000) (3000) (4000) (5000) (6000) (7000) (8000) (9000)
+
+REGION met1+2
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4
+DATA 4
+DATA 5
+DATA 6
+DATA 7
+DATA 8
+
+
+REGION met1+5
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4
+DATA 14
+DATA 15
+DATA 16
+DATA 17
+DATA 18
\ No newline at end of file
diff --git a/tests/data/text/strong_scaling/one_parameter_1.txt b/tests/data/text/strong_scaling/one_parameter_1.txt
new file mode 100644
index 00000000..389f2186
--- /dev/null
+++ b/tests/data/text/strong_scaling/one_parameter_1.txt
@@ -0,0 +1,15 @@
+PARAMETER x
+
+POINTS ( 20 )
+POINTS ( 30 )
+POINTS ( 40 )
+POINTS ( 50 )
+POINTS ( 60 )
+
+REGION compute
+METRIC time
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795 82.54252892405795
\ No newline at end of file
diff --git a/tests/data/text/strong_scaling/one_parameter_2.txt b/tests/data/text/strong_scaling/one_parameter_2.txt
new file mode 100644
index 00000000..34c9c0cb
--- /dev/null
+++ b/tests/data/text/strong_scaling/one_parameter_2.txt
@@ -0,0 +1,115 @@
+PARAMETER x
+
+POINTS ( 20 )
+POINTS ( 30 )
+POINTS ( 40 )
+POINTS ( 50 )
+POINTS ( 60 )
+
+
+METRIC time
+
+REGION main
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->merge
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->merge->comp1
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->merge->comp2
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->sort
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->sort->comp1
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION main->sort->comp2
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->test1
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->test2
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->test2->func1
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->merge
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->sort
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->sort->comp1
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
+
+REGION side->sort->comp2
+DATA 725.3886983183301 727.8628493044997 727.0517005284136 729.5284355624206 721.0616186790007
+DATA 509.81064352081336 495.74457198852593 501.0059487282256 502.5272204028191 509.6665018646357
+DATA 315.26845992523744 314.8769218843138 315.7165660477195 324.22329944880516 323.2719163782126
+DATA 184.5140079299443 177.46069577925178 179.12260901809867 178.9221229310546 177.93461023001083
+DATA 82.00848518948744 81.42023198759563 81.98984501217201 80.76019835580321 82.54252892405795
\ No newline at end of file
diff --git a/tests/data/text/strong_scaling/one_parameter_6.txt b/tests/data/text/strong_scaling/one_parameter_6.txt
new file mode 100644
index 00000000..a956a2fd
--- /dev/null
+++ b/tests/data/text/strong_scaling/one_parameter_6.txt
@@ -0,0 +1,32 @@
+PARAMETER p
+POINTS (1000) (2000) (4000) (8000) (16000)
+
+REGION met1 
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.3 4.0 4.1
+DATA 4.1 3.9 4.0 4.0 4.1
+DATA 4.1 3.9 4.9 4.0 4.1
+
+REGION met2 
+DATA 256.01 255.99 256 256
+DATA 64 64 64 64.01 63.99
+DATA 16 15.999 16.01 16.01 15.99
+DATA 4 4 4 3.99 4.01
+DATA 1 1 1 1 1
+
+REGION met3 
+DATA 261.1 260.9 260.9 261.0 261.1
+DATA 68.1 67.9 68.0 68.0 68.1
+DATA 19.1 18.9 19 19.0 19.1
+DATA 6.1 5.9 6.0 6.0 6.1
+DATA 2 2 2 2 2.001
+
+REGION met4 
+DATA 256000000 256000003 256000002
+DATA 64000000 64000003 64000002
+DATA 16000000 16000003 16000005
+DATA 4000000 4000004 4000002
+DATA 1000000 1000001 1000002
+
+
diff --git a/tests/data/text/strong_scaling/two_parameter_1.txt b/tests/data/text/strong_scaling/two_parameter_1.txt
new file mode 100644
index 00000000..e8ef6a9d
--- /dev/null
+++ b/tests/data/text/strong_scaling/two_parameter_1.txt
@@ -0,0 +1,56 @@
+PARAMETER x
+PARAMETER y
+
+POINTS ( 20 5 )
+POINTS ( 20 4 )
+POINTS ( 20 3 ) 
+POINTS ( 20 2 )
+POINTS ( 20 1 )
+POINTS ( 30 5 )
+POINTS ( 30 4 )
+POINTS ( 30 3 )
+POINTS ( 30 2 )
+POINTS ( 30 1 )
+POINTS ( 40 5 )
+POINTS ( 40 4 )
+POINTS ( 40 3 )
+POINTS ( 40 2 )
+POINTS ( 40 1 )
+POINTS ( 50 5 )
+POINTS ( 50 4 )
+POINTS ( 50 3 )
+POINTS ( 50 2 )
+POINTS ( 50 1 )
+POINTS ( 60 5 )
+POINTS ( 60 4 )
+POINTS ( 60 3 ) 
+POINTS ( 60 2 )
+POINTS ( 60 1 )
+
+REGION reg
+METRIC metr
+DATA 1.007912824061828 1.0003031166379763 1.0146314163801136 0.9888371709656842 0.9814784961141532
+DATA 82.31483486850328 80.77055829825096 81.65028667288966 79.84894914331908 80.69133265056031
+DATA 127.0982588521472 129.97720256124214 126.37233644578495 129.95328527792617 128.79143884701386
+DATA 163.761116922403 160.64424490477055 163.15711078980416 159.9466809220868 161.16513023758068
+DATA 186.84836895674908 186.88261474927117 189.29212868834847 187.00694904185926 183.26480034190746
+DATA 1.0195866920777528 0.9965626548891021 1.0181840262519737 0.9863362890535387 0.9893290137007527
+DATA 179.55463831422327 180.74821882024628 183.8728518616761 182.54443671884627 182.24933579588313
+DATA 280.6908234409108 281.788263615464 283.863933677854 283.8298464032916 288.8773066881149
+DATA 366.23299595860806 361.4313754663896 364.474908755492 367.28360995699853 359.8692941474022
+DATA 424.1140021695517 416.5453483049455 425.49863768871654 419.9658709243348 418.9048409420776
+DATA 0.9832332399984872 0.9815834670845136 1.0003806927451324 0.9968558578424265 0.9820664393235233
+DATA 317.0754172371989 317.36884825095814 315.9495390075145 320.4995994903179 318.6762830447799
+DATA 504.63767808259195 516.0708239589679 507.81689745670286 511.8768307930591 506.2807943653508
+DATA 644.8143141699127 637.3099257201826 649.9174626712269 647.3368427699369 633.2245986830611
+DATA 744.9220213789664 743.3804480439476 741.3314847916288 749.8014199318746 739.8083987550706
+DATA 1.0072335327043695 1.005198595503928 1.0076803056588366 1.003963391133719 0.9931552405725348
+DATA 495.8101061967628 501.64162008360864 507.00077184697335 498.83459734803967 507.88731718138513
+DATA 793.4292405511786 808.4271169186493 794.4488419240894 792.3783818373039 786.1581110613066
+DATA 986.8778797337357 1007.0264271966056 1012.1774356734794 1012.8234104247143 994.8558593224009
+DATA 1168.925970055696 1152.305843765793 1163.3320257986136 1181.721114617381 1174.9194990604099
+DATA 1.0123953645414927 0.9808925858487914 0.9883957051154446 1.0097027886766448 1.0160649467948064
+DATA 732.9718870349534 713.5887652750822 731.5598226968582 727.8160069274114 721.843240633473
+DATA 1139.5493742034544 1151.1355124340896 1136.4341443870837 1123.1009473444153 1139.5682409322808
+DATA 1461.1663792149404 1456.927809881706 1468.5789635626325 1425.7927730830347 1412.4260646792911
+DATA 1644.9469847409239 1687.8899020378108 1659.733621973487 1646.4234125252788 1641.8554614544396
diff --git a/tests/data/text/strong_scaling/two_parameter_2.txt b/tests/data/text/strong_scaling/two_parameter_2.txt
new file mode 100644
index 00000000..2bcf71e8
--- /dev/null
+++ b/tests/data/text/strong_scaling/two_parameter_2.txt
@@ -0,0 +1,36 @@
+PARAMETER x
+PARAMETER y
+
+POINTS ( 60 1 ) ( 60 2 ) ( 60 3 ) ( 60 4 ) ( 60 5 )
+POINTS ( 50 1 ) ( 50 2 ) ( 50 3 ) ( 50 4 ) ( 50 5 )
+POINTS ( 40 1 ) ( 40 2 ) ( 40 3 ) ( 40 4 ) ( 40 5 )
+POINTS ( 30 1 ) ( 30 2 ) ( 30 3 ) ( 30 4 ) ( 30 5 )
+POINTS ( 20 1 ) ( 20 2 ) ( 20 3 ) ( 20 4 ) ( 20 5 )
+
+REGION reg
+METRIC metr
+DATA 1.007912824061828 1.0003031166379763 1.0146314163801136 0.9888371709656842 0.9814784961141532
+DATA 82.31483486850328 80.77055829825096 81.65028667288966 79.84894914331908 80.69133265056031
+DATA 127.0982588521472 129.97720256124214 126.37233644578495 129.95328527792617 128.79143884701386
+DATA 163.761116922403 160.64424490477055 163.15711078980416 159.9466809220868 161.16513023758068
+DATA 186.84836895674908 186.88261474927117 189.29212868834847 187.00694904185926 183.26480034190746
+DATA 1.0195866920777528 0.9965626548891021 1.0181840262519737 0.9863362890535387 0.9893290137007527
+DATA 179.55463831422327 180.74821882024628 183.8728518616761 182.54443671884627 182.24933579588313
+DATA 280.6908234409108 281.788263615464 283.863933677854 283.8298464032916 288.8773066881149
+DATA 366.23299595860806 361.4313754663896 364.474908755492 367.28360995699853 359.8692941474022
+DATA 424.1140021695517 416.5453483049455 425.49863768871654 419.9658709243348 418.9048409420776
+DATA 0.9832332399984872 0.9815834670845136 1.0003806927451324 0.9968558578424265 0.9820664393235233
+DATA 317.0754172371989 317.36884825095814 315.9495390075145 320.4995994903179 318.6762830447799
+DATA 504.63767808259195 516.0708239589679 507.81689745670286 511.8768307930591 506.2807943653508
+DATA 644.8143141699127 637.3099257201826 649.9174626712269 647.3368427699369 633.2245986830611
+DATA 744.9220213789664 743.3804480439476 741.3314847916288 749.8014199318746 739.8083987550706
+DATA 1.0072335327043695 1.005198595503928 1.0076803056588366 1.003963391133719 0.9931552405725348
+DATA 495.8101061967628 501.64162008360864 507.00077184697335 498.83459734803967 507.88731718138513
+DATA 793.4292405511786 808.4271169186493 794.4488419240894 792.3783818373039 786.1581110613066
+DATA 986.8778797337357 1007.0264271966056 1012.1774356734794 1012.8234104247143 994.8558593224009
+DATA 1168.925970055696 1152.305843765793 1163.3320257986136 1181.721114617381 1174.9194990604099
+DATA 1.0123953645414927 0.9808925858487914 0.9883957051154446 1.0097027886766448 1.0160649467948064
+DATA 732.9718870349534 713.5887652750822 731.5598226968582 727.8160069274114 721.843240633473
+DATA 1139.5493742034544 1151.1355124340896 1136.4341443870837 1123.1009473444153 1139.5682409322808
+DATA 1461.1663792149404 1456.927809881706 1468.5789635626325 1425.7927730830347 1412.4260646792911
+DATA 1644.9469847409239 1687.8899020378108 1659.733621973487 1646.4234125252788 1641.8554614544396
diff --git a/tests/data/text/strong_scaling/two_parameter_7.txt b/tests/data/text/strong_scaling/two_parameter_7.txt
new file mode 100644
index 00000000..e7a76902
--- /dev/null
+++ b/tests/data/text/strong_scaling/two_parameter_7.txt
@@ -0,0 +1,57 @@
+PARAMETER x
+PARAMETER y
+
+POINTS ( 20 1 )
+POINTS ( 20 2 )
+POINTS ( 20 3 )
+POINTS ( 20 4 )
+POINTS ( 20 5 )
+POINTS ( 30 1 )
+POINTS ( 30 2 )
+POINTS ( 30 3 )
+POINTS ( 30 4 )
+POINTS ( 30 5 )
+POINTS ( 40 1 )
+POINTS ( 40 2 )
+POINTS ( 40 3 )
+POINTS ( 40 4 )
+POINTS ( 40 5 )
+POINTS ( 50 1 )
+POINTS ( 50 2 )
+POINTS ( 50 3 )
+POINTS ( 50 4 )
+POINTS ( 50 5 )
+POINTS ( 60 1 )
+POINTS ( 60 2 )
+POINTS ( 60 3 )
+POINTS ( 60 4 )
+POINTS ( 60 5 )
+
+REGION reg
+METRIC metr
+
+DATA 393.7823477 392.4621362 392.6296949 390.248276 386.9305547
+DATA 198.1270521 197.944445 197.4374387 199.0865133 197.7027033
+DATA 130.6065962 129.9071656 128.7267674 129.1746852 129.6288721
+DATA 99.98196505 99.31738854 98.38510618 97.96034476 97.85976627
+DATA 80.29202039 80.353246 80.67858715 80.56671304 79.90704003
+DATA 709.3991228 705.2382858 703.0475592 702.3320087 704.7922621
+DATA 359.2635922 360.5898199 363.4876973 360.5811926 360.2192722
+DATA 238.8348258 238.2225463 236.3451779 237.7866589 238.015244
+DATA 180.9052875 180.9703155 181.4802509 182.8372864 184.1572843
+DATA 144.485761 145.2511706 145.137687 145.5453878 144.55488
+DATA 1098.682254 1093.809001 1103.463249 1097.896547 1094.605809
+DATA 552.9127563 558.3100827 554.8131138 552.084194 548.6551603
+DATA 364.466931 361.198023 362.4423867 364.359398 366.0010795
+DATA 275.4664654 277.9056439 278.6076371 279.5856397 277.1399809
+DATA 221.550211 223.3038576 222.4979038 223.6011991 225.2688507
+DATA 1514.04837 1502.188233 1504.23685 1489.310496 1500.806831
+DATA 765.0654747 758.6770635 754.7386284 759.1871093 762.7552434
+DATA 512.5845349 510.1754522 505.9887842 509.9037179 510.4877108
+DATA 386.7275918 385.0140958 386.5093461 387.9784136 389.609633
+DATA 304.1188245 302.2364894 303.8758631 302.8437869 301.7619601
+DATA 2005.020331 1986.116329 1974.465783 1979.776138 1967.406255
+DATA 1002.354242 1009.191981 1015.671581 1019.865217 1016.267162
+DATA 674.6816319 680.5229154 674.0535231 667.8525597 670.5038063
+DATA 504.9071904 507.1880382 504.3077994 509.2659208 507.1879585
+DATA 405.702436 407.0918941 410.1301861 411.6504679 415.2485629
diff --git a/tests/data/text/two_parameter_5.txt b/tests/data/text/two_parameter_5.txt
new file mode 100644
index 00000000..71beac05
--- /dev/null
+++ b/tests/data/text/two_parameter_5.txt
@@ -0,0 +1,30 @@
+PARAMETER p s
+POINTS ( 2 10 ) ( 4 10 ) ( 8 10 ) ( 16 10 ) ( 32 10 ) ( 2 20 ) ( 4 20 ) ( 8 20 ) ( 16 20 ) ( 32 20 ) ( 2 30 ) ( 4 30 ) ( 8 30 ) ( 16 30 ) ( 32 30 ) ( 2 40 ) ( 4 40 ) ( 8 40 ) ( 16 40 ) ( 32 40 ) ( 2 50 ) ( 4 50 ) ( 8 50 ) ( 16 50 ) ( 32 50 )
+
+REGION main
+# formula: 2 + 0.884 * s * log(p)
+DATA	10.84
+DATA	19.68
+DATA	28.52
+DATA	37.36
+DATA	46.2
+DATA	19.68
+DATA	37.36
+DATA	55.04
+DATA	72.72
+DATA	90.4
+DATA	28.52
+DATA	55.04
+DATA	81.56
+DATA	108.08
+DATA	134.6
+DATA	37.36
+DATA	72.72
+DATA	108.08
+DATA	143.44
+DATA	178.8
+DATA	46.2
+DATA	90.4
+DATA	134.6
+DATA	178.8
+DATA	223
\ No newline at end of file
diff --git a/tests/data/text/two_parameter_6.txt b/tests/data/text/two_parameter_6.txt
new file mode 100644
index 00000000..cc30c253
--- /dev/null
+++ b/tests/data/text/two_parameter_6.txt
@@ -0,0 +1,30 @@
+PARAMETER p s
+POINTS ( 2 10 ) ( 4 10 ) ( 8 10 ) ( 16 10 ) ( 32 10 ) ( 2 20 ) ( 4 20 ) ( 8 20 ) ( 16 20 ) ( 32 20 ) ( 2 30 ) ( 4 30 ) ( 8 30 ) ( 16 30 ) ( 32 30 ) ( 2 40 ) ( 4 40 ) ( 8 40 ) ( 16 40 ) ( 32 40 ) ( 2 50 ) ( 4 50 ) ( 8 50 ) ( 16 50 ) ( 32 50 )
+
+REGION main
+# formula: 1 + 0.05 * s^1.5 * p
+DATA	4.16227766
+DATA	7.32455532
+DATA	13.64911064
+DATA	26.29822128
+DATA	51.59644256
+DATA	9.94427191
+DATA	18.88854382
+DATA	36.77708764
+DATA	72.55417528
+DATA	144.1083506
+DATA	17.43167673
+DATA	33.86335345
+DATA	66.7267069
+DATA	132.4534138
+DATA	263.9068276
+DATA	26.29822128
+DATA	51.59644256
+DATA	102.1928851
+DATA	203.3857703
+DATA	405.7715405
+DATA	36.35533906
+DATA	71.71067812
+DATA	142.4213562
+DATA	283.8427125
+DATA	566.6854249
diff --git a/tests/data/text/two_parameter_segmented_1.txt b/tests/data/text/two_parameter_segmented_1.txt
new file mode 100644
index 00000000..42989dcc
--- /dev/null
+++ b/tests/data/text/two_parameter_segmented_1.txt
@@ -0,0 +1,131 @@
+PARAMETER x
+PARAMETER y
+
+POINTS	(	10	2	)
+POINTS	(	20	2	)
+POINTS	(	30	2	)
+POINTS	(	40	2	)
+POINTS	(	50	2	)
+POINTS	(	60	2	)
+POINTS	(	70	2	)
+POINTS	(	80	2	)
+POINTS	(	90	2	)
+POINTS	(	100	2	)
+POINTS	(	10	4	)
+POINTS	(	20	4	)
+POINTS	(	30	4	)
+POINTS	(	40	4	)
+POINTS	(	50	4	)
+POINTS	(	60	4	)
+POINTS	(	70	4	)
+POINTS	(	80	4	)
+POINTS	(	90	4	)
+POINTS	(	100	4	)
+POINTS	(	10	8	)
+POINTS	(	20	8	)
+POINTS	(	30	8	)
+POINTS	(	40	8	)
+POINTS	(	50	8	)
+POINTS	(	60	8	)
+POINTS	(	70	8	)
+POINTS	(	80	8	)
+POINTS	(	90	8	)
+POINTS	(	100	8	)
+POINTS	(	10	16	)
+POINTS	(	20	16	)
+POINTS	(	30	16	)
+POINTS	(	40	16	)
+POINTS	(	50	16	)
+POINTS	(	60	16	)
+POINTS	(	70	16	)
+POINTS	(	80	16	)
+POINTS	(	90	16	)
+POINTS	(	100	16	)
+POINTS	(	10	32	)
+POINTS	(	20	32	)
+POINTS	(	30	32	)
+POINTS	(	40	32	)
+POINTS	(	50	32	)
+POINTS	(	60	32	)
+POINTS	(	70	32	)
+POINTS	(	80	32	)
+POINTS	(	90	32	)
+POINTS	(	100	32	)
+POINTS	(	10	64	)
+POINTS	(	20	64	)
+POINTS	(	30	64	)
+POINTS	(	40	64	)
+POINTS	(	50	64	)
+POINTS	(	60	64	)
+POINTS	(	70	64	)
+POINTS	(	80	64	)
+POINTS	(	90	64	)
+POINTS	(	100	64	)
+
+# =IF(x<55;x^2*0.4*y;1400+x*10*y)
+REGION reg
+METRIC metr
+DATA	80	80.21	80.49	80.03	80.07
+DATA	320	316.93	315.61	314.93	313.86
+DATA	720	726.97	729.87	732.92	732
+DATA	1280	1267.9	1277.84	1278.19	1277.06
+DATA	2000	1985.87	1986.48	1978.27	1994.37
+DATA	2600	2611.84	2613.29	2603.29	2611.47
+DATA	2800	2813.42	2839.69	2830.42	2830.76
+DATA	3000	2971.17	2967.17	2938.88	2967.08
+DATA	3200	3187.82	3208.82	3214.25	3186.13
+DATA	3400	3410	3437.83	3468.79	3468
+DATA	160	159.27	158.61	157.06	156.48
+DATA	640	635.6	637.41	636.63	640.5
+DATA	1440	1433.2	1441.73	1437.74	1432.92
+DATA	2560	2547.98	2550.19	2569.86	2586.31
+DATA	4000	3993.77	4011.98	3996.84	3982.85
+DATA	3800	3830.82	3825.74	3827.95	3865.78
+DATA	4200	4169.02	4180.3	4198.82	4175.31
+DATA	4600	4571.14	4584.92	4551.9	4523.69
+DATA	5000	5033.17	5018.45	5006.02	5047.04
+DATA	5400	5350.64	5298.97	5296.69	5306.15
+DATA	320	318.3	316.08	313.2	310.12
+DATA	1280	1275.34	1267.68	1274.4	1272.01
+DATA	2880	2903.21	2880.96	2894.91	2876.83
+DATA	5120	5132.46	5142.65	5188.07	5236.78
+DATA	8000	8021.27	8040.54	8030.69	8073.21
+DATA	6200	6248.53	6283.98	6266.8	6219.29
+DATA	7000	7037.16	7045	7030.36	7014.04
+DATA	7800	7764.17	7752.95	7809.59	7769.28
+DATA	8600	8573.77	8550.75	8479.83	8551.05
+DATA	9400	9465.78	9542.16	9606.63	9663.79
+DATA	640	645.92	644.17	639.27	635.05
+DATA	2560	2569.93	2556.6	2558.73	2564.26
+DATA	5760	5790.54	5789.55	5760.95	5730.02
+DATA	10240	10186.95	10206.25	10198.98	10259.7
+DATA	16000	16095.26	16229.86	16266.59	16185.33
+DATA	11000	11016.21	10920.83	10892.77	10958.41
+DATA	12600	12657.26	12656.66	12698.03	12709.56
+DATA	14200	14191	14284.66	14311.37	14388.9
+DATA	15800	15920.01	15802.75	15877.95	16011.7
+DATA	17400	17460.42	17549.19	17613.78	17573.92
+DATA	1280	1290.64	1291.18	1282.01	1280.32
+DATA	5120	5159.08	5122.92	5096.12	5085.07
+DATA	11520	11416.16	11437.98	11469.77	11510.93
+DATA	20480	20394.11	20222.76	20301.74	20177.92
+DATA	32000	31786.29	31932.38	31847.95	31857.07
+DATA	20600	20405.76	20505.88	20606.89	20453.3
+DATA	23800	23898.91	24110.97	23909.54	23897.15
+DATA	27000	27265.31	27099.27	27063.93	26907.14
+DATA	30200	30119.38	30387.99	30634.29	30549.98
+DATA	33400	33494.52	33594.32	33518.95	33685.89
+DATA	2560	2555.55	2530.87	2531.76	2508.92
+DATA	10240	10198.71	10152.37	10133.41	10123.56
+DATA	23040	22828.9	22768.67	22727.46	22693.5
+DATA	40960	41003.53	40693.4	40349.94	40046.37
+DATA	64000	63593.86	63726.15	63227.06	63574.53
+DATA	39800	39893.28	39673.58	39819.49	39960.61
+DATA	46200	46388.19	46333.2	46429.94	46416.12
+DATA	52600	53077.99	53380.17	53354.92	53793.97
+DATA	59000	59052.01	59439.77	58945.5	58637.6
+DATA	65400	65159.07	64982.89	64516.84	64564.3
+
+
+
+
diff --git a/tests/serialization_testcase.py b/tests/serialization_testcase.py
new file mode 100644
index 00000000..99982ad5
--- /dev/null
+++ b/tests/serialization_testcase.py
@@ -0,0 +1,83 @@
+import unittest
+from typing import TYPE_CHECKING
+
+from extrap.entities.experiment import Experiment
+from extrap.modelers.modeler_options import modeler_options
+
+if TYPE_CHECKING:
+    class TestCaseProtocol(unittest.TestCase):
+        ...
+else:
+    class TestCaseProtocol:
+        ...
+
+
+class BasicExperimentSerializationTest(TestCaseProtocol):
+    experiment: Experiment
+    reconstructed: Experiment
+
+    def test_setup(self):
+        self.setUpClass()
+
+    def test_parameters(self):
+        self.assertListEqual(self.experiment.parameters, self.reconstructed.parameters)
+
+    def test_measurements(self):
+        self.assertDictEqual(self.experiment.measurements, self.reconstructed.measurements)
+
+    def test_coordinates(self):
+        self.assertListEqual(self.experiment.coordinates, self.reconstructed.coordinates)
+
+    def test_callpaths(self):
+        self.assertListEqual(self.experiment.callpaths, self.reconstructed.callpaths)
+
+    def test_metrics(self):
+        self.assertListEqual(self.experiment.metrics, self.reconstructed.metrics)
+
+    def test_call_tree(self):
+        self.assertEqual(self.experiment.call_tree, self.reconstructed.call_tree)
+
+    def test_modelers(self):
+        if self.experiment.modelers != self.reconstructed.modelers:
+            self.assertEqual(len(self.experiment.modelers), len(self.reconstructed.modelers))
+            for (e_mg, r_mg) in zip(self.experiment.modelers, self.reconstructed.modelers):
+                self.assertEqual(e_mg._modeler.NAME, r_mg._modeler.NAME)
+                self.assertEqual(e_mg._modeler.use_median, r_mg._modeler.use_median)
+                self.assertTrue(modeler_options.equal(e_mg, r_mg))
+                self.assertEqual(len(e_mg.models), len(r_mg.models))
+
+    def test_hypothesis(self):
+        for (e_mg, r_mg) in zip(self.experiment.modelers, self.reconstructed.modelers):
+            for k in e_mg.models:
+                self.assertListEqual(e_mg.models[k].measurements, r_mg.models[k].measurements)
+                self.assertEqual(e_mg.models[k].callpath, r_mg.models[k].callpath)
+                self.assertEqual(e_mg.models[k].metric, r_mg.models[k].metric)
+
+                e_hypothesis_val = {k: v for k, v in e_mg.models[k].hypothesis.__dict__.items() if k != 'function'}
+                r_hypothesis_val = {k: v for k, v in r_mg.models[k].hypothesis.__dict__.items() if k != 'function'}
+                self.assertDictEqual(e_hypothesis_val, r_hypothesis_val)
+
+    def test_scaling(self):
+        self.assertEqual(self.experiment.scaling, self.reconstructed.scaling)
+
+
+class BasicMPExperimentSerializationTest(BasicExperimentSerializationTest):
+    def test_functions(self):
+        for (e_mg, r_mg) in zip(self.experiment.modelers, self.reconstructed.modelers):
+            for k in e_mg.models:
+                e_mg_models_k_ = e_mg.models[k].hypothesis.function
+                r_mg_models_k_ = r_mg.models[k].hypothesis.function
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 10]), r_mg_models_k_.evaluate([10, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([1000, 1000]), r_mg_models_k_.evaluate([1000, 1000]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10000, 10000]), r_mg_models_k_.evaluate([10000, 10000]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([100000, 100000]),
+                                       r_mg_models_k_.evaluate([100000, 100000]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 10]), r_mg_models_k_.evaluate([10, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([1000, 10]), r_mg_models_k_.evaluate([1000, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10000, 10]), r_mg_models_k_.evaluate([10000, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([100000, 10]), r_mg_models_k_.evaluate([100000, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 10]), r_mg_models_k_.evaluate([10, 10]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 1000]), r_mg_models_k_.evaluate([10, 1000]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 10000]), r_mg_models_k_.evaluate([10, 10000]))
+                self.assertAlmostEqual(e_mg_models_k_.evaluate([10, 100000]),
+                                       r_mg_models_k_.evaluate([10, 100000]))
diff --git a/tests/test_console.py b/tests/test_console.py
index 3688dc34..d3c574b0 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -71,6 +71,76 @@ def test_print(self):
         extrap.main(['--print', 'metrics', '--text', 'data/text/two_parameter_1.txt'])
         extrap.main(['--print', 'parameters', '--text', 'data/text/two_parameter_1.txt'])
 
+    def test_print_python(self):
+        self.assertOutputRegex(
+            "Callpath\:\s+compute\s+"
+            "Metric\:\s+time\s+"
+            "Measurement\s+point\:\s+\(2\.00E\+01\)\s+Mean\:\s+8\.19E\+01\s+Median\:\s+8\.20E\+01\s+"
+            "Measurement\s+point\:\s+\(3\.00E\+01\)\s+Mean\:\s+1\.79E\+02\s+Median\:\s+1\.78E\+02\s+"
+            "Measurement\s+point\:\s+\(4\.00E\+01\)\s+Mean\:\s+3\.19E\+02\s+Median\:\s+3\.19E\+02\s+"
+            "Measurement\s+point\:\s+\(5\.00E\+01\)\s+Mean\:\s+5\.05E\+02\s+Median\:\s+5\.06E\+02\s+"
+            "Measurement\s+point\:\s+\(6\.00E\+01\)\s+Mean\:\s+7\.25E\+02\s+Median\:\s+7\.26E\+02\s+"
+            "Model\:\s+\-0\.88979\d+\+0\.20168\d+\*x\*\*\(2\)\s+"
+            "RSS\:\s+3\.43E\+01\s+"
+            "Adjusted\s+R\^2\:\s+1\.00E\+00",
+            extrap.main, ['--print', 'all-python', '--text', 'data/text/one_parameter_1.txt'])  # noqa
+        # noqa
+        self.assertOutputRegex(r"-0\.88979\d+\+0\.20168\d+\*x\*\*\(2\)", extrap.main,
+                               ['--print', 'functions-python', '--text', 'data/text/one_parameter_1.txt'])
+
+        extrap.main(['--print', 'all-python', '--text', 'data/text/two_parameter_1.txt'])
+        self.assertOutputRegex(r"1\.37420\d+\+6\.69808\d+\*log2\(y\)\*\*\(1\)\+0\.043841\d+\*x\*\*\(3/2\)"
+                               r"\*log2\(x\)\*\*\(2\)\*log2\(y\)\*\*\(1\)",
+                               extrap.main,
+                               ['--print', 'functions-python', '--text', 'data/text/two_parameter_1.txt'])
+
+    def test_print_latex(self):
+        self.assertOutputRegex(
+            "Callpath\:\s+compute\s+"
+            "Metric\:\s+time\s+"
+            "Measurement\s+point\:\s+\(2\.00E\+01\)\s+Mean\:\s+8\.19E\+01\s+Median\:\s+8\.20E\+01\s+"
+            "Measurement\s+point\:\s+\(3\.00E\+01\)\s+Mean\:\s+1\.79E\+02\s+Median\:\s+1\.78E\+02\s+"
+            "Measurement\s+point\:\s+\(4\.00E\+01\)\s+Mean\:\s+3\.19E\+02\s+Median\:\s+3\.19E\+02\s+"
+            "Measurement\s+point\:\s+\(5\.00E\+01\)\s+Mean\:\s+5\.05E\+02\s+Median\:\s+5\.06E\+02\s+"
+            "Measurement\s+point\:\s+\(6\.00E\+01\)\s+Mean\:\s+7\.25E\+02\s+Median\:\s+7\.26E\+02\s+"
+            r"Model\:\s+\$\-8\.898\\times10\^\{−1\}\+2\.017\\times10\^\{−1\}\\cdot\ x\^\{2\}\$\s+"
+            "RSS\:\s+3\.43E\+01\s+"
+            "Adjusted\s+R\^2\:\s+1\.00E\+00",
+            extrap.main, ['--print', 'all-latex', '--text', 'data/text/one_parameter_1.txt'])  # noqa
+        # noqa
+        self.assertOutputRegex(r"\$\-8\.898\\times10\^\{−1\}\+2\.017\\times10\^\{−1\}\\cdot\ x\^\{2\}\$", extrap.main,
+                               ['--print', 'functions-latex', '--text', 'data/text/one_parameter_1.txt'])
+
+        extrap.main(['--print', 'all-python', '--text', 'data/text/two_parameter_1.txt'])
+        self.assertOutputRegex(
+            r"\$1\.374\+6\.698\\cdot\ \\log_2\{y\}\^\{1\}\+4\.384\\times10\^\{−2\}\\cdot\ x\^\{3/2\}\\cdot\ "
+            r"\\log_2\{x\}\^\{2\}\\cdot\ \\log_2\{y\}\^\{1\}\$",
+            extrap.main,
+            ['--print', 'functions-latex', '--text', 'data/text/two_parameter_1.txt'])
+
+    def test_segmented_output(self):
+        self.assertOutputRegex(
+            r"Callpath:\s+main\s+"
+            r"\s+Metric:\s+runtime\s+"
+            r"\s+Measurement\s+point:\s+\(1\.00E\+00\)\s+Mean:\s+1\.00E\+00\s+Median:\s+1\.00E\+00\s+"
+            r"\s+Measurement\s+point:\s+\(2\.00E\+00\)\s+Mean:\s+4\.00E\+00\s+Median:\s+4\.00E\+00\s+"
+            r"\s+Measurement\s+point:\s+\(3\.00E\+00\)\s+Mean:\s+9\.00E\+00\s+Median:\s+9\.00E\+00\s+"
+            r"\s+Measurement\s+point:\s+\(4\.00E\+00\)\s+Mean:\s+1\.60E\+01\s+Median:\s+1\.60E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(5\.00E\+00\)\s+Mean:\s+2\.50E\+01\s+Median:\s+2\.50E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(6\.00E\+00\)\s+Mean:\s+3\.60E\+01\s+Median:\s+3\.60E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(7\.00E\+00\)\s+Mean:\s+3\.70E\+01\s+Median:\s+3\.70E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(8\.00E\+00\)\s+Mean:\s+3\.80E\+01\s+Median:\s+3\.80E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(9\.00E\+00\)\s+Mean:\s+3\.90E\+01\s+Median:\s+3\.90E\+01\s+"
+            r"\s+Measurement\s+point:\s+\(1\.00E\+01\)\s+Mean:\s+4\.00E\+01\s+Median:\s+4\.00E\+01\s+"
+            r"\s+Model\s+1:\s+\-5\.753052622747037e\-16\s+\+\s+p\^\(2\)\s+for\s+p<=6\.0\s+"
+            r"\s+Model\s+2:\s+29\.999999999999975\s+\+\s+1\.0000000000000029\s+\*\s+p\^\(1\)\s+for\s+p>=6\.0\s+"
+            r"\s+RSS\s+Model\s+1:\s+5\.45E\-29\s+"
+            r"\s+Adjusted\s+R\^2\s+Model\s+1:\s+1\.00E\+00\s+"
+            r"\s+RSS\s+Model\s+2:\s+5\.55E\-28\s+"
+            r"\s+Adjusted\s+R\^2\s+Model\s+2:\s+1\.00E\+00",
+            extrap.main,
+            ['--text', 'data/text/one_parameter_segmented_1.txt', '--modeler', 'segmented'])
+
     def assertOutput(self, text, command, *args, **kwargs):
         temp_stdout = StringIO()
         with contextlib.redirect_stdout(temp_stdout):
@@ -83,7 +153,14 @@ def assertOutputRegex(self, regex, command, *args, **kwargs):
         with contextlib.redirect_stdout(temp_stdout):
             command(*args, **kwargs)
         output = temp_stdout.getvalue().strip()
-        self.assertRegex(output, regex)
+        try:
+            self.assertRegex(output, regex)
+        except Exception:
+            print()
+            print("####### ORIGINAL OUTPUT #######")
+            print(output)
+            print("####### END OF ORIGINAL OUTPUT #######")
+            raise
 
     def test_experiment(self):
         temp_dir = tempfile.mkdtemp()
diff --git a/tests/test_load_cube_files.py b/tests/test_load_cube_files.py
index b677dd88..547ade0e 100644
--- a/tests/test_load_cube_files.py
+++ b/tests/test_load_cube_files.py
@@ -1,6 +1,6 @@
 # This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
 #
-# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
 #
 # This software may be modified and distributed under the terms of a BSD-style license.
 # See the LICENSE file in the base directory for details.
@@ -105,8 +105,11 @@ def test_no_cube_files(self):
         self.assertRaises(FileFormatError, CubeFileReader2().read_cube_file, 'data/cubeset', 'strong')
 
     def test_strong_scaling_warning(self):
-        self.assertWarnsRegex(UserWarning, 'Strong scaling', CubeFileReader2().read_cube_file,
-                              'data/cubeset/multi_parameter', 'strong')
+        with warnings.catch_warnings(record=True) as record:
+            warnings.simplefilter('ignore', DeprecationWarning)
+            warnings.filterwarnings('ignore', r'^((?!Strong scaling).)*$')
+            CubeFileReader2().read_cube_file('data/cubeset/multi_parameter', 'strong')
+        self.assertFalse(record)
 
         with warnings.catch_warnings(record=True) as record:
             warnings.simplefilter('ignore', DeprecationWarning)
diff --git a/tests/test_output.py b/tests/test_output.py
index f929fbbe..3dee68a5 100644
--- a/tests/test_output.py
+++ b/tests/test_output.py
@@ -57,6 +57,42 @@ def test_print(self):
             extrap.main,
             ['--print', '{metric}, {callpath}: {rss}/{smape} {model}', '--text', 'data/text/two_parameter_3.txt'])
 
+    def test_model_python(self):
+        self.assertOutputRegex(
+            r"time,\s+merge\:\s+312.47721968\d{5,7}\/6.644640850\d{5,7}\s+1\.3742083125\d{5,7}\+"
+            r"6\.698080399\d{5,7}\*log2\(y\)\*\*\(1\)\+0\.043841655290\d{5,7}\*x\*\*\(3\/2\)\*"
+            r"log2\(x\)\*\*\(2\)\*log2\(y\)\*\*\(1\)\s+"
+            r"time,\s+sort\:\s+312.47721968\d{5,7}\/6.644640850\d{5,7}\s+1\.3742083125\d{5,7}\+"
+            r"6\.698080399\d{5,7}\*log2\(y\)\*\*\(1\)\+0\.043841655290\d{5,7}\*x\*\*\(3\/2\)\*"
+            r"log2\(x\)\*\*\(2\)\*log2\(y\)\*\*\(1\)\s+"
+            r"flops,\s+merge\:\s+312.47721968\d{5,7}\/6.644640850\d{5,7}\s+1\.3742083125\d{5,7}\+"
+            r"6\.698080399\d{5,7}\*log2\(y\)\*\*\(1\)\+0\.043841655290\d{5,7}\*x\*\*\(3\/2\)\*"
+            r"log2\(x\)\*\*\(2\)\*log2\(y\)\*\*\(1\)\s+"
+            r"flops,\s+sort\:\s+312.47721968\d{5,7}\/6.644640850\d{5,7}\s+1\.3742083125\d{5,7}\+"
+            r"6\.698080399\d{5,7}\*log2\(y\)\*\*\(1\)\+0\.043841655290\d{5,7}\*x\*\*\(3\/2\)\*"
+            r"log2\(x\)\*\*\(2\)\*log2\(y\)\*\*\(1\)",
+            extrap.main,
+            ['--print', '{metric}, {callpath}: {rss}/{smape} {model:python}', '--text',
+             'data/text/two_parameter_3.txt'])
+
+
+    #NOTE: there is no point in test like this, every time we change something in the modeler that leads to slightly different outputs
+    # all of these test will fail, instead we should have a test that checks general conversion capabilities using hardcoded models as strings
+    # that are not produced by the modeler or any data read from file
+    """def test_model_latex(self):
+        self.assertOutputRegex(
+            r"time,\ merge:\ 312\.47721968273464/6\.6446408501203775\ \$1\.374\+6\.698\\cdot\ \\log_2\{y\}\^\{1\}\+"
+            r"4\.384\\times10\^\{−2\}\\cdot\ x\^\{3/2\}\\cdot\ \\log_2\{x\}\^\{2\}\\cdot\ \\log_2\{y\}\^\{1\}\$\s+"
+            r"time,\ sort:\ 312\.47721968273464/6\.6446408501203775\ \$1\.374\+6\.698\\cdot\ \\log_2\{y\}\^\{1\}\+"
+            r"4\.384\\times10\^\{−2\}\\cdot\ x\^\{3/2\}\\cdot\ \\log_2\{x\}\^\{2\}\\cdot\ \\log_2\{y\}\^\{1\}\$\s+"
+            r"flops,\ merge:\ 312\.47721968273464/6\.6446408501203775\ \$1\.374\+6\.698\\cdot\ \\log_2\{y\}\^\{1\}\+"
+            r"4\.384\\times10\^\{−2\}\\cdot\ x\^\{3/2\}\\cdot\ \\log_2\{x\}\^\{2\}\\cdot\ \\log_2\{y\}\^\{1\}\$\s+"
+            r"flops,\ sort:\ 312\.47721968273464/6\.6446408501203775\ \$1\.374\+6\.698\\cdot\ \\log_2\{y\}\^\{1\}\+"
+            r"4\.384\\times10\^\{−2\}\\cdot\ x\^\{3/2\}\\cdot\ \\log_2\{x\}\^\{2\}\\cdot\ \\log_2\{y\}\^\{1\}\$",
+            extrap.main,
+            ['--print', '{metric}, {callpath}: {rss}/{smape} {model:latex}', '--text',
+             'data/text/two_parameter_3.txt'])"""
+
     def test_invalid(self):
         with self.assertRaises(OutputFormatError):
             extrap.main(['--print', '{metric}, {a}', '--text', 'data/text/two_parameter_3.txt'])
@@ -141,6 +177,18 @@ def test_point_formatting5(self):
         self.assertEqual(truth,
                          output.format_output(self.exp, r"{callpath},{metric}:{points:format:'{point:sep:'\''}'}"))
 
+    def test_point_formatting8(self):
+        truth = self._make_expected(
+            "2.00E+01\t1.00E+00 | 2.00E+01\t2.00E+00 | 2.00E+01\t3.00E+00 | 2.00E+01\t4.00E+00 | "
+            "2.00E+01\t5.00E+00 | 3.00E+01\t1.00E+00 | 3.00E+01\t2.00E+00 | 3.00E+01\t3.00E+00 | "
+            "3.00E+01\t4.00E+00 | 3.00E+01\t5.00E+00 | 4.00E+01\t1.00E+00 | 4.00E+01\t2.00E+00 | "
+            "4.00E+01\t3.00E+00 | 4.00E+01\t4.00E+00 | 4.00E+01\t5.00E+00 | 5.00E+01\t1.00E+00 | "
+            "5.00E+01\t2.00E+00 | 5.00E+01\t3.00E+00 | 5.00E+01\t4.00E+00 | 5.00E+01\t5.00E+00 | "
+            "6.00E+01\t1.00E+00 | 6.00E+01\t2.00E+00 | 6.00E+01\t3.00E+00 | 6.00E+01\t4.00E+00 | "
+            "6.00E+01\t5.00E+00\n")
+        self.assertEqual(truth,
+                         output.format_output(self.exp, r"{callpath},{metric}:{points:format:'{point:sep:'\t'}'}"))
+
     def test_point_formatting6(self):
         truth = self._make_expected(
             "x20.000;y1.000 | x20.000;y2.000 | x20.000;y3.000 | x20.000;y4.000 | x20.000;y5.000 | "
diff --git a/tests/test_scaling_detection.py b/tests/test_scaling_detection.py
new file mode 100644
index 00000000..c8a5e9e9
--- /dev/null
+++ b/tests/test_scaling_detection.py
@@ -0,0 +1,56 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import unittest
+
+from extrap.fileio import io_helper
+from extrap.fileio.file_reader.text_file_reader import TextFileReader
+
+
+class TestWeakScalingTextFiles(unittest.TestCase):
+    def test_read_one_parameter(self):
+        reader = TextFileReader()
+        for file in ["data/text/one_parameter_1.txt", "data/text/one_parameter_2.txt", "data/text/one_parameter_3.txt",
+                     "data/text/one_parameter_4.txt", "data/text/one_parameter_5.txt", "data/text/one_parameter_6.txt",
+                     "data/text/one_parameter_7.txt"]:
+            experiment = reader.read_experiment(file)
+            self.assertEqual([0], io_helper.check_for_strong_scaling(experiment))
+
+    def test_read_two_parameter(self):
+        reader = TextFileReader()
+        for file in ["data/text/two_parameter_1.txt", "data/text/two_parameter_2.txt", "data/text/two_parameter_3.txt",
+                     "data/text/two_parameter_4.txt", "data/text/two_parameter_5.txt", "data/text/two_parameter_6.txt"]:
+            experiment = reader.read_experiment(file)
+            self.assertEqual([0, 0], io_helper.check_for_strong_scaling(experiment))
+
+    def test_read_three_parameter(self):
+        reader = TextFileReader()
+        for file in ["data/text/three_parameter_1.txt", "data/text/three_parameter_2.txt",
+                     "data/text/three_parameter_3.txt"]:
+            experiment = reader.read_experiment(file)
+            self.assertEqual([0, 0, 0], io_helper.check_for_strong_scaling(experiment))
+
+
+class TestStrongScalingTextFiles(unittest.TestCase):
+    def test_read_one_parameter(self):
+        reader = TextFileReader()
+        experiment = reader.read_experiment("data/text/strong_scaling/one_parameter_1.txt")
+        self.assertEqual([1], io_helper.check_for_strong_scaling(experiment))
+        reader = TextFileReader()
+        experiment = reader.read_experiment("data/text/strong_scaling/one_parameter_2.txt")
+        self.assertEqual([1], io_helper.check_for_strong_scaling(experiment))
+        reader = TextFileReader()
+        experiment = reader.read_experiment("data/text/strong_scaling/one_parameter_6.txt")
+        self.assertEqual([1], io_helper.check_for_strong_scaling(experiment))
+
+    def test_read_two_parameter(self):
+        reader = TextFileReader()
+        experiment = reader.read_experiment("data/text/strong_scaling/two_parameter_1.txt")
+        self.assertEqual([0, 1], io_helper.check_for_strong_scaling(experiment))
+        reader = TextFileReader()
+        experiment = reader.read_experiment("data/text/strong_scaling/two_parameter_2.txt")
+        self.assertEqual([1, 0], io_helper.check_for_strong_scaling(experiment))
diff --git a/tests/test_segmented_modeler.py b/tests/test_segmented_modeler.py
new file mode 100644
index 00000000..f63ea053
--- /dev/null
+++ b/tests/test_segmented_modeler.py
@@ -0,0 +1,154 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2020-2021, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+import math
+import unittest
+from math import log2
+
+import numpy
+import numpy as np
+
+from extrap.entities.callpath import Callpath
+from extrap.entities.coordinate import Coordinate
+from extrap.entities.experiment import Experiment
+from extrap.entities.functions import SingleParameterFunction, SegmentedFunction
+from extrap.entities.measurement import Measurement
+from extrap.entities.metric import Metric
+from extrap.entities.model import Model, SegmentedModel
+from extrap.entities.parameter import Parameter
+from extrap.entities.terms import CompoundTerm
+from extrap.modelers.model_generator import ModelGenerator
+from extrap.modelers.single_parameter.segmented import SegmentedModeler
+from extrap.util.progress_bar import ProgressBar
+from tests.modelling_testcase import TestCaseWithFunctionAssertions
+
+
+def create_experiment(g, f, parameter_values, changing_point):
+    parameter = Parameter("p")
+    metric = Metric("runtime")
+    callpath = Callpath("main")
+    experiment = Experiment()
+    experiment.add_callpath(callpath)
+    experiment.add_metric(metric)
+    experiment.add_parameter(parameter)
+    for i in range(len(parameter_values)):
+        coordinate = Coordinate(parameter_values[i])
+        experiment.add_coordinate(coordinate)
+        p = parameter_values[i]
+        if p >= changing_point:
+            metric_value = g(p)
+        else:
+            metric_value = f(p)
+        experiment.add_measurement(Measurement(coordinate, callpath, metric, metric_value))
+    return experiment
+
+
+def get_segmented_model(experiment):
+    model_generator = ModelGenerator(experiment, modeler="SEGMENTED", name="Segmented", use_median=True)
+    with ProgressBar(desc='Generating models', disable=True) as pbar:
+        model_generator.model_all(pbar)
+    modeler = experiment.modelers[0]
+    models = modeler.models
+    model = models[(Callpath("main"), Metric("runtime"))]
+    functions = []
+    if isinstance(model, SegmentedModel):
+        for m in model.segment_models:
+            func = m.hypothesis.function
+            functions.append(func)
+    elif isinstance(model, Model):
+        func = model.hypothesis.function
+        functions.append(func)
+    else:
+        raise NotImplementedError()
+    return functions
+
+
+class TestSegmentedModeler(TestCaseWithFunctionAssertions):
+
+    def test_segmented_data_one(self):
+        parameter_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+        f = lambda p: p ** 2
+        g = lambda p: 30 + p
+        changing_point = 6
+
+        experiment = create_experiment(g, f, parameter_values, changing_point)
+        functions = get_segmented_model(experiment)
+
+        term = CompoundTerm.create(2, 0)
+        function = SingleParameterFunction(term)
+        function.constant_coefficient = -5.753052622747037e-16
+
+        term2 = CompoundTerm.create(1, 0)
+        term2.coefficient = 1.0
+        function2 = SingleParameterFunction(term2)
+        function2.constant_coefficient = 29.999999999999975
+
+        self.assertApproxFunction(function, functions[0])
+        self.assertApproxFunction(function2, functions[1])
+
+    def test_segmented_data_two(self):
+        parameter_values = [4, 8, 12, 16, 20, 24, 28, 32, 36, 40]
+        f = lambda p: log2(p) ** 1
+        g = lambda p: p ** 2
+        changing_point = 22
+
+        experiment = create_experiment(g, f, parameter_values, changing_point)
+        functions = get_segmented_model(experiment)
+
+        term = CompoundTerm.create(0, 1, c=1)
+        function = SingleParameterFunction(term)
+        function.constant_coefficient = -1.1467601473192458e-16
+
+        term = CompoundTerm.create(2, 0)
+        term.coefficient = 1.0
+        function2 = SingleParameterFunction(term)
+        function2.constant_coefficient = 4.258612552374011e-13
+
+        self.assertApproxFunction(function, functions[0])
+        self.assertApproxFunction(function2, functions[1])
+
+    def test_no_segmentation(self):
+        parameter_values = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
+        f = lambda p: p ** 2 * log2(p) ** 1
+        g = lambda p: p ** 2 * log2(p) ** 1
+        changing_point = 50
+
+        experiment = create_experiment(g, f, parameter_values, changing_point)
+        functions = get_segmented_model(experiment)
+
+        term = CompoundTerm.create(2, 1, c=1)
+        term.coefficient = 1.0
+        function = SingleParameterFunction(term)
+        function.constant_coefficient = 1.2179330821573644e-11
+
+        self.assertApproxFunction(function, functions[0])
+
+    def test_const_square_segmentation(self):
+        measuremnts = [Measurement(Coordinate(500), None, None, [4.1, 3.9, 4.0, 4.0, 4.1]),
+                       Measurement(Coordinate(1000), None, None, [4.1, 3.9, 4.0, 4.0, 4.1]),
+                       Measurement(Coordinate(2000), None, None, [4.1, 3.9, 4.0, 4.0, 4.1]),
+                       Measurement(Coordinate(4000), None, None, [16, 15.999, 16.01, 16.01, 15.99]),
+                       Measurement(Coordinate(8000), None, None, [64, 64, 64, 64.01, 63.99]),
+                       Measurement(Coordinate(16000), None, None, [256.01, 255.99, 256, 256]),
+                       ]
+        SegmentedModeler().model([measuremnts])
+
+    def test_segmented_function(self):
+        segments = [SingleParameterFunction(CompoundTerm.create(1, 1, 0)),
+                    SingleParameterFunction(CompoundTerm.create(2, 1, 0)),
+                    SingleParameterFunction(CompoundTerm.create(3, 1, 0))]
+        change_points = [(-math.inf, 1), (1, 5), (5, math.inf)]
+
+        class SegmentedTestFunction(SegmentedFunction):
+            MAX_NUM_SEGMENTS = 3
+
+        f = SegmentedTestFunction(segments, change_points)
+        res = f.evaluate(np.array([0.5, 1, 2, 8]))
+        numpy.testing.assert_array_equal(np.array([0.5, 1, 4, 512]), res)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index 698ebb1a..8fa58bab 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -16,9 +16,10 @@
 from extrap.entities.measurement import Measurement
 from extrap.fileio.file_reader.text_file_reader import TextFileReader
 from extrap.modelers.model_generator import ModelGenerator
+from tests.serialization_testcase import BasicExperimentSerializationTest, BasicMPExperimentSerializationTest
 
 
-class TestSingleParameter(unittest.TestCase):
+class TestSingleParameter(BasicExperimentSerializationTest, unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         cls.experiment = TextFileReader().read_experiment("data/text/one_parameter_1.txt")
@@ -27,35 +28,8 @@ def setUpClass(cls) -> None:
         exp_str = schema.dumps(cls.experiment)
         cls.reconstructed: Experiment = schema.loads(exp_str)
 
-    def test_setup(self):
-        self.setUpClass()
 
-    def test_parameters(self):
-        self.assertListEqual(self.experiment.parameters, self.reconstructed.parameters)
-
-    def test_measurements(self):
-        self.assertDictEqual(self.experiment.measurements, self.reconstructed.measurements)
-
-    def test_coordinates(self):
-        self.assertListEqual(self.experiment.coordinates, self.reconstructed.coordinates)
-
-    def test_callpaths(self):
-        self.assertListEqual(self.experiment.callpaths, self.reconstructed.callpaths)
-
-    def test_metrics(self):
-        self.assertListEqual(self.experiment.metrics, self.reconstructed.metrics)
-
-    def test_call_tree(self):
-        self.assertEqual(self.experiment.call_tree, self.reconstructed.call_tree)
-
-    def test_modelers(self):
-        self.assertListEqual(self.experiment.modelers, self.reconstructed.modelers)
-
-    def test_scaling(self):
-        self.assertEqual(self.experiment.scaling, self.reconstructed.scaling)
-
-
-class TestSingleParameterAfterModeling(unittest.TestCase):
+class TestSingleParameterAfterModeling(BasicExperimentSerializationTest, unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         cls.experiment = TextFileReader().read_experiment("data/text/one_parameter_1.txt")
@@ -65,34 +39,6 @@ def setUpClass(cls) -> None:
         exp_str = schema.dumps(cls.experiment)
         cls.reconstructed: Experiment = schema.loads(exp_str)
 
-    def test_setup(self):
-        self.setUpClass()
-
-    def test_parameters(self):
-        self.assertListEqual(self.experiment.parameters, self.reconstructed.parameters)
-        pass
-
-    def test_measurements(self):
-        self.assertDictEqual(self.experiment.measurements, self.reconstructed.measurements)
-
-    def test_coordinates(self):
-        self.assertListEqual(self.experiment.coordinates, self.reconstructed.coordinates)
-
-    def test_callpaths(self):
-        self.assertListEqual(self.experiment.callpaths, self.reconstructed.callpaths)
-
-    def test_metrics(self):
-        self.assertListEqual(self.experiment.metrics, self.reconstructed.metrics)
-
-    def test_call_tree(self):
-        self.assertEqual(self.experiment.call_tree, self.reconstructed.call_tree)
-
-    def test_modelers(self):
-        self.assertListEqual(self.experiment.modelers, self.reconstructed.modelers)
-
-    def test_scaling(self):
-        self.assertEqual(self.experiment.scaling, self.reconstructed.scaling)
-
 
 class TestSerialization(unittest.TestCase):
 
@@ -139,7 +85,7 @@ def test_serialize_constant_hypothesis(self):
         self.assertEqual(hyp, reconstructed)
 
 
-class TestMultiParameter(unittest.TestCase):
+class TestMultiParameter(BasicMPExperimentSerializationTest, unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         cls.experiment = TextFileReader().read_experiment("data/text/two_parameter_3.txt")
@@ -148,35 +94,8 @@ def setUpClass(cls) -> None:
         exp_str = schema.dumps(cls.experiment)
         cls.reconstructed: Experiment = schema.loads(exp_str)
 
-    def test_setup(self):
-        self.setUpClass()
 
-    def test_parameters(self):
-        self.assertListEqual(self.experiment.parameters, self.reconstructed.parameters)
-
-    def test_measurements(self):
-        self.assertDictEqual(self.experiment.measurements, self.reconstructed.measurements)
-
-    def test_coordinates(self):
-        self.assertListEqual(self.experiment.coordinates, self.reconstructed.coordinates)
-
-    def test_callpaths(self):
-        self.assertListEqual(self.experiment.callpaths, self.reconstructed.callpaths)
-
-    def test_metrics(self):
-        self.assertListEqual(self.experiment.metrics, self.reconstructed.metrics)
-
-    def test_call_tree(self):
-        self.assertEqual(self.experiment.call_tree, self.reconstructed.call_tree)
-
-    def test_modelers(self):
-        self.assertListEqual(self.experiment.modelers, self.reconstructed.modelers)
-
-    def test_scaling(self):
-        self.assertEqual(self.experiment.scaling, self.reconstructed.scaling)
-
-
-class TestMultiParameterAfterModeling(unittest.TestCase):
+class TestMultiParameterAfterModeling(BasicMPExperimentSerializationTest, unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         cls.experiment = TextFileReader().read_experiment("data/text/two_parameter_3.txt")
@@ -186,34 +105,6 @@ def setUpClass(cls) -> None:
         exp_str = schema.dumps(cls.experiment)
         cls.reconstructed: Experiment = schema.loads(exp_str)
 
-    def test_setup(self):
-        self.setUpClass()
-
-    def test_parameters(self):
-        self.assertListEqual(self.experiment.parameters, self.reconstructed.parameters)
-        pass
-
-    def test_measurements(self):
-        self.assertDictEqual(self.experiment.measurements, self.reconstructed.measurements)
-
-    def test_coordinates(self):
-        self.assertListEqual(self.experiment.coordinates, self.reconstructed.coordinates)
-
-    def test_callpaths(self):
-        self.assertListEqual(self.experiment.callpaths, self.reconstructed.callpaths)
-
-    def test_metrics(self):
-        self.assertListEqual(self.experiment.metrics, self.reconstructed.metrics)
-
-    def test_call_tree(self):
-        self.assertEqual(self.experiment.call_tree, self.reconstructed.call_tree)
-
-    def test_modelers(self):
-        self.assertListEqual(self.experiment.modelers, self.reconstructed.modelers)
-
-    def test_scaling(self):
-        self.assertEqual(self.experiment.scaling, self.reconstructed.scaling)
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_serializing_segments.py b/tests/test_serializing_segments.py
new file mode 100644
index 00000000..b9b006dc
--- /dev/null
+++ b/tests/test_serializing_segments.py
@@ -0,0 +1,36 @@
+import unittest
+
+from extrap.entities.experiment import ExperimentSchema, Experiment
+from extrap.fileio.file_reader.text_file_reader import TextFileReader
+from extrap.modelers.model_generator import ModelGenerator
+from extrap.modelers.multi_parameter.multi_parameter_modeler import MultiParameterModeler
+from extrap.modelers.single_parameter.segmented import SegmentedModeler
+from tests.serialization_testcase import BasicExperimentSerializationTest
+
+
+class TestSerializingSegments(BasicExperimentSerializationTest, unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.experiment = TextFileReader().read_experiment("data/text/one_parameter_segmented_5.txt")
+        ModelGenerator(cls.experiment, SegmentedModeler()).model_all()
+        schema = ExperimentSchema()
+        # print(json.dumps(schema.dump(cls.experiment), indent=1))
+        exp_str = schema.dumps(cls.experiment)
+        cls.reconstructed: Experiment = schema.loads(exp_str)
+
+
+class TestSerializingMultiParameterSegments(BasicExperimentSerializationTest, unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.experiment = TextFileReader().read_experiment("data/text/two_parameter_segmented_1.txt")
+        mp = MultiParameterModeler()
+        mp.single_parameter_modeler = SegmentedModeler()
+        ModelGenerator(cls.experiment, mp).model_all()
+        schema = ExperimentSchema()
+        # print(json.dumps(schema.dump(cls.experiment), indent=1))
+        exp_str = schema.dumps(cls.experiment)
+        cls.reconstructed: Experiment = schema.loads(exp_str)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_serializing_values.py b/tests/test_serializing_values.py
new file mode 100644
index 00000000..6ed5baae
--- /dev/null
+++ b/tests/test_serializing_values.py
@@ -0,0 +1,68 @@
+# This file is part of the Extra-P software (http://www.scalasca.org/software/extra-p)
+#
+# Copyright (c) 2023, Technical University of Darmstadt, Germany
+#
+# This software may be modified and distributed under the terms of a BSD-style license.
+# See the LICENSE file in the base directory for details.
+
+import tempfile
+import unittest
+import zipfile
+
+import numpy as np
+
+from extrap.fileio.experiment_io import write_experiment, read_experiment
+from extrap.fileio.file_reader.text_file_reader import TextFileReader
+from extrap.fileio.values_io import ValueWriter, ValueReader
+
+
+class TestSerializingValues(unittest.TestCase):
+    def test_load_store_values(self):
+        with tempfile.TemporaryFile() as path:
+            with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED,
+                                 compresslevel=1, allowZip64=True) as file:
+                val_writer = ValueWriter(file)
+                id1 = val_writer.write_values(np.array([1, 2, 3, 4, 5, 6]))
+                val_writer.write_values(np.array([2, 2]))
+                id3 = val_writer.write_values(np.array([3, 3]))
+                val_writer.flush()
+                print('before close')
+
+            with zipfile.ZipFile(path, 'r', compression=zipfile.ZIP_DEFLATED,
+                                 compresslevel=1, allowZip64=True) as file:
+                val_reader = ValueReader(file)
+                self.assertTrue(np.all(np.array([1, 2, 3, 4, 5, 6]) == val_reader.read_values(*id1)))
+                self.assertTrue(np.all(np.array([3, 3]) == val_reader.read_values(*id3)))
+                print('done')
+
+                if __name__ == '__main__':
+                    unittest.main()
+
+    def test_experiment_with_values(self):
+        reader = TextFileReader()
+        reader.keep_values = True
+        experiment_org = reader.read_experiment("data/text/two_parameter_3.txt")
+        with tempfile.TemporaryFile() as path:
+            write_experiment(experiment_org, path)
+            experiment = read_experiment(path)
+        self.assertDictEqual(experiment_org.measurements, experiment.measurements)
+        for k in experiment_org.measurements:
+            ms1 = experiment.measurements[k]
+            ms2 = experiment_org.measurements[k]
+            for m1, m2 in zip(ms1, ms2):
+                self.assertTrue(np.all(m1.values == m2.values))
+
+    def test_experiment_without_values(self):
+        reader = TextFileReader()
+        reader.keep_values = False
+        experiment_org = reader.read_experiment("data/text/two_parameter_3.txt")
+        with tempfile.TemporaryFile() as path:
+            write_experiment(experiment_org, path)
+            experiment = read_experiment(path)
+        self.assertDictEqual(experiment_org.measurements, experiment.measurements)
+        for k in experiment_org.measurements:
+            ms1 = experiment.measurements[k]
+            ms2 = experiment_org.measurements[k]
+            for m1, m2 in zip(ms1, ms2):
+                self.assertIsNone(m1.values)
+                self.assertIsNone(m2.values)