# Simulating readout noise on the Rigetti Quantum Virtual Machine
© Copyright 2018, Rigetti Computing.
$$
\newcommand{ket}[1]{\left|{#1}\right\rangle}
\newcommand{bra}[1]{\left\langle {#1}\right|}
\newcommand{tr}[1]{\mathrm{Tr}\,\left[ {#1}\right]}
\newcommand{expect}[1]{\left\langle {#1} \right \rangle}
$$

## Theoretical Overview

Qubit-Readout can be corrupted in a variety of ways. The two most relevant error mechanisms on the Rigetti QPU right now are:

1. Transmission line noise that makes a 0-state look like a 1-state or vice versa. We call this **classical readout bit-flip error**. This type of readout noise can be reduced by tailoring optimal readout pulses and using superconducting, quantum limited amplifiers to amplify the readout signal before it is corrupted by classical noise at the higher temperature stages of our cryostats.
2. T1 qubit decay during readout (our readout operations can take more than a µsecond unless they have been specially optimized), which leads to readout signals that initially behave like 1-states but then collapse to something resembling a 0-state. We will call this **T1-readout error**. This type of readout error can be reduced by achieving shorter readout pulses relative to the T1 time, i.e., one can try to reduce the readout pulse length, or increase the T1 time or both.

## Qubit measurements

This section provides the necessary theoretical foundation for accurately modeling noisy quantum measurements on superconducting quantum processors. It relies on some of the abstractions (density matrices, Kraus maps) introduced in our notebook on [gate noise models](GateNoiseModels.ipynb).

The most general type of measurement performed on a single qubit at a single time can be characterized by some set $\mathcal{O}$ of measurement outcomes, e.g., in the simplest case $\mathcal{O} = \{0, 1\}$, and some unnormalized quantum channels (see notebook on gate noise models) that encapsulate
1. the probability of that outcome
2. how the qubit state is affected conditional on the measurement outcome.

Here the _outcome_ is understood as classical information that has been extracted from the quantum system.

### Projective, ideal measurement
The simplest case that is usually taught in introductory quantum mechanics and quantum information courses are Born's rule and the projection postulate which state that there exist a complete set of orthogonal projection operators
$$
P_{\mathcal{O}} := \{\Pi_x \text{ Projector }\mid x \in \mathcal{O}\},
$$
i.e., one for each measurement outcome. Any projection operator must satisfy $\Pi_x^\dagger = \Pi_x = \Pi_x^2$ and for an _orthogonal_ set of projectors any two members satisfy
$$
\Pi_x\Pi_y = \delta_{xy} \Pi_x = \begin{cases} 0 & \text{ if } x \ne y \\ \Pi_x & \text{ if } x = y \end{cases}
$$
and for a _complete_ set we additionally demand that $\sum_{x\in\mathcal{O}} \Pi_x = 1$.
Following our introduction to gate noise, we write quantum states as density matrices as this is more general and in closer correspondence with classical probability theory.

With these the probability of outcome $x$ is given by $p(x) = \tr{\Pi_x \rho \Pi_x} = \tr{\Pi_x^2 \rho} = \tr{\Pi_x \rho}$ and the post measurement state is
$$
\rho_x = \frac{1}{p(x)} \Pi_x \rho \Pi_x,
$$
which is the projection postulate applied to mixed states.

If we were a sloppy quantum programmer and accidentally erased the measurement outcome then our best guess for the post measurement state would be given by something that looks an awful lot like a Kraus map:
$$
\rho_{\text{post measurement}} = \sum_{x\in\mathcal{O}} p(x) \rho_x = \sum_{x\in\mathcal{O}} \Pi_x \rho \Pi_x.
$$
The completeness of the projector set ensures that the trace of the post measurement is still 1 and the Kraus map form of this expression ensures that $\rho_{\text{post measurement}}$ is a positive (semi-)definite operator.

### Classical readout bit-flip error

Consider now the ideal measurement as above, but where the outcome $x$ is transmitted across a noisy classical channel that produces a final outcome $x'\in \mathcal{O}' = \{0', 1'\}$ according to some conditional probabilities $p(x'|x)$ that can be recorded in the _assignment probability matrix_
$$
P_{x'|x} = \begin{pmatrix}
p(0 | 0) & p(0 | 1) \\
p(1 | 0) & p(1 | 1)
\end{pmatrix}
$$
Note that this matrix has only two independent parameters as each column must be a valid probability distribution, i.e. all elements are non-negative and each column sums to 1.

This matrix allows us to obtain the probabilities $\mathbf{p}' := (p(x'=0), p(x'=1))^T$ from the original outcome probabilities $\mathbf{p} := (p(x=0), p(x=1))^T$  via $\mathbf{p}' = P_{x'|x}\mathbf{p}$.
The difference relative to the ideal case above is that now an outcome $x' = 0$ does not necessarily imply that the post measurement state is truly $\Pi_{0} \rho \Pi_{0} / p(x=0)$. Instead, the post measurement state given a noisy outcome $x'$ must be
\begin{align}
\rho_{x'} & = \sum_{x\in \mathcal{O}} p(x|x') \rho_x \\
          & = \sum_{x\in \mathcal{O}} p(x'|x)\frac{p(x)}{p(x')} \rho_x \\
          & = \frac{1}{p(x')}\sum_{x\in \mathcal{O}} p(x'|x) \Pi_x \rho \Pi_x
\end{align}
where
\begin{align}
p(x') & = \sum_{x\in\mathcal{O}} p(x'|x) p(x)  \\
& = \tr{\sum_{x\in \mathcal{O}} p(x'|x) \Pi_x \rho \Pi_x} \\
& = \tr{\rho \sum_{x\in \mathcal{O}} p(x'|x)\Pi_x} \\
& = \tr{\rho E_x'}.
\end{align}
where we have exploited the cyclical property of the trace $\tr{ABC}=\tr{BCA}$ and the projection property $\Pi_x^2 = \Pi_x$. This has allowed us to derive the noisy outcome probabilities from a set of positive operators
$$
E_{x'} := \sum_{x\in \mathcal{O}} p(x'|x)\Pi_x \ge 0
$$
that must sum to 1:
$$
\sum_{x'\in\mathcal{O}'} E_x' = \sum_{x\in\mathcal{O}}\underbrace{\left[\sum_{x'\in\mathcal{O}'} p(x'|x)\right]}_{1}\Pi_x = \sum_{x\in\mathcal{O}}\Pi_x = 1.
$$
The above result is a type of generalized **Bayes' theorem** that is extremely useful for this type of (slightly) generalized measurement and the family of operators $\{E_{x'}| x' \in \mathcal{O}'\}$ whose expectations give the probabilities is called a **positive operator valued measure** (POVM). These operators are not generally orthogonal nor valid projection operators but they naturally arise in this scenario. This is not yet the most general type of measurement, but it will get us pretty far.

### How to model $T_1$ error
T1 type errors fall outside our framework so far as they involve a scenario in which the _quantum state itself_ is corrupted during the measurement process in a way that potentially erases the pre-measurement information as opposed to a loss of purely classical information. The most appropriate framework for describing this is given by that of measurement instruments, but for the practical purpose of arriving at a relatively simple description, we propose describing this by a T1 damping Kraus map followed by the noisy readout process as described above.

### Further reading
Chapter 3 of John Preskill's lecture notes http://www.theory.caltech.edu/people/preskill/ph229/notes/chap3.pdf

## How do I get started?

1. Come up with a good guess for your readout noise parameters $p(0|0)$ and $p(1|1)$, the off-diagonals then follow from the normalization of $P_{x'|x}$. If your assignment fidelity $F$ is given, and you assume that the classical bit flip noise is roughly symmetric, then a good approximation is to set $p(0|0)=p(1|1)=F$.
2. For your QUIL program `p`, and a qubit index `q` call:
    ```
    p.define_noisy_readout(q, p00, p11)
    ```
    where you should replace `p00` and `p11` with the assumed probabilities.

### Estimate $P_{x'|x}$ yourself!
You can also run some simple experiments to estimate the assignment probability matrix directly from a QPU.

**Scroll down for some examples!**

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

from pyquil.quil import Program, MEASURE, Pragma
from pyquil.api import QVMConnection
from pyquil.gates import I, X, RX, H, CNOT
from pyquil.noise import (estimate_bitstring_probs, correct_bitstring_probs,
                          bitstring_probs_to_z_moments, estimate_assignment_probs)

DARK_TEAL = '#48737F'
FUSCHIA = '#D6619E'
BEIGE = '#EAE8C6'

cxn = QVMConnection()
```

## Example 1: Rabi sequence with noisy readout

```python
%%time

# number of angles
num_theta = 101

# number of program executions
trials = 200

thetas = np.linspace(0, 2*np.pi, num_theta)

p00s = [1., 0.95, 0.9, 0.8]

results_rabi = np.zeros((num_theta, len(p00s)))

for jj, theta in enumerate(thetas):
    for kk, p00 in enumerate(p00s):
        cxn.random_seed = hash((jj, kk))
        p = Program(RX(theta, 0))
        ro = p.declare("ro")
        # assume symmetric noise p11 = p00
        p.define_noisy_readout(0, p00=p00, p11=p00)
        p.measure(0, ro[0])
        res = cxn.run(p, [0], trials=trials)
        results_rabi[jj, kk] = np.sum(res)

```

```python
plt.figure(figsize=(14, 6))
for jj, (p00, c) in enumerate(zip(p00s, [DARK_TEAL, FUSCHIA, "k", "gray"])):
    plt.plot(thetas, results_rabi[:, jj]/trials, c=c, label=r"$p(0|0)=p(1|1)={:g}$".format(p00))
plt.legend(loc="best")
plt.xlim(*thetas[[0,-1]])
plt.ylim(-.1, 1.1)
plt.grid(alpha=.5)
plt.xlabel(r"RX angle $\theta$ [radian]", size=16)
plt.ylabel(r"Excited state fraction $n_1/n_{\rm trials}$", size=16)
plt.title("Effect of classical readout noise on Rabi contrast.", size=18)
```

## Example 2: Estimate the assignment probabilities

### Estimate assignment probabilities for a perfect quantum computer

```python
estimate_assignment_probs(0, 1000, cxn, Program())
```

### Re-Estimate assignment probabilities for an imperfect quantum computer

```python
cxn.seed = None
header0 = Program().define_noisy_readout(0, .85, .95)
header1 = Program().define_noisy_readout(1, .8, .9)
header2 = Program().define_noisy_readout(2, .9, .85)

ap0 = estimate_assignment_probs(0, 100000, cxn, header0)
ap1 = estimate_assignment_probs(1, 100000, cxn, header1)
ap2 = estimate_assignment_probs(2, 100000, cxn, header2)
```

```python
print(ap0, ap1, ap2, sep="\n")
```

## Example 3: Use `pyquil.noise.correct_bitstring_probs` to correct for noisy readout

### 3a) Correcting the Rabi signal from above

```python
ap_last = np.array([[p00s[-1], 1 - p00s[-1]],
                    [1 - p00s[-1], p00s[-1]]])
corrected_last_result = [correct_bitstring_probs([1-p, p], [ap_last])[1] for p in results_rabi[:, -1] / trials]
```

```python
plt.figure(figsize=(14, 6))
for jj, (p00, c) in enumerate(zip(p00s, [DARK_TEAL, FUSCHIA, "k", "gray"])):
    if jj not in [0, 3]:
        continue
    plt.plot(thetas, results_rabi[:, jj]/trials, c=c, label=r"$p(0|0)=p(1|1)={:g}$".format(p00), alpha=.3)
plt.plot(thetas, corrected_last_result, c="red", label=r"Corrected $p(0|0)=p(1|1)={:g}$".format(p00s[-1]))
plt.legend(loc="best")
plt.xlim(*thetas[[0,-1]])
plt.ylim(-.1, 1.1)
plt.grid(alpha=.5)
plt.xlabel(r"RX angle $\theta$ [radian]", size=16)
plt.ylabel(r"Excited state fraction $n_1/n_{\rm trials}$", size=16)
plt.title("Corrected contrast", size=18)
```

**We find that the corrected signal is fairly noisy (and sometimes exceeds the allowed interval $[0,1]$) due to the overall very small number of samples $n=200$.**

### 3b) In this example we will create a GHZ state $\frac{1}{\sqrt{2}}\left[\left|000\right\rangle + \left|111\right\rangle \right]$ and measure its outcome probabilities with and without the above noise model. We will then see how the Pauli-Z moments that indicate the qubit correlations are corrupted (and corrected) using our API.

```python
ghz_prog = Program(H(0), CNOT(0, 1), CNOT(1, 2),
                   MEASURE(0, 0), MEASURE(1, 1), MEASURE(2, 2))
print(ghz_prog)
results = cxn.run(ghz_prog, [0, 1, 2], trials=10000)
```

```python
header = header0 + header1 + header2
noisy_ghz = header + ghz_prog
print(noisy_ghz)
noisy_results = cxn.run(noisy_ghz, [0, 1, 2], trials=10000)
```

### Uncorrupted probability for $\left|000\right\rangle$ and $\left|111\right\rangle$

```python
probs = estimate_bitstring_probs(results)
probs[0, 0, 0], probs[1, 1, 1]
```

As expected the outcomes `000` and `111` each have roughly probability $1/2$.

### Corrupted probability for $\left|000\right\rangle$ and $\left|111\right\rangle$

```python
noisy_probs = estimate_bitstring_probs(noisy_results)
noisy_probs[0, 0, 0], noisy_probs[1, 1, 1]
```

The noise-corrupted outcome probabilities deviate significantly from their ideal values!

### Corrected probability for $\left|000\right\rangle$ and $\left|111\right\rangle$

```python
corrected_probs = correct_bitstring_probs(noisy_probs, [ap0, ap1, ap2])
corrected_probs[0, 0, 0], corrected_probs[1, 1, 1]
```

The corrected outcome probabilities are much closer to the ideal value.

### Estimate $\langle Z_0^{j} Z_1^{k} Z_2^{\ell}\rangle$ for $jkl=100, 010, 001$ from non-noisy data
*We expect these to all be very small*

```python
zmoments = bitstring_probs_to_z_moments(probs)
zmoments[1, 0, 0], zmoments[0, 1, 0], zmoments[0, 0, 1]
```

### Estimate $\langle Z_0^{j} Z_1^{k} Z_2^{\ell}\rangle$ for $jkl=110, 011, 101$ from non-noisy data
*We expect these to all be close to 1.*

```python
zmoments[1, 1, 0], zmoments[0, 1, 1], zmoments[1, 0, 1]
```

### Estimate $\langle Z_0^{j} Z_1^{k} Z_2^{\ell}\rangle$ for $jkl=100, 010, 001$ from noise-corrected data

```python
zmoments_corr = bitstring_probs_to_z_moments(corrected_probs)
zmoments_corr[1, 0, 0], zmoments_corr[0, 1, 0], zmoments_corr[0, 0, 1]
```

### Estimate $\langle Z_0^{j} Z_1^{k} Z_2^{\ell}\rangle$ for $jkl=110, 011, 101$ from noise-corrected data

```python
zmoments_corr[1, 1, 0], zmoments_corr[0, 1, 1], zmoments_corr[1, 0, 1]
```

##### Overall the correction can restore the contrast in our multi-qubit observables, though we also see that the correction can lead to slightly non-physical expectations. This effect is reduced the more samples we take.


# GolemFlavor Inference

In this example, we will take the fake data generated in the `tutorial.ipynb` example and use it to make an inference the source flavor composition using Bayesian techniques.

```python
from __future__ import absolute_import, division, print_function

from functools import partial

import numpy as np
import matplotlib.pyplot as plt
```

## Define Fake Data

We will generate some fake data using a multivariate Gaussian likelihood, as described in the `tutorial.ipynb` notebook. We set our injected source composition to the pion decay model $(1:2:0)_S$ and use the global neutrino data fit mixing matrix values to calculate the expected measured composition.

```python
from golemflavor.fr import NUFIT_U
from golemflavor.fr import normalize_fr, u_to_fr

source_composition = normalize_fr((1, 0, 0))
measured_composition = u_to_fr(source_composition, NUFIT_U)
```

We also set the smearing, which represents detector related imperfections in our Gaussian likelihood:

```python
smearing = 0.02
```

Then we define the `asimov_paramset` which contains `Params` objects for each of our measured quantities:

```python
from golemflavor.fr import fr_to_angles
from golemflavor.enums import ParamTag
from golemflavor.param import Param, ParamSet

# Convert from flavor composition to flavor angles
measured_flavor_angles = fr_to_angles(measured_composition)

# Parameters can be tagged for later convenience
tag = ParamTag.BESTFIT

# Define the asimov `ParamSet`, with `Param` objects containing information such as name, value and ranges.
asimov_paramset = [
    Param(name='measured_angle1', value=measured_flavor_angles[0], ranges=[ 0., 1.], std=smearing, tag=tag, tex=r'\sin^4\phi_\oplus'),
    Param(name='measured_angle2', value=measured_flavor_angles[1], ranges=[-1., 1.], std=smearing, tag=tag, tex=r'\cos(2\psi_\oplus)')
]
asimov_paramset = ParamSet(asimov_paramset)
```

## Physics Model

The goal here is to make an inference of the source flavor composition from our fake data measurement. In order to do this, we need a model that links the two. Of course, in this case we will use the neutrino mixing model that we generated the fake data with, but it is worth mentioning that with real data we sometimes do not have this luxury. Model dependence is a heavily debated topic in physics. That isn't to say that having model dependence is a bad thing, indeed the best physicists are ones which do make model assumptions but can however justify their simplifications to the wider community.

As a reminder from `tutorial.ipynb`, the measured flavor composition can be written as a function of the source flavor composition and mixing matrix:

$$ \phi_{\alpha,\oplus}=\sum_{i,\beta} \mid{U_{\alpha i}}\mid^2\mid{U_{\beta i}}\mid^2\phi_{\beta,\text{S}} $$

So here we must sample over the source flavor compositions to see which agrees best with the data. However, we must also take into account that the values of the mixing matrix are perfect and have uncertainties of their own.

### Nuisance Parameters

Not all parameters of a model are of direct inferential interest, however they still need to be included as they may reduce the effect of systematic bias. These are called *nuisance parameters*. Here the mixing matrix parameters are examples of such nuisance parameters and we must include the effect of their uncertainties in our inference of the source flavor composition.

#### Anarchic Sampling

In the same way that flavor compositions cannot directly be sampled over, the mixing matrix values also cannot directly be sampled (it's also **extremely** inefficient to brute force sample values in a matrix such that the resulting matrix is unitary). As with any Bayesian inference, the prior distribution needs to be chosen carefully. Firstly we can rewrite the $3\times3$ unitary mixing matrix in terms of *mixing angles*. The following is the standard representation most commonly used:

$$
\begin{align}
  U=
  \begin{pmatrix}
    1    & 0       & 0      \\
    0    & c_{23}  & s_{23} \\
    0    & -s_{23} & c_{23} \\
  \end{pmatrix}
  \begin{pmatrix}
    c_{13}             & 0 & s_{13}e^{-i\delta} \\
    0                  & 1 & 0                  \\
    -s_{13}e^{i\delta} & 0 &c_{13}              \\
  \end{pmatrix}
  \begin{pmatrix}
    c_{12}  & s_{12} & 0 \\
    -s_{12} & c_{12} & 0 \\
    0       & 0      & 1 \\
  \end{pmatrix}
\end{align}
$$

where $s_{ij}\equiv\sin\theta_{ij}$, $c_{ij}\equiv\cos\theta_{ij}$, $\theta_{ij}$ are the three mixing angles and $\delta$ is the CP violating phase. Overall phases in the mixing matrix do not affect neutrino oscillations, which only depend on quartic products, and so they have been omitted.

Now we have an efficient way of generating $3\times3$ unitary matrices we can focus on how to define the prior space which we sample from. As we did for the flavor angles, we must do so under the integration invariant [*Haar measure*](https://doi.org/10.1016/j.physletb.2003.08.045). For the group $U(3)$, the Haar measure is given by the volume element $\text{d}U$, which can be written in terms of the above mixing angles:

$$
\begin{align}
    \text{d} U=\text{d}\left(\sin^2\theta_{12}\right)\wedge\,
        \text{d}\left(\cos^4\theta_{13}\right)\wedge\,
        \text{d}\left(\sin^2\theta_{23}\right)\wedge\,\text{d}\delta
\end{align}
$$

which says that the Haar measure for the group $U(3)$ is flat in $\sin^2\theta_{12}$, $\cos^4\theta_{13}$, $\sin^2\theta_{23}$ and $\delta$. Therefore, in order to ensure the distribution over the mixing matrix $U$ is unbiased, the prior space of the mixing angles must be chosen according to this Haar measure, i.e. in $\sin^2\theta_{12}$, $\cos^4\theta_{13}$, $\sin^2\theta_{23}$ and $\delta$.

Of course, GolemFlavor provides the handy function to be able to do this conversion `angles_to_u`.

```python
from golemflavor.fr import angles_to_u
help(angles_to_u)
```

    Help on function angles_to_u in module golemflavor.fr:

    angles_to_u(bsm_angles)
        Convert angular projection of the mixing matrix elements back into the
        mixing matrix elements.

        Parameters
        ----------
        bsm_angles : list, length = 4
            sin(12)^2, cos(13)^4, sin(23)^2 and deltacp

        Returns
        ----------
        unitary numpy ndarray of shape (3, 3)

        Examples
        ----------
        >>> from fr import angles_to_u
        >>> print(angles_to_u((0.2, 0.3, 0.5, 1.5)))
        array([[ 0.66195018+0.j        ,  0.33097509+0.j        ,  0.04757188-0.6708311j ],
               [-0.34631487-0.42427084j,  0.61741198-0.21213542j,  0.52331757+0.j        ],
               [ 0.28614067-0.42427084j, -0.64749908-0.21213542j,  0.52331757+0.j        ]])

#### Gaussian Priors

The [global fit to world neutrino data](<https://doi.org/10.1007/JHEP01(2017)087>) includes estimates of the uncertainty of each mixing angle. These uncertainties can be included as an extra Gaussian prior in our likelihood by specifying the `prior` keyword when defining the `Param`, with the `std` keyword being the one standard deviation from the central value.

```python
from golemflavor.enums import PriorsCateg

# Params can be tagged for later convenience
tag = ParamTag.SM_ANGLES

# Include with a Limited Gaussian prior, which is a Gaussian adjusted for boundaries defined by `ranges`
lg_prior = PriorsCateg.LIMITEDGAUSS

# Define the nuisance `Param` objects containing information such as name, value, ranges, prior and std.
nuisance = [
    Param(name='s_12_2', value=0.307,            seed=[0.26, 0.35],   ranges=[0., 1.],      std=0.013,   tex=r's_{12}^2', prior=lg_prior,  tag=tag),
    Param(name='c_13_4', value=(1-(0.02206))**2, seed=[0.950, 0.961], ranges=[0., 1.],      std=0.00147, tex=r'c_{13}^4', prior=lg_prior,  tag=tag),
    Param(name='s_23_2', value=0.538,            seed=[0.31, 0.75],   ranges=[0., 1.],      std=0.069,   tex=r's_{23}^2', prior=lg_prior,  tag=tag),
    Param(name='dcp',    value=4.08404,          seed=[0, 2*np.pi],   ranges=[0., 2*np.pi], std=2.0,     tex=r'\delta_{CP}', tag=tag),
]

# Define the source flavor angles `Param` objects
tag = ParamTag.SRCANGLES
src_compositions = [
    Param(name='source_angle1', value=0, ranges=[ 0., 1.], tag=tag, tex=r'\sin^4\phi_S'),
    Param(name='source_angle2', value=0, ranges=[-1., 1.], tag=tag, tex=r'\cos(2\psi_S)')
]

# Define the llh `ParamSet`, containing the nuisance parameters plus our parameter of interest
llh_paramset = ParamSet(nuisance + src_compositions)
```

As a reminder, we have 2 `ParamSet` objects:
* `asimov_paramset` contains the measured parameters
* `llh_paramset` contains the model parameter values

## Markov Chain Monte Carlo

Now, we wrap our physics model along with the `multi_gaussian` likelihood into a function that accepts input parameters `theta` from the MCMC:

```python
from golemflavor.fr import angles_to_fr
from golemflavor.llh import multi_gaussian

def triangle_llh(theta, asimov_paramset, llh_paramset):
    """Log likelihood function for a given theta."""
    if len(theta) != len(llh_paramset):
        raise AssertionError(
            'Length of MCMC scan is not the same as the input '
            'params\ntheta={0}\nparamset]{1}'.format(theta, llh_paramset)
        )

    # Set llh_parameters values to the sampled parameters
    for idx, param in enumerate(llh_paramset):
        param.value = theta[idx]

    # Convert sampled mixing angles to a mixing matrix
    sm_angles = llh_paramset.from_tag(ParamTag.SM_ANGLES, values=True)
    sm_u = angles_to_u(sm_angles)

    # Convert flavor angles to flavor compositions for the model parameters
    source_angles = llh_paramset.from_tag(ParamTag.SRCANGLES, values=True)
    source_composition = angles_to_fr(source_angles)

    # Calculate the expected measured flavor composition for our sampled values
    measured_composition = u_to_fr(source_composition, sm_u)

    # Convert flavor angles to flavor compositions for the injected parameters
    bestfit_measured_comp = angles_to_fr(asimov_paramset.from_tag(ParamTag.BESTFIT, values=True))

    # Get the value of `smearing`
    smearing = asimov_paramset['measured_angle1'].std

    # Calculate the log likelihood using `multi_gaussian`
    llh = multi_gaussian(measured_composition, bestfit_measured_comp, smearing)
    return llh
```

    Running without GolemFit

Last thing we need to setup is our prior distribution, which in this case includes both the bounds on our model parameters, as well as the extra priors for the mixing angle parameters. As we have defined this already in the `ParamSet` object using the `prior`, `std` and `ranges` keyword, we can use the GolemFlavor function `lnprior` to do the work for us:

```python
from golemflavor.llh import lnprior

def ln_prob(theta, asimov_paramset, llh_paramset):
    """Posterior function for a given theta."""
    # Get the value of the log prior (prior from mixing matrix Params is calculated here)
    lp = lnprior(theta, paramset=llh_paramset)
    if not np.isfinite(lp):
        return -np.inf

    # Return the log prior + log likelihood
    return lp + triangle_llh(theta, asimov_paramset, llh_paramset)

# Evalaute the posterior using the defined `asimov_paramset` and `llh_paramset`
ln_prob_eval = partial(
    ln_prob,
    asimov_paramset=asimov_paramset,
    llh_paramset=llh_paramset
)
```

```python
import golemflavor.mcmc as mcmc_utils

# Reduce these values for a quicker runtime
nwalkers = 60
burnin = 1000
nsteps = 10000

# Generate initial seed using a flat distribution
p0 = mcmc_utils.flat_seed(
    llh_paramset, nwalkers=nwalkers
)

# Run the MCMC!
# Progress bar provided by tqdm (this took about 30mins on my laptop)
samples = mcmc_utils.mcmc(
    p0       = p0,
    ln_prob  = ln_prob_eval,
    ndim     = len(llh_paramset),
    nwalkers = nwalkers,
    burnin   = burnin,
    nsteps   = nsteps,
    threads  = 4
)
```

    Running burn-in

    HBox(children=(FloatProgress(value=0.0, max=1000.0), HTML(value='')))

    Finished burn-in
    Running

    HBox(children=(FloatProgress(value=0.0, max=10000.0), HTML(value='')))

    Finished
    acceptance fraction [0.4288 0.4305 0.4342 0.4181 0.4278 0.4304 0.4294 0.4315 0.4169 0.4133
     0.428  0.4408 0.4145 0.4163 0.4364 0.4226 0.4222 0.4071 0.4179 0.4299
     0.4419 0.4208 0.431  0.418  0.4301 0.4332 0.4291 0.415  0.4194 0.431
     0.4299 0.4429 0.4419 0.4216 0.4281 0.4245 0.4136 0.4278 0.4275 0.4275
     0.4167 0.4266 0.4308 0.4174 0.4339 0.4245 0.4331 0.4383 0.4129 0.4408
     0.4221 0.4331 0.4229 0.4316 0.428  0.4263 0.433  0.4309 0.4136 0.4331]
    sum of acceptance fraction 25.601
    np.unique(samples[:,0]).shape (256044,)
    autocorrelation [ 84.76795567  90.34226183 101.72273776 124.98444783 103.45178419
     101.3873447 ]

## Visualization

One of the great advantages of Bayesian inferences is the access we have to the full posterior distributions. We can visualize the relationships between our model parameters by plotting the joint posterior distributions, as is done here using the [getdist package](https://getdist.readthedocs.io/en/latest/).

```python
import golemflavor.plot as plot_utils
# getdist package requires `%matplotlib inline` to come after the import for inline notebook figures.
%matplotlib inline

plot_utils.plot_Tchain(samples, llh_paramset.labels, llh_paramset.ranges, llh_paramset.names)
plt.show()
```

Here, the non-diagonal plots show joint distributions between two parameters, labelled on the x- and y-axis and the diagonal plots show the marginalised distributions for each parameter, as labelled on the x-axis. The blue (light blue) shows the 90% (99%) credibility intervals.

As we did in the previous example, we can also see how this looks in a ternary plot.

```python
source_angles = samples[:,-2:]
source_compositions = np.array(
    list(map(angles_to_fr, source_angles))
)
```

```python
nbins = 25
fontsize = 23

# Figure
fig = plt.figure(figsize=(12, 12))

# Axis
ax = fig.add_subplot(111)
ax_labels = [
    r'$\nu_e\:\:{\rm fraction}\:\left( f_{e,S}\right)$',
    r'$\nu_\mu\:\:{\rm fraction}\:\left( f_{\mu,S}\right)$',
    r'$\nu_\tau\:\:{\rm fraction}\:\left( f_{\tau,S}\right)$'
]
tax = plot_utils.get_tax(ax, scale=nbins, ax_labels=ax_labels, rot_ax_labels=True)

# Plot source composition posteriors
coverages = [(99, 'cornflowerblue'), (90, 'royalblue')]
for cov, color in coverages:
    plot_utils.flavor_contour(
        frs=source_compositions,
        fill=True,
        ax=ax,
        nbins=nbins,
        coverage=cov,
        linewidth=2.5,
        color=color,
        alpha=0.7,
        oversample=5
    )

plt.show()
```

Great! Looks like our inference of the source flavor composition reflects the injected value $(1:0:0)_S$. Here, the credbility regions include the effect of smearing as well as our uncertainity about the values of the mixing matrix, which is why the values are not exactly at the injected $(1:0:0)_S$ value.

In a real analysis, an ensemble of nuisance parameters is usually required, related to uncertainties arising from things such as the astrophysical flux, detector calibration and backgrounds from atmospherically produced neutrinos. All these effects come into play when making inferences and careful analysis must be done for each in order to minimize potential biases.


# $H_{\rm Orb, NS}$, up to and including third post-Newtonian order

## This notebook constructs the orbital, nonspinning Hamiltonian up to third post-Newtonian order, as summarized in [Buonanno, Chen, and Damour (2006)](https://arxiv.org/abs/gr-qc/0508067) (see references therein for sources)

**Notebook Status:**  Validated

**Validation Notes:** All expressions in this notebook were transcribed twice by hand on separate occasions, and expressions were corrected as needed to ensure consistency with published PN expressions. In addition, this tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). **Additional validation tests may have been performed, but are as yet, undocumented.**

## Author: Zach Etienne

### This notebook exists as the following Python module:
1. [PN_Hamiltonian_NS.py](../../edit/NRPyPN/PN_Hamiltonian_NS.py)

### This notebook & corresponding Python module depend on the following NRPy+/NRPyPN Python modules:
1. [indexedexp.py](../../edit/indexedexp.py): [**documentation+tutorial**](../Tutorial-Indexed_Expressions.ipynb)
1. [NRPyPN_shortcuts.py](../../edit/NRPyPN/NRPyPN_shortcuts.py): [**documentation**](NRPyPN_shortcuts.ipynb)

# Table of Contents
$$\label{toc}$$

1. Part 1: [$H_{\rm Orb,\ NS,\ Newt}+H_{\rm Orb,\ NS,\ 1PN}+H_{\rm Orb,\ NS,\ 2PN}$](#uptotwopn): Contributions up to and including second post-Newtonian order
1. Part 2: [$H_{\rm Orb,\ NS,\ 3PN}$](#threepn): Third post-Newtonian contribution
1. Part 3: [Validation against second transcription and corresponding Python module](#code_validation)
1. Part 4: [LaTeX PDF output](#latex_pdf_output): $\LaTeX$ PDF Output

# Part 1: $H_{\rm Orb,\ NS,\ Newt}+H_{\rm Orb,\ NS,\ 1PN}+H_{\rm Orb,\ NS,\ 2PN}$ \[Back to [top](#toc)\]
$$\label{uptotwopn}$$

As detailed in [Buonanno, Chen, and Damour (2006)](https://arxiv.org/abs/gr-qc/0508067) (henceforth BCD2006),
this model assumes two point masses of mass $m_1$ and $m_2$ with corresponding momentum vectors $\mathbf{P}_1$ and $\mathbf{P}_2$, and displacement vectors $\mathbf{X}_1$ and $\mathbf{X}_2$ with respect to the center of mass.

Following [BCD2006](https://arxiv.org/abs/gr-qc/0508067), we define the following quantities
\begin{align}
\mu &= m_1 m_2 / (m_1+m_2)\\
\eta &= m_1 m_2 / (m_1+m_2)^2\\
\mathbf{p} &= \mathbf{P}_1/\mu = -\mathbf{P}_2/\mu\\
\mathbf{q} &= (\mathbf{X}_1-\mathbf{X}_2)/M\\
q &= |\mathbf{q}|\\
\mathbf{n} &= \frac{\mathbf{q}}{q}
\end{align}

Then the Hamiltonian up to and including second PN order is given by (to reduce possibility of copying error, these equations are taken directly from Eqs 2.2-4 of the LaTeX source code of [BCD2006](https://arxiv.org/abs/gr-qc/0508067), and only mildly formatted to (1) improve presentation in Jupyter notebooks and (2) to ensure some degree of consistency in notation across different terms in other Hamiltonian notebooks):
\begin{align}
H_{\rm Newt}\left({\bf q},{\bf p}\right) &= \mu \left[\frac{{\bf p}^2}{2} -
\frac{1}{q}\right]\,, \\
H_{\rm 1PN}\left({\bf q},{\bf p}\right) &= \mu\left[\frac{1}{8}(3\eta-1)({\bf p}^2)^2
- \frac{1}{2}\left[(3+\eta){\bf p}^2+\eta({\bf n}\cdot{\bf p})^2\right]\frac{1}{q} + \frac{1}{2q^2}\right]\,,\\
H_{\rm 2PN}\left({\bf q},{\bf p}\right)
&= \mu\left[\frac{1}{16}\left(1-5\eta+5\eta^2\right)({\bf p}^2)^3
+ \frac{1}{8} \left[
\left(5-20\eta-3\eta^2\right)({\bf p}^2)^2-2\eta^2({\bf n}\cdot{\bf p})^2{\bf p}^2-3\eta^2({\bf n}\cdot{\bf p})^4 \right]\frac{1}{q}\right.
\\
&\quad\quad\quad \left.+ \frac{1}{2} \left[(5+8\eta){\bf p}^2+3\eta({\bf n}\cdot{\bf p})^2\right]\frac{1}{q^2}
- \frac{1}{4}(1+3\eta)\frac{1}{q^3}\right]\,,
\end{align}

```python
# Step 0: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import os,sys                    # Standard Python modules for multiplatform OS-level functions
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)
import sympy as sp                   # SymPy: The Python computer algebra package upon which NRPy+ depends
import indexedexp as ixp             # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
from NRPyPN_shortcuts import div,dot # NRPyPN: shortcuts for e.g., vector operations

def f_H_Newt__H_NS_1PN__H_NS_2PN(m1,m2, PU, nU, q):
    mu  = m1*m2 / (m1+m2)
    eta = m1*m2 / (m1+m2)**2
    pU = ixp.zerorank1()
    for i in range(3):
        pU[i] = PU[i]/mu

    global H_Newt, H_NS_1PN, H_NS_2PN
    H_Newt   = mu*(+div(1,2)*dot(pU,pU) - 1/q)

    H_NS_1PN = mu*(+div(1,8)*(3*eta-1)*dot(pU,pU)**2
                   -div(1,2)*((3+eta)*dot(pU,pU) + eta*dot(nU,pU)**2)/q
                   +div(1,2)/q**2)

    H_NS_2PN = mu*(+div(1,16)*(1 -  5*eta + 5*eta**2)*dot(pU,pU)**3
                   +div(1,8)*(+(5 - 20*eta - 3*eta**2)*dot(pU,pU)**2
                              -2*eta**2*dot(nU,pU)**2*dot(pU,pU)
                              -3*eta**2*dot(nU,pU)**4)/q
                   +div(1,2)*((5+8*eta)*dot(pU,pU) + 3*eta*dot(nU,pU)**2)/q**2
                   -div(1,4)*(1+3*eta)/q**3)
```

```python
# Second version. This one was mostly a search+replace version
#                 of the original TeX'ed up equations in the paper.
# Used for validation purposes only.
def f_H_Newt__H_NS_1PN__H_NS_2PNv2(m1,m2, PU, nU, q):
    mu = m1*m2/(m1+m2)
    eta = m1*m2/(m1+m2)**2
    pU = ixp.zerorank1()
    for i in range(3):
        pU[i] = PU[i]/mu
    p_dot_p = dot(pU,pU)
    n_dot_p = dot(nU,pU)

    # H_{\rm Newt} = \frac{p^i p^i}{2} - \frac{1}{q}
    global H_Newtv2, H_NS_1PNv2, H_NS_2PNv2
    H_Newtv2    = mu*(div(1,2)*p_dot_p - 1/q)
    H_NS_1PNv2  = mu*(div(1,8)*(3*eta-1)*p_dot_p**2 - \
                   div(1,2)*((3+eta)*p_dot_p + eta*n_dot_p**2)/q + 1/(2*q**2))
    H_NS_2PNv2  = mu*(div(1,16)*(1 - 5*eta + 5*eta**2)*p_dot_p**3 +
                   div(1,8)*((5 - 20*eta - 3*eta**2)*p_dot_p**2
                             - 2*eta**2*n_dot_p**2*p_dot_p - 3*eta**2*n_dot_p**4)/q +
                   div(1,2)*((5 + 8*eta)*p_dot_p + 3*eta*n_dot_p**2)/q**2 -
                   div(1,4)*(1 + 3*eta)/q**3)
```

# Part 2: $H_{\rm Orb,\ NS,\ 3PN}$ Third post-Newtonian contribution \[Back to [top](#toc)\]
$$\label{threepn}$$

To reduce possibility of copying error, equations are taken directly from the LaTeX source code of Eqs 2.2-4 in [BCD2006](https://arxiv.org/abs/gr-qc/0508067), and only mildly formatted to (1) improve presentation in Jupyter notebooks and (2) to ensure some degree of consistency in notation across different terms in other Hamiltonian notebooks:

\begin{align}
H_{\rm 3PN}\left({\bf q},{\bf p}\right)
&= \mu\left\{\frac{1}{128}\left(-5+35\eta-70\eta^2+35\eta^3\right)({\bf p}^2)^4\right.
 \\
&\quad\quad + \frac{1}{16}\left[
\left(-7+42\eta-53\eta^2-5\eta^3\right)({\bf p}^2)^3
+ (2-3\eta)\eta^2({\bf n}\cdot{\bf p})^2({\bf p}^2)^2
+ 3(1-\eta)\eta^2({\bf n}\cdot{\bf p})^4{\bf p}^2 - 5\eta^3({\bf n}\cdot{\bf p})^6
\right]\frac{1}{q}
 \\
&\quad\quad +\left[ \frac{1}{16}\left(-27+136\eta+109\eta^2\right)({\bf p}^2)^2
+ \frac{1}{16}(17+30\eta)\eta({\bf n}\cdot{\bf p})^2{\bf p}^2 + \frac{1}{12}(5+43\eta)\eta({\bf n}\cdot{\bf p})^4
\right]\frac{1}{q^2} \\
&\quad\quad +\left\{ \left[ -\frac{25}{8} + \left(\frac{1}{64}\pi^2-\frac{335}{48}\right)\eta
- \frac{23}{8}\eta^2 \right]{\bf p}^2
+ \left(-\frac{85}{16}-\frac{3}{64}\pi^2-\frac{7}{4}\eta\right)\eta({\bf n}\cdot{\bf p})^2
\right\}\frac{1}{q^3}
\\
&\quad\quad\left. + \left[ \frac{1}{8} + \left(\frac{109}{12}-\frac{21}{32}\pi^2\right)\eta
\right]\frac{1}{q^4}\right\}\,,
\end{align}

```python
def f_H_NS_3PN(m1,m2, PU, nU, q):
    mu  = m1*m2 / (m1+m2)
    eta = m1*m2 / (m1+m2)**2
    pU = ixp.zerorank1()
    for i in range(3):
        pU[i] = PU[i]/mu

    global H_NS_3PN
    H_NS_3PN = mu*(+div(1,128)*(-5 + 35*eta - 70*eta**2 + 35*eta**3)*dot(pU,pU)**4
                   +div(1, 16)*(+(-7 + 42*eta - 53*eta**2 -  5*eta**3)*dot(pU,pU)**3
                                +(2-3*eta)*eta**2*dot(nU,pU)**2*dot(pU,pU)**2
                                +3*(1-eta)*eta**2*dot(nU,pU)**4*dot(pU,pU) - 5*eta**3*dot(nU,pU)**6)/q
                   +(+div(1,16)*(-27 + 136*eta + 109*eta**2)*dot(pU,pU)**2
                     +div(1,16)*(+17 +  30*eta)*eta*dot(nU,pU)**2*dot(pU,pU)
                     +div(1,12)*(+ 5 +  43*eta)*eta*dot(nU,pU)**4)/q**2
                   +(+(-div(25, 8) + (div(1,64)*sp.pi**2 - div(335,48))*eta - div(23,8)*eta**2)*dot(pU,pU)
                     +(-div(85,16) - div(3,64)*sp.pi**2 - div(7,4)*eta)*eta*dot(nU,pU)**2)/q**3
                   +(+div(1,8)+(div(109,12) - div(21,32)*sp.pi**2)*eta)/q**4)
```

```python
# Second version. This one was mostly a search+replace version
#                 of the original TeX'ed up equations in the paper.
# Used for validation purposes only.
def f_H_NS_3PNv2(m1,m2, pU, nU, q):
    mu  = m1*m2/(m1+m2)
    eta = m1*m2/(m1+m2)**2
    PU = ixp.zerorank1()
    for i in range(3):
        PU[i] = pU[i]/mu
    P_dot_P = dot(PU,PU)
    n_dot_P = dot(nU,PU)

    global H_NS_3PNv2
    # The following is simply by-hand search/replaced from the above LaTeX to minimize error
    H_NS_3PNv2 = \
    mu*( div(1,128)*(-5+35*eta-70*eta**2+35*eta**3)*P_dot_P**4 +
         div(1,16)* ( (-7+42*eta-53*eta**2-5*eta**3)*P_dot_P**3
                     +(2-3*eta)*eta**2*n_dot_P**2*P_dot_P**2 +
                     +3*(1-eta)*eta**2*n_dot_P**4*P_dot_P - 5*eta**3*n_dot_P**6 )/(q) +
 (  div(1,16)*(-27+136*eta+109*eta**2)*P_dot_P**2
 + div(1,16)*(17+30*eta)*eta*n_dot_P**2*P_dot_P + div(1,12)*(5+43*eta)*eta*n_dot_P**4)/(q**2) +
 ( ( -div(25,8) + (div(1,64)*sp.pi**2-div(335,48))*eta
 - div(23,8)*eta**2 )*P_dot_P
 + (-div(85,16)-div(3,64)*sp.pi**2-div(7,4)*eta)*eta*n_dot_P**2)/(q**3) +
 ( div(1,8) + (div(109,12)-div(21,32)*sp.pi**2)*eta)/(q**4) )
```

# Part 3: Validation against second transcription and corresponding Python module \[Back to [top](#toc)\]
$$\label{code_validation}$$

As a code validation check, we verify agreement between
* the SymPy expressions transcribed from the cited published work on two separate occasions, and
* the SymPy expressions generated in this notebook, and the corresponding Python module.

```python
from NRPyPN_shortcuts import m1,m2,pU,nU,q # NRPyPN: import needed input variables.

f_H_Newt__H_NS_1PN__H_NS_2PN(m1,m2, pU, nU, q)
f_H_NS_3PN(m1,m2, pU, nU, q)

def error(varname):
    print("ERROR: When comparing Python module & notebook, "+varname+" was found not to match.")
    sys.exit(1)

# Validation against second transcription of the expressions.
f_H_Newt__H_NS_1PN__H_NS_2PNv2(m1,m2, pU, nU, q)
f_H_NS_3PNv2(m1,m2, pU, nU, q)
if sp.simplify(H_Newt   - H_Newtv2)   != 0: error("H_Newtv2")
if sp.simplify(H_NS_1PN - H_NS_1PNv2) != 0: error("H_NS_1PNv2")
if sp.simplify(H_NS_2PN - H_NS_2PNv2) != 0: error("H_NS_2PNv2")
if sp.simplify(H_NS_3PN - H_NS_3PNv2) != 0: error("H_NS_3PNv2")

# Validation against corresponding Python module
import PN_Hamiltonian_NS as HNS
HNS.f_H_Newt__H_NS_1PN__H_NS_2PN(m1,m2, pU, nU, q)
HNS.f_H_NS_3PN(m1,m2, pU, nU, q)
if sp.simplify(H_Newt   - HNS.H_Newt)   != 0: error("H_Newt")
if sp.simplify(H_NS_1PN - HNS.H_NS_1PN) != 0: error("H_NS_1PN")
if sp.simplify(H_NS_2PN - HNS.H_NS_2PN) != 0: error("H_NS_2PN")
if sp.simplify(H_NS_3PN - HNS.H_NS_3PN) != 0: error("H_NS_3PN")

print("ALL TESTS PASS")
```

    ALL TESTS PASS

# Step 4: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[PN-Hamiltonian-Nonspinning.pdf](PN-Hamiltonian-Nonspinning.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import os,sys                    # Standard Python modules for multiplatform OS-level functions
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("PN-Hamiltonian-Nonspinning",location_of_template_file=os.path.join(".."))
```

    Created PN-Hamiltonian-Nonspinning.tex, and compiled LaTeX file to PDF file
        PN-Hamiltonian-Nonspinning.pdf


## 1-2. 量子ビットに対する基本演算

量子ビットについて理解が深まったところで、次に量子ビットに対する演算がどのように表されるかについて見ていこう。
これには、量子力学の性質が深く関わっている。

1. 線型性：
詳しくは第4章で学ぶのだが、量子力学では状態（量子ビット）の時間変化はつねに（状態の重ね合わせに対して）線型になっている。つまり、**量子コンピュータ上で許された操作は状態ベクトルに対する線型変換**ということになる
。１つの量子ビットの量子状態は規格化された２次元複素ベクトルとして表現されるのだったから、
１つの量子ビットに対する操作＝線型演算は$2 \times 2$の**複素行列**によって表現される。

2. ユニタリ性：
さらに、確率の合計は常に1であるという規格化条件から、量子操作に表す線形演算（量子演算）に対してさらなる制限を導くことができる。まず、各測定結果を得る確率は複素確率振幅の絶対値の2乗で与えられるので、その合計は状態ベクトルの（自分自身との）内積と一致することに注目する:

$$
|\alpha|^2 + |\beta|^2 =
(\alpha^*, \beta^*)
\left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right) = 1.
$$

（アスタリスク $^*$ は複素共役を表す）
 量子コンピュータで操作した後の状態は、量子演算に対応する線形変換(行列)を$U$とすると、

$$
U
\left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right)
$$

と書ける。この状態についても上記の規格化条件が成り立つ必要があるので、

$$
(\alpha^*, \beta^*)
U^\dagger U
\left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right) = 1
$$

が要請される。（ダガー $^\dagger$ は行列の転置と複素共役を両方適用したものを表し、エルミート共役という）

 この関係式が任意の$\alpha$, $\beta$について成り立つ必要があるので、量子演算$U$は以下の条件を満たす**ユニタリー行列**に対応する:
$$
U^{\dagger} U = U U^{\dagger} = I.
$$

すなわち、**量子ビットに対する操作は、ユニタリー行列で表される**のである。

ここで、用語を整理しておく。量子力学では、状態ベクトルに対する線形変換のことを**演算子** (operator) と呼ぶ。単に演算子という場合は、ユニタリーとは限らない任意の線形変換を指す。それに対して、上記のユニタリー性を満たす線形変換のことを**量子演算** (quantum gate) と呼ぶ。量子演算は、量子状態に対する演算子のうち、（少なくとも理論的には）**物理的に実現可能なもの**と考えることができる。

### １量子ビット演算の例：パウリ演算子
１つの量子ビットに作用する基本的な量子演算として**パウリ演算子**を導入する。
これは量子コンピュータを学んでいく上で最も重要な演算子であるので、定義を体に染み込ませておこう。

$$
\begin{eqnarray}
I&=&
\left(\begin{array}{cc}
1 & 0
\\
0 & 1
\end{array}
\right),\;\;\;
X=
\left(\begin{array}{cc}
0 & 1
\\
1 & 0
\end{array}
\right),\;\;\;
Y &=&
\left(\begin{array}{cc}
0 & -i
\\
i & 0
\end{array}
\right),\;\;\;
Z=
\left(\begin{array}{cc}
1 & 0
\\
0 & -1
\end{array}
\right).
\end{eqnarray}
$$

各演算子のイメージを説明する。

まず、$I$は恒等演算子で、要するに「何もしない」ことを表す。

$X$は古典ビットの反転（NOT）に対応し

$$X|0\rangle = |1\rangle, \;\;
X|1\rangle = |0\rangle
$$

のように作用する。（※ブラケット表記を用いた。下記コラムも参照。）

$Z$演算子は$|0\rangle$と$|1\rangle$の位相を反転させる操作で、

$$
Z|0\rangle = |0\rangle, \;\;
Z|1\rangle = -|1\rangle
$$

と作用する。
これは$|0\rangle$と$|1\rangle$の重ね合わせの「位相」という情報を保持できる量子コンピュータ特有の演算である。
例えば、

$$
Z \frac{1}{\sqrt{2}} ( |0\rangle + |1\rangle ) = \frac{1}{\sqrt{2}} ( |0\rangle - |1\rangle )
$$

となる。

$Y$演算子は$Y=iXZ$と書け、
位相の反転とビットの反転を組み合わせたもの（全体にかかる複素数$i$を除いて）であると考えることができる。

(詳細は Nielsen-Chuang の `1.3.1 Single qubit gates` を参照)

### SymPyを用いた一量子ビット演算
SymPyではよく使う基本演算はあらかじめ定義されている。

```python
from IPython.display import Image, display_png
from sympy import *
from sympy.physics.quantum import *
from sympy.physics.quantum.qubit import Qubit,QubitBra
init_printing() # ベクトルや行列を綺麗に表示するため
```

```python
# Google Colaboratory上でのみ実行してください
from IPython.display import HTML
def setup_mathjax():
    display(HTML('''

    '''))
get_ipython().events.register('pre_run_cell', setup_mathjax)
```

```python
from sympy.physics.quantum.gate import X,Y,Z,H,S,T,CNOT,SWAP, CPHASE
```

演算子は何番目の量子ビットに作用するか、
というのを指定して `X(0)` のように定義する。
また、これを行列表示するときには、いくつの量子ビットの空間で表現するか
`nqubits`というのを指定する必要がある。
まだ、量子ビットは１つしかいないので、
`X(0)`、`nqubits=1`としておこう。

```python
X(0)
```

```python
represent(X(0),nqubits=1)  # パウリX
```

$$\left[\begin{matrix}0 & 1\\1 & 0\end{matrix}\right]$$

同様に、`Y`, `Z`なども利用することができる。それに加え、アダマール演算 `H` や、位相演算 `S`、そして$\pi/4$の位相演算 `T` も利用することができる（これらもよく出てくる演算で、定義は各行列を見てほしい）：

```python
represent(H(0),nqubits=1)
```

$$\left[\begin{matrix}\frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}}\\\frac{1}{\sqrt{2}} & - \frac{\sqrt{2}}{2}\end{matrix}\right]$$

```python
represent(S(0),nqubits=1)
```

$$\left[\begin{matrix}1 & 0\\0 & i\end{matrix}\right]$$

```python
represent(T(0),nqubits=1)
```

$$\left[\begin{matrix}1 & 0\\0 & e^{\frac{i \pi}{4}}\end{matrix}\right]$$

これらの演算を状態に作用させるのは、

```python
ket0 = Qubit('0')
S(0)*Y(0)*X(0)*H(0)*ket0
```

のように `*`で書くことができる。実際に計算をする場合は `qapply()`を利用する。

```python
qapply(S(0)*Y(0)*X(0)*H(0)*ket0)
```

この列ベクトル表示が必要な場合は、

```python
represent(qapply(S(0)*Y(0)*X(0)*H(0)*ket0))
```

$$\left[\begin{matrix}- \frac{\sqrt{2} i}{2}\\- \frac{\sqrt{2}}{2}\end{matrix}\right]$$

のような感じで、SymPyは簡単な行列の計算はすべて自動的にやってくれる。

---

### コラム：ブラケット記法
ここで一旦、量子力学でよく用いられるブラケット記法というものについて整理しておく。ブラケット記法に慣れると非常に簡単に見通しよく計算を行うことができる。

列ベクトルは

$$
|\psi \rangle = \left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right)
$$

とかくのであった。これを**ケット**と呼ぶ。同様に、行ベクトルは

$$
\langle \psi | = ( |\psi \rangle ) ^{\dagger} = ( \alpha ^* , \beta ^*)
$$

とかき、これを**ブラ**と呼ぶ。${\dagger}$マークは転置と複素共役を取る操作で、列ベクトルを行ベクトルへと移す。

２つのベクトル、

$$
|\psi \rangle = \left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right), \;\;\;
|\phi \rangle = \left(
\begin{array}{c}
\gamma
\\
\delta
\end{array}
\right)
$$

があったとする。ブラとケットを抱き合わせると

$$
\langle \phi | \psi \rangle = (\gamma ^* , \delta ^* ) \left(
\begin{array}{c}
\alpha
\\
\beta
\end{array}
\right) = \gamma ^* \alpha + \delta ^* \beta
$$

となり、**内積**に対応する。行ベクトルと列ベクトルをそれぞれブラ・ケットと呼ぶのは、このように並べて内積を取ると「ブラケット」になるからである。

逆に、背中合わせにすると

$$
|\phi \rangle \langle \psi | = \left(
\begin{array}{c}
\gamma
\\
\delta
\end{array}
\right) (\alpha ^* , \beta ^*) = \left(
\begin{array}{cc}
\gamma \alpha ^*  & \gamma \beta ^*
\\
\delta \alpha ^*   & \delta \beta ^*
\end{array}
\right)
$$

となり、演算子となる。例えば、$X$演算子は

$$
X= \left(
\begin{array}{cc}
0 & 1
\\
1 & 0
\end{array}
\right)
=
|0\rangle \langle 1 | + |1\rangle \langle 0|
$$

のように書ける。このことを覚えておけば

$$
\langle 0| 0\rangle = \langle 1 | 1\rangle = 1, \;\;\; \langle 0 | 1 \rangle = \langle 1 | 0 \rangle = 0
$$

から

$$
X |0\rangle = |1\rangle
$$

を行列を書かずに計算できるようになる。
**量子情報の解析計算においては、実際にベクトルの要素を書き下して計算をすることはほとんどなく、このようにブラケットを使って形式的に書いて計算してしまう場合が多い**（古典計算機上で量子コンピュータをシミュレーションする場合はベクトルをすべて書き下すことになる）。

同様に、

$$
I = |0\rangle \langle 0 | + |1\rangle \langle 1| , \;\;\; Z = |0\rangle \langle 0| - |1\rangle \langle 1|
$$

も覚えておくと便利である。


\begin{equation}\label{eq:4}
    \rho (\omega_t, \omega_{\tau}, \epsilon_{xx}, \epsilon_{yy}, \epsilon_{zz}, \epsilon_{yz}, \epsilon_{xy}, \epsilon_{zx}) = \left( \frac{1}{\omega_t-[\Delta_{ZPL} \pm \Delta_{gs} \pm \Delta_{es} - i \gamma_{10}]} \right ) \left (\frac{1}{\omega_{\tau}+[\Delta_{ZPL} \pm \Delta_{gs} \pm \Delta_{es} + i \gamma_{10}]} \right)
\end{equation}

```python
# Adaptive method, taking too long. 1 hr for 1 set of ω_t ω_τ

# @numba.generated_jit
def randDirInt(func):

    def real_func(eps_zz_0, theta, phi):
        return np.real(func(eps_zz_0, theta, phi))

    def imag_func(eps_zz_0, theta, phi):
        return np.imag(func(eps_zz_0, theta, phi))

    real_int = spint.nquad(real_func,
                           [[-np.inf, np.inf], [0., np.pi], [0., 2. * np.pi]])
    imag_int = spint.nquad(imag_func,
                           [[-np.inf, np.inf], [0., np.pi], [0., 2. * np.pi]])

    return [real_int[0] + 1.j * imag_int[0], real_int[1], imag_int[1]]

func = lambda eps_zz_0, theta, phi: np.sin(theta) * Gaus(eps_zz_0) * rho(
    0.,
    0.,
    eps_xx=eps_xx_p(eps_zz_0, theta, phi),
    eps_yy=eps_yy_p(eps_zz_0, theta, phi),
    eps_zz=eps_zz_p(eps_zz_0, theta, phi),
    eps_yz=eps_yz_p(eps_zz_0, theta, phi),
    eps_xy=eps_xy_p(eps_zz_0, theta, phi),
    eps_zx=eps_zx_p(eps_zz_0, theta, phi))

# func = lambda eps_zz_0, theta, phi: Gaus(eps_zz_0) * np.sin(theta) * (eps_zz_0 + theta + phi)

I_randDir, int_err_randDir = randDirInt(func)[0], randDirInt(func)[1]
I_randDir /= np.sqrt(np.max(abs2(I_randDir)))

https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.nquad.html

https://stackoverflow.com/questions/5965583/use-scipy-integrate-quad-to-integrate-complex-numbers@numba.jit
def rho(omega_t,
        omega_tau,
        eps_xx = 0.,
        eps_yy = 0.,
        eps_zz = 0.,
        eps_yz = 0.,
        eps_xy = 0.,
        eps_zx = 0.):

    Del_ZPL = Delta_ZPL(eps_xx,
                        eps_yy,
                        eps_zz)

    Del_gs = Delta_gs(eps_xx,
                      eps_yy,
                      eps_yz,
                      eps_xy,
                      eps_zx)

    Del_es = Delta_es(eps_xx,
                      eps_yy,
                      eps_yz,
                      eps_xy,
                      eps_zx)

    return  np.sum([(1. / (omega_t - (Del_ZPL + Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL + Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs + Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL + Del_gs - Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs + Del_es + 1j*gamma_10)),
                    (1. / (omega_t - (Del_ZPL - Del_gs - Del_es - 1j*gamma_10))) * (1. / (omega_tau + Del_ZPL - Del_gs - Del_es + 1j*gamma_10))
                   ], 0)a
```

# Adaptive method, taking too long. 1 hr for 1 set of ω_t ω_τ

# @numba.generated_jit
def randDirInt(func):

    def real_func(eps_zz_0, theta, phi):
        return np.real(func(eps_zz_0, theta, phi))

    def imag_func(eps_zz_0, theta, phi):
        return np.imag(func(eps_zz_0, theta, phi))

    real_int = spint.nquad(real_func,
                           [[-np.inf, np.inf], [0., np.pi], [0., 2. * np.pi]])
    imag_int = spint.nquad(imag_func,
                           [[-np.inf, np.inf], [0., np.pi], [0., 2. * np.pi]])

    return [real_int[0] + 1.j * imag_int[0], real_int[1], imag_int[1]]

func = lambda eps_zz_0, theta, phi: np.sin(theta) * Gaus(eps_zz_0) * rho(
    0.,
    0.,
    eps_xx=eps_xx_p(eps_zz_0, theta, phi),
    eps_yy=eps_yy_p(eps_zz_0, theta, phi),
    eps_zz=eps_zz_p(eps_zz_0, theta, phi),
    eps_yz=eps_yz_p(eps_zz_0, theta, phi),
    eps_xy=eps_xy_p(eps_zz_0, theta, phi),
    eps_zx=eps_zx_p(eps_zz_0, theta, phi))

# func = lambda eps_zz_0, theta, phi: Gaus(eps_zz_0) * np.sin(theta) * (eps_zz_0 + theta + phi)

I_randDir, int_err_randDir = randDirInt(func)[0], randDirInt(func)[1]
I_randDir /= np.sqrt(np.max(abs2(I_randDir)))

https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.nquad.html

https://stackoverflow.com/questions/5965583/use-scipy-integrate-quad-to-integrate-complex-numbers

\begin{pmatrix}
\left [p \, \cos^2{\phi} + \sin^2{\phi}\left (p \, \cos^2{\theta} + \sin^2{\theta} \right ) \right ]\epsilon_{zz_0} & \left( p - 1 \right) \frac{\sin{2 \phi}}{2} \, \sin^2{\theta} \, \epsilon_{zz_0} & \left( 1 - p \right) \sin{\phi} \, \frac{\sin{2 \theta}}{2} \epsilon_{zz_0}\\
\left( p - 1 \right) \frac{\sin{2 \phi}}{2} \, \sin^2{\theta} \, \epsilon_{zz_0} & \left [p \, \sin^2{\phi} + \cos^2{\phi}\left (p \, \cos^2{\theta} + \sin^2{\theta} \right ) \right ]\epsilon_{zz_0} & \left( p - 1 \right) \cos{\phi} \, \frac{\sin{2 \theta}}{2} \epsilon_{zz_0}\\
\left( 1 - p \right) \sin{\phi} \, \frac{\sin{2 \theta}}{2} \epsilon_{zz_0} & \left( p - 1 \right) \cos{\phi} \, \frac{\sin{2 \theta}}{2} \epsilon_{zz_0} & \left (\cos^2{\theta} + p \, \sin^2{\theta} \right )\epsilon_{zz_0}\\
\end{pmatrix}

```python

```


# Progetto R

```R
library(ggplot2)
library(RColorBrewer)
library(corrplot)
library(caret)
library(mlbench)
library(MLmetrics)
library(pROC)
library(scatterplot3d)
options(warn=-1)
set.seed(42)
```

    corrplot 0.84 loaded

    Loading required package: lattice

    Attaching package: ‘MLmetrics’

    The following objects are masked from ‘package:caret’:

        MAE, RMSE

    The following object is masked from ‘package:base’:

        Recall

    Type 'citation("pROC")' for a citation.

    Attaching package: ‘pROC’

    The following objects are masked from ‘package:stats’:

        cov, smooth, var

Before perfoming the following steps it is necessary to download data from [GECO](http://geco.deib.polimi.it/genosurf/) following the instructions displayed. It is necessarelecting the following for the query parameters:
* project_name: ['tcga-brca']
* assembly: ['grch38']
* data_type: ['gene expression quantification']
* is_healthy:
    * ['false'] for tumoral data
    * [true] for normal data.

## Read input file
First of all we read the input file and we create a dataframe, we check if it contains null valuse. In our case thare are not null value so we are lucky because we do not have to handle them.
We can see that the dimensions of the dataframe are $ rows = 222$ and $columns = 60483$. Each row corresponds to a sample while each column correspond to a features that in our case is a gene. Our gool is to classify tumoral vs normal samples, so we can start plot the number of tumoral vs the number of normal sample in our dataframe.
We than normalize the data to center them in the origin.

```R
data <- read.delim(file = 'data.txt', header = TRUE, sep = '\t')
print('There is any null value?')
any(is.na(data))
# get the target class
target <- data$is_healthy
```

    [1] "There is any null value?"

FALSE

```R
dim(data)
```

222

60484

```R
barplot(table(data$is_healthy),
        xlab="is healthy",
        ylab="number of sample",
        main="Count number of sample divided by type",
        col=brewer.pal(3, "Set3")) # ho messo 3 perché lo richiede come numero minimo altrimenit mi esce il wharning
```

```R
data$is_healthy <- NULL
data <- as.matrix(data)
data <- (data - mean(data)) / sd(data)
head(data)
```

A matrix: 6 × 60483 of type dbl

	ENSG00000223972.5	ENSG00000227232.5	ENSG00000278267.1	ENSG00000243485.3	ENSG00000274890.1	ENSG00000237613.2	ENSG00000268020.3	ENSG00000240361.1	ENSG00000186092.4	ENSG00000238009.5	⋯	ENSGR0000237040.4	ENSGR0000124333.13	ENSGR0000228410.4	ENSGR0000223484.5	ENSGR0000124334.15	ENSGR0000270726.4	ENSGR0000185203.10	ENSGR0000182484.13	ENSGR0000276543.3	ENSGR0000227159.6

1	-0.04284205	-0.03967896	-0.04127097	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04281338	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

2	-0.04284205	-0.03902230	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04272664	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

3	-0.04284205	-0.03595032	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04232146	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

4	-0.04284205	-0.03843439	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

5	-0.04284205	-0.03740185	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04277033	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

6	-0.04284205	-0.03401461	-0.03797037	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04279760	⋯	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205	-0.04284205

## Feature Selection
We have to select the features that are most important because the number of features is huge and so the performance as well as the results obtained in the classification will improve after the features selection. We will try to use the principal components in order to performe the features selection. We plot the proportion of variance expressed and the cumulative variance expressed by the first 20 principal components. As we can see we can select the first 5 principal components because they expresse more than 90% of the total variance. Now that we have the princiapl components we use the first 3 principal component to plot the data in a 3 dimensions features space. As we can see with only 3 principal components the data do not seems to be very well separated even if we can clearly see that they are clusterd in two groups. So using the first 5 principal component the data in the 5-dimensions features space will be certainly well separated.

```R
pcs <- prcomp(data)
#compute standard deviation
std_dev <-pcs$sdev
#compute the variance
pr_var <- std_dev^2
#proportion of variance expressed by each pcs
prop_varex <- pr_var/sum(pr_var)
```

```R
# plot the first 20 principal components
barplot(prop_varex[1:20], xlab = "Principal Component",
     ylab = "Proportion of Variance Explained",  col=brewer.pal(20, "Set3"))

#plot the cumulative variance expressed by the first 20 principal components
colors
barplot(cumsum(prop_varex[1:20]), xlab = "Principal Component",
       ylab = "Cumulative Proportion of Variance Explained",
       type = "b", col=brewer.pal(20, "Set3"))
abline(h = .90)

```

```R
# create the new axes and value
data.pca <- as.matrix(predict(pcs, newdata = data))
# normalize the data
data.pca <- (data.pca - mean(data.pca)) / sd(data.pca)
data.pca <- data.pca[, 1:5]
data.pca <- cbind(data.pca, target)
```

```R
data.plot <- as.data.frame(data.pca)
colors <- c("#999999", "#E69F00")
colors <- colors[as.factor(data.plot$target)]
scatterplot3d(data.plot[1:3], pch = 16, box= FALSE, color=colors)
```

## Model selection
Now we are finally ready to perform the model selection. But first we have to split the dataset. We can follow two way:
* Split the dataset in train, validation and test
* Split the dataset in train and test and use k-fold cross validation

We will follow the second way, initially we will split the dataframe in train and test using the 75% of the data for the train and the remaning for the test. Than we will perform the training of our models using a 10-fold cross validation.
In particular we will train the following models:
* Linear Support Vector Machine
* Polynomial Support Vector Machine
* Radial Support Vector Machine
* Logistic Regression
* K Nearest Neighbor

All the classifiers achieve good performance (the results are plotted below), but in the end we select the Polynomial Support Vector Machine because it seems the one that  gives the most consistent results.

```R
# test and train split
smp_size <- floor(0.75 * nrow(data.pca))

train_ind <- sample(seq_len(nrow(data.pca)), size = smp_size)

train <- data.pca[train_ind, ]
test <- data.pca[-train_ind, ]
```

```R
# k-fold cross val
train_control <- trainControl(method="repeatedcv", number=10, repeats=3)

svmLin <- train(as.factor(target) ~., data = train, method = "svmLinear",
                trControl = train_control,  preProcess = c("center","scale"),
                tuneGrid = expand.grid(C = seq(0, 2, length = 20)))

svmPol<- train(as.factor(target) ~., data = train, method = "svmPoly",
                trControl = train_control,  preProcess = c("center","scale"))

svmRad <- train(as.factor(target) ~., data = train, method = "svmRadial",
                trControl = train_control,  preProcess = c("center","scale"))

logreg <- train(as.factor(target) ~., data = train, method="glm",
                family = binomial(), trControl = train_control,
                preProcess = c("center","scale"))

knn <- train(as.factor(target) ~., data = train, method="knn",
                trControl = train_control, preProcess = c("center","scale"),
                tuneLength = 10) #  10 differents K
```

```R
svmLin
```

    Support Vector Machines with Linear Kernel

    166 samples
      5 predictor
      2 classes: '0', '1'

    Pre-processing: centered (5), scaled (5)
    Resampling: Cross-Validated (10 fold, repeated 3 times)
    Summary of sample sizes: 148, 149, 150, 150, 149, 150, ...
    Resampling results across tuning parameters:

      C          Accuracy   Kappa
      0.0000000        NaN        NaN
      0.1052632  0.8852533  0.7701049
      0.2105263  0.8867375  0.7731383
      0.3157895  0.8926198  0.7849182
      0.4210526  0.8867375  0.7729245
      0.5263158  0.8830474  0.7655149
      0.6315789  0.8791258  0.7577553
      0.7368421  0.8770425  0.7535887
      0.8421053  0.8771650  0.7538477
      0.9473684  0.8771650  0.7538477
      1.0526316  0.8791258  0.7577553
      1.1578947  0.8791258  0.7577553
      1.2631579  0.8791258  0.7577553
      1.3684211  0.8791258  0.7577553
      1.4736842  0.8791258  0.7577553
      1.5789474  0.8791258  0.7577553
      1.6842105  0.8752042  0.7498299
      1.7894737  0.8752042  0.7498299
      1.8947368  0.8752042  0.7498299
      2.0000000  0.8752042  0.7498299

    Accuracy was used to select the optimal model using the largest value.
    The final value used for the model was C = 0.3157895.

```R
svmPol
```

    Support Vector Machines with Polynomial Kernel

    166 samples
      5 predictor
      2 classes: '0', '1'

    Pre-processing: centered (5), scaled (5)
    Resampling: Cross-Validated (10 fold, repeated 3 times)
    Summary of sample sizes: 149, 149, 150, 149, 150, 150, ...
    Resampling results across tuning parameters:

      degree  scale  C     Accuracy   Kappa
      1       0.001  0.25  0.5174020  0.05833333
      1       0.001  0.50  0.5174020  0.05833333
      1       0.001  1.00  0.5193627  0.06184211
      1       0.010  0.25  0.6150735  0.24100930
      1       0.010  0.50  0.7364379  0.47208599
      1       0.010  1.00  0.8067130  0.61249247
      1       0.100  0.25  0.8569989  0.71345489
      1       0.100  0.50  0.8730528  0.74554970
      1       0.100  1.00  0.8753949  0.74967569
      2       0.001  0.25  0.5174020  0.05833333
      2       0.001  0.50  0.5193627  0.06184211
      2       0.001  1.00  0.5448529  0.10797202
      2       0.010  0.25  0.7383987  0.47600811
      2       0.010  0.50  0.8128404  0.62484432
      2       0.010  1.00  0.8465822  0.69262156
      2       0.100  0.25  0.8973312  0.79432676
      2       0.100  0.50  0.9074074  0.81426913
      2       0.100  1.00  0.9015114  0.80221543
      3       0.001  0.25  0.5194853  0.06250000
      3       0.001  0.50  0.5234069  0.06951754
      3       0.001  1.00  0.7033633  0.41500982
      3       0.010  0.25  0.7884532  0.57630067
      3       0.010  0.50  0.8325980  0.66447005
      3       0.010  1.00  0.8609205  0.72116391
      3       0.100  0.25  0.9012800  0.80175442
      3       0.100  0.50  0.9076389  0.81445680
      3       0.100  1.00  0.8979575  0.79507210

    Accuracy was used to select the optimal model using the largest value.
    The final values used for the model were degree = 3, scale = 0.1 and C = 0.5.

```R
svmRad
```

    Support Vector Machines with Radial Basis Function Kernel

    166 samples
      5 predictor
      2 classes: '0', '1'

    Pre-processing: centered (5), scaled (5)
    Resampling: Cross-Validated (10 fold, repeated 3 times)
    Summary of sample sizes: 149, 150, 149, 149, 148, 150, ...
    Resampling results across tuning parameters:

      C     Accuracy   Kappa
      0.25  0.8475899  0.6954673
      0.50  0.8629493  0.7267784
      1.00  0.8647876  0.7297990

    Tuning parameter 'sigma' was held constant at a value of 0.6188278
    Accuracy was used to select the optimal model using the largest value.
    The final values used for the model were sigma = 0.6188278 and C = 1.

```R
logreg
```

    Generalized Linear Model

    166 samples
      5 predictor
      2 classes: '0', '1'

    Pre-processing: centered (5), scaled (5)
    Resampling: Cross-Validated (10 fold, repeated 3 times)
    Summary of sample sizes: 149, 150, 148, 149, 149, 150, ...
    Resampling results:

      Accuracy   Kappa
      0.8732162  0.7458081

```R
knn
```

    k-Nearest Neighbors

    166 samples
      5 predictor
      2 classes: '0', '1'

    Pre-processing: centered (5), scaled (5)
    Resampling: Cross-Validated (10 fold, repeated 3 times)
    Summary of sample sizes: 148, 150, 150, 149, 149, 149, ...
    Resampling results across tuning parameters:

      k   Accuracy   Kappa
       5  0.8772331  0.7538789
       7  0.8892429  0.7780623
       9  0.8849537  0.7695262
      11  0.8831155  0.7657682
      13  0.8873911  0.7743609
      15  0.8893519  0.7782116
      17  0.8835784  0.7665213
      19  0.8817266  0.7628176
      21  0.8779139  0.7552062
      23  0.8757217  0.7506743

    Accuracy was used to select the optimal model using the largest value.
    The final value used for the model was k = 15.

## Model evaluation
We will use different metrics to evaluate our model on test set, in particular:
* accuracy:

\begin{equation} \label{eq:accuracy}
  accuracy = \frac{number \; of \; incorrect \; prediction}{Total \; number \; of \; prediction}
\end{equation}

* precision (specificity):
\begin{equation} \label{eq:precision}
  accuracy = \frac{true \; positive}{true \; positive \; + \; false \; positive}
\end{equation}
* recall (sensitivity):
\begin{equation} \label{eq:recall}
  accuracy = \frac{true \; positive}{true \; positive \; + \; false \; negative}
\end{equation}

* roc: which plot the $1 - Specificity$ vs $Sensitivity$
* confusion matrix: a table in which the principal diagonal contain the number of sample correctly classified and the element off diagonal are the sample misclassified

In particular we can notice that we have a very high precision, so if our classifier says that a sample belongs to a certain class we are pretty sure that is correct, but a sample is correctly classified "only" with a probability of 0.84.

For the roc curve we can see that the AUC is 0.85 that is a quite good value considering that the best value is 1.

In the end we can notice, using the correlation matrix, that our classifier tends to classify a tumoral sample when in reality is a normal sample. This is results is not so bad after all, because if a sample that is normal is classified as tumoral we can always performe other test to be sure.

```R
target.test <- test[, ncol(test)]
test <- test[, -ncol(test)]
predicted <- as.numeric(as.character(predict(svmPol, test)))
```

```R
accuracy <- round(Accuracy(predicted, target.test), 2)
precision <- round(Precision(predicted, target.test, positive = 1), 2)
recall <- round(Recall(predicted, target.test, positive = 1), 2)
metrics <- data.frame(accuracy, precision, recall)
print(metrics)
```

      accuracy precision recall
    1     0.86       0.9   0.84

```R
# in my case controll = 1 cases = 0 this because target = is_healthy
pROC <- roc(target.test, predicted,
                plot=TRUE, auc.polygon=TRUE, max.auc.polygon=TRUE,
                print.auc=TRUE, levels = c(1, 0), direction = '>')
```

```R
matrix <- confusionMatrix(as.factor(target.test), as.factor(predicted),
                dnn = c("Predicted Class", "True Class"), positive = '1')

matrix$table
```

                   True Class
    Predicted Class  0  1
                  0 21  5
                  1  3 27

# THE END