# [cknowledge.org](http://cknowledge.org): Community-driven benchmarking and optimization of computing systems - from classical to quantum

[Quantum Computing](https://github.com/ctuning/ck-quantum/wiki)
* [CK-QISKit](https://github.com/ctuning/ck-qiskit) (IBM)
* [CK-Rigetti](https://github.com/ctuning/ck-rigetti) ([Rigetti Computing](https://rigetti.com/))
* [CK-ProjectQ](https://github.com/ctuning/ck-projectq) ([ProjectQ](https://projectq.ch/))

[Artificial Intelligence and Machine Learning](http://cknowledge.org/ai)
* [Reproducible Quality-Efficient Systems Tournaments](http://cknowledge.org/request) ([ReQuEST initiative](http://cknowledge.org/request.html#organizers))
* [AI artifacts](http://cknowledge.org/ai-artifacts) (cTuning foundation)
* [Android app](https://play.google.com/store/apps/details?id=openscience.crowdsource.video.experiments) (dividiti)
* [Desktop app](https://github.com/dividiti/ck-crowdsource-dnn-optimization) (dividiti)
* [CK-Caffe](https://github.com/dividiti/ck-caffe) (Berkeley)
* [CK-Caffe2](https://github.com/ctuning/ck-caffe2) (Facebook)
* [CK-CNTK](https://github.com/ctuning/ck-cntk) (Microsoft)
* [CK-KaNN](https://github.com/ctuning/ck-kann) (Kalray)
* [CK-MVNC](https://github.com/ctuning/ck-mvnc) (Movidius / Intel)
* [CK-MXNet](https://github.com/ctuning/ck-mxnet) (Apache)
* [CK-NNTest](https://github.com/ctuning/ck-nntest) (cTuning foundation)
* [CK-TensorFlow](https://github.com/ctuning/ck-tensorflow) (Google)
* [CK-TensorRT](https://github.com/ctuning/ck-tensorrt) (NVIDIA)
* etc.

# Variational Quantum Eigensolver (VQE) on Rigetti machines
[Quantum Collective Knowledge Hackaton](https://www.eventbrite.co.uk/e/quantum-computing-hackathon-tickets-46441126660#), [Centre for Mathematical Science - University of Cambridge](http://www.cms.cam.ac.uk/), 15 June 2018

## Table of Contents

1. [Organisers](#organisers)
1. [References](#references)
1. [Time-to-solution metric](#metric)
1. [Setting up](#settingup)
1. [Running experiments](#running)
1. [Experimental data](#data)
1. [Data wrangling code](#code) (for developers)
1. [Analysis](#analysis)

## Organisers

- [Rigetti Computing](https://rigetti.com): access to [Quantum Virtual Machine](http://pyquil.readthedocs.io/en/latest/qvm.html) (QVM) and [Quantum Processing Unit](http://pyquil.readthedocs.io/en/latest/qpu.html) (QPU).
- [River Lane Research](https://riverlane.io/): Steve Brierley, Oscar Higgott, Daochen Wang, Amy Flower
- [dividiti](http://dividiti.com/): Anton Lokhmotov, Leo Gordon, Flavio Vella, Grigori Fursin

## References

- [Rigetti's docs](http://grove-docs.readthedocs.io/en/latest/vqe.html)
- ["A variational eigenvalue solver on a quantum processor"](https://arxiv.org/abs/1304.3061) (2013)
- ["The theory of variational hybrid quantum-classical algorithms"](https://arxiv.org/abs/1509.04279) (2015)
- ["Quantum optimization using variational algorithms
on near-term quantum devices"](https://arxiv.org/abs/1710.01022) [2017]

## Time-to-solution metric

To compare solutions of participants we use a **time-to-solution** $T$ metric defined as follows.

### Definition

Let's assume that to find the ground state of a given molecule (e.g. [Helium](https://en.wikipedia.org/wiki/Helium)) a participant makes $N$ runs of their implementation (e.g. $N=3$). A run is considered _successful_ if the ground state found in this run is equal to the ground state known for the molecule to given precision $\delta$ (e.g. $\delta=0.1$). Assume that a single run takes $t$ samples (calls to the quantum computer) on average.

Let $s$ be the probability of success of the participant's VQE implementation (i.e. the number of successful runs divided by the total number of runs $N$).

Let $R$ be the number of runs required to find the ground state with given probability $p$ (e.g. $p=0.6$):
\begin{equation}
R = {\frac{\log(1-p)}{\log(1-s)}}.
\end{equation}

The **time-to-solution** $T$, defined as the total number of samples used throughout the whole optimisation procedure of VQE, is then calculated as:
\begin{equation}
T = R \times t.
\end{equation}

### Derivation

If the probability of success is $s$, then the probability of _failing to find_ the ground state after $R$ runs is $(1-s)^R$. Therefore, the probability of finding the ground state at least once after $R$ runs is $p =1 - (1-s)^R$. Therefore, the number of runs $R$ required to find the ground state at least once with probability $p$ can be found by solving $p=1-(1-s)^R$.

### Uncertainties

We can also calculate the standard error associated with the calculated time-to-solution $T$.

From the [binomial distribution](Binomial_distribution), the uncertainty $\sigma_s$ in the success probability $s$ is:
\begin{equation}
  \sigma_s=\sqrt{\frac{s(1-s)}{N}}
\end{equation}
where $N$ is the number of runs used to determine $s$.

The uncertainty in the time taken per run $t$ is:
\begin{equation}
  \sigma_t=\frac{\mathrm{std}}{\sqrt{N}}
\end{equation}
where $\mathrm{std}$ is the standard deviation of the times taken by all $N$ runs.

The uncertainty in total time taken is:
\begin{equation}
\sigma_T=\sqrt{0.25 \cdot (T(t+\sigma_t, s) - T(t-\sigma_t,s))^2 + (T(t, s + \sigma_s) - T(t,s))^2}
\end{equation}

## Setting up

Please follow instructions [here](https://github.com/ctuning/ck-quantum/blob/master/README.md).

## Running experiments

```
$ ck benchmark program:rigetti-vqe \
  --env.RIGETTI_QUANTUM_DEVICE=<platform> \
  --env.VQE_MINIMIZER_METHOD=<minimizer_method> \
  --env.VQE_SAMPLE_SIZE=<sample_number> \
  --env.VQE_MAX_ITERATIONS=<max_iterations> \
  --record --record_repo=local --record_uoa=<email>-<plaform> \
  --tags=qck,hackathon-2018_06_15,<email>,<platform>,<minimizer_method> \
  --repetitions=<repetitions>
```
where:
- `platform`: `8Q-Agave` or `QVM`;
- `minimizer_method`: `my_melder_nead` or `my_cobyla` or `my_minimizer` (as defined in [optimizers.py](https://github.com/ctuning/ck-quantum/blob/master/package/tool-hackathon/hackathon-src/hackathon/optimizers.py) installed under e.g. `$CK_TOOLS/hackathon-1.0-linux-64/lib/hackathon`);
- `sample_size`: e.g. `100` (or another resolution);
- `max_iterations`: e.g. `80` (or another cut-off point);
- `email`: a valid email address (later to be replaced with a team id e.g. `team-01`);
- `repetitions`: how many times to run the experiment with the given parameters: e.g. `3`.

## Get sample experimental data

The sample experimental data can be downloaded and registered with CK as follows:

```
$ wget https://www.dropbox.com/s/a1odux4asze9zpd/ck-quantum-hackathon-20180615.zip
$ ck add repo --zip=ck-quantum-hackathon-20180615.zip
```

```python
repo_uoa = 'ck-quantum-hackathon-20180615'
!ck list $repo_uoa:experiment:* --print_full | sort
```

## Data wrangling code

**NB:** Please ignore this section if you are not interested in re-running or modifying this notebook.

### Includes

#### Standard

```python
import os
import sys
import json
import re
```

#### Scientific

If some of the scientific packages are missing, please install them using:
```
# pip install jupyter pandas numpy matplotlib
```

```python
import IPython as ip
import pandas as pd
import numpy as np
import matplotlib as mp
```

```python
print ('IPython version: %s' % ip.__version__)
print ('Pandas version: %s' % pd.__version__)
print ('NumPy version: %s' % np.__version__)
print ('Matplotlib version: %s' % mp.__version__)
```

```python
from IPython.display import Image, display
def display_in_full(df):
    pd.options.display.max_columns = len(df.columns)
    pd.options.display.max_rows = len(df.index)
    display(df)
```

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

```python
default_colormap = cm.autumn
default_fontsize = 16
default_barwidth = 0.8
default_figwidth = 16
default_figheight = 8
default_figdpi = 200
default_figsize = [default_figwidth, default_figheight]
```

```python
if mp.__version__[0]=='2': mp.style.use('classic')
mp.rcParams['figure.max_open_warning'] = 200
mp.rcParams['figure.dpi'] = default_figdpi
mp.rcParams['font.size'] = default_fontsize
mp.rcParams['legend.fontsize'] = 'medium'
```

#### Collective Knowledge

If CK is not installed, please install it using:
```
# pip install ck
```

```python
import ck.kernel as ck
print ('CK version: %s' % ck.__version__)
```

```python
# NB: Make sure the quantum hackathon tool is installed. (It should be if you have run any experiments.)
# $ ck install package --tags=ck-quantum,tool,hackathon,v1
r=ck.access({'action':'show', 'module_uoa':'env', 'tags':'tool,hackathon'})
if r['return']>0:
    print ("Error: %s" % r['error'])
    exit(1)

# Get the path to the first returned environment entry.
tool_hackathon_dir=r['lst'][0]['meta']['env']['CK_ENV_LIB_HACKATHON_LIB']
sys.path.append(tool_hackathon_dir)
from hackathon.utils import *
```

### Access experimental data

```python
def get_experimental_results(repo_uoa, tags='qck', module_uoa='experiment'):
    r = ck.access({'action':'search', 'repo_uoa':repo_uoa, 'module_uoa':module_uoa, 'tags':tags})
    if r['return']>0:
        print('Error: %s' % r['error'])
        exit(1)
    experiments = r['lst']

    dfs = []
    for experiment in experiments:
        data_uoa = experiment['data_uoa']
        r = ck.access({'action':'list_points', 'repo_uoa':repo_uoa, 'module_uoa':module_uoa, 'data_uoa':data_uoa})
        if r['return']>0:
            print('Error: %s' % r['error'])
            exit(1)
        tags = r['dict']['tags']

        skip = False
        # Get team name (final data) or email (submission data).
        team_tags = [ tag for tag in tags if tag.startswith('team-') ]
        email_tags = [ tag for tag in tags if tag.find('@')!=-1 ]
        if len(team_tags) > 0:
            team = team_tags[0][0:7]
        elif len(email_tags) > 0:
            team = email_tags[0]
        else:
            print('[Warning] Cannot determine team name for experiment in: \'%s\'' % r['path'])
            team = 'team-default'

        if skip:
            print('[Warning] Skipping experiment with bad tags:')
            print(tags)
            continue

        # For each point.
        for point in r['points']:
            point_file_path = os.path.join(r['path'], 'ckp-%s.0001.json' % point)
            with open(point_file_path) as point_file:
                point_data_raw = json.load(point_file)
            characteristics_list = point_data_raw['characteristics_list']
            num_repetitions = len(characteristics_list)
            data = [
                {
                    # features
                    'platform': characteristics['run'].get('vqe_input', {}).get('q_device_name', 'unknown').lower(),
                    # choices
                    'minimizer_method': characteristics['run'].get('vqe_input', {}).get('minimizer_method', 'n/a'),
                    'minimizer_options': characteristics['run'].get('vqe_input', {}).get('minimizer_options', {'maxfev':-1}),
                    'minimizer_src': characteristics['run'].get('vqe_input', {}).get('minimizer_src', ''),
                    'sample_number': characteristics['run'].get('vqe_input', {}).get('sample_number','n/a'),
                    # statistical repetition
                    'repetition_id': repetition_id,
                    # runtime characteristics
                    'run': characteristics['run'],
                    'report': characteristics['run'].get('report', {}),
                    'vqe_output': characteristics['run'].get('vqe_output', {}),
                }
                for (repetition_id, characteristics) in zip(range(num_repetitions), characteristics_list)
                if len(characteristics['run']) > 0
            ]

            for datum in data:
                datum['team'] = team
                datum['point'] = point
                datum['success'] = datum.get('vqe_output',{}).get('success',False)
                datum['nfev'] = np.int64(datum.get('vqe_output',{}).get('nfev',-1))
                datum['nit'] = np.int64(datum.get('vqe_output',{}).get('nit',-1))
                datum['fun'] = np.float64(datum.get('vqe_output',{}).get('fun',0))
                datum['fun_validated'] = np.float64(datum.get('vqe_output',{}).get('fun_validated',0))
                datum['fun_exact'] = np.float64(datum.get('vqe_output',{}).get('fun_exact',0))
                datum['total_seconds'] = np.float64(datum.get('report',{}).get('total_seconds',0))
                datum['total_q_seconds'] = np.float64(datum.get('report',{}).get('total_q_seconds',0))
                datum['total_q_shots'] = np.int64(datum.get('report',{}).get('total_q_shots',0))
                tmp_max_iterations = list(datum.get('minimizer_options',{'maxfev':-1}).values())
                datum['max_iterations'] = tmp_max_iterations[0] if len(tmp_max_iterations)>0 else -1
            index = [
                'platform', 'team', 'minimizer_method', 'sample_number', 'max_iterations', 'point', 'repetition_id'
            ]
            # Construct a DataFrame.
            df = pd.DataFrame(data)
            df = df.set_index(index)
            # Append to the list of similarly constructed DataFrames.
            dfs.append(df)
    if dfs:
        # Concatenate all thus constructed DataFrames (i.e. stack on top of each other).
        result = pd.concat(dfs)
        result.sort_index(ascending=True, inplace=True)
    else:
        # Construct a dummy DataFrame the success status of which can be safely checked.
        result = pd.DataFrame(columns=['success'])
    return result
```

```python
# Merge experimental results from the same team with the same parameters
# (minimizer_method, sample_number, max_iterations) and minimizer source.
def merge_experimental_results(df):
    dfs = []
    df_prev = None
    for index, row in df.iterrows():
        # Construct a DataFrame.
        df_curr = pd.DataFrame(row).T
        # Check if this row is similar to the previous row.
        if df_prev is not None: # if not the very first row
            if df_prev.index.levels[:-2]==df_curr.index.levels[:-2]: # if the indices match for all but the last two levels
                if df_prev.index.levels[-2]!=df_curr.index.levels[-2]: # if the experiments are different
                    if df_prev['minimizer_src'].values==df_curr['minimizer_src'].values: # if the minimizer source is the same
                        print('[Info] Merging experiment:')
                        print(df_curr.index.levels)
                        print('[Info] into:')
                        print(df_prev.index.levels)
                        print('[Info] as:')
    #                     df_curr.index = df_prev.index.copy() # TODO: increment repetition_id
                        df_curr.index = pd.MultiIndex.from_tuples([(x[0],x[1],x[2],x[3],x[4],x[5],x[6]+1) for x in df_prev.index])
                        print(df_curr.index.levels)
                        print
                    else:
                        print('[Warning] Cannot merge experiments as the minimizer source is different:')
    #                     print('------------------------------------------------------------------------')
                        print(df_prev.index.levels)
    #                     print(df_prev['minimizer_src'].values[0])
    #                     print
    #                     print('------------------------------------------------------------------------')
                        print(df_curr.index.levels)
    #                     print(df_curr['minimizer_src'].values[0])
                        print
    #             else:
    #                 print('[Info] Keeping experiments separate:')
    #                 print(df_prev.index.levels)
    #                 print(df_curr.index.levels)
    #                 print
        # Append to the list of similarly constructed DataFrames.
        dfs.append(df_curr)
        # Prepare for next iteration.
        df_prev = df_curr

    # Concatenate all thus constructed DataFrames (i.e. stack on top of each other).
    result = pd.concat(dfs)
    result.index.names = df.index.names
    result.sort_index(ascending=True, inplace=True)

    return result
```

```python
def get_metrics(df, delta=0.1, prob=0.5, which_fun_key='fun_exact', which_time_key='total_q_shots'):
    dfs = []
    names_no_repetitions = df.index.names[:-1]
    for index, group in df.groupby(level=names_no_repetitions):
        # Compute metrics.
        classical_energy, minimizer_method, minimizer_src, n_succ, T_ave, T_err, t_ave, t_err, s, s_err = \
            benchmark_list_of_runs(group['run'], verbose=False, delta=delta, prob=prob,
                                   which_fun_key=which_fun_key, which_time_key=which_time_key)
        # Construct a DataFrame from the metrics.
        data = {
            # Time to solution.
            'T_ave' : T_ave,
            'T_err' : T_err,
            # Time metric (seconds or shots).
            't_ave' : t_ave,
            't_err' : t_err,
            # Tries metric.
            's' : s,
            's_err' : s_err
        }
        data.update({ k : v for (k, v) in zip(names_no_repetitions, index) })
        data['num_repetitions'] = len(group)
        # NB: index must be something.
        df_ = pd.DataFrame(data=data, index=[0])
        df_ = df_.set_index(names_no_repetitions)
        # Append to the list of similarly constructed DataFrames.
        dfs.append(df_)
    if dfs:
        # Concatenate all thus constructed DataFrames (i.e. stack on top of each other).
        result = pd.concat(dfs).dropna()
        result.sort_index(ascending=True, inplace=True)
    return result
```

### Plot experimental data

```python
def plot(df, platform_set=None, minimizer_method_set=None, sample_number_set=None, max_iterations_set=None,
         markersize_divisor=20,
         xmin=0.0, xmax=85.01, xstep=5.00,
         ymin=-3.0, ymax=-0.49, ystep=0.25,
         figsize=(18,9), dpi=200, legend_loc='lower right'):

    platform_set = platform_set or df.index.get_level_values(level='platform').unique()
    minimizer_method_set = minimizer_method_set or df.index.get_level_values(level='minimizer_method').unique()
    sample_number_set = sample_number_set or df.index.get_level_values(level='sample_number').unique()
    max_iterations_set = max_iterations_set or df.index.get_level_values(level='max_iterations').unique()

    # Options.
    minimizer_method_to_color = {
        'my_cobyla' : 'orange',
        'my_nelder_mead' : 'green',
        'my_minimizer' : 'blue'
    }
    platform_to_marker = {
        '8q-agave' : '8', # octagon
        'qvm' : 's',      # square
        'local_qasm_simulator' : 'p' # pentagon
    }

    last_marker_size = 10
    last_marker_color = 'black'
    last_marker_success_true = '^'
    last_marker_success_false = 'v'

    fig = plt.figure(figsize=figsize, dpi=dpi)
    ax = fig.gca()
    for index, row in df.iterrows():
        (platform, team, minimizer_method, sample_number, max_iterations, point, repetition_id) = index
        if platform not in platform_set: continue
        if sample_number not in sample_number_set: continue
        if minimizer_method not in minimizer_method_set: continue
        # NB: This uses 'fun', not 'fun_exact' or 'fun_validated'.
        energies = [ iteration['energy'] for iteration in row['report']['iterations'] ]
        marker=platform_to_marker[platform]
        markersize=sample_number/markersize_divisor
        color=minimizer_method_to_color.get(minimizer_method, 'red')
        markerfacecolor=color
        linestyle='-'
        ax.plot(range(len(energies)), energies, marker=marker, color=color, linestyle=linestyle,
                markerfacecolor=markerfacecolor, markersize=markersize)
        # Mark last function evaluation.
        last_energy = energies[-1]
        last_fev = row['nfev']-1 if minimizer_method=='my_cobyla' or 'my_nelder_mead' else row['nfev']
        last_marker = last_marker_success_true if row['success'] else last_marker_success_false
        ax.plot(last_fev, last_energy, color=last_marker_color, marker=last_marker, markersize=last_marker_size)

    # Horizontal line for the known ground state.
    plt.axhline(y=-2.80778395754, color='red', linestyle='--')
    # Vertical lines for max_iterations.
    for max_iterations in max_iterations_set:
        plt.axvline(x=max_iterations, color='black')
    # Grid.
    plt.grid()
    # Title.
    title = 'Variational Quantum Eigensolver (VQE)'
    ax.set_title(title)
    # X axis.
    xlabel='Function evaluation'
    ax.set_xlabel(xlabel)
    ax.set_xlim(xmin, xmax)
    ax.set_xticks(np.arange(xmin, xmax, xstep))
    # Y axis.
    ylabel='Energy'
    ax.set_ylabel(ylabel)
    ax.set_ylim(ymin, ymax)
    ax.set_yticks(np.arange(ymin, ymax, ystep))
    # Legend. https://matplotlib.org/users/legend_guide.html
    handles = [
        mp.lines.Line2D([], [], label='platform="%s",minimizer_method="%s"' % (p,m), color=minimizer_method_to_color.get(m, 'red'),
                        marker=platform_to_marker[p], markersize=last_marker_size)
        for p in sorted(platform_set)
        for m in sorted(minimizer_method_set)
    ]
    handles.append(mp.lines.Line2D([],[], label='ground state', color='red', linestyle='--'))
    plt.legend(handles=handles, title='platform,minimizer_method', loc=legend_loc)
    # Save figure.
#    plt.savefig('vqe.energy.png')
```

```python
def plot_metric(df, metric='total_q_seconds'):
    df.columns.name='metric'
    # "df.index.names[:-1]" means reduce along 'repetition_id' (statistical variation).
    df_mean = df[[metric]].groupby(level=df.index.names[:-1]).mean().unstack('platform')
    df_std = df[[metric]].groupby(level=df.index.names[:-1]).std().unstack('platform')
    ax = df_mean.plot(kind='bar', yerr=df_std, grid=True, legend=True, rot=45,
                      fontsize=default_fontsize, figsize=default_figsize, colormap=default_colormap)
```

## Analysis

### All experimental data

```python
df = get_experimental_results(repo_uoa=repo_uoa)
display_in_full(df)
```

### Merge experimental results from different runs with the same parameters

```python
df = merge_experimental_results(df)
display_in_full(df)
```

### Compute the time-to-solution metric etc.

```python
df_metrics = get_metrics(df, delta=0.1, prob=0.5, which_fun_key='fun_exact', which_time_key='total_q_shots')
df_metrics
```

### The (unexpected) winner

Somewhat unexpectedly, the winner obtained the exact answer (!) with `sample_number=1` (!!) on real hardware (!!!). Please see below an explanation why this was the case.

```python
idxmin1 = df_metrics['T_ave'].idxmin()
idxmin1
```

```python
df_metrics.loc[[idxmin1]]
```

```python
df.loc[idxmin1]
```

```python
# Platform, Team, minimizer Function, number of Samples, number of function eValuations, Experiment, Repetition
(p,t,f,s,v,e) = idxmin1
# Plot the winner.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[idxmin1]))]],
     xmax=7, xstep=1, ymin=-3.00, ymax=0.00+0.01, legend_loc='upper center')
```

```python
# Exclude the winner.
df_metrics = df_metrics.drop(idxmin1)
```

#### Explanation

The [Hamiltonian](https://en.wikipedia.org/wiki/Hamiltonian_(quantum_mechanics)) of [Helium](https://en.wikipedia.org/wiki/Helium) in the [STO-3G](https://en.wikipedia.org/wiki/STO-nG_basis_sets) basis is given by:

\begin{equation}
    H = -1.6678202144537553 + 0.7019459893849936 \cdot Z_0 + 0.7019459893849936 \cdot Z_1 + 0.263928235683768058 \cdot Z_0 \cdot Z_1
\end{equation}

Since the Hamiltonian consists of a sum of commuting operators ($Z_0$, $Z_1$, $Z_0\cdot Z1$), there exists a simultaneous eigenbasis (including the ground state) on which the value of each of the operators is $+1$ or $-1$.

When `sample_number=1`, measuring an operator once in any state also results in $+1$ or $-1$. This implies that one get "lucky" with _any_ mininizer method even on noisy hardware. (Indeed, we have been able to reproduce this result with `my_nelder_mead`.)

For more complex molecules, the Hamiltonian is unlikely to consist of a sum of commuting operators, hence picking up a good optimizer and its parameters will be crucial for success.

### The (conditional) runner-up

The runner-up also used `sample_number=1` but only a single run (hence, it's a "conditional" runner-up, as we can determine the error only from multiple runs).

```python
idxmin2 = df_metrics['T_ave'].idxmin()
idxmin2
```

```python
df_metrics.loc[[idxmin2]]
```

```python
df.loc[idxmin2]
```

```python
# Platform, Team, minimizer Function, number of Samples, number of function eValuations, Experiment, Repetition
(p,t,f,s,v,e) = idxmin2
# Plot the winner.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[idxmin2]))]],
     xmax=7, xstep=1, ymin=-3.00, ymax=0.00+0.01, legend_loc='upper center')
```

```python
# Print the minimizer source.
print(df.loc[(p,t,f,s,v,e,0)]['minimizer_src'])
```

```python
# Exclude the conditional runner-up.
df_metrics = df_metrics.drop(idxmin2)
```

### The QVM runner-up

```python
platform_qvm = 'qvm'
df_metrics_qvm = df_metrics.loc[platform_qvm]
idxmin_qvm = df_metrics_qvm['T_ave'].idxmin()
idxmin_qvm
```

```python
df_metrics.loc[platform_qvm].loc[[idxmin_qvm]]
```

```python
df.loc[platform_qvm].loc[idxmin_qvm]
```

```python
# Platform, Team, minimizer Function, number of Samples, number of function eValuations, Experiment, Repetition
p = platform_qvm
(t,f,s,v,e) = idxmin_qvm
# Plot the runner up.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[(p,t,f,s,v,e)]))]],
     xmax=9, xstep=1, ymin=-3.00, ymax=0.00+0.01, legend_loc='upper right')
```

```python
# Print the minimizer source.
print(df.loc[(p,t,f,s,v,e,0)]['minimizer_src'])
```

```python
# Exclude the QVM runner-up.
df_metrics = df_metrics.drop((p,t,f,s,v,e))
```

### The QPU runner-up

```python
platform_qpu = '8q-agave'
df_metrics_qpu = df_metrics.loc[platform_qpu]
idxmin_qpu = df_metrics_qpu['T_ave'].idxmin()
idxmin_qpu
```

```python
df_metrics_qpu.loc[[idxmin_qpu]]
```

```python
df.loc[platform_qpu].loc[idxmin_qpu]
```

```python
# Platform, Team, Experiment, minimizer Function, number of Samples, number of function eValuations, Repetition
p = platform_qpu
(t,f,s,v,e) = idxmin_qpu
# Plot the QPU runner-up.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[(p,t,f,s,v,e)]))]],
     xmax=7, xstep=1, ymin=-3.00, ymax=-0.00+0.01, legend_loc='upper right')
```

```python
# Print the minimizer source.
print(df.loc[(p,t,f,s,v,e,0)]['minimizer_src'])
```

```python
# Exclude the QPU runner-up.
df_metrics = df_metrics.drop((p,t,f,s,v,e))
```

### The best entry with 100% convergence (prob=0.999)

```python
df_metrics_prob100 = get_metrics(df, delta=0.1, prob=0.999, which_fun_key='fun_exact', which_time_key='total_q_shots')
df_metrics_prob100 = df_metrics_prob100[(df_metrics_prob100['s']==1) & (df_metrics_prob100['num_repetitions']>1)]
```

```python
idxmin_prob100 = df_metrics_prob100['T_ave'].idxmin()
idxmin_prob100
```

```python
df_metrics_prob100.loc[[idxmin_prob100]]
```

```python
df.loc[idxmin_prob100]
```

```python
# Platform, Team, Experiment, minimizer Function, number of Samples, number of function eValuations, Repetition
(p,t,f,s,v,e) = idxmin_prob100
# Plot.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[(p,t,f,s,v,e)]))]],
     xmax=30, xstep=1, ymin=-3.00, ymax=-0.00+0.01, legend_loc='upper right')
```

### The worst entry with 100% convergence (prob=0.999)

```python
idxmax_prob100 = df_metrics_prob100['T_ave'].idxmax()
idxmax_prob100
```

```python
df_metrics_prob100.loc[[idxmax_prob100]]
```

```python
df.loc[idxmax_prob100]
```

```python
# Platform, Team, Experiment, minimizer Function, number of Samples, number of function eValuations, Repetition
(p,t,f,s,v,e) = idxmax_prob100
# Plot.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[(p,t,f,s,v,e)]))]],
     xmax=31, xstep=1, ymin=-3.00, ymax=-0.00+0.01, legend_loc='upper right')
```

```python
# The ratio of the worst of the best with 100% convergence.
df_metrics_prob100.loc[idxmax_prob100]['T_ave'] / df_metrics_prob100.loc[idxmin_prob100]['T_ave']
```

```python
# Exclude the best entry with 100% convergence.
df_metrics_prob100 = df_metrics_prob100.drop(idxmin_prob100)
```

### The most accurate entry (also the runner-up with 100% convergence!)

```python
df_metrics_delta0 = get_metrics(df, delta=0.01, prob=0.999, which_fun_key='fun_exact', which_time_key='total_q_shots')
df_metrics_delta0 = df_metrics_delta0[(df_metrics_delta0['num_repetitions']>1)]
```

```python
idxmin_delta0 = df_metrics_delta0['T_ave'].idxmin()
idxmin_delta0
```

```python
# Also, the runner-up entry with 100% convergence!
idxmin_prob100 = df_metrics_prob100['T_ave'].idxmin()
idxmin_prob100
```

```python
df_metrics_delta0.loc[[idxmin_delta0]]
```

```python
df.loc[idxmin_delta0]
```

```python
# Platform, Team, Experiment, minimizer Function, number of Samples, number of function eValuations, Repetition
(p,t,f,s,v,e) = idxmin_delta0
# Plot.
plot(df.loc[[(p,t,f,s,v,e,k) for k in range(len(df.loc[(p,t,f,s,v,e)]))]],
     xmax=9, xstep=1, ymin=-3.00, ymax=-0.00+0.01, legend_loc='upper right')
```

```python
# Print the minimizer source.
print(df.loc[(p,t,f,s,v,e,0)]['minimizer_src'])
```

### Plot convergence

```python
# # Plot all.
# plot(df, legend_loc='center')
```

```python
# Plot QPU only.
plot(df, platform_set=[platform_qpu], markersize_divisor=10,
     xmin=0, xmax=34+0.01, xstep=1, ymin=-2.80, ymax=-1.80-0.01, ystep=0.05, legend_loc='lower left')
```

```python
# Plot COBYLA only.
plot(df, minimizer_method_set=['my_cobyla'], sample_number_set=[50], markersize_divisor=5,
     xmin=5, xmax=32+0.01, xstep=1, ymin=-2.89, ymax=-1.79+0.01, ystep=0.05, legend_loc='upper right')
```

### Plot execution metrics

```python
# plot_metric(df)
```

```python
# plot_metric(df, metric='total_q_shots')
```


```python
%load_ext watermark
%watermark -a 'cs224' -u -d -v -p numpy,pandas,matplotlib,sklearn,h5py,pytest
```

    Author: cs224

    Last updated: 2021-09-10

    Python implementation: CPython
    Python version       : 3.8.10
    IPython version      : 7.22.0

    numpy     : 1.20.2
    pandas    : 1.2.5
    matplotlib: 3.3.4
    sklearn   : 0.24.2
    h5py      : 2.10.0
    pytest    : 6.2.4

```python
%matplotlib inline
import numpy as np, scipy, scipy.stats as stats, pandas as pd, matplotlib.pyplot as plt, seaborn as sns
import sklearn, sklearn.pipeline, sklearn.model_selection, sklearn.preprocessing, sklearn.linear_model

pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1000)
# pd.set_option('display.float_format', lambda x: '%.2f' % x)
np.set_printoptions(edgeitems=10)
np.set_printoptions(linewidth=1000)
np.set_printoptions(suppress=True)
np.core.arrayprint._line_width = 180

SEED = 42
np.random.seed(SEED)

sns.set()
```

```python
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:60% !important; }</style>"))
```

<style>.container { width:60% !important; }</style>

```python
# import os,sys
# sys.path.append(os.path.realpath(os.path.abspath('') + '/../../lib'))
```

```python
# %load_ext autoreload
# %autoreload 1
# %aimport somemodule
```

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

from IPython.display import display_html
def display_side_by_side(*args):
    html_str=''
    for df in args:
        if type(df) == np.ndarray:
            df = pd.DataFrame(df)
        html_str+=df.to_html()
    html_str = html_str.replace('table','table style="display:inline"')
    # print(html_str)
    display_html(html_str,raw=True)

CSS = """
.output {
    flex-direction: row;
}
"""

def display_graphs_side_by_side(*args):
    html_str='<table><tr>'
    for g in args:
        html_str += '<td>'
        html_str += g._repr_svg_()
        html_str += '</td>'
    html_str += '</tr></table>'
    display_html(html_str,raw=True)

display(HTML("<style>.container { width:70% !important; }</style>"))
```

<style>.container { width:70% !important; }</style>

```python
# import importlib, logging
# importlib.reload(logging)
# logging.basicConfig(level=logging.DEBUG)
# logging.info('test')
```

```python
# import warnings
# with warnings.catch_warnings():
#     warnings.simplefilter("ignore")
#     warnings.filterwarnings("ignore", module='xxx_module_xxx')
#     warnings.filterwarnings('error', category=RuntimeWarning, module='empyrical.stats')

# with warnings.catch_warnings(record=True) as w:
#     # Cause all warnings to always be triggered.
#     warnings.simplefilter("always")

# warnings.filterwarnings('error, message='', category=Warning, module='', lineno=0, append=False)
# warnings.filterwarnings('error', module='pandas')
# warnings.filterwarnings('ignore', module='numba_scipy')
# warnings.filterwarnings('ignore', module='zipline.assets')
```

```python
N_subjects = 1000

X = stats.norm(loc=0, scale=10).rvs(size=(N_subjects,1), random_state=np.random.RandomState(43))
X[:5,:]
```

    array([[ 2.57399925],
           [-9.08481433],
           [-3.78503106],
           [-5.34915599],
           [ 8.58073346]])

```python
a = 0.5
b = 1
y = a * X + b
y = y + stats.norm(loc=0, scale=0.5).rvs(size=(N_subjects,1), random_state=np.random.RandomState(44))
y[:5,:]
```

    array([[ 1.91169227],
           [-2.8842285 ],
           [-0.26944552],
           [-2.47703586],
           [ 4.55629489]])

```python
x_min = np.min(X)
x_max = np.max(X)
x_lin = np.linspace(x_min, x_max, 100)
y_lin = a * x_lin + b
```

```python
lr_poly = sklearn.linear_model.LinearRegression()
lr_poly.fit(X, y)
lr_poly.intercept_, lr_poly.coef_
```

    (array([0.99580863]), array([[0.49856981]]))

```python
plt.figure(figsize=(15, 8), dpi=80, facecolor='w', edgecolor='k')
ax = plt.subplot(1, 1, 1)
ax.scatter(X,y)
ax.plot(x_lin,y_lin)
```

# LaTeX

Here is a `newcommand` (see in the source):
$$\newcommand{gpvec}[1]{{\bf #1}}$$
$$\newcommand{bfvec}[1]{{\pmb{#1}}}$$
$$p(f_*|x_*, X, y) = \mathcal{N}\left(\frac{1}{\sigma_n^2}\gpvec{x_*}^TA^{-1}X\gpvec{y},\; \gpvec{x_*}^TA^{-1}\bfvec{x_*}\right)$$
$$A=\frac{1}{\sigma_n^2}XX^T+\Sigma_p^{-1}$$

$\displaystyle
\begin{array}{rcl}
\text{μ} & \sim & a\\
\text{σ} & \sim & b\\
\end{array}
$

$$
\begin{align}
s_{t+1}=\begin{pmatrix}\beta_{t+1}\\\alpha_{t+1}\end{pmatrix}&=&\begin{pmatrix}\beta_t\\\alpha_t\end{pmatrix}+\eta_t&,\qquad& \eta_t \sim N(0, I_2\sigma_\eta^2)\\
y_t &=& Z_t\cdot s_t + \varepsilon_t = \begin{pmatrix}x_t&1\end{pmatrix}\cdot\begin{pmatrix}\beta_t\\\alpha_t\end{pmatrix} + \varepsilon_t =\beta_tx_t+\alpha_t \
&,\qquad&\varepsilon_t \sim N(0, \sigma_\varepsilon^2)
\end{align}
$$

Given a joint distribution:
$$\left[\begin{matrix}x\\ y\end{matrix}\right]\sim\mathcal{N}\left(
\left[\begin{matrix}\mu_x\\\mu_y\end{matrix}\right],
\left[\begin{matrix}A&C\\ C^T&B\end{matrix}\right]
\right)$$

We get the conditional distribution analytically as follows:

$$p(x\,|\, y)\sim\mathcal{N}(\mu_x+CB^{-1}(y-\mu_y), A-CB^{-1}C^T)$$

And as we set the mean to $0$ this reduces to:

$$p(x\,|\, y)\sim\mathcal{N}(CB^{-1}y, A-CB^{-1}C^T)$$

# SymPy

```python
import sympy
sympy.init_printing()
```

```python
def myfn(x):
    return (x+1)*(x-2)*(x-2.5)*(x-4)

symx = sympy.symbols('x')
sympy.expand(myfn(symx))
```

```python
sx = sympy.symbols('x')
sfx = \
      -0.0000009957371   * sx**16 + \
       0.000056100257831 * sx**15 + \
      -0.0014371151322   * sx**14 + \
       0.022152182049259 * sx**13 + \
      -0.229167949577895 * sx**12 + \
       1.680307250977133 * sx**11 + \
      -8.98984498449258  * sx**10 + \
      35.59846747504916  * sx** 9 + \
    -104.74198119349185  * sx**8  + \
     227.94559624952703  * sx**7  + \
    -362.2994941897943   * sx**6  + \
     411.6650485438511   * sx**5  + \
    -323.9001750849299   * sx**4  + \
     168.63970442305256  * sx**3  + \
     -54.90472890518606  * sx**2  + \
      11.515498193577798 * sx
sfx2 = sfx**2
isfx2 = sympy.Integral(sfx2, sx)
isfx2
```

```python
isfx2 = sympy.integrate(sfx2, (sx,0,6.56))
isfx2*sympy.pi
```

```python
precision=50
sympy.N(isfx2*sympy.pi, precision)
```

```python
sympy.plotting.plot(sfx, (sx, 0, 6.56))
```

```python
sa, sb, sc, sd = sympy.symbols(['a', 'b', 'c', 'd'])
se, sf, sg, sh = sympy.symbols('e f g h')
```

```python
sm = sympy.Matrix([[sa, sb], [sc, sd]])
sm, sm.inv()
```

```python
sympy.S("{:6.2f}".format(1.23))
```

```python
solution = sympy.solveset(sympy.Eq(sx**2 - 1, 0), sx)
solution
```

```python
list(solution)[0]
```


```python
import open3d as o3d
import numpy as np
import copy
import os
import sys

# monkey patches visualization and provides helpers to load geometries
sys.path.append('..')
import open3d_tutorial as o3dtut
# change to True if you want to interact with the visualization windows
o3dtut.interactive = not "CI" in os.environ
```

# Mesh
Open3D has a data structure for 3D triangle meshes called `TriangleMesh`.
The code below shows how to read a triangle mesh from a `ply` file and print its vertices and triangles.

```python
print("Testing mesh in Open3D...")
armadillo_mesh = o3d.data.ArmadilloMesh()
mesh = o3d.io.read_triangle_mesh(armadillo_mesh.path)

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
print(mesh)
print('Vertices:')
print(np.asarray(mesh.vertices))
print('Triangles:')
print(np.asarray(mesh.triangles))
```

The `TriangleMesh` class has a few data fields such as `vertices` and `triangles`. Open3D provides direct memory access to these fields via numpy.

## Visualize a 3D mesh

```python
print("Try to render a mesh with normals (exist: " +
      str(mesh.has_vertex_normals()) + ") and colors (exist: " +
      str(mesh.has_vertex_colors()) + ")")
o3d.visualization.draw_geometries([mesh])
print("A mesh with no normals and no colors does not look good.")
```

You can rotate and move the mesh but it is painted with uniform gray color and does not look `3d`. The reason is that the current mesh does not have normals for vertices or faces. So uniform color shading is used instead of a more sophisticated Phong shading.

## Surface normal estimation
Let’s draw the mesh with surface normals.

```python
print("Computing normal and rendering it.")
mesh.compute_vertex_normals()
print(np.asarray(mesh.triangle_normals))
o3d.visualization.draw_geometries([mesh])
```

It uses `compute_vertex_normals` and `paint_uniform_color` which are member functions of `mesh`.

## Crop mesh
We remove half of the surface by directly operating on the `triangle` and `triangle_normals` data fields of the mesh. This is done via numpy.

```python
print("We make a partial mesh of only the first half triangles.")
mesh1 = copy.deepcopy(mesh)
mesh1.triangles = o3d.utility.Vector3iVector(
    np.asarray(mesh1.triangles)[:len(mesh1.triangles) // 2, :])
mesh1.triangle_normals = o3d.utility.Vector3dVector(
    np.asarray(mesh1.triangle_normals)[:len(mesh1.triangle_normals) // 2, :])
print(mesh1.triangles)
o3d.visualization.draw_geometries([mesh1])
```

## Paint mesh
`paint_uniform_color` paints the mesh with a uniform color. The color is in RGB space, [0, 1] range.

```python
print("Painting the mesh")
mesh1.paint_uniform_color([1, 0.706, 0])
o3d.visualization.draw_geometries([mesh1])
```

## Mesh properties
A triangle mesh has several properties that can be tested with Open3D. One important property is the manifold property, where we can test the triangle mesh if it is edge manifold `is_edge_manifold` and if it is `is_vertex_manifold`. A triangle mesh is edge manifold, if each edge is bounding either one or two triangles. The function `is_edge_manifold` has the `bool` parameter `allow_boundary_edges` that defines if boundary edges should be allowed. Further, a triangle mesh is vertex manifold if the star of the vertex is edge-manifold and edge-connected, e.g., two or more faces connected only by a vertex and not by an edge.

Another property is the test of self-intersection. The function `is_self_intersecting` returns `True` if there exists a triangle in the mesh that is intersecting another mesh. A watertight mesh can be defined as a mesh that is edge manifold, vertex manifold and not self intersecting. The function `is_watertight` implements this check in Open3D.

We also can test the triangle mesh, if it is orientable, i.e. the triangles can be oriented in such a way that all normals point towards the outside. The corresponding function in Open3D is called `is_orientable`.

The code below tests a number of triangle meshes against those properties and visualizes the results. Non-manifold edges are shown in red, boundary edges in green, non-manifold vertices are visualized as green points, and self-intersecting triangles are shown in pink.

```python
def check_properties(name, mesh):
    mesh.compute_vertex_normals()

    edge_manifold = mesh.is_edge_manifold(allow_boundary_edges=True)
    edge_manifold_boundary = mesh.is_edge_manifold(allow_boundary_edges=False)
    vertex_manifold = mesh.is_vertex_manifold()
    self_intersecting = mesh.is_self_intersecting()
    watertight = mesh.is_watertight()
    orientable = mesh.is_orientable()

    print(name)
    print(f"  edge_manifold:          {edge_manifold}")
    print(f"  edge_manifold_boundary: {edge_manifold_boundary}")
    print(f"  vertex_manifold:        {vertex_manifold}")
    print(f"  self_intersecting:      {self_intersecting}")
    print(f"  watertight:             {watertight}")
    print(f"  orientable:             {orientable}")

    geoms = [mesh]
    if not edge_manifold:
        edges = mesh.get_non_manifold_edges(allow_boundary_edges=True)
        geoms.append(o3dtut.edges_to_lineset(mesh, edges, (1, 0, 0)))
    if not edge_manifold_boundary:
        edges = mesh.get_non_manifold_edges(allow_boundary_edges=False)
        geoms.append(o3dtut.edges_to_lineset(mesh, edges, (0, 1, 0)))
    if not vertex_manifold:
        verts = np.asarray(mesh.get_non_manifold_vertices())
        pcl = o3d.geometry.PointCloud(
            points=o3d.utility.Vector3dVector(np.asarray(mesh.vertices)[verts]))
        pcl.paint_uniform_color((0, 0, 1))
        geoms.append(pcl)
    if self_intersecting:
        intersecting_triangles = np.asarray(
            mesh.get_self_intersecting_triangles())
        intersecting_triangles = intersecting_triangles[0:1]
        intersecting_triangles = np.unique(intersecting_triangles)
        print("  # visualize self-intersecting triangles")
        triangles = np.asarray(mesh.triangles)[intersecting_triangles]
        edges = [
            np.vstack((triangles[:, i], triangles[:, j]))
            for i, j in [(0, 1), (1, 2), (2, 0)]
        ]
        edges = np.hstack(edges).T
        edges = o3d.utility.Vector2iVector(edges)
        geoms.append(o3dtut.edges_to_lineset(mesh, edges, (1, 0, 1)))
    o3d.visualization.draw_geometries(geoms, mesh_show_back_face=True)
```

```python
knot_mesh_data = o3d.data.KnotMesh()
knot_mesh = o3d.io.read_triangle_mesh(knot_mesh_data.path)
check_properties('KnotMesh', knot_mesh)
check_properties('Mobius', o3d.geometry.TriangleMesh.create_mobius(twists=1))
check_properties("non-manifold edge", o3dtut.get_non_manifold_edge_mesh())
check_properties("non-manifold vertex", o3dtut.get_non_manifold_vertex_mesh())
check_properties("open box", o3dtut.get_open_box_mesh())
check_properties("intersecting_boxes", o3dtut.get_intersecting_boxes_mesh())
```

## Mesh filtering
Open3D contains a number of methods to filter meshes. In the following we show the implemented filters to smooth noisy triangle meshes.

### Average filter
The simplest filter is the average filter. A given vertex $v_i$ is given by the average of the adjacent vertices $\mathcal{N}$

\begin{equation}
v_i = \frac{v_i + \sum_{n \in \mathcal{N}} v_n}{|N| + 1} \,.
\end{equation}

This filter can be used to denoise meshes as demonstrated in the code below. The parameter `number_of_iterations` in the function `filter_smooth_simple` defines the how often the filter is applied to the mesh.

```python
print('create noisy mesh')
knot_mesh = o3d.data.KnotMesh()
mesh_in = o3d.io.read_triangle_mesh(knot_mesh.path)
vertices = np.asarray(mesh_in.vertices)
noise = 5
vertices += np.random.uniform(0, noise, size=vertices.shape)
mesh_in.vertices = o3d.utility.Vector3dVector(vertices)
mesh_in.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_in])

print('filter with average with 1 iteration')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations=1)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('filter with average with 5 iterations')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations=5)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
```

### Laplacian
Another important mesh filter is the Laplacian defined as

\begin{equation}
v_i = v_i \cdot \lambda \sum_{n \in N} w_n v_n - v_i \,,
\end{equation}

where $\lambda$ is the strength of the filter and $w_n$ are normalized weights that relate to the distance of the neighboring vertices. The filter is implemented in `filter_smooth_laplacian` and has the parameters `number_of_iterations` and `lambda`.

```python
print('filter with Laplacian with 10 iterations')
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('filter with Laplacian with 50 iterations')
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=50)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
```

### Taubin filter
The problem with the average and Laplacian filter is that they lead to a shrinkage of the triangle mesh. [\[Taubin1995\]](../reference.html#Taubin1995) showed that the application of two Laplacian filters with different $\lambda$ parameters can prevent the mesh shrinkage. The filter is implemented in `filter_smooth_taubin`.

```python
print('filter with Taubin with 10 iterations')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('filter with Taubin with 100 iterations')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=100)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
```

## Sampling
Open3D includes functions to sample point clouds from a triangle mesh. The simplest method is `sample_points_uniformly` that uniformly samples points from the 3D surface based on the triangle area. The parameter `number_of_points` defines how many points are sampled from the triangle surface.

```python
mesh = o3d.geometry.TriangleMesh.create_sphere()
mesh.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh])
pcd = mesh.sample_points_uniformly(number_of_points=500)
o3d.visualization.draw_geometries([pcd])
```

```python
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()

o3d.visualization.draw_geometries([mesh])
pcd = mesh.sample_points_uniformly(number_of_points=500)
o3d.visualization.draw_geometries([pcd])
```

Uniform sampling can yield clusters of points on the surface, while a method called Poisson disk sampling can evenly distribute the points on the surface. The method `sample_points_poisson_disk` implements sample elimination. It starts with a sampled point cloud and removes points to satisfy the sampling criterion. The method supports two options to provide the initial point cloud:

1. Default via the parameter `init_factor`: The method first samples uniformly a point cloud from the mesh with `init_factor` x `number_of_points` and uses this for the elimination.
2. One can provide a point cloud and pass it to the `sample_points_poisson_disk` method. Then, this point cloud is used for elimination.

```python
mesh = o3d.geometry.TriangleMesh.create_sphere()
pcd = mesh.sample_points_poisson_disk(number_of_points=500, init_factor=5)
o3d.visualization.draw_geometries([pcd])

pcd = mesh.sample_points_uniformly(number_of_points=2500)
pcd = mesh.sample_points_poisson_disk(number_of_points=500, pcl=pcd)
o3d.visualization.draw_geometries([pcd])
```

```python
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()

pcd = mesh.sample_points_poisson_disk(number_of_points=500, init_factor=5)
o3d.visualization.draw_geometries([pcd])

pcd = mesh.sample_points_uniformly(number_of_points=2500)
pcd = mesh.sample_points_poisson_disk(number_of_points=500, pcl=pcd)
o3d.visualization.draw_geometries([pcd])
```

## Mesh subdivision
In mesh subdivision we divide each triangle into a number of smaller triangles. In the simplest case, we compute the midpoint of each side per triangle and divide the triangle into four smaller triangles. This is implemented in the `subdivide_midpoint` function. The 3D surface and area stays the same, but the number of vertices and triangles increases. The parameter `number_of_iterations` defines how many times this process should be repeated.

```python
mesh = o3d.geometry.TriangleMesh.create_box()
mesh.compute_vertex_normals()
print(
    f'The mesh has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
mesh = mesh.subdivide_midpoint(number_of_iterations=1)
print(
    f'After subdivision it has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
```

Open3D implements an additional subdivision method based on [\[Loop1987\]](../reference.html#Loop1987). The method is based on a quartic box spline, which generates $C^2$ continuous limit surfaces everywhere except at extraordinary vertices where they are $C^1$ continuous. This leads to smoother corners.

```python
mesh = o3d.geometry.TriangleMesh.create_sphere()
mesh.compute_vertex_normals()
print(
    f'The mesh has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
mesh = mesh.subdivide_loop(number_of_iterations=2)
print(
    f'After subdivision it has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
```

```python
knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
mesh.compute_vertex_normals()
print(
    f'The mesh has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
mesh = mesh.subdivide_loop(number_of_iterations=1)
print(
    f'After subdivision it has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], zoom=0.8, mesh_show_wireframe=True)
```

## Mesh simplification
Sometimes we want to represent a high-resolution mesh with fewer triangles and vertices, but the low-resolution mesh should still be close to the high-resolution mesh. For this purpose Open3D implements a number of mesh simplification methods.

### Vertex clustering
The vertex clustering method pools all vertices that fall into a voxel of a given size to a single vertex. The method is implemented in `simplify_vertex_clustering` and has as parameters `voxel_size` that defines the size of the voxel grid and `contraction` that defines how the vertices are pooled. `o3d.geometry.SimplificationContraction.Average` computes a simple average.

```python
bunny = o3d.data.BunnyMesh()
mesh_in = o3d.io.read_triangle_mesh(bunny.path)
mesh_in.compute_vertex_normals()

print(
    f'Input mesh has {len(mesh_in.vertices)} vertices and {len(mesh_in.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_in])

voxel_size = max(mesh_in.get_max_bound() - mesh_in.get_min_bound()) / 32
print(f'voxel_size = {voxel_size:e}')
mesh_smp = mesh_in.simplify_vertex_clustering(
    voxel_size=voxel_size,
    contraction=o3d.geometry.SimplificationContraction.Average)
print(
    f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])

voxel_size = max(mesh_in.get_max_bound() - mesh_in.get_min_bound()) / 16
print(f'voxel_size = {voxel_size:e}')
mesh_smp = mesh_in.simplify_vertex_clustering(
    voxel_size=voxel_size,
    contraction=o3d.geometry.SimplificationContraction.Average)
print(
    f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
```

### Mesh decimation
Another category of mesh simplification methods is mesh decimation that operates in incremental steps. We select a single triangle that minimizes an error metric and removes it. This is repeated until a required number of triangles is achieved. Open3D implements `simplify_quadric_decimation` that minimizes error quadrics (distances to neighboring planes). The parameter `target_number_of_triangles` defines the stopping criteria of the decimation algorithm.

```python
mesh_smp = mesh_in.simplify_quadric_decimation(target_number_of_triangles=6500)
print(
    f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])

mesh_smp = mesh_in.simplify_quadric_decimation(target_number_of_triangles=1700)
print(
    f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
```

## Connected components
The result of various reconstruction methods. Open3D implements a connected components algorithm `cluster_connected_triangles` that assigns each triangle to a cluster of connected triangles. It returns for each triangle the index of the cluster in `triangle_clusters`, and per cluster the number of triangles in `cluster_n_triangles` and the surface area of the cluster in `cluster_area`.

This is useful in for instance [RGBD Integration](../pipelines/rgbd_integration.ipynb), which is not always a single triangle mesh, but a number of meshes. Some of the smaller parts are due to noise and we most likely want to remove them.

The code below shows the application of `cluster_connected_triangles` and how it can be used to remove spurious triangles.

```python
print("Generate data")
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()

mesh = mesh.subdivide_midpoint(number_of_iterations=2)
vert = np.asarray(mesh.vertices)
min_vert, max_vert = vert.min(axis=0), vert.max(axis=0)
for _ in range(30):
    cube = o3d.geometry.TriangleMesh.create_box()
    cube.scale(0.005, center=cube.get_center())
    cube.translate(
        (
            np.random.uniform(min_vert[0], max_vert[0]),
            np.random.uniform(min_vert[1], max_vert[1]),
            np.random.uniform(min_vert[2], max_vert[2]),
        ),
        relative=False,
    )
    mesh += cube
mesh.compute_vertex_normals()
print("Show input mesh")
o3d.visualization.draw_geometries([mesh])
```

```python
print("Cluster connected triangles")
with o3d.utility.VerbosityContextManager(
        o3d.utility.VerbosityLevel.Debug) as cm:
    triangle_clusters, cluster_n_triangles, cluster_area = (
        mesh.cluster_connected_triangles())
triangle_clusters = np.asarray(triangle_clusters)
cluster_n_triangles = np.asarray(cluster_n_triangles)
cluster_area = np.asarray(cluster_area)
```

```python
print("Show mesh with small clusters removed")
mesh_0 = copy.deepcopy(mesh)
triangles_to_remove = cluster_n_triangles[triangle_clusters] < 100
mesh_0.remove_triangles_by_mask(triangles_to_remove)
o3d.visualization.draw_geometries([mesh_0])
```

```python
print("Show largest cluster")
mesh_1 = copy.deepcopy(mesh)
largest_cluster_idx = cluster_n_triangles.argmax()
triangles_to_remove = triangle_clusters != largest_cluster_idx
mesh_1.remove_triangles_by_mask(triangles_to_remove)
o3d.visualization.draw_geometries([mesh_1])

```


## This Jupyter notebook is available at https://github.com/dkp-quantum/Tutorials

## Further Information

#### * Qiskit: https://qiskit.org

#### * Qiskit GitHub: https://github.com/Qiskit

## What is Qiskit?

* Quantum Information Science Kit

* Open-source SDK for working with quantum computers at the level of pulses, circuits and algorithms

* Founded by IBM Research in 2017 to allow software development for their cloud quantum computing service known as IBM Q Experience.

* The primary version of Qiskit uses the Python programming language. It is used to create quantum programs based on the OpenQASM (Open Quantum Assembly Language) representation of quantum instructions.

## What can we do with Qiskit?

* Access to circuits
* Access to a library of quantum algorithms
* Access to quantum hardware
* Access to optimization against noise

## IBM Quantum Experience Backend

* 9 real quantum devices with number of qubits 1, 5, and 16.
* 32-qubit simulator

## Main simulator backends

1. __QASM Simulator__: It emulates execution of a quantum circuits on a real device and returns __measurement counts__. It includes highly configurable noise models and can even be loaded with automatically generated approximate noise models based on the calibration parameters of actual hardware devices.

    * What is __QASM__? QASM = Quantum Assembly Language (see https://arxiv.org/abs/1707.03429 for more detail)

2. __Statevector Simulator__: It simulates the ideal execution of a quantum circuit and returns __the final quantum state vector__ of the device at the end of simulation.

3. __Unitary Simulator__: It allows simulation of the final unitary matrix implemented by an ideal quantum circuit. This only works if all the elements in the circuit are unitary operations.

## How to start

1. Start Online: https://quantum-computing.ibm.com

## How to start

2. Start Locally

* Qiskit is tested and supported on the following 64-bit systems:

    * Ubuntu 16.04 or later
    * macOS 10.12.6 or later
    * Windows 7 or later

* To install Qiskit locally, you will need Python 3.5+.

* IBM recommends using a virtual environment with Anaconda, a cross-platform Python distribution for scientific computing: https://www.anaconda.com/products/individual

## Let's start on Mac

## Let's start on Windows

## Let's start on Windows

## Let's start on Windows

```python
%matplotlib inline
# Importing standard Qiskit libraries and configuring account
from qiskit import *
from qiskit.visualization import *
import numpy as np
```

```python
# Create a quantum register with 2 qubits
q = QuantumRegister(2,'q')
# Form a quantum circuit
# Note that the circuit name is optional
qc = QuantumCircuit(q,name="first_qc")
# Display the quantum circuit
qc.draw()
```

q_0: |0>

q_1: |0>

## Qiskit quantum operations summary:
###  https://qiskit.org/documentation/tutorials/circuits/3_summary_of_quantum_operations.html

```python
# Add a Hadamard gate on qubit 0, putting this in superposition.
qc.h(0)
# Add a CX (CNOT) gate on control qubit 0
# and target qubit 1 to create an entangled state.
qc.cx(0, 1)
qc.draw()
```

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

```python
# Create a classical register with 2 bits
c = ClassicalRegister(2,'c')

meas = QuantumCircuit(q,c,name="first_m")
meas.barrier(q)
meas.measure(q, c)

meas.draw()
```

         ░ ┌─┐
q_0: |0>─░─┤M├───
         ░ └╥┘┌─┐
q_1: |0>─░──╫─┤M├
         ░  ║ └╥┘
 c_0: 0 ════╩══╬═
               ║
 c_1: 0 ═══════╩═

```python
# Quantum circuits can be added with + operations
# Add two pre-defined circuits
qc_all=qc+meas
qc_all.draw()
```

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

```python
# Draw the quantum circuit in a different (slightly better) format
qc_all.draw(output='mpl')
```

```python
# Create the quantum circuit with the measurement in one go.
qc_all = QuantumCircuit(q,c,name="2q_all")
qc_all.h(0)
qc_all.cx(0,1)
qc_all.barrier()
qc_all.measure(0,0)
qc_all.measure(1,1)
qc_all.draw(output='mpl')
```

```python
# Use Aer's qasm_simulator
backend_q = Aer.get_backend('qasm_simulator')

# Execute the circuit on the qasm simulator.
job_sim1 = execute(qc_all, backend_q, shots=4096)
```

```python
job_sim1.status()
```

    <JobStatus.DONE: 'job has successfully run'>

```python
# Grab the results from the job.
result_sim1 = job_sim1.result()
result_sim1
```

    namespace(backend_name='qasm_simulator',
              backend_version='0.3.2',
              qobj_id='944ac6fa-832c-4ecd-a579-f14eb1b1be04',
              job_id='7d81d747-8230-4474-b9d5-f73191109997',
              success=True,
              results=[namespace(shots=4096,
                                 success=True,
                                 data=namespace(counts=namespace(0x0=1995,
                                                                 0x3=2101)),
                                 meas_level=2,
                                 header=namespace(memory_slots=2,
                                                  clbit_labels=[['c', 0],
                                                                ['c', 1]],
                                                  name='circuit2',
                                                  qubit_labels=[['q', 0],
                                                                ['q', 1]],
                                                  n_qubits=2,
                                                  qreg_sizes=[['q', 2]],
                                                  creg_sizes=[['c', 2]]),
                                 status='DONE',
                                 time_taken=0.010838524,
                                 seed_simulator=4177817008,
                                 metadata={'measure_sampling': True,
                                           'method': 'stabilizer',
                                           'parallel_shots': 1,
                                           'parallel_state_update': 8})],
              date=datetime.datetime(2020, 8, 5, 11, 22, 8, 660150),
              header=namespace(backend_name='qasm_simulator',
                               backend_version='0.3.2'),
              status='COMPLETED',
              time_taken=0.028506040573120117,
              metadata={'max_memory_mb': 4096,
                        'omp_enabled': True,
                        'parallel_experiments': 1,
                        'time_taken': 0.011715323000000001})

```python
result_sim1.get_counts(qc_all)
```

    {'00': 1995, '11': 2101}

```python
plot_histogram(result_sim1.get_counts(qc_all))
```

```python
# Use Aer's statevector_simulator
backend_sv = Aer.get_backend('statevector_simulator')

# Execute the circuit on the statevector simulator.
# It is important to note that the measurement has been excluded
job_sim2 = execute(qc, backend_sv)
```

```python
# Grab the results from the job.
result_sim2 = job_sim2.result()
# Output the entire result
result_sim2
```

    namespace(backend_name='statevector_simulator',
              backend_version='0.3.2',
              qobj_id='1861012f-18e1-4186-80eb-848ae00b8cad',
              job_id='2e6f72fa-2de1-4166-8414-d03ce5c6ec02',
              success=True,
              results=[namespace(shots=1,
                                 success=True,
                                 data=namespace(statevector=[(0.7071067811865476+0j),
                                                             0j,
                                                             0j,
                                                             (0.7071067811865475+0j)]),
                                 meas_level=2,
                                 header=namespace(memory_slots=0,
                                                  clbit_labels=[],
                                                  name='first_qc',
                                                  qubit_labels=[['q', 0],
                                                                ['q', 1]],
                                                  n_qubits=2,
                                                  qreg_sizes=[['q', 2]],
                                                  creg_sizes=[]),
                                 status='DONE',
                                 time_taken=0.000285063,
                                 seed_simulator=1215084373,
                                 metadata={'parallel_shots': 1,
                                           'parallel_state_update': 8})],
              date=datetime.datetime(2020, 8, 5, 11, 28, 16, 708104),
              header=namespace(backend_name='statevector_simulator',
                               backend_version='0.3.2'),
              status='COMPLETED',
              time_taken=0.006955862045288086,
              metadata={'max_memory_mb': 4096,
                        'omp_enabled': True,
                        'parallel_experiments': 1,
                        'time_taken': 0.002053295})

```python
# See output state as a vector
outputstate = result_sim2.get_statevector(qc, decimals=5)
print(outputstate)

# Visualize density matrix
plot_state_city(outputstate)
```

```python
# Create the quantum circuit with the measurement in one go.
qc_3 = QuantumCircuit(3,3,name="qc_bloch")
qc_3.x(1)
qc_3.h(2)
qc_3.barrier()
qc_3.draw(output='mpl')
```

```python
# Execute the circuit on the statevector simulator.
# It is important to note that the measurement has been excluded
job_sim_bloch = execute(qc_3, backend_sv)

# Grab the results from the job.
result_sim_bloch = job_sim_bloch.result()

# See output state as a vector
output_bloch = result_sim_bloch.get_statevector(qc_3, decimals=5)

# Draw on the Bloch sphere
plot_bloch_multivector(output_bloch)
```

```python
# Use Aer's unitary_simulator
backend_u = Aer.get_backend('unitary_simulator')

# Execute the circuit on the unitary simulator.
job_usim = execute(qc, backend_u)

# Grab the results from the job.
result_usim = job_usim.result()
result_usim
```

    namespace(backend_name='unitary_simulator',
              backend_version='0.3.2',
              qobj_id='5518d9ba-fed3-4b4c-a9aa-a92c72010b39',
              job_id='e3d1367f-6943-4fa0-a8ef-25156827f137',
              success=True,
              results=[namespace(shots=1,
                                 success=True,
                                 data=namespace(unitary=[[(0.7071067811865476+0j),
                                                          (0.7071067811865475+0j),
                                                          0j,
                                                          0j],
                                                         [0j,
                                                          0j,
                                                          (0.7071067811865475+0j),
                                                          (-0.7071067811865476+0j)],
                                                         [0j,
                                                          0j,
                                                          (0.7071067811865476+0j),
                                                          (0.7071067811865475+0j)],
                                                         [(0.7071067811865475+0j),
                                                          (-0.7071067811865476+0j),
                                                          0j,
                                                          0j]]),
                                 meas_level=2,
                                 status='DONE',
                                 header=namespace(qreg_sizes=[['q', 2]],
                                                  creg_sizes=[],
                                                  n_qubits=2,
                                                  name='first_qc',
                                                  qubit_labels=[['q', 0],
                                                                ['q', 1]],
                                                  memory_slots=0,
                                                  clbit_labels=[]),
                                 seed_simulator=2335392040,
                                 time_taken=9.462500000000001e-05,
                                 metadata={'parallel_shots': 1,
                                           'parallel_state_update': 8})],
              status='COMPLETED',
              header=namespace(backend_version='0.3.2',
                               backend_name='unitary_simulator'),
              date=datetime.datetime(2020, 8, 3, 15, 36, 22, 517107),
              time_taken=0.002722024917602539,
              metadata={'max_memory_mb': 4096,
                        'omp_enabled': True,
                        'parallel_experiments': 1,
                        'time_taken': 0.00021459200000000002})

```python
# Output the unitary matrix
unitary = result_usim.get_unitary(qc)
print('%s\n' % unitary)
```

    [[ 0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j]
     [ 0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j]
     [ 0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j]
     [ 0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j]]

## Exercise 1:
## Design a quantum circuit to create the following 2-qubit entangled state: $\frac{|01\rangle+|10\rangle}{\sqrt{2}}$.
## Check the answer with the QASM simulation and by plotting the histogram of the measurement statistics.

```python
# Create the quantum circuit with the measurement in one go.
qc_ex1 = QuantumCircuit(q,c,name="ex1")
```

```python
# Put the first qubit in equal superposition
qc_ex1.h(0)
```

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

```python
# Rest of the circuit
qc_ex1.x(1)
qc_ex1.cx(0,1)
qc_ex1.barrier()
qc_ex1.measure(0,0)
qc_ex1.measure(1,1)
qc_ex1.draw(output='mpl')
```

```python
# Execute the circuit on the qasm simulator.
job_ex1 = execute(qc_ex1, backend_q, shots=4096)
```

```python
job_ex1.status()
```

    <JobStatus.DONE: 'job has successfully run'>

```python
# Grab the results from the job.
result_ex1 = job_ex1.result()
result_ex1.get_counts(qc_ex1)
```

    {'10': 2117, '01': 1979}

```python
plot_histogram(result_ex1.get_counts(qc_ex1))
```

```python
# Or get the histogram in one go
plot_histogram(job_ex1.result().get_counts(qc_ex1))
```

## Exercise 2:
## Design a quantum circuit that creates a 3-qubit entangled state: $\alpha|000\rangle+\beta|111\rangle$,
## such that $|\alpha|^2 = 0.25$, and $|\beta|^2 = 0.75$.
## Check the answer with the QASM simulation and by plotting the histogram of the measurement statistics.

### Hint:
$R_y(\theta) = \begin{bmatrix} \cos(\theta/2) & -\sin(\theta/2)\\ \sin(\theta/2) & \cos(\theta/2)\end{bmatrix}$ can be implemented by the qiskit code: `qc.ry(theta,q)`

```python
# Create a quantum register with 3 qubits
q3 = QuantumRegister(3,'q')
# Create a classical register with 3 qubits
c3 = ClassicalRegister(3,'c')

# Create the quantum circuit with the measurement in one go.
qc_ex2 = QuantumCircuit(q3,c3,name="ex1")

qc_ex2.ry(2*np.pi/3,0)
qc_ex2.cx(0,1)
qc_ex2.cx(1,2)
qc_ex2.barrier()
qc_ex2.measure(q3,c3)
qc_ex2.draw(output='mpl')
```

```python
# Execute the circuit on the qasm simulator.
job_ex2 = execute(qc_ex2, backend_q, shots=4096)

# Grab the results from the job.
result_ex2 = job_ex2.result()
plot_histogram(result_ex2.get_counts(qc_ex2))
```

## 3-qubit gate example: Toffoli gate

```python
# Create a quantum register with 3 qubits
q3 = QuantumRegister(3,'q')
# Create a classical register with 3 qubits
c3 = ClassicalRegister(3,'c')

# Create the quantum circuit without a Toffoli gate
qc_toff = QuantumCircuit(q3,c3,name="ex1")

qc_toff.ry(2*np.pi/3,0)
qc_toff.h(1)
qc_toff.h(2)
qc_toff.barrier()
qc_toff.measure(q3,c3)
qc_toff.draw(output='mpl')
```

```python
# Execute the circuit on the qasm simulator.
job_toff = execute(qc_toff, backend_q, shots=4096)

# Grab the results from the job.
result_toff = job_toff.result()
plot_histogram(result_toff.get_counts(qc_toff))
```

```python
# Now, add a Toffoli gate
qc_toff = QuantumCircuit(q3,c3,name="ex1")

qc_toff.ry(2*np.pi/3,0)
qc_toff.h(1)
qc_toff.h(2)
qc_toff.ccx(1,2,0)
qc_toff.barrier()
qc_toff.measure(q3,c3)
qc_toff.draw(output='mpl')
```

```python
# Execute the circuit on the qasm simulator.
job_toff = execute(qc_toff, backend_q, shots=4096)

# Grab the results from the job.
result_toff = job_toff.result()
plot_histogram(result_toff.get_counts(qc_toff))
```

## Native single-qubit gates of IBM Q devices

One way to write a general form of a single qubit unitary:

$$
U(\theta,\phi,\lambda)=\begin{bmatrix}
 \cos(\theta/2) & -e^{i\lambda}\sin(\theta/2) \\
e^{i\phi}\sin(\theta/2) & e^{i(\lambda+\phi)}\cos(\theta/2)
\end{bmatrix}
$$
***
Native single qubit gates:
* `u3`=$U(\theta,\phi,\lambda)$
* `u2`=$U(\pi/2,\phi,\lambda)$
* `u1`=$U(0,0,\lambda)$

Native two qubit gate:
* controlled-NOT

### But why such form? $\rightarrow$ This is related to gate implementations on real devices

### Note that $$
U(\theta,\phi,\lambda)=\begin{bmatrix}
 \cos(\theta/2) & -e^{i\lambda}\sin(\theta/2) \\
e^{i\phi}\sin(\theta/2) & e^{i(\lambda+\phi)}\cos(\theta/2)
\end{bmatrix}
$$
### can be written as:
### \begin{align}
U(\theta,\phi,\lambda)&=R_z(\phi)R_y(\theta)R_z(\lambda)\\
&=R_z(\phi)R_x(-\pi/2)R_z(\theta)R_x(\pi/2)R_z(\lambda)
\end{align}

### In RF/MW based quantum control, $R_z$ is given for free, and $R_x(\pm\pi/2)$ can be calibrated with high precision.

## Running Quantum Circuits on IBM Q

### Need IBM Token for running an experiment on a IBM cloud quantum computer:  https://quantum-computing.ibm.com

```python
IBMQ.disable_account()
provider = IBMQ.enable_account('IBM_TOKEN')
```

```python
# provider = IBMQ.get_provider(hub='ibm-q-research')
```

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

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

```python
from qiskit.tools.monitor import backend_overview, backend_monitor, job_monitor
from qiskit.tools.visualization import plot_gate_map, plot_error_map
```

```python
# Retrieve IBM Quantum device information
backend_overview()
```

    ibmq_rome                    ibmq_armonk                  ibmq_essex
    ---------                    -----------                  ----------
    Num. Qubits:  5              Num. Qubits:  1              Num. Qubits:  5
    Pending Jobs: 6              Pending Jobs: 2              Pending Jobs: 70
    Least busy:   False          Least busy:   False          Least busy:   False
    Operational:  True           Operational:  True           Operational:  True
    Avg. T1:      77.2           Avg. T1:      149.4          Avg. T1:      104.2
    Avg. T2:      109.1          Avg. T2:      225.6          Avg. T2:      143.5

    ibmq_burlington              ibmq_london                  ibmq_valencia
    ---------------              -----------                  -------------
    Num. Qubits:  5              Num. Qubits:  5              Num. Qubits:  5
    Pending Jobs: 7              Pending Jobs: 7              Pending Jobs: 6
    Least busy:   False          Least busy:   False          Least busy:   False
    Operational:  True           Operational:  True           Operational:  True
    Avg. T1:      80.5           Avg. T1:      60.5           Avg. T1:      98.0
    Avg. T2:      75.7           Avg. T2:      68.1           Avg. T2:      65.1

    ibmq_ourense                 ibmq_vigo                    ibmq_16_melbourne
    ------------                 ---------                    -----------------
    Num. Qubits:  5              Num. Qubits:  5              Num. Qubits:  15
    Pending Jobs: 1              Pending Jobs: 7              Pending Jobs: 8
    Least busy:   True           Least busy:   False          Least busy:   False
    Operational:  True           Operational:  True           Operational:  True
    Avg. T1:      106.2          Avg. T1:      95.5           Avg. T1:      53.2
    Avg. T2:      64.7           Avg. T2:      63.0           Avg. T2:      56.6

    ibmqx2
    ------
    Num. Qubits:  5
    Pending Jobs: 13
    Least busy:   False
    Operational:  True
    Avg. T1:      64.1
    Avg. T2:      40.6

```python
# Let's get two quantum devices as an example
backend_qx2 = provider.get_backend('ibmqx2')
backend_vigo = provider.get_backend('ibmq_vigo')
```

```python
backend_monitor(backend_qx2)
```

    ibmqx2
    ======
    Configuration
    -------------
        n_qubits: 5
        operational: True
        status_msg: active
        pending_jobs: 13
        backend_version: 2.1.0
        basis_gates: ['id', 'u1', 'u2', 'u3', 'cx']
        local: False
        simulator: False
        sample_name: sparrow
        memory: True
        credits_required: True
        meas_map: [[0, 1, 2, 3, 4]]
        max_shots: 8192
        n_registers: 1
        coupling_map: [[0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 3], [2, 4], [3, 2], [3, 4], [4, 2], [4, 3]]
        description: 5 qubit device
        backend_name: ibmqx2
        quantum_volume: 8
        conditional: False
        open_pulse: False
        allow_q_object: True
        url: None
        max_experiments: 75
        allow_object_storage: True
        online_date: 2017-01-24T05:00:00+00:00

    Qubits [Name / Freq / T1 / T2 / U1 err / U2 err / U3 err / Readout err]
    -----------------------------------------------------------------------
        Q0 / 5.2829 GHz / 73.79798 µs / 27.86901 µs / 0.0 / 0.00133 / 0.00266 / 0.0495
        Q1 / 5.24766 GHz / 61.62679 µs / 27.59343 µs / 0.0 / 0.00122 / 0.00243 / 0.023
        Q2 / 5.03384 GHz / 54.81727 µs / 62.96767 µs / 0.0 / 0.0009 / 0.0018 / 0.02
        Q3 / 5.29225 GHz / 59.02961 µs / 41.38983 µs / 0.0 / 0.00053 / 0.00107 / 0.014
        Q4 / 5.07847 GHz / 71.2559 µs / 43.16323 µs / 0.0 / 0.00051 / 0.00102 / 0.026

    Multi-Qubit Gates [Name / Type / Gate Error]
    --------------------------------------------
        cx0_1 / cx / 0.0194
        cx0_2 / cx / 0.01643
        cx1_0 / cx / 0.0194
        cx1_2 / cx / 0.02873
        cx2_0 / cx / 0.01643
        cx2_1 / cx / 0.02873
        cx2_3 / cx / 0.01804
        cx2_4 / cx / 0.01461
        cx3_2 / cx / 0.01804
        cx3_4 / cx / 0.01251
        cx4_2 / cx / 0.01461
        cx4_3 / cx / 0.01251

```python
plot_error_map(backend_qx2)
```

```python
backend_monitor(backend_vigo)
```

    ibmq_vigo
    =========
    Configuration
    -------------
        n_qubits: 5
        operational: True
        status_msg: active
        pending_jobs: 7
        backend_version: 1.0.3
        basis_gates: ['id', 'u1', 'u2', 'u3', 'cx']
        local: False
        simulator: False
        sample_name: Giraffe
        memory: True
        credits_required: True
        meas_map: [[0, 1, 2, 3, 4]]
        max_shots: 8192
        n_registers: 1
        coupling_map: [[0, 1], [1, 0], [1, 2], [1, 3], [2, 1], [3, 1], [3, 4], [4, 3]]
        description: 5 qubit device Vigo
        backend_name: ibmq_vigo
        quantum_volume: 16
        conditional: False
        open_pulse: False
        allow_q_object: True
        url: None
        max_experiments: 75
        allow_object_storage: True
        online_date: 2019-07-03T04:00:00+00:00

    Qubits [Name / Freq / T1 / T2 / U1 err / U2 err / U3 err / Readout err]
    -----------------------------------------------------------------------
        Q0 / 4.79649 GHz / 122.04295 µs / 13.28137 µs / 0.0 / 0.00035 / 0.0007 / 0.008
        Q1 / 4.94014 GHz / 64.04633 µs / 78.91768 µs / 0.0 / 0.0005 / 0.001 / 0.013
        Q2 / 4.83352 GHz / 85.55076 µs / 103.29922 µs / 0.0 / 0.00038 / 0.00076 / 0.011
        Q3 / 4.80797 GHz / 64.45464 µs / 65.45159 µs / 0.0 / 0.00061 / 0.00123 / 0.026
        Q4 / 4.74967 GHz / 141.21974 µs / 54.21269 µs / 0.0 / 0.00054 / 0.00107 / 0.022

    Multi-Qubit Gates [Name / Type / Gate Error]
    --------------------------------------------
        cx0_1 / cx / 0.00953
        cx1_0 / cx / 0.00953
        cx1_2 / cx / 0.00934
        cx1_3 / cx / 0.01271
        cx2_1 / cx / 0.00934
        cx3_1 / cx / 0.01271
        cx3_4 / cx / 0.00726
        cx4_3 / cx / 0.00726

```python
plot_error_map(backend_vigo)
```

## Let's create a 5-qubit GHZ state, i.e. $ \frac{|00000\rangle + |11111\rangle}{\sqrt{2}}$.

```python
# Create a 5-qubit GHZ state (i.e. (|00000> + |11111>)/sqrt(2))
q5 = QuantumRegister(5,'q')
c5 = ClassicalRegister(5,'c')
ghz5= QuantumCircuit(q5,c5)

ghz5.h(0)
for i in range(1,5):
    ghz5.cx(0,i)

ghz5.barrier()
ghz5.measure(q5,c5)
ghz5.draw(output='mpl')
```

## Now, let's run it on a real IBMQ device.

```python
# Run the 5-qubit GHZ experiment on a 5-qubit device (try vigo)
job_exp1 = execute(ghz5, backend=backend_vigo, shots=4096)
job_monitor(job_exp1)
```

    Job Status: job has successfully run

```python
# Grab experimental results
result_vigo = job_exp1.result()
counts_vigo = result_vigo.get_counts(ghz5)
```

```python
# Let's also try the same experiment on the 14-qubit device.
job_exp2 = execute(ghz5, backend=provider.get_backend('ibmq_16_melbourne'), shots=4096)
job_monitor(job_exp2)
```

    Job Status: job has successfully run

```python
# Grab experimental results
result_mel = job_exp2.result()
counts_mel = result_mel.get_counts(ghz5)
```

```python
# Now, compare to theory by running it on qasm_simulator
job_qasm = execute(ghz5,backend=backend_q)
result_qasm = job_qasm.result()
counts_qasm = result_qasm.get_counts(ghz5)

# Plot both experimental and ideal results
plot_histogram([counts_qasm,counts_vigo,counts_mel],
               color=['black','green','blue'],
               legend=['QASM','Vigo','Melbourne'],figsize = [20,8])
```

## Elementary Quantum Protocols

* Superdense coding
* Quantum teleportation

```python

```