## First Assignment

#### 1) Apply the appropriate string methods to the **x** variable (as '.upper') to change it exactly to: "$Dichlorodiphenyltrichloroethane$".

```python
x = "DiClOrod  IFeNi  lTRicLOr oETaNo  DiChlorod iPHeny lTrichL oroEThaNe"
```

```python
answer = x[x.find('DiChl'):].replace(" ","").capitalize()
print(answer)
```

    Dichlorodiphenyltrichloroethane

#### 2) Assign respectively the values: 'word', 15, 3.14 and 'list' to variables A, B, C and D in a single line of code. Then, print them in that same order on a single line separated by a space, using only one print statement.

```python
A, B, C, D = 'word', 15, 3.14, 'list'
print(A,B,C,D,sep=" ")

# Other option:
#print(f"{A} {B} {C} {D}")
```

    word 15 3.14 list

#### 3) Use the **input()** function to receive an input in the form **'68.4 1.71'**, that is, two floating point numbers in a line separated by space. Then, assign these numbers to the variables **w** and **h** respectively, which represent an individual's weight and height (hint: take a look at the '.split()' method). With this data, calculate the individual's Body Mass Index (BMI) from the following relationship:

\begin{equation}
BMI = \dfrac{weight}{height^2}
\end{equation}

```python
w,h = input("Enter your weight and height, respectively, separated by a space: ").split()
BMI = float(w)/float(h)**2
print(BMI)
```

    Enter your weight and height, respectively, separated by a space:  73 1.83

    21.798202394816204

#### This value can also be classified according to ranges of values, following to the table below. Use conditional structures to classify and print the classification assigned to the individual.

<\center>

(source: https://healthtravelguide.com/bmi-calculator/)

```python
if BMI <= 18.5:
    print("underweight")
elif BMI <= 24.9:
    print("normal weight")
elif BMI <= 29.9:
    print("pre-obesity")
elif BMI <= 34.9:
    print("Obesity class I")
elif BMI <= 39.9:
    print("Obesity class II")
else:
    print("Obesity class III")
```

    normal weight

#### 4) Receive an integer as an input and, using a loop, calculate the factorial of this number, that is, the product of all the integers from one to the number provided.

```python
num = int(input("Enter an integer: "))
fact = 1
for i in range(1,num+1):
    fact *= i # Equivalent to fact = fact * i
print(f"{num}! = {fact}")
```

    Enter an integer:  12

    12! = 479001600

#### 5) Using a while loop and the input function, read an indefinite number of integers until the number read is -1. Present the sum of all these numbers in the form of a print, excluding the -1 read at the end.

```python
sum_numbers, number = 0, 0
while number != -1:
    sum_numbers += number
    number = int(input("Enter an integer: "))
print(f"Sum of numbers = {sum_numbers}")
```

    Enter an integer:  12
    Enter an integer:  23
    Enter an integer:  12
    Enter an integer:  98
    Enter an integer:  23
    Enter an integer:  12
    Enter an integer:  -1

    Sum of numbers = 180

#### 6) Read the **first name** of an employee, his **amount of hours worked** and the **salary per hour** in a single line separated by commas. Next, calculate the **total salary** for this employee and show it to two decimal places.

```python
name, hours_wk, hour_sal = input("Insert name, amount of hours worked and salary per hour, separated by commas: ").split(",")
total_sal = float(hours_wk)*float(hour_sal)
print('{:.2f}'.format(total_sal))
```

    Insert name, amount of hours worked and salary per hour separated by commas:  James, 23, 100

    2300.00

#### 7) Read three floating point values **A**, **B** and **C** respectively. Then calculate itens a, b, c, d and e:

```python
A, B, C = [float(x) for x in input("Insert A, B and C").split(",")]
# a, b, c = input().split(",")
# A, B, C = float(a), float(b), float(c)
```

    Insert A, B and C 3,2,5

    a) the area of the triangle rectangle with A as the base and C as the height.

```python
print("Area:", A*C/2)
```

    Area: 7.5

    b) the area of the circle of radius C. (pi = 3.14159)

```python
print("Area:", 3.14159*C**2)
```

    Area: 78.53975

    c) the area of the trapezoid that has A and B for bases and C for height.

```python
print("Area:", (A+B)*C/2)
```

    Area: 12.5

    d) the area of the square that has side B.

```python
print("Area:", B**2)
```

    Area: 4.0

    e) the area of the rectangle that has sides A and B.

```python
print("Area:", A*B)
```

    Area: 6.0

#### 8) Read **the values a, b and c** and calculate the **roots of the second degree equation** $ax^{2}+bx+c=0$ using [this formula](https://en.wikipedia.org/wiki/Quadratic_equation). If it is not possible to calculate the roots, display the message **“There are no real roots”**.

```python
a, b, c = [float(x) for x in input("Enter a, b and c separated by commas: ").split(",")]
if a == 0:
    print("'a' should not be equal to zero!")
else:
    delta = (b**2) - (4*a*c)
    if delta < 0:
        print("no real roots")
    elif delta == 0:
        x = -b/(2*a)
        print(f"Root: {x}")
    else:
        #Reminder: sqrt(n) = n^(1/2)
        x1 = (-b+delta**0.5)/(2*a)
        x2 = (-b-delta**0.5)/(2*a)
        print(f"Roots: {x1}, {x2}")
```

    Enter a, b and c separated by commas:  2,1,-4

    Roots: 1.1861406616345072, -1.6861406616345072

#### 9) Read four floating point numerical values corresponding to the coordinates of two geographical coordinates in the cartesian plane. Each point will come in a line with its coordinates separated by commas. Then calculate and show the distance between these two points.

(obs: $d=\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$)

```python
x1, y1 = [float(i) for i in input("Enter coordinates x1 and y1: ").split(",")]
x2, y2 = [float(i) for i in input("Enter coordinates x2 and y2: ").split(",")]
d = ((x1-x2)**2+(y1-y2)**2)**0.5
print(f"d = {d}")
```

    Enter coordinates x1 and y1:  3,5
    Enter coordinates x2 and y2:  2,6

    d = 1.4142135623730951

#### 10) Read **two floating point numbers** on a line that represent **coordinates of a cartesian point**. With this, use **conditional structures** to determine if you are at the origin, printing the message **'origin'**; in one of the axes, printing **'x axis'** or **'y axis'**; or in one of the four quadrants, printing **'q1'**, **'q2**', **'q3'** or **'q4'**.

```python
x, y = [float(x) for x in input("Insert x e y: ").split(",")]

if x == 0 and y == 0:
    print('origin')
elif x == 0:
    print('y axis')
elif y == 0:
    print('x axis')
elif x > 0 and y > 0:
    print('q1')
elif x > 0:
    print('q4')
elif y > 0:
    print('q2')
else:
    print('q3')
```

#### 11) Read an integer that represents a phone code for international dialing.
#### Then, inform to which country the code belongs to, considering the generated table below:
(You just need to consider the first 10 entries)

```python
import pandas as pd
df = pd.read_html('https://en.wikipedia.org/wiki/Telephone_numbers_in_Europe')[1]
df = df.iloc[:,:2]
df.head(20)
```

      Country
      Country calling code

      0
      Austria
      43

      1
      Belgium
      32

      2
      Bulgaria
      359

      3
      Croatia
      385

      4
      Cyprus
      357

      5
      Czech Republic
      420

      6
      Denmark
      45

      7
      Estonia
      372

      8
      Finland
      358

      9
      France
      33

      10
      Germany
      49

      11
      Greece
      30

      12
      Hungary
      36

      13
      Iceland
      354

      14
      Ireland
      353

      15
      Italy
      39

      16
      Latvia
      371

      17
      Liechtenstein
      423

      18
      Lithuania
      370

      19
      Luxembourg
      352

```python
ccode = int(input("Enter the country calling code:"))
code_dict = {43:'Austria',
             32:'Belgium',
             359:'Bulgaria',
             385:'Croatia',
             357:'Cyprus',
             420:'Czech Republic',
             45:'Denmark',
             372:'Estonia',
             359:'Finland',
             33:'France',
            }
if ccode in code_dict.keys():
    print(code_dict[ccode])
else:
    print('Code not recognized.')
```

    Enter the country calling code: 43

    Austria

#### 12) Write a piece of code that reads 6 numbers in a row. Next, show the number of positive values entered. On the next line, print the average of the values to one decimal place.

```python
responses = [float(x) for x in input("Enter six values separated by commas: ").split(",")]

pos, total = 0, 0
for i in responses:
    total += i
    if i > 0:
        pos += 1
print(f"{pos} positive numbers")
print(f"Average = {total/6}")
```

    Enter six values separated by commas:  3,-4,98,1,-4,5

    4 positive numbers
    Average = 16.5

#### 13) Read an integer **N**. Then print the **square of each of the even values**, from 1 to N, including N, if applicable, arranged one per line.

```python
N = int(input("Enter number N: "))
for i in range(1,N+1):
    if i%2==0:
        print(i**2)
```

    Enter number N:  20

    4
    16
    36
    64
    100
    144
    196
    256
    324
    400

#### 14) Using **input()**, read an integer and print its classification as **'even / odd'** and **'positive / negative'** . The two classes for the number must be printed on the same line separated by a space. In the case of zero, print only **'null'**.

```python
number = int(input('Enter an integer: '))
class1 = "even" if number%2 == 0 else "odd"
class2 = 'positive' if number == abs(number) else 'negative'
if number == 0:
    print("Null")
else:
    print(class1, class2)
```

    Enter an integer:  23

    odd positive

## Challenge
#### 15) Ordering problems are recurrent in the history of programming. Over time, several algorithms have been developed to fulfill this function. The simplest of these algorithms is the [**Bubble Sort**](https://en.wikipedia.org/wiki/Bubble_sort), which is based on comparisons of elements two by two in a loop of passes through the elements. Your mission, if you decide to accept it, will be to input six whole numbers ramdonly ordered. Then implement the **Bubble Sort** principle to order these six numbers **using only loops and conditionals**.
#### At the end, print the six numbers in ascending order on a single line separated by spaces.

```python
numbers = [int(x) for x in input("Enter any 6 integers: ").split(",")]
changed = True
while changed:
    changed = False
    for i in range(0,len(numbers)-1):
        if numbers[i] > numbers[i+1]:
            numbers[i],numbers[i+1] = numbers[i+1],numbers[i]
#           aux = numbers[i]                                        #alternative way of doing
#           numbers[i] = numbers[i+1]
#           numbers[i+1] = aux
            changed = True
print(' '.join([str(x) for x in numbers]))
```

    Enter any 6 integers:  4,6,1,4,6,9

    1 4 4 6 6 9

```python

```


## Decay Law and Other Functions
\begin{equation}
 A      (t)=A_{0}e^{-\lambda t}
\end{equation}

```python
import numpy as np
import matplotlib.pyplot as plt

Ao=50 # Initial Activity of radioactive substance in Curie
#Half Life of Ir192= 74 Years

t = np.linspace(0,74,10)
A = Ao*np.exp(-0.693*t/74)

#xerr = np.random.random_sample(10)
yerr = np.random.random_sample(10)

fig, ax = plt.subplots()

ax.errorbar(t, A,

            yerr=yerr,
            fmt='-o',capsize=5)

ax.set_xlabel('Time (Year)')
ax.set_ylabel('Activity (Curie)')
ax.set_title(' Decay Scheme')

plt.show()

```

```python
import numpy as np
import matplotlib.pyplot as plt
Ao=50 # Initial Activity of radioactive substance in Curie
#Half Life of Ir192= 74 Years
t = np.linspace(0,74,20)
A = Ao*np.exp(-0.693*t/74)
xerr = 5 #year
yerr = 5 # Curie

fig, ax = plt.subplots()

ax.errorbar(t, A,
            xerr=xerr,
            yerr=yerr,
            fmt='-o', color='blue',
             ecolor='black', elinewidth=2, capsize=2)

ax.set_xlabel('Time (Year)')
ax.set_ylabel('Activity (Curie)')
ax.set_title(' Decay Scheme')

plt.show()
```

## Plot Four functions plots in a single call to plot.

```python
import numpy as np
import matplotlib.pyplot as plt

# evenly sampled time at 200ms intervals
x = np.arange(1., 5., 0.4)

# red dashes, blue squares, green triangles and reverse dashes with opposite triangle
plt.plot(x, x, 'r--', x, x**2, 'bs', x, x**3, 'g^', x,1/x,'v--')

plt.show()
```

```python
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
import numpy as np

x = np.linspace(0, 10, 1000)

fig, ax = plt.subplots()
ax.plot(x, np.sin(x), '-b', label='Sine')
ax.plot(x, np.cos(x), '--r', label='Cosine')
plt.ylabel('trigronometic function')
plt.xlabel('x(radian)')

ax.axis('equal')
leg = ax.legend();

```


## Introduction
-----

In this assignment you will recursively estimate the position of a vehicle along a trajectory using available measurements and a motion model.

The vehicle is equipped with a very simple type of LIDAR sensor, which returns range and bearing measurements corresponding to individual landmarks in the environment. The global positions of the landmarks are assumed to be known beforehand. We will also assume known data association, that is, which measurment belong to which landmark.

## Motion and Measurement Models
-----

### Motion Model

The vehicle motion model recieves linear and angular velocity odometry readings as inputs, and outputs the state (i.e., the 2D pose) of the vehicle:

\begin{align}
\mathbf{x}_{k} &= \mathbf{x}_{k-1} + T
\begin{bmatrix}
\cos\theta_{k-1} &0 \\
\sin\theta_{k-1} &0 \\
0 &1
\end{bmatrix}
\left(
\begin{bmatrix}
v_k \\
\omega_k
\end{bmatrix}
+ \mathbf{w}_k
\right)
\, , \, \, \, \, \, \mathbf{w}_k = \mathcal{N}\left(\mathbf{0}, \mathbf{Q}\right)
\end{align}

- $\mathbf{x}_k = \left[ x \, y \, \theta \right]^T$ is the current 2D pose of the vehicle
- $v_k$ and $\omega_k$ are the linear and angular velocity odometry readings, which we use as inputs to the model

The process noise $\mathbf{w}_k$ has a (zero mean) normal distribution with a constant covariance $\mathbf{Q}$.

### Measurement Model

The measurement model relates the current pose of the vehicle to the LIDAR range and bearing measurements $\mathbf{y}^l_k = \left[r \, \phi \right]^T$.

\begin{align}
\mathbf{y}^l_k =
\begin{bmatrix}
\sqrt{(x_l - x_k - d\cos\theta_{k})^2 + (y_l - y_k - d\sin\theta_{k})^2} \\
atan2\left(y_l - y_k - d\sin\theta_{k},x_l - x_k - d\cos\theta_{k}\right) - \theta_k
\end{bmatrix}
+
\mathbf{n}^l_k
\, , \, \, \, \, \, \mathbf{n}^l_k = \mathcal{N}\left(\mathbf{0}, \mathbf{R}\right)
\end{align}

- $x_l$ and $y_l$ are the ground truth coordinates of the landmark $l$
- $x_k$ and $y_k$ and $\theta_{k}$ represent the current pose of the vehicle
- $d$ is the known distance between robot center and laser rangefinder (LIDAR)

The landmark measurement noise $\mathbf{n}^l_k$ has a (zero mean) normal distribution with a constant covariance $\mathbf{R}$.

## Getting Started
-----

Since the models above are nonlinear, we recommend using the extended Kalman filter (EKF) as the state estimator.
Specifically, you will need to provide code implementing the following steps:
- the prediction step, which uses odometry measurements and the motion model to produce a state and covariance estimate at a given timestep, and
- the correction step, which uses the range and bearing measurements provided by the LIDAR to correct the pose and pose covariance estimates

### Unpack the Data
First, let's unpack the available data:

```python
from math import cos, sin, sqrt, atan2
import pickle
import numpy as np
from numpy.linalg import inv
import matplotlib.pyplot as plt

with open('data/data.pickle', 'rb') as f:
    data = pickle.load(f)

t = data['t']  # timestamps [s]

x_init  = data['x_init'] # initial x position [m]
y_init  = data['y_init'] # initial y position [m]
th_init = data['th_init'] # initial theta position [rad]

# input signal
v  = data['v']  # translational velocity input [m/s]
om = data['om']  # rotational velocity input [rad/s]

# bearing and range measurements, LIDAR constants
b = data['b']  # bearing to each landmarks center in the frame attached to the laser [rad]
r = data['r']  # range measurements [m]
l = data['l']  # x,y positions of landmarks [m]
d = data['d']  # distance between robot center and laser rangefinder [m]
```

Note that distance from the LIDAR frame to the robot center is provided and loaded as an array into the `d` variable.

### Ground Truth
If available, it is useful to plot the ground truth position and orientation before starting the assignment.

Notice that the orientation values are wrapped to the $\left[-\pi,\pi\right]$ range in radians.

### Initializing Parameters

Now that our data is loaded, we can begin getting things set up for our solver. One of the
most important aspects of designing a filter is determining the input and measurement noise covariance matrices, as well as the initial state and covariance values. We set the values here:

```python
v_var = 1  # translation velocity variance
om_var = 5  # rotational velocity variance
r_var = 0.05  # range measurements variance
b_var = 0.05  # bearing measurement variance

Q_km = np.diag([v_var, om_var]) # input noise covariance
cov_y = np.diag([r_var, b_var])  # measurement noise covariance

x_est = np.zeros([len(v), 3])  # estimated states, x, y, and theta
P_est = np.zeros([len(v), 3, 3])  # state covariance matrices

x_est[0] = np.array([x_init, y_init, th_init]) # initial state
P_est[0] = np.diag([1, 1, 0.1]) # initial state covariance

I = np.eye(3)
```

**Remember:** that it is neccessary to tune the measurement noise variances `r_var`, `b_var` in order for the filter to perform well!

In order for the orientation estimates to coincide with the bearing measurements, it is also neccessary to wrap all estimated $\theta$ values to the $(-\pi , \pi]$ range.

```python
# Wraps angle to (-pi,pi] range
def wraptopi(x):
   # return (x + np.pi) % (2 * np.pi) - np.pi
    if x > np.pi:
        x = x - (np.floor(x / (2 * np.pi)) + 1) * 2 * np.pi
    elif x < -np.pi:
        x = x + (np.floor(x / (-2 * np.pi)) + 1) * 2 * np.pi
    return x
```

## Correction Step
-----
First, let's implement the measurement update function, which takes an available landmark measurement $l$ and updates the current state estimate $\mathbf{\check{x}}_k$.
For each landmark measurement received at a given timestep $k$, you should implement the following steps:

- Compute the measurement model Jacobians at $\mathbf{\check{x}}_{k}$
\begin{align}
\mathbf{y}^l_k = &\mathbf{h}(\mathbf{x}_{k}, \mathbf{n}^l_k) \\\\
\mathbf{H}_{k} = \frac{\partial \mathbf{h}}{\partial \mathbf{x}_{k}}\bigg|_{\mathbf{\check{x}}_{k},0}& \, , \, \, \, \,
\mathbf{M}_{k} = \frac{\partial \mathbf{h}}{\partial \mathbf{n}_{k}}\bigg|_{\mathbf{\check{x}}_{k},0} \, .
\end{align}
- Compute the Kalman Gain
\begin{align}
\mathbf{K}_k &= \mathbf{\check{P}}_k \mathbf{H}_k^T \left(\mathbf{H}_k \mathbf{\check{P}}_k \mathbf{H}_k^T + \mathbf{M}_k \mathbf{R}_k \mathbf{M}_k^T \right)^{-1}
\end{align}
- Correct the predicted state
\begin{align}
\mathbf{\check{y}}^l_k &= \mathbf{h}\left(\mathbf{\check{x}}_k, \mathbf{0}\right) \\
\mathbf{\hat{x}}_k &= \mathbf{\check{x}}_k + \mathbf{K}_k \left(\mathbf{y}^l_k - \mathbf{\check{y}}^l_k\right)
\end{align}
- Correct the covariance
\begin{align}
\mathbf{\hat{P}}_k &= \left(\mathbf{I} - \mathbf{K}_k \mathbf{H}_k \right)\mathbf{\check{P}}_k
\end{align}

```python
def measurement_update(lk, rk, bk, P_check, x_check):
    x_check[2] = wraptopi(x_check[2])
    x = x_check
    P = P_check
    xk = x[0]
    yk = x[1]
    theta = x[2]
    xl = lk[0]
    yl = lk[1]

    dx = xl - xk  - d * cos(theta)
    dy = yl - yk  - d * sin(theta)
    r = sqrt(dx**2 + dy**2)
    phi = atan2(dy, dx) - theta
    y = np.vstack([r, wraptopi(phi)])
    y_measured = np.vstack([rk, wraptopi(bk)])

    # 1. Compute measurement Jacobian
    M = np.eye(2)
    H = np.zeros((2, 3))
    H[0, 0] = -dx / r
    H[0, 1] = -dy / r
    H[0, 2] = d * (dx * sin(theta) - dy * cos(theta)) / r
    H[1, 0] = dy / r**2
    H[1, 1] = -dx / r**2
    H[1, 2] = (d * (dy * sin(theta) - dx * cos(theta)) / r**2) -1

    # 2. Compute Kalman Gain
    K = P @ H.T @ inv(H @ P @ H.T + M @ cov_y @ M.T)

    # 3. Correct predicted state (remember to wrap the angles to [-pi,pi])
    x_check = x + K @ (y_measured - y)
    x_check[2] = wraptopi(x_check[2])

    # 4. Correct covariance
    P_check = (I - K @ H) @ P

    return x_check, P_check
```

## Prediction Step
-----
Now, implement the main filter loop, defining the prediction step of the EKF using the motion model provided:

\begin{align}
\mathbf{\check{x}}_k &= \mathbf{f}\left(\mathbf{\hat{x}}_{k-1}, \mathbf{u}_{k-1}, \mathbf{0} \right) \\
\mathbf{\check{P}}_k &= \mathbf{F}_{k-1}\mathbf{\hat{P}}_{k-1}\mathbf{F}_{k-1}^T + \mathbf{L}_{k-1}\mathbf{Q}_{k-1}\mathbf{L}_{k-1}^T \, .
\end{align}

Where

\begin{align}
\mathbf{F}_{k-1} = \frac{\partial \mathbf{f}}{\partial \mathbf{x}_{k-1}}\bigg|_{\mathbf{\hat{x}}_{k-1},\mathbf{u}_{k},0}  \, , \, \, \, \,
\mathbf{L}_{k-1} = \frac{\partial \mathbf{f}}{\partial \mathbf{w}_{k}}\bigg|_{\mathbf{\hat{x}}_{k-1},\mathbf{u}_{k},0} \, .
\end{align}

```python
#### 5. Main Filter Loop #######################################################################
x_check = x_est[0, :].reshape(3,1)
P_check = P_est[0]
for k in range(1, len(t)):  # start at 1 because we've set the initial prediciton

    delta_t = t[k] - t[k - 1]  # time step (difference between timestamps)
    x_check[2] = wraptopi(x_check[2])
    theta = wraptopi(x_check[2])

    # 1. Update state with odometry readings (remember to wrap the angles to [-pi,pi])
    F = np.array([[np.cos(theta), 0],
                  [np.sin(theta), 0],
                  [0, 1]], dtype='float')
    inp = np.array([[v[k-1]], [om[k-1]]])

    x_check = x_check + F.dot(inp).dot(delta_t)
    x_check[2] = wraptopi(x_check[2])

    # 2. Motion model jacobian with respect to last state
    F_km = np.zeros([3, 3])
    F_km = np.array([[1, 0, -np.sin(theta)*delta_t*v[k-1]],
                     [0, 1, np.cos(theta)*delta_t*v[k-1]],
                     [0, 0, 1]], dtype='float')

    # 3. Motion model jacobian with respect to noise
    L_km = np.zeros([3, 2])
    L_km = np.array([[np.cos(theta)*delta_t, 0],
                    [np.sin(theta)*delta_t, 0],
                    [0,1]], dtype='float')

    # 4. Propagate uncertainty
    P_check = F_km @ P_check @ F_km.T + L_km @ Q_km @ L_km.T

    # 5. Update state estimate using available landmark measurements
    for i in range(len(r[k])):
        x_check, P_check = measurement_update(l[i], r[k, i], b[k, i], P_check, x_check)

    # Set final state predictions for timestep
    x_est[k, 0] = x_check[0]
    x_est[k, 1] = x_check[1]
    x_est[k, 2] = x_check[2]
    P_est[k, :, :] = P_check
```

Let's plot the resulting state estimates:

```python
e_fig = plt.figure()
ax = e_fig.add_subplot(111)
ax.plot(x_est[:, 0], x_est[:, 1])
ax.set_xlabel('x [m]')
ax.set_ylabel('y [m]')
ax.set_title('Estimated trajectory')
plt.show()

e_fig = plt.figure()
ax = e_fig.add_subplot(111)
ax.plot(t[:], x_est[:, 2])
ax.set_xlabel('Time [s]')
ax.set_ylabel('theta [rad]')
ax.set_title('Estimated trajectory')
plt.show()
```

Are you satisfied wth your results? The resulting trajectory should closely resemble the ground truth, with minor "jumps" in the orientation estimate due to angle wrapping. If this is the case, run the code below to produce your solution file.

```python
with open('submission.pkl', 'wb') as f:
    pickle.dump(x_est, f, pickle.HIGHEST_PROTOCOL)
```

```python

```


#Digital Convolution
  💊 💊 * 🧑 🧑 🧑 = ?
##Introduction
Convolution is the mathematical operation that combines two functions to obtain a third.  It can be applied to both continuous time and as well as discrete time signals.  Convolution of discrete time signals will be consider here.

##Mathematical Definition
Digital Convolution is a mathematical operation that maps two discrete-time sequences, x(n) and h(n), into a third discrete-time sequence, y(n).
\begin{equation}
\begin{split}
 y(n) &= h(n) \ast x(n)  \;\;\;\;\;-\infty < n < \infty \\
 y(n) &= \sum_{k=-\infty}^{\infty}h(k) x(n-k) =  \sum_{k=-\infty}^{\infty}h(n-k) x(k)
\end{split}
\end{equation}
Where y(n) is the resultant of the *convolutional sum* of h(n) with x(n). _[where $\ast$ represents the convolutional operator]_.

If both sequences are causal, that is $x(n) = h(n) = 0 \;\;\forall \;\;n \lt 0$, then the limits on the summation are restrained between 0 and n.
\begin{equation}
\begin{split}
 y(n) &= h(n) \ast x(n) = \sum_{k=0}^{n}h(k) x(n-k) \;\;\;\forall\;n \ge 0
\end{split}
\end{equation}

Convolution is linear operation and has the associative, commutative, and  distributive properties which will be explored in detail below.

Convolution can best be described by examples.  Consider a patient who receives medication over several consecutive days with a different dose each day.  We will see that the number of units of medication administered to a group of patients over several days can be obtained from the convolution of the medication dosage regime with the patient population.

In another example, assume you have two fair dice.  When the *fair* dice are rolled, the probability that any number 1 to 6 appears at the face value of each die is 1/6 since there is an equal probability that any will appear.  Therefore, the sum of the face values of the two dice will be in the range between two and twelve where two is the result of two ones and twelve is two sixes.  It will be shown that the probability density function of the sum of the face values is the convolution of the two sequences with elements [1/6,1/6,1/6,1/6,1/6,1/6].

##Medication Example
Let us first consider the following examples that will show how _convolution_ describes the amount of medication required for a group of patients over a series of a few days.  These examples will verify the linearity, commutative, and distributive properties of convolution.

```python
# First Load Required Libraries
import numpy as np
import scipy as sp
import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt
```

##Daily Medication Delivered
The medication administered daily can be obtained from the number of patients admitted each day combined with the units of medication administered each day.  Assume the medication was given to each patient over a three day sequence.

On the initial day: the units of medication administered, y(0), is obtained from the number of patients admitted on the initial day, x(0), times units of medication for the initial day, h(0) **_(indexing begins with 0 corresponding to initial day)_**.

On the following day: the units of medication administered, y(1), is obtained from the patients admitted on following day, x(1), times the units of medication received for their initial day's dosage, h(0), plus the patients admitted on the initial day, x(0), times the units of medication for their following day's dosage, h(1).  The number of units of medication administered for each day is defined by the following equations.  The medication is administered to each patient over three consecutive days.

* Daily medication units, y(n), obtained from h(n) _(daily medication units)_ and x(n) _(daily patient admissions)_

\begin{equation}
\begin{split}
 y(0) &= h(0) x(0)\\
 y(1) &= h(0) x(1) + h(1) x(0)\\
 y(2) &= h(0) x(2) + h(1) x(1) + h(2) x(0) \\
 y(3) &= h(0) x(3) + h(1) x(2) + h(2) x(1) \\
& \vdots \\
 y(n) &= \sum_{k=0}^{n}h(k) x(n-k)
\end{split}
\end{equation}
* The daily units of medication y(n) is the **convolution** of x(n) with h(n)
* **Commutative** Property of Convolution _(order independent)_.  As can be observed, convolution is independent of the order.
\begin{equation}
\begin{split}
 y(0) &= h(0) x(0) = x(0) h(0)\\
 y(1) &= h(0) x(1) + h(1) x(0) = x(0) h(1) + x(1) h(0)\\
 y(2) &= h(0) x(2) + h(1) x(1) + h(2) x(0) = x(0) h(2) + x(1) h(1) + x(2) h(0)\\
 & \vdots \\
 y(n) &= \sum_{k=0}^{n}h(k) x(n-k) = \sum_{k=0}^{n} x(k)h(n-k) = h(n) \ast x(n) = x(n) \ast h(n)
\end{split}
\end{equation}
 _[where $\ast$ represents the convolutional operator]_
* In Block Diagram Form

 $$x(n) \longrightarrow \boxed{\\h(n)}
\longrightarrow y(n)$$

###Daily Medication Units Administered
A group of patients were admitted on several consecutive days, and each patient receive medication on the day of admission and several consecutive days.  The number of patients admitted each day is contained in the array x, and the units of medication administered each day is defined by the array h.

```python
# Next Enter Example Data
# Input, x, in this exmaple, the number of patients receiving medication each day
# One patient on the initial day, two patients on the following day, ...
x = [1, 2, 3, 4]
# The units of medicine administered once a day per patient for a three day sequence
h = [3, 2, 1]
```

```python
# Convolve h with x to find the daily medication distribution
y = np.convolve(h,x)        # medication units per day
med_total = np.sum(y)       # total medication
n = np.array(range(0,len(y)))   # the day sequence starting with 0
fig = plt.figure()
(markers, stemlines, baseline) = plt.stem(n,y,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units (y)', fontsize = 14)
plt.title('Medication Units Administered Daily, (Total Medication over 6 days = '+str(med_total)+' Units)',fontsize = 14)
plt.show()
print('Daily Patient Admissions       : x(n) =',x)
print('Daily Medication Dosage        : h(n) =',h)
print('Daily Medication Admisistered  : y(n) = h(n) * x(n) =',y)
```

##Verify Linearity of the Convolution Operator
Linear operators must satisfy both **additivity** and **homogeneity** (scaling) properties.  First consider scaling.

###Double the Number of Patients

### Twice the Patients Doubles the Medication
If the number of patients is scaled by a constant factor each day, the medication administered is increased by that same amount.  In the example shown below, the number of patients scalede by a factor of two.
\begin{equation}
\begin{split}
 y_{scaled} (n) &= h(n) \ast (2\times x(n)) = 2\times (h(n) \ast x(n)) = 2\times y(n)\\
 y_{scaled} (n) &= \sum_{k=0}^{n}h(k) (2\times x(n-k)) = 2\times \sum_{k=0}^{n} h(k)x(n-k) = 2\times y(n)
\end{split}
\end{equation}
Scaling the input, scales the output by the same factor which satisfies the **homogeneity** (or **scaling**) property of convolution.

```python
xD = np.array(x)*2      # Double the number of patients
yD = np.convolve(h,xD)
print('Day Sequence                                        :',n)
print('Daily Units Administered with Initial Patient Group :',y)
print('Daily Units Administered with Twice the Patients    :',yD)
print('\nGraphical Format')
dict = {'y':y,'y_D':yD}
med_total = np.sum(y)
df = pd.DataFrame(dict)
df.style

plt.figure(figsize=[14, 5])
plt.subplot(1, 2, 1)
(markers, stemlines, baseline) = plt.stem(n,y, use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units', fontsize = 14)
plt.title('Initial Patient Group, (Total Medication Given = '+str(med_total)+')',
          fontsize = 14)
#plt.show()
plt.subplot(1, 2, 2)
med_total = np.sum(yD)
(markers, stemlines, baseline) = plt.stem(n,yD,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units', fontsize = 14)
plt.title('Twice the Patients, (Total Medication Given = '+str(med_total)+')',
          fontsize = 14)
plt.show()

```

###Next Consider the Additivity Property
###Add a Second Group of Patients

###Medication Requirement for Two Groups of Patients
* $x_1$ - patient group 1, $x_2$ - patient group 2, h - medication dosage

\begin{equation}
\begin{split}
 y_{1 + 2} (n) &= h(n) \ast (x_1(n)+x_2(n)) = h(n) \ast x_1(n) + h(n) \ast x_2(n)\\
 y_{1 + 2} (n) &= y_1(n) + y_2(n)
\end{split}
\end{equation}

* The medication required from the combination (sum) of two groups of patients is equivalent to the sum of the medication given to each group individually.
* Convolution satisfies the **additivity** propperty.
* Convolution is a **linear** operation since it meets both the **additivity** and **homogeneity** (scaling) properties.

```python
x2 = np.ones(len(x))*2  # Group 2
y2 = np.convolve(h,x2)  # units per day
n = np.array(range(0,len(y2)))
(markers, stemlines, baseline) = plt.stem(n, y2, use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units', fontsize = 14)
plt.title('Medication for Patient Group Two', fontsize = 14)
plt.show()
```

```python
x1 = x
y1_2 = np.convolve(h,np.array(x1)+np.array(x2))
y1 = y
y1py2 = np.array(y1) + np.array(y2)

plt.figure(figsize=[14, 5])
plt.subplot(1, 2, 1)
(markers, stemlines, baseline) = plt.stem(n, y1py2, use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units', fontsize = 14)
plt.title('Sum of Medications for Group 1 & 2', fontsize = 14)
plt.subplot(1, 2, 2)
(markers, stemlines, baseline) = plt.stem(n, y1_2, use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Days Since First Patient Admission', fontsize = 14)
plt.ylabel('Daily Medication Units', fontsize = 14)
plt.title('Medication With Combined Groups', fontsize = 14)
plt.show()
```

###Medication Requirement for Two Groups of Patients

* The medication required from the combination (sum) of two groups of patients is equivalent to the sum of the medication given to each group individually.
* Convolution satisfies the **additivity** propperty.
* Convolution is a **linear** operation since it meets both the **additivity** and **homogeneity** (scaling) properties.

##Associative and Distributive Properties

In addition to linearity and commutative property, the convolution operator exhibits **associative** and **distributive** properties.

First consider the distributive property.

##Distributed Property
###Medication Administered BID (Morning and Evening)
* Separating Morning and Evening Medication

\begin{equation}
\begin{split}
 y_{Morning} (n) &= h_{Morning}(n) \ast x(n)\\
 y_{Evening} (n) &= h_{Evening}(n) \ast x(n)\\
 y_{Daily Total (BID)} &= (h_{Morning}(n) * x(n) + h_{Evening}(n) \ast x(n)) = (h_{Morning}(n) + h_{Evening}(n)) \ast x(n)
\end{split}
\end{equation}

* $h_{Morning}(n)$ - Morning Medication, $h_{Evening}(n)$ - Evening Medication, x(n) - Patient Admissions
* This is the **Distributive** Property of Convolution.

```python
# Medication Administered Twice a Day
#  h2d = [morning day 1, evening day 1, morning day 2, evening day 2, ... ]
#  h2d[morning day 1] = (2/3)*h[day 1]
#  h2d[evening day 1] = (1/3)*h[day 1]
#  h2d[morning and evening following days] = (1/2) * h[daily dose]
#  x2d = [morning admission day 1, evening admission day 1, ... ]
#     patients admitted once a day
h2d = np.zeros(2*len(h))
h2d[::2] = h                      # Medication units administrated BID
h2d = (h2d + np.roll(h2d,1))/2
h2d[0] = 4*h2d[0]/3
h2d[1] = 2*h2d[1]/3
x2d = np.zeros(2*len(x))
x2d[::2] = x                      # Patients admits on 12 hour intervals

#h2d = [2, 1, 1, 1, 1/2, 1/2]     # Medication units administrated BID
#x2d = [1, 0, 2, 0, 3, 0, 4, 0]    # Patients admits on 12 hour intervals

y2d = np.convolve(h2d,x2d)
med_total = np.sum(y2d)
n2d = np.array(range(0,len(y2d)))

# Separate Morning and Evening Medication
#h2dM = [2, 0, 1, 0, 1/2, 0]     # Medication units administrated Morning
h2dM = np.copy(h2d)
for index in range(1,len(h2dM),2):
  h2dM[index] = 0
#h2dE = [0, 1, 0, 1, 0, 1/2]     # Medication units administrated Evening
h2dE = np.copy(h2d)
for index in range(0,len(h2dE),2):
  h2dE[index] = 0
#x2d = [1, 0, 2, 0, 3, 0, 4, 0]    # Patients admits on 12 hour intervals

y2dM = np.convolve(h2dM,x2d)
y2dE = np.convolve(h2dE,x2d)
med_totalM = np.sum(y2dM)
med_totalE = np.sum(y2dE)
n2d = np.array(range(0,len(y2d)))
#fig = plt.figure()
plt.figure(figsize=[16, 5])
plt.subplot(1, 2, 1)

(markers, stemlines, baseline) = plt.stem(n2d,y2dM,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2,mfc="green")
plt.grid()
plt.xlabel('12 Hour Periods Since First Patient Admission', fontsize = 14)
plt.ylabel('Medication Units ($y_{Morning}$)', fontsize = 14)
plt.title('Morning Medication Units, (Total Morning Medication = '+str(med_totalM)+')',
          fontsize = 14)
#plt.show()
#fig = plt.figure()
plt.subplot(1, 2, 2)

(markers, stemlines, baseline) = plt.stem(n2d,y2dE,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('12 Hour Periods Since First Patient Admission', fontsize = 14)
plt.ylabel('Medication Units ($y_{Evening}$)', fontsize = 14)
plt.title('Evening Medication Units, (Total Evening Medication = '+str(med_totalE)+')',
          fontsize = 14)
plt.show()

fig = plt.figure(figsize=[15, 5])
(markers, stemlines, baseline) = plt.stem(n2d,y2d,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('12 Hour Periods Since First Patient Admission', fontsize = 14)
plt.ylabel('Medication Units ($y_{BID}$)', fontsize = 14)
plt.title('Medication Units Given BID (Morning and Evening), (Total Medication Given = '+str(med_total)+')',
          fontsize = 14)
plt.show()
```

##Associative Property of Convolution
The relationship between the input and output of a system with cascaded processes is independent of the order in which the processes occur.

###Consider Two Separate Processes

Cascading two systems, $h_1(n)$ and $h_2(n)$, in the first equation the input, x(n), is convolved with $h_1(n)$ followed by the convolution with $h_2(n)$.  In the second, the input, x(n), is convolved with $h_2(n)$ followed by the convolution with $h_1(n)$.  The result is independent of the order of convolution.  This is the **associative** property of convolution.
\begin{equation}
\begin{split}
 y (n) &= h_2(n) \ast (h_1(n) \ast x(n)) = h_2(n) \ast w_1(n) \\
 y (n) &= h_1(n) \ast (h_2(n) \ast x(n)) = h_1(n) \ast w_2(n) \\
 y (n) &= (h_1(n) \ast h_2(n)) \ast x(n)
\end{split}
\end{equation}
* In block diagram form:
$$x(n) \longrightarrow \boxed{\\h_1(n)} \stackrel{w_1(n)}
\longrightarrow \boxed{\\h_2(n)} \longrightarrow y(n)$$
And
$$x(n) \longrightarrow \boxed{\\h_2(n)}
\stackrel{w_2(n)} \longrightarrow \boxed{\\h_1(n)} \longrightarrow y(n)$$
Equivalent to
$$x(n) \longrightarrow \boxed{\\h_1(n) \ast h_2(n)}
\longrightarrow y(n)$$

###Example: Probability Density Function for Three Dice
The probability that the number 1 to 6 is obtained from a single *fair* die is p = 1/6. For two dice the probability of the sum of their values is the convolution of the sequences p1 = p2 = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6] where these are the probabilities that the number on the die is [1, 2, 3, 4, 5, or 6].  There is one combination for the sum = 2: [(1,1)], p=(1/6)(1/6)=1/36. There are two combinations for the sum = 3: [(1,2), (2,1)], p=(2/36), and three possibilities for sum = 4: [(1,3), (2,2), (3,1)], p=(3/36), up to six possibilities for sum = 7, p=(6/36).

If a third die is included in the roll, the sum of their values will be in the range (3 to 18).  Assume the third die in *biased* such that the probability of a one side is 1/2 the probability of the opposite side. Since opposite sides sum to seven, if a one is twice the probability of a six then the unfair die will have the probability sequence in p3 shown below.  The Probability Density Function (pdf) for the sum of the values on the three dice is
\begin{equation}
\begin{split}
 pdf (n) &= (p_1(n) \ast p_2(n)) \ast p_3(n) \\
 pdf (n) &= (p_1(n) \ast p_3(n)) \ast p_2(n)
\end{split}
\end{equation}

```python
# Fair die with equal probabilities for the face value [1, 2, 3, 4, 5, 6]
p1 = p2 = [1/6,1/6,1/6,1/6,1/6,1/6]
# Unfair die probability density function for the face value [1, 2, 3, 4, 5, 6]
# A one has three times the probability of a six.
p3 = [1/4, 1/6, 1/6, 1/6, 1/6, 1/12]
w1 = np.convolve(p1,p2)     # odf for two fair dice
y12 = np.convolve(p3,w1)    # pdf for two fair and one unfair dice
w2 = np.convolve(p1,p3)     # pdf of one fair with one unfair dice
y21 = np.convolve(p2,w2)    # pdf for two fair and one unfair dice
n1 = np.array(range(2,len(w1)+2)) # sum of the face values for two dice
n = np.array(range(3,len(y12)+3)) # sum of the face values for three dice

plt.figure(figsize=[14, 5])
plt.suptitle('Convolution Order: (Fair Die ($p_1$) --> Fair Die($p_2$)) --> UnFair Die($p_3$)',
             y = 1.05, fontsize = 18)
plt.subplot(1, 2, 1)
(markers, stemlines, baseline) = plt.stem(n1,w1,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values Two Dice',fontsize = 14)
plt.ylabel('$pdf \;(p_1*p_2 = w_1)$',fontsize = 14)
plt.title('Probability Density Function for Two Fair Dice',fontsize = 14)
plt.subplot(1, 2, 2)
(markers, stemlines, baseline) = plt.stem(n,y12,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values for Three Dice',fontsize = 14)
plt.ylabel('$pdf\;(w_1*p_3)$',fontsize = 14)
plt.title('Probability Density Function for Two Fair and One UnFair Dice',fontsize = 14)
plt.show()

plt.figure(figsize=[14, 5])
plt.suptitle('Convolution Order: (Fair Die($p_1$) --> UnFair Die($p_3$)) --> Fair Die($p_2$)',
             y = 1.05, fontsize = 18)
plt.subplot(1, 2, 1)
(markers, stemlines, baseline) = plt.stem(n1,w2,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values for Two Dice',fontsize = 14)
plt.ylabel('$pdf \;(p_1*p_3 = w_2)$',fontsize = 14)
plt.title('pdf - One Fair and One UnFair Dice',fontsize = 14)
plt.subplot(1, 2, 2)
(markers, stemlines, baseline) = plt.stem(n,y21,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values for Three Dice',fontsize = 14)
plt.ylabel('$pdf\;(w_2*p_2)$',fontsize = 14)
plt.title('pdf - Two Fair and One UnFair Dice',fontsize = 14)
plt.show()
print('\nThe final probability density functions (pdf) are independent of order of convolution.')

```

###Compare the probability density function for three fair vs two fair and one unfair dice

```python
# Fair die with equal probabilities for the face value [1, 2, 3, 4, 5, 6]
p1 = p2 = [1/6,1/6,1/6,1/6,1/6,1/6]
# Unfair die probability density function for the face value [1, 2, 3, 4, 5, 6]
p3 = [1/4,1/6,1/6,1/6,1/6,1/12]
w1 = np.convolve(p1,p2)     # odf for two fair dice
y12 = np.convolve(p3,w1)    # pdf for two fair and one unfair dice
w2 = np.convolve(p1,p3)     # pdf of one fair with one unfair dice
y21 = np.convolve(p2,w2)    # pdf for two fair and one unfair dice
y22 = np.convolve(w1, p1)   # pdf for three fair dice
n = np.array(range(3,len(y12)+3)) # sum of the face values for three dice

plt.figure(figsize=[14, 5])
plt.suptitle('Probability of Three Fair Dice vs Two Fair with One Unfair Dice',
             y = 1.05, fontsize = 18)
plt.subplot(1, 2, 1)
(markers, stemlines, baseline) = plt.stem(n,y22,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values for Three Dice',fontsize = 14)
plt.ylabel('$pdf$',fontsize = 14)
plt.title('Probability Density Function for Three Fair Dice',fontsize = 14)
plt.subplot(1, 2, 2)
(markers, stemlines, baseline) = plt.stem(n,y21,use_line_collection=True)
plt.setp(markers, marker='D', markersize=10, markeredgecolor="orange", markeredgewidth=2)
plt.grid()
plt.xlabel('Sum of the Face Values for Three Dice',fontsize = 14)
plt.ylabel('$pdf$',fontsize = 14)
plt.title('Probability Density Function for Two Fair with One Unfair Dice',fontsize = 14)
plt.show()
print('\nThe pdf of the three fair dice on the left vs pdf with two fair and one unfair dice on the right')

```

##Summary - Digital Convolution

###Mathematical Definition
Digital Convolution is a mathematical operation that maps two discrete-time sequences, x(n) and h(n), into a third discrete-time sequence, y(n).
\begin{equation}
\begin{split}
 y(n) &= h(n) \ast x(n) = x(n) \ast h(n) \;\;\;\;\;-\infty < n < \infty \\
 y(n) &= \sum_{k=-\infty}^{\infty}h(k) x(n-k) = \sum_{k=-\infty}^{\infty}x(k) h(n-k)
\end{split}
\end{equation}
Where y(n) is the resultant of the *convolutional sum* of h(n) with x(n). _[where $\ast$ represents the convolutional operator]_.

###Properties of Convolution

* Linear Operator

    * Scaling (Homogeneity) & Additivity

 $$a_1x_1(n) + a_2x_2(n) \longrightarrow \boxed{\\h(n)}
\longrightarrow a_1y_1(n)+a_2y_2(n) \\
where  \\
 x_1(n) \longrightarrow \boxed{\\h(n)}
\longrightarrow y_1(n) \;\;\;\;and \;\;\;  x_2(n) \longrightarrow \boxed{\\h(n)}
\longrightarrow y_2(n)$$

* Commutative Property

    Independent of the order of operation

\begin{equation}
\begin{split}
 y(n) &= h(n) \ast x(n) = x(n) \ast h(n)
\end{split}
\end{equation}

* Associative Property

    Independent of the sequence of operation

\begin{equation}
\begin{split}
 y(n) &= [h_1(n) \ast h_2(n)] \ast x(n) = h_1(n) \ast [h_2(n) \ast x(n)]
\end{split}
\end{equation}

* Distributive Property

    Superposition Principle

\begin{equation}
\begin{split}
 y(n) &= [h_1(n) +  h_2(n)] \ast x(n) = [h_1(n) \ast x(n)] + [h_2(n) \ast x(n)] \\
 or \;\;\;\;& \\
 y(n) &= h(n) \ast [x_1(n) +  x_2(n)]  = [h(n) \ast x_1(n)] + [h(n) \ast x_2(n)]
\end{split}
\end{equation}


# Getting started with TensorFlow (Eager Mode)

**Learning Objectives**
  - Understand difference between Tensorflow's two modes: Eager Execution and Graph Execution
  - Practice defining and performing basic operations on constant Tensors
  - Use Tensorflow's automatic differentiation capability

## Introduction

**Eager Execution**

Eager mode evaluates operations immediatley and return concrete values immediately. To enable eager mode simply place `tf.enable_eager_execution()` at the top of your code. We recommend using eager execution when prototyping as it is intuitive, easier to debug, and requires less boilerplate code.

**Graph Execution**

Graph mode is TensorFlow's default execution mode (although it will change to eager with TF 2.0). In graph mode operations only produce a symbolic graph which doesn't get executed until run within the context of a tf.Session(). This style of coding is less inutitive and has more boilerplate, however it can lead to performance optimizations and is particularly suited for distributing training across multiple devices. We recommend using delayed execution for performance sensitive production code.

```python
# Ensure that we have Tensorflow 1.13.1 installed.
!pip3 freeze | grep tensorflow==1.13.1 || pip3 install tensorflow==1.13.1
```

    /bin/sh: pip3: command not found
    /bin/sh: pip3: command not found

```python
import tensorflow as tf
print(tf.__version__)
```

    /Users/crawles/anaconda3/lib/python3.6/importlib/_bootstrap.py:219: RuntimeWarning: compiletime version 3.5 of module 'tensorflow.python.framework.fast_tensor_util' does not match runtime version 3.6
      return f(*args, **kwds)

    1.10.1

## Eager Execution

```python
tf.enable_eager_execution()
```

### Adding Two Tensors

The value of the tensor, as well as its shape and data type are printed

```python
a = tf.constant(value = [5, 3, 8], dtype = tf.int32)
b = tf.constant(value = [3, -1, 2], dtype = tf.int32)
c = tf.add(x = a, y = b)
print(c)
```

    tf.Tensor([ 8  2 10], shape=(3,), dtype=int32)

#### Overloaded Operators
We can also perform a `tf.add()` using the `+` operator. The `/,-,*` and `**` operators are similarly overloaded with the appropriate tensorflow operation.

```python
c = a + b # this is equivalent to tf.add(a,b)
print(c)
```

    tf.Tensor([ 8  2 10], shape=(3,), dtype=int32)

### NumPy Interoperability

In addition to native TF tensors, tensorflow operations can take native python types and NumPy arrays as operands.

```python
import numpy as np

a_py = [1,2] # native python list
b_py = [3,4] # native python list

a_np = np.array(object = [1,2]) # numpy array
b_np = np.array(object = [3,4]) # numpy array

a_tf = tf.constant(value = [1,2], dtype = tf.int32) # native TF tensor
b_tf = tf.constant(value = [3,4], dtype = tf.int32) # native TF tensor

for result in [tf.add(x = a_py, y = b_py), tf.add(x = a_np, y = b_np), tf.add(x = a_tf, y = b_tf)]:
    print("Type: {}, Value: {}".format(type(result), result))
```

    Type: <class 'EagerTensor'>, Value: [4 6]
    Type: <class 'EagerTensor'>, Value: [4 6]
    Type: <class 'EagerTensor'>, Value: [4 6]

You can convert a native TF tensor to a NumPy array using .numpy()

```python
a_tf.numpy()
```

    array([1, 2], dtype=int32)

### Linear Regression

Now let's use low level tensorflow operations to implement linear regression.

Later in the course you'll see abstracted ways to do this using high level TensorFlow.

#### Toy Dataset

We'll model the following function:

\begin{equation}
y= 2x + 10
\end{equation}

```python
X = tf.constant(value = [1,2,3,4,5,6,7,8,9,10], dtype = tf.float32)
Y = 2 * X + 10
print("X:{}".format(X))
print("Y:{}".format(Y))
```

    X:[ 1.  2.  3.  4.  5.  6.  7.  8.  9. 10.]
    Y:[12. 14. 16. 18. 20. 22. 24. 26. 28. 30.]

#### Loss Function

Using mean squared error, our loss function is:
\begin{equation}
MSE = \frac{1}{m}\sum_{i=1}^{m}(\hat{Y}_i-Y_i)^2
\end{equation}

$\hat{Y}$ represents the vector containing our model's predictions:
\begin{equation}
\hat{Y} = w_0X + w_1
\end{equation}

```python
def loss_mse(X, Y, w0, w1):
    Y_hat = w0 * X + w1
    return tf.reduce_mean(input_tensor = (Y_hat - Y)**2)
```

#### Gradient Function

To use gradient descent we need to take the partial derivative of the loss function with respect to each of the weights. We could manually compute the derivatives, but with Tensorflow's automatic differentiation capabilities we don't have to!

During gradient descent we think of the loss as a function of the parameters $w_0$ and $w_1$. Thus, we want to compute the partial derivative with respect to these variables. The `params=[2,3]` argument tells TensorFlow to only compute derivatives with respect to the 2nd and 3rd arguments to the loss function (counting from 0, so really the 3rd and 4th).

```python
# Counting from 0, the 2nd and 3rd parameter to the loss function are our weights
grad_f = tf.contrib.eager.gradients_function(f = loss_mse, params=[2,3])
```

#### Training Loop

Here we have a very simple training loop that converges. Note we are ignoring best practices like batching, creating a separate test set, and random weight initialization for the sake of simplicity.

```python
STEPS = 1000
LEARNING_RATE = .02

# Initialize weights
w0 = tf.constant(value = 0.0, dtype = tf.float32)
w1 = tf.constant(value = 0.0, dtype = tf.float32)

for step in range(STEPS):
    #1. Calculate gradients
    d_w0, d_w1 = grad_f(X, Y, w0, w1)

    #2. Update weights
    w0 = w0 - d_w0 * LEARNING_RATE
    w1 = w1 - d_w1 * LEARNING_RATE

    #3. Periodically print MSE
    if step % 100 == 0:
        print("STEP: {} MSE: {}".format(step, loss_mse(X, Y, w0, w1)))

# Print final MSE and weights
print("STEP: {} MSE: {}".format(STEPS,loss_mse(X, Y, w0, w1)))
print("w0:{}".format(round(float(w0), 4)))
print("w1:{}".format(round(float(w1), 4)))
```

    STEP: 0 MSE: 167.6111297607422
    STEP: 100 MSE: 3.5321757793426514
    STEP: 200 MSE: 0.6537718176841736
    STEP: 300 MSE: 0.12100745737552643
    STEP: 400 MSE: 0.022397063672542572
    STEP: 500 MSE: 0.004145540297031403
    STEP: 600 MSE: 0.0007674093940295279
    STEP: 700 MSE: 0.0001420201879227534
    STEP: 800 MSE: 2.628635775181465e-05
    STEP: 900 MSE: 4.86889211970265e-06
    STEP: 1000 MSE: 9.178326081382693e-07
    w0:2.0003
    w1:9.9979

## Bonus

Try modelling a non-linear function such as: $y=xe^{-x^2}$

```python
X = tf.constant(value = np.linspace(0,2,1000), dtype = tf.float32)
Y = X*np.exp(-X**2) * X

from matplotlib import pyplot as plt
%matplotlib inline
plt.plot(X, Y)
```

```python
def make_features(X):
    features = [X]
    features.append(tf.ones_like(X))  # Bias.
    features.append(tf.square(X))
    features.append(tf.sqrt(X))
    features.append(tf.exp(X))
    return tf.stack(features, axis=1)

def make_weights(n_weights):
    W = [tf.constant(value = 0.0, dtype = tf.float32) for _ in range(n_weights)]
    return tf.expand_dims(tf.stack(W),-1)

def predict(X, W):
    Y_hat = tf.matmul(X, W)
    return tf.squeeze(Y_hat, axis=-1)

def loss_mse(X, Y, W):
    Y_hat = predict(X, W)
    return tf.reduce_mean(input_tensor = (Y_hat - Y)**2)

X = tf.constant(value = np.linspace(0,2,1000), dtype = tf.float32)
Y = np.exp(-X**2) * X

grad_f = tf.contrib.eager.gradients_function(f = loss_mse, params=[2])
```

```python
STEPS = 2000
LEARNING_RATE = .02

# Weights/features.
Xf = make_features(X)
# Xf = Xf[:,0:2]  # Linear features only.
W = make_weights(Xf.get_shape()[1].value)

# For plotting
steps = []
losses = []

plt.figure()
for step in range(STEPS):
    #1. Calculate gradients
    dW = grad_f(Xf, Y, W)[0]
    #2. Update weights
    W -= dW * LEARNING_RATE
    #3. Periodically print MSE
    if step % 100 == 0:
        loss = loss_mse(Xf, Y, W)
        steps.append(step)
        losses.append(loss)
        plt.clf()
        plt.plot(steps, losses)
# Print final MSE and weights
print("STEP: {} MSE: {}".format(STEPS,loss_mse(Xf, Y, W)))

# Plot results
plt.figure()
plt.plot(X, Y, label='actual')
plt.plot(X, predict(Xf, W), label='predicted')
plt.legend()
```

Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License


# Generalisation and overfitting

In this notebook we will explore the issue of overfitting and how we can measure how well the models we train generalise their predictions to unseen data. This will build upon the introduction to generalisation given in the [fourth lecture](http://www.inf.ed.ac.uk/teaching/courses/mlp/2016/mlp04-learn.pdf).

## Exercise: overfitting and model complexity in a 1D regression problem

As an exercise we will consider a regression problem. In particular we will attempt to use a multiple layer network model to learn to predict output values from inputs, given a fixed set of (noisy) observations of the underlying functional relationship between inputs and outputs. The aim of the exercise will be to visualise how increasing the complexity of the model we fit to the training data effects the ability of the model to make predictions across the input space.

### Function

To keep things simple we will consider a single input-output function defined by a fourth degree polynomial (quartic)

$$ f(x) = 10 x^4 - 17 x^3 + 8 x^2 - x $$

with the observed values being the function values plus zero-mean Gaussian noise

$$ y = f(x) + 0.01 \epsilon \qquad \epsilon \sim \mathcal{N}\left(\cdot;\,0,\,1\right) $$

The inputs will be drawn from the uniform distribution on $[0, 1]$.

First import the necessary modules and seed the random number generator by running the cell below.

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')
seed = 17102016
rng = np.random.RandomState(seed)
```

Write code in the cell below to calculate a polynomial function of one dimensional inputs.

If $\boldsymbol{c}$ is a length $P$ vector of coefficients corresponding to increasing powers in the polynomial (starting from the constant zero power term up to the $P-1^{\textrm{th}}$ power) the function should correspond to the following

\begin{equation}
  f_{\textrm{polynomial}}(x,\ \boldsymbol{c}) = \sum_{p=0}^{P-1} \left( c_p x^p \right)
\end{equation}

```python
def polynomial_function(inputs, coefficients):
    """Calculates polynomial with given coefficients of an array of inputs.

    Args:
        inputs: One-dimensional array of input values of shape (num_inputs,)
        coefficients: One-dimensional array of polynomial coefficient terms
           with `coefficients[0]` corresponding to the coefficient for the
           zero order term in the polynomial (constant) and `coefficients[-1]`
           corresponding to the highest order term.

    Returns:
        One dimensional array of output values of shape (num_inputs,)

    """
    return (inputs[:, None]**np.arange(coefficients.shape[0])).dot(coefficients)
```

Run the cell below to test your implementation.

```python
test_coefficients = np.array([-1., 3., 4.])
test_inputs = np.array([0., 0.5, 1., 2.])
test_outputs = np.array([-1., 1.5, 6., 21.])
assert polynomial_function(test_inputs, test_coefficients).shape == (4,), (
    'Function gives wrong shape output.'
)
assert np.allclose(polynomial_function(test_inputs, test_coefficients), test_outputs), (
    'Function gives incorrect output values.'
)
```

We now need to use the random number generator to sample input values and calculate the corresponding target outputs using your polynomial implementation with the relevant coefficients for our function. Do this by running the cell below.

```python
coefficients = np.array([0, -1., 8., -17., 10.])
input_dim, output_dim = 1, 1
noise_std = 0.01
num_data = 80
inputs = rng.uniform(size=(num_data, input_dim))
epsilons = rng.normal(size=num_data)
targets = (polynomial_function(inputs[:, 0], coefficients) +
           epsilons * noise_std)[:, None]
```

We will split the generated data points in to equal sized training and validation data sets and use these to create data provider objects which we can use to train models in our framework. As the dataset is small here we will use a batch size equal to the size of the data set. Run the cell below to split the data and set up the data provider objects.

```python
from mlp.data_providers import DataProvider
num_train = num_data // 2
batch_size = num_train
inputs_train, targets_train = inputs[:num_train], targets[:num_train]
inputs_valid, targets_valid = inputs[num_train:], targets[num_train:]
train_data = DataProvider(inputs_train, targets_train, batch_size=batch_size, rng=rng)
valid_data = DataProvider(inputs_valid, targets_valid, batch_size=batch_size, rng=rng)
```

We can now visualise the data we will be modelling. Run the cell below to plot the target outputs against inputs for both the training and validation sets. Note the clear underlying smooth functional relationship evident in the noisy data.

```python
fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(111)
ax.plot(inputs_train[:, 0], targets_train[:, 0], '.', label='training data')
ax.plot(inputs_valid[:, 0], targets_valid[:, 0], '.', label='validation data')
ax.set_xlabel('Inputs $x$', fontsize=14)
ax.set_ylabel('Ouputs $y$', fontsize=14)
ax.legend(loc='best')
fig.tight_layout()
```

### Model

We will fit models with a varying number of parameters to the training data. As multi-layer logistic sigmoid models do not tend to perform well in regressions tasks like this we will instead use a [radial basis function (RBF) network](https://en.wikipedia.org/wiki/Radial_basis_function_network).

This model predicts the output as the weighted sum of basis functions (here Gaussian like bumps) tiled across the input space. The cell below generates a random set of weights and bias for a RBF network and plots the modelled input-output function across inputs $[0, 1]$. Run the cell below for several different number of weight parameters (specified with `num_weights` variable) to get a feel for the sort of predictions the RBF network models produce.

```python
num_weights = 15
weights_scale = 1.
bias_scale = 1.

def basis_function(x, centre, scale):
    return np.exp(-(x - centre)**2 / scale**2)

weights = rng.normal(size=num_weights) * weights_scale
bias = rng.normal() * bias_scale

centres = np.linspace(0, 1, weights.shape[0])
scale = 1. / weights.shape[0]

xs = np.linspace(0, 1, 200)
ys = np.zeros(xs.shape[0])

fig = plt.figure(figsize=(12, 4))
ax = fig.add_subplot(1, 1, 1)
for weight, centre in zip(weights, centres):
    ys += weight * basis_function(xs, centre, scale)
ax.plot(xs, ys)
ax.set_xlabel('Input', fontsize=14)
ax.set_ylabel('Output', fontsize=14)
```

You do not need to read in to the details of how to implement this model. All of the additional code you need to fit RBF networks is provided in the `RadialBasisFunctionLayer` in the `mlp.layers` module. The `RadialBasisFunctionLayer` class has the same interface as the layer classes we encountered in the previous lab, defining both `fprop` and `bprop` methods, and we can therefore include it as a layer in a `MultipleLayerModel` as with any other layer.

Here we will use the `RadialBasisFunctionLayer` as the first layer in a two layer model. This first layer calculates the basis function terms which are then be weighted and summed together in an `AffineLayer`, the second and final layer. This illustrates the advantage of using a modular modelling framework - we can reuse the code we previously implemented to train a quite different model architecture just by defining a new layer class.

Run the cell below to run some necessary setup code.

```python
from mlp.models import MultipleLayerModel
from mlp.layers import AffineLayer, RadialBasisFunctionLayer
from mlp.errors import SumOfSquaredDiffsError
from mlp.initialisers import ConstantInit, UniformInit
from mlp.learning_rules import GradientDescentLearningRule
from mlp.optimisers import Optimiser

# Regression problem therefore use sum of squared differences error
error = SumOfSquaredDiffsError()
# Use basic gradient descent learning rule with fixed learning rate
learning_rule = GradientDescentLearningRule(0.1)
# Initialise weights from uniform distribution and zero bias
weights_init = UniformInit(-0.1, 0.1)
biases_init = ConstantInit(0.)
# Train all models for 2000 epochs
num_epoch = 2000
```

The cell below defines RBF network models with varying number of weight parameters (equal to the number of basis functions) and fits each to the training set, recording the final training and validation set errors for the fitted models. Run it now to fit the models and calculate the error values.

```python
num_weight_list = [2, 5, 10, 25, 50, 100]
models = []
train_errors = []
valid_errors = []
for num_weight in num_weight_list:
    model = MultipleLayerModel([
        RadialBasisFunctionLayer(num_weight),
        AffineLayer(input_dim * num_weight, output_dim,
                    weights_init, biases_init)
    ])
    optimiser = Optimiser(model, error, learning_rule,
                          train_data, valid_data)
    print('-' * 80)
    print('Training model with {0} weights'.format(num_weight))
    print('-' * 80)
    _ = optimiser.train(num_epoch, -1)
    outputs_train = model.fprop(inputs_train)[-1]
    outputs_valid = model.fprop(inputs_valid)[-1]
    models.append(model)
    train_errors.append(error(outputs_train, targets_train))
    valid_errors.append(error(outputs_valid, targets_valid))
    print('  Final training set error: {0:.1e}'.format(train_errors[-1]))
    print('  Final validation set error: {0:.1e}'.format(valid_errors[-1]))
```

    --------------------------------------------------------------------------------
    Training model with 2 weights
    --------------------------------------------------------------------------------
      Final training set error: 2.0e-03
      Final validation set error: 1.1e-03
    --------------------------------------------------------------------------------
    Training model with 5 weights
    --------------------------------------------------------------------------------
      Final training set error: 4.5e-04
      Final validation set error: 3.0e-04
    --------------------------------------------------------------------------------
    Training model with 10 weights
    --------------------------------------------------------------------------------
      Final training set error: 5.1e-05
      Final validation set error: 8.3e-05
    --------------------------------------------------------------------------------
    Training model with 25 weights
    --------------------------------------------------------------------------------
      Final training set error: 3.9e-05
      Final validation set error: 9.5e-05
    --------------------------------------------------------------------------------
    Training model with 50 weights
    --------------------------------------------------------------------------------
      Final training set error: 1.5e-05
      Final validation set error: 1.6e-03
    --------------------------------------------------------------------------------
    Training model with 100 weights
    --------------------------------------------------------------------------------
      Final training set error: 1.0e-05
      Final validation set error: 4.2e-03

In the cell below write code to [plot bar charts](http://matplotlib.org/examples/api/barchart_demo.html) of the training and validation set errors for the different fitted models.

Some questions to think about from the plots:

  * Do the models with more free parameters fit the training data better or worse?
  * What does the validation set error value tell us about the models?
  * Of the models fitted here which would you say seems like it is most likely to generalise well to unseen data?
  * Do any of the models seem to be overfitting?

```python
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(1, 2, 1)
ax1.bar(0.1 + np.arange(len(num_weight_list)), train_errors)
ax1.set_xticks(0.5 + np.arange(len(num_weight_list)))
ax1.set_xticklabels(num_weight_list)
ax1.set_xlabel('Number of weight parameters')
ax1.set_ylabel('Error')
ax1.set_title('Training set')
ax2 = fig.add_subplot(1, 2, 2)
ax2.bar(0.1 + np.arange(len(num_weight_list)), valid_errors)
ax2.set_xticks(0.5 + np.arange(len(num_weight_list)))
ax2.set_xticklabels(num_weight_list)
ax2.set_xlabel('Number of weight parameters')
ax2.set_ylabel('Error')
ax2.set_title('Validation set')
fig.tight_layout()
```

Now let's visualise what the fitted model's predictions look like across the whole input space compared to the 'true' function we were trying to fit.

In the cell below, for each of the fitted models stored in the `models` list above:
  * Compute output predictions for the model across a linearly spaced series of 500 input points between 0 and 1 in the input space.
  * Plot the computed predicted outputs and true function values at the corresponding inputs as line plots on the same axis (use a new axis for each model).
  * On the same axis plot the training data sets input-target pairs as points.

```python
inputs_grid = np.linspace(0., 1., 500)
true_func_grid = polynomial_function(inputs_grid, coefficients)
for num_weight, model in zip(num_weight_list, models):
    fig = plt.figure(figsize=(8, 4))
    ax = fig.add_subplot(111)
    outputs_grid = model.fprop(inputs_grid)[-1]
    ax.plot(inputs_train, targets_train, '.', label='training data')
    ax.plot(inputs_grid, true_func_grid, label='true function')
    ax.plot(inputs_grid, outputs_grid, label='fitted function')
    ax.set_xlabel('Inputs $x$', fontsize=14)
    ax.set_ylabel('Ouputs $y$', fontsize=14)
    ax.set_title('{0} weight parameters'.format(num_weight))
    ax.legend()
```

You should be able to relate your answers to the questions above to what you see in these plots - ask a demonstrator if you are unsure what is going on. In particular for the models which appeared to be overfitting and generalising poorly you should now have an idea how this looks in terms of the model's predictions and how these relate to the training data points and true function values.

```python

```