# Goal
- implement the gradient-based learning using sampling
- implement the EM method
- compare the two using synthetic data

# Introduction
Our goal in this exercise is to learn the parameters of a model from incomplete data. The model is a mixture of Guassians. According to this model, each data point is sampled from one of $C$ Gaussian distributions, according to the class of that data point. The prior probability for class label follows a multinomial distribution:

\begin{equation}
P(Z_i = j) = p_j
\end{equation}

Given the class of a data point, its value follows a normal distribution with parameters that are specific to that class:

\begin{equation}
P(Y_i = y | Z_i = j) = N(y; \mu_j, \sigma_j)
\end{equation}

## Sampling the latent variables
For each data point $Y_i$ we have:

\begin{align*}
P(Z_i=j|Y_i=y_i;\Theta) & \propto P(Y_i=y_i|Z_i=j;\Theta)P(Z_i=j;\Theta)   \\
                  & = N(y_i;\mu_j,\sigma_j) \cdot p_j
\end{align*}

Having the posterior of class label, we can sample the label from this posterior:

```python
%matplotlib inline
from scipy.stats import norm
from numpy.random import multinomial
from scipy.optimize import check_grad
from numpy.random import random_sample, normal
import numpy as np
```

```python

# returns a vector with counts of each class
def sample_label(parameters, y, num_samples=1):
    multinomial_parameters, normal_parameters = parameters
    assert len(multinomial_parameters) == len(normal_parameters)
    num_classes = len(multinomial_parameters)

    posterior = [0.0] * num_classes
    for i in range(num_classes):
        mu, sigma = normal_parameters[i]
        p = multinomial_parameters[i]
        posterior[i] = norm.pdf(y, loc=mu, scale=sigma) * p
    s = sum(posterior)
    posterior = [ps/s for ps in posterior]
    return multinomial(num_samples, posterior)

```

Let us test the correctness of gradients of normal distribution. Given $f(x, \mu, \sigma) = \frac{1}{\sigma\sqrt{2\pi}} e^{\frac{(x-\mu)^2}{2\sigma^2}}$, I believe that:
\begin{align}
\frac{\partial f}{\partial \mu} &= f . \frac{1}{\sigma} . \big(\frac{x-\mu}{\sigma}\big) \\
\frac{\partial f}{\partial \sigma} &= f . \frac{1}{\sigma} . \big( (\frac{x-\mu}{\sigma})^2 -1 \big)
\end{align}

```python

def f_normal(x):
    def _f(inp):
        mu, sigma = inp
        return norm.pdf(x, loc=mu, scale=sigma)
    return _f

def fprime_normal(x):
    def _fprime(inp):
        f_val = f(x)(inp)
        mu, sigma = inp
        xms = (x-mu)/sigma

        grad=[0.0]*2
        grad[0] = f_val/sigma * xms
        grad[1] = f_val/sigma * (xms**2 - 1)
        return grad
    return _fprime

# for i in range(10000):
#     mu = 30 * random_sample()
#     sigma = 10 * random_sample()
#     val = normal(mu, sigma)
#     diff =  check_grad(f(val), fprime(val), [mu, sigma])
#     if diff > 1e-4:
#         print mu, sigma, val, diff
```

```python

def f_normal_log(x):
    return lambda y : norm.logpdf(x, loc=y[0], scale=y[1])

def fprime_normal_log(x):
    def fprime_log_(y):
        mu, sigma = y
        xms = (x-mu)/sigma
        fprime_mu = xms / sigma
        fprime_sigma = (xms**2 - 1) / sigma
        return (fprime_mu, fprime_sigma)
    return fprime_log_

for i in range(5000):
    mu = 30 * random_sample()
    sigma = 10 * random_sample()
    val = normal(mu, sigma)
    diff =  check_grad(f_normal_log(val), fprime_normal_log(val), [mu, sigma])
    if diff > 1e-4:
        print mu, sigma, val, diff
```

    17.777933425 0.00519144999703 17.7702702985 0.00155532435165
    17.722124324 0.00532207852345 17.7172243 0.000483683138791
    29.7969329959 0.00667637649714 29.8008845277 0.000167342991154
    15.0489365255 1.41541760423e-06 15.0489407036 92251.9554
    18.5032739357 0.0225645777213 18.5432384119 0.000123929740921
    3.51859455211 0.00482887323073 3.52117467029 0.000322790197203
    4.16938370126 0.0113444023514 4.15434776841 0.000253916766627

```python
def test_multinomial(f_multinomial_pos, fprime_multinomial_pos,
                    num_tests = 1000):
    num_classes = np.random.randint(40)
    for i in range(num_tests):
        params = [normal(loc=0, scale=1) for i in range(num_classes)]
        p2 = np.exp(params)
        p2 = [i/sum(p2) for i in p2]
        val = np.random.multinomial(1, p2)
        val = np.where(val==1)[0][0]
        diff =  check_grad(f_multinomial_pos(val), fprime_multinomial_pos(val), params)
        if diff > 1e-4:
            print params, val, diff
            print f_multinomial_pos(val)(params)
            print fprime_multinomial_pos(val)(params)
            print approx_fprime(params, f_multinomial_pos(val), 1.49e-08)
            print
    print 'test finished'
```

```python
def f_multinomial_pos(label):
    return (lambda parameters : np.exp(parameters[label]) / sum(np.exp(parameters)))

def f_multinomial_log(label):
    return (lambda parameters : parameters[label] - np.log(sum(np.exp(parameters))))

def fprime_multinomial_log(label):
    def _fprime_multinomial_log(parameters):
        fprime = [-f_multinomial_pos(i)(parameters) for i in range(len(parameters))]
        fprime[label] += 1
        return fprime
    return _fprime_multinomial_log

test_multinomial(f_multinomial_log, fprime_multinomial_log)
```

    test finished

## Approximating the gradient
The log likelihood of data point $i$ can be written as $L(\Theta) = \log P(Y_i, Z_i;\Theta)$.
So the gradient of log-likelihood function can be written as:

\begin{equation}
\partial_{\Theta} L(\Theta) =
\end{equation}

## A very important observation
The differences in the values of `multinomial_parameters` leads to exponential differences, and does not let samples from other classes to be generated.

```python

def generate_data():
    num_classes = 2
    normal_parameters = [
        [10, 5],
        [25, 3]
    ]
    multinomial_parameters = [15.5, 16]
    p2 = np.exp(multinomial_parameters)
    p2 = [i/sum(p2) for i in p2]

    num_data = 1000
    values = []
    labels = []
    for i in range(num_data):
        label = np.random.multinomial(1, p2)
        label = np.where(label==1)[0][0]
        mu, sigma = normal_parameters[label]
        value = np.random.normal(mu, sigma)
        values.append(value)
        labels.append(label)
    return labels, values

labels, values = generate_data()
data = zip(labels, values)
```

```python
import pickle
pickle.dump(data, open('./data.dat', 'wb'))
```

```python
import matplotlib.pyplot as plt
from scipy.stats import norm
plt.hist(values,100)
x = np.linspace(-20, 50, num=100)
y1 = norm.pdf(x, loc=10, scale=5) * 200
y3 = norm.pdf(x, loc=25, scale=3) * 300
plt.plot(x, y1)
plt.plot(x, y3)
```

Assuming that the class labels are hidden, we only have access to `values`. In each iteration, we complete the data using current parameters, compute the likelihood and gradient of completed data, and update the parameters:

```python
def ll(parameters, data):
    """Compute the likelihood and its gradients.

    arguments:
    parameters -- current value of parameters
    data     -- values for all random variables
    """
    p_n, p_m = parameters
    num_classes = len(p_m)
    likelihood = 0
    grad_n = [[0.0] * len(p_n[0])] * num_classes
    grad_m = [0.0] * num_classes
    for instance in data:
        label, value = instance
        likelihood += f_normal_log(value)(p_n[label])
        fprime_n = fprime_normal_log(value)(p_n[label])
        grad_n[label] = [grad_n[label][i] + fprime_n[i] for i in range(num_classes)]

        likelihood += f_multinomial_log(label)(p_m)
        fprime_m = fprime_multinomial_log(label)(p_m)
        grad_m = [grad_m[i]+fprime_m[i] for i in range(num_classes)]

    gradients = grad_n, grad_m
    return likelihood, gradients

def get_samples(value, parameters, num_samples):
    p_n, p_m = parameters
    num_classes = len(p_m)
    prior = np.exp(p_m)
    prior = [p/sum(prior) for p in prior]

    posterior = [0.0] * num_classes
    for i in range(num_classes):
        mu, sigma = p_n[i]
        posterior[i] = norm.pdf(value, loc=mu, scale=sigma) * prior[i]
    posterior = [p/sum(posterior) for p in posterior]

    samples = np.random.multinomial(num_samples, posterior)
    completed = []
    for i in range(num_classes):
        for j in range(samples[i]):
            completed.append((i, value))
    return completed

# unnecessary complication
def complete_data(values, parameters, num_samples=2):
    completed_data = []
    for value in values:
        completed_data += get_samples(value, parameters, num_samples)
    return completed_data

```

```python
num_classes = 2
normal_parameters = [
    [15.5, 5],
    [15, 5]
]
multinomial_parameters = [15.7, 15.8]
parameters = [normal_parameters, multinomial_parameters]
completed_data = complete_data(values, parameters)
```

Before starting to learn the weights, let us compute the log-likelihood of complete data:

```python
data = zip(values, labels)
normal_parameters = [
    [10, 5],
    [25, 3]
]
multinomial_parameters = [15.5, 16]
p = np.exp(multinomial_parameters)
p = [i/sum(p) for i in p]

log_likelihood = 0
for value, label in data:
    log_likelihood += np.log(p[label])
    mu, sigma = normal_parameters[label]
    log_likelihood += norm.logpdf(value, loc=mu, scale=sigma)

print log_likelihood
```

    -3389.62720501

```python
current_params = parameters
current_data = complete_data(values, current_params, num_samples=10)
ll_series = []
for iterations in range(200):
    likelihood, gradients = ll(current_params, completed_data)
    ll_series.append(likelihood)
    g_n, g_m = gradients
    nu = 0.1 / len(completed_data)
    p_n, p_m = parameters
    for i in range(2):
        for j in range(2):
            p_n[i][j] += nu * g_n[i][j]
    for i in range(2):
        p_m[i] += nu * g_m[i]
    current_params = (p_n, p_m)
    current_data = complete_data(values, current_params, num_samples=10)

plt.plot(ll_series)
```

```python
print current_params
```

    ([[16.55104836045259, 6.829974864553629], [15.709475390466789, 6.905680062219306]], [15.798033405566205, 15.701966594433795])


# Spectral Estimation of Random Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## The Periodogram

The [periodogram](https://en.wikipedia.org/wiki/Spectral_density_estimation#Periodogram) is an estimator for the power spectral density (PSD) $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of a random signal $x[k]$. For the following it is assumed that $x[k]$ is drawn from a wide-sense ergodic real-valued random process.

### Definition

The PSD is defined as the [discrete-time Fourier transformation (DTFT) of the auto-correlation function (ACF)](../random_signals/power_spectral_densities.ipynb#Definition)

\begin{equation}
\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \mathcal{F}_* \{ \varphi_{xx}[\kappa] \}
\end{equation}

Hence, the PSD can be computed from an estimate of the ACF. Let's assume that we want to estimate the PSD from $N$ samples of the random signal $x[k]$ by way of the ACF. The signal $x[k]$ is truncated to $N$ samples by multiplication (windowing) with the rectangular signal $\text{rect}_N[k]$ of length $N$

\begin{equation}
x_N[k] = x[k] \cdot \text{rect}_N[k]
\end{equation}

where $x_N[k]$ denotes the truncated signal.
The ACF is estimated by applying its definition in a straightforward manner. For a random signal $x_N[k]$ of finite length, the estimated ACF $\hat{\varphi}_{xx}[\kappa]$ can be expressed [in terms of a convolution](../random_signals/correlation_functions.ipynb#Definition)

\begin{equation}
\hat{\varphi}_{xx}[\kappa] = \frac{1}{N} \cdot x_N[k] * x_N[-k]
\end{equation}

Applying the DTFT to both sides and rearranging the terms yields

\begin{equation}
\hat{\Phi}_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{1}{N} \, X_N(\mathrm{e}^{\,\mathrm{j}\,\Omega})\, X_N(\mathrm{e}^{\,-\mathrm{j}\,\Omega}) =
\frac{1}{N} \, | X_N(\mathrm{e}^{\,\mathrm{j}\,\Omega}) |^2
\end{equation}

where the intermediate and last equalities result from the symmetry relations of the DTFT. This estimate of the PSD is known as the periodogram. It can be computed directly from the DTFT

\begin{equation}
X_N(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \sum_{k=0}^{N-1} x_N[k] \, \mathrm{e}^{\,-\mathrm{j}\,\Omega\,k}
\end{equation}

of the truncated random signal.

### Example - Periodogram

The following example estimates the PSD of a random process which draws samples from normal distributed white noise with zero mean and unit variance. The true PSD is hence given as $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = 1$. In order to compute the periodogram by the discrete Fourier transformation (DFT), the signal $x[k]$ has to be zero-padded to ensure that squaring (multiplying) the spectra does not result in a circular convolution.

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

N = 128  # number of samples

# generate random signal
np.random.seed(5)
x = np.random.normal(size=N)

# compute magnitude of the periodogram
x = np.concatenate((x, np.zeros_like(x)))
X = np.fft.rfft(x)
Om = np.linspace(0, np.pi, len(X))
Sxx = 1/N * abs(X)**2

# plot results
plt.figure(figsize=(10,4))
plt.stem(Om, Sxx, 'b', label=r'$|\hat{\Phi}_{xx}(e^{j \Omega})|$')
plt.plot(Om, np.ones_like(Sxx), 'r', label=r'$\Phi_{xx}(e^{j \Omega})$')
plt.title('Estimated and true PSD')
plt.xlabel(r'$\Omega$')
plt.axis([0, np.pi, 0, 5])
plt.legend()

# compute mean value of the periodogram
print('Mean value of the periodogram: %f' %np.mean(np.abs(Sxx)))
```

**Exercise**

* What do you have to change to evaluate experimentally if the periodogram is a consistent estimator?
* Based on the results, is the periodogram a consistent estimator?

The conditions for consistency have to be checked for the limiting case of a infinitely long signal ($N \to \infty$). Increasing the length `N` of the random signal in above example reveals that the periodogram can be assumed to be bias free, $b_{\hat{\Phi}_{xx}} = 0$. However, its variance $\sigma^2_{\hat{\Phi}_{xx}}$ does not tend to zero. The reason for this is that by increasing the length $N$ of the random signal also the number of spectral coefficients is increased by the same amount.

### Evaluation

From above numerical example it should have become clear that the periodogram is no consistent estimator for the PSD $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$. It can be shown that the estimator is asymptotically bias free for $N \to \infty$, hence

\begin{equation}
\lim_{N \to \infty} E\{ \hat{\Phi}_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})  \} = \Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})
\end{equation}

This is due to the [leakage effect](../spectral_analysis_deterministic_signals/leakage_effect.ipynb) which limits the spectral resolution for signals of finite length.

The variance of the estimator does not converge towards zero

\begin{equation}
\lim_{N \to \infty} \sigma^2_{\hat{\Phi}_{xx}} \neq 0
\end{equation}

This is due to the fact that by increasing $N$ also the number of independent frequencies $\Omega = \frac{2 \pi}{N} \mu$ for $\mu = 0,1,\dots,N-1$ increases.

The periodogram is the basis for a variety of advanced estimation techniques for the PSD. These techniques rely on averaging or smoothing of (overlapping) periodograms.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2017*.


Teoria Regulacji, Wtorek 17:05-18:45

Jan Bronicki 249011

### Zadanie 2 z Listy 2 ("mini-projekt")

Dla systemu o następującej transmitancji:
$$ K(s) = \frac{1}{(s+1)(s+2)} $$
Należy  wyznaczyć pobudzienie na wejście:

$u(t) = 1(t)$

Z następującymi warunkami początkowymi:

$y(0)=1$
$y^{\prime}(0)=2$

System dodatkowo można przedstawić graficznie za pomocą schematu blokowego jako:

Na początku wiedząc, że $Y(s) = K(s) \cdot U(s)$ obliczamy $y_{1}(t)$:
Gdzie za $U(s)$ podstawiamy to, czemu równałoby się $u(t)$ po transformacie Laplace'a:

$$ Y(s) = \frac{1}{(s+1)(s+2)} \cdot \frac{1}{s}  $$

$$ Y(s) = \frac{A}{s} + \frac{B}{s+1} + \frac{C}{s+2}  $$

\begin{cases}
            A=\frac{1}{2}
            \\
            B=-1
            \\
            C=\frac{1}{2}
\end{cases}

$$ Y(s) = \frac{\frac{1}{2}}{s} + \frac{-1}{s+1} + \frac{\frac{1}{2}}{s+2}  $$

Następnie stosująć wzory na odwrotną transformatę Laplace'a, uzyskujemy wynik w dziedzinie czasu:

$$ \mathcal{L}^{-1}\left\{Y(s)\right\} = \mathcal{L}^{-1}\left\{\frac{\frac{1}{2}}{s} + \frac{-1}{s+1} + \frac{\frac{1}{2}}{s+2}\right\}  $$

$$ y_{1}(t) = \frac{1}{2}e^{-2t} - e^{-t} + \frac{1}{2} $$

#### Możemy to osiągnąć również stosując bibliotekę taką jak SymPy umożliwiającą, nam używanie zapisów symbolicznych w Python'ie

```python
# Biblioteka SymPy
import sympy as sp
# NumPy uzywanany do numerycznych operacji matematycznych
import numpy as np
# Matplotlib sluzacy do wizualizacji
import matplotlib.pyplot as plt
```

```python
# Definiujemy obiekty biblioteki SymPy
t, y, s = sp.symbols('t y s')
# Tworzymy rownanie
Ys = 1/(s*(s+1)*(s+2))
Ys
```

$\displaystyle \frac{1}{s \left(s + 1\right) \left(s + 2\right)}$

Następnie dokonujemy rozbicia:

```python
Ys = Ys.apart()
Ys
```

$\displaystyle \frac{1}{2 \left(s + 2\right)} - \frac{1}{s + 1} + \frac{1}{2 s}$

```python
y1 = sp.expand(sp.inverse_laplace_transform(Ys.apart(), s, t))
y1
# theta(t) to 1(t) w bibliotece SymPy
```

$\displaystyle \frac{\theta\left(t\right)}{2} - e^{- t} \theta\left(t\right) + \frac{e^{- 2 t} \theta\left(t\right)}{2}$

Teraz możemy narysować rozwiązanie $y_{1}(t)$:

```python
'''
"time" bedzie nasza osia czasu,
a y1_time odpowiedza jaka dostaniemy w konkretnym punkcie czasu
'''
time = np.arange(0, 10, 0.01)
y1_time = np.arange(0, 10, 0.01)

y1_lambda = sp.lambdify(t, y1, modules=['numpy', 'sympy'])

for each in range(0, len(time)):
    y1_time[each] = y1_lambda(time[each])
```

Teraz możemy narysować wynik rozwiązania numerycznego poprzez, zamienienie równania symbolicznego biblioteki SymPy na funkcję (lambdę) w Pythonie z implementacją w NumPy'u dzięki funkcji:
```python
sympy.utilities.lambdify(symfunc, implementation)
```

```python
plt.plot(time, y1_time, label=("$y_{1}(t)$"))
plt.grid(True)
plt.title("Odpowiedz "+"$"+"y_{1}(t) = "+sp.latex(y1)+"$")
plt.xlabel("Czas[s]")
plt.ylabel("Amplituda")
plt.legend()
plt.show()
```

#### Następnie na podstawie równania charakterystycznego $(s+1)(s+2)$ możemy dojść do oryginalnego równania różniczkowego:
$$Y(s) = \frac{1}{(s+1)(s+2)} \cdot \frac{1}{s}$$

$$ Y(s)\left[s^{2}+3s+2\right] = \frac{1}{s}$$

$$ y^{\prime\prime}+3y^{\prime}+2y=u(t) $$

$$ s^{2}Y(s)-sy(0)-y^{\prime}+3sY(s)-3y(0)+2Y(s) = 0 $$

gdzie $y(0)=1$ oraz $y^{\prime}(0)=2 $

$$ Y(s) = \frac{s+5}{s^{2}+3s+2} $$

$$ Y(s) = \frac{s+5}{(s+2)(s+1)} $$

$$ Y(s) = \frac{A}{s+1}+\frac{B}{s+2} $$

\begin{cases}
            A=4
            \\
            B=-3
\end{cases}

$$ Y(s) = \frac{4}{s+1}+\frac{-3}{s+2} $$

Następnie stosujemy odwrotną transformatę Laplace'a:

$$ \mathcal{L}^{-1}\left\{Y(s)\right\} = \mathcal{L}^{-1}\left\{\frac{4}{s+1}+\frac{-3}{s+2}\right\} $$

$$ y_{2}(t) = 4e^{-t}-3e^{-2t} $$

#### Następnie tak samo możemy postąpić w tym przypadku, używając możliwości symbolicznych obliczeń jakie daje nam Python

```python
Ys = (s+5)/((s**2)+3*s+2)
Ys
```

$\displaystyle \frac{s + 5}{s^{2} + 3 s + 2}$

```python
Ys = Ys.factor()
Ys
```

$\displaystyle \frac{s + 5}{\left(s + 1\right) \left(s + 2\right)}$

```python
Ys = Ys.apart()
Ys
```

$\displaystyle - \frac{3}{s + 2} + \frac{4}{s + 1}$

```python
y2 = sp.expand(sp.inverse_laplace_transform(Ys.apart(), s, t))
y2
```

$\displaystyle 4 e^{- t} \theta\left(t\right) - 3 e^{- 2 t} \theta\left(t\right)$

Teraz zamieniamy postać symboliczną na funkcję z, której uzyskamy wartości numeryczne:

```python
'''
"time" bedzie nasza osia czasu,
a y2_time odpowiedza jaka dostaniemy w konkretnym punkcie czasu
'''
time = np.arange(0, 10, 0.01)
y2_time = np.arange(0, 10, 0.01)

y2_lambda = sp.lambdify(t, y2, modules=['numpy', 'sympy'])

'''
Z powodu implementacji Heaviside w SymPy'u obecna funkcja nie bedzie mogla zostac wyliczona,
dla 0, dlatego wpisujemy jej wartosc, dla 0 reczne
'''
y2_time[0] = 1.0
for each in range(1, len(time)):
    y2_time[each] = y2_lambda(time[each])
```

```python
plt.plot(time, y2_time, label=("$y_{2}(t)$"))
plt.grid(True)
plt.title("Odpowiedz "+"$"+"y_{2}(t) = "+sp.latex(y2)+"$")
plt.xlabel("Czas[s]")
plt.ylabel("Amplituda")
plt.legend()
plt.show()
```

#### Teraz możemy końcowo dodać nasze dwie otrzymane funkcje $y_{1}(t)$ oraz $y_{2}(t)$:

$$ y(t) = y_{1}(t) + y_{2}(t) = 3e^{-t} - \frac{5}{2}e^{-2t} + \frac{1}{2} $$

Następnie rysujemy otrzymane $y(t)$ wraz z $y_{1}(t) + y_{2}(t)$, dla porównania:

```python
y = y1+y2
y
```

$\displaystyle \frac{\theta\left(t\right)}{2} + 3 e^{- t} \theta\left(t\right) - \frac{5 e^{- 2 t} \theta\left(t\right)}{2}$

```python
# y1(t)
plt.plot(time, y1_time, label=("$y_{1}(t)$"))

# y2(t)
plt.plot(time, y2_time, label=("$y_{2}(t)$"))

plt.plot(time, y1_time+y2_time, label=("$y(t)=y_{1}(t)+y_{2}(t)$"))
plt.grid(True)
plt.title("Odpowiedz "+"$"+"y_{1}(t), y_{2}(t), y(t)"+"$")
plt.xlabel("Czas[s]")
plt.ylabel("Amplituda")
plt.legend()
plt.show()
```

### Przykład zastosowania

Przykładem zjawiska fizycznego, które jest opisywane za pomocą równania różniczkoweo drugiego stopnia jest sprężyna, której drgania są tłumione przez otoczenie (np.: gęsty gaz, ciecz itp.). Taka sprężyna jest zwykle opisana w następujący sposób:

$$ mx^{\prime\prime}(t)+bx^{\prime}(t)+kx(t) = 0$$

Gdzie:
* m - masa
* b - stała np. współczcynnik tarcia, charakterystyczny, dla otoczenia
* k - stała np. współczynnik sprężystości, charakterystyczny, dla sprężyny
* $x(t)$ - jest wtedy pozycją na osi $x$
* $x^{\prime}(t)$ - jest wtedy prędkością na osi $x$
* $x^{\prime\prime}(t)$ - jest wtedy przyspieszeniem na osi $x$

Zajwiska opisywane tego stopnia równaniem są prawdopodobnie najczęściej spotykanymi.

### Wnioski

Matematyczny opis modeli fizycznych jest bardzo przydatny w inżynierii, pozwala nam opisać oraz przewidzieć zachowanie jakiegoś systemu. Zwłaszcza dzięki rozwiązaniom numerycznym, rysując odpowiedź jakiegoś modelu na jakieś wejście, dzieki takiej wizualizacji bardzo łatwo nam jest zrozumieć model.

Dodatkowo uważam, że forma zadania jest bardzo ciekawa, ale prosiłbym o więcej szczegółów apropos tego co Pan oczekuje/w jaki sposob Pan oczekuje, że wykonamy zadanie.


```python
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import numpy as np
np.set_printoptions(precision=3)
np.set_printoptions(suppress=True)
```

# Machine Learning Excercise 1

## Binary Classification

We have a data set below, containing 6 data points, each containing a 2-dimensional vector associated with a label. For example, a vector `[1.2, 0.7]` is associated with the label `+1`.

| vector [x,y] 	| label 	|
|--------------	|-------	|
| [ 1.2, 0.7]  	| +1    	|
| [-0.3, -0.5] 	| -1    	|
| [3.0, 0.1]   	| +1    	|
| [-0.1, -1.0] 	| -1    	|
| [-1.0, 1.1]  	| -1    	|
| [2.1, -3.0]  	| +1    	|

To get a better understanding of the data, we can plot these two-dimensional vectors on a cartesian plane:

Here, the location on the point in the plane is given by the pair of coordinates, and its colour is defined by the label. Red points have the label `+1`, and the blue points have the label `-1`.  When we give the computer a pair of (x,y) coordinates, we want the computer to tell us should whether the point should be marked `-1`—red or `+1`—blue, according to what it knows from the data.  What does learning means in this case? The computer needs to define the boundary between these two given classes.

### Classification by using a simple linear function

First we will use a simple linear function to implement binary classification:

\begin{align}
F = f(\mathbf{X},\mathbf{Y})=A\mathbf{X}+B\mathbf{Y}+C
\end{align}

Here, $\mathbf{X}$ and $\mathbf{Y}$ are the input vectors, and $A$, $B$ and $C$ are the parameters that we are trying to learn. This can be represented schematically:

So, if our data point is $(\mathbf{X},\mathbf{Y})=$ `[1.2,0.7]`, and if the chosen parameters are $A=$ `1`, $B=$ `-2`, $C=$ `-3`, initialise the variables:

```python

```

```python

```

For the corresponding data point label, initialise the variable `label`:

```python

```

Compute the output of the function $F = f(\mathbf{X},\mathbf{Y})$ and assign it to the variable `output`:

```python

```

The prediction has to do with how do we interpret the output $F$ of the function $f(\mathbf{X},\mathbf{Y})$.<br>

__We can define that in our algorithm, the parameters $A$, $B$, and $C$ must be tuned such that the function $f$ outputs a number greater than 0 when it encounters a data point associated with the label `+1`. In the same way, the function should output a number less (or equal to) than 0 when it encounters a data point associated with the label `-1`.__

| Neuron Output 	| Label | Action         |
|--------------	|---------- | ------         |
| Output > +1  	| +1    	| do nothing     |
| Output > -1 	| -1    	| minimise output|
| Output < +1   	| +1    	| maximise output|
| Output < -1 	| -1    	| do nothing     |

Decide whether we should minimise or maximise the output of the function $F = f(\mathbf{X},\mathbf{Y})$ for the current data point?

```python

```

Create a python function `decide(output, label)`, that prints whether we should minimise or maximise our function $F = f(\mathbf{X},\mathbf{Y})$:

```python

```

Test the function `decide` for the current output and label, and try some other values:

```python

```

If the formulas for maximising and minimising functions are respectively:
$$x_{new} = x_{old} + \frac{dy}{dx}x_{old} \cdot c$$
and
$$x_{new} = x_{old} - \frac{dy}{dx}x_{old} \cdot c$$

How can we use a number-holding variable, for example `sign` to modify this function so that it can maximise and minimise according to its value:

```python

```

Initialise the variable `sign` to zero, and write code that will set its value to `-1` or `1` according to the current values of the variables `output` and `label`

```python

```

What is the value of `sign` for the output of our current data points and their label?

```python

```

### Computing the gradient

We have computed the partial derivatives in terms of parameters $A$, $B$, and $C$:

\begin{align*}
\frac{\partial F}{\partial A} &=\frac{\partial F}{\partial G}*\frac{\partial G}{\partial A}; &\frac{\partial F}{\partial B} &=\frac{\partial F}{\partial G}*\frac{\partial G}{\partial B}; &\frac{\partial F}{\partial C} ;& \\\\
\frac{\partial F}{\partial A} &=1*X &\frac{\partial F}{\partial B} &=1*Y &\frac{\partial F}{\partial C} &=1 \\\\
\frac{\partial F}{\partial A} &=X &\frac{\partial F}{\partial B} &=Y &\frac{\partial F}{\partial C} &=1
\end{align*}

Initialise the partial derivatives using the variables `dA`, `dB` and `dC`, and print their values:

```python

```

Introduce the variable `step_size` to hold a very small number and minimise/maximise the value of `dA`, `dB` and `dC`:

```python

```

Evaluate the  function $f(\mathbf{X},\mathbf{Y})$, with the updated parameters $A$, $B$, and $C$:

```python

```

This result should be better then the original result `-3.2` !

***

#### Exercise 1:
Write a simple algorithm for binary classification by using the previously written code:

```python

```

***

### Simple linear classification algorithm

Here, the previous steps are put together in an working algorithm.

| vector [x,y] 	| label 	|
|--------------	|-------	|
| [ 0.0, 0.7]  	| +1    	|
| [-0.3, -0.5] 	| -1    	|
| [3.0, 0.1]   	| +1    	|
| [-0.1, -1.0] 	| -1    	|
| [-1.0, 1.1]  	| -1    	|
| [2.1, -3.0]  	| +1    	|

We can represent the data as a 2-dimensional numpy array

```python
data = np.array([[ 0.0, 0.7],
                 [-0.3,-0.5],
                 [ 3.0, 0.1],
                 [-0.1,-1.0],
                 [-1.0, 1.1],
                 [ 2.1,-3.0]])
```

We can represent the labels as a simple numpy array of numbers

```python
labels = np.array([ 1,
                   -1,
                    1,
                   -1,
                   -1,
                   +1])
```

We can plot the data using the following function:

```python
def plot_data(data, labels):
    fig = plt.figure(figsize=(5,5))
    ax = fig.add_subplot(111)
    ax.scatter(data[:,0], data[:,1], c=labels, s=50,  cmap=plt.cm.bwr,zorder=50)
    nudge = 0.08
    for i in range(data.shape[0]):
        d = data[i]
        ax.annotate(f'{i}',(d[0]+nudge,d[1]+nudge))
    ax.set_aspect('equal', 'datalim')
    plt.show()
```

```python
plot_data(data,labels)
```

This is a function to evaluate the accuracy of the training

```python
def eval_accuracy(data,labels, A,B,C):
    num_correct = 0;
    data_len = data.shape[0]

    for i in range(data_len):
        X,Y = data[i]
        current_label = labels[i]
        output = A*X + B*Y + C
        predicted_label = 1 if output >= 1 else -1 if output <= -1 else 0
        if (predicted_label == current_label):
            num_correct += 1
    return np.round(num_correct / data_len,3)

def create_meshgrid(data):
    h = 0.02
    x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
    y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    return (xx,yy,np.ones(xx.shape))

def plot_learning_simple(grid,data,labels,A,B,C,iteration, accuracy):
    xx,yy,Z = grid

    for i in range(xx.shape[0]): # row
        for j in range(yy.shape[1]): #column
            X, Y = xx[i][j],yy[i][j]
            output = A*X + B*Y + C
            output = 1 if output > 0 else -1
            Z[i][j] = output

    fig = plt.figure(figsize=(5,5))
    ax = fig.add_subplot(111)
    plt.title(f'accuracy at the iteration {iteration}: {accuracy}')
    ax.contourf(xx, yy, Z, cmap=plt.cm.binary, alpha=0.1, zorder=15)
    ax.scatter(data[:, 0], data[:, 1], c=labels, s=50,  cmap=plt.cm.bwr,zorder=50)
    ax.set_aspect('equal')
    nudge = 0.08
    for i in range(data.shape[0]):
        d = data[i]
        ax.annotate(f'{i}',(d[0]+nudge,d[1]+nudge))
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.show()
```

Here is the main algorithm.

```python
def train_neural_network(data, labels, step_size, no_loops, iter_info):
    #A, B, and C are parameters of the function F. Here, they are set to 1, -2, -1
    A, B, C = 1, -2, -1
    # this function is used for plotting, it can be ignored
    grid = create_meshgrid(data)

    # the main training loop
    for i in range(no_loops):
        # we randomly select the data point, and store its info into: x,y,label
        index = np.random.randint(data.shape[0])
        X,Y = data[index]
        label = labels[index]
        # we calculate the output of the function
        output = A*X + B*Y + C
        # We need to define how to affect parameters.
        # If the label is 1 but the output is smaller than 1, we want to push the score up.
        # If the label is -1 but the output is larger than -1, we want to push the score down.
        sign = 0.0
        if (label == 1 and output <= 1):
            sign = 1.0
        if (label ==-1 and output > -1):
            sign = -1.0
         # partial derivative of dF/dA is X, dF/dB is Y, and of dF/dC is 1.
        dA, dB, dC = X, Y, 1
        # here we update the parameter values using partial derivatives
        A = A + dA * sign * step_size
        B = B + dB * sign * step_size
        C = C + dC * sign * step_size;

        # after a number of iterations, show training accuracy and plot it
        if (i%iter_info==0):
            accuracy = eval_accuracy(data, labels, A,B,C)
            plot_learning_simple(grid,data,labels,A,B,C,i,accuracy)
    # the algorithm returns the learned parameters A, B, and C
    return (A,B,C)
```

```python
train_1 = train_neural_network(data, labels, 0.01, 2501, 500)
```

We can inspect the result by comparing the real label of a data point and the predicted label:

```python
def show_prediction(train, data, labels):
    A, B, C = train
    for i in range(data.shape[0]):
        X,Y = data[i]
        label = labels[i]
        output = A*X + B*Y + C
        predicted_label = 1 if output >= 1 else -1 if output <= -1 else 0
        print (f'data point {i}: real label : {label}, pred. label: {predicted_label}, {(label==predicted_label)}')
```

```python
show_prediction(train_1,data,labels)
```

    data point 0: real label : 1, pred. label: 1, True
    data point 1: real label : -1, pred. label: -1, True
    data point 2: real label : 1, pred. label: 1, True
    data point 3: real label : -1, pred. label: -1, True
    data point 4: real label : -1, pred. label: -1, True
    data point 5: real label : 1, pred. label: 1, True

Exercise 04—Binary Classification Using a Simple Linear Function.ipynb


# Lecture 2

```python
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
import tellurium as te
```

```python
FONTSIZE  = 18
```

## Phase Portrait

```python
def plotPhase(xlim, ylim, dFunc, numPt=20):
    """
    Constructs a phase plot for a system of 2-d system of differential equations.

    Parameters
    ----------
    xlim: tuple
        lower and upper value of x
    ylim: tuple
        lower and upper value of y
    dFunc: Function
        args:
            tuple of current x, y
    numPt: int
        number of points in the grid
    """
    # Create the mesh
    xGrid = np.linspace(xlim[0], xlim[1], numPt)
    yGrid = np.linspace(ylim[0], ylim[1], numPt)
    xMesh, yMesh = np.meshgrid(xGrid, yGrid)
    xVals, yVals = np.zeros(xMesh.shape), np.zeros(yMesh.shape)
    # Populate mesh with values
    numX, numY = yMesh.shape
    for xIdx in range(numX):
        for yIdx in range(numY):
            x = xMesh[xIdx, yIdx]
            y = yMesh[xIdx, yIdx]
            yprime = dFunc([x, y])
            xVals[xIdx, yIdx] = yprime[0]
            yVals[xIdx, yIdx] = yprime[1]
    # Construct quiver plot
    fig, ax = plt.subplots(1, figsize=(10, 12))
    quiverPlt = ax.quiver(xMesh, yMesh, xVals, yVals, color='r')
    ax.set_xlabel('$x$')
    ax.set_ylabel('$y$')
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)

def dFunc(Y):
    y1, y2 = Y
    return [y2, -np.sin(y1)]
numPt = 50
xlim = [-2, 8]
ylim = [-4, 4]

plotPhase(xlim, ylim, dFunc, numPt=numPt)
```

## 1-dimensional System

$\frac{dS_1}{dt} = -(k_f +k_r)S_1 + k_r S_T$

```python

```

```python
C_k_f = 1
C_k_r = 0.8
C_S_T = 10
model = """
S1 -> S2; k_f*S1
S2 -> S1; k_r*S2

k_f = 0
k_r = 0
S1 = 0
S2 = 0
"""
rr = te.loada(model)
rr.k_f = C_k_f
rr.k_r = C_k_r
rr.S1 = C_S_T
data = rr.simulate()
fig, ax = plt.subplots(figsize=(5, 5))
_ = ax.plot(data[:,0], data[:, 1:])
ax.set_ylabel("concentration", fontsize=FONTSIZE)
ax.set_xlabel("time (sec)", fontsize=FONTSIZE)
_ = ax.legend(data.colnames[1:])
```

```python
values = np.array([0.1*v for v in range(0, 10*C_S_T+1)])
def ds(s):
     return np.array(-(C_k_f + C_k_r)*s + C_k_r*C_S_T)
fig, ax = plt.subplots(1, figsize=(8, 6))
ax.plot(values, ds(values))
ax.set_xlabel(r'$S_1$', fontsize=FONTSIZE)
ax.set_ylabel(r'$\frac{dS_1}{dt}$', fontsize=FONTSIZE, rotation=0)
_ = ax.plot([0, 10], [0, 0], linestyle="--")
```

$\frac{dS_1}{dt} = -(k_f +k_r)S_1(S_T - S_1) + k_r S_T = $

```python
sy.init_printing()
dS1, S1, k_f, k_r, S_T, k_2, k_3, k_0, S2 = sy.symbols("dS1 S1 k_f k_r S_T k_2 k_3 k_0 S2")
```

```python
f_dS1 = -(k_f + k_r)*S1*(S_T - S1) + (k_r)*S_T
sy.simplify(f_dS1)
```

```python
ans = sy.solve(f_dS1, S1)
ans
```

```python
ans[0].evalf(subs={S_T: C_S_T, k_f: C_k_f, k_r: C_k_r})
```

## 2-d Linear System
- $\emptyset \rightarrow S_1$; $k_0$
- $S_1 \leftrightarrow S_2$; $-k_f S_1 + k_r S_2$
- $S_1 \rightarrow 2 S_1$; $k_2 S_1$
- $S_2 \rightarrow \emptyset$; $k_3 S_2$

$\frac{S_1}{dt} = k_0 -k_f S_1 + k_r S_2 + k_2 S_1 = k_0 + S_1 (k_2 - k_f) + k_r S_2$

$\frac{S_2}{dt} = k_f S_1 - k_r S_2 - k_3 S_2 = k_f S_1 - S_2 (k_r + k_3)$

The model turns unstable when k2 transitions from .5 to .6.

```python
model = """
J0:  -> S1; k_0
J1: S1 -> S2; k_f*S1
J2: S2 -> S1; k_r*S2
J3: S1 -> 2 S1; k_2*S1
J4: S2 -> ; k_3*S2

k_0 = 1
k_2 = 0.5
k_3 = 1
k_f = 1.0
k_r = 0.8
S1 = 180
S2 = 100

//at (time > 10): S1 = 200
"""
rr = te.loada(model)
#rr.k_f = C_k_f
#rr.k_r = C_k_r
#rr.S_T = C_S_T
data = rr.simulate(0, 100)
rr.plot(data)
```

```python
dS1 = k_0 + S1*(k_2 - k_f) + k_r*S2
dS2 = k_f*S1 - S2*(k_3 + k_r)
```

```python
C_k_f = 1
C_k_r = 0.8
C_k_0 = 1
C_k_2 = .55
C_k_3 = 1
def dFunc(Svec):
    s1, s2 = Svec
    ds1 = dS1.subs({k_0: C_k_0, S1: s1, S2: s2, k_2: C_k_2, k_f: C_k_f, k_r: C_k_r})
    ds2 = dS2.subs({k_0: C_k_0, S1: s1, S2: s2, k_2: C_k_2, k_f: C_k_f, k_r: C_k_r, k_3: C_k_3})
    return (ds1, ds2)
```

```python
subDct = {k_0: C_k_0, k_2: C_k_2, k_f: C_k_f, k_r: C_k_r, k_3: C_k_3}
def findFixedPoints(expressions, subDct, vars):
    newExpressions = []
    for expression in expressions:
        newExpressions.append(expression.subs(subDct))
    return sy.solve(newExpressions, vars)

findFixedPoints([dS1, dS2], subDct, [S1, S2])
```

```python
plotPhase([0, 50], [0, 50], dFunc, numPt=20)
```

```python
xx = k_0 + S1
xx.subs({k_0: 1})
```

```python
dS1.subs({k_0:1, k_2:1, k_f:1, k_r: 1})
```

```python
dS1.find(k_0)
```

```python
vMat = sy.Matrix([dS1, dS2])
vMat
```

$\displaystyle \left[\begin{matrix}S_{1} \left(k_{2} - k_{f}\right) + S_{2} k_{r} + k_{0}\\S_{1} k_{f} - S_{2} \left(k_{3} + k_{r}\right)\end{matrix}\right]$

```python
jacobian = vMat.jacobian((S1, S2))
jacobian
```

$\displaystyle \left[\begin{matrix}k_{2} - k_{f} & k_{r}\\k_{f} & - k_{3} - k_{r}\end{matrix}\right]$

```python
det = sy.simplify(jacobian.det())
det
```

Note that if $k_2 = 0$, then the determinant is always positive.

```python
fixedPt = sy.solve([dS1, dS2], [S1, S2])
fixedPt
```

## Lac Operon

```python
dI_int, a, I_ext, I_int, P, b, c, e, dP, I_int_str, P_str = sy.symbols("dI_int a I_ext I_int P b c e dP I_int_str P_str")
dP = c*I_int - e*P
dI_int = a*(I_ext - I_int)*P - b*I_int
```

```python
fixedPoints = sy.solve([dP, dI_int], [I_int, P])
```

```python
fixedPoints[1]
```

```python
subDct = {a:1, b:0.1, c:1, e:0.5, I_ext:3}
C_I_int_str, C_P_str = [s.evalf(subs=subDct) for s in fixedPoints[1]]
subDct[I_int_str] = C_I_int_str
subDct[P_str] = C_P_str
```

```python
# Jacobian
jacobian = sy.Matrix([dI_int, dP])
jacobian = jacobian.jacobian([I_int, P])
linearization = jacobian.subs({P: P_str, I_int:I_int_str})
```

```python
linearization.subs(subDct)
```

$\displaystyle \left[\begin{matrix}-6.0 & 0.0499999999999998\\1 & -0.5\end{matrix}\right]$

```python
2.95*4
```

## Nonlinear System

```python
values = np.array([0.1*v for v in range(0, 10*C_S_T+1)])
def ds(s):
     return np.array(-(C_k_f + C_k_r)*s*(C_S_T-s) + C_k_r*C_S_T)
fig, ax = plt.subplots(1, figsize=(8, 6))
ax.plot(values, ds(values))
ax.set_xlabel(r'$S_1$', fontsize=FONTSIZE)
ax.set_ylabel(r'$\frac{dS_1}{dt}$', fontsize=FONTSIZE, rotation=0)
_ = ax.plot([0, 10], [0, 0], linestyle="--")
```

```python
dx3, dx1, x1, ll, k1, y1, y2, x2, dx2, k2 = sy.symbols("dx3, dx1, x1, ll, k1, y1, y2, x2, dx2, k2 ")
```

```python
dx1 = ll - k1*y1*(x1/(1 + x1))
dx2 = k1*y1*(x1/(1 + x1)) - k2*y2*(x2/1+x2)
```

```python
fixedPoints = sy.solve([dx1, dx2], [x1, x2])
fixedPoints
```

```python
subDct = {x2: fixedPoints[0][1]}
dx3 = k2*y2*(x2/1+x2)
dx3.evalf(subs=subDct)
```


<figure>
  <IMG SRC="https://raw.githubusercontent.com/mbakker7/exploratory_computing_with_python/master/tudelft_logo.png" WIDTH=250 ALIGN="right">
</figure>

# Exploratory Computing with Python
*Developed by David B. Steffelbauer and Mark Bakker*
## Notebook xx: Optimization with Genetic Algorithms

### Introduction

In this notebook we will learn how to find minima in arbitrary functions. Finding minima is a field in mathematics that is called [Mathematical Optimization](https://en.wikipedia.org/wiki/Mathematical_optimization). Optimization of functions is of high importance in various fields ranging from finance to engineering. For example, airline companies have to schedule flights and airplanes in an optimal way to minimise costs, delivery companies have to find the shortest path between their customers and investors seek to minimise their risk while optimising their profit. All these problems are optimisation problems.

Not only engineers, but also nature itself optimises. The principle behind the evolution of species is that individuals that are best adapted to the environment are more likely to survive. This is called the survival of the fittest. Increasing the adaption of species to their environment can be seen as optimising the fitness of a species.

The whole information of one individual is decoded in its genes, which can be seen as input parameters of functions. Better adaption leads to higher chance of breeding with other individuals of the same species and passing your fit genes to the next generation. We will learn throughout this notebook how the rules of evolution can be translated into computer code and how the underlying mechanisms can be used to find minima in functions.

Note: Make sure that all graphs that you produce include labels along the horizontal and vertical axes, a title and, if you are plotting multiple things in one graph, a legend.

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

### The Rastrigin function

We will try to find an optimum in a very nasty function, the [Rastrigin Function](https://en.wikipedia.org/wiki/Rastrigin_function). The Rastrigin function is widespread used as a test problem for optimization algorithms. The function is a multi-modal function which means that it possesses many local optima and  has one global optimum.

It is defined in $D$ dimensions as follows
\begin{align}
f(\mathbf{x}) \ = \ a \cdot D + \sum_{i=1}^{D} \left(x_{i}^{2} - a \cdot \cos (2 \pi x_i) \right)
\end{align}

and has besides its many local minima a global minimum $\mathbf{x}^\ast$ at
\begin{align}
\mathbf{x}^\ast = \mathbf{0} \quad \text{with} \quad f(\mathbf{x}^\ast) = 0
\end{align}
This global minimum is the minimum we want to find.

The gradient (first derrivative) results in

\begin{align}
\frac{\partial f(\mathbf{x})}{\partial x_i} = 2 x_i + 2 \pi  a \sin \left(2 \pi x_i\right)
\end{align}

Setting the gradient to zero, we can see that the function has an infinite number of local optima at
\begin{align}
	x_i = \frac{n_i}{2} \quad \text{with} \quad n_i \in \mathbb{Z} \quad \forall \ i
\end{align}

The Hessian calculation leads to
\begin{equation}
	\frac{\partial^2 f(\mathbf{x})}{\partial x_i \partial x_j} = \begin{cases}
	\text{if } i = j: &2 + 4 \pi^2 a \cos \left( 2 \pi x_i\right) \\
	\text{if } i\neq j:	&\qquad \quad 0
	\end{cases}
\end{equation}
The Hessian is a sparse, diagonal matrix. The determinant of the Hessian is positive if $n_i$ is even leading to a local minimum, whereas if $n_i$ is odd, a negative determinant and a local maximum results.

### Exercise 1. The Rastrigin function

The first exercise is to implement the Rastrigin function in arbitrary dimensions. Set $a$ to 10. Use a vector of arbitrary length as input parameter of the function ($\rightarrow$ numpy package!). Compute the function values for the two dimensional points (a) (x,y)=(-0.5,0.5), (b)  (x,y)=(0.0,0.5), (c) for the global optimum in two dimensions, (d) for a vector containing just ones of length 5 and, finally, (e) a vector of length 10 with the numbers one to ten. Print the results to the screen. Note: Use the numpy package for obtaining the numerical value for $\pi$ ([$\rightarrow\texttt{np.pi}$](https://docs.scipy.org/doc/numpy/reference/constants.html#numpy.pi)) and for the cosine function ([$\rightarrow\texttt{np.cos}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.cos.html)).

```python
def rastrigin(xvector, a=10):

    D = len(xvector)
    value = D * a
    for x in xvector:
        value += x ** 2 - a * np.cos(2 * np.pi * x)
    return value
```

```python
points = [[-0.5, 0.5],
          [0.0, 0.5],
          np.ones(5),
          np.arange(10)+1]

for point in points:
    string = ', '.join(f'{i: 2.2f}' for i in point)
    print(f'Point ({string}) => {rastrigin(point)}')
```

    Point (-0.50,  0.50) => 40.5
    Point ( 0.00,  0.50) => 20.25
    Point ( 1.00,  1.00,  1.00,  1.00,  1.00) => 5.0
    Point ( 1.00,  2.00,  3.00,  4.00,  5.00,  6.00,  7.00,  8.00,  9.00,  10.00) => 385.0

### Exercise 2. Computation of 2d-function values of the Rastrigin function
Compute the Rastrigin function for different x-y combinations at once on a two dimensional mesh. Use [$\texttt{np.linspace}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html) to produce a vector between -2 and +2 with 100 samples. Use [$\texttt{np.meshgrid}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.meshgrid.html) to produce a two dimensional mesh $X$ and $Y$. Compute the two-dimensional function values $Z$ with your implementation of the rastrigin function (Hint: Give the function values as list to the rastrigin function $\texttt{Z = rastrigin([X,Y])}$). Compute the minimum of $Z$ and the $x$ and $y$ coordinates belonging to this minimum and print the minimum and the coordinate values to the screen. Why is the computed minimum different to the minimum of the analytical computation? What can you do to retrieve the real global optimum?

```python
x = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, x)

Z = rastrigin([X,Y])

f_min = np.min(Z)
index = np.unravel_index(np.argmin(Z), Z.shape)
coords = [x[i] for i in index]

print(f'Computed minimum min(Z) = {f_min:.2f} at point ({coords[0]:.3f}, {coords[1]:.3f})')

```

    Computed minimum min(Z) = 0.16 at point (-0.020, -0.020)

### Exercise 3. Plotting of 2d-Rastrigin function
Plot the two-dimensional Rastrigin function with matplotlib's [$\texttt{contourf}$](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.contourf.html) function. Add axis labels ($x$ and $y$) and a [colorbar](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.colorbar.html) with a colorbar label ($f(x,y)$). Compute the minimum at $x_0=0$ and $y_0=0$, add a title containing the minimum value and [annotate](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.annotate.html) the minimum in the contourplot with an arrow and a text with '$f(x_0, y_0) = $' and the computed minimum value ([$\rightarrow$Hint](https://matplotlib.org/users/annotations.html)).

```python
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
plt.contourf(X, Y, Z)
cb = plt.colorbar()
plt.xlabel('x')
plt.ylabel('y')

cb.set_label(r'$f(x, y)$')

# Get minimum value in Rastrigin function and plot it
x0 = 0.0
y0 = 0.0
z0 = rastrigin([x0, y0])

plt.plot(0, 0, marker='x', color='k')
# plt.text(0, 0, f'$f(x_0, y_0) = {z0}$')
plt.title(f'$x_0 = 0.0, y_0=0.0 \\rightarrow f(x_0, y_0) = {z0}$')
plt.annotate(f'$f(x_0, y_0) = {z0}$',
             xy=[x0, y0],
             arrowprops=dict(color='r', shrink=0.05),
             horizontalalignment='left',
             verticalalignment='bottom',
             xytext=(0.5, 0.5));

```

### Exercise 4. Find the minimum by coincidence and show the convergence
Produce 100000 uniformly distributed two-dimensional random points between -2 and +2 ([$\rightarrow \texttt{np.random.uniform}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.uniform.html)) and compute the Rastrigin function values. Show the convergence to the minimum, in other words, how the minimum value of the function gets smaller and smaller in dependency of the number of random numbers (Hint: use [$\texttt{np.minimum.accumulate}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ufunc.accumulate.html)). Plot the result as a [loglog](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.loglog.html) plot and print the found minimum value to the screen. Don't forget to add axis labels and a title.

```python
R = np.random.uniform(low=-2.0, high=2.0, size=(2, 100000))
R_x = R[0, :]
R_y = R[1, :]

values = np.minimum.accumulate(rastrigin([R_x, R_y]))

fig, ax = plt.subplots(1, 1)

plt.loglog(values)
plt.title('Convergence plot')
plt.xlabel('# function evaluations')
plt.ylabel(r'$f(x,y)$')
plt.grid(True)
print('Monte Carlo minimum is = ', values[-1])
```

### Genetic Algorithms

[Genetic Algorithms (GA)](https://en.wikipedia.org/wiki/Genetic_algorithm) are widely used to obtain optimal solutions to countless problems in the water related field. GAs — first proposed by [John H. Holland](https://en.wikipedia.org/wiki/John_Henry_Holland) in 1975 — mimic the principles of evolution to solve optimization problems. GAs are population based. Hence, not just a single solution evolves over time, but rather they utilize the collective learning process of a population consisting of many single solutions. Each solution —called individual— consists of parameters —called genes— which represents a single search point in the parameter space. Descendants of individuals are produced by random either (i) through reproduction by exchanging genes with other individuals or (ii) by mutation, introducing randomly small changes in genes mimicking germ line mutation effects. Subsequently, the fitness of each individual is determined and fitter individuals are more likely to survive and reproduce, thus, giving their good genes to potentially more descendants and hence increasing the fitness of the whole population over time.

We will learn step by step in the following exercises how to implement a GA and show that the theory of evolution can be used to find minima in functions. First, we will start by implementing the individual class. Second, we will initialize a whole population of individuals over the whole search space. Then we will learn how to combine the genes of multiple individuals to produce children, how to mutate the genes of the children to obtain new solutions, how to select the best solutions and how to put everything together resulting in a working implementation of a Genetic Algorithm.

### Exercise 5. Construct an Individual class object

Implement a Python class Individual which has two properties (1) a genome and (2) a fitness value. The genome is a vector of real numbers (genes), the fitness value is the function value obtained by the Rastrigin function with the genome as its input. The $\texttt{__init__}$ method should take as arguments the genome, the fitness value should be [NaN](https://en.wikipedia.org/wiki/NaN) (not a number) by default ([$\texttt{np.nan}$](https://docs.scipy.org/doc/numpy-1.13.0/user/misc.html)).  Add an evaluate method to the class that computes the Rastrigin function only if the fitness is NaN. Override the $\texttt{__repr__}$ method to have a nice representation of the individual's genes and fitness when the print function is called. Use the points from exercise 1 to check if your implementation and the evaluate method is working (Hints on how to construct classes and override methods can be found in
[Notebook 12](https://nbviewer.jupyter.org/github/mbakker7/exploratory_computing_with_python/blob/master/notebook12_oop/py_exploratory_comp_12_sol.ipynb)).

```python
class Individual(object):

    def __init__(self, genome=None):
        self.genome = genome
        self.fitness = np.nan

    def evaluate(self):
        if np.isnan(self.fitness):
            self.fitness = rastrigin(self.genome)

    def __repr__(self):
        string = f'Individual with Genome: ['
        string += ', '.join(f'{i: 2.2f}' for i in self.genome)
        string += f'] => f(x): {self.fitness:=6.3f}'
        return string
```

```python
points = [[-0.5, 0.5],
          [0.0, 0.5],
          np.ones(5),
          np.arange(10)+1]

for genome in points:
    ind = Individual(genome=genome)
    ind.evaluate()
    print(ind)

```

    Individual with Genome: [-0.50,  0.50] => f(x): 40.500
    Individual with Genome: [ 0.00,  0.50] => f(x): 20.250
    Individual with Genome: [ 1.00,  1.00,  1.00,  1.00,  1.00] => f(x):  5.000
    Individual with Genome: [ 1.00,  2.00,  3.00,  4.00,  5.00,  6.00,  7.00,  8.00,  9.00,  10.00] => f(x): 385.000

### Intitialization
An initialization procedure generates a population of individuals with hundreds or thousands of possible solutions. The number of individuals in a population is called the population size ($popsize$). Usually, the single individuals are produced randomly over the whole parameter space. Occasionally, the solutions may be "seeded" in areas where the optimal solutions is supposed to be found.

### Exercise 6. Initialize population

Write an initialization function ($\texttt{initialize}$) that returns a population of $popsize$ individuals in a two dimensional ($D=2$) parameter space within $x_i \in [-2, 2]$. The population should be a $\texttt{list}$ of $\texttt{Individual}$ instances. Use $popsize=10$. Subsequently, evaluate each individual in the population and print the whole population on the screen by looping over all Individuals.

```python
def initialize(popsize, dimensions=2, low=-2.0, high=+2.0):

    population = []
    for _ in range(popsize):
        genes = np.random.uniform(low=low, high=high, size=2)
        ind = Individual(genome=genes)
        population.append(ind)
    return population

popsize = 10

population = initialize(popsize)

print('The Population consists of ...')
for ind in population:
    ind.evaluate()
    print('->\t an', ind)
```

    The Population consists of ...
    ->	 an Individual with Genome: [ 0.61,  0.65] => f(x): 34.175
    ->	 an Individual with Genome: [-1.67,  0.90] => f(x): 20.561
    ->	 an Individual with Genome: [-1.15, -0.01] => f(x):  5.416
    ->	 an Individual with Genome: [-0.54,  1.21] => f(x): 28.956
    ->	 an Individual with Genome: [ 1.12, -0.85] => f(x):  8.900
    ->	 an Individual with Genome: [-1.60, -0.86] => f(x): 24.766
    ->	 an Individual with Genome: [ 1.51, -0.48] => f(x): 42.381
    ->	 an Individual with Genome: [-1.67, -1.89] => f(x): 23.296
    ->	 an Individual with Genome: [ 0.26,  1.14] => f(x): 15.225
    ->	 an Individual with Genome: [-1.46,  1.24] => f(x): 32.917

### Exercise 7. Plot the population in fitness landscape

Plot the fitness landscape, which is the 2d Rastrigin function from Exercise 3, as a $\texttt{contourf}$ plot. Then plot the individuals of the population of exercise 6 in the fitness landscape as black points. Highlight the fittest individual (the individual with the smallest fitness value) in the fitness landscape by surrounding it with a red circle and print the fittest individual to the screen.

```python
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
plt.contourf(X, Y, Z)
cb = plt.colorbar()
plt.xlabel('x')
plt.ylabel('y')

cb.set_label(r'$f(x, y)$')

fitness = np.inf
for ind in population:
    plt.plot(*ind.genome, 'ko')
    if ind.fitness < fitness:
        fitness = ind.fitness
        x, y = ind.genome

plt.plot(x, y, 'o', markerfacecolor='None', markeredgecolor='r', markersize=12);
print(f'Minimum of population at ({x:.2f},{y:.2f}) with f(x)={fitness}')
```

### Recombination - the crossover operator

The first operator in the evolution is the [recombination operator](https://en.wikipedia.org/wiki/Crossover_(genetic_algorithm)). Recombination is responsible for large changes in the solution vectors. The operator produces $k$ new solutions by combining genes of different individuals of the population.

In addition to the population, the operator also depends on an additional set of parameters, controlling the reproduction of the individuals such as the probability $p_r$ that the genes of two individuals are recombined. GAs have in common that they favor recombination over mutation, hence, $p_r$ is chosen to be high (e.g. $p_r = 0.8$).

### Exercise 8. Implement crossover for two individuals
Single point crossover represents a possibility to combine the genes of two individuals, the two parents. A point on both parents' genes is picked randomly (use [$\texttt{randint}$](https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.randint.html)), and designated a 'crossover point'. Genes to the right of that point are swapped between the two parent chromosomes. This results in two children, each carrying some genetic information from both parents. Implement a Python function called $\texttt{crossover}$ that takes as input two $\texttt{Individuals}$ (the parents) that performs a single point crossover between two Individuals and returns two new $\texttt{Individuals}$ (the children). Test the function on two individuals with 5 genes, one having only zeros (parent 1) and one having only ones (parent 2) in their genes. Apply crossover on the parents to produce the children, evaluate the children and print the parents and children to the screen to check, if the crossover operator is working the right way. In this example, the fitness of the children should be between the fitness values of the parents.

```python
def crossover(parent1, parent2):
    cr_point = np.random.randint(1, len(parent1.genome))
    genome1 = np.concatenate([parent1.genome[:cr_point], parent2.genome[cr_point:]])
    genome2 = np.concatenate([parent2.genome[:cr_point], parent1.genome[cr_point:]])

    child1 = Individual(genome=genome1)
    child2 = Individual(genome=genome2)

    return child1, child2
```

```python
p1 = Individual(genome=np.zeros(5))
p2 = Individual(genome=np.ones(5))

c1, c2 = crossover(p1, p2)

for i in [p1, p2, c1, c2]:
    i.evaluate()

print('\nParents')
print(p1)
print(p2)

print('\nChildren')
print(c1)
print(c2)
```

    Parents
    Individual with Genome: [ 0.00,  0.00,  0.00,  0.00,  0.00] => f(x):  0.000
    Individual with Genome: [ 1.00,  1.00,  1.00,  1.00,  1.00] => f(x):  5.000

    Children
    Individual with Genome: [ 0.00,  0.00,  1.00,  1.00,  1.00] => f(x):  3.000
    Individual with Genome: [ 1.00,  1.00,  0.00,  0.00,  0.00] => f(x):  2.000

### Exercise 9. Recombination of the whole population - let the mating season begin

Write a Python function called $\texttt{recombination}$ which takes as input parameters a population and the crossover probability $p_c$. Build random pairs within the population (all Individuals will find a partner) and iterate over these pairs. Decide with a uniform random number $u \in [0, 1]$ if the parents will crossover their genes to produce new children ($u \leq p_c$). If $u > p_c$, then two new children are produced with the same genes as the parents. Return all new children as a Python list. Test the $\texttt{recombination}$ function on the population of exercise 6 with a crossover probability of $p_c=0.8$ . Evaluate the children and print them to the screen.

```python
def recombination(population, CXPB):

    popsize = len(population)

    combinations = np.arange(0, popsize)
    np.random.shuffle(combinations)
    combinations = combinations.reshape((int(popsize/2), 2))

    children = []
    for i, j in combinations:

        parent1 = population[i]
        parent2 = population[j]

        if np.random.rand() < CXPB:
            child1, child2 = crossover(parent1, parent2)
        else:
            child1 = Individual(genome=parent1.genome)
            child2 = Individual(genome=parent2.genome)

        children.append(child1)
        children.append(child2)

    return children
```

```python
children = recombination(population, 0.8)

for child in children:
    child.evaluate()
```

```python
children
```

    [Individual with Genome: [-1.15, -0.48] => f(x): 25.506,
     Individual with Genome: [ 1.51, -0.01] => f(x): 22.291,
     Individual with Genome: [-0.54,  1.21] => f(x): 28.956,
     Individual with Genome: [ 0.26,  1.14] => f(x): 15.225,
     Individual with Genome: [-1.67,  1.24] => f(x): 28.835,
     Individual with Genome: [-1.46,  0.90] => f(x): 24.644,
     Individual with Genome: [ 0.61, -0.86] => f(x): 22.165,
     Individual with Genome: [-1.60,  0.65] => f(x): 36.776,
     Individual with Genome: [ 1.12, -1.89] => f(x):  9.936,
     Individual with Genome: [-1.67, -0.85] => f(x): 22.259]

### Mutation operator

The second operator in the GA is the mutation operator. This operator produces small changes to the genes of an individual, hence, broadening the genetic variability of a population. GAs have in common that the mutation operator is applied with a low probability $p_m$ so that mutation works more as a "background operator" (usually $p_m \leq 0.2$).

A gene in the genome of the children is taken at random and altered due to a certain random procedure with a certain probability.

### Exercise 10. Implement and apply mutation on the children

Implement the mutation operator by writing a function $\texttt{mutation}$. The input parameters of the function are the children obtained through the recombination function, the mutation probability $p_m$ and a standard deviation $\sigma$. Iterate over all children and randomly decide with help of $p_m$, if the genome of the child should be mutated. If the child is mutated, choose a gene of the genome at random and add a Gaussian distributed random number with mean $mu=0.0$ and standard deviation $\sigma=0.25$ to the gene. The function should return the children. Apply the function on the children that were produced in exercise 9 to test it.

```python
def mutation(children, MUTPB, sigma=0.25):

    for child in children:
        if np.random.rand() < MUTPB:
            n = len(child.genome)
            mut_int = np.random.randint(0, n)
            child.genome[mut_int] += np.random.normal(0.0, scale=sigma)

    return children

```

```python
children = mutation(children, 0.2)
```

```python
children
```

    [Individual with Genome: [-1.15, -0.52] => f(x): 25.506,
     Individual with Genome: [ 1.51, -0.01] => f(x): 22.291,
     Individual with Genome: [-0.54,  1.21] => f(x): 28.956,
     Individual with Genome: [ 0.26,  1.14] => f(x): 15.225,
     Individual with Genome: [-1.67,  1.24] => f(x): 28.835,
     Individual with Genome: [-1.46,  0.90] => f(x): 24.644,
     Individual with Genome: [ 0.61, -0.67] => f(x): 22.165,
     Individual with Genome: [-1.60,  0.65] => f(x): 36.776,
     Individual with Genome: [ 1.12, -1.74] => f(x):  9.936,
     Individual with Genome: [-1.67, -0.85] => f(x): 22.259]

### Selection operator

The last operator in the GA is the selection operator. This operator chooses $popsize$ individuals
from the through mutation and recombination altered population of parents and children based on their fitness. Fitter individuals are chosen with higher probability.

### Roulette wheel selection

Roulette wheel selection is also called [fitness proportionate selection](https://en.wikipedia.org/wiki/Fitness_proportionate_selection). Individuals with a higher fitness (lower fitness value) are chosen with higher probability. The probability to choose an individual is
\begin{align}
    p_i \ = \ \frac{max(f) - f_i}{\sum_{j=1}^{N} (max(f) - f_j)}
\end{align}

max(f) is the maximum of all fitness values in the population and is necessary to scale the fitnesses for minimisation.
Additionally, to make sure that the best individual of a population –the one with the lowest fitness value–  is not lost during the evolution, one can make sure to always choose the best individual as part of the selected population. This is called elitism.

### Exercise 11. The selection operator.

Write a Python function $\texttt{selection}$ that takes a population and the number of chosen individuals of this population as input parameters. Implement the roulette wheel selection algorithm with elitism as described above. Remove Individuals that have been chosen from the population so that they can't be chosen multiple times ([$\texttt{pop}$](https://docs.python.org/3.7/tutorial/datastructures.html) removes an element from a list by index). Don't forget to recompute the probabilities each time you remove an individual! The function should return individuals that are chosen from the population according to the probabilities build by their fitness value. (Hint: Use the cumulative sum of the probabilities and a random number between 0 and 1 to decide which individual of the population is chosen. With numpy's [argmin](https://docs.scipy.org/doc/numpy/reference/generated/numpy.argmin.html) function one can find the best individual in the fitnesses).

```python
def selection(population, number=20, elitism=True):

    # Produce vector containing all fitnesses:
    fitness = []
    for ind in population:
        ind.evaluate()
        fitness.append(ind.fitness)

    if elitism:
        # Find fittest individual
        index = np.argmin(fitness)
        best = population[index]
        chosen = [best]

        # Remove best individual from population and fitness
        population.pop(index)
        fitness.pop(index)

        # Reduce the number of the chosen individuals by one, since one individual has already been chosen
        number -= 1
    else:
        chosen = []
    for n in range(number):

        # Compute the scaled fitness values and the probabilities
        scaled = np.max(fitness) - fitness
        probabilities = np.cumsum(scaled / np.sum(scaled))

        r = np.random.random()
        index = (r >= probabilities).sum()
        chosen.append(population[index])

        # Remove best individual from population and fitness
        population.pop(index)
        fitness.pop(index)

    return chosen
```

```python
selection(population + children, number=4)
```

    [Individual with Genome: [-1.15, -0.01] => f(x):  5.416,
     Individual with Genome: [ 0.61, -0.67] => f(x): 22.165,
     Individual with Genome: [-1.67,  0.90] => f(x): 20.561,
     Individual with Genome: [ 0.26,  1.14] => f(x): 15.225]

### Exercise 12. A simple genetic algorithm - Putting it all together

Write a Python script where you utilize all operators that you have developed in the exercises before.

First, initialize a population of individuals with $D=2$ between $x_i \in [-2, 2]$ with a population size of 100. Then make a for loop over 500 generations. In each generation, use the recombination function to produce children with a crossover probability of $p_c=0.8$. Mutate the children with a mutation probability of $p_m=0.2$ and $\sigma=0.25$. Use the roulette wheel selection function to select $popsize$ individuals from the parent population and children combined. Make sure that the fittest individual is always chosen (elitism). If the fitness of the fittest individual is lower than 0.0001, terminate the evolution by using the [$\texttt{break}$](https://docs.python.org/3.7/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) command. Track the fitness of the fittest individual in each generation and plot this value as a function of the generations in a semi-logarithmic plot ([$\texttt{semilogy}$](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.semilogy.html)). Don't forget axis labels and titles!
Print the overall fittest individual to the screen.

```python
popsize = 100
ngen = 500

CXPB = 0.8
MUTPB = 0.2

# Initialisation
population = []
for _ in range(popsize):
    ind = Individual(genome=np.random.uniform(low=-2.0, high=2.0, size=2))
    population.append(ind)

f_min = np.full((ngen, 1), np.nan)

for gen in range(ngen):

    children = recombination(population, CXPB)

    children = mutation(children, MUTPB)

    population = selection(population + children, number=popsize, elitism=True)

    # Extract fitness values for statistics
    fits = [ind.fitness for ind in population]
    genomes = np.asarray([ind.genome for ind in population])
    f_min[gen] = np.min(fits)

    if f_min[gen] < 0.0001:
        break

# Plot fittest individual:
fitness = [individual.fitness for individual in population]
index = np.argmin(fitness)
print(population[index])
```

    Individual with Genome: [ 0.00,  0.00] => f(x):  0.000

```python
plt.semilogy(f_min)
plt.xlabel('generations')
plt.ylabel('min(f(x))')
plt.title('Convergence of the GA')
plt.grid(True)
```

### Exercise 13. Escape local optima with GAs
Use the code from above, but alter it a little bit. Intitialize the population in the right upper corner with $x_i \in [0.5, 2.0]$. Increase the number of generations to 2000. Additionally, track the mean $x$ and $y$ coordinate positions over all individuals of the population during the evolution. This is called the trajectory. Run the optimisation.

Plot the contour plot of the Rastrigin function as in exercise 3. Highlight the area where the solutions were initially generated in the contour plot with two black dashed lines. Plot the trajectory of the evolution as black line. Can you see that the population jumps from local optimum to local optimum until it finds the global one?

This is an advantage of [Metaheuristic algorithms](https://en.wikipedia.org/wiki/Metaheuristic). They are able to escape local optima in contrast to deterministic methods.

```python
popsize = 100
ngen = 2000

CXPB = 0.8
MUTPB = 0.2

# Initialisation
population = []
for _ in range(popsize):
    ind = Individual(genome=np.random.uniform(low=0.5, high=2.0, size=2))
    population.append(ind)

xy_mean = np.full((ngen, 2), np.nan)#(np.empty((ngen, 2))).fill(np.nan)
f_min = np.full((ngen, 1), np.nan)
f_std = np.full((ngen, 1), np.nan)

for gen in range(ngen):

    children = recombination(population, CXPB)

    children = mutation(children, MUTPB)

    population = selection(population + children, number=popsize, elitism=True)

    # Extract fitness values for statistics

    fits = [ind.fitness for ind in population]
    genomes = np.asarray([ind.genome for ind in population])
    xy_mean[gen, :] = np.mean(genomes, axis=0)
    f_min[gen] = np.min(fits)
    f_std[gen] = np.std(fits)

    if f_min[gen] < 0.0001:
        break

fig, ax = plt.subplots(1, 1, figsize=(8, 6))
plt.contourf(X, Y, Z)
cb = plt.colorbar()
plt.xlabel('x')
plt.ylabel('y')

plt.plot([0.5, 2.0], [0.5, 0.5], 'k--')
plt.plot([0.5, 0.5], [0.5, 2.0], 'k--')

cb.set_label(r'$f(x, y)$')
plt.plot(xy_mean[:,0], xy_mean[:,1], 'k-');

```

```python

```