diff --git a/Exercise/EKF_UKF/EKF.m b/Exercise/EKF_UKF/EKF.m
new file mode 100644
index 0000000000000000000000000000000000000000..588df4f60958f11d55f61c01d55cfd8e4948763a
--- /dev/null
+++ b/Exercise/EKF_UKF/EKF.m
@@ -0,0 +1,16 @@
+function xhat = EKF(model,init,data)
+
+  M = length(data.y);
+  P = init.P0;
+  n = numel(init.x0);
+  
+  xhat = zeros(n,M); xhat(:,1) = init.x0;
+  
+  for l=1:M-1
+    % Measurement update
+    xhat(:,l) = FILL_IN_CODE_HERE;
+
+    % Time update
+    xhat(:,l+1) = FILL_IN_CODE_HERE;
+  end
+end
\ No newline at end of file
diff --git a/Exercise/EKF_UKF/task1.ipynb b/Exercise/EKF_UKF/task1.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..7ef82a7e1640471147f702d7756aa84821871747
--- /dev/null
+++ b/Exercise/EKF_UKF/task1.ipynb
@@ -0,0 +1,360 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "import seaborn as sns\n",
+    "import pandas as pd"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Using matplotlib backend: Qt5Agg\n"
+     ]
+    }
+   ],
+   "source": [
+    "%matplotlib\n",
+    "sns.set(style='white', rc={'lines.linewidth': 0.75})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Simulate robot movement"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Define discrete-time model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "Tfinal = 20\n",
+    "Ts = 0.1\n",
+    "V = 3\n",
+    "x0 = [10, 10, 0]  # Initial state\n",
+    "model = {}\n",
+    "model['f'] = lambda x, u, w: [x[0] + Ts*V*np.cos(x[2]), x[1] + Ts*V*np.sin(x[2]), x[2] + Ts*u]\n",
+    "model['h'] = lambda x, u: np.array([np.sqrt(x[0]**2 + x[1]**2), np.arctan2(x[1], x[0])])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Simulate robot movement and store results in a dictionary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "t = np.arange(0, Tfinal, Ts)\n",
+    "N = len(t)\n",
+    "x = np.zeros((3, N))\n",
+    "x[:, 0] = x0\n",
+    "\n",
+    "u = np.zeros((1, N))\n",
+    "u[0][np.logical_and(t >= 5, t < 10)] = -1\n",
+    "u[0][np.logical_and(t >= 10, t < 12)] = 0\n",
+    "u[0][np.logical_and(t >= 12, t < 15)] = 1\n",
+    "u[0][np.logical_and(t >= 15, t < 17)] = -1\n",
+    "\n",
+    "for ti in np.arange(0, N-1):\n",
+    "  x[:, ti+1] = model['f'](x[:, ti], u[0, ti], 0)\n",
+    "y0 = model['h'](x, u)\n",
+    "y = y0 + (np.diag([0.6, 0.03])@np.random.normal(0, 1, size=(2, N)))\n",
+    "data = {'t': t, 'x': x, 'u': u, 'y0': y0, 'y': y}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Plot nominal path"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plt.figure(10, clear=True)\n",
+    "plt.plot(data['x'][0, :], data['x'][1, :])\n",
+    "plt.plot(data['x'][0, 0], data['x'][1, 0], 'bo')\n",
+    "plt.plot(data['x'][0, -1], data['x'][1, -1], 'rx')\n",
+    "plt.axis('square')\n",
+    "plt.xlabel('x')\n",
+    "plt.ylabel('y')\n",
+    "sns.despine()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Plot measurements"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plt.figure(20, clear=True)\n",
+    "plt.subplot(2, 1, 1)\n",
+    "plt.plot(data['t'], data['y0'][0], 'b', lw=2)\n",
+    "plt.plot(data['t'], data['y'][0], 'r')\n",
+    "plt.xlabel('t [s]')\n",
+    "plt.ylabel('[m]')\n",
+    "plt.title('Range measurement')\n",
+    "sns.despine()\n",
+    "\n",
+    "plt.subplot(2, 1, 2)\n",
+    "plt.plot(data['t'], data['y0'][1]*180/np.pi, 'b', lw=2)\n",
+    "plt.plot(data['t'], data['y'][1]*180/np.pi, 'r')\n",
+    "plt.xlabel('t [s]')\n",
+    "plt.ylabel('[deg]')\n",
+    "plt.title('Angle measurement')\n",
+    "sns.despine()\n",
+    "plt.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Plot path computed from direct measurement"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Compute position estimate by inverting the measurement equation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dist = data['y'][0]\n",
+    "ang = data['y'][1]\n",
+    "pos_hat_0 = np.row_stack((dist*np.cos(ang), dist*np.sin(ang)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Plot the estimated path"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plt.figure(30, clear=True)\n",
+    "plt.plot(data['x'][0], data['x'][1])\n",
+    "plt.plot(data['x'][0, 0], data['x'][1, 0], 'bo')\n",
+    "plt.plot(data['x'][0, -1], data['x'][1, -1], 'rx')\n",
+    "plt.plot(pos_hat_0[0], pos_hat_0[1], 'k')\n",
+    "plt.axis('square')\n",
+    "plt.xlabel('x')\n",
+    "plt.ylabel('y')\n",
+    "plt.title('Direct computation from measurements')\n",
+    "sns.despine()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Plot position errors"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plt.figure(31, clear=True)\n",
+    "plt.subplot(2, 1, 1)\n",
+    "plt.plot(data['t'], data['x'][0] - pos_hat_0[0])\n",
+    "plt.xlabel('t [s]');\n",
+    "plt.ylabel('[m]')\n",
+    "plt.title('x position error in direct computation');\n",
+    "sns.despine()\n",
+    "\n",
+    "plt.subplot(2, 1, 2)\n",
+    "plt.plot(data['t'], data['x'][1] - pos_hat_0[1])\n",
+    "plt.xlabel('t [s]');\n",
+    "plt.ylabel('[m]')\n",
+    "plt.title('y position error in direct computation')\n",
+    "sns.despine()\n",
+    "\n",
+    "plt.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Extended Kalman Filter"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Implement your Extended Kalman Filter below"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 34,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def EKF(model, init, data):\n",
+    "    \"\"\"Extended Kalman Filter\n",
+    "    \n",
+    "    Input:\n",
+    "      model - Dictionary with keys n, f, h, fx, hx, Q, R\n",
+    "      init - Dictionary with x0, P0\n",
+    "    \"\"\"\n",
+    "    M = data['y'].shape[1]  # Number of data points\n",
+    "    P = init['P0']  # Initial covariance P_0|-1\n",
+    "    n = model['n']  # Number of states\n",
+    "\n",
+    "    xhat = np.zeros((n, M))  # Prepare output array\n",
+    "    xhat[:, 0] = init['x0']  # Initialize xhat\n",
+    "\n",
+    "    for l in range(0, M - 1):\n",
+    "        pass\n",
+    "        # Measurement update\n",
+    "        # FILL IN YOUR CODE HERE\n",
+    "        \n",
+    "        # Time update\n",
+    "        # FILL IN YOUR CODE HERE\n",
+    "        \n",
+    "    return xhat"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Define model dictionary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 35,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "modelEKF = {}\n",
+    "Q = None  # YOUR CODE HERE: 3x3 numpy array matrix, Process noise covariance \n",
+    "R = None # YOUR CODE HERE: 2x2 numpy array matrix, Measurement noise covariance\n",
+    "\n",
+    "modelEKF['n'] = 3\n",
+    "modelEKF['m'] = 2\n",
+    "modelEKF['Q'] = lambda x: Q\n",
+    "modelEKF['R'] = lambda x: R\n",
+    "modelEKF['f'] = lambda x, u: model['f'](x, u, 0)\n",
+    "modelEKF['h'] = lambda x, u: model['h'](x,u)\n",
+    "modelEKF['fx'] = lambda x, u: None  # YOUR CODE HERE: return df/dx(x, u)\n",
+    "modelEKF['hx'] = lambda x, u: None  # YOUR CODE HERE: return dh/dx(x, u)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Define inital conditions and run filter"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "init = {}\n",
+    "P0 = None  # YOUR CODE HERE: 2x2 numpy array matrix, Initial estimate covariance\n",
+    "x0 = None  # YOUR CODE HERE: numpy array with initial state (x0, y0, theta0)\n",
+    "\n",
+    "init['x0'] = x0\n",
+    "init['P0'] = P0\n",
+    "\n",
+    "xhatEKF = EKF(modelEKF, init, data)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Explore, investigate, and plot interesting results."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "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.0"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/Exercise/EKF_UKF/task1.m b/Exercise/EKF_UKF/task1.m
new file mode 100644
index 0000000000000000000000000000000000000000..2743e3bd5709b5d8a3c2e376715fdbd00a5a13b0
--- /dev/null
+++ b/Exercise/EKF_UKF/task1.m
@@ -0,0 +1,138 @@
+clear
+close all
+
+%% Simulate robot movement
+Ts = 0.1;
+V = 3;
+
+model.f = @(x,u,w) [x(1) + Ts*V*cos(x(3));x(2) + Ts*V*sin(x(3));x(3) + Ts*u];
+model.h = @(x,u) [sqrt(x(1,:).^2 + x(2,:).^2);atan(x(2,:)./x(1,:))];
+
+x0 = [10;10;0];
+Tfinal = 20;
+
+data.t = (0:Ts:Tfinal);
+N = numel(data.t);
+data.x = zeros(3,N); data.x(:,1) = x0;
+
+data.u = zeros(1,N);
+data.u(data.t>=5 & data.t < 10) = -1;
+data.u(data.t>=10 & data.t < 12) = 0;
+data.u(data.t>=12 & data.t < 15) = 1;
+data.u(data.t>=15 & data.t < 17) = -1;
+for t=1:N-1
+  data.x(:,t+1) = model.f(data.x(:,t),data.u(t),0);
+end
+data.y0 = model.h(data.x, data.u);
+data.y  = data.y0 + diag([0.6,0.03])*randn(2,N); 
+
+%% Plot mobile robot path
+figure(1)
+plot( data.x(1,:)', data.x(2,:)' )
+hold on
+plot( data.x(1,1)', data.x(2,1)', 'bo', ...
+  data.x(1,end)', data.x(2,end)', 'rx');
+hold off
+axis([0 max(data.x(1,:))*1.1 0 max(data.x(2,:))*1.1]);
+axis square
+xlabel('x');
+ylabel('y');
+
+%% Plot measurements
+figure(2)
+subplot( 211 )
+plot( data.t', [data.y(1,:)', data.y0(1,:)'])
+xlabel('t [s]');
+ylabel('[m]')
+title('Range measurement');
+box off
+
+subplot( 212 )
+plot( data.t', 180/pi*[data.y(2,:)', data.y0(2,:)'])
+xlabel('t [s]');
+ylabel('[deg]');
+title('Angle measurement');
+box off
+
+%% Plot path from direct measurements
+figure(3)
+plot( data.x(1,:)', data.x(2,:)' )
+hold on
+plot( data.x(1,1)', data.x(2,1)', 'bo', ...
+  data.x(1,end)', data.x(2,end)', 'rx');
+plot( (data.y(1,:).*cos(data.y(2,:)))', (data.y(1,:).*sin(data.y(2,:)))', 'k')
+hold off
+axis([0 max(data.x(1,:))*1.1 0 max(data.x(2,:))*1.1]);
+axis square
+xlabel('x');
+ylabel('y');
+title('Direct computation from measurements');
+
+%% EKF estimation
+modelEKF.f = @(x,u) FILL_IN_CODE_HERE;
+modelEKF.h = @(x,u) FILL_IN_CODE_HERE;
+modelEKF.fx = @(x,u) FILL_IN_CODE_HERE;
+modelEKF.hx = @(x,u) FILL_IN_CODE_HERE;
+modelEKF.Q = @(x) FILL_IN_CODE_HERE;
+modelEKF.R = @(x) FILL_IN_CODE_HERE;
+
+init.x0 = [10;5;0]; % Initial state estimate 
+init.P0 = FILL_IN_CODE_HERE; % Initial covariance
+tic; xhatEKF = EKF(modelEKF,init,data); toc
+
+%% Plot estimation results
+figure(20)
+plot( data.x(1,:)', data.x(2,:)', xhatEKF(1,:)', xhatEKF(2,:)' )
+hold on
+plot( data.x(1,1)', data.x(2,1)', 'bo', ...
+  data.x(1,end)', data.x(2,end)', 'rx');
+hold off
+axis([0 max(data.x(1,:))*1.1 0 max(data.x(2,:))*1.1]);
+axis square
+xlabel('x');
+ylabel('y');
+
+figure(21)
+subplot( 211 )
+plot( data.t', [xhatEKF(1,:)', data.x(1,:)'])
+xlabel('t [s]');
+ylabel('[m]')
+title('x position estimation');
+box off
+
+subplot( 212 )
+plot( data.t', xhatEKF(1,:)'-data.x(1,:)')
+xlabel('t [s]');
+ylabel('[m]')
+title('Estimation error');
+box off
+
+figure(22)
+subplot( 211 )
+plot( data.t', [xhatEKF(2,:)', data.x(2,:)'])
+xlabel('t [s]');
+ylabel('[m]')
+title('y position estimation');
+box off
+
+subplot( 212 )
+plot( data.t', xhatEKF(2,:)'-data.x(2,:)')
+xlabel('t [s]');
+ylabel('[m]');
+title('Estimation error');
+box off
+
+figure(23)
+subplot( 211 )
+plot( data.t', 180/pi*[xhatEKF(3,:)', data.x(3,:)'])
+xlabel('t [s]');
+ylabel('[deg]')
+title('Heading estimation');
+box off
+
+subplot( 212 )
+plot( data.t', 180/pi*(xhatEKF(3,:)'-data.x(3,:)'))
+xlabel('t [s]');
+ylabel('[deg]');
+title('Estimation error');
+box off
diff --git a/Lectures/slides_3.pdf b/Lectures/slides_3.pdf
index 7b37e671c899216da79dca63a5de5537c22b60f1..782e67468a3b05912fd59b48489056f27f0975ea 100644
Binary files a/Lectures/slides_3.pdf and b/Lectures/slides_3.pdf differ
diff --git a/README.md b/README.md
index 71463751e174c0c12b052fa21d10988402c49d1b..cdeb21afabad8293c0354cf2ddc8616a66b41ec9 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ A preliminary course schedule is as follows. Extra discussion seminars can be ad
 | Discussion&nbsp;2 | March 26<br> 08:15 - 10:00 | Exercise discussion | [[30](https://doi.org/10.1016/j.automatica.2006.01.026)]||
 | Lecture 3 | April 9<br> 08:15 - 10:00 | Lec 3a: Design methods, EKF and UKF, square root algorithms |[[3](https://doi.org/10.1115/1.3153059), [4](https://doi.org/10.1080/00207178708933870)], [[17;p. 55-82](http://urn.kb.se/resolve?urn=urn:nbn:se:liu:diva-11032)], [[22](https://www.fs.isy.liu.se/Edu/Courses/ObserversPhD/CourseMaterial/ArrayAlgorithm_Kailath.pdf), [9](https://doi.org/10.1109/TAC.1978.1101825), [14](https://doi.org/10.1109/JPROC.2003.823141), [23](https://doi.org/10.1109/TSP.2011.2172431)] | [slides](Lectures/slides_3.pdf)|
 | Lecture 4 | April 16<br> 08:15 - 10:00| Lec 3b: Design methods, Particle filters |||
-| Discussion&nbsp;3 | April 23<br> 08:15 - 10:00 | Exercise discussion|||
+| Discussion&nbsp;3 | April 23<br> 08:15 - 10:00 | Exercise discussion| [code skeletons](Exercise/EKF_UKF)||
 | Lecture 5 | April 30 <br> 08:15 - 10:00| Lec 4: Linearization, Lyapunov, High gain, Sliding mode, Moving Horizon Estimation |||
 | Discussion&nbsp;4 | TBD | Exercise discussion |||