diff --git a/.gitignore b/.gitignore
index 5751acbae22ad40ac32e07f6b242ae258ca4c2ee..f5cbd29aa0a89fa7413399a59698df33522950ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,11 @@ __pycache__/
 
 # Jupyter
 .ipynb_checkpoints
+
+# pycharm
+.idea
+Excercise_3/Cryptography-Solutions.ipynb
+Exercise_2_5/Exercise_4 _solution.ipynb
+Excercise_2/Exercise_2_DEV_UE2_A2_v2_students.ipynb
+Exercise_4/exercise_4_A6_v1_students.ipynb
+*.zip
diff --git a/Dockerfile b/Dockerfile
index 4288bd29a0c560e31e5fc3ba6d63565c002d514f..24ef2056fb0378f6d23cc3896f4e573e39d54c69 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,11 +2,7 @@
 ARG BASE_IMAGE=registry.git.rwth-aachen.de/jupyter/profiles/rwth-courses:latest
 FROM ${BASE_IMAGE}
 
-# Install packages via requirements.txt
-ADD requirements.txt .
-RUN pip install -r requirements.txt
-
-# .. Or update conda base environment to match specifications in environment.yml
+# Update conda base environment to match specifications in environment.yml
 ADD environment.yml /tmp/environment.yml
 
 # All packages specified in environment.yml are installed in the base environment
diff --git a/Excercise_1/Ex2_Battery_Optimization_noSolutions.ipynb b/Excercise_1/Ex2_Battery_Optimization_noSolutions.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ef56481a5c0aa2590d9bd5ef80895b38210bdf7b
--- /dev/null
+++ b/Excercise_1/Ex2_Battery_Optimization_noSolutions.ipynb
@@ -0,0 +1,439 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# DEV Excercise 3\n",
+    "Task 2 - Battery storage optimization\n",
+    "\n",
+    "In this task, an energy system consisting of a building with an electrical load (e.g. household appliances), PV system, battery storage and electricity grid connection is considered.\n",
+    "24 time steps are considered, corresponding to the 24 hours of a day.\n",
+    "Complete the code snippets where needed at places market with \"!!!\"."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "7d6b628352d925e2"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Definition of the individual components:"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "7c9c80da0ef98b06"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "outputs": [],
+   "source": [
+    "# Time series of electrical load [kW]\n",
+    "el_demand_kW = [0.3, 0.4, 0.3, 0.3, 0.3, 0.3, 0.6, 0.7, 0.5, 0.4, 0.4, 0.7, 1.1, 0.8, 0.3, 0.3, 0.3, 0.3, 0.5, 0.7, 1.2, 1, 0.8, 0.4]\n",
+    "\n",
+    "# Time series for costs of electricity from the grid [€/kWh]\n",
+    "c_euro_kWh = [0.24, 0.32, 0.26, 0.25, 0.23, 0.32, 0.33, 0.35, 0.32, 0.32, 0.31, 0.28, 0.24, 0.33, 0.22, 0.27, 0.32, 0.32, 0.35, 0.32, 0.3, 0.33, 0.32, 0.31]\n",
+    "\n",
+    "# PV system\n",
+    "pv_pu = [0, 0, 0, 0, 0, 0, 0.1, 0.3, 0.6, 0.7, 0.8, 0.9, 1, 1, 0.9, 0.8, 0.7, 0.6, 0.3, 0.1, 0, 0, 0, 0] # Generation time series per unit [p.u.]\n",
+    "pv_kWp = 8 # peak power [kW]\n",
+    "\n",
+    "# Battery storage system\n",
+    "E_bat_kWh = 3 # capacity [kWh]\n",
+    "P_bat_max_charge_kW = 1 # max charging power [kW] (charging efficiency of 100 % is assumed)\n",
+    "P_bat_max_discharge_kW = 1 # max discharging power [kW] (discharging efficiency of 100 % is assumed)\n"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "b0157151aac2977a"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Task 2 a)\n",
+    "Plot the electrical demand, the electricity price as well as the PV generation for a 8 kWp plant. Use the python library matplotlib. You can find a user guide under the following [link](https://matplotlib.org/stable/users/index)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "259a9633f492299a"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "\n",
+    "# define the function\n",
+    "def plot_time_series(el_demand_kW, c_euro_kWh, pv_pu, pv_kWp):\n",
+    "    \"\"\"\n",
+    "    Plots the curves for electrical demand, the electricity price and the PV generation for a 8kWp plant.\n",
+    "    :param el_demand_kW: time series of electrical load \n",
+    "    :param c_euro_kWh: time series for costs of electricity from the grid\n",
+    "    :param pv_pu: generation time series per unit [p.u.]\n",
+    "    :param pv_kWp: peak power\n",
+    "    \"\"\"\n",
+    "    # !!! complete the code\n",
+    " \n",
+    "    pass\n",
+    "\n",
+    "# Call the function\n",
+    "plot_time_series(el_demand_kW, c_euro_kWh, pv_pu, pv_kWp)"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "a291137bccc17837"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Task 2 b)\n",
+    "Calculate the costs of electricity supply assuming the PV plant and battery storage system are not operating. (Using numpy might be helpfull. You can find a user guide under the following [link](https://numpy.org/doc/stable/).)"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "b3f112adafd4323a"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "The total electricity costs are 0 €\n"
+     ]
+    }
+   ],
+   "source": [
+    "# define function\n",
+    "def get_total_costs(energy, costs):\n",
+    "    \"\"\"\n",
+    "    Returns the costs of electricity supply assuming the PV plant and battery storage system are not operating\n",
+    "    :param energy: time series of electrical load \n",
+    "    :param costs: time series for costs of electricity from the grid\n",
+    "    :return: total cost value\n",
+    "    \"\"\"    \n",
+    "    \n",
+    "    c_total = 0 # !!! <-- insert calculation\n",
+    "    return c_total\n",
+    "\n",
+    "# call function\n",
+    "c_total = get_total_costs(el_demand_kW, c_euro_kWh)\n",
+    "print(f\"The total electricity costs are {round(c_total,2)} €\")"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "72143018a6eed25"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Result: The total electricity costs are 3.9 €"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "13535b9a1577d9fb"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Task 2 c)\n",
+    "Calculate the costs of electricity considering a 8 kWp PV plant, but no battery storage system. Assume that no income is generated by feeding the PV generated electricity into the grid."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "af8783fe2a5abeba"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "outputs": [],
+   "source": [
+    "# calculate residuum\n",
+    "residuum_kW = 0 #  !!! <-- insert calculation\n",
+    "\n",
+    "# call cost calculating function\n",
+    "c_total_residuum = get_total_costs(residuum_kW, c_euro_kWh)\n",
+    "print(f\"The total electricity costs are {round(c_total_residuum,2)} €\")"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-01-29T12:05:29.914599200Z",
+     "start_time": "2024-01-29T12:05:29.220590900Z"
+    }
+   },
+   "id": "eab981a9d9c604fe"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Result: The total electricity costs are 1.59 €"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "15d46ddd46dbad9b"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Task 2d)\n",
+    "In the following, an optimization problem is set up to optimize the operation of the battery in such a way that the electricity supply for the house is as cost-effective as possible. "
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "a34b5d21c3844d90"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "outputs": [],
+   "source": [
+    "import sys\n",
+    "!{sys.executable} -m pip install pulp\n",
+    "\n",
+    "# Import of necessary packages\n",
+    "from pulp import LpProblem, LpVariable, lpSum, LpMinimize\n",
+    "\n",
+    "# Create a new model\n",
+    "def optimze_battery_operation(el_demand_kW, pv_kWp, pv_pu, E_bat_kWh, P_bat_max_charge_kW, P_bat_max_discharge_kW, c_euro_kWh):\n",
+    "    model = LpProblem(\"charging_optimization\", LpMinimize)\n",
+    "\n",
+    "    # Decision Variables\n",
+    "    buy = LpVariable.dicts(\"buy\", range(24), lowBound=0)  # The amount of electricity (in kW) bought from the grid at time t\n",
+    "    soc = LpVariable.dicts(\"soc\", range(25), lowBound=0, upBound=E_bat_kWh)  # The State of Charge (in kWh) of the battery at time t\n",
+    "    discharge = LpVariable.dicts(\"discharge\", range(24), lowBound=0, upBound=P_bat_max_discharge_kW)  # Discharge rate (in kW) at time t\n",
+    "    charge = LpVariable.dicts(\"charge\", range(24), lowBound=0, upBound=P_bat_max_charge_kW)  # Charge rate (in kW) at time t\n",
+    "    feedin = LpVariable.dicts(\"feedin\", range(24), lowBound=0)  # The amount of electricity (in kW) sold to the grid at time t\n",
+    "    consumed_pv = LpVariable.dicts(\"consumed_pv\", range(24), lowBound=0)  # The amount of electricity (in kW) consumed directly from PV at time t\n",
+    "\n",
+    "    # Objective Function\n",
+    "    model += lpSum(c_euro_kWh[t] * buy[t] for t in range(24))\n",
+    "\n",
+    "    # Constraints\n",
+    "    model += soc[0] == E_bat_kWh/2\n",
+    "    model += soc[23] == E_bat_kWh/2\n",
+    "\n",
+    "    # Energy balance for consumed electricity\n",
+    "    for t in range(24):\n",
+    "        model += el_demand_kW[t] == consumed_pv[t] + buy[t] + discharge[t]\n",
+    "\n",
+    "    # Energy balance for generated electricity\n",
+    "    for t in range(24):\n",
+    "        model += pv_kWp * pv_pu[t] ==  charge[t] + feedin[t] + consumed_pv[t]\n",
+    "\n",
+    "    # State of Charge\n",
+    "    for t in range(23):\n",
+    "        model += soc[t+1] == soc[t] + charge[t] - discharge[t]\n",
+    "\n",
+    "    # Solve the optimization problem\n",
+    "    model.solve()\n",
+    "\n",
+    "    return model, buy, soc, feedin"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "9bc932d40fa4a87f"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Calculate the costs of electricity considering a 8 kWp PV plant and 3 kWh battery storage system. Assume that no income is generated by feeding the PV generated electricity into the grid. Plot the resulting state of charge (SOC) and buy time series and compare them to the other time series.\n"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "4716844fbe8d1a33"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "outputs": [],
+   "source": [
+    "## call the optimization function\n",
+    "model, buy, soc, feedin_cost_minimizing = optimze_battery_operation(el_demand_kW, pv_kWp, pv_pu, E_bat_kWh, P_bat_max_charge_kW, P_bat_max_discharge_kW, c_euro_kWh)\n",
+    "\n",
+    "# read results\n",
+    "buy_results = []\n",
+    "soc_results = []\n",
+    "feedin_cost_minimizing_results = []\n",
+    "\n",
+    "for i in range(0,24):\n",
+    "    buy_results.append(buy[i].value())\n",
+    "    soc_results.append(soc[i].value())\n",
+    "    feedin_cost_minimizing_results.append(feedin_cost_minimizing[i].value())\n",
+    "\n",
+    "# print results calculated with time series\n",
+    "c_total_optimal = get_total_costs(buy_results, c_euro_kWh)\n",
+    "print(f\"The total electricity costs are {round(c_total_optimal,2)} €\")\n",
+    "\n",
+    "# print results from objective value (validation)\n",
+    "c_total_optimal = model.objective.value()\n",
+    "print(f\"The total electricity costs are {round(c_total_optimal,2)} €\")\n",
+    "\n",
+    "# plots\n",
+    "plot_time_series(el_demand_kW, c_euro_kWh, pv_pu, pv_kWp)\n",
+    "\n",
+    "# !!! insert plot for Battery SOC\n",
+    "\n",
+    "# !!! insert plot for electricity bought\n",
+    "\n",
+    "\n",
+    "plt.show()"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "36908ba960de9d4b"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "The SOC of the battery starts and ends at 50% of the maximum capacity, as this is specified by the constraints. Initially, the battery discharges as no PV power is generated. It is sufficient to charge the battery in the last hours of the day, as the size of the battery is comparatively small. It can be seen that the grid power is drawn in the time steps when the grid power is most favorable."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "f7e51d061cdba850"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Task 2 e)\n",
+    "Change the battery optimization function (1 Variable, 1 Objective, 1 Constraint) below so that the maximium (single peak value, not sum of all values!) feed in from the pv plant to the grid gets minimized. How much was the maximum feed in without battery operation, with cost minimizing battery operation and with feed-in minimizing battery operation? Plot the resulting feed-in time series. You can take a look at the user guide of the library Pulp under the following [link](https://coin-or.github.io/pulp/main/includeme.html)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "e323c8dc005ac6c6"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "outputs": [],
+   "source": [
+    "# Import of necessary packages\n",
+    "from pulp import LpProblem, LpVariable, lpSum, LpMinimize\n",
+    "\n",
+    "# Create a new model\n",
+    "def optimze_battery_operation_PV(el_demand_kW, pv_kWp, pv_pu, E_bat_kWh, P_bat_max_charge_kW, P_bat_max_discharge_kW, c_euro_kWh):\n",
+    "    model = LpProblem(\"charging_optimization\", LpMinimize)\n",
+    "\n",
+    "    # Decision Variables\n",
+    "    buy = LpVariable.dicts(\"buy\", range(24), lowBound=0)  # The amount of electricity (in kW) bought from the grid at time t\n",
+    "    soc = LpVariable.dicts(\"soc\", range(25), lowBound=0, upBound=E_bat_kWh)  # The State of Charge (in kWh) of the battery at time t\n",
+    "    discharge = LpVariable.dicts(\"discharge\", range(24), lowBound=0, upBound=P_bat_max_discharge_kW)  # Discharge rate (in kW) at time t\n",
+    "    charge = LpVariable.dicts(\"charge\", range(24), lowBound=0, upBound=P_bat_max_charge_kW)  # Charge rate (in kW) at time t\n",
+    "    feedin = LpVariable.dicts(\"feedin\", range(24), lowBound=0)  # The amount of electricity (in kW) sold to the grid at time t\n",
+    "    consumed_pv = LpVariable.dicts(\"consumed_pv\", range(24), lowBound=0)  # The amount of electricity (in kW) consumed directly from PV at time t\n",
+    "\n",
+    "    #  !!! insert new variable\n",
+    "    max_pv_feed_in = 0\n",
+    "\n",
+    "    # !!! insert new Objective Function\n",
+    "    model += lpSum(0)\n",
+    "\n",
+    "    # Constraints\n",
+    "    model += soc[0] == E_bat_kWh/2\n",
+    "    model += soc[23] == E_bat_kWh/2\n",
+    "\n",
+    "    # Energy balance for consumed electricity\n",
+    "    for t in range(24):\n",
+    "        model += el_demand_kW[t] == consumed_pv[t] + buy[t] + discharge[t]\n",
+    "\n",
+    "    # Energy balance for generated electricity\n",
+    "    for t in range(24):\n",
+    "        model += pv_kWp * pv_pu[t] ==  charge[t] + feedin[t] + consumed_pv[t]\n",
+    "\n",
+    "    # State of Charge\n",
+    "    for t in range(23):\n",
+    "        model += soc[t+1] == soc[t] + charge[t] - discharge[t]\n",
+    "\n",
+    "    # !!! insert new constraint\n",
+    "    for t in range(23):\n",
+    "        model += max_pv_feed_in[0] >= feedin[t]\n",
+    "\n",
+    "    # Solve the optimization problem\n",
+    "    model.solve()\n",
+    "\n",
+    "    return model, buy, soc, feedin\n",
+    "\n",
+    "# call optimization function\n",
+    "model, buy, soc, feedin = optimze_battery_operation_PV(el_demand_kW, pv_kWp, pv_pu, E_bat_kWh, P_bat_max_charge_kW, P_bat_max_discharge_kW, c_euro_kWh)\n",
+    "\n",
+    "# read results\n",
+    "feedin_results = []\n",
+    "\n",
+    "for i in range(0,24):\n",
+    "    feedin_results.append(feedin[i].value())\n",
+    "\n",
+    "# max feed-in without battery:\n",
+    "max_feedin_1 = 0 # !!! <-- insert calculation\n",
+    "print(f\"The maximum feed-in without battery operation is {max_feedin_1} kW.\")\n",
+    "\n",
+    "# max feed-in with cost minimizing battery operation:\n",
+    "max_feedin_2 = 0 # !!! <-- insert calculation\n",
+    "print(f\"The maximum feed-in with cost minimizing battery operation is {max_feedin_2} kW.\")\n",
+    "\n",
+    "# max feed-in with feed-in minimizing battery operation:\n",
+    "max_feedin_3 = 0 # !!! <-- insert calculation\n",
+    "print(f\"The maximum feed-in with feed-in minimizing battery operation is {max_feedin_3} kW.\")\n",
+    "\n",
+    "# !!! insert plots\n",
+    "\n",
+    "\n",
+    "plt.show()"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "649d87d692c5365"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "21ab0d83302acf04"
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/Excercise_2/Exercise_2_DEV_A2_v2.ipynb b/Excercise_2/Exercise_2_DEV_A2_v2.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..deb8b6f223ac714690ce9b34661f166c33d50ca4
--- /dev/null
+++ b/Excercise_2/Exercise_2_DEV_A2_v2.ipynb
@@ -0,0 +1,729 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# DEV Exercise 2"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "This Jupyter notebook contains tasks that are calculated for you on the blackboard during the exercise session. To practice using python and to see the advantage of programming when dealing with data, you will reprogram parts of it in this Jupyter notebook. If you have not yet installed the packages required for this exercise, run the following cell to install them."
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Requirement already satisfied: pandas in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (2.0.3)\n",
+      "Requirement already satisfied: numpy>=1.20.3 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from pandas) (1.24.4)\n",
+      "Requirement already satisfied: python-dateutil>=2.8.2 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from pandas) (2.8.2)\n",
+      "Requirement already satisfied: pytz>=2020.1 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from pandas) (2023.4)\n",
+      "Requirement already satisfied: tzdata>=2022.1 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from pandas) (2023.4)\n",
+      "Requirement already satisfied: six>=1.5 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from python-dateutil>=2.8.2->pandas) (1.16.0)\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "WARNING: You are using pip version 21.1.1; however, version 24.0 is available.\n",
+      "You should consider upgrading via the 'c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\python.exe -m pip install --upgrade pip' command.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Requirement already satisfied: numpy in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (1.24.4)\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "WARNING: You are using pip version 21.1.1; however, version 24.0 is available.\n",
+      "You should consider upgrading via the 'c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\python.exe -m pip install --upgrade pip' command.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Requirement already satisfied: scipy in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (1.10.1)\n",
+      "Requirement already satisfied: numpy<1.27.0,>=1.19.5 in c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\lib\\site-packages (from scipy) (1.24.4)\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "WARNING: You are using pip version 21.1.1; however, version 24.0 is available.\n",
+      "You should consider upgrading via the 'c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\python.exe -m pip install --upgrade pip' command.\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Collecting openpyxl\n",
+      "  Using cached openpyxl-3.1.2-py2.py3-none-any.whl (249 kB)\n",
+      "Collecting et-xmlfile\n",
+      "  Using cached et_xmlfile-1.1.0-py3-none-any.whl (4.7 kB)\n",
+      "Installing collected packages: et-xmlfile, openpyxl\n",
+      "Successfully installed et-xmlfile-1.1.0 openpyxl-3.1.2\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "WARNING: You are using pip version 21.1.1; however, version 24.0 is available.\n",
+      "You should consider upgrading via the 'c:\\users\\f.tischbein\\appdata\\local\\programs\\python\\python38\\python.exe -m pip install --upgrade pip' command.\n"
+     ]
+    }
+   ],
+   "source": [
+    "!pip install pandas\n",
+    "!pip install numpy\n",
+    "!pip install scipy\n",
+    "!pip install openpyxl"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T12:58:13.139796300Z",
+     "start_time": "2024-02-23T12:57:57.909215100Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Now run the following cell to import the most important libraries. You can run a cell either by clicking `Run` on the toolbar or by pressing `CTRL+RETURN`."
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": true,
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "ExecuteTime": {
+     "end_time": "2024-02-23T12:58:40.909857800Z",
+     "start_time": "2024-02-23T12:58:28.941463400Z"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from scipy.optimize import curve_fit"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Predefine functions\n",
+    "It is good programming practice to define functions at the beginning that you want to use again and again in the course of the code. You can find instructions with examples for defining functions under the following [link](https://www.w3schools.com/python/python_functions.asp). For reasons of overview, however, we will always define these in the respective task section first. As an example, we give you the function <code> round_up(x, decimals) </code>. This function always rounds up values and will be used more frequently throughout the code."
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [
+    "def round_up(x, decimals: int):\n",
+    "    \"\"\"\n",
+    "    Returns the numerical value given in \"value\", rounded up to the given number of decimal places given in \"decimals\". This function is particularly relevant when dealing with measurement uncertainties, as these must always be rounded up.\n",
+    "\n",
+    "    :param x: value to be rounded\n",
+    "    :param decimals: number of decimals\n",
+    "    :return: rounded up value\n",
+    "    \"\"\"\n",
+    "    value = np.ceil(pow(10, decimals)*x) / pow(10, decimals)\n",
+    "\n",
+    "    return value"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2a\n",
+    "First, the measurement data must be read in from the Excel spreadsheet. The table contains the measured voltages and currents, each with their uncertainties. Use the [pd.read_excel](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html) function to do this.\n"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "outputs": [],
+   "source": [
+    "### First, the data needs to be loaded from the given xlsx-file. Note that all datatypes shall be set to float.\n",
+    "\n",
+    "data = pd.read_excel(\"measurement_data.xlsx\", index_col=0, dtype=float)\n",
+    "\n",
+    "### Define arrays with the corresponding voltage and current values for later use.\n",
+    "U       = data.loc[:, \"voltage\"]\n",
+    "sig_U   = data.loc[:, \"voltage_sig\"]\n",
+    "I       = data.loc[:, \"current\"]\n",
+    "sig_I   = data.loc[:, \"current_sig\"]"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:00:28.694732900Z",
+     "start_time": "2024-02-23T13:00:28.119399700Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2b\n",
+    "After the data has been read in, the resistance and its uncertainty are to be determined for the value pairs. \n",
+    "First add the corresponding code to the functions in the cell below to calculate the resistance with the function <code> def uri(U,I) </code> and the uncertainty with the function def <code> def calc_sig_R(U, I, sig_I) </code>. Why does the function <code> def calc_sig_R(U, I, sig_I) </code> not have the uncertainty of the voltage as an input variable? "
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "outputs": [],
+   "source": [
+    "def uri(U,I):\n",
+    "    \"\"\"\n",
+    "    Returns the value for the resistance following Ohm's law U=R*I\n",
+    "    :param U: voltage value\n",
+    "    :param I: current value\n",
+    "    :return: resistance value\n",
+    "    \"\"\"\n",
+    "    # Complete the code\n",
+    "    R = U/I\n",
+    "    return R\n",
+    "\n",
+    "def calc_sig_R(U, I, sig_I):\n",
+    "    \"\"\"\n",
+    "    Returns the value of the uncertainty of the resistance according to Ohm's law and Gaussian error propagation\n",
+    "    :param U: voltage value\n",
+    "    :param I: current value\n",
+    "    :param sig_I: uncertainty of the resistance\n",
+    "    :return: \n",
+    "    \"\"\"\n",
+    "    # Complete the code\n",
+    "    sig_R = U/I**2 * sig_I\n",
+    "    return sig_R"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:00:33.278431500Z",
+     "start_time": "2024-02-23T13:00:33.248408400Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Afterwards, add the code here to calculate the resistance with the function<code> def uri(U,I) </code> and add them to your pandas dataframe. Round the values to a suitable number of decimal places using the function [round](https://docs.python.org/3/library/functions.html#round)."
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "outputs": [],
+   "source": [
+    "# Calculate and round the resistance\n",
+    "data.loc[:, \"resistance\"]       = round( uri(U,I) , 2)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:00:43.809786300Z",
+     "start_time": "2024-02-23T13:00:43.786756300Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Now calculate the uncertainty, round it up and add it to the dataframe. Then print the dataframe to check your results."
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "   voltage  voltage_sig  current  current_sig  resistance  resistance_sig\n",
+      "0     10.0          0.0     1.55         0.08        6.45            0.34\n",
+      "1     15.0          0.0     2.19         0.11        6.85            0.35\n",
+      "2     20.0          0.0     3.00         0.15        6.67            0.34\n",
+      "3     25.0          0.0     3.97         0.20        6.30            0.32\n",
+      "4     30.0          0.0     6.18         1.55        4.85            1.22\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Calculate resistance uncertainty\n",
+    "# Remember that uncertainties shall always be rounded up!\n",
+    "data.loc[:, \"resistance_sig\"]   = round_up( calc_sig_R(U, I, sig_I), 2)\n",
+    "\n",
+    "### Print resulting table in one line\n",
+    "pd.set_option(\"expand_frame_repr\", False)\n",
+    "print(data)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:00:55.126280100Z",
+     "start_time": "2024-02-23T13:00:55.105678900Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2e\n",
+    "Now we have several pairs of measured values with different measurement uncertainties. In order to combine the information including the measurement uncertainties, we need to determine a suitable estimated value for the resistance. Calculate an estimation value for the resistance first using the arithmetic mean by using the function [np.mean](https://numpy.org/doc/stable/reference/generated/numpy.mean.html) and secondly using the expression for the weighted mean. The weighted mean is defined by the following formula, which you must add under the function <code> def weighted_mean(x, sig_x) </code> in the cell below:\n",
+    "\n",
+    "\n",
+    "\n",
+    "$\\left<x\\right>=\\frac{\\sum_{i=1}^N x_i / \\sigma_{i,abs}^2}{\\sum_{i=1}^N 1 / \\sigma_{i,abs}^2}$\n",
+    "\n",
+    "\n",
+    "Also calculate the uncertainty of the calculated mean values, in each case using the function [np.std](https://numpy.org/doc/stable/reference/generated/numpy.std.html) for the arithmetic mean and the function <code> def sig_weighted_mean(sig_x) </code> for the weighted mean. In advance, add the following formula to the function <code> def sig_weighted_mean(sig_x) </code> using [np.sqrt](https://numpy.org/doc/stable/reference/generated/numpy.sqrt.html):\n",
+    "\n",
+    "$\\sigma_{\\left<x\\right>}=\\frac{1}{\\sqrt{\\sum_{i=1}^N 1 / \\sigma_{i,abs}^2}}$\n",
+    "\n",
+    "Remember to round the values to a meaningful number of decimal places using [round](https://docs.python.org/3/library/functions.html#round) and <code> def round_up(x, decimals) </code>. \n",
+    "Compare your results and explain where the differences come from! \n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "outputs": [],
+   "source": [
+    "def weighted_mean(x, sig_x):\n",
+    "    \"\"\"\n",
+    "    Returns the weighted mean of variables x and their uncertainties sig_x\n",
+    "    :param x: array with x values\n",
+    "    :param sig_x: array with uncertainty of respective x value\n",
+    "    :return: weighted mean\n",
+    "    \"\"\"\n",
+    "    nen = 0\n",
+    "    zae = 0\n",
+    "    for i in range(len(x)):\n",
+    "        nen += x[i]/sig_x[i]**2\n",
+    "        zae += 1/sig_x[i]**2\n",
+    "    \n",
+    "    wm = nen/zae\n",
+    "    return wm\n",
+    "\n",
+    "def sig_weighted_mean(sig_x):\n",
+    "    \"\"\"\n",
+    "    Returns the uncertainty of the weighted mean of variables x and their uncertainties sig_x\n",
+    "    :param sig_x: array with uncertainty of respective x value\n",
+    "    :return: uncertainty of weighted mean\n",
+    "    \"\"\"\n",
+    "    sum_weights = 0\n",
+    "    for i in range(len(sig_x)):\n",
+    "        sum_weights += 1/sig_x[i]**2\n",
+    "        \n",
+    "    sig_wm = 1/np.sqrt(sum_weights)\n",
+    "    return sig_wm"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:01:31.910988Z",
+     "start_time": "2024-02-23T13:01:31.900940200Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "             Weighted  Arithmetic\n",
+      "Mean             6.52        6.22\n",
+      "Uncertainty      0.17        0.72\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Extract the needed data from our dataframe\n",
+    "\n",
+    "n                   = data.shape[0] # number of measurements\n",
+    "R                   = data.loc[:, \"resistance\"]\n",
+    "sig_R               = data.loc[:, \"resistance_sig\"]\n",
+    "\n",
+    "# Calculate the arithmetic mean and its uncertainty and round your results\n",
+    "ar_mean        = round(np.mean(R), 2)\n",
+    "sig_ar_mean    = round_up(np.std(R), 2)\n",
+    "\n",
+    "# Calculate the weighted mean and its uncertainty and round your results\n",
+    "w_mean     = round( weighted_mean(R, sig_R), 2)\n",
+    "sig_w_mean = round_up(sig_weighted_mean(sig_R), 2)\n",
+    "\n",
+    "# Create and print a Dataframe with results\n",
+    "results_2e           = pd.DataFrame(np.array([[ w_mean, ar_mean], [ sig_w_mean, sig_ar_mean ]]), columns=[\"Weighted\", \"Arithmetic\"], index=[\"Mean\", \"Uncertainty\"])\n",
+    "print(results_2e)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:01:34.538085900Z",
+     "start_time": "2024-02-23T13:01:34.513089800Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2f\n",
+    "\n",
+    "In the lecture and the first part of this exercise session, we learned about the maximum likelihood method on the one hand and solved the analytical solution of the problem here using the ML method on the other.\n",
+    "According to Ohm's law, $I = U/R = m*U$ with $m=1/R$. This results in a linear relationship between the current and the voltage. Using the ML method, we have calculated that the following equation applies for m:\n",
+    "\n",
+    "$m = \\frac{\\sum_{i=1}^N \\frac{x_i \\cdot y_i}{\\sigma_i^2}}{\\sum_{i=1}^N \\frac{ x_i^2}{\\sigma_i^2}}$\n",
+    "\n",
+    "In this case, $x = U$, $y = I$ and $\\sigma_y = \\sigma_I$. Add the function <code> def ml_estimator(x, y, sig_y) </code> to the cell below and determine both the value for the ML estimator $m$ and then the value for the resistance. Round the value for the resistance to a meaningful number of decimal places and compare the value for the resistance with the previous ones. \n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "outputs": [],
+   "source": [
+    "def ml_estimator(x, y, sig_y):\n",
+    "    \"\"\"\n",
+    "    Returns the parameter m that is determined by using the maximum likelihood method.\n",
+    "    :param x:\n",
+    "    :param y:\n",
+    "    :param sigma:\n",
+    "    :return: m\n",
+    "    \"\"\"\n",
+    "    nen = 0\n",
+    "    zae = 0\n",
+    "    for i in range(len(x)):\n",
+    "        nen += (x[i]*y[i]/sig_y[i]**2)\n",
+    "        zae += (x[i]/sig_y[i])**2\n",
+    "    m = nen/zae\n",
+    "    return m"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:01:40.086529600Z",
+     "start_time": "2024-02-23T13:01:40.070463800Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "m = 0.15238572230070688\n",
+      "\n",
+      "   weighted  arithmetic  ML-method\n",
+      "0      6.52        6.22       6.56\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Extract needed data from dataframe\n",
+    "x           = data.loc[:, \"voltage\"]\n",
+    "y           = data.loc[:, \"current\"]\n",
+    "sig_y       = data.loc[:, \"current_sig\"]\n",
+    "\n",
+    "# Calculate best estimate for the parameter m\n",
+    "m           = ml_estimator(x, y, sig_y)\n",
+    "R_ml        = 1/m\n",
+    "R_ml  = round(R_ml, 2)\n",
+    "print(\"m = \" + str(m) + \"\\n\")\n",
+    "\n",
+    "# Create and print a Dataframe with results\n",
+    "results_2f  = pd.DataFrame(np.array([[w_mean, ar_mean, R_ml]]), columns=[\"weighted\", \"arithmetic\", \"ML-method\"])\n",
+    "print(results_2f)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:01:42.678661600Z",
+     "start_time": "2024-02-23T13:01:42.649206600Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2g\n",
+    "\n",
+    "In practice, the majority of functional relationships can no longer be determined analytically using the ML method. For these cases, the Python package [Scipy](https://scipy.org/) can be used. The [scipy.optimize.curve_fit](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html) function can be used to estimate the parameters of previously defined functional relationships for a specific data set. In the scipy documentation you will find all relevant information and steps for the implementation. \n",
+    "Use the function to fit a linear relationship to the measurement data according to task 2f). Compare the result for the resistance with the previous ones. \n",
+    "\n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%% md\n"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Optimal Parameter m   = 0.15\n",
+      "Uncertainty = 0.0031424538988126835\n",
+      "   weighted  arithmetic  ML-method  Scipy Curve-Fit\n",
+      "0      6.52        6.22       6.56             6.56\n"
+     ]
+    }
+   ],
+   "source": [
+    "# First, the linear relationship needs to be defined as a function\n",
+    "def linear(x, a):\n",
+    "    \"\"\"\n",
+    "    Linear relationship between y=m*x and x\n",
+    "    :param x: x Variables\n",
+    "    :param a: Slope\n",
+    "    :return: y\n",
+    "    \"\"\"\n",
+    "    return x*a\n",
+    "\n",
+    "def sig_R_m(m, sig_m):\n",
+    "    \"\"\"\n",
+    "    Gaussian Error Distribution to calculate the uncertainty of R=1/m\n",
+    "    :param m: slope\n",
+    "    :param sig_m: uncertainty of the slope\n",
+    "    :return: uncertainty of the resistance\n",
+    "    \"\"\"\n",
+    "    sig_R = sig_m/m**2\n",
+    "    return sig_R\n",
+    "    \n",
+    "\n",
+    "# Use curve_fit to solve the problem and print the parameter m with its uncertainty\n",
+    "popt, pcov  = curve_fit(linear, U, I, sigma=sig_I)\n",
+    "m = popt[0]\n",
+    "sig_m = np.sqrt(pcov[0,0])\n",
+    "print(\"Optimal Parameter m   = \" + str(round(m, 2)))\n",
+    "print(\"Uncertainty = \" + str(sig_m))\n",
+    "\n",
+    "# Calculate the resistance and its uncertainty (Hint: Think about Gaussian Error Propagation!)\n",
+    "R_sc = round(1/m, 2)\n",
+    "sig_sc = round_up(sig_R_m(m, sig_m),2)\n",
+    "\n",
+    "\n",
+    "# Create and print a Dataframe with results\n",
+    "results_2g  = pd.DataFrame(np.array([[w_mean, ar_mean, R_ml, R_sc]]), columns=[\"weighted\", \"arithmetic\", \"ML-method\", \"Scipy Curve-Fit\"])\n",
+    "print(results_2g)\n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:01:47.668723700Z",
+     "start_time": "2024-02-23T13:01:47.652728400Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2h\n",
+    "\n",
+    "The $\\Chi^2$ test, which is defined as follows, is suitable for determining the quality of the fit:\n",
+    "\n",
+    "$\\chi^2 = \\sum_{i=1}^N \\frac{(y_i - f(x_i, m))^2}{\\sigma_i^2}$\n",
+    "\n",
+    "Where $x=U$, $y=I$,  $\\sigma_i = \\sigma_y_i$ and $f(x_i,m) = U_i/R$. Complete the function <code> def chi2_formula(x, y, sig_y, R) </code> in the cell below and calculate the $\\Chi^2/n_{d.o.f.}$ for the results from 2e) (arithmetic and weighted mean) and 2g) (Scipy Curve-Fit). Explain what the numerical values for $\\Chi^2/n_{d.o.f.}$ mean.\n",
+    "\n"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "outputs": [],
+   "source": [
+    "def chi2_formula(x, y, sig_y, R):\n",
+    "    \"\"\"\n",
+    "    Formula to calculate the Chi^2 value in the case of the relationship y = x/R\n",
+    "    :param x: x values\n",
+    "    :param y: y values\n",
+    "    :param sig_y: uncertainty of y values\n",
+    "    :param m: slope\n",
+    "    :return: \n",
+    "    \"\"\"\n",
+    "    chi2 = 0\n",
+    "    for i in range(len(x)):\n",
+    "        chi2 += (y[i] - x[i]/R)**2/sig_y[i]**2\n",
+    "    return chi2"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:07:01.068534900Z",
+     "start_time": "2024-02-23T13:07:01.050115Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Chi^2/ndof (Airthmetic Mean) = 1.867\n",
+      "Chi^2/ndof (Weighted Mean) = 0.6881\n",
+      "Chi^2/ndof (Scipy) = 0.6715\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Determine n_dof\n",
+    "\n",
+    "n = 5-1 \n",
+    "\n",
+    "# Calculate the Chi^2 for task 2e arithmetic mean\n",
+    "chi2_ar = chi2_formula(U, I, sig_I, ar_mean)\n",
+    "chi2_ar_n = chi2_ar/n\n",
+    "\n",
+    "# Calculate the Chi^2 for task 2e wighted mean\n",
+    "chi2_weigh = chi2_formula(U, I, sig_I, w_mean)\n",
+    "chi2_weigh_n = chi2_weigh/n\n",
+    "\n",
+    "# Calculate the Chi^2 for task 2g\n",
+    "chi2_sc = chi2_formula(U, I, sig_I, R_sc)\n",
+    "chi2_sc_n = chi2_sc/n\n",
+    "\n",
+    "# Print the resulting Chi^2/ndof\n",
+    "print(\"Chi^2/ndof (Airthmetic Mean) = \" + str(round(chi2_ar_n, 4)))\n",
+    "print(\"Chi^2/ndof (Weighted Mean) = \" + str(round(chi2_weigh_n, 4)))\n",
+    "print(\"Chi^2/ndof (Scipy) = \" + str(round(chi2_sc_n, 4)))\n",
+    "print(\"Chi^2/ndof < 1 means, that the uncertainties are overestimated while Chi^2/ndof > 1, means that the uncertainties are underestimated. A fit good to the data would be Chi^2/ndof = 1\")"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "pycharm": {
+     "name": "#%%\n"
+    },
+    "ExecuteTime": {
+     "end_time": "2024-02-23T13:07:55.840103800Z",
+     "start_time": "2024-02-23T13:07:55.707676300Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   }
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/Excercise_2/measurement_data.xlsx b/Excercise_2/measurement_data.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..e94f54b2f1a9ca80fe7a64efee4abe9889a89d2f
Binary files /dev/null and b/Excercise_2/measurement_data.xlsx differ
diff --git a/Excercise_3/340px-English-slf(1).png b/Excercise_3/340px-English-slf(1).png
new file mode 100644
index 0000000000000000000000000000000000000000..6431723cc3736d149d90d33dd835198b795b3c41
Binary files /dev/null and b/Excercise_3/340px-English-slf(1).png differ
diff --git a/Excercise_3/Cryptography.ipynb b/Excercise_3/Cryptography.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..4a40c6ba2b3f46d60ecd3f467f4c45f67f365f4e
--- /dev/null
+++ b/Excercise_3/Cryptography.ipynb
@@ -0,0 +1,474 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "7bdcd773",
+   "metadata": {},
+   "source": [
+    "# Introduction to Cryptography and Letter Frequencies\n",
+    "\n",
+    "**Cryptography** is a field of study focused on secure communication techniques that allow only the sender and intended recipient of a message to view its contents. **Substitution ciphers** are a basic method of encryption where elements of the plaintext (the original message) are replaced systematically to form the ciphertext (the encoded message).\n",
+    "\n",
+    "In this exercise, we'll explore a simple **shift cipher** (also known as a Caesar cipher), where each letter in the plaintext is 'shifted' a certain number of places down the alphabet. We'll also delve into **frequency analysis**, a method to break ciphers by analyzing the frequency of symbols in the encoded message.\n",
+    "\n",
+    "The first objective of this exercise is to implement a simple shift cypher using python. Afterwards, we will try to break the shift cypher, by analyzing the letter frequencies."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "595a8870",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Setting Up the Environment\n",
+    "\n",
+    "import string # might be helpful to look at\n",
+    "from collections import Counter\n",
+    "import random\n",
+    "import matplotlib.pyplot as plt"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ed7799e9",
+   "metadata": {},
+   "source": [
+    "## Implementation of the Caesar Cypher\n",
+    "\n",
+    "Let's begin by encoding the message. As described above we aim at a simple shift cypher, which shifts every letter in the message a certain number of letters down the alphabet, looping back to the beginning after the end. For instance, when we apply a shift of 2, the letter 'a' becomes 'c', 'b' becomes 'd', etc. As a result, the word \"at\" transforms into \"cv\". This applies to lower-case letters as well as upper-case. We will ignore special characters like punctuation or white spaces for now."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "caa9910f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Encoding a Message\n",
+    "\n",
+    "def encode_message(message, key=2):\n",
+    "    \"\"\"\n",
+    "    Encodes a message using a shift cipher.\n",
+    "    \n",
+    "    Parameters:\n",
+    "        message (str): The message to encode.\n",
+    "        key (int): The encryption key (how times the letters are shifted)\n",
+    "    Returns:\n",
+    "        str: The encoded message.\n",
+    "    \"\"\"\n",
+    "    # Put your own code here\n",
+    "    \n",
+    "    return\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7efd44c6-484b-47ee-9472-6b3a36ae9fc4",
+   "metadata": {},
+   "source": [
+    "### Testing the cypher\n",
+    "Now let us quickly test you encryption. Try out some strings and some keys. You can use a single letter at a time. Think about special keys: What should happen if you use 0? or 26? Does it work as expected?\n",
+    "\n",
+    "The `assert` statement can help you here design some easy tests, like this:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "45f1c38b-3650-4209-a288-dd5dbc004b94",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert encode_message(\"a\", 0) == \"a\"\n",
+    "\n",
+    "# Add some more tests to check if your encryption works\n",
+    "\n",
+    "print(\"If no error occured: everything worked as expected!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "df31d050",
+   "metadata": {},
+   "source": [
+    "## Decyphering\n",
+    "Since the purpose of encryption is to deliver the information to another place or a later time, we need a way to get our original message back as long as we know the encryption key. Next, implement the decryption function. In our case, it should be somewhat similar to the encryption from before. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "91d2ec6b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Decoding a Message\n",
+    "\n",
+    "def decode_message(encoded_message, key):\n",
+    "    \"\"\"\n",
+    "    Decodes a message encoded with a shift cipher.\n",
+    "    \n",
+    "    Parameters:\n",
+    "        encoded_message (str): The encoded message.\n",
+    "        shift (int): The number of places each character was shifted.\n",
+    "        \n",
+    "    Returns:\n",
+    "        str: The decoded (original) message.\n",
+    "    \"\"\"\n",
+    "    # Put your own code here \n",
+    "    \n",
+    "    return "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1974d1d-703c-4e7e-8636-4047efdc9a91",
+   "metadata": {},
+   "source": [
+    "### Testing the decypher\n",
+    "As before we can test our decoding algorithm by many statements. However, since we have two functions know, this might help us. Can you figure out how?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b63b9859-0b69-447a-a39e-b5ca555d0348",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "assert decode_message(\"a\", 0) == \"a\"\n",
+    "\n",
+    "# Add some more tests to check if your encryption works\n",
+    "\n",
+    "print(\"If no error occured: everything worked as expected!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "261c284d",
+   "metadata": {},
+   "source": [
+    "## Breaking a cypher\n",
+    "### Brute Force\n",
+    "As you might have noticed already this is not a particularly strong cypher algorithm. Since we only have 25 usable keys, a simple \"brute force\" approch can yield a result. This could even be done by hand, and certainly with computers. That's why it is important for modern encryption algorithms to have a huge number of possible key. AES for examples uses between 128 and 256 bit keys, making the respective numbers of keys 2^128 - 2^256.\n",
+    "\n",
+    "The following code produces a cypher text with a random key. See if you can figure out the key using brute force."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cabfa50c-5366-4728-af0a-18791b558fac",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "message = \"Great! You found the correct key.\"\n",
+    "key = random.randint(1, 25)\n",
+    "cypher_message = encode_message(message, key)\n",
+    "\n",
+    "# Your code goes here\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3ad2956c-aba5-4d92-93bd-1080d5064f10",
+   "metadata": {},
+   "source": [
+    "### Frequency analysis\n",
+    "A more intelligent aproach to breaking a cyper is called frequency analysis and uses statistics to find the correct key. The concept is based on the fact, that in structued data such as text, certain statical effect emerge. For example: in the English language certain letters, like 'e', 't' or 'a' are used much more often than others, like 'x' or 'j'.\n",
+    "\n",
+    "The letter frequencies for the English language can be found here: https://www3.nd.edu/~busiforc/handouts/cryptography/letterfrequencies.html\n",
+    "\n",
+    "In order to visualize these frequencies for a given text we need to count their occurences. Write a function, that counts all letters in a text. It should return a dict containing all letters (lower-case) in alphabetical order as keys and their respective amounts as values. Ignore special characters and white space for now. The second function should then be able to visualize the frequencies in a histogram."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8d4767f6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def count_letters(text):\n",
+    "    \"\"\"\n",
+    "    Counts the occurence of each letter in a string.\n",
+    "    \n",
+    "    Parameters:\n",
+    "        text (str): Text, of which the letters should be counted.\n",
+    "    Returns:\n",
+    "        dict[str, int]: A dictionary with letters in alphabetical order as keys and their amounts as values.\n",
+    "    \"\"\"\n",
+    "    # Your code goes here\n",
+    "    \n",
+    "    return\n",
+    "\n",
+    "def plot_frequencies(letter_frequencies):\n",
+    "    \"\"\"\n",
+    "    Plots a histogram of letter frequencies\n",
+    "    \n",
+    "    Parameters:\n",
+    "        letter_frequencies (dict[str, int]): A dictionary with letters as keys and their amounts as values.\n",
+    "    Returns:\n",
+    "        None\n",
+    "    \"\"\"\n",
+    "    # Create a bar plot\n",
+    "    plt.figure(figsize=(8, 6))\n",
+    "    plt.bar(letter_frequencies.keys(), letter_frequencies.values())\n",
+    "\n",
+    "    # Customize the plot\n",
+    "    plt.xlabel(\"Letters\")\n",
+    "    plt.ylabel(\"Frequency\")\n",
+    "    plt.title(\"Letter Distribution in Text (Ascending Order)\")\n",
+    "    plt.xticks(rotation=45)  # Rotate x-axis labels for better readability\n",
+    "\n",
+    "    # Show the plot\n",
+    "    plt.show()\n",
+    "    return\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1fcf1a0",
+   "metadata": {},
+   "source": [
+    "### Using the frequency analysis\n",
+    "\n",
+    "You just built all the tools you need to systematically break a Caesar cypher. The text document `shakethatpear.txt` holds some example text. Load this as a string and compare the distribution to the normal English distribution shown below."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f8168f9a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Put your code here\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "03674516-86f8-42a3-83e3-5ecb1c376a30",
+   "metadata": {},
+   "source": [
+    "<img src=\"340px-English-slf(1).png\" alt=\"Letter Frequencies English\" width=\"500\" height=\"auto\">\n",
+    "\n",
+    "This should look roughly similar, as we are using an English text.\n",
+    "\n",
+    "Now we use our encryption with a random key. Can you figure out the correct key from the frequency analysis of the cypher text and the standard English letter distribution? Try out your guess by decoding the cypher text!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8ae904cd",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cypher_text = encode_message(original_text, random.randint(1,25)) # If you re=run this cell, the key will change!\n",
+    "# Your code goes here"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3ed45915-a7cf-4d93-9d95-cdb591a45771",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Decoding\n",
+    "guessed_key = 0 # Put your guess here\n",
+    "decoded_text = decode_message(cypher_text, guessed_key)\n",
+    "print(\"Decoded Message:\\n\", decoded_text)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2bd70c5e",
+   "metadata": {},
+   "source": [
+    "## Conclusion and final thoughts\n",
+    "Did the last cell yield an understandable English text? If yes: Congratulations! You cracked the code.\n",
+    "\n",
+    "Let's have a few closing questions:\n",
+    "- The presented method of frequency analysis above has some advantages but also some disadvantages. Can you name some?\n",
+    "- As stated above, this cypher is not used in reality, because it is very easily broken by brute force alone. However, increasing the key space does not solve this problem on its own, as frequency analysis is easily capable to find the correct key without trying out all possible ones. What is necessary to mitigate this problem as well? "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ab55c9fd-cfa5-4aca-afa6-b6c7c976cb3c",
+   "metadata": {},
+   "source": [
+    "Answers:\n",
+    "- Advantage: Finds key without having to try all possible ones; Disadvantage: Needs a somewhat large amount of data for statistical properties to emerge, as well as knowledge about the underlying statistics.\n",
+    "- In order to make statistical properties disapear, we need to introduce randomness into the cypher. The symbol frequencies of the cypher text should not depend on the original message."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a34f3806",
+   "metadata": {},
+   "source": [
+    "## Diffie-Hellman key exchange\n",
+    "If we want to use symmetric encryption, both communication partners need to use the same key. If we simply transmit the key, everyone listening can use them. We need a secure way to exchange symmetric keys over an insecure network. The Diffie-Hellman algorithm provides a way of generating symetric keys by exchanging public info, without outsiders being able to reconstruct the key.\n",
+    "\n",
+    "It works as follows:\n",
+    "1. A public pair of numbers is used/shared, anyone can know them (g, p; g < p) \n",
+    "2. Each participant chooses a private key (Alice uses key “a” and Bob key “b”)\n",
+    "3. Both calculate $s_a = g^a \\, mod\\, p$ and $s_b = g^b \\, mod\\, p$\n",
+    "4. They exchange the results\n",
+    "5. Each one then calculates the key as: $k=s_b^a\\, mod\\, p = s_a^b\\, mod\\, p$\n",
+    "\n",
+    "Please implement an example below to check if it works"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4deae21e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import random\n",
+    "\n",
+    "g = random.randint(10,50)\n",
+    "p =\n",
+    "\n",
+    "a =\n",
+    "b =\n",
+    "\n",
+    "s_a =\n",
+    "s_b =\n",
+    "\n",
+    "k_a =\n",
+    "k_b =\n",
+    "\n",
+    "print(f\"Public info: g = {g}; p = {p}\")\n",
+    "print(f\"Secret Key of Alice: {a}. Public key of Alice: {s_a}. Shared secret as calculated by Alice: {k_a}\")\n",
+    "print(f\"Secret Key of Bob: {b}. Public key of Bob: {s_b}. Shared secret as calculated by Bob: {k_b}\")\n",
+    "print(f\"Did the procedure work: {k_a == k_b}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9889ea2c",
+   "metadata": {},
+   "source": [
+    "## RSA\n",
+    "The RSA cryptosystem is an asymetric system. Therefore it supports creating two different keys, where one key is the inverse of the other. This supports public-key encryption, so everyone can encrypt messages using the public key that only the intended receriver can read using the private one. Reversely, an entity can generate a message using its private key, which everyone can confirm is generated by that entity using its public key.\n",
+    "\n",
+    "To generate the keys several steps are necessary:\n",
+    "1. Generate two random prime numbers p and q\n",
+    "2. Calculate $n=pq$; This is the systems modulo\n",
+    "3. Calculate $\\phi(n)=(p-1)(q-1)$\n",
+    "4. Choose encryption key e such that it is coprime with $\\phi(n)$ (gcd = 1)\n",
+    "5. Generate decryption key d such that $de = 1 \\quad mod\\, \\phi(n)$\n",
+    "\n",
+    "Our public key is now the combination of e and n\n",
+    "\n",
+    "Encryption of a message m is now done like this:\n",
+    "- $m_e = m^e \\quad mod \\, n$\n",
+    "\n",
+    "And using decryption d should give back m:\n",
+    "- $m = m_e^d \\quad mod \\, n$\n",
+    "\n",
+    "While using e again should generally not work:\n",
+    "- $m \\neq m_e^e \\quad mod \\, n$\n",
+    "\n",
+    "Please implement and try out for yourself below:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2cb73e37",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import random\n",
+    "import math\n",
+    "\n",
+    "def check_prime(number):\n",
+    "    # Put code for checking if a number is prinme here\n",
+    "    \n",
+    "    \n",
+    "def check_coprime(a, b):\n",
+    "    # Put code to check if two numbers are coprime here"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ad60e2dd",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "primes = []\n",
+    "\n",
+    "# Put code generating two random primes here\n",
+    "\n",
+    "p = primes[0]\n",
+    "q = primes[1]\n",
+    "\n",
+    "# Calculate the necessary numbers and keys\n",
+    "n = \n",
+    "phi_n = \n",
+    "\n",
+    "e=\n",
+    "d=\n",
+    "\n",
+    "print(f\"Primes chosen: ({p}, {q})\")\n",
+    "print(f\"n: {n}\")\n",
+    "print(f\"Phi_n: {phi_n}\")\n",
+    "print(f\"encryption: {e}\")\n",
+    "print(f\"decryption: {d}\")\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b310b0ee",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# you can coose any message here\n",
+    "message = 721\n",
+    "\n",
+    "# Calculate the encryptet message, the wrong decryption using the same key and the correct decryption using the private key\n",
+    "message_encrypt = \n",
+    "message_decrypt_wrong = \n",
+    "message_decrypt = \n",
+    "\n",
+    "print(f\"Message: {message}\")\n",
+    "print(f\"Encrypted message: {message_encrypt}\")\n",
+    "print(f\"Wrongly decrypted message: {message_decrypt_wrong}\")\n",
+    "print(f\"Right decrypted message: {message_decrypt}\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/Excercise_3/shakethatpear.txt b/Excercise_3/shakethatpear.txt
new file mode 100644
index 0000000000000000000000000000000000000000..01e5e647d7111fc8edf6a6a9f5b26fb673761098
--- /dev/null
+++ b/Excercise_3/shakethatpear.txt
@@ -0,0 +1,33 @@
+To be, or not to be, that is the question:
+Whether 'tis nobler in the mind to suffer
+The slings and arrows of outrageous fortune,
+Or to take arms against a sea of troubles
+And by opposing end them. To die--to sleep,
+No more; and by a sleep to say we end
+The heart-ache and the thousand natural shocks
+That flesh is heir to: 'tis a consummation
+Devoutly to be wish'd. To die, to sleep;
+To sleep, perchance to dream--ay, there's the rub:
+For in that sleep of death what dreams may come,
+When we have shuffled off this mortal coil,
+Must give us pause--there's the respect
+That makes calamity of so long life.
+For who would bear the whips and scorns of time,
+Th'oppressor's wrong, the proud man's contumely,
+The pangs of dispriz'd love, the law's delay,
+The insolence of office, and the spurns
+That patient merit of th'unworthy takes,
+When he himself might his quietus make
+With a bare bodkin? Who would fardels bear,
+To grunt and sweat under a weary life,
+But that the dread of something after death,
+The undiscovere'd country, from whose bourn
+No traveller returns, puzzles the will,
+And makes us rather bear those ills we have
+Than fly to others that we know not of?
+Thus conscience doth make cowards of us all,
+And thus the native hue of resolution
+Is sicklied o'er with the pale cast of thought,
+And enterprises of great pith and moment
+With this regard their currents turn awry
+And lose the name of action.
\ No newline at end of file
diff --git a/Exercise_2_5/ASKnPSK.png b/Exercise_2_5/ASKnPSK.png
new file mode 100644
index 0000000000000000000000000000000000000000..82276b78c65e794f7f1ecf37b12834bca063e287
Binary files /dev/null and b/Exercise_2_5/ASKnPSK.png differ
diff --git a/Exercise_2_5/Exercise_4.ipynb b/Exercise_2_5/Exercise_4.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..41b2a63a63c8ff1e8a8dfbf08680043a8650d8be
--- /dev/null
+++ b/Exercise_2_5/Exercise_4.ipynb
@@ -0,0 +1,368 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Exercise 4\n",
+    "This exercise cover topics of ICT and Grid Control Sysytems\n",
+    "For all calculation tasks this Notebook was prepared."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Task 2: Modulation\n",
+    "Modulation is the process of imprinting an information signal onto a carrier signal. In many cases the carrier signal is a sinusoidal, electromagnetic wave. When  the frequency is fixed, amplitude and phase of the signal can be represented by a vector in the complex plane. Modulation schemes such as ASK or PSK use discrete values for these parameters to represent symbols. They can be visualized as points in this plane.\n",
+    "\n",
+    "<img src=\"ImagRep.png\" alt=\"Imaginary Representation of a signal\" width=\"300\" height=\"auto\">\n",
+    "<img src=\"ASKnPSK.png\" alt=\"ASK and PSK examples\" width=\"400\" height=\"auto\">\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Naturally, the closer these point are to each other, the easier it is to confuse them or for transmission errors to make one symbol look like the other. QAM tries to efficiently use the whole complex plane, while minimizing the possibility of confusion and the error rate.\n",
+    "For 16-QAM, it looks like this:\n",
+    "\n",
+    "\n",
+    "<img src=\"QAM_Amplitude.png\" alt=\"\" width=\"300\" height=\"auto\">\n",
+    "<img src=\"QAM_Phases.png\" alt=\"\" width=\"300\" height=\"auto\">\n",
+    "\n",
+    " Here, we need 3 Amplitudes and 12 phase angles to represent all symbols. The amplitudes ca be determined from the first given amplitude value `A1 = 1`"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import math"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "A1 = 1.\n",
+    "# Calculate missing values A2 and A3\n",
+    "\n",
+    "A2 = \n",
+    "A3 = \n",
+    "\n",
+    "print(f\"A1: {round(A1, 2)}\")\n",
+    "print(f\"A2: {round(A2, 2)}\")\n",
+    "print(f\"A3: {round(A3, 2)}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The Phases can be calculated using the amplitudes and some trigonometry. You may even use symmetry to you advantage"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "phase = [[],[],[]] # Use phase[i][j] as the indices as indicated in the slides\n",
+    "\n",
+    "# You code goes here\n",
+    "\n",
+    "\n",
+    "for j in range(4):\n",
+    "    for i in range(3):\n",
+    "        print(f\"phi_{i+1},{j+1}: {round(math.degrees(phase[i][j]),2)}\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Task 3: Multi-Dimensional Parity check\n",
+    "Multi-dimensional parity checks are a form of Forward-Error-Correction. A message in enlaced with parity bits such that in a matrix arangement and the parities can be checked per line and column. If a single bit is fliped, the corresponding line and column parity won't fit anymore and the error can be corrected."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "line_parity = False\n",
+    "column_parity = False\n",
+    "line_index = -1\n",
+    "column_index = -1\n",
+    "\n",
+    "block_line_size = 4\n",
+    "number = 0xDFC966\n",
+    "total_message_size = 24\n",
+    "#number = 0b1111111111001001011001101\n",
+    "#total_message_size = 25\n",
+    "\n",
+    "# Your code goes here\n",
+    "\n",
+    "# This should correct the error is the parity check works correctly:\n",
+    "if column_index != -1 and line_index != -1:\n",
+    "    fault_shift = total_message_size-1 - column_index - line_index*(block_line_size+1)\n",
+    "    corrected_number = number ^ 1<<fault_shift\n",
+    "    print(f\"Corrected number: {hex(corrected_number)}\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Task 4: Addressing and Routing\n",
+    "Dynamic routing protocols can build tables of valid routes to all connected networks by themselves. One such example is the Bellman-Ford-Algorithm. In this task we try to implement it in a distributed manner, as it would be in actual routers.\n",
+    "\n",
+    "It is you task to fill in the update table function of the Router-class below, such that the shourtest routes are determined dynamically for any scenario."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from typing import Self\n",
+    "\n",
+    "class Router:\n",
+    "    def __init__(self, initial_table: pd.DataFrame) -> None:\n",
+    "        self.neighbors = initial_table.copy()\n",
+    "        self.neighbors = self.neighbors[self.neighbors[\"time\"] != 0]\n",
+    "        self.routing_table = initial_table\n",
+    "        self.name = self.routing_table[self.routing_table[\"time\"] == 0].index[0]\n",
+    "\n",
+    "    def get_routing_table(self) -> pd.DataFrame:\n",
+    "        return self.routing_table\n",
+    "    \n",
+    "    def update_routing_table(self, routers: dict[str, Self]):\n",
+    "        for neighbor, dist in self.neighbors.iterrows():\n",
+    "            # Your code goes here\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now let's try this from a fresh topology, where each router only knows their neighbor. Does it work? What can you observe in the outputs? Will this always look the same?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "r1 = Router(pd.DataFrame(data={\"time\": [0, 1, 4], \"via\": [\"A\", \"C\", \"D\"]}, index=[\"A\", \"C\", \"D\"]))\n",
+    "r2 = Router(pd.DataFrame(data={\"time\": [0, 2, 1], \"via\": [\"B\", \"C\", \"F\"]}, index=[\"B\", \"C\", \"F\"]))\n",
+    "r3 = Router(pd.DataFrame(data={\"time\": [1, 2, 0, 2], \"via\": [\"A\", \"B\", \"C\", \"D\"]}, index=[\"A\", \"B\", \"C\", \"D\"]))\n",
+    "r4 = Router(pd.DataFrame(data={\"time\": [4, 2, 0, 2, 1], \"via\": [\"A\", \"C\", \"D\", \"E\", \"F\"]}, index=[\"A\", \"C\", \"D\", \"E\", \"F\"]))\n",
+    "r5 = Router(pd.DataFrame(data={\"time\": [2, 0, 5], \"via\": [\"D\", \"E\", \"F\"]}, index=[\"D\", \"E\", \"F\"]))\n",
+    "r6 = Router(pd.DataFrame(data={\"time\": [1, 1, 5, 0], \"via\": [\"B\", \"D\", \"E\", \"F\"]}, index=[\"B\", \"D\", \"E\", \"F\"]))\n",
+    "\n",
+    "routers = {\"A\": r1, \"B\": r2, \"C\": r3, \"D\": r4, \"E\": r5, \"F\": r6}\n",
+    "\n",
+    "for _ in range(3):\n",
+    "    table = pd.concat([r1.get_routing_table(),r2.get_routing_table(),r3.get_routing_table(),r4.get_routing_table(),r5.get_routing_table(),r6.get_routing_table()], axis=1).sort_index()\n",
+    "    table.columns = pd.MultiIndex.from_tuples([(\"A\", \"time\"), (\"A\", \"via\"), (\"B\", \"time\"), (\"B\", \"via\"), (\"C\", \"time\"), (\"C\", \"via\"), (\"D\", \"time\"), (\"D\", \"via\"), (\"E\", \"time\"), (\"E\", \"via\"), (\"F\", \"time\"), (\"F\", \"via\")], names=[\"Router\", \"Parameters\"])\n",
+    "    print(table)\n",
+    "    for r in reversed(routers.values()):\n",
+    "        r.update_routing_table(routers)\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now let's check what happens if a communication link is droped ..."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "r4.routing_table = r4.routing_table[r4.routing_table[\"via\"] != \"F\"] # We delete all routes D uses via F\n",
+    "r4.neighbors = r4.neighbors[r4.neighbors.index != \"F\"] # We delete F from Ds neighbor-table\n",
+    "r6.routing_table = r6.routing_table[r6.routing_table[\"via\"] != \"D\"] # We delete all routes F uses via D\n",
+    "r6.neighbors = r6.neighbors[r6.neighbors.index != \"D\"] # We delete D from F's neighbor-table\n",
+    "for _ in range(5):\n",
+    "    table = pd.concat([r1.get_routing_table(),r2.get_routing_table(),r3.get_routing_table(),r4.get_routing_table(),r5.get_routing_table(),r6.get_routing_table()], axis=1).sort_index()\n",
+    "    table.columns = pd.MultiIndex.from_tuples([(\"A\", \"time\"), (\"A\", \"via\"), (\"B\", \"time\"), (\"B\", \"via\"), (\"C\", \"time\"), (\"C\", \"via\"), (\"D\", \"time\"), (\"D\", \"via\"), (\"E\", \"time\"), (\"E\", \"via\"), (\"F\", \"time\"), (\"F\", \"via\")], names=[\"Router\", \"Parameters\"])\n",
+    "    print(table)\n",
+    "    for r in reversed(routers.values()):\n",
+    "        r.update_routing_table(routers)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Task 5: Network Performance Metrics\n",
+    "In a communication link there is several performance metrics which can be calculated, measured and evaluated. These metrics are for example latency, bandwidth, troughput, framerate, or the delay-bandwidth-product. These parameters can vary due to physical reasons, but also depending on the protocol layer, meassaging patter, etc."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dist_NY = 6000e3\n",
+    "dist_SF = 4000e3\n",
+    "c = 300e6\n",
+    "t_queue = 2e-3\n",
+    "t_proc = 5e-3\n",
+    "bw_NY = 10e6\n",
+    "bw_SF = 7e6\n",
+    "frame_size = 12e3\n",
+    "frame_per_min_NY = 10e3\n",
+    "frame_per_min_SF = 20e3"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### a) Latency\n",
+    "What is the one-way latency of this communication link?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "latency = \n",
+    "print(f\"Latency: {round(latency*1e3,2)} ms\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### b) Layer 1 troughput\n",
+    "What is the max. throughput of raw bits, the Layer 1 throughput?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "throughput_l1 = \n",
+    "print(f\"Layer 1 Throughut: {round(throughput_l1/1e6, 2)} MBit/s\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### c) Layer 4 troughput\n",
+    "How much layer 4 data throughput can we expect?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "headder_size = (42 + 20 + 20) * 8\n",
+    "throughput_l4 = \n",
+    "print(f\"Layer 4 Throughut: {round(throughput_l4/1e6, 2)} MBit/s\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### d) Smaller Frames, but more of them\n",
+    "How much layer 4 data throughput do we get under the same conditions but with a frame size of 1200 bit? Assume the link is able to handle 10x the amount of frames to keep a similar layer 1 throughput!\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "new_framerate = 100e3\n",
+    "throughput_l4_alt = \n",
+    "print(f\"Layer 4 Throughut: {round(throughput_l4_alt/1e3, 2)} kBit/s\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### e) Round-Trip\n",
+    "Assume conditions of subtask c). Now the top layer protocol requires acknowledgements from the receiving station after each packet before continuing to send data. What is the layer 4 throughput now? For acknowledgement, only transmission time can be neglected."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "throughput_l4_ack = \n",
+    "print(f\"Layer 4 Throughut: {round(throughput_l4_ack/1e3, 2)} kBit/s\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### f) Delay-Bandwidth-Product\n",
+    "In Full-Duplex communication, a more efficient scheme would be to acknowledge only every few packets. How many max. size packets would the transmitter need to buffer to be able to continuously send data? (round up)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "frames_to_buffer = \n",
+    "print(f\"The tranmitter needs to buffer {frames_to_buffer} frames for continuous transmission.\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": ".venv",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/Exercise_2_5/ImagRep.png b/Exercise_2_5/ImagRep.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f5f2dba2f6ceb4e9060c1d9a3ba2c3feeda47f7
Binary files /dev/null and b/Exercise_2_5/ImagRep.png differ
diff --git a/Exercise_2_5/QAM_Amplitude.png b/Exercise_2_5/QAM_Amplitude.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ae31a7fbade25b6479715a36c03a4dd142b4449
Binary files /dev/null and b/Exercise_2_5/QAM_Amplitude.png differ
diff --git a/Exercise_2_5/QAM_Phases.png b/Exercise_2_5/QAM_Phases.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bbeb9389b76cfa42b38f77657d133dbbc69c639
Binary files /dev/null and b/Exercise_2_5/QAM_Phases.png differ
diff --git a/Exercise_4/exercise_4_A6_v1_students.ipynb b/Exercise_4/exercise_4_A6_v1_students.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..95c1b9789c2d4954efade821997e8ba2b9dcb33a
--- /dev/null
+++ b/Exercise_4/exercise_4_A6_v1_students.ipynb
@@ -0,0 +1,401 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# DEV Exercise 6\n",
+    "In this Jupyter notebook we will deal with the calculation of investment and operational costs. If you have not yet installed the packages required for this exercise, run the following cell to install them."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "72ab3cf5505dbbc2"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [
+    "!pip install pandas\n",
+    "!pip install pandapower\n",
+    "!pip install simbench"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "e99cc0f65671eb71"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Now run the following cell to import the most important libraries. You can run a cell either by clicking Run on the toolbar or by pressing CTRL+RETURN."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "ddeb3acb51e3e4cd"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "initial_id",
+   "metadata": {
+    "collapsed": true,
+    "ExecuteTime": {
+     "end_time": "2024-06-03T14:20:24.081312800Z",
+     "start_time": "2024-06-03T14:20:24.064701400Z"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "import warnings\n",
+    "warnings.simplefilter(action='ignore', category=FutureWarning)\n",
+    "\n",
+    "import pandas as pd\n",
+    "pd.options.display.max_rows = 10\n",
+    "\n",
+    "import pandapower as pp\n",
+    "import simbench as sb"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Example grid\n",
+    "\n",
+    "We will carry out the calculation using a [SimBench](https://simbench.readthedocs.io/en/stable/) example grid. SimBench is a database with synthetic grid data that maps different grid structures in low voltage. Import the grid “1-LV-rural1--0-sw” and print its properties (number and type of assets). The standard types in particular are required later for the calculation of annuities. \n"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "4963cb76c621b1bc"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "ce2565ab7f9d932b"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Afterwards, you can plot the SimBench grid by using [the pandapower simple plotting tool](https://pandapower.readthedocs.io/en/v2.0.1/plotting/matplotlib/simple_plot.html)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "f25c7b3b43552621"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "2eb6d9517e99be23"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2a \n",
+    "\n",
+    "To calculate the annuities, we must first determine the investment costs of the lines and transformer types. For this purpose, read these from the Excel table using [the pandas function \"read_excel\"](https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "e43234c36ca7e4ba"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-06-03T14:21:58.923764700Z",
+     "start_time": "2024-06-03T14:21:58.809907500Z"
+    }
+   },
+   "id": "c6f5410eee07a074"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2b\n",
+    "Calculate the total investment cost in t=0 for the transformer and the lines.\n",
+    "For the equipment with unit prices the total costs can be calculated via\n",
+    "$C_{inv} = \\sum_{pc=1}^{n} {C_{pc}} = n \\cdot C_{pc}$"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "90aeb0e3ed2ee0d4"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "527eba7f1fdd1827"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "For the equipment with length related costs:\n",
+    "$C_{inv} = \\sum_{pc=1}^{n} {l_{pc} \\cdot C_l} = (\\sum_{pc=1}^{n} {l_{pc}}) \\cdot C_l$.\n",
+    "First sum up over the total line length using [pandas groupby](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.groupby.html)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "de5e30b6e28016b7"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "4731e97d3b9e66f1"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "3e4b323df9b033ec"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "Afterwards, calculate the total investment cost of the grid by:\n",
+    "$C_{grid,inv} = \\sum_{equip.} {C_{inv,equip.}}$"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "1262245f8ee4319d"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "f56701e0558fd50e"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2c\n",
+    "Calculate the annuity factors for the different lifetimes and components.\n",
+    "The annuity factors for the different lifetimes can be calculated via:\n",
+    "$a=\\frac{q^{T,max} \\cdot (q-1)}{q^{T,max}-1}$\n",
+    "\n",
+    "The lifetimes $T_{max}$ of the assets can be found in the Excel tables or data frames. First, define a function to calculate the annuity. This should have the lifetime and the discount rate $q = 1+r$ as input data. Usually, distribution grid operators assume an interest rate of $r=5\\%$."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "4a8f16d1e3d93bcc"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "outputs": [],
+   "source": [
+    "interestrate=0.05\n",
+    "\n",
+    "def annuity(n, r=interestrate):\n",
+    "    \"\"\"Calculate the annuity factor for an asset with lifetime n years and\n",
+    "    discount rate of r\"\"\"\n"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-06-03T14:22:21.141660Z",
+     "start_time": "2024-06-03T14:22:21.122542800Z"
+    }
+   },
+   "id": "ba8c3aebb2b2edf1"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "The following annuity costs then result for the components:"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "8bc1eeef24887502"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "e63a003d70957a59"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "966f8b18dc754e17"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2d\n",
+    "Afterwards, calculate the operating costs for the transformer and the lines. The operating costs can be found in the corresponding Excel files or data frames. The operating costs correspond to the multiplication of the investment costs with the specific operating costs from the Excel table or data frame."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "6e9b5e1600f9bf78"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "644d4a2306910ee6"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "1faf1a9504e028da"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2e\n",
+    "Calculate the annual cost of the grid equipment (sum of operating and investment costs)."
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "79769693879cb833"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "de48aec2ff9058bf"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2f\n",
+    "In the course of digitizing the distribution grids, it is advisable to equip most of the LV system equipment with measurement technology. For our system, the following assumptions can be made:\n",
+    "- The specific investment costs for the measurement technology system in the transformer station is 500.000€, the interest rate equals 5%/a and its lifetime equals 20 years\n",
+    "- Every grid customer within our grid will also be equipped with a smart meter. As the smart meters belong to the metering point operator and not the distribution grid operator, there are no investment costs here.\n",
+    "- Because metering technology allows the grid to operate more efficiently, the lifetimes of all other technologies increase by 5 years\n",
+    "\n",
+    "Calculate the new annuity of investment costs for the total system with the measurement technology"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "ee9bcd85e1c42abc"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "e9bf09de56cfedb1"
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Task 2g\n",
+    "What are the maximum annual operating costs for the measurement equipment in order to still be cheaper than without measurement equipment?"
+   ],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "51cf92fb26790ff"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "b749b6d14d16ff43"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "outputs": [],
+   "source": [],
+   "metadata": {
+    "collapsed": false
+   },
+   "id": "dfd9b25d20fc7b6c"
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/Exercise_4/std_type_costs.xlsx b/Exercise_4/std_type_costs.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..a5491ca162bb4c77ff6db38b4bc55047e3340a2f
Binary files /dev/null and b/Exercise_4/std_type_costs.xlsx differ
diff --git a/Quickstart.ipynb b/Quickstart.ipynb
deleted file mode 100644
index c38cd0b5b0edfad5cf668e12029d65bff03a4407..0000000000000000000000000000000000000000
--- a/Quickstart.ipynb
+++ /dev/null
@@ -1,272 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "![RWTH Logo](https://www.rwth-aachen.de/global/show_picture.asp?id=aaaaaaaaaaagazb)\n",
-    "\n",
-    "# Jupyter Example Profile Quickstart\n",
-    "\n",
-    "Welcome to JupyterLab!\n",
-    "\n",
-    "* Execute a single cell: <span class=\"fa-play fa\"></span>\n",
-    "* Execute all cells: Menu: Run <span class=\"fa-chevron-right fa\"></span> Run All Cells\n",
-    "* To reboot kernel: <span class=\"fa-refresh fa\"></span>\n",
-    "\n",
-    "Find more in the reference (menu: Help <span class=\"fa-chevron-right fa\"></span> Jupyter Reference)."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Markdown\n",
-    "\n",
-    "You can specify the cell type which is either _Code_ or _Markdown_. \n",
-    "\n",
-    "### Lists\n",
-    "\n",
-    "* Like\n",
-    "* this\n",
-    "  1. We can even nest them like\n",
-    "  2. this!\n",
-    "* Isn't that wonderfull?\n",
-    "  \n",
-    "### Images \n",
-    "\n",
-    "![Newtons cradle](https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Newtons_cradle_animation_book_2.gif/200px-Newtons_cradle_animation_book_2.gif)\n",
-    "\n",
-    "### LaTeX equations\n",
-    "\n",
-    "$$\\mathrm{e}^{\\mathrm{j} x} = \\cos(x)+\\mathrm{j}\\sin(x)$$\n",
-    "\n",
-    "### Code\n",
-    "\n",
-    "``` python\n",
-    "print(\"Hello world!\")\n",
-    "```\n",
-    "\n",
-    "### Further reading\n",
-    "Read more in the reference (menu: Help <span class=\"fa-chevron-right fa\"></span> Markdown Reference)."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Python"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "print(\"Hello world!\")"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## numpy\n",
-    "\n",
-    "Execute the cell below to see the contents of variable `a`"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import numpy as np\n",
-    "\n",
-    "a = np.array([0, 1, -5])\n",
-    "a"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Plots with matplotlib\n",
-    "\n",
-    "Nice matplotlib [tutorial](https://matplotlib.org/tutorials/introductory/usage.html#sphx-glr-tutorials-introductory-usage-py)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "%matplotlib widget\n",
-    "import matplotlib.pyplot as plt\n",
-    "import rwth_nb.plots.mpl_decorations as rwth_plots\n",
-    "\n",
-    "fs = 44100;\n",
-    "(t, deltat) = np.linspace(-10, 10, 20*fs, retstep=True) # t axis in seconds\n",
-    "\n",
-    "fig,ax = plt.subplots(); ax.grid();\n",
-    "ax.plot(t, np.sin(2*np.pi*t), 'rwth:blue')\n",
-    "ax.set_xlabel(r'$t$'); ax.set_ylabel(r'$s(t) = \\sin(2 \\pi t)$'); "
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Interactive Widgets\n",
-    "\n",
-    "Jupyter Widgets. You can find a detailled widget list [here](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html).\n",
-    "This requires to install [Jupyter Matplotlib](https://github.com/matplotlib/jupyter-matplotlib) which is called with the `%matplotlib widget` magic.\n",
-    "\n",
-    "Uses Python [decorators](https://docs.python.org/3/glossary.html#term-decorator)."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import ipywidgets as widgets\n",
-    "\n",
-    "# Create figure\n",
-    "fig0, ax0 = plt.subplots(); \n",
-    "\n",
-    "# Create update function and decorate them with widgets\n",
-    "@widgets.interact(F = widgets.FloatSlider(min=0.1, max=10, step=0.1, value=0, description='$F$:'))\n",
-    "def update_plot(F): \n",
-    "    # Generate signal with given F\n",
-    "    s = np.sin(2*np.pi*F*t)\n",
-    "          \n",
-    "    # Plot\n",
-    "    if not ax0.lines: # decorate axes with labels etc. (which is only needed once)\n",
-    "        ax0.plot(t, s, 'rwth:blue'); \n",
-    "        ax0.set_xlabel(r'$t$'); ax.set_ylabel(r'$s(t)=\\sin(2 \\pi F t)$')\n",
-    "    else: # update only lines and leave everything else as is (gives huge speed-up)\n",
-    "        ax0.lines[0].set_ydata(s)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Audio"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from IPython.display import Audio, Latex\n",
-    "\n",
-    "def audio_play(s, fs, txt=\"\", autoplay=False):\n",
-    "    if txt: display(Latex(txt))\n",
-    "    display(Audio(s, rate=fs, autoplay=autoplay))\n",
-    "\n",
-    "# Create sin\n",
-    "s = np.sin(2*np.pi*440*t)\n",
-    "\n",
-    "# Play back\n",
-    "audio_play(s, fs, r'$s(t)$')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## RWTH Colors"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# adapted from https://matplotlib.org/2.0.0/examples/color/named_colors.html\n",
-    "\n",
-    "colors = rwth_plots.colors.rwth_colors;\n",
-    "ncols = 5; nrows = len(colors.keys()) // ncols + 1;\n",
-    "    \n",
-    "fig, ax = plt.subplots()\n",
-    "X, Y = fig.get_dpi() * fig.get_size_inches() # Get height and width\n",
-    "w = X / ncols; h = Y / (nrows + 1)\n",
-    "\n",
-    "for i, name in enumerate(colors.keys()):\n",
-    "    col = i % ncols\n",
-    "    row = i // ncols\n",
-    "    y = Y - (row * h) - h\n",
-    "    \n",
-    "    xi_line = w * (col + 0.05); xf_line = w * (col + 0.25); xi_text = w * (col + 0.3)\n",
-    "    ax.text(xi_text, y, name, fontsize=10, horizontalalignment='left', verticalalignment='center')\n",
-    "    ax.hlines(y + h * 0.1, xi_line, xf_line, color=colors[name], linewidth=(h * 0.6))\n",
-    "    \n",
-    "ax.set_xlim(0, X); ax.set_ylim(0, Y); ax.set_axis_off();\n",
-    "fig.subplots_adjust(left=0, right=1, top=1, bottom=0, hspace=0, wspace=0)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Magic\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "%%svg\n",
-    "\n",
-    "<svg width='300px' height='300px'>\n",
-    "\n",
-    "<title>Small SVG example</title>\n",
-    "\n",
-    "<circle cx='120' cy='150' r='60' style='fill: gold;'>\n",
-    " <animate attributeName='r' from='2' to='80' begin='0' \n",
-    " dur='3' repeatCount='indefinite' /></circle>\n",
-    "\n",
-    "<polyline points='120 30, 25 150, 290 150' \n",
-    " stroke-width='4' stroke='brown' style='fill: none;' />\n",
-    "\n",
-    "<polygon points='210 100, 210 200, 270 150' \n",
-    " style='fill: lawngreen;' /> \n",
-    "   \n",
-    "<text x='60' y='250' fill='blue'>Hello, World!</text>\n",
-    "\n",
-    "</svg>"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.9.7"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/environment.yml b/environment.yml
index c22af46c4fc2215ba8df509db3685ce00ceec64b..596dacf13e320daa0294849eeac6dcef9576c1e3 100644
--- a/environment.yml
+++ b/environment.yml
@@ -2,4 +2,7 @@ name: base
 channels:
   - conda-forge
 dependencies:
-  - bokeh==2.4.0
+  - pandas==2.0.0
+  - numpy==1.26.2
+  - matplotlib==3.8.2
+  - pulp==2.7.0
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index fb6c7ed7ec60dafcf523d2e12daa17abc92ae384..0000000000000000000000000000000000000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-pandas