# 3. Imagined movement

In this tutorial we will look at imagined movement. Our movement is controlled in the motor cortex where there is an increased level of mu activity (8–12 Hz) when we perform movements. This is accompanied by a reduction of this mu activity in specific regions that deal with the limb that is currently moving. This decrease is called Event Related Desynchronization (ERD). By measuring the amount of mu activity at different locations on the motor cortex, we can determine which limb the subject is moving. Through mirror neurons, this effect also occurs when the subject is not actually moving his limbs, but merely imagining it.

## Credits
The CSP code was originally written by Boris Reuderink of the Donders
Institute for Brain, Cognition and Behavior. It is part of his Python EEG
toolbox: https://github.com/breuderink/eegtools

Inspiration for this tutorial also came from the excellent code example
given in the book chapter:

Arnaud Delorme, Christian Kothe, Andrey Vankov, Nima Bigdely-Shamlo,
Robert Oostenveld, Thorsten Zander, and Scott Makeig. MATLAB-Based Tools
for BCI Research, In _(B+H)CI: The Human in Brain-Computer Interfaces and
the Brain in Human-Computer Interaction._ Desney S. Tan and Anton Nijholt
(eds.), 2009, 241-259, http://dx.doi.org/10.1007/978-1-84996-272-8

## About the data
The dataset for this tutorial is provided by the fourth BCI competition.
Before you continue with this tutorial, please, go to http://www.bbci.de/competition/iv/#download
and fill in your name and email address.
You don't need to actually download the data, since it is installed in the virtual server already, but this way, the wonderful organizers of the competition get notified that their data is still being used and by whom.

[Description of the data](http://bbci.de/competition/iv/desc_1.html)

```python
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

Executing the code cell below will load the data:

```python
import numpy as np
import scipy.io

m = scipy.io.loadmat('data/BCICIV_calib_ds1d.mat', struct_as_record=True)

# SciPy.io.loadmat does not deal well with Matlab structures, resulting in lots of
# extra dimensions in the arrays. This makes the code a bit more cluttered

sample_rate = m['nfo']['fs'][0][0][0][0]
EEG = m['cnt'].T
nchannels, nsamples = EEG.shape

channel_names = [s[0] for s in m['nfo']['clab'][0][0][0]]
event_onsets = m['mrk'][0][0][0]
event_codes = m['mrk'][0][0][1]
labels = np.zeros((1, nsamples), int)
labels[0, event_onsets] = event_codes

cl_lab = [s[0] for s in m['nfo']['classes'][0][0][0]]
cl1 = cl_lab[0]
cl2 = cl_lab[1]
nclasses = len(cl_lab)
nevents = len(event_onsets)
```

Now we have the data in the following python variables:

```python
# Print some information
print('Shape of EEG:', EEG.shape)
print('Sample rate:', sample_rate)
print('Number of channels:', nchannels)
print('Channel names:', channel_names)
print('Number of events:', len(event_onsets))
print('Event codes:', np.unique(event_codes))
print('Class labels:', cl_lab)
print('Number of classes:', nclasses)
```

    Shape of EEG: (59, 190473)
    Sample rate: 100
    Number of channels: 59
    Channel names: ['AF3', 'AF4', 'F5', 'F3', 'F1', 'Fz', 'F2', 'F4', 'F6', 'FC5', 'FC3', 'FC1', 'FCz', 'FC2', 'FC4', 'FC6', 'CFC7', 'CFC5', 'CFC3', 'CFC1', 'CFC2', 'CFC4', 'CFC6', 'CFC8', 'T7', 'C5', 'C3', 'C1', 'Cz', 'C2', 'C4', 'C6', 'T8', 'CCP7', 'CCP5', 'CCP3', 'CCP1', 'CCP2', 'CCP4', 'CCP6', 'CCP8', 'CP5', 'CP3', 'CP1', 'CPz', 'CP2', 'CP4', 'CP6', 'P5', 'P3', 'P1', 'Pz', 'P2', 'P4', 'P6', 'PO1', 'PO2', 'O1', 'O2']
    Number of events: 1
    Event codes: [-1  1]
    Class labels: ['left', 'right']
    Number of classes: 2

This is a large recording: 59 electrodes where used, spread across the entire scalp. The subject was given a cue and then imagined either right hand movement or the movement of his feet. As can be seen from the [Homunculus](http://en.wikipedia.org/wiki/Cortical_homunculus), foot movement is controlled at the center of the motor cortex (which makes it hard to distinguish left from right foot), while hand movement is controlled more lateral.

## Plotting the data

The code below cuts trials for the two classes and should look familiar if you've completed the previous tutorials. Trials are cut in the interval [0.5–2.5 s] after the onset of the cue.

```python
# Dictionary to store the trials in, each class gets an entry
trials = {}

# The time window (in samples) to extract for each trial, here 0.5 -- 2.5 seconds
win = np.arange(int(0.5*sample_rate), int(2.5*sample_rate))

# Length of the time window
nsamples = len(win)

# Loop over the classes (right, foot)
for cl, code in zip(cl_lab, np.unique(event_codes)):

    # Extract the onsets for the class
    cl_onsets = event_onsets[event_codes == code]

    # Allocate memory for the trials
    trials[cl] = np.zeros((nchannels, nsamples, len(cl_onsets)))

    # Extract each trial
    for i, onset in enumerate(cl_onsets):
        trials[cl][:,:,i] = EEG[:, win+onset]

# Some information about the dimensionality of the data (channels x time x trials)
print('Shape of trials[cl1]:', trials[cl1].shape)
print('Shape of trials[cl2]:', trials[cl2].shape)
```

    Shape of trials[cl1]: (59, 200, 100)
    Shape of trials[cl2]: (59, 200, 100)

Since the feature we're looking for (a decrease in $\mu$-activity) is a frequency feature, lets plot the PSD of the trials in a similar manner as with the SSVEP data. The code below defines a function that computes the PSD for each trial (we're going to need it again later on):

```python
from matplotlib import mlab

def psd(trials):
    '''
    Calculates for each trial the Power Spectral Density (PSD).

    Parameters
    ----------
    trials : 3d-array (channels x samples x trials)
        The EEG signal

    Returns
    -------
    trial_PSD : 3d-array (channels x PSD x trials)
        the PSD for each trial.
    freqs : list of floats
        Yhe frequencies for which the PSD was computed (useful for plotting later)
    '''

    ntrials = trials.shape[2]
    trials_PSD = np.zeros((nchannels, 101, ntrials))

    # Iterate over trials and channels
    for trial in range(ntrials):
        for ch in range(nchannels):
            # Calculate the PSD
            (PSD, freqs) = mlab.psd(trials[ch,:,trial], NFFT=int(nsamples), Fs=sample_rate)
            trials_PSD[ch, :, trial] = PSD.ravel()

    return trials_PSD, freqs
```

```python
# Apply the function
psd_r, freqs = psd(trials[cl1])
psd_f, freqs = psd(trials[cl2])
trials_PSD = {cl1: psd_r, cl2: psd_f}
```

The function below plots the PSDs that are calculated with the above function. Since plotting it for 118 channels will clutter the display, it takes the indices of the desired channels as input, as well as some metadata to decorate the plot.

```python
import matplotlib.pyplot as plt

def plot_psd(trials_PSD, freqs, chan_ind, chan_lab=None, maxy=None):
    '''
    Plots PSD data calculated with psd().

    Parameters
    ----------
    trials : 3d-array
        The PSD data, as returned by psd()
    freqs : list of floats
        The frequencies for which the PSD is defined, as returned by psd()
    chan_ind : list of integers
        The indices of the channels to plot
    chan_lab : list of strings
        (optional) List of names for each channel
    maxy : float
        (optional) Limit the y-axis to this value
    '''
    plt.figure(figsize=(12,5))

    nchans = len(chan_ind)

    # Maximum of 3 plots per row
    nrows = int(np.ceil(nchans / 3))
    ncols = min(3, nchans)

    # Enumerate over the channels
    for i,ch in enumerate(chan_ind):
        # Figure out which subplot to draw to
        plt.subplot(nrows,ncols,i+1)

        # Plot the PSD for each class
        for cl in trials.keys():
            plt.plot(freqs, np.mean(trials_PSD[cl][ch,:,:], axis=1), label=cl)

        # All plot decoration below...

        plt.xlim(1,30)

        if maxy != None:
            plt.ylim(0,maxy)

        plt.grid()

        plt.xlabel('Frequency (Hz)')

        if chan_lab == None:
            plt.title('Channel %d' % (ch+1))
        else:
            plt.title(chan_lab[i])

        plt.legend()

    plt.tight_layout()
```

Lets put the `plot_psd()` function to use and plot three channels:

 1. C3: Central, left
 2. Cz: Central, central
 3. C4: Central, right

```python
plot_psd(
    trials_PSD,
    freqs,
    [channel_names.index(ch) for ch in ['C3', 'Cz', 'C4']],
    chan_lab=['left', 'center', 'right'],
    maxy=500
)
```

A spike of mu activity can be seen on each channel for both classes. At the right hemisphere, the mu for the left hand movement is lower than for the right hand movement due to the ERD. At the left electrode, the mu for the right hand movement is reduced and at the central electrode the mu activity is about equal for both classes. This is in line with the theory that the left hand is controlled by the right hemisphere and the feet are controlled centrally.

## Classifying the data

We will use a machine learning algorithm to construct a model that can distinguish between the right hand and foot movement of this subject. In order to do this we need to:

 1. find a way to quantify the amount of mu activity present in a trial
 2. make a model that describes expected values of mu activity for each class
 3. finally test this model on some unseen data to see if it can predict the correct class label

We will follow a classic BCI design by Blankertz et al. [1] where they use the logarithm of the variance of the signal in a certain frequency band as a feature for the classifier.

[1] Blankertz, B., Dornhege, G., Krauledat, M., Müller, K.-R., & Curio, G. (2007). The non-invasive Berlin Brain-Computer Interface: fast acquisition of effective performance in untrained subjects. *NeuroImage*, 37(2), 539–550. doi:10.1016/j.neuroimage.2007.01.051

The script below designs a band pass filter using [`scipy.signal.irrfilter`](http://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.iirfilter.html) that will strip away frequencies outside the 8--15Hz window. The filter is applied to all trials:

```python
import scipy.signal

def bandpass(trials, lo, hi, sample_rate):
    '''
    Designs and applies a bandpass filter to the signal.

    Parameters
    ----------
    trials : 3d-array (channels x samples x trials)
        The EEGsignal
    lo : float
        Lower frequency bound (in Hz)
    hi : float
        Upper frequency bound (in Hz)
    sample_rate : float
        Sample rate of the signal (in Hz)

    Returns
    -------
    trials_filt : 3d-array (channels x samples x trials)
        The bandpassed signal
    '''

    # The iirfilter() function takes the filter order: higher numbers mean a sharper frequency cutoff,
    # but the resulting signal might be shifted in time, lower numbers mean a soft frequency cutoff,
    # but the resulting signal less distorted in time. It also takes the lower and upper frequency bounds
    # to pass, divided by the niquist frequency, which is the sample rate divided by 2:
    a, b = scipy.signal.iirfilter(6, [lo/(sample_rate/2.0), hi/(sample_rate/2.0)])

    # Applying the filter to each trial
    ntrials = trials.shape[2]
    trials_filt = np.zeros((nchannels, nsamples, ntrials))
    for i in range(ntrials):
        trials_filt[:,:,i] = scipy.signal.filtfilt(a, b, trials[:,:,i], axis=1)

    return trials_filt
```

```python
# Apply the function
trials_filt = {cl1: bandpass(trials[cl1], 8, 15, sample_rate),
               cl2: bandpass(trials[cl2], 8, 15, sample_rate)}
```

Plotting the PSD of the resulting `trials_filt` shows the suppression of frequencies outside the passband of the filter:

```python
psd_r, freqs = psd(trials_filt[cl1])
psd_f, freqs = psd(trials_filt[cl2])
trials_PSD = {cl1: psd_r, cl2: psd_f}

plot_psd(
    trials_PSD,
    freqs,
    [channel_names.index(ch) for ch in ['C3', 'Cz', 'C4']],
    chan_lab=['left', 'center', 'right'],
    maxy=300
)
```

As a feature for the classifier, we will use the logarithm of the variance of each channel. The function below calculates this:

```python
# Calculate the log(var) of the trials
def logvar(trials):
    '''
    Calculate the log-var of each channel.

    Parameters
    ----------
    trials : 3d-array (channels x samples x trials)
        The EEG signal.

    Returns
    -------
    logvar - 2d-array (channels x trials)
        For each channel the logvar of the signal
    '''
    return np.log(np.var(trials, axis=1))
```

```python
# Apply the function
trials_logvar = {cl1: logvar(trials_filt[cl1]),
                 cl2: logvar(trials_filt[cl2])}
```

Below is a function to visualize the logvar of each channel as a bar chart:

```python
def plot_logvar(trials):
    '''
    Plots the log-var of each channel/component.
    arguments:
        trials - Dictionary containing the trials (log-vars x trials) for 2 classes.
    '''
    plt.figure(figsize=(12,5))

    x0 = np.arange(nchannels)
    x1 = np.arange(nchannels) + 0.4

    y0 = np.mean(trials[cl1], axis=1)
    y1 = np.mean(trials[cl2], axis=1)

    plt.bar(x0, y0, width=0.5, color='b')
    plt.bar(x1, y1, width=0.4, color='r')

    plt.xlim(-0.5, nchannels+0.5)

    plt.gca().yaxis.grid(True)
    plt.title('log-var of each channel/component')
    plt.xlabel('channels/components')
    plt.ylabel('log-var')
    plt.legend(cl_lab)
```

```python
# Plot the log-vars
plot_logvar(trials_logvar)
```

We see that most channels show a small difference in the log-var of the signal between the two classes. The next step is to go from 118 channels to only a few channel mixtures. The CSP algorithm calculates mixtures of channels that are designed to maximize the difference in variation between two classes. These mixures are called spatial filters.

```python
from numpy import linalg

def cov(trials):
    ''' Calculate the covariance for each trial and return their average '''
    ntrials = trials.shape[2]
    covs = [ trials[:,:,i].dot(trials[:,:,i].T) / nsamples for i in range(ntrials) ]
    return np.mean(covs, axis=0)

def whitening(sigma):
    ''' Calculate a whitening matrix for covariance matrix sigma. '''
    U, l, _ = linalg.svd(sigma)
    return U.dot( np.diag(l ** -0.5) )

def csp(trials_r, trials_f):
    '''
    Calculate the CSP transformation matrix W.
    arguments:
        trials_r - Array (channels x samples x trials) containing right hand movement trials
        trials_f - Array (channels x samples x trials) containing foot movement trials
    returns:
        Mixing matrix W
    '''
    cov_r = cov(trials_r)
    cov_f = cov(trials_f)
    P = whitening(cov_r + cov_f)
    B, _, _ = linalg.svd( P.T.dot(cov_f).dot(P) )
    W = P.dot(B)
    return W

def apply_mix(W, trials):
    ''' Apply a mixing matrix to each trial (basically multiply W with the EEG signal matrix)'''
    ntrials = trials.shape[2]
    trials_csp = np.zeros((nchannels, nsamples, ntrials))
    for i in range(ntrials):
        trials_csp[:,:,i] = W.T.dot(trials[:,:,i])
    return trials_csp
```

```python
# Apply the functions
W = csp(trials_filt[cl1], trials_filt[cl2])
trials_csp = {cl1: apply_mix(W, trials_filt[cl1]),
              cl2: apply_mix(W, trials_filt[cl2])}
```

To see the result of the CSP algorithm, we plot the log-var like we did before:

```python
trials_logvar = {cl1: logvar(trials_csp[cl1]),
                 cl2: logvar(trials_csp[cl2])}
plot_logvar(trials_logvar)
```

Instead of 118 channels, we now have 118 mixtures of channels, called components. They are the result of 118 spatial filters applied to the data.

The first filters maximize the variation of the first class, while minimizing the variation of the second. The last filters maximize the variation of the second class, while minimizing the variation of the first.

This is also visible in a PSD plot. The code below plots the PSD for the first and last components as well as one in the middle:

```python
psd_r, freqs = psd(trials_csp[cl1])
psd_f, freqs = psd(trials_csp[cl2])
trials_PSD = {cl1: psd_r, cl2: psd_f}

plot_psd(trials_PSD, freqs, [0,28,-1], chan_lab=['first component', 'middle component', 'last component'], maxy=0.75 )
```

In order to see how well we can differentiate between the two classes, a scatter plot is a useful tool. Here both classes are plotted on a 2-dimensional plane: the x-axis is the first CSP component, the y-axis is the last.

```python
def plot_scatter(left, foot):
    plt.figure()
    plt.scatter(left[0,:], left[-1,:], color='b')
    plt.scatter(foot[0,:], foot[-1,:], color='r')
    plt.xlabel('Last component')
    plt.ylabel('First component')
    plt.legend(cl_lab)
```

```python
plot_scatter(trials_logvar[cl1], trials_logvar[cl2])
```

We will apply a linear classifier to this data. A linear classifier can be thought of as drawing a line in the above plot to separate the two classes. To determine the class for a new trial, we just check on which side of the line the trial would be if plotted as above.

The data is split into a train and a test set. The classifier will fit a model (in this case, a straight line) on the training set and use this model to make predictions about the test set (see on which side of the line each trial in the test set falls). Note that the CSP algorithm is part of the model, so for fairness sake it should be calculated using only the training data.

```python
# Percentage of trials to use for training (50-50 split here)
train_percentage = 0.5

# Calculate the number of trials for each class the above percentage boils down to
ntrain_r = int(trials_filt[cl1].shape[2] * train_percentage)
ntrain_f = int(trials_filt[cl2].shape[2] * train_percentage)
ntest_r = trials_filt[cl1].shape[2] - ntrain_r
ntest_f = trials_filt[cl2].shape[2] - ntrain_f

# Splitting the frequency filtered signal into a train and test set
train = {cl1: trials_filt[cl1][:,:,:ntrain_r],
         cl2: trials_filt[cl2][:,:,:ntrain_f]}

test = {cl1: trials_filt[cl1][:,:,ntrain_r:],
        cl2: trials_filt[cl2][:,:,ntrain_f:]}

# Train the CSP on the training set only
W = csp(train[cl1], train[cl2])

# Apply the CSP on both the training and test set
train[cl1] = apply_mix(W, train[cl1])
train[cl2] = apply_mix(W, train[cl2])
test[cl1] = apply_mix(W, test[cl1])
test[cl2] = apply_mix(W, test[cl2])

# Select only the first and last components for classification
comp = np.array([0,-1])
train[cl1] = train[cl1][comp,:,:]
train[cl2] = train[cl2][comp,:,:]
test[cl1] = test[cl1][comp,:,:]
test[cl2] = test[cl2][comp,:,:]

# Calculate the log-var
train[cl1] = logvar(train[cl1])
train[cl2] = logvar(train[cl2])
test[cl1] = logvar(test[cl1])
test[cl2] = logvar(test[cl2])
```

For a classifier the Linear Discriminant Analysis (LDA) algorithm will be used. It fits a gaussian distribution to each class, characterized by the mean and covariance, and determines an optimal separating plane to divide the two. This plane is defined as $r = W_0 \cdot X_0 + W_1 \cdot X_1 + \ldots + W_n \cdot X_n - b$, where $r$ is the classifier output, $W$ are called the feature weights, $X$ are the features of the trial, $n$ is the dimensionality of the data and $b$ is called the offset.

In our case we have 2 dimensional data, so the separating plane will be a line: $r = W_0 \cdot X_0 + W_1 \cdot X_1 - b$. To determine a class label for an unseen trial, we can calculate whether the result is positive or negative.

```python
def train_lda(class1, class2):
    '''
    Trains the LDA algorithm.
    arguments:
        class1 - An array (observations x features) for class 1
        class2 - An array (observations x features) for class 2
    returns:
        The projection matrix W
        The offset b
    '''
    nclasses = 2

    nclass1 = class1.shape[0]
    nclass2 = class2.shape[0]

    # Class priors: in this case, we have an equal number of training
    # examples for each class, so both priors are 0.5
    prior1 = nclass1 / float(nclass1 + nclass2)
    prior2 = nclass2 / float(nclass1 + nclass1)

    mean1 = np.mean(class1, axis=0)
    mean2 = np.mean(class2, axis=0)

    class1_centered = class1 - mean1
    class2_centered = class2 - mean2

    # Calculate the covariance between the features
    cov1 = class1_centered.T.dot(class1_centered) / (nclass1 - nclasses)
    cov2 = class2_centered.T.dot(class2_centered) / (nclass2 - nclasses)

    W = (mean2 - mean1).dot(np.linalg.pinv(prior1*cov1 + prior2*cov2))
    b = (prior1*mean1 + prior2*mean2).dot(W)

    return (W,b)

def apply_lda(test, W, b):
    '''
    Applies a previously trained LDA to new data.
    arguments:
        test - An array (features x trials) containing the data
        W    - The project matrix W as calculated by train_lda()
        b    - The offsets b as calculated by train_lda()
    returns:
        A list containing a classlabel for each trial
    '''
    ntrials = test.shape[1]

    prediction = []
    for i in range(ntrials):
        # The line below is a generalization for:
        # result = W[0] * test[0,i] + W[1] * test[1,i] - b
        result = W.dot(test[:,i]) - b
        if result <= 0:
            prediction.append(1)
        else:
            prediction.append(2)

    return np.array(prediction)
```

Training the LDA using the training data gives us $W$ and $b$:

```python
W,b = train_lda(train[cl1].T, train[cl2].T)

print('W:', W)
print('b:', b)
```

    W: [ 5.31347949 -5.52963938]
    b: 0.3802472099930121

It can be informative to recreate the scatter plot and overlay the decision boundary as determined by the LDA classifier. The decision boundary is the line for which the classifier output is exactly 0. The scatterplot used $X_0$ as $x$-axis and $X_1$ as $y$-axis. To find the function $y = f(x)$ describing the decision boundary, we set $r$ to 0 and solve for $y$ in the equation of the separating plane:

<div style="width:600px">
$$\begin{align}
W_0 \cdot X_0 + W_1 \cdot X_1 - b &= r &&\text{the original equation} \\\
W_0 \cdot x + W_1 \cdot y - b &= 0     &&\text{filling in $X_0=x$, $X_1=y$ and $r=0$} \\\
W_0 \cdot x + W_1 \cdot y &= b         &&\text{solving for $y$}\\\
W_1 \cdot y &= b - W_0 \cdot x \\\
\\\
y &= \frac{b - W_0 \cdot x}{W_1}
\end{align}$$
</div>

We first plot the decision boundary with the training data used to calculate it:

```python
# Scatterplot like before
plot_scatter(train[cl1], train[cl2])
title('Training data')

# Calculate decision boundary (x,y)
x = np.arange(-5, 1, 0.1)
y = (b - W[0]*x) / W[1]

# Plot the decision boundary
plt.plot(x,y, linestyle='--', linewidth=2, color='k')
plt.xlim(-5, 1)
plt.ylim(-2.2, 1)
```

The code below plots the boundary with the test data on which we will apply the classifier. You will see the classifier is going to make some mistakes.

```python
plot_scatter(test[cl1], test[cl2])
title('Test data')
plt.plot(x,y, linestyle='--', linewidth=2, color='k')
plt.xlim(-5, 1)
plt.ylim(-2.2, 1)
```

Now the LDA is constructed and fitted to the training data. We can now apply it to the test data. The results are presented as a confusion matrix:

<table>
    <tr><td></td><td colspan='2' style="font-weight:bold">True labels →</td></tr>
    <tr><td style="font-weight:bold">↓ Predicted labels</td><td>Right</td><td>Foot</td></tr>
    <tr><td>Right</td><td></td><td></td></tr>
    <tr><td>Foot</td><td></td><td></td></tr>
</table>

The number at the diagonal will be trials that were correctly classified, any trials incorrectly classified (either a false positive or false negative) will be in the corners.

```python
# Print confusion matrix
conf = np.array([
    [(apply_lda(test[cl1], W, b) == 1).sum(), (apply_lda(test[cl2], W, b) == 1).sum()],
    [(apply_lda(test[cl1], W, b) == 2).sum(), (apply_lda(test[cl2], W, b) == 2).sum()],
])

print('Confusion matrix:')
print(conf)
print()
print('Accuracy: %.3f' % (np.sum(np.diag(conf)) / float(np.sum(conf))))
```

    Confusion matrix:
    [[45  4]
     [ 5 46]]

    Accuracy: 0.910

The confusion matrix shows that 4 out of the 50 trials with foot movement were incorrectly classified as right hand movement and 5 out of the 50 trials with right hand movement were incorrectly classified as foot movement. In total, 91% of the trials were correctly classified, not a bad score!

If you want, you can continue with the next tutorial:
[4. Classifiying the P300](4.%20Classifying%20the%20P300.ipynb)


# Generative models - auto-encoders

### Author: Philippe Esling (esling@ircam.fr)

In this course we will cover
1. A brief introduction to [generative models](#generative)
2. A formal presentation of [Auto-Encoders](#ae) (AEs)
3. An explanation of how to [implement AEs](#implement)
4. An [application](#application) of AEs for modeling images
4. An practical exemple of [convolutional denoising AEs](#denoising) for image data **(exercise)**

## Generative models

### Supervised refresher

Until now, we have mostly discussed models that are developed for _supervised_ and _discriminative_ tasks. To formalize this problem, we have a set of data $\{\mathbf{x}_{i}, \mathbf{y}_{i}\}_{i\in[1,n]}$, where the $\mathbf{x}_{i}$ and $\mathbf{y}_{i}$ are linked. Therefore, we want to approximate this relation through
\begin{equation}
    \mathbf{\hat{y}} = \mathcal{F}_\mathbf{\theta}(\mathbf{x})
\end{equation}
where we train the parameters $\mathbf{\theta}$ so that $\mathbf{\hat{y}}\approx\mathbf{y}$. The existence of a label $\mathbf{y}$ ("correct answer") defines a _supervised_ problem

### Going unsupervised

In some cases, we might only have a set of data $\{\mathbf{x}_{i}\}_{i\in[1,n]}$, and still be interested in learning some underlying properties or structure of this set. In that case, the problem is _unsupervised_ as we have to learn without a given answer.

Here, we can turn to _generative_ models [[1](#reference1)], which allows to create new data instances based on the observation of existing examples. Although these models are more naturally defined in a _probabilistic way_, we will assume that we have some simple _code_ $\mathbf{z}$, which allows to control the properties of the generation, and need to learn
\begin{equation}
    \mathbf{\hat{x}} = \mathcal{F}_\mathbf{\theta}(\mathbf{z})
\end{equation}
where we still need to learn $\mathbf{\theta}$, so that $\mathbf{\hat{x}}$ have similar properties to that of the examples in $\{\mathbf{x}_{i}\}_{i\in[1,n]}$.

Now the problem to solve is how we can learn directly from a set of data.

## Auto-encoders

One way to understand a set of data is to try to _compress_, or _simplify_ the corresponding dataset. So the idea is to learn simultaneously how to _encode_ our unlabeled input $\{\mathbf{x}_{i}\}_{i\in[1,n]}$ and to _decode_ the corresponding representation. This idea give rise to the notion of **auto-encoder**.

### Architecture

The auto-encoder is an unsupervised architecture originally proposed to perform _dimensionality reduction_ [[3](#reference3)]. As its name indicates, we will try to train this model to learn an efficient _encoding_ $\mathbf{z}$ of unlabeled input data $\mathbf{x}$. The only way to learn efficient parameters is to also learn a _decoding_ function to _reconstruct_ $\mathbf{x}$ from $\mathbf{z}$.

As shown here, a first model $\mathcal{E}_\phi$ \textit{encodes} the input into a \textit{latent code} $\mathbf{z}$ in order to provide a low-dimensional representation of the data. A second model $\mathcal{D}_\theta$ designated as the \textit{decoder} aims to generate outputs from $\mathbf{z}$ that are as close to the original inputs as possible.

### Formal definition

The latent code $\mathbf{z}$ can be seen as a compressed abstract representation, and may be used as an intermediate space for analysis or generation. This helps to govern the distribution of the data through a simpler and higher-level representation, while enhancing the \textit{expressiveness} of the generative model.
The behaviour of an auto-encoder can be formalized as:
\begin{align}
    \mathbf{z} &= \mathcal{E}_\phi(\mathbf{x}) \\
    \mathbf{\hat{x}} &= \mathcal{D}_\theta(\mathbf{z})
\end{align}
with the \textit{encoder} $\mathcal{E}_\phi$ and \textit{decoder} $\mathcal{D}_\theta$ functions parameterized respectively by $\phi$ and $\theta$. As we can see this defines the reconstruction relationship
\begin{equation}
    \mathbf{\hat{x}} = \mathcal{D}_\theta(\mathcal{E}_\phi(\mathbf{x}))
\end{equation}

### Training

The training of an auto-encoder consists in finding the optimal functions of encoding $\mathcal{E}^*$ and decoding $\mathcal{D}^*$ by evaluating the \textit{reconstruction error} $\mathcal{L}$ between $\mathbf{x}$ and $\mathbf{\hat{x}}$, such that
\begin{equation}
    \mathcal{E}^*, \mathcal{D}^* = arg\,min_{ \phi, \theta}{\mathcal{L}}(\mathbf{x}, \mathcal{D}_\theta(\mathcal{E}_\phi(\mathbf{x})))
\end{equation}

As the latent space usually has a smaller dimensionality than the input, it acts as an incentive for the network to find the main attributes of variations in the dataset (and also explains its use for _dimensionality reduction_).

### Variants and discussion

There are several variants of auto-encoders, such as denoising auto-encoders or variational auto-encoders. Each address some downside of the basic AE model. For instance, the deterministic nature of the basic auto-encoder implies a point-wise mapping of the latent space, meaning that not all the latent positions can be leveraged to produce relevant reconstructions. Because of this reason, there is no way to ensure that the latent space could allow a robust generalization and that any random $\mathbf{z}$ would generate a meaningful output.

## Implementation

Here, we discuss how we can implement and train a simple auto-encoder network in _TensorFlow_. As discussed earlier, an AE is composed of two parts, an **encoder** and a **decoder**. The goal of the encoder is to "compress" the dataset, representing its principal features with a very small code, while the goal of the decoder is to learn how to reproduce the initial input from this code. Hence, we will first need to use some basic imports and definition to setup our problem

### Import TensorFlow and other libraries

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf

from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
from tensorflow.keras import layers, losses
from tensorflow.keras.datasets import fashion_mnist
from tensorflow.keras.models import Model
```

### Load the dataset

To start with a pragmatic and simple to understand example, we  will try to train the basic AE using the Fashon MNIST dataset. This dataset contains images of size 28x28 pixels, with different pieces of clothing. The following code allows to load (and eventually download) the dataset, by using the `tensorflow.keras.datasets` module. We also plot some randomly selected test examples

```python
# Load (and eventually download) the dataset
(x_train, _), (x_test, _) = fashion_mnist.load_data()
# Normalize the dataset in the [0, 1] range]
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
print('Number of training examples')
print (x_train.shape)
print('Number of testing examples')
print (x_test.shape)
plt.figure(figsize=(20, 4))
# Plot some randomly selected input
for i in range(10):
    ax = plt.subplot(1, 10, i + 1)
    i_val = np.random.randint(x_test.shape[0])
    plt.imshow(x_test[i_val])
    plt.gray()
    plt.title("Example %d"%(i_val))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()
```

## Basic auto-encoder

We recall here that in order to define an autoencoder, we will need an `encoder`, which compresses the images into a small latent vector, and a `decoder`, that reconstructs the original image from this code. Here, we will start very basic and define the encoder and decoder as simple `Dense` layers. To define the model simply, we will use the [Keras API](https://www.tensorflow.org/guide/keras/custom_layers_and_models) defined in the `tensorflow.keras` module (Note that we pre-loaded the `layers` submodule

```python
class AE(Model):
    def __init__(self, encoding_dim):
        super(AE, self).__init__()
        self.latent_dim = encoding_dim
        self.encoder = tf.keras.Sequential([
          layers.Flatten(),
          layers.Dense(self.latent_dim, activation='relu'),
        ])
        self.decoder = tf.keras.Sequential([
          layers.Dense(784, activation='sigmoid'),
          layers.Reshape((28, 28))
        ])

    def call(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded
```

Here we can see that the model depends on a given `encoding_dim` variable, which defines the size of the latent code. Therefore, we can instantiate our model arbitrarliy with `64` dimensions

```python
latent_dim = 64
autoencoder = AE(latent_dim)
```

The only remaining part that we did not discuss yet is what type of _loss_ (defined as $\mathcal{L}$) we can use to train our model. First, we will simply rely on the _Mean Squared Error_ (MSE) loss, which is defined as
\begin{equation}
    \mathcal{L}_{MSE}(\hat{\mathbf{x}}, \mathbf{x}) = \mid \hat{\mathbf{x}}, \mathbf{x} \mid^{2}
\end{equation}

```python
autoencoder.compile(optimizer='adam', loss=losses.MeanSquaredError())
```

### Training the model

Train the model using `x_train` as both the input and the target. The `encoder` will learn to compress the dataset from 784 dimensions to the latent space, and the `decoder` will learn to reconstruct the original images.
.

```python
autoencoder.fit(x_train, x_train, epochs=10, shuffle=True, validation_data=(x_test, x_test))
```

    Epoch 1/10
    1875/1875 [==============================] - 2s 992us/step - loss: 0.0086 - val_loss: 0.0088
    Epoch 2/10
    1875/1875 [==============================] - 2s 966us/step - loss: 0.0086 - val_loss: 0.0088
    Epoch 3/10
    1875/1875 [==============================] - 2s 983us/step - loss: 0.0086 - val_loss: 0.0087
    Epoch 4/10
    1875/1875 [==============================] - 2s 993us/step - loss: 0.0086 - val_loss: 0.0087
    Epoch 5/10
    1875/1875 [==============================] - 2s 978us/step - loss: 0.0086 - val_loss: 0.0088
    Epoch 6/10
    1875/1875 [==============================] - 2s 1ms/step - loss: 0.0086 - val_loss: 0.0088
    Epoch 7/10
    1875/1875 [==============================] - 2s 1ms/step - loss: 0.0086 - val_loss: 0.0087
    Epoch 8/10
    1875/1875 [==============================] - 2s 1ms/step - loss: 0.0086 - val_loss: 0.0087
    Epoch 9/10
    1875/1875 [==============================] - 2s 1ms/step - loss: 0.0086 - val_loss: 0.0087
    Epoch 10/10
    1875/1875 [==============================] - 2s 1ms/step - loss: 0.0085 - val_loss: 0.0087

    <tensorflow.python.keras.callbacks.History at 0x15a4bf750>

Now that the model is trained, we can test it by encoding and decoding images from the test set.

```python
encoded_imgs = autoencoder.encoder(x_test).numpy()
decoded_imgs = autoencoder.decoder(encoded_imgs).numpy()
```

By plotting the images, we can see that the model is able to perform an adequate (yet somewhat blurry) reconstruction of the input images. The interesting point is that this reconstruction comes from a code of only `64` dimensions, whereas the original images have `784` dimensions.

```python
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
  # display original
  ax = plt.subplot(2, n, i + 1)
  plt.imshow(x_test[i])
  plt.title("original")
  plt.gray()
  ax.get_xaxis().set_visible(False)
  ax.get_yaxis().set_visible(False)

  # display reconstruction
  ax = plt.subplot(2, n, i + 1 + n)
  plt.imshow(decoded_imgs[i])
  plt.title("reconstructed")
  plt.gray()
  ax.get_xaxis().set_visible(False)
  ax.get_yaxis().set_visible(False)
plt.show()
```

### Improvements

Even though this very basic example seems to work, several improvements can be made over the original model. First, we can see that the overall framework does not depend on the exact nature of the `encoder` and `decoder`. Therefore, we can rewrite the original class to accept any type of architecture for these (see code below). Another aspect that has a major impact on the results is the type of `loss` used. The `MeanSquaredError` tend to favor blurry reconstructions, and could be replaced by a `Multinoulli` loss. You can experiment with these ideas below.

```python
class AE(Model):
  def __init__(self, encoder, decoder, encoding_dim):
    super(AE, self).__init__()
    self.latent_dim = encoding_dim
    self.encoder = encoder
    self.decoder = decoder

  def call(self, x):
    encoded = self.encoder(x)
    decoded = self.decoder(encoded)
    return decoded
```

## Exercise: Denoising AE

Imagine (for the sake of argument), that we choose an encoding dimension which is of same dimensionality as the input one. Then, one huge problem is that nothing prevents the AE from simply learning the _identity_ function (try to imagine why). An autoencoder can also be trained to remove noise from images. This type of _regularization_ prevents the model from learning this degenerate situation.

In this exercise, you will need to create your own denoising AE, by relying on a noisy version of the Fashion MNIST dataset (adding random Gaussian noise to each image). You will then train an autoencoder using the noisy image as input, and the original image as the target.

Let's reimport the dataset to omit the modifications made earlier.

```python
(x_train, _), (x_test, _) = fashion_mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
print(x_train.shape)
```

    (60000, 28, 28)

Here, we create two new train and test sets by adding random noise to the images

```python
noise_factor = 0.2
x_train_noisy = x_train + noise_factor * tf.random.normal(shape=x_train.shape)
x_test_noisy = x_test + noise_factor * tf.random.normal(shape=x_test.shape)
x_train_noisy = tf.clip_by_value(x_train_noisy, clip_value_min=0., clip_value_max=1.)
x_test_noisy = tf.clip_by_value(x_test_noisy, clip_value_min=0., clip_value_max=1.)
```

Plot the noisy images.

```python
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
    ax = plt.subplot(2, n, i + 1)
    plt.title("original")
    plt.imshow(tf.squeeze(x_test[i]))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    plt.gray()
    ax = plt.subplot(2, n, i + 1 + n)
    plt.title("original + noise")
    plt.imshow(tf.squeeze(x_test_noisy[i]))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    plt.gray()
plt.show()
```

### Define a convolutional autoencoder

In this example, you will train a convolutional autoencoder using  [Conv2D](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D) layers in the `encoder`, and [Conv2DTranspose](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2DTranspose) layers in the `decoder`.

```python
class DenoisingAE(AE):
  def __init__(self):
    super(DenoisingAE, self).__init__()
    self.encoder = ...
    self.decoder = ...

  def call(self, x):
    encoded = ...
    decoded = ...
    return decoded

autoencoder = ...
```

Compile and fit your model with appropriate losses

```python
autoencoder.compile(...)
autoencoder.fit(...)
```

Let's take a look at a summary of the encoder and decoder. Notice how the images are downsampled and then upsampled back to the original input size.

```python
autoencoder.encoder.summary()
autoencoder.decoder.summary()
```

Plot both the noisy images and the denoised images produced by the autoencoder to check that your implementation is correct

```python
encoded_imgs = ...
decoded_imgs = ...
plt.figure(figsize=(20, 4))
for i in range(n):
    ...
plt.show()
```

### References

[1] Rezende, Danilo Jimenez, and Shakir Mohamed. "Variational inference with normalizing flows." _arXiv preprint arXiv:1505.05770_ (2015). [link](http://arxiv.org/pdf/1505.05770)

[2] Kingma, Diederik P., Tim Salimans, and Max Welling. "Improving Variational Inference with Inverse Autoregressive Flow." _arXiv preprint arXiv:1606.04934_ (2016). [link](https://arxiv.org/abs/1606.04934)

[3] Kingma, D. P., & Welling, M. Auto-encoding variational bayes. arXiv preprint arXiv:1312.6114. (2013). [link](https://arxiv.org/pdf/1312.6114)

[4] Rezende, D. J., Mohamed, S., & Wierstra, D. Stochastic backpropagation and approximate inference in deep generative models. arXiv preprint arXiv:1401.4082. (2014). [link](https://arxiv.org/pdf/1401.4082)

[5] Gulrajani, I., Kumar, K., Ahmed, F., Taiga, A. A., Visin, F., Vazquez, D., & Courville, A. (2016). Pixelvae: A latent variable model for natural images. arXiv preprint arXiv:1611.05013. [link](https://arxiv.org/pdf/1611.05013)

[6] Van den Oord, A., & Vinyals, O. (2017). Neural discrete representation learning. In NIPS 2017 (pp. 6306-6315). [link](http://papers.nips.cc/paper/7210-neural-discrete-representation-learning.pdf)


### Imports

```python
# Imports

import sys
'''
!{sys.executable} -m pip install --user -q cirq
!{sys.executable} -m pip install --user -q google-api-python-client==1.8.0
'''
import cirq
import math
import random
import numpy as np
import sympy
!{sys.executable} --version
print("\nCirq version " + cirq.__version__)
```

    Python 3.7.7

    Cirq version 0.7.0

# Quantum Computing Simulation using Google's cirq library for Python
### Dmitri Friedenberg

Quantum programming languages are a major developing topic in the field of quantum computing. As the availability of different quantum computing systems increases, the need rises for a clear syntax with which to program a series of operations on an arbitrary quantum computer.

# Review of literature and high-level quantum programming proposals

Discussion about quantum pseudoprogramming began immediately after Shor's algorithm and Grover's algorithm each took the spotlight in 1994 and 1996 respectively. In "Conventions for quantum pseudocode" \[1\] in 1996, E. Knill's often cited paper began the initiative to develop a common method for programming quantum computers. Although a physical implementation was nowhere in sight, the need for adopting expressions for constructing a quantum system was immediately clear. A number of quantum programming language proposals followed in the decade after Knill's paper, well documented by Peter Selinger's "A brief survey of quantum programming languages" \[2\].

### Imperative languages

Within a few years of Knill's paper, several proposals for *imperative* quantum programming languages emerged. The first proposed imperative language was **Quantum Computation Language (QCL)**, introduced in 1998 in B. Ömer's Master's thesis, "A Procedural Formalism for Quantum Computing" \[3\]. His thesis presented a fully developed imperative language, with examples such as the QCL implementation of Coppersmith's algorithm of a fast quantum discrete Fourier Transform (Table 2.1):

```java
    operator dft(qureg q) { // main operator
        const n=#q; // set n to length of input
        int i; int j; // declare loop counters
        for i=0 to n-1 {
            for j=0 to i-1 { // apply conditional phase gates
                CPhase(2*pi/2^(i-j+1),q[n-i-1] & q[n-j-1]);
            }
            Mix(q[n-i-1]); // qubit rotation
        }
    flip(q); // swap bit order of the output
    }
```

Generally, initial proposals for quantum programming languages were imperative, likely due to the intuition that the languages are designed to manipulate physical objects (qubits and gates), more easily expressed in an object-oriented language. The imperative languages that followed included **Q Language** (an extension of C++, from "Toward an architecture for quantum programming", S. Bettelli 2002), which tracked quantum memory using a class "Qreg" to create registers; and **quantum Guarded-Command Language (qGCL)**, modeled after Edsger Dijkstra's "Guarded Command Language", defined by P. Zuliani in his thesis, "Quantum Programming" (2001).

### Functional languages

As functional programming has risen back into the zeitgeist of the last decade or two among the software engineering community, quantum computation has seen its share of attempts at functional languages. Selinger defined the **Quantum Programming Language (QPL)** in another paper from 2004, "Towards a quantum programming language" \[4\]. His paper describes QPL as, "*functional*, in the sense that each (atomic or composite) statement operates by transforming a specific set of inputs to outputs." The language implements natural solutions for issues like the no-cloning property, where the syntax of the language prevents an implied duplication of a quantum state. Selinger's paper also defines **Quantum flow charts (QFC)**, an excellent tool for illustrating a quantum functional program:

### Quantum Computing Simulators

Going further high-level, we come to quantum circuit simulators. Quantum computing simulators not only allow for the simple construction of circuits in a programming language, but may run an input vector through a circuit to simulate the probabalistic results moment by moment. For instance, take the Quantum Forier Transform (QFT) - a simulator can simply compute the Fourier transform classically, and update the qubit states (note that it *could* also properly simulate the full circuit for QFT). This allows for advanced simulations to include features such as noise, error rates, run-times for individual operations, and so on.

# Cirq

Finally, we come to the simulator of focus in this notebook. Cirq was announced as an open source Python framework for Noisy Intermediate Scale Quantum (NISQ) computers on Google's AI Blog, on July 18, 2018 \[5\], fully available at https://github.com/quantumlib/Cirq. Cirq is actively used on Google's "Bristlecone" quantum processor, and has even recently been incorporated into TensorFlow Quantum (also by Google), "an open-source library for the rapid prototyping of quantum ML models" \[6\]. TensorFlow Quantum is a very recent development just announced on March 9, 2020.

### Notable features

#### Graphical ASCII printing of circuits

```python
# This example is modified from https://github.com/quantumlib/Cirq .

# Pick a qubit.
qubit = cirq.GridQubit(0, 0)

# Create a circuit
circuit = cirq.Circuit(
    cirq.X(qubit)**0.5,  # Square root of NOT.
    cirq.measure(qubit, key='m')  # Measurement.
)
print("Circuit:")
print(circuit)

# Simulate the circuit several times.
simulator = cirq.Simulator()
result = simulator.run(circuit, repetitions=20)
print("Ran circuit 20 times. Results:")
print(result)
```

    Circuit:
    (0, 0): ───X^0.5───M('m')───
    Ran circuit 20 times. Results:
    m=00101001001100001011

#### Moment-based circuits

(credit [cirq.readthedocs.io]())

By grouping operations into "moments", Cirq constructs operators in parallel and better simulation of scheduled operations on real hardware. This also permits inspection of the state of a circuit at each moment:

```python
qubits = cirq.LineQubit.range(3)
circuit = cirq.Circuit(
    cirq.IdentityGate(num_qubits = 3).on(*qubits),
    cirq.H(qubits[0]),
    cirq.H(qubits[2]),
    cirq.CNOT(qubits[0], qubits[1]),
    cirq.CNOT(qubits[1], qubits[2])
)
print(circuit, "\n")

simulator = cirq.Simulator()
for i, step in enumerate(simulator.simulate_moment_steps(circuit)):
    print('state at step %d: %s' % (i, step.dirac_notation(3)) + "\n")
```

    0: ───I───H───@───────
          │       │
    1: ───I───────X───@───
          │           │
    2: ───I───H───────X───

    state at step 0: |000⟩

    state at step 1: 0.5|000⟩ + 0.5|001⟩ + 0.5|100⟩ + 0.5|101⟩

    state at step 2: 0.5|000⟩ + 0.5|001⟩ + 0.5|110⟩ + 0.5|111⟩

    state at step 3: 0.5|000⟩ + 0.5|001⟩ + 0.5|110⟩ + 0.5|111⟩

### Easily extensible operations and classes

Cirq is designed to be extensible with abstract classes for gates/operations, simulators, etc. This allows for creation of operations such as unitary operations corresponding to a complex function. The catch is that Cirq allows such operations not to be reflective of the full circuit - that is, the operation does not have to be the sum of normally-defined gates, but can simply hide a classical operation. In the next section, we will see how Cirq can use this to abstract the hardest part of Shor's algorithm.

# Shor's algorithm with Cirq

### Review

A brief review of the procedure for Shor's algorithm \[7\] is as follows:

___

We begin by picking a number $N$ to be factored.

##### Classical component

1. Pick a random number $a < N$.
2. Compute $\gcd(a, N)$. If $\gcd(a, N) \neq 1$, then this common divisor is a nontrivial factor of $N$, so the algorithm is done (though for large numbers it is likely that the algorithm can be run again on those factors). Otherwise, we proceed.
3. Use the **quantum component** to determine $r$, the period of the function $f(x) = a^x \mod N$.
4. If any of the following conditions are true, return to step 1.
    - $r = 0$
    - No candidate $r$ was found
    - $r$ is odd
    - $a^{\frac{r}{2}} \equiv -1 \mod N$
    - $\gcd(a^{\frac{r}{2}}+1\text{ mod }N, N) = 1$
5. We can factor $N$ by $\gcd(a^{\frac{r}{2}}+1\text{ mod }N, N)$.

##### Quantum component

We receive $a, N$ with which we must solve the period $r$ for $f(x) = a^x \mod N$. We define the unitary operation $$U_f |x, 0^q\rangle = |x, f(x)\rangle, \ \ f(x) = a^x \text{ mod } N.$$

1. Let $n_0$ be the number of bits in N. Let $n$ be the unique integer satisfying $N^2 \leq 2^n \leq 2N^2$. Initialize an input and output qubit register, with $n_0$ and $n$ qubits respectively, to the state $|\psi_0\rangle = |0\ldots 0\rangle_n|0\ldots 0\rangle_{n_0}$.
2. Let $q=2^n$. Prepare a superposition $$|\psi_1\rangle = \frac{1}{\sqrt{q}}\sum_{x=0}^{q-1}|x\rangle|0\rangle$$ by applying $q$ Hadamard gates.
3. Apply $U_f$ so that $|\psi_2\rangle = U_f|\psi_1\rangle.$
4. Measure the output register of $|\psi_2\rangle$ (recall this register has size $n_0$), and discard the result of that measurement (the point is to force the input register into a particular superposition). This puts the input register into state $$|\psi_3\rangle = \frac{1}{\sqrt{m}}\sum_{k=0}^{m-1}|x_0 + kr\rangle.$$
5. Apply the Quantum Fourier Transform (QFT) to $|\psi_3\rangle$ to obtain $|\psi_4\rangle = QFT|\psi_3\rangle$.
    - *Note: Many sources describe incorrectly describe this as the "inverse QFT". This confusion is due to the fact that QFT is the quantum analog of the inverse discrete Fourier transform - but it should properly be referred to as just "QFT". Still, Cirq follows the trend of calling this the "inverse QFT".*
6. Measure $y$ from the input register of $|\psi_4\rangle$.
7. Determine the continued fraction representation of $y/q$. Test each convergent $j'/r'$ in order, where $j'/r'$ is reduced to lowest terms. If at any point $r' < N$ and $$|\frac{y}{q}-\frac{j'}{r'}|\leq\frac{1}{2q}, $$ then $r'$ is the candidate value for the period. Return $r'$ to the classical component's step 3.

___

With this in mind, let's first examine how Cirq can deal with the circuit for modular exponentiation in Shor's algorithm, satisfying the unitary operation

$$U_f |x, 0^q\rangle = |x, f(x)\rangle, \ \ f(x) = a^x \text{ mod } N$$

This is not at all a simple circuit, and minimizing the number of operations to achieve this unitary function for variables $a, N$ has been the subject of much recent research and contention. Shor's landmark paper did not specify how this unitary operation could be achieved, not even discussing the number of qubits needed for the operation. Given that this operation is seen as **the bottle-neck** for applying Shor's algorithm, the debate over optimizing such a circuit is warranted.

In “Fast Quantum Modular Exponentiation Architecture for Shor’s Factorization Algorithm” \[8\] (2013), Pavlidis and Gizopoulos present a circuit with a depth near $2000n^2$ requiring $9n+2$ qubits, where $n$ represents the number of bits of the classical number being factored, where "the total quantum cost of the proposed design is $1600n^3$. The implementation is complicated, and this notebook will not attempt to program that circuit into Cirq. Instead, we'll use a simple trick to ignore the physical circuit requirements entirely, and simply *pretend* we have such a physical device ready for use. The following code creates an ArithmeticOperation in Cirq which sweeps all the troublesome circuitry under the rug.

```python
# Code modified from https://github.com/quantumlib/Cirq/blob/master/examples/shor.py

from typing import Callable, List, Optional, Sequence, Union

class ModularExp(cirq.ArithmeticOperation):
    def __init__(self, target: Sequence[cirq.Qid],
                 exponent: Union[int, Sequence[cirq.Qid]], base: int,
                 modulus: int) -> None:
        if len(target) < modulus.bit_length():
            raise ValueError(f'Register with {len(target)} qubits is too small '
                             f'for modulus {modulus}')
        self.target = target
        self.exponent = exponent
        self.base = base
        self.modulus = modulus

    def registers(self) -> Sequence[Union[int, Sequence[cirq.Qid]]]:
        return self.target, self.exponent, self.base, self.modulus

    def with_registers(
            self,
            *new_registers: Union[int, Sequence['cirq.Qid']],
    ) -> cirq.ArithmeticOperation:
        if len(new_registers) != 4:
            raise ValueError(f'Expected 4 registers (target, exponent, base, '
                             f'modulus), but got {len(new_registers)}')
        target, exponent, base, modulus = new_registers
        return ModularExp(target, exponent, base, modulus)

    def apply(self, *register_values: int) -> int:
        assert len(register_values) == 4
        target, exponent, base, modulus = register_values
        if target >= modulus:
            return target
        return target ^ (base**exponent) % modulus

    def _circuit_diagram_info_(
            self,
            args: cirq.CircuitDiagramInfoArgs,
    ) -> cirq.CircuitDiagramInfo:
        assert args.known_qubits is not None
        wire_symbols: List[str] = []
        t, e = 0, 0
        for qubit in args.known_qubits:
            if qubit in self.target:
                if t == 0:
                    if isinstance(self.exponent, Sequence):
                        e_str = 'e'
                    else:
                        e_str = str(self.exponent)
                    wire_symbols.append(
                        f'ModularExp(t*{self.base}**{e_str} % {self.modulus})')
                else:
                    wire_symbols.append('t' + str(t))
                t += 1
            if isinstance(self.exponent, Sequence) and qubit in self.exponent:
                wire_symbols.append('e' + str(e))
                e += 1
        return cirq.CircuitDiagramInfo(wire_symbols=tuple(wire_symbols))
```

This may seem oddly simple for such a complicated circuit. The magic is in the method:

```
    def apply(self, *register_values: int) -> int:
        assert len(register_values) == 4
        target, exponent, base, modulus = register_values
        if target >= modulus:
            return target
        return (target * base**exponent) % modulus
```

Here, we see that this Cirq operation really just wraps `a**x % N`. Of course, this still means we get a classical run-time when factoring numbers, but that is to be expected.

### The Algorithm

```python
'''
Attempt to factor N with a single run of Shor's algorithm.
If this returns None, then it probablistically failed and
must be run again.
If this returns -1, then the number is prime.
'''

def shor_factor(N, seed=None, verbose=False):
    if type(N) is not int:
        raise TypeError("n must be an integer.")
    if N > (2 ** 30):
        raise ValueError("Number is too large. Try n <= 2^30.")
    if N < 1:
        raise ValueError("Number must be positive integer greater than 1.")

    if N % 2 == 0:
        if verbose:
            print(f"{N} has trivial factor of 2.")
        return 2, N // 2

    # using sympy.isprime is certainly 'cheating' - but Shor's algorithm
    # doesn't work on prime numbers, so we can save some wasted effort here.
    if sympy.isprime(N):
        if verbose:
            print(f"{N} is prime. Aborting.")
        return -1

    '''
    1. Pick a random number  𝑎<𝑁
    '''
    random.seed(seed)
    a = random.randint(2, N-1)
    if verbose:
        print(f"Chose random number a={a}.")

    '''
    2. Compute  gcd(𝑎,𝑁) . If  gcd(𝑎,𝑁)≠1 , then this common divisor
    is a nontrivial factor of  𝑁 , so the algorithm is done
    (though for large numbers it is likely that the algorithm
    can be run again on those factors). Otherwise, we proceed.
    '''
    gcd = math.gcd(N, a)
    if gcd != 1:
        if verbose:
            print(f"gcd({N}, {a}) is {gcd}, which is a trivial factor.")
        return gcd, N // gcd
    if verbose:
        print(f"a is relatively prime to N.")

    '''
    3. Use the quantum component to determine  𝑟 ,
    the period of the function  𝑓(𝑥)=𝑎^𝑥 mod𝑁 .
    '''
    if verbose:
        print(f"Finding order of `{a}**x % {N}`.")
    r = quantum_find_order(a, N, seed, verbose)
    if verbose:
        print(f"Quantum routine returned period r={r}.")

    '''
    4. If any of the following conditions are true, return to step 1.
    '''
    # No candidate 𝑟′ was found
    # 𝑟 is odd
    # 𝑎^(𝑟/2)≡−1mod𝑁
    # gcd(𝑎^(r/2),𝑁)=1
    if (r == 0 or
            r is None or
            r % 2 == 1):
        if verbose:
            print(f"The period r={r} failed on classical step 4. Try algorithm again.")
        return None

    c = (a**(r // 2)) % N
    d = math.gcd(c+1, N)
    if (c % N == N-1) or d == 1:
        if verbose:
            print(f"The period r={r} failed on classical step 4. Try algorithm again.")
        return None

    '''
    5. We can factor  𝑁  into  𝑔𝑐𝑑(𝑎^(𝑟/2)±1,𝑁) .
    '''
    if verbose:
        print("Algorithm succeeded. Returning factors.")
    return d, N // d

'''
The quantum component of Shor's algorithm. Returns 'r',
the candidate period.
'''
def quantum_find_order(a, N, seed=None, verbose=False):
    n_0 = N.bit_length()
    n = 2 * n_0 + 3

    '''
    1. Initialize an input and output qubit register,
    with  𝑛  and  𝑛0  qubits respectively, to
    the state  |𝜓0⟩=|0…0⟩𝑛|0…0⟩𝑛0 .
    '''
    input_qubits = cirq.LineQubit.range(n)
    output_qubits = cirq.LineQubit.range(n, n + n_0)
    order_circuit = cirq.Circuit()

    '''
    2. Let  𝑞=2^𝑛 . Prepare a superposition
    |𝜓1⟩=1𝑞⎯⎯√∑𝑥=0𝑞−1|𝑥⟩|0⟩

    by applying  𝑞  Hadamard gates.
    '''
    q = 2 ** n
    order_circuit.append(cirq.H.on_each(*input_qubits))

    '''
    3. Apply  𝑈𝑓  so that  |𝜓2⟩=𝑈𝑓|𝜓1⟩.
    '''
    order_circuit.append(
        ModularExp(output_qubits, input_qubits, a, N)
    )

    '''
    4. Measure the output register of  |𝜓2⟩
    (recall this register has size  𝑛 ),
    and discard the result of that measurement
    (the point is to force the input register into
    a particular superposition). This puts the input
    register into state |𝜓3⟩.
    '''
    order_circuit.append(cirq.measure(*output_qubits, key='output'))

    '''
    5. Apply the Quantum Fourier Transform (QFT)
    to  |𝜓3⟩  to obtain  |𝜓4⟩=𝑄𝐹𝑇|𝜓3⟩ .
    '''
    order_circuit.append(cirq.QFT(*input_qubits, inverse=True))

    '''
    6. Measure  𝑦  from the input register of  |𝜓4⟩
    '''
    order_circuit.append(cirq.measure(*input_qubits, key='input'))
    if verbose:
        print("Generating order-finding circuit:\n")
        print(order_circuit, "\n")
    simulator = cirq.Simulator(seed=seed)
    input_result = simulator.run(order_circuit).measurements['input'][0]
    y = int("".join(str(x) for x in input_result), 2)
    if verbose:
        print(f"Circuit returned value of input register, y={y}")

    '''
    7. Determine the continued fraction representation of  𝑦/𝑞 .
    Test each convergent  𝑗′/𝑟′  in order, where  𝑗′/𝑟′  is reduced
    to lowest terms. If at any point  𝑟′<𝑁  and |𝑦/𝑞−𝑗′/𝑟′|≤1/2𝑞,
    then  𝑟′  is the candidate value for the period.
    Return  𝑟′  to the classical component's step 3.
    '''
    def continued_fraction(num, denom):
        res = []
        quo, rem = divmod(num, denom)
        while rem != 0:
            res = res + [quo]
            quo, rem = divmod(denom, rem)
            denom = (denom-rem)//quo
        return res + [quo]

    def cf_to_frac(cf):
        num, denom = 1, 0
        for u in reversed(cf):
            num, denom = denom + (num * u), num
        return num, denom

    cf = continued_fraction(y, q)
    if verbose:
        print(f"Continued fraction for {y}/{q} is {cf}.")

    # test each convergent
    for i in range(len(cf)):
        j, r = cf_to_frac(cf[0:i+1])
        if math.fabs((y/q)-(j/r)) <= (1/(2*q)):
            if verbose:
                print(f"Using convergent j/r = {j}/{r}. Returning to classical routine.")
            if r == 1:
                return 0
            return r
    # all convergents failed
    if verbose:
        print("All convergents failed. Returning to classical routine.")
    return None
```

Now, we are ready to run the algorithm. Note that this is a *very expensive* algorithm on classical systems - so we use a rather low $N$ to test the algorithm, or else we wait an eternity to factor a number like 99.

```python
for i in range(5):
    res = shor_factor(21, seed=i+13, verbose=True)
    if res:
        print(res)
        break
    print("-"*30 + "\n")
```

    Chose random number a=10.
    a is relatively prime to N.
    Finding order of `10**x % 21`.
    Generating order-finding circuit:

    0: ────H───e0─────────────────────────QFT^-1────────M('input')───
               │                          │             │
    1: ────H───e1─────────────────────────#2────────────M────────────
               │                          │             │
    2: ────H───e2─────────────────────────#3────────────M────────────
               │                          │             │
    3: ────H───e3─────────────────────────#4────────────M────────────
               │                          │             │
    4: ────H───e4─────────────────────────#5────────────M────────────
               │                          │             │
    5: ────H───e5─────────────────────────#6────────────M────────────
               │                          │             │
    6: ────H───e6─────────────────────────#7────────────M────────────
               │                          │             │
    7: ────H───e7─────────────────────────#8────────────M────────────
               │                          │             │
    8: ────H───e8─────────────────────────#9────────────M────────────
               │                          │             │
    9: ────H───e9─────────────────────────#10───────────M────────────
               │                          │             │
    10: ───H───e10────────────────────────#11───────────M────────────
               │                          │             │
    11: ───H───e11────────────────────────#12───────────M────────────
               │                          │             │
    12: ───H───e12────────────────────────#13───────────M────────────
               │
    13: ───────ModularExp(t*10**e % 21)───M('output')────────────────
               │                          │
    14: ───────t1─────────────────────────M──────────────────────────
               │                          │
    15: ───────t2─────────────────────────M──────────────────────────
               │                          │
    16: ───────t3─────────────────────────M──────────────────────────
               │                          │
    17: ───────t4─────────────────────────M──────────────────────────

    Circuit returned value of input register, y=4096
    Continued fraction for 4096/8192 is [0, 2].
    Using convergent j/r = 1/2. Returning to classical routine.
    Quantum routine returned period r=2.
    The period r=2 failed on classical step 4. Try algorithm again.
    ------------------------------

    Chose random number a=5.
    a is relatively prime to N.
    Finding order of `5**x % 21`.
    Generating order-finding circuit:

    0: ────H───e0────────────────────────QFT^-1────────M('input')───
               │                         │             │
    1: ────H───e1────────────────────────#2────────────M────────────
               │                         │             │
    2: ────H───e2────────────────────────#3────────────M────────────
               │                         │             │
    3: ────H───e3────────────────────────#4────────────M────────────
               │                         │             │
    4: ────H───e4────────────────────────#5────────────M────────────
               │                         │             │
    5: ────H───e5────────────────────────#6────────────M────────────
               │                         │             │
    6: ────H───e6────────────────────────#7────────────M────────────
               │                         │             │
    7: ────H───e7────────────────────────#8────────────M────────────
               │                         │             │
    8: ────H───e8────────────────────────#9────────────M────────────
               │                         │             │
    9: ────H───e9────────────────────────#10───────────M────────────
               │                         │             │
    10: ───H───e10───────────────────────#11───────────M────────────
               │                         │             │
    11: ───H───e11───────────────────────#12───────────M────────────
               │                         │             │
    12: ───H───e12───────────────────────#13───────────M────────────
               │
    13: ───────ModularExp(t*5**e % 21)───M('output')────────────────
               │                         │
    14: ───────t1────────────────────────M──────────────────────────
               │                         │
    15: ───────t2────────────────────────M──────────────────────────
               │                         │
    16: ───────t3────────────────────────M──────────────────────────
               │                         │
    17: ───────t4────────────────────────M──────────────────────────

    Circuit returned value of input register, y=0
    Continued fraction for 0/8192 is [0].
    Using convergent j/r = 0/1. Returning to classical routine.
    Quantum routine returned period r=0.
    The period r=0 failed on classical step 4. Try algorithm again.
    ------------------------------

    Chose random number a=8.
    a is relatively prime to N.
    Finding order of `8**x % 21`.
    Generating order-finding circuit:

    0: ────H───e0────────────────────────QFT^-1────────M('input')───
               │                         │             │
    1: ────H───e1────────────────────────#2────────────M────────────
               │                         │             │
    2: ────H───e2────────────────────────#3────────────M────────────
               │                         │             │
    3: ────H───e3────────────────────────#4────────────M────────────
               │                         │             │
    4: ────H───e4────────────────────────#5────────────M────────────
               │                         │             │
    5: ────H───e5────────────────────────#6────────────M────────────
               │                         │             │
    6: ────H───e6────────────────────────#7────────────M────────────
               │                         │             │
    7: ────H───e7────────────────────────#8────────────M────────────
               │                         │             │
    8: ────H───e8────────────────────────#9────────────M────────────
               │                         │             │
    9: ────H───e9────────────────────────#10───────────M────────────
               │                         │             │
    10: ───H───e10───────────────────────#11───────────M────────────
               │                         │             │
    11: ───H───e11───────────────────────#12───────────M────────────
               │                         │             │
    12: ───H───e12───────────────────────#13───────────M────────────
               │
    13: ───────ModularExp(t*8**e % 21)───M('output')────────────────
               │                         │
    14: ───────t1────────────────────────M──────────────────────────
               │                         │
    15: ───────t2────────────────────────M──────────────────────────
               │                         │
    16: ───────t3────────────────────────M──────────────────────────
               │                         │
    17: ───────t4────────────────────────M──────────────────────────

    Circuit returned value of input register, y=4096
    Continued fraction for 4096/8192 is [0, 2].
    Using convergent j/r = 1/2. Returning to classical routine.
    Quantum routine returned period r=2.
    Algorithm succeeded. Returning factors.
    (3, 7)

# Sources:

\[1\]: E. Knill. Conventions for quantum pseudocode. Technical Report LAUR-96-2724, LANL, 1996. (https://pdfs.semanticscholar.org/60d1/e63ca31555ec7013c5eb9a8a63788398fd14.pdf)

\[2\]: Selinger, Peter. (2004). A Brief Survey of Quantum Programming Languages. 1-6. (https://www.mscs.dal.ca/~selinger/papers/flops04.pdf)

\[3\]: B. Ömer, A Procedural Formalism for Quantum Computing, Master thesis (computing science), Technical University of Vienna, 1998. (http://tph.tuwien.ac.at/~oemer/doc/qcldoc.pdf)

\[4\]: Selinger, Peter. “Towards a Quantum Programming Language.” Mathematical Structures in Computer Science, vol. 14, no. 4, Aug. 2004, pp. 527–86. Cambridge Core, doi:10.1017/S0960129504004256. (https://www.mscs.dal.ca/~selinger/papers/qpl.pdf)

\[5\]: "Announcing Cirq: An Open Source Framework for NISQ Algorithms." (https://ai.googleblog.com/2018/07/announcing-cirq-open-source-framework.html)

\[6\]: "Announcing TensorFlow Quantum: An Open Source Library for Quantum Machine Learning." (https://ai.googleblog.com/2020/03/announcing-tensorflow-quantum-open.html)

\[7\]: Shor, Peter W. “Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer.” SIAM Journal on Computing, vol. 26, no. 5, Oct. 1997, pp. 1484–509. arXiv.org, doi:10.1137/S0097539795293172. (https://arxiv.org/pdf/quant-ph/9508027.pdf)

\[7\]: Pavlidis, Archimedes, and Dimitris Gizopoulos. “Fast Quantum Modular Exponentiation Architecture for Shor’s Factorization Algorithm.” ArXiv:1207.0511 [Quant-Ph], Nov. 2013. arXiv.org, http://arxiv.org/abs/1207.0511. (https://arxiv.org/pdf/1207.0511.pdf)

\[8\]: Cirq on GitHub. (https://github.com/quantumlib/Cirq/blob/master/)


```python
import ipywidgets as widgets
```

# Inductive Reasoning and Deductive Reasoning

There are two forms of reasoning that that are useful when investigating a piece of mathematics.

* `Inductive reasoning` involves looking for __patterns__ in evidence in order to come up with conjectures (i.e. things that are likely to be true). This sort of reasoning will __not__ tell you whether or not something actually _is_ true but it is still very useful for making connections and figuring out what to investigate next.

* `Deductive reasoning` involves starting with what you __know__ and logically figuring out if some conjecture __must__ also be true (and why). While deductive reasoning is stronger than inductive reasoning, it can also be more difficult to use.

In practice, one will often use `inductive reasoning` to make conjectures and `deductive reasoning` to verify them. In some cases producing a conjecture will require a mix of inductive and deductive reasoning.

In this notebook we will go over some example problems to help illustrate how one would go about using `inductive` and `deductive` reasoning in problem solving while avoiding pitfalls. Being able to apply these skills will make you a more effective problem solver. Being able to distinguish between the two will help you maintain a clear understanding of what you're doing, why you're doing it, and avoid mistakes in the processs.

## A Flawed Application of Inductive Reasoning

In this problem a circle is partitioned into regions by adding dots to the edges and drawing chords connecting them. The circle on the top left has only a single dot and a single region. The next one has two dots and two regions.

Before we can apply inductive reasoning we need some examples. To that end I've counted the number of regions for the first five cases:

$$1, 2, 4, 8, 16.$$

It looks like there is a pattern here. Inductive reasoning might suggest that the circle with $n$ dots will have $2^{n-1}$ regions, is that true?

Try counting the number of regions in the sixth case.

Unfortunately, as it turns out the sixth circle will break down into $31$ regions (the last one is contained in the intersection at the center, almost inexistent because of how I drew it), not $32$. This is an example where inductive reasoning can lead you astray. Fortunately for us we managed to find a counterexample right away but there are conjectures where the first counterexample took decades to find and required numbers so large that it's virtually impossible for people to find them by hand. So we should always be very skeptical about the things inductive thinking may lead us to believe.

## Some Flawed Applications of Deductive Reasoning

Deductive reasoning can also fail us if we are not careful. It is possible to get caught up manipulating equations and not realize there's an underlying logical problem.

Here are two flawed proofs. Try and find the problem in each one!

### A Classic Flawed Proof

There are a few variations of this proof (with the same flaw) floating around. Every so often a student rediscovers it and thinks they've broken math.

Let $a=b$.

Then it follows that $b^2=ab$.

$$
\begin{align*}
    a^2 - b^2 &= a^2 - b^2\\
    a^2 - b^2 &= a^2 - ab \tag{Since $b^2=ab$}\\
    (a+b)(a-b) &= (a)(a-b) \tag{Factoring}\\
    a+b &= a \tag{Divide by sides by $a-b$}\\
    2a &= a \tag{Since $a=b$}\\
    2 &= 1 \tag{Divide both sides by $a$}
\end{align*}
$$

Hint: The problem involves division.

The problem is introduced when both sides are divided by $a-b$ because $a-b=0$ and division by zero is not allowed (for reasons like this).

### A Flawed Proof Involving Radicals

This one is somewhat less common but still interesting.

$$
\begin{align*}
    -1
    &= i^2 \\
    &= (i)(i) \\
    &= \sqrt{-1}\sqrt{-1} \\
    &= \sqrt{(-1)(-1)} \\
    &= \sqrt{1} \\
    &= 1
\end{align*}
$$

So $-1=1$.

Hint: The problem involves distributing roots.

The problem occurs because $\sqrt{ab}=\sqrt{a}\sqrt{b}$ only holds when $a$ and $b$ are both greater than or equal to $0$ (neither $a$ nor $b$ are allowed to be negative).

## Some Applications of Inductive Reasoning

### Sum of the first n odd numbers

Suppose you need to compute the sum of the first $100$ odd numbers. You could do this directly but that likely wouldn't be very fun or interesting. Let's instead try applying inductive reasoning to try to come up with a better way to do it.

Before we can start looking for patterns we'll first need to generate some examples (so that we can use them as evidence later). Let's do that in Python:

```python
# Create a list of odd numbers from 1 to 20 (incrementing by 2 each time).
oddNumbers = range(1,20,2)

# Print a nice heading.
print('|  n | Odd | S(n)|')
print('------------------')

# For each odd number print the step, the number, and the sum of all odd numbers so far.
step = 0
oddSum = 0
for odd in oddNumbers:
    step = step + 1
    oddSum = oddSum + odd
    print('|{:3d} | {:3d} | {:3d} |'.format(step, odd, oddSum))
```

    |  n | Odd | S(n)|
    ------------------
    |  1 |   1 |   1 |
    |  2 |   3 |   4 |
    |  3 |   5 |   9 |
    |  4 |   7 |  16 |
    |  5 |   9 |  25 |
    |  6 |  11 |  36 |
    |  7 |  13 |  49 |
    |  8 |  15 |  64 |
    |  9 |  17 |  81 |
    | 10 |  19 | 100 |

For brevity we'll use $S(n)$ to refer to the __sum of the first $n$ odd numbers__.

The code above gives us a list of the first $10$ odd numbers as well as $S(n)$ for each one (eg. for $n=3$, the $3$rd odd is $5$ and $S(3) = 1 + 3 + 5 = 9$).

Now look closely at the data and try to see if there is a pattern there. Maybe consider changing the 20 in `range(1,20,2)` to a larger value to obtain more examples.

Hint: $1+3+5=3^2$.

A good conjecture might be that

$$S(n)=n^2.$$

Here is a slider that tests our conjecture against a larger range of values:

```python
num = widgets.IntSlider(description='n:', min=1)
def oddCompare(num):
    oddNumbers = range(1, num*2, 2)
    oddSum = sum(list(oddNumbers))
    print('S(n): {}'.format(oddSum))
    print('n^2: {}'.format((num*num)))

out = widgets.interactive_output(oddCompare, {'num': num})
widgets.VBox([num, out])
```

Failed to display Jupyter Widget of type VBox.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Now that we have a conjecture it is typically very helpful if we're able to take it further and come up with some guesses about __why__ the conjecture holds. In this case the trick is to realize that we can compute the sum of the first $n$ odd numbers by taking the sum of the first $n-1$ odd numbers and adding the $n$'th odd. In other words:

$$S(n+1)=S(n) + (n+1)^{\text{th}} \text{ odd number}.$$

For instance, $S(5) = 1 + 3 + 5 + 7 + 9 = S(4) + 9$.

Then combining this insight with the fact that we can represent square numbers as squares yields this visualization:

Unfortunately as convincing as this visual representation may be, it isn't strong enough to prove that $S(n)=n^2$ for all numbers $n$. In order to prove that it holds for all $n$ we require a more advanced proof technique that we don't currently have access to. So we must grit our teeth and accept the fact that _as far as we know_ there could exist some number out there for which this fails.

### Triangular numbers

There is famous story about the mathematician Carl Friedrich Gauss who as a child in primary school was tasked with computing the sum of the first 100 numbers as a way to keep him busy. As the story goes, Gauss quickly realized a pattern and wrote down the answer of 5050 within a few seconds.

For brevity we'll use $T(n)$ to refer to the __sum of the first $n$ numbers__.

The trick to seeing the pattern in this problem isn't as straightforward as the last one. As before we'll need to generate some examples to analyze first.

```python
# Create a list of the first 10 numbers.
numbers = range(1,11)

# Print a nice heading.
print('|  n  | T(n)|')
print('-------------')

# For each odd number print the number and the sum of all numbers so far.
tSum = 0
for num in numbers:
    tSum = tSum + num
    print('| {:3d} | {:3d} |'.format(num, tSum))
```

    |  n  | T(n)|
    -------------
    |   1 |   1 |
    |   2 |   3 |
    |   3 |   6 |
    |   4 |  10 |
    |   5 |  15 |
    |   6 |  21 |
    |   7 |  28 |
    |   8 |  36 |
    |   9 |  45 |
    |  10 |  55 |

Unfortunately this didn't turn out to be very insightful.

Another approach we can take is to try to represent the sum differently. Taking a cue from the previous section we'll draw the sum visually:

It is because of this representation that the sum of the first $n$ numbers is often referred to as a $n$'th `triangle number`. The value of our sum is represented by the 'area' of its triangular representation. Now, while it may not be easy to compute the area of such a triangle it is easy to compute the area of a rectangle and we can produce one by setting two triangles face to face:

This representation suggests a good conjecture for computing the $n$'th triangle number:
$$T(n)=\frac{(n)(n+1)}{2}.$$

Unfortunately we once again lack the advanced proof technique we need to prove (using deductive thinking) that this is true for all numbers $n$. So like before we've managed to obtain a really good conjecture through inductive thinking but are not able to confirm with certainty whether or not it's true.

### One Weird Trick

From time to time neat computational tricks like this will go viral on social media. Unfortunately the people presenting them will typically only show a few flashy examples and leave the readers feeling completely mystified about __why__ the trick works (or worse, feeling betrayed when it fails).

Before we start lets first rephrase what the picture is saying:

To compute $(97)(96)$:
1. For each of our values, compute their difference from $100$:
  - $3=100-97$
  - $4=100-96$
2. Multiply the differences to compute the first two digits of the result:
  - $12=(3)(4)$
3. Add the differences and subtract the result from $100$ to compute the remaining digits of the result:
  - $93=100-(3+4)$
4. Glue the two results together to get the final result:
  - $(97)(96)=9312$

It looks like step 3 could be simplified a bit to $93 = 97 - 4$ or $93 = 96 - 3$

In general it looks like the algorithm may be something like this:

To compute $(a)(b)$:
1. For each of our values, compute their difference from $100$:
  - $a'=100-a$
  - $b'=100-b$
2. Multiply the differences to compute the first two digits of the result:
  - $D=(a')(b')$
3. Add the differences and subtract the result from $100$ to compute the remaining digits of the result:
  - $C=a-b'$
4. Glue the two results together to get the final result:
  - $(a)(b)=C\text{ appended with }D$

Next lets have the computer generate some more examples for us so that we can get a better sense of the problem through `inductive reasoning`. The two sliders below let us choose some inputs and present the result created by the algorithm as well as the actual result with a message saying `Success!` if the algorithm gave the correct answer and `Fail!` if it gave the incorrect answer.

```python
a = widgets.IntSlider(description='a:', min=85, max=115, value=100)
b = widgets.IntSlider(description='b:', min=85, max=115, value=100)

def multiply(a,b):
    aDiff = 100-a
    bDiff = 100-b

    firstTwo = aDiff*bDiff
    lastTwo = a - bDiff

    result = str(lastTwo).lstrip('0') + str(firstTwo).zfill(2)
    print('Result: {}'.format(result))
    print('Actual product: {}'.format((a*b)))
    if (result == str(a*b)):
        print('Success!')
    else:
        print('Fail!')

out = widgets.interactive_output(multiply, {'a': a, 'b':b})
widgets.VBox([a,b, out])
```

Failed to display Jupyter Widget of type VBox.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Playing around with the sliders it seems that the algorithm fails in two cases:
1. Where the first digits in the result are greater than $100$.
    * for instance, for $(101)(99)$ it gives `100-1` instead of `9999`
2. Where the first digits in the result are negative.
    * For instance, for $(110)(110)$ it gives `120120` instead of `12100`

Can you see a pattern in the way the numbers fail, maybe a way to fix it?

It seems like both instances can be fixed by carrying values. Perhaps, instead of gluing values together like strings we're actually supposed to be multiplying the last digits by $100$ and adding the first digits! For instance, instead of saying $$9312 = 93\text{ appended with }12$$ we would say $$9312=(93)(100)+12.$$

Lets update the algorithm with this change:

To compute $(a)(b)$:
1. For each of our values, compute their difference from $100$:
  - $a'=100-a$
  - $b'=100-b$
2. Multiply the differences to compute the first two digits of the result:
  - $D=(a')(b')$
3. Add the differences and subtract the result from $100$ to compute the remaining digits of the result:
  - $C=a - b'$
4. Combine the two results together to get the final result:
  - $(a)(b)=(C)(100) + D$

In other words: $$ab = [a-(100-b)](100) + (100-b)(100-a).$$

Next let's create a new version of the sliders:

```python
a = widgets.IntSlider(description='a:', min=85, max=115, value=100)
b = widgets.IntSlider(description='b:', min=85, max=115, value=100)

def multiply(a,b):
    aDiff = 100-a
    bDiff = 100-b

    firstTwo = aDiff*bDiff
    lastTwo = 100 - (aDiff + bDiff)

    result = lastTwo*100 + firstTwo
    print('Result: {}'.format(result))
    print('Actual product: {}'.format((a*b)))
    if (result == a*b):
        print('Success!')
    else:
        print('Fail!')

out = widgets.interactive_output(multiply, {'a': a, 'b':b})
widgets.VBox([a,b, out])
```

Failed to display Jupyter Widget of type VBox.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

For the two failing examples mentioned above we now get:
* For $(101)(99)$ we get `9999` which is correct!
* For $(110)(110)$ we get `12100` which is correct!

Now that we have a conjecture let's getting a better sense of why it works. One thing we can do is to take our equation from above:
$$
\begin{align}
ab &= [a-(100-b)](100) + (100-b)(100-a) \\
&= (a)(100) - (100-b)(100) + (100-b)(100-a)
\end{align}
$$

We can visualize this:

Note: This visualization assumes that $a$ and $b$ are between $0$ and $100$ (though in our conjecture we also allow them be greater than $100$).

In general these sorts of techniques where one performs a computation by manipulating the digits of a value is called an 'algorism' (not to be confused with algorithm). They're not really used very much these days (except for fast mental math gimicks).

This particular algorism has a lot of generalizations for dealing with larger numbers but the reasoning behind them gets quite convoluted and in the end the most important part isn't proving that a algorism works for all numbers but that it works for all the numbers for which a mental computation is fast. In this case we can be satisfied by saying that this algorism works for numbers between $91$ and $109$ since those values are the easiest to use in practice.

## Some Applications of Deductive Reasoning

### Fractions

Fractions can be difficult to manipulate. Perhaps we can use deductive reasoning to come up with some easier ways to manipulate them.

First some observations:
1. Every number $a$ can be written in fraction form: $$ a=\frac{a}{1}. $$
2. For every number $a$, except in the case where $a=0$, we can write: $$ 1=\frac{a}{a}. $$
3. Multiplying two fractions just multiplies the numerators and denominators: $$ \left( \frac{a}{b}\right) \left( \frac{c}{d} \right) = \frac{ac}{bd}. $$

The first thing to note is that observation (3) gives us a way to factor any fraction: $$ \frac{a}{b} = \left( \frac{a}{1}\right) \left( \frac{1}{b} \right). $$

Cancellation follows from these observations as well: $$ (b) \left(\frac{a}{b}\right) = \left(\frac{b}{1}\right) \left(\frac{a}{1}\right)\left(\frac{1}{b}\right) = \left(\frac{b}{b}\right) \left(\frac{a}{1}\right) = (1)(a) = a$$

Let's use these to manipulate some complicated fractions.

$$
\begin{align*}
    \frac{a}{\frac{1}{c}}
    & = \left( \frac{a}{\frac{1}{c}}\right) (1) \tag{Multiply by $1$}\\
    & = \left( \frac{a}{\frac{1}{c}}\right) \left(\frac{c}{c} \right) \tag{By observation 2}\\
    & = \frac{ac}{\left( \frac{1}{c}\right) (c)} \tag{By observation 3}\\
    & = \frac{ac}{1} \tag{By canceling}\\
    & = ac \tag{By observation 1}
\end{align*}
$$

Another more complicated example:
$$
\begin{align*}
    \frac{\frac{a}{b}}{\frac{d}{c}}
    & = \left( \frac{\frac{a}{b}}{\frac{d}{c}} \right) (1)(1) \tag{Multiply by $1$}\\
    & = \left( \frac{\frac{a}{b}}{\frac{d}{c}} \right) \left(\frac{b}{b} \right)\left(\frac{c}{c} \right) \tag{By observation 2}\\
    & = \frac{\left( \frac{a}{b} \right) (b)(c)}{\left(\frac{d}{c} \right) (b)(c)} \tag{By observation 3}\\
    & = \frac{ac}{db} \tag{By canceling}
\end{align*}
$$

We can manipulate even the most complicated fractions by __cleverly multiplying by 1__ in this way.

### Distributive Property

The distributive property is extremely useful in simplifying expressions and performing computations. In fact, every multiplication algorithm you encounter will at some level boil down to some clever application of the distributive property. Simply put the distributive property tells us how addition and multiplication interact: $$(a+b)c = ac + bc.$$

Since multiplication is commutative this statement is equivalent: $$a(c+d) = ac + ad.$$

The FOIL mnemonic is just a special case of the distributive property:
$$ (a+b)(c+d) = (a+b)c + (a+b)d = ac + bc + ad + bd. $$

It is important to remember that the distributive property can be read two ways. In one sense it tells us how to distribute multiplication accross addition but in another sense it tells us how to undo that distribution. For example, suppose you have something like $6x + 10xy$. If we notice that both $6x$ and $10xy$ have $2x$ as a factor then we can rewrite that as $6x + 10xy = 2x(3 + 5y)$. This technique is an extremely useful application of deductive reasoning.

### Mentally Computing Simple Percentages

There are many occasions where one might be asked to compute a percentage of some value on the spot (eg. tipping at a restaurant). Fortunately there's a trick to doing it quickly.

First notice that computing $10\%$ is as easy as moving the decimal point one digit to the left (eg. $10\%$ of $25.3$ is $2.53$). Similarly, $1\%$ can be computed by moving the decimal point two digits to the left.

From there on it's just a matter of adding, subtracting, and/or multiplying these percentages to get to the desired percentage. For instance, $18\%$ can be computed by first computing $10\%$, doubling the value to get $20\%$, moving the decimal over one more time to get $2\%$ and then subtracting the $2\%$ value from the $20\%$ value. It's easier than it sounds.

This is an application of deductive reasoning because we reached all of the assertions here logically, not by looking at any patterns and conjecturing.