# Many-body quantum theory in condensed matter physics

# Chapter 1 First and second quantization

In order to help myself remember these notes easily, I gave up writing in English, and choose to use my mother language-Chinese.

为了帮助我自己记忆，我放弃了使用英文，而换之以母语中文。然而，为了使得他人阅读我笔记的方便，人名和专有名词，我尽量都只会以英文的方式给出。

量子物理最初只处理在经典电磁场中粒子的运动的量子化问题, 即量子力学 (Heisenberg，Schr$\ddot{ \mathrm o}$dinger, Drac, 1925-26). 之后, 电磁场本身也引入了量子化概念 (Dirac, 1927). Jordan 和 Wignar 于1928年进一步提出粒子本身也可以被量子化的场所表示，从而促进了量子电动力学（Quantum electrodynamics, QED）和量子场论（Quantum field theory, QFT）的发展。按照惯例，量子力学的最初形式（即 Schr$\ddot{ \mathrm o}$dinger 方程）被定义为一次量子化, 而量子场论则被定义为二次量子化.

量子理论中假定所有的量子态都可以表示为希尔伯特空间(Hilbert space) 中的态矢 (sate vector), 并且所有的可观测量都可以表示为作用在该空间上的厄米算符 (Hermitian operator). 平行的态矢代表相同的物理性质，并且我们通常处理的是归一化的态矢. 对于给定的任意 Hermitian operation $A$, 它都有许多个本征值 |${\varphi_\alpha}$>，并且做用于这些本征值上的 scale factor $\alpha$ 是不变的，即 $A$|${\varphi_\alpha}$> = $\alpha$ |${\varphi_\alpha}$>. 这些 scale factor 本称作本征值的算符. 这其实是 Hilbert space theory中的一个基本原理 (theorem), 即任意给定的 Hermitian 算符的一整套本征矢 (eiganvectors) 组成了该 Hilber space 中的完备基矢 (a
complete basis set). 一般而言，两个不同算符 $A$ 和 $B$, 其对应的本征态 (eiganstates) |${\varphi_\alpha}$> 和 |${\varphi_\beta}$> 是不同的. 对类型 $B$ 的测量所得到的量子态肯定是算符 $B$ 的一个本征态 |${\varphi_\beta}$>. 这个态也可以表示为算符 $A$ 的本征态 |${\varphi_\alpha}$> 的叠加 （superposition): |${\varphi_\beta}$> = ${\sum_\alpha}$ |${\varphi_\alpha}$> ${C_{\alpha\beta}}$. 在这个状态时，当我们去实际测量算符 $A$ 涉及到的动力学变量时 (dynamical variables), 我们（一般）不能确定地预测出所得到的结果一定是某个本征值，而只能用一定的概率来描述得到这些本征值的可能性。对于任意的本征态 |${\varphi_\alpha}$>，它可能作为测量结果的概率是 |${C_{\alpha\beta}}$|$^2$. 量子理论中的这种非因果性因此也被称作波函数的坍缩 (wave function collapse).

有意思的是，在坍缩事件之间，量子态的时间演变 (time evolution) 是可以被完美确定的。一个本征矢 |${\varphi(t)}$> 的时间演化是被量子力学的中心算符, 即哈密顿量算符 Hamiltonian $H$， 通过 Schr${\ddot{\mathrm o}}$dinger equation 所掌控的：

\begin{equation} i\hslash \partial_t|\varphi(t) = H|\varphi(t)> \end{equation}

```python

```


# A tutorial on probabilistic models of single-cell gene expression and Bayesian inference within STAN

*Nick Phillips, Naef lab, EPFL, 28/06/21*

## Introduction

This is a tutorial to accompany our paper that was recently published in Molecular Systems Biology: "The circadian oscillator analysed at the single‐transcript level" https://doi.org/10.15252/msb.202010135.

While our particular model was customised for circadian biology, the approach and tools are general and can be adapted to a wide range of different biological problems. Within this approach, the goal is to define a generative model of single-cell gene expression and infer the parameters of the model within a Bayesian framework, which can be used to e.g. compare two different genes or cell populations.

As part of this Bayesian inference strategy we'll be forced to use a probabilistic, generative model of the single-cell data. But how do we define a model or decide which is the best? We'll see that there are multiple models that can be used for single-cell gene expression, and each model has its strengths and weaknesses. There are two main objectives of this tutorial:

* to give an overview of several probability distributions of single-cell gene expression based on simple biophysical models
* to show how these models can be implemented in STAN for Bayesian inference

The most important message is this: probabilistic programming languages such as STAN make Bayesian inference so much more accessible for practitioners, and the straightforward implementation of the analysis pipeline means that users can spend more time thinking about the biological questions and experimenting with different models. However, while the implementation of Bayesian models is now much easier, there are still an enormous number of problems/subtleties that can arise, and we will also see a few basic failure modes that are important to think about.

Contents:

1. A quick intro to Bayesian data analysis and STAN
2. A quick intro to biophysical models of single-cell gene expression
3. Model 1: the Poisson distribution
4. Model 2: the beta-Poisson distribution
5. Model 3: the negative binomial (gamma-Poisson) distribution
6. Model 4: the multivariate log-normal Poisson mixture distribution
7. Model 5: incorporating extrinsic noise explicitly
8. Technical noise
9. Model selection and validation
10. Strengths and limitations of the approach

## A quick intro to Bayesian data analysis and STAN

If you're a complete beginner to Bayesian analysis then I recommend first starting with the book "Statistical Rethinking" by Richard McElreath [1](https://xcelab.net/rm/statistical-rethinking/), which starts from the basics and is aimed at individuals from diverse (including non-quantitative) backgrounds. And here are a few videos that dive into Bayesian inference and STAN a bit more:

* Julia Galef - A visual guide to Bayesian thinking [2](https://youtu.be/BrK7X_XlGB8)
* Richard McElreath - Stastistical Rethinking course [3](https://youtu.be/4WVelCswXo4)
* Aki Vehtari - Bayesian Data Analysis course [4](https://avehtari.github.io/BDA_course_Aalto/index.html)
* Michael Betancourt - on MCMC [5](https://www.youtube.com/watch?v=UzcLe-kpMDQ).

Here I'll attempt to summarise some of the core concepts of Bayesian data analysis, but I hope the actual mechanics will become clearer when we look at some practical examples below.

Before launching into the practical details of Bayesian inference for single-cell gene expression, it's worth noting some of the advantages and disadvantages of this approach. An essential feature of Bayesian analysis is that we're forced to describe a generative model of the data, which isn't necessarily true for all data analysis approaches (e.g. t-SNE). With a model we're able to ask hypothetical "but what if?" type questions that can predict outcomes of interventions to biological systems *before* doing the experiment. We can also use models to explicitly disentangle cell-to-cell variability caused by biological sources vs technical noise, and this recent article is a concrete example of why this can be important [6](https://doi.org/10.1038/s41587-021-00875-x). However, a natural question is: how do we know that we have a good model of our data? And how do we even define a "good" model? We'll revisit this question in section 8.

We'll define our models and perform Bayesian inference with STAN [7](https://mc-stan.org), and here are some of the benefits:

* In STAN you define the model at a high level and you don't have to write a custom inference algorithm every time you'd like to change the model (it's a big time-saver)
* Several textbooks for Bayesian inference (Statistical Rethinking, Bayesian Data Analysis 3rd edition, Regression and Other Stories) show many examples of different models in STAN .
* There are loads of interesting online tutorials and resources (see e.g. tutorials from Aki Vehtari [8](https://avehtari.github.io) or Michael Betancourt [9](https://betanalpha.github.io/writing/) )
* There's a large user community and the reference manual is comprehensive.
* It's available for R, Python and more.

The goal of this tutorial is not to cover the best practices/workflow for Bayesian inference (see e.g. this paper [10](https://arxiv.org/pdf/2011.01808.pdf) or this blog [11](https://betanalpha.github.io/assets/case_studies/principled_bayesian_workflow.html) ), as we'll mainly focus on the probabilistic models of single-cell gene expression. We'll skip some (quite important) steps in the Bayesian analysis pipeline (including MCMC diagnostics to check that the inference behaved as expected), so check out these other resources if you'd like further information on these general principles.

So what's the big picture? Starting from a simple biophysical model of gene expression, we'll derive several different probability distributions that can be used as part of the STAN inference framework to describe single-cell gene expression datasets. For each gene in our experiment we have a dataset of single-cell mRNA counts (that could come from either smFISH or single-cell RNA-seq) that we'll label $y$, and each model will have some parameters that we label $\theta$ (e.g. transcription rate, mRNA degradation rate etc., see below). We'll use STAN to infer the parameters of our model from the data. Specifically, we'll write our model in STAN, feed it with our data and then draw samples from the "posterior probability distribution" of our model. These samples will (hopefully) reveal the range of plausible values of the model parameters, given the data. We can also use these parameter samples to simulate synthetic datasets (known as the posterior predictive distribution) and check that the model simulations resemble the data (perhaps there's a particular qualitative feature of the dataset that you're interested in?). We can also change a subset of parameters to explore "what if" type questions (e.g. what would the mRNA distribution look like if we could double the transcription rate?).

For the purposes of this tutorial, the formula we need for Bayesian inference is this:

$p(\theta|y) \propto p(y|\theta)p(\theta)$

Which has three components:

* $p(\theta|y)$ - the posterior parameter distribution. This is a probability distribution that expresses our belief/uncertainty of the parameters $\theta$ given the data $y$. This is the output of STAN, and it's the thing we're interested in.
* $p(y|\theta)$ - the likelihood of the data $y$ given the model parameters $\theta$. In order to calculate this, we'll need to input a model that describes the distribution of mRNA counts ($y$) given the model parameters $\theta$. We'll discuss four possible options based on simple biophysical models below.
* $p(\theta)$ - the prior distribution. This encodes our state of belief about the parameter values before we see any data. Ideally, here we would insert some expert knowledge on the range of plausible values for each parameter (for example, there are certainly upper limits on possible transcription rates due to the physical constraints of the cell). For illustration, we'll assume that we know very little about the possible values of the parameters and use a "weakly informative prior", but in some cases this is actually not recommended.

In order for STAN to sample the posterior parameter distribution, we'll need to supply:

1. Data
2. A model
3. A prior distribution for each parameter

We'll next go over some simple models that can be used within STAN.

## A quick intro to biophysical models of single-cell gene expression

mRNA production and degradation result from random collisions between finite numbers of interacting molecules within the cell, which creates biological noise known as intrinsic stochasticity. I particularly like this paper from Dattani & Barahona [12](https://doi.org/10.1098/rsif.2016.0833) that describes a conceptual framework for unifying many diverse models of gene expression. We'll use their framework for defining a general, stochastic model of gene expression, where mRNA molecules are produced and degraded according to the following reaction scheme:

\begin{align}
\emptyset \overset{M_t}\longrightarrow \text{mRNA} \overset{L_t}\longrightarrow \emptyset
\end{align}

and where the production rate $M_t$ and degradation rate $L_t$ can be stochastic, time-varying functions. The interesting aspect of the approach is that you can break the solution into two simpler problems. First, you solve a differential equation for a new variable $X_t$ that has the same production and degradation rates as the original problem (this will be a random differential equation if either $M_t$ or $L_t$ is stochastic)

\begin{align}
\frac{dX_t}{dt} = M_t - L_t X_t
\end{align}

Solving this equation will give the probability density function of the process $X_t$ as $p_{X_t}(x,t)$. To describe the probability distribution of the mRNA molecules, we next take this distribution and plug it into this formula that describes a Poisson mixture distribution (ignoring the ‘burn-in’ transient towards stationarity):

\begin{align}
p(n,t)=\int \frac{x^n}{n!} e^{-x}p_{X_t}(x,t) dx
\end{align}

So to reiterate: we first solve a simpler problem to calculate a mixing density $p_{X_t}(x,t)$ and then use it to define the Poisson mixture distribution. In some cases the integral can be perfomed analytically, and in other cases we can introduce "latent variables" as extra parameters within STAN to take care of the integration. Hopefully this will become clearer with a few examples.

## Model 1: the Poisson distribution

We'll start with the simplest example: let's imagine that there's a constant rate of production and degradation (transcription rate $M_t = \alpha$ and degradation rate $L_t = \gamma$), which is often referred to a "consitutive gene expression". In this case we simply recover a Poisson distribution, which only has one parameter known as the rate parameter $\lambda$ (equal to $\lambda = \alpha/\gamma$ for this model). Both the mean and variance of the Poisson distribution are given by this rate parameter, and so the larger the ratio of production to degradation, the larger the mean and variance of the mRNA distribution. Interestingly, even for this simple model we can see that there might be problems with recovering the parameters during inference. Below I've simulated this model (externally, with the Gillespie algorithm) using two different combinations of parameters (fast dynamics: $\alpha = 30$, $\gamma = 1$, slow dynamics: $\alpha = 3$, $\gamma = 0.1$).

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import nbinom

t = pd.read_csv('Time.csv',header=None).values
y_fast = pd.read_csv('Model1_fast.csv',header=None).values
y_slow = pd.read_csv('Model1_slow.csv',header=None).values

def plot_time_series(t,y):
    plt.plot(t,y,linestyle = 'steps')
    plt.ylabel('mRNA')
    plt.xlabel('Time (hours)')

def plot_hist(y,bins):
    plt.hist(y,bins=bins,density=True)
    plt.xlabel('mRNA')
    plt.ylabel('Density')

fig = plt.figure(figsize=(8,8))
plt.subplot(2,2,1)
plot_time_series(t,y_fast[:,0])
plt.title(r'Poisson model (fast dynamics). $\alpha = 30$, $\gamma = 1$')

plt.subplot(2,2,2)
plot_hist(y_fast[-1,:],bins=np.linspace(15,50,10))
plt.title(r'mRNA distribution (fast dynamics)')

plt.subplot(2,2,3)
plot_time_series(t,y_slow[:,0])
plt.title(r'Poisson model (slow dynamics). $\alpha = 3$, $\gamma = 0.1$')

plt.subplot(2,2,4)
plot_hist(y_slow[-1,:],bins=np.linspace(15,50,10))
plt.title(r'mRNA distribution (slow dynamics)')

plt.tight_layout()
```

In the left column we see the simulated mRNA dynamics in a single example cell, and in the right column we see the mRNA distribution across 1000 simulated cells. Even though the dynamics are different, the resulting mRNA distributions are the same for both fast and slow dynamics! Incidentally, this shows why the time-series generated by live-cell imaging can sometimes carry more parameter information than snapshot measurements.

Now let's try and perform inference on our Poisson model. As we'll see, a STAN program is divided into different blocks.

```python
import pystan

model1 = """
data {
    int<lower=1> N;
    int y[N];
}
parameters {
    real<lower=0> alpha;
    real<lower=0> gamma;
}
transformed parameters{
    real<lower=0> lambda;
    lambda = alpha/gamma;
}
model {
    y ~ poisson(lambda);
    alpha ~ normal(0,100);
    gamma ~ normal(0,100);
}

"""
```

* Data - here we define the size of our data (the number of cells that we measured, $N$) and then supply the measured values as a variable that we call $y$
* Parameters - we have two parameters: the production rate $\alpha$ and the degradation rate $\gamma$. We constrain these rates to be positive with: <lower=0>
* Transformed parameters - we'll also create a transformed parameter $\lambda = \alpha/\gamma$ that will act as the input rate parameter to the Poisson distribution
* Model - here we enter our model of the data $y$ (a Poisson distribution) AND we specify the priors for the parameters $\alpha$ and $\gamma$. We use weakly informative priors $\alpha, \gamma\sim N(0,100)$ i.e. enormously wide normal distributions.

And that's it! Let's see what happens! We next perform parameter inference for this model (using MCMC within STAN).

```python
dat = {
    'N' : len(y_fast[-1,:]),
    'y' : y_fast[-1,:]
}

sm = pystan.StanModel(model_code=model1)

fit = sm.sampling(data=dat, iter=2000, chains=4)

a = fit.extract(permuted=True)
alpha_inferred = a['alpha']
gamma_inferred = a['gamma']
lambda_inferred = a['lambda']
```

    INFO:pystan:COMPILING THE C++ CODE FOR MODEL anon_model_911d21b04865cbe9659afd57dd6e747f NOW.

After running STAN, we can retrieve the posterior parameter samples and plot them (at this stage you would normally run diagnostic checks).

```python
def plot_posterior_oned(x,label):
    plt.hist(x,bins=15,density=True)
    plt.xlabel(label)
    plt.ylabel('Density')

fig = plt.figure(figsize=(12,3))
plt.subplot(1,4,1)
plot_posterior_oned(alpha_inferred,r'$\alpha$')
plt.plot([30,30],[0,0.007],'r--')
plt.subplot(1,4,2)
plot_posterior_oned(gamma_inferred,r'$\gamma$')
plt.plot([1,1],[0,0.2],'r--')
plt.subplot(1,4,3)
plt.scatter(alpha_inferred,gamma_inferred,s=2)
plt.xlabel(r'$\alpha$')
plt.ylabel(r'$\gamma$')
plt.subplot(1,4,4)
plot_posterior_oned(lambda_inferred,r'$\lambda$')
plt.plot([30,30],[0,2.5],'r--')

plt.tight_layout()
```

We can see from the histograms that $\alpha$ and $\gamma$ are all over the place, and plotting both parameters together (the plot of $\alpha$ vs $\gamma$) shows that they slide along a valley together. We know from the simulations that it's really the *ratio* of $\alpha/\gamma$ that's important (and sets the $\lambda$ parameter), so we have a horrible correlation in the parameter estimates between $\alpha$ and $\gamma$. However, the posterior parameter estimates for $\lambda$ includes the true value (30, shown in red). In other terms, we can't disentangle and individually infer the production and degradation rate parameters, but we can reliably infer their ratio. For this particular case, we can say that we're able to infer the transcription rate *scaled* by the mRNA degradation rate. This is a rather trivial example of problems with parameter identifiability, but we'll see another case that's more subtle in Model 2.

## Model 2 - the beta-Poisson mixture model

One of the major problems with the basic Poisson model is that it's too simple: it only has one parameter, and the variance is always equal to the mean. In real single-cell gene expression data this is rarely observed, and the Poisson model is too restrictive.

Live-cell imaging has shown that transcription is highly bursty [13](https://science.sciencemag.org/content/332/6028/472.abstract), and a common model known as the "telegraph model" is often used to describe this burstiness. The telegraph model assumes that genes switches discontinuously between an active and inactive state, where mRNA molecules are only produced in the active state. This can be represented with the following set of reactions:

\begin{align}
g_{\text{off}} &\overset{k_{\text{on}}}\longrightarrow g_{\text{on}} \: & \text{Promoter activation}\\
g_{\text{on}} &\overset{k_{\text{off}}}\longrightarrow g_{\text{off}} \: & \text{Promoter deactivation} \\
g_{\text{on}} &\overset{\alpha}\longrightarrow g_{\text{on}} + M \: & \text{Transcription} \\
M &\overset{\gamma}\longrightarrow \emptyset \: & \text{mRNA degradation} \\
\end{align}

The differences between the basic Poisson model (equivalent to constitutive expression) and the telegraph model is discussed in [14](https://science.sciencemag.org/content/336/6078/183), which includes some great visualisations.

Let's see an example of the telegraph model in action. Below we see an example time series, where $k_{\text{on}}$ and $k_{\text{off}}$ are both equal to 0.5, the transcription rate $\alpha=50$ and the degradation rate $\gamma=1$.

```python
y_slow_mRNA = pd.read_csv('Model2_slow_mRNA.csv',header=None).values
y_slow_promoter = pd.read_csv('Model2_slow_promoter.csv',header=None).values

fig = plt.figure(figsize=(8,4))
plt.subplot(2,2,1)
plot_time_series(t,y_slow_mRNA[:,0])
plt.title(r'mRNA time series. $k_{on} = 0.5$, $k_{off} = 0.5$')

plt.subplot(2,2,3)
plot_time_series(t,y_slow_promoter[:,0])
plt.ylabel('Promoter state')
plt.title(r'Promoter time series')

plt.subplot(1,2,2)
plot_hist(y_slow_mRNA[-1,:],bins=10)
plt.title(r'mRNA distribution')

plt.tight_layout()
```

In this parameter regime, the switching between the on and off promoter state is relatively slow. We can see that mRNA is increasing while the promoter is in an active state (left column). Averaging across 1000 cells (right column) we see a bimodal mRNA distribution. But what is the exact probability distribution? To solve this model using the formulation from above, the production rate $M_t$ follows a telegraph process that randomly switches between the on and off state. It's then possible to solve the random differential equation for $X_t$, which turns out to be a beta distribution (see [15](https://link.springer.com/article/10.1007/s00285-009-0298-z) or [Dattani](https://doi.org/10.1098/rsif.2016.0833) for details). The full model for the mRNA distribution then becomes a beta-Poisson mixture, and (to my knowledge) the first paper to use this formulation for single-cell RNA-seq data was Kim and Marioni, 2013 [16](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2013-14-1-r7).

We can code this model in STAN using latent variables (extra, unseen parameters). Instead of having a constant rate parameter for the Poisson, *each* cell *i* will have have its own parameter $p_i$, where each $p_i$ is drawn from a beta distribution (parameterised by $k_{\text{on}}$ and $k_{\text{off}}$). Mathematically, the model is given by:

\begin{align}
p_i \sim \text{Beta}(k_{\text{on}},k_{\text{off}}) \\
y_i \sim \text{Poisson}(\alpha p_i)
\end{align}

Note that the degradation rate $\gamma$ is missing from this formulation, which is again impossible to infer using only the mRNA distribution and hence timescales are normalised in terms of the mRNA degradation rate.  Now let's code the model in STAN and see if it's possible to reliably infer the parameters!

```python
model2 = """
data {
    int<lower=1> N;
    int y[N];
}
parameters {
    real<lower=0> k_on;
    real<lower=0> k_off;
    real<lower=0> alpha;
    vector<lower=0, upper=1>[N] p;
}
model {
    y ~ poisson(alpha*p);
    p ~ beta(k_on,k_off);
    k_on ~ normal(0,100);
    k_off ~ normal(0,100);
    alpha ~ normal(0,100);
}

"""

dat = {
    'N' : len(y_slow_mRNA[-1,:]),
    'y' : y_slow_mRNA[-1,:]
}

sm = pystan.StanModel(model_code=model2)
fit = sm.sampling(data=dat, iter=2000, chains=4)
a = fit.extract(permuted=True)

k_on_inferred = a['k_on']
k_off_inferred = a['k_off']
alpha_inferred = a['alpha']
p_inferred = a['p']
```

    INFO:pystan:COMPILING THE C++ CODE FOR MODEL anon_model_e2f232d338e122c2857e8baec2526a9e NOW.
    WARNING:pystan:Maximum (flat) parameter count (1000) exceeded: skipping diagnostic tests for n_eff and Rhat.
    To run all diagnostics call pystan.check_hmc_diagnostics(fit)

```python
fig = plt.figure(figsize=(9,3))
plt.subplot(1,3,1)
plot_posterior_oned(k_on_inferred,r'$k_{on}$')
plt.plot([0.5,0.5],[0,14],'r--')
plt.subplot(1,3,2)
plot_posterior_oned(k_off_inferred,r'$k_{off}$')
plt.plot([0.5,0.5],[0,10],'r--')
plt.subplot(1,3,3)
plot_posterior_oned(alpha_inferred,r'$\alpha$')
plt.plot([50,50],[0,0.7],'r--')

plt.tight_layout()
```

As we can see in the posterior parameter histograms, the true value (shown in red) is contained within most of the posterior parameter distribution.

The beta-Poisson model might seem like an attractive model in that it provides more flexibility than the standard Poisson distribution while still being based on a biophysical model of transcription with interpretable parameters.

However, we will now see an example that illustrates some potential pathologies of this model. Let's imagine that the switching between on and off state becomes fast relative to the mRNA degradation timescale ($k_{\text{on}}$ and $k_{\text{off}}$ are both equal to 30) and look at another time series:

```python
y_fast_mRNA = pd.read_csv('Model2_fast_mRNA.csv',header=None).values
y_fast_promoter = pd.read_csv('Model2_fast_promoter.csv',header=None).values

fig = plt.figure(figsize=(8,4))
plt.subplot(2,2,1)
plot_time_series(t,y_fast_mRNA[:,0])
plt.title(r'mRNA time series. $k_{on} = 30$, $k_{off} = 30$')

plt.subplot(2,2,3)
plot_time_series(t,y_fast_promoter[:,0])
plt.ylabel('Promoter state')
plt.title(r'Promoter time series')

plt.subplot(1,2,2)
plot_hist(y_fast_mRNA[-1,:],bins=10)
plt.title(r'mRNA distribution')

plt.tight_layout()
```

Now let's use the same STAN model for inference:

```python
dat = {
    'N' : len(y_fast_mRNA[-1,:]),
    'y' : y_fast_mRNA[-1,:]
}

fit = sm.sampling(data=dat, iter=2000, chains=4)
a = fit.extract(permuted=True)

k_on_inferred = a['k_on']
k_off_inferred = a['k_off']
alpha_inferred = a['alpha']
p_inferred = a['p']
```

    WARNING:pystan:Maximum (flat) parameter count (1000) exceeded: skipping diagnostic tests for n_eff and Rhat.
    To run all diagnostics call pystan.check_hmc_diagnostics(fit)
    WARNING:pystan:7 of 4000 iterations ended with a divergence (0.175 %).
    WARNING:pystan:Try running with adapt_delta larger than 0.8 to remove the divergences.
    WARNING:pystan:Chain 1: E-BFMI = 0.0424
    WARNING:pystan:Chain 2: E-BFMI = 0.0538
    WARNING:pystan:Chain 3: E-BFMI = 0.0772
    WARNING:pystan:Chain 4: E-BFMI = 0.0993
    WARNING:pystan:E-BFMI below 0.2 indicates you may need to reparameterize your model

```python
fig = plt.figure(figsize=(9,3))
plt.subplot(1,3,1)
plot_posterior_oned(k_on_inferred,r'$k_{on}$')
plt.plot([30,30],[0,0.2],'r--')
plt.subplot(1,3,2)
plot_posterior_oned(k_off_inferred,r'$k_{off}$')
plt.plot([30,30],[0,0.1],'r--')
plt.subplot(1,3,3)
plot_posterior_oned(alpha_inferred,r'$\alpha$')
plt.plot([50,50],[0,0.1],'r--')

plt.tight_layout()
```

We can see STAN is not happy with the inference procedure (we see several warning messages related to STAN diagnostics) and the parameter posterior distributions are a mess. In fact, for this parameter regime there is a problem of parameter identifiability as the switching between on and off states is so fast that the mRNA distribution becomes similar to a standard Poisson. In other words, for some parameter combinations the inference procedure is not robust for Model 2.

## Model 3: the negative binomial (gamma-Poisson) distribution

The negative binomial distribution has become common for modelling mRNA distributions in both smFISH and RNA-seq datasets. But where does this negative binomial distribution come from, and how is it related to transcriptional dynamics? Interestingly, there are (at least) three different ways you can generate a negative binomial distribution. We'll first look at stochastic simulations before describing them below

```python
y_method1_mRNA = pd.read_csv('Model3_method1_mRNA.csv',header=None).values
y_method1_promoter = pd.read_csv('Model3_method1_promoter.csv',header=None).values
y_method2_mRNA = pd.read_csv('Model3_method2_mRNA.csv',header=None).values
y_method3_mRNA = pd.read_csv('Model3_method3_mRNA.csv',header=None).values
y_method3_alpha = pd.read_csv('Model3_method3_alpha.csv',header=None).values

fig = plt.figure(figsize=(12,12))
plt.subplot(3,3,1)
plot_time_series(t,y_method1_mRNA[:,0])
plot_time_series(t,y_method1_mRNA[:,1])
plt.title(r'Method 1 - approximation of full model')

plt.subplot(3,3,4)
plot_time_series(t,y_method1_promoter[:,0])
plot_time_series(t,y_method1_promoter[:,1])
plt.ylabel('Promoter state')
plt.title(r'Promoter time series')

plt.subplot(3,3,7)
plot_hist(y_method1_mRNA[-1,:],bins=np.linspace(0,100,12))
plt.title(r'mRNA distribution')

b = 50/3
r = 1
x_vec = range(100)
p_tot = np.zeros(len(x_vec))

for i in range(len(x_vec)):
        x = x_vec[i]
        m = b*r
        p = r/(m+r)
        y = nbinom.pmf(x, r, p)
        p_tot[i] = y

plt.plot(x_vec,p_tot,'r',label='NB approx')
legend = plt.legend(loc='upper right', shadow=True)

plt.subplot(3,3,2)
plot_time_series(t,y_method2_mRNA[:,0])
plot_time_series(t,y_method2_mRNA[:,1])
plt.title(r'Method 2 - random bursts')

plt.subplot(3,3,8)
plot_hist(y_method2_mRNA[-1,:],bins=np.linspace(0,100,12))
plt.title(r'mRNA distribution')
plt.plot(x_vec,p_tot,'r',label='NB exact')
legend = plt.legend(loc='upper right', shadow=True)

plt.subplot(3,3,3)
plot_time_series(t,y_method3_mRNA[:,0])
plot_time_series(t,y_method3_mRNA[:,1])
plot_time_series(t,y_method3_mRNA[:,2])
plt.plot([t[0],t[-1]],[y_method3_alpha[0],y_method3_alpha[0]],'k--')
plt.plot([t[0],t[-1]],[y_method3_alpha[1],y_method3_alpha[1]],'k--')
plt.plot([t[0],t[-1]],[y_method3_alpha[2],y_method3_alpha[2]],'k--')
plt.title(r'Method 3 - gamma distributed $\alpha$')

plt.subplot(3,3,9)
plot_hist(y_method3_mRNA[-1,:],bins=np.linspace(0,100,12))
plt.title(r'mRNA distribution')
plt.plot(x_vec,p_tot,'r',label='NB exact')
legend = plt.legend(loc='upper right', shadow=True)

plt.tight_layout()
```

*Method 1: as an approximation of the full telegraph model when burst are short*. In the regime where burst are short (i.e. a large ratio $k_{\text{off}}/\gamma$), the full model can be approximated by the negative binomial distribution (see Raj et al. 2006 Supp Material [17](https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.0040309) ). In contrast to the full telegraph model (which has 3 parameters, exluding the degradation rate that is impossible to infer), the negative binomial distribution has two parameters: a burst size and burst frequency. A negative binomial distribution is less flexible than the beta-Poisson distribution and can never give rise to a bimodal distribution. However, the short burst size is probably a reasonable assumption for many mammalian genes [13](https://science.sciencemag.org/content/332/6028/472.abstract) and parameter inference is generally more robust.

*Method 2: instantaneous bursts of mRNA production*. In contrast to the approximation made in Method 1, it's possible to use a slightly different model of transcription that will give rise to a negative binomial as the exact distribution. In Friedman et al. 2006 [18](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.97.168302), the authors consider a model of protein dynamics where protein production occurs in random bursts with an exponentially distributed number of molecules, and they show that this model leads to a gamma distribution in steady state. In fact, the model they used was equivalent to solving the random differential equation for $X_t$ where the production term $M_t$ is described by instantaneous, exponentially distributed injections. In the time series for Method 2 you can see that mRNA increases in discontinuous bursts. When the mixture distribution of the Poisson distribution is a gamma distribution, the integral can be performed analytically and leads to a negative binomial distribution (see wiki page for details [19](https://en.wikipedia.org/wiki/Negative_binomial_distribution)).

*Method 3: a simple model of extrinsic noise*. A negative binomial distribution can also be created with an entirely different generative model. It is unrealistic to assume that all cells have exactly the same kinetic parameters, and parameter variability could arise from many different mechanisms (e.g. differences in concentration of transcriptional enzymes, cell volume etc.). Let's imagine that the transcription rate varies according to a gamma distribution. In the time series for Method 3 we see that cells fluctuate around their own mean, which is dictated by their individual transcription parameter. In each cell there is a Poisson distribution with a rate parameter equal to the hidden (latent) transcription rate. Given that the transcription rate is gamma distributed, we once again recover a negative binomial distribution across the whole population.

Methods 2 and 3 once again illustrate a very important principle: different dynamical models can produce the same stationary distributions.

We can code any of the models in STAN using a negative binomial distribution.

Option 1: directly use a negative binomial distribution.

```python
model3 = """
data {
    int<lower=1> N;
    int y[N];
}
parameters {
    real<lower=0> b;
    real<lower=0> f;
}
model {
    y ~ neg_binomial_2( b*f , f);
    b ~ normal(0,100);
    f ~ normal(0,100);
}

"""
```

For Method 3, we could alternatively give each cell its own (latent) transcription parameter that is drawn from a gamma distribution. This method is less efficient because each cell has an associated latent variable.

Option 2: use a latent variable representation with a gamma mixture of Poisson distributions.

```python
model3 = """
data {
    int<lower=1> N;
    int y[N];
}
parameters {
    real<lower=0> b;
    real<lower=0> f;
    real<lower=0> alpha[N];
}
model {
    y ~ poisson(alpha);
    alpha ~ gamma(f,b):
    b ~ normal(0,100);
    f ~ normal(0,100);
}

"""
```

## Model 4: the multivariate log-normal Poisson mixture distribution

Instead of using a gamma distribution, we could be more open-minded about what type of distribution we use for the mixing distribution. We could, for example, use a log-normal distribution instead of a gamma distribution (similarly to as performed here: [6](https://doi.org/10.1038/s41587-021-00875-x)). There are many different ways that one could generate a log-normal distribution. Mechanistically, we could assume that the transcription process $X_t$ follows a modified Ornstein-Uhlenbeck process in log space. Alternatively, the central limit theorem in the log domain tells us that we expect a log-normal distribution when we multiply many independent variables.

We saw that the negative binomial distribution could be generated by either purely intrinsic (transcriptional bursting) or extrinsic (differences in transcription rate) mechanisms. We could also decide to be agnostic and say: we'll use the log-normal to model everything (both intrinsic and extrinsic) that we can't measure and that will create super-poissonian noise.

With a multivariate log-normal distribution it's also possible to introduce correlation between genes and hence encode networks of statistical relationships. This is also an opportunity to show another nice feature of STAN. A correlation matrix has a certain structure (symmetric and positive semi-definite), so we could have problems if we encode each entry in the correlation matrix as a free parameter. Fortunately, in STAN there is a special data type for correlation matrices that will ensure that this doesn't cause a problem. Here is an example of how you could code this in STAN for a matrix of gene expression counts $Y$ with $N$ cells and $K$ number of genes. Note that we use a "non-centred" parameterisation for the log-normal latent variables (see the STAN reference manual for discussion).

```python
model4 = """
data {
    int<lower=1> N;
    int<lower=1> K;
    int<lower=0> Y[N,K];
}
parameters {
    row_vector[K] mu_vec;
    matrix[N, K] eta;
    cholesky_factor_corr[K] L;
    vector[K] sigma_vec;
}
transformed parameters{
    matrix[N, K] Z;
    matrix[K, K] C;
    corr = L*L';
    for ( n in 1:N ) {
        Z[n,:] = exp(mu_vec + (diag_pre_multiply(sigma_vec,L)*(eta[n,:])')');
    }
}
model {
    to_vector(eta) ~ normal(0, 1);
    for ( k in 1:K ) {
        Y[:,k] ~ poisson(Z[:,k]);
    }
    mu_vec ~ normal(0,100);
    sigma_vec ~ normal(0,100);
    L ~ lkj_corr_cholesky(2.0);
}

"""
```

## Model 5: incorportating extrinsic noise explicitly

There is somewhat of a debate within the single-cell gene expression community about how much single cell noise is due to intrinsic vs extrinsic factors.

We saw a very interesting example for the negative binomial distribution, where intrinsic noise (caused by transcriptional bursting) and extrinsic noise (caused by cell-to-cell parameter differences) generated exactly the same mRNA distribution.

So how will we ever know the real contribution of extrinsic noise? One approach is to measure as many possible cellular variables as possible and see whether knowledge of these variables helps predict gene expression in a given cell. In Battich et al. 2015 [20](https://www.cell.com/fulltext/S0092-8674(15)01498-1), for an example, they measured 183 features including cell area, shape, cell crowding, neighbourhood activity etc.

We can incorporate any additional cellular measurements into our STAN model. The measured cellular features can be supplied as an additional data input in the STAN data block. You could either use the original measured cellular features or perhaps a lower-dimensional representation using either a linear or nonlinear dimensionality reduction technique. You could even try and do this in STAN if you were feeling adventurous!

We still need to define the distribution of mRNA counts *given* all of the external cellular variables (i.e. we still need to define a model of the remaining intrinsic noise once we have the cellular information). You can experiment with any of the Models 1-4. In our recent paper we used a negative binomial distribution, but if you decide to do this then you need to define whether the additional measurements affect either burst size or burst frequency.

There is previous work ([21](https://www.cell.com/molecular-cell/pdfExtended/S1097-2765(15)00170-7)) showing that burst size is a function of cell volume, so let's code this as a STAN model as a simple example where the burst size $b$ is now rescaled by the volume $v$ to become $bv$, and where we enter the volume $v$ as an extra data input.

```python
model5 = """
data {
    int<lower=1> N;
    int y[N];
    int v[N]; // cell volume
}
parameters {
    real<lower=0> b;
    real<lower=0> f;
}
model {
    y ~ neg_binomial_2( b*v*f , f);
    b ~ normal(0,100);
    f ~ normal(0,100);
}

"""
```

## Technical noise

We've discussed models of biological cell-to-cell variability originating from either intrinsic or extrinsic sources (as modelled with models 1-5), but the measurement process can also contribute technical noise. The measurement process will inevitably introduce extra variability into the data and distort the "true" molecule count.

For smFISH, for example, it's possible to either miss an existing mRNA molecule (i.e. false negative) or call an mRNA molecule when it doesn't exist (i.e. a false positive). Modelling technical noise is an area where detailed consideration of the measurement process is essential. We can add an extra layer of measurement noise in any way we like in STAN. For the smFISH example, perhaps it's reasonable to assume that for each true mRNA molecule there is a fixed probability $p$ of missing a true mRNA molecule, and we could then add an extra binomial noise level to our model using this probability $p$. But what value of $p$ do we use? This is where careful positive and negative control experiments can supply you with numbers to use as part of the STAN model.

For single-cell RNA-seq the question of technical noise is even more controversial. Grün et al. 2014 [22](https://www.nature.com/articles/nmeth.2930) proposed several models of RNA-seq technical noise to account for sampling noise and global cell-to-cell variation in sequencing efficiency, including a model where the measured data is sampled from the underlying mRNA count according to a negative binomial distribution. Many RNA-seq papers also include "dropout" (or zero inflation) as part of their technical noise model, although there is some debate about whether this is really justified [23](https://www.nature.com/articles/s41587-019-0379-5). There are many different options for implementing these technical noise models, and it's particularly important to consider the consequences of the technical noise on the ability to successfully infer the underlying biological parameters.

## Model selection and validation

Let's first acknowledge that model (or theory) selection and validation is a crucial feature of basically every scientific discipline, and unfortunately there is no magic formula that will tell us whether a theory or model is a good representation of objective reality.

One important point to note regarding our STAN models: our models are defined in terms of discrete probability distributions that explicitly describe count data, and we evaluate the likelihood of observing a given data point using this discrete probability distribution. Notions of model fit based e.g. on mean squared error make little sense in this context (at least to me).

After model fitting, a reasonable starting point is to present a visual, side-by-side comparison of the data and model predictions. There are often obvious qualitative similarities or differences that are revealed by this basic comparison, and it's a simple form of model checking that everyone can understand.

Beyond this basic check, we'd like to put numbers on our preference for a model. There are so many options for model selection, including AIC, BIC, WAIC, Bayes factors, cross-validation (CV) etc., that choosing a method can feel quite overwhelming.

You can find a detailed discussion of the various model selection options in most Bayesian analysis textbooks. Despite the potential pitfalls and caveats (including problems when comparing many different models), cross-validation seems overall like a reasonable approach. In our paper we tried two different approaches:

* I found this paper particularly helpful for clarifying different concepts [21](https://link.springer.com/article/10.1007/s11222-016-9696-4) and subsequently used Pareto-smoothed importance sampling leave-one-out cross-validation (PSIS-LOO) for model selection in our paper. PSIS-LOO is available as a package that accepts the output from STAN inference.

* If there's structure in the data that naturally provides an interesting way of splitting data, you can also be more creative about cross-validation strategies. For example, in our paper we used "leave-replicate-out" CV where each test subsample was a separate slide that had been physically isolated from other cells during the experiment.

An even more interesting method of model validation is to test predictions on previously unseen perturbations. A really nice illustration of this approach is here: [22](https://www.sciencedirect.com/science/article/pii/S2589004220307574). Just like the construction of the STAN models, it's possible to combine or mix-and-match between these different techniques.

## Strengths and limitations of the approach

When defining our models we assumed that the biological system is in a stationary state, although it's still possible to use the framework without this assumption. We need to solve the random differential equation to calculate the probability distribution $p_{X_t}(x,t)$ of the process $X_t$, and for our simple models of bursty gene expression this was possible.

The underlying dynamics of real biological systems are undoubtably far more complicated. For example, transcription might be regulated by nonlinear positive and negative feedback interactions between multiple transcription factors. We've also neglected spatial dimensions and diffusion in our model.

If you'd like to add e.g. feedback of proteins onto transcription rates then in most cases these models have no analytical solution. There are several alternative techniques that allow more general models, such as the Finite State Projection [23](https://aip.scitation.org/doi/abs/10.1063/1.4960505) or likelihood-free computational inference techniques [24](https://www.sciencedirect.com/science/article/abs/pii/S0022519320301107?via%3Dihub). It's currently quite difficult to scale these techniques to thousands of genes, but they are certainly interesting options that for exploring more detailed, mechanistic models. The downside is that it's often not possible to use the efficient implementation of parameter exploration (HMC) that STAN uses for these approaches.

On the other end of the spectrum, neural networks are becoming increasingly popular for analysing single-cell RNA-seq data. Tensorflow Probability would be particularly interesting if you'd like to combine some of the models presented in this tutorial with neural networks. In Tensorflow Probability, for example, you can use a neural network to set the parameters of a negative binomial likelihood function.

These choices form part of a wider debate on whether more detailed, mechanistic models or more flexible but mechanistically agnostic machine learning approaches are going to bring the most insight into single-cell datasets (or indeed hybrid models). Who knows what we'll be doing 10 years from now? All I can say is that if you're interested in Bayesian analysis then STAN is definitely worth checking out.


```python
from IPython.display import HTML

HTML('''
<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code for QISKit exercises."></form>''')
```

<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code for QISKit exercises."></form>

# 7. Quantum teleportation

Quantum teleportation was first described in [1], it allows the transfer of an unknown quantum state of a qubit between two parties. In order to do so, the two parties, Alice and Bob, must first share an entangled pair of qubits in a Bell state. Alice can then transfer the unknown quantum state of a qubit to Bob by measuring her share of the entangled pair and the qubit she wants to transfer. Depending on the outcome of these measurements, she then tells Bob, through classical communication, which gates he should apply to his qubit in order to convert the state of his share of the entangled pair of qubit into the unknown state Alice wished to transfer.
Another algorithm, called superdense coding, which allows to encode 2 bits of classical information in one qubit has been described in [2]. This again involves two parties initially sharing an entangled pair of qubits in the Bell state. Alice can then apply any of the four Pauli gates (I,X,Y,Z) to her qubit and then send it to Bob. When Bob jointly measures the two qubits, he can learn which operator was used by Alice. Therefore, he can read 2 bits of information.
These algorithms are somewhat similar to each other, the key new feature of both is that they exploit the non-classical correlation arising in entangled states as a resource for communication.

## 7.1 Quantum teleportation algorithm

$$\text{1. Quantum circuit for quantum teleportation.}$$

Let's take a look at the details of the quantum teleportation algorithm.

Let us Assume that Alice (A) and Bob (B) each have a qubit from a shared entangled pair whose wave function is

\begin{equation}
\lvert\Phi^{+} \rangle =\frac{1}{\sqrt{2}}( \lvert 0\rangle_A \lvert 0 \rangle_B+ \lvert 1\rangle_A \lvert 1 \rangle_B)
\end{equation}

Alice wants to teleport another qubit in an unknown quantum state $ \lvert \phi \rangle$ to Bob.

\begin{equation}
 \lvert \phi \rangle=\alpha \lvert 0 \rangle+\beta \lvert 1 \rangle
\end{equation}

The total wave function $ \lvert \psi_1 \rangle$ of all the qubits is

\begin{eqnarray}
 \lvert \psi_1 \rangle = \lvert \phi \rangle \otimes \lvert\Phi^{+} \rangle  =\frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_A +\beta \lvert 1 \rangle_A)\otimes ( \lvert 0\rangle_A \lvert 0 \rangle_B+ \lvert 1\rangle_A \lvert 1 \rangle_B)=  \\
\frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B +\alpha \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B + \beta \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B + \beta \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B)
\end{eqnarray}

Next, Alice applies the CNOT gate to her two qubits using the unknown qubit as the control and the qubit from the entangled pair as the target

\begin{equation}
\lvert \psi_2 \rangle = \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B +\alpha \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B + \beta \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 0 \rangle_B + \beta \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 1 \rangle_B)
\end{equation}

Then, Alice applies the Hadamard gate to the qubit in the unknown state

\begin{equation}
\lvert \psi_{3} \rangle=\frac{1}{\sqrt{2^3}}(\alpha  \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B + \alpha  \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B + \alpha  \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B + \alpha  \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B+ \beta  \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 0 \rangle_B-\beta  \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 0 \rangle_B + \beta  \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 1 \rangle_B-\beta  \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 1 \rangle_B)
\end{equation}

Alice measures the state of her two qubits and communicate two bits of classical information to Bob to inform him of the outcome of her measurements. The system collapses to one of the four following states:

\begin{equation}
\lvert \psi_{4} \rangle=\frac{1}{\sqrt{2}}(\alpha  \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B  + \beta  \lvert 0 \rangle_A \lvert 0 \rangle_A \lvert 1 \rangle_B)
\end{equation}

\begin{equation}
\lvert \psi_{4} \rangle=\frac{1}{\sqrt{2}}(\alpha  \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B  + \beta  \lvert 0 \rangle_A \lvert 1 \rangle_A \lvert 0 \rangle_B)
\end{equation}

\begin{equation}
\lvert \psi_{4} \rangle=\frac{1}{\sqrt{2}}(\alpha  \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 0 \rangle_B  - \beta  \lvert 1 \rangle_A \lvert 0 \rangle_A \lvert 1 \rangle_B)
\end{equation}

\begin{equation}
\lvert \psi_{4} \rangle=\frac{1}{\sqrt{2}}(\alpha  \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 1 \rangle_B  - \beta  \lvert 1 \rangle_A \lvert 1 \rangle_A \lvert 0 \rangle_B)
\end{equation}

Depending on the values of the classical bits which Bob receives from Alice, he will apply one of four possible gates to his qubit

00: Bob applies the identity gate to his qubit and obtains (we neglect the measured qubits)

\begin{equation}
\lvert \psi_{5} \rangle = I \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_B  + \beta \lvert 1   \rangle_B) = \lvert \phi \rangle
\end{equation}

01: Bob applies the $X$ gate to his qubit

\begin{equation}
\lvert \psi_{5} \rangle = X \frac{1}{\sqrt{2}}(\alpha \lvert 1 \rangle_B  + \beta \lvert 0   \rangle_B) = \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_B  + \beta \lvert 1   \rangle_B) = \lvert \phi \rangle
\end{equation}

10: Bob applies the $Z$ gate to his qubit

\begin{equation}
\lvert \psi_{5} \rangle = Z \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_B  - \beta \lvert 1   \rangle_B) = \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_B  + \beta \lvert 1   \rangle_B) = \lvert \phi \rangle
\end{equation}

11: Bob applies the $X$ and the $Z$ gate to his qubit

\begin{equation}
\lvert \psi_{5} \rangle = ZX \frac{1}{\sqrt{2}}(\alpha \lvert 1 \rangle_B  - \beta \lvert 0   \rangle_B) = \frac{1}{\sqrt{2}}(\alpha \lvert 0 \rangle_B  + \beta \lvert 1   \rangle_B) = \lvert \phi \rangle
\end{equation}

Therefore, Bob is able to reproduce the state $\lvert \phi \rangle$ on his qubit

###  QISKit: implement the quantum teleportation algorithm

####  1) Teleport a qubit in the state $\frac{1}{\sqrt{2}}( \lvert 0 \rangle+ \lvert 1 \rangle)$

```python
from qiskit import *

# Quantum program setup
Q_program = QuantumProgram()

# Creating registers
q = Q_program.create_quantum_register('q', 3)
c0 = Q_program.create_classical_register('c0', 1)
c1 = Q_program.create_classical_register('c1', 1)
c2 = Q_program.create_classical_register('c2', 1)

# Creates the quantum circuit
teleport = Q_program.create_circuit('teleport', [q], [c0,c1,c2])

# Make the shared entangled state
teleport.h(q[1])
teleport.cx(q[1], q[2])

# Prepare Alice's qubit
teleport.h(q[0])

# Alice applies teleportation gates (or projects to Bell basis)
teleport.cx(q[0], q[1])
teleport.h(q[0])

# Alice measures her qubits
teleport.measure(q[0], c0[0])
teleport.measure(q[1], c1[0])

# Bob applies certain gates based on the outcome of Alice's measurements
teleport.z(q[2]).c_if(c0, 1)
teleport.x(q[2]).c_if(c1, 1)

# Bob checks the state of the teleported qubit
teleport.measure(q[2], c2[0])

# Shows gates of the circuit
circuits = ['teleport']
print(Q_program.get_qasms(circuits)[0])

# Parameters for execution on simulator
backend = 'local_qasm_simulator'
shots = 1024 # the number of shots in the experiment

# Run the algorithm
result = Q_program.execute(circuits, backend=backend, shots=shots)

#Shows the results obtained from the quantum algorithm
counts = result.get_counts('teleport')

print('\nThe measured outcomes of the circuits are:', counts)

# credits to:  https://github.com/QISKit/qiskit-tutorial
```

    OPENQASM 2.0;
    include "qelib1.inc";
    qreg q[3];
    creg c0[1];
    creg c1[1];
    creg c2[1];
    h q[1];
    cx q[1],q[2];
    h q[0];
    cx q[0],q[1];
    h q[0];
    measure q[0] -> c0[0];
    measure q[1] -> c1[0];
    if(c0==1) z q[2];
    if(c1==1) x q[2];
    measure q[2] -> c2[0];

    The measured outcomes of the circuits are: {'0 0 0': 119, '0 0 1': 123, '0 1 0': 134, '0 1 1': 121, '1 0 0': 136, '1 0 1': 117, '1 1 0': 145, '1 1 1': 129}

## 7.2 Superdense coding

Here we take a look at the algorithm for superdense coding [2]. This quantum algorithm allows to transfer two bits of classical information just by transferring a qubit. The only requirement is that it is possible to generate an entangled pair of qubit in the Bell state $\lvert\Phi^{+} \rangle$.

$$\text{2. Quantum circuit for superdense coding.}$$

The algorithm involves the following steps:

As in the quantum teleportation algorithm, Alice and Bob share an entangled pair of qubit in the Bell state to start with.

\begin{equation}
\lvert \psi_1 \rangle = \lvert\Phi^{+} \rangle =\frac{1}{\sqrt{2}}( \lvert 0\rangle_A \lvert 0 \rangle_B+ \lvert 1\rangle_A \lvert 1 \rangle_B)
\end{equation}

Depending on the two classical bits $b_0$, $b_1$ that Alice wants to send to Bob, Alice will apply on of four possible gates $X^{b_0}Z^{b_1}$ to her qubit

$b_0b_1=00$: Alice applies $X^{0}Z^{0}=I$ to her qubit. The state of the system is now:

\begin{equation}
\lvert \psi_2 \rangle =\frac{1}{\sqrt{2}}( \lvert 0\rangle_A \lvert 0 \rangle_B+ \lvert 1\rangle_A \lvert 1 \rangle_B)
\end{equation}

$b_0b_1=01$: Alice applies $X^{0}Z^{1}=Z$ to her qubit. The state of the system is now:

\begin{equation}
\lvert \psi_2 \rangle =\frac{1}{\sqrt{2}}( \lvert 0\rangle_A \lvert 0 \rangle_B - \lvert 1\rangle_A \lvert 1 \rangle_B)
\end{equation}

$b_0b_1=10$: Alice applies $X^{1}Z^{0}=X$ to her qubit. The state of the system is now:

\begin{equation}
\lvert \psi_2 \rangle =\frac{1}{\sqrt{2}}( \lvert 1 \rangle_A \lvert 0 \rangle_B+ \lvert 0\rangle_A \lvert 1 \rangle_B)
\end{equation}

$b_0b_1=11$: Alice applies $X^{1}Z^{1}=XZ$ to her qubit. The state of the system is now:

\begin{equation}
\lvert \psi_2 \rangle =\frac{1}{\sqrt{2}}( \lvert 1 \rangle_A \lvert 0 \rangle_B - \lvert 0\rangle_A \lvert 1 \rangle_B)
\end{equation}

Now, Alice sends her qubit to Bob, who can read the two-bit message sent by Alice through her qubit by making a joint measurement of the two qubits in the Bell basis. Since the quantum computer available today can carry out measurement only in the standard (Z) basis, let us see how to convert a two-qubit measurement in the Bell basis into a two-qubit measurement in the standard basis.

$$\text{3. Equivalence between different measurement bases.}$$

Measuring two qubits in the Bell basis is equivalent to performing a CNOT gate $CX_{12}$, where the first qubit is the control qubit and the second qubit is the target one, and an Hadamard gate $H$ on the first qubit and then measuring in the standard basis. The quantum circuit with measurements in the standard basis is depicted in Fig. 4.

$$\text{4. Superdense coding quantum circuit with measurements in the standard basis.}$$

Bob's possible outcomes are:

\begin{equation}
\lvert \psi_3 \rangle =\frac{1}{2}( \lvert 0\rangle_B \lvert 0 \rangle_B + \lvert 1\rangle_B \lvert 0 \rangle_B + \lvert 0\rangle_B \lvert 0 \rangle_B - \lvert 1\rangle_B \lvert 0 \rangle_B) = \lvert 0\rangle_B \lvert 0 \rangle_B
\end{equation}

Therefore, Bob decodes $b_0b_1=00$

\begin{equation}
\lvert \psi_3 \rangle =\frac{1}{2}( \lvert 0\rangle_B \lvert 0 \rangle_B + \lvert 1\rangle_B \lvert 0 \rangle_B - \lvert 0\rangle_B \lvert 0 \rangle_B + \lvert 1\rangle_B \lvert 0 \rangle_B) = \lvert 1\rangle_B \lvert 0 \rangle_B
\end{equation}

Therefore, Bob decodes $b_0b_1=01$

\begin{equation}
\lvert \psi_3 \rangle =\frac{1}{2}( \lvert 0\rangle_B \lvert 1 \rangle_B - \lvert 1\rangle_B \lvert 1 \rangle_B + \lvert 0\rangle_B \lvert 1 \rangle_B + \lvert 1\rangle_B \lvert 1 \rangle_B) = \lvert 0\rangle_B \lvert 1 \rangle_B
\end{equation}

Therefore, Bob decodes $b_0b_1=10$

\begin{equation}
\lvert \psi_3 \rangle =\frac{1}{2}( \lvert 0\rangle_B \lvert 1 \rangle_B - \lvert 1\rangle_B \lvert 1 \rangle_B - \lvert 0\rangle_B \lvert 1 \rangle_B - \lvert 1\rangle_B \lvert 1 \rangle_B) = - \lvert 1 \rangle_B \lvert 1 \rangle_B
\end{equation}

Therefore, Bob decodes $b_0b_1=11$

###  QISKit: implement the superdense coding algorithm

####  3) Send the two bits $01$ through superdense coding

```python
from initialize import *
from qiskit import *

#initialize quantum program
my_alg = initialize(circuit_name = 'superdense', qubit_number=2, bit_number=2, backend = 'local_qasm_simulator', shots = 1024)

#add gates to the circuit

#creates a bell pair
my_alg.q_circuit.h(my_alg.q_reg[0]) # applies H gate to first qubit
my_alg.q_circuit.cx(my_alg.q_reg[0],my_alg.q_reg[1]) ## applies CX gate

#Alice encodes 01
my_alg.q_circuit.x(my_alg.q_reg[0])

#to measure in the Bell basis, Bob does the following operations before measuring in the standard basis
my_alg.q_circuit.cx(my_alg.q_reg[0],my_alg.q_reg[1]) ## applies CX gate
my_alg.q_circuit.h(my_alg.q_reg[0]) # applies H gate to first qubit
my_alg.q_circuit.measure(my_alg.q_reg[0], my_alg.c_reg[0]) # measures the first qubit
my_alg.q_circuit.measure(my_alg.q_reg[1], my_alg.c_reg[1]) # measures the second qubit

print('List of gates:')
for circuit in my_alg.q_circuit:
    print(circuit.name)

#Execute the quantum algorithm
result = my_alg.Q_program.execute(my_alg.circ_name, backend=my_alg.backend, shots= my_alg.shots)

#Show the results obtained from the quantum algorithm
counts = result.get_counts(my_alg.circ_name)

print('\nThe measured outcomes of the circuits are:',counts)

# credits to:  https://github.com/QISKit/qiskit-tutorial
```

    List of gates:
    h
    cx
    x
    cx
    h
    measure
    measure

    The measured outcomes of the circuits are: {'10': 1024}

## Problems

Show the step-by-step evolution of the total state of the system $\lvert \psi \rangle$ for the quantum teleportation algorithm for the following states to be teleported:

$\lvert 1 \rangle$

$\frac{1}{\sqrt{1}} ( \lvert 0 \rangle+ \lvert 1 \rangle )$

$\sqrt{\frac{2}{3}} \lvert 0 \rangle+\frac{1}{\sqrt{3}} \lvert 1 \rangle$

Design quantum circuits for the quantum teleportation of the states given in problem 1.

Write QISKit programs which implement the quantum teleportation algorithm for each state specified in problem 1.

Alice wants to send 4 classical bits $b_0b_1b_2b_3=0110$ to Bob by sending only two qubits. Explain how this can be done. Then, design a quantum circuit which implements it.

Show the step-by-step evolution of the total state of the system for the situation described in problem 4.

Write a QISKit program which implements the superdense coding algorithm specified in problem 4.

## References

[1] C. H. Bennett et al., Physical Review Letters, 70, 1895 (1993).

[1] C. H. Bennett and S. J. Wiesner, Physical Review Letters, 69, 2881
(1992).

[3] K. Mattle et al., Physical Review Letters, 76, 4656 (1996).

[4] M. Riebe et al., Nature, 429,734 (2004).

[5] M. D. Barrett et al., Nature, 429, 737 (2004).

[6] H. J. Kimble and S. J. van Euk, Nature, 429, 712 (2004).

[7] M. B. Plenio and V. Vedral, Contemporary Physics, 39, 431 (1998).

[8] D. Bouwmeester et al., Nature, 390, 575 (1997)

[9] S. Fedortchenko, arXiv:1607.02398 (2016).


```
%%capture
!pip install --upgrade pip
!pip install cirq==0.7
```

# Rabi Oscillation Experiment

In this experiment, you are going to use Cirq to check that rotating a qubit by an increasing angle, and then measuring the qubit, produces Rabi oscillations. This requires you to do the following things:

1. Prepare the $|0\rangle$ state.
2. Rotate by an angle $\theta$ around the $X$ axis.
3. Measure to see if the result is a 1 or a 0.
4. Repeat steps 1-3 $k$ times.
5. Report the fraction of $\frac{\text{Number of 1's}}{k}$
found in step 3.

## 1. Getting to know Cirq

Cirq emphasizes the details of implementing quantum algorithms on near term devices.
For example, when you work on a qubit in Cirq you don't operate on an unspecified qubit that will later be mapped onto a device by a hidden step.
Instead, you are always operating on specific qubits at specific locations that you specify.

Suppose you are working with a 54 qubit Sycamore chip.
This device is included in Cirq by default.
It is called `cirq.google.Sycamore`, and you can see its layout by printing it.

```
import cirq
working_device = cirq.google.Sycamore
print(working_device)
```

For this experiment you only need one qubit and you can just pick whichever one you like.

```
my_qubit = cirq.GridQubit(5, 6)
```

Once you've chosen your qubit you can build circuits that use it.

```
from cirq.contrib.svg import SVGCircuit

# Create a circuit with X, Ry(pi/2) and H.
my_circuit = cirq.Circuit(
    # Rotate the qubit pi/2 radians around the X axis.
    cirq.rx(3.141 / 2).on(my_qubit),
    # Measure the qubit.
    cirq.measure(my_qubit, key='out')
)
SVGCircuit(my_circuit)
```

Now you can simulate sampling from your circuit using `cirq.Simulator`.

```
sim = cirq.Simulator()
samples = sim.sample(my_circuit, repetitions=10)
samples
```

You can also get properties of the circuit, such as the density matrix of the circuit's output or the wavefunction just before the terminal measurement.

```
wavefuntion_before_measurement = sim.simulate(my_circuit[:-1])
sampled_wavefunction_after_measurement = sim.simulate(my_circuit)

print(f'State before measurement:')
print(wavefuntion_before_measurement)
print(f'State after measurement:')
print(sampled_wavefunction_after_measurement)

```

You can also examine the outputs from a noisy environment.
For example, an environment where 10% depolarization is applied to each qubit after each operation in the circuit:

```
noisy_sim = cirq.DensityMatrixSimulator(noise=cirq.depolarize(0.1))
noisy_post_measurement_state = noisy_sim.simulate(my_circuit)
noisy_pre_measurement_state = noisy_sim.simulate(my_circuit[:-1])

print('Noisy state after measurement:' + str(noisy_post_measurement_state))
print('Noisy state before measurement:' + str(noisy_pre_measurement_state))
```

# 2. Parameterized Circuits and Sweeps

Now that you have some of the basics end to end, you can create a parameterized circuit that rotates by an angle $\theta$:

```
import sympy
theta = sympy.Symbol('theta')

parameterized_circuit = cirq.Circuit(
    cirq.rx(theta).on(my_qubit),
    cirq.measure(my_qubit, key='out')
)
SVGCircuit(parameterized_circuit)
```

In the above block you saw that there is a `sympy.Symbol` that you placed in the circuit. Cirq supports symbolic computation involving circuits. What this means is that when you construct `cirq.Circuit` objects you can put placeholders in many of the classical control parameters of the circuit which you can fill with values later on.

Now if you wanted to use `cirq.simulate` or `cirq.sample` with the parameterized circuit you would also need to specify a value for `theta`.

```
samples_at_theta_equals_2 = sim.sample(
    parameterized_circuit,
    params={theta: 2},
    repetitions=10)
samples_at_theta_equals_2
```

You can also specify *multiple* values of `theta`, and get samples back for each value.

```
samples_at_multiple_theta = sim.sample(
    parameterized_circuit,
    params=[{theta: 0.5}, {theta: 3.141}],
    repetitions=10)
samples_at_multiple_theta
```

Cirq has shorthand notation you can use to sweep `theta` over a range of values.

```
samples_at_swept_theta = sim.sample(
    parameterized_circuit,
    params=cirq.Linspace(theta, start=0, stop=3.14159, length=5),
    repetitions=5)
samples_at_swept_theta
```

The result value being returned by `sim.sample` is a `pandas.DataFrame` object.
Pandas is a common library for working with table data in python.
You can use standard pandas methods to analyze and summarize your results.

```
import pandas

big_results = sim.sample(
    parameterized_circuit,
    params=cirq.Linspace(theta, start=0, stop=3.14159, length=20),
    repetitions=10_000)

# big_results is too big to look at. Plot cross tabulated data instead.
pandas.crosstab(big_results.theta, big_results.out).plot()
```

# 3. The built-in experiment

Cirq comes with a pre-written Rabi oscillation experiment `cirq.experiments.rabi_oscillations`.
This method takes a `cirq.Sampler`, which could be a simulator or a network connection to real hardware.
The method takes a few more experimental parameters, and returns a result object
that can be plotted.

```
import datetime

result = cirq.experiments.rabi_oscillations(
    sampler=noisy_sim,
    qubit=my_qubit,
    num_points=50,
    repetitions=10000)
result.plot()
```

Notice that you can tell from the plot that you used the noisy simulator you defined earlier.
You can also tell that the amount of depolarization is roughly 10%.

# 4. Exercise: Find the best qubit

As you have seen, you can use Cirq to perform a Rabi oscillation experiment.
You can either make the experiment yourself out of the basic pieces made available by Cirq, or use the prebuilt experiment method.

Now you're going to put this knowledge to the test.

There is some amount of depolarizing noise on each qubit.
Your goal is to characterize every qubit from the Sycamore chip using a Rabi oscillation experiment, and find the qubit with the lowest noise according to the secret noise model.

```
import hashlib

class SecretNoiseModel(cirq.NoiseModel):
    def noisy_operation(self, op):
        # Hey! No peeking!
        q = op.qubits[0]
        v = hashlib.sha256(str(q).encode()).digest()[0] / 256
        yield cirq.depolarize(v).on(q)
        yield op

secret_noise_sampler = cirq.DensityMatrixSimulator(noise=SecretNoiseModel())
```

```
q = cirq.google.Sycamore.qubits[3]
print('qubit', repr(q))
cirq.experiments.rabi_oscillations(
    sampler=secret_noise_sampler,
    qubit=q
).plot()
```

```

```


# Classical Support Vector Machines

This notebook will serve as a summary of some of the resources below and is not meant to be used as a stand-alone reading material for Classical Support Vector Machines. We encourage you to complete reading the resources below before going forward with the notebook.

### Resources:
1. MIT Open Courseware lecture: https://youtu.be/_PwhiWxHK8o
3. MIT lecture slides: http://web.mit.edu/6.034/wwwbob/svm-notes-long-08.pdf
4. SVM Wikipedia page : https://en.wikipedia.org/wiki/Support_vector_machine
2. SVM tutorial using sklearn: https://jakevdp.github.io/PythonDataScienceHandbook/05.07-support-vector-machines.html

## Contents

1. [Introduction](#intro)
2. [SVMs as Linear Classifiers](#linear)
3. [Lagrange Multipliers and the Primal and Dual form](#primal)
4. [Class Prediction For a New Datapoint](#pred)
5. [Classifying Linearly Separable Data](#class-linear)
6. [Dealing With Non-Linearly Separable Data](#non-linear)
7. [Feature Map and Kernel](#kernel)
8. [Additional Resources](#add)

## Introduction

```python
# installing a few dependencies
!pip install --upgrade seaborn==0.10.1
!pip install --upgrade scipy==1.4.1
!pip install --upgrade scikit-learn==0.23.1
!pip install --upgrade matplotlib==3.2.0

# the output will be cleared after installation
from IPython.display import clear_output
clear_output()
```

    Collecting seaborn==0.10.1
      Downloading seaborn-0.10.1-py3-none-any.whl (215 kB)
    Requirement already satisfied, skipping upgrade: pandas>=0.22.0 in c:\users\anura\anaconda3\lib\site-packages (from seaborn==0.10.1) (1.0.1)
    Requirement already satisfied, skipping upgrade: scipy>=1.0.1 in c:\users\anura\anaconda3\lib\site-packages (from seaborn==0.10.1) (1.4.0)
    Requirement already satisfied, skipping upgrade: matplotlib>=2.1.2 in c:\users\anura\anaconda3\lib\site-packages (from seaborn==0.10.1) (3.1.3)
    Requirement already satisfied, skipping upgrade: numpy>=1.13.3 in c:\users\anura\anaconda3\lib\site-packages (from seaborn==0.10.1) (1.19.1)
    Requirement already satisfied, skipping upgrade: pytz>=2017.2 in c:\users\anura\anaconda3\lib\site-packages (from pandas>=0.22.0->seaborn==0.10.1) (2019.3)
    Requirement already satisfied, skipping upgrade: python-dateutil>=2.6.1 in c:\users\anura\anaconda3\lib\site-packages (from pandas>=0.22.0->seaborn==0.10.1) (2.8.1)
    Requirement already satisfied, skipping upgrade: kiwisolver>=1.0.1 in c:\users\anura\anaconda3\lib\site-packages (from matplotlib>=2.1.2->seaborn==0.10.1) (1.1.0)
    Requirement already satisfied, skipping upgrade: cycler>=0.10 in c:\users\anura\anaconda3\lib\site-packages (from matplotlib>=2.1.2->seaborn==0.10.1) (0.10.0)
    Requirement already satisfied, skipping upgrade: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in c:\users\anura\anaconda3\lib\site-packages (from matplotlib>=2.1.2->seaborn==0.10.1) (2.4.6)
    Requirement already satisfied, skipping upgrade: six>=1.5 in c:\users\anura\anaconda3\lib\site-packages (from python-dateutil>=2.6.1->pandas>=0.22.0->seaborn==0.10.1) (1.14.0)
    Requirement already satisfied, skipping upgrade: setuptools in c:\users\anura\anaconda3\lib\site-packages (from kiwisolver>=1.0.1->matplotlib>=2.1.2->seaborn==0.10.1) (45.2.0.post20200210)
    Installing collected packages: seaborn
      Attempting uninstall: seaborn
        Found existing installation: seaborn 0.10.0
        Uninstalling seaborn-0.10.0:
          Successfully uninstalled seaborn-0.10.0
    Successfully installed seaborn-0.10.1

Suppose you are a Botanist trying to distinguish which one of the **three species** a flower belongs to just by looking at **four features** of a flower - the length and the width of the sepals and petals. As part of your research you create a **dataset** of these features for a set of flowers for which the **species is already known**, where each **datapoint** of this dataset corresponds to a single flower. Now, your colleague brings in a new flower and asks you which species it belongs to. You could go into the lab and do the necessary tests to figure out what species it is, however, the lab is under renovation. So, left with no other choice you pull up the dataset that you created earlier and after a few minutes of trying to find a pattern you realise that this new flower has a petal width and sepal length similar to all the flowers of species 1. Thus, you **predict** this new flower to be of the species 1. This process of assigning a new datapoint to one of the known **classes** (flower species) is called **classfication**. And, as we used a dataset where we knew the classes corresponding to the datapoints before-hand, thus, this classification procedure comes under the umbrella of [**supervised learning**](https://en.wikipedia.org/wiki/Supervised_learning).

Support Vector Machines (SVMs) are **supervised learning models** that are mainly used for **classification** and **regression** tasks. In the context of classification, which is the topic of discussion, we use SVMs to find a **linear decision boundary with maximum width** splitting the space such that datapoints belonging to different classes are on either side of the boundary. Classification takes place based on which side of the decision boundary a new datapoint lands.

Before we try to understand how SVMs work, let's take a look at the [Iris dataset](https://en.wikipedia.org/wiki/Iris_flower_data_set) which was the dataset mentioned in the first paragraph.

```python
# importing the iris dataset
from sklearn.datasets import load_iris
import numpy as np

iris = load_iris()

print("Number of datapoints: {}".format(iris['data'].shape[0]))
print("Number of features: {}".format(iris['data'].shape[1]))
print("Sample of the dataset:")
print(iris['data'][:5])
print("Unique species : {}".format(np.unique(iris['target'])))
```

    Number of datapoints: 150
    Number of features: 4
    Sample of the dataset:
    [[5.1 3.5 1.4 0.2]
     [4.9 3.  1.4 0.2]
     [4.7 3.2 1.3 0.2]
     [4.6 3.1 1.5 0.2]
     [5.  3.6 1.4 0.2]]
    Unique species : [0 1 2]

Looking at the first 5 datapoints of the Iris dataset we realize that each datapoint is an array with four features. The number of features in a dataset is called the **dimension of the dataset**. Further, there are three unique species, which implies, three **classes** in the dataset. It's important to note that SVMs are natively binary classification algorithms, i.e, can classify between only 2 classes. However, there are methods to convert a binary classifier to a multi-class classifier, mentioned [here](https://datascience.stackexchange.com/questions/46514/how-to-convert-binary-classifier-to-multiclass-classifier). Let us now dig deeper into the mathematics of how SVMs work.

**Reminder:** Read the resources provided above to understand the next section with greater degree of clarity.

## SVMs as Linear Classifiers

Source: [wikipedia](https://en.wikipedia.org/wiki/Support_vector_machine)

Our input dataset is of the form $(\vec{x}_{1}, y_{1}), ..., (\vec{x}_{n}, y_{n})$,
where, $\vec{x}$ is a $d$ dimensional vector where $d$ is the number of features and $y_{i}$'s are the labels $ y_{i} \in {-1, +1}$ as it is a binary classification problem.

$\vec{w}$ is a vector perpendicular to the **decision boundary** (hyperplane that cuts the space into two parts and is the result of classification). Let $\vec{u}$ be a vector representing a point on our feature space. Now, to understand whether a point is on the +ve side or the -ve side we'll have to project the point $\vec{u}$ onto $\vec{w}$, which will give us a scaled version of $\vec{u}$'s projection in the direction perpendicular to the decision boundary. Depending on the value of this quantity we'll have the point either on the +ve side or the -ve side. This can be represented mathematically as

$$\begin{equation} \vec{w}\cdot\vec{x}_{+} + b \geq 1 \label{eq:vector_ray} \tag{1}\end{equation}$$
$$\begin{equation} \vec{w}\cdot\vec{x}_{-} + b \leq -1  \tag{2}\end{equation}$$

where, $\vec{x}_{+}$ is a datapoint with label $y_{i} = +1$,

$\vec{x}_{-}$ is a datapoint with label $y_{i} = -1$ and

      b is parameter that has to be learnt

These two lines are separated by a distance of $\frac{2}{||{\vec{w}}||}$. The line in the middle of both of these, i.e,

$$\begin{equation} \vec{w}\cdot\vec{u} + b = 0  \tag{3}\end{equation}$$

is the equation of the hyperplane denoting our decision boundary. Together, the space between (1) and (2) forms what is usually know as the **street** or the **gutter**.

equation (1) and (2) can be conveniently combined to give

$$y_{i}(\vec{w}\cdot\vec{x}_{i} + b) \geq 1\tag{4}$$

And the limiting case would be

$$y_{i}(\vec{w}\cdot\vec{x}_{i} + b) -1 = 0 \tag{5}$$

Which is attained when the points lie on the edges of the street, i.e, on (1) or (2). These points are responsible for the change in the width of the street and are called **support vectors**. Once the support vectors are calculated in the training phase we only need these vectors to classify new datapoints during the prediction phase, hence reducing the computational load significantly. Equation (4) is a constraint in the optimization process of maximizing the street width $\frac{2}{||{\vec{w}}||}$. In the next section let us see how we can combine the optimization problem and the constraints together into a single optimization equation using the concept of  lagrange multipliers.

## Lagrange Multipliers and the Primal and Dual form

Support Vector Machines are trying to solve the optimization problem of maximizing the street width $\frac{2}{||{\vec{w}}||}$ (which is equivalent to minimizing $\frac{||w||^2}{2}$) with the contraint $y_{i}(\vec{w}\cdot\vec{x}_{i} + b) \geq 1$. This can be elegantly written in a single equation with the help of [lagrange multipliers](https://en.wikipedia.org/wiki/Lagrange_multiplier). The resulting equation to be minimized is called the **primal form** (6).

**Primal form:** $$ L_{p} = \frac{||w||^2}{2} - \sum_{i}{\alpha_{i}[y_{i}(\vec{w}\cdot\vec{x}_{i} + b) -1]}\tag{6}$$

$$\frac{{\partial L}}{\partial \vec{w}} = \vec{w} - \sum_{i}{\alpha_{i}y_{i}\vec{x_{i}}}$$

equating $\frac{{\partial L}}{\partial \vec{w}}$ to 0 we get,

$$ \vec{w} = \sum_{i}{\alpha_{i}y_{i}\vec{x_{i}}}\tag{7}$$

$$\frac{{\partial L}}{\partial \vec{b}} = \sum_{i}{\alpha_{i}y_{i}}$$

and equating $\frac{{\partial L}}{\partial \vec{b}}$ to 0 we convert the primal form to the dual form,

$$\sum_{i}{\alpha_{i}y_{i}} = 0\tag{8}$$

$$L = \frac{1}{2}(\sum_{i}{\alpha_{i}y_{i}\vec{x_{i}}})(\sum_{j}{\alpha_{j}y_{j}\vec{x_{j}}}) - (\sum_{i}{\alpha_{i}y_{i}\vec{x_{i}}})(\sum_{j}{\alpha_{j}y_{j}\vec{x_{j}}}) - \sum_{i}{\alpha_{i}y_{i}b} + \sum_{i}{\alpha_{i}}$$

**Dual form:** $$L_{d} = \sum_{i}{\alpha_{i}} - \frac{1}{2}\sum_{i}\sum_{j}\alpha_{i}\alpha_{j}y_{i}y_{j}(\vec{x}_{i}\cdot\vec{x}_{j})\tag{9}$$
subject to: $$\sum_{i}{\alpha_{i}y_{i}} = 0$$

Taking a closer look at the dual form $L_{d}$ we can see that it is a function quadratic in the lagrange multipler terms which can be solved efficiently on a classical computer using [quadratic programming](https://en.wikipedia.org/wiki/Quadratic_programming) techniques. However, Note that finding the dot product $\vec{x}_{i}\cdot\vec{x}_{j}$ becomes computationally expensive as the dimension of our data increases. In the days to come we'll learn how a quantum computer could be used to classify a classical dataset using an algorithm called the Variational Quantum Classifier (VQC) Algorithm as given in [this paper](https://arxiv.org/abs/1804.11326). Understanding of Classical SVM may not be required, however, some of the concepts such as kernels and feature maps will be crucial in understanding the VQC algorithm.

## Class Prediction for a New Datapoint

The output of the training step are values of lagrange multipliers. Now, when a new datapoint $\vec z$ is given lets see how we can find the classification result corresponding to it:

* Step 1: Use the obtained values of lagrange multipliers to calculate the value of $\vec{w}$ using $(7)$.
* Step 2: Substitute the value of $\vec{w}$ in equation $(5)$ and substitute a support vector in the place of $\vec{x}_{i}$ to find the value of $b$.
* Step 3: Find the value of $\vec{w}\cdot\vec{z} + b$. If it $>0$ then assign $\vec{z}$ a label $y_{z} = 1$ and $y_{z} = -1$ if the obtained value is $< 0$.

## Classifying Linearly Separable Data

Lets switch gears and look at how we can use scikit-learn's Support Vector Classifier method to draw a decision boundary on a linearly separable dataset. This section of the notebook is a recap of resource \[4\] and thus we recommend reading it before going forward. The code used in this section is from the corresponding Github [repo](https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/05.07-Support-Vector-Machines.ipynb).

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

# use seaborn plotting defaults
import seaborn as sns; sns.set()
```

### Importing the dataset

```python
# we are importing the make_blobs dataset as it can be clearly seen to be linearly separable
from sklearn.datasets.samples_generator import make_blobs
X, y = make_blobs(n_samples=50, centers=2,
                  random_state=0, cluster_std=0.60)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn');
```

```python
from sklearn.svm import SVC # "Support vector classifier"
model = SVC(kernel='linear', C=1E10)
model.fit(X, y)
```

    SVC(C=10000000000.0, kernel='linear')

```python
# helper plotting function
def plot_svc_decision_function(model, ax=None, plot_support=True):
    if ax is None:
        ax = plt.gca()
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    # create grid to evaluate model
    x = np.linspace(xlim[0], xlim[1], 30)
    y = np.linspace(ylim[0], ylim[1], 30)
    Y, X = np.meshgrid(y, x)
    xy = np.vstack([X.ravel(), Y.ravel()]).T
    P = model.decision_function(xy).reshape(X.shape)

    # plot decision boundary and margins
    ax.contour(X, Y, P, colors='k',
               levels=[-1, 0, 1], alpha=0.5,
               linestyles=['--', '-', '--'])

    # plot support vectors
    if plot_support:
        ax.scatter(model.support_vectors_[:, 0],
                   model.support_vectors_[:, 1],
                   s=300, linewidth=1, facecolors='none', edgecolors='b');
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
```

```python
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(model);
```

As we can see SVM works quite well when it deals with linearly separable datasets. The points which lie on the dotted lines denoted by $y_{i}(\vec{w}\cdot\vec{x}_{i} + b) = \pm1$ are the **support vectors**. Part of the reason why SVMs are popular are because, during the classification step only support vectors are used to classify a new point. This reduces the computational load significantly. The reason for this is because values of the lagrange multipliers turn out to be zero for vectors which are not support vectors.

```python
model.support_vectors_
```

    array([[0.44359863, 3.11530945],
           [2.33812285, 3.43116792],
           [2.06156753, 1.96918596]])

## Dealing With Non-Linearly Separable Data

In the previous example we've seen how we can find a model to classify linearly separable data. Lets look at an example and see if SVMs can find a solution when the data is non-linear.

```python
from sklearn.datasets.samples_generator import make_circles
X, y = make_circles(100, factor=.1, noise=.1)

clf = SVC(kernel='linear').fit(X, y)

plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf, plot_support=False);
```

When the data is circular, like in the example above, SVM fails to find a satisfactory linear classification model. However, if we cleverly introduce a new parameter $r$ such that $r = e^{-x^{2}}$ and using that as our third parameter construct a new dataset (see picture below), we'll observe that a plane can be drawn horizontally passing through, say, $r=0.7$ to classify the dataset! This method in which we are mapping our dataset into a higher dimension to be able to find a linear boundary in the higher dimension is called a **feature map**.

```python
r = np.exp(-(X ** 2).sum(1))
```

```python
from mpl_toolkits import mplot3d
# from ipywidgets import interact, fixed

def plot_3D(elev=30, azim=30, X=X, y=y):
    ax = plt.subplot(projection='3d')
    ax.scatter3D(X[:, 0], X[:, 1], r, c=y, s=50, cmap='autumn')
    ax.view_init(elev=elev, azim=azim)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('r')

plot_3D()
# interact(plot_3D, elev=[-90, 90], azip=(-180, 180),
#          X=fixed(X), y=fixed(y));
```

## Feature Map and Kernel

As we have seen earlier a **feature map** maps our (non-linearly separable) input data to a higher dimensional  **feature space** where our data is now linearly separable. This helps circumvent the problem of dealing with non-linearly separable data, however, a new problem arises. As we keep increasing the dimension of our data, computing the coordinates of our data and the dot product $\phi(\vec{x}_{i})\cdot\phi(\vec{x}_{j})$ in this higher dimentional feature space becomes computationally expensive. This is where the idea of the [Kernel functions](https://en.wikipedia.org/wiki/Kernel_method) comes in.

Kernel functions allow us to deal with our data in the higher dimensional feature space (where our data is linearly separable) without ever having to compute the dot product in that space.

if $\phi(\vec{x})$ is the feature map, then the corresponding kernel function is the dot product $\phi(\vec{x}_{i})\cdot\phi(\vec{x}_{j})$, therefore, the kernel function $k$ is

$$k(x_{i},x_{j}) = \phi(\vec{x}_{i})\cdot\phi(\vec{x}_{j})$$

Therefore, the corresponding transformed optimization problem can we written as,

**Primal form:** $$ L_{p} = \frac{||w||^2}{2} - \sum_{i}{\alpha_{i}[y_{i}(\vec{w}\cdot\phi(\vec{x}_{i}) + b) -1]}\tag{6}$$

**Dual form:** $$L_{d} = \sum_{i}{\alpha_{i}} - \frac{1}{2}\sum_{i}\sum_{j}\alpha_{i}\alpha_{j}y_{i}y_{j}(\phi(\vec{x}_{i})\cdot\phi(\vec{x}_{j}))$$
or $$L_{d} = \sum_{i}{\alpha_{i}} - \frac{1}{2}\sum_{i}\sum_{j}\alpha_{i}\alpha_{j}y_{i}y_{j}k(x_{i},x_{j})$$
subject to: $$\sum_{i}{\alpha_{i}y_{i}} = 0$$
where $$ \vec{w} = \sum_{i}{\alpha_{i}y_{i}\phi(\vec{x_{i}})}$$

To understand why Kernel functions are useful lets look at an example using the Radial Basis Function (rbf) Kernel.

the rbf kernel is written as,

$$k(x_{i},x_{j}) = exp(-||x_{i} - x_{j}||^{2}/2\sigma^{2}) $$

where $\sigma$ is a tunable parameter

What we should understand here is that the rbf kernel projects our data into an infinite dimensional feature space, however, the computational power required to compute the kernel function's value is quite negligible! As you see, we don't have to compute the dot product of the infinite dimensional vectors. This is how kernels help SVMs tackle non-linearly separable data.

Rbf kernel in action:

```python
clf = SVC(kernel='rbf', C=1E6)
clf.fit(X, y)
```

    SVC(C=1000000.0)

```python
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf)
plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
            s=300, lw=1, facecolors='none');
```

In the next notebook we will learn how to use Quantum Computers to do the same task of classification and why it may be advantageous in the future.

## Additional Resources

1. Andrew NG notes: http://cs229.stanford.edu/notes/cs229-notes3.pdf
2. Andrew NG lecture: https://youtu.be/lDwow4aOrtg

Day 5/.ipynb_checkpoints/Classical Support Vector Machines-checkpoint.ipynb