$$\newcommand{\ket}[1]{\left|{#1}\right\rangle}$$
$$\newcommand{\bra}[1]{\left\langle{#1}\right|}$$

## Tehtävä 1

Kvanttitietokoneen muistissa tieto esitetään kubittien avulla. Kertauksena viime kierrokselta, kubitin perustilat ovat $\ket 0$ ja $\ket{1}$, ja ne esitetään pystyvektoreina

$$
\begin{align}
\ket 0 &= \pmatrix {1 \\ 0}, \\
\ket 1 &= \pmatrix {0 \\ 1}.
\end{align}
$$

NOT-portin matriisiesitys on seuraava:

$$
X=\pmatrix {0 & 1 \\ 1 & 0}
$$

Kierroksella 1 laskimme, että kun $X$-portti operoi kubittiin, niin kubitin tila muuttuu käänteiseksi (bit-flip gate), eli

$$
X\ket 0 = \ket 1 \\
X\ket 1 = \ket 0
$$

Voit palauttaa a- ja b-kohdat Classroomiin haluamassasi muodossa.

a) Osoita matriisiesitystä käyttäen, että

$$
\begin{align}
XX\ket 0 &= \ket 0 \text{ja} \\
XX\ket 1 &= \ket 1
\end{align}
$$

ohjevideo NOT-portin toiminnasta

b) Osoita laskemalla matriisien tulo, että $XX=I$, missä $I$ on yksikkömatriisi. Tämä tarkoittaa, että $X$ on itsensä käänteismatriisi.

c) Seuraavassa esimerkissä luodaan Qiskitiä käyttäen kvanttipiiri, jossa on yksi kubitti, jota operoidaan $X$-portilla.

```python
# Otetaan qiskit-kirjasto käyttöön
from qiskit import *

# Luodaan kvanttipiiri muuttujaan circ, jossa on yksi kubitti (diagrammissa q).
circ1 = QuantumCircuit(1)

# Operoidaan kubittia 0, eli piirin ainoaa kubittia X-portilla.
circ1.x(0)

# Piirretään luotu piiri.
# Parametri output="mpl" kertoo, että piirto tapahtuu graafisesti.
circ1.draw(output="mpl")
```

Kirjoita alle yllä olevan esimerkin mukaisesti Python-koodi, joka luo kvanttipiirin, jossa on kaksi kubittia. Sen jälkeen lisää piiriin portteja siten, että ensimmäiseen kubittiin tehdään kaksi $X$-operaatiota, ja toiseen yksi $X$-operaatio. Lopuksi piirrä luomasi piiri.

```python
# Kirjoita koodisi tähän

```

## Tehtävä 2

Hadamard eli $H$-portti muuttaa kubitin tilat $\ket 0$ ja $\ket 1$ superpositioon seuraavasti:

$$
H\ket 0 = \frac{1}{\sqrt 2} (\ket 0 +  \ket 1) = \ket +   \\
H\ket 1 = \frac{1}{\sqrt 2} (\ket 0 -  \ket 1) = \ket -
$$

Huomaa, että näitä kyseisiä tiloja merkitään välillä myös $\ket +$ ja $\ket -$.

$H$-portin matriisiesitys on seuraava:

$$
H = \frac{1}{\sqrt 2} \pmatrix {1 & 1 \\ 1 & -1}
$$

a) Osoita matriisiesitystä käyttäen, että

$$
\begin{align}
HH\ket 0 &= \ket 0 \text{ja} \\
HH\ket 1 &= \ket 1.
\end{align}
$$

b) Osoita laskemalla matriisien tulo, että $HH=I$, missä $I$ on yksikkömatriisi. Tämä tarkoittaa jälleen, että $H$ on itsensä käänteismatriisi.

ohjevideo H-portin toiminnasta

c) Seuraavassa esimerkissä luodaan Qiskitiä käyttäen kvanttipiiri, jossa on yksi kubitti ja $H$-portti.

```python
from qiskit import *

# Luodaan yhden kubitin kvanttipiiri kuten edellisessä esimerkissä.
circ2 = QuantumCircuit(1)

# Operoidaan kubittia 0 H-portilla.
circ2.h(0)

# Piirretään luotu piiri.
circ2.draw(output="mpl")
```

Kirjoita alle yllä olevan esimerkin mukaisesti Python-koodi, joka luo kvanttipiirin, jossa on kaksi kubittia. Sen jälkeen lisää piiriin portteja siten, että ensimmäiseen kubittiin tehdään yksi $H$-operaatio, ja toiseen yksi $X$-operaatio. Piirrä myös tämä piiri.

```python
# Kirjoita koodisi tähän

```

## Tehtävä 3
Kubittien tilat $\ket +$ ja $\ket -$ määritettiin
$$
 \ket + = \frac{1}{\sqrt 2} (\ket 0 +  \ket 1), \\
 \ket - = \frac{1}{\sqrt 2} (\ket 0 -  \ket 1).
$$

Z-portin (ns. phase-flip gate) matriisiesitys on seuraava:

$$
Z=  \pmatrix {1 & 0 \\ 0 & -1}
$$

Osoita seuraavat yhtälöt oikeiksi laskemalla matriisien tulo:

$$
\begin{align}
Z\ket + &= \ket - \text{ja}\\
Z\ket - &= \ket +
\end{align}
$$

## Tehtävä 4

Tehtävät 1 - 3 käsittelivät yhden kubitin systeemiä. Seuraavassa kahdessa tehtävässä tarkastelemme controlled NOT -porttia ($\mathit{CNOT}$), joka on kahden kubitin välinen operaatio. Kahden kubitin kvanttipiirissä systeemin tila esitetään seuraavilla kantavektoreilla:

$$
\ket{00},\ \ket{01},\ \ket{10},\ \ket{11},
$$

missä

$$
\ket{00}=\pmatrix{1 \\ 0 \\ 0 \\0},\ \ket{01}=\pmatrix{0 \\ 1 \\ 0 \\0},\
\ket{10}=\pmatrix{0 \\ 0 \\ 1 \\0},\ \ket{11}=\pmatrix{0 \\ 0 \\ 0 \\1}.
$$

$\mathit{CNOT}$-portin operoidessa ensimmäinen qubitti on ns. ohjaava kubitti (control qubi1t), ja jälkimmäinen on kohdekubitti (target qubit). $\mathit{CNOT}$ vaikuttaa koko systeemin tilaan seuraavasti:

$$
\ket{00} \rightarrow  \ket{00}   \\
\ket{01} \rightarrow  \ket{01}   \\
\ket{10} \rightarrow  \ket{11}   \\
\ket{11} \rightarrow  \ket{10}   \\
$$

Tulos tarkoittaa, että jos ensimmäinen, ns. ohjaava kubitti on $0$, niin kohdekubitin tila ei muutu. Jos ohjaavakubitti $1$, niin kohdekubitin tila muuttuu.

$\mathit{CNOT}$-portin matriisiesitys on seuraava:

$$
\mathit{CNOT} = \pmatrix {1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0}
$$

Huomaa, että vaikka matriisia merkitään monella kirjaimella, se ei ole monen matriisin tulo.

Piirrosmerkki, jossa ylempänä esitetty kubitti on ohjaava, ja alempi toimii kohteena:

a) Laske CNOT-matriisin ja 2 kubitin tilaa kuvaavan pystyvektorin tulo, ja osoita että

$$
\ket{01} \rightarrow  \ket{01} \text{eli laske tulo  } \mathit{CNOT} \ket{01}  \\
\ket{10} \rightarrow  \ket{11}  \text{eli laske tulo  } \mathit{CNOT} \ket{10} \\
$$

ohjevideo CNOT-portin toiminnasta

b) Alla on esimerkki, jossa Qiskitillä luodaan kvanttipiiriin kaksi kubittia. Ensimmäistä kubittia operoi ensin Hadamard-portti $H$, jonka jälkeen tehdään molempien kubittien välinen $\mathit{CNOT}$-operaatio.

```python
from qiskit import *
circ3 = QuantumCircuit(2)

circ3.h(0)
circ3.cx(0,1)

# Piirretään luotu piiri.
circ3.draw(output="mpl")
```

Huomaa, että kuvassa aika eli suoritusjärjestys etenee vasemmalta oikealle.

Kirjoita alle koodi, joka luo kolmen kubitin kvanttipiirin.
Ensimmäiseen kubittiin operoi H-portti.
Tämän jälkeen kvanttipiirissä on $\mathit{CNOT}$-portti kubittien 0 ja 1 välillä siten, että kubitti 0 on ohjaava kubitti ja kubitti 1 kohde. Lisää piiriin $\mathit{CNOT}$-portti myös kubittien 1 ja 2 välille, jossa kubitti 1 on ohjaava.

```python
# Kirjoita koodisi tähän

```

## Tehtävä 5

Seuraavassa esimerkissä kvanttipiiriä täydennetään kahdella kvanttirekisterillä ja kahdella klassisella rekisterillä. Kvanttipiiri luodaan syöttämällä rekisterit parametrina komennolle QuantumCircuit(). Kun H-portti operoi systeemiin, niin systeemin, niin kubitin 0 tila on määritetty tilojen $\ket 0$ ja $\ket 1$ superpositiona. Kun piirin tila mitataan, niin silloin systeemi siirtyy tiettyyn hyvin määritettyyn tilaan, jonka arvo tallenetaan klassiseen rekisteriin.

Kvanttimekaanisella systeemillä jokaisella mahdollisella mitatulla tilalla on tietty todennäköisyys, ja kaikkien eri tilavaihtoehtojen todennäköisyyksien summa on 1.

```python
from qiskit import *
# Luodaan 2 kubitin rekisteri piiriä varten.
qr = QuantumRegister(2)

# Luodaan kahden bitin klassinen rekisteri mittaustulosten tallentamista varten.
cr = ClassicalRegister(2)

# Luodaan nyt kvanttipiiri käyttäen juuri luotuja rekistereitä.
mycircuit = QuantumCircuit(qr, cr)

# H-portti operoi kubittiin 0Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads

mycircuit.h(qr[0])

# Määrätään piiri mittaamaan molemmat kubitit ja tallennetaan tulokset klassiseen rekisteriin.
mycircuit.measure(qr, cr)

mycircuit.draw(output='mpl')
```

a) Kerro alla olevan kvanttipiirin toimintaperiate. Miten piiri toimii?

Lisämateriaalia: Measuring a qubit?

Kirjoita vastauksesi tähän.

b) Kokeile määritellä piiri Python-kooodilla.

```python
# Kirjoita koodisi tähän

```

## Tehtävä 6

Pelkät vastaukset riittävät.

a) Mikä on kompleksiluvun $-2+3i$ reaaliosa ja imaginääriosa?

Olkoot  $z_1=2+3i$ ja $z_2=5-4i$ kompleksilukuja. Laske

b) summa $z_1 +z_2$ ja

c) tulo $z_1 z_2$.

d) ratkaise yhtälö $x^2 = -1$ kompleksilukujen joukossa.

e) Mikä on luvun $z=3+2i$ kompleksikonjukaatti eli liittoluku $\bar{z}$?

f) Määritä luvun $z=-3+4i$ moduli eli itseisarvo $\vert z\vert = \sqrt{z\bar{z}}$.

g) Ratkaise seuraavien polaarimuotoisten kompleksilukujen reaali- ja imaginaariosat:
$$
\begin{gather}
e^{i \pi} \\
e^{i {3\pi}/2} \\
\sqrt 2 e^{i \pi/4}
\end{gather}
$$

Esim. $e^{i \pi/2} = \cos \frac \pi 2 + i \sin \frac \pi 2 = 0 + 1i$. Tässä tapauksessa kulma oli $\frac \pi 2  = 90^{\circ}.$

Kompleksilukujen laskutoimitukset

Kirjoita vastauksesi tähän.

a)

b)

c)

d)
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads

e)

f)

g)

e^{i pi} =

e^{i 3pi/4} =

sqrt(2) e^{i pi/4} =

## Tehtävä 7 (lisätehtävä, ei pakollinen)

Lataa Classroomista Skrolli-lehden pdf-tiedosto, ja lue sieltä artikkeli "Kvantit horisontissa".

Kerro lyhyesti kvanttialgoritmin suorituksen keskeisistä vaiheista (s. 7-8).

Mitä tarkoittaa kvanttiherruus? Selitä lyhyesti.

Kirjoita vastauksesi tähän.


# Traveling salesman

This problem involves minimizing the sum of edge weights $W_{u\nu}$ between nodes $u$ and $\nu$ in a graph $G=(V,E)$ under the constraint that the sum over edges is restricted to a set of $E$ which are connected by vertices $V$, where each vertex is used exactly once in joining the edges.

Let $\nu$ represent the vertex and $i$ represent its order in a prospective cycle, the $x_{\nu,i}$ are binary variables representing the possible states of the system: one if the vertex $\nu$ is the $i$-th order in the cycle and zero otherwise. The Hamiltonian has four terms:
1. The $x_{\nu,i}$ one-hot encode the order in which vertices appear. A penalisation term appears if not exactly one vertex is active at each stage. I.e. the salesman cannot be at multiple vertices at the same time: `simultaneous_constraint`.
1. A penalisation term appears if the salesman doesn't arrive at a vertex exactly once: `visits_constraint`.
1. A penalisation term if an edge is included which doesn't appear in the graph $G$.
1. The distance term taken from the sum over all $W_{u\nu}$ for $u$ and $\nu$ vertices appearing in the cycle.

$$
\begin{align}
H = & A\sum_{\nu=1}^{n}\left(1 - \sum_{j=1}^{N}x_{\nu,j}\right)^2
    + A\sum_{j=1}^{n}\left(1 - \sum_{\nu=1}^{N}x_{\nu,j}\right)^2 \\
    & + A\sum_{u\nu\notin E}\sum_{j=1}^{N}x_{u,j}x_{\nu,j+1}
    + B\sum_{u\nu\in E}W_{u\nu}\sum_{j=1}^{N}x_{u,j}x_{\nu,j+1}
\end{align}
$$

The parameter $B$ must be small enough that it is never favourable to violate the other constraints: i.e. $0<B\max(W_{u\nu})<A$.

In the traveling salesman problem the set of edges includes all possible combinations between the vertices. Therefore, there will be no penalisation from connections not appearing in the edges of the graph. Therefore, the Hamiltonian is reduced to:

$$
\begin{align}
H = & A\sum_{\nu=1}^{N}\left(1 - \sum_{j=1}^{N}x_{\nu,j}\right)^2
    + A\sum_{j=1}^{N}\left(1 - \sum_{\nu=1}^{N}x_{\nu,j}\right)^2 \\
    & + \sum_{u=1}^{N}\sum_{\nu=1,\nu\neq u}^{N}W_{u\nu}\sum_{j=1}^{N}x_{u,j}x_{\nu,j+1}
\end{align}
$$

The parameter $B$ has been absorbed into $A$, and hence $A>\max(W_{u\nu})$. The weight $W_{u\nu}$ is simply the euclidean distance between two points:

$$W_{u\nu} = \sqrt{(u_x - \nu_x)^2 + (u_y - \nu_y)^2}$$

```python
pip install numpy matplotlib scipy pyqubo
```

    Requirement already satisfied: numpy in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (1.18.1)
    Requirement already satisfied: matplotlib in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (3.2.1)
    Requirement already satisfied: scipy in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (1.4.1)
    Requirement already satisfied: pyqubo in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (0.4.0)
    Requirement already satisfied: kiwisolver>=1.0.1 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from matplotlib) (1.2.0)
    Requirement already satisfied: cycler>=0.10 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from matplotlib) (0.10.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from matplotlib) (2.4.7)
    Requirement already satisfied: python-dateutil>=2.1 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from matplotlib) (2.8.1)
    Requirement already satisfied: six<2.0.0,>=1.10.0 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from pyqubo) (1.14.0)
    Requirement already satisfied: dimod>=0.7.4 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from pyqubo) (0.9.3)
    Requirement already satisfied: dwave-neal>=0.4.2 in /home/shane/miniconda2/envs/torch38/lib/python3.8/site-packages (from pyqubo) (0.5.4)
    Note: you may need to restart the kernel to use updated packages.

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import cdist

from pyqubo import Array, Placeholder, Sum, Constraint, solve_qubo

from utils import plot_graph
```

# Graph

Define the graph and the vertices. The number of vertices is equal to the number of binary variables we need to initialise in the array.

# Hamiltonian

```python
#graph = np.random.randn(6, 2)
graph = np.array([
    [-0.8494, -0.0292],
    [ 0.5966, -0.2025],
    [-0.7303,  1.5206],
    [-0.6110, -0.6992],
    [-1.0172, -0.5384],
    [ 0.2233, -0.0027],
])
plot_graph(graph)
```

The weights are precalculated from the euclidean distance between each point (using the scipy function `cdist`)

```python
weights = cdist(graph, graph)
weights.shape
```

    (6, 6)

```python
x = Array.create('x', shape=weights.shape, vartype='BINARY')
```

Placeholder for the parameters of the Hamiltonian used to exercise the constraints.

```python
A = Placeholder('A')
```

```python
# (sum_i ((sum_j x[i,j])-1)**2)
simultaneous_constraint = 0.
for i in range(graph.shape[0]):
    simultaneous_constraint += Constraint(
        (Sum(0, graph.shape[0], lambda j: x[i,j])-1)**2,
        label=f"vertex{i}",
    )

# (sum_j ((sum_i x[i,j])-1)**2)
visits_constraint = 0.
for j in range(graph.shape[0]):
    visits_constraint += Constraint(
        (Sum(0, graph.shape[0], lambda i: x[i,j])-1)**2,
        label=f"edge{i}",
    )

# sum_i sum_j d[i,j] (sum_k (x[k,i] * x[k+1,j]))
distance = 0.
for i in range(graph.shape[0]):
    for j in range(graph.shape[0]):
        for k in range(graph.shape[0]-1):
            distance += weights[i,j]*x[k,i]*x[k+1,j]

```

We want to minimize the distance under certain constraints, hence we'll absort $B$ into $A$.

```python
H = distance + A*(simultaneous_constraint + visits_constraint)
model = H.compile()
```

```python
feed_dict = {"A": 4.}
qubo, offset = model.to_qubo(feed_dict=feed_dict)
sol = solve_qubo(qubo)
solution, broken, energy = model.decode_solution(
    sol, vartype='BINARY', feed_dict=feed_dict
)
print(f'Number of broken constraints: {len(broken)}')
print(f'Energy: {energy:.3f}')
```

    Number of broken constraints: 0
    Energy: 4.658

```python
edges = np.array([
    list(x.values())
    for x in solution['x'].values()
], dtype='bool')
edges = np.where(edges)[1]
edges
```

    array([0, 4, 3, 5, 1, 2])

```python
plot_graph(graph, edges=edges)
```


# Social Networks and Text Analysis - Ranking algorithms

## 1. PageRank

Define a function that computes the Page Rank given an Adjacency matrix.

```python
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import re

#Magic functions (%) for setting up the matplotlib and increase the resolution of the plots:
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
```

```python

```

```python
#Create a network:
G = nx.erdos_renyi_graph(n=100, p=0.2)
print(nx.info(G))
```

    Name:
    Type: Graph
    Number of nodes: 100
    Number of edges: 984
    Average degree:  19.6800

```python
#Save the adjacency matrix:
Adj = nx.to_numpy_matrix(G).A
```

```python
#Plot the matrix of zeros and ones:
plt.imshow(Adj, origin='lower', interpolation='nearest')
plt.title("Adjacency - Matrix")
plt.show()
```

```python
#Create the transition probabilities function.
#Which is the adjacency divided by the degree of each node
#In this case is the row or the column (Because is undirected and simetrical)
R = Adj/Adj.sum(axis=1)[:,None]
plt.imshow(R, origin='lower', interpolation='nearest')
plt.title("Adjacency divided by the degree - Matrix")
plt.colorbar()
plt.show()
```

```python
#Create your page rank function:
def Page_Rank(G):
    #Set the adjacency matrix
    Adj = nx.to_numpy_matrix(G).A

    #Divide the adjacency matrix by the degree of the node = sum of the columns by row:
    R = Adj/Adj.sum(axis=1)[:,None]

    #Compute the eigen values and eigenvectors for the corrected matrix
    eigen_values, eigenvectors = np.linalg.eig(R.T)

    #Select the largest eigenvalue
    ind = np.argmax(eigen_values)

    #Select the corresponding eigenvector:
    largest = np.array(eigenvectors[:, ind]).flatten().real

    #Normalize the vector:
    norm = float(largest.sum())

    #Return the values:
    return(dict(zip(G, map(float, largest / norm))))
```

```python
#Check the values:
Page_Rank(G)
```

    {0: 0.013719512195121948,
     1: 0.014227642276422757,
     2: 0.007113821138211382,
     3: 0.013211382113821134,
     4: 0.006605691056910564,
     5: 0.009654471544715446,
     6: 0.011178861788617876,
     7: 0.009146341463414644,
     8: 0.010162601626016251,
     9: 0.010670731707317074,
     10: 0.011178861788617888,
     11: 0.009654471544715437,
     12: 0.009654471544715446,
     13: 0.008130081300813012,
     14: 0.010670731707317065,
     15: 0.010162601626016265,
     16: 0.01016260162601625,
     17: 0.011178861788617885,
     18: 0.005081300813008127,
     19: 0.009146341463414628,
     20: 0.009654471544715444,
     21: 0.01067073170731707,
     22: 0.008638211382113821,
     23: 0.010670731707317065,
     24: 0.008638211382113825,
     25: 0.007113821138211386,
     26: 0.007113821138211376,
     27: 0.008130081300813012,
     28: 0.01067073170731707,
     29: 0.008638211382113826,
     30: 0.010162601626016258,
     31: 0.013211382113821137,
     32: 0.01016260162601625,
     33: 0.00965447154471545,
     34: 0.01117886178861789,
     35: 0.00914634146341463,
     36: 0.009146341463414625,
     37: 0.010162601626016258,
     38: 0.010162601626016263,
     39: 0.007113821138211378,
     40: 0.010670731707317062,
     41: 0.006097560975609755,
     42: 0.009654471544715446,
     43: 0.009146341463414623,
     44: 0.010162601626016255,
     45: 0.008130081300813012,
     46: 0.011686991869918692,
     47: 0.0076219512195121976,
     48: 0.011178861788617891,
     49: 0.00914634146341463,
     50: 0.006097560975609758,
     51: 0.012195121951219507,
     52: 0.007621951219512186,
     53: 0.009654471544715453,
     54: 0.009654471544715451,
     55: 0.012703252032520323,
     56: 0.0116869918699187,
     57: 0.011178861788617885,
     58: 0.015243902439024386,
     59: 0.008638211382113814,
     60: 0.00914634146341464,
     61: 0.011178861788617883,
     62: 0.005081300813008127,
     63: 0.009654471544715451,
     64: 0.008638211382113826,
     65: 0.00965447154471544,
     66: 0.012703252032520328,
     67: 0.013211382113821148,
     68: 0.008130081300813006,
     69: 0.008638211382113814,
     70: 0.006097560975609753,
     71: 0.010670731707317067,
     72: 0.008130081300813018,
     73: 0.008638211382113833,
     74: 0.007621951219512189,
     75: 0.011178861788617893,
     76: 0.009146341463414637,
     77: 0.00965447154471544,
     78: 0.009146341463414637,
     79: 0.010670731707317072,
     80: 0.012195121951219506,
     81: 0.011178861788617888,
     82: 0.010162601626016269,
     83: 0.013719512195121943,
     84: 0.010670731707317077,
     85: 0.010670731707317072,
     86: 0.013211382113821142,
     87: 0.010670731707317074,
     88: 0.01270325203252032,
     89: 0.009654471544715446,
     90: 0.010162601626016243,
     91: 0.00914634146341463,
     92: 0.015752032520325202,
     93: 0.012703252032520315,
     94: 0.009146341463414639,
     95: 0.011686991869918697,
     96: 0.009146341463414639,
     97: 0.009654471544715455,
     98: 0.01016260162601626,
     99: 0.0116869918699187}

Define a function that computes topic specific Page Rank following the advice of Prof.  Leskovec:

```python
#Create your topic page rank function:
def Topic_Specific_Page_Rank(G, special_Set,Beta):
    #Set the adjacency matrix
    Adj = nx.to_numpy_matrix(G).A

    #Divide the adjacency matrix by the degree of the node = sum of the columns by row:
    R = Adj/Adj.sum(axis=1)[:,None]

    for i,row in enumerate(R):
        if i in special_Set:
            R[i] = Beta*row+(1-Beta)/len(special_Set)
        else:
            R[i] = Beta*row

    plt.imshow(R, origin='lower', interpolation='nearest')
    plt.title("Adjacency divided by the degree with the specific Set - Matrix")
    plt.xlabel('ID')
    plt.ylabel('ID')
    plt.colorbar()
    plt.show()
    #----------------------------------------
    #This part comes from the other function:
    #----------------------------------------
    #Compute the eigen values and eigenvectors for the corrected matrix
    eigen_values, eigenvectors = np.linalg.eig(R.T)

    #Select the largest eigenvalue
    ind = np.argmax(eigen_values)

    #Select the corresponding eigenvector:
    largest = np.array(eigenvectors[:, ind]).flatten().real

    #Normalize the vector:
    norm = float(largest.sum())

    #Return the values:
    return(dict(zip(G, map(float, largest / norm))))
```

```python
#Run the function and plot the matrix:
#Change the special set to random nodes!
Topic_Specific_Page_Rank(G, special_Set=list(range(5)),Beta=0.5)
```

# 2. LexRank

Define the LexRank function.  Select a piece of text (10-20 sentences) that you would like to summarize.  Thereare two parameters you can vary:

the cosine similarity threshold determines how similar two sentences have to be in order to share and edge.

the salience threshold determines how high a sentence’s PageRank has to be in order for that sentence tobe included in the summary.

Cosine similarity function for two vectors:

\begin{equation} similarity = \cos ({\theta})= {{\bf A}\cdot {\bf B} \over \|{\bf A}\| \|{\bf B}\|} = \frac{ \sum_{i=1}^{n}{{\bf A}_i{\bf B}_i} }{ \sqrt{\sum_{i=1}^{n}{({\bf A}_i)^2}} \sqrt{\sum_{i=1}^{n}{({\bf B}_i)^2}} } \end{equation}

```python
with open('Text.txt') as f: #<-- #Read the piece of text: INVISIBLE WOMEN.
    text = f.read()
#Separate the text into sentences by the dot:
List_Sentences = text.split('.')
```

```python
from collections import Counter #<-function for counting frequency in an array

#1. We need to convert the text to vector of words with the frequency of ech word.
def get_vector_of_words(text):
    Words = re.findall(r'\w+', text)
    Words_Frequency = Counter(Words)
    return(Words_Frequency)

#Example with the first sentence:
get_vector_of_words(List_Sentences[0])
```

    Counter({'When': 1,
             'Hillary': 1,
             'Clinton': 1,
             'wanted': 1,
             'to': 1,
             'speak': 1,
             'about': 1,
             'women': 1,
             's': 1,
             'rights': 1,
             'at': 1,
             'the': 1,
             '1995': 1,
             'United': 1,
             'Nations': 1,
             'Fourth': 1,
             'World': 1,
             'Conference': 1,
             'on': 1,
             'Women': 1,
             'in': 1,
             'Beijing': 1,
             'even': 1,
             'her': 1,
             'own': 1,
             'side': 1,
             'was': 1,
             'dubious': 1})

```python
#2. Compute the cosine similarity:
def Cosine_Similarity(vec_1, vec_2):
    #Get the bag words with all the words:
    Bag_words = set(set(vec_1.keys()).union(set(vec_2.keys())))

    #Multiply the number of times that the words appear in both texts:
    Dot_multiplication = sum([vec_1[w]*vec_2[w] for w in Bag_words if w in vec_1 and w in vec_2])

    #Multiply the norms of each vector:
    Sum_Vec_1 = np.sqrt(sum([vec_1[w]**2 for w in vec_1.keys()]))
    Sum_Vec_2 = np.sqrt(sum([vec_2[w]**2 for w in vec_2.keys()]))
    Norm_multiplication = Sum_Vec_1*Sum_Vec_2
    if Norm_multiplication != 0:
        return(Dot_multiplication/Norm_multiplication)
    else:
        return(np.nan)

```

```python
#Example for the first two sentences:
vec_1 = get_vector_of_words(List_Sentences[1])
vec_2 = get_vector_of_words(List_Sentences[0])
Cosine_Similarity(vec_1, vec_2)
```

    0.27940948018117095

```python
#3. Create the Lex-Rank function:
def LexRank(List_Sentences, c_t, s_t):
    #Create the empty network:
    G = nx.Graph()

    #Iterate over all the sentences:
    for n1 in range(len(List_Sentences)):
        for n2 in range(len(List_Sentences)):

            #Avoid overwrite links and create loops:
            if n1 != n2 and (n1,n2) not in G.edges():

                #Compute the cosine similarity:
                vec_1 = get_vector_of_words(List_Sentences[n1])
                vec_2 = get_vector_of_words(List_Sentences[n2])
                Cos_Sim = Cosine_Similarity(vec_1, vec_2)

                #Add the edge just if the cosine similarity is larger than the threshold:
                if Cos_Sim > c_t:
                    G.add_edge(n1,n2, weight =Cos_Sim)

                    for node in [n1,n2]:
                        if 'Sentence' not in G.nodes[node].keys():
                            G.nodes[node]['Sentence'] = List_Sentences[node]
    print(nx.info(G))

    #Compute the pagerank of the network.
    #Select the nodes in the threshold for the summary:
    PR = nx.pagerank(G)
    #Select the nodes that overpass the threshold:
    Selected_Nodes = {u:v for u,v in PR.items() if v>s_t}
    print('')
    print('Selected nodes by Page-Rank:')
    print(Selected_Nodes)
    print({i: G.nodes[i]['Sentence'] for i in Selected_Nodes})

    #Draw the network and highlight in red the selected nodes:
    color = ['darkred' if n in Selected_Nodes.keys() else 'darkblue' for n in G.nodes()]
    pos = nx.fruchterman_reingold_layout(G)
    nx.draw_networkx(G,pos=pos, node_color = color, alpha=0.8, edge_color='white', font_color='white')
    nx.draw_networkx_edges(G, pos=pos, alpha=0.5)
    plt.show()
```

```python
# 3. Set the threshold:
Cosine_Threshold = 0.2
Salience_Threshold = 0.1
LexRank(List_Sentences, Cosine_Threshold, Salience_Threshold)
```

```python

```

```python

```


```python
from sympy import Symbol, symbols, cos, sin, Matrix, simplify
from sympy.physics.mechanics import dynamicsymbols, mlatex, init_vprinting
init_vprinting()
from IPython.display import display, Math
```

```python
t = Symbol('t')
d, L = symbols('d L', positive=True)
a = dynamicsymbols('alpha')
```

```python
x, y = d*sin(a), d*cos(a)
xd, yd = x.diff(t), y.diff(t)
xdd, ydd = xd.diff(t), yd.diff(t)

display(Math(r'x=' + mlatex(x)))
display(Math(r'\dot{x}=' + mlatex(xd)))
display(Math(r'\ddot{x}=' + mlatex(xdd)))
display(Math(r'y=' + mlatex(y)))
display(Math(r'\dot{y}=' + mlatex(yd)))
display(Math(r'\ddot{y}=' + mlatex(ydd)))
```

$$x=d \operatorname{sin}\left(\alpha\right)$$

$$\dot{x}=d \operatorname{cos}\left(\alpha\right) \dot{\alpha}$$

$$\ddot{x}=- d \operatorname{sin}\left(\alpha\right) \dot{\alpha}^{2} + d \operatorname{cos}\left(\alpha\right) \ddot{\alpha}$$

$$y=d \operatorname{cos}\left(\alpha\right)$$

$$\dot{y}=- d \operatorname{sin}\left(\alpha\right) \dot{\alpha}$$

$$\ddot{y}=- d \operatorname{sin}\left(\alpha\right) \ddot{\alpha} - d \operatorname{cos}\left(\alpha\right) \dot{\alpha}^{2}$$

```python
m, I, g = symbols('m I g', positive=True)
Fex, Fey = symbols('F_ex F_ey')
add = a.diff(t, 2)
```

```python
Frx = m*xdd - Fex
Fry = m*ydd + m*g - Fey
display(Math(r'F_{rx}=' + mlatex(Frx)))
display(Math(r'F_{ry}=' + mlatex(Fry)))
```

$$F_{rx}=- F_{ex} + m \left(- d \operatorname{sin}\left(\alpha\right) \dot{\alpha}^{2} + d \operatorname{cos}\left(\alpha\right) \ddot{\alpha}\right)$$

$$F_{ry}=- F_{ey} + g m + m \left(- d \operatorname{sin}\left(\alpha\right) \ddot{\alpha} - d \operatorname{cos}\left(\alpha\right) \dot{\alpha}^{2}\right)$$

```python
T = I*add - d*sin(a)*Frx + d*cos(a)*Fry + (L-d)*sin(a)*Fex - (L-d)*cos(a)*Fey
display(Math(r'T\quad=\quad ' + mlatex(T)))
```

$$T\quad=\quad F_{ex} \left(L - d\right) \operatorname{sin}\left(\alpha\right) - F_{ey} \left(L - d\right) \operatorname{cos}\left(\alpha\right) + I \ddot{\alpha} - d \left(- F_{ex} + m \left(- d \operatorname{sin}\left(\alpha\right) \dot{\alpha}^{2} + d \operatorname{cos}\left(\alpha\right) \ddot{\alpha}\right)\right) \operatorname{sin}\left(\alpha\right) + d \left(- F_{ey} + g m + m \left(- d \operatorname{sin}\left(\alpha\right) \ddot{\alpha} - d \operatorname{cos}\left(\alpha\right) \dot{\alpha}^{2}\right)\right) \operatorname{cos}\left(\alpha\right)$$

```python
T = simplify(T)
display(Math(r'T=' + mlatex(T)))
```

$$T=F_{ex} L \operatorname{sin}\left(\alpha\right) - F_{ey} L \operatorname{cos}\left(\alpha\right) + I \ddot{\alpha} - d^{2} m \operatorname{sin}\left(2 \alpha\right) \ddot{\alpha} - d^{2} m \operatorname{cos}\left(2 \alpha\right) \dot{\alpha}^{2} + d g m \operatorname{cos}\left(\alpha\right)$$

```python
#2 joint
```

```python
t = Symbol('t')
d1, d2, L1, L2 = symbols('d1, d2, L_1 L_2', positive=True)
a1, a2 = dynamicsymbols('alpha1 alpha2')
a1d, a2d = a1.diff(t), a2.diff(t)
a1dd, a2dd = a1.diff(t, 2), a2.diff(t, 2)
```

```python
x1, y1 = d1*sin(a1), d1*cos(a1)
x1d, y1d = x1.diff(t), y1.diff(t)
x1dd, y1dd = x1d.diff(t), y1d.diff(t)

display(Math(r'x_1=' + mlatex(x1)))
display(Math(r'\dot{x}_1=' + mlatex(x1d)))
display(Math(r'\ddot{x}_1=' + mlatex(x1dd)))
display(Math(r'y_1=' + mlatex(y1)))
display(Math(r'\dot{y}_1=' + mlatex(y1d)))
display(Math(r'\ddot{y}_1=' + mlatex(y1dd)))
```

$$x_1=d_{1} \operatorname{sin}\left(\alpha_{1}\right)$$

$$\dot{x}_1=d_{1} \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1}$$

$$\ddot{x}_1=- d_{1} \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + d_{1} \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1}$$

$$y_1=d_{1} \operatorname{cos}\left(\alpha_{1}\right)$$

$$\dot{y}_1=- d_{1} \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1}$$

$$\ddot{y}_1=- d_{1} \operatorname{sin}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - d_{1} \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2}$$

```python
x2, y2 = L1*sin(a1) + d2*sin(a1+a2), L1*cos(a1) + d2*cos(a1+a2)
x2d, y2d = x2.diff(t), y2.diff(t)
x2dd, y2dd = x2d.diff(t), y2d.diff(t)

display(Math(r'x_2=' + mlatex(x2)))
display(Math(r'\dot{x}_2=' + mlatex(x2d)))
display(Math(r'\ddot{x}_2=' + mlatex(x2dd)))
display(Math(r'y_2=' + mlatex(y2)))
display(Math(r'\dot{y}_2=' + mlatex(y2d)))
display(Math(r'\ddot{y}_2=' + mlatex(y2dd)))
```

$$x_2=L_{1} \operatorname{sin}\left(\alpha_{1}\right) + d_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right)$$

$$\dot{x}_2=L_{1} \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1} + d_{2} \left(\dot{\alpha}_{1} + \dot{\alpha}_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)$$

$$\ddot{x}_2=- L_{1} \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + L_{1} \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - d_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) + d_{2} \left(\ddot{\alpha}_{1} + \ddot{\alpha}_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)$$

$$y_2=L_{1} \operatorname{cos}\left(\alpha_{1}\right) + d_{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)$$

$$\dot{y}_2=- L_{1} \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1} - d_{2} \left(\dot{\alpha}_{1} + \dot{\alpha}_{2}\right) \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right)$$

$$\ddot{y}_2=- L_{1} \operatorname{sin}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - L_{1} \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} - d_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) - d_{2} \left(\ddot{\alpha}_{1} + \ddot{\alpha}_{2}\right) \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right)$$

```python
m1, m2, I1, I2, g = symbols('m_1, m_2, I_1 I_2 g', positive=True)
```

```python
# link 2
Fr2x = m2*x2dd - Fex
Fr2y = m2*y2dd + m2*g - Fey
T2 = I2*(a1dd+a2dd) - d2*Fr2x*sin(a1+a2) + d2*Fr2y*cos(a1+a2) + (L2-d2)*Fex*sin(a1+a2) - (L2-d2)*Fey*cos(a1+a2)
T2 = simplify(T2)
# link 1
Fr1x = m1*x1dd + Fr2x
Fr1y = m1*y1dd + Fr2y + m1*g
T1 = I1*a1dd + T2 - d1*Fr1x*sin(a1) + d1*Fr1y*cos(a1) - (L1-d1)*Fr2x*sin(a1) + (L1-d1)*Fr2y*cos(a1)
T1 = simplify(T1)
```

```python
display(Math(r'T_1\quad = \quad ' + mlatex(T1)))
display(Math(r'T_2\quad = \quad ' + mlatex(T2)))
```

$$T_1\quad = \quad F_{ex} L_{1} \operatorname{sin}\left(\alpha_{1}\right) + F_{ex} L_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) - F_{ey} L_{1} \operatorname{cos}\left(\alpha_{1}\right) - F_{ey} L_{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) + I_{1} \ddot{\alpha}_{1} + I_{2} \ddot{\alpha}_{1} + I_{2} \ddot{\alpha}_{2} + L_{1}^{2} m_{2} \operatorname{sin}^{2}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} - 2 L_{1}^{2} m_{2} \operatorname{sin}\left(\alpha_{1}\right) \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - L_{1}^{2} m_{2} \operatorname{cos}^{2}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + L_{1} d_{2} m_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{sin}\left(\alpha_{1}\right) - L_{1} d_{2} m_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1}\right) + L_{1} d_{2} m_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} - 2 L_{1} d_{2} m_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - L_{1} d_{2} m_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{2} - 2 L_{1} d_{2} m_{2} \operatorname{sin}\left(\alpha_{1}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \ddot{\alpha}_{1} - L_{1} d_{2} m_{2} \operatorname{sin}\left(\alpha_{1}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \ddot{\alpha}_{2} - L_{1} d_{2} m_{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + L_{1} g m_{2} \operatorname{cos}\left(\alpha_{1}\right) + d_{1}^{2} m_{1} \operatorname{sin}^{2}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} - 2 d_{1}^{2} m_{1} \operatorname{sin}\left(\alpha_{1}\right) \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1} - d_{1}^{2} m_{1} \operatorname{cos}^{2}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + d_{1} g m_{1} \operatorname{cos}\left(\alpha_{1}\right) + d_{2}^{2} m_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{sin}^{2}\left(\alpha_{1} + \alpha_{2}\right) - d_{2}^{2} m_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{cos}^{2}\left(\alpha_{1} + \alpha_{2}\right) - 2 d_{2}^{2} m_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \ddot{\alpha}_{1} - 2 d_{2}^{2} m_{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) \ddot{\alpha}_{2} + d_{2} g m_{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)$$

$$T_2\quad = \quad F_{ex} \left(L_{2} - d_{2}\right) \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) - F_{ey} \left(L_{2} - d_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) + I_{2} \left(\ddot{\alpha}_{1} + \ddot{\alpha}_{2}\right) + d_{2} \left(F_{ex} + m_{2} \left(L_{1} \operatorname{sin}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} - L_{1} \operatorname{cos}\left(\alpha_{1}\right) \ddot{\alpha}_{1} + d_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) - d_{2} \left(\ddot{\alpha}_{1} + \ddot{\alpha}_{2}\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)\right)\right) \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right) - d_{2} \left(F_{ey} - g m_{2} + m_{2} \left(L_{1} \operatorname{sin}\left(\alpha_{1}\right) \ddot{\alpha}_{1} + L_{1} \operatorname{cos}\left(\alpha_{1}\right) \dot{\alpha}_{1}^{2} + d_{2} \dot{\alpha}_{1} + \dot{\alpha}_{2}^{2} \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right) + d_{2} \left(\ddot{\alpha}_{1} + \ddot{\alpha}_{2}\right) \operatorname{sin}\left(\alpha_{1} + \alpha_{2}\right)\right)\right) \operatorname{cos}\left(\alpha_{1} + \alpha_{2}\right)$$

```python

```

```python

```

courses/modsim2018/caiolima/task 19 new Caio Lima.ipynb


Variational Quantum Linear Solver
====================================

This notebook is an implementation of the pennylane notebook into myqlm notebook developed  by Andrea Mari (Xanadu) who implemented "the Variational quantum linear solver" (VQLS) algorithm  originaly introduced by Bravo-Prieto et al. (2019) <https://arxiv.org/abs/1909.05820>.
In the following we have reproduced Andrea's explanations from her original notebook.
We thanks Jean-christophe Jaskula (ATOS) for his help.
In this notebook, unstead of using a gradient descent based method for optimizing the objective function as in the orginal implementation, we use COBYLA.

Introduction
------------------

We first define the problem and the general structure of a VQLS.
As a second step, we consider a particular case and we solve it explicitly with PennyLane.

## The problem

We are given a $2^n \times 2^n$ matrix $A$ which can be expressed as a linear
combination of $L$ unitary matrices $A_0, A_1, \dots A_{L-1}$, i.e.,

\begin{align}A = \sum_{l=0}^{L-1} c_l A_l,\end{align}

where $c_l$ are arbitrary complex numbers. Importantly, we assume that each of the
unitary components $A_l$ can be efficiently implemented with a quantum circuit
acting on $n$ qubits.

We are also given a normalized complex vector in the physical form of a quantum
state $|b\rangle$, which can be generated by a unitary operation $U$
applied to the ground state of $n$ qubits. , i.e.,

\begin{align}|b\rangle = U |0\rangle,\end{align}

where again we assume that $U$ can be efficiently implemented with a quantum circuit.

The problem that we aim to solve is that of preparing a quantum state $|x\rangle$, such that
$A |x\rangle$ is proportional to $|b\rangle$ or, equivalently, such that

\begin{align}|\Psi\rangle :=  \frac{A |x\rangle}{\sqrt{\langle x |A^\dagger A |x\rangle}} \approx |b\rangle.\end{align}

## Variational quantum linear solver

The approach used in a VQLS is to approximate the solution $|x\rangle$ with a variational quantum
circuit, i.e., a unitary circuit $V$ depending on a finite number of classical real parameters
$w = (w_0, w_1, \dots)$:

\begin{align}|x \rangle = V(w) |0\rangle.\end{align}

The parameters should be optimized in order to maximize the overlap between the quantum states
$|\Psi\rangle$ and $|b\rangle$. This suggests to define the following cost function:

\begin{align}C_G = 1- |\langle b | \Psi \rangle|^2,\end{align}

such that its minimization with respect to the variational parameters should lead towards the problem solution.

In her original implementation Andrea Mari  propose to replace $\color{blue}{|0\rangle\langle 0|}$ in the original objective fonction $C_G$:

\begin{align}C_G = 1- \frac{ \sum_{l, l'}  c_l c_{l'}^* \langle 0|  V^\dagger A_{l'}^\dagger U \color{blue}{|0\rangle \langle 0|} U^\dagger A_l  V |0\rangle}
    {\sum_{l,l'} c_l c_{l'}^* \langle 0| V^\dagger A_{l'}^\dagger A_l V |0\rangle} .\end{align}

with the following positive operator:

\begin{align}\color{blue}{P} =  \frac{1}{2} + \frac{1}{2n}\sum_{j=0}^{n-1} Z_j,\end{align}

where $Z_j$ is the Pauli $Z$ operator locally applied to the $j\rm{th}$ qubit. This gives a new cost function:

\begin{align}C_L = 1- \frac{ \sum_{l, l'}  c_l c_{l'}^* \langle 0|  V^\dagger A_{l'}^\dagger U \color{blue}{P} U^\dagger A_l  V |0\rangle}
    {\sum_{l,l'} c_l c_{l'}^* \langle 0| V^\dagger A_{l'}^\dagger A_l V |0\rangle},\end{align}

which satisfies

\begin{align}C_G \rightarrow 0   \Leftrightarrow C_L \rightarrow 0,\end{align}

and so we can solve our problem by minimizing $C_L$ instead of $C_G$.

Substituting the definition of $P$ into the expression for $C_L$ we get:

\begin{align}C_L
    &=& \frac{1}{2} - \frac{1}{2n} \frac{ \sum_{j=0}^{n-1} \sum_{l, l'}  c_l c_{l'}^* \langle 0|  V^\dagger A_{l'}^\dagger U Z_j U^\dagger A_l  V |0\rangle}
    {\sum_{l,l'} c_l c_{l'}^* \langle 0| V^\dagger A_{l'}^\dagger A_l V |0\rangle} \\
    &&\\
    &=& \frac{1}{2} - \frac{1}{2n} \frac{ \sum_{j=0}^{n-1} \sum_{l, l'}  c_l c_{l'}^* \mu_{l,l',j}}
    {\sum_{l,l'} c_l c_{l'}^* \mu_{l,l',-1}},\end{align}

which can be computed whenever we are able to measure the following coefficients

\begin{align}\mu_{l, l', j} = \langle 0|  V^\dagger A_{l'}^\dagger U Z_j U^\dagger A_l  V |0\rangle,\end{align}

where we used the convention that if $j=-1$,  $Z_{-1}$ is replaced with the identity.

In our implementation we don't use the hadamard test  as in the original notebook but take advantage of myQLM to provide the expectaion value of the Observable we have defined:  $ A_{l'}^\dagger U Z_j U^\dagger A_l$

### A simple example

In this notebook we reproduce the  simple example based on a system of 3 qubits  proposed in Andrea Mari's implementation.

\begin{align}
    A  &=  c_0 A_0 + c_1 A_1 + c_2 A_2 = \mathbb{I} + 0.2 X_0 Z_1 + 0.2 X_0, \\
    |b\rangle &= U |0 \rangle = H_0  H_1  H_2 |0\rangle,
\end{align}

where $Z_j, X_j, H_j$ represent the Pauli $Z$, Pauli $X$ and Hadamard gates applied to the qubit with index $j$.

This problem is computationally quite easy since a single layer of local rotations is enough to generate the
solution state, i.e., we can use the following simple ansatz:

\begin{align}
|x\rangle = V(w) |0\rangle = \Big [  R_y(w_0) \otimes  R_y(w_1) \otimes  R_y(w_2) \Big ]  H_0  H_1  H_2 |0\rangle.
\end{align}

In the code presented below we solve this particular problem by minimizing the local cost function $C_L$.
Eventually we will compare the quantum solution with the classical one.

```python
import time
import numpy as np
import importlib.util
from scipy.optimize import minimize

from qat.lang.AQASM import Program,QRoutine,RY,RZ,H,CNOT,Z,PH
from qat.core import Observable, Term
from qat.plugins import ObservableSplitter
from qat.qpus import get_default_qpu

class Timer():
    text: str = "Elapsed time: {:0.4f} seconds"
    _start_time: float = 0

    def start(self) -> None:
        """Start a new timer"""
        self._start_time = time.perf_counter()

    def stop(self) -> float:
        """Stop the timer, and report the elapsed time"""
        elapsed_time = time.perf_counter() - self._start_time
        self._start_time = None

        return elapsed_time

    def __enter__(self):
        """Start a new timer as a context manager"""
        self.start()
        return self

    def __exit__(self, *exc_info):
        """Stop the context manager timer"""
        print(self.text.format(self.stop()))

qpu = ObservableSplitter() | get_default_qpu()

def U_b(qprog,qubits,n_qubits):
    """Unitary matrix rotating the ground state to the problem vector |b> = U_b |0>."""
    for idx in range(n_qubits):
        qprog.apply(H,qubits[idx])

H_mat = np.array([[1,1],[1,-1]])/np.sqrt(2)
X_m = np.array([[0,1],[1,0]])
Z_m = np.array([[1,0],[0,-1]])

def apply_n(f, m, n):
    r = m
    for i in range(n-1):
        r = f(r,m)
    return r

def variational_block(qprog,qubits,n_qubits,weights):
    """Variational circuit mapping the ground state |0> to the ansatz state |x>."""
    # We first prepare an equal superposition of all the states of the computational basis.
    for idx in range(n_qubits):
        qprog.apply(H,qubits[idx])

    # A very minimal variational circuit.
    for idx, element in enumerate(weights):
        var = qprog.new_var(float, '\\theta'+str(idx))
        qprog.apply(RY(var), qubits[idx])

def psi_norm(c):
    """Returns the normalization constant <psi|psi>, where |psi> = A |x>."""

    return abs(qpu.submit(c.to_job(job_type='OBS', observable=A*A)).value)

def bind(c, w):
    #print({'\\theta'+str(idx):e for idx, e in enumerate(w)})
    return c.bind_variables({'\\theta'+str(idx):e for idx, e in enumerate(w)})

# Expectation value of the Observable  and corresponding cost function
def cost_loc(weights: np.array,
             O:       Observable
            )->float:
    c = bind(parametrized_circuit, weights)
    num = qpu.submit(c.to_job(job_type='OBS', observable=O, nbshots=0)).value
    res = 0.5 - 0.5 * abs(num) / (n_qubits * psi_norm(c))
    cost_history.append(res)
    return res

n_qubits = 3       # Number of system qubits.
n_shots = 10 ** 2  # Number of quantum measurements.
steps = 30         # Number of optimization steps
eta = 0.8          # Learning rate
q_delta = 0.001    # Initial spread of random quantum weights
rng_seed = 0       # Seed for random number generator

np.random.seed(rng_seed)
w0 = np.random.randn(n_qubits) * q_delta

# define the observables
A = Observable(n_qubits, pauli_terms=[Term(0.2, "XZ", [0,1]),Term(0.2, "X", [0])], constant_coeff=1)
U = Observable(n_qubits, matrix=apply_n(np.kron, H_mat, 3)/(2**(3-1)))
Z = Observable(n_qubits, pauli_terms=[Term(1, "Z", [i]) for i in range(n_qubits)])
# note: by defining O will give a ~4x speedup in the algotihm
O=A*U*Z*U*A

# Main program
qprog=Program()
qubits=qprog.qalloc(n_qubits)
variational_block(qprog,qubits,n_qubits,w0)
parametrized_circuit = qprog.to_circ()
cost_history = []
with Timer():
    w = minimize(cost_loc,w0,O, method='COBYLA',
                           constraints=(), tol=0.0001, callback=None,
                           options={'rhobeg': 1.0, 'maxiter': 50, 'disp': True, 'catol': 0.001})

# Preparation of the quantum solution
# ------------------------------------
# Given the variational weights ``w`` that we have previously optimized,
# we can generate the quantum state $|x\rangle$. By measuring $|x\rangle$
# in the computational basis we can estimate the probability of each basis state.
# For this task, we initialize a new PennyLane device and define the associated *qnode* circuit.
# To estimate the probability distribution over the basis states we first take ``n_shots``
# samples and then compute the relative frequency of each outcome.
def prob_dist(r):
    n = 2**len(r.raw_data[0].state.bitstring)
    p = np.zeros(n)
    for j, e in enumerate(r.raw_data):
        p[sum([int(s)*2**int(i) for i,s in enumerate(e.state.bitstring)])] = e.probability

    return p

q_probs = prob_dist(qpu.submit(bind(parametrized_circuit,w.x).to_job()))
q_probs_init = prob_dist(qpu.submit(bind(parametrized_circuit,w0).to_job()))

# Classical algorithm
# ----------------------
# To solve the problem in a classical way, we use the explicit matrix representation in
# terms of numerical NumPy arrays.c = np.array([1.0, 0.2, 0.2])
c = np.array([1.0, 0.2, 0.2])
Id = np.identity(2)
Z_mat = np.array([[1, 0], [0, -1]])
X_mat = np.array([[0, 1], [1, 0]])

A_0 = np.identity(8)
A_1 = np.kron(np.kron(X_mat, Z_mat), Id)
A_2 = np.kron(np.kron(X_mat, Id), Id)

A_num = c[0] * A_0 + c[1] * A_1 + c[2] * A_2
b = np.ones(8) / np.sqrt(8)

# We can print the explicit values of  𝐴  and  𝑏 :
print("A = \n", A_num)
print("b = \n", b)

# The solution can be computed via a matrix inversion:
A_inv = np.linalg.inv(A_num)
x = np.dot(A_inv, b)

# Finally, in order to compare x with the quantum state $|x\rangle$, we normalize
# and square its elements.
c_probs = (x / np.linalg.norm(x)) ** 2

# Comparison
# Let us print the classical result.
print("classical result")
print("x_n^2 =\n", c_probs)

# The previous probabilities should match the following quantum state probabilities.
print("VQLS result")
print("|<x|n>|^2=\n", q_probs)
```

    Elapsed time: 5.9850 seconds
    A =
     [[1.  0.  0.  0.  0.4 0.  0.  0. ]
     [0.  1.  0.  0.  0.  0.4 0.  0. ]
     [0.  0.  1.  0.  0.  0.  0.  0. ]
     [0.  0.  0.  1.  0.  0.  0.  0. ]
     [0.4 0.  0.  0.  1.  0.  0.  0. ]
     [0.  0.4 0.  0.  0.  1.  0.  0. ]
     [0.  0.  0.  0.  0.  0.  1.  0. ]
     [0.  0.  0.  0.  0.  0.  0.  1. ]]
    b =
     [0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339
     0.35355339 0.35355339]
    classical result
    x_n^2 =
     [0.08445946 0.08445946 0.16554054 0.16554054 0.08445946 0.08445946
     0.16554054 0.16554054]
    VQLS result
    |<x|n>|^2=
     [0.08447514 0.08445027 0.16557213 0.16552339 0.08446807 0.0844432
     0.16555827 0.16550953]

```python

```


# Linear algebra overview projection example

Linear algebra is the study of **vectors** and **linear transformations**. This notebook introduces concepts form linear algebra in a birds-eye overview. The goal is not to get into the details, but to give the reader a taste of the different types of thinking: computational, geometrical, and theoretical, that are used in linear algebra.

## Plan

  - 1. Prerequisites
  - 2. Vectors
    - Definition
    - Geomterical interpretation
    - Dot product
  - Projections
    - Projection operation
    - Example: projection onto the line with equation $x-y=0$
  - Vector functions
    - Linear property: $f(a\mathbf{x} + b\mathbf{y}) = af(\mathbf{x}) + bf(\mathbf{y})$
    - Projection transformation P
  - Matrix representation of linear transformations
    - Linear transformation <--> Matrix-vector product equivalence
    - Show matrix representation M_P of Projection transformation P
  - Matrices
    - Definition
    - Matrix operations
      - Matrix-vector product
      - Matrix-matrix product
      - Trace
      - Determinant
      - Matrix inverse
      - Matrix equations
    - Reduced row echelon form

```python
# setup SymPy
from sympy import *
x, y, z, t = symbols('x y z t')
init_printing()

# a vector is a special type of matrix (an n-vector is either a nx1 or a 1xn matrix)
Vector = Matrix   # define alias Vector so I don't have to explain this during video

# setup plotting
%matplotlib inline
import matplotlib.pyplot as mpl
from plot_helpers import plot_vec, plot_vecs, plot_line, plot_plane, autoscale_arrows
```

# Prerequisites

Linear algebra builds upon high school math concepts like:
  - Geometry (lines, curves, areas, triangles)
  - Numbers (integers, rationals, reals, complex numbers)
  - Functions ($f(x)$ takes an input $x$ and produces an output $y$)

# Vectors

```python
# define two vectors
u = Vector([1,1])
v = Vector([1,-1])
u
```

```python
v
```

```python
plot_vecs(u, v)
autoscale_arrows()
```

### Vector addition

```python
# graphical
plot_vecs(u,v)
plot_vec(v, at=u, color='b')
plot_vec(u+v, color='r')
autoscale_arrows()

# algebraic
u+v
```

### Vector length $\|\vec{u}\|$

```python
u.norm()
```

### Unit-length vectors $\hat{u}$

```python
uhat = u/u.norm()

plot_vecs(u, uhat)

uhat
```

## Dot product

### Definition

The dot product of two vectors is proporional to the lengths for which the two vectors extend in the same direction.

If $\vec{u}=(u_1,u_2)$ and $\vec{v}=(v_1,v_2)$, then:

$$
  \vec{u} \cdot \vec{v} = u_1v_1 + u_2v_2 = \|\vec{u}\| \|\vec{v}\| \cos \theta_{uv},
$$

where $\theta_{uv}$ is the angle between the vectors.

```python
u = Vector([2,2])
v = Vector([3,0])

plot_vecs(u,v)
autoscale_arrows()

u.dot(v)
```

### Intuition

```python
# split the vector u into two parts:
u_parallel_to_v = Vector([2,0])
u_perp_to_v = Vector([0,2])

plot_vecs(u, v, u_parallel_to_v, u_perp_to_v)
autoscale_arrows()

u == u_parallel_to_v + u_perp_to_v
```

```python
# the dot product uses only the part of u that is parallel to v
u.dot(v) == u_parallel_to_v.dot(v) == u_parallel_to_v.norm()*v.norm()
```

    True

```python
# two vetors that are perpendicular, have zero dot product together
u_perp_to_v.dot(v)
```

# Projections

A projection of the vector $\vec{v}$ in the direction $\vec{d}$ is denoted $\Pi_{\vec{d}}(\vec{v})$. The formula for computing the projections uses the dot product operation:

$$
   \Pi_{\vec{d}}(\vec{v})
      \ \equiv \
      (\vec{v} \cdot \hat{d}) \hat{d}
      \ =  \
      \left(\vec{v} \cdot \frac{\vec{d}}{\|\vec{d}\|} \right) \frac{\vec{d}}{\|\vec{d}\|}.
$$

## General projection operation

```python
def proj(v, d):
    """Computes the projection of vector `v` onto direction `d`."""
    return v.dot( d/d.norm() )*( d/d.norm() )
```

```python
v = Vector([2,2])
d = Vector([3,0])
proj_v_on_d = proj(v,d)

plot_vecs(d, v, proj_v_on_d)
autoscale_arrows()
```

Projections play an important role in physics. For example, when solving a two dimensional projectile problem we often decompose vector quantities like forces $\vec{F}$, velocities $\vec{v}$, and momenta $\vec{p}$ into their $x$- and $y$-components: $(F_x,F_y)$, $(v_x,v_y)$, and $(p_x,p_y)$. This decomposition of vectors can transform a complicated two-dimensional problem into two simpler one-dimensaional problems, which can be solved independetly.

## Example: projection onto the line with equation $y=x$

```python
# The line with equation y = x can also be written as a paramteric equation
# [x,y] = [0,0] + s*[1,1] where d = [1,1] is called the direction vector the line
d = Vector([1,1])

plot_line(d,[0,0])
```

## Take 1: using projection operation

```python
# want a function that computes the projection onto the line with equation y = x for any vec
def P(vec):
    """Compute the projection of vector `vec` onto line y=x."""
    return proj(vec, d)

v = Vector([5,0])

plot_line(d,[0,0])
plot_vecs(v, P(v))
P(v)
```

# Vector functions

Observe that the function `P` is a *vector function*—a function that takes vectors as inputs and produces vectors as outputs. In mathematical notation we write this as

$$
  P : \mathbb{R}^2 \to \mathbb{R}^2.
$$

## Linear property:

A *linear transformation* $T$ is a vector function that obeys the linear property:

$$
 T(a\vec{x} + b\vec{y}) = aT(\vec{x}) + bT(\vec{y}).
$$

## Take 2: projection transformation P

The projection $P$ is a linear transformation, so it obeeys:

$$
 P\left( \begin{bmatrix}a \\ b \end{bmatrix} \right)
 = P(a\hat{\imath} + b\hat{\jmath})
 = aP(\hat{\imath}) + bP(\hat{\jmath}).
$$

```python
ihat = Vector([1,0])
jhat = Vector([0,1])

Pihat = P(ihat)
Pjhat = P(jhat)

Pihat, Pjhat
```

```python
def P2(vec):
    """Compute the projection of vector `vec` onto line y=x."""
    return vec[0]*Pihat + vec[1]*Pjhat

v = Vector([5,0])

plot_line(d,[0,0])
plot_vecs(v, P2(v))
```

## Take 3: linear transformation as matrix-vector product

## Matrix definition

$$
  \alpha \vec{u} + \beta \vec{v}
  =
  \alpha
  \begin{bmatrix}u_1 \\ u_2 \end{bmatrix}
  +
  \beta
  \begin{bmatrix}v_1 \\ v_2 \end{bmatrix}
  =
  \begin{bmatrix}u_1 & v_1 \\ u_2 & v_2 \end{bmatrix}
  \!
  \begin{bmatrix} \alpha \\ \beta \end{bmatrix}.
$$

```python
M_P = Matrix([[1,1],
              [1,1]])/2

M_P
```

```python
def P3(vec):
    """Compute the projection of vector `vec` onto the line y=x."""
    return M_P*vec

v = Vector([4,0])

plot_line(d, [0,0])
plot_vecs(v, P3(v))
```

```python
M_P.shape
```

Equivalence relationship between linear transformstions $T$ and matrices $M_T$:

$$
  T : \mathbb{R}^n \to \mathbb{R}^m
  \qquad
  \Leftrightarrow
  \qquad
  M_T \in \mathbb{R}^{m \times n}
$$

# Matrices

A matrix is a two-dimensional array of numbers.

### Example

```python
A = Matrix([[1,2],
            [3,4],
            [5,6]])
A
```

```python
A.shape
```

## Matrix operations

  - Addition (denoted $A+B$)
  - Subtraction, the inverse of addition (denoted $A-B$)
  - Scaling by a constant $\alpha$  (denoted $\alpha A$)
  - Matrix-vector product (denoted $A\vec{x}$)
  - Matrix product (denoted $AB$)
  - Matrix inverse (denoted $A^{-1}$)
  - Trace (denoted $\textrm{Tr}(A)$)
  - Determinant (denoted $\textrm{det}(A)$ or $|A|$)

### Matrix-vector product

$$
  A \vec{x}
  \quad
  \Leftrightarrow
  \quad
    \begin{bmatrix}
    a_{11} & a_{12} \\
    a_{21} & a_{22} \\
    a_{31} & a_{32}
    \end{bmatrix}
    \!\!
    \begin{bmatrix}
    x_1 \\
    x_2
    \end{bmatrix}
 \equiv
    \begin{bmatrix}
    x_1a_{11} + x_2a_{12} \\
    x_1a_{21} + x_2a_{22} \\
    x_1a_{31} + x_2a_{32}
    \end{bmatrix}
$$

The matrix-vector product is defined this way so it can represent linear transformations.

```python
a_11, a_12, a_21, a_22, a_31, a_32 = symbols('a_11 a_12 a_21 a_22 a_31 a_32')
x_1, x_2 = symbols('x_1 x_2')

A = Matrix([
        [a_11, a_12],
        [a_21, a_22],
        [a_31, a_32]])
x = Vector([x_1,x_2])

A*x
```

### Matrix-matrix product

$$
  AB
  \quad \Leftrightarrow  \quad
\begin{bmatrix}
 a_{11} 		&  a_{12}	 \\[1.5mm]
 a_{21} 		&  a_{22}	 \\[1.5mm]
 a_{31} 		&  a_{32}
\end{bmatrix}
\!\!
\begin{bmatrix}
 b_{11} 		&  b_{12}	 \\[1.5mm]
 b_{21} 		&  b_{22}	 \\
\end{bmatrix}
=
\begin{bmatrix}
a_{11}b_{11} + a_{12}b_{21} 	&  a_{11}b_{12} + a_{12}b_{22} 	 \\[1.5mm]
a_{21}b_{11} + a_{22}b_{21} 	&  a_{21}b_{12} + a_{22}b_{22} 	 \\[1.5mm]
a_{31}b_{11} + a_{32}b_{21} 	&  a_{31}b_{12} + a_{32}b_{22}
\end{bmatrix}
$$

```python
b_11, b_12, b_21, b_22 = symbols('b_11 b_12 b_21 b_22')
B = Matrix([[b_11, b_12],
            [b_21, b_22]])

A*B
```

```python
#  (AB)_ij = dot product of ith row of A with jth col of B
(A*B)[2,1] == A[2,:].dot( B[:,1])
```

    True

The matrix-matrix product implements **composition of linear transformations**:

```python
A*(B*x)
```

```python
expand( A*(B*x) ) == expand( (A*B)*x )
```

    True

```python
# analogy with ordinary functions...
x = symbols('x')
def f(x):
    return 2*x

def g(x):
    return  3*x

f(g(x))
```

```python
def h(x):
    return 6*x

h(x)
```

### Matrix inverse

For an invertible matrix $A$, the matrix inverse $A^{-1}$ acts to undo the effects of $A$:

$$
 A^{-1} A \vec{v} = \vec{v}.
$$

```python
A = Matrix([[1,2],
            [3,9]])

A.inv()
```

```python
A.inv()*A
```

### Matrix equations

Suppose we're given the equation
$$
 A\vec{x} = \vec{b}
$$
and we want to solve for $\vec{x}$.

One way to find $\vec{x}$ is to multiply both sides of the equation by $A^{-1}$:
$$
 A^{-1}A\vec{x} = A^{-1}\vec{b}
$$
since $A^{-1}$ cancels $A$ we obtain:
$$
 \vec{x} = A^{-1}\vec{b}.
$$

#### Example

$$
    \begin{bmatrix}
    1 & 2 \\
    3 & 9
    \end{bmatrix}
    \!\!
    \begin{bmatrix}
    x_1 \\
    x_2
    \end{bmatrix}
    =
    \begin{bmatrix}
    5 \\
    21
    \end{bmatrix}
\qquad
\Rightarrow
\qquad
    \begin{bmatrix}
    x_1 \\
    x_2
    \end{bmatrix}
    =
    \begin{bmatrix}
    1 & 2 \\
    3 & 9
    \end{bmatrix}^{-1}
    \!\!
    \begin{bmatrix}
    5 \\
    21
    \end{bmatrix}
$$

```python
A = Matrix([[1,2],
            [3,9]])

b = Vector([5,21])

x = A.inv()*b
x
```

```python
# verify A*x == b
A*x
```

## Gauss-Jordan elimination procedure for matrices

Used to transform any matrix into *reduced row echelon form* (RREF) by performing row operations.
This procedure has applications to solving equations, finding matrix inverses, and solving certain vector space problems.

# Linear transformations

We can understand the properties of linear transformations $T$, and their matrix representations $M_T$ in analogy with ordinary functions:

\begin{align*}
    \textrm{function }
     f:\mathbb{R}\to \mathbb{R}
         & \ \Leftrightarrow \,
         \begin{array}{l}
    \textrm{linear transformation }
    T:\mathbb{R}^{n}\! \to \mathbb{R}^{m}		 \\
    \textrm{represented by the matrix } M_T \in \mathbb{R}^{m \times n}
    \end{array} \\
    %
    \textrm{input }  x\in \mathbb{R}
         & \ \Leftrightarrow \
        \textrm{input }  \vec{x} \in \mathbb{R}^n           \\
    %\textrm{compute }
    \textrm{output } f(x) \in \mathbb{R}
         & \ \Leftrightarrow \
    %      \textrm{compute matrix-vector product }
     \textrm{output }      T(\vec{x}) \equiv M_T\vec{x} \in \mathbb{R}^m \\
    %\textrm{function composition  }
    g\circ\! f \: (x) =  g(f(x))
         & \ \Leftrightarrow \
    %      \textrm{matrix product }
    S(T(\vec{x})) \equiv M_SM_T \vec{x} \\
    \textrm{function inverse } f^{-1}
         & \ \Leftrightarrow \
          \textrm{matrix inverse } M_T^{-1} \\
    \textrm{zeros of } f
         & \ \Leftrightarrow \
          \textrm{kernel of } T  \equiv  \textrm{null space of } M_T \equiv \mathcal{N}(A)  \\
    \textrm{image of } f
         & \ \Leftrightarrow \
             \begin{array}{l}
             \textrm{image of } T \equiv  \textrm{column space of } M_T  \equiv \mathcal{C}(A)
              \end{array}
\end{align*}

Observe we refer to the linear transformation $T$ and its matrix representation $M_T$ interchangeably.


```python
from galgebra.ga import Ga
from sympy import symbols
from galgebra.printer import Format

Format()
coords = (et,ex,ey,ez) = symbols('t,x,y,z',real=True)
base=Ga('e*t|x|y|z',g=[1,-1,-1,-1],coords=symbols('t,x,y,z',real=True),wedge=False)

potential=base.mv('phi','vector',f=True)
potential
```

\begin{equation*}phi = \phi ^{t}  \boldsymbol{e}_{t} + \phi ^{x}  \boldsymbol{e}_{x} + \phi ^{y}  \boldsymbol{e}_{y} + \phi ^{z}  \boldsymbol{e}_{z}\end{equation*}

```python
field=base.grad*potential
field
```

\begin{equation*}\left ( \partial_{t} \phi ^{t}  + \partial_{x} \phi ^{x}  + \partial_{y} \phi ^{y}  + \partial_{z} \phi ^{z} \right )  + \left ( \partial_{x} \phi ^{t}  + \partial_{t} \phi ^{x} \right ) \boldsymbol{e}_{tx} + \left ( \partial_{y} \phi ^{t}  + \partial_{t} \phi ^{y} \right ) \boldsymbol{e}_{ty} + \left ( \partial_{z} \phi ^{t}  + \partial_{t} \phi ^{z} \right ) \boldsymbol{e}_{tz} + \left ( \partial_{y} \phi ^{x}  - \partial_{x} \phi ^{y} \right ) \boldsymbol{e}_{xy} + \left ( \partial_{z} \phi ^{x}  - \partial_{x} \phi ^{z} \right ) \boldsymbol{e}_{xz} + \left ( \partial_{z} \phi ^{y}  - \partial_{y} \phi ^{z} \right ) \boldsymbol{e}_{yz}\end{equation*}

```python
grad_field = base.grad*field
grad_field
```

\begin{equation*}\left ( \partial^{2}_{t} \phi ^{t}  - \partial^{2}_{x} \phi ^{t}  - \partial^{2}_{y} \phi ^{t}  - \partial^{2}_{z} \phi ^{t} \right ) \boldsymbol{e}_{t} + \left ( \partial^{2}_{t} \phi ^{x}  - \partial^{2}_{x} \phi ^{x}  - \partial^{2}_{y} \phi ^{x}  - \partial^{2}_{z} \phi ^{x} \right ) \boldsymbol{e}_{x} + \left ( \partial^{2}_{t} \phi ^{y}  - \partial^{2}_{x} \phi ^{y}  - \partial^{2}_{y} \phi ^{y}  - \partial^{2}_{z} \phi ^{y} \right ) \boldsymbol{e}_{y} + \left ( \partial^{2}_{t} \phi ^{z}  - \partial^{2}_{x} \phi ^{z}  - \partial^{2}_{y} \phi ^{z}  - \partial^{2}_{z} \phi ^{z} \right ) \boldsymbol{e}_{z}\end{equation*}

```python
part=field.proj([base.mv()[0]^base.mv()[1]])
part
```

\begin{equation*}\left ( \partial_{x} \phi ^{t}  + \partial_{t} \phi ^{x} \right ) \boldsymbol{e}_{tx}\end{equation*}

```python
dpart = base.grad*part
dpart
```

\begin{equation*}\left ( - \partial^{2}_{x} \phi ^{t}  - \partial_{t}\partial_{x} \phi ^{x} \right ) \boldsymbol{e}_{t} + \left ( \partial^{2}_{t} \phi ^{x}  + \partial_{t}\partial_{x} \phi ^{t} \right ) \boldsymbol{e}_{x} + \left ( - \partial_{x}\partial_{y} \phi ^{t}  - \partial_{t}\partial_{y} \phi ^{x} \right ) \boldsymbol{e}_{txy} + \left ( - \partial_{x}\partial_{z} \phi ^{t}  - \partial_{t}\partial_{z} \phi ^{x} \right ) \boldsymbol{e}_{txz}\end{equation*}


## Homework II: Logistic Regression, SVM and some computational drug design

__Given date:__ Saturday March 7

__Due date:__ Tuesday March 31

__Total__ : 30 pts + Bonus (One of the bonus question is on 3pts. For the other, it depends on what you can do)

# __Student: Muhammad Wajahat Mirza__

# **NetID: mwm356**

### Exercise I.1. Logistic regression (10pts)

Use the lines below to load the variables __HW2_ExI_X__ and __HW2_ExI_Y__. In this first exercise, you will learn a logistic regression classifier on this dataset. Recall that the logistig regression model takes the form

$$p(t=1|\mathbf{x}) = \sigma(\mathbf{\beta}^T\tilde{\mathbf{x}})$$

where $\tilde{\mathbf{x}} = [1, \mathbf{x}] = [1, x_1, x_2, \ldots, x_D]$. Consequently, we thus have

$$p(t = 0|\mathbf{x}) = 1 - \sigma(\mathbf{\beta}^T\tilde{\mathbf{x}})$$

we can then write the total probability that a point $\mathbf{x}$ will be from class $c = \left\{0,1\right\}$ as

$$p(t = c|\mathbf{x}) = p(t = 1|\mathbf{x})^{c} p(t = 0|\mathbf{x})^{1-c} $$

or equivalently

$$p(t = c|\mathbf{x}) = (\sigma(\mathbf{\beta}^T\tilde{\mathbf{x}}))^{c} (1-\sigma(\mathbf{\beta}^T\tilde{\mathbf{x}}))^{1-c}  = p^c (1-p)^{1-c}$$

which is a binomial distribution with probability of success $\sigma(\mathbf{\beta}^T\tilde{\mathbf{x}})$. If we assume that all the samples are independent, the probability of observing the dataset can read as the product

$$p(\left\{\mathbf{x}_i, t_i\right\}) = \prod_{i=1}^N p(t = t(\mathbf{x}_i)|\mathbf{x}_i)\quad  (*)$$

We can then try to learn the parameters $\mathbf{\beta}$ that maximize this probability (i.e such that the probability $p(t = t_i |\mathbf{x}_i)$ is high for every sample pair in the dataset. To do this, we take the negative logarithm of $(*)$ which gives

$$-\log \prod_{i=1}^N p(t = t(\mathbf{x}_i)|\mathbf{x}_i) = -\sum_{i=1}^N c_i \log(\sigma(\mathbf{\beta}^T\tilde{\mathbf{x}}_i)) - \sum_{i=1}^N (1-c_i)\log(1-\sigma(\mathbf{\beta}^T\tilde{\mathbf{x}_i}))\quad  (**)$$

and find the $\mathbf{\beta}$ that minimizes this expression.

```python
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

X = np.load('Ex1_HW2_X.npy')
Y = np.load('Ex1_HW2_t.npy')

X_class1 = X[Y==0,:]
X_class2 = X[Y==1,:]

plt.scatter(X_class1[:, 0], X_class1[:, 1], marker='o', c='r',
            s=35)
plt.scatter(X_class2[:, 0], X_class2[:, 1], marker='o', c='b',
            s=35)

plt.show()

```

    <Figure size 640x480 with 1 Axes>

# __Question I.1.1 Logistic from scratch (7pts)__
Write a function that takes as inputs a set of training pairs $\left\{\mathbf{x}_i, t_i\right\}$ such as those stored in the variables $\texttt{HW2_ExI_X}$ and $\texttt{HW2_ExI_Y}$, and return the logistic regression classifier by learning it through gradient descent from the minimization of the negative log likelihood function $(**)$. Apply your function to the dataset given above and plot the discriminant function on top of this dataset.

# Solution

# 1. Mathematical base

**Our Goal is:**  $ 0  \leq h_\beta (x)  \leq 1 $

For that, we define our Sigmoid Function as:

$$ h_{(\beta)}(x) = \sigma(\mathbf{\beta}^T\tilde{\mathbf{x}}) = \frac{\mathrm{1} }{\mathrm{1} + e^{- \beta^Tx}} $$

Assign discrete value of $ 0 $  or $1$ to our dataset by using **Decision Boundary** through:

$$ h_\beta (x) \geq 0.5 \Rightarrow t = \mathrm{1} $$
$$ h_\beta (x) < 0.5 \Rightarrow t = \mathrm{0} $$

__Cost Function__

$$ l_{(\beta)} = -\frac{1}{N}\sum_{i=1}^N (t^{(i)}(\log (h_\beta (x^{(i)})) + ((1 - t^{(i)})(\log(1 - h_\beta (x^{(i)}))$$

**Vectorized form**

$$ h = \sigma(X\beta)  = \frac{\mathrm{1} }{\mathrm{1} + e^{-X\beta}} $$
$$ l_{(\beta)} = \frac{1}{N} ((- t ^ T \log(h)) - ((1-t)^Tlog(1-h)))$$

__Gradient Descent__

$$ \beta_{j} := \beta_{j} - \eta .\frac{1}{N} \sum_{i=1}^N(h_\beta (x^{(i)}) - t^{(i)}).x_{j}^{(i)}) $$
where $$ h_\beta (x^{(i)}) = \frac{\mathrm{1} }{\mathrm{1} + e^{- \beta^Tx^{(i)}}} $$
**Vectorized Form:**
$$ \beta_{j} := \beta_{j} - \eta .\frac{1}{N}.X^T(\sigma(X\beta) - \overrightarrow{t}) $$

# 2. Programming

```python
# start by coding the sigmoid function.
def sigmoid(X, beta):
    beta_x = np.dot(X, beta[1:]) + beta[0]
    sigmoid  = np.true_divide(1, (1+np.exp(-beta_x)))
    return sigmoid

def Cost_function(Y,X_beta):
    loss_beta = np.sum((-Y.dot(np.log(X_beta)) - ((1-Y).dot(np.log(1 - X_beta))))).mean()
    return loss_beta

def gradient_descent(X,Y,beta,eta,num_iter):
    cost = []
    for i in range(num_iter):
        X_beta = sigmoid(X,beta)
        error = X_beta - Y
        gradient = X.T.dot(error)
        beta[0] = beta[0] - eta * error.sum()
        beta[1:] = beta[1:] - eta * gradient
        cost.append(Cost_function(Y, X_beta))
        ''' IF you want to check Cost, uncomment following lines'''
#         if (i % 20 == 0):
#             print("iter: {}  cost={}".format(i, cost))
#     print("This is COST: ", cost)

    return cost, beta
```

```python
def logistic_regression(X,num_iter):
    m, N = X.shape
    beta = np.zeros(1 + N)
    eta = 0.01
    cost, beta = gradient_descent(X, Y, beta, eta, num_iter)

    plt.plot(range(1, len(cost) + 1), cost)
    plt.xlabel('Iterations')
    plt.ylabel('Cost')
    plt.title('Logistic Regression')

    print ('Logisitc Regression bias:', beta[0])
    print ('Logisitc Regression coefficients :', beta[1:])

    return beta

# def boundary_line(X,beta):
#     decrete_0_1 = np.where(sigmoid(X,beta) >= 0.5, 1, 0)
#     return decrete_0_1
# boundary_line(X,beta)
```

```python
# First way to plot using class lab code. My problem is with the prediction line
def plot_boundary_line(X,Y,beta):
    x1min = np.amin(X[:,0])
    x1max = np.amax(X[:,0])

    x2min = np.amin(X[:,1])
    x2max = np.amax(X[:,1])

    x1mesh = np.linspace(x1min, x1max, 100)
    x2mesh = np.linspace(x2min, x2max, 100)

    x1MeshMat, x2MeshMat = np.meshgrid(x1mesh.reshape(-1,1), x2mesh.reshape(-1,1))

    x1MeshMat = x1MeshMat.flatten()
    x2MeshMat = x2MeshMat.flatten()

    X12 = np.vstack((x1MeshMat, x2MeshMat)).T

    totalNumPointsGrid = len(x1MeshMat)

    addon = np.ones((totalNumPointsGrid, 1))

    pointsGrid = np.hstack((addon.reshape(-1,1), X12))

    prediction = np.dot(pointsGrid, beta)

    prediction[prediction<.5] = 0
    prediction[prediction>.5] = 1

    coordinatesX1 = np.reshape(x1MeshMat, (100,100))
    coordinatesX2 = np.reshape(x2MeshMat, (100,100))
    reshaped_prediction = np.reshape(prediction, (100,100))

    fig, ax = plt.subplots(constrained_layout=True)
    plt.scatter(X[:,0], X[:,1], c = Y)
    ax.contourf(coordinatesX1, coordinatesX2, reshaped_prediction, alpha=0.1)
    plt.show()

beta = logistic_regression(X,160)
plot_boundary_line(X,Y,beta)
```

# [Alternative(Optional)] Solution to I.1 with STD skewed Data

```python
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

def STD_skewed_data(X):
    X[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
    X[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()

    print("Here, the mean of the Data is taken followed by taking STD of it. \
    This contraints the data which is why we get a slightly better result on boundary line than un-constraint work as shown above")
    beta = logistic_regression(X,20)
    plot_boundary_line(X,Y,beta)
STD_skewed_data(X)
```

# __Question I.1.2 Logistic vs OLS (3pts)__
Consider the dataset given below. On this dataset, __using the corresponding modules from scikit__, learn a linear regression classifier. Then learn a logistic classifier. How do the two approaches compare with each other? Display each each of the classifiers using 'meshgrid + contourf'

```python
import matplotlib.pyplot as plt
import numpy as np
import scipy.io as sio

from sklearn.datasets import make_classification

data_class1 = sio.loadmat('XHW2_EX2_Class1.mat')['XHW2_EX2_Class1']
data_class2 = sio.loadmat('XHW2_EX2_Class2.mat')['XHW2_EX2_Class2']

print(data_class1.shape)
print(data_class2.shape)
plt.scatter(data_class1[:, 0], data_class1[:, 1], marker='o', c='r',
            s=35)
plt.scatter(data_class2[:, 0], data_class2[:, 1], marker='o', c='b',
            s=35)

plt.show()
```

# Solution: Logistic VS OLS Classification Using Scikit-Learn

# 1. For Logistic Regression Classifier

1. Create a Function that does logistic regression using Scikit Learn
2. It will output a C-SVC linear classification which is with regularized parameters.
3. It will also output a Logistic Regression Classifier without regularization
3. Write a function that displays the results

# 2. For Linear Regression Classifier

1. Write a Function to display Linear Regression Classification
2. Call the Function for C-SVC Classification for reference

```python
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.linear_model import LinearRegression
import scipy.io as sio
from sklearn.svm import SVC

def Regression_classifier_Scikit(data_class1, data_clas2):
    Class1 = np.hstack((data_class1[:,0].reshape(-1,1),data_class1[:,1].reshape(-1,1)))
    Class2 = np.hstack((data_class2[:,0].reshape(-1,1),data_class2[:,1].reshape(-1,1)))
    data = np.vstack((Class1,Class2))
    print("Data shape: ",data.shape)

    target1 = np.ones((np.shape(data_class1[:,1])[0],1))
    target2 = -np.ones((np.shape(data_class2[:,1])[0],1))
    target = np.vstack((target1,target2))
    print("Target shape: ",target.shape)

    clf = SVC(kernel = 'linear',gamma = 5,max_iter = 500)
    clf1 = clf.fit(data,target)

    clf2 = LogisticRegression(random_state = 0,solver ="lbfgs",max_iter = 18)
    clf2 = clf2.fit(data,target)

    clf3 = LinearRegression()
    clf3 = clf3.fit(data,target)

    return clf1, clf2,clf3, data
```

```python
def plot_classifer(clf1,clf2,clf3,data):
    x1min = np.amin(data[:,0])
    x1max = np.amax(data[:,0])
    x2min = np.amin(data[:,1])
    x2max = np.amax(data[:,1])

    x1mesh = np.linspace(x1min, x1max, 100)
    x2mesh = np.linspace(x2min, x2max, 100)

    x1MeshMat, x2MeshMat = np.meshgrid(x1mesh, x2mesh)

    x1MeshMat_vec = x1MeshMat.flatten()
    x2MeshMat_vec = x2MeshMat.flatten()

    Mesh_grid = np.vstack((x1MeshMat_vec, x2MeshMat_vec)).T

    target1 = clf1.predict(Mesh_grid)
    print("target SVC vector 1: ",target1)
    target2 = clf2.predict(Mesh_grid)
    print("target Logistic vector 2:",target2.T)

    target3 = clf3.predict(Mesh_grid)
    print("target OLS vector 2:",target2.T)

    import warnings
    warnings.filterwarnings("ignore")
    plt.contourf(x1MeshMat,x2MeshMat, np.reshape(target1, np.shape(x1MeshMat)),alpha = 0.1)
    plt.scatter(data_class1[:, 0], data_class1[:, 1], marker='o', c='r',s=35)
    plt.scatter(data_class2[:, 0], data_class2[:, 1], marker='o', c='b',s=35)
    plt.show()
    print("Above is classification with the C-SVC Regularization Parameter!")

    plt.contourf(x1MeshMat,x2MeshMat, np.reshape(target2.T, np.shape(x1MeshMat)),alpha = 0.1)
    plt.scatter(data_class1[:, 0], data_class1[:, 1], marker='o', c='r',s=35)
    plt.scatter(data_class2[:, 0], data_class2[:, 1], marker='o', c='b',s=35)
    plt.show()
    print("Above is Logistic Classification!")

    plt.contourf(x1MeshMat,x2MeshMat, np.reshape(target3.T, np.shape(x1MeshMat)),alpha = 0.1)
    plt.scatter(data_class1[:, 0], data_class1[:, 1], marker='o', c='r',s=35)
    plt.scatter(data_class2[:, 0], data_class2[:, 1], marker='o', c='b',s=35)
    plt.show()
    print("Above is Linear (OLS) Classification!")
```

```python
'''Display Results'''
clf1, clf2, clf3, data = Regression_classifier_Scikit(data_class1, data_class2)
plot_classifer(clf1,clf2,clf3,data)

```

## Exercise II: Maximal Margin classifier and non linearly separable data (10pts)

So far we have studied Maximal Margin classifier when the data was linearly separable. In this case, the plane will naturally position itself in between the two classes. The formulation in the linearly separable case is of the form

$$\max_{\mathbf{w}, b_0}\min_{i} \frac{y(\mathbf{x}^{(i)})t^{(i)}}{\|\mathbf{w}\|} = \max_{\mathbf{w}, b_0}\min_i \frac{(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)t^{(i)}}{\|\mathbf{w}\|}$$

This formulation is not very nice because the optimization variable appears at the denominator. Note that the formulation $(*)$ can be written as

\begin{align}
\max_{\gamma, \mathbf{w}, b_0} & \quad \frac{\gamma}{\|\mathbf{w}\|} \\
\text{subject to} &\quad  t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq \gamma
\end{align}

Here we simply introduce the parameter $\gamma$ to represent the margin (smallest distance between any point from the dataset and the plane). An important thing to notice when looking at $(**)$ is that any solution for $\mathbf{w}, b_0, \gamma$ can generate an other solution with the same objective by simply scaling it with a positive weight $\alpha$. Indeed if $\mathbf{w},b_0, \gamma$ is an optimal solution to $(**)$, it is easy to check that $\alpha \mathbf{w}, \alpha \gamma, \alpha b_0$ will be a valid solution as well. Since we only need one solution, we can choose to optimize over the set of solution such that $\gamma = 1$. Concretely this means that for each 'line' of solution $(\alpha \gamma, \alpha b_0, \alpha \mathbf{w})$, we only retain the $\alpha$ corresponding to $\alpha\gamma = 1$. The problem then becomes

\begin{align}
\max_{\mathbf{w}, b_0} & \quad \frac{1}{\|\mathbf{w}\|} \\
\text{subject to} &\quad  t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1.
\end{align}

To get the final formulation, note that the maximum value for $1/\|\mathbf{w}\|$ is achieved when $\|\mathbf{w}\|$ is the smallest. We an thus solve the problem

\begin{align}
\min_{\mathbf{w}, b_0} &\quad  \|\mathbf{w}\| \\
\text{subject to} &\quad  t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1., \quad\quad (***)
\end{align}

So far we have assumed that the dataset was linearly separable. In this case, all the points will satisfy $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)\geq 1$. Sometimes, however, we could be in a situation where the dataset is not linearly separable and there are points which will be misclassified so that $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)$ might be negative. To account for such situation, we will consider slack variables $\xi_i$ and use a more general formulation of the form

\begin{align}
\min_{\mathbf{w}, b_0} &\quad  \|\mathbf{w}\| + C\sum_{i} \xi_i \\
\text{subject to} &\quad  t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1 - \xi_i., \quad\quad (***)
\end{align}

It is in fact possible to write formulation $(***)$ as an unconstrained optimization problem. Recall that we decided to set the minimum margin to $1$. This in particular means that all the points that are correctly classified should satisfy $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)\geq 1$. The approach encoded in $(***)$ corresponds to penalazing those points for which $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)<1$. As soon as we have a positive value for one of the variables

# Question II.1 Hinge Loss (3pts)

We consider the hinge loss $\ell(x, y) = \max(0, 1-x\cdot y)$ for $x = t^{(i)}$ and $y = (\mathbf{w}^T\mathbf{x}^{(i)} + b_0)$. Note that if $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)\geq 0$ (meaning the pair $\left\{\mathbf{x}^{(i)}\right\}$ is correctly classified), the output of the hinge function is $0$. On the other hand, if $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)<0$ (which corresponds to a pair that is incorrectly classified, as can occur when the data is not linearly separable), then the cost is $1 - t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)$ (i.e. we can think of this cost as by how much we violate the constraint $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)\geq 1$).  From this, we can now write the objective as

\begin{align}
\min_{\mathbf{w}, b_0} &\quad  \|\mathbf{w}\|^2 + C\sum_{i} \max(0, 1 - t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0))
\end{align}

Instead of weighting the constraints, we can weight the first term. This gives a similar formulation of the form

\begin{align}
\min_{\mathbf{w}, b_0} &\quad  \frac{1}{N}\sum_{i=1}^{N} \left\{\frac{\lambda}{2} \|\mathbf{w}\|^2 + \max(0, 1 - t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0))\right\}\quad (****)
\end{align}

The PEGASOS algorithm finds the corresponding Soft Margin classifier by applying batch gradient descent to this last objective.

What is the gradient of the Hinge loss?

# Solution:
**Hinge Loss**

__Mathematical Base__
$$
\ell(x, y) = \max(0, 1-x\cdot y)
$$
for $x = t^{(i)}$ and $y = (\mathbf{w}^T\mathbf{x}^{(i)} + b_0)$. Opening up yields:
$$
\ell(x, y) = \max(0, 1-t^{(i)}\cdot (\mathbf{w}^T\mathbf{x}^{(i)} + b_0))
$$

**Gradient of Hinge Loss yields:**
$$
∂l(x,y)=
\begin{cases}
0 & t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1\\
-t^{(i)}x^{(i)} & t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) < 1\\
\end{cases}
$$

As stated in question, objective function along with regulariation term is:
\begin{align}
\min_{\mathbf{w}, b_0} &\quad  \frac{1}{N}\sum_{i=1}^{N} \left\{\frac{\lambda}{2} \|\mathbf{w}\|^2 + \max(0, 1 - t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0))\right\}\quad
\end{align}

Gradient of Objective Function:
$$
∂ \text{obj func}=
\begin{cases}
\lambda w & t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1\\
\lambda w -t^{(i)}x^{(i)} & t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) < 1\\
\end{cases}
$$

## For this problem, Function " HingeLoss_Gradient " will be used

```python
def hingLoss_nonVectorized(X, t, w, b):
    '''Answer the question above by '''
    ''' Non-Vectorized Form Implementation of HingeLosee'''
    num_examples = X.shape[0]
    hingeLoss = 0.0
    for example in range(num_examples):
        x_i = X[example,:]
        t_i = t[example]
        hingeLoss += max(0,1 - t_i * (np.dot(w,x_i) + b) )
    print(hingeLoss)
    return hingeLoss

def hingLoss_vectorized(X, t, w, b):
    '''Vectorized Form Implementation of HingeLoss '''
    ## vectorized form
    y_yhat = t * (np.dot(X,w) + b)
    loss_per_example = np.maximum(0, 1 - y_yhat)
    hingeLoss = np.sum(loss_per_example)

    print(hingeLoss)
    return hingeLoss

def gradient_regularized_obj_func_using_hingLoss(X_i, t_i, w, b0, lambda0):
    ''' Calculates the Gradient of the HingeLoss'''
    gradient = lambda0 * w
    y_yhat = t_i * (np.dot(w.T,X_i) + b0)
    if y_yhat < 1:
        gradient -= X_i * t_i
    print("Grad: ",gradient)
    return gradient

''' For this Question, HingeLoss_Gradient will be used: Dual-combined Code'''
def hingeLoss_Gradient(X,t,w):
    cost = np.dot(t,w)
    t = np.reshape(t,(len(t),1))
    if cost < 1:
        grad = np.dot(np.transpose(X),t)
        print("grad: ",grad)
        return grad
    else:
        return 0
```

# Question II.2 Maximum Margin Classifier from scratch (7pts)

Using your answer to question II.2.1 above, implement the PEGASOS algorithm. Use a simple batch gradient descent approach with a sufficiently small learning rate (let us say $\eta = 1e-4$). Update the lambda as $1/t$ where $t$ is the step counter.

# Mathematical Base for Pegasos
The update step for $w$ in GD is as follows:
$$
w = w - \eta \partial J(w)
$$

There are two solutions to $\partial J(w)$ depending upon the size of $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0)$.

If $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) \geq 1$,

$$
w = w - \eta \lambda w =  (1-\eta \lambda)w
$$

If $t^{(i)}(\mathbf{w}^T\mathbf{x}^{(i)} + b_0) < 1$,

$$
w = w - \eta (\lambda w - t^{(i)}x^{(i)})
$$
$$
w = w - \eta \lambda w + \eta t^{(i)}x^{(i)}
$$
$$
w = (1 - \eta \lambda) w + \eta t^{(i)}x^{(i)}
$$

## For this Question, Func " approximatePEGASOS "  is used

```python
def approximatePEGASOS(X, t, lambda0, eta,max_iter):
        '''The function takes as input a set of sample pairs {x^{(i)}, t_^{(i)}} as well as an initial value for b0
    and a value for the learning rate eta. It should return the weight vector w and the bias b0
    for the Max Margin Classifier'''

        weight = np.zeros((len(X[0])+1,1))

        X = np.hstack((X,np.ones((len(X),1))))
        for i in range(max_iter):
            y = np.dot(X,weight)
            returned_grad = hingeLoss_Gradient(X,t,y)
            updated_grad = lambda0*weight - returned_grad
            weight = weight - eta*updated_grad
        print("weight: ",weight)
        return weight

''' Following coding is done from the PEGASOS Original Algorithm'''
def Approximate_PEGASOS(X, t, lambda0, eta, max_epoch):

    w = np.zeros((X.shape[0]))
    b0 = 0
    epoch = 1
    #eta = np.exp(-4)
    num_instances, num_features = X.shape[0], X.shape[1]
    while(epoch <= max_epoch):
        randomly_shuffled = np.random.permutation(num_instances)
        for r_instance in randomly_shuffled:
            gradient_single_point =  gradient_regularized_obj_func_using_hingLoss(X[r_instance], t[r_instance], w[r_instance], b0, lambda0)
            print("eta", eta * gradient_single_point)
            w = w - eta * gradient_single_point
        epoch += 1

#In case of following Pegosos line by line
#             y_yhat = t[r_instance] * (np.dot(X[r_instance],w) + b0)
#             if y_yhat < 1:
#                 w = (1 - eta*lambda0) * w + (eta * t[r_instance] * X[r_instance])
#             else:
#                 w = (1 - eta*lambda0) * w
    return w, b0
```

## Apply the algorithm to the dataset below

```python
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

X = np.load('Ex2_HW2_X.npy')
Y = np.load('Ex2_HW2_t.npy')

print(X.shape)
print(Y.shape)
XEx2_Class1 = X[Y==0,:]
XEx2_Class2 = X[Y==1,:]

plt.scatter(XEx2_Class1[:, 0], XEx2_Class1[:, 1], marker='o', c='r',
            s=35)
plt.scatter(XEx2_Class2[:, 0], XEx2_Class2[:, 1], marker='o', c='b',
            s=35)

plt.show()

```

### Following is the Display Function for PEGASOS to display classification or Boundary line

```python
def display_pegasos(X,Y,beta):
    x1min = np.amin(X[:,0])
    x1max = np.amax(X[:,0])
    x2min = np.amin(X[:,1])
    x2max = np.amax(X[:,1])

    x1mesh = np.linspace(x1min, x1max, 100)
    x2mesh = np.linspace(x2min, x2max, 100)

    x1MeshMat, x2MeshMat = np.meshgrid(x1mesh.reshape(-1,1), x2mesh.reshape(-1,1))
    x1MeshMat = x1MeshMat.flatten()
    x2MeshMat = x2MeshMat.flatten()

    X12 = np.vstack((x1MeshMat, x2MeshMat)).T
    totalNumPointsGrid = len(x1MeshMat)
    addon = np.ones((totalNumPointsGrid, 1))
    pointsGrid = np.hstack((addon.reshape(-1,1), X12))
    prediction = np.dot(pointsGrid, beta)

    prediction[prediction<.5] = 0
    prediction[prediction>.5] = 1

    coordinatesX1 = np.reshape(x1MeshMat, (100,100))
    coordinatesX2 = np.reshape(x2MeshMat, (100,100))
    reshaped_prediction = np.reshape(prediction, (100,100))

    fig, ax = plt.subplots(constrained_layout=True)
    plt.scatter(X[:,0], X[:,1], c = Y)
    ax.contourf(coordinatesX1, coordinatesX2, reshaped_prediction.T, alpha=0.1)
    plt.show()
```

### Heads up: If you face Shape error of (10000,3) and (4,1), re-run the cells as sometimes they carry information from other cells

```python
weight = approximatePEGASOS(X,Y,0.1, 0.01,1000)
X = np.hstack((X,np.ones((len(X),1))))
display_pegasos(X,Y,weight)
```

# Bonus II.3. dynamic learning rate. (3pts)
The exact PEGASOS algorithm updates the learning rate dynamically as $\eta_t = 1/{(\lambda \cdot  t)}$ for some particular choice of $\lambda$ and where $t$ denotes the step counter. Modify the approximate version of the PEGASOS algorithm that you derived in question II.2.2 to make it work with such a dynamical learning rate.

# Solution

Just as Approximate Pegasos Function, it will be same here except we will use dynamic learning rate defined as:
$$ \eta_t = 1/{(\lambda \cdot  t)} $$

```python
def dynamic_approximatePEGASOS(X, t, lambda0,max_iter):

        weight = np.zeros((len(X[0])+1,1))
        X = np.hstack((X,np.ones((len(X),1))))
        for i in range(1,max_iter,1):
            ''' Dynamic Eta Value'''
            eta = np.true_divide(1,(lambda0*i))

            y = np.dot(X,weight)
            returned_grad = hingeLoss_Gradient(X,t,y)
            updated_grad = lambda0*weight - returned_grad
            weight = weight - eta*updated_grad
        print("weight: ",weight)
        return weight
```

```python
X = np.load('Ex2_HW2_X.npy')
```

```python
weight = dynamic_approximatePEGASOS(X,Y,0.16,1000)
X = np.hstack((X,np.ones((len(X),1))))
display_pegasos(X,Y,weight)
```

# Exercise III: Computational drug design: Part I, Training on Thrombin (10pts)

With the recent spreading of the COVID-19 "Corona" Virus. Many initiatives relying on Machine Learning, have been taken to find new drugs that would be able to prevent the virus from infecting host cells. The Human Angiotensin-Converting Enzyme 2 (ACE2) has been proved by many studies to be the specific receptor for the Spike RBD of SARS-CoV. A common approach (followed for example for researchers at Oak Ridge National Laboratory) is to design compounds (such as the one shown in gray in the figure above) that would be able to bind to the SARS-CoV-2 spike protein (shown in cyan), thus making the virus unable to dock to the human ACE2 receptors (shown in purple in the figure below).

Source: [Physics.org](https://phys.org/news/2020-03-early-drug-compounds-supercomputing-combat.html)

We do not have enough data on the COVID-19 yet so we are going to train on an older computational drug design dataset from NIPS 2003. The general idea is the same. A Drug is a small organic molecule that can achieve its desired activity by binding to a target site on a receptor. The first step in the discovery of a new drug is usually to identify and isolate the receptor to which it should bind, followed by testing many small molecules for their ability to bind to the target site (see the [UCI ML Repo](https://archive.ics.uci.edu/ml/datasets/Dorothea) for more details). For this particular dataset, we are interested in checking which compounds can or cannot bind to thrombin.

# Question III.1 (4pts)
Start by downloading the DOROTHEA dataset on the [UCI ML website ](https://archive.ics.uci.edu/ml/datasets/Dorothea). Once you have downloaded the data, as we did for face recognition, use the PCA module from scikit-learn to reduce the dimension of your feature vectors from their initial size (100000) to about 100. Split the data into a training and test part using the train_test_split module from scikit learn

# Solution
 1. Create a Function to Read Data and Split the lines

 2. Create another Function to split the data into Test and Train using PCA

```python
import numpy as np
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split

def reading_data():
    label = open('dorothea_train.labels', "r")
    label = label.read()
    label = label.split()
    label = [int(i) for i in label]
    targets = np.array(label)

    f = open('dorothea_train.data', "r")
    line_read = f.readline()
    line_read = map(int, line_read.split())

    maxNumData = 800
    tmp = np.zeros((maxNumData, 100000))
    for i in range(maxNumData):
        line = f.readline()
        line = map(int, line.split())
        for j in line:
            tmp[i][j-1] = 1
    print("Shape of tmp: ",tmp.shape)

    return tmp,targets

# computing PCA and Use the PCA module to reduce compress the feature vectors
def compute_PCA(tmp, targets):
    pca = PCA(n_components=100)
    pca.fit(tmp)
    tmp_transformed = pca.transform(tmp)
    print("Tmp Transform Shape: ",tmp_transformed.shape)
    X_train, X_test,Y_train,Y_test = train_test_split(tmp_transformed,targets,test_size =0.01)

    return X_train, X_test, Y_train, Y_test

```

```python
''' Print out some of the Values of dataset'''
tmp, targets = reading_data()
X_train, X_test,Y_train,Y_test = compute_PCA(tmp, targets)
```

    Shape of tmp:  (800, 100000)
    Tmp Transform Shape:  (800, 100)

 # Question III.2 (6pts)

Once you have learned the compressed representation for your samples, learn the following two classifiers

- SVC with RBF kernel (Combine the SVC with GridSearchCVto determine the optimal values for $C$ (how much you penalize misclassification) and $\gamma$ (width of kernel)). To start you can take your grid to be defined as below

param_grid = {'C': [1e2, 5e2, 1e3, 1e4, 5e4],
              'gamma': [0.0005, 0.001, 0.005, 0.01, 0.1], }

Keep in mind that larger $\gamma$ means smaller kernel. I.e. $K(x, y) = exp(-gamma ||x-y||^2)$. $C$ is defined as in exercise II.

- Logistic regression classifier

# Solution

1. Create a Function that carries Grid Search using Scikit SCV

2. Create another Function that does Grid Search using Scikit Logisitc Regression

3. Print out the fit model, the Best Estimate, and the Score

```python
from sklearn.model_selection import GridSearchCV
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

import warnings

param_grid_SVC = {'C': [1e2, 5e2, 1e3, 1e4, 5e4], 'gamma': [0.0005, 0.001, 0.005, 0.01, 0.1], }
def Grid_search_SCV(param_grid_SVC):

    clf = GridSearchCV(SVC(kernel = "rbf", class_weight = 'balanced'), param_grid_SVC)
    print("first clf: ",clf)
    best_model = clf.fit(X_train, Y_train)
    print("***************************************************************************************")
    print("Grid Search SVC Best Estimator: ",best_model.best_estimator_)
    print("***************************************************************************************")
    print("Grid Search SVC Score: ",clf.score(X_test, Y_test), "(",(clf.score(X_test, Y_test))*100,"%)")
    print("***************************************************************************************")

    return

param_grid_log = {'C': [1e2, 5e2, 1e3, 1e4, 5e4] }
def Grid_Search_logistic_Reg(param_grid_log):
    clf = GridSearchCV(LogisticRegression(random_state = 0), param_grid_log)
    print("first clf: ",clf)
    print("***************************************************************************************")
    clf = clf.fit(X_train, Y_train)
    warnings.filterwarnings("ignore")
    print("Grid Search Logistic Regression Best Estimator: ",clf.best_estimator_)
    print("***************************************************************************************")
    print("Grid Search Logistic Regression Score: ",clf.score(X_test, Y_test),"(",(clf.score(X_test, Y_test))*100,"%)")
    print("***************************************************************************************")

    return
Grid_search_SCV(param_grid_SVC)
Grid_Search_logistic_Reg(param_grid_log)
```

    first clf:  GridSearchCV(cv=None, error_score=nan,
                 estimator=SVC(C=1.0, break_ties=False, cache_size=200,
                               class_weight='balanced', coef0=0.0,
                               decision_function_shape='ovr', degree=3,
                               gamma='scale', kernel='rbf', max_iter=-1,
                               probability=False, random_state=None, shrinking=True,
                               tol=0.001, verbose=False),
                 iid='deprecated', n_jobs=None,
                 param_grid={'C': [100.0, 500.0, 1000.0, 10000.0, 50000.0],
                             'gamma': [0.0005, 0.001, 0.005, 0.01, 0.1]},
                 pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
                 scoring=None, verbose=0)
    ***************************************************************************************
    Grid Search SVC Best Estimator:  SVC(C=100.0, break_ties=False, cache_size=200, class_weight='balanced',
        coef0=0.0, decision_function_shape='ovr', degree=3, gamma=0.1, kernel='rbf',
        max_iter=-1, probability=False, random_state=None, shrinking=True,
        tol=0.001, verbose=False)
    ***************************************************************************************
    Grid Search SVC Score:  1.0 ( 100.0 %)
    ***************************************************************************************
    first clf:  GridSearchCV(cv=None, error_score=nan,
                 estimator=LogisticRegression(C=1.0, class_weight=None, dual=False,
                                              fit_intercept=True,
                                              intercept_scaling=1, l1_ratio=None,
                                              max_iter=100, multi_class='auto',
                                              n_jobs=None, penalty='l2',
                                              random_state=0, solver='lbfgs',
                                              tol=0.0001, verbose=0,
                                              warm_start=False),
                 iid='deprecated', n_jobs=None,
                 param_grid={'C': [100.0, 500.0, 1000.0, 10000.0, 50000.0]},
                 pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
                 scoring=None, verbose=0)
    ***************************************************************************************
    Grid Search Logistic Regression Best Estimator:  LogisticRegression(C=100.0, class_weight=None, dual=False, fit_intercept=True,
                       intercept_scaling=1, l1_ratio=None, max_iter=100,
                       multi_class='auto', n_jobs=None, penalty='l2',
                       random_state=0, solver='lbfgs', tol=0.0001, verbose=0,
                       warm_start=False)
    ***************************************************************************************
    Grid Search Logistic Regression Score:  0.875 ( 87.5 %)
    ***************************************************************************************

### Bonus : Computational drug design. Part II: Tackling COVID-19

Now that you understand the basic idea behind computational drug design, we are ready to try to tackle the real challenge. A couple of days ago, Sage Health started a competition which was aimed at developing new drugs to fight corona virus. The competition, which was [advertised through youtube](https://www.youtube.com/watch?v=1LJgkovowgA), is now closed. However it would be interesting to see whether anybody could make additional discoveries from the data. Check the competition video and try to implement some of the step, using the data from the github pages of the winners (see here for the main [SageHealth webpage](https://www.sage-health.org/) and [https://www.sage-health.org/coronavirus/](here for the links to each of the winners github pages))

Points for this question depends on how far you can go.