# Goldbach Runner

This notebook demonstrates how to create a grading workflow using PyBryt.

```python
import pybryt
```

This demo has the following directory structure. This notebook, `index.ipynb`, runs PyBryt, `goldbach.ipynb` is the assignment reference implementation, and `submissions` contains notebooks with student code in them.

```bash
%%bash
tree
```

    .
    ├── goldbach-ref.pkl
    ├── goldbach.ipynb
    ├── index.ipynb
    ├── out-1.ipynb
    ├── out-2.ipynb
    ├── out-3.ipynb
    ├── out-4.ipynb
    ├── out-5.ipynb
    ├── out-6.ipynb
    └── submissions
        ├── 1.ipynb
        ├── 1.py
        ├── 2.ipynb
        ├── 2.py
        ├── 3.ipynb
        ├── 3.py
        ├── 4.ipynb
        ├── 4.py
        ├── 5.ipynb
        ├── 5.py
        ├── 6.ipynb
        └── 6.py

    1 directory, 21 files

## Reference Implementations

If you have marked up a reference implementation, like the one in [`goldbach.ipynb`](goldbach.ipynb), you can load this reference using `pybryt.ReferenceImplementation.compile`. Because references are relatively static and can take some time to execute, you can pickle the reference implementations to a file with `pybryt.ReferenceImplementation.dump`.

```python
ref = pybryt.ReferenceImplementation.compile("goldbach.ipynb")
ref.dump("goldbach-ref.pkl")
```

To load a pickled reference implementation, use `pybryt.ReferenceImplementation.load`:

```python
ref = pybryt.ReferenceImplementation.load("goldbach-ref.pkl")
```

## Assessing Submissions

To use PyBryt for grading multiple submissions, you can build a reproducible grading pipeline for an arbitrary number of submissions. To grab the submission notebook paths, the cell below uses `glob.glob`.

```python
from glob import glob
subms = glob("submissions/*.py")
subms
```

    ['submissions/6.py',
     'submissions/2.py',
     'submissions/3.py',
     'submissions/4.py',
     'submissions/5.py',
     'submissions/1.py']

```python
import os
import nbformat  as nbf
from pathlib import Path

notebooks = []

for sub in subms:

    code = Path(sub).read_text()

    cells = []

    cell = nbf.v4.new_code_cell(source=code)
    cells.append(cell)
    cell = nbf.v4.new_code_cell(source="from sympy.ntheory.generate import primerange\nprimes = set(primerange(50,1000))\ncheck_goldbach_for_num(116, primes)")
    cells.append(cell)

    nb = nbf.v4.new_notebook(cells = cells)

    pre, ext = os.path.splitext(sub)
    notebook = pre + '.ipynb'
    with open(notebook, 'w') as f:

            nbf.write(nb, f, 4)
    notebooks.append(notebook)
```

To use PyBryt to grade a student's submission, a `pybryt.StudentImplementation` must be created from that submission. The constructor takes the path to the notebook as its only positional argument. It is in this step that the student's code is executed, so this cell will need to be rerun whenever changes are made to the submission notebooks.

```python
student_impls = []
for subm in notebooks:
    print(subm)
    student_impls.append(pybryt.StudentImplementation(subm))

student_impls
```

    submissions/6.ipynb
    submissions/2.ipynb
    submissions/3.ipynb
    submissions/4.ipynb
    submissions/5.ipynb
    submissions/1.ipynb

    [<pybryt.student.StudentImplementation at 0x110b84ef0>,
     <pybryt.student.StudentImplementation at 0x119c08588>,
     <pybryt.student.StudentImplementation at 0x110ef2550>,
     <pybryt.student.StudentImplementation at 0x1114f1518>,
     <pybryt.student.StudentImplementation at 0x110b84fd0>,
     <pybryt.student.StudentImplementation at 0x119cf5c18>]

Once you have created the `pybryt.StudentImplementation` objects, use the `pybryt.StudentImplementation.check` method to run the check of a submission against a reference implementation. This method returns a single `pybryt.ReferenceResult` or a list of them, depending on the argument passed to `check`. In the cell below, the results are collected into a list.

```python
results = []
for si in student_impls:
    results.append(si.check(ref))

results
```

    [ReferenceResult([
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation)
     ]),
     ReferenceResult([
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation)
     ]),
     ReferenceResult([
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation)
     ]),
     ReferenceResult([
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation)
     ]),
     ReferenceResult([
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation)
     ]),
     ReferenceResult([
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=True, annotation=pybryt.NotAnnotation),
       AnnotationResult(satisfied=False, annotation=pybryt.NotAnnotation)
     ])]

To view the results in a concise manner, the `pybryt.ReferenceResult` class has some helpful instance variables:

```python
from textwrap import indent
for sp, res in zip(subms, results):
    print(f"SUBMISSION: {sp}")
    # res.messages is a list of messages returned by the reference during grading
    messages = "\n".join(res.messages)

    # res.correct is a boolean for whether the reference was satisfied
    message = f"SATISFIED: {res.correct}\nMESSAGES:\n{indent(messages, '  - ')}"

    # some pretty-printing
    print(indent(message, "  "))
    print("\n")
```

    SUBMISSION: submissions/6.py
      SATISFIED: True
      MESSAGES:

    SUBMISSION: submissions/2.py
      SATISFIED: False
      MESSAGES:
        - ADVICE: it is good that you are looking only at primes smaller than n but to save memory you can do that without creating a new set
        - ADVICE: if the prime is bigger then n it cannot be a member of a pair that sum to n

    SUBMISSION: submissions/3.py
      SATISFIED: False
      MESSAGES:
        - ADVICE: the program checks if 35 is prime (35 is just an example), try to see if this can be avoided

    SUBMISSION: submissions/4.py
      SATISFIED: False
      MESSAGES:
        - ADVICE: instead of going over all primes and checking if they sum to n you can check if n-p is in the list of primes

    SUBMISSION: submissions/5.py
      SATISFIED: False
      MESSAGES:
        - ADVICE: if the prime is bigger then n it cannot be a member of a pair that sum to n

    SUBMISSION: submissions/1.py
      SATISFIED: False
      MESSAGES:
        - ADVICE: sorting the list of primes only increases the complexity of the solution
        - ADVICE: instead of going over all primes and checking if they sum to n you can check if n-p is in the list of primes
        - ADVICE: the program checks if 35 is prime (35 is just an example), try to see if this can be avoided


```python
import pandas as pd
import numpy as np
import scipy as sp
import math
import sympy as sym
import time
```

```python
priorsDF = pd.DataFrame(columns = ['mass_1','mass_2','sma_2','ecc_2','aop_2','inc_2','lan_2','mea_2','mass_3','sma_3','ecc_3','aop_3','inc_3','lan_3','mea_3','j2r2_1','c22r2_1','spaop_1','spinc_1','splan_1','sprate_1'],index=['dist. shape','uni-low','uni-up','log-uni-low','log-uni-up','norm-cen','norm-spread','log-norm-cen','log-norm-spread'])
allParamsDF = pd.DataFrame(columns = ['name_1','mass_1','name_2','mass_2','sma_2','ecc_2','aop_2','inc_2','lan_2','mea_2','name_3','mass_3','sma_3','ecc_3','aop_3','inc_3','lan_3','mea_3','j2r2_1','c22r2_1','spaop_1','spinc_1','splan_1','sprate_1'],index=[0])

#Set Parameters, or known data of objects
allParamsDF['name_1'][0] = 'Haumea'
allParamsDF['name_2'][0] = 'Hi\'aka'
allParamsDF['name_3'][0] = 'Namaka'

allParamsDF['mass_1'][0] = 4.006*10**21
allParamsDF['mass_2'][0] = 1.79*10**19
allParamsDF['mass_3'][0] = 1.79*10*18

allParamsDF['sma_2'][0] = 49880
allParamsDF['sma_3'][0] = 25657

allParamsDF['ecc_2'][0] = 0.0513
allParamsDF['ecc_3'][0] = 0.249

allParamsDF['aop_2'][0] = 154.1
allParamsDF['aop_3'][0] = 178.9

allParamsDF['inc_2'][0] = 126.356
allParamsDF['inc_3'][0] = 113.013

allParamsDF['lan_2'][0] = 206.766
allParamsDF['lan_3'][0] = 205.016

allParamsDF['mea_2'][0] = 152.8
allParamsDF['mea_3'][0] = 178.5

allParamsDF['j2r2_1'][0] = 1
allParamsDF['c22r2_1'][0] = 12
allParamsDF['spaop_1'][0] = 'idk'
allParamsDF['spinc_1'][0] = 'irdk'
allParamsDF['splan_1'][0] = 'isdk'
allParamsDF['sprate_1'][0] = 'wotfam'

#Set Priors
priorsDF['mass_1'][0] = 2
priorsDF['mass_1'][1] = 0
priorsDF['mass_1'][2] = 0
priorsDF['mass_1'][3] = 0
priorsDF['mass_1'][4] = 0
priorsDF['mass_1'][5] = 4.006*10**21
priorsDF['mass_1'][6] = 0.0040*10**21
priorsDF['mass_1'][7] = 4.006*10**21
priorsDF['mass_1'][8] = 0.0040*10**21

priorsDF['mass_2'][0] = 2
priorsDF['mass_2'][1] = 0
priorsDF['mass_2'][2] = 0
priorsDF['mass_2'][3] = 0
priorsDF['mass_2'][4] = 0
priorsDF['mass_2'][5] = 1.79*10**19
priorsDF['mass_2'][6] = 0.11*10**19
priorsDF['mass_2'][7] = 1.79*10**19
priorsDF['mass_2'][8] = 0.11*10**19

priorsDF['mass_3'][0] = 2
priorsDF['mass_3'][1] = 0
priorsDF['mass_3'][2] = 0
priorsDF['mass_3'][3] = 0
priorsDF['mass_3'][4] = 0
priorsDF['mass_3'][5] = 1.79*10**18
priorsDF['mass_3'][6] = 1.48*10**18
priorsDF['mass_3'][7] = 1.79*10**18
priorsDF['mass_3'][8] = 1.48*10**18

priorsDF['sma_2'][0] = 2
priorsDF['sma_2'][1] = 0
priorsDF['sma_2'][2] = 0
priorsDF['sma_2'][3] = 0
priorsDF['sma_2'][4] = 0
priorsDF['sma_2'][5] = 49880
priorsDF['sma_2'][6] = 198
priorsDF['sma_2'][7] = 49880
priorsDF['sma_2'][8] = 198

priorsDF['sma_3'][0] = 2
priorsDF['sma_3'][1] = 0
priorsDF['sma_3'][2] = 0
priorsDF['sma_3'][3] = 0
priorsDF['sma_3'][4] = 0
priorsDF['sma_3'][5] = 25657
priorsDF['sma_3'][6] = 91
priorsDF['sma_3'][7] = 25657
priorsDF['sma_3'][8] = 91

priorsDF['ecc_2'][0] = 2
priorsDF['ecc_2'][1] = 0
priorsDF['ecc_2'][2] = 1
priorsDF['ecc_2'][3] = 0
priorsDF['ecc_2'][4] = 1
priorsDF['ecc_2'][5] = 0.0513
priorsDF['ecc_2'][6] = 0.0078
priorsDF['ecc_2'][7] = 0.0513
priorsDF['ecc_2'][8] = 0.0078

priorsDF['ecc_3'][0] = 2
priorsDF['ecc_3'][1] = 0
priorsDF['ecc_3'][2] = 1
priorsDF['ecc_3'][3] = 0
priorsDF['ecc_3'][4] = 1
priorsDF['ecc_3'][5] = 0.249
priorsDF['ecc_3'][6] = 0.015
priorsDF['ecc_3'][7] = 0.249
priorsDF['ecc_3'][8] = 0.015

priorsDF['aop_2'][0] = 2
priorsDF['aop_2'][1] = 0
priorsDF['aop_2'][2] = 180
priorsDF['aop_2'][3] = 0
priorsDF['aop_2'][4] = 180
priorsDF['aop_2'][5] = 154.1
priorsDF['aop_2'][6] = 5.8
priorsDF['aop_2'][7] = 154.1
priorsDF['aop_2'][8] = 5.8

priorsDF['aop_3'][0] = 2
priorsDF['aop_3'][1] = 0
priorsDF['aop_3'][2] = 180
priorsDF['aop_3'][3] = 0
priorsDF['aop_3'][4] = 180
priorsDF['aop_3'][5] = 178.9
priorsDF['aop_3'][6] = 2.3
priorsDF['aop_3'][7] = 178.9
priorsDF['aop_3'][8] = 2.3

priorsDF['inc_2'][0] = 2
priorsDF['inc_2'][1] = 0
priorsDF['inc_2'][2] = 180
priorsDF['inc_2'][3] = 0
priorsDF['inc_2'][4] = 180
priorsDF['inc_2'][5] = 126.356
priorsDF['inc_2'][6] = 0.064
priorsDF['inc_2'][7] = 126.356
priorsDF['inc_2'][8] = 0.064

priorsDF['inc_3'][0] = 2
priorsDF['inc_3'][1] = 0
priorsDF['inc_3'][2] = 180
priorsDF['inc_3'][3] = 0
priorsDF['inc_3'][4] = 180
priorsDF['inc_3'][5] = 113.013
priorsDF['inc_3'][6] = 0.075
priorsDF['inc_3'][7] = 113.013
priorsDF['inc_3'][8] = 0.075

priorsDF['lan_2'][0] = 2
priorsDF['lan_2'][1] = 0
priorsDF['lan_2'][2] = 1
priorsDF['lan_2'][3] = 0
priorsDF['lan_2'][4] = 1
priorsDF['lan_2'][5] = 206.766
priorsDF['lan_2'][6] = 0.033
priorsDF['lan_2'][7] = 206.766
priorsDF['lan_2'][8] = 0.033

priorsDF['lan_3'][0] = 2
priorsDF['lan_3'][1] = 0
priorsDF['lan_3'][2] = 1
priorsDF['lan_3'][3] = 0
priorsDF['lan_3'][4] = 1
priorsDF['lan_3'][5] = 205.016
priorsDF['lan_3'][6] = 0.228
priorsDF['lan_3'][7] = 205.016
priorsDF['lan_3'][8] = 0.228

priorsDF['mea_2'][0] = 2
priorsDF['mea_2'][1] = 0
priorsDF['mea_2'][2] = 1
priorsDF['mea_2'][3] = 0
priorsDF['mea_2'][4] = 1
priorsDF['mea_2'][5] = 152.8
priorsDF['mea_2'][6] = 6.1
priorsDF['mea_2'][7] = 152.8
priorsDF['mea_2'][8] = 6.1

priorsDF['mea_3'][0] = 2
priorsDF['mea_3'][1] = 0
priorsDF['mea_3'][2] = 1
priorsDF['mea_3'][3] = 0
priorsDF['mea_3'][4] = 1
priorsDF['mea_3'][5] = 178.5
priorsDF['mea_3'][6] = 1.7
priorsDF['mea_3'][7] = 178.5
priorsDF['mea_3'][8] = 1.7

priorsDF['j2r2_1'] = 1
priorsDF['c22r2_1'] = 12
priorsDF['spaop_1'] = 'idk'
priorsDF['spinc_1'] = 'irdk'
priorsDF['splan_1'] = 'isdk'
priorsDF['sprate_1'] = 'wotfam'

#print(allParamsDF.transpose(),'\n\n',priorsDF.transpose())

priorsDF.to_csv(r'Priors.csv',sep='\t',index=True)
priorsDF.transpose().to_csv(r'TransposedPriors.csv',sep='\t',index=True)
```

```python
def mm_priors(priors, params):
    columnList = list(priors)
    totalLogProb = 0

    probDist = pd.DataFrame(columns = ['mass_1','mass_2','sma_2','ecc_2','aop_2','inc_2','lan_2','mea_2','mass_3','sma_3','ecc_3','aop_3','inc_3','lan_3','mea_3','j2r2_1','c22r2_1','spaop_1','spinc_1','splan_1','sprate_1'],index=['PDF'])

    count = 0
    allProbs = []
    numNaNs = 0
    #This loop runs through every column in the priors dataframe, and evaluates the probability density
    #function of the specified type.
    for i in columnList:
        count += 1

        #Uniform Distribution Shape
        if priors[i][0] == 0:
            if params[i][0] < priors[i][2] and params[i][0] > priors[i][1]:
                allProbs.append(1)
            elif np.isnan(x[count]):
                numNaNs += 1
            else:
                allProbs.append(0)

        #Log-Uniform Distribution Shape
        elif priors[i][0] == 0:
            if params[i][0] < priors[i][4] and params[i][0] > priors[i][3]:
                allProbs.append(1)
            elif np.isnan(params[i][0]):
                numNaNs += 1
            else:
                allProbs.append(0)

        # Normal Distribution Shape
        elif priors[i][0] == 2:
            if not np.isnan(params[i][0]):
                allProbs.append(np.exp(-1/2*((params[i][0]-priors[i][6])/priors[i][5])**2))

        #Log Normal Distribution Shape
        elif priors[i][0] == 3:
            if not np.isnan(params[i][0]):
                allProbs.append(np.exp(-1/2*(((np.log(params[i][0])-priors[i][8])**2)/(priors[i][7])**2))/params[i][0])
        else:
            print('Invalid input for: ', i)

        #Here, add the Prior Probability Density function for this element to the total
    for x in allProbs:
        totalLogProb = totalLogProb + np.log(x)
        print(x)

    return totalLogProb
```

```python
dist = mm_priors(priorsDF,allParamsDF)
print('Total Probability: 10^(',dist,')')
```

    Invalid input for:  j2r2_1
    Invalid input for:  c22r2_1
    Invalid input for:  spaop_1
    Invalid input for:  spinc_1
    Invalid input for:  splan_1
    Invalid input for:  sprate_1
    0.6071362817376864
    0.6437558808339235
    0.608938286802129
    0.6980166458512227
    0.6293483196697449
    0.6068378707584218
    0.6066274624345537
    0.630731264295467
    0.7104817248522851
    0.6086818877746099
    0.6430238118476723
    0.6143279957288144
    0.606933177943272
    0.6072051872294486
    0.6123069671246485
    Total Probability: 10^( -6.9740862131907955 )

```python

```


# 回路量子電磁力学 (Circuit Quantum Electrodynamics)

## Contents

1. [序論](#intro)
2. [Schrieffer-Wolff変換](#tswt)
3. [Jaynes-Cummingsハミルトニアンのブロック対角化](#bdotjch)
4. [多準位のトランズモンへの拡張](#full-transmon)
5. [量子ビットのドライブ](#qdwcqed)
6. [交差共鳴 (Cross Resonance) ゲート](#tcreg)

## 1. 序論

共振器量子電磁力学（Cavity Quantum Electrodynamics、CQED）における原子と光共振器の相互作用とのアナロジーで、回路量子電磁力学（circuit QED、cQED）は量子ビットとマイクロ波共振器との相互作用を記述する単純なモデルです。このモデルは、共振器中の光子数、原子（量子ビット）の状態、原子（量子ビット）と共振器との電気双極子相互作用を含みます。前節で見たように、トランズモンは多準位系ですが、トランズモンの非調和性（Anharmonicity）により基底状態 $|0\rangle = |g\rangle$ と第一励起状態 $|1\rangle = |e\rangle$ に限定することができます。従って、トランズモンを、以下のパウリスピン行列で記述した量子ビットと考えることができます。
$$
\sigma^x = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} \qquad
\sigma^y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} \qquad
\sigma^z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \qquad
$$
これらはブロッホ球の各軸周りの回転を意味します。この相互作用を記述する最も単純なモデルは、回転波近似をしたJaynes-Cummingsハミルトニアン
$$
H_{\rm JC}^{\rm (RWA)}/\hbar = \omega_r a^\dagger a - \frac{1}{2} \omega_q \sigma^z + g(a^\dagger \sigma^- + a \sigma^+)
$$
です。 $\omega_r$ と $\omega_q$ はそれぞれ共振器と"量子ビット"の共振周波数で、 $a$ ($a^\dagger$) は共振器中の光子の消滅（生成）演算子、 $g$ は電気双極子の結合（真空ラビ分裂の半分）です。ここで、演算子のハットを省いています。この式で、第一項は共振器の中の光子数、第二項は量子ビットの状態、第三項は電気双極子相互作用に対応し、 $\sigma^\pm = (1/2)(\sigma^x \mp i\sigma^y)$ は量子ビットの昇降演算子です。（前章で述べたように、スピンの昇降演算子と逆符号になっています。）

このハミルトニアンは厳密に解くことができ、解は量子ビットと共振器の状態のハイブリッドになっていて、共振器中の光子と量子ビットが共振（$\omega_r = \omega_q$）している時、それらの励起のスワップがレート $g$ で起こっています。例えば、第3項の $a^\dagger \sigma^-$ は、共振器内に光子を生成し、量子ビットを $|1\rangle$ から $|0\rangle$ に下げ、 $a\sigma^+$ は逆に光子を消滅し、量子ビットを $|0\rangle$ から $|1\rangle$ に上げます。興味深い現象ですが、量子コンピューターにとって、扱いたいのは量子ビットであって、このようなハイブリッド状態ではありません。これは、共振器が量子ビットの摂動として振る舞う（逆も成り立つ）ようにしたいということで、従って互いの存在に "dressed" された状態になります。 Schrieffer-Wolff (S-W) 変換と呼ばれる摂動論を用いることにより、望ましい形で量子ビットと共振器の特性を計算できるようになります。ここでは、量子ビットにこの手法を適用していますが、同様の手法はトランズモンの全準位に適用できます。トランズモンの高準位では、無視できない効果が表れ、設計・シミュレーションの際に考慮しなければなりません。

## 2. Schrieffer-Wolff変換

    Schrödinger方程式（ここをクリック）
量子力学の問題は、多くの場合ハミルトニアンの固有値方程式を対角化することに帰結します。
$$
H\psi_m = E_m \psi_m \qquad {\rm for} \quad 1 \le m \le n
$$
$\psi_m$ は固有値 $E_m$ に対応した固有状態です。この時、 $H' = U H U^\dagger$ が対角になるようなユニタリー行列 $U$ が存在し、固有値方程式は
$$
\hat{H} \psi_m = E_m \psi_m \Longrightarrow U H U^\dagger U \psi_m = E_m U \psi_m \Longrightarrow H' \psi_m' = E_m \psi_m'
$$
となり、 $\psi_m' = U\psi_m$ は変換した固有状態で、ハミルトニアンは
$$
H' = \begin{pmatrix}
E_1 & 0 & \cdots & 0 \\
0 & E_2 & \cdots & 0 \\
\vdots & \vdots & \ddots & 0 \\
0 & 0 & \cdots & E_n \end{pmatrix}
$$
と対角化されます。

S-W変換では、ハミルトニアンの対角化でなく、*ブロック対角化*ができます。ハミルトニアンが対角項と摂動項に分けられると仮定し、
$$
H \quad = \quad \underbrace{\begin{pmatrix}
\Box &  &  &  &  &  &  \\
 & \Box &  &  &  &  &  \\
 &  & \Box &  &  &  &  \\
 &  &  & \Box &  &  &  \\
 &  &  &  & \Box &  &  \\
 &  &  &  &  & \Box &  \\
 &  &  &  &  &  & \Box \end{pmatrix}}_\text{対角項} \quad + \quad
\underbrace{\begin{pmatrix}
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \end{pmatrix}}_\text{摂動項}
$$
$H_0$ を対角項とし、摂動項をさらにブロック対角項 $H_1$ と非ブロック対角項 $H_2$ に分け、 $H = H_0 + H_1 + H_2$ とします。
$$
H \quad = \quad \underbrace{\begin{pmatrix}
\Box &  &  &  &  &  &  \\
 & \Box &  &  &  &  &  \\
 &  & \Box &  &  &  &  \\
 &  &  & \Box &  &  &  \\
 &  &  &  & \Box &  &  \\
 &  &  &  &  & \Box &  \\
 &  &  &  &  &  & \Box \end{pmatrix}}_\text{対角項}
\quad + \quad
\underbrace{\begin{pmatrix}
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
 &  &  &  & \times & \times & \times \\
 &  &  &  & \times & \times & \times \\
 &  &  &  & \times & \times & \times \end{pmatrix}}_\text{blockブロック対角項}
\quad + \quad
\underbrace{\begin{pmatrix}
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
\cdot & \cdot & \cdot & \cdot &  &  &  \\
\cdot & \cdot & \cdot & \cdot &  &  &  \\
\cdot & \cdot & \cdot & \cdot &  &  &  \end{pmatrix}}_\text{非ブロック対角項}
$$

以下の有効ハミルトニアン $H_{\rm eff}$ がブロック対角化されるように、演算子 $S$ を求めます。

$$
H_{\rm eff} = e^{-S} H e^{S} = \sum_{m=0}^\infty \frac{1}{m!} [H, S]^{(m)} = \sum_{m=0}^\infty \lambda^m H^{(m)}
$$

$H^{(m)}$ は $H$ の逐次近似($H^{(0)} = H_0$)であり、交換関係の一般項は再帰的に

$$
[H,S]^{(m)} = [[H,S]^{(m-1)},S] \qquad {\rm with} \qquad [H,S]^{(0)} = H
$$

と定義します。ここで、 $S$ を $\lambda$ の次数を持ったテーラー級数で表し、

$$ S = \sum_{m=1}^\infty \lambda^m S^{(m)} $$

有効ハミルトニアンを $H_1+H_2$ の $\lambda$ 2次までの摂動で展開します。

$$
H_{\rm eff} = H_0 + \lambda (H_1+H_2) + \left[H_0 + \lambda(H_1+H_2), \lambda S^{(1)}\right]
  + \frac{1}{2} \left[ \left[ H_0 + \lambda(H_1+H_2), \lambda S^{(1)}\right], \lambda S^{(1)}\right]
  + \left[H_0 + \lambda(H_1+H_2), \lambda^2 S^{(2)}\right] + \ldots \\
  \approx H_0 + \lambda \left( H_1 + H_2 + \left[H_0, S^{(1)}\right] \right)
  + \lambda^2 \left( \left[H_1+H_2, S^{(1)}\right] + \frac{1}{2} \left[ \left[H_0, S^{(1)},\right] S^{(1)}\right] + \left[H_0, S^{(2)}\right]\right)
$$

ここで、 $H_{\rm eff}$ の非ブロック対角項を消すために、 $S$ は非ブロック対角で反エルミートでなければなりません。その結果、 $H_{\rm eff}$ の非ブロック対角項は

$$
H_{\rm eff}^{\rm off-diag} = \sum_{m=0}^\infty \frac{1}{(2m+1)!} [\underbrace{H_0 + \lambda H_1}_\text{ブロック対角}, S]^{(2m+1)} + \sum_{m=0}^\infty \frac{1}{(2m)!} [\underbrace{\lambda H_2}_\text{非ブロック対角}, S]^{(2m)} \equiv 0
$$

となります。ここで、1つ目の級数の交換関係 $[H_0 + \lambda H_1, S]$ は非ブロック対角であり、2つ目の級数の交換関係 $[\lambda H_2, S]$ はブロック対角です。これは、ブロック対角行列と非ブロック対角行列の交換関係は非ブロック対角であり、2つの非ブロック対角行列の交換関係はブロック対角になるからです。1つ目の項は、 $[H_0 + \lambda H_1, S]^{(n)}$（$n$ は奇数）、1つ目の項は、 $[\lambda H_2, S]^{(n)}$（$n$ は偶数）と表記することもでき、これらは全て非ブロック対角になります。ハミルトニアンの非対角項を2次の摂動まで展開すると、

$$
H_{\rm eff}^{\rm off-diag} = \left[ H_0 + \lambda H_1, \lambda S^{(1)} \right]+\lambda H_2 + \left[H_0 + \lambda H_1, \lambda^2 S^{(2)}\right]
  + \frac{1}{3!} \left[ H_0+\lambda H_1, \lambda S^{(1)}\right]^{(3)} + \frac{1}{2!} \left[ \lambda H_2, \lambda S^{(1)}\right]^{(2)} \\
  = \lambda \left( \left[ H_0, S^{(1)} \right] + H_2 \right) + \lambda^2 \left( \left[H_1, S^{(1)} \right] + \left[H_0, S^{(2)}\right]\right) + \ldots.
$$
となります。 $\lambda$ の各次数でゼロになるため、 $S^{(m)}$ を以下の式で決めることができます。
$$
[H_0, S^{(1)}] = -H_2 \qquad
[H_0, S^{(2)}] = -[H_1, S^{(1)}] \qquad
[H_0, S^{(3)}] = -[H_1, S^{(2)}] - \frac{1}{3} [[H_2, S^{(1)}], S^{(1)}]
$$
これらの式を満たすアンザッツが一意に決まることはWinklerによって示されました。この結果、有効ハミルトニアンは

$$
H_{\rm eff} = H_0+H_1+[H_2,S^{(1)}] + \frac{1}{2} [[H_0, S^{(1)}], S^{(1)}] + \ldots = H_0+H_1+\frac{1}{2}[H_2,S^{(1)}] + \ldots
$$

とブロック対角化されます。ここで、 $\lambda \to 1$ としました。

## 3. Jaynes-Cummingsハミルトニアンのブロック対角化

S-W変換を行う際に2つ問題があります：1) 正しいアンザッツを見つけることと、2) 計算を実行すること、です。多くの例では、非対角項 $H_2$ と似た形のアンザッツ（$H_2$ を反エルミートにしたもの）を用い、帰納的に確かめます。最近、arXivに掲載された、[A Systematic Method for Schrieffer-Wolff Transformation and Its Generalizations](http://www.arxiv.org/abs/2004.06534) という論文では、システマチックにアンザッツを得る方法を示し、以下に示したJaynes-Cummingsハミルトニアンを含め多くの系に適用しています。

この方法では、*ジェネレーター* $\eta$ を $\eta=[H_0, H_2]$ と計算し、その係数を未知とします。 $[H_0, \eta]=-H_2$ を満たす $\eta$ の係数を求め、 $S^{(1)}=\eta$ とします。ここで、 $H_0$ と $H_2$ のエルミート性から、 $\eta$ や $S^{(1)}$ が反エルミートであることが示せます。

計算を簡単にするために、代数計算のためのPythonパッケージ [`sympy`](http://www.sympy.org) を用います。

```python
# import SymPy and define symbols
import sympy as sp
sp.init_printing(use_unicode=True)
wr = sp.Symbol('\omega_r') # resonator frequency
wq = sp.Symbol('\omega_q') # qubit frequency
g = sp.Symbol('g', real=True) # vacuum Rabi coupling
Delta = sp.Symbol('Delta', real=True) # wr - wq; defined later
```

```python
# import operator relations and define them
from sympy.physics.quantum.boson import BosonOp
a = BosonOp('a') # resonator photon annihilation operator
from sympy.physics.quantum import pauli, Dagger, Commutator
from sympy.physics.quantum.operatorordering import normal_ordered_form

# Pauli matrices
sx = pauli.SigmaX()
sy = pauli.SigmaY()
sz = pauli.SigmaZ()

# qubit raising and lowering operators
splus = pauli.SigmaPlus()
sminus = pauli.SigmaMinus()
```

```python
# define J-C Hamiltonian in terms of diagonal and non-block diagonal terms
H0 = wr*Dagger(a)*a - (1/2)*wq*sz;
H2 = g*(Dagger(a)*sminus + a*splus);
HJC = H0 + H2; HJC # print
```

```python
# using the above method for finding the ansatz
eta = Commutator(H0, H2); eta
```

`sympy`で`doit()`、`expand`、`normal_ordered_form`、`qsimplify_pauli`のメソッドを用いることで、交換関係の演算を行い、展開し、ボーズ粒子を正規順序（生成演算子、消滅演算子の順）にし、パウリ代数を簡単化できます。これを $\eta$ に適用します。

```python
pauli.qsimplify_pauli(normal_ordered_form(eta.doit().expand()))
```

ここで $A$、$B$ をそれぞれ $a^\dagger \sigma_-$、$a\sigma_+$ の係数とすると、交換関係 $[H_0, \eta]$ は以下のようになり、

```python
A = sp.Symbol('A')
B = sp.Symbol('B')
eta = A * Dagger(a) * sminus - B * a * splus;
pauli.qsimplify_pauli(normal_ordered_form(Commutator(H0, eta).doit().expand()))
```

これが $-H_2$ と等しくなるので、

```python
-H2
```

$A = B = -g/\Delta$ であることがわかります。ここで、 $\Delta = \omega_r - \omega_q$ は共振器と量子ビットの周波数差です。従って、 $S^{(1)}$ は

```python
S1 = eta.subs(A, -g/Delta)
S1 = S1.subs(B, -g/Delta); S1.factor()
```

と求まります。そして、 $H_0$ に2次の補正を加えた有効ハミルトニアンを以下のように計算することができます。

```python
Heff = H0 + 0.5*pauli.qsimplify_pauli(normal_ordered_form(Commutator(H2, S1).doit().expand())).simplify(); Heff
```

これは以下のように書き換えることができ、
$$
H_{\rm eff} = \left(\omega_r + \frac{g^2}{\Delta}\sigma_z\right)a^\dagger a  - \frac{1}{2}\left(\omega_q -\frac{g^2}{\Delta}\right) \sigma_z
$$
これは共振器周波数が*ac Starkシフト*と呼ばれる量子ビットの状態に依存した周波数シフト $\chi \equiv g^2/\Delta$ を受けることを示し、同時に量子ビットの側では*Lambシフト*と呼ばれる量子的真空場のゆらぎによる周波数シフトを示しています。

## 4. 多準位のトランズモンへの拡張

実際は2準位の*量子ビット*でなく、多準位の*トランズモン*を用いているので、デバイスを設計・シミュレートする際には、高次のエネルギー項を考慮する必要があります。読み出し共振器と結合した、完全なトランズモンのハミルトニアンは以下のように書けます。

$$
H^{\rm tr} = \omega_r a^\dagger a + \sum_j \omega_j |j\rangle\langle j| + g\left(a^\dagger c + ac^\dagger \right)
$$

ここで、 $c = \sum_j \sqrt{j+1}|j\rangle\langle j+1|$ はトランズモンの下降演算子、 $\omega_j = \left(\omega - \frac{\delta}{2}\right)j + \frac{\delta}{2} j^2$ は前節で定義したトランズモンの各準位の周波数、 $\omega$ は量子ビット周波数、 $\delta$ は非調和性(Anharmonicity)です。

$$
S^{(1)} = \sum_j \alpha_j a^\dagger \sqrt{j+1}|j\rangle\langle j+1| - \alpha_j^* a \sqrt{j+1}|j+1\rangle\langle j|
$$

S-W変換の条件式 $[H_0, S^{(1)}] = -H_2$ から $\alpha = \alpha^* = -\frac{g}{\omega_r - \omega - \delta j}$ と求まり、以下のように2次の有効ハミルトニアンが得られます。

$$
H^{\rm tr}_{\rm eff} = \left( \omega_r + \sum_j \frac{g^2(\omega_r-\omega+\delta)}{(\omega_r-\omega-\delta j)(\omega_r - \omega - \delta(j-1))} |j\rangle\langle j| \right) a^\dagger a + \sum_j
\left[
j\omega + \frac{\delta}{2} (j-1)j + \frac{jg^2}{\omega-\omega_r+(j-1)\delta} \right]|j\rangle\langle j|
$$

## 5. cQEDでの量子ビットのドライブ

[Blais *et al* (2004)](https://arxiv.org/abs/cond-mat/0402216) より、ドライブハミルトニアンを
$$
H^d(t) = \xi(t)\left( a^\dagger e^{-i\omega_d t} + ae^{i\omega_d t}\right)
$$
とし、これをJaynes-Cummingsハミルトニアンに加え、ドライブ周波数の回転座標系に変換します。
$$
H = \Delta_r a^\dagger a - \frac{1}{2} \Delta_q \sigma^z + g(a^\dagger \sigma^- + a\sigma^+) + \xi(t)(a^\dagger + a)
$$
ここで、 $\Delta_r = \omega_r - \omega_d$ 、 $\Delta_q = \omega_q - \omega_d$ です。[Lev Bishopの博士論文](https://arxiv.org/abs/1007.3520) に示されるように、以下のGlauber演算子を用いて、ドライブが量子ビットに直接作用するよう変形します。
$$
D(\alpha) = e^{\alpha(t) a^\dagger - \alpha^*(t) a}
$$
Hadamardの補題
$$
e^{A}Be^{-A} = B + [A,B] + \frac{1}{2!} [A,[A,B]] + \frac{1}{3!}[A,[A,[A,B]]] + \ldots
$$
を用いて以下の計算をすると、 $D$ は $a^{(\dagger)}$ を $\alpha^{(*)}$ だけ変位させるように作用することがわかり、
$$
D^\dagger a^{(\dagger)} D = \exp\{-\alpha(t) a^\dagger + \alpha^*(t) a\} a^{(\dagger)}\exp\{\alpha(t) a^\dagger - \alpha^*(t) a\}
= a^{(\dagger)} + \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^{(\dagger)}\right] + \frac{1}{2!}\left[-\alpha(t) a^\dagger + \alpha^*(t) a, \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^{(\dagger)}\right]\right] + \ldots
 = a^{(\dagger)} + \alpha^{(*)}
$$

$$
D^\dagger a^\dagger a D = a^\dagger a + \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^\dagger a\right] + \frac{1}{2!}\left[-\alpha(t) a^\dagger + \alpha^*(t) a, \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^\dagger a\right]\right] + \ldots
= a^\dagger a + \alpha(t)a^\dagger + \alpha^*(t)a + |\alpha(t)|^2
$$

従ってハミルトニアンは以下のようになります。

$$
\tilde{H} = D^\dagger H D - iD^\dagger \dot{D} = \Delta_r\left(a^\dagger a + \alpha(t)a^\dagger + \alpha^*(t)a + |\alpha(t)|^2\right) - \frac{1}{2} \Delta_q \sigma^z \\ + g\left((a^\dagger + \alpha^*(t))\sigma^- + (a+\alpha(t))\sigma^+\right) + \xi(t)\left(a^\dagger + \alpha^*(t) + a + \alpha(t) \right) - i\left(\dot{\alpha}(t) a^\dagger - \dot{\alpha}^*(t) a\right) \\
= \Delta_r a^\dagger a - \frac{1}{2}\Delta_q \sigma^z + g\left((a^\dagger + \alpha^*(t))\sigma^- + (a+\alpha(t))\sigma^+\right) \\
+\xi(t)\left(a^\dagger  + a \right) + \Delta_r\left(\alpha(t)a^\dagger + \alpha^*(t)a\right)- i\left(\dot{\alpha}(t) a^\dagger - \dot{\alpha}^*(t) a\right)
$$

ここで演算子を含まない項を落としました。以下の式を満たすように $\alpha(t)$ を選ぶと、最後の行はゼロになります。

$$
-i\dot{\alpha}(t) + \Delta_r \alpha(t) + \xi(t) = 0
$$

最後に、Rabi周波数 $\Omega(t) = 2g\alpha(t)$ を導入すると、ハミルトニアンは

$$
\tilde{H} = \Delta_r a^\dagger a - \frac{1}{2}\Delta_q \sigma^z + g\left(a^\dagger\sigma^- + a\sigma^+\right)
+\frac{1}{2} \left( \Omega^*(t)\sigma^- + \Omega(t) \sigma^+\right)
$$

となります。ハミルトニアンのドライブ項は非ブロック対角なので、Schrieffer-Wolff変換を適用します。ここでRabi周波数を実数とします($\Omega^*(t) = \Omega(t)$)。

$$
[\tilde{H}^d, S^{(1)}] = -\frac{\Omega(t)}{2} \left[ (\sigma^- + \sigma^+),\frac{g}{\Delta}\left( a^\dagger \sigma^- - a\sigma^+\right)\right] = \frac{g\Omega(t)}{2\Delta}(a + a^\dagger)\sigma^z
$$

これを有効ハミルトニアンに加えると、以下の式が得られます。

$$
\tilde{H}_{\rm eff} = \left( \Delta_r + \frac{g^2}{\Delta}\sigma^z\right) a^\dagger a - \frac{1}{2}\left(\Delta_q - \frac{g^2}{\Delta}\right) \sigma^z + \frac{\Omega(t)}{2}\sigma^x
+ \frac{g\Omega(t)}{4\Delta}(a + a^\dagger)\sigma^z
$$

ここで、 $z$ 回転を除去するために、Lambシフトした量子ビットでドライブする必要があることがわかります($\Delta_q = \frac{g^2}{\Delta}$)。もう1つの $\sigma^z$ 項は、分散結合領域(dispersive regime)では $\Delta \gg g$ となるため無視できます。

## 6. 交差共鳴 (Cross Resonance) ゲート

量子ビット1を量子ビット2の周波数でドライブするハミルトニアンは、以下のように書けます。

$$
H^d(t) = \frac{\Omega(t)}{2} \left( \sigma_1^+ e^{-i\tilde{\omega}_2 t} + \sigma_1^- e^{i\tilde{\omega}_2 t}\right)
$$

2つの量子ビットとその相互作用を表すハミルトニアンに対するS-W変換のアンザッツ $S^{(1)}=-\frac{J}{\Delta_{12}}(\sigma_1^+ \sigma_2^- - \sigma_1^- \sigma_2^+)$ を用いて、ドライブ項にS-W変換を適用します。

$$
[\tilde{H}^d, S^{(1)}] = -\frac{J\Omega(t)}{2\Delta_{12}} \left[ \sigma_1^+ e^{-i\tilde{\omega}_2 t} + \sigma_1^- e^{i\tilde{\omega}_2 t}, \sigma_1^+ \sigma_2^- - \sigma_2^+ \sigma_1^-\right]
=-\frac{J\Omega(t)}{2\Delta_{12}} \left(\sigma_1^z \sigma_2^+ e^{-i\tilde{\omega}_2 t}
+\sigma_1^z \sigma_2^- e^{i\tilde{\omega}_2 t} \right)
$$

$\tilde{\omega}_2$ の回転座標系に移ると、有効交差共鳴ハミルトニアンを得ます。

$$
\tilde{H}_{\rm eff}^{\rm CR} = - \frac{\tilde{\omega}_1-\tilde{\omega}_2}{2}\sigma_1^z
+ \frac{\Omega(t)}{2} \left(\sigma_2^x - \frac{J}{2\Delta_{12}} \sigma_1^z \sigma_2^x \right)
$$

始めの2つの項はそれぞれ、量子ビット1へのStarkシフトによる $ZI$ 相互作用、量子ビット2の無条件 $IX$ 回転に関連し、最後の項はエンタングルメントを生じる $ZX$ 相互作用を示します。量子ビット1を $|0\rangle$ と $|1\rangle$ の等しい重ね合わせ状態にし、$x$軸周りの $\pi/2$ 回転に相当する交差共鳴ゲートを適用すると、最大のエンタングル状態が得られます。[このチュートリアル](https://github.com/Qiskit/qiskit-tutorials/blob/9405254b38312771f8d5c2dd6f451cec35307995/tutorials/noise/1_hamiltonian_and_gate_characterization.ipynb)で、トランズモンの交差共鳴ハミルトニアンをQiskitで特性評価しています。他に交差共鳴ゲートについての参考文献として、 [Chow *et al* (2011)](https://arxiv.org/abs/1106.0553) や [Sheldon *et al* (2016)](https://arxiv.org/abs/1603.04821) もあります。

```python

```


#  CS109A/STAT121A/AC209A/CSCIE109A  Introduction to Data Science

## Lab 3: plotting, K-NN Regression, Simple Linear Regression

**Harvard University**

**Fall 2019**

**Instructors:** Pavlos Protopapas, Kevin Rader, and Chris Tanner

**Material prepared by**: David Sondak, Will Claybaugh, Pavlos Protopapas, and Eleni Kaxiras.

---

```python
#RUN THIS CELL
import requests
from IPython.core.display import HTML
styles = requests.get("https://raw.githubusercontent.com/Harvard-IACS/2018-CS109A/master/content/styles/cs109.css").text
HTML(styles)
```

## Learning Goals

By the end of this lab, you should be able to:
* Review `numpy` including 2-D arrays and understand array reshaping
* Use `matplotlib` to make plots
* Feel comfortable with simple linear regression
* Feel comfortable with $k$ nearest neighbors

**This lab corresponds to lectures 4 and 5 and maps on to homework 2 and beyond.**

## Table of Contents

####  HIGHLIGHTS FROM PRE-LAB

* [1 - Review of numpy](#first-bullet)
* [2 - Intro to matplotlib plus more ](#second-bullet)

####  LAB 3 MATERIAL

* [3 - Simple Linear Regression](#third-bullet)
* [4 - Building a model with `statsmodels` and `sklearn`](#fourth-bullet)
* [5 - Example: Simple linear regression with automobile data](#fifth-bullet)
* [6 - $k$Nearest Neighbors](#sixth-bullet)

```python
import numpy as np
import scipy as sp
import matplotlib as mpl
import matplotlib.cm as cm
import matplotlib.pyplot as plt
import pandas as pd
import time
pd.set_option('display.width', 500)
pd.set_option('display.max_columns', 100)
pd.set_option('display.notebook_repr_html', True)
#import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# Displays the plots for us.
%matplotlib inline
```

```python
# Use this as a variable to load solutions: %load PATHTOSOLUTIONS/exercise1.py. It will be substituted in the code
# so do not worry if it disappears after you run the cell.
PATHTOSOLUTIONS = 'solutions'
```

## 1 - Review of  the  `numpy` Python library

In lab1 we learned about the `numpy` library [(documentation)](http://www.numpy.org/) and its fast array structure, called the `numpy array`.

```python
# import numpy
import numpy as np
```

```python
# make an array
my_array = np.array([1,4,9,16])
my_array
```

    array([ 1,  4,  9, 16])

```python
print(f'Size of my array: {my_array.size}, or length of my array: {len(my_array)}')
print (f'Shape of my array: {my_array.shape}')
```

    Size of my array: 4, or length of my array: 4
    Shape of my array: (4,)

#### Notice the way the shape appears in numpy arrays

- For a 1D array, .shape returns a tuple with 1 element (n,)
- For a 2D array, .shape returns a tuple with 2 elements (n,m)
- For a 3D array, .shape returns a tuple with 3 elements (n,m,p)

```python
# How to reshape a 1D array to a 2D
my_array.reshape(2,-1)
```

    array([[ 1,  4],
           [ 9, 16]])

Numpy arrays support the same operations as lists! Below we slice and iterate.

```python
print("array[2:4]:", my_array[2:4]) # A slice of the array

# Iterate over the array
for ele in my_array:
    print("element:", ele)
```

    array[2:4]: [ 9 16]
    element: 1
    element: 4
    element: 9
    element: 16

Remember `numpy` gains a lot of its efficiency from being **strongly typed** (all elements are of the same type, such as integer or floating point). If the elements of an array are of a different type, `numpy` will force them into the same type (the longest in terms of bytes)

```python
mixed = np.array([1, 2.3, 'eleni', True])
print(type(1), type(2.3), type('eleni'), type(True))
mixed # all elements will become strings
```

    <class 'int'> <class 'float'> <class 'str'> <class 'bool'>

    array(['1', '2.3', 'eleni', 'True'], dtype='<U32')

Next, we push ahead to two-dimensional arrays and begin to dive into some of the deeper aspects of `numpy`.

```python
# create a 2d-array by handing a list of lists
my_array2d = np.array([ [1, 2, 3, 4],
                        [5, 6, 7, 8],
                        [9, 10, 11, 12]
])

my_array2d
```

    array([[ 1,  2,  3,  4],
           [ 5,  6,  7,  8],
           [ 9, 10, 11, 12]])

### Array Slicing (a reminder...)

Numpy arrays can be sliced, and can be iterated over with loops.  Below is a schematic illustrating slicing two-dimensional arrays.

Notice that the list slicing syntax still works!
`array[2:,3]` says "in the array, get rows 2 through the end, column 3]"
`array[3,:]` says "in the array, get row 3, all columns".

### Pandas Slicing (a reminder...)

`.iloc` is by position (position is unique), `.loc` is by label (label is not unique)

```python
# import cast dataframe
cast = pd.read_csv('../data/cast.csv', encoding='utf_8')
cast.head()
```

      title
      year
      name
      type
      character
      n

      0
      Closet Monster
      2015
      Buffy #1
      actor
      Buffy 4
      31.0

      1
      Suuri illusioni
      1985
      Homo $
      actor
      Guests
      22.0

      2
      Battle of the Sexes
      2017
      $hutter
      actor
      Bobby Riggs Fan
      10.0

      3
      Secret in Their Eyes
      2015
      $hutter
      actor
      2002 Dodger Fan
      NaN

      4
      Steve Jobs
      2015
      $hutter
      actor
      1988 Opera House Patron
      NaN

```python
# get me rows 10 to 13 (python slicing style : exclusive of end)
cast.iloc[10:13]
```

      title
      year
      name
      type
      character
      n

      10
      When the Man Went South
      2014
      Taipaleti 'Atu'ake
      actor
      Two Palms - Ua'i Paame
      8.0

      11
      Little Angel (Angelita)
      2015
      Michael 'babeepower' Viera
      actor
      Chico
      9.0

      12
      Mixing Nia
      1998
      Michael 'babeepower' Viera
      actor
      Rapper
      NaN

```python
# get me columns 0 to 2 but all rows - use head()
cast.iloc[:, 0:2].head()
```

      title
      year

      0
      Closet Monster
      2015

      1
      Suuri illusioni
      1985

      2
      Battle of the Sexes
      2017

      3
      Secret in Their Eyes
      2015

      4
      Steve Jobs
      2015

```python
# get me rows 10 to 13 AND only columns 0 to 2
cast.iloc[10:13, 0:2]
```

      title
      year

      10
      When the Man Went South
      2014

      11
      Little Angel (Angelita)
      2015

      12
      Mixing Nia
      1998

```python
# COMPARE: get me rows 10 to 13 (pandas slicing style : inclusive of end)
cast.loc[10:13]
```

      title
      year
      name
      type
      character
      n

      10
      When the Man Went South
      2014
      Taipaleti 'Atu'ake
      actor
      Two Palms - Ua'i Paame
      8.0

      11
      Little Angel (Angelita)
      2015
      Michael 'babeepower' Viera
      actor
      Chico
      9.0

      12
      Mixing Nia
      1998
      Michael 'babeepower' Viera
      actor
      Rapper
      NaN

      13
      The Replacements
      2000
      Steven 'Bear'Boyd
      actor
      Defensive Tackle - Washington Sentinels
      NaN

```python
# give me columns 'year' and 'type' by label but only for rows 5 to 10
cast.loc[5:10,['year','type']]
```

      year
      type

      5
      2015
      actor

      6
      2015
      actor

      7
      2009
      actor

      8
      2014
      actor

      9
      2014
      actor

      10
      2014
      actor

```python

```

## 2 - Plotting with matplotlib and beyond

`matplotlib` is a very powerful `python` library for making scientific plots.

We will not focus too much on the internal aspects of `matplotlib` in today's lab. There are many excellent tutorials out there for `matplotlib`.  For example,
* [`matplotlib` homepage](https://matplotlib.org/)
* [`matplotlib` tutorial](https://github.com/matplotlib/AnatomyOfMatplotlib)

Conveying your findings convincingly is an absolutely crucial part of any analysis. Therefore, you must be able to write well and make compelling visuals.  Creating informative visuals is an involved process and we won't cover that in this lab.  However, part of creating informative data visualizations means generating *readable* figures.  If people can't read your figures or have a difficult time interpreting them, they won't understand the results of your work.  Here are some non-negotiable commandments for any plot:
* Label $x$ and $y$ axes
* Axes labels should be informative
* Axes labels should be large enough to read
* Make tick labels large enough
* Include a legend if necessary
* Include a title if necessary
* Use appropriate line widths
* Use different line styles for different lines on the plot
* Use different markers for different lines

There are other important elements, but that list should get you started on your way.

We will work with `matplotlib` and `seaborn` for plotting in this class.  `matplotlib` is a very powerful `python` library for making scientific plots.  `seaborn` is a little more specialized in that it was developed for statistical data visualization.  We will cover some `seaborn` later in class. In the meantime you can look at the [seaborn documentation](https://seaborn.pydata.org)

First, let's generate some data.

#### Let's plot some functions

We will use the following three functions to make some plots:

* Logistic function:
  \begin{align*}
    f\left(z\right) = \dfrac{1}{1 + be^{-az}}
  \end{align*}
  where $a$ and $b$ are parameters.
* Hyperbolic tangent:
  \begin{align*}
    g\left(z\right) = b\tanh\left(az\right) + c
  \end{align*}
  where $a$, $b$, and $c$ are parameters.
* Rectified Linear Unit:
  \begin{align*}
    h\left(z\right) =
    \left\{
      \begin{array}{lr}
        z, \quad z > 0 \\
        \epsilon z, \quad z\leq 0
      \end{array}
    \right.
  \end{align*}
  where $\epsilon < 0$ is a small, positive parameter.

You are given the code for the first two functions.  Notice that $z$ is passed in as a `numpy` array and that the functions are returned as `numpy` arrays.  Parameters are passed in as floats.

You should write a function to compute the rectified linear unit.  The input should be a `numpy` array for $z$ and a positive float for $\epsilon$.

```python
import numpy as np

def logistic(z: np.ndarray, a: float, b: float) -> np.ndarray:
    """ Compute logistic function
      Inputs:
         a: exponential parameter
         b: exponential prefactor
         z: numpy array; domain
      Outputs:
         f: numpy array of floats, logistic function
    """

    den = 1.0 + b * np.exp(-a * z)
    return 1.0 / den

def stretch_tanh(z: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
    """ Compute stretched hyperbolic tangent
      Inputs:
         a: horizontal stretch parameter (a>1 implies a horizontal squish)
         b: vertical stretch parameter
         c: vertical shift parameter
         z: numpy array; domain
      Outputs:
         g: numpy array of floats, stretched tanh
    """
    return b * np.tanh(a * z) + c

def relu(z: np.ndarray, eps: float = 0.01) -> np.ndarray:
    """ Compute rectificed linear unit
      Inputs:
         eps: small positive parameter
         z: numpy array; domain
      Outputs:
         h: numpy array; relu
    """
    return np.fmax(z, eps * z)
```

Now let's make some plots.  First, let's just warm up and plot the logistic function.

```python
x = np.linspace(-5.0, 5.0, 100) # Equally spaced grid of 100 pts between -5 and 5

f = logistic(x, 1.0, 1.0) # Generate data
```

```python
plt.plot(x, f)
plt.xlabel('x')
plt.ylabel('f')
plt.title('Logistic Function')
plt.grid(True)
```

#### Figures with subplots

Let's start thinking about the plots as objects. We have the `figure` object which is like a matrix of smaller plots named `axes`. You can use array notation when handling it.

```python
fig, ax = plt.subplots(1,1) # Get figure and axes objects

ax.plot(x, f) # Make a plot

# Create some labels
ax.set_xlabel('x')
ax.set_ylabel('f')
ax.set_title('Logistic Function')

# Grid
ax.grid(True)
```

Wow, it's *exactly* the same plot!  Notice, however, the use of `ax.set_xlabel()` instead of `plt.xlabel()`.  The difference is tiny, but you should be aware of it.  I will use this plotting syntax from now on.

What else do we need to do to make this figure better?  Here are some options:
* Make labels bigger!
* Make line fatter
* Make tick mark labels bigger
* Make the grid less pronounced
* Make figure bigger

Let's get to it.

```python
fig, ax = plt.subplots(1,1, figsize=(10,6)) # Make figure bigger

# Make line plot
ax.plot(x, f, lw=4)

# Update ticklabel size
ax.tick_params(labelsize=24)

# Make labels
ax.set_xlabel(r'$x$', fontsize=24) # Use TeX for mathematical rendering
ax.set_ylabel(r'$f(x)$', fontsize=24) # Use TeX for mathematical rendering
ax.set_title('Logistic Function', fontsize=24)

ax.grid(True, lw=1.5, ls='--', alpha=0.75)
```

Notice:
* `lw` stands for `linewidth`.  We could also write `ax.plot(x, f, linewidth=4)`
* `ls` stands for `linestyle`.
* `alpha` stands for transparency.

The only thing remaining to do is to change the $x$ limits.  Clearly these should go from $-5$ to $5$.

```python
# fig.savefig('logistic.png')

# Put this in a markdown cell and uncomment this to check what you saved.

```

    /bin/sh: -c: line 0: syntax error near unexpected token `../images/logistic.png'
    /bin/sh: -c: line 0: `[](../images/logistic.png)'

#### Resources
If you want to see all the styles available, please take a look at the documentation.
* [Line styles](https://matplotlib.org/2.0.1/api/lines_api.html#matplotlib.lines.Line2D.set_linestyle)
* [Marker styles](https://matplotlib.org/2.0.1/api/markers_api.html#module-matplotlib.markers)
* [Everything you could ever want](https://matplotlib.org/2.0.1/api/lines_api.html#matplotlib.lines.Line2D.set_marker)

We haven't discussed it yet, but you can also put a legend on a figure.  You'll do that in the next exercise.  Here are some additional resources:
* [Legend](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html)
* [Grid](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.grid.html)

`ax.legend(loc='best', fontsize=24);`

Exercise

Do the following:
* Make a figure with the logistic function, hyperbolic tangent, and rectified linear unit.
* Use different line styles for each plot
* Put a legend on your figure

Here's an example of a figure:

```python
# your code here

# First get the data
f = logistic(x, 2.0, 1.0)
g = stretch_tanh(x, 2.0, 0.5, 0.5)
h = relu(x)

fig, ax = plt.subplots(1,1, figsize=(10,6)) # Create figure object

# Make actual plots
# (Notice the label argument!)
ax.plot(x, f, lw=4, ls='-', label=r'$L(x;1)$')
ax.plot(x, g, lw=4, ls='--', label=r'$\tanh(2x)$')
ax.plot(x, h, lw=4, ls='-.', label=r'$relu(x; 0.01)$')

# Make the tick labels readable
ax.tick_params(labelsize=24)

# Set axes limits to make the scale nice
ax.set_xlim(x.min(), x.max())
ax.set_ylim(h.min(), 1.1)

# Make readable labels
ax.set_xlabel(r'$x$', fontsize=24)
ax.set_ylabel(r'$h(x)$', fontsize=24)
ax.set_title('Activation Functions', fontsize=24)

# Set up grid
ax.grid(True, lw=1.75, ls='--', alpha=0.75)

# Put legend on figure
ax.legend(loc='best', fontsize=24);

fig.savefig('../images/nice_plots.png')
```

Exercise

These figures look nice in the plot and it makes sense for comparison. Now let's put the 3 different figures in separate plots.

* Make a separate plot for each figure and line them up on the same row.

```python
# your code here
fig2, ax2 = plt.subplots(1,3, figsize=(30,6)) # Create figure object

# Make actual plots
# (Notice the label argument!)
log_graph=ax2[0].plot(x, f, lw=4, ls='-', label=r'$L(x;1)$')
tanh_graph=ax2[1].plot(x, g, lw=4, ls='--', label=r'$\tanh(2x)$')
relu_graph=ax2[2].plot(x, h, lw=4, ls='-.', label=r'$relu(x; 0.01)$')
```

```python
%load solutions/three_subplots.py
```

Exercise

* Make a grid of 2 x 3 separate plots, 3 will be empty. Just plot the functions and do not worry about cosmetics. We just want you ro see the functionality.

```python
# your code here
fig3, ax3= plt.subplots(2,3)
```

```python
 %load solutions/six_subplots.py
```

## 3 - Simple Linear Regression

Linear regression and its many extensions are a workhorse of the statistics and data science community, both in application and as a reference point for other models. Most of the major concepts in machine learning can be and often are discussed in terms of various linear regression models. Thus, this section will introduce you to building and fitting linear regression models and some of the process behind it, so that you can 1) fit models to data you encounter 2) experiment with different kinds of linear regression and observe their effects 3) see some of the technology that makes regression models work.

### Linear regression with a toy dataset
We first examine a toy problem, focusing our efforts on fitting a linear model to a small dataset with three observations.  Each observation consists of one predictor $x_i$ and one response $y_i$ for $i = 1, 2, 3$,

\begin{align*}
(x , y) = \{(x_1, y_1), (x_2, y_2), (x_3, y_3)\}.
\end{align*}

To be very concrete, let's set the values of the predictors and responses.

\begin{equation*}
(x , y) = \{(1, 2), (2, 2), (3, 4)\}
\end{equation*}

There is no line of the form $\beta_0 + \beta_1 x = y$ that passes through all three observations, since the data are not collinear. Thus our aim is to find the line that best fits these observations in the *least-squares sense*, as discussed in lecture.

Exercise (for home)

* Make two numpy arrays out of this data, x_train and y_train
* Check the dimentions of these arrays
* Try to reshape them into a different shape
* Make points into a very simple scatterplot
* Make a better scatterplot

```python
# your code here
```

```python
# solution
x_train = np.array([1,2,3])
y_train = np.array([2,3,6])
type(x_train)
```

    numpy.ndarray

```python
x_train.shape
```

    (3,)

```python
print(x_train)
x_train = x_train.reshape(3,1)
x_train.shape
x_train

```

    [1 2 3]

    array([[1],
           [2],
           [3]])

```python
xx = np.array([[1,3,5],[6,2,1]])
print(xx.shape)
print(xx)
```

    (2, 3)
    [[1 3 5]
     [6 2 1]]

```python
xx = xx.reshape(3,-1)
xx
```

    array([[1, 3],
           [5, 6],
           [2, 1]])

```python
# %load solutions/simple_scatterplot.py
# Make a simple scatterplot

plt.scatter(x_train,y_train)

# check dimensions
print(x_train.shape,y_train.shape)

print (x_train)
print (y_train)
```

```python
# %load solutions/nice_scatterplot.py
def nice_scatterplot(x, y, title):
    # font size
    f_size = 18

    # make the figure
    fig, ax = plt.subplots(1,1, figsize=(8,5)) # Create figure object

    # set axes limits to make the scale nice
    ax.set_xlim(np.min(x)-1, np.max(x) + 1)
    ax.set_ylim(np.min(y)-1, np.max(y) + 1)

    # adjust size of tickmarks in axes
    ax.tick_params(labelsize = f_size)

    # remove tick labels
    ax.tick_params(labelbottom=False,  bottom=False)

    # adjust size of axis label
    ax.set_xlabel(r'$x$', fontsize = f_size)
    ax.set_ylabel(r'$y$', fontsize = f_size)

    # set figure title label
    ax.set_title(title, fontsize = f_size)

    # you may set up grid with this
    ax.grid(True, lw=1.75, ls='--', alpha=0.15)

    # make actual plot (Notice the label argument!)
    #ax.scatter(x, y, label=r'$my points$')
    #ax.scatter(x, y, label='$my points$')
    ax.scatter(x, y, label=r'$my\,points$')
    ax.legend(loc='best', fontsize = f_size);

    return ax

nice_scatterplot(x_train, y_train, 'hello nice plot')

```

#### Formulae
Linear regression is special among the models we study because it can be solved explicitly. While most other models (and even some advanced versions of linear regression) must be solved itteratively, linear regression has a formula where you can simply plug in the data.

For the single predictor case it is:
    \begin{align}
      \beta_1 &= \frac{\sum_{i=1}^n{(x_i-\bar{x})(y_i-\bar{y})}}{\sum_{i=1}^n{(x_i-\bar{x})^2}}\\
      \beta_0 &= \bar{y} - \beta_1\bar{x}\
    \end{align}

Where $\bar{y}$ and $\bar{x}$ are the mean of the y values and the mean of the x values, respectively.

From the re-aranged second equation we can see that the best-fit line  passes through $(\bar{x},\bar{y})$, the center of mass of the data

From any of the first equations, we can see that the slope of the line has to do with whether or not an x value that is above/below the center of mass is typically paired with a y value that is likewise above/below, or typically paired with one that is opposite.

###  Building a model from scratch
In this part, we will solve the equations for simple linear regression and find the best fit solution to our toy problem.

The snippets of code below implement the linear regression equations on the observed predictors and responses, which we'll call the training data set.  Let's walk through the code.

We have to reshape our arrrays to 2D. We will see later why.

Exercise

* make an array with shape (2,3)
* reshape it to a size that you want

```python
# your code here
xx = np.array([[1,2,3],[4,6,8]])
xxx = xx.reshape(-1,2)
xxx.shape
```

    (3, 2)

```python
# Reshape to be a proper 2D array
print(x_train.shape)
x_train = x_train.reshape(x_train.shape[0], 1)
y_train = y_train.reshape(y_train.shape[0], 1)

print(x_train.shape)
```

    (3, 1)
    (3, 1)

```python
# first, compute means
y_bar = np.mean(y_train)
x_bar = np.mean(x_train)

# build the two terms
numerator = np.sum( (x_train - x_bar)*(y_train - y_bar) )
denominator = np.sum((x_train - x_bar)**2)

print(numerator.shape, denominator.shape) #check shapes
```

    () ()

* Why the empty brackets? (The numerator and denominator are scalars, as expected.)

```python
#slope beta1
beta_1 = numerator/denominator

#intercept beta0
beta_0 = y_bar - beta_1*x_bar

print(beta_0)

print("The best-fit line is {0:3.2f} + {1:3.2f} * x".format(beta_0, beta_1))
print(f'The best fit is {beta_0}')
```

    -0.3333333333333335
    The best-fit line is -0.33 + 2.00 * x
    The best fit is -0.3333333333333335

Exercise

Turn the code from the above cells into a function called `simple_linear_regression_fit`, that inputs the training data and returns `beta0` and `beta1`.

To do this, copy and paste the code from the above cells below and adjust the code as needed, so that the training data becomes the input and the betas become the output.

```python
def simple_linear_regression_fit(x_train: np.ndarray, y_train: np.ndarray) -> np.ndarray:

    return
```

Check your function by calling it with the training data from above and printing out the beta values.

```python
# Your code here
```

```python
# %load solutions/simple_linear_regression_fit.py
def simple_linear_regression_fit(x_train: np.ndarray, y_train: np.ndarray) -> np.ndarray:
    """
    Inputs:
    x_train: a (num observations by 1) array holding the values of the predictor variable
    y_train: a (num observations by 1) array holding the values of the response variable

    Returns:
    beta_vals:  a (num_features by 1) array holding the intercept and slope coeficients
    """

    # Check input array sizes
    if len(x_train.shape) < 2:
        print("Reshaping features array.")
        x_train = x_train.reshape(x_train.shape[0], 1)

    if len(y_train.shape) < 2:
        print("Reshaping observations array.")
        y_train = y_train.reshape(y_train.shape[0], 1)

    # first, compute means
    y_bar = np.mean(y_train)
    x_bar = np.mean(x_train)

    # build the two terms
    numerator = np.sum( (x_train - x_bar)*(y_train - y_bar) )
    denominator = np.sum((x_train - x_bar)**2)

    #slope beta1
    beta_1 = numerator/denominator

    #intercept beta0
    beta_0 = y_bar - beta_1*x_bar

    return np.array([beta_0,beta_1])

```

* Let's run this function and see the coefficients

```python
x_train = np.array([1 ,2, 3])
y_train = np.array([2, 2, 4])

betas = simple_linear_regression_fit(x_train, y_train)

beta_0 = betas[0]
beta_1 = betas[1]

print("The best-fit line is {0:8.6f} + {1:8.6f} * x".format(beta_0, beta_1))
```

Exercise

* Do the values of `beta0` and `beta1` seem reasonable?
* Plot the training data using a scatter plot.
* Plot the best fit line with `beta0` and `beta1` together with the training data.

```python
# Your code here
```

```python
# %load solutions/best_fit_scatterplot.py
fig_scat, ax_scat = plt.subplots(1,1, figsize=(10,6))

# Plot best-fit line
x_train = np.array([[1, 2, 3]]).T

best_fit = beta_0 + beta_1 * x_train

ax_scat.scatter(x_train, y_train, s=300, label='Training Data')
ax_scat.plot(x_train, best_fit, ls='--', label='Best Fit Line')

ax_scat.set_xlabel(r'$x_{train}$')
ax_scat.set_ylabel(r'$y$');

```

The values of `beta0` and `beta1` seem roughly reasonable.  They capture the positive correlation.  The line does appear to be trying to get as close as possible to all the points.

## 4 - Building a model with `statsmodels` and `sklearn`

Now that we can concretely fit the training data from scratch, let's learn two `python` packages to do it all for us:
* [statsmodels](http://www.statsmodels.org/stable/regression.html) and
* [scikit-learn (sklearn)](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html).

Our goal  is to show how to implement simple linear regression with these packages.  For an important sanity check, we compare the $\beta$ values from `statsmodels` and `sklearn` to the $\beta$ values that we found from above with our own implementation.

For the purposes of this lab, `statsmodels` and `sklearn` do the same thing.  More generally though, `statsmodels` tends to be easier for inference \[finding the values of the slope and intercept and dicussing uncertainty in those values\], whereas `sklearn` has machine-learning algorithms and is better for prediction \[guessing y values for a given x value\]. (Note that both packages make the same guesses, it's just a question of which activity they provide more support for.

**Note:** `statsmodels` and `sklearn` are different packages!  Unless we specify otherwise, you can use either one.

Below is the code for `statsmodels`.  `Statsmodels` does not by default include the column of ones in the $X$ matrix, so we include it manually with `sm.add_constant`.

```python
import statsmodels.api as sm
```

```python
# create the X matrix by appending a column of ones to x_train
X = sm.add_constant(x_train)
print(x_train)
print(y_train)

# this is the same matrix as in our scratch problem!
print(X)

# build the OLS model (ordinary least squares) from the training data
toyregr_sm = sm.OLS(y_train, X)

# do the fit and save regression info (parameters, etc) in results_sm
results_sm = toyregr_sm.fit()

# pull the beta parameters out from results_sm
beta0_sm = results_sm.params[0]
beta1_sm = results_sm.params[1]

print(f'The regression coef from statsmodels are: beta_0 = {beta0_sm:8.6f} and beta_1 = {beta1_sm:8.6f}')
```

    [[1]
     [2]
     [3]]
    [[2]
     [3]
     [6]]
    [[1. 1.]
     [1. 2.]
     [1. 3.]]
    The regression coef from statsmodels are: beta_0 = -0.333333 and beta_1 = 2.000000

Besides the beta parameters, `results_sm` contains a ton of other potentially useful information.

```python
import warnings
warnings.filterwarnings('ignore')
print(results_sm.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      y   R-squared:                       0.923
    Model:                            OLS   Adj. R-squared:                  0.846
    Method:                 Least Squares   F-statistic:                     12.00
    Date:                Tue, 24 Sep 2019   Prob (F-statistic):              0.179
    Time:                        01:28:47   Log-Likelihood:                -2.0007
    No. Observations:                   3   AIC:                             8.001
    Df Residuals:                       1   BIC:                             6.199
    Df Model:                           1
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const         -0.3333      1.247     -0.267      0.834     -16.181      15.514
    x1             2.0000      0.577      3.464      0.179      -5.336       9.336
    ==============================================================================
    Omnibus:                          nan   Durbin-Watson:                   3.000
    Prob(Omnibus):                    nan   Jarque-Bera (JB):                0.531
    Skew:                          -0.707   Prob(JB):                        0.767
    Kurtosis:                       1.500   Cond. No.                         6.79
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Now let's turn our attention to the `sklearn` library.

```python
from sklearn import linear_model
```

```python
# build the least squares model
toyregr = linear_model.LinearRegression()

# save regression info (parameters, etc) in results_skl
results = toyregr.fit(x_train, y_train)

# pull the beta parameters out from results_skl
beta0_skl = toyregr.intercept_
beta1_skl = toyregr.coef_[0]

print("The regression coefficients from the sklearn package are: beta_0 = {0:8.6f} and beta_1 = {1:8.6f}".format(beta0_skl, beta1_skl))
```

We should feel pretty good about ourselves now, and we're ready to move on to a real problem!

### The `scikit-learn` library and the shape of things

Before diving into a "real" problem, let's discuss more of the details of `sklearn`.

`Scikit-learn` is the main `Python` machine learning library. It consists of many learners which can learn models from data, as well as a lot of utility functions such as `train_test_split()`.

Use the following to add the library into your code:

```python
import sklearn
```

In `scikit-learn`, an **estimator** is a Python object that implements the methods `fit(X, y)` and `predict(T)`

Let's see the structure of `scikit-learn` needed to make these fits. `fit()` always takes two arguments:
```python
estimator.fit(Xtrain, ytrain)
```
We will consider two estimators in this lab: `LinearRegression` and `KNeighborsRegressor`.

It is very important to understand that `Xtrain` must be in the form of a **2x2 array** with each row corresponding to one sample, and each column corresponding to the feature values for that sample.

`ytrain` on the other hand is a simple array of responses.  These are continuous for regression problems.

### Practice with `sklearn` and a real dataset
We begin by loading up the `mtcars` dataset. This data was extracted from the 1974 Motor Trend US magazine, and comprises of fuel consumption and 10 aspects of automobile design and performance for 32 automobiles (1973–74 models). We will load this data to a dataframe with 32 observations on 11 (numeric) variables. Here is an explanation of the features:

- `mpg` is Miles/(US) gallon
- `cyl` is Number of cylinders,
- `disp` is	Displacement (cu.in.),
- `hp` is	Gross horsepower,
- `drat` is	Rear axle ratio,
- `wt` is the Weight (1000 lbs),
- `qsec` is 1/4 mile time,
- `vs` is Engine (0 = V-shaped, 1 = straight),
- `am` is Transmission (0 = automatic, 1 = manual),
- `gear` is the Number of forward gears,
- `carb` is	Number of carburetors.

```python
import pandas as pd

#load mtcars
dfcars = pd.read_csv("../data/mtcars.csv")
dfcars.head()
```

      Unnamed: 0
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      1
      Mazda RX4 Wag
      21.0
      6
      160.0
      110
      3.90
      2.875
      17.02
      0
      1
      4
      4

      2
      Datsun 710
      22.8
      4
      108.0
      93
      3.85
      2.320
      18.61
      1
      1
      4
      1

      3
      Hornet 4 Drive
      21.4
      6
      258.0
      110
      3.08
      3.215
      19.44
      1
      0
      3
      1

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

```python
# Fix the column title
dfcars = dfcars.rename(columns={"Unnamed: 0":"car name"})
dfcars.head()
```

      car name
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      1
      Mazda RX4 Wag
      21.0
      6
      160.0
      110
      3.90
      2.875
      17.02
      0
      1
      4
      4

      2
      Datsun 710
      22.8
      4
      108.0
      93
      3.85
      2.320
      18.61
      1
      1
      4
      1

      3
      Hornet 4 Drive
      21.4
      6
      258.0
      110
      3.08
      3.215
      19.44
      1
      0
      3
      1

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

```python
dfcars.shape
```

    (32, 12)

#### Searching for values: how many cars have 4 gears?

```python
len(dfcars[dfcars.gear == 4].drop_duplicates(subset='car name', keep='first'))
```

    12

Next, let's split the dataset into a training set and test set.

```python
# split into training set and testing set
from sklearn.model_selection import train_test_split

#set random_state to get the same split every time
traindf, testdf = train_test_split(dfcars, test_size=0.2, random_state=42)
```

```python
# testing set is around 20% of the total data; training set is around 80%
print("Shape of full dataset is: {0}".format(dfcars.shape))
print("Shape of training dataset is: {0}".format(traindf.shape))
print("Shape of test dataset is: {0}".format(testdf.shape))
print(testdf)
print(traindf)
#data frames are listed with #rows, columns
```

    Shape of full dataset is: (32, 12)
    Shape of training dataset is: (25, 12)
    Shape of test dataset is: (7, 12)
                   car name   mpg  cyl   disp   hp  drat     wt   qsec  vs  am  gear  carb
    29         Ferrari Dino  19.7    6  145.0  175  3.62  2.770  15.50   0   1     5     6
    15  Lincoln Continental  10.4    8  460.0  215  3.00  5.424  17.82   0   0     3     4
    24     Pontiac Firebird  19.2    8  400.0  175  3.08  3.845  17.05   0   0     3     2
    17             Fiat 128  32.4    4   78.7   66  4.08  2.200  19.47   1   1     4     1
    8              Merc 230  22.8    4  140.8   95  3.92  3.150  22.90   1   0     4     2
    9              Merc 280  19.2    6  167.6  123  3.92  3.440  18.30   1   0     4     4
    30        Maserati Bora  15.0    8  301.0  335  3.54  3.570  14.60   0   1     5     8
                  car name   mpg  cyl   disp   hp  drat     wt   qsec  vs  am  gear  carb
    25           Fiat X1-9  27.3    4   79.0   66  4.08  1.935  18.90   1   1     4     1
    12          Merc 450SL  17.3    8  275.8  180  3.07  3.730  17.60   0   0     3     3
    0            Mazda RX4  21.0    6  160.0  110  3.90  2.620  16.46   0   1     4     4
    4    Hornet Sportabout  18.7    8  360.0  175  3.15  3.440  17.02   0   0     3     2
    16   Chrysler Imperial  14.7    8  440.0  230  3.23  5.345  17.42   0   0     3     4
    5              Valiant  18.1    6  225.0  105  2.76  3.460  20.22   1   0     3     1
    13         Merc 450SLC  15.2    8  275.8  180  3.07  3.780  18.00   0   0     3     3
    11          Merc 450SE  16.4    8  275.8  180  3.07  4.070  17.40   0   0     3     3
    23          Camaro Z28  13.3    8  350.0  245  3.73  3.840  15.41   0   0     3     4
    1        Mazda RX4 Wag  21.0    6  160.0  110  3.90  2.875  17.02   0   1     4     4
    2           Datsun 710  22.8    4  108.0   93  3.85  2.320  18.61   1   1     4     1
    26       Porsche 914-2  26.0    4  120.3   91  4.43  2.140  16.70   0   1     5     2
    3       Hornet 4 Drive  21.4    6  258.0  110  3.08  3.215  19.44   1   0     3     1
    21    Dodge Challenger  15.5    8  318.0  150  2.76  3.520  16.87   0   0     3     2
    27        Lotus Europa  30.4    4   95.1  113  3.77  1.513  16.90   1   1     5     2
    22         AMC Javelin  15.2    8  304.0  150  3.15  3.435  17.30   0   0     3     2
    18         Honda Civic  30.4    4   75.7   52  4.93  1.615  18.52   1   1     4     2
    31          Volvo 142E  21.4    4  121.0  109  4.11  2.780  18.60   1   1     4     2
    20       Toyota Corona  21.5    4  120.1   97  3.70  2.465  20.01   1   0     3     1
    7            Merc 240D  24.4    4  146.7   62  3.69  3.190  20.00   1   0     4     2
    10           Merc 280C  17.8    6  167.6  123  3.92  3.440  18.90   1   0     4     4
    14  Cadillac Fleetwood  10.4    8  472.0  205  2.93  5.250  17.98   0   0     3     4
    28      Ford Pantera L  15.8    8  351.0  264  4.22  3.170  14.50   0   1     5     4
    19      Toyota Corolla  33.9    4   71.1   65  4.22  1.835  19.90   1   1     4     1
    6           Duster 360  14.3    8  360.0  245  3.21  3.570  15.84   0   0     3     4

Now we have training and test data.  We still need to select a predictor and a response from this dataset.  Keep in mind that we need to choose the predictor and response from both the training and test set.  You will do this in the exercises below.  However, we provide some starter code for you to get things going.

```python
traindf.head()
```

      car name
      mpg
      cyl
      disp
      hp
      drat
      wt
      qsec
      vs
      am
      gear
      carb

      25
      Fiat X1-9
      27.3
      4
      79.0
      66
      4.08
      1.935
      18.90
      1
      1
      4
      1

      12
      Merc 450SL
      17.3
      8
      275.8
      180
      3.07
      3.730
      17.60
      0
      0
      3
      3

      0
      Mazda RX4
      21.0
      6
      160.0
      110
      3.90
      2.620
      16.46
      0
      1
      4
      4

      4
      Hornet Sportabout
      18.7
      8
      360.0
      175
      3.15
      3.440
      17.02
      0
      0
      3
      2

      16
      Chrysler Imperial
      14.7
      8
      440.0
      230
      3.23
      5.345
      17.42
      0
      0
      3
      4

```python
# Extract the response variable that we're interested in
y_train = traindf.mpg
y_train
```

    25    27.3
    12    17.3
    0     21.0
    4     18.7
    16    14.7
    5     18.1
    13    15.2
    11    16.4
    23    13.3
    1     21.0
    2     22.8
    26    26.0
    3     21.4
    21    15.5
    27    30.4
    22    15.2
    18    30.4
    31    21.4
    20    21.5
    7     24.4
    10    17.8
    14    10.4
    28    15.8
    19    33.9
    6     14.3
    Name: mpg, dtype: float64

Exercise

Use slicing to get the same vector `y_train`

----

Now, notice the shape of `y_train`.

```python
y_train.shape, type(y_train)
```

    ((25,), pandas.core.series.Series)

### Array reshape
This is a 1D array as should be the case with the **Y** array.  Remember, `sklearn` requires a 2D array only for the predictor array.  You will have to pay close attention to this in the exercises later. `Sklearn` doesn't care too much about the shape of `y_train`.

The whole reason we went through that whole process was to show you how to reshape your data into the correct format.

**IMPORTANT:** Remember that your response variable `ytrain` can be a vector but your predictor variable `xtrain` ***must*** be an array!

## 5 - Example: Simple linear regression with automobile data
We will now use `sklearn` to predict automobile mileage per gallon (mpg) and evaluate these predictions. We already loaded the data and split them into a training set and a test set.

We need to choose the variables that we think will be good predictors for the dependent variable `mpg`.

Exercise in pairs

* Pick one variable to use as a predictor for simple linear regression.  Discuss your reasons with the person next to you.
* Justify your choice with some visualizations.
* Is there a second variable you'd like to use? For example, we're not doing multiple linear regression here, but if we were, is there another variable you'd like to include if we were using two predictors?

```python
x_wt = dfcars.wt
x_wt.shape
```

    (32,)

```python
# Your code here

```

```python
 %load solutions/cars_simple_EDA.py
```

Exercise

* Use `sklearn` to fit the training data using simple linear regression.
* Use the model to make mpg predictions on the test set.
* Plot the data and the prediction.
* Print out the mean squared error for the training set and the test set and compare.

```python
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

dfcars = pd.read_csv("../data/mtcars.csv")
dfcars = dfcars.rename(columns={"Unnamed: 0":"name"})

dfcars.head()
```

```python
traindf, testdf = train_test_split(dfcars, test_size=0.2, random_state=42)

y_train = np.array(traindf.mpg)
X_train = np.array(traindf.wt)
X_train = X_train.reshape(X_train.shape[0], 1)
```

```python
y_test = np.array(testdf.mpg)
X_test = np.array(testdf.wt)
X_test = X_test.reshape(X_test.shape[0], 1)
```

```python
# Let's take another look at our data
dfcars.head()
```

```python
# And out train and test sets
y_train.shape, X_train.shape
```

```python
y_test.shape, X_test.shape
```

```python
#create linear model
regression = LinearRegression()

#fit linear model
regression.fit(X_train, y_train)

predicted_y = regression.predict(X_test)

r2 = regression.score(X_test, y_test)
print(f'R^2 = {r2:.5}')
```

```python
print(regression.score(X_train, y_train))

print(mean_squared_error(predicted_y, y_test))
print(mean_squared_error(y_train, regression.predict(X_train)))

print('Coefficients: \n', regression.coef_[0], regression.intercept_)
```

```python
fig, ax = plt.subplots(1,1, figsize=(10,6))
ax.plot(y_test, predicted_y, 'o')
grid = np.linspace(np.min(dfcars.mpg), np.max(dfcars.mpg), 100)
ax.plot(grid, grid, color="black") # 45 degree line
ax.set_xlabel("actual y")
ax.set_ylabel("predicted y")

fig1, ax1 = plt.subplots(1,1, figsize=(10,6))
ax1.plot(dfcars.wt, dfcars.mpg, 'o')
xgrid = np.linspace(np.min(dfcars.wt), np.max(dfcars.wt), 100)
ax1.plot(xgrid, regression.predict(xgrid.reshape(100, 1)))
```

## 6 - $k$-nearest neighbors

Now that you're familiar with `sklearn`, you're ready to do a KNN regression.

Sklearn's regressor is called `sklearn.neighbors.KNeighborsRegressor`. Its main parameter is the `number of nearest neighbors`. There are other parameters such as the distance metric (default for 2 order is the Euclidean distance). For a list of all the parameters see the [Sklearn kNN Regressor Documentation](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html).

Let's use $5$ nearest neighbors.

```python
# Import the library
from sklearn.neighbors import KNeighborsRegressor
```

```python
# Set number of neighbors
k = 5
knnreg = KNeighborsRegressor(n_neighbors=k)
```

```python
# Fit the regressor - make sure your numpy arrays are the right shape
knnreg.fit(X_train, y_train)

# Evaluate the outcome on the train set using R^2
r2_train = knnreg.score(X_train, y_train)

# Print results
print(f'kNN model with {k} neighbors gives R^2 on the train set: {r2_train:.5}')
```

```python
knnreg.predict(X_test)
```

Exercise

Calculate and print the $R^{2}$ score on the test set

```python
# Your code here
```

Not so good? Lets vary the number of neighbors and see what we get.

```python
# Make our lives easy by storing the different regressors in a dictionary
regdict = {}

# Make our lives easier by entering the k values from a list
k_list = [1, 2, 4, 15]

# Do a bunch of KNN regressions
for k in k_list:
    knnreg = KNeighborsRegressor(n_neighbors=k)
    knnreg.fit(X_train, y_train)
    # Store the regressors in a dictionary
    regdict[k] = knnreg

# Print the dictionary to see what we have
regdict
```

Now let's plot all the k values in same plot.

```python
fig, ax = plt.subplots(1,1, figsize=(10,6))

ax.plot(dfcars.wt, dfcars.mpg, 'o', label="data")

xgrid = np.linspace(np.min(dfcars.wt), np.max(dfcars.wt), 100)

# let's unpack the dictionary to its elements (items) which is the k and Regressor
for k, regressor in regdict.items():
    predictions = regressor.predict(xgrid.reshape(-1,1))
    ax.plot(xgrid, predictions, label="{}-NN".format(k))

ax.legend();
```

Exercise

Explain what you see in the graph. **Hint** Notice how the $1$-NN goes through every point on the training set but utterly fails elsewhere.

Lets look at the scores on the training set.

```python
ks = range(1, 15) # Grid of k's
scores_train = [] # R2 scores
for k in ks:
    # Create KNN model
    knnreg = KNeighborsRegressor(n_neighbors=k)

    # Fit the model to training data
    knnreg.fit(X_train, y_train)

    # Calculate R^2 score
    score_train = knnreg.score(X_train, y_train)
    scores_train.append(score_train)

# Plot
fig, ax = plt.subplots(1,1, figsize=(12,8))
ax.plot(ks, scores_train,'o-')
ax.set_xlabel(r'$k$')
ax.set_ylabel(r'$R^{2}$')
```

Exercise

* Why do we get a perfect $R^2$ at k=1 for the training set?
* Make the same plot as above on the *test* set.
* What is the best $k$?

```python
# Your code here

```

```python
# %load solutions/knn_regression.py
```

```python
# solution to previous exercise
r2_test = knnreg.score(X_test, y_test)
print(f'kNN model with {k} neighbors gives R^2 on the test set: {r2_test:.5}')
```


##### Copyright 2020 The TensorFlow Authors.

```
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
```

# Quantum data

    View on TensorFlow.org

    Run in Google Colab

    View source on GitHub

    Download notebook

Building off of the comparisons made in the [MNIST](https://www.tensorflow.org/quantum/tutorials/mnist) tutorial, this tutorial explores the recent work of [Huang et al.](https://arxiv.org/abs/2011.01938) that shows how different datasets affect performance comparisons. In the work, the authors seek to understand how and when classical machine learning models can learn as well as (or better than) quantum models. The work also showcases an empirical performance separation between classical and quantum machine learning model via a carefully crafted dataset. You will:

1.   Prepare a reduced dimension Fashion-MNIST dataset.
2.   Use quantum circuits to re-label the dataset and compute Projected Quantum Kernel features (PQK).
3.   Train a classical neural network on the re-labeled dataset and compare the performance with a model that has access to the PQK features.

## Setup

```
!pip -q install tensorflow==2.3.1 tensorflow-quantum
```

```
import cirq
import sympy
import numpy as np
import tensorflow as tf
import tensorflow_quantum as tfq

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
np.random.seed(1234)
```

## 1. Data preparation

You will begin by preparing the fashion-MNIST dataset for running on a quantum computer.

### 1.1 Download fashion-MNIST

The first step is to get the traditional fashion-mnist dataset. This can be done using the `tf.keras.datasets` module.

```
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

# Rescale the images from [0,255] to the [0.0,1.0] range.
x_train, x_test = x_train[..., np.newaxis]/255.0, x_test[..., np.newaxis]/255.0

print("Number of original training examples:", len(x_train))
print("Number of original test examples:", len(x_test))
```

Filter the dataset to keep just the shirts and dresses, remove the other classes. At the same time convert the label, `y`, to boolean: True for 0 and False for 3.

```
def filter_03(x, y):
    keep = (y == 0) | (y == 3)
    x, y = x[keep], y[keep]
    y = y == 0
    return x,y
```

```
x_train, y_train = filter_03(x_train, y_train)
x_test, y_test = filter_03(x_test, y_test)

print("Number of filtered training examples:", len(x_train))
print("Number of filtered test examples:", len(x_test))
```

```
print(y_train[0])

plt.imshow(x_train[0, :, :, 0])
plt.colorbar()
```

### 1.2 Downscale the images

Just like the MNIST example, you will need to downscale these images in order to be within the boundaries for current quantum computers. This time however you will use a PCA transformation to reduce the dimensions instead of a `tf.image.resize` operation.

```
def truncate_x(x_train, x_test, n_components=10):
  """Perform PCA on image dataset keeping the top `n_components` components."""
  n_points_train = tf.gather(tf.shape(x_train), 0)
  n_points_test = tf.gather(tf.shape(x_test), 0)

  # Flatten to 1D
  x_train = tf.reshape(x_train, [n_points_train, -1])
  x_test = tf.reshape(x_test, [n_points_test, -1])

  # Normalize.
  feature_mean = tf.reduce_mean(x_train, axis=0)
  x_train_normalized = x_train - feature_mean
  x_test_normalized = x_test - feature_mean

  # Truncate.
  e_values, e_vectors = tf.linalg.eigh(
      tf.einsum('ji,jk->ik', x_train_normalized, x_train_normalized))
  return tf.einsum('ij,jk->ik', x_train_normalized, e_vectors[:,-n_components:]), \
    tf.einsum('ij,jk->ik', x_test_normalized, e_vectors[:, -n_components:])
```

```
DATASET_DIM = 10
x_train, x_test = truncate_x(x_train, x_test, n_components=DATASET_DIM)
print(f'New datapoint dimension:', len(x_train[0]))
```

The last step is to reduce the size of the dataset to just 1000 training datapoints and 200 testing datapoints.

```
N_TRAIN = 1000
N_TEST = 200
x_train, x_test = x_train[:N_TRAIN], x_test[:N_TEST]
y_train, y_test = y_train[:N_TRAIN], y_test[:N_TEST]
```

```
print("New number of training examples:", len(x_train))
print("New number of test examples:", len(x_test))
```

## 2. Relabeling and computing PQK features

You will now prepare a "stilted" quantum dataset by incorporating quantum components and re-labeling the truncated fashion-MNIST dataset you've created above. In order to get the most seperation between quantum and classical methods, you will first prepare the PQK features and then relabel outputs based on their values.

### 2.1 Quantum encoding and PQK features
You will create a new set of features, based on `x_train`, `y_train`, `x_test` and `y_test` that is defined to be the 1-RDM on all qubits of:

$V(x_{\text{train}} / n_{\text{trotter}}) ^ {n_{\text{trotter}}} U_{\text{1qb}} | 0 \rangle$

Where $U_\text{1qb}$ is a wall of single qubit rotations and $V(\hat{\theta}) = e^{-i\sum_i \hat{\theta_i} (X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1})}$

First, you can generate the wall of single qubit rotations:

```
def single_qubit_wall(qubits, rotations):
  """Prepare a single qubit X,Y,Z rotation wall on `qubits`."""
  wall_circuit = cirq.Circuit()
  for i, qubit in enumerate(qubits):
    for j, gate in enumerate([cirq.X, cirq.Y, cirq.Z]):
      wall_circuit.append(gate(qubit) ** rotations[i][j])

  return wall_circuit
```

You can quickly verify this works by looking at the circuit:

```
SVGCircuit(single_qubit_wall(
    cirq.GridQubit.rect(1,4), np.random.uniform(size=(4, 3))))
```

Next you can prepare $V(\hat{\theta})$ with the help of `tfq.util.exponential` which can exponentiate any commuting `cirq.PauliSum` objects:

```
def v_theta(qubits):
  """Prepares a circuit that generates V(\theta)."""
  ref_paulis = [
      cirq.X(q0) * cirq.X(q1) + \
      cirq.Y(q0) * cirq.Y(q1) + \
      cirq.Z(q0) * cirq.Z(q1) for q0, q1 in zip(qubits, qubits[1:])
  ]
  exp_symbols = list(sympy.symbols('ref_0:'+str(len(ref_paulis))))
  return tfq.util.exponential(ref_paulis, exp_symbols), exp_symbols
```

This circuit might be a little bit harder to verify by looking at, but you can still examine a two qubit case to see what is happening:

```
test_circuit, test_symbols = v_theta(cirq.GridQubit.rect(1, 2))
print(f'Symbols found in circuit:{test_symbols}')
SVGCircuit(test_circuit)
```

Now you have all the building blocks you need to put your full encoding circuits together:

```
def prepare_pqk_circuits(qubits, classical_source, n_trotter=10):
  """Prepare the pqk feature circuits around a dataset."""
  n_qubits = len(qubits)
  n_points = len(classical_source)

  # Prepare random single qubit rotation wall.
  random_rots = np.random.uniform(-2, 2, size=(n_qubits, 3))
  initial_U = single_qubit_wall(qubits, random_rots)

  # Prepare parametrized V
  V_circuit, symbols = v_theta(qubits)
  exp_circuit = cirq.Circuit(V_circuit for t in range(n_trotter))

  # Convert to `tf.Tensor`
  initial_U_tensor = tfq.convert_to_tensor([initial_U])
  initial_U_splat = tf.tile(initial_U_tensor, [n_points])

  full_circuits = tfq.layers.AddCircuit()(
      initial_U_splat, append=exp_circuit)
  # Replace placeholders in circuits with values from `classical_source`.
  return tfq.resolve_parameters(
      full_circuits, tf.convert_to_tensor([str(x) for x in symbols]),
      tf.convert_to_tensor(classical_source*(n_qubits/3)/n_trotter))
```

Chooe some qubits and prepare the data encoding circuits:

```
qubits = cirq.GridQubit.rect(1, DATASET_DIM + 1)
q_x_train_circuits = prepare_pqk_circuits(qubits, x_train)
q_x_test_circuits = prepare_pqk_circuits(qubits, x_test)
```

Next, compute the PQK features based on the 1-RDM of the dataset circuits above and store the results in `rdm`, a `tf.Tensor` with shape `[n_points, n_qubits, 3]`. The entries in `rdm[i][j][k]` = $\langle \psi_i | OP^k_j | \psi_i \rangle$ where `i` indexes over datapoints, `j` indexes over qubits and `k` indexes over $\lbrace \hat{X}, \hat{Y}, \hat{Z} \rbrace$ .

```
def get_pqk_features(qubits, data_batch):
  """Get PQK features based on above construction."""
  ops = [[cirq.X(q), cirq.Y(q), cirq.Z(q)] for q in qubits]
  ops_tensor = tf.expand_dims(tf.reshape(tfq.convert_to_tensor(ops), -1), 0)
  batch_dim = tf.gather(tf.shape(data_batch), 0)
  ops_splat = tf.tile(ops_tensor, [batch_dim, 1])
  exp_vals = tfq.layers.Expectation()(data_batch, operators=ops_splat)
  rdm = tf.reshape(exp_vals, [batch_dim, len(qubits), -1])
  return rdm
```

```
x_train_pqk = get_pqk_features(qubits, q_x_train_circuits)
x_test_pqk = get_pqk_features(qubits, q_x_test_circuits)
print('New PQK training dataset has shape:', x_train_pqk.shape)
print('New PQK testing dataset has shape:', x_test_pqk.shape)
```

### 2.2 Re-labeling based on PQK features
Now that you have these quantum generated features in `x_train_pqk` and `x_test_pqk`, it is time to re-label the dataset. To achieve maximum seperation between quantum and classical performance you can re-label the dataset based on the spectrum information found in `x_train_pqk` and `x_test_pqk`.

Note: This preparation of your dataset to explicitly maximize the seperation in performance between the classical and quantum models might feel like cheating, but it provides a **very** important proof of existance for datasets that are hard for classical computers and easy for quantum computers to model. There would be no point in searching for quantum advantage in QML if you couldn't first create something like this to demonstrate advantage.

```
def compute_kernel_matrix(vecs, gamma):
  """Computes d[i][j] = e^ -gamma * (vecs[i] - vecs[j]) ** 2 """
  scaled_gamma = gamma / (
      tf.cast(tf.gather(tf.shape(vecs), 1), tf.float32) * tf.math.reduce_std(vecs))
  return scaled_gamma * tf.einsum('ijk->ij',(vecs[:,None,:] - vecs) ** 2)

def get_spectrum(datapoints, gamma=1.0):
  """Compute the eigenvalues and eigenvectors of the kernel of datapoints."""
  KC_qs = compute_kernel_matrix(datapoints, gamma)
  S, V = tf.linalg.eigh(KC_qs)
  S = tf.math.abs(S)
  return S, V
```

```
S_pqk, V_pqk = get_spectrum(
    tf.reshape(tf.concat([x_train_pqk, x_test_pqk], 0), [-1, len(qubits) * 3]))

S_original, V_original = get_spectrum(
    tf.cast(tf.concat([x_train, x_test], 0), tf.float32), gamma=0.005)

print('Eigenvectors of pqk kernel matrix:', V_pqk)
print('Eigenvectors of original kernel matrix:', V_original)
```

Now you have everything you need to re-label the dataset! Now you can consult with the flowchart to better understand how to maximize performance seperation when re-labeling the dataset:

In order to maximize the seperation between quantum and classical models, you will attempt to maximize the geometric difference between the original dataset and the PQK features kernel matrices $g(K_1 || K_2) = \sqrt{ || \sqrt{K_2} K_1^{-1} \sqrt{K_2} || _\infty}$ using `S_pqk, V_pqk` and `S_original, V_original`. A large value of $g$ ensures that you initially move to the right in the flowchart down towards a prediction advantage in the quantum case.

Note: Computing quantities for $s$ and $d$ are also very useful when looking to better understand performance seperations. In this case ensuring a large $g$ value is enough to see performance seperation.

```
def get_stilted_dataset(S, V, S_2, V_2, lambdav=1.1):
  """Prepare new labels that maximize geometric distance between kernels."""
  S_diag = tf.linalg.diag(S ** 0.5)
  S_2_diag = tf.linalg.diag(S_2 / (S_2 + lambdav) ** 2)
  scaling = S_diag @ tf.transpose(V) @ \
            V_2 @ S_2_diag @ tf.transpose(V_2) @ \
            V @ S_diag

  # Generate new lables using the largest eigenvector.
  _, vecs = tf.linalg.eig(scaling)
  new_labels = tf.math.real(
      tf.einsum('ij,j->i', tf.cast(V @ S_diag, tf.complex64), vecs[-1])).numpy()
  # Create new labels and add some small amount of noise.
  final_y = new_labels > np.median(new_labels)
  noisy_y = (final_y ^ (np.random.uniform(size=final_y.shape) > 0.95))
  return noisy_y
```

```
y_relabel = get_stilted_dataset(S_pqk, V_pqk, S_original, V_original)
y_train_new, y_test_new = y_relabel[:N_TRAIN], y_relabel[N_TRAIN:]
```

## 3. Comparing models
Now that you have prepared your dataset it is time to compare model performance. You will create two small feedforward neural networks and compare performance when they are given access to the PQK features found in `x_train_pqk`.

### 3.1 Create PQK enhanced model
Using standard `tf.keras` library features you can now create and a train a model on the `x_train_pqk` and `y_train_new` datapoints:

```
def create_pqk_model():
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Dense(32, activation='sigmoid', input_shape=[len(qubits) * 3,]))
    model.add(tf.keras.layers.Dense(16, activation='sigmoid'))
    model.add(tf.keras.layers.Dense(1))
    return model

pqk_model = create_pqk_model()
pqk_model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(learning_rate=0.003),
              metrics=['accuracy'])

pqk_model.summary()
```

```
pqk_history = pqk_model.fit(tf.reshape(x_train_pqk, [N_TRAIN, -1]),
          y_train_new,
          batch_size=32,
          epochs=1000,
          verbose=0,
          validation_data=(tf.reshape(x_test_pqk, [N_TEST, -1]), y_test_new))
```

### 3.2 Create a classical model
Similar to the code above you can now also create a classical model that doesn't have access to the PQK features in your stilted dataset. This model can be trained using `x_train` and `y_label_new`.

```
def create_fair_classical_model():
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Dense(32, activation='sigmoid', input_shape=[DATASET_DIM,]))
    model.add(tf.keras.layers.Dense(16, activation='sigmoid'))
    model.add(tf.keras.layers.Dense(1))
    return model

model = create_fair_classical_model()
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(learning_rate=0.03),
              metrics=['accuracy'])

model.summary()
```

```
classical_history = model.fit(x_train,
          y_train_new,
          batch_size=32,
          epochs=1000,
          verbose=0,
          validation_data=(x_test, y_test_new))
```

### 3.3 Compare performance
Now that you have trained the two models you can quickly plot the performance gaps in the validation data between the two. Typically both models will achieve > 0.9 accuaracy on the training data. However on the validation data it becomes clear that only the information found in the PQK features is enough to make the model generalize well to unseen instances.

```
plt.figure(figsize=(10,5))
plt.plot(classical_history.history['accuracy'], label='accuracy_classical')
plt.plot(classical_history.history['val_accuracy'], label='val_accuracy_classical')
plt.plot(pqk_history.history['accuracy'], label='accuracy_quantum')
plt.plot(pqk_history.history['val_accuracy'], label='val_accuracy_quantum')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
```

Success: You have engineered a stilted quantum dataset that can intentionally defeat classical models in a fair (but contrived) setting. Try comparing results using other types of classical models. The next step is to try and see if you can find new and interesting datasets that can defeat classical models without needing to engineer them yourself!

## 4. Important conclusions

There are several important conclusions you can draw from this and the [MNIST](https://www.tensorflow.org/quantum/tutorials/mnist) experiments:

1. It's very unlikely that the quantum models of today will beat classical model performance on classical data. Especially on today's classical datasets that can have upwards of a million datapoints.

2. Just because the data might come from a hard to classically simulate quantum circuit, doesn't necessarily make the data hard to learn for a classical model.

3. Datasets (ultimately quantum in nature) that are easy for quantum models to learn and hard for classical models to learn do exist, regardless of model architecture or training algorithms used.