```python
%matplotlib inline
```

```python
import math
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import sympy
```

# High-School Maths Exercise
## Getting to Know Jupyter Notebook. Python Libraries and Best Practices. Basic Workflow

### Problem 1. Markdown
Jupyter Notebook is a very light, beautiful and convenient way to organize your research and display your results. Let's play with it for a while.

First, you can double-click each cell and edit its content. If you want to run a cell (that is, execute the code inside it), use Cell > Run Cells in the top menu or press Ctrl + Enter.

Second, each cell has a type. There are two main types: Markdown (which is for any kind of free text, explanations, formulas, results... you get the idea), and code (which is, well... for code :D).

Let me give you a...
#### Quick Introduction to Markdown
##### Text and Paragraphs
There are several things that you can do. As you already saw, you can write paragraph text just by typing it. In order to create a new paragraph, just leave a blank line. See how this works below:
```
This is some text.
This text is on a new line, but it will continue the same paragraph (so you can make your paragraphs more easily readable by just continuing on a new line, or just go on and on like this one line is ever continuing).

This text is displayed in a new paragraph.

And this is yet another paragraph.
```
**Result:**

This is some text.
This text is on a new line, but it will continue the same paragraph (so you can make your paragraphs more easily readable by just continuing on a new line, or just go on and on like this one line is ever continuing).

This text is displayed in a new paragraph.

And this is yet another paragraph.

##### Headings
There are six levels of headings. Level one is the highest (largest and most important), and level 6 is the smallest. You can create headings of several types by prefixing the header line with one to six "#" symbols (this is called a pound sign if you are ancient, or a sharp sign if you're a musician... or a hashtag if you're too young :D). Have a look:
```
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```

**Result:**

# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6

It is recommended that you have **only one** H1 heading - this should be the header of your notebook (or scientific paper). Below that, you can add your name or just jump to the explanations directly.

##### Emphasis
You can create emphasized (stonger) text by using a **bold** or _italic_ font. You can do this in several ways (using asterisks (\*) or underscores (\_)). In order to "escape" a symbol, prefix it with a backslash (\). You can also strike thorugh your text in order to signify a correction.
```
**bold** __bold__
*italic* _italic_

This is \*\*not \*\* bold.

I ~~didn't make~~ a mistake.
```

**Result:**

**bold** __bold__
*italic* _italic_

This is \*\*not\*\* bold.

I ~~didn't make~~ a mistake.

##### Lists
You can add two types of lists: ordered and unordered. Lists can also be nested inside one another. To do this, press Tab once (it will be converted to 4 spaces).

To create an ordered list, just type the numbers. Don't worry if your numbers are wrong - Jupyter Notebook will create them properly for you. Well, it's better to have them properly numbered anyway...
```
1. This is
2. A list
10. With many
9. Items
    1. Some of which
    2. Can
        3. Be nested
42. You can also
    * Mix
    * list
    * types
```

**Result:**
1. This is
2. A list
10. With many
9. Items
    1. Some of which
    2. Can
        3. Be nested
42. You can also
    * Mix
    * list
    * types

To create an unordered list, type an asterisk, plus or minus at the beginning:
```
* This is
* An
    + Unordered
    - list
```

**Result:**
* This is
* An
    + Unordered
        - list

##### Links
There are many ways to create links but we mostly use one of them: we present links with some explanatory text. See how it works:
```
This is [a link](http://google.com) to Google.
```

**Result:**

This is [a link](http://google.com) to Google.

##### Images
They are very similar to links. Just prefix the image with an exclamation mark. The alt(ernative) text will be displayed if the image is not available. Have a look (hover over the image to see the title text):
```
 Do you know that "taco cat" is a palindrome? Thanks to The Oatmeal :)
```

**Result:**

 Do you know that "taco cat" is a palindrome? Thanks to The Oatmeal :)

If you want to resize images or do some more advanced stuff, just use HTML.

Did I mention these cells support HTML, CSS and JavaScript? Now I did.

##### Tables
These are a pain because they need to be formatted (somewhat) properly. Here's a good [table generator](http://www.tablesgenerator.com/markdown_tables). Just select File > Paste table data... and provide a tab-separated list of values. It will generate a good-looking ASCII-art table for you.
```
| Cell1 | Cell2 | Cell3 |
|-------|-------|-------|
| 1.1   | 1.2   | 1.3   |
| 2.1   | 2.2   | 2.3   |
| 3.1   | 3.2   | 3.3   |
```

**Result:**

| Cell1 | Cell2 | Cell3 |
|-------|-------|-------|
| 1.1   | 1.2   | 1.3   |
| 2.1   | 2.2   | 2.3   |
| 3.1   | 3.2   | 3.3   |

##### Code
Just use triple backtick symbols. If you provide a language, it will be syntax-highlighted. You can also use inline code with single backticks.

```python
def square(x):
    return x ** 2
```
This is `inline` code. No syntax highlighting here.

**Result:**
```python
def square(x):
    return x ** 2
```
This is `inline` code. No syntax highlighting here.

**Now it's your turn to have some Markdown fun.** In the next cell, try out some of the commands. You can just throw in some things, or do something more structured (like a small notebook).

This is my first markdown try

I'll do my best to make it structured

Let's test all the headings

Three left

Two left

Aaaaand the last one

Now let's see what is this thing called paragraph. Hmmm looks interesting.

Actually I can put new lines and new paragraphs.
Awsome!

Let's see how we can put image to our first try with Markdown.

Now let's try some text formats in a paragraph.

    Here is my first bold text.

    Let's try how italic looks like.

    Now let's buy some milkshnitte dark chocolate.

    See this is Emphasized text.

    Now this is Underline text.

    Aaaaand this is x2.

    Aaaaaand this is L1.

 Now let's try some lists data

Coffee

Sugar

Chocolate

Waffles

Biscuits

Bread

And some code format:

```python
   for i in range(1, 10):
        print(i)
```

And some code inline format:

This is called anonymous function: `lambda s: s + 1` in python.

Let's see what is a link - Software University

Sample table example:

    Company
    Contact
    Country

    Alfreds Futterkiste
    Maria Anders
    Germany

    Centro comercial Moctezuma
    Francisco Chang
    Mexico

    Ernst Handel
    Roland Mendel
    Austria

    Island Trading
    Helen Bennett
    UK

    Laughing Bacchus Winecellars
    Yoshi Tannamuri
    Canada

    Magazzini Alimentari Riuniti
    Giovanni Rovelli
    Italy

### Problem 2. Formulas and LaTeX
Writing math formulas has always been hard. But scientists don't like difficulties and prefer standards. So, thanks to Donald Knuth (a very popular computer scientist, who also invented a lot of algorithms), we have a nice typesetting system, called LaTeX (pronounced _lah_-tek). We'll be using it mostly for math formulas, but it has a lot of other things to offer.

There are two main ways to write formulas. You could enclose them in single `$` signs like this: `$ ax + b $`, which will create an **inline formula**: $ ax + b $. You can also enclose them in double `$` signs `$$ ax + b $$` to produce $$ ax + b $$.

Most commands start with a backslash and accept parameters either in square brackets `[]` or in curly braces `{}`. For example, to make a fraction, you typically would write `$$ \frac{a}{b} $$`: $$ \frac{a}{b} $$.

[Here's a resource](http://www.stat.pitt.edu/stoffer/freetex/latex%20basics.pdf) where you can look up the basics of the math syntax. You can also search StackOverflow - there are all sorts of solutions there.

You're on your own now. Research and recreate all formulas shown in the next cell. Try to make your cell look exactly the same as mine. It's an image, so don't try to cheat by copy/pasting :D.

Note that you **do not** need to understand the formulas, what's written there or what it means. We'll have fun with these later in the course.

Equation of a line:               $$y=ax + b$$
Roots of the quadric equation $ax^2+bx+c=0$: $$\begin{array}{*{20}c} {x_{1,2} = \large{\frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}}} \\ \end{array}$$

Taylor series expansion: $$f(x)|_{x=a} = f(a) + f^\prime(a)(x-a)+\frac{f^n(a)}{2!}(x-a)^2+\dots+\frac{f^{(n)}(a)}{n!}(x-a)^n+\dots$$
Binominal theorem: $$(x+y)^n=\binom{n}{0}x^ny^0+\binom{n}{1}x^{n-1}y^1+\dots+\binom{n}{n}x^0y^n=\sum^n_{k=0}\binom{n}{k}x^{n-k}y^k$$
An integral: $$\normalsize{\int^{+\infty}_{-\infty}e^{-x^2}dx = \sqrt {\pi}}$$
A short matrix: $$\begin{pmatrix}
2 & 1 & 3\\
2 & 6 & 8\\
6 & 8 & 18
\end{pmatrix}$$
A long matrix: $$A = \begin{pmatrix}
a_{11} & a_{12} & \dots & a_{1n}\\
a_{21} & a_{22} & \dots & a_{2n}\\
\vdots & \vdots & \ddots & \vdots\\
a_{m1} & a_{m2} & \dots & a_{mn}\\
\end{pmatrix}$$

### Problem 3. Solving with Python
Let's first do some symbolic computation. We need to import `sympy` first.

**Should your imports be in a single cell at the top or should they appear as they are used?** There's not a single valid best practice. Most people seem to prefer imports at the top of the file though. **Note: If you write new code in a cell, you have to re-execute it!**

Let's use `sympy` to give us a quick symbolic solution to our equation. First import `sympy` (you can use the second cell in this notebook):
```python
import sympy
```

Next, create symbols for all variables and parameters. You may prefer to do this in one pass or separately:
```python
x = sympy.symbols('x')
a, b, c = sympy.symbols('a b c')
```

Now solve:
```python
sympy.solve(a * x**2 + b * x + c)
```

Hmmmm... we didn't expect that :(. We got an expression for $a$ because the library tried to solve for the first symbol it saw. This is an equation and we have to solve for $x$. We can provide it as a second paramter:
```python
sympy.solve(a * x**2 + b * x + c, x)
```

Finally, if we use `sympy.init_printing()`, we'll get a LaTeX-formatted result instead of a typed one. This is very useful because it produces better-looking formulas.

```python
sympy.init_printing()
x = sympy.symbols('x')
a, b, c = sympy.symbols('a b c')
sympy.solve(a * x**2 + b*x + c)
sympy.solve(a * x**2 + b*x + c, x)
```

How about a function that takes $a, b, c$ (assume they are real numbers, you don't need to do additional checks on them) and returns the **real** roots of the quadratic equation?

Remember that in order to calculate the roots, we first need to see whether the expression under the square root sign is non-negative.

If $b^2 - 4ac > 0$, the equation has two real roots: $x_1, x_2$

If $b^2 - 4ac = 0$, the equation has one real root: $x_1 = x_2$

If $b^2 - 4ac < 0$, the equation has zero real roots

Write a function which returns the roots. In the first case, return a list of 2 numbers: `[2, 3]`. In the second case, return a list of only one number: `[2]`. In the third case, return an empty list: `[]`.

```python
def solve_quadratic_equation(a, b, c):
    """
    Returns the real solutions of the quadratic equation ax^2 + bx + c = 0
    """
    solution_check = b ** 2 - 4 * a * c
    if a != 0:
        if solution_check > 0:
            x1 = (-b + math.sqrt(-4 * a * c + b ** 2)) / (2 * a)
            x2 = -(b + math.sqrt(-4 * a * c + b ** 2)) / (2 * a)
            return [x2, x1]
        elif solution_check == 0:
            x = -b / (2 * a)
            return [x]
        else:
            return []
    else:
        if solution_check > 0:
            x = -(c / b)
            return [x]

```

```python
# Testing: Execute this cell. The outputs should match the expected outputs. Feel free to write more tests
print(solve_quadratic_equation(1, -1, -2)) # [-1.0, 2.0]
print(solve_quadratic_equation(1, -8, 16)) # [4.0]
print(solve_quadratic_equation(1, 1, 1)) # []
print(solve_quadratic_equation(0, 2, 3)) # [-1.5]
```

    [-1.0, 2.0]
    [4.0]
    []
    [-1.5]

**Bonus:** Last time we saw how to solve a linear equation. Remember that linear equations are just like quadratic equations with $a = 0$. In this case, however, division by 0 will throw an error. Extend your function above to support solving linear equations (in the same way we did it last time).

### Problem 4. Equation of a Line
Let's go back to our linear equations and systems. There are many ways to define what "linear" means, but they all boil down to the same thing.

The equation $ax + b = 0$ is called *linear* because the function $f(x) = ax+b$ is a linear function. We know that there are several ways to know what one particular function means. One of them is to just write the expression for it, as we did above. Another way is to **plot** it. This is one of the most exciting parts of maths and science - when we have to fiddle around with beautiful plots (although not so beautiful in this case).

The function produces a straight line and we can see it.

How do we plot functions in general? Ww know that functions take many (possibly infinitely many) inputs. We can't draw all of them. We could, however, evaluate the function at some points and connect them with tiny straight lines. If the points are too many, we won't notice - the plot will look smooth.

Now, let's take a function, e.g. $y = 2x + 3$ and plot it. For this, we're going to use `numpy` arrays. This is a special type of array which has two characteristics:
* All elements in it must be of the same type
* All operations are **broadcast**: if `x = [1, 2, 3, 10]` and we write `2 * x`, we'll get `[2, 4, 6, 20]`. That is, all operations are performed at all indices. This is very powerful, easy to use and saves us A LOT of looping.

There's one more thing: it's blazingly fast because all computations are done in C, instead of Python.

First let's import `numpy`. Since the name is a bit long, a common convention is to give it an **alias**:
```python
import numpy as np
```

Import that at the top cell and don't forget to re-run it.

Next, let's create a range of values, e.g. $[-3, 5]$. There are two ways to do this. `np.arange(start, stop, step)` will give us evenly spaced numbers with a given step, while `np.linspace(start, stop, num)` will give us `num` samples. You see, one uses a fixed step, the other uses a number of points to return. When plotting functions, we usually use the latter. Let's generate, say, 1000 points (we know a straight line only needs two but we're generalizing the concept of plotting here :)).
```python
x = np.linspace(-3, 5, 1000)
```
Now, let's generate our function variable
```python
y = 2 * x + 3
```

We can print the values if we like but we're more interested in plotting them. To do this, first let's import a plotting library. `matplotlib` is the most commnly used one and we usually give it an alias as well.
```python
import matplotlib.pyplot as plt
```

Now, let's plot the values. To do this, we just call the `plot()` function. Notice that the top-most part of this notebook contains a "magic string": `%matplotlib inline`. This hints Jupyter to display all plots inside the notebook. However, it's a good practice to call `show()` after our plot is ready.
```python
plt.plot(x, y)
plt.show()
```

```python
x = np.linspace(-3, 5, 1000)
y = 2 * x + 3
plt.plot(x, y)
plt.show()
```

It doesn't look too bad bit we can do much better. See how the axes don't look like they should? Let's move them to zeto. This can be done using the "spines" of the plot (i.e. the borders).

All `matplotlib` figures can have many plots (subfigures) inside them. That's why when performing an operation, we have to specify a target figure. There is a default one and we can get it by using `plt.gca()`. We usually call it `ax` for "axis".
Let's save it in a variable (in order to prevent multiple calculations and to make code prettier). Let's now move the bottom and left spines to the origin $(0, 0)$ and hide the top and right one.
```python
ax = plt.gca()
ax.spines["bottom"].set_position("zero")
ax.spines["left"].set_position("zero")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
```

**Note:** All plot manipulations HAVE TO be done before calling `show()`. It's up to you whether they should be before or after the function you're plotting.

This should look better now. We can, of course, do much better (e.g. remove the double 0 at the origin and replace it with a single one), but this is left as an exercise for the reader :).

```python
def center_spines(ax, centerx=0, centery=0):
    """Centers the axis spines at <centerx, centery> on the axis 'ax' """

    # Set the axis's spines to be centered at the given point
    # (Setting all 4 spines so that the tick marks go in both directions)
    ax.spines['left'].set_position(('data', centerx))
    ax.spines['bottom'].set_position(('data', centery))
    ax.spines['right'].set_position(('data', centerx - 1))
    ax.spines['top'].set_position(('data', centery - 1))

    # Hide the line (but not ticks) for "extra" spines
    for side in ['right', 'top']:
        ax.spines[side].set_color('none')

    # On both the x and y axes
    for axis, center in zip([ax.xaxis, ax.yaxis], [centerx, centery]):
        # Hide the ticklabels at <centerx, centery>
        formatter = CenteredFormatter()
        formatter.center = center
        axis.set_major_formatter(formatter)

    # Add offset ticklabels at <centerx, centery> using annotation
    # (Should probably make these update when the plot is redrawn...)
    xlabel, ylabel = map(formatter.format_data, [centerx, centery])
    ax.annotate('%s' % xlabel, (centerx, centery),
            xytext=(-2, -4), textcoords='offset points',
            ha='right', va='top')

    ax.xaxis.set_ticks_position('bottom')
    ax.yaxis.set_ticks_position('left')

# Custom formatter
class CenteredFormatter(mpl.ticker.ScalarFormatter):
    """Acts exactly like the default Scalar Formatter, but yields an empty
    label for ticks at "center"."""
    center = 0
    def __call__(self, value, pos=None):
        if value == self.center:
            return ''
        else:
            return mpl.ticker.ScalarFormatter.__call__(self, value, pos)
```

```python
def get_plot_graph(x, y):
    ax = plt.gca()
    ax.spines["bottom"].set_position("zero")
    ax.spines["left"].set_position("zero")
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    plt.plot(x, y)
    plt.xticks(np.arange(-10, 13, 2))
    plt.yticks(np.arange(-10, 13, 2))
    ax.set_aspect('equal')
    center_spines(ax)
    return plt.show()

x = np.linspace(-3, 5, 1000)
y = 2 * x + 3
get_plot_graph(x, y)
```

### * Problem 5. Linearizing Functions
Why is the line equation so useful? The main reason is because it's so easy to work with. Scientists actually try their best to linearize functions, that is, to make linear functions from non-linear ones. There are several ways of doing this. One of them involves derivatives and we'll talk about it later in the course.

A commonly used method for linearizing functions is through algebraic transformations. Try to linearize
$$ y = ae^{bx} $$

Hint: The inverse operation of $e^{x}$ is $\ln(x)$. Start by taking $\ln$ of both sides and see what you can do. Your goal is to transform the function into another, linear function. You can look up more hints on the Internet :).

$$ln(y) = ln(ae^{bx}) \\
ln(y) = ln(a) + ln(e^{bx}) \\
ln(y) = bx + ln(a) \\
P = bx + Q$$

### * Problem 6. Generalizing the Plotting Function
Let's now use the power of Python to generalize the code we created to plot. In Python, you can pass functions as parameters to other functions. We'll utilize this to pass the math function that we're going to plot.

Note: We can also pass *lambda expressions* (anonymous functions) like this:
```python
lambda x: x + 2```
This is a shorter way to write
```python
def some_anonymous_function(x):
    return x + 2
```

We'll also need a range of x values. We may also provide other optional parameters which will help set up our plot. These may include titles, legends, colors, fonts, etc. Let's stick to the basics now.

Write a Python function which takes another function, x range and number of points, and plots the function graph by evaluating it at every point.

**BIG hint:** If you want to use not only `numpy` functions for `f` but any one function, a very useful (and easy) thing to do, is to vectorize the function `f` (e.g. to allow it to be used with `numpy` broadcasting):
```python
f_vectorized = np.vectorize(f)
y = f_vectorized(x)
```

```python
def plot_math_function(f, min_x, max_x, num_points):
    x = np.linspace(min_x, max_x, num_points)
    y = f(x)
    return get_plot_graph(x, y)
```

```python
plot_math_function(lambda x: 2 * x + 3, -3, 5, 1000)
plot_math_function(lambda x: -x + 8, -1, 10, 1000)
plot_math_function(lambda x: x**2 - x - 2, -3, 4, 1000)
plot_math_function(lambda x: np.sin(x), -np.pi, np.pi, 1000)
plot_math_function(lambda x: np.sin(x) / x, -4 * np.pi, 4 * np.pi, 1000)
```

### * Problem 7. Solving Equations Graphically
Now that we have a general plotting function, we can use it for more interesting things. Sometimes we don't need to know what the exact solution is, just to see where it lies. We can do this by plotting the two functions around the "=" sign ans seeing where they intersect. Take, for example, the equation $2x + 3 = 0$. The two functions are $f(x) = 2x + 3$ and $g(x) = 0$. Since they should be equal, the point of their intersection is the solution of the given equation. We don't need to bother marking the point of intersection right now, just showing the functions.

To do this, we'll need to improve our plotting function yet once. This time we'll need to take multiple functions and plot them all on the same graph. Note that we still need to provide the $[x_{min}; x_{max}]$ range and it's going to be the same for all functions.

```python
vectorized_fs = [np.vectorize(f) for f in functions]
ys = [vectorized_f(x) for vectorized_f in vectorized_fs]
```

```python
def plot_math_functions(functions, min_x, max_x, num_points):
    x = np.linspace(min_x, max_x, num_points)
    vectorized_fs = [np.vectorize(f) for f in functions]
    ys = [vectorized_f(x) for vectorized_f in vectorized_fs]

    ax = plt.gca()
    ax.spines["bottom"].set_position("zero")
    ax.spines["left"].set_position("zero")
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    for y in ys:
        plt.plot(x, y)
    plt.xticks(np.arange(-6, 13, 2))
    plt.yticks(np.arange(-6, 13, 2))
    ax.set_aspect('equal')
    center_spines(ax)
    return plt.show()

```

```python
plot_math_functions([lambda x: 2 * x + 3, lambda x: 0], -3, 5, 1000)
plot_math_functions([lambda x: 3 * x**2 - 2 * x + 5, lambda x: 3 * x + 7], -2, 3, 1000)
```

This is also a way to plot the solutions of systems of equation, like the one we solved last time. Let's actually try it.

```python
plot_math_functions([lambda x: (-4 * x + 7) / 3, lambda x: (-3 * x + 8) / 5, lambda x: (-x - 1) / -2], -1, 4, 1000)
```

### Problem 8. Trigonometric Functions
We already saw the graph of the function $y = \sin(x)$. But, how do we define the trigonometric functions once again? Let's quickly review that.

The two basic trigonometric functions are defined as the ratio of two sides:
$$ \sin(x) = \frac{\text{opposite}}{\text{hypotenuse}} $$
$$ \cos(x) = \frac{\text{adjacent}}{\text{hypotenuse}} $$

And also:
$$ \tan(x) = \frac{\text{opposite}}{\text{adjacent}} = \frac{\sin(x)}{\cos(x)} $$
$$ \cot(x) = \frac{\text{adjacent}}{\text{opposite}} = \frac{\cos(x)}{\sin(x)} $$

This is fine, but using this, "right-triangle" definition, we're able to calculate the trigonometric functions of angles up to $90^\circ$. But we can do better. Let's now imagine a circle centered at the origin of the coordinate system, with radius $r = 1$. This is called a "unit circle".

We can now see exactly the same picture. The $x$-coordinate of the point in the circle corresponds to $\cos(\alpha)$ and the $y$-coordinate - to $\sin(\alpha)$. What did we get? We're now able to define the trigonometric functions for all degrees up to $360^\circ$. After that, the same values repeat: these functions are **periodic**:
$$ \sin(k.360^\circ + \alpha) = \sin(\alpha), k = 0, 1, 2, \dots $$
$$ \cos(k.360^\circ + \alpha) = \cos(\alpha), k = 0, 1, 2, \dots $$

We can, of course, use this picture to derive other identities, such as:
$$ \sin(90^\circ + \alpha) = \cos(\alpha) $$

A very important property of the sine and cosine is that they accept values in the range $(-\infty; \infty)$ and produce values in the range $[-1; 1]$. The two other functions take values in the range $(-\infty; \infty)$ **except when their denominators are zero** and produce values in the same range.

#### Radians
A degree is a geometric object, $1/360$th of a full circle. This is quite inconvenient when we work with angles. There is another, natural and intrinsic measure of angles. It's called the **radian** and can be written as $\text{rad}$ or without any designation, so $\sin(2)$ means "sine of two radians".

It's defined as *the central angle of an arc with length equal to the circle's radius* and $1\text{rad} \approx 57.296^\circ$.

We know that the circle circumference is $C = 2\pi r$, therefore we can fit exactly $2\pi$ arcs with length $r$ in $C$. The angle corresponding to this is $360^\circ$ or $2\pi\ \text{rad}$. Also, $\pi rad = 180^\circ$.

(Some people prefer using $\tau = 2\pi$ to avoid confusion with always multiplying by 2 or 0.5 but we'll use the standard notation here.)

**NOTE:** All trigonometric functions in `math` and `numpy` accept radians as arguments. In order to convert between radians and degrees, you can use the relations $\text{[deg]} = 180/\pi.\text{[rad]}, \text{[rad]} =  \pi/180.\text{[deg]}$. This can be done using `np.deg2rad()` and `np.rad2deg()` respectively.

#### Inverse trigonometric functions
All trigonometric functions have their inverses. If you plug in, say $\pi/4$ in the $\sin(x)$ function, you get $\sqrt{2}/2$. The inverse functions (also called, arc-functions) take arguments in the interval $[-1; 1]$ and return the angle that they correspond to. Take arcsine for example:
$$ \arcsin(y) = x: sin(y) = x $$
$$ \arcsin\left(\frac{\sqrt{2}}{2}\right) = \frac{\pi}{4} $$

Please note that this is NOT entirely correct. From the relations we found:
$$\sin(x) = sin(2k\pi + x), k = 0, 1, 2, \dots $$

it follows that $\arcsin(x)$ has infinitely many values, separated by $2k\pi$ radians each:
$$ \arcsin\left(\frac{\sqrt{2}}{2}\right) = \frac{\pi}{4} + 2k\pi, k = 0, 1, 2, \dots $$

In most cases, however, we're interested in the first value (when $k = 0$). It's called the **principal value**.

Note 1: There are inverse functions for all four basic trigonometric functions: $\arcsin$, $\arccos$, $\arctan$, $\text{arccot}$. These are sometimes written as $\sin^{-1}(x)$, $\cos^{-1}(x)$, etc. These definitions are completely equivalent.

Just notice the difference between $\sin^{-1}(x) := \arcsin(x)$ and $\sin(x^{-1}) = \sin(1/x)$.

#### Exercise
Use the plotting function you wrote above to plot the inverse trigonometric functions. Use `numpy` (look up how to use inverse trigonometric functions).

```python
def plot_math_functions(x, ys, labels):
    ax = plt.gca()
    ax.spines["bottom"].set_position("zero")
    ax.spines["left"].set_position("zero")
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    for i in range(len(ys)):
        y = ys[i]
        plt.plot(x, y, label = labels[i])
    plt.xticks(np.arange(-2, 2, 0.5))
    plt.yticks(np.arange(-3, 5, 0.5))
    #ax.set_aspect('equal')
    center_spines(ax)
    plt.title("Inverse Trigonometric Functions")
    plt.legend(loc='lower right')
    return plt.show()
def plot_inverse_trigonoetric_function(min_x, max_x, num_points):
    x = np.linspace(min_x, max_x, num_points)
    labels = ['arcsin', 'arccos', 'arctan', 'arccot']
    ys = [np.arcsin(x), np.arccos(x), np.arctan(x), np.pi/2-np.arctan(x)]
    return plot_math_functions(x, ys, labels)
```

```python
plot_inverse_trigonoetric_function(-1, 1, 10000)
```

### ** Problem 9. Perlin Noise
This algorithm has many applications in computer graphics and can serve to demonstrate several things... and help us learn about math, algorithms and Python :).
#### Noise
Noise is just random values. We can generate noise by just calling a random generator. Note that these are actually called *pseudorandom generators*. We'll talk about this later in this course.
We can generate noise in however many dimensions we want. For example, if we want to generate a single dimension, we just pick N random values and call it a day. If we want to generate a 2D noise space, we can take an approach which is similar to what we already did with `np.meshgrid()`.

$$ \text{noise}(x, y) = N, N \in [n_{min}, n_{max}] $$

This function takes two coordinates and returns a single number N between $n_{min}$ and $n_{max}$. (This is what we call a "scalar field").

Random variables are always connected to **distributions**. We'll talk about these a great deal but now let's just say that these define what our noise will look like. In the most basic case, we can have "uniform noise" - that is, each point in our little noise space $[n_{min}, n_{max}]$ will have an equal chance (probability) of being selected.

#### Perlin noise
There are many more distributions but right now we'll want to have a look at a particular one. **Perlin noise** is a kind of noise which looks smooth. It looks cool, especially if it's colored. The output may be tweaked to look like clouds, fire, etc. 3D Perlin noise is most widely used to generate random terrain.

#### Algorithm
... Now you're on your own :). Research how the algorithm is implemented (note that this will require that you understand some other basic concepts like vectors and gradients).

#### Your task
1. Research about the problem. See what articles, papers, Python notebooks, demos, etc. other people have created
2. Create a new notebook and document your findings. Include any assumptions, models, formulas, etc. that you're using
3. Implement the algorithm. Try not to copy others' work, rather try to do it on your own using the model you've created
4. Test and improve the algorithm
5. (Optional) Create a cool demo :), e.g. using Perlin noise to simulate clouds. You can even do an animation (hint: you'll need gradients not only in space but also in time)
6. Communicate the results (e.g. in the Softuni forum)

Hint: [This](http://flafla2.github.io/2014/08/09/perlinnoise.html) is a very good resource. It can show you both how to organize your notebook (which is important) and how to implement the algorithm.


##### Copyright 2020 The TensorFlow Authors.

```python
#@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.
```

# MNIST classification

    View on TensorFlow.org

    Run in Google Colab

    View source on GitHub

    Download notebook

This tutorial builds a quantum neural network (QNN) to classify a simplified version of MNIST, similar to the approach used in Farhi et al. The performance of the quantum neural network on this classical data problem is compared with a classical neural network.

## Setup

```python
!pip install -q tensorflow==2.1.0
```

Install TensorFlow Quantum:

```python
!pip install -q tensorflow-quantum
```

Now import TensorFlow and the module dependencies:

```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np
import seaborn as sns
import collections

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

## 1. Load the data

In this tutorial you will build a binary classifier to distinguish between the digits 3 and 6, following Farhi et al. This section covers the data handling that:

- Loads the raw data from Keras.
- Filters the dataset to only 3s and 6s.
- Downscales the images so they fit can fit in a quantum computer.
- Removes any contradictory examples.
- Converts the binary images to Cirq circuits.
- Converts the Circ circuits to TensorFlow Quantum circuits.

### 1.1 Load the raw data

Load the MNIST dataset distributed with Keras.

```python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.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_train))
```

    Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
    11493376/11490434 [==============================] - 0s 0us/step
    Number of original training examples: 60000
    Number of original test examples: 60000

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

```python
def filter_36(x, y):
    keep = (y == 3) | (y == 6)
    x, y = x[keep], y[keep]
    y = y == 3
    return x,y
```

```python
x_train, y_train = filter_36(x_train, y_train)
x_test, y_test = filter_36(x_test, y_test)

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

    Number of filtered training examples: 12049
    Number of filtered test examples: 1968

Show the first example:

```python
print(y_train[0])

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

### 1.2 Downscale the images

An image size of 28x28 is much too large for current quantum computers. Resize the image down to 4x4:

```python
x_train_small = tf.image.resize(x_train, (4,4)).numpy()
x_test_small = tf.image.resize(x_test, (4,4)).numpy()
```

Again, display the first training example—after resize:

```python
print(y_train[0])

plt.imshow(x_train_small[0,:,:,0], vmin=0, vmax=1)
plt.colorbar()
```

### 1.3 Remove contradictory examples

From section *3.3 Learning to Distinguish Digits* of Farhi et al., filter the dataset to remove images that are labeled as belonging to both classes.

This is not a standard machine-learning procedure, but is included in the interest of following the paper.

```python
def remove_contradicting(xs, ys):
    mapping = collections.defaultdict(set)
    # Determine the set of labels for each unique image:
    for x,y in zip(xs,ys):
       mapping[tuple(x.flatten())].add(y)

    new_x = []
    new_y = []
    for x,y in zip(xs, ys):
      labels = mapping[tuple(x.flatten())]
      if len(labels) == 1:
          new_x.append(x)
          new_y.append(list(labels)[0])
      else:
          # Throw out images that match more than one label.
          pass

    num_3 = sum(1 for value in mapping.values() if True in value)
    num_6 = sum(1 for value in mapping.values() if False in value)
    num_both = sum(1 for value in mapping.values() if len(value) == 2)

    print("Number of unique images:", len(mapping.values()))
    print("Number of 3s: ", num_3)
    print("Number of 6s: ", num_6)
    print("Number of contradictory images: ", num_both)
    print()
    print("Initial number of examples: ", len(xs))
    print("Remaining non-contradictory examples: ", len(new_x))

    return np.array(new_x), np.array(new_y)
```

The resulting counts do not closely match the reported values, but the exact procedure is not specified.

It is also worth noting here that applying filtering contradictory examples at this point does not totally prevent the model from receiving contradictory training examples: the next step binarizes the data which will cause more collisions.

```python
x_train_nocon, y_train_nocon = remove_contradicting(x_train_small, y_train)
```

    Number of unique images: 10387
    Number of 3s:  4961
    Number of 6s:  5475
    Number of contradictory images:  49

    Initial number of examples:  12049
    Remaining non-contradictory examples:  11520

### 1.3 Encode the data as quantum circuits

To process images using a quantum computer, Farhi et al. proposed representing each pixel with a qubit, with the state depending on the value of the pixel. The first step is to convert to a binary encoding.

```python
THRESHOLD = 0.5

x_train_bin = np.array(x_train_nocon > THRESHOLD, dtype=np.float32)
x_test_bin = np.array(x_test_small > THRESHOLD, dtype=np.float32)
```

The qubits at pixel indices with values that exceed a threshold, are rotated through an $X$ gate.

```python
def convert_to_circuit(image):
    """Encode truncated classical image into quantum datapoint."""
    values = np.ndarray.flatten(image)
    qubits = cirq.GridQubit.rect(4, 4)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))
    return circuit

x_train_circ = [convert_to_circuit(x) for x in x_train_bin]
x_test_circ = [convert_to_circuit(x) for x in x_test_bin]
```

Here is the circuit created for the first example (circuit diagrams do not show qubits with zero gates):

```python
SVGCircuit(x_train_circ[0])
```

    findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.

Compare this circuit to the indices where the image value exceeds the threshold:

```python
bin_img = x_train_bin[0,:,:,0]
indices = np.array(np.where(bin_img)).T
indices
```

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

Convert these `Cirq` circuits to tensors for `tfq`:

```python
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)
```

## 2. Quantum neural network

There is little guidance for a quantum circuit structure that classifies images. Since the classification is based on the expectation of the readout qubit, Farhi et al. propose using two qubit gates, with the readout qubit always acted upon. This is similar in some ways to running small a Unitary RNN across the pixels.

### 2.1 Build the model circuit

This following example shows this layered approach. Each layer uses *n* instances of the same gate, with each of the data qubits acting on the readout qubit.

Start with a simple class that will add a layer of these gates to a circuit:

```python
class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)
```

Build an example circuit layer to see how it looks:

```python
demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)
```

Now build a two-layered model, matching the data-circuit size, and include the preparation and readout operations.

```python
def create_quantum_model():
    """Create a QNN model circuit and readout operation to go along with it."""
    data_qubits = cirq.GridQubit.rect(4, 4)  # a 4x4 grid.
    readout = cirq.GridQubit(-1, -1)         # a single qubit at [-1,-1]
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))

    return circuit, cirq.Z(readout)
```

```python
model_circuit, model_readout = create_quantum_model()
```

### 2.2 Wrap the model-circuit in a tfq-keras model

Build the Keras model with the quantum components. This model is fed the "quantum data", from `x_train_circ`, that encodes the classical data. It uses a *Parametrized Quantum Circuit* layer, `tfq.layers.PQC`, to train the model circuit, on the quantum data.

To classify these images, Farhi et al. proposed taking the expectation of a readout qubit in a parameterized circuit. The expectation returns a value between 1 and -1.

```python
# Build the Keras model.
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
])
```

Next, describe the training procedure to the model, using the `compile` method.

Since the the expected readout is in the range `[-1,1]`, optimizing the hinge loss is a somewhat natural fit.

Note: Another valid approach would be to shift the output range to `[0,1]`, and treat it as the probability the model assigns to class `3`. This could be used with a standard a `tf.losses.BinaryCrossentropy` loss.

To use the hinge loss here you need to make two small adjustments. First convert the labels, `y_train`, from boolean to `[-1,1]`, as expected by the hinge loss.

```python
y_train_hinge = 2.0*y_train-1.0
y_test_hinge = 2.0*y_test-1.0
```

Second, use a custiom `hinge_accuracy` metric that correctly handles `[-1, 1]` as the `y_true` labels argument.
`tf.losses.BinaryAccuracy(threshold=0.0)` expects `y_true` to be a boolean, and so can't be used with hinge loss).

```python
def hinge_accuracy(y_true, y_pred):
    y_true = tf.squeeze(y_true) > 0.0
    y_pred = tf.squeeze(y_pred) > 0.0
    result = tf.cast(y_true == y_pred, tf.float32)

    return tf.reduce_mean(result)
```

```python
model.compile(
    loss=tf.keras.losses.Hinge(),
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[hinge_accuracy])
```

```python
print(model.summary())
```

    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    pqc (PQC)                    (None, 1)                 32
    =================================================================
    Total params: 32
    Trainable params: 32
    Non-trainable params: 0
    _________________________________________________________________
    None

### Train the quantum model

Now train the model—this takes about 45 min. If you don't want to wait that long, use a small subset of the data (set `NUM_EXAMPLES=500`, below). This doesn't really affect the model's progress during training (it only has 32 parameters, and doesn't need much data to constrain these). Using fewer examples just ends training earlier (5min), but runs long enough to show that it is making progress in the validation logs.

```python
EPOCHS = 3
BATCH_SIZE = 32

NUM_EXAMPLES = len(x_train_tfcirc)
```

```python
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_hinge[:NUM_EXAMPLES]
```

Training this model to convergence should achieve >85% accuracy on the test set.

```python
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test_hinge))

qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

    Train on 11520 samples, validate on 1968 samples
    Epoch 1/3
    11520/11520 [==============================] - 377s 33ms/sample - loss: 1.0000 - hinge_accuracy: 0.5016 - val_loss: 1.0005 - val_hinge_accuracy: 0.4798
    Epoch 2/3
    11520/11520 [==============================] - 369s 32ms/sample - loss: 1.0000 - hinge_accuracy: 0.4995 - val_loss: 1.0011 - val_hinge_accuracy: 0.4098
    Epoch 3/3
    11520/11520 [==============================] - 369s 32ms/sample - loss: 0.9997 - hinge_accuracy: 0.5090 - val_loss: 1.0054 - val_hinge_accuracy: 0.4844
    1968/1968 [==============================] - 3s 1ms/sample - loss: 1.0054 - hinge_accuracy: 0.4844

Note: The training accuracy reports the average over the epoch. The validation accuracy is evaluated at the end of each epoch.

## 3. Classical neural network

While the quantum neural network works for this simplified MNIST problem, a basic classical neural network can easily outperform a QNN on this task. After a single epoch, a classical neural network can achieve >98% accuracy on the holdout set.

In the following example, a classical neural network is used for for the 3-6 classification problem using the entire 28x28 image instead of subsampling the image. This easily converges to nearly 100% accuracy of the test set.

```python
def create_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Conv2D(32, [3, 3], activation='relu', input_shape=(28,28,1)))
    model.add(tf.keras.layers.Conv2D(64, [3, 3], activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(128, activation='relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(1))
    return model

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

model.summary()
```

    Model: "sequential_1"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    conv2d (Conv2D)              (None, 26, 26, 32)        320
    _________________________________________________________________
    conv2d_1 (Conv2D)            (None, 24, 24, 64)        18496
    _________________________________________________________________
    max_pooling2d (MaxPooling2D) (None, 12, 12, 64)        0
    _________________________________________________________________
    dropout (Dropout)            (None, 12, 12, 64)        0
    _________________________________________________________________
    flatten (Flatten)            (None, 9216)              0
    _________________________________________________________________
    dense (Dense)                (None, 128)               1179776
    _________________________________________________________________
    dropout_1 (Dropout)          (None, 128)               0
    _________________________________________________________________
    dense_1 (Dense)              (None, 1)                 129
    =================================================================
    Total params: 1,198,721
    Trainable params: 1,198,721
    Non-trainable params: 0
    _________________________________________________________________

```python
model.fit(x_train,
          y_train,
          batch_size=128,
          epochs=1,
          verbose=1,
          validation_data=(x_test, y_test))

cnn_results = model.evaluate(x_test, y_test)
```

    Train on 12049 samples, validate on 1968 samples
    12049/12049 [==============================] - 3s 277us/sample - loss: 0.0409 - accuracy: 0.9849 - val_loss: 0.0056 - val_accuracy: 0.9990
    1968/1968 [==============================] - 0s 105us/sample - loss: 0.0056 - accuracy: 0.9990

The above model has nearly 1.2M parameters. For a more fair comparison, try a 37-parameter model, on the subsampled images:

```python
def create_fair_classical_model():
    # A simple model based off LeNet from https://keras.io/examples/mnist_cnn/
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten(input_shape=(4,4,1)))
    model.add(tf.keras.layers.Dense(2, activation='relu'))
    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(),
              metrics=['accuracy'])

model.summary()
```

    Model: "sequential_2"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    flatten_1 (Flatten)          (None, 16)                0
    _________________________________________________________________
    dense_2 (Dense)              (None, 2)                 34
    _________________________________________________________________
    dense_3 (Dense)              (None, 1)                 3
    =================================================================
    Total params: 37
    Trainable params: 37
    Non-trainable params: 0
    _________________________________________________________________

```python
model.fit(x_train_bin,
          y_train_nocon,
          batch_size=128,
          epochs=20,
          verbose=2,
          validation_data=(x_test_bin, y_test))

fair_nn_results = model.evaluate(x_test_bin, y_test)
```

    Train on 11520 samples, validate on 1968 samples
    Epoch 1/20
    11520/11520 - 0s - loss: 0.6929 - accuracy: 0.5027 - val_loss: 0.6903 - val_accuracy: 0.4868
    Epoch 2/20
    11520/11520 - 0s - loss: 0.6848 - accuracy: 0.5027 - val_loss: 0.6751 - val_accuracy: 0.4868
    Epoch 3/20
    11520/11520 - 0s - loss: 0.6530 - accuracy: 0.5240 - val_loss: 0.6218 - val_accuracy: 0.6448
    Epoch 4/20
    11520/11520 - 0s - loss: 0.5901 - accuracy: 0.7362 - val_loss: 0.5542 - val_accuracy: 0.8349
    Epoch 5/20
    11520/11520 - 0s - loss: 0.5267 - accuracy: 0.8625 - val_loss: 0.4975 - val_accuracy: 0.8694
    Epoch 6/20
    11520/11520 - 0s - loss: 0.4745 - accuracy: 0.8777 - val_loss: 0.4527 - val_accuracy: 0.8709
    Epoch 7/20
    11520/11520 - 0s - loss: 0.4325 - accuracy: 0.8826 - val_loss: 0.4171 - val_accuracy: 0.8725
    Epoch 8/20
    11520/11520 - 0s - loss: 0.3987 - accuracy: 0.8842 - val_loss: 0.3891 - val_accuracy: 0.8714
    Epoch 9/20
    11520/11520 - 0s - loss: 0.3718 - accuracy: 0.8843 - val_loss: 0.3668 - val_accuracy: 0.8704
    Epoch 10/20
    11520/11520 - 0s - loss: 0.3504 - accuracy: 0.8852 - val_loss: 0.3486 - val_accuracy: 0.8709
    Epoch 11/20
    11520/11520 - 0s - loss: 0.3331 - accuracy: 0.8854 - val_loss: 0.3339 - val_accuracy: 0.8709
    Epoch 12/20
    11520/11520 - 0s - loss: 0.3190 - accuracy: 0.8854 - val_loss: 0.3217 - val_accuracy: 0.8709
    Epoch 13/20
    11520/11520 - 0s - loss: 0.3073 - accuracy: 0.8868 - val_loss: 0.3114 - val_accuracy: 0.8720
    Epoch 14/20
    11520/11520 - 0s - loss: 0.2973 - accuracy: 0.8917 - val_loss: 0.3024 - val_accuracy: 0.9187
    Epoch 15/20
    11520/11520 - 0s - loss: 0.2886 - accuracy: 0.9155 - val_loss: 0.2947 - val_accuracy: 0.9187
    Epoch 16/20
    11520/11520 - 0s - loss: 0.2810 - accuracy: 0.9155 - val_loss: 0.2878 - val_accuracy: 0.9187
    Epoch 17/20
    11520/11520 - 0s - loss: 0.2743 - accuracy: 0.9155 - val_loss: 0.2817 - val_accuracy: 0.9187
    Epoch 18/20
    11520/11520 - 0s - loss: 0.2684 - accuracy: 0.9155 - val_loss: 0.2767 - val_accuracy: 0.9187
    Epoch 19/20
    11520/11520 - 0s - loss: 0.2631 - accuracy: 0.9155 - val_loss: 0.2718 - val_accuracy: 0.9187
    Epoch 20/20
    11520/11520 - 0s - loss: 0.2584 - accuracy: 0.9155 - val_loss: 0.2676 - val_accuracy: 0.9197
    1968/1968 [==============================] - 0s 23us/sample - loss: 0.2676 - accuracy: 0.9197

## 4. Comparison

Higher resolution input and a more powerful model make this problem easy for the CNN. While a classical model of similar power (~32 parameters) trains to a similar accuracy in a fraction of the time. One way or the other, the classical neural network easily outperforms the quantum neural network. For classical data, it is difficult to beat a classical neural network.

```python
qnn_accuracy = qnn_results[1]
cnn_accuracy = cnn_results[1]
fair_nn_accuracy = fair_nn_results[1]

sns.barplot(["Quantum", "Classical, full", "Classical, fair"],
            [qnn_accuracy, cnn_accuracy, fair_nn_accuracy])
```


            prepared by Abuzer Yakaryilmaz (QLatvia) and
 Maksim Dimitrijev(QLatvia)

This cell contains some macros. If there is a problem with displaying mathematical formulas, please run this cell to load these macros.

$\newcommand{\bra}[1]{\langle #1|}$
$ \newcommand{\ket}[1]{|#1\rangle} $
$ \newcommand{\braket}[2]{\langle #1|#2\rangle} $
$ \newcommand{\dot}[2]{ #1 \cdot #2} $
$ \newcommand{\biginner}[2]{\left\langle #1,#2\right\rangle} $
$ \newcommand{\mymatrix}[2]{\left( \begin{array}{#1} #2\end{array} \right)} $
$ \newcommand{\myvector}[1]{\mymatrix{c}{#1}} $
$ \newcommand{\myrvector}[1]{\mymatrix{r}{#1}} $
$ \newcommand{\mypar}[1]{\left( #1 \right)} $
$ \newcommand{\mybigpar}[1]{ \Big( #1 \Big)} $
$ \newcommand{\sqrttwo}{\frac{1}{\sqrt{2}}} $
$ \newcommand{\dsqrttwo}{\dfrac{1}{\sqrt{2}}} $
$ \newcommand{\onehalf}{\frac{1}{2}} $
$ \newcommand{\donehalf}{\dfrac{1}{2}} $
$ \newcommand{\hadamard}{ \mymatrix{rr}{ \sqrttwo & \sqrttwo \\ \sqrttwo & -\sqrttwo }} $
$ \newcommand{\vzero}{\myvector{1\\0}} $
$ \newcommand{\vone}{\myvector{0\\1}} $
$ \newcommand{\vhadamardzero}{\myvector{ \sqrttwo \\  \sqrttwo } } $
$ \newcommand{\vhadamardone}{ \myrvector{ \sqrttwo \\ -\sqrttwo } } $
$ \newcommand{\myarray}[2]{ \begin{array}{#1}#2\end{array}} $
$ \newcommand{\X}{ \mymatrix{cc}{0 & 1 \\ 1 & 0}  } $
$ \newcommand{\Z}{ \mymatrix{rr}{1 & 0 \\ 0 & -1}  } $
$ \newcommand{\Htwo}{ \mymatrix{rrrr}{ \frac{1}{2} & \frac{1}{2} & \frac{1}{2} & \frac{1}{2} \\ \frac{1}{2} & -\frac{1}{2} & \frac{1}{2} & -\frac{1}{2} \\ \frac{1}{2} & \frac{1}{2} & -\frac{1}{2} & -\frac{1}{2} \\ \frac{1}{2} & -\frac{1}{2} & -\frac{1}{2} & \frac{1}{2} } } $
$ \newcommand{\CNOT}{ \mymatrix{cccc}{1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0} } $
$ \newcommand{\norm}[1]{ \left\lVert #1 \right\rVert } $
$ \newcommand{\pstate}[1]{ \lceil \mspace{-1mu} #1 \mspace{-1.5mu} \rfloor } $

# Qiskit, Qutip installation and test

## Install Qiskit

You can install Qiskit by executing the following cell.

```python
!pip install "qiskit[visualization]" --user
```

    Requirement already satisfied: qiskit[visualization] in /home/dev/.local/lib/python3.8/site-packages (0.28.0)
    Requirement already satisfied: qiskit-ignis==0.6.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.6.0)
    Requirement already satisfied: qiskit-terra==0.18.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.18.0)
    Requirement already satisfied: qiskit-ibmq-provider==0.15.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.15.0)
    Requirement already satisfied: qiskit-aqua==0.9.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.9.4)
    Requirement already satisfied: qiskit-aer==0.8.2 in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.8.2)
    Requirement already satisfied: ipywidgets>=7.3.0; extra == "visualization" in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (7.6.3)
    Requirement already satisfied: pylatexenc>=1.4; extra == "visualization" in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (2.10)
    Requirement already satisfied: pydot; extra == "visualization" in /usr/lib/python3/dist-packages (from qiskit[visualization]) (1.4.1)
    Requirement already satisfied: pillow>=4.2.1; extra == "visualization" in /usr/lib/python3/dist-packages (from qiskit[visualization]) (7.0.0)
    Requirement already satisfied: matplotlib>=2.1; extra == "visualization" in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (3.4.2)
    Requirement already satisfied: seaborn>=0.9.0; extra == "visualization" in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (0.11.1)
    Requirement already satisfied: pygments>=2.4; extra == "visualization" in /home/dev/.local/lib/python3.8/site-packages (from qiskit[visualization]) (2.9.0)
    Requirement already satisfied: numpy>=1.13 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit[visualization]) (1.21.0)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/lib/python3/dist-packages (from qiskit-ignis==0.6.0->qiskit[visualization]) (45.2.0)
    Requirement already satisfied: retworkx>=0.8.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit[visualization]) (0.9.0)
    Requirement already satisfied: scipy!=0.19.1,>=0.19 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit[visualization]) (1.7.0)
    Requirement already satisfied: ply>=3.10 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (3.11)
    Requirement already satisfied: fastjsonschema>=2.10 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (2.15.1)
    Requirement already satisfied: tweedledum<2.0,>=1.1 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (1.1.0)
    Requirement already satisfied: sympy>=1.3 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (1.8)
    Requirement already satisfied: psutil>=5 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (5.8.0)
    Requirement already satisfied: python-dateutil>=2.8.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (2.8.2)
    Requirement already satisfied: jsonschema>=2.6 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (3.2.0)
    Requirement already satisfied: python-constraint>=1.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (1.4.0)
    Requirement already satisfied: symengine>0.7; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "amd64" or platform_machine == "arm64" in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (0.7.2)
    Requirement already satisfied: dill>=0.3 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit[visualization]) (0.3.4)
    Requirement already satisfied: websocket-client>=1.0.1 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (1.1.0)
    Requirement already satisfied: requests-ntlm>=1.1.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (1.1.0)
    Requirement already satisfied: requests>=2.19 in /usr/lib/python3/dist-packages (from qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (2.22.0)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/lib/python3/dist-packages (from qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (1.25.8)
    Requirement already satisfied: scikit-learn>=0.20.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (0.24.2)
    Requirement already satisfied: h5py<3.3.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (3.2.1)
    Requirement already satisfied: yfinance<0.1.63 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (0.1.62)
    Requirement already satisfied: dlx<=1.0.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (1.0.4)
    Requirement already satisfied: docplex>=2.21.207 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (2.21.207)
    Requirement already satisfied: pandas in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (1.3.0)
    Requirement already satisfied: fastdtw<=0.3.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (0.3.4)
    Requirement already satisfied: quandl in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit[visualization]) (3.6.1)
    Requirement already satisfied: pybind11>=2.6 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aer==0.8.2->qiskit[visualization]) (2.7.0)
    Requirement already satisfied: ipython>=4.0.0; python_version >= "3.3" in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (7.25.0)
    Requirement already satisfied: traitlets>=4.3.1 in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (5.0.5)
    Requirement already satisfied: nbformat>=4.2.0 in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (5.1.3)
    Requirement already satisfied: jupyterlab-widgets>=1.0.0; python_version >= "3.6" in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.0.0)
    Requirement already satisfied: widgetsnbextension~=3.5.0 in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (3.5.1)
    Requirement already satisfied: ipykernel>=4.5.1 in /home/dev/.local/lib/python3.8/site-packages (from ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (6.0.1)
    Requirement already satisfied: cycler>=0.10 in /home/dev/.local/lib/python3.8/site-packages (from matplotlib>=2.1; extra == "visualization"->qiskit[visualization]) (0.10.0)
    Requirement already satisfied: pyparsing>=2.2.1 in /home/dev/.local/lib/python3.8/site-packages (from matplotlib>=2.1; extra == "visualization"->qiskit[visualization]) (2.4.7)
    Requirement already satisfied: kiwisolver>=1.0.1 in /home/dev/.local/lib/python3.8/site-packages (from matplotlib>=2.1; extra == "visualization"->qiskit[visualization]) (1.3.1)
    Requirement already satisfied: mpmath>=0.19 in /home/dev/.local/lib/python3.8/site-packages (from sympy>=1.3->qiskit-terra==0.18.0->qiskit[visualization]) (1.2.1)
    Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.8.0->qiskit-terra==0.18.0->qiskit[visualization]) (1.14.0)
    Requirement already satisfied: pyrsistent>=0.14.0 in /home/dev/.local/lib/python3.8/site-packages (from jsonschema>=2.6->qiskit-terra==0.18.0->qiskit[visualization]) (0.18.0)
    Requirement already satisfied: attrs>=17.4.0 in /home/dev/.local/lib/python3.8/site-packages (from jsonschema>=2.6->qiskit-terra==0.18.0->qiskit[visualization]) (21.2.0)
    Requirement already satisfied: ntlm-auth>=1.0.2 in /home/dev/.local/lib/python3.8/site-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (1.5.0)
    Requirement already satisfied: cryptography>=1.3 in /usr/lib/python3/dist-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.15.0->qiskit[visualization]) (2.8)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /home/dev/.local/lib/python3.8/site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.4->qiskit[visualization]) (2.2.0)
    Requirement already satisfied: joblib>=0.11 in /home/dev/.local/lib/python3.8/site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.4->qiskit[visualization]) (1.0.1)
    Requirement already satisfied: lxml>=4.5.1 in /home/dev/.local/lib/python3.8/site-packages (from yfinance<0.1.63->qiskit-aqua==0.9.4->qiskit[visualization]) (4.6.3)
    Requirement already satisfied: multitasking>=0.0.7 in /home/dev/.local/lib/python3.8/site-packages (from yfinance<0.1.63->qiskit-aqua==0.9.4->qiskit[visualization]) (0.0.9)
    Requirement already satisfied: pytz>=2017.3 in /usr/lib/python3/dist-packages (from pandas->qiskit-aqua==0.9.4->qiskit[visualization]) (2019.3)
    Requirement already satisfied: more-itertools in /home/dev/.local/lib/python3.8/site-packages (from quandl->qiskit-aqua==0.9.4->qiskit[visualization]) (8.8.0)
    Requirement already satisfied: inflection>=0.3.1 in /home/dev/.local/lib/python3.8/site-packages (from quandl->qiskit-aqua==0.9.4->qiskit[visualization]) (0.5.1)
    Requirement already satisfied: pickleshare in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.7.5)
    Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (3.0.19)
    Requirement already satisfied: backcall in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.2.0)
    Requirement already satisfied: jedi>=0.16 in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.18.0)
    Requirement already satisfied: decorator in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (4.4.2)
    Requirement already satisfied: matplotlib-inline in /home/dev/.local/lib/python3.8/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.1.2)
    Requirement already satisfied: pexpect>4.3; sys_platform != "win32" in /usr/lib/python3/dist-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (4.6.0)
    Requirement already satisfied: ipython-genutils in /home/dev/.local/lib/python3.8/site-packages (from traitlets>=4.3.1->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.2.0)
    Requirement already satisfied: jupyter-core in /home/dev/.local/lib/python3.8/site-packages (from nbformat>=4.2.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (4.7.1)
    Requirement already satisfied: notebook>=4.4.1 in /home/dev/.local/lib/python3.8/site-packages (from widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (6.4.0)
    Requirement already satisfied: tornado>=4.2 in /home/dev/.local/lib/python3.8/site-packages (from ipykernel>=4.5.1->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (6.1)
    Requirement already satisfied: debugpy>=1.0.0 in /home/dev/.local/lib/python3.8/site-packages (from ipykernel>=4.5.1->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.3.0)
    Requirement already satisfied: jupyter-client in /home/dev/.local/lib/python3.8/site-packages (from ipykernel>=4.5.1->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (6.1.12)
    Requirement already satisfied: wcwidth in /home/dev/.local/lib/python3.8/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.2.5)
    Requirement already satisfied: parso<0.9.0,>=0.8.0 in /home/dev/.local/lib/python3.8/site-packages (from jedi>=0.16->ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.8.2)
    Requirement already satisfied: jinja2 in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (3.0.1)
    Requirement already satisfied: nbconvert in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (6.1.0)
    Requirement already satisfied: terminado>=0.8.3 in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.10.1)
    Requirement already satisfied: prometheus-client in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.11.0)
    Requirement already satisfied: pyzmq>=17 in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (22.1.0)
    Requirement already satisfied: argon2-cffi in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (20.1.0)
    Requirement already satisfied: Send2Trash>=1.5.0 in /home/dev/.local/lib/python3.8/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.7.1)
    Requirement already satisfied: MarkupSafe>=2.0 in /home/dev/.local/lib/python3.8/site-packages (from jinja2->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (2.0.1)
    Requirement already satisfied: mistune<2,>=0.8.1 in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.8.4)
    Requirement already satisfied: entrypoints>=0.2.2 in /usr/lib/python3/dist-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.3)
    Requirement already satisfied: bleach in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (3.3.0)
    Requirement already satisfied: defusedxml in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.7.1)
    Requirement already satisfied: jupyterlab-pygments in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.1.2)
    Requirement already satisfied: nbclient<0.6.0,>=0.5.0 in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.5.3)
    Requirement already satisfied: testpath in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.5.0)
    Requirement already satisfied: pandocfilters>=1.4.1 in /home/dev/.local/lib/python3.8/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.4.3)
    Requirement already satisfied: ptyprocess; os_name != "nt" in /home/dev/.local/lib/python3.8/site-packages (from terminado>=0.8.3->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.7.0)
    Requirement already satisfied: cffi>=1.0.0 in /home/dev/.local/lib/python3.8/site-packages (from argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.14.6)
    Requirement already satisfied: packaging in /home/dev/.local/lib/python3.8/site-packages (from bleach->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (21.0)
    Requirement already satisfied: webencodings in /home/dev/.local/lib/python3.8/site-packages (from bleach->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (0.5.1)
    Requirement already satisfied: async-generator in /home/dev/.local/lib/python3.8/site-packages (from nbclient<0.6.0,>=0.5.0->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.10)
    Requirement already satisfied: nest-asyncio in /home/dev/.local/lib/python3.8/site-packages (from nbclient<0.6.0,>=0.5.0->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (1.5.1)
    Requirement already satisfied: pycparser in /home/dev/.local/lib/python3.8/site-packages (from cffi>=1.0.0->argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.3.0; extra == "visualization"->qiskit[visualization]) (2.20)

__*Restart the kernel*__ (check "Kernel" menu) to apply the changes to the current notebook.

You may also visit the following links for further information.

- https://github.com/Qiskit/qiskit-tutorials/blob/master/INSTALL.md

- https://pypi.org/project/qiskit/0.15.0/

__*Restart the kernel*__ (check "Kernel" menu) to apply the changes to the current notebook.

### Tips

_Any terminal/shell command can be executed in the notebook cells by putting exclamation mark (!) to the beginning of the command._

_$\rightarrow$ For updating Qiskit version, execute the following command on a code cell_

    !pip install -U qiskit --user

_$\rightarrow$ For uninstall Qiskit, execute the following command on a code cell_

    !pip uninstall qiskit

```python
!pip install -U qiskit --user
#!pip uninstall qiskit
```

    Requirement already up-to-date: qiskit in /home/dev/.local/lib/python3.8/site-packages (0.28.0)
    Requirement already satisfied, skipping upgrade: qiskit-ignis==0.6.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit) (0.6.0)
    Requirement already satisfied, skipping upgrade: qiskit-ibmq-provider==0.15.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit) (0.15.0)
    Requirement already satisfied, skipping upgrade: qiskit-aqua==0.9.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit) (0.9.4)
    Requirement already satisfied, skipping upgrade: qiskit-terra==0.18.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit) (0.18.0)
    Requirement already satisfied, skipping upgrade: qiskit-aer==0.8.2 in /home/dev/.local/lib/python3.8/site-packages (from qiskit) (0.8.2)
    Requirement already satisfied, skipping upgrade: scipy!=0.19.1,>=0.19 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit) (1.7.0)
    Requirement already satisfied, skipping upgrade: numpy>=1.13 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit) (1.21.0)
    Requirement already satisfied, skipping upgrade: retworkx>=0.8.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ignis==0.6.0->qiskit) (0.9.0)
    Requirement already satisfied, skipping upgrade: setuptools>=40.1.0 in /usr/lib/python3/dist-packages (from qiskit-ignis==0.6.0->qiskit) (45.2.0)
    Requirement already satisfied, skipping upgrade: websocket-client>=1.0.1 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ibmq-provider==0.15.0->qiskit) (1.1.0)
    Requirement already satisfied, skipping upgrade: requests-ntlm>=1.1.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ibmq-provider==0.15.0->qiskit) (1.1.0)
    Requirement already satisfied, skipping upgrade: python-dateutil>=2.8.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-ibmq-provider==0.15.0->qiskit) (2.8.2)
    Requirement already satisfied, skipping upgrade: requests>=2.19 in /usr/lib/python3/dist-packages (from qiskit-ibmq-provider==0.15.0->qiskit) (2.22.0)
    Requirement already satisfied, skipping upgrade: urllib3>=1.21.1 in /usr/lib/python3/dist-packages (from qiskit-ibmq-provider==0.15.0->qiskit) (1.25.8)
    Requirement already satisfied, skipping upgrade: pandas in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (1.3.0)
    Requirement already satisfied, skipping upgrade: quandl in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (3.6.1)
    Requirement already satisfied, skipping upgrade: docplex>=2.21.207 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (2.21.207)
    Requirement already satisfied, skipping upgrade: yfinance<0.1.63 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (0.1.62)
    Requirement already satisfied, skipping upgrade: dlx<=1.0.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (1.0.4)
    Requirement already satisfied, skipping upgrade: h5py<3.3.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (3.2.1)
    Requirement already satisfied, skipping upgrade: fastdtw<=0.3.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (0.3.4)
    Requirement already satisfied, skipping upgrade: psutil>=5 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (5.8.0)
    Requirement already satisfied, skipping upgrade: sympy>=1.3 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (1.8)
    Requirement already satisfied, skipping upgrade: scikit-learn>=0.20.0 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aqua==0.9.4->qiskit) (0.24.2)
    Requirement already satisfied, skipping upgrade: tweedledum<2.0,>=1.1 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (1.1.0)
    Requirement already satisfied, skipping upgrade: ply>=3.10 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (3.11)
    Requirement already satisfied, skipping upgrade: jsonschema>=2.6 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (3.2.0)
    Requirement already satisfied, skipping upgrade: python-constraint>=1.4 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (1.4.0)
    Requirement already satisfied, skipping upgrade: fastjsonschema>=2.10 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (2.15.1)
    Requirement already satisfied, skipping upgrade: symengine>0.7; platform_machine == "x86_64" or platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "amd64" or platform_machine == "arm64" in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (0.7.2)
    Requirement already satisfied, skipping upgrade: dill>=0.3 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-terra==0.18.0->qiskit) (0.3.4)
    Requirement already satisfied, skipping upgrade: pybind11>=2.6 in /home/dev/.local/lib/python3.8/site-packages (from qiskit-aer==0.8.2->qiskit) (2.7.0)
    Requirement already satisfied, skipping upgrade: cryptography>=1.3 in /usr/lib/python3/dist-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.15.0->qiskit) (2.8)
    Requirement already satisfied, skipping upgrade: ntlm-auth>=1.0.2 in /home/dev/.local/lib/python3.8/site-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.15.0->qiskit) (1.5.0)
    Requirement already satisfied, skipping upgrade: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.8.0->qiskit-ibmq-provider==0.15.0->qiskit) (1.14.0)
    Requirement already satisfied, skipping upgrade: pytz>=2017.3 in /usr/lib/python3/dist-packages (from pandas->qiskit-aqua==0.9.4->qiskit) (2019.3)
    Requirement already satisfied, skipping upgrade: more-itertools in /home/dev/.local/lib/python3.8/site-packages (from quandl->qiskit-aqua==0.9.4->qiskit) (8.8.0)
    Requirement already satisfied, skipping upgrade: inflection>=0.3.1 in /home/dev/.local/lib/python3.8/site-packages (from quandl->qiskit-aqua==0.9.4->qiskit) (0.5.1)
    Requirement already satisfied, skipping upgrade: lxml>=4.5.1 in /home/dev/.local/lib/python3.8/site-packages (from yfinance<0.1.63->qiskit-aqua==0.9.4->qiskit) (4.6.3)
    Requirement already satisfied, skipping upgrade: multitasking>=0.0.7 in /home/dev/.local/lib/python3.8/site-packages (from yfinance<0.1.63->qiskit-aqua==0.9.4->qiskit) (0.0.9)
    Requirement already satisfied, skipping upgrade: mpmath>=0.19 in /home/dev/.local/lib/python3.8/site-packages (from sympy>=1.3->qiskit-aqua==0.9.4->qiskit) (1.2.1)
    Requirement already satisfied, skipping upgrade: joblib>=0.11 in /home/dev/.local/lib/python3.8/site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.4->qiskit) (1.0.1)
    Requirement already satisfied, skipping upgrade: threadpoolctl>=2.0.0 in /home/dev/.local/lib/python3.8/site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.4->qiskit) (2.2.0)
    Requirement already satisfied, skipping upgrade: attrs>=17.4.0 in /home/dev/.local/lib/python3.8/site-packages (from jsonschema>=2.6->qiskit-terra==0.18.0->qiskit) (21.2.0)
    Requirement already satisfied, skipping upgrade: pyrsistent>=0.14.0 in /home/dev/.local/lib/python3.8/site-packages (from jsonschema>=2.6->qiskit-terra==0.18.0->qiskit) (0.18.0)

## Install QuTiP

Type

    !pip install qutip

directly inside the cell of a Jupyter notebook.

For a workaround without pip you may execute the following commands in the Anaconda terminal:

conda create -n qutip-env

conda config --append channels conda-forge

conda install qutip

```python
!pip install qutip
```

    Requirement already satisfied: qutip in /home/dev/.local/lib/python3.8/site-packages (4.6.2)
    Requirement already satisfied: scipy>=1.0 in /home/dev/.local/lib/python3.8/site-packages (from qutip) (1.7.0)
    Requirement already satisfied: packaging in /home/dev/.local/lib/python3.8/site-packages (from qutip) (21.0)
    Requirement already satisfied: numpy>=1.16.6 in /home/dev/.local/lib/python3.8/site-packages (from qutip) (1.21.0)
    Requirement already satisfied: pyparsing>=2.0.2 in /home/dev/.local/lib/python3.8/site-packages (from packaging->qutip) (2.4.7)

## Check Qiskit installation

```python
import qiskit
versions = qiskit.__qiskit_version__
print("The version of Qiskit is",versions['qiskit'])
print()
print("The version of each component:")
for key in versions:
    print(key,"->",versions[key])
```

    The version of Qiskit is 0.28.0

    The version of each component:
    qiskit-terra -> 0.18.0
    qiskit-aer -> 0.8.2
    qiskit-ignis -> 0.6.0
    qiskit-ibmq-provider -> 0.15.0
    qiskit-aqua -> 0.9.4
    qiskit -> 0.28.0
    qiskit-nature -> None
    qiskit-finance -> None
    qiskit-optimization -> None
    qiskit-machine-learning -> None

    /home/dev/.local/lib/python3.8/site-packages/qiskit/aqua/__init__.py:86: DeprecationWarning: The package qiskit.aqua is deprecated. It was moved/refactored to qiskit-terra For more information see <https://github.com/Qiskit/qiskit-aqua/blob/main/README.md#migration-guide>
      warn_package('aqua', 'qiskit-terra')

## Execute an example program

1) Create a quantum circuit

```python
# import the objects from qiskit
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, execute, Aer
from random import randrange

# create a quantum circuit and its register objects
qreg = QuantumRegister(2) # quantum register with two quantum bits
creg = ClassicalRegister(2) # classical register with two classical bits
circuit = QuantumCircuit(qreg,creg) # quantum circuit composed by a quantum register and a classical register

# apply a Hadamard gate to the first qubit
circuit.h(qreg[0])

# set the second qubit to state |1>
circuit.x(qreg[1])

# apply CNOT(first_qubit,second_qubit)
circuit.cx(qreg[0],qreg[1])

# measure the both qubits
circuit.measure(qreg,creg)

print("The execution of the cell was completed, and the circuit was created :)")
```

    The execution of the cell was completed, and the circuit was created :)

2) Draw the circuit

_Run the cell once more if the figure is not shown_

```python
# draw circuit
circuit.draw(output='mpl')

# the output will be a "matplotlib.Figure" object
```

3) Execute the circuit 1024 times in the local simulator and print the observed the outcomes

```python
## execute the circuit 1024 times
job = execute(circuit,Aer.get_backend('qasm_simulator'),shots=1024)
# get the result
counts = job.result().get_counts(circuit)
print(counts)
```

    {'10': 510, '01': 514}

4) Check QuTiP - draw a Bloch sphere

```python
from qiskit.visualization import plot_bloch_vector, bloch
from matplotlib.pyplot import text
from math import pi, cos, sin
from qutip import *

theta = pi/2
phi = 4*pi/3

x = sin(theta)*cos(phi)
y = sin(theta)*sin(phi)
z = cos(theta)

sphere = bloch
sphere.Bloch
b = Bloch()
b.ylpos = [1.1, -1.2]
b.xlabel = ['$\\left|0\\right>+\\left|1\\right>$', '$\\left|0\\right>-\\left|1\\right>$']
b.ylabel = ['$\\left|0\\right>+i\\left|1\\right>$', '$\\left|0\\right>-i\\left|1\\right>$']
b.vector_color = ['b','b','b','b','b','b','r']
b.add_vectors([[0,0,1],[0,0,-1],[0,1,0],[0,-1,0],[1,0,0],[-1,0,0]])
b.add_vectors([x,y,z])
b.show()

# re-execute this cell if you DO NOT see the Bloch sphere
```

## Successfully Installed

```python

```