Repository: precice/micro-manager (language: python)

Issue:
Add test for solverdummy with adaptivity
Closes #28 
This PR adds tests for the solverdummies in python and c++ with adaptivity.
At the moment, there is a new `precice-config-adaptivity.xml`, `micro-manager-adaptivity-config.json` and `run_micro_manager_adaptivity.py`. I would prefer a different way that does not add so many new files to the solverdummies as they should be easy to navigate.

This PR also adds a `__deepcopy__()` function to the c++ solverdummies which is required for [deepcopy support](https://pybind11.readthedocs.io/en/latest/advanced/classes.html#deepcopy-support) of pybind11 classes.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml
new file mode 100644
index 00000000..f7e0ce61
--- /dev/null
+++ b/.github/workflows/check-links.yml
@@ -0,0 +1,14 @@
+name: Check links (manual)
+on: workflow_dispatch
+jobs:
+  check_links:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - name: Check links in markdown files (markdown-link-check)
+        uses: gaurav-nelson/github-action-markdown-link-check@v1
+        with:
+          use-quiet-mode: 'yes'
+          use-verbose-mode: 'no'
+          config-file: '.markdown-link-check-config.json'
diff --git a/.github/workflows/check-markdown.yml b/.github/workflows/check-markdown.yml
new file mode 100644
index 00000000..39819fb4
--- /dev/null
+++ b/.github/workflows/check-markdown.yml
@@ -0,0 +1,13 @@
+name: Lint docs
+on: [push, pull_request]
+jobs:
+  check_md:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - name: Lint markdown files (markdownlint)
+        uses: articulate/actions-markdownlint@v1
+        with:
+          config: .markdownlint.json
+          files: '.'
diff --git a/.github/workflows/check-pep8.yml b/.github/workflows/check-pep8.yml
index 6f6e71bc..46ef5313 100644
--- a/.github/workflows/check-pep8.yml
+++ b/.github/workflows/check-pep8.yml
@@ -2,7 +2,8 @@ name: autopep8
 on:
   push:
     branches:
-      - "*"
+      - main
+      - develop
   pull_request:
     branches:
       - "*"
diff --git a/.github/workflows/run-macro-micro-dummy.yml b/.github/workflows/run-macro-micro-dummy.yml
index f852f5c6..2394f853 100644
--- a/.github/workflows/run-macro-micro-dummy.yml
+++ b/.github/workflows/run-macro-micro-dummy.yml
@@ -1,30 +1,58 @@
-name: Run macro-micro dummy case
+name: Run macro-micro dummy
 on:
   push:
     branches:
-      - "*"
+      - main
+      - develop
   pull_request:
     branches:
       - "*"
 jobs:  
-  run_macro_micro_dummy:
-    name: Run macro-micro dummy
+  run_dummy:
+    name: Run dummy
     runs-on: ubuntu-latest
     container: precice/precice
     steps:
+
       - name: Checkout Repository
-        uses: actions/checkout@v2      
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
       - name: Install Dependencies
+        working-directory: micro-manager
         run: |
           apt-get -qq update
           apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          apt-get -qq install sudo
           python -m pip install --upgrade pip
           pip install setuptools wheel twine
-      - name: Install pyprecice
-        run: pip3 install --user pyprecice
+
       - name: Install micro-manager
+        working-directory: micro-manager
         run: pip3 install --user .
-      - name: Run macro-micro dummy
+
+      - name: Run python macro-micro dummy
+        timeout-minutes: 3
+        working-directory: micro-manager/examples
+        run: python3 python-dummy/run_micro_manager.py --config micro-manager-config.json & python3 macro_dummy.py
+
+      - name: Run adaptive python macro-micro dummy
+        timeout-minutes: 3
+        working-directory: micro-manager/examples
+        run: python3 python-dummy/run_micro_manager.py --config micro-manager-adaptivity-config.json & python3 macro_dummy.py
+
+      - name: Run c++ macro-micro dummy
+        timeout-minutes: 3
+        working-directory: micro-manager/examples
         run: |
-          cd examples/macro-micro-dummy/
-          python3 macro_dummy.py & python3 run_micro_manager.py
+          cd cpp-dummy/
+          pip install pybind11
+          c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) micro_cpp_dummy.cpp -o micro_dummy$(python3-config --extension-suffix)
+          cd ../
+          python3 cpp-dummy/run_micro_manager.py --config micro-manager-config.json & python3 macro_dummy.py
+
+      - name: Run adaptive c++ macro-micro dummy
+        timeout-minutes: 3
+        working-directory: micro-manager/examples
+        run: python3 cpp-dummy/run_micro_manager.py --config micro-manager-adaptivity-config.json & python3 macro_dummy.py
diff --git a/.github/workflows/update-website.yml b/.github/workflows/update-website.yml
new file mode 100644
index 00000000..ecdd6dbe
--- /dev/null
+++ b/.github/workflows/update-website.yml
@@ -0,0 +1,22 @@
+name: Update website
+on:
+  push:
+    branches:
+      - 'develop'
+    paths:
+      - 'docs/**'
+jobs:
+  trigger:
+    runs-on: ubuntu-latest
+    env:
+      WORKFLOW_FILENAME: update-submodules.yml
+    steps:
+    - name: Trigger workflow
+      run: |
+        curl \
+        --request POST \
+        --url https://api.github.com/repos/precice/precice.github.io/actions/workflows/$WORKFLOW_FILENAME/dispatches \
+        --header "authorization: token ${{ secrets.WORKFLOW_DISPATCH_TOKEN }}" \
+        --header "Accept: application/vnd.github.v3+json" \
+        --data '{"ref":"master"}' \
+        --fail
diff --git a/.gitignore b/.gitignore
index 7dfa6f97..3251b572 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,14 @@ __pycache__
 
 # Building artifacts
 build
+micro_manager_precice.egg-info/
 micro_manager.egg-info/
 
 # Packaging artifacts
 dist
+
+# vscode
+.vscode
+
+# Tests output
+*.log
diff --git a/.markdown-link-check-config.json b/.markdown-link-check-config.json
new file mode 100644
index 00000000..3fff32c2
--- /dev/null
+++ b/.markdown-link-check-config.json
@@ -0,0 +1,3 @@
+{
+  "aliveStatusCodes": [429, 200]
+}
\ No newline at end of file
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 00000000..979cb285
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,5 @@
+{
+    "MD013": false,
+    "MD033": false,
+    "MD034": false
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf27cdf0..c784ef1d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,18 @@
 
 ## latest
 
+## v0.3.0
+
+- Add global variant to adaptivity (still experimental) https://github.com/precice/micro-manager/pull/42
+- Add norm-based (L1 and L2) support for functions in similarity distance calculation with absolute and relative variants https://github.com/precice/micro-manager/pull/40
+- New domain decomposition strategy based on user input of number of processors along each axis https://github.com/precice/micro-manager/pull/41
+- Add pickling support for C++ solver dummy https://github.com/precice/micro-manager/pull/30
+- Add C++ solver dummy to show how a C++ micro simulation can be controlled by the Micro Manager https://github.com/precice/micro-manager/pull/22
+- Add local adaptivity https://github.com/precice/micro-manager/pull/21
+
 ## v0.2.1
 
-- Fixing the broken action workflow `run-macro-micro-dummy` 
+- Fixing the broken action workflow `run-macro-micro-dummy`
 
 ## v0.2.0
 
diff --git a/README.md b/README.md
index db293d53..af8f6b4f 100644
--- a/README.md
+++ b/README.md
@@ -1,199 +1,21 @@
 # Micro Manager
 
-A Manager tool to facilitate solving two-scale (macro-micro) coupled problems using the coupling library [preCICE](https://github.com/precice/precice).
+<a style="text-decoration: none" href="https://github.com/precice/micro-manager/blob/develop/LICENSE" target="_blank">
+    <img src="https://img.shields.io/github/license/precice/micro-manager.svg" alt="GNU LGPL license">
+</a>
 
-## Installing the Micro Manager
+<a style="text-decoration: none" href="https://github.com/precice/fenics-adapter/actions/workflows/build-and-test.yml" target="_blank">
+    <img src="https://github.com/precice/micro-manager/actions/workflows/run-adaptivity-test.yml/badge.svg" alt="Test Adaptivity">
+</a>
 
-### Option 1: Using pip
+<a style="text-decoration: none" href="https://pypi.org/project/micro-manager-precice/" target="_blank">
+    <img src="https://github.com/precice/micro-manager/actions/workflows/pythonpublish.yml/badge.svg" alt="Upload Python Package">
+</a>
 
-It is recommended to install [micro-manager-precice from PyPI](https://pypi.org/project/micro-manager-precice/) by running
+A tool to facilitate solving two-scale (macro-micro) coupled problems using the coupling library [preCICE](https://www.precice.org/).
 
-```bash
-pip install --user micro-manager-precice
-```
+The main documentation is rendered on the [preCICE website](https://precice.org/tooling-micro-manager-overview.html).
 
-If the dependencies are not installed, then `pip` will attempt to install them for you. If you encounter problems in the direct installation, see the [dependencies section](https://github.com/precice/micro-manager#required-dependencies) below for links to installation procedures of all dependencies.
+Please report any [issues](https://github.com/precice/micro-manager/issues) and give us feedback through [one of our community channels](https://precice.org/community-channels.html).
 
-### Option 2: Clone this repository and install manually
-
-#### Required dependencies
-
-Ensure that the following dependencies are installed:
-
-* Python 3 or higher
-* [preCICE](https://github.com/precice/precice/wiki)
-* [pyprecice: Python language bindings for preCICE](https://github.com/precice/python-bindings)
-* [numpy](https://numpy.org/install/)
-* [mpi4py](https://mpi4py.readthedocs.io/en/stable/install.html)
-
-#### Build and install the Manager using pip
-
-After cloning this repository, go to the project directory `micro-manager` and run `pip3 install --user .`.
-
-#### Build and install the Manager using Python
-
-After cloning this repository, go to the project directory `micro-manager` and run `python setup.py install --user`.
-
-## Using the Micro Manager
-
-The Micro Manager facilitates two-scale coupling between one macro-scale simulation and many micro-scale simulations. It creates instances of several micro simulations and couples them to one macro simulation, using preCICE. An existing micro simulation code written in Python needs to be converted into a library with a specific class name and specific function names. The next section describes the required library structure of the micro simulation code. On the other hand, the micro-problem is coupled to preCICE directly. The section [couple your code](https://precice.org/couple-your-code-overview.html) of the preCICE documentation gives more details on coupling existing codes.
-
-### Steps to convert micro simulation code to a callable library
-
-* Create a class called `MicroSimulation`. It is good practice to define class member variables in the class constructor `__init__`. The constructor of the class `MicroSimulation` gets an integer value from the Micro Manager which is the ID of the micro simulation from the perspective of the Micro Manager. This ID can be used later on for writing output.
-* **Optional**: Define a function `initialize` which computes the initial state of the micro simulation and returns initial values, which need to be transferred to the macro simulation. The return value needs to be a Python dictionary with the names of the quantities as keys and the values of the quantities as the dictionary values.
-* Create a function `solve`, which consists of all solving steps of one time step of a micro simulation or, if the micro problem is a steady-state simulation, all solving steps until the steady state is reached. `solve` should take a Python dictionary as an input, which has the name of the input data as keys and the corresponding data values as values. The `solve` function should return the quantities that need to be communicated to the macro-side. The return entity needs to again be a Python dictionary with the names of the quantities as keys and the values of the quantities as values.
-* If implicit coupling is required between the macro and all micro problems, then you can additionally define two functions `save_checkpoint` and `revert_to_checkpoint`.
-  * `save_checkpoint` should save the current state of the micro problem.
-  * `revert_to_checkpoint` should revert to the saved state (required if the coupling loop does not convergence after a time step).
-* **Optional**: Define a function `output` which writes the micro simulation output. The micro Manager will call this function with the frequency set by the configuration option `simulation_params: micro_output_n`.
-
-An example of a MicroSimulation class as used in `/examples/macro-micro-dummy`:
-
-```python
-class MicroSimulation:
-
-    def __init__(self):
-        """
-        Constructor of MicroSimulation class.
-        """
-        self._dims = 3
-        self._micro_scalar_data = None
-        self._micro_vector_data = None
-        self._checkpoint = None
-
-    def initialize(self):
-        self._micro_scalar_data = 0
-        self._micro_vector_data = []
-        self._checkpoint = 0
-
-    def solve(self, macro_data, dt):
-        assert dt != 0
-        self._micro_vector_data = []
-        self._micro_scalar_data = macro_data["macro-scalar-data"]
-        for d in range(self._dims):
-            self._micro_vector_data.append(macro_data["macro-vector-data"][d])
-
-        return {"micro-scalar-data": self._micro_scalar_data.copy(),
-                "micro-vector-data": self._micro_vector_data.copy()}
-
-    def save_checkpoint(self):
-        print("Saving state of micro problem")
-        self._checkpoint = self._micro_scalar_data
-
-    def reload_checkpoint(self):
-        print("Reverting to old state of micro problem")
-        self._micro_scalar_data = self._checkpoint
-
-    def output(self):
-        print("Writing VTK output of micro problem")
-        self._write_vtk()
-```
-
-### Configuring the Micro Manager
-
-The Micro Manager is configured at runtime using a JSON file `micro-manager-config.json`. An example configuration file is:
-
-```json
-{
-    "micro_file_name": "micro_heat",
-    "coupling_params": {
-            "participant_name": "Micro-Manager",
-            "config_file_name": "precice-config.xml",
-            "macro_mesh_name": "macro-mesh",
-            "write_data_names": {"k_00": "scalar", "k_11": "scalar", "porosity": "scalar"},
-            "read_data_names": {"concentration": "scalar"}
-    },
-    "simulation_params": {
-      "macro_domain_bounds": [0.0, 1.0, 0.0, 0.5],
-      "micro_output_n": 5
-    },
-    "diagnostics": {
-      "data_from_micro_sims": {"grain_size": "scalar"},
-      "output_micro_sim_solve_time": "True"
-    }
-}
-```
-
-The following quantities need to be configured:
-
-* `micro_file`: Path to the micro-simulation script. **Do not add the file extension** `.py`.
-* `coupling_params`:
-  * `precice_config`: Path to the preCICE XML configuration file.
-  * `macro_mesh`: Name of the macro mesh as stated in the preCICE configuration.
-  * `read_data_names`: A Python dictionary with the names of the data to be read from preCICE as keys and `"scalar"` or `"vector"`  as values.
-  * `write_data_names`: A Python dictionary with the names of the data to be written to preCICE as keys and `"scalar"` or `"vector"`  as values.
-* `simulation_params`:
-  * `macro_domain_bounds`: Minimum and maximum limits of the macro-domain, having the format `[xmin, xmax, ymin, ymax, zmin, zmax]`.
-
-In addition to the aforementioned configuration options there are optional choices:
-
-* `simulation_params`:
-  * `micro_output_n`: Frequency of calling the output functionality of the micro simulation in terms of number of time steps. If this quantity is configured the Micro Manager will attempt to call the `output()` function of the micro simulation.
-
-The Micro Manager is capable of generating diagnostics type output of the micro simulations, which is critical in the development phase of two-scale simulations. The following configuration options are available:
-
-* `diagnostics`:
-  * `data_from_micro_sims`: A Python dictionary with the names of the data from the micro simulation to be written to VTK files as keys and `"scalar"` or `"vector"`  as values.
-  * `output_micro_sim_solve_time`: When `True`, the Manager writes the wall clock time of the `solve()` function of each micro simulation to the VTK output.
-
-#### Changes to preCICE configuration file
-
-The Micro Manager relies on the [export functionality](https://precice.org/configuration-export.html#enabling-exporters) of preCICE to write diagnostics data output. If the option `diagnotics: data_from_micro_sims` is configured, the corresponding export tag also needs to be set in the preCICE XML configuration script.
-
-### Running the Micro Manager
-
-The Micro Manager is run directly from the terminal by providing the configuration file as an input argument in the following way:
-
-```bash
-micro_manager micro-manager-config.json
-```
-
-Alternatively the Manager can also be run by creating a Python script which imports the Micro Manager package and calls its run function. For example a run script `run-micro-manager.py` would look like:
-
-```python
-from micro_manager import MicroManager
-
-manager = MicroManager("micro-manager-config.json")
-
-manager.run()
-```
-
-The script is then run:
-
-```bash
-python run-micro-manager.py
-```
-
-The Micro Manager can also be run in parallel, using the same script as stated above:
-
-```bash
-mpirun -n <number-of-procs> python3 run-micro-manager.py
-```
-
-### Advanced configuration options
-
-In addition to the above mentioned configuration options, the Manager offers more options for diagnostics output.
-
-If the user wants to output the clock time required to solve each micro simulation, They can add the following keyword to the configuration:
-
-```json
-"diagnostics": {
-  "output_micro_sim_solve_time": "True"
-}
-```
-
-Additionally if the micro simulation code has a function called `output`, the Manager will try to call it in order to generate output of all micro simulations. In this situation, the Manager can be configured to output at a particular interval. This configuration is done as follows:
-
-```json
-"simulation_params": {
-  "micro_output_n": 10
-}
-```
-
-Here, the Manager will write output of micro simulations every 10 time steps. If the entity `micro_output_n` is not defined, then the Manager will output the micro simulation output in every time step.
-
-### Creating a preCICE configuration file for a macro-micro problem
-
-In addition to configuring the Micro Manager, preCICE itself also needs to be configured via a [XML configuration file](https://precice.org/configuration-overview.html).
-The user is expected to configure preCICE with the correct names of the data being exchanged between the macro and micro side. An example of such a macro-micro configuration for preCICE can be found in [this two-scale heat conduction example](https://github.com/IshaanDesai/coupled-heat-conduction).
+The concept and initial design of the Micro Manager is described in *Desai, Ishaan, & Bringedal, Carina & Uekermann, Benjamin. A flexible software approach to simulate two-scale coupled problems. ECCOMAS Congress 2022. [10.23967/eccomas.2022.037](https://doi.org/10.23967/eccomas.2022.037)*.
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..6fee76fc
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,29 @@
+---
+title: The Micro Manager
+permalink: tooling-micro-manager-overview.html
+keywords: tooling, macro-micro, two-scale
+summary: A tool to manage many micro simulations and couple them to a macro simulation via preCICE.
+---
+
+## What is this?
+
+The Micro Manager manages many simulations on a micro scale and couples them to one simulation on a macro scale. For the coupling itself, it heavily relies on the coupling library [preCICE](https://precice.org/index.html).
+
+![Micro Manager strategy schematic](images/tooling-micro-manager-manager-solution.png)
+
+## What can it do?
+
+The Micro Manager couples many micro simulations with one macro simulation. This includes ...
+
+- ... transferring scalar and vector data to and from a large number of micro simulations.
+- ... running micro simulations in parallel using MPI.
+- ... adaptively activating and deactivating micro simulations based on a similarity calculation.
+
+## Documentation
+
+To use the Micro Manager for a macro-micro coupling, your micro simulation code needs to be in a library format with a specific class name and functions with specific names. For a macro-micro coupled problem, the macro simulation code is coupled to preCICE directly. The section [couple your code](couple-your-code-overview.html) of the preCICE documentation gives more details on coupling existing codes. To setup a macro-micro coupled simulation using the Micro Manager, follow these steps:
+
+- [Installation](tooling-micro-manager-installation.html)
+- [Preparing micro simulation](tooling-micro-manager-prepare-micro-simulation.html)
+- [Configuration](tooling-micro-manager-configuration.html)
+- [Running](tooling-micro-manager-running.html)
diff --git a/docs/ReleaseGuide.md b/docs/ReleaseGuide.md
new file mode 100644
index 00000000..e4333d7e
--- /dev/null
+++ b/docs/ReleaseGuide.md
@@ -0,0 +1,26 @@
+# Guide to release new version of the Micro Manager
+
+The developer who is releasing a new version of the the Micro Manager is expected to follow this work flow:
+
+The release of the `micro-manager` repository is made directly from a release branch called `micro-manager-v1.2.3`. This branch is mainly needed to help other developers with testing.
+
+1. Create a branch called `micro-manager-v1.2.3` from the latest commit of the `develop` branch.
+
+2. If it is a real release, [open a Pull Request `main` <-- `micro-manager-v1.2.3`](https://github.com/precice/micro-manager/compare/main...main) named after the version (i.e. `Release v1.2.3`) and briefly describe the new features of the release in the PR description.
+
+3. Bump the version in the `CHANGELOG.md` and in `setup.py` on `micro-manager-v1.2.3`.
+
+4. [Draft a new release](https://github.com/precice/micro-manager/releases/new) in the `Releases` section of the repository page in a web browser. The release tag needs to be the exact version number (i.e.`v1.2.3` or `v1.2.3rc1`, compare to [existing tags](https://github.com/precice/micro-manager/tags)). Use `@target:main`. Release title is also the version number (i.e. `v1.2.3` or `v1.2.3rc1`, compare to [existing releases](https://github.com/precice/micro-manager/tags)).
+
+    * *Note:* If it is a pre-release then the option *This is a pre-release* needs to be selected at the bottom of the page. Use `@target:micro-manager-v1.2.3` for a pre-release, since we will never merge a pre-release into `main`.
+    * Use the `Auto-generate release notes` feature.
+
+    a) If a pre-release is made: Directly hit the "Publish release" button in your Release Draft. Now you can check the artifacts (e.g. release on [PyPI](https://pypi.org/project/micro-manager-precice/#history)) of the release. *Note:* As soon as a new tag is created github actions will take care of deploying the new version on PyPI using [this workflow](https://github.com/precice/micro-manager/actions?query=workflow%3A%22Upload+Python+Package%22).
+
+    b) If this is a "real" release: As soon as one approving review is made, merge the release PR (from `micro-manager-v1.2.3`) into `main`.
+
+5. Merge `main` into `develop` for synchronization of `develop`.
+
+6. If everything is in order up to this point then the new version can be released by hitting the "Publish release" button in your Release Draft. This will create the corresponding tag and trigger [publishing the release to PyPI](https://github.com/precice/micro-manager/actions?query=workflow%3A%22Upload+Python+Package%22).
+
+7. Add an empty commit on main via `git checkout main`, then `git commit --allow-empty -m "post-tag bump"`. Check that everything is in order via `git log`. Important: The `tag` and `origin/main` should not point to the same commit.
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 00000000..50282950
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,166 @@
+---
+title: Configure the Micro Manager
+permalink: tooling-micro-manager-configuration.html
+keywords: tooling, macro-micro, two-scale
+summary: Provide a JSON file to configure the Micro Manager.
+---
+
+{% note %} In the preCICE XML configuration the Micro Manager is a participant with the name `Micro-Manager`. {% endnote %}
+
+The Micro Manager is configured with a JSON file. An example configuration file is
+
+```json
+{
+    "micro_file_name": "micro_solver",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-mesh",
+        "read_data_names": {"temperature": "scalar", "heat-flux": "vector"},
+        "write_data_names": {"porosity": "scalar", "conductivity": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
+    },
+    "diagnostics": {
+      "output_micro_sim_solve_time": "True"
+    }
+}
+```
+
+This example configuration file is in [`examples/micro-manager-config.json`](https://github.com/precice/micro-manager/tree/develop/examples/micro-manager-config.json).
+
+The path to the file containing the Python importable micro simulation class is specified in the `micro_file_name` parameter. If the file is not in the working directory, give the relative path.
+
+There are three main sections in the configuration file, the `coupling_params`, the `simulation_params` and the optional `diagnostics`.
+
+## Coupling Parameters
+
+Parameter | Description
+--- | ---
+`config_file_name` |  Path to the preCICE XML configuration file from the current working directory.
+`macro_mesh_name` |  Name of the macro mesh as stated in the preCICE configuration.
+`read_data_names` |  A Python dictionary with the names of the data to be read from preCICE as keys and `"scalar"` or `"vector"`  as values depending on the nature of the data.
+`write_data_names` |  A Python dictionary with the names of the data to be written to preCICE as keys and `"scalar"` or `"vector"`  as values depending on the nature of the data.
+
+## Simulation Parameters
+
+Parameter | Description
+--- | ---
+`macro_domain_bounds`| Minimum and maximum bounds of the macro-domain, having the format `[xmin, xmax, ymin, ymax, zmin, zmax]` in 3D and `[xmin, xmax, ymin, ymax]` in 2D.
+Domain decomposition parameters | See section on [domain decomposition](#domain-decomposition). But default, the Micro Manager assumes that it will be run in serial.
+Adaptivity parameters | See section on [adaptivity](#adaptivity). By default, adaptivity is disabled.
+
+## Diagnostics
+
+Parameter | Description
+--- | ---
+`data_from_micro_sims` | A Python dictionary with the names of the data from the micro simulation to be written to VTK files as keys and `"scalar"` or `"vector"` as values.
+`output_micro_sim_solve_time` | If `True`, the Micro Manager writes the wall clock time of the `solve()` function of each micro simulation.
+`micro_output_n`|  Frequency of calling the optional output functionality of the micro simulation in terms of number of time steps. If not given, `micro_sim.output()` is called every time step.
+
+### Adding diagnostics in the preCICE XML configuration
+
+If the parameter `data_from_micro_sims` is set, the data to be output needs to be written to preCICE, and an export tag needs to be added for the participant `Micro-Manager`. For example, let us consider the case that the data `porosity`, which is a scalar, needs to be exported. Unless already defined, define the data, and then write it to preCICE. Also, add an export tag. The resulting entries in the XML configuration file look like:
+
+```xml
+<data:scalar name="porosity"/>
+
+<participant name="Micro-Manager">
+  ...
+  <write-data name="porosity" mesh="macro-mesh"/>
+  <export:vtu directory="Micro-Manager-output" every-n-time-windows="5"/>
+</participant>
+```
+
+If `output_micro_sim_solve_time` is set, add similar entries for the data `micro_sim_time` in the following way:
+
+```xml
+<data:scalar name="micro_sim_time"/>
+
+<participant name="Micro-Manager">
+  ...
+  <write-data name="micro_sim_time" mesh="macro-mesh"/>
+  <export:vtu directory="Micro-Manager-output" every-n-time-windows="5"/>
+</participant>
+```
+
+## Domain decomposition
+
+The Micro Manager can be run in parallel. For a parallel run, set the desired partitions in each axis by setting the `decomposition` parameter. For example, if the domain is 3D and the decomposition needs to be two partitions in x, one partition in y, and sixteen partitions in for z, the setting is
+
+```json
+"simulation_params": {
+    "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+    "decomposition": [2, 1, 16]
+}
+```
+
+For a 2D domain, only two values need to be set for `decomposition`. The total number of partitions provided in the `decomposition` should be the same as the number of processors provided in the `mpirun`/`mpiexec` command.
+
+## Adaptivity
+
+The Micro Manager can adaptively control micro simulations. The adaptivity strategy is taken from
+
+1. Redeker, Magnus & Eck, Christof. (2013). A fast and accurate adaptive solution strategy for two-scale models with continuous inter-scale dependencies. Journal of Computational Physics. 240. 268-283. [10.1016/j.jcp.2012.12.025](https://doi.org/10.1016/j.jcp.2012.12.025).
+
+2. Bastidas, Manuela & Bringedal, Carina & Pop, Iuliu Sorin. (2021). A two-scale iterative scheme for a phase-field model for precipitation and dissolution in porous media. Applied Mathematics and Computation. 396. 125933. [10.1016/j.amc.2020.125933](https://doi.org/10.1016/j.amc.2020.125933).
+
+All the adaptivity parameters are chosen from the second publication.
+
+To turn on adaptivity, the following options need to be set in `simulation_params` under the sub-heading `adaptivity`:
+
+Parameter | Description
+--- | ---
+`type` | Set to either `local` or `global`. The type of adaptivity matters when the Micro Manager is run in parallel. `local` means comparing micro simulations within a local partitioned domain for similarity. `global` means comparing micro simulations from all partitions, so over the entire domain.
+`data` | List of names of data which are to be used to calculate if micro-simulations are similar or not. For example `["temperature", "porosity"]`.
+`history_param` | History parameter $$ \Lambda $$, set as $$ \Lambda >= 0 $$.
+`coarsening_constant` | Coarsening constant $$ C_c $$, set as $$ C_c < 1 $$.
+`refining_constant` | Refining constant $$ C_r $$, set as $$ C_r >= 0 $$.
+`every_implicit_iteration` | If True, adaptivity is calculated in every implicit iteration. <br> If False, adaptivity is calculated once at the start of the time window and then reused in every implicit time iteration.
+`similarity_measure`| Similarity measure to be used for adaptivity. Can be either `L1`, `L2`, `L1rel` or `L2rel`. By default, `L1` is used. The `rel` variants calculate the respective relative norms. This parameter is *optional*.
+
+Example of adaptivity configuration
+
+```json
+"simulation_params": {
+    "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+    "adaptivity" {
+        "type": "local",
+        "data": ["temperature", "porosity"],
+        "history_param": 0.5,
+        "coarsening_constant": 0.3,
+        "refining_constant": 0.4,
+        "every_implicit_iteration": "True"
+    }
+}
+```
+
+### Adding adaptivity in the preCICE XML configuration
+
+If adaptivity is used, the Micro Manager will attempt to write two scalar data per micro simulation to preCICE, called `active_state` and `active_steps`.
+
+Parameter | Description
+--- | ---
+`active_state` | `1` if the micro simulation is active in the time window, and `0` if inactive.
+`active_steps` | Summation of `active_state` up to the current time window.
+
+The Micro Manager uses the output functionality of preCICE, hence these data sets need to be manually added to the preCICE configuration file. In the mesh and the participant Micro-Manager add the following lines:
+
+```xml
+<data:scalar name="active_state"/>
+<data:scalar name="active_steps"/>
+
+<mesh name="macro-mesh">
+    <use-data name="active_state"/>
+    <use-data name="active_steps"/>
+</mesh>
+
+<participant name="Micro-Manager">
+    <write-data name="active_state" mesh="macro-mesh"/>
+    <write-data name="active_steps" mesh="macro-mesh"/>
+</participant>
+```
+
+## Next step
+
+After creating a configuration file you are ready to [run the Micro Manager](tooling-micro-manager-running.html).
diff --git a/docs/images/tooling-micro-manager-manager-solution.png b/docs/images/tooling-micro-manager-manager-solution.png
new file mode 100644
index 00000000..7ecc25e3
Binary files /dev/null and b/docs/images/tooling-micro-manager-manager-solution.png differ
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 00000000..03f3f243
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,60 @@
+---
+title: Get the Micro Manager
+permalink: tooling-micro-manager-installation.html
+keywords: tooling, macro-micro, two-scale
+summary: Install the Micro Manager by running `pip install --user micro-manager-precice`.
+---
+
+## Get the latest Micro Manager release
+
+The Micro Manager can be installed using `pip`. Make sure [preCICE](installation-overview.html) is installed before installing the Micro Manager. The Micro Manager is compatible with preCICE version [2.3.0](https://github.com/precice/precice/releases/tag/v2.3.0) and higher.
+
+### Option 1: Install from PyPI
+
+The Micro Manager package has the name [micro-manager-precice](https://pypi.org/project/micro-manager-precice/) on PyPI. To install `micro-manager-precice`, run
+
+```bash
+pip install --user micro-manager-precice
+```
+
+Unless already installed, the dependencies will be installed by `pip` during the installation procedure. preCICE itself needs to be installed separately. If you encounter problems in the direct installation, see the [dependencies section](#required-dependencies) below.
+
+### Option 2: Install manually
+
+#### Required dependencies
+
+Ensure that the following dependencies are installed:
+
+* Python 3
+* [preCICE](installation-overview.html) [v2.3.0](https://github.com/precice/precice/releases/tag/v2.3.0) or higher
+* [pyprecice: Python language bindings for preCICE](installation-bindings-python.html)
+* [numpy](https://numpy.org/install/)
+* [mpi4py](https://mpi4py.readthedocs.io/en/stable/install.html)
+
+#### Clone the Micro Manager
+
+```bash
+git clone https://github.com/precice/micro-manager.git
+```
+
+#### Install manually
+
+To install using `pip`, go to the directory `micro-manager/` and run
+
+```bash
+pip install --user .
+```
+
+To install using Python, go to the project directory `micro-manager/` and run
+
+```bash
+python setup.py install --user
+```
+
+## Get the latest development version
+
+If you want to use the latest development version of the Micro Manager, clone the [develop](https://github.com/precice/micro-manager/tree/develop) branch and then [build manually using pip](#install-manually).
+
+## Next step
+
+After successfully installing the Micro Manager, proceed to [preparing your micro simulation for the coupling](tooling-micro-manager-prepare-micro-simulation.html).
diff --git a/docs/micro-simulation-convert-to-library.md b/docs/micro-simulation-convert-to-library.md
new file mode 100644
index 00000000..8cafb9ec
--- /dev/null
+++ b/docs/micro-simulation-convert-to-library.md
@@ -0,0 +1,95 @@
+---
+title: Prepare micro simulation
+permalink: tooling-micro-manager-prepare-micro-simulation.html
+keywords: tooling, macro-micro, two-scale
+toc: off
+summary: Create an Python-importable class from your micro simulation code.
+---
+
+The Micro Manager requires that the micro simulation code be in a predefined class structure. As the Micro Manager is written in Python, micro simulation codes written in Python are the easiest to prepare. For micro simulation codes not written in Python, look at the [C++ micro simulation section](#create-an-python-importable-class-from-your-micro-simulation-code-written-in-c) below.
+
+{% note %} The Micro Manager [solver dummy examples](https://github.com/precice/micro-manager/tree/develop/examples) are minimal code examples with the predefined class structure. We recommend copying the appropriate example and modifying it with your micro simulation code to create a Python-importable class. {% endnote %}
+
+Restructure your micro simulation code into a Python class with the structure given below. The docstring of each function gives information on what it should do and what its input and output should be.
+
+```python
+class MicroSimulation: # Name is fixed
+    def __init__(self):
+        """
+        Constructor of class MicroSimulation.
+        """
+
+    def initialize(self) -> dict:
+        """
+        Initialize the micro simulation. This function is *optional*.
+
+        Returns
+        -------
+        data : dict
+            Python dictionary with names of micro data as keys and the data as values at the initial condition
+        """
+
+    def solve(self, macro_data: dict, dt: float) -> dict:
+        """
+        Solve one time step of the micro simulation for transient problems or solve until steady state for steady-state problems.
+
+        Parameters
+        ----------
+        macro_data : dict
+            Dictionary with names of macro data as keys and the data as values.
+        dt : float
+            Current time step size.
+
+        Returns
+        -------
+        micro_data : dict
+            Dictionary with names of micro data as keys and the updated micro data a values.
+        """
+
+    def set_state(self, state):
+        """
+        Set the state of the micro simulation.
+        """
+
+    def get_state(self):
+        """
+        Return the state of the micro simulation.
+        """
+
+    def output(self):
+        """
+        This function writes output of the micro simulation in some form.
+        It will be called with frequency set by configuration option `simulation_params: micro_output_n`
+        This function is *optional*.
+        """
+```
+
+A dummy code of a sample MicroSimulation class can be found in the [examples/python-dummy/micro_dummy.py](https://github.com/precice/micro-manager/blob/develop/examples/python-dummy/micro_dummy.py) directory.
+
+## Create an Python-importable class from your micro simulation code written in C++
+
+A dummy C++ dummy micro simulation code having a Python-importable class structure is provided in [`examples/cpp-dummy/micro_cpp_dummy.cpp`](https://github.com/precice/micro-manager/blob/develop/examples/cpp-dummy/micro_cpp_dummy.cpp). It uses [pybind11](https://pybind11.readthedocs.io/en/stable/) to enable control and use from Python. Restructuring a C++ micro simulation code has the following steps
+
+1. Create a C++ class which implements the functions given in the code snippet above.
+The `solve()` function should have the following signature:
+
+    ```cpp
+    py::dict MicroSimulation::solve(py::dict macro_data, double dt)
+    ```
+
+    [`py::dict`](https://pybind11.readthedocs.io/en/stable/advanced/pycpp/object.html?#instantiating-compound-python-types-from-c) is a Python dictionary which can be used to pass data between Python and C++. Cast the data to the correct type before using it in C++ and vice versa.
+
+2. Export the C++ class to Python using pybind11. Follow the instructions to exporting classes in the [pybind11 documentation](https://pybind11.readthedocs.io/en/stable/classes.html) or read their [first steps](https://pybind11.readthedocs.io/en/stable/basics.html) to get started.
+
+3. Compile the C++ library including pybind11. For example, for the solverdummy, the command is
+
+    ```bash
+    c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) micro_cpp_dummy.cpp -o micro_dummy$(python3-config --extension-suffix)
+    ```
+
+    This will create a shared library `micro_dummy.so` which can be directly imported in Python.
+    For more information on compiling C++ libraries, see the [pybind11 documentation](https://pybind11.readthedocs.io/en/stable/compiling.html).
+
+## Next step
+
+After restructuring your micro simulation code into a Python-importable class structure, [configure the Micro Manager](tooling-micro-manager-configuration.html).
diff --git a/docs/running.md b/docs/running.md
new file mode 100644
index 00000000..a1ba74ca
--- /dev/null
+++ b/docs/running.md
@@ -0,0 +1,30 @@
+---
+title: Run the Micro Manager
+permalink: tooling-micro-manager-running.html
+keywords: tooling, macro-micro, two-scale
+summary: Run the Micro Manager from the terminal with a configuration file as input argument or from a Python script.
+---
+
+The Micro Manager is run directly from the terminal by providing the path to the configuration file as an input argument in the following way
+
+```bash
+micro_manager micro-manager-config.json
+```
+
+Alternatively the Manager can also be run by creating a Python script which imports the Micro Manager package and calls its run function. For example a run script `run-micro-manager.py` would look like
+
+```python
+from micro_manager import MicroManager
+
+manager = MicroManager("micro-manager-config.json")
+
+manager.initialize()
+
+manager.solve()
+```
+
+The Micro Manager can also be run in parallel, using the same script as stated above
+
+```bash
+mpirun -n <number-of-procs> python3 run-micro-manager.py
+```
diff --git a/examples/macro-micro-dummy/.gitignore b/examples/.gitignore
similarity index 100%
rename from examples/macro-micro-dummy/.gitignore
rename to examples/.gitignore
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..98af528f
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,53 @@
+# Solverdummies
+
+The `solverdummies` are minimal working examples for using the preCICE Micro Manager with different languages. At the moment, there are examples for Python, and C++. They can be coupled with any other solver, for example the `macro-dummy.py` in this directory.
+
+## Python
+
+To run the Python solverdummies, run the following commands in the `examples/` directory in **two different terminals**:
+
+```bash
+python macro-dummy.py
+python python-dummy/run_micro_manager.py
+```
+
+Note that running `micro_manager micro-manager-config.json` from the terminal will not work, as the path in the configuration file is relative to the current working directory. See [#36](https://github.com/precice/micro-manager/issues/36) for more information.
+
+## C++
+
+The C++ solverdummies have to be compiled first using [`pybind11`](https://pybind11.readthedocs.io/en/stable/index.html). To do so, install `pybind11` using `pip`:
+
+```bash
+pip install pybind11
+```
+
+Then, run the following commands in the `cpp-dummy` directory:
+
+```bash
+c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) micro_cpp_dummy.cpp -o micro_dummy$(python3-config --extension-suffix)
+```
+
+<details>
+<summary>Explanation</summary>
+
+The command above compiles the C++ solverdummy and creates a shared library that can be imported from python using `pybind11`.
+
+- The `$(python3 -m pybind11 --includes)` part is necessary to include the correct header files for `pybind11`.
+- The `$(python3-config --extension-suffix)` part is necessary to create the correct file extension for the shared library. For more information, see the [pybind11 documentation](https://pybind11.readthedocs.io/en/stable/compiling.html#building-manually).
+- If you have multiple versions of Python installed, you might have to replace `python3-config` with `python3.8-config` or similar.
+
+</details>
+
+Then, run the following commands in the `examples/` directory, in **two different terminals**:
+
+```bash
+python macro_dummy.py
+python cpp-dummy/run_micro_manager.py
+```
+
+When changing the C++ solverdummy to your own solver, make sure to change the `PYBIND11_MODULE` in `micro_cpp_dummy.cpp` to the name that you want to compile to.
+For example, if you want to import the module as `my_solver`, change the line to `PYBIND11_MODULE(my_solver, m) {`. Then, change the `micro_file_name` in `micro-manager-config.json` to `my_solver`.
+
+### Adaptivity
+
+For the case of adaptivity, the deepcopy function also has to be implemented for the C++ class. An example is provided in the `cpp-dummy` directory.
diff --git a/examples/clean-example.sh b/examples/clean-example.sh
new file mode 100755
index 00000000..aa44870d
--- /dev/null
+++ b/examples/clean-example.sh
@@ -0,0 +1,5 @@
+rm -fv *.log
+rm -r -fv precice-run/
+rm -fv *-events.json
+rm -fv cpp-dummy/micro-manager.log
+rm -fv cpp-dummy/micro_dummy.cpython-310-x86_64-linux-gnu.so
diff --git a/examples/cpp-dummy/.gitignore b/examples/cpp-dummy/.gitignore
new file mode 100644
index 00000000..9604e78e
--- /dev/null
+++ b/examples/cpp-dummy/.gitignore
@@ -0,0 +1,11 @@
+# preCICE related files
+*.log
+*events.json
+precice-run/
+
+# PyCharm related files
+.idea
+__pycache__
+
+# Compiled files
+*.so
\ No newline at end of file
diff --git a/examples/cpp-dummy/micro_cpp_dummy.cpp b/examples/cpp-dummy/micro_cpp_dummy.cpp
new file mode 100644
index 00000000..78b473ba
--- /dev/null
+++ b/examples/cpp-dummy/micro_cpp_dummy.cpp
@@ -0,0 +1,97 @@
+// Micro simulation
+// In this file we solve a dummy micro problem which is controlled by the Micro Manager
+// This dummy is written in C++ and is controllable via Python using pybind11
+//
+// Compile your pybind-11 wrapped code with:
+//
+// c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) micro_cpp_dummy.cpp -o micro_dummy$(python3-config --extension-suffix)
+//
+// To check if python is able to import it, run:
+// python3 -c "import micro_dummy; micro_dummy.MicroSimulation(1)"
+// from the same directory
+
+#include "micro_cpp_dummy.hpp"
+
+// Constructor
+MicroSimulation::MicroSimulation() : _micro_scalar_data(0), _state(0) {}
+
+// Initialize
+void MicroSimulation::initialize()
+{
+    std::cout << "Initialize micro problem\n";
+    _micro_scalar_data = 0;
+    _micro_vector_data.clear();
+    _state = 0;
+}
+
+// Solve
+py::dict MicroSimulation::solve(py::dict macro_data, double dt)
+{
+    //! Code below shows how to convert input macro data and use it in your C++ solver
+
+    // Create a double from macro_data["micro_scalar_data"], which is a Python float
+    double macro_scalar_data = macro_data["macro-scalar-data"].cast<double>();
+
+    // Create a pybind style Numpy array from macro_write_data["micro_vector_data"], which is a Numpy array
+    py::array_t<double> macro_vector_data = macro_data["macro-vector-data"].cast<py::array_t<double>>();
+    _micro_vector_data = std::vector<double>(macro_vector_data.data(), macro_vector_data.data() + macro_vector_data.size()); // convert numpy array to std::vector.
+
+    // Change data
+    _micro_scalar_data = macro_scalar_data + 1.;
+    for (uint i = 0; i < _micro_vector_data.size(); i++)
+    {
+        _micro_vector_data[i] += 1.;
+    }
+
+    // Convert data to a py::dict again to send it back to the Micro Manager
+    py::dict micro_write_data;
+
+    // add micro_scalar_data and micro_vector_data to micro_write_data
+    micro_write_data["micro-scalar-data"] = _micro_scalar_data;
+    micro_write_data["micro-vector-data"] = _micro_vector_data; // numpy array is automatically converted to python list
+
+    return micro_write_data;
+}
+
+// This function needs to set the complete state of a micro simulation
+void MicroSimulation::set_state(py::list state)
+{
+    _micro_scalar_data = state[0].cast<double>();
+    _state = state[1].cast<double>();
+}
+
+// This function needs to return variables which can fully define the state of a micro simulation
+py::list MicroSimulation::get_state() const
+{
+    std::vector<double> state{_micro_scalar_data, _state};
+    py::list state_python = py::cast(state);
+    return state_python;
+}
+
+PYBIND11_MODULE(micro_dummy, m) {
+    // optional docstring
+    m.doc() = "pybind11 micro dummy plugin";
+
+    py::class_<MicroSimulation>(m, "MicroSimulation")
+        .def(py::init())
+        .def("initialize", &MicroSimulation::initialize)
+        .def("solve", &MicroSimulation::solve)
+        .def("get_state", &MicroSimulation::get_state)
+        .def("set_state", &MicroSimulation::set_state)
+        .def(py::pickle(
+            [](const MicroSimulation &ms) { // __getstate__
+                return ms.get_state();
+            },
+            [](py::list t) { // __setstate__
+                if (t.size() != 2)
+                    throw std::runtime_error("Invalid state!");
+
+                /* Create a new C++ instance */
+                MicroSimulation ms;
+
+                ms.set_state(t);
+
+                return ms;
+            }
+        ));
+}
diff --git a/examples/cpp-dummy/micro_cpp_dummy.hpp b/examples/cpp-dummy/micro_cpp_dummy.hpp
new file mode 100644
index 00000000..4e9828d7
--- /dev/null
+++ b/examples/cpp-dummy/micro_cpp_dummy.hpp
@@ -0,0 +1,30 @@
+// This is the header file for the micro simulation class.
+// It is included in the micro_cpp_dummy.cpp file and the micro_cpp_dummy.cpp file is compiled with pybind11 to create a python module.
+// The python module is then imported in the Micro Manager.
+
+#pragma once
+#include <iostream>
+#include <vector>
+#include <pybind11/pybind11.h>
+#include <pybind11/numpy.h> // numpy arrays
+#include <pybind11/stl.h>   // std::vector conversion
+
+namespace py = pybind11;
+
+class MicroSimulation
+{
+public:
+    MicroSimulation();
+    void initialize();
+    // solve takes a python dict data, and the timestep dt as inputs, and returns a python dict
+    py::dict solve(py::dict macro_write_data, double dt);
+    MicroSimulation __deepcopy__(py::dict memo);
+
+    void set_state(py::list state);
+    py::list get_state() const;
+
+private:
+    double _micro_scalar_data;
+    std::vector<double> _micro_vector_data;
+    double _state;
+};
diff --git a/examples/cpp-dummy/run_micro_manager.py b/examples/cpp-dummy/run_micro_manager.py
new file mode 100644
index 00000000..e6afd394
--- /dev/null
+++ b/examples/cpp-dummy/run_micro_manager.py
@@ -0,0 +1,16 @@
+"""
+Script to run the Micro Manager
+"""
+
+from micro_manager import MicroManager
+from argparse import ArgumentParser
+
+parser = ArgumentParser()
+parser.add_argument("--config", required=True, help="Path to the micro manager configuration file")
+args = parser.parse_args()
+
+manager = MicroManager(args.config)
+
+manager.initialize()
+
+manager.solve()
diff --git a/examples/macro-micro-dummy/README.md b/examples/macro-micro-dummy/README.md
deleted file mode 100644
index eb1ae458..00000000
--- a/examples/macro-micro-dummy/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-...
diff --git a/examples/macro-micro-dummy/clean-example.sh b/examples/macro-micro-dummy/clean-example.sh
deleted file mode 100755
index 44fea7d9..00000000
--- a/examples/macro-micro-dummy/clean-example.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-rm -fv *.log
-rm -r -fv precice-run/
-rm -fv *-events.json
diff --git a/examples/macro-micro-dummy/run_micro_manager.py b/examples/macro-micro-dummy/run_micro_manager.py
deleted file mode 100644
index bfb69ab5..00000000
--- a/examples/macro-micro-dummy/run_micro_manager.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-Script to run the Micro Manager
-"""
-
-from micro_manager import MicroManager
-
-manager = MicroManager("./micro-manager-config.json")
-
-manager.initialize()
-
-manager.solve()
diff --git a/examples/macro-micro-dummy/macro_dummy.py b/examples/macro_dummy.py
similarity index 92%
rename from examples/macro-micro-dummy/macro_dummy.py
rename to examples/macro_dummy.py
index 765ff7e3..82917f2c 100644
--- a/examples/macro-micro-dummy/macro_dummy.py
+++ b/examples/macro_dummy.py
@@ -73,7 +73,6 @@ def main():
             n_checkpoint = n
             interface.mark_action_fulfilled(precice.action_write_iteration_checkpoint())
 
-        # Read porosity and apply
         for name, dim in read_data_names.items():
             if dim == 0:
                 read_scalar_data = interface.read_block_scalar_data(read_data_ids[name], vertex_ids)
@@ -84,6 +83,10 @@ def main():
         for i in range(nv):
             for d in range(interface.get_dimensions()):
                 write_vector_data[i, d] = read_vector_data[i, d]
+                if t > 1:  # to trigger adaptivity after some time
+                    # ensure that the data is different from the previous time step
+                    # previously inactive microsimulations will be activated
+                    write_vector_data[i, d] += np.random.randint(0, 10)
 
         for name, dim in write_data_names.items():
             if dim == 0:
diff --git a/examples/micro-manager-adaptivity-config.json b/examples/micro-manager-adaptivity-config.json
new file mode 100644
index 00000000..76a7fd78
--- /dev/null
+++ b/examples/micro-manager-adaptivity-config.json
@@ -0,0 +1,23 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "./precice-config-adaptivity.xml",
+        "macro_mesh_name": "macro-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0.0, 25.0, 0.0, 25.0, 0.0, 25.0],
+        "adaptivity": {
+            "type": "local",
+            "data": ["macro-scalar-data", "macro-vector-data"],
+            "history_param": 0.5,
+            "coarsening_constant": 0.3,
+            "refining_constant": 0.4,
+            "every_implicit_iteration": "True"
+        }
+    },
+    "diagnostics": {
+      "output_micro_sim_solve_time": "True"
+    }
+}
diff --git a/examples/macro-micro-dummy/micro-manager-config.json b/examples/micro-manager-config.json
similarity index 89%
rename from examples/macro-micro-dummy/micro-manager-config.json
rename to examples/micro-manager-config.json
index a2709c4d..4be15a61 100644
--- a/examples/macro-micro-dummy/micro-manager-config.json
+++ b/examples/micro-manager-config.json
@@ -1,7 +1,7 @@
 {
     "micro_file_name": "micro_dummy",
     "coupling_params": {
-        "config_file_name": "precice-config.xml",
+        "config_file_name": "./precice-config.xml",
         "macro_mesh_name": "macro-mesh",
         "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
         "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
diff --git a/examples/precice-config-adaptivity.xml b/examples/precice-config-adaptivity.xml
new file mode 100644
index 00000000..a3764b26
--- /dev/null
+++ b/examples/precice-config-adaptivity.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+
+<precice-configuration>
+
+    <log>
+	<sink type="stream" output="stdout"  filter= "%Severity% > debug" enabled="true" />
+    </log>
+  
+  <solver-interface experimental="true" dimensions="3">
+    
+    <data:scalar name="macro-scalar-data"/>
+    <data:vector name="macro-vector-data"/>
+    <data:scalar name="micro-scalar-data"/>
+    <data:vector name="micro-vector-data"/>
+    <data:scalar name="micro_sim_time"/>
+    <data:scalar name="active_state"/>
+    <data:scalar name="active_steps"/>
+
+    <mesh name="macro-mesh">
+       <use-data name="macro-scalar-data"/>
+       <use-data name="macro-vector-data"/>
+       <use-data name="micro-scalar-data"/>
+       <use-data name="micro-vector-data"/>
+       <use-data name="micro_sim_time"/>
+       <use-data name="active_state"/>
+       <use-data name="active_steps"/>
+    </mesh>
+
+    <participant name="Macro-dummy">
+      <use-mesh name="macro-mesh" provide="yes"/>
+      <read-data name="micro-scalar-data" mesh="macro-mesh"/>
+      <read-data name="micro-vector-data" mesh="macro-mesh"/>
+      <write-data name="macro-scalar-data" mesh="macro-mesh"/>
+      <write-data name="macro-vector-data" mesh="macro-mesh"/>
+    </participant>
+
+    <participant name="Micro-Manager">
+      <use-mesh name="macro-mesh" from="Macro-dummy" direct-access="true" safety-factor="0.0"/>
+      <read-data name="macro-scalar-data" mesh="macro-mesh"/>
+      <read-data name="macro-vector-data" mesh="macro-mesh"/>
+      <write-data name="micro-scalar-data" mesh="macro-mesh"/>
+      <write-data name="micro-vector-data" mesh="macro-mesh"/>
+      <write-data name="micro_sim_time" mesh="macro-mesh"/>
+      <write-data name="active_state" mesh="macro-mesh"/>
+      <write-data name="active_steps" mesh="macro-mesh"/>
+    </participant>
+
+    <m2n:sockets from="Micro-Manager" to="Macro-dummy"/>
+
+    <coupling-scheme:serial-implicit>
+       <participants first="Macro-dummy" second="Micro-Manager"/>
+       <max-time value="3.0"/>
+       <time-window-size value="1.0"/>
+       <max-iterations value="5"/>
+       <exchange data="micro-scalar-data" mesh="macro-mesh" from="Micro-Manager" to="Macro-dummy" initialize="yes"/>
+       <exchange data="micro-vector-data" mesh="macro-mesh" from="Micro-Manager" to="Macro-dummy" initialize="yes"/>
+       <exchange data="macro-scalar-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager"/>
+       <exchange data="macro-vector-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager"/>
+       <relative-convergence-measure limit="1e-5" data="macro-scalar-data" mesh="macro-mesh"/>
+       <relative-convergence-measure limit="1e-5" data="micro-scalar-data" mesh="macro-mesh"/>
+    </coupling-scheme:serial-implicit>
+
+  </solver-interface>
+</precice-configuration>
diff --git a/examples/macro-micro-dummy/precice-config.xml b/examples/precice-config.xml
similarity index 70%
rename from examples/macro-micro-dummy/precice-config.xml
rename to examples/precice-config.xml
index 3362ffc6..9c487c7c 100644
--- a/examples/macro-micro-dummy/precice-config.xml
+++ b/examples/precice-config.xml
@@ -41,31 +41,18 @@
 
     <m2n:sockets from="Micro-Manager" to="Macro-dummy"/>
 
-    <coupling-scheme:parallel-implicit>
+    <coupling-scheme:serial-implicit>
        <participants first="Macro-dummy" second="Micro-Manager"/>
-       <max-time value="10.0"/>
+       <max-time value="3.0"/>
        <time-window-size value="1.0"/>
-       <max-iterations value="30"/>
+       <max-iterations value="5"/>
        <exchange data="micro-scalar-data" mesh="macro-mesh" from="Micro-Manager" to="Macro-dummy" initialize="yes"/>
        <exchange data="micro-vector-data" mesh="macro-mesh" from="Micro-Manager" to="Macro-dummy" initialize="yes"/>
-       <exchange data="macro-scalar-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager" initialize="yes"/>
-       <exchange data="macro-vector-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager" initialize="yes"/>
+       <exchange data="macro-scalar-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager"/>
+       <exchange data="macro-vector-data" mesh="macro-mesh" from="Macro-dummy" to="Micro-Manager"/>
        <relative-convergence-measure limit="1e-5" data="macro-scalar-data" mesh="macro-mesh"/>
-       <relative-convergence-measure limit="1e-5" data="macro-vector-data" mesh="macro-mesh"/>
        <relative-convergence-measure limit="1e-5" data="micro-scalar-data" mesh="macro-mesh"/>
-       <relative-convergence-measure limit="1e-5" data="micro-vector-data" mesh="macro-mesh"/>
-       <acceleration:IQN-ILS>
-          <data name="micro-scalar-data" mesh="macro-mesh"/>
-          <data name="micro-vector-data" mesh="macro-mesh"/>
-          <data name="macro-scalar-data" mesh="macro-mesh"/>
-          <data name="macro-vector-data" mesh="macro-mesh"/>
-          <preconditioner type="residual-sum"/>
-          <initial-relaxation value="0.9"/>
-          <max-used-iterations value="40"/>
-          <time-windows-reused value="20"/>
-          <filter type="QR2" limit="1e-2"/>
-       </acceleration:IQN-ILS>
-    </coupling-scheme:parallel-implicit>
+    </coupling-scheme:serial-implicit>
 
   </solver-interface>
 </precice-configuration>
diff --git a/examples/python-dummy/.gitignore b/examples/python-dummy/.gitignore
new file mode 100644
index 00000000..cc479d19
--- /dev/null
+++ b/examples/python-dummy/.gitignore
@@ -0,0 +1,8 @@
+# preCICE related files
+*.log
+*events.json
+precice-run/
+
+# PyCharm related files
+.idea
+__pycache__
diff --git a/examples/macro-micro-dummy/micro_dummy.py b/examples/python-dummy/micro_dummy.py
similarity index 57%
rename from examples/macro-micro-dummy/micro_dummy.py
rename to examples/python-dummy/micro_dummy.py
index 08fd56b9..783d36d8 100644
--- a/examples/macro-micro-dummy/micro_dummy.py
+++ b/examples/python-dummy/micro_dummy.py
@@ -6,37 +6,32 @@
 
 class MicroSimulation:
 
-    def __init__(self, sim_id):
+    def __init__(self):
         """
         Constructor of MicroSimulation class.
         """
-        self._sim_id = sim_id
         self._dims = 3
         self._micro_scalar_data = None
         self._micro_vector_data = None
-        self._checkpoint = None
+        self._state = None
 
     def initialize(self):
-        print("Initialize micro problem ({})".format(self._sim_id))
         self._micro_scalar_data = 0
         self._micro_vector_data = []
-        self._checkpoint = 0
+        self._state = 0
 
     def solve(self, macro_data, dt):
-        print("Solve timestep of micro problem ({})".format(self._sim_id))
         assert dt != 0
         self._micro_vector_data = []
-        self._micro_scalar_data = macro_data["macro-scalar-data"]
+        self._micro_scalar_data = macro_data["macro-scalar-data"] + 1
         for d in range(self._dims):
-            self._micro_vector_data.append(macro_data["macro-vector-data"][d])
+            self._micro_vector_data.append(macro_data["macro-vector-data"][d] + 1)
 
         return {"micro-scalar-data": self._micro_scalar_data.copy(),
                 "micro-vector-data": self._micro_vector_data.copy()}
 
-    def save_checkpoint(self):
-        print("Saving state of micro problem ({})".format(self._sim_id))
-        self._checkpoint = self._micro_scalar_data
+    def set_state(self, state):
+        self._state = state
 
-    def reload_checkpoint(self):
-        print("Reverting to old state of micro problem ({})".format(self._sim_id))
-        self._micro_scalar_data = self._checkpoint
+    def get_state(self):
+        return self._state
diff --git a/examples/python-dummy/run_micro_manager.py b/examples/python-dummy/run_micro_manager.py
new file mode 100644
index 00000000..a3d0dd6c
--- /dev/null
+++ b/examples/python-dummy/run_micro_manager.py
@@ -0,0 +1,16 @@
+"""
+Script to run the Micro Manager
+"""
+
+from micro_manager import MicroManager
+from argparse import ArgumentParser
+
+parser = ArgumentParser()
+parser.add_argument("--config", help="Path to the micro manager configuration file")
+args = parser.parse_args()
+
+manager = MicroManager(args.config)
+
+manager.initialize()
+
+manager.solve()
diff --git a/micro_manager/adaptivity/__init__.py b/micro_manager/adaptivity/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/micro_manager/adaptivity/adaptivity.py b/micro_manager/adaptivity/adaptivity.py
new file mode 100644
index 00000000..44e2e2ee
--- /dev/null
+++ b/micro_manager/adaptivity/adaptivity.py
@@ -0,0 +1,297 @@
+"""
+Functionality for adaptive initialization and control of micro simulations
+"""
+import sys
+import numpy as np
+from math import exp
+from typing import Callable
+
+
+class AdaptivityCalculator:
+    def __init__(self, configurator, logger) -> None:
+        """
+        Class constructor.
+
+        Parameters
+        ----------
+        configurator : object of class Config
+            Object which has getter functions to get parameters defined in the configuration file.
+        logger : Logger defined from the standard package logging
+        """
+        self._refine_const = configurator.get_adaptivity_refining_const()
+        self._coarse_const = configurator.get_adaptivity_coarsening_const()
+        self._hist_param = configurator.get_adaptivity_hist_param()
+        self._adaptivity_data_names = configurator.get_data_for_adaptivity()
+        self._adaptivity_type = configurator.get_adaptivity_type()
+
+        self._logger = logger
+
+        self._coarse_tol = 0.0
+        self._ref_tol = 0.0
+
+        self._similarity_measure = self._get_similarity_measure(configurator.get_adaptivity_similarity_measure())
+
+    def _get_similarity_dists(self, dt: float, similarity_dists: np.ndarray, data: dict) -> np.ndarray:
+        """
+        Calculate metric which determines if two micro simulations are similar enough to have one of them deactivated.
+
+        Parameters
+        ----------
+        dt : float
+            Current time step
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair
+        data : dict
+            Data to be used in similarity distance calculation
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            Updated 2D array having similarity distances between each micro simulation pair
+        """
+        _similarity_dists = np.copy(similarity_dists)
+
+        data_diff = np.zeros_like(_similarity_dists)
+        for name in self._adaptivity_data_names:
+            data_vals = data[name]
+            if data_vals.ndim == 1:
+                # If the adaptivity-data is a scalar for each simulation,
+                # expand the dimension to make it a 2D array to unify the calculation.
+                # The axis is later reduced with a norm.
+                data_vals = np.expand_dims(data_vals, axis=1)
+
+            data_diff += self._similarity_measure(data_vals)
+
+        return exp(-self._hist_param * dt) * _similarity_dists + dt * data_diff
+
+    def _update_active_sims(
+            self,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray) -> np.ndarray:
+        """
+        Update set of active micro simulations. Active micro simulations are compared to each other
+        and if found similar, one of them is deactivated.
+
+        Parameters
+        ----------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation
+
+        Returns
+        -------
+        _is_sim_active : numpy array
+            Updated 1D array having state (active or inactive) of each micro simulation
+        """
+        self._coarse_tol = self._coarse_const * self._refine_const * np.amax(similarity_dists)
+
+        _is_sim_active = np.copy(is_sim_active)  # Input is_sim_active is not longer used after this point
+
+        # Update the set of active micro sims
+        for i in range(_is_sim_active.size):
+            if _is_sim_active[i]:  # if sim is active
+                if self._check_for_deactivation(i, similarity_dists, _is_sim_active):
+                    _is_sim_active[i] = False
+
+        return _is_sim_active
+
+    def _associate_inactive_to_active(
+            self,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray,
+            sim_is_associated_to: np.ndarray) -> np.ndarray:
+        """
+        Associate inactive micro simulations to most similar active micro simulation.
+
+        Parameters
+        ----------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation
+        sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+
+        Returns
+        -------
+        _sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+        """
+        active_ids = np.where(is_sim_active)[0]
+        inactive_ids = np.where(is_sim_active == False)[0]
+
+        _sim_is_associated_to = np.copy(sim_is_associated_to)
+
+        # Associate inactive micro sims to active micro sims
+        for inactive_id in inactive_ids:
+            dist_min = sys.float_info.max
+            for active_id in active_ids:
+                # Find most similar active sim for every inactive sim
+                if similarity_dists[inactive_id, active_id] < dist_min:
+                    associated_active_id = active_id
+                    dist_min = similarity_dists[inactive_id, active_id]
+
+            _sim_is_associated_to[inactive_id] = associated_active_id
+
+        return _sim_is_associated_to
+
+    def _check_for_activation(
+            self,
+            inactive_id: int,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray) -> bool:
+        """
+        Check if an inactive simulation needs to be activated.
+
+        Parameters
+        ----------
+        inactive_id : int
+            ID of inactive simulation which is checked for activation.
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair.
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation.
+
+        Return
+        ------
+        tag : bool
+            True if the inactive simulation needs to be activated, False otherwise.
+        """
+        active_sim_ids = np.where(is_sim_active)[0]
+
+        dists = similarity_dists[inactive_id, active_sim_ids]
+
+        # If inactive sim is not similar to any active sim, activate it
+        return min(dists) > self._ref_tol
+
+    def _check_for_deactivation(
+            self,
+            active_id: int,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray) -> bool:
+        """
+        Check if an active simulation needs to be deactivated.
+
+        Parameters
+        ----------
+        active_id : int
+            ID of active simulation which is checked for deactivation.
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair.
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation.
+
+        Return
+        ------
+        tag : bool
+            True if the active simulation needs to be deactivated, False otherwise.
+        """
+        active_sim_ids = np.where(is_sim_active)[0]
+
+        for active_id_2 in active_sim_ids:
+            if active_id != active_id_2:  # don't compare active sim to itself
+                # If active sim is similar to another active sim, deactivate it
+                if similarity_dists[active_id, active_id_2] < self._coarse_tol:
+                    return True
+        return False
+
+    def _get_similarity_measure(self, similarity_measure: str) -> Callable[[np.ndarray], np.ndarray]:
+        """
+        Get similarity measure to be used for similarity calculation
+
+        Parameters
+        ----------
+        similarity_measure : str
+            String specifying the similarity measure to be used
+
+        Returns
+        -------
+        similarity_measure : function
+            Function to be used for similarity calculation. Takes data as input and returns similarity measure
+        """
+        if similarity_measure == 'L1':
+            return self._l1
+        elif similarity_measure == 'L2':
+            return self._l2
+        elif similarity_measure == 'L1rel':
+            return self._l1rel
+        elif similarity_measure == 'L2rel':
+            return self._l2rel
+        else:
+            raise ValueError(
+                'Similarity measure not supported. Currently supported similarity measures are "L1", "L2", "L1rel", "L2rel".')
+
+    def _l1(self, data: np.ndarray) -> np.ndarray:
+        """
+        Calculate L1 norm of data
+
+        Parameters
+        ----------
+        data : numpy array
+            Data to be used in similarity distance calculation
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            Updated 2D array having similarity distances between each micro simulation pair
+        """
+        return np.linalg.norm(data[np.newaxis, :] - data[:, np.newaxis], ord=1, axis=-1)
+
+    def _l2(self, data: np.ndarray) -> np.ndarray:
+        """
+        Calculate L2 norm of data
+
+        Parameters
+        ----------
+        data : numpy array
+            Data to be used in similarity distance calculation
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            Updated 2D array having similarity distances between each micro simulation pair
+        """
+        return np.linalg.norm(data[np.newaxis, :] - data[:, np.newaxis], ord=2, axis=-1)
+
+    def _l1rel(self, data: np.ndarray) -> np.ndarray:
+        """
+        Calculate L1 norm of relative difference of data.
+        The relative difference is calculated by dividing the difference of two data points by the maximum of the two data points.
+
+        Parameters
+        ----------
+        data : numpy array
+            Data to be used in similarity distance calculation
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            Updated 2D array having similarity distances between each micro simulation pair
+        """
+        pointwise_diff = data[np.newaxis, :] - data[:, np.newaxis]
+        # divide by data to get relative difference
+        # divide i,j by max(data[i],data[j]) to get relative difference
+        relative = np.nan_to_num((pointwise_diff / np.maximum(data[np.newaxis, :], data[:, np.newaxis])))
+        return np.linalg.norm(relative, ord=1, axis=-1)
+
+    def _l2rel(self, data: np.ndarray) -> np.ndarray:
+        """
+        Calculate L2 norm of relative difference of data.
+        The relative difference is calculated by dividing the difference of two data points by the maximum of the two data points.
+
+        Parameters
+        ----------
+        data : numpy array
+            Data to be used in similarity distance calculation
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            Updated 2D array having similarity distances between each micro simulation pair
+        """
+        pointwise_diff = data[np.newaxis, :] - data[:, np.newaxis]
+        # divide by data to get relative difference
+        # divide i,j by max(data[i],data[j]) to get relative difference
+        relative = np.nan_to_num((pointwise_diff / np.maximum(data[np.newaxis, :], data[:, np.newaxis])))
+        return np.linalg.norm(relative, ord=2, axis=-1)
diff --git a/micro_manager/adaptivity/global_adaptivity.py b/micro_manager/adaptivity/global_adaptivity.py
new file mode 100644
index 00000000..6164efdf
--- /dev/null
+++ b/micro_manager/adaptivity/global_adaptivity.py
@@ -0,0 +1,322 @@
+"""
+Class GlobalAdaptivityCalculator provides methods to adaptively control of micro simulations
+in a global way. If the Micro Manager is run in parallel, an all-to-all comparison of simulations
+on each rank is done.
+
+Note: All ID variables used in the methods of this class are global IDs, unless they have *local* in their name.
+"""
+import numpy as np
+import hashlib
+from copy import deepcopy
+from mpi4py import MPI
+from typing import Dict
+from .adaptivity import AdaptivityCalculator
+
+
+class GlobalAdaptivityCalculator(AdaptivityCalculator):
+    def __init__(
+            self,
+            configurator,
+            logger,
+            is_sim_on_this_rank: list,
+            rank_of_sim: np.ndarray,
+            global_ids: list,
+            rank: int,
+            comm) -> None:
+        """
+        Class constructor.
+
+        Parameters
+        ----------
+        configurator : object of class Config
+            Object which has getter functions to get parameters defined in the configuration file.
+        logger : object of logging
+            Logger defined from the standard package logging
+        is_sim_on_this_rank : list
+            List of booleans. True if simulation is on this rank, False otherwise.
+        rank_of_sim : numpy array
+            1D array consisting of rank on which the simulation lives.
+        global_ids : list
+            List of global IDs of simulations living on this rank.
+        rank : int
+            MPI rank.
+        comm : MPI.COMM_WORLD
+            Global communicator of MPI.
+        """
+        super().__init__(configurator, logger)
+        self._is_sim_on_this_rank = is_sim_on_this_rank
+        self._rank_of_sim = rank_of_sim
+        self._global_ids = global_ids
+        self._comm = comm
+        self._rank = rank
+
+    def compute_adaptivity(
+            self,
+            dt: float,
+            micro_sims: list,
+            similarity_dists_nm1: np.ndarray,
+            is_sim_active_nm1: np.ndarray,
+            sim_is_associated_to_nm1: np.ndarray,
+            data_for_adaptivity: dict) -> tuple:
+        """
+        Compute adaptivity globally based on similarity distances and micro simulation states
+
+        Parameters
+        ----------
+        dt : float
+            Current time step of the macro-micro coupled problem
+        micro_sims : list
+            List of objects of class MicroProblem, which are the micro simulations
+        similarity_dists_nm1 : numpy array
+            2D array having similarity distances between each micro simulation pair
+        is_sim_active_nm1 : numpy array
+            1D array having state (active or inactive) of each micro simulation on this rank
+        sim_is_associated_to_nm1 : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+        data_for_adaptivity : dict
+            Dictionary with keys as names of data to be used in the similarity calculation, and values as the respective data for the micro simulations
+
+        Results
+        -------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation
+        """
+        # Gather adaptivity data from all ranks
+        global_data_for_adaptivity = dict()
+        for name in self._adaptivity_data_names.keys():
+            data_as_list = self._comm.allgather(data_for_adaptivity[name])
+            global_data_for_adaptivity[name] = np.concatenate((data_as_list[:]), axis=0)
+
+        similarity_dists = self._get_similarity_dists(dt, similarity_dists_nm1, global_data_for_adaptivity)
+
+        is_sim_active = self._update_active_sims(similarity_dists, is_sim_active_nm1)
+
+        is_sim_active, sim_is_associated_to = self._update_inactive_sims(
+            similarity_dists, is_sim_active_nm1, sim_is_associated_to_nm1, micro_sims)
+
+        sim_is_associated_to = self._associate_inactive_to_active(
+            similarity_dists, is_sim_active, sim_is_associated_to)
+
+        self._logger.info(
+            "{} active simulations, {} inactive simulations".format(
+                np.count_nonzero(
+                    is_sim_active[self._global_ids[0]:self._global_ids[-1] + 1]),
+                np.count_nonzero(
+                    is_sim_active[self._global_ids[0]:self._global_ids[-1] + 1] == False)))
+
+        return similarity_dists, is_sim_active, sim_is_associated_to
+
+    def communicate_micro_output(
+            self,
+            is_sim_active: np.ndarray,
+            sim_is_associated_to: np.ndarray,
+            micro_output: list) -> None:
+        """
+        Communicate micro output from active simulation to their associated inactive simulations.
+        Process to process (p2p) communication is done.
+
+        Parameters
+        ----------
+        micro_sims : list
+            List of objects of class MicroProblem, which are the micro simulations
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation on this rank
+        sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have -2
+        micro_output : list
+            List of dicts having individual output of each simulation. Only the active simulation outputs are entered.
+        """
+        inactive_local_ids = np.where(is_sim_active[self._global_ids[0]:self._global_ids[-1] + 1] == False)[0]
+
+        local_sim_is_associated_to = sim_is_associated_to[self._global_ids[0]:self._global_ids[-1] + 1]
+
+        # Keys are global IDs of active simulations associated to inactive
+        # simulations on this rank. Values are global IDs of the inactive
+        # simulations.
+        active_to_inactive_map: Dict[int, list] = dict()
+
+        for i in inactive_local_ids:
+            assoc_active_id = local_sim_is_associated_to[i]
+            # Gather global IDs of associated active simulations not on this rank
+            if not self._is_sim_on_this_rank[assoc_active_id]:
+                if assoc_active_id in active_to_inactive_map:
+                    active_to_inactive_map[assoc_active_id].append(i)
+                else:
+                    active_to_inactive_map[assoc_active_id] = [i]
+            else:  # If associated active simulation is on this rank, copy the output directly
+                micro_output[i] = deepcopy(micro_output[self._global_ids.index(assoc_active_id)])
+
+        assoc_active_ids = list(active_to_inactive_map.keys())
+
+        recv_reqs = self._p2p_comm(assoc_active_ids, micro_output)
+
+        # Add received output of active sims to inactive sims on this rank
+        for count, req in enumerate(recv_reqs):
+            output = req.wait()
+            for local_id in active_to_inactive_map[assoc_active_ids[count]]:
+                micro_output[local_id] = deepcopy(output)
+
+    def _update_inactive_sims(
+            self,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray,
+            sim_is_associated_to: np.ndarray,
+            micro_sims: list) -> tuple:
+        """
+        Update set of inactive micro simulations. Each inactive micro simulation is compared to all active ones
+        and if it is not similar to any of them, it is activated.
+
+        Parameters
+        ----------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation
+        sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+        micro_sims : list
+            List of objects of class MicroProblem, which are the micro simulations
+
+        Returns
+        -------
+        _is_sim_active : numpy array
+            Updated 1D array having state (active or inactive) of each micro simulation
+        _sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+        """
+        self._ref_tol = self._refine_const * np.amax(similarity_dists)
+
+        _is_sim_active = np.copy(is_sim_active)  # Input is_sim_active is not longer used after this point
+        _sim_is_associated_to = np.copy(sim_is_associated_to)
+        _sim_is_associated_to_updated = np.copy(sim_is_associated_to)
+
+        # Check inactive simulations for activation and collect IDs of those to be activated
+        to_be_activated_ids = []  # Global IDs to be activated
+        for i in range(_is_sim_active.size):
+            if not _is_sim_active[i]:  # if id is inactive
+                if self._check_for_activation(i, similarity_dists, _is_sim_active):
+                    _is_sim_active[i] = True
+                    _sim_is_associated_to_updated[i] = -2  # Active sim cannot have an associated sim
+                    if self._is_sim_on_this_rank[i]:
+                        to_be_activated_ids.append(i)
+
+        local_sim_is_associated_to = _sim_is_associated_to[self._global_ids[0]:self._global_ids[-1] + 1]
+
+        # Keys are global IDs of active sims not on this rank, values are lists of local and
+        # global IDs of inactive sims associated to the active sims which are on this rank
+        to_be_activated_map: Dict[int, list] = dict()
+
+        for i in to_be_activated_ids:
+            # Only handle activation of simulations on this rank -- LOCAL SCOPE HERE ON
+            if self._is_sim_on_this_rank[i]:
+                to_be_activated_local_id = self._global_ids.index(i)
+                assoc_active_id = local_sim_is_associated_to[to_be_activated_local_id]
+
+                if self._is_sim_on_this_rank[assoc_active_id]:  # Associated active simulation is on the same rank
+                    assoc_active_local_id = self._global_ids.index(assoc_active_id)
+                    micro_sims[to_be_activated_local_id].set_state(micro_sims[assoc_active_local_id].get_state())
+                else:  # Associated active simulation is not on this rank
+                    if assoc_active_id in to_be_activated_map:
+                        to_be_activated_map[assoc_active_id].append(to_be_activated_local_id)
+                    else:
+                        to_be_activated_map[assoc_active_id] = [to_be_activated_local_id]
+
+        sim_states_and_global_ids = []
+        for sim in micro_sims:
+            sim_states_and_global_ids.append((sim.get_state(), sim.get_global_id()))
+
+        recv_reqs = self._p2p_comm(list(to_be_activated_map.keys()), sim_states_and_global_ids)
+
+        # Use received micro sims to activate the required simulations
+        for req in recv_reqs:
+            state, global_id = req.wait()
+            local_ids = to_be_activated_map[global_id]
+            for local_id in local_ids:
+                micro_sims[local_id].set_state(state)
+
+        return _is_sim_active, _sim_is_associated_to_updated
+
+    def _create_tag(self, sim_id: int, src_rank: int, dest_rank: int) -> int:
+        """
+        For a given simulations ID, source rank, and destination rank, a unique tag is created.
+
+        Parameters
+        ----------
+        sim_id : int
+            Global ID of a simulation.
+        src_rank : int
+            Rank on which the simulation lives
+        dest_rank : int
+            Rank to which data of a simulation is to be sent to.
+
+        Returns
+        -------
+        tag : int
+            Unique tag.
+        """
+        send_hashtag = hashlib.sha256()
+        send_hashtag.update((str(src_rank) + str(sim_id) + str(dest_rank)).encode('utf-8'))
+        tag = int(send_hashtag.hexdigest()[:6], base=16)
+        return tag
+
+    def _p2p_comm(self, assoc_active_ids: list, data: list) -> list:
+        """
+        Handle process to process communication for a given set of associated active IDs and data.
+
+        Parameters
+        ----------
+        assoc_active_ids : list
+            Global IDs of active simulations which are not on this rank and are associated to
+            the inactive simulations on this rank.
+        data : list
+            Complete data from which parts are to be sent and received.
+
+        Returns
+        -------
+        recv_reqs : list
+            List of MPI requests of receive operations.
+        """
+        send_map_local: Dict[int, int] = dict()  # keys are global IDs, values are rank to send to
+        send_map: Dict[int, list] = dict()  # keys are global IDs of sims to send, values are ranks to send the sims to
+        recv_map: Dict[int, int] = dict()  # keys are global IDs to receive, values are ranks to receive from
+
+        for i in assoc_active_ids:
+            # Add simulation and its rank to receive map
+            recv_map[i] = self._rank_of_sim[i]
+            # Add simulation and this rank to local sending map
+            send_map_local[i] = self._rank
+
+        # Gather information about which sims to send where, from the sending perspective
+        send_map_list = self._comm.allgather(send_map_local)
+
+        for d in send_map_list:
+            for i, rank in d.items():
+                if self._is_sim_on_this_rank[i]:
+                    if i in send_map:
+                        send_map[i].append(rank)
+                    else:
+                        send_map[i] = [rank]
+
+        # Asynchronous send operations
+        send_reqs = []
+        for global_id, send_ranks in send_map.items():
+            local_id = self._global_ids.index(global_id)
+            for send_rank in send_ranks:
+                tag = self._create_tag(global_id, self._rank, send_rank)
+                req = self._comm.isend(data[local_id], dest=send_rank, tag=tag)
+                send_reqs.append(req)
+
+        # Asynchronous receive operations
+        recv_reqs = []
+        for global_id, recv_rank in recv_map.items():
+            tag = self._create_tag(global_id, recv_rank, self._rank)
+            bufsize = 1 << 30  # allocate and use a temporary 1 MiB buffer size https://github.com/mpi4py/mpi4py/issues/389
+            req = self._comm.irecv(bufsize, source=recv_rank, tag=tag)
+            recv_reqs.append(req)
+
+        # Wait for all non-blocking communication to complete
+        MPI.Request.Waitall(send_reqs)
+
+        return recv_reqs
diff --git a/micro_manager/adaptivity/local_adaptivity.py b/micro_manager/adaptivity/local_adaptivity.py
new file mode 100644
index 00000000..2f82c877
--- /dev/null
+++ b/micro_manager/adaptivity/local_adaptivity.py
@@ -0,0 +1,117 @@
+"""
+Class LocalAdaptivityCalculator provides methods to adaptively control of micro simulations
+in a local way. If the Micro Manager is run in parallel, simulations on one rank are compared to
+each other. A global comparison is not done.
+"""
+import numpy as np
+from .adaptivity import AdaptivityCalculator
+
+
+class LocalAdaptivityCalculator(AdaptivityCalculator):
+    def __init__(self, configurator, logger) -> None:
+        """
+        Class constructor.
+
+        Parameters
+        ----------
+        configurator : object of class Config
+            Object which has getter functions to get parameters defined in the configuration file.
+        logger : object of logging
+            Logger defined from the standard package logging
+        """
+        super().__init__(configurator, logger)
+
+    def compute_adaptivity(
+            self,
+            dt,
+            micro_sims,
+            similarity_dists_nm1: np.ndarray,
+            is_sim_active_nm1: np.ndarray,
+            sim_is_associated_to_nm1: np.ndarray,
+            data_for_adaptivity: dict):
+        """
+        Compute adaptivity locally (within a rank).
+
+        Parameters
+        ----------
+        dt : float
+            Current time step
+        micro_sims : list
+            List containing simulation objects
+        similarity_dists_nm1 : numpy array
+            2D array having similarity distances between each micro simulation pair.
+        is_sim_active_nm1 : numpy array
+            1D array having True if sim is active, False if sim is inactive.
+        sim_is_associated_to_nm1 : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None.
+        data_for_adaptivity : dict
+            A dictionary containing the names of the data to be used in adaptivity as keys and information on whether
+            the data are scalar or vector as values.
+
+        Returns
+        -------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair.
+        is_sim_active : numpy array
+            1D array, True is sim is active, False if sim is inactive.
+        """
+        similarity_dists = self._get_similarity_dists(dt, similarity_dists_nm1, data_for_adaptivity)
+
+        # Operation done globally if global adaptivity is chosen
+        is_sim_active = self._update_active_sims(similarity_dists, is_sim_active_nm1)
+
+        is_sim_active, sim_is_associated_to = self._update_inactive_sims(
+            similarity_dists, is_sim_active_nm1, sim_is_associated_to_nm1, micro_sims)
+
+        sim_is_associated_to = self._associate_inactive_to_active(
+            similarity_dists, is_sim_active, sim_is_associated_to)
+
+        self._logger.info(
+            "{} active simulations, {} inactive simulations".format(
+                np.count_nonzero(is_sim_active), np.count_nonzero(is_sim_active == False)))
+
+        return similarity_dists, is_sim_active, sim_is_associated_to
+
+    def _update_inactive_sims(
+            self,
+            similarity_dists: np.ndarray,
+            is_sim_active: np.ndarray,
+            sim_is_associated_to: np.ndarray,
+            micro_sims: list) -> tuple:
+        """
+        Update set of inactive micro simulations. Each inactive micro simulation is compared to all active ones
+        and if it is not similar to any of them, it is activated.
+
+        Parameters
+        ----------
+        similarity_dists : numpy array
+            2D array having similarity distances between each micro simulation pair.
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation.
+        sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None.
+        micro_sims : list
+            List containing micro simulation objects.
+
+        Returns
+        -------
+        _is_sim_active : numpy array
+            Updated 1D array having state (active or inactive) of each micro simulation
+        _sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
+        """
+        self._ref_tol = self._refine_const * np.amax(similarity_dists)
+
+        _is_sim_active = np.copy(is_sim_active)  # Input is_sim_active is not longer used after this point
+        _sim_is_associated_to = np.copy(sim_is_associated_to)
+
+        # Update the set of inactive micro sims
+        for i in range(_is_sim_active.size):
+            if not _is_sim_active[i]:  # if id is inactive
+                if self._check_for_activation(i, similarity_dists, _is_sim_active):
+                    associated_active_local_id = _sim_is_associated_to[i]
+                    micro_sims[i].set_state(micro_sims[associated_active_local_id].get_state())
+                    _is_sim_active[i] = True
+                    _sim_is_associated_to[i] = -2  # Active sim cannot have an associated sim
+
+        return _is_sim_active, _sim_is_associated_to
diff --git a/micro_manager/config.py b/micro_manager/config.py
index 3b207ffc..d881c09e 100644
--- a/micro_manager/config.py
+++ b/micro_manager/config.py
@@ -1,10 +1,9 @@
 """
-Configuration module of the Micro Manager
+Class Config provides functionality to read a JSON file and pass the values to the Micro Manager.
 """
 
 import json
 import os
-import sys
 
 
 class Config:
@@ -13,7 +12,7 @@ class Config:
     the config class in https://github.com/precice/fenics-adapter/tree/develop/fenicsadapter
     """
 
-    def __init__(self, config_filename):
+    def __init__(self, logger, config_filename):
         """
         Constructor of the Config class.
 
@@ -22,6 +21,8 @@ def __init__(self, config_filename):
         config_filename : string
             Name of the JSON configuration file
         """
+        self._logger = logger
+
         self._micro_file_name = None
 
         self._config_file_name = None
@@ -30,11 +31,21 @@ def __init__(self, config_filename):
         self._write_data_names = dict()
 
         self._macro_domain_bounds = None
+        self._ranks_per_axis = None
         self._micro_output_n = 1
         self._diagnostics_data_names = dict()
 
         self._output_micro_sim_time = False
 
+        self._adaptivity = False
+        self._adaptivity_type = "local"
+        self._data_for_adaptivity = dict()
+        self._adaptivity_history_param = 0.5
+        self._adaptivity_coarsening_constant = 0.5
+        self._adaptivity_refining_constant = 0.5
+        self._adaptivity_every_implicit_iteration = False
+        self._adaptivity_similarity_measure = "L1"
+
         self.read_json(config_filename)
 
     def read_json(self, config_filename):
@@ -46,19 +57,13 @@ def read_json(self, config_filename):
         config_filename : string
             Name of the JSON configuration file
         """
-        folder = os.path.dirname(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]), config_filename))
+        folder = os.path.dirname(os.path.join(os.getcwd(), config_filename))
         path = os.path.join(folder, os.path.basename(config_filename))
-        read_file = open(path, "r")
-        data = json.load(read_file)
-
-        self._micro_file_name = data["micro_file_name"]
-        i = 0
-        micro_filename = list(self._micro_file_name)
-        for c in micro_filename:
-            if c == '/':
-                micro_filename[i] = '.'
-            i += 1
-        self._micro_file_name = ''.join(micro_filename)
+        with open(path, "r") as read_file:
+            data = json.load(read_file)
+
+        # convert paths to python-importable paths
+        self._micro_file_name = data["micro_file_name"].replace("/", ".").replace("\\", ".").replace(".py", "")
 
         self._config_file_name = os.path.join(folder, data["coupling_params"]["config_file_name"])
         self._macro_mesh_name = data["coupling_params"]["macro_mesh_name"]
@@ -74,7 +79,7 @@ def read_json(self, config_filename):
                 else:
                     raise Exception("Write data dictionary as a value other than 'scalar' or 'vector'")
         except BaseException:
-            print("No write data names provided. Micro manager will only read data from preCICE.")
+            self._logger.info("No write data names provided. Micro manager will only read data from preCICE.")
 
         try:
             self._read_data_names = data["coupling_params"]["read_data_names"]
@@ -87,15 +92,59 @@ def read_json(self, config_filename):
                 else:
                     raise Exception("Read data dictionary as a value other than 'scalar' or 'vector'")
         except BaseException:
-            print("No read data names provided. Micro manager will only write data to preCICE.")
+            self._logger.info("No read data names provided. Micro manager will only write data to preCICE.")
 
         self._macro_domain_bounds = data["simulation_params"]["macro_domain_bounds"]
 
         try:
-            self._micro_output_n = data["simulation_params"]["micro_output_n"]
+            self._ranks_per_axis = data["simulation_params"]["decomposition"]
         except BaseException:
-            print("Output interval of micro simulations not specified, if output is available then it will be called "
-                  "in every time window.")
+            self._logger.info(
+                "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial.")
+
+        try:
+            if data["simulation_params"]["adaptivity"]:
+                self._adaptivity = True
+            else:
+                self._adaptivity = False
+        except BaseException:
+            self._logger.info(
+                "Micro Manager will not adaptively run micro simulations, but instead will run all micro simulations in all time steps.")
+
+        if self._adaptivity:
+            if data["simulation_params"]["adaptivity"]["type"] == "local":
+                self._adaptivity_type = "local"
+            elif data["simulation_params"]["adaptivity"]["type"] == "global":
+                self._adaptivity_type = "global"
+            else:
+                raise Exception("Adaptivity type can be either local or global.")
+
+            exchange_data = {**self._read_data_names, **self._write_data_names}
+            for dname in data["simulation_params"]["adaptivity"]["data"]:
+                self._data_for_adaptivity[dname] = exchange_data[dname]
+
+            self._adaptivity_history_param = data["simulation_params"]["adaptivity"]["history_param"]
+            self._adaptivity_coarsening_constant = data["simulation_params"]["adaptivity"]["coarsening_constant"]
+            self._adaptivity_refining_constant = data["simulation_params"]["adaptivity"]["refining_constant"]
+
+            if "similarity_measure" in data["simulation_params"]["adaptivity"]:
+                self._adaptivity_similarity_measure = data["simulation_params"]["adaptivity"]["similarity_measure"]
+            else:
+                self._logger.info("No similarity measure provided, using L1 norm as default")
+                self._adaptivity_similarity_measure = "L1"
+
+            adaptivity_every_implicit_iteration = data["simulation_params"]["adaptivity"]["every_implicit_iteration"]
+
+            if adaptivity_every_implicit_iteration == "True":
+                self._adaptivity_every_implicit_iteration = True
+            elif adaptivity_every_implicit_iteration == "False":
+                self._adaptivity_every_implicit_iteration = False
+
+            if not self._adaptivity_every_implicit_iteration:
+                self._logger.info("Micro Manager will compute adaptivity once at the start of every time window")
+
+            self._write_data_names["active_state"] = False
+            self._write_data_names["active_steps"] = False
 
         try:
             diagnostics_data_names = data["diagnostics"]["data_from_micro_sims"]
@@ -108,16 +157,22 @@ def read_json(self, config_filename):
                 else:
                     raise Exception("Diagnostics data dictionary as a value other than 'scalar' or 'vector'")
         except BaseException:
-            print("No diagnostics data is defined. Micro Manager will not output any diagnostics data.")
+            self._logger.info("No diagnostics data is defined. Micro Manager will not output any diagnostics data.")
+
+        try:
+            self._micro_output_n = data["diagnostics"]["micro_output_n"]
+        except BaseException:
+            self._logger.info(
+                "Output interval of micro simulations not specified, if output is available then it will be called "
+                "in every time window.")
 
         try:
             if data["diagnostics"]["output_micro_sim_solve_time"]:
                 self._output_micro_sim_time = True
                 self._write_data_names["micro_sim_time"] = False
         except BaseException:
-            print("Micro manager will not output time required to solve each micro simulation in each time step.")
-
-        read_file.close()
+            self._logger.info(
+                "Micro manager will not output time required to solve each micro simulation in each time step.")
 
     def get_config_file_name(self):
         """
@@ -180,6 +235,17 @@ def get_macro_domain_bounds(self):
         """
         return self._macro_domain_bounds
 
+    def get_ranks_per_axis(self):
+        """
+        Get the ranks per axis for a parallel simulation
+
+        Returns
+        -------
+        ranks_per_axis : list
+            List containing ranks in the x, y and z axis respectively.
+        """
+        return self._ranks_per_axis
+
     def get_micro_file_name(self):
         """
         Get the path to the Python script of the micro-simulation.
@@ -212,3 +278,97 @@ def write_micro_solve_time(self):
             True if micro simulation solve time is required.
         """
         return self._output_micro_sim_time
+
+    def turn_on_adaptivity(self):
+        """
+        Boolean stating whether adaptivity is ot or not.
+
+        Returns
+        -------
+        adaptivity : bool
+            True is adaptivity settings are done, False otherwise.
+
+        """
+        return self._adaptivity
+
+    def get_adaptivity_type(self):
+        """
+        String stating type of adaptivity computation, either "local" or "global".
+
+        Returns
+        -------
+        adaptivity_type : str
+            Either "local" or "global" depending on the type of adaptivity computation
+        """
+        return self._adaptivity_type
+
+    def get_data_for_adaptivity(self):
+        """
+        Get names of data to be used for similarity distance calculation in adaptivity
+
+        Returns
+        -------
+        data_for_adaptivity : dict_like
+            A dictionary containing the names of the data to be used in adaptivity as keys and information on whether
+            the data are scalar or vector as values.
+        """
+        return self._data_for_adaptivity
+
+    def get_adaptivity_hist_param(self):
+        """
+        Get adaptivity history parameter.
+        More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity
+
+        Returns
+        -------
+        adaptivity_hist_param : float
+            Adaptivity history parameter
+        """
+        return self._adaptivity_history_param
+
+    def get_adaptivity_coarsening_const(self):
+        """
+        Get adaptivity coarsening constant.
+        More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity
+
+        Returns
+        -------
+        adaptivity_coarsening_constant : float
+            Adaptivity coarsening constant
+        """
+        return self._adaptivity_coarsening_constant
+
+    def get_adaptivity_refining_const(self):
+        """
+        Get adaptivity refining constant.
+        More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity
+
+        Returns
+        -------
+        adaptivity_refining_constant : float
+            Adaptivity refining constant
+        """
+        return self._adaptivity_refining_constant
+
+    def get_adaptivity_similarity_measure(self):
+        """
+        Get measure to be used to calculate similarity between pairs of simulations.
+        More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity
+
+        Returns
+        -------
+        adaptivity_similarity_measure : str
+            String of measure to be used in calculating similarity between pairs of simulations.
+        """
+        return self._adaptivity_similarity_measure
+
+    def is_adaptivity_required_in_every_implicit_iteration(self):
+        """
+        Check if adaptivity needs to be calculated in every time iteration or every time window.
+
+        Returns
+        -------
+        adaptivity_every_implicit_iteration : bool
+            True if adaptivity needs to be calculated in every time iteration, False otherwise.
+        """
+        return self._adaptivity_every_implicit_iteration
diff --git a/micro_manager/domain_decomposition.py b/micro_manager/domain_decomposition.py
new file mode 100644
index 00000000..91fd6bf8
--- /dev/null
+++ b/micro_manager/domain_decomposition.py
@@ -0,0 +1,95 @@
+"""
+Class DomainDecomposer provides the method decompose_macro_domain which returns partitioned bounds
+"""
+
+import numpy as np
+
+
+class DomainDecomposer:
+    def __init__(self, logger, dims, rank, size) -> None:
+        """
+        Class constructor.
+
+        Parameters
+        ----------
+        logger : object of logging
+            Logger defined from the standard package logging.
+        dims : int
+            Dimensions of the problem.
+        rank : int
+            MPI rank.
+        size : int
+            Total number of MPI processes.
+        """
+        self._logger = logger
+        self._rank = rank
+        self._size = size
+        self._dims = dims
+
+    def decompose_macro_domain(self, macro_bounds: list, ranks_per_axis: list) -> list:
+        """
+        Decompose the macro domain equally among all ranks, if the Micro Manager is run in parallel.
+
+        Parameters
+        ----------
+        macro_bounds : list
+            List containing upper and lower bounds of the macro domain.
+            Format in 2D is [x_min, x_max, y_min, y_max]
+            Format in 3D is [x_min, x_max, y_min, y_max, z_min, z_max]
+        ranks_per_axis : list
+            List containing axis wise ranks for a parallel run
+            Format in 2D is [ranks_x, ranks_y]
+            Format in 3D is [ranks_x, ranks_y, ranks_z]
+
+        Returns
+        -------
+        mesh_bounds : list
+            List containing the upper and lower bounds of the domain pertaining to this rank.
+            Format is same as input parameter macro_bounds.
+        """
+        assert np.prod(
+            ranks_per_axis) == self._size, "Total number of processors provided in the Micro Manager configuration and in the MPI execution command do not match."
+
+        dx = []
+        for d in range(self._dims):
+            dx.append(abs(macro_bounds[d * 2 + 1] - macro_bounds[d * 2]) / ranks_per_axis[d])
+
+        rank_in_axis: list[int] = [0] * self._dims
+        if ranks_per_axis[0] == 1:  # if serial in x axis
+            rank_in_axis[0] = 0
+        else:
+            rank_in_axis[0] = self._rank % ranks_per_axis[0]  # x axis
+
+        if self._dims == 2:
+            if ranks_per_axis[1] == 1:  # if serial in y axis
+                rank_in_axis[1] = 0
+            else:
+                rank_in_axis[1] = int(self._rank / ranks_per_axis[0])  # y axis
+        elif self._dims == 3:
+            if ranks_per_axis[2] == 1:  # if serial in z axis
+                rank_in_axis[2] = 0
+            else:
+                rank_in_axis[2] = int(self._rank / (ranks_per_axis[0] * ranks_per_axis[1]))  # z axis
+
+            if ranks_per_axis[1] == 1:  # if serial in y axis
+                rank_in_axis[1] = 0
+            else:
+                rank_in_axis[1] = (self._rank - ranks_per_axis[0] * ranks_per_axis[1]
+                                   * rank_in_axis[2]) % ranks_per_axis[2]  # y axis
+
+        mesh_bounds = []
+        for d in range(self._dims):
+            if rank_in_axis[d] > 0:
+                mesh_bounds.append(macro_bounds[d * 2] + rank_in_axis[d] * dx[d])
+                mesh_bounds.append(macro_bounds[d * 2] + (rank_in_axis[d] + 1) * dx[d])
+            elif rank_in_axis[d] == 0:
+                mesh_bounds.append(macro_bounds[d * 2])
+                mesh_bounds.append(macro_bounds[d * 2] + dx[d])
+
+            # Adjust the maximum bound to be exactly the domain size
+            if rank_in_axis[d] + 1 == ranks_per_axis[d]:
+                mesh_bounds[d * 2 + 1] = macro_bounds[d * 2 + 1]
+
+        self._logger.info("Bounding box limits are {}".format(mesh_bounds))
+
+        return mesh_bounds
diff --git a/micro_manager/micro_manager.py b/micro_manager/micro_manager.py
index 9580119f..ef747d7c 100644
--- a/micro_manager/micro_manager.py
+++ b/micro_manager/micro_manager.py
@@ -1,64 +1,46 @@
 #!/usr/bin/env python3
 """
-Micro manager to organize many micro simulations and couple them via preCICE to a macro simulation
+Micro Manager is a tool to initialize and adaptively control micro simulations and couple them via preCICE to a macro simulation.
+This files the class MicroManager which has the following callable public methods:
+
+- initialize
+- solve
+
+This file is directly executable as it consists of a main() function. Upon execution, an object of the class MicroManager is created using a given JSON file,
+and the initialize and solve methods are called.
+
+Detailed documentation: https://precice.org/tooling-micro-manager-overview.html
 """
 
 import argparse
 import os
 import sys
 import precice
-from .config import Config
 from mpi4py import MPI
-from math import sqrt
 import numpy as np
-from functools import reduce
-from operator import iconcat
-import hashlib
 import logging
 import time
+from copy import deepcopy
+from typing import Dict
 
-sys.path.append(os.getcwd())
-
-
-def create_micro_problem_class(base_micro_simulation):
-    """
-    Creates a class MicroProblem which inherits from the class of the micro simulation.
-    Parameters
-    ----------
-    base_micro_simulation : class
-        The base class from the micro simulation script.
-    Returns
-    -------
-    MicroProblem : class
-        Definition of class MicroProblem defined in this function.
-    """
-    class MicroProblem(base_micro_simulation):
-        def __init__(self, micro_sim_id):
-            base_micro_simulation.__init__(self, micro_sim_id)
-            self._id = micro_sim_id
-            self._is_active = False
-
-        def get_id(self):
-            return self._id
-
-        def activate(self):
-            self._is_active = True
-
-        def deactivate(self):
-            self._is_active = False
+from .config import Config
+from .micro_simulation import create_simulation_class
+from .adaptivity.local_adaptivity import LocalAdaptivityCalculator
+from .adaptivity.global_adaptivity import GlobalAdaptivityCalculator
+from .domain_decomposition import DomainDecomposer
 
-    return MicroProblem
+sys.path.append(os.getcwd())
 
 
 class MicroManager:
-    def __init__(self, config_file):
+    def __init__(self, config_file: str) -> None:
         """
-        Constructor of MicroManager class.
+        Constructor.
 
         Parameters
         ----------
         config_file : string
-            Name of the JSON configuration file (to be provided by the user)
+            Name of the JSON configuration file (provided by the user).
         """
         self._comm = MPI.COMM_WORLD
         self._rank = self._comm.Get_rank()
@@ -66,8 +48,11 @@ def __init__(self, config_file):
 
         self._logger = logging.getLogger(__name__)
         self._logger.setLevel(level=logging.INFO)
-        fh = logging.FileHandler('micro-manager.log')  # Create file handler which logs messages
+
+        # Create file handler which logs messages
+        fh = logging.FileHandler('micro-manager.log')
         fh.setLevel(logging.INFO)
+
         # Create formatter and add it to handlers
         formatter = logging.Formatter('[' + str(self._rank) + '] %(name)s -  %(levelname)s - %(message)s')
         fh.setFormatter(formatter)
@@ -77,231 +62,395 @@ def __init__(self, config_file):
         self._micro_sims_have_output = False
 
         self._logger.info("Provided configuration file: {}".format(config_file))
-        config = Config(config_file)
+        self._config = Config(self._logger, config_file)
 
         # Define the preCICE interface
-        self._interface = precice.Interface("Micro-Manager", config.get_config_file_name(), self._rank, self._size)
-
-        micro_file_name = config.get_micro_file_name()
-        self._micro_problem = getattr(__import__(micro_file_name, fromlist=["MicroSimulation"]), "MicroSimulation")
+        self._interface = precice.Interface(
+            "Micro-Manager",
+            self._config.get_config_file_name(),
+            self._rank,
+            self._size)
 
-        self._macro_mesh_id = self._interface.get_mesh_id(config.get_macro_mesh_name())
+        self._macro_mesh_id = self._interface.get_mesh_id(self._config.get_macro_mesh_name())
 
         # Data names and ids of data written to preCICE
-        self._write_data_names = config.get_write_data_names()
+        self._write_data_names = self._config.get_write_data_names()
         self._write_data_ids = dict()
         for name in self._write_data_names.keys():
             self._write_data_ids[name] = self._interface.get_data_id(name, self._macro_mesh_id)
 
         # Data names and ids of data read from preCICE
-        self._read_data_names = config.get_read_data_names()
+        self._read_data_names = self._config.get_read_data_names()
         self._read_data_ids = dict()
         for name in self._read_data_names.keys():
             self._read_data_ids[name] = self._interface.get_data_id(name, self._macro_mesh_id)
 
-        self._macro_bounds = config.get_macro_domain_bounds()
-        self._is_micro_solve_time_required = config.write_micro_solve_time()
+        self._macro_bounds = self._config.get_macro_domain_bounds()
 
-        self._number_of_micro_simulations = None
-        self._is_rank_empty = False
-        self._micro_sims = None
-        self._dt = None
-        self._mesh_vertex_ids = None
-        self._micro_n_out = config.get_micro_output_n()
+        if self._is_parallel:  # Simulation is run in parallel
+            self._ranks_per_axis = self._config.get_ranks_per_axis()
 
-    def decompose_macro_domain(self, macro_bounds):
-        """
-        Decompose the macro domain equally among all ranks, if the Micro Manager is run in paralle.
+        self._is_micro_solve_time_required = self._config.write_micro_solve_time()
 
-        Parameters
-        ----------
-        macro_bounds : list
-            List containing upper and lower bounds of the macro domain.
-            Format in 2D is [x_min, x_max, y_min, y_max]
-            Format in 2D is [x_min, x_max, y_min, y_max, z_min, z_max]
+        self._local_number_of_sims = 0
+        self._global_number_of_sims = 0
+        self._is_rank_empty = False
+        self._dt = 0
+        self._mesh_vertex_ids = None  # IDs of macro vertices as set by preCICE
+        self._micro_n_out = self._config.get_micro_output_n()
 
-        Returns
-        -------
-        mesh_bounds : list
-            List containing the upper and lower bounds of the domain pertaining to this rank.
-            Format is same as input parameter macro_bounds.
-        """
-        size_x = int(sqrt(self._size))
-        while self._size % size_x != 0:
-            size_x -= 1
+        self._is_adaptivity_on = self._config.turn_on_adaptivity()
 
-        size_y = int(self._size / size_x)
+        if self._is_adaptivity_on:
+            self._number_of_sims_for_adaptivity = 0
 
-        dx = abs(macro_bounds[1] - macro_bounds[0]) / size_x
-        dy = abs(macro_bounds[3] - macro_bounds[2]) / size_y
+            self._data_for_adaptivity: Dict[str, np.ndarray] = dict()
+            self._adaptivity_type = self._config.get_adaptivity_type()
 
-        local_xmin = macro_bounds[0] + dx * (self._rank % size_x)
-        local_ymin = macro_bounds[2] + dy * int(self._rank / size_x)
+            self._adaptivity_data_names = self._config.get_data_for_adaptivity()
 
-        mesh_bounds = []
-        if self._interface.get_dimensions() == 2:
-            mesh_bounds = [local_xmin, local_xmin + dx, local_ymin, local_ymin + dy]
-        elif self._interface.get_dimensions() == 3:
-            # TODO: Domain needs to be decomposed optimally in the Z direction too
-            mesh_bounds = [local_xmin, local_xmin + dx, local_ymin, local_ymin + dy, macro_bounds[4],
-                           macro_bounds[5]]
+            # Names of macro data to be used for adaptivity computation
+            self._adaptivity_macro_data_names = dict()
+            # Names of micro data to be used for adaptivity computation
+            self._adaptivity_micro_data_names = dict()
+            for name, is_data_vector in self._adaptivity_data_names.items():
+                if name in self._read_data_names:
+                    self._adaptivity_macro_data_names[name] = is_data_vector
+                if name in self._write_data_names:
+                    self._adaptivity_micro_data_names[name] = is_data_vector
 
-        self._logger.info("Bounding box limits are {}".format(mesh_bounds))
+            self._is_adaptivity_required_in_every_implicit_iteration = self._config.is_adaptivity_required_in_every_implicit_iteration()
+            self._micro_sims_active_steps = None
 
-        return mesh_bounds
+    # **************
+    # Public methods
+    # **************
 
-    def initialize(self):
+    def initialize(self) -> None:
         """
-        This function does the following things:
-        - If the Micro Manager has been executed in parallel, it decomposes the domain as equally as possible.
-        - Initializes preCICE.
-        - Get the macro mesh information.
-        - Creates all micro simulation objects and initializes them if the an initialization procedure is available.
-        - Writes initial data to preCICE.
+        Initialize the Micro Manager by performing the following tasks:
+        - Decompose the domain if the Micro Manager is executed in parallel.
+        - Initialize preCICE.
+        - Gets the macro mesh information from preCICE.
+        - Create all micro simulation objects and initialize them if an initialize() method is available.
+        - If required, write initial data to preCICE.
         """
         # Decompose the macro-domain and set the mesh access region for each partition in preCICE
-        assert len(
-            self._macro_bounds) / 2 == self._interface.get_dimensions(), "Provided macro mesh bounds are of " \
-                                                                         "incorrect dimension"
+        assert len(self._macro_bounds) / \
+            2 == self._interface.get_dimensions(), "Provided macro mesh bounds are of incorrect dimension"
         if self._is_parallel:
-            coupling_mesh_bounds = self.decompose_macro_domain(self._macro_bounds)
+            domain_decomposer = DomainDecomposer(self._logger, self._interface.get_dimensions(), self._rank, self._size)
+            coupling_mesh_bounds = domain_decomposer.decompose_macro_domain(self._macro_bounds, self._ranks_per_axis)
         else:
             coupling_mesh_bounds = self._macro_bounds
 
         self._interface.set_mesh_access_region(self._macro_mesh_id, coupling_mesh_bounds)
 
-        # initialize preCICE
+        # Initialize preCICE
         self._dt = self._interface.initialize()
 
         self._mesh_vertex_ids, mesh_vertex_coords = self._interface.get_mesh_vertices_and_ids(self._macro_mesh_id)
-        self._number_of_micro_simulations, _ = mesh_vertex_coords.shape
-        self._logger.info("Number of micro simulations = {}".format(self._number_of_micro_simulations))
+        self._local_number_of_sims, _ = mesh_vertex_coords.shape
+        self._logger.info("Number of local micro simulations = {}".format(self._local_number_of_sims))
 
-        if self._number_of_micro_simulations == 0:
+        if self._local_number_of_sims == 0:
             if self._is_parallel:
-                self._logger.info("Rank {} has no micro simulations and hence will not do any computation.".format(
-                    self._rank))
+                self._logger.info(
+                    "Rank {} has no micro simulations and hence will not do any computation.".format(
+                        self._rank))
                 self._is_rank_empty = True
             else:
                 raise Exception("Micro Manager has no micro simulations.")
 
         nms_all_ranks = np.zeros(self._size, dtype=np.int64)
         # Gather number of micro simulations that each rank has, because this rank needs to know how many micro
-        # simulations have been created by previous ranks, so that it can set the correct IDs
-        self._comm.Allgather(np.array(self._number_of_micro_simulations), nms_all_ranks)
-
-        # Create all micro simulations
-        sim_id = 0
-        if self._rank != 0:
-            for i in range(self._rank - 1, -1, -1):
-                sim_id += nms_all_ranks[i]
-
-        self._micro_sims = []
-        for _ in range(self._number_of_micro_simulations):
-            self._micro_sims.append(create_micro_problem_class(self._micro_problem)(sim_id))
-            sim_id += 1
+        # simulations have been created by previous ranks, so that it can set
+        # the correct global IDs
+        self._comm.Allgather(np.array(self._local_number_of_sims), nms_all_ranks)
 
-        write_data = dict()
-        for name in self._write_data_names.keys():
-            write_data[name] = []
+        # Get global number of micro simulations
+        self._global_number_of_sims = np.sum(nms_all_ranks)
 
-        # Initialize all micro simulations
-        if hasattr(self._micro_problem, 'initialize') and callable(getattr(self._micro_problem, 'initialize')):
-            for micro_sim in self._micro_sims:
-                micro_sims_output = micro_sim.initialize()
-                if micro_sims_output is not None:
-                    if self._is_micro_solve_time_required:
-                        micro_sims_output["micro_sim_time"] = 0.0
+        if self._is_adaptivity_on:
+            for name, is_data_vector in self._adaptivity_data_names.items():
+                if is_data_vector:
+                    self._data_for_adaptivity[name] = np.zeros(
+                        (self._local_number_of_sims, self._interface.get_dimensions()))
+                else:
+                    self._data_for_adaptivity[name] = np.zeros((self._local_number_of_sims))
 
-                    for data_name, data in micro_sims_output.items():
-                        write_data[data_name].append(data)
+        # Create lists of local and global IDs
+        sim_id = np.sum(nms_all_ranks[:self._rank])
+        self._global_ids_of_local_sims = []  # DECLARATION
+        for i in range(self._local_number_of_sims):
+            self._global_ids_of_local_sims.append(sim_id)
+            sim_id += 1
+
+        self._micro_sims = [None] * self._local_number_of_sims  # DECLARATION
+        micro_sims_output = [None] * self._local_number_of_sims
+
+        micro_problem = getattr(
+            __import__(
+                self._config.get_micro_file_name(),
+                fromlist=["MicroSimulation"]),
+            "MicroSimulation")
+
+        if self._is_adaptivity_on:
+            # Create micro simulation objects
+            for i in range(self._local_number_of_sims):
+                self._micro_sims[i] = create_simulation_class(
+                    micro_problem)(self._global_ids_of_local_sims[i])
+
+            # Create a map of micro simulation global IDs and the ranks on which they are
+            micro_sims_on_this_rank = np.zeros(self._local_number_of_sims, dtype=np.intc)
+            for i in range(self._local_number_of_sims):
+                micro_sims_on_this_rank[i] = self._rank
+
+            self._rank_of_sim = np.zeros(self._global_number_of_sims, dtype=np.intc)  # DECLARATION
+            self._comm.Allgather(micro_sims_on_this_rank, self._rank_of_sim)
+
+            self._is_sim_on_this_rank = [False] * self._global_number_of_sims  # DECLARATION
+            for i in range(self._global_number_of_sims):
+                if self._rank_of_sim[i] == self._rank:
+                    self._is_sim_on_this_rank[i] = True
+
+            if self._adaptivity_type == "local":
+                self._adaptivity_controller = LocalAdaptivityCalculator(
+                    self._config, self._logger)
+                self._number_of_sims_for_adaptivity = self._local_number_of_sims
+            elif self._adaptivity_type == "global":
+                self._adaptivity_controller = GlobalAdaptivityCalculator(
+                    self._config,
+                    self._logger,
+                    self._is_sim_on_this_rank,
+                    self._rank_of_sim,
+                    self._global_ids_of_local_sims,
+                    self._rank,
+                    self._comm)
+                self._number_of_sims_for_adaptivity = self._global_number_of_sims
+
+            self._micro_sims_active_steps = np.zeros(self._local_number_of_sims)
+        else:
+            for i in range(self._local_number_of_sims):
+                self._micro_sims[i] = (
+                    create_simulation_class(micro_problem)(self._global_ids_of_local_sims[i]))
+
+        # Initialize micro simulations if initialize() method exists
+        if hasattr(micro_problem, 'initialize') and callable(getattr(micro_problem, 'initialize')):
+            for i in range(self._local_number_of_sims):
+                micro_sims_output[i] = self._micro_sims[i].initialize()
+                if micro_sims_output[i] is not None:
+                    if self._is_micro_solve_time_required:
+                        micro_sims_output[i]["micro_sim_time"] = 0.0
+                    if self._is_adaptivity_on:
+                        micro_sims_output[i]["active_state"] = 0
+                        micro_sims_output[i]["active_steps"] = 0
                 else:
+                    micro_sims_output[i] = dict()
                     for name, is_data_vector in self._write_data_names.items():
                         if is_data_vector:
-                            write_data[name].append(np.zeros(self._interface.get_dimensions()))
+                            micro_sims_output[i][name] = np.zeros(self._interface.get_dimensions())
                         else:
-                            write_data[name].append(0.0)
+                            micro_sims_output[i][name] = 0.0
 
-        self._logger.info("Micro simulations {} - {} initialized.".format(self._micro_sims[0].get_id(),
-                                                                          self._micro_sims[-1].get_id()))
+        self._logger.info("Micro simulations with global IDs {} - {} initialized.".format(
+            self._global_ids_of_local_sims[0], self._global_ids_of_local_sims[-1]))
 
         self._micro_sims_have_output = False
-        if hasattr(self._micro_problem, 'output') and callable(getattr(self._micro_problem, 'output')):
+        if hasattr(micro_problem, 'output') and callable(getattr(micro_problem, 'output')):
             self._micro_sims_have_output = True
 
-        # Initialize coupling data
+        # Write initial data if required
         if self._interface.is_action_required(precice.action_write_initial_data()):
-            for dname, dim in self._write_data_names.items():
-                if dim == 1:
-                    self._interface.write_block_vector_data(self._write_data_ids[dname], self._mesh_vertex_ids,
-                                                            write_data[dname])
-                elif dim == 0:
-                    self._interface.write_block_scalar_data(self._write_data_ids[dname], self._mesh_vertex_ids,
-                                                            write_data[dname])
+            self._write_data_to_precice(micro_sims_output)
             self._interface.mark_action_fulfilled(precice.action_write_initial_data())
 
         self._interface.initialize_data()
 
-    def read_data_from_precice(self):
+    def solve(self) -> None:
+        """
+        Solve the problem using preCICE.
+        - Handle checkpointing is implicit coupling is done.
+        - Read data from preCICE, solve micro simulations, and write data to preCICE
+        - If adaptivity is on, compute micro simulations adaptively.
+        """
+        t, n = 0, 0
+        t_checkpoint, n_checkpoint = 0, 0
+
+        if self._is_adaptivity_on:
+            similarity_dists = np.zeros(
+                (self._number_of_sims_for_adaptivity,
+                 self._number_of_sims_for_adaptivity))
+
+            # Start adaptivity calculation with all sims inactive
+            is_sim_active = np.array([False] * self._number_of_sims_for_adaptivity)
+
+            # Activate the first one (a random choice)
+            is_sim_active[0] = True
+
+            # Associate all sims to the one active sim
+            sim_is_associated_to = np.zeros((self._number_of_sims_for_adaptivity), dtype=np.intc)
+            sim_is_associated_to[0] = -2  # An active sim does not have an associated sim
+
+        similarity_dists_cp = None
+        is_sim_active_cp = None
+        sim_is_associated_to_cp = None
+        sim_states_cp = [None] * self._local_number_of_sims
+
+        while self._interface.is_coupling_ongoing():
+            # Write a checkpoint
+            if self._interface.is_action_required(precice.action_write_iteration_checkpoint()):
+                for i in range(self._local_number_of_sims):
+                    sim_states_cp[i] = self._micro_sims[i].get_state()
+                t_checkpoint = t
+                n_checkpoint = n
+
+                if self._is_adaptivity_on:
+                    if not self._is_adaptivity_required_in_every_implicit_iteration:
+                        similarity_dists, is_sim_active, sim_is_associated_to = self._adaptivity_controller.compute_adaptivity(
+                            self._dt, self._micro_sims, similarity_dists, is_sim_active, sim_is_associated_to, self._data_for_adaptivity)
+
+                        # Only checkpoint the adaptivity configuration if adaptivity is computed
+                        # once in every time window
+                        similarity_dists_cp = np.copy(similarity_dists)
+                        is_sim_active_cp = np.copy(is_sim_active)
+                        sim_is_associated_to_cp = np.copy(sim_is_associated_to)
+
+                    if self._adaptivity_type == "local":
+                        active_sim_ids = np.where(is_sim_active)[0]
+                    elif self._adaptivity_type == "global":
+                        active_sim_ids = np.where(
+                            is_sim_active[self._global_ids_of_local_sims[0]:self._global_ids_of_local_sims[-1] + 1])[0]
+
+                    for active_id in active_sim_ids:
+                        self._micro_sims_active_steps[active_id] += 1
+
+                self._interface.mark_action_fulfilled(
+                    precice.action_write_iteration_checkpoint())
+
+            micro_sims_input = self._read_data_from_precice()
+
+            if self._is_adaptivity_on:
+                if self._is_adaptivity_required_in_every_implicit_iteration:
+                    similarity_dists, is_sim_active, sim_is_associated_to = self._adaptivity_controller.compute_adaptivity(
+                        self._dt, self._micro_sims, similarity_dists, is_sim_active, sim_is_associated_to, self._data_for_adaptivity)
+
+                    if self._adaptivity_type == "local":
+                        active_sim_ids = np.where(is_sim_active)[0]
+                    elif self._adaptivity_type == "global":
+                        active_sim_ids = np.where(
+                            is_sim_active[self._global_ids_of_local_sims[0]:self._global_ids_of_local_sims[-1] + 1])[0]
+
+                    for active_id in active_sim_ids:
+                        self._micro_sims_active_steps[active_id] += 1
+
+                micro_sims_output = self._solve_micro_simulations_with_adaptivity(
+                    micro_sims_input, is_sim_active, sim_is_associated_to)
+            else:
+                micro_sims_output = self._solve_micro_simulations(micro_sims_input)
+
+            self._write_data_to_precice(micro_sims_output)
+
+            self._dt = self._interface.advance(self._dt)
+
+            t += self._dt
+            n += 1
+
+            # Revert micro simulations to their last checkpoints if required
+            if self._interface.is_action_required(precice.action_read_iteration_checkpoint()):
+                for i in range(self._local_number_of_sims):
+                    self._micro_sims[i].set_state(sim_states_cp[i])
+                n = n_checkpoint
+                t = t_checkpoint
+
+                # If adaptivity is computed only once per time window, the states of sims need to be reset too
+                if self._is_adaptivity_on:
+                    if not self._is_adaptivity_required_in_every_implicit_iteration:
+                        similarity_dists = np.copy(similarity_dists_cp)
+                        is_sim_active = np.copy(is_sim_active_cp)
+                        sim_is_associated_to = np.copy(sim_is_associated_to_cp)
+
+                self._interface.mark_action_fulfilled(
+                    precice.action_read_iteration_checkpoint())
+            else:  # Time window has converged, now micro output can be generated
+                self._logger.info("Micro simulations {} - {} have converged at t = {}".format(
+                    self._micro_sims[0].get_global_id(), self._micro_sims[-1].get_global_id(), t))
+
+                if self._micro_sims_have_output:
+                    if n % self._micro_n_out == 0:
+                        for sim in self._micro_sims:
+                            sim.output()
+
+        self._interface.finalize()
+
+    # ***************
+    # Private methods
+    # ***************
+
+    def _read_data_from_precice(self) -> list:
         """
-        Read data from preCICE. Depending on initial definition of whether a data is scalar or vector, the appropriate
-        preCICE API command is called.
+        Read data from preCICE.
 
         Returns
         -------
-        list : list
+        local_read_data : list
             List of dicts in which keys are names of data being read and the values are the data from preCICE.
         """
-        read_data = dict()
+        read_data: Dict[str, list] = dict()
         for name in self._read_data_names.keys():
             read_data[name] = []
 
         for name, is_data_vector in self._read_data_names.items():
             if is_data_vector:
-                read_data.update({name: self._interface.read_block_vector_data(self._read_data_ids[name],
-                                                                               self._mesh_vertex_ids)})
+                read_data.update({name: self._interface.read_block_vector_data(
+                    self._read_data_ids[name], self._mesh_vertex_ids)})
             else:
-                read_data.update({name: self._interface.read_block_scalar_data(self._read_data_ids[name],
-                                                                               self._mesh_vertex_ids)})
+                read_data.update({name: self._interface.read_block_scalar_data(
+                    self._read_data_ids[name], self._mesh_vertex_ids)})
+
+            if self._is_adaptivity_on:
+                if name in self._adaptivity_macro_data_names:
+                    self._data_for_adaptivity[name] = read_data[name]
 
         return [dict(zip(read_data, t)) for t in zip(*read_data.values())]
 
-    def write_data_to_precice(self, micro_sims_output):
+    def _write_data_to_precice(self, data: list) -> None:
         """
-        Write output of micro simulations to preCICE.
+        Write data to preCICE.
 
         Parameters
         ----------
-        micro_sims_output : list
+        data : list
             List of dicts in which keys are names of data and the values are the data to be written to preCICE.
         """
-        write_data = dict()
+        data_dict: Dict[str, list] = dict()
         if not self._is_rank_empty:
-            for name in micro_sims_output[0]:
-                write_data[name] = []
+            for name in data[0]:
+                data_dict[name] = []
 
-            for dic in micro_sims_output:
-                for name, values in dic.items():
-                    write_data[name].append(values)
+            for output_dict in data:
+                for name, values in output_dict.items():
+                    data_dict[name].append(values)
 
             for dname, is_data_vector in self._write_data_names.items():
                 if is_data_vector:
-                    self._interface.write_block_vector_data(self._write_data_ids[dname], self._mesh_vertex_ids,
-                                                            write_data[dname])
+                    self._interface.write_block_vector_data(
+                        self._write_data_ids[dname], self._mesh_vertex_ids, data_dict[dname])
                 else:
-                    self._interface.write_block_scalar_data(self._write_data_ids[dname], self._mesh_vertex_ids,
-                                                            write_data[dname])
+                    self._interface.write_block_scalar_data(
+                        self._write_data_ids[dname], self._mesh_vertex_ids, data_dict[dname])
         else:
             for dname, is_data_vector in self._write_data_names.items():
                 if is_data_vector:
-                    self._interface.write_block_vector_data(self._write_data_ids[dname], [], np.array([]))
+                    self._interface.write_block_vector_data(
+                        self._write_data_ids[dname], [], np.array([]))
                 else:
-                    self._interface.write_block_scalar_data(self._write_data_ids[dname], [], np.array([]))
+                    self._interface.write_block_scalar_data(
+                        self._write_data_ids[dname], [], np.array([]))
 
-    def solve_micro_simulations(self, micro_sims_input):
+    def _solve_micro_simulations(self, micro_sims_input: list) -> list:
         """
-        Solve all micro simulations using the input data and assemble the micro simulations outputs in a list of dicts
-        format.
+        Solve all micro simulations and assemble the micro simulations outputs in a list of dicts format.
 
         Parameters
         ----------
@@ -315,66 +464,96 @@ def solve_micro_simulations(self, micro_sims_input):
             List of dicts in which keys are names of data and the values are the data of the output of the micro
             simulations.
         """
-        micro_sims_output = []
-        for i in range(self._number_of_micro_simulations):
-            self._logger.info("Solving micro simulation ({})".format(self._micro_sims[i].get_id()))
+        micro_sims_output = [None] * self._local_number_of_sims
+
+        for count, sim in enumerate(self._micro_sims):
             start_time = time.time()
-            micro_sims_output.append(self._micro_sims[i].solve(micro_sims_input[i], self._dt))
+            micro_sims_output[count] = sim.solve(micro_sims_input[count], self._dt)
             end_time = time.time()
+
             if self._is_micro_solve_time_required:
-                micro_sims_output[i]["micro_sim_time"] = end_time - start_time
+                micro_sims_output[count]["micro_sim_time"] = end_time - start_time
 
         return micro_sims_output
 
-    def solve(self):
+    def _solve_micro_simulations_with_adaptivity(
+            self,
+            micro_sims_input: list,
+            is_sim_active: np.ndarray,
+            sim_is_associated_to: np.ndarray) -> list:
         """
-        This function handles the coupling time loop, including checkpointing and output.
-        """
-        t, n = 0, 0
-        t_checkpoint, n_checkpoint = 0, 0
+        Solve all micro simulations and assemble the micro simulations outputs in a list of dicts format.
 
-        while self._interface.is_coupling_ongoing():
-            # Write checkpoints for all micro simulations
-            if self._interface.is_action_required(precice.action_write_iteration_checkpoint()):
-                for micro_sim in self._micro_sims:
-                    micro_sim.save_checkpoint()
-                t_checkpoint = t
-                n_checkpoint = n
-                self._interface.mark_action_fulfilled(precice.action_write_iteration_checkpoint())
+        Parameters
+        ----------
+        micro_sims_input : list
+            List of dicts in which keys are names of data and the values are the data which are required inputs to
+            solve a micro simulation.
+        is_sim_active : numpy array
+            1D array having state (active or inactive) of each micro simulation
+        sim_is_associated_to : numpy array
+            1D array with values of associated simulations of inactive simulations. Active simulations have None
 
-            micro_sims_input = self.read_data_from_precice()
+        Returns
+        -------
+        micro_sims_output : list
+            List of dicts in which keys are names of data and the values are the data of the output of the micro
+            simulations.
+        """
+        if self._adaptivity_type == "global":
+            active_sim_ids = np.where(
+                is_sim_active[self._global_ids_of_local_sims[0]:self._global_ids_of_local_sims[-1] + 1])[0]
+            inactive_sim_ids = np.where(
+                is_sim_active[self._global_ids_of_local_sims[0]:self._global_ids_of_local_sims[-1] + 1] == False)[0]
+        elif self._adaptivity_type == "local":
+            active_sim_ids = np.where(is_sim_active)[0]
+            inactive_sim_ids = np.where(is_sim_active == False)[0]
+
+        micro_sims_output = [None] * self._local_number_of_sims
+
+        # Solve all active micro simulations
+        for active_id in active_sim_ids:
+            start_time = time.time()
+            micro_sims_output[active_id] = self._micro_sims[active_id].solve(micro_sims_input[active_id], self._dt)
+            end_time = time.time()
 
-            micro_sims_output = self.solve_micro_simulations(micro_sims_input)
+            # Mark the micro sim as active for export
+            micro_sims_output[active_id]["active_state"] = 1
+            micro_sims_output[active_id]["active_steps"] = self._micro_sims_active_steps[active_id]
 
-            self.write_data_to_precice(micro_sims_output)
+            if self._is_micro_solve_time_required:
+                micro_sims_output[active_id]["micro_sim_time"] = end_time - start_time
 
-            self._dt = self._interface.advance(self._dt)
+        # For each inactive simulation, copy data from most similar active simulation
+        if self._adaptivity_type == "global":
+            self._adaptivity_controller.communicate_micro_output(is_sim_active, sim_is_associated_to, micro_sims_output)
+        elif self._adaptivity_type == "local":
+            for inactive_id in inactive_sim_ids:
+                micro_sims_output[inactive_id] = deepcopy(
+                    micro_sims_output[sim_is_associated_to[inactive_id]])
 
-            t += self._dt
-            n += 1
+        # Resolve micro sim output data for inactive simulations
+        for inactive_id in inactive_sim_ids:
+            micro_sims_output[inactive_id]["active_state"] = 0
+            micro_sims_output[inactive_id]["active_steps"] = self._micro_sims_active_steps[inactive_id]
 
-            # Revert all micro simulations to checkpoints if required
-            if self._interface.is_action_required(precice.action_read_iteration_checkpoint()):
-                for micro_sim in self._micro_sims:
-                    micro_sim.reload_checkpoint()
-                n = n_checkpoint
-                t = t_checkpoint
-                self._interface.mark_action_fulfilled(precice.action_read_iteration_checkpoint())
-            else:  # Time window has converged, now micro output can be generated
-                self._logger.info("Micro simulations {} - {}: time window t = {} has converged".format(
-                    self._micro_sims[0].get_id(), self._micro_sims[-1].get_id(), t))
+            if self._is_micro_solve_time_required:
+                micro_sims_output[inactive_id]["micro_sim_time"] = 0
 
-                if self._micro_sims_have_output:
-                    if n % self._micro_n_out == 0:
-                        for micro_sim in self._micro_sims:
-                            micro_sim.output(n)
+        # Collect micro sim output for adaptivity calculation
+        for i in range(self._local_number_of_sims):
+            for name in self._adaptivity_micro_data_names:
+                self._data_for_adaptivity[name][i] = micro_sims_output[i][name]
 
-        self._interface.finalize()
+        return micro_sims_output
 
 
 def main():
     parser = argparse.ArgumentParser(description='.')
-    parser.add_argument('config_file', type=str, help='Path to the JSON config file of the manager.')
+    parser.add_argument(
+        'config_file',
+        type=str,
+        help='Path to the JSON config file of the manager.')
 
     args = parser.parse_args()
     config_file_path = args.config_file
diff --git a/micro_manager/micro_simulation.py b/micro_manager/micro_simulation.py
new file mode 100644
index 00000000..df505bb3
--- /dev/null
+++ b/micro_manager/micro_simulation.py
@@ -0,0 +1,33 @@
+"""
+This file provides a function which creates a class Simulation. This class inherits from the user-provided
+class MicroSimulation. A global ID member variable is defined for the class Simulation, which ensures that each
+created object is uniquely identifiable in a global setting.
+"""
+
+
+def create_simulation_class(micro_simulation_class):
+    """
+    Creates a class Simulation which inherits from the class of the micro simulation.
+
+    Parameters
+    ----------
+    base_micro_simulation : class
+        The base class from the micro simulation script.
+
+    Returns
+    -------
+    Simulation : class
+        Definition of class Simulation defined in this function.
+    """
+    class Simulation(micro_simulation_class):
+        def __init__(self, global_id):
+            micro_simulation_class.__init__(self)
+            self._global_id = global_id
+
+        def get_global_id(self) -> int:
+            return self._global_id
+
+        def set_global_id(self, global_id) -> None:
+            self._global_id = global_id
+
+    return Simulation
diff --git a/setup.py b/setup.py
index 8a13a625..38030dcb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,5 @@
 import os
 from setuptools import setup, find_packages
-import warnings
 
 # from https://stackoverflow.com/a/9079062
 import sys
@@ -14,11 +13,11 @@
 
 setup(
     name='micro-manager-precice',
-    version='v0.2.1',
+    version='v0.3.0',
     description='micro-manager-precice is a package which facilitates two-scale macro-micro coupled simulations using preCICE',
     long_description=long_description,
     long_description_content_type='text/markdown',
-    url='https://github.com/precice/micro-manager',
+    url='https://precice.org/tooling-micro-manager-overview.html',
     entry_points={
         'console_scripts': ['micro_manager=micro_manager.micro_manager:main']},
     author='Ishaan Desai',
@@ -27,7 +26,7 @@
     packages=find_packages(
         exclude=['examples']),
     install_requires=[
-        'pyprecice>=2.3.0',
+        'pyprecice==2.5.0.4',
         'numpy>=1.13.3',
         'mpi4py'],
     test_suite='tests',


Test patch:
diff --git a/.github/workflows/run-adaptivity-test.yml b/.github/workflows/run-adaptivity-test.yml
new file mode 100644
index 00000000..b5ec9145
--- /dev/null
+++ b/.github/workflows/run-adaptivity-test.yml
@@ -0,0 +1,105 @@
+name: Test adaptivity
+on:
+  push:
+    branches:
+      - main
+      - develop
+  pull_request:
+    branches:
+      - "*"
+jobs:  
+  integration_test:
+    name: Run integration test
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install dependencies
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          python -m pip install --upgrade pip
+          pip install setuptools wheel
+
+      - name: Install Micro Manager
+        working-directory: micro-manager
+        run: pip3 install --user .
+
+      - name: Run integration test with local adaptivity
+        timeout-minutes: 3
+        working-directory: micro-manager/tests/integration/test_unit_cube
+        run: python3 unit_cube.py & python3 run_micro_manager.py --config micro-manager-config-local-adaptivity.json
+
+      - name: Run integration test serially with global adaptivity
+        timeout-minutes: 3
+        working-directory: micro-manager/tests/integration/test_unit_cube
+        run: python3 unit_cube.py & python3 run_micro_manager.py --config micro-manager-config-global-adaptivity.json
+
+  unit_tests_serial:
+    name: Run unit tests (serial variants)
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install Dependencies
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          python -m pip install --upgrade pip
+          pip install setuptools wheel
+
+      - name: Install Micro Manager
+        working-directory: micro-manager
+        run: pip3 install --user .
+
+      - name: Run unit tests
+        working-directory: micro-manager/tests/unit
+        run: python3 -m unittest test_adaptivity_serial.py
+
+  unit_tests_parallel:
+    name: Run unit tests (parallel variants)
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install sudo for MPI
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install sudo
+
+      - name: Use mpi4py
+        uses: mpi4py/setup-mpi@v1
+
+      - name: Add user precice
+        run: useradd -m -s /bin/bash precice
+
+      - name: Install Dependencies
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          su -c "python -m pip install --upgrade pip" precice
+          su -c "pip install setuptools wheel twine" precice
+
+      - name: Install Micro Manager
+        working-directory: micro-manager
+        run: su -c "pip3 install --user ." precice
+
+      - name: Run unit tests
+        working-directory: micro-manager/tests/unit
+        run: su -c "mpiexec -n 2 python3 -m unittest test_adaptivity_parallel.py" precice
diff --git a/.github/workflows/run-domain-decomposition-tests.yml b/.github/workflows/run-domain-decomposition-tests.yml
new file mode 100644
index 00000000..444ebff3
--- /dev/null
+++ b/.github/workflows/run-domain-decomposition-tests.yml
@@ -0,0 +1,83 @@
+name: Test domain decomposition
+on:
+  push:
+    branches:
+      - main
+      - develop
+  pull_request:
+    branches:
+      - "*"
+jobs:  
+  integration_test:
+    name: Run integration tests
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install sudo for MPI
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install sudo
+
+      - name: Use mpi4py
+        uses: mpi4py/setup-mpi@v1
+
+      - name: Add user precice
+        run: useradd -m -s /bin/bash precice
+
+      - name: Install Dependencies
+        working-directory: micro-manager
+        run: |
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          su -c "python -m pip install --upgrade pip" precice
+          su -c "pip install setuptools wheel twine" precice
+
+      - name: Install micro-manager
+        working-directory: micro-manager
+        run: su -c "pip3 install --user ." precice
+
+      - name: Run integration test (variant 1)
+        timeout-minutes: 3
+        working-directory: micro-manager/tests/integration
+        run: |
+          chown -R precice test_unit_cube/
+          cd test_unit_cube/
+          su -c "mpiexec -n 2 python3 run_micro_manager.py --config micro-manager-config-parallel-1.json & python3 unit_cube.py" precice
+      
+      - name: Run integration test (variant 2)
+        timeout-minutes: 3
+        working-directory: micro-manager/tests/integration
+        run: |
+          cd test_unit_cube/
+          su -c "mpiexec -n 6 --oversubscribe python3 run_micro_manager.py --config micro-manager-config-parallel-2.json & python3 unit_cube.py" precice
+
+  unit_tests:
+    name: Run unit tests
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install Dependencies
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          python -m pip install --upgrade pip
+          pip install setuptools wheel
+
+      - name: Install Micro Manager
+        working-directory: micro-manager
+        run: pip3 install --user .
+
+      - name: Run unit tests
+        working-directory: micro-manager/tests/unit
+        run: python3 -m unittest test_domain_decomposition.py
diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml
new file mode 100644
index 00000000..daa0d23b
--- /dev/null
+++ b/.github/workflows/run-unit-tests.yml
@@ -0,0 +1,29 @@
+name: Run unit tests
+on:
+  push:
+    branches:
+      - main
+      - develop
+  pull_request:
+    branches:
+      - "*"
+jobs:
+  unit-tests:
+    runs-on: ubuntu-latest
+    container: precice/precice
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          path: micro-manager
+
+      - name: Install Micro Manager and uninstall pyprecice
+        working-directory: micro-manager
+        run: |
+          apt-get -qq update
+          apt-get -qq install python3-dev python3-pip git python-is-python3 pkg-config
+          python3 -m pip install --user .
+          python3 -m pip uninstall -y pyprecice
+
+      - name: Run unit tests
+        working-directory: micro-manager/tests/unit
+        run: python3 -m unittest test_micro_manager.py
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..8e56cf89
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,16 @@
+# Tests
+
+This folder contains everything related to testing. The tests are split into two categories:
+
+* `unit` contains unit tests that only check independent functions and modules. These tests do not use preCICE. Instead a mocked `precice.py` is used.
+* `integration` contains an integration test which uses preCICE and a Micro Manager.
+
+## Unit tests
+
+The unit tests can be run with [unittest](https://docs.python.org/3/library/unittest.html). For example, the tests for domain decomposition can be run with `python -m unittest test_domain_decomposition.py`.
+
+The tests in `test_adaptivity_parallel.py` are designed to be run with 2 MPI processes. This can be run in the following way: `mpiexec -n 2 python -m unittest test_adaptivity_parallel.py`.
+
+## Integration test
+
+The integration test is a macro-micro case where the macro simulation is a unit cube. The micro simulation is a dummy which increments the data received from preCICE.
diff --git a/tests/integration/test_unit_cube/clean-test.sh b/tests/integration/test_unit_cube/clean-test.sh
new file mode 100755
index 00000000..5a0def56
--- /dev/null
+++ b/tests/integration/test_unit_cube/clean-test.sh
@@ -0,0 +1,10 @@
+rm -fv *-events-summary.json
+rm -fv *-events.json
+rm -fv *.log
+rm -r -fv precice-run/
+rm -fv *.vtk
+rm -fv *.out
+rm -fv *.err
+rm -fv output/*.vtu
+rm -fv output/*.pvtu
+rm -r -fv __pycache__
diff --git a/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity-parallel.json b/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity-parallel.json
new file mode 100644
index 00000000..4edbd391
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity-parallel.json
@@ -0,0 +1,21 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-cube-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+        "decomposition": [2, 1, 1],
+        "adaptivity": {
+            "type": "global",
+            "data": ["macro-scalar-data", "micro-vector-data"],
+            "history_param": 0.5,
+            "coarsening_constant": 0.3,
+            "refining_constant": 0.4,
+            "every_implicit_iteration": "True"
+        }
+    }
+}
diff --git a/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity.json b/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity.json
new file mode 100644
index 00000000..808bcae4
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro-manager-config-global-adaptivity.json
@@ -0,0 +1,20 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-cube-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+        "adaptivity": {
+            "type": "global",
+            "data": ["macro-scalar-data", "macro-vector-data"],
+            "history_param": 0.5,
+            "coarsening_constant": 0.3,
+            "refining_constant": 0.4,
+            "every_implicit_iteration": "True"
+        }
+    }
+}
diff --git a/tests/integration/test_unit_cube/micro-manager-config-local-adaptivity.json b/tests/integration/test_unit_cube/micro-manager-config-local-adaptivity.json
new file mode 100644
index 00000000..f6884e83
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro-manager-config-local-adaptivity.json
@@ -0,0 +1,20 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-cube-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+        "adaptivity": {
+            "type": "local",
+            "data": ["macro-scalar-data", "macro-vector-data"],
+            "history_param": 0.5,
+            "coarsening_constant": 0.3,
+            "refining_constant": 0.4,
+            "every_implicit_iteration": "True"
+        }
+    }
+}
diff --git a/tests/integration/test_unit_cube/micro-manager-config-parallel-1.json b/tests/integration/test_unit_cube/micro-manager-config-parallel-1.json
new file mode 100644
index 00000000..bcbe34b6
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro-manager-config-parallel-1.json
@@ -0,0 +1,16 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-cube-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+	    "decomposition": [1, 1, 2]
+    },
+    "diagnostics": {
+        "output_micro_sim_solve_time": "True"
+    }
+}
diff --git a/tests/integration/test_unit_cube/micro-manager-config-parallel-2.json b/tests/integration/test_unit_cube/micro-manager-config-parallel-2.json
new file mode 100644
index 00000000..63b5c1f0
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro-manager-config-parallel-2.json
@@ -0,0 +1,16 @@
+{
+    "micro_file_name": "micro_dummy",
+    "coupling_params": {
+        "config_file_name": "precice-config.xml",
+        "macro_mesh_name": "macro-cube-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0, 1, 0, 1, 0, 1],
+	    "decomposition": [1, 2, 3]
+    },
+    "diagnostics": {
+        "output_micro_sim_solve_time": "True"
+    }
+}
diff --git a/tests/integration/test_unit_cube/micro_dummy.py b/tests/integration/test_unit_cube/micro_dummy.py
new file mode 100644
index 00000000..59bef26c
--- /dev/null
+++ b/tests/integration/test_unit_cube/micro_dummy.py
@@ -0,0 +1,35 @@
+"""
+Micro simulation
+In this script we solve a dummy micro problem to just show the working of the macro-micro coupling
+"""
+
+
+class MicroSimulation:
+
+    def __init__(self):
+        """
+        Constructor of MicroSimulation class.
+        """
+        self._micro_scalar_data = None
+        self._micro_vector_data = None
+        self._checkpoint = None
+
+    def initialize(self):
+        self._micro_scalar_data = 0
+        self._micro_vector_data = []
+        self._checkpoint = 0
+
+    def solve(self, macro_data, dt):
+        assert dt != 0
+        self._micro_vector_data = macro_data["macro-vector-data"]
+        self._micro_scalar_data = macro_data["macro-scalar-data"]
+
+        return {"micro-scalar-data": self._micro_scalar_data,
+                "micro-vector-data": self._micro_vector_data}
+
+    def get_state(self):
+        return [self._micro_scalar_data, self._micro_vector_data]
+
+    def set_state(self, state):
+        self._micro_scalar_data = state[0]
+        self._micro_vector_data = state[0]
diff --git a/tests/integration/test_unit_cube/precice-config.xml b/tests/integration/test_unit_cube/precice-config.xml
new file mode 100644
index 00000000..e98d801f
--- /dev/null
+++ b/tests/integration/test_unit_cube/precice-config.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+
+<precice-configuration>
+
+    <log>
+	<sink type="stream" output="stdout"  filter= "%Severity% > debug" enabled="true" />
+    </log>
+  
+  <solver-interface experimental="true" dimensions="3">
+    
+    <data:scalar name="macro-scalar-data"/>
+    <data:vector name="macro-vector-data"/>
+    <data:scalar name="micro-scalar-data"/>
+    <data:vector name="micro-vector-data"/>
+    <data:scalar name="micro_sim_time"/>
+    <data:scalar name="active_state"/>
+    <data:scalar name="active_steps"/>
+
+    <mesh name="macro-cube-mesh">
+       <use-data name="macro-scalar-data"/>
+       <use-data name="macro-vector-data"/>
+       <use-data name="micro-scalar-data"/>
+       <use-data name="micro-vector-data"/>
+       <use-data name="micro_sim_time"/>
+       <use-data name="active_state"/>
+       <use-data name="active_steps"/>
+    </mesh>
+
+    <participant name="macro-cube">
+      <use-mesh name="macro-cube-mesh" provide="yes"/>
+      <read-data name="micro-scalar-data" mesh="macro-cube-mesh"/>
+      <read-data name="micro-vector-data" mesh="macro-cube-mesh"/>
+      <write-data name="macro-scalar-data" mesh="macro-cube-mesh"/>
+      <write-data name="macro-vector-data" mesh="macro-cube-mesh"/>
+    </participant>
+
+    <participant name="Micro-Manager">
+      <use-mesh name="macro-cube-mesh" from="macro-cube" direct-access="true" safety-factor="0"/>
+      <read-data name="macro-scalar-data" mesh="macro-cube-mesh"/>
+      <read-data name="macro-vector-data" mesh="macro-cube-mesh"/>
+      <write-data name="micro-scalar-data" mesh="macro-cube-mesh"/>
+      <write-data name="micro-vector-data" mesh="macro-cube-mesh"/>
+      <write-data name="micro_sim_time" mesh="macro-cube-mesh"/>
+      <write-data name="active_state" mesh="macro-cube-mesh"/>
+      <write-data name="active_steps" mesh="macro-cube-mesh"/>
+      <export:vtu directory="output"/>
+    </participant>
+
+    <m2n:sockets from="Micro-Manager" to="macro-cube"/>
+
+    <coupling-scheme:serial-explicit>
+       <participants first="Micro-Manager" second="macro-cube"/>
+       <max-time value="10"/>
+       <time-window-size value="1"/>
+       <exchange data="micro-scalar-data" mesh="macro-cube-mesh" from="Micro-Manager" to="macro-cube"/>
+       <exchange data="micro-vector-data" mesh="macro-cube-mesh" from="Micro-Manager" to="macro-cube"/>
+       <exchange data="macro-scalar-data" mesh="macro-cube-mesh" from="macro-cube" to="Micro-Manager" initialize="yes"/>
+       <exchange data="macro-vector-data" mesh="macro-cube-mesh" from="macro-cube" to="Micro-Manager" initialize="yes"/>
+    </coupling-scheme:serial-explicit>
+
+  </solver-interface>
+</precice-configuration>
diff --git a/tests/integration/test_unit_cube/run_micro_manager.py b/tests/integration/test_unit_cube/run_micro_manager.py
new file mode 100644
index 00000000..a3d0dd6c
--- /dev/null
+++ b/tests/integration/test_unit_cube/run_micro_manager.py
@@ -0,0 +1,16 @@
+"""
+Script to run the Micro Manager
+"""
+
+from micro_manager import MicroManager
+from argparse import ArgumentParser
+
+parser = ArgumentParser()
+parser.add_argument("--config", help="Path to the micro manager configuration file")
+args = parser.parse_args()
+
+manager = MicroManager(args.config)
+
+manager.initialize()
+
+manager.solve()
diff --git a/tests/integration/test_unit_cube/unit_cube.py b/tests/integration/test_unit_cube/unit_cube.py
new file mode 100644
index 00000000..1e65ac43
--- /dev/null
+++ b/tests/integration/test_unit_cube/unit_cube.py
@@ -0,0 +1,148 @@
+#! /usr/bin/env python3
+#
+
+import numpy as np
+import precice
+
+
+def main():
+    """
+    Dummy macro simulation which is coupled to a set of micro simulations via preCICE and the Micro Manager
+    """
+    n = n_checkpoint = 0
+    t = t_checkpoint = 0
+
+    t_end = 10
+
+    # preCICE setup
+    interface = precice.Interface("macro-cube", "precice-config.xml", 0, 1)
+
+    # define coupling meshes
+    read_mesh_name = write_mesh_name = "macro-cube-mesh"
+    read_mesh_id = interface.get_mesh_id(read_mesh_name)
+    read_data_names = {"micro-scalar-data": 0, "micro-vector-data": 1}
+
+    write_mesh_id = interface.get_mesh_id(write_mesh_name)
+    write_data_names = {"macro-scalar-data": 0, "macro-vector-data": 1}
+
+    # Coupling mesh - unit cube with 5 points in each direction
+    np_axis = 2
+    x_coords, y_coords, z_coords = np.meshgrid(
+        np.linspace(0, 1, np_axis),
+        np.linspace(0, 1, np_axis),
+        np.linspace(0, 1, np_axis)
+    )
+
+    nv = np_axis ** interface.get_dimensions()
+    coords = np.zeros((nv, interface.get_dimensions()))
+
+    write_scalar_data = np.zeros(nv)
+    write_vector_data = np.zeros((nv, interface.get_dimensions()))
+
+    # Define unit cube coordinates
+    for z in range(np_axis):
+        for y in range(np_axis):
+            for x in range(np_axis):
+                n = x + y * np_axis + z * np_axis * np_axis
+                coords[n, 0] = x_coords[x, y, z]
+                coords[n, 1] = y_coords[x, y, z]
+                coords[n, 2] = z_coords[x, y, z]
+
+    # Define initial data to write to preCICE
+    scalar_value = 1.0
+    vector_value = [2.0, 3.0, 4.0]
+    for z in range(np_axis):
+        for y in range(np_axis):
+            for x in range(np_axis):
+                n = x + y * np_axis + z * np_axis * np_axis
+                write_scalar_data[n] = scalar_value
+                write_vector_data[n, 0] = vector_value[0]
+                write_vector_data[n, 1] = vector_value[1]
+                write_vector_data[n, 2] = vector_value[2]
+        scalar_value += 1
+        vector_value = [x + 1 for x in vector_value]
+
+    # Define Gauss points on entire domain as coupling mesh
+    vertex_ids = interface.set_mesh_vertices(read_mesh_id, coords)
+
+    read_data_ids = dict()
+    # coupling data
+    for name, dim in read_data_names.items():
+        read_data_ids[name] = interface.get_data_id(name, read_mesh_id)
+
+    write_data_ids = dict()
+    for name, dim in write_data_names.items():
+        write_data_ids[name] = interface.get_data_id(name, write_mesh_id)
+
+    # initialize preCICE
+    dt = interface.initialize()
+
+    # Set initial data to write to preCICE
+    if interface.is_action_required(precice.action_write_initial_data()):
+        for name, dim in write_data_names.items():
+            if dim == 0:
+                interface.write_block_scalar_data(write_data_ids[name], vertex_ids, write_scalar_data)
+            elif dim == 1:
+                interface.write_block_vector_data(write_data_ids[name], vertex_ids, write_vector_data)
+        interface.mark_action_fulfilled(precice.action_write_initial_data())
+
+    interface.initialize_data()
+
+    # time loop
+    while interface.is_coupling_ongoing():
+        # write checkpoint
+        if interface.is_action_required(precice.action_write_iteration_checkpoint()):
+            print("Saving macro state")
+            t_checkpoint = t
+            n_checkpoint = n
+            interface.mark_action_fulfilled(precice.action_write_iteration_checkpoint())
+
+        # Read data from preCICE
+        for name, dim in read_data_names.items():
+            if dim == 0:
+                read_scalar_data = interface.read_block_scalar_data(read_data_ids[name], vertex_ids)
+            elif dim == 1:
+                read_vector_data = interface.read_block_vector_data(read_data_ids[name], vertex_ids)
+
+        # Set the read data as the write data with an increment
+        write_scalar_data = read_scalar_data + 1
+        write_vector_data = read_vector_data + 1
+
+        # Define new data to write to preCICE midway through the simulation
+        if t == t_end / 2:
+            scalar_value = 1.0
+            vector_value = [2.0, 3.0, 4.0]
+            for z in range(np_axis):
+                for y in range(np_axis):
+                    for x in range(np_axis):
+                        n = x + y * np_axis + z * np_axis * np_axis
+                        write_scalar_data[n] = scalar_value
+                        write_vector_data[n, 0] = vector_value[0]
+                        write_vector_data[n, 1] = vector_value[1]
+                        write_vector_data[n, 2] = vector_value[2]
+
+        # Write data to preCICE
+        for name, dim in write_data_names.items():
+            if dim == 0:
+                interface.write_block_scalar_data(write_data_ids[name], vertex_ids, write_scalar_data)
+            elif dim == 1:
+                interface.write_block_vector_data(write_data_ids[name], vertex_ids, write_vector_data)
+
+        # do the coupling
+        dt = interface.advance(dt)
+
+        # advance variables
+        n += 1
+        t += dt
+
+        if interface.is_action_required(precice.action_read_iteration_checkpoint()):
+            print("Reverting to old macro state")
+            t = t_checkpoint
+            n = n_checkpoint
+            interface.mark_action_fulfilled(precice.action_read_iteration_checkpoint())
+
+    interface.finalize()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/micro-manager-config.json b/tests/unit/micro-manager-config.json
new file mode 100644
index 00000000..42a4181f
--- /dev/null
+++ b/tests/unit/micro-manager-config.json
@@ -0,0 +1,25 @@
+{
+    "micro_file_name": "test_micro_manager",
+    "coupling_params": {
+        "config_file_name": "dummy-config.xml",
+        "macro_mesh_name": "dummy-macro-mesh",
+        "read_data_names": {"macro-scalar-data": "scalar", "macro-vector-data": "vector"},
+        "write_data_names": {"micro-scalar-data": "scalar", "micro-vector-data": "vector"}
+    },
+    "simulation_params": {
+        "macro_domain_bounds": [0.0, 25.0, 0.0, 25.0, 0.0, 25.0],
+        "adaptivity": {
+            "type": "local",
+            "data": ["macro-scalar-data", "macro-vector-data"],
+            "history_param": 0.5,
+            "coarsening_constant": 0.3,
+            "refining_constant": 0.4,
+            "every_implicit_iteration": "False",
+            "similarity_measure": "L1"
+        }
+    },
+    "diagnostics": {
+        "output_micro_sim_solve_time": "True",
+        "micro_output_n": 10
+    }
+}
diff --git a/tests/unit/precice.py b/tests/unit/precice.py
new file mode 100644
index 00000000..210bf9ba
--- /dev/null
+++ b/tests/unit/precice.py
@@ -0,0 +1,78 @@
+# This file mocks pyprecice, the Python bindings for preCICE, and is used _only_ for unit testing the Micro Manager.
+import numpy as np
+
+
+def action_write_initial_data():
+    return "ActionWriteInitialData"
+
+
+def action_write_iteration_checkpoint():
+    return "ActionWriteIterationCheckpoint"
+
+
+class Interface:
+    def __init__(self, solver_name, config_file_name, solver_process_index, solver_process_size):
+        self.read_write_vector_buffer = []
+        self.read_write_scalar_buffer = []
+
+    def get_mesh_id(self, mesh_name):
+        return 0
+
+    def get_data_id(self, data_name, mesh_id):
+        return int(data_name == "micro-scalar-data")
+
+    def get_dimensions(self):
+        return 3
+
+    def set_mesh_access_region(self, mesh_id, bounds):
+        pass
+
+    def initialize(self):
+        return 0.1  # dt
+
+    def get_mesh_vertices_and_ids(self, mesh_id):
+        return np.array([0, 1, 2, 3]), np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
+
+    def is_action_required(self, action):
+        return True
+
+    def mark_action_fulfilled(self, action):
+        pass
+
+    def initialize_data(self):
+        pass
+
+    def write_block_scalar_data(self, data_id, vertex_ids, data):
+        if data_id == 1:  # micro-scalar-data not micro_sim_time
+            self.read_write_scalar_buffer = data
+
+    def write_block_vector_data(self, data_id, vertex_ids, data):
+        self.read_write_vector_buffer = data
+
+    def write_scalar_data(self, data_id, vertex_id, data):
+        pass
+
+    def write_vector_data(self, data_id, vertex_id, data):
+        pass
+
+    def read_block_scalar_data(self, data_id, vertex_ids):
+        return self.read_write_scalar_buffer
+
+    def read_block_vector_data(self, data_id, vertex_ids):
+        return self.read_write_vector_buffer
+
+    def read_scalar_data(self, data_id, vertex_id):
+        return 0
+
+    def read_vector_data(self, data_id, vertex_id):
+        return [0, 0]
+
+    def finalize(self):
+        pass
+
+    def is_coupling_ongoing(self):
+        yield True
+        yield False
+
+    def advance(self, dt):
+        pass
diff --git a/tests/unit/test_adaptivity_parallel.py b/tests/unit/test_adaptivity_parallel.py
new file mode 100644
index 00000000..f237feb8
--- /dev/null
+++ b/tests/unit/test_adaptivity_parallel.py
@@ -0,0 +1,117 @@
+from unittest import TestCase
+from unittest.mock import MagicMock
+from micro_manager.adaptivity.global_adaptivity import GlobalAdaptivityCalculator
+import numpy as np
+from mpi4py import MPI
+
+
+class TestGlobalAdaptivity(TestCase):
+    def setUp(self):
+        self._comm = MPI.COMM_WORLD
+        self._rank = self._comm.Get_rank()
+        self._size = self._comm.Get_size()
+
+    def test_update_inactive_sims_global_adaptivity(self):
+        """
+        Test functionality to update inactive simulations in a particular setting, for a global adaptivity setting.
+        Run this test in parallel using MPI with 2 ranks.
+        """
+        if self._rank == 0:
+            is_sim_on_this_rank = [True, True, True, False, False]
+            global_ids = [0, 1, 2]
+        elif self._rank == 1:
+            is_sim_on_this_rank = [False, False, False, True, True]
+            global_ids = [3, 4]
+
+        is_sim_active = np.array([False, False, True, True, False])
+        rank_of_sim = [0, 0, 0, 1, 1]
+        sim_is_associated_to = [3, 3, -2, -2, 2]
+        expected_is_sim_active = np.array([True, False, True, True, True])
+        expected_sim_is_associated_to = [-2, 3, -2, -2, -2]
+
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1")
+        adaptivity_controller = GlobalAdaptivityCalculator(
+            configurator,
+            MagicMock(),
+            is_sim_on_this_rank,
+            rank_of_sim,
+            global_ids,
+            rank=self._rank,
+            comm=self._comm)
+
+        # Force the activation of sim #0 and #4
+        def check_for_activation(i, sim_dists, active):
+            if i == 0 or i == 4:
+                return True
+            else:
+                return False
+
+        adaptivity_controller._check_for_activation = check_for_activation
+
+        class MicroSimulation():
+            def __init__(self, global_id) -> None:
+                self._global_id = global_id
+                self._state = [global_id] * 3
+
+            def get_global_id(self):
+                return self._global_id
+
+            def set_state(self, state):
+                self._state = state
+
+            def get_state(self):
+                return self._state.copy()
+
+        dummy_micro_sims = []
+        for i in global_ids:
+            dummy_micro_sims.append(MicroSimulation(i))
+
+        is_sim_active, sim_is_associated_to = adaptivity_controller._update_inactive_sims(
+            np.array([0]), is_sim_active, sim_is_associated_to, dummy_micro_sims)
+
+        self.assertTrue(np.array_equal(expected_is_sim_active, is_sim_active))
+        self.assertTrue(np.array_equal(expected_sim_is_associated_to, sim_is_associated_to))
+
+        if self._rank == 0:
+            self.assertTrue(np.array_equal([3, 3, 3], dummy_micro_sims[0].get_state()))
+        elif self._rank == 1:
+            self.assertTrue(np.array_equal([2, 2, 2], dummy_micro_sims[1].get_state()))
+
+    def test_communicate_micro_output(self):
+        """
+        Test functionality to communicate micro output from active sims to their associated inactive sims, for a global adaptivity setting.
+        Run this test in parallel using MPI with 2 ranks.
+        """
+        output_0 = {"data0.1": 1.0, "data0.2": [1.0, 2.0]}
+        output_1 = {"data1.1": 10.0, "data1.2": [10.0, 20.0]}
+
+        if self._rank == 0:
+            is_sim_on_this_rank = [True, True, True, False, False]
+            global_ids = [0, 1, 2]
+            sim_output = [None, None, output_0]
+            expected_sim_output = [output_1, output_1, output_0]
+        elif self._rank == 1:
+            is_sim_on_this_rank = [False, False, False, True, True]
+            global_ids = [3, 4]
+            sim_output = [output_1, None]
+            expected_sim_output = [output_1, output_0]
+
+        is_sim_active = np.array([False, False, True, True, False])
+        rank_of_sim = [0, 0, 0, 1, 1]
+        sim_is_associated_to = [3, 3, -2, -2, 2]
+
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1")
+        adaptivity_controller = GlobalAdaptivityCalculator(
+            configurator,
+            MagicMock(),
+            is_sim_on_this_rank,
+            rank_of_sim,
+            global_ids,
+            rank=self._rank,
+            comm=self._comm)
+
+        adaptivity_controller.communicate_micro_output(is_sim_active, sim_is_associated_to, sim_output)
+
+        self.assertTrue(np.array_equal(expected_sim_output, sim_output))
diff --git a/tests/unit/test_adaptivity_serial.py b/tests/unit/test_adaptivity_serial.py
new file mode 100644
index 00000000..9aaa7227
--- /dev/null
+++ b/tests/unit/test_adaptivity_serial.py
@@ -0,0 +1,223 @@
+from unittest import TestCase
+from unittest.mock import MagicMock
+from micro_manager.adaptivity.adaptivity import AdaptivityCalculator
+from micro_manager.adaptivity.local_adaptivity import LocalAdaptivityCalculator
+from micro_manager.config import Config
+import numpy as np
+from math import exp
+
+
+class TestLocalAdaptivity(TestCase):
+    def setUp(self):
+        self._number_of_sims = 5
+        self._dt = 0.1
+        self._dim = 3
+
+        self._micro_scalar_data = np.zeros(5)
+        np.put(self._micro_scalar_data, [0, 1, 2], [3.0, 3.0, 3.0])
+        np.put(self._micro_scalar_data, [3, 4], [5.0, 5.0])
+
+        self._micro_vector_data = np.zeros((5, 3))
+        # First three simulations have similar micro_vector_data
+        for i in range(3):
+            self._micro_vector_data[i, :] = 5.0
+
+        # Last two simulations have similar micro_vector_data
+        for i in range(3, 5):
+            self._micro_vector_data[i, :] = 10.0
+
+        self._macro_scalar_data = np.zeros(5)
+        np.put(self._micro_scalar_data, [0, 1, 2], [130.0, 130.0, 130.0])
+        np.put(self._micro_scalar_data, [3, 4], [250.0, 250.0])
+
+        self._macro_vector_data = np.zeros((5, 3))
+        # First three simulations have similar micro_vector_data
+        for i in range(3):
+            self._macro_vector_data[i, :] = 100.0
+
+        # Last two simulations have similar micro_vector_data
+        for i in range(3, 5):
+            self._macro_vector_data[i, :] = 300.0
+
+        # Adaptivity constants
+        self._refine_const = 0.5
+        self._coarse_const = 0.5
+        self._coarse_tol = 0.2
+
+        self._data_diff = np.zeros((self._number_of_sims, self._number_of_sims))
+        for i in range(self._number_of_sims):
+            for j in range(self._number_of_sims):
+                dist = abs(self._micro_scalar_data[i] - self._micro_scalar_data[j])
+                dist += abs(self._macro_scalar_data[i] - self._macro_scalar_data[j])
+                for d in range(self._dim):
+                    dist += abs(self._micro_vector_data[i, d] - self._micro_vector_data[j, d])
+                    dist += abs(self._macro_vector_data[i, d] - self._macro_vector_data[j, d])
+                self._data_diff[i, j] = dist
+
+        self._similarity_dists = self._dt * self._data_diff
+
+    def test_get_similarity_dists(self):
+        """
+        Test functionality of calculating the similarity distance matrix in class AdaptivityCalculator.
+        """
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value='L1')
+        adaptivity_controller = AdaptivityCalculator(configurator, logger=MagicMock())
+        adaptivity_controller._hist_param = 0.5
+        adaptivity_controller._adaptivity_data_names = [
+            "micro-scalar-data",
+            "micro-vector-data",
+            "macro-scalar-data",
+            "macro-vector-data"]
+
+        similarity_dists = np.zeros((self._number_of_sims, self._number_of_sims))
+
+        adaptivity_data = dict()
+        adaptivity_data["micro-scalar-data"] = self._micro_scalar_data
+        adaptivity_data["micro-vector-data"] = self._micro_vector_data
+        adaptivity_data["macro-scalar-data"] = self._macro_scalar_data
+        adaptivity_data["macro-vector-data"] = self._macro_vector_data
+
+        similarity_dists = adaptivity_controller._get_similarity_dists(
+            self._dt, self._similarity_dists, adaptivity_data)
+
+        expected_similarity_dists = exp(-adaptivity_controller._hist_param * self._dt) * \
+            self._similarity_dists + self._dt * self._data_diff
+
+        self.assertTrue(np.array_equal(expected_similarity_dists, similarity_dists))
+
+    def test_update_active_sims(self):
+        """
+        Test functionality of updating active simulations in class AdaptivityCalculator.
+        """
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1")
+        adaptivity_controller = AdaptivityCalculator(configurator, logger=MagicMock())
+        adaptivity_controller._refine_const = self._refine_const
+        adaptivity_controller._coarse_const = self._coarse_const
+        adaptivity_controller._adaptivity_data_names = ["macro-scalar-data", "macro-vector-data"]
+
+        # Third and fifth micro sim are active, rest are inactive
+        expected_is_sim_active = np.array([False, False, True, False, True])
+
+        is_sim_active = np.array([True, True, True, True, True])  # Activate all micro sims before calling functionality
+
+        class MicroSimulation():
+            pass
+
+        dummy_micro_sims = []
+        for _ in range(self._number_of_sims):
+            dummy_micro_sims.append(MicroSimulation())
+
+        is_sim_active = adaptivity_controller._update_active_sims(
+            self._similarity_dists, is_sim_active)
+
+        self.assertTrue(np.array_equal(expected_is_sim_active, is_sim_active))
+
+    def test_adaptivity_norms(self):
+        """
+        Test functionality for calculating similarity criteria between pairs of simulations using different norms in class AdaptivityCalculator.
+        """
+        logger = MagicMock()
+        calc = AdaptivityCalculator(Config(logger, 'micro-manager-config.json'), logger)
+
+        fake_data = np.array([[1], [2], [3]])
+        self.assertTrue(np.allclose(calc._l1(fake_data), np.array([[0, 1, 2], [1, 0, 1], [2, 1, 0]])))
+        # norm taken over last axis -> same as before
+        self.assertTrue(np.allclose(calc._l2(fake_data), np.array([[0, 1, 2], [1, 0, 1], [2, 1, 0]])))
+        self.assertTrue(np.allclose(calc._l1rel(fake_data), np.array(
+            [[0, 0.5, 2 / 3], [0.5, 0, 1 / 3], [2 / 3, 1 / 3, 0]])))
+        self.assertTrue(np.allclose(calc._l2rel(fake_data), np.array(
+            [[0, 0.5, 2 / 3], [0.5, 0, 1 / 3], [2 / 3, 1 / 3, 0]])))
+
+        fake_2d_data = np.array([[1, 2], [3, 4]])
+        self.assertTrue(np.allclose(calc._l1(fake_2d_data), np.array([[0, 4], [4, 0]])))
+        self.assertTrue(np.allclose(calc._l2(fake_2d_data), np.array([[0, np.sqrt((1 - 3)**2 + (2 - 4)**2)],
+                                                                      [np.sqrt((1 - 3)**2 + (2 - 4)**2), 0]])))
+        self.assertTrue(np.allclose(calc._l1rel(fake_2d_data), np.array(
+            [[0, abs((1 - 3) / max(1, 3) + (2 - 4) / max(2, 4))], [abs((1 - 3) / max(1, 3) + (2 - 4) / max(2, 4)), 0]])))
+        self.assertTrue(np.allclose(calc._l2rel(fake_2d_data), np.array([[0, np.sqrt(
+            (1 - 3)**2 / max(1, 3)**2 + (2 - 4)**2 / max(2, 4)**2)], [np.sqrt((1 - 3)**2 / max(1, 3)**2 + (2 - 4)**2 / max(2, 4)**2), 0]])))
+
+    def test_associate_active_to_inactive(self):
+        """
+        Test functionality to associate inactive sims to active ones, in the class AdaptivityCalculator.
+        """
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1")
+        adaptivity_controller = AdaptivityCalculator(configurator, logger=MagicMock())
+        adaptivity_controller._refine_const = self._refine_const
+        adaptivity_controller._coarse_const = self._coarse_const
+        adaptivity_controller._adaptivity_data_names = ["macro-scalar-data", "macro-vector-data"]
+
+        is_sim_active = np.array([True, False, False, True, False])
+        expected_sim_is_associated_to = np.array([-2, 0, 0, -2, 3])
+
+        class MicroSimulation():
+            def __init__(self, global_id):
+                self._global_id = global_id
+
+            def get_global_id(self):
+                return self._global_id
+
+        dummy_micro_sims = []
+        for i in range(self._number_of_sims):
+            dummy_micro_sims.append(MicroSimulation(i))
+
+        sim_is_associated_to = np.array([-2, -2, -2, -2, -2])
+
+        sim_is_associated_to = adaptivity_controller._associate_inactive_to_active(
+            self._similarity_dists, is_sim_active, sim_is_associated_to)
+
+        self.assertTrue(np.array_equal(expected_sim_is_associated_to, sim_is_associated_to))
+
+    def test_update_inactive_sims_local_adaptivity(self):
+        """
+        Test functionality to update inactive simulations in a particular setting, for a local adaptivity setting.
+        """
+        configurator = MagicMock()
+        configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1")
+        adaptivity_controller = LocalAdaptivityCalculator(configurator, logger=MagicMock())
+        adaptivity_controller._refine_const = self._refine_const
+        adaptivity_controller._coarse_const = self._coarse_const
+        adaptivity_controller._adaptivity_data_names = ["macro-scalar-data", "macro-vector-data"]
+
+        # Third and fifth micro sim are active, rest are deactivate
+        expected_is_sim_active = np.array([True, False, False, True, False])
+        expected_sim_is_associated_to = np.array([-2, 0, 0, -2, 3])
+
+        similarity_dists = np.zeros((self._number_of_sims, self._number_of_sims))
+        for i in range(self._number_of_sims):
+            for j in range(self._number_of_sims):
+                similarity_dist = abs(self._micro_scalar_data[i] - self._micro_scalar_data[j])
+                similarity_dist += abs(self._macro_scalar_data[i] - self._macro_scalar_data[j])
+                for d in range(self._dim):
+                    similarity_dist += abs(self._micro_vector_data[i, d] - self._micro_vector_data[j, d])
+                    similarity_dist += abs(self._macro_vector_data[i, d] - self._macro_vector_data[j, d])
+                similarity_dists[i, j] = self._dt * similarity_dist
+
+        is_sim_active = np.array([True, False, False, False, False])
+        sim_is_associated_to = np.array([-2, 0, 0, 0, 3])
+
+        class MicroSimulation():
+            def get_global_id(self):
+                return 1
+
+            def set_global_id(self, global_id):
+                pass
+
+            def set_state(self, state):
+                pass
+
+            def get_state(self):
+                pass
+
+        dummy_micro_sims = []
+        for i in range(self._number_of_sims):
+            dummy_micro_sims.append(MicroSimulation())
+
+        is_sim_active, sim_is_associated_to = adaptivity_controller._update_inactive_sims(
+            similarity_dists, is_sim_active, sim_is_associated_to, dummy_micro_sims)
+
+        self.assertTrue(np.array_equal(expected_is_sim_active, is_sim_active))
+        self.assertTrue(np.array_equal(expected_sim_is_associated_to, sim_is_associated_to))
diff --git a/tests/unit/test_domain_decomposition.py b/tests/unit/test_domain_decomposition.py
new file mode 100644
index 00000000..a07d996d
--- /dev/null
+++ b/tests/unit/test_domain_decomposition.py
@@ -0,0 +1,49 @@
+from unittest import TestCase
+from unittest.mock import MagicMock
+from micro_manager.domain_decomposition import DomainDecomposer
+import numpy as np
+
+
+class TestDomainDecomposition(TestCase):
+    def setUp(self) -> None:
+        self._logger = MagicMock()
+        self._macro_bounds_3d = [-1, 1, -2, 2, -2, 8]  # Cuboid which is not symmetric around origin
+
+    def test_rank5_outof_10_3d(self):
+        """
+        Test domain decomposition for rank 5 in a setting of axis-wise ranks: [1, 2, 5]
+        """
+        rank = 5
+        size = 10
+        ranks_per_axis = [1, 2, 5]
+        domain_decomposer = DomainDecomposer(self._logger, 3, rank, size)
+        domain_decomposer._dims = 3
+        mesh_bounds = domain_decomposer.decompose_macro_domain(self._macro_bounds_3d, ranks_per_axis)
+
+        self.assertTrue(np.allclose(mesh_bounds, [-1, 1, 0, 2, 2, 4]))
+
+    def test_rank10_out_of_32_3d(self):
+        """
+        Test domain decomposition for rank 10 in a setting of axis-wise ranks: [4, 1, 8]
+        """
+        rank = 10
+        size = 32
+        ranks_per_axis = [4, 1, 8]
+        domain_decomposer = DomainDecomposer(self._logger, 3, rank, size)
+        domain_decomposer._dims = 3
+        mesh_bounds = domain_decomposer.decompose_macro_domain(self._macro_bounds_3d, ranks_per_axis)
+
+        self.assertTrue(np.allclose(mesh_bounds, [0, 0.5, -2, 2, 0.5, 1.75]))
+
+    def test_rank7_out_of_16_3d(self):
+        """
+        Test domain decomposition for rank 7 in a setting of axis-wise ranks: [8, 2, 1]
+        """
+        rank = 7
+        size = 16
+        ranks_per_axis = [8, 2, 1]
+        domain_decomposer = DomainDecomposer(self._logger, 3, rank, size)
+        domain_decomposer._dims = 3
+        mesh_bounds = domain_decomposer.decompose_macro_domain(self._macro_bounds_3d, ranks_per_axis)
+
+        self.assertTrue(np.allclose(mesh_bounds, [0.75, 1, -2, 0, -2, 8]))
diff --git a/tests/unit/test_micro_manager.py b/tests/unit/test_micro_manager.py
new file mode 100644
index 00000000..af5d5fc8
--- /dev/null
+++ b/tests/unit/test_micro_manager.py
@@ -0,0 +1,119 @@
+import numpy as np
+from unittest import TestCase
+from unittest.mock import MagicMock
+import micro_manager
+
+
+class MicroSimulation:
+    def __init__(self):
+        self.very_important_value = 0
+
+    def initialize(self):
+        pass
+
+    def solve(self, macro_data, dt):
+        assert macro_data["macro-scalar-data"] == 1
+        assert macro_data["macro-vector-data"].tolist() == [0, 1, 2]
+        return {"micro-scalar-data": macro_data["macro-scalar-data"] + 1,
+                "micro-vector-data": macro_data["macro-vector-data"] + 1}
+
+
+class TestFunctioncalls(TestCase):
+    def setUp(self):
+        self.fake_read_data_names = {"macro-scalar-data": False, "macro-vector-data": True}
+        self.fake_read_data = [{"macro-scalar-data": 1, "macro-vector-data": np.array([0, 1, 2])}] * 4
+        self.fake_write_data_names = {
+            "micro-scalar-data": False,
+            "micro-vector-data": True,
+            'micro_sim_time': False,
+            'active_state': False,
+            'active_steps': False}
+        self.fake_write_data = [{"micro-scalar-data": 1,
+                                 "micro-vector-data": np.array([0, 1, 2]),
+                                 "micro_sim_time": 0,
+                                 "active_state": 0,
+                                 "active_steps": 0}] * 4
+        self.macro_bounds = [0.0, 25.0, 0.0, 25.0, 0.0, 25.0]
+
+    def test_micromanager_constructor(self):
+        """
+        Test if the constructor of the MicroManager class passes correct values to member variables.
+        """
+        manager = micro_manager.MicroManager('micro-manager-config.json')
+
+        self.assertListEqual(manager._macro_bounds, self.macro_bounds)
+        self.assertDictEqual(manager._read_data_names, self.fake_read_data_names)
+        self.assertDictEqual(self.fake_write_data_names, manager._write_data_names)
+        self.assertEqual(manager._micro_n_out, 10)
+
+    def test_initialize(self):
+        """
+        Test if the initialize function of the MicroManager class initializes member variables to correct values
+        """
+        manager = micro_manager.MicroManager('micro-manager-config.json')
+
+        manager.initialize()
+
+        self.assertEqual(manager._dt, 0.1)  # from Interface.initialize
+        self.assertEqual(manager._global_number_of_sims, 4)
+        self.assertListEqual(manager._macro_bounds, self.macro_bounds)
+        self.assertListEqual(manager._mesh_vertex_ids.tolist(), [0, 1, 2, 3])
+        self.assertEqual(len(manager._micro_sims), 4)
+        self.assertEqual(manager._micro_sims[0].very_important_value, 0)  # test inheritance
+        self.assertDictEqual(manager._read_data_names, self.fake_read_data_names)
+        self.assertDictEqual(self.fake_write_data_names, manager._write_data_names)
+
+    def test_read_write_data_from_precice(self):
+        """
+        Test if the internal functions _read_data_from_precice and _write_data_to_precice work as expected.
+        """
+        manager = micro_manager.MicroManager('micro-manager-config.json')
+
+        manager._write_data_to_precice(self.fake_write_data)
+        read_data = manager._read_data_from_precice()
+
+        for data, fake_data in zip(read_data, self.fake_read_data):
+            self.assertEqual(data["macro-scalar-data"], 1)
+            self.assertListEqual(data["macro-vector-data"].tolist(),
+                                 fake_data["macro-vector-data"].tolist())
+
+    def test_solve_mico_sims(self):
+        """
+        Test if the internal function _solve_micro_simulations works as expected.
+        """
+        manager = micro_manager.MicroManager('micro-manager-config.json')
+        manager._local_number_of_sims = 4
+        manager._micro_sims = [MicroSimulation() for _ in range(4)]
+        manager._micro_sims_active_steps = np.zeros(4, dtype=np.int32)
+
+        micro_sims_output = manager._solve_micro_simulations(self.fake_read_data)
+
+        for data, fake_data in zip(micro_sims_output, self.fake_write_data):
+            self.assertEqual(data["micro-scalar-data"], 2)
+            self.assertListEqual(data["micro-vector-data"].tolist(),
+                                 (fake_data["micro-vector-data"] + 1).tolist())
+
+    def test_config(self):
+        """
+        Test if the functions in the Config class work.
+        """
+        config = micro_manager.Config(MagicMock(), 'micro-manager-config.json')
+
+        self.assertEqual(config._config_file_name.split("/")[-1], "dummy-config.xml")
+        self.assertEqual(config._micro_file_name, "test_micro_manager")
+        self.assertEqual(config._macro_mesh_name, "dummy-macro-mesh")
+        self.assertEqual(config._micro_output_n, 10)
+        self.assertDictEqual(config._read_data_names, self.fake_read_data_names)
+        self.assertDictEqual(self.fake_write_data_names, config._write_data_names)
+        self.assertEqual(config._adaptivity, True)
+        self.assertDictEqual(config._data_for_adaptivity, self.fake_read_data_names)
+        self.assertEqual(config._adaptivity_type, "local")
+        self.assertEqual(config._adaptivity_history_param, 0.5)
+        self.assertEqual(config._adaptivity_coarsening_constant, 0.3)
+        self.assertEqual(config._adaptivity_refining_constant, 0.4)
+        self.assertEqual(config._adaptivity_every_implicit_iteration, False)
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()