# **Save this file as studentid1_studentid2_lab#.ipynb**
(Your student-id is the number shown on your student card.)

E.g. if you work with 3 people, the notebook should be named:
12301230_3434343_1238938934_lab1.ipynb.

**This will be parsed by a regexp, so please double check your filename.**

Before you turn this problem in, please make sure everything runs correctly. First, **restart the kernel** (in the menubar, select Kernel$\rightarrow$Restart) and then **run all cells** (in the menubar, select Cell$\rightarrow$Run All). Note, that **you are not allowed to use Google Colab**.

**Make sure you fill in any place that says `YOUR CODE HERE` or "YOUR ANSWER HERE", as well as your names and email adresses below.**

```python
NAME = "Philipp Lintl"
NAME2 = "Anca Diana VIcol"
EMAIL = "lintl.philipp@gmail.com"
EMAIL2 = "ancadianavicol@gmail.com"

```

# Lab 3: Gaussian Processes and Support Vector Machines

### Machine Learning 1, November 2018

Notes on implementation:

* You should write your code and answers in this IPython Notebook: http://ipython.org/notebook.html. If you have problems, please contact your teaching assistant.
* Please write your answers right below the questions.
* Among the first lines of your notebook should be "%pylab inline". This imports all required modules, and your plots will appear inline.
* Refer to last week's lab notes, i.e. http://docs.scipy.org/doc/, if you are unsure about what function to use. There are different correct ways to implement each problem!
* use the provided test boxes to check if your answers are correct

```python
%pylab inline
plt.rcParams["figure.figsize"] = [20,10]
```

    Populating the interactive namespace from numpy and matplotlib

```python
# This cell makes sure that you have all the necessary libraries installed

import sys
import platform
from importlib.util import find_spec, module_from_spec

def check_newer_version(version_inst, version_nec):
    version_inst_split = version_inst.split('.')
    version_nec_split = version_nec.split('.')
    for i in range(min(len(version_inst_split), len(version_nec_split))):
        if int(version_nec_split[i]) > int(version_inst_split[i]):
            return False
        elif int(version_nec_split[i]) < int(version_inst_split[i]):
            return True
    return True

module_list = [('jupyter', '1.0.0'),
               ('matplotlib', '2.0.2'),
               ('numpy', '1.13.1'),
               ('python', '3.6.2'),
               ('sklearn', '0.19.0'),
               ('scipy', '0.19.1'),
               ('nb_conda', '2.2.1')]

packages_correct = True
packages_errors = []

for module_name, version in module_list:
    if module_name == 'scikit-learn':
        module_name = 'sklearn'
    if module_name == 'pyyaml':
        module_name = 'yaml'
    if 'python' in module_name:
        python_version = platform.python_version()
        if not check_newer_version(python_version, version):
            packages_correct = False
            error = f'Update {module_name} to version {version}. Current version is {python_version}.'
            packages_errors.append(error)
            print(error)
    else:
        spec = find_spec(module_name)
        if spec is None:
            packages_correct = False
            error = f'Install {module_name} with version {version} or newer, it is required for this assignment!'
            packages_errors.append(error)
            print(error)
        else:
            x = __import__(module_name)
            if hasattr(x, '__version__') and not check_newer_version(x.__version__, version):
                packages_correct = False
                error = f'Update {module_name} to version {version}. Current version is {x.__version__}.'
                packages_errors.append(error)
                print(error)

try:
    from google.colab import drive
    packages_correct = False
    error = """Please, don't use google colab!
It will make it much more complicated for us to check your homework as it merges all the cells into one."""
    packages_errors.append(error)
    print(error)
except:
    pass

packages_errors = '\n'.join(packages_errors)
```

## Part 1: Gaussian Processes

For part 1 we will be refer to Bishop sections 6.4.2 and 6.4.3. You may also want to refer to Rasmussen's Gaussian Process text which is available online at http://www.gaussianprocess.org/gpml/chapters/ and especially to the project found at https://www.automaticstatistician.com/index/ by Ghahramani for some intuition in GP.  To understand Gaussian processes, it is highly recommended understand how marginal, partitioned Gaussian distributions can be converted into conditional Gaussian distributions.  This is covered in Bishop 2.3 and summarized in Eqns 2.94-2.98.
$\newcommand{\bt}{\mathbf{t}}$
$\newcommand{\bx}{\mathbf{x}}$
$\newcommand{\by}{\mathbf{y}}$
$\newcommand{\bw}{\mathbf{w}}$
$\newcommand{\ba}{\mathbf{a}}$

### Periodic Data

We will use the same data generating function that we used previously for regression.

```python
def true_mean_function(x):
    return np.sin(2*pi*(x+1))

def add_noise(y, sigma):
    return y + sigma*np.random.randn(len(y))

def generate_t(x, sigma):
    return add_noise(true_mean_function(x), sigma)

```

```python
sigma = 0.2
beta  = 1.0 / pow(sigma, 2)
N_test = 100

x_test = np.linspace(-1, 1, N_test)
mu_test = np.zeros(N_test)
y_test = true_mean_function(x_test)
t_test = add_noise(y_test, sigma)

plt.plot( x_test, y_test, 'b-', lw=2)
plt.plot( x_test, t_test, 'go')
plt.show()
```

### 1. Sampling from the Gaussian process prior (30 points)
We will implement Gaussian process regression using the kernel function in Bishop Eqn. 6.63.

#### 1.1 Why kernel function? (5 points)

Before implementing kernel function, it would be useful to understand why kernels are crucial for high dimensional data. Why are kernels useful when you are dealing with high dimensional data?

- In GENERAL: Kernel methods allow for much more complex decision boundaries within the infinite dimensional space it is acting. Also, by projecting into even more high dimensional space, it becomes much more likely to actually represent the datapoints in a way, they are linearly separable, which is advantageous (Covers Theorem).

- For HIGH-DIM Data: Within the process, a Kernel martix K $\in \mathbb{R}^{N\times N}$  is constructed, whereas N is the number of training points. Compared to the number of dimensions for some very high dimensional data situation (Number of features D $>>N$), this will decrease training complexity. However, if the number of training instances N gets very large, the gains are limited.

#### 1.2 k_n_m( xn, xm, thetas ) (5 points)
To start, implement function `k_n_m(xn, xm, thetas)` that takes scalars $x_n$ and $x_m$, and a vector of $4$ thetas, and computes the kernel function Bishop Eqn. 6.63 (10 points).  NB: usually the kernel function will take $D$ by $1$ vectors, but since we are using a univariate problem, this makes things easier.

```python
def k_n_m(xn, xm, thetas):
    norm_x = (xn - xm)**2
    k = thetas[0]*np.exp(-(thetas[1]/2)*norm_x)+thetas[2]+thetas[3]*xn*xm
    return k
```

```python
# DO NOT REMOVE THIS CELL!
# It contains hidden tests

```

#### 1.3 computeK( X1, X2, thetas ) (10 points)
Eqn 6.60 is the marginal distribution of mean output of $N$ data vectors: $p(\mathbf{y}) = \mathcal{N}(0, \mathbf{K})$.  Notice that the expected mean function is $0$ at all locations, and that the covariance is a $N$ by $N$ kernel matrix $\mathbf{K}$.  Write a function `computeK(x1, x2, thetas)`
that computes the kernel matrix. Use k_n_m as part of an inner loop (of course, there are more efficient ways of computing the kernel function making better use of vectorization, but that is not necessary) (5 points).

```python
def computeK(x1, x2, thetas):

    K = [[k_n_m(i, j, thetas) for j in x2] for i in x1]
    K = np.array(K)
    return K
```

```python
### Test your function
x1 = [0, 1, 2]
x2 = [1, 2, 3, 4]
thetas = [1, 2, 3, 4]
K = computeK(x1, x2, thetas)

assert K.shape == (len(x1), len(x2)), "the shape of K is incorrect"

```

#### 1.4 Plot function samples (15 points)
Now sample mean functions at the x_test locations for the theta values in Bishop Figure 6.5, make a figure with a 2 by 3 subplot and make sure the title reflects the theta values (make sure everything is legible).  In other words, sample $\by_i \sim \mathcal{N}(0, \mathbf{K}_{\theta})$.  Make use of numpy.random.multivariate_normal().  On your plots include the expected value of $\by$ with a dashed line and fill_between 2 standard deviations of the uncertainty due to $\mathbf{K}$ (the diagonal of $\mathbf{K}$ is the variance of the model uncertainty) (15 points).

```python
# store all theta values in list of lists
all_thetas = [[1,4,0,0], [9,4,0,0], [1,64,0,0] , [1,0.25,0,0], [1,4,10,0], [1,4,0,5]]

def make_one_subplot(thetas):
    # number of x's
    N = 200
    # test x's
    x = np.linspace(-1, 1, N)
    # compute Kernel Matrix for x with itself
    K = computeK(x, x, thetas)
    # uncertainty of
    var = np.diagonal(K)

    for g in range(4):

        y = np.random.multivariate_normal(np.zeros(N), K)
        plt.plot(x, y)
        # expected y is 0
        avg = np.zeros(N)
        plt.fill_between(x,avg+np.sqrt(var), avg-np.sqrt(var), alpha=0.1)
        plt.plot(x, avg , linestyle = "--")

    plt.title('Thetas: ' +  str(thetas))

plt.figure(1, figsize = [20,10])
#fig, ax = plt.subplots(2, 3, figsize=[18,8])

random.seed(42)

for i in range(2):
    for j in range(3):
        ind = 231 + i*3 +j
        plt.subplot(ind)
        make_one_subplot(all_thetas[i * 3 +j])

plt.show()

```

### 2. Predictive distribution (35 points)
So far we have sampled mean functions from the prior.  We can draw actual data $\bt$ two ways.  The first way is generatively, by first sampling $\by | \mathbf{K}$, then sampling $\bt | \by, \beta$ (Eqns 6.60 followed by 6.59).  The second way is to integrate over $\by$ (the mean draw) and directly sample $\bt | \mathbf{K}, \beta$ using Eqn 6.61.    This is the generative process for $\bt$.  Note that we have not specified a distribution over inputs $\bx$;  this is because Gaussian processes are conditional models.  Because of this we are free to generate locations $\bx$ when playing around with the GP; obviously a dataset will give us input-output pairs.

Once we have data, we are interested in the predictive distribution (note: the prior is the predictive distribution when there is no data).  Consider the joint distribution for $N+1$ targets, given by Eqn 6.64.  Its covariance matrix is composed of block components $C_N$, $\mathbf{k}$, and $c$.  The covariance matrix $C_N$ for $\bt_N$ is $C_N = \mathbf{K}_N + \beta^{-1}\mathbf{I}_N$.  We have just made explicit the size $N$ of the matrix; $N$ is the number of training points.  The kernel vector $\mathbf{k}$ is a $N$ by $1$ vector of kernel function evaluations between the training input data and the test input vector.  The scalar $c$ is a kernel evaluation at the test input.

#### 2.1 gp_predictive_distribution(...) (10 points)
Write a function `gp_predictive_distribution(x_train, t_train, x_test, theta, beta, C=None)` that computes  Eqns 6.66 and 6.67, except allow for an arbitrary number of test points (not just one) and now the kernel matrix is for training data.  By having C as an optional parameter, we can avoid computing it more than once (for this problem it is unimportant, but for real problems this is an issue).  The function should compute $\mathbf{C}$, $\mathbf{k}$, and return the mean, variance and $\mathbf{C}$.  Do not forget: the computeK function computes $\mathbf{K}$, not $\mathbf{C}$.(10 points)

```python
# change it for to compute for more than one x
def compute_more_K(x_1, x_2, theta):
    K = []
    for x1 in x_1:
            K.append(compute_pair_K(x1,x_2, theta))

    print(np.array(K))
    return np.array(K)

def gp_predictive_distribution(x_train, t_train, x_test, theta, beta, C = None):

    K = computeK(x_train, x_train, theta)

    k = computeK(x_train, x_test, theta)

    k_transpose = np.transpose(k)

    N = len(x_train)

    if C is None:
        C = K + (1/beta) * np.identity(N)

    C_inverse = np.linalg.inv(C)

    k_test = computeK(x_test, x_test, theta)

    c = k_test + (1/beta)*np.identity(len(x_test))

    mean_test = k_transpose @ C_inverse @ t_train
    covar_test = c -  k_transpose @ C_inverse @ k

    return mean_test, covar_test, C

```

```python
### Test your function
N = 2
train_x = np.linspace(-1, 1, N)
train_t = 2*train_x
test_N = 3
test_x = np.linspace(-1, 1, test_N)
theta = [1, 2, 3, 4]
beta = 25
test_mean, test_covar, C = gp_predictive_distribution(train_x, train_t, test_x, theta, beta, C=None)

assert test_mean.shape == (test_N,), "the shape of mean is incorrect"
assert test_covar.shape == (test_N, test_N), "the shape of var is incorrect"
assert C.shape == (N, N), "the shape of C is incorrect"

C_in = np.array([[0.804, -0.098168436], [-0.098168436, 0.804]])
_, _, C_out = gp_predictive_distribution(train_x, train_t, test_x, theta, beta, C=C_in)

assert np.allclose(C_in, C_out), "C is not reused!"

```

#### 2.2 gp_log_likelihood(...) (10 points)
To learn the hyperparameters, we would need to compute the log-likelihood of the of the training data.  Implicitly, this is conditioned on the value setting for $\mathbf{\theta}$.  Write a function `gp_log_likelihood(x_train, t_train, theta, C=None, invC=None, beta=None)`, where C and invC can be stored and reused. It should return the log-likelihood, `C` and `invC`  (10 points)

```python
def gp_log_likelihood(x_train, t_train, theta, beta, C=None, invC=None):

    N = len(x_train)

    if C is None:
        K = computeK(x_train, x_train, theta)
        C = K + (1/beta) * np.identity(len(x_train))
    if invC is None:
        invC = np.linalg.inv(C)

    lnCn = np.log(np.linalg.det(C))
    tt_cn_t = np.transpose(t_train) @ invC @ t_train

    lp = -(1/2)* lnCn -(1/2) * tt_cn_t -(N/2) * np.log(2*math.pi)

    return lp, C, invC
```

```python
### Test your function
N = 2
train_x = np.linspace(-1, 1, N)
train_t = 2 * train_x
theta = [1, 2, 3, 4]
beta = 25
lp, C, invC = gp_log_likelihood(train_x, train_t, theta, beta, C=None, invC=None)

assert lp < 0, "the log-likelihood should be smaller than 0"
assert C.shape == (N, N), "the shape of var is incorrect"
assert invC.shape == (N, N), "the shape of C is incorrect"

C_in = np.array([[0.804, -0.098168436], [-0.098168436, 0.804]])
_, C_out, _ = gp_log_likelihood(train_x, train_t, theta, beta, C=C_in, invC=None)

assert np.allclose(C_in, C_out), "C is not reused!"

invC_in = np.array([[1.26260453, 0.15416407], [0.15416407, 1.26260453]])
_, _, invC_out = gp_log_likelihood(train_x, train_t, theta, beta, C=None, invC=invC_in)

assert np.allclose(invC_in, invC_out), "invC is not reused!"

```

#### 2.3 Plotting (10 points)
Repeat the 6 plots above, but this time conditioned on the training points.  Use the periodic data generator to create 2 training points where x is sampled uniformly between $-1$ and $1$.  For these plots, feel free to use the provided function "gp_plot".  Make sure you put the parameters in the title and this time also the log-likelihood. Try to understand the two types of uncertainty!  If you do not use `gp_plot(...)`, please add a fill between for the model and target noise. (10 points)

```python
def gp_plot( x_test, y_test, mean_test, covar_test, x_train, t_train, theta, beta ):
    # x_test:
    # y_test:    the true function at x_test
    # mean_test: predictive mean at x_test
    # var_test:  predictive covariance at x_test
    # t_train:   the training values
    # theta:     the kernel parameters
    # beta:      the precision (known)

    # the reason for the manipulation is to allow plots separating model and data stddevs.
    std_total = np.sqrt(np.diag(covar_test))       # includes all uncertainty, model and target noise
    std_model = np.sqrt(std_total**2 - 1.0/beta) # remove data noise to get model uncertainty in stddev
    std_combo = std_model + np.sqrt(1.0/beta)    # add stddev (note: not the same as full)

    plt.plot(x_test, y_test, 'b', lw=3)
    plt.plot(x_test, mean_test, 'k--', lw=2)
    plt.fill_between(x_test, mean_test+2*std_combo,mean_test-2*std_combo, color='k', alpha=0.25)
    plt.fill_between(x_test, mean_test+2*std_model,mean_test-2*std_model, color='r', alpha=0.25)
    plt.plot(x_train, t_train, 'ro', ms=10)

```

```python
sigma = 0.2
beta  = 1.0 / pow(sigma, 2)

def plot_N_train(N):

    x_train = np.random.uniform(-1, 1, N)

    y_train = true_mean_function(x_train)
    t_train = add_noise(y_train, sigma)

    N_test = 100
    x_test = np.linspace(-1, 1, N_test)
    y_test = true_mean_function(x_test)

    plt.figure(1, figsize = [20,10])

    for i in range(2):
        for j in range(3):
            ind = 231 + i*3 +j
            plt.subplot(ind)
            mean_test, covar_test, C = gp_predictive_distribution(x_train, t_train, x_test, all_thetas[i*3 + j], beta, C=None)
            gp_plot(x_test, y_test, mean_test, covar_test, x_train, t_train, all_thetas[i*3 + j], beta)
            lp, _, _ = gp_log_likelihood(x_train, t_train, all_thetas[i*3 +j], beta, C)
            title('Thetas: ' + str(all_thetas[i*3 + j]) + "  " + "log-lik: " + str(lp))

    plt.show()

random.seed(42)
plot_N_train(2)
```

#### 2.4 More plotting (5 points)
Repeat the 6 plots above, but this time conditioned a new set of 10 training points. (5 points)

```python
random.seed(42)
plot_N_train(10)
```

## Part 2: Support Vector Machines (45 points)
As seen in Part 1: Gaussian Processes, one of the significant limitations of many such algorithms is that the kernel function $k(\bx_n , \bx_m)$ must be evaluated for all possible pairs $\bx_n$ and $\bx_m$ of training points, which can be computationally infeasible during training and can lead to excessive computation times when making predictions for new data points.
In Part 2: Support Vector Machines, we shall look at kernel-based algorithms that have sparse solutions, so that predictions for new inputs depend only on the kernel function evaluated at a subset of the training data points. We are using the same notation as in Bishop chapter 7.

### 2.1 Generating a linearly separable dataset (15 points)
a) (5 points) First of all, we are going to create our own 2D toy dataset $X$. The dataset will consists of two i.i.d. subsets $X_1$ and $X_2$, each of the subsets will be sampled from a multivariate Gaussian distribution,

\begin{align}
X_1 \sim &\mathcal{N}(\mu_1, \Sigma_1)\\
&\text{ and }\\
X_2 \sim &\mathcal{N}(\mu_2, \Sigma_2).
\end{align}

In the following, $X_1$ will have $N_1=20$ samples and a mean $\mu_1=(1,1)$. $X_2$ will have $N_2=30$ samples and a mean $\mu_2=(3,3)$.

Plot the two subsets in one figure, choose two colors to indicate which sample belongs to which subset. In addition you should choose, $\Sigma_1$ and $\Sigma_2$ in a way that the two subsets become linearly separable. (Hint: Which form has the covariance matrix for a i.i.d. dataset?)

```python
def create_X_1_and_2():
    #np.random.seed(1234)
    N1 = 20
    N2 = 30
    mu1 = [1, 1]
    mu2 = [3, 3]
    var1 = 0.1
    var2 = 0.3
    sigma1 = [[var1, 0], [0, var1]]
    sigma2 = [[var2, 0], [0, var2]]

    X1 = np.random.multivariate_normal(mu1, sigma, (N1))
    X2 = np.random.multivariate_normal(mu2, sigma, (N2))

    return X1, X2

def plot_dataset(X, t):
    plt.scatter(X.T[0], X.T[1], c=t)

#(X1, X2) = create_X_1_and_2()
#plot_dataset(X1, numpy.ones(len(X1)))
#plot_dataset(X2, -1 * numpy.ones(len(X2)))
```

b) (10 points) In the next step we will combine the two datasets X_1, X_2 and generate a vector `t` containing the labels. Write a function `create_X_and_t(X1, X2)` it should return the combined data set X and the corresponding target vector t.

```python
def create_X_and_t(X1, X2):
    X = []
    t = []
    dataset = [X1, X2]
    indexes = [0, 0]
    while True:
        choice = np.round(np.random.randint(0, 2))
        if indexes[choice] < len(dataset[choice]):
            X += [dataset[choice][indexes[choice]]]
            t += [1 * choice - 1 * (1 - choice)]
            indexes[choice] += 1
        if indexes[0] >= len(X1) and indexes[1] >= len(X2):
            break

    return np.array(X), np.array(t)

(X1, X2) = create_X_1_and_2()
X, t = create_X_and_t(X1, X2)
plot_dataset(X, t)
```

```python
### Test your function
dim = 2
N1_test = 3
N2_test = 4
X1_test = np.arange(6).reshape((N1_test, dim))
X2_test = np.arange(8).reshape((N2_test, dim))
X_test, t_test = create_X_and_t(X1_test, X2_test)

assert X_test.shape == (N1_test + N2_test, dim), "the shape of X is incorrect"
assert t_test.shape == (N1_test + N2_test,), "the shape of t is incorrect"

```

### 2.2 Finding the support vectors (15 points)
Finally we going to use a SVM to obtain the decision boundary for which the margin is maximized. We have to solve the optimization problem

\begin{align}
\arg \min_{\bw, b} \frac{1}{2} \lVert \bw \rVert^2,
\end{align}

subject to the constraints

\begin{align}
t_n(\bw^T \phi(\bx_n) + b) \geq 1, n = 1,...,N.
\end{align}

In order to solve this constrained optimization problem, we introduce Lagrange multipliers $a_n \geq 0$. We obtain the dual
representation of the maximum margin problem in which we maximize

\begin{align}
\sum_{n=1}^N a_n - \frac{1}{2}\sum_{n=1}^N\sum_{m=1}^N a_n a_m t_n t_m k(\bx_n, \bx_m),
\end{align}

with respect to a subject to the constraints

\begin{align}
a_n &\geq 0, n=1,...,N,\\
\sum_{n=1}^N a_n t_n &= 0.
\end{align}

This takes the form of a quadratic programming problem in which we optimize a quadratic function of $\mathbf{a}$, subject to a set of inequality constraints.

a) (5 points) In this example we will use a linear kernel $k(\bx, \bx') = \bx^T\bx'$. Write a function `computeK(X)` that computes the kernel matrix $K$ for the 2D dataset $X$.

```python
def computeK(X):
    return X @ np.transpose(X)
```

```python
dim = 2
N_test = 3
X_test = np.arange(2, 8).reshape((N_test, dim))
K_test = computeK(X_test)

assert K_test.shape == (N_test, N_test)

```

Next, we will rewrite the dual representation so that we can make use of computationally efficient vector-matrix multiplication. The objective becomes

\begin{align}
\min_{\ba} \frac{1}{2} \ba^T K' \ba - 1^T\ba,
\end{align}

subject to the constraints

\begin{align}
a_n &\geq 0, n=1,...,N,\\
\bt^T\ba &= 0.
\end{align}

Where
\begin{align}
K'_{nm} = t_n t_m k(\bx_n, \bx_m),
\end{align}
and in the special case of a linear kernel function,
\begin{align}
K'_{nm} = t_n t_m k(\bx_n, \bx_m) = k(t_n \bx_n, t_m \bx_m).
\end{align}

To solve the quadratic programming problem we will use a python module called cvxopt. You first have to install the module in your virtual environment (you have to activate it first), using the following command:

`conda install -c anaconda cvxopt`

The quadratic programming solver can be called as

`cvxopt.solvers.qp(P, q[, G, h[, A, b[, solver[, initvals]]]])`

This solves the following problem,

\begin{align}
\min_{\bx} \frac{1}{2} \bx^T P \bx + q^T\bx,
\end{align}

subject to the constraints,

\begin{align}
G\bx &\leq h,\\
A\bx &= b.
\end{align}

All we need to do is to map our formulation to the cvxopt interface.

b) (10 points) Write a function `compute_multipliers(X, t)` that solves the quadratic programming problem using the cvxopt module and returns the lagrangian multiplier for every sample in the dataset.

```python
import cvxopt

def compute_multipliers(X, t):
    N = len(X)
    K = computeK(X)

    P = cvxopt.matrix(K, tc='d')
    q = cvxopt.matrix(np.ones((N, 1)), tc='d')
    G = cvxopt.matrix(-1 * np.identity(N), tc='d')
    h = cvxopt.matrix(np.zeros((N, 1)), tc='d')
    A =  cvxopt.matrix(t.reshape(1, N), tc='d')
    b =  cvxopt.matrix([[0]], tc='d')
    sol = cvxopt.solvers.qp(P, q, G, h, A, b)
    a = np.array(sol['x'])
    return a
```

```python
### Test your function
dim = 2
N_test = 3
X_test = np.arange(2, 8).reshape((N_test, dim))
t_test = np.array([-1., 1., 1.])
a_test = compute_multipliers(X_test, t_test)

assert a_test.shape == (N_test, 1)

```

         pcost       dcost       gap    pres   dres
     0: -1.2064e-01  1.4615e-01  4e+00  2e+00  1e+00
     1:  2.1251e-02 -1.1827e-03  2e-01  8e-02  4e-02
     2:  3.5039e-03 -1.5844e-04  4e-03  1e-17  4e-16
     3:  4.2438e-05 -2.7429e-08  4e-05  3e-19  3e-16
     4:  4.2415e-07 -2.7435e-12  4e-07  7e-21  0e+00
     5:  4.2415e-09 -2.7435e-16  4e-09  6e-23  2e-16
    Optimal solution found.

### 2.3 Plot support vectors (5 points)
Now that we have obtained the lagrangian multipliers $\ba$, we use them to find our support vectors. Repeat the plot from 2.1, this time use a third color to indicate which samples are the support vectors.

```python
(X1, X2) = create_X_1_and_2()
X, t = create_X_and_t(X1, X2)

a = compute_multipliers(X, t)
print(a)
a_aux = np.array([i if i == 0 else 1 for i in a])
print(a_aux.shape)
print(t.shape)
print(np.multiply(t, a_aux))
plot_dataset(X, np.multiply(t, a_aux))

```

### 2.4 Plot the decision boundary (10 Points)
The decision boundary is fully specified by a (usually very small) subset of training samples, the support vectors. Make use of

\begin{align}
\bw &= \sum_{n=1}^N a_n t_n \mathbf{\phi}(\bx_n)\\
b &= \frac{1}{N_S}\sum_{n \in S} (t_n - \sum_{m \in S} a_m t_m k(\bx_n, \bx_m)),
\end{align}

where $S$ denotes the set of indices of the support vectors, to calculate the slope and intercept of the decision boundary. Generate a last plot that contains the two subsets, support vectors and decision boundary.

```python
# YOUR CODE HERE
raise NotImplementedError()

```


##### Copyright 2020 The OpenFermion Developers

```python
#@title 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
#
# https://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.
```

# Circuits 1: Compiling arbitrary single-particle basis rotations in linear depth

    View on QuantumLib

    Run in Google Colab

    View source on GitHub

    Download notebook

This is the first of several tutorials demonstrating the compilation of quantum circuits. These tutorials build on one another and should be studied in order. In this tutorial we will discuss the compilation of circuits for implementing arbitrary rotations of the single-particle basis of an electronic structure simulation. As an example, we show how one can use these methods to simulate the evolution of an arbitrary non-interacting fermion model.

## Setup

Install the OpenFermion package:

```python
try:
    import openfermion
except ImportError:
    !pip install git+https://github.com/quantumlib/OpenFermion.git@master#egg=openfermion
```

## Background

### Second quantized fermionic operators

In order to represent fermionic systems on a quantum computer one must first discretize space. Usually, one expands the many-body wavefunction in a basis of spin-orbitals $\varphi_p = \varphi_p(r)$ which are single-particle basis functions. For reasons of spatial efficiency, all NISQ (and even most error-corrected) algorithms for simulating fermionic systems focus on representing operators in second-quantization. Second-quantized operators are expressed using the fermionic creation and annihilation operators, $a^\dagger_p$ and $a_p$. The action of $a^\dagger_p$ is to excite a fermion in spin-orbital $\varphi_p$ and the action of $a_p$ is to annihilate a fermion from spin-orbital $\varphi_p$. Specifically, if electron $i$ is represented in a space of spin-orbitals $\{\varphi_p(r_i)\}$ then $a^\dagger_p$ and $a_p$ are related to Slater determinants through the equivalence,
$$
\langle r_0 \cdots r_{\eta-1} | a^\dagger_{0} \cdots a^\dagger_{\eta-1} | \varnothing\rangle \equiv \sqrt{\frac{1}{\eta!}}
\begin{vmatrix}
\varphi_{0}\left(r_0\right) & \varphi_{1}\left( r_0\right) & \cdots & \varphi_{\eta-1} \left( r_0\right) \\
\varphi_{0}\left(r_1\right) & \varphi_{1}\left( r_1\right) & \cdots & \varphi_{\eta-1} \left( r_1\right) \\
\vdots & \vdots & \ddots & \vdots\\
\varphi_{0}\left(r_{\eta-1}\right) & \varphi_{1}\left(r_{\eta-1}\right) & \cdots & \varphi_{\eta-1} \left(r_{\eta-1}\right) \end{vmatrix}
$$
where $\eta$ is the number of electrons in the system, $|\varnothing \rangle$ is the Fermi vacuum and $\varphi_p(r)=\langle r|\varphi_p \rangle$ are the single-particle orbitals that define the basis. By using a basis of Slater determinants, we ensure antisymmetry in the encoded state.

### Rotations of the single-particle basis

Very often in electronic structure calculations one would like to rotate the single-particle basis. That is, one would like to generate new orbitals that are formed from a linear combination of the old orbitals. Any particle-conserving rotation of the single-particle basis can be expressed as
$$
\tilde{\varphi}_p = \sum_{q} \varphi_q u_{pq}
\quad
\tilde{a}^\dagger_p = \sum_{q} a^\dagger_q u_{pq}
\quad
\tilde{a}_p = \sum_{q} a_q u_{pq}^*
$$
where $\tilde{\varphi}_p$, $\tilde{a}^\dagger_p$, and $\tilde{a}^\dagger_p$ correspond to spin-orbitals and operators in the rotated basis and $u$ is an $N\times N$ unitary matrix. From the Thouless theorem, this single-particle rotation
is equivalent to applying the $2^N \times 2^N$ operator
$$
  U(u) = \exp\left(\sum_{pq} \left[\log u \right]_{pq} \left(a^\dagger_p a_q - a^\dagger_q a_p\right)\right)
$$
where $\left[\log u\right]_{pq}$ is the $(p, q)$ element of the matrix $\log u$.

There are many reasons that one might be interested in performing such basis rotations. For instance, one might be interested in preparing the Hartree-Fock (mean-field) state of a chemical system, by rotating from some initial orbitals (e.g. atomic orbitals or plane waves) into the molecular orbitals of the system. Alternatively, one might be interested in rotating from a basis where certain operators are diagonal (e.g. the kinetic operator is diagonal in the plane wave basis) to a basis where certain other operators are diagonal (e.g. the Coulomb operator is diagonal in the position basis). Thus, it is a very useful thing to be able to apply circuits corresponding to $U(u)$ on a quantum computer in low depth.

### Compiling linear depth circuits to rotate the orbital basis

OpenFermion prominently features routines for implementing the linear depth / linear connectivity basis transformations described in [Phys. Rev. Lett. 120, 110501](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.120.110501). While we will not discuss this functionality here, we also support routines for compiling the more general form of these transformations which do not conserve particle-number, known as a Bogoliubov transformation, using routines described in [Phys. Rev. Applied 9, 044036](https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.9.044036). We will not discuss the details of how these methods are implemented here and instead refer readers to those papers. All that one needs in order to compile the circuit $U(u)$ using OpenFermion is the $N \times N$ matrix $u$, which we refer to in documentation as the "basis_transformation_matrix". Note that if one intends to apply this matrix to a computational basis state with only $\eta$ electrons, then one can reduce the number of gates required by instead supplying the $\eta \times N$ rectangular matrix that characterizes the rotation of the occupied orbitals only. OpenFermion will automatically take advantage of this symmetry.

## OpenFermion example implementation: exact evolution under tight binding models
In this example will show how basis transforms can be used to implement exact evolution under a random Hermitian one-body fermionic operator
\begin{equation}
H = \sum_{pq} T_{pq} a^\dagger_p a_q.
\end{equation}
That is, we will compile a circuit to implement $e^{-i H t}$ for some time $t$. Of course, this is a tractable problem classically but we discuss it here since it is often useful as a subroutine for more complex quantum simulations. To accomplish this evolution, we will use basis transformations. Suppose that $u$ is the basis transformation matrix that diagonalizes $T$. Then, we could implement $e^{-i H t}$ by implementing $U(u)^\dagger (\prod_{k} e^{-i \lambda_k Z_k}) U(u)$ where $\lambda_k$ are the eigenvalues of $T$.

Below, we initialize the T matrix characterizing $H$ and then obtain the eigenvalues $\lambda_k$ and eigenvectors $u_k$ of $T$. We print out the OpenFermion FermionOperator representation of $T$.

```python
import openfermion
import numpy

# Set the number of qubits in our example.
n_qubits = 3
simulation_time = 1.
random_seed = 8317

# Generate the random one-body operator.
T = openfermion.random_hermitian_matrix(n_qubits, seed=random_seed)

# Diagonalize T and obtain basis transformation matrix (aka "u").
eigenvalues, eigenvectors = numpy.linalg.eigh(T)
basis_transformation_matrix = eigenvectors.transpose()

# Print out familiar OpenFermion "FermionOperator" form of H.
H = openfermion.FermionOperator()
for p in range(n_qubits):
    for q in range(n_qubits):
        term = ((p, 1), (q, 0))
        H += openfermion.FermionOperator(term, T[p, q])
print(H)
```

Now we're ready to make a circuit! First we will use OpenFermion to generate the basis transform $U(u)$ from the basis transformation matrix $u$ by calling the Bogoliubov transform function (named as such because this function can also handle non-particle conserving basis transformations). Then, we'll apply local $Z$ rotations to phase by the eigenvalues, then we'll apply the inverse transformation. That will finish the circuit. We're just going to print out the first rotation to keep things easy-to-read, but feel free to play around with the notebook.

```python
import openfermion
import cirq

# Initialize the qubit register.
qubits = cirq.LineQubit.range(n_qubits)

# Start circuit with the inverse basis rotation, print out this step.
inverse_basis_rotation = cirq.inverse(openfermion.bogoliubov_transform(qubits, basis_transformation_matrix))
circuit = cirq.Circuit(inverse_basis_rotation)
print(circuit)

# Add diagonal phase rotations to circuit.
for k, eigenvalue in enumerate(eigenvalues):
    phase = -eigenvalue * simulation_time
    circuit.append(cirq.rz(rads=phase).on(qubits[k]))

# Finally, restore basis.
basis_rotation = openfermion.bogoliubov_transform(qubits, basis_transformation_matrix)
circuit.append(basis_rotation)
```

Finally, we can check whether our circuit applied to a random initial state with the exact result. Print out the fidelity with the exact result.

```python
# Initialize a random initial state.
initial_state = openfermion.haar_random_vector(
    2 ** n_qubits, random_seed).astype(numpy.complex64)

# Numerically compute the correct circuit output.
import scipy
hamiltonian_sparse = openfermion.get_sparse_operator(H)
exact_state = scipy.sparse.linalg.expm_multiply(
    -1j * simulation_time * hamiltonian_sparse, initial_state)

# Use Cirq simulator to apply circuit.
simulator = cirq.Simulator()
result = simulator.simulate(circuit, qubit_order=qubits,
                            initial_state=initial_state)
simulated_state = result.final_state_vector

# Print final fidelity.
fidelity = abs(numpy.dot(simulated_state, numpy.conjugate(exact_state)))**2
print(fidelity)
```

Thus, we see that the circuit correctly effects the intended evolution. We can now use Cirq's compiler to output the circuit using gates native to near-term devices, and then optimize those circuits. We'll output in QASM 2.0 just to demonstrate that functionality.

```python
xmon_circuit = cirq.google.optimized_for_xmon(circuit)
print(xmon_circuit.to_qasm())
```


```python
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: #888;
    color: #f6f6f6;
}

div #notebook { /* centre the content */
    background: #fff;
    color: #333;
    width: 1200px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

div.cell { /* set cell width to about 80 chars */
    width: 1000px;
}

div.cell.border-box-sizing.code_cell.running {
    /* draw border around running cells */
    border: 3px solid #111;
}

h1 {
    font-family: 'Philosopher', sans-serif;
}
h2 {
    font-family: 'Philosopher', serif;
}
h3{
    font-family: 'Philosopher', serif;
    font-style: 'italic';
    margin-top:12px;
    margin-bottom: 3px;
}
h4{
    font-family: 'Philosopher', serif;
}
h5 {
    font-family: 'Alegreya Sans', sans-serif;
}
h6 {
    font-family: 'PT Mono', sans-serif;
}

div.text_cell_render{
    font-family: 'Arvo' sans-serif;
    line-height: 130%;
    font-size: 115%;
    width:900px;
    margin-left:auto;
    margin-right:auto;
}

.CodeMirror{
        font-family: "PT Mono";
                    font-size: 100%;
            }

.text_cell_render h1 {
    font-weight: 400;
    font-size: 64pt;
    line-height: 100%;
    color: rgb(12,85,97);
    margin-bottom: 0.1em;
    margin-top: 0.1em;
    display: block;
}
.text_cell_render h2 {
    font-weight: 700;
    font-size: 24pt;
    line-height: 100%;
    color: rgb(171,165,131);
    margin-bottom: 0.1em;
    margin-top: 0.1em;
    display: block;
}

.text_cell_render h3 {
    font-style: italic;
    color: rgb(95,92,72);
}

.text_cell_render h5 {
    font-weight: 300;
    font-size: 16pt;
    color: grey;
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.1em;
    display: block;
}

.text_cell_render h6 {
    font-weight: 300;
    font-size: 10pt;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}
div.cell.code_cell {  /* Group area containing both code and output */
background-color: rgba(171,165,131,0.3);
border-radius: 10px; /* rounded borders */
padding: 1em;
}

</style>

# Theory results

There are a number of key theory results that we need when solving the Riemann problem, written as
$$
  \partial_t {\bf q} + \partial_x {\bf f}({\bf q}) = {\bf 0}, \qquad {\bf q}(x, t=0) = \begin{cases} {\bf q}_l & x < 0, \\ {\bf q}_r & x > 0, \end{cases}
$$
where the left and right states ${\bf q}_{l, r}$ are constant.

## Self similarity

We need to show that the solution is *self-similar*, which in this case means the solution can be written as a function of one variable, ${\bf q} \equiv {\bf q}(\xi)$ where $\xi = x/t$.

First, it is obvious that the equation defining the conservation law,
$$
  \partial_t {\bf q} + \partial_x {\bf f}({\bf q}) = {\bf 0},
$$
is *scale-free*. That is, if we change coordinates by any constant scaling to $t' = a t$ and $x' =  a x$ where $a > 0$, the conservation law is unchanged in form,
$$
  \partial_{t'} {\bf q} + \partial_{x'} {\bf f}({\bf q}) = {\bf 0}.
$$

Second, we need the initial data to also be scale free. For the Riemann problem this is clearly true: applying the same scaling to the initial data leaves it unchanged.

Combining these results we see that the solution must be scale free so that if $\xi = x / t$ is constant (so that $x$ and $t$ are scaled in the same way), the solution must be constant. So ${\bf q} \equiv {\bf q}(\xi)$.

We will often think of $\xi$ as a *spatial* coordinate, by considering the result at a fixed time $t = T$, say.

## Differentiable solutions

For sections of the solution that are differentiable, we can take two steps. First we can write the conservation law in the quasilinear form
$$
  \partial_t {\bf q} + \frac{\partial {\bf f}}{\partial {\bf q}} \partial_x {\bf q} = {\bf 0}.
$$
Second we can use the self-similarity to replace the partial derivatives, using
$$
\begin{align}
  \partial_t & = \frac{\partial \xi}{\partial t} \partial_{\xi} & \partial_x & = \frac{\partial \xi}{\partial x} \partial_{\xi} \\
  & = -\frac{\xi}{t} \partial_{\xi} & & = \frac{1}{t} \partial_{\xi},
\end{align}
$$
to get
$$
  \frac{1}{t} \left( \frac{\partial {\bf f}}{\partial {\bf q}} - \xi \textrm{Id} \right) \partial_{\xi} {\bf q} = {\bf 0}.
$$

For $t > 0$ there are two possible cases:

1. $\partial_{\xi} {\bf q} = {\bf 0}$: the state is *constant*.
2. $\xi$ is an eigenvalue of the Jacobian matrix $\partial {\bf f} / \partial {\bf q}$, and $\partial_{\xi} {\bf q}$ is the eigenvector corresponding to this eigenvalue.

We note two consequences from this. First, it only makes sense if the eigenvalues of the Jacobian are real. This is one of the requirements of the system being *hyperbolic*: the eigenvalues must be real and the eigenvectors independent. Second, if we denote the eigenvalue by $\lambda$ and its associated eigenvector by ${\bf r}$, then we have
$$
\begin{align}
  && \lambda({\bf q}) & = \xi \\
  \implies && \frac{\partial \lambda}{\partial {\bf q}} \frac{\partial {\bf q}}{\partial \xi} & = 1,
\end{align}
$$
by differentiating with respect to $\xi$. Then, as $\partial_{\xi} {\bf q}$ is, as noted above, an eigenvector, we write
$$
  \frac{\partial {\bf q}}{\partial \xi} = \alpha {\bf r},
$$
and combining the two results determines the proportionality constant $\alpha$ as
$$
  \alpha = \left[ {\bf r} \cdot \frac{\partial \lambda}{\partial {\bf q}} \right]^{-1}
$$
Therefore we have the *ordinary* differential equation for ${\bf q}$
$$
  \frac{\partial {\bf q}}{\partial \xi} = \frac{{\bf r}}{{\bf r} \cdot \frac{\partial \lambda}{\partial {\bf q}}}.
$$
This gives a continuous solution for ${\bf q}$, *provided* that ${\bf r} \cdot \frac{\partial \lambda}{\partial {\bf q}} \ne 0$. This is the requirement that the solution across this wave is *genuinely nonlinear*.

A final consequence of this section is the *number* of waves. If the system has size $N$ then there are $N$ eigenvalues, and hence $N$ waves. These will separate constant states, so we expect the solution to consist of $N+1$ constant states separated by $N$ waves.

## Discontinuous solutions

There remains only one possibility: the solution is not a continuous function of the similarity coordinate $\xi$. That such solutions may exist is no surprise, as it is true of the initial data.

When the solution is discontinuous we must use the weak form
$$
  \frac{\textrm{d}}{\textrm{d}t} \int_{x_l}^{x_r} {\bf q} \, \textrm{d}x = {\bf f}({\bf q}(x_l)) - {\bf f}({\bf q}(x_r)).
$$

Let us assume that the solution is discontinuous along the path $X(t)$. Consider the volume close to this discontinuity, so that $x_r$ and $x_l$ remain fixed points, but $x_r - x_l = \epsilon \ll 1$. We then split the integrals at the shock, as the data jumps there. In the weak form this gives
$$
\begin{align}
  && \frac{\textrm{d}}{\textrm{d}t} \int_{x_l}^{X(t)} {\bf q} \, \textrm{d}x + \frac{\textrm{d}}{\textrm{d}t} \int_{X(t)}^{x_r} {\bf q} \, \textrm{d}x & = {\bf f}({\bf q}(x_l)) - {\bf f}({\bf q}(x_r)) \\
  \implies && \frac{\textrm{d}X}{\textrm{d}t} \left\{ {\bf q} \left( X(t)_{-}, t \right) - {\bf q} \left( X(t)_{+}, t \right) \right\} + \int_{x_l}^{x_r} \frac{\partial {\bf q}}{\partial t} \, \textrm{d}x & = {\bf f}({\bf q}(x_l)) - {\bf f}({\bf q}(x_r)).
\end{align}
$$
Taking the limit $\epsilon \to 0$ the integral vanishes (as the width of the integral is $\epsilon$) and we have $x_l \to X(t)_{-}$ and $x_r \to X(t)_{+}$. We are left with the *Rankine-Hugoniot conditions*
$$
  \frac{\textrm{d}X}{\textrm{d}t} \left[ {\bf q} \right] =  \left[ {\bf f}({\bf q}) \right].
$$
Here the notation $\left[ {\bf u} \right]$ means the jump in the quantity ${\bf u}$ across the discontinuity.

As the solution must be self-similar, the shock speed must be constant, $\textrm{d}X / \textrm{d}t = V_s$, giving the standard form
$$
  V_s \left[ {\bf q} \right] =  \left[ {\bf f}({\bf q}) \right].
$$

## Summary

1. The solution to the Riemann problem is self-similar.
2. The solution contains $N+1$ constant states separated by $N$ waves.
3. The wave may be continuous, in which case it solves an ordinary differential equation determined by eigenvalues and eigenvectors of the Jacobian matrix.
4. The wave may be discontinuous, in which case it satisfies the Rankine-Hugoniot equations.

This is not the most general solution covering all cases, but is sufficient for most cases of interest.