# Antenna Selection as an Optimization Problem

## One Antenna Selection Schemes

These schemes choose one antenna out of a total of $N_t$ available transmit antennas.

#### Optimal Antenna Selection(OAS) Scheme

In this scheme it is assumed that the channel state information(CSI) is known for both the user(Adam) and the eavesdropper(Eve). It is also assumed that both use maximal ratio combining. The maximal achievable rate at Adam using antenna $i$ is given by

\begin{equation}
    R_i^A = \log_2\left(1 + \sum_{j=1}^{N_A}\frac{P|h_{ij}^{A}|}{\sigma_n^2}\right).
\end{equation}

Similarly the maximal achievable rate at Eve is given by

\begin{equation}
    R_i^E = \log_2\left(1 + \sum_{j=1}^{N_E}\frac{P|h_{ij}^{E}|}{\sigma_n^2}\right).
\end{equation}

$N_A$ and $N_E$ indicate the number of receive antennas at Adam and Eve respectively. $h_{ij}^{A}$ is the channel coefficient from the $i$th transmit antenna to the $j$th receive antenna of Adam. The same convention is used for $h_{ij}^{E}$.

The optimal antenna for maximizing the secrecy capacity is the antenna that gives the largest difference in the channel capacity of Adam and Eve. Hence the optimal antenna selection criterion is given by

\begin{equation}
    b = \arg\max_{i\in\mathcal{S}}\left(R_i^A - R_i^E\right) = \arg\max_{i\in\mathcal{S}}\left(
    \frac{1 + \sum_{j=1}^{N_A}\frac{P|h_{ij}^{A}|}{\sigma_n^2}}{1 + \sum_{j=1}^{N_E}\frac{P|h_{ij}^{E}|}{\sigma_n^2}}
    \right),
\end{equation}

where $b$ denotes the best antenna and $\mathcal{S}$ is the set of the N transmit antennas.

P is known at the source and also the noise variance can be assumed to be equal. This can be supported by noting that thermal noise variance is given by $\sigma^2_n = 4kTB$. Therefore by knowing the channel coefficients of the two channels the best antenna can be determined at the source.

#### Sub Optimal Antenna Selection(SAS) scheme

In the case that the the channel of Eve is not known then the OAS scheme cannot work. The best antenna in this case is the one that maximizes the channel capacity of Adam, hoping that this will also maximize the secrecy capacity.

Therefore the antenna selection can be done using

\begin{equation}
    b = \arg\max_{i\in\mathcal{S}}\left(R_i^A\right) = \arg\max_{i\in\mathcal{S}}\left(\sum_{j=1}^{N_A}\left| h_{ij}^{A} \right| \right)
\end{equation}

#### Multiple Transmit Antenna Selection for Linear Receivers

Instead of choosing only the best antenna this scheme chooses the subset of transmit antennas that give the best BER. This is done by maximizing the smallest singular value of the reduced channel matrix.

\begin{equation}
    S_o = \max_{s\in\mathcal{P}(\mathcal{S})} SV_{min} \left( H_s \right)
\end{equation}

$S_o$ is the set of antennae that maximizes the smallest singular value of the reduced channel matrix. $\mathcal{P}(\mathcal{S})$ is the power set of the antennae set, $\mathcal{S}$. $H_s$ is the effective channel matrix when transmitting from the antennae in $s$. $SV_{min}$ denotes the smallest singular value of the matrix.

# Results

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

plt.rcParams['axes.titlesize'] = 26
plt.rcParams['axes.labelsize']=18
plt.rcParams['xtick.labelsize']=18
plt.rcParams['ytick.labelsize']=18
plt.rcParams['legend.fontsize']=18
plt.rcParams['lines.linewidth'] = 2
plt.rcParams['lines.markersize'] = 10
plt.rcParams['axes.labelsize'] = 22
```

```python
import numpy as np
import pickle

from DDAS import MakeReal
from AntennaSelection import generateLROASdata, generateOASdata, generateSASdata, calculateBERwithAS
from symbols import SymbolGenerator, MLdecoder

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_predict, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.svm import SVC
from sklearn.base import clone
```

```python
data_prep_pipeline = Pipeline([
    ('toReal', MakeReal(RealPart = False, ImaginaryPart=False, magnitude = True, Phase = False)),
    ('stdScaler', StandardScaler()),
])
```

```python
Nt = 2
Nr = 10

# N = 100000

# X_oas, y_oas= generateOASdata(N, nt = Nt, nr = Nr)
# X_sas, y_sas = generateSASdata(N, nt = Nt, nr = Nr)
# X_lroas, y_lroas = generateLROASdata(N, nt = Nt, nr = Nr)

# outfile = open('./train_data/x_oasNt2Nr10','wb')
# pickle.dump(X_oas, outfile)
# outfile.close()

# outfile = open('./train_data/y_oasNt2Nr10','wb')
# pickle.dump(y_oas, outfile)
# outfile.close()

# outfile = open('./train_data/X_sasNt2Nr10','wb')
# pickle.dump(X_sas, outfile)
# outfile.close()

# outfile = open('./train_data/y_sasNt2Nr10','wb')
# pickle.dump(y_sas, outfile)
# outfile.close()

# outfile = open('./train_data/X_lroasNt2Nr10','wb')
# pickle.dump(X_lroas, outfile)
# outfile.close()

# outfile = open('./train_data/y_lroasNt2Nr10','wb')
# pickle.dump(y_lroas, outfile)
# outfile.close()
```

```python
X_oas_test, y_oas_test = generateOASdata(1000,nt = Nt, nr =Nr)
X_sas_test, y_sas_test = generateSASdata(1000,nt = Nt, nr =Nr)
X_lroas_test, y_lroas_test = generateLROASdata(1000,nt = Nt, nr =Nr)

infile = open('./train_data/x_oasNt2Nr10','rb')
X_oas = pickle.load(infile)
infile.close()

infile = open('./train_data/y_oasNt2Nr10','rb')
y_oas = pickle.load(infile)
infile.close()

infile = open('./train_data/X_sasNt2Nr10','rb')
X_sas = pickle.load(infile)
infile.close()

infile = open('./train_data/y_sasNt2Nr10','rb')
y_sas = pickle.load(infile)
infile.close()

infile = open('./train_data/X_lroasNt2Nr10','rb')
X_lroas = pickle.load(infile)
infile.close()

infile = open('./train_data/y_lroasNt2Nr10','rb')
y_lroas = pickle.load(infile)
infile.close()

pl_oas = clone(data_prep_pipeline)
pl_sas = clone(data_prep_pipeline)
pl_lroas = clone(data_prep_pipeline)

X_oas = pl_oas.fit_transform(X_oas)
X_oas_test = pl_oas.transform(X_oas_test)

outfile = open('./models/pl_oas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(pl_oas,outfile)
outfile.close()

X_sas = pl_sas.fit_transform(X_sas)
X_sas_test = pl_sas.transform(X_sas_test)

outfile = open('./models/pl_sas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(pl_sas,outfile)
outfile.close()

X_lroas = pl_lroas.fit_transform(X_lroas)
X_lroas_test = pl_lroas.transform(X_lroas_test)

outfile = open('./models/pl_lroas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(pl_lroas,outfile)
outfile.close()
```

## Model Accuracy

Machine learning algorithms work only with real data and perform better when the data has been normalized. Different possibilities exist on how to prepare the channel matrix for ML algorithms.

The matrix can be vectorized, and then expanded so that each complex element corresponds to two elements in the new vector. One element will be the real part and the second the imaginary part. Another approach will be to make one element equal to the magnitude of the Channel matrix element and the other the phase of the complex number(the one used in the paper)

Another approach would be to decompose each channel element into the four numbers discussed above: real and imaginary part, magnitude and phase.

However by looking at the equations above only the magnitude of the channel coefficients influences the decision. It is thus expected that the best approach would be to use only the magnitudes of the channel coefficients.

This was indeed the case. Different variations of the methods discussed above were tested and the best results were from using the magnitudes only. In this respect the information needed to train the machine learning algorithms is half of the information needed to characterize the channel. This speeds up the learning time and also decreases the memory requirements for storing the training data.

```python
mlpc_oas = MLPClassifier(max_iter=1000)
svc_oas = SVC()
knn_oas = KNeighborsClassifier(n_neighbors=201)

mlpc_sas = MLPClassifier(max_iter=1000)
svc_sas = SVC()
knn_sas = KNeighborsClassifier(n_neighbors=201)

mlpc_lroas = MLPClassifier(max_iter=1000)
svc_lroas = SVC()
knn_lroas = KNeighborsClassifier(n_neighbors=201)

print('Classifiers for OAS')
mlpc_oas.fit(X_oas, y_oas)
print('MLP Classifier score:',mlpc_oas.score(X_oas_test, y_oas_test))
svc_oas.fit(X_oas,y_oas)
print('SVM score:',svc_oas.score(X_oas_test, y_oas_test))
knn_oas.fit(X_oas,y_oas)
print('KNN score:',knn_oas.score(X_oas_test, y_oas_test))

print('Classifiers for SAS')
mlpc_sas.fit(X_sas, y_sas)
print('MLP Classifier score:',mlpc_sas.score(X_sas_test, y_sas_test))
svc_sas.fit(X_sas,y_sas)
print('SVM score:',svc_sas.score(X_sas_test, y_sas_test))
knn_sas.fit(X_sas,y_sas)
print('KNN score:',knn_sas.score(X_sas_test, y_sas_test))

print('Classifiers for LROAS')
mlpc_lroas.fit(X_lroas, y_lroas)
print('MLP Classifier score:',mlpc_lroas.score(X_lroas_test, y_lroas_test))
#svc_lroas.fit(X_lroas,y_lroas)
# print('SVM score:',svc_lroas.score(X_lroas_test, y_lroas_test))
knn_lroas.fit(X_lroas,y_lroas)
print('KNN score:',knn_sas.score(X_sas_test, y_sas_test))
```

    Classifiers for OAS
    MLP Classifier score: 0.967
    SVM score: 0.975
    KNN score: 0.922
    Classifiers for SAS
    MLP Classifier score: 0.982
    SVM score: 0.989
    KNN score: 0.97
    Classifiers for LROAS
    MLP Classifier score: 0.986
    KNN score: 0.97

```python
outfile = open('./models/mlpc_oas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(mlpc_oas,outfile)
outfile.close()

outfile = open('./models/mlpc_sas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(mlpc_sas,outfile)
outfile.close()

outfile = open('./models/mlpc_lroas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(mlpc_lroas,outfile)
outfile.close()

outfile = open('./models/knn_oas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(knn_oas,outfile)
outfile.close()

outfile = open('./models/knn_sas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(knn_sas,outfile)
outfile.close()

outfile = open('./models/knn_lroas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(knn_lroas,outfile)
outfile.close()

outfile = open('./models/svm_oas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(svc_oas,outfile)
outfile.close()

outfile = open('./models/svm_sas_Nt{}Nr{}'.format(Nt,Nr), 'wb')
pickle.dump(svc_sas,outfile)
outfile.close()
```

```python
infile = open('./analytic_results/ber_snr','rb')
snr = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_all','rb')
BER_all = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_oas','rb')
BER_oas = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_sas','rb')
BER_sas = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_lroas','rb')
BER_lroas = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_random_single','rb')
BER_random_single = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_random_subset','rb')
BER_random_subset = pickle.load(infile)
infile.close()

infile = open('./analytic_results/ber_MaxMinNorm','rb')
BER_MaxMinNorm = pickle.load(infile)
infile.close()
```

## Analytic Techniques

```python
plt.figure(figsize=(20,15))
plt.semilogy(snr, BER_all, 'o--', color = 'k', label='All Antennae')
BER_oas[snr==2] = 0
plt.semilogy(snr, BER_oas, 'o--', color = 'C0', label='OAS')
plt.semilogy(snr, BER_sas, '^-.', color = 'C1', label='SAS')
plt.semilogy(snr, BER_lroas, '*:', color = 'C4', label='LROAS')

plt.semilogy(snr, BER_MaxMinNorm, 'b.--', label='Max Min Norm')

plt.semilogy(snr, BER_random_single, '>--', color = 'g', label='Random Single')
plt.semilogy(snr, BER_random_subset, '>-.', color = 'g', label='Random Multi')

plt.title('Analytic tecniques')
plt.legend()
plt.grid(b=True, which='major')
plt.grid(b=True, which='minor')
plt.xlabel('Eb/No')
plt.ylabel('BER')
plt.show()
```

```python
infile = open('./model_results/ber_oas_mlpc_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_mlpc_oas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_sas_mlpc_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_mlpc_sas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_lroas_mlpc_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_mlpc_lroas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_oas_knn_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_knn_oas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_sas_knn_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_knn_sas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_lroas_knn_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_knn_lroas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_oas_svm_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_svm_oas = pickle.load(infile)
infile.close()

infile = open('./model_results/ber_sas_svm_Nt{}Nr{}'.format(Nt,Nr), 'rb')
BER_svm_sas = pickle.load(infile)
infile.close()
```

## Optimal Antenna Selection Comparison

```python
plt.figure(figsize=(20,15))
# plt.semilogy(snr, BER_all, 'o--', color = 'k', label='All Antennae')

plt.semilogy(snr, BER_oas, 'xk-.', label="OAS")

plt.semilogy(snr, BER_svm_oas, 'o--', color = 'C0', label='SVM')
plt.semilogy(snr, BER_knn_oas, '^-.', color = 'C1', label='KNN')
plt.semilogy(snr, BER_mlpc_oas, '*:', color = 'C3', label='MLP')

plt.semilogy(snr, BER_random_single, 'o--', color = 'g', label='Random Single')
plt.semilogy(snr, BER_random_subset, '^-.', color = 'g', label='Random Multi')

# plt.title('OAS')
plt.legend()
plt.grid(b=True, which='major')
plt.grid(b=True, which='minor')
plt.xlabel('Eb/No')
plt.ylabel('BER')
plt.show()
```

##  Suboptimal Antenna Selection Comparison

```python
plt.figure(figsize=(20,15))
# plt.semilogy(snr, BER_all, 'o--', color = 'k', label='All Antennae')

plt.semilogy(snr, BER_sas, 'xk-.', label="SAS")

plt.semilogy(snr, BER_svm_sas, 'o--', color = 'C0', label='SVM')
plt.semilogy(snr, BER_knn_sas, '^-.', color = 'C1', label='KNN')
plt.semilogy(snr, BER_mlpc_sas, '*:', color = 'C3', label='MLP')

plt.semilogy(snr, BER_random_single, 'o--', color = 'g', label='Random Single')
plt.semilogy(snr, BER_random_subset, '^-.', color = 'g', label='Random Multi')

# plt.title('OAS')
plt.legend()
plt.grid(b=True, which='major')
plt.grid(b=True, which='minor')
plt.xlabel('Eb/No')
plt.ylabel('BER')

plt.show()
```

## Linear Receiver Optimal Antenna Selection Comparison

```python
plt.figure(figsize=(20,15))
# plt.semilogy(snr, BER_all, 'o--', color = 'k', label='All Antennae')

plt.semilogy(snr, BER_lroas, 'xk-.', label="LROAS")

plt.semilogy(snr, BER_knn_lroas, '^-.', color = 'C1', label='KNN')
plt.semilogy(snr, BER_mlpc_lroas, '*:', color = 'C3', label='MLP')

plt.semilogy(snr, BER_random_single, 'o--', color = 'g', label='Random Single')
plt.semilogy(snr, BER_random_subset, '^-.', color = 'g', label='Random Multi')

plt.title('LROAS')
plt.legend()
plt.grid(b=True, which='major')
plt.grid(b=True, which='minor')
plt.xlabel('Eb/No')
plt.ylabel('BER')
plt.show()
```

```python

```


# Алгоритм поиска квантового блуждания

Квантовые блуждания представляют собой квантовую эквивалентность классической цепи Маркова и стали ключевыми во многих квантовых алгоритмах. В этом разделе мы реализуем алгоритм поиска квантового блуждания, который находит отмеченные элементы в графе. Алгоритм имеет квадратичное ускорение по сравнению со своим классическим аналогом.

## 1.Классические цепи Маркова

Цепь Маркова — это стохастический процесс, часто используемый для моделирования реальных процессов. Он состоит из состояний и связанных с ними вероятностей перехода, которые описывают вероятности перехода между состояниями на каждом временном шаге. В цепях Маркова с дискретным временем, с которыми мы здесь работаем, временные шаги дискретны. Цепи Маркова удовлетворяют свойству Маркова, что означает, что следующий шаг процесса зависит только от текущего, а не от любого из предыдущих шагов. Цепь Маркова имеет связанную матрицу перехода P, которая описывает вероятность перехода между каждым из состояний. Ниже мы показываем пример цепи Маркова и связанной с ней переходной матрицы $P$. \begin{equation} P= \begin{pmatrix} 0,1 & 0,3 & 0,3\ 0,1 & 0,1 & 0,2 \ 0,8 & 0,6 & 0,5 \end{pmatrix} \label{eq:matrix_example} \end{equation}

Учитывая матрицу перехода $P$, мы можем получить распределение вероятностей после $t$ временных шагов по $P^t$.

## 2. Квантовые прогулки

Квантовые блуждания — это квантовый эквивалент классической цепи Маркова. Из-за суперпозиции квантовое блуждание будет проходить по всем возможным путям одновременно, пока мы не измерим цепь. Из-за квантовой интерференции некоторое состояние уравновешивается. Это делает алгоритмы квантового блуждания быстрее классических, поскольку мы можем спроектировать их таким образом, чтобы неправильные ответы быстро исключались. Существуют две распространенные модели квантовых блужданий, придуманные квантовые блуждания и квантовые блуждания Сегеди, которые эквивалентны при определенных обстоятельствах. Придуманное квантовое блуждание — это блуждание по вершинам графа, в то время как квантовое блуждание Сегеди происходит по ребрам. Прежде чем мы покажем, как мы можем реализовать квантовое блуждание, мы представим обе модели.

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

### 2. Придуманные квантовые блуждания

Простым примером придуманного квантового блуждания является блуждание по бесконечной целочисленной прямой. В этом случае мы представляем позицию ходока целым числом ${\ket{j} : j \in \mathbb{Z} }$, поскольку ходок может пройти все целые числа в $\mathbb{Z}$. Монета решает, как должен двигаться ходячий. В целочисленной строке ходок может идти как влево, так и вправо. Таким образом, вычислительная база монеты ${\ket{0}, \ket{1}}$, и мы двигаем ходунка в одном направлении, если монета $\ket{0}$, и в другом направлении, если монета составляет $\ket{1}$.

Придуманный квант — это обход узлов в графе, и мы называем узлы состояниями. Ходок может перемещаться между состояниями, связанными с ребром. В модели монеты у нас есть два квантовых состояния и два оператора. Первое состояние — это состояние положения, которое представляет положение ходока. Для приведенного выше обхода это целое число, поскольку ходок может находиться в любом месте целочисленной строки. Другое состояние — состояние монеты. Состояние монеты определяет, как ходячий должен двигаться на следующем шаге. Мы можем представить как состояние монеты, так и состояние позиции векторами в гильбертовом пространстве. Если мы можем выразить состояние монеты вектором из $\mathcal{H}_C$, а состояние позиции - вектором из $\mathcal{H}_P$, мы можем выразить квантовое пространство для всего ходунка как $\mathcal {H} = \mathcal{H}_C \otimes \mathcal{H}_P$.

Как мы уже упоминали, в модели также есть два оператора; оператор монеты $C$ и оператор сдвига $S$. Оператор монеты действует на $\mathcal{H}_C$ на каждом временном шаге и помещает ходячего в суперпозицию, так что он проходит все возможные пути одновременно. Для обхода целочисленной линии это означает, что она движется как влево, так и вправо на каждом временном шаге. Существуют разные операторы монет, но наиболее распространенными являются монета Адамара и монета Гровера. Монета Адамара — это ворота Адамара, которые помещают ходячего в равную суперпозицию:

\begin{equation} H = \frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \ 1 & -1 \end{bmatrix} \end{equation}

Монета Гровера — это оператор диффузии Гровера из алгоритма Гровера. Мы определяем его как

\begin{equation} G = \begin{bmatrix} \frac{2}{n} -1 & \frac{2}{n} & \ldots & \frac{2}{n}\ \frac{2}{ n} & \frac{2}{n} - 1 & \ldots & \frac{2}{n} \ \vdots & \vdots & \ddots & \vdots \ \frac{2}{n} & \frac{ 2}{n} & \ldots & \frac{2}{n} -1 \end{bmatrix} \end{уравнение}

Как и монета Адамара, монета Гровера помещает ходячего в суперпозицию. Однако ведет себя немного по-другому. Если мы применим монету Гровера к пешеходу в позиции $\ket{000}$, мы получим вероятности вектора состояния, показанные на рисунке ниже. Как мы видим, монета не ставит ходока в равную суперпозицию, как это делает монета Адамара. Вместо этого $\ket{000}$ имеет гораздо большую вероятность, чем другие состояния.

Другой оператор в модели, оператор сдвига, действует на $\mathcal{H}_P$ и перемещает обходчик на следующую позицию. Для обхода целочисленной строки оператор сдвига перемещает ходок слева от монеты $\ket{0}$ и справа, если монета $\ket{1}$:

\begin{уравнение} S \ket{0}\ket{j} = \ket{0}\ket{j+1} \end{уравнение}

\begin{equation} S \ket{1}\ket{j} = \ket{1}\ket{j-1} \end{equation}

С оператором сдвига, определенным выше, мы можем представить один шаг придуманного кванта как унитарный оператор $U$, заданный \begin{equation} U = SC, \end{equation}

где C — монетный оператор. Для квантового блуждания по целочисленной строке мы используем монету Адамара, но C может быть монетой Адамара, монетой Гровера или любым другим оператором монеты.

Мы также можем смотреть на несколько шагов вперед. Мы можем выразить квантовое состояние $\ket{\psi}$ после $t$ временных шагов как \begin{equation} \ket{\psi (t)} = U^t \ket{\psi(0)}, \ end{equation} где $\ket{\psi(0)}$ — начальное состояние, а U — оператор одного шага блуждания [1].

Придуманные квантовые блуждания больше всего подходят для регулярных графов, графов, в которых все узлы имеют одинаковое количество соседей [2]. Альтернативной моделью квантового блуждания, которая более удобна для нерегулярных графов, является модель Сегеди, которую мы рассмотрим далее.

### 2.B Квантовая прогулка Сегеди

В то время как придуманное блуждание — это блуждание по узлам графа, блуждание Сегеди — это блуждание по ребрам двудольного двойного покрытия исходного графа. Граф с двойным покрытием — это граф, в котором вершин в два раза больше, чем в исходном графе. Две вершины в двудольном графе с двойным покрытием связаны ребром тогда и только тогда, когда вершины также связаны в исходном графе. Чтобы создать эту модель, мы начнем с матрицы вероятности перехода P для классического блуждания. Как описано в разделе 1, мы представляем классическое случайное блуждание с дискретным временем переходной матрицей $P$. Для любого $N$-вершинного графа с $N \times N$ матрицей перехода $P$ мы можем определить соответствующее квантовое блуждание с дискретным временем как унитарную операцию в гильбертовом пространстве $\mathcal{H}^N \otimes \ математический{H}^N$. Пусть $P_{jk}$ определяет вероятность того, что мы совершим переход из состояния $j$ в $k$. Прежде чем мы определим блуждание, мы определим нормализованные состояния

\begin{equation} \ket{\psi_j} := \sum_{k=1}^N \sqrt{P_{kj}} \ket{j,k}, ; j=1,...,N \end{уравнение}

и проекцию на ${\ket{\psi_j}}:j=1,...,N$

\begin{equation} \Pi := \sum_{j=1}^N \ket{\psi_j} \bra{\psi_j} \label{eq:sz_pi} \end{equation}

Введем также оператор сдвига S:

\begin{equation} S := \sum_{j,k=1}^N \ket{j,k} \bra{k,j} \label{eq:sz_s} \end{equation}

С $S$ и $\Pi$, определенными выше, мы можем ввести один шаг квантового блуждания с дискретным временем:

\begin{уравнение} U := S(2 \Pi - 1), \label{eq:sz_op} \end{уравнение}

где $(2\Pi - 1)$ — оператор отражения. Мы также определяем $t$ шагов обхода как $U^t$ [2].

### 2.C Эквивалентность придуманных квантовых блужданий и квантовых блужданий Сегеди

Известно, что придуманная прогулка с монетой Гровера эквивалентна квантовой прогулке Сегеди. За более подробной информацией мы отсылаем к этой статье [3] Томаса Г. Вонга, где он также показывает эквивалентность между операторами в двух моделях.

## 3. Пример: реализация квантового блуждания по гиперкубу

Гиперкуб — это $n$-мерный аналог трехмерного куба. Все узлы имеют степень $n$, всего гиперкуб имеет $N=2^n$ узлов. Мы можем представить узлы гиперкубического графа наборами из $n$ двоичных чисел. Двоичное представление соседей узла будет отличаться только одним двоичным числом. Например, в четырехмерном гиперкубе соседями $0000$ являются $0001$, $0010$, $0100$ и $1000$. Таким образом, узел соединен со всеми узлами, до которых расстояние Хэмминга равно 1. Ребра также помечены. Два соседних узла, различающихся битом a:, соединены ребром, помеченным $a$.

Гильбертово пространство, представляющее придуманное квантовое блуждание по гиперкубу, имеет вид $\mathcal{H} = \mathcal{H}^n \otimes \mathcal{H}^{2^n}$, где $\mathcal{H}^n $ обозначает место для монет, а $\mathcal{H}^{2^n}$ - положение ходока. Расчетная база

\begin{equation} \big{ \ket{a,\vec{v}}, 0 \leq a \leq n-1, \vec{v} \in {(00...00), (00.. .01), ....., (11...11 )} \big}. \end{уравнение}

Значение вычислительного базиса монеты $a$, связанное с ребром $a$, определяет, куда должен двигаться ходок. Если $a=0$, обходчик перейдет к узлу, где первое двоичное значение отличается от текущего узла. Если $a=1$, ходок перейдет к узлу, в котором второе значение отличается от текущего, и так далее. Пусть $\vec{e}_a$ — n-кортеж, в котором все двоичные значения, кроме значения с индексом $a$, равны $0$. Тогда оператор сдвига $S$ переводит ходок из состояния $\ket{a} \ket{\vec{v}}$ в $\ket{\vec{v} \oplus \vec{e}_a}$:

\begin{equation} S \ket{a} \ket{\vec{v}} = \ket{a} \ket{\vec{v} \oplus \vec{e}_a}. \end{уравнение}

Для этой прогулки мы используем монету Гровера $G$. Таким образом, оператор эволюции

\begin{уравнение} U = SG. \end{уравнение}

Теперь мы покажем, как мы можем реализовать квантовое блуждание по 4-мерному гиперкубу. Нам нужно реализовать оператор монеты и оператор сдвига. Начнем с импорта всех необходимых библиотек из Qiskit.

```python
# Importing standard Qiskit libraries
from qiskit import QuantumCircuit, execute, Aer, IBMQ, QuantumRegister, ClassicalRegister
from qiskit.compiler import transpile, assemble
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from qiskit.circuit.library import QFT
from numpy import pi
from qiskit.quantum_info import Statevector
from matplotlib import pyplot as plt
import numpy as np
# Loading your IBM Q account(s)
provider = IBMQ.load_account()
```

Схема будет иметь 6 кубитов, 4 из которых представляют позицию, а 2 — монету. Как мы упоминали ранее, монета — это монета Гровера, которая является диффузором в алгоритме Гровера. Начнем с реализации этого.

```python
one_step_circuit = QuantumCircuit(6, name=' ONE STEP')
# Coin operator
one_step_circuit.h([4,5])
one_step_circuit.z([4,5])
one_step_circuit.cz(4,5)
one_step_circuit.h([4,5])
one_step_circuit.draw()
```

Теперь давайте реализуем оператор сдвига. Мы знаем, что ходок может двигаться только к соседним узлам, а все соседние узлы отличаются всего одним битом. Мы хотим переместить ходок в соответствии с монетой, и мы перемещаем ходок, применяя вентиль НЕ к одному из кубитов узла. Если монета находится в состоянии $\ket{11}$, мы переводим ходок в состояние, в котором отличается кубит первого узла. Если монета равна $\ket{10}$ или $\ket{01}$, ходок переходит в состояние, в котором второй и третий кубит соответственно различаются. Наконец, если монета Гровера равна $\ket{00}$, мы переворачиваем четвертый кубит. Мы реализуем это с помощью CCNOT- и NOT-гейтов после монеты Гровера. Вместе они представляют собой один шаг квантового блуждания по 4-мерному гиперкубу.

```python
# Shift operator function for 4d-hypercube
def shift_operator(circuit):
    for i in range(0,4):
        circuit.x(4)
        if i%2==0:
            circuit.x(5)
        circuit.ccx(4,5,i)

shift_operator(one_step_circuit)

one_step_gate = one_step_circuit.to_instruction()
one_step_circuit.draw()
```

#### 4. Алгоритм поиска квантового блуждания

Теперь мы реализуем алгоритм поиска квантового блуждания, который находит отмеченную вершину в графе. Сначала опишем алгоритм, затем пройдемся по его реализации. Алгоритм квантового блуждания решает задачу поиска помеченных вершин в графе с помощью квантового блуждания. То есть мы помечаем некоторый набор вершин $|M|$, начинаем с произвольного узла графа и движемся по обходу, пока не найдем отмеченные узлы. Базисные состояния в алгоритме поиска квантового блуждания имеют два регистра, один из которых соответствует текущему узлу, а другой — предыдущему узлу. То есть базисные состояния соответствуют ребрам графа. Обозначим квантовое блуждание на основе классической цепи Маркова с переходной матрицей $P$ унитарной операцией $W(P)$ на $\mathcal{H}$. Мы также определяем $\ket{p_x} = \sum_y \sqrt{P_{xy}}\ket{y}$ как равномерную суперпозицию по соседям узла $x$. Пусть $\ket{x}\ket{y}$ — базисное состояние. Мы определяем базисное состояние $\ket{x}\ket{y}$ как «хорошее», если $x$ — отмеченный узел. В противном случае мы называем это «плохим». Теперь введем «хорошие» и «плохие» состояния:

\begin{equation} \ket{G} = \frac{1}{\sqrt{|M|}} \sum_{x \in M} \ket{x} \ket{p_x}, ; \ket{B} = \frac{1}{\sqrt{N-|M|}} \sum_{x \notin M} \ket{x} \ket{p_x}, \end{equation}

которые являются суперпозициями над хорошими и плохими базисными состояниями. Далее определим $\epsilon = |M|/N$ и $\theta = \arcsin(\sqrt{\epsilon})$.

Короче говоря, алгоритм состоит из трех шагов:

1. Установите начальное состояние $\ket{U} = \frac{1}{\sqrt{N}} \sum_{x} \ket{x} \ket{p_x} = \sin{\theta} \ket{G } + \cos{\theta} \ket{B}$, равномерная суперпозиция по всем ребрам

2. Повторите $O(1/\sqrt{\epsilon})$ раз:

    (a) Отражение через $\ket{B}$

    (b) Отражение через $\ket{U}$

3. Сделайте измерение в вычислительной базе

Мы можем легко реализовать шаг $1$ с вентилями Адамара и отражение через $\ket{B}$ с помощью фазового оракула, который сдвигает фазу $x$, если $x$ находится в первом регистре, и оставляет схему неизменной в противном случае.

Шаг 2(b) эквивалентен нахождению унитарного $R(P)$, выполняющего следующее отображение: \begin{align} \label{eq:mapping_1} \ket{U} &\mapsto \ket{U}, : \text{и} \ \\ket{\psi} &\mapsto -\ket{\psi}, : \forall \ket{\psi} \text{в промежутке собственных векторов $W(P)$, ортогональных в $\ket{U}$} \label{eq:mapping_2} \end{align}

Чтобы найти этот оператор, мы применяем фазовую оценку на $W(P)$. Выше мы определили $W(P)$ как оператор эволюции случайного блуждания. Как мы видели в разделе 2.A, это унитарный оператор. Отсюда следует, что собственные значения оператора $W(P)$ имеют норму $1$. Благодаря этому мы можем записать собственные значения $W(P)$ в виде $e^{\pm 2i\theta_j}$. Унитарная $W(P)$ имеет один собственный вектор с соответствующим собственным значением $1$, равным $\ket{U}$. Это дается $\theta_1=0$. $R(P)$ найдет этот вектор $\ket{U}$, добавив регистр со вспомогательными кубитами, и выполнит оценку фазы с точностью $O(1/\sqrt{\delta})$, где $\delta$ спектральная щель $P$. Для этого нам нужно применить $W(P)$ $O(1/\sqrt{\delta})$ раз. Пусть $\ket{w}$ — собственный вектор $W(P)$ с собственным значением $e^{\pm 2i\theta_j}$. Предположим, что $\tilde{\theta_j}$ — наилучшее приближение к $\theta_j$, полученное с помощью фазовой оценки. Операция $R(P)$, которая выполняет отображения в для $\ket{w}$ на шаге 2(b), задается формулой [4]

\begin{equation} \ket{w} \ket{0} \mapsto \ket{w} \ket{\tilde{\theta_j}} \mapsto (-1)^{|\tilde{\theta_j} \neq 0 |} \ket{w} \ket{\tilde{\theta_j}} \mapsto (-1)^{|\tilde{\theta_j} \neq 0|} \ket{w} \ket{0} \end{ уравнение}

## 5.Пример: поиск квантового блуждания по 4-мерному гиперкубу

Алгоритм квантового блуждания позволяет найти отмеченное множество узлов за $O(1/\sqrt{\epsilon})$ шагов, $\epsilon = |M|/N$, где $M$ — количество отмеченные узлы, а $N$ — общее количество узлов. Этот алгоритм первоначально использовался с квантовыми блужданиями по Сегеди, где мы использовали регистры двух узлов для представления квантового состояния. Однако придуманное блуждание с монетой Гровера эквивалентно квантовому блужданию Сегеди, и, поскольку реализации придуманных блужданий в целом менее сложны, мы решили реализовать алгоритм с придуманным блужданием. Мы будем использовать 4-мерный гиперкуб, который мы реализовали в разделе 3.

Вкратце алгоритм будем реализовывать следующим образом. Мы достигаем шага 1, равномерной суперпозиции по всем ребрам, применяя вентили Адамара к кубитам-узлам, а также к кубитам-монетам. Для шага 2(а) мы реализуем фазовый оракул. Шаг 2(b) реализуется оценкой фазы на одном шаге квантового блуждания по гиперкубу с последующей пометкой всех квантовых состояний, где $\theta\neq 0$. Мы делаем это, вращая вспомогательный кубит. В последней части этого шага мы обращаем оценку фазы. Количество тета-кубитов зависит от точности $\theta$.

Ниже мы реализуем алгоритм поиска квантового блуждания на 4-мерном гиперкубе.

Для этого алгоритма нам нужно будет использовать обратный одношаговый вентиль, реализованный ранее. Мы получаем это с помощью встроенной в схему функции inverse().

```python
one_step_circuit.inverse().draw()
```

Обратный одношаговый вентиль будет использоваться позже для обращения оценки фазы. Нам нужно сделать управляемые ворота как из одношаговых ворот, которые мы реализовали в разделе 3, так и из их обратных. Позже мы будем использовать их в зависимости от значения управляющего кубита.

```python
# Make controlled gates
inv_cont_one_step = one_step_circuit.inverse().control()
inv_cont_one_step_gate = inv_cont_one_step.to_instruction()
cont_one_step = one_step_circuit.control()
cont_one_step_gate = cont_one_step.to_instruction()
```

При оценке фазы будут использоваться как управляемый одношаговый вентиль, так и управляемый инверсный одношаговый вентиль. Еще одна вещь, которую мы будем использовать при оценке фазы, — это квантовое преобразование Фурье. В Qiskit есть функция QFT, реализующая квантовое преобразование Фурье. Для оценки фазы используется обратное квантовое преобразование Фурье, но нам также потребуется использовать обычную КТП для обратной оценки фазы.

```python
inv_qft_gate = QFT(4, inverse=True).to_instruction()
qft_gate = QFT(4, inverse=False).to_instruction()

QFT(4, inverse=True).decompose().draw("mpl")
```

Прежде чем мы реализуем оценку фазы, мы реализуем фазовый оракул, который отмечает состояния 1011 и 1111. Затем мы создаем схему. Это шаг 2(а) алгоритма.

```python
phase_circuit =  QuantumCircuit(6, name=' phase oracle ')
# Mark 1011
phase_circuit.x(2)
phase_circuit.h(3)
phase_circuit.mct([0,1,2], 3)
phase_circuit.h(3)
phase_circuit.x(2)
# Mark 1111
phase_circuit.h(3)
phase_circuit.mct([0,1,2],3)
phase_circuit.h(3)
phase_oracle_gate = phase_circuit.to_instruction()
# Phase oracle circuit
phase_oracle_circuit =  QuantumCircuit(11, name=' PHASE ORACLE CIRCUIT ')
phase_oracle_circuit.append(phase_oracle_gate, [4,5,6,7,8,9])
phase_circuit.draw()
```

Теперь мы реализуем вентиль, который вращает вспомогательный кубит, если другие кубиты не равны нулю. Мы будем использовать этот вентиль в оценке фазы, где он будет вращать вспомогательный кубит, если $\theta\neq 0$.

```python
# Mark q_4 if the other qubits are non-zero
mark_auxiliary_circuit = QuantumCircuit(5, name=' mark auxiliary ')
mark_auxiliary_circuit.x([0,1,2,3,4])
mark_auxiliary_circuit.mct([0,1,2,3], 4)
mark_auxiliary_circuit.z(4)
mark_auxiliary_circuit.mct([0,1,2,3], 4)
mark_auxiliary_circuit.x([0,1,2,3,4])

mark_auxiliary_gate = mark_auxiliary_circuit.to_instruction()
mark_auxiliary_circuit.draw()
```

Теперь мы реализуем шаг 2(b) алгоритма. Этот шаг состоит из фазовой оценки одного шага квантового блуждания, за которым следует вспомогательный кубит, который мы вращаем, если $\theta \neq 0$. Для этого мы используем только что созданный mark_auxiliary_gate. После этого мы обращаем оценку фазы.

```python
# Phase estimation
phase_estimation_circuit = QuantumCircuit(11, name=' phase estimation ')
phase_estimation_circuit.h([0,1,2,3])
for i in range(0,4):
    stop = 2**i
    for j in range(0,stop):
        phase_estimation_circuit.append(cont_one_step, [i,4,5,6,7,8,9])

# Inverse fourier transform
phase_estimation_circuit.append(inv_qft_gate, [0,1,2,3])

# Mark all angles theta that are not 0 with an auxiliary qubit
phase_estimation_circuit.append(mark_auxiliary_gate, [0,1,2,3,10])

# Reverse phase estimation
phase_estimation_circuit.append(qft_gate, [0,1,2,3])

for i in range(3,-1,-1):
    stop = 2**i
    for j in range(0,stop):
        phase_estimation_circuit.append(inv_cont_one_step, [i,4,5,6,7,8,9])
phase_estimation_circuit.barrier(range(0,10))
phase_estimation_circuit.h([0,1,2,3])

# Make phase estimation gate
phase_estimation_gate = phase_estimation_circuit.to_instruction()
phase_estimation_circuit.draw()
```

Теперь мы реализуем весь алгоритм поиска квантового блуждания, используя вентили, которые мы сделали ранее. Мы начинаем с применения вентилей Адамара к кубитам узла и монеты, что является шагом 1 в алгоритме. После этого мы итеративно применяем вентиль фазового оракула и вентиль оценки фазы (этапы 2(a) и 2(b)). Нам потребуется $O(1/\sqrt{\epsilon})$ итераций, как указано в описании алгоритма в разделе 4. Наконец, мы измеряем кубиты узла.

```python
# Implementation of the full quantum walk search algorithm
theta_q = QuantumRegister(4, 'theta')
node_q = QuantumRegister(4, 'node')
coin_q = QuantumRegister(2, 'coin')
auxiliary_q = QuantumRegister(1, 'auxiliary')
creg_c2 = ClassicalRegister(4, 'c')
circuit = QuantumCircuit(theta_q, node_q, coin_q, auxiliary_q, creg_c2)
# Apply Hadamard gates to the qubits that represent the nodes and the coin
circuit.h([4,5,6,7,8,9])
iterations = 2

for i in range(0,iterations):
    circuit.append(phase_oracle_gate, [4,5,6,7,8,9])
    circuit.append(phase_estimation_gate, [0,1,2,3,4,5,6,7,8,9,10])

circuit.measure(node_q[0], creg_c2[0])
circuit.measure(node_q[1], creg_c2[1])
circuit.measure(node_q[2], creg_c2[2])
circuit.measure(node_q[3], creg_c2[3])
circuit.draw()
```

Наконец, мы запускаем реализацию на симуляторе qasm. Мы видим, что в большинстве случаев схема схлопывается до отмеченных состояний.

```python
backend = Aer.get_backend('qasm_simulator')
job = execute( circuit, backend, shots=1024 )
hist = job.result().get_counts()
plot_histogram( hist )
```

## 6. Ссылки

1. Ренато Португалия. Квантовые блуждания и алгоритмы поиска. Нью-Йорк, штат Нью-Йорк: Springer, Нью-Йорк, 2013 г.
2. Маркус Г. Кун. Некоторые вводные заметки о квантовых вычислениях. апрель 2000 г.
3. Томас Г. Вонг. «Эквивалентность Сегеди и придуманных квантовых блужданий». В: Квантовая обработка информации 16.9 (июль 2017 г.). ISSN: 1573-1332. DOI: 10.1007/s11128-017-1667-y. URL-адрес: http://dx.doi.org/10.1007/s11128-017-1667-y.37
4. Рональд де Вольф. Квантовые вычисления: конспект лекций. 2021. arXiv: 1907.09415 [квант-ф]

```python
import qiskit.tools.jupyter
%qiskit_version_table
```

    /usr/local/anaconda3/envs/terra-unstable/lib/python3.9/site-packages/qiskit/aqua/__init__.py:86: DeprecationWarning: The package qiskit.aqua is deprecated. It was moved/refactored to qiskit-terra For more information see <https://github.com/Qiskit/qiskit-aqua/blob/main/README.md#migration-guide>
      warn_package('aqua', 'qiskit-terra')

Version Information

Qiskit Software	Version

qiskit-terra	0.19.0

qiskit-aer	0.9.1

qiskit-ignis	0.6.0

qiskit-ibmq-provider	0.17.0

qiskit-aqua	0.9.5

qiskit	0.31.0

System information

Python version	3.9.7

Python compiler	Clang 10.0.0

Python build	default, Sep 16 2021 08:50:36

OS	Darwin

CPUs	8

Memory (Gb)	32.0

Fri Nov 05 15:38:53 2021 GMT


# Multidimensional Scaling
This is the second part in *Manifold Learning: Introduction and Foundational Algorithms*, and the entire project can be viewed at https://github.com/drewwilimitis/Manifold-Learning

`Drew Wilimitis`

**Multidimensional scaling** (MDS) is an algorithm used for dimensionality reduction, and is most often applied to visualize the similarities in datasets. Although MDS can be considered as a manifold learning algorithm, in general we will see that MDS uses a linear projection, and that the Classical MDS algorithm is equivalent to Principal Component Analysis (PCA). MDS was one of the earliest developed methods for dimensionality reduction, and it has broad applications for high dimensional data in genetics, psychology, sociology, political science, ecology, and many other domains related to modeling structural relationships between objects and conveying their similarities spatially [1](#fn1).

Multidimensional scaling uses pairwise similarity measures (which can be similarity measures of any kind, even qualitative ratings of similarity) to construct a spatial representation that keeps similar objects close together and dissimilar objects further apart. In this way, the goal is to capture the structure of the higher dimensional data in a lower dimensional representation.

Here's a few examples from the social sciences:

The figure shows an application of multidimensional scaling in clinical psychology - the spatial representation illustrates dimensions of repetitive thought. The horizontal axis 'Valence' reflects whether the content of thoughts is positive or negative. The vertical axis 'Purpose' reflects whether the tone of thoughts is searching, questioning, and/or uncertain (negative numerical values) or largely solving, planning, and/or certain (positive numerical values). Notice that similar types are close together (self-analysis, reflection) and dissimilar types are further apart (reminiscing, worry). Source: https://www.apa.org/science/about/psa/2011/03/repetitive-thought

 This figure shows Multidimensional scaling applied to voting patterns of Democrats (blue dots) and Republicans (red dots) in the US House of Representatives. Source: https://en.wikipedia.org/wiki/Multidimensional_scaling#Types

**Goal**: MDS differs from many other dimensionality reduction methods in that the input to MDS is only the dissimilarity matrix, instead of the actual position vectors of the data. Given a pairwise dissimilarity matrix $D$ with entries $d_{ij}$ for the distance/dissimilarity between observations $i$ and $j$, we find $x_1, ..., x_n \in \mathbb{R}^k$ such that:

$$\underbrace{d_{ij}^2}_{\text{original distances}} \approx \underbrace{||x_{i} - x_{j}||^2}_{\text{output configuration}}$$

We find a configuration (typically a lower dimensional configuration in $\mathbb{R}^2$) that keeps the Euclidean distances in $\mathbb{R}^k$ as close as possible to our original distances/similarities.

**Note:** MDS considers global similarities by attempting to preserve all the pairwise distances (instead of preserving local neighborhood similarities like many other manifold learning algorithms), and this limits the ability of MDS to produce non-linear embeddings.

**Note:** the original distances in $D$ do not have to be Euclidean distances. They can be pairwise dissimilarities, even for qualitative comparisons between any type of objects. The algorithm will be slightly modified when the original distances are Euclidean (classical MDS) vs. non-Euclidean or even direct qualitative similarities (general metric or non-metric MDS).

### Algorithm Outline (Classical MDS):
**Input:** Pairwise distances/dissimilarities for n objects given by matrix $D \in \mathbb{R}^{n \times n}$ with entries $d_{ij}$, with one parameter k (specified output dimension)

**Output:** Configuration vectors $x_1, ..., x_n \in \mathbb{R}^k$

**Steps**:

1. Compute Gram matrix & double centering: from the pairwise dissimilarity matrix find Gram matrix $B \in \mathbb{R}^{n \times n}$ where we double center with $B = \frac{-1}{2} C_n D^{2} C_{n}$

2. Find eigenvalues and eigenvectors: Find eigenvalues $\lambda$ and eigenvectors $\mathbf{v}$ of Gram matrix and form matrices $\mathbf{\Lambda}_{n}=\operatorname{diag}\left(\lambda_{1}, \ldots, \lambda_{n}\right)$ and $\boldsymbol{V}_{n}=\left(\boldsymbol{v}_{1}, \ldots, \boldsymbol{v}_{n}\right)$
3. Lower dimensional embedding:  use the top k eigenvectors $v_1, ..., v_k$ (with the largest eigenvalues) and decomposition $X = V_{k} \sqrt{\Lambda}_{k} $ to form matrix $X = (x_1, \ldots, x_n)^{T}$ with lower dimensional output vectors $x_i \in \mathbb{R}^k$

**Classical MDS:** When the original distances are Euclidean, we use classical multidimensional scaling and we try to find a configuration so that the configured distances are approximately equal to the original Euclidean distances.

## Step 1: Double Centering and the Gram Matrix

To find the matrix $X$ with the output coordinates, we use the fact that the $n\times n$ Gram matrix $B$ will be defined by the inner products of $X$.

$$\underbrace{\mathbf{B}}_{n\times n} = \underbrace{\mathbf{X}}_{n\times k} \cdot \underbrace{\mathbf{X}^T}_{k\times n}$$

Which we write out explicitly as

$$\mathbf{B} = \mathbf{X X^T} = \begin{bmatrix}
x_{1,1} & x_{1,2} & \cdots & x_{1,k} \\
x_{2,1} & x_{2,2} & \cdots & x_{2,k} \\
\vdots & \vdots & \vdots & \vdots \\
x_{n,1} & x_{n,2} & \cdots & x_{n,k}
\end{bmatrix}
\begin{bmatrix}
x_{1,1} & x_{2,1} & \cdots & x_{n,1} \\
x_{1,2} & x_{2,2} & \cdots & x_{n,2} \\
\vdots & \vdots & \vdots & \vdots \\
x_{1,k} & x_{2,k} & \cdots & x_{n,k}
\end{bmatrix}$$

The entries of $B$ are given by $b_{ij} = \langle x_i , x_j \rangle = \sum_{k} x_{ik} x_{jk}$. The expression of the gram matrix involving output configuration $X$ is only shown here to illustrate that once we compute the gram matrix, we can then derive the output vectors. However, we will obviously have to use our only input, the dissimilarity matrix, in order to find the gram matrix $B$.

**Important detail/constraint**: The configuration of our solution will not be unique, as arbitrary translations, rotations, and reflections will not affect the pairwise distances. For example, if we have the solution as matrix $X = [x_1, \ldots, x_n]^{T}$, then we also have the solution $X^* = [x_1 + c, \ldots, x_n + c]^{T}$, translated by some vector $c \in \mathbb{R}^k$:

$$
\begin{equation}
\begin{aligned}
d_{ij}^2 & \approx ||x_{i} - x_{j}||^2 & \text{since X is a solution}\\
\\
            & = ||(x_{i} + c) - (x_{j} + c)||^2\\
\\
            & = ||x^*_{i} - x^*_{j}||^2\\
\end{aligned}
\end{equation}
$$

Therefore, we must also have $d_{ij}^2 \approx ||x^*_{i} - x^*_{j}||^2$.

The configurations that result from an arbitrary translation will all be solutions, and similar arguments can show that the pairwise distances will also be unaffected by rotations and reflections. Intuitively, the reasoning is that the distances are only defined pairwise, and not in terms of a global coordinate system.

### Double Centering

To incorporate the translational invariance, we need to center the gram matrix around 0 by subtracting the column and row means from every entry. Double centering is a technique that uses the centering matrix $C_n$:

$$C_{n} = I_{n} - \frac{1}{n} \mathbf{1}\mathbf{1^T}$$

where $I_{n}$ is the Identity matrix for dimension $n$, $\mathbf{1}$ is a column vector of n ones, and $\mathbf{1}\mathbf{1^T}$ gives the $n\times n$ matrix of all ones.

For example, $$C_{2} = \begin{bmatrix}
1 & 0 \\
0 & 1
\end{bmatrix} - \frac{1}{2} \begin{bmatrix}
1 & 1 \\
1 & 1
\end{bmatrix} = \begin{bmatrix}
1/2 & -1/2 \\
-1/2 & 1/2
\end{bmatrix} $$

For some matrix $X \in \mathbb{R}^{n \times n}$: $C_{n} X$ subtracts the column means from the entries of $X$, and $X C_{n}$ subtracts the row means from every entry of $X$. Hence, the operation $C_{n}X C_{n}$ removes both row and column means and is referred to as double centering.

We now generate some data and demonstrate the use of the centering matrix in python:

```python
# import libraries
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# plotting style
%matplotlib inline
plt.style.use('seaborn')
# display multiple outputs within a cell
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all";
# ignore warnings
import warnings
warnings.filterwarnings('ignore');

N = 100 # number of data points
u = 3 + np.random.randn(N) # first feature dimension with mean = 3
v = 7 + np.random.randn(N) # second feature dimensions with mean = 7
X = np.hstack((u.reshape(-1,1),v.reshape(-1,1))) # 100 x 2 data matrix
# view first 5 rows and the column means
X[:5,:]
print('Column 1 mean: ', np.mean(X[:, 0]))
print('Column 2 mean: ', np.mean(X[:, 1]))
```

    array([[1.75735143, 7.60942924],
           [2.06384298, 8.36373258],
           [3.30784778, 5.83195902],
           [2.88511324, 5.35317008],
           [1.97823489, 8.02038741]])

    Column 1 mean:  3.0044980230041336
    Column 2 mean:  7.079762533566981

```python
# lets visualize the data:
plt.scatter(X[:, 0], X[:, 1], cmap=plt.cm.jet)
plt.show();
```

The data is centered around (3, 7), and we can find the pairwise distances with the scikit-learn library, and plot a heatmap to visualize a corner of the 100x100 distance matrix.

```python
from sklearn.metrics import pairwise_distances
D = pairwise_distances(X)
plt.figure(figsize=(10,6))
sns.heatmap(D[:10, :10], cmap="coolwarm", annot=True);
```

Notice that the distance matrix is symmetric and non-negative with zeros on the diagonal since the distance from every point to itself is zero. Now we define the centering matrix and multiply with our data matrix X to remove the column means, resulting in data centered around (0, 0).

```python
# function to form centering matrix
centering_matrix = lambda n: np.identity(n) -(1/n)*np.ones((n,n))
# multiply by data matrix X
X_centered = np.dot(centering_matrix(100), X)
# plot the centered data:
plt.scatter(X_centered[:, 0], X_centered[:, 1], cmap=plt.cm.jet)
plt.show();
```

```python
# verify that the pairwise distances are unchanged
D_centered = pairwise_distances(X_centered)
np.allclose(D, D_centered)
```

    True

**Closed Form of the Gram Matrix with Centering:**

We can now succinctly state the closed matrix form of $B$ by making use of the centering matrix:

$$\boxed {B = \frac{-1}{2} C_n D^{2} C_{n}}$$

**Note:** $C_n$ is the centering matrix with $C_{n} = I_{n} - \frac{1}{n} \mathbf{1}\mathbf{1^T}$ and from the original distance matrix we have $D^{2}$ = matrix with entries ${d_{ij}^2}$

## Step 2: Find eigenvalues and eigenvectors of matrix B

Since the gram matrix $B$ is a real symmetric, positive definite matrix, we know that it will have real eigenvalues and we can use the following eigendecomposition of B in order to find an expression for our output configuration:

$$
\begin{aligned}
B & = V \Lambda V^T\\
            & = {(\Lambda^\frac{1}{2} V^{T})}^T (\Lambda^\frac{1}{2} V^{T})\\
            & = X X^T & \text{(from original def. of B)}\\
\end{aligned}
$$

and therefore we have

$$X = V \sqrt{\Lambda}$$

where the eigenvalues are given by diagonal matrix $\mathbf{\Lambda}=\operatorname{diag}\left(\lambda_{1}, \ldots, \lambda_{n}\right)$ and the eigenvectors are given by the following matrix with the columns set as the eigenvectors $V=\left(v_{1}, \ldots, v_{n}\right)^{T}$

## Step 3: Find coordinates of output configuration

We can now define a k-dimensional configuration by choosing the largest k eigenvalues and the corresponding eigenvectors from k columns of V:

$$ \boxed{X_{k} = V_{k} \sqrt{\Lambda_{k}}}$$

where $\Lambda_{k}$ is the k x k diagonal submatrix of $\Lambda$ and $V_{k}$ is the n x k submatrix of $V$.

## Classical MDS and equivalence to PCA

Without going into the full details of Principal Component Analysis, I'll attempt to show that Classical MDS with Euclidean distances produces the same results as PCA.

In the standard PCA approach, we have an input data matrix $X$ and (after centering the data) we find the singular value decomposition of $X$:

$$
\mathbf{X}=\mathbf{U \Sigma W}^{T}
$$

where $\Sigma$ is a diagonal matrix with singular values $\sigma_{(k)}$, and matrices $U, W$ are orthogonal matrices s.t $\mathbf{W}^{T} \mathbf{W}=\mathbf{I} = \mathbf{U}^{T} \mathbf{U}$

The output $Y_k$ is produced by projecting the data onto the first $k$ principal components by multiplying $X$ and the first $k$ columns of $W$:

$$\begin{aligned} \mathbf{Y_k} &= \mathbf{X} \mathbf{W_k} \\
&= \mathbf{U_k \Sigma_k W_k^{T} W_k} \\
&= \mathbf{U_k} \Sigma_k & (\text{since we have  } \mathbf{W}^{T} \mathbf{W}=\mathbf{I})\\
\end{aligned}
$$

To see the equivalence between PCA and classical MDS, consider our previous eigenvector decomposition of $X X^T$ from classical MDS. We now show that these left singular values $U_k$ are equivalent to the eigenvectors of $X X^T$, and that the singular values $\sigma_{(k)}$ of $\mathbf{X}$ are equivalent to the square root of the eigenvalues of $X X^T$. We rewrite $X X^T$ using the SVD:

$$\begin{aligned} \mathbf{X} \mathbf{X}^{T} &=(\mathbf{U} \boldsymbol{\Sigma} \mathbf{W}^{T}) (\mathbf{W} \boldsymbol{\Sigma}^{T} \mathbf{U}^{T}) \\
&=\mathbf{U} \boldsymbol{\Sigma} \mathbf{\Sigma}^{T} \mathbf{U}^{T} & (\text{since we have  } \mathbf{W}^{T} \mathbf{W}=\mathbf{I}) \\
&= (\mathbf{U} \mathbf{\Sigma}) (\mathbf{\Sigma} \mathbf{U}^{T}) \\
&= V \Lambda V^T & \text{(eigendecomposition above)} \\
&= (V_{k} \sqrt{\Lambda_{k}}) (\sqrt{\Lambda_{k}} V^{T})\\
\end{aligned}$$

Therefore, we have:

$$ \mathbf{U_k} \Sigma_k = V_{k} \sqrt{\Lambda_{k}}$$

and so finding the eigenvalue decomposition of the Gram matrix above (classical MDS) produces the same results as using the singular value decomposition and projecting onto the principal components (PCA).

## Metric vs. Non-metric MDS
There are many different variants of MDS that can be applied in different situations, and so the broad categories are only briefly covered below.

**Metric MDS** - dissimilarities are quantitative but not necessarily Euclidean (classical MDS is a special case)

For metric MDS, we have loss function defined as:

$$Stress_{D}\left(x_{1}, x_{2}, \ldots, x_{N}\right)=\left(\sum_{i \neq j=1, \ldots, N}\left(d_{i j}-\left\|x_{i}-x_{j}\right\|\right)^{2}\right)^{1 / 2}
$$

We attempt to find the configuration $x_1, ..., x_n$ to minimize this error given our input matrix $D$

**Non-metric MDS** - dissimilarities are qualitative (similarity measures are interpreted more as rankings or ordinal classifications)

In the non-metric case, the dissimilarity matrix we are given is only important in its relative ranking of the observations, not the quantitative degree to which observations differ. For non-metric MDS, we have a monotonic function $f$ and we find a configuration that *only preserves relative ranking of the data*.

If $d_{ij} < d_{ik}, \Rightarrow f(d_{ij}) \leq f(d_{ik})$

Non-metric MDS can also be stated as the problem of finding the optimal configuration $x_1, ..., x_n \in \mathbb{R}^k$ that minimizes the following loss function:

$$
Stress_{D}\left(x_{1}, x_{2}, \ldots, x_{N}\right)=\sqrt{\frac{\sum(f(x)-d)^{2}}{\sum d^{2}}}
$$

###  Example - MDS and Non-linear Embeddings

We use `sklearn.datasets` to generate a non-linear dataset that is often used to test manifold learning algorithms, and we use `sklearn.manifold` to apply MDS and visualize the resulting embedding.

```python
from sklearn.datasets.samples_generator import make_s_curve
from mpl_toolkits import mplot3d
X, color = make_s_curve(1000, random_state=0)
plt.style.use('default')
#plt.rcParams['figure.facecolor'] = 'white'

# set up a figure twice as wide as it is tall
fig = plt.figure(figsize=plt.figaspect(0.5))

# set up the axes for the first plot
ax = fig.add_subplot(1, 2, 1, projection='3d')
ax.scatter3D(X[:, 0], X[:, 1], X[:, 2], c=color, cmap=plt.cm.jet);
plt.title('S-curve in $\mathbb{R}^3$')

# set up the axes for the second plot
ax = fig.add_subplot(1, 2, 2, projection='3d')
plt.title('Alternate view')
ax.scatter3D(X[:, 0], X[:, 1], X[:, 2], c=color, cmap=plt.cm.jet)
ax.view_init(4,90);
```

```python
from sklearn.manifold import MDS
model = MDS(n_components=2, random_state=2)
out = model.fit_transform(X)
fig = plt.figure(figsize=(4,4))
plt.scatter(out[:, 0], out[:, 1], c=color, cmap=plt.cm.jet)
plt.title('2D MDS Projection')
plt.show();
```

Multidimensional scaling is unable to accurately reproduce the non-linear, manifold structure of the S-curve and instead seemingly just produces a linear projection onto a plane. The reason MDS is generally unable to learn non-linear embeddings is that MDS attempts to preserve global distances based on an ambient or extrinsic coordinate system instead of learning the intrinsic structure by only considering local neighborhoods.

The local neighborhood approach is used by the Isomap, Locally Linear Embedding, and Spectral Embedding algorithms that I cover in the subsequent sections, and fitting locally instead of globally produces much better non-linear embeddings.

###  Example - MDS with Handwritten Digits

To get a sense of how MDS works on datasets, the MDS algorithm is implemented below and compared with the `sklearn.manifold` package on the MNIST handwritten digits dataset

```python
# import digits data
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')

X = mnist['data']
y = mnist['target']
mnist.data.shape

fig, ax = plt.subplots(6, 8, subplot_kw=dict(xticks=[], yticks=[]))
for i, axi in enumerate(ax.flat):
    _ = axi.imshow(mnist.data[1250 * i].reshape(28, 28), cmap='gray_r');
```

Each of the 70,000 data points is a 784 dimensional vector, where each feature-dimension is a number from 0 to 255 representing pixel intensity. We will attempt to find a 2-dimensional spatial representation that conveys the similarities among the data points and keeps data in the same classes close together.

Next I define a function to perform Classical MDS and to demonstrate its equivalence to PCA I plot their results on the digits dataset side by side.

```python
def ClassicalMDS(dist_matrix, k):
    """Performs Classical Multidimensional Scaling
    Parameters
    ----------
    dist_matrix : Pairwise dissimilarity/distance matrix (n x n)
    k: Dimension of the output configuration

    Returns
    -------
    X : Matrix with columns as the output configuration vectors (k x n)
    """
    # get shape of distance matrix
    n = dist_matrix.shape[0]

    # check distance matrix is symmetric
    if not np.allclose(np.transpose(dist_matrix),dist_matrix):
        print('Distance matrix must be symmetric')
        return

    # centering matrix
    C = np.identity(n) -(1/n)*np.ones((n,n))

    # compute gram matrix
    B = -(1/2)*C.dot(dist_matrix**2).dot(C)

    # solve for eigenvectors and eigenvalues and sort descending
    w, v = np.linalg.eigh(B)
    idx   = np.argsort(w)[::-1]
    eigvals = w[idx]
    eigvecs = v[:,idx]

    # select k largest eigenvalues and eigenvectors
    Lambda  = np.diag(np.sqrt(eigvals[:k]))
    V  = eigvecs[:,:k]
    X  = np.dot(Lambda, np.transpose(V))
    X = np.transpose(X)

    return X
```

```python
# get random subset of digits data
sample = np.random.permutation(mnist.data.shape[0])[:1000]
data = mnist.data[sample]
labels = mnist.target[sample]
```

```python
# get a subset of the mnist dataset
np.random.seed(0)
# set up a figure twice as wide as it is tall
fig = plt.figure(figsize=plt.figaspect(0.5))

# compute pairwise distance matrix with sklearn
D = pairwise_distances(data)

# set up the axes for the first plot
ax = fig.add_subplot(1, 2, 1)
# plot results of MDS
X = ClassicalMDS(pairwise_distances(data), 2)
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap=plt.cm.get_cmap('jet', 10))
plt.colorbar(ticks=range(10))
plt.clim(-0.5, 9.5);
plt.title('Classical MDS')

# set up the axes for the second plot
ax = fig.add_subplot(1, 2, 2)
clf = PCA(n_components=2)
X_pca = clf.fit_transform(data)
ax.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap=plt.cm.get_cmap('jet', 10))
plt.colorbar(ticks=range(10))
plt.clim(-0.5, 9.5)
plt.title('PCA');
```

The results are the same for PCA and Classical MDS, although they may be off by a rotational degree of freedom due to underlying randomness in the implementations.

Classical MDS does a decent job of keeping similar digits clustered together in the plane. It seems that the 0 and the 1 groups were respectively clustered the closest together. The 0's and 1's are also the furthest apart which makes sense considering they have quite different visual structures.

We now use MDS from the `sklearn.manifold` library

```python
from sklearn.manifold import MDS

model = MDS(n_components=2)
proj = model.fit_transform(data)
plt.scatter(proj[:, 0], proj[:, 1], c=labels, cmap=plt.cm.get_cmap('jet', 10))
plt.colorbar(ticks=range(10))
plt.clim(-0.5, 9.5);
```

The scikit-learn implementation uses a variation of the metric MDS algorithm called SMACOF that seems to perform better than the previous classical MDS implementation. There is more apparent segmentation of the digits into the various clusters.

Next, we apply MDS to some correlation data for various crimes in the United States (Borg & Groenen, 1997).

```python
murder = np.array([1.00, 0.52, 0.34, 0.81, 0.28, 0.06, 0.11])
rape = np.array([0.52, 1.00, 0.55, 0.70, 0.68, 0.60, 0.44])
robbery = np.array([0.34, 0.55, 1.00, 0.56, 0.62, 0.44, 0.62])
assault = np.array([0.81, 0.70, 0.56, 1.00, 0.52, 0.32, 0.33])
burglary = np.array([0.28, 0.68, 0.62, 0.52, 1.00, 0.80, 0.70])
larceny = np.array([0.06, 0.60, 0.44, 0.32, 0.80, 1.00, 0.55])
auto_theft = np.array([0.11, 0.44, 0.62, 0.33, 0.70, 0.55, 1.00])

crime_matrix = np.array([murder, rape, robbery, assault, burglary, larceny, auto_theft])
crime_matrix
crime_matrix.shape
```

    array([[1.  , 0.52, 0.34, 0.81, 0.28, 0.06, 0.11],
           [0.52, 1.  , 0.55, 0.7 , 0.68, 0.6 , 0.44],
           [0.34, 0.55, 1.  , 0.56, 0.62, 0.44, 0.62],
           [0.81, 0.7 , 0.56, 1.  , 0.52, 0.32, 0.33],
           [0.28, 0.68, 0.62, 0.52, 1.  , 0.8 , 0.7 ],
           [0.06, 0.6 , 0.44, 0.32, 0.8 , 1.  , 0.55],
           [0.11, 0.44, 0.62, 0.33, 0.7 , 0.55, 1.  ]])

    (7, 7)

We use this correlation matrix as the pairwise similarity matrix so we can simply input this correlation data into the MDS algorithm. A good spatial representation will keep data points with high correlations closer to each other.

```python
# set random seed
np.random.seed(0)
plt.style.use('seaborn')
# metric MDS
model = MDS(n_components=2)
X = model.fit_transform(crime_matrix)
X
labels = ['murder', 'rape', 'robbery', 'assault', 'burglary', 'larceny', 'auto_theft']
# plot results
_ = plt.figure(figsize=(16,7))
_ = plt.subplot(1,2,1)
_ = plt.scatter(X[:, 0], X[:, 1], c='black', s=30)
_ = plt.title('Metric MDS', size=20)
for i in range(len(labels)):
    _ = plt.text(X[i][0]+0.02, X[i][1]+0.02, s=labels[i], size=18)

# non-metric MDS
_ = plt.subplot(1,2,2)
model = MDS(n_components=2, metric=False)
proj = model.fit_transform(crime_matrix)
labels = ['murder', 'rape', 'robbery', 'assault', 'burglary', 'larceny', 'auto_theft']
# plot results
_ = plt.scatter(proj[:, 0], proj[:, 1], c='black', s=30)
_ = plt.title('Non-metric MDS', size=20)
for i in range(len(labels)):
    _ = plt.text(proj[i][0]+0.02, proj[i][1]+0.02, s=labels[i], size=18)
```

The 2-d representation does a pretty good job of spatially displaying the similarities between the different crimes. There's not a lot of noticeable difference for the metric vs. non-metric results. Looking at the metric MDS projection, the y-axis might represent the personal vs. impersonal nature of the crimes and the x-axis might represent the level of violence inherent in the crime. As the data goes from the top right to the bottom left, the crimes get more personal and violent.

Using the pairwise distances between US cities, we can use MDS to try and recreate a two dimensional representation that accurately portrays the spatial relationships in the United States.

```python
import pandas as pd
cities = pd.read_csv('C:/Users/dreww/Desktop/manifolds/cities_df.csv')
cities.head()
labels = list(cities.columns[1:])
cities = cities.iloc[:,1:]
dist_cities = np.array(cities)
```

      Unnamed: 0
      Atlanta
      Chicago
      Denver
      Houston
      LosAngeles
      Miami
      NewYork
      SanFrancisco
      Seattle
      Washington.DC

      0
      Atlanta
      0
      587
      1212
      701
      1936
      604
      748
      2139
      2182
      543

      1
      Chicago
      587
      0
      920
      940
      1745
      1188
      713
      1858
      1737
      597

      2
      Denver
      1212
      920
      0
      879
      831
      1726
      1631
      949
      1021
      1494

      3
      Houston
      701
      940
      879
      0
      1374
      968
      1420
      1645
      1891
      1220

      4
      LosAngeles
      1936
      1745
      831
      1374
      0
      2339
      2451
      347
      959
      2300

```python
# metric MDS
np.random.seed(0)
model = MDS(n_components=2)
X = model.fit_transform(dist_cities)
# plot results
_ = plt.figure(figsize=(8,8))
_ = plt.scatter(X[:, 0], X[:, 1], c='black')
_ = plt.title('US Cities', size=20)
for i in range(len(labels)):
    _ = plt.text(X[i][0]+0.02, X[i][1]+0.02, s=labels[i], size=16)
```

We can do the same for European cities

```python
eurodist = pd.read_csv('C:/Users/dreww/Desktop/manifolds/eurodist.csv')
eurodist.head()
labels = list(eurodist.columns)
```

      Athens
      Barcelona
      Brussels
      Calais
      Cherbourg
      Cologne
      Copenhagen
      Geneva
      Gibraltar
      Hamburg
      ...
      Lisbon
      Lyons
      Madrid
      Marseilles
      Milan
      Munich
      Paris
      Rome
      Stockholm
      Vienna

      0
      0
      3313
      2963
      3175
      3339
      2762
      3276
      2610
      4485
      2977
      ...
      4532
      2753
      3949
      2865
      2282
      2179
      3000
      817
      3927
      1991

      1
      3313
      0
      1318
      1326
      1294
      1498
      2218
      803
      1172
      2018
      ...
      1305
      645
      636
      521
      1014
      1365
      1033
      1460
      2868
      1802

      2
      2963
      1318
      0
      204
      583
      206
      966
      677
      2256
      597
      ...
      2084
      690
      1558
      1011
      925
      747
      285
      1511
      1616
      1175

      3
      3175
      1326
      204
      0
      460
      409
      1136
      747
      2224
      714
      ...
      2052
      739
      1550
      1059
      1077
      977
      280
      1662
      1786
      1381

      4
      3339
      1294
      583
      460
      0
      785
      1545
      853
      2047
      1115
      ...
      1827
      789
      1347
      1101
      1209
      1160
      340
      1794
      2196
      1588

5 rows × 21 columns

```python
np.random.seed(0)
euro_cities = np.array(eurodist)
model = MDS(n_components=2)
X = model.fit_transform(euro_cities)
# plot results
_ = plt.figure(figsize=(8,8))
_ = plt.scatter(X[:, 0], X[:, 1], c='black')
_ = plt.title('European Cities', size=20)
for i in range(len(labels)):
    _ = plt.text(X[i][0]+0.02, X[i][1]+0.02, s=labels[i], size=16)
```

In practice, multidimensional scaling has a broad range of applications and it offers a significant capability to produce spatial representations, even for qualitative similarity comparisons when we only need to input a set of pairwise similarity comparisons. However, MDS is quite limited by the fact that it considers the entire global set of pairwise distances. This significantly limits the ability of MDS to learn non-linear manifold structures. As we will see with the next algorithms, methods based only on the local neighborhoods can more effectively learn the intrinsic structure of the data.

**Note:** verifying the geographical accuracy of these last projections will, of course, be left as an exercise to the reader

```python
from IPython.core.display import HTML

def css_styling():
    styles = open("custom_style.css", "r").read()
    return HTML(styles)
css_styling()
```

___

### References

-  [1] https://en.wikipedia.org/wiki/Multidimensional_scaling
-  [2] https://en.wikipedia.org/wiki/Centering_matrix
-  [3] Wickelmaier, Florian. "An introduction to MDS." Sound Quality Research Unit, Aalborg University, Denmark (2003)

### Notes
 **1:**
Perhaps the most fascinating discussion of Multidimensional scaling comes from the cognitive scientist RN Shepard, who proposed a 'Universal law of generalization' to describe perception and mental representations in terms of an underlying mathematical structure, such that the universal laws governing the universe might be reflected in laws that govern conscious minds. He states "Invariance of the laws is achieved by constructing the representational spaces from psychological rather than physical data (using multidimensional scaling) and from considerations of geometry, group theory, and symmetry" (RN Shepard 2004). Shepard also says "These empirical regularities are mathematically derivable from universal principles of natural kinds and probabilistic geometry that may, through evolutionary internalization, tend to govern the behaviors of all sentient organisms" (RN Shepard 1987)


## Quantum Computing - Exploration

```python
import numpy as np
from qiskit import *
```

### First, create a quantum circuit with 3 qubits.

```python
circ = QuantumCircuit(3)
```

This circuit provides the basis for the quantum calculation's we'll do later. Essentially, this is the statevector. Let's perform some gate operations on this circuit to create a GHZ statevector.

```python
circ.h(0)
circ.cx(0,1)
circ.cx(0,2)
```

    <qiskit.circuit.instructionset.InstructionSet at 0x7fd63a583eb8>

Awesome! Now let's visualize this state circuit.

```python
circ.draw()
```

        ┌───┐
q_0: |0>┤ H ├──■────■──
        └───┘┌─┴─┐  │
q_1: |0>─────┤ X ├──┼──
             └───┘┌─┴─┐
q_2: |0>──────────┤ X ├
                  └───┘

Now that we have a quantum circuit set up, let's use a package to simulate it. This package is Qiskit Aer. To start the simulation, we need to choose a backend (section of Aer we want to use).

```python
backend = Aer.get_backend('statevector_simulator')
```

Below I go through the commands to get the quantum state from a given circuit.
1. execute a circuit on a backend simulation, which returns a job object
2. take the result of that given job object and pul the statevector from that result.

This returns a statevector of dimension $2^n$, where n is the number of qubits.

```python
job = execute(circ, backend)
result = job.result()
outputstate = result.get_statevector(circ,decimals=3)
print(outputstate)
```

    [0.707+0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j 0.   +0.j
     0.707+0.j]

Let's visualize this state.

```python
from qiskit.visualization import plot_state_city
plot_state_city(outputstate)
```

Interpreting this output can be pretty difficult. These are the real and imaginary state density matrices, from which the probability of any measurment can be asertained.

### Next up: Unitary Simulations

Aer also has a unitary simulator for use in calculations where all the elements in the circuit are unitary operations. The unitary simulator returns a $2^n x 2^n$ matrix.

```python
backend = Aer.get_backend('unitary_simulator')
job = execute(circ, backend)
result = job.result()
print(result.get_unitary(circ,decimals=3))
plot_state_city(result.get_unitary(circ,decimals=3))
```

### Measurment Simulations

Yet another Aer backend is available to simulate actual measurments, not simply ideal/theoretical information on the state that the previous backends gave us.

```python
meas = QuantumCircuit(3,3)
meas.barrier(range(3))

#This next line maps the quantum measurment to classical bits
meas.measure(range(3),range(3))

qc = circ+meas

qc.draw()
```

        ┌───┐           ░ ┌─┐
q_0: |0>┤ H ├──■────■───░─┤M├──────
        └───┘┌─┴─┐  │   ░ └╥┘┌─┐
q_1: |0>─────┤ X ├──┼───░──╫─┤M├───
             └───┘┌─┴─┐ ░  ║ └╥┘┌─┐
q_2: |0>──────────┤ X ├─░──╫──╫─┤M├
                  └───┘ ░  ║  ║ └╥┘
 c_0: 0 ═══════════════════╩══╬══╬═
                              ║  ║
 c_1: 0 ══════════════════════╩══╬═
                                 ║
 c_2: 0 ═════════════════════════╩═

To simulate the circuit, we use the qasm simulator in Aer. Each run of this GHZ circuit will yield either 000 or 111. We can use the execute command to build up statistics on the distribution of these results.

```python
backend_sim = Aer.get_backend('qasm_simulator')

job_sim = execute(qc,backend_sim,shots=1024)
result_sim = job_sim.result()

counts = result_sim.get_counts(qc)
print(counts)
```

    {'000': 504, '111': 520}

```python
from qiskit.visualization import plot_histogram
plot_histogram(counts)
```

### IBM Q

Qiskit is supported by IBM, and we can use IBM's public quantum computers and API to test our code!

```python
from qiskit import IBMQ
```

```python
IBMQ.save_account('caa130bd16fc8c10d09c1e18bf99e63d1285cbe215c8b82ed6c30cc5997077238a2908271e30cc407577d4d34bd400fbabe66a90c959be45f8269f891cc645c9', overwrite = True)
```

```python
IBMQ.load_account()
```

    <AccountProvider for IBMQ(hub='ibm-q', group='open', project='main')>

```python
IBMQ.providers()
provider = IBMQ.get_provider(group='open')
```

```python
provider.backends()
```

    [<IBMQSimulator('ibmq_qasm_simulator') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmqx2') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_16_melbourne') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_vigo') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_ourense') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_london') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_burlington') from IBMQ(hub='ibm-q', group='open', project='main')>,
     <IBMQBackend('ibmq_essex') from IBMQ(hub='ibm-q', group='open', project='main')>]

```python
backend = provider.get_backend('ibmqx2')
```

```python
from qiskit.tools.monitor import job_monitor

job_exp = execute(qc,backend=backend)
job_monitor(job_exp)
```

    Job Status: job has successfully run

```python
result_exp = job_exp.result()
```

```python
counts_exp = result_exp.get_counts(qc)
plot_histogram([counts_exp,counts], legend=['Device','Simulator'])
```

## Part 4 - Characteristics of Quantum Circuits

```python
#create a 12 qubit circuit
qc = QuantumCircuit(12)

#run a hadamard gate on each of the first five, and put each of the results of these into a bell state (through a c-not gate) with the subsequent 5 qubits.
for idx in range(5):
    qc.h(idx)
    qc.cx(idx, idx+5)

#q1 is also in a bell state to q7
qc.cx(1, 7)
#simply NOT q8
qc.x(8)
#etc
qc.cx(1, 9)
qc.x(7)
qc.cx(1, 11)

qc.swap(6, 11)
qc.swap(6, 9)
qc.swap(6, 10)
qc.x(6)
qc.draw()
```

         ┌───┐
 q_0: |0>┤ H ├──■────────────────────────────────────────────────────────
         ├───┤  │
 q_1: |0>┤ H ├──┼────■───────────────────■────■─────────■────────────────
         ├───┤  │    │                   │    │         │
 q_2: |0>┤ H ├──┼────┼────■──────────────┼────┼─────────┼────────────────
         ├───┤  │    │    │              │    │         │
 q_3: |0>┤ H ├──┼────┼────┼────■─────────┼────┼─────────┼────────────────
         ├───┤  │    │    │    │         │    │         │
 q_4: |0>┤ H ├──┼────┼────┼────┼────■────┼────┼─────────┼────────────────
         └───┘┌─┴─┐  │    │    │    │    │    │         │
 q_5: |0>─────┤ X ├──┼────┼────┼────┼────┼────┼─────────┼────────────────
              └───┘┌─┴─┐  │    │    │    │    │         │           ┌───┐
 q_6: |0>──────────┤ X ├──┼────┼────┼────┼────┼─────────┼───X──X──X─┤ X ├
                   └───┘┌─┴─┐  │    │  ┌─┴─┐  │  ┌───┐  │   │  │  │ └───┘
 q_7: |0>───────────────┤ X ├──┼────┼──┤ X ├──┼──┤ X ├──┼───┼──┼──┼──────
                        └───┘┌─┴─┐  │  ├───┤  │  └───┘  │   │  │  │
 q_8: |0>────────────────────┤ X ├──┼──┤ X ├──┼─────────┼───┼──┼──┼──────
                             └───┘┌─┴─┐└───┘┌─┴─┐       │   │  │  │
 q_9: |0>─────────────────────────┤ X ├─────┤ X ├───────┼───┼──X──┼──────
                                  └───┘     └───┘       │   │     │
q_10: |0>───────────────────────────────────────────────┼───┼─────X──────
                                                      ┌─┴─┐ │
q_11: |0>─────────────────────────────────────────────┤ X ├─X────────────
                                                      └───┘

```python
#we can get the number of gates or operations performed on the qubits by using:
print(qc.count_ops())
#or, for total:
print(qc.size())
```

    OrderedDict([('cx', 8), ('h', 5), ('x', 3), ('swap', 3)])
    19

The performance of a quantum circuit can roughly trace back to the number of layers it has, or its "depth". This is generally the number of operations the computer needs to perform in sequence. This doesn't add to the count any operations that can be performed in parallel.

```python
qc.depth()
```

    9

We can also get the maximum number of individual circuits this calculation could be broken down to. This is especially useful for simplifying complicated circuits.

```python
qc.num_unitary_factors()
```

    3

Let's take the circuit above and add classical registers!

```python
qc2 = QuantumCircuit(12, 12)

for idx in range(5):
    qc2.h(idx)
    qc2.cx(idx, idx+5)

qc2.cx(1, 7)
qc2.x(8)
qc2.cx(1, 9)
qc2.x(7)
qc2.cx(1, 11)
qc2.swap(6, 11)
qc2.swap(6, 9)
qc2.swap(6, 10)
qc2.x(6)
qc2.barrier()
qc2.measure(range(12), range(12))
qc2.draw()
```

         ┌───┐                                                            ░ ┌─┐»
 q_0: |0>┤ H ├──■─────────────────────────────────────────────────────────░─┤M├»
         ├───┤  │                                                         ░ └╥┘»
 q_1: |0>┤ H ├──┼────■───────────────────■────■─────────■─────────────────░──╫─»
         ├───┤  │    │                   │    │         │                 ░  ║ »
 q_2: |0>┤ H ├──┼────┼────■──────────────┼────┼─────────┼─────────────────░──╫─»
         ├───┤  │    │    │              │    │         │                 ░  ║ »
 q_3: |0>┤ H ├──┼────┼────┼────■─────────┼────┼─────────┼─────────────────░──╫─»
         ├───┤  │    │    │    │         │    │         │                 ░  ║ »
 q_4: |0>┤ H ├──┼────┼────┼────┼────■────┼────┼─────────┼─────────────────░──╫─»
         └───┘┌─┴─┐  │    │    │    │    │    │         │                 ░  ║ »
 q_5: |0>─────┤ X ├──┼────┼────┼────┼────┼────┼─────────┼─────────────────░──╫─»
              └───┘┌─┴─┐  │    │    │    │    │         │           ┌───┐ ░  ║ »
 q_6: |0>──────────┤ X ├──┼────┼────┼────┼────┼─────────┼───X──X──X─┤ X ├─░──╫─»
                   └───┘┌─┴─┐  │    │  ┌─┴─┐  │  ┌───┐  │   │  │  │ └───┘ ░  ║ »
 q_7: |0>───────────────┤ X ├──┼────┼──┤ X ├──┼──┤ X ├──┼───┼──┼──┼───────░──╫─»
                        └───┘┌─┴─┐  │  ├───┤  │  └───┘  │   │  │  │       ░  ║ »
 q_8: |0>────────────────────┤ X ├──┼──┤ X ├──┼─────────┼───┼──┼──┼───────░──╫─»
                             └───┘┌─┴─┐└───┘┌─┴─┐       │   │  │  │       ░  ║ »
 q_9: |0>─────────────────────────┤ X ├─────┤ X ├───────┼───┼──X──┼───────░──╫─»
                                  └───┘     └───┘       │   │     │       ░  ║ »
q_10: |0>───────────────────────────────────────────────┼───┼─────X───────░──╫─»
                                                      ┌─┴─┐ │             ░  ║ »
q_11: |0>─────────────────────────────────────────────┤ X ├─X─────────────░──╫─»
                                                      └───┘               ░  ║ »
  c_0: 0 ════════════════════════════════════════════════════════════════════╩═»
                                                                               »
  c_1: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_2: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_3: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_4: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_5: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_6: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_7: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_8: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
  c_9: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
 c_10: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
 c_11: 0 ══════════════════════════════════════════════════════════════════════»
                                                                               »
«
« q_0: ─────────────────────────────────
«      ┌─┐
« q_1: ┤M├──────────────────────────────
«      └╥┘┌─┐
« q_2: ─╫─┤M├───────────────────────────
«       ║ └╥┘┌─┐
« q_3: ─╫──╫─┤M├────────────────────────
«       ║  ║ └╥┘┌─┐
« q_4: ─╫──╫──╫─┤M├─────────────────────
«       ║  ║  ║ └╥┘┌─┐
« q_5: ─╫──╫──╫──╫─┤M├──────────────────
«       ║  ║  ║  ║ └╥┘┌─┐
« q_6: ─╫──╫──╫──╫──╫─┤M├───────────────
«       ║  ║  ║  ║  ║ └╥┘┌─┐
« q_7: ─╫──╫──╫──╫──╫──╫─┤M├────────────
«       ║  ║  ║  ║  ║  ║ └╥┘┌─┐
« q_8: ─╫──╫──╫──╫──╫──╫──╫─┤M├─────────
«       ║  ║  ║  ║  ║  ║  ║ └╥┘┌─┐
« q_9: ─╫──╫──╫──╫──╫──╫──╫──╫─┤M├──────
«       ║  ║  ║  ║  ║  ║  ║  ║ └╥┘┌─┐
«q_10: ─╫──╫──╫──╫──╫──╫──╫──╫──╫─┤M├───
«       ║  ║  ║  ║  ║  ║  ║  ║  ║ └╥┘┌─┐
«q_11: ─╫──╫──╫──╫──╫──╫──╫──╫──╫──╫─┤M├
«       ║  ║  ║  ║  ║  ║  ║  ║  ║  ║ └╥┘
« c_0: ═╬══╬══╬══╬══╬══╬══╬══╬══╬══╬══╬═
«       ║  ║  ║  ║  ║  ║  ║  ║  ║  ║  ║
« c_1: ═╩══╬══╬══╬══╬══╬══╬══╬══╬══╬══╬═
«          ║  ║  ║  ║  ║  ║  ║  ║  ║  ║
« c_2: ════╩══╬══╬══╬══╬══╬══╬══╬══╬══╬═
«             ║  ║  ║  ║  ║  ║  ║  ║  ║
« c_3: ═══════╩══╬══╬══╬══╬══╬══╬══╬══╬═
«                ║  ║  ║  ║  ║  ║  ║  ║
« c_4: ══════════╩══╬══╬══╬══╬══╬══╬══╬═
«                   ║  ║  ║  ║  ║  ║  ║
« c_5: ═════════════╩══╬══╬══╬══╬══╬══╬═
«                      ║  ║  ║  ║  ║  ║
« c_6: ════════════════╩══╬══╬══╬══╬══╬═
«                         ║  ║  ║  ║  ║
« c_7: ═══════════════════╩══╬══╬══╬══╬═
«                            ║  ║  ║  ║
« c_8: ══════════════════════╩══╬══╬══╬═
«                               ║  ║  ║
« c_9: ═════════════════════════╩══╬══╬═
«                                  ║  ║
«c_10: ════════════════════════════╩══╬═
«                                     ║
«c_11: ═══════════════════════════════╩═
«

It's also useful to see that some gates, such as the SWAP gate, is not actually an atomic operation. This can be seen using the decompose function:

```python
qc3 = QuantumCircuit(2)
qc3.swap(0,1)
qc3.decompose().draw() # This decomposes the swap gate into the gates we want
```

             ┌───┐
q_0: |0>──■──┤ X ├──■──
        ┌─┴─┐└─┬─┘┌─┴─┐
q_1: |0>┤ X ├──■──┤ X ├
        └───┘     └───┘

## Part 5 - Let's introduce the Transpiler

```python
from qiskit.visualization import plot_gate_map, plot_circuit_layout
import matplotlib.pyplot as plt
```

Consider this 5 qubit GHZ state circuit:

```python
ghz = QuantumCircuit(5, 5)
ghz.h(0)
for idx in range(1,5):
    ghz.cx(0,idx)
ghz.barrier(range(5))
ghz.measure(range(5), range(5))
ghz.draw()
```

        ┌───┐                     ░ ┌─┐
q_0: |0>┤ H ├──■────■────■────■───░─┤M├────────────
        └───┘┌─┴─┐  │    │    │   ░ └╥┘┌─┐
q_1: |0>─────┤ X ├──┼────┼────┼───░──╫─┤M├─────────
             └───┘┌─┴─┐  │    │   ░  ║ └╥┘┌─┐
q_2: |0>──────────┤ X ├──┼────┼───░──╫──╫─┤M├──────
                  └───┘┌─┴─┐  │   ░  ║  ║ └╥┘┌─┐
q_3: |0>───────────────┤ X ├──┼───░──╫──╫──╫─┤M├───
                       └───┘┌─┴─┐ ░  ║  ║  ║ └╥┘┌─┐
q_4: |0>────────────────────┤ X ├─░──╫──╫──╫──╫─┤M├
                            └───┘ ░  ║  ║  ║  ║ └╥┘
 c_0: 0 ═════════════════════════════╩══╬══╬══╬══╬═
                                        ║  ║  ║  ║
 c_1: 0 ════════════════════════════════╩══╬══╬══╬═
                                           ║  ║  ║
 c_2: 0 ═══════════════════════════════════╩══╬══╬═
                                              ║  ║
 c_3: 0 ══════════════════════════════════════╩══╬═
                                                 ║
 c_4: 0 ═════════════════════════════════════════╩═

Before we go further into learning how transpile() affects this circuit, let's introduce a way to understand some of the obscure quantum computing functions.

```python
import inspect
#This shows all the default parameters for the transpile function.
inspect.signature(transpile)
```

    <Signature (circuits, backend=None, basis_gates=None, coupling_map=None, backend_properties=None, initial_layout=None, seed_transpiler=None, optimization_level=None, pass_manager=None, callback=None, output_name=None)>

```python
inspect.signature(QuantumCircuit)
```

    <Signature (*regs, name=None)>

```python
qc = QuantumCircuit(2, 1)

qc.h(0)
qc.x(1)
qc.cu1(np.pi/4, 0, 1)
qc.h(0)
qc.measure([0], [0])
qc.draw()
```

        ┌───┐       ┌───┐┌─┐
q_0: |0>┤ H ├─■─────┤ H ├┤M├
        ├───┤ │pi/4 └───┘└╥┘
q_1: |0>┤ X ├─■───────────╫─
        └───┘             ║
 c_0: 0 ══════════════════╩═

In the above circuit, we see we have an H, X, and U1 gate. None of these are basis gates. Let's decompose the circuit:

```python
qc_basis = qc.decompose()
qc_basis.draw()
```

          ┌──────────┐ ┌──────────┐                       ┌──────────┐┌─┐
q_0: |0>──┤ U2(0,pi) ├─┤ U1(pi/8) ├──■─────────────────■──┤ U2(0,pi) ├┤M├
        ┌─┴──────────┴┐└──────────┘┌─┴─┐┌───────────┐┌─┴─┐├──────────┤└╥┘
q_1: |0>┤ U3(pi,0,pi) ├────────────┤ X ├┤ U1(-pi/8) ├┤ X ├┤ U1(pi/8) ├─╫─
        └─────────────┘            └───┘└───────────┘└───┘└──────────┘ ║
 c_0: 0 ═══════════════════════════════════════════════════════════════╩═

Let's look at another example: the Tofolli gate, a controlled-controlled-not gate. This is a 3 qubit gate!

```python
ccx_circ = QuantumCircuit(3)
ccx_circ.ccx(0, 1, 2)
ccx_circ.draw()
```

q_0: |0>──■──
          │
q_1: |0>──■──
        ┌─┴─┐
q_2: |0>┤ X ├
        └───┘

```python
ccx_circ.decompose().draw()
```

                                                          ┌───┐
q_0: |0>───────────────────■─────────────────────■────■───┤ T ├───■──
                           │             ┌───┐   │  ┌─┴─┐┌┴───┴┐┌─┴─┐
q_1: |0>───────■───────────┼─────────■───┤ T ├───┼──┤ X ├┤ Tdg ├┤ X ├
        ┌───┐┌─┴─┐┌─────┐┌─┴─┐┌───┐┌─┴─┐┌┴───┴┐┌─┴─┐├───┤└┬───┬┘└───┘
q_2: |0>┤ H ├┤ X ├┤ Tdg ├┤ X ├┤ T ├┤ X ├┤ Tdg ├┤ X ├┤ T ├─┤ H ├──────
        └───┘└───┘└─────┘└───┘└───┘└───┘└─────┘└───┘└───┘ └───┘

Wow! This is a big decomposition.

We also want to be able to map the circuit, which is entirely virtual, to physical qubits on specific providers. This is done with the plot_gate_map tool

```python
backend = provider.get_backend('ibmq_16_melbourne')
plot_gate_map(backend, plot_directed=True)
```

```python
backend = provider.get_backend('ibmq_16_melbourne')
new_circ_lv0 = transpile(ghz, backend=backend, optimization_level=0)

plot_circuit_layout(new_circ_lv0, backend)
```

```python
backend = provider.get_backend('ibmq_16_melbourne')
new_circ_lv3 = transpile(ghz, backend=backend, optimization_level=3)

print('Depth:', new_circ_lv3.depth())
plot_circuit_layout(new_circ_lv3, backend)
```

With these three layouts, we can choose between them and execute one. This step of transpiling is typically done within the execute function.

```python
from qiskit.tools.monitor import job_monitor
```

```python
job1 = execute(new_circ_lv3, backend)
job_monitor(job1)
```

    Job Status: job has successfully run

From the initial chart, we can choose our own initial mapping if we want. Here's two examples of using this level of control:

```python
# Virtual -> physical
#    0    ->    11
#    1    ->    12
#    2    ->    10
#    3    ->    2
#    4    ->    4

good_ghz = transpile(ghz, backend, initial_layout=[11,12,10,2,4])
print('Depth:', good_ghz.depth())
plot_circuit_layout(good_ghz, backend)
```

```python
job2 = execute(good_ghz, backend)
job_monitor(job2)
```

    Job Status: job has successfully run

```python
# Virtual -> physical
#    0    ->    0
#    1    ->    6
#    2    ->    10
#    3    ->    13
#    4    ->    7

bad_ghz = transpile(ghz, backend, initial_layout=[0,6,10,13,7])
print('Depth:', bad_ghz.depth())
plot_circuit_layout(bad_ghz, backend)
```

```python
job3 = execute(bad_ghz, backend)
job_monitor(job3)
```

    Job Status: job has successfully run

```python
counts1 = job1.result().get_counts()
counts2 = job2.result().get_counts()
counts3 = job3.result().get_counts()
```

```python
plot_histogram([counts1, counts2, counts3],
               figsize=(15,6),
               legend=['level3', 'good', 'bad'])
```

Different provider devices also change the topology of the circuit depending on the entanglement they support! This can be further explored in the transpiling tutorial; but I'm gunna move on.

## Intro to Qiskit Terra:

```python
from math import pi
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, execute
from qiskit.tools.visualization import circuit_drawer
from qiskit.quantum_info import state_fidelity
from qiskit import BasicAer

backend = BasicAer.get_backend('unitary_simulator')
```

## Single Qubit Quantum states

A single qubit quantum state can be written as

$$\left|\psi\right\rangle = \alpha\left|0\right\rangle + \beta \left|1\right\rangle$$

where $\alpha$ and $\beta$ are complex numbers. In a measurement the probability of the bit being in $\left|0\right\rangle$ is $|\alpha|^2$ and $\left|1\right\rangle$ is $|\beta|^2$. As a vector this is

$$
\left|\psi\right\rangle =
\begin{pmatrix}
\alpha \\
\beta
\end{pmatrix}.
$$

Note due to conservation probability $|\alpha|^2+ |\beta|^2 = 1$ and since global phase is undetectable $\left|\psi\right\rangle := e^{i\delta} \left|\psi\right\rangle$ we only requires two real numbers to describe a single qubit quantum state.

A convenient representation is

$$\left|\psi\right\rangle = \cos(\theta/2)\left|0\right\rangle + \sin(\theta/2)e^{i\phi}\left|1\right\rangle$$

where $0\leq \phi < 2\pi$, and $0\leq \theta \leq \pi$.  From this it is clear that there is a one-to-one correspondence between qubit states ($\mathbb{C}^2$) and the points on the surface of a unit sphere ($\mathbb{R}^3$). This is called the Bloch sphere representation of a qubit state.

Quantum gates/operations are usually represented as matrices. A gate which acts on a qubit is represented by a $2\times 2$ unitary matrix $U$. The action of the quantum gate is found by multiplying the matrix representing the gate with the vector which represents the quantum state.

$$\left|\psi'\right\rangle = U\left|\psi\right\rangle$$

A general unitary must be able to take the $\left|0\right\rangle$ to the above state. That is

$$
U = \begin{pmatrix}
\cos(\theta/2) & a \\
e^{i\phi}\sin(\theta/2) & b
\end{pmatrix}
$$

where $a$ and $b$ are complex numbers constrained such that $U^\dagger U = I$ for all $0\leq\theta\leq\pi$ and $0\leq \phi<2\pi$. This gives 3 constraints and as such $a\rightarrow -e^{i\lambda}\sin(\theta/2)$ and $b\rightarrow e^{i\lambda+i\phi}\cos(\theta/2)$ where $0\leq \lambda<2\pi$ giving

$$
U = \begin{pmatrix}
\cos(\theta/2) & -e^{i\lambda}\sin(\theta/2) \\
e^{i\phi}\sin(\theta/2) & e^{i\lambda+i\phi}\cos(\theta/2)
\end{pmatrix}.
$$

This is the most general form of a single qubit unitary.

## Single-Qubit Gates

The single-qubit gates available are:
- u gates
- Identity gate
- Pauli gates
- Clifford gates
- $C3$ gates
- Standard rotation gates

We have provided a backend: `unitary_simulator` to allow you to calculate the unitary matrices.

```python
q = QuantumRegister(1)
```

```python
qc = QuantumCircuit(q)
qc.u3(pi/2,pi/2,pi/2,q)
qc.draw()
```

          ┌────────────────────┐
q38_0: |0>┤ U3(pi/2,pi/2,pi/2) ├
          └────────────────────┘

```python
backend = provider.get_backend('ibmqx2')
job = execute(qc,backend)
job.result().get_unitary(qc,decimals=3)
```

### u gates

In Qiskit we give you access to the general unitary using the $u3$ gate

$$
u3(\theta, \phi, \lambda) = U(\theta, \phi, \lambda)
$$

The $u2(\phi, \lambda) =u3(\pi/2, \phi, \lambda)$ has the matrix form

$$
u2(\phi, \lambda) =
\frac{1}{\sqrt{2}} \begin{pmatrix}
1 & -e^{i\lambda} \\
e^{i\phi} & e^{i(\phi + \lambda)}
\end{pmatrix}.
$$

This is a useful gate as it allows us to create superpositions

The $u1(\lambda)= u3(0, 0, \lambda)$ gate has the matrix form

$$
u1(\lambda) =
\begin{pmatrix}
1 & 0 \\
0 & e^{i \lambda}
\end{pmatrix},
$$

which is a useful as it allows us to apply a quantum phase.

### Pauli gates

#### $X$: bit-flip gate

The bit-flip gate $X$ is defined as:

$$
X   =
\begin{pmatrix}
0 & 1\\
1 & 0
\end{pmatrix}= u3(\pi,0,\pi)
$$

```python
qc = QuantumCircuit(q)
qc.x(q)
qc.draw()
```

#### $Y$: bit- and phase-flip gate

The $Y$ gate is defined as:

$$
Y  =
\begin{pmatrix}
0 & -i\\
i & 0
\end{pmatrix}=u3(\pi,\pi/2,\pi/2)
$$

#### $Z$: phase-flip gate

The phase flip gate $Z$ is defined as:

$$
Z =
\begin{pmatrix}
1 & 0\\
0 & -1
\end{pmatrix}=u1(\pi)
$$

### Clifford gates

#### Hadamard gate

$$
H =
\frac{1}{\sqrt{2}}
\begin{pmatrix}
1 & 1\\
1 & -1
\end{pmatrix}= u2(0,\pi)
$$

The Hadamard gate acts on a single qubit. It maps the basis state $|0>$ to $\frac{1}{\sqrt 2}(|0> + |1>)$ and $|1>$ to $\frac{1}{\sqrt 2}(|0> - |1>)$, thus it creates a superposition state within a single qubit.

## Multi-Qubit Gates

### Mathematical Preliminaries

The space of quantum computer grows exponential with the number of qubits. For $n$ qubits the complex vector space has dimensions $d=2^n$. To describe states of a multi-qubit system, the tensor product is used to "glue together" operators and basis vectors.

Let's start by considering a 2-qubit system. Given two operators $A$ and $B$ that each act on one qubit, the joint operator $A \otimes B$ acting on two qubits is

$$\begin{equation}
	A\otimes B =
	\begin{pmatrix}
		A_{00} \begin{pmatrix}
			B_{00} & B_{01} \\
			B_{10} & B_{11}
		\end{pmatrix} & A_{01} 	\begin{pmatrix}
				B_{00} & B_{01} \\
				B_{10} & B_{11}
			\end{pmatrix} \\
		A_{10} 	\begin{pmatrix}
					B_{00} & B_{01} \\
					B_{10} & B_{11}
				\end{pmatrix} & A_{11} 	\begin{pmatrix}
							B_{00} & B_{01} \\
							B_{10} & B_{11}
						\end{pmatrix}
	\end{pmatrix},
\end{equation}$$

where $A_{jk}$ and $B_{lm}$ are the matrix elements of $A$ and $B$, respectively.

Analogously, the basis vectors for the 2-qubit system are formed using the tensor product of basis vectors for a single qubit:
$$\begin{equation}\begin{split}
	\left|{00}\right\rangle &= \begin{pmatrix}
		1 \begin{pmatrix}
			1  \\
			0
		\end{pmatrix} \\
		0 \begin{pmatrix}
			1  \\
			0
		\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 1 \\ 0 \\ 0 \\0 \end{pmatrix}~~~\left|{01}\right\rangle = \begin{pmatrix}
	1 \begin{pmatrix}
	0 \\
	1
	\end{pmatrix} \\
	0 \begin{pmatrix}
	0  \\
	1
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix}0 \\ 1 \\ 0 \\ 0 \end{pmatrix}\end{split}
\end{equation}$$

$$\begin{equation}\begin{split}\left|{10}\right\rangle = \begin{pmatrix}
	0\begin{pmatrix}
	1  \\
	0
	\end{pmatrix} \\
	1\begin{pmatrix}
	1 \\
	0
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 1 \\ 0 \end{pmatrix}~~~ 	\left|{11}\right\rangle = \begin{pmatrix}
	0 \begin{pmatrix}
	0  \\
	1
	\end{pmatrix} \\
	1\begin{pmatrix}
	0  \\
	1
	\end{pmatrix}
	\end{pmatrix} = \begin{pmatrix} 0 \\ 0 \\ 0 \\1 \end{pmatrix}\end{split}
\end{equation}.$$

Note we've introduced a shorthand for the tensor product of basis vectors, wherein $\left|0\right\rangle \otimes \left|0\right\rangle$ is written as $\left|00\right\rangle$. The state of an $n$-qubit system can described using the $n$-fold tensor product of single-qubit basis vectors. Notice that the basis vectors for a 2-qubit system are 4-dimensional; in general, the basis vectors of an $n$-qubit sytsem are $2^{n}$-dimensional, as noted earlier.

### Basis vector ordering in Qiskit

Within the physics community, the qubits of a multi-qubit systems are typically ordered with the first qubit on the left-most side of the tensor product and the last qubit on the right-most side. For instance, if the first qubit is in state $\left|0\right\rangle$ and second is in state $\left|1\right\rangle$, their joint state would be $\left|01\right\rangle$. Qiskit uses a slightly different ordering of the qubits, in which the qubits are represented from the most significant bit (MSB) on the left to the least significant bit (LSB) on the right (big-endian). This is similar to bitstring representation on classical computers, and enables easy conversion from bitstrings to integers after measurements are performed. For the example just given, the joint state would be represented as $\left|10\right\rangle$. Importantly, _this change in the representation of multi-qubit states affects the way multi-qubit gates are represented in Qiskit_, as discussed below.

The representation used in Qiskit enumerates the basis vectors in increasing order of the integers they represent. For instance, the basis vectors for a 2-qubit system would be ordered as $\left|00\right\rangle$, $\left|01\right\rangle$, $\left|10\right\rangle$, and $\left|11\right\rangle$. Thinking of the basis vectors as bit strings, they encode the integers 0,1,2 and 3, respectively.

### Controlled operations on qubits

A common multi-qubit gate involves the application of a gate to one qubit, conditioned on the state of another qubit. For instance, we might want to flip the state of the second qubit when the first qubit is in $\left|0\right\rangle$. Such gates are known as _controlled gates_. The standard multi-qubit gates consist of two-qubit gates and three-qubit gates. The two-qubit gates are:
- controlled Pauli gates
- controlled Hadamard gate
- controlled rotation gates
- controlled phase gate
- controlled u3 gate
- swap gate

The three-qubit gates are:
- Toffoli gate
- Fredkin gate

```python

```