```python
%matplotlib inline
```

```python
# Write your imports here
import sympy
import math
import numpy as np
import matplotlib.pyplot as plt
```

### 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 (stronger) 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 through 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 just a test title
## With a subtitle
### And an even smaller subtitle

For more playing arround, please check out the attached notebook

### 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.

Write your formulas here.

Equation of a line: $$ y = ax + b $$

Roots of the quadratic equation $ax^{2}+bx+c=0$: $$ x_{1,2}=\frac{-b\pm\sqrt[2]{b^{2}-4ac}}{2a} $$

Taylor series expansion: $$f(x)\mid_{x=a}=f(a)+f'(a)(x-a)+\frac{f''(a)}{2!}(x-a)^{2}+...+\frac{f^{n}(a)}{n!}(x-a)^{n}+...$$

Binomial theorem: $$ (x+y)^{n}=\biggl({n \atop 0}\biggr)x^{n}y^{0}+\biggl({n \atop 1}\biggr)x^{n-1}y^{1}+...+\biggl({n \atop n}\biggr)x^{0}y^{n}=\sum^{n}_{k=0}\biggl({n \atop k}\biggr)x^{n-k}y^{k} $$

An integral (this one is a lot of fun to solve :D): $$ \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: $$\begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1n}\\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\a_{m1} & a_{m2} & \cdots & 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)
```

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

```python
# Write your code here
x = sympy.symbols('x')
a, b, c = sympy.symbols('a b c')
sympy.solve(a * x**2 + b * x + c)
```

    [{a: (-b*x - c)/x**2}]

```python
# Write your code here
sympy.init_printing()
sympy.solve(a * x**2 + b * x + c, x)
```

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 parameter:
```python
sympy.solve(a * x**2 + b * x + c, x)
```

Finally, if we start with `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. **Note:** This means we have to add the line BEFORE we start working with `sympy`.

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 format_decimal(func):
    """
    Decorator to convert the sympy output to Python float format
    """
    def inner(*args, **kwargs):
        raw_res = func(*args, **kwargs)
        if isinstance(raw_res, list) and len(raw_res) > 0:
            retval = []
            for el in raw_res:
                retval.append(float(el))
            return retval
        return raw_res
    return inner

@format_decimal
def solve_quadratic_equation(a, b, c):
    """
    Returns the real solutions of the quadratic equation ax^2 + bx + c = 0
    """
    # Delete the "pass" statement below and write your code
    def sqrt_part():
        return b**2 - 4 * a * c

    if a == 0:
        return sympy.solve(b * x + c, x)
    if sqrt_part() < 0:
        return []
    return sympy.solve(a * x**2 + b * x + c, 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)) # []
```

    [-1.0, 2.0]
    [4.0]
    []

**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).

```python
# Bonus: Calling the function with a = 0 for a linear equation
print(solve_quadratic_equation(0, -1, -2)) # [-2.0]
print(solve_quadratic_equation(0, -8, 16)) # [2.0]
print(solve_quadratic_equation(0, 1, 1)) # [-1.0]
```

    [-2.0]
    [2.0]
    [-1.0]

### 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? We 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
# Write your code here
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 zero. 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
# Copy and edit your code here
plt.clf()
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)
xticks = ax.xaxis.get_major_ticks()
yticks = ax.yaxis.get_major_ticks()

plt.plot(x, y)

# Let's try to remove the double 0 at the origin.
xloc, labels = plt.xticks()
yloc, labels = plt.yticks()
x_zero_loc = np.where(xloc==0)[0][0]
y_zero_loc = np.where(yloc==0)[0][0]

xticks[x_zero_loc].set_visible(False)
yticks[y_zero_loc].set_visible(False)

plt.show()
```

### * 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 :).

Write your result here.

We start by taking the ln of both sides: $ \ln(y) = \ln(a) + bx\ln(e) $

The resulting linear function is: $ \ln(y) = \ln(a) + bx $

Where **ln(y)** is the dependent that we plot on the y-axis, **ln(a)** is the constant and **b** is the slope. This equation is commontly presented as $y = mx + b$

### * 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 remove_zero_tick(loc, axticks):
    """
    This function will be used in this book to remove the 0 at origin
    """
    try:
        zero_loc = np.where(loc==0)[0][0]
        axticks[zero_loc].set_visible(False)
    except IndexError:
        ax.spines["bottom"].set_position(('data', 1))

def plot_math_function(f, min_x, max_x, num_points):
    x_array = np.linspace(min_x, max_x, num_points)
    f_vectorized = np.vectorize(f)
    y = f_vectorized(x_array)

    plt.clf()
    plt.cla()
    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)
    xticks = ax.xaxis.get_major_ticks()
    yticks = ax.yaxis.get_major_ticks()

    plt.plot(x_array, y)

    xloc, labels = plt.xticks()
    yloc, labels = plt.yticks()
    remove_zero_tick(xloc, xticks)
    remove_zero_tick(yloc, yticks)

    plt.show()

```

```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_array = np.linspace(min_x, max_x, num_points)
    if hasattr(functions, "__iter__"):
        vectorized_fs = [np.vectorize(func) for func in functions]
        ys = [vectorized_f(x_array) for vectorized_f in vectorized_fs]
    else:
        ys = [np.vectorize(functions)(x_array)]

    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)
    xticks = ax.xaxis.get_major_ticks()
    yticks = ax.yaxis.get_major_ticks()

    for y in ys:
        plt.plot(x_array, y)

    xloc, labels = plt.xticks()
    yloc, labels = plt.yticks()
    remove_zero_tick(xloc, xticks)
    remove_zero_tick(yloc, yticks)

    plt.show()
```

```python
# plot_math_functions(4, -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 then again, how do we define the trigonometric functions? 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
# Write your code here
plot_math_functions([lambda x: np.arcsin(x), lambda x: np.arccos(x)], -1, 1, 1000)
plot_math_functions(lambda x: np.arctan(x), -1, 1, 1000)

plot_math_functions(lambda x: np.arctan(1 / x), -1, 1, 1000)
plot_math_functions(lambda x: (3.14 / 2) - np.arctan(x), -1, 1, 1000)
```

### ** 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.

Math/High School Math/High-School Maths Exercise.ipynb


```python
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

__PULL__ the changes you made at home to your local copy on the M drive.

If you need a reminder of how to do this:

Open Jupyter notebook:

 Start >> Programs >> Programming >> Anaconda3 >> JupyterNotebook

(Start >> すべてのプログラム >> Programming >> Anaconda3 >> JupyterNotebook)

Navigate to where your interactive textbook is stored.

Open __S1_Introduction_to_Version_Control__.

We will start by learning to __PULL__ the solutions to the Review Excercises an online repository.

This will allow you to check your answers.

Open Jupyter notebook:

 Start >> Programs >> Programming >> Anaconda3 >> JupyterNotebook

(Start >> すべてのプログラム >> Programming >> Anaconda3 >> JupyterNotebook)

Navigate to where your interactive textbook is stored.

Open __S1_Introduction_to_Version_Control__.

Open Jupyter notebook:

 Start >> Programs >> Programming >> Anaconda3 >> JupyterNotebook

(Start >> すべてのプログラム >> Programming >> Anaconda3 >> JupyterNotebook)

We will start by learning how to add the __solutions to the Review Exercises__ to your interative textbook.

Navigate to where your interactive textbook is stored.

Open __S1_Introduction_to_Version_Control__.

```python
In Jupyter notebook, select the tab with the contents list of the interactive textbook:

Open __Seminar 3__  by clicking on __3_Data_Structures__.
```

# Data Structures

# Lesson Goal

 - Compose simple programs to control the flow with which the operators we have studied so far are executed on:
  - single value variables.
  - data structures (holding mutiple variables)

# Objectives

 - Express collections of mulitple variables as `list`, `tuple` and dictionary (`dict`).

- Use iteratation to visit entries in a data structure

- Learn to select the right data structure for an application

Why we are studying this:

To use Python to solve more complex engineering problems you are likely to encounter involving:
 - multi-variable values (e.g. vectors)
 - large data sets (e.g. experiment results)
 - manipulating your data using logic

 (e.g. sorting and categorising answers to an operation performed on multiple data points)

 Lesson structure:
 - Learn new skills together:
  - __Demonstration__ on slides.
  - __Completing examples__ in textbooks.
  - __Feedback answers__ (verbally / whiteboards)
 - Practise alone: __Completing review excercises__.
 - Skills Review: Updating your local repository using an __upstream repository.__
 - __Summary__and __quiz__.

In the last seminar we learnt to generate a rnage of numbers for use in control flow of  a program, using the function `range()`:

```python
for j in range(20):

    if j % 4 == 0:  # Check remainer of j/4
        continue    # continue to next value of j

    print(j, "is not a multiple of 4")
```

## Data Structures

Often we want to manipulate data that is more meaningful than ranges of numbers.

These collections of variables might include:
 - the results of an experiment
 - a list of names
 - the components of a vector
 - a telephone directory with names and associated numbers.

Python has different __data structures__ that can be used to store and manipulate these values.

Like variable types (`string`, `int`,`float`...) different data structures behave in different ways.

Today we will learn to use `list`, `tuple` and dictionary (`dict`) data structures.

We will study the differences in how they behave so that you can learn to select the most suitable data structure for an application.

Programs use data structure to collect data into useful packages.

>$ r = [u, v, w] $

For example, rather than representing a vector `r` of length 3 using three seperate floats `ru`, `rv` and `rw`, we could represent
it as a __list__ of floats:

```Python
r = [u, v, w]
```

(We will learn what a __list__ is in a moment.)

If we want to store the names of students in a laboratory group, rather than representing each students using an individual string variable, we could use a list of names, e.g.:

```python
lab_group0 = ["Sarah", "John", "Joe", "Emily"]
lab_group1 = ["Roger", "Rachel", "Amer", "Caroline", "Colin"]
```

This is useful because we can perform operations on lists such as:
 - checking its length (number of students in a lab group)
 - sorting the names in the list into alphabetical order
 - making a list of lists (we call this a *nested list*):

```python
lab_groups = [lab_group0, lab_group1]
```

## Lists

A list is a sequence of data.

We call each item in the sequence an *element*.

A list is constructed using square brackets:

```python
a = [1, 2, 3]
```

A `range` can be converted to a list with the `list` function.

```python
print(list(range(10)))
```

When `range` has just one *argument* (the entry in the parentheses), it will generate a range from 0 up to but not including the specified number.

```python
print(list(range(10,20)))
```

When a range has two arguments:
 - the first value is the starting value.
 - the second value is the stoping value.
 - the stopping value is not included in the range

You can optionally include a step:

```python
print(list(range(10, 20, 2)))
```

A list can hold a mixture of types (`int`, `string`....).

```python
a = [1, 2.0, "three"]
```

An empty list is created by

```python
my_list = []
```

A list of length 5 with repeated values can be created by

```python
my_list = ["Hello"]*5
print(my_list)
```

We can check if an item is in a list using the function `in`:

```python
print("Hello" in my_list)
print("Goodbye" in my_list)
```

### Iterating Over Lists

Looping over each item in a list is called *iterating*.

To iterate over a list of the lab group we use a `for` loop.

Each iteration, variable `d` takes the value of the next item in the list:

```python
for d in [1, 2.0, "three"]:
    print('the value of d is:', d)
```

__Try it yourself__

In the cell provided in your textbook *iterate* over the list `[1, 2.0, "three"]`.

Each time the code loops:
1. print the value of data __cast as a string__ (Seminar 1 Data Types and Operators)
1. print the variable type to demonstrate that the variable has been cast (note that otherwise the variable appeares to remain unchanged).

```python
# Iterate over a list and cast each item as a string
```

### Manipulating Lists

There are many functions for manipulating lists.

### Finding the Length of a List

We can find the length (number of items) of a list using the function `len()`, by including the name of the list in the brackets.

In the example below, we find the length of the list `lab_group0`.

```python
lab_group0 = ["Sara", "Mari", "Quang"]

size = len(lab_group0)

print("Lab group members:", lab_group0)

print("Size of lab group:", size)

print("Check the Python object type:", type(lab_group0))
```

### Sorting Lists

To sort the list we use `sorted()`.

#### Sorting Numerically

If the list contains numerical variables, the numbers is sorted in ascending order.

```python
numbers = [7, 1, 3.0]

print(numbers)

numbers = sorted(numbers)

print(numbers)
```

__Note:__ We can sort a list with mixed numeric types (e.g. `float` and `int`).

However, we cannot sort a list with types that cannot be sorted by the same ordering rule

(e.g. `numbers = sorted([seven, 1, 3.0])` causes an error.)

```python
# numbers = sorted([seven, 1, 3.0])
```

#### Sorting Alphabetically

If the list contains strings, the list is sorted by alphabetical order.

```python
lab_group0 = ["Sara", "Mari", "Quang"]

print(lab_group0)

lab_group0 = sorted(lab_group0)

print(lab_group0)
```

As with `len()` we include the name of the list we want to sort in the brackets.

There is a shortcut for sorting a list

`sort` is known as a 'method' of a `list`.

If we suffix a list with `.sort()`, it performs an *in-place* sort.

```python
lab_group0 = ["Sara", "Mari", "Quang"]

print(lab_group0)

#lab_group0 = sorted(lab_group0)
lab_group0.sort()

print(lab_group0)
```

__Try it yourself__

In the cell provided in your textbook create a list of __numeric__ or __string__ values.

Sort the list using `sorted()` __or__ `.sort()`.

Print the sorted list.

Print the length of the list using `len()`.

```python
# Sorting a list
```

### Removing an Item from a List

We can remove items from a list using the method `pop`.

We place the index of the element we wich to remove in brackets.

```python
# Remove the second student from the list: lab_group0
# remember indexing starts from 0
# 1 is the second element

print(lab_group0)

lab_group0.pop(1)

print(lab_group0)
```

We can add items at the end of a list using the method `append`.

We place the element we want to add to the end of the list in brackets.

```python
# Add new student "Lia" at the end of the list
lab_group0.append("Lia")
print(lab_group0)
```

__Try it yourself__

In the cell provided in your textbook.

Remove Sara from the list.

Print the new list.

Add a new lab group member, Tom, to the list.

Print the new list.

```python
# Adding and removing items from a list.
```

### Indexing

Lists store data in order.

We can select a single element of a list using its __index__.

You are familiar with this process; it is the same as selecting individual characters of a `string`:

```python
a = "string"
b = a[1]
print(b)
```

```python
first_member = lab_group0[0]
print(first_member)
```

Indices can be useful when looping through the items in a list.`

```python
# We can express the following for loop:
# ITERATING
for i in lab_group0:
    print(i)

# as:
# INDEXING
for i in range(len(lab_group0)):
    print(lab_group0[i])
```

An example of where __indexing__ is more appropraite than __iterating__:

Sometimes we want to perform an operation on all items of a list.

Consider the example we looked at earlier, where we looped through a list, expressing each element as a string.

You may have written something like this...

```python
for d in [1, 2.0, "three"]:

    d = str(d)

    print(d, type(d))

```

```python
We can re-write this:
```

```python
data = [1, 2.0, "three"]

for d in data:

    d = str(d)

    print(d, type(d))
```

```python
__Iterating:__ The type of each element in the list `data` remains unchanged.
```

```python
print(type(data[0]))
print(type(data[1]))
print(type(data[2]))
```

```python
__Indexing__: We can modify each element of the list (e.g. to change its type)
```

```python
for d in range(len(data)):

    data[d] = str(data[d])

    print(data[d], type(data[d]))

print(type(data[0]))
print(type(data[1]))
print(type(data[2]))
```

__Note:__

- Some data structures that support *iterating* but do not support *indexing* (e.g. dictionaries, which we eill learn about later).
 When possible, it is better to iterate over a list rather than use indexing.
- When indexing:
   - the first value in the range is 0.
   - the last value in the range is (list length - 1).

Lists and indexing can be useful for numerical computations.

### Indexing Example: Vectors

__Vector:__ A quantity with magnitude and direction.

Position vectors (or displacement vectors) in 3D space can always be expressed in terms of x,y, and z-directions.

The position vector 𝒓 indicates the position of a point in 3D space.

$$
\mathbf{r} = x\mathbf{i} + y\mathbf{j} + z\mathbf{k}
$$

$$
\mathbf{r} = x\mathbf{i} + y\mathbf{j} + z\mathbf{k}
$$

𝒊 is the displacement one unit in the x-direction

𝒋 is the displacement one unit in the y-direction

𝒌 is the displacement one unit in the z-direction

We can conveniently express $\mathbf{r}$ as a matrix:
$$
\mathbf{r} = [x, y, z]
$$

__...which looks a lot like a Python list!__

You will encounter 3D vectors a lot in your engineering studies as they are used to describe many physical quantities, e.g. force.

### Indexing Example: The dot product of two vectors:

The __dot product__ is a really useful algebraic operation that takes two equal-length sequences of numbers (usually coordinate vectors) and returns a single number.

It can be expressed mathematically as...

__GEOMETRIC REPRESENTATION__

\begin{align}
\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| cos(\theta)
\end{align}

     $\mathbf{B} cos(\theta)$ is the component of $B$ acting in the direction of $A$.

     $\mathbf{B} cos(\theta)$ is the component of $B$ acting in the direction of $A$.

For example, the component of a force, $\mathbf{F_{app}}$, acting in the direction of the velocity of an object (x direction):

$$
\mathbf{F_{app,x}} = \mathbf{F_{app}}cos(\theta)
$$

__ALGEBRAIC REPRESENTATION__

>The dot product of two $n$-length-vectors:
>
 $ \mathbf{A} = [A_1, A_2, ... A_n]$
>
 $ \mathbf{B} = [B_1, B_2, ... B_n]$
>
 is:

\begin{align}
\mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^n A_i B_i.
\end{align}

>So the dot product of two 3D vectors:
>
 $ \mathbf{A} = [A_x, A_y, A_z]$
>
 $ \mathbf{B} = [B_x, B_y, B_z]$
>
 is:

\begin{align}
\mathbf{A} \cdot \mathbf{B} &= \sum_{i=1}^n A_i B_i \\
&= A_x B_x + A_y B_y + A_z B_z.
\end{align}

__Example:__

 The dot product $\mathbf{A} \cdot \mathbf{B}$:
>
 $ \mathbf{A} = [1, 3, −5]$
>
 $ \mathbf{B} = [4, −2, −1]$

\begin{align}
      {\displaystyle {\begin{aligned}\ [1,3,-5]\cdot [4,-2,-1]&=(1)(4)+(3)(-2)+(-5)(-1)\\&=4-6+5\\&=3\end{aligned}}}
\end{align}

We can solve this very easily using a Python `for` loop.

```python
A = [1.0, 3.0, -5.0]
B = [4.0, -2.0, -1.0]

# Create a variable called dot_product with value, 0.
dot_product = 0.0

for i in range(len(A)):
    dot_product += A[i]*B[i]

print(dot_product)
```

From is __GEOMETRIC__ representation, we can see that the dot product allows us to quickly solve many engineering-related problems...

\begin{align}
\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| cos(\theta)
\end{align}

Examples:
 - Test if two vectors are:
   - perpendicular ($\mathbf{A} \cdot \mathbf{B}==0$)
   - acute ($\mathbf{A} \cdot \mathbf{B}>0$)
   - obtuse ($\mathbf{A} \cdot \mathbf{B}<0$)
 - Find the angle between two vectors (from its cosine).
 - Find the magnitude of one vector in the direction of another.
 - Find physical quantities e.g. the work, W, when pushing an object a certain distance, d, with force, F:

__Try it yourself:__

$\mathbf{C} = [2, 4, 3.5]$

$\mathbf{D} = [1, 2, -6]$

In the cell below find the dot product:
$\mathbf{C} \cdot \mathbf{D}$

Is the angle between the vectors obtuse or acute or are the vectors perpendicular?

(Perpendicular if $\mathbf{A} \cdot \mathbf{B}==0$, acute if $\mathbf{A} \cdot \mathbf{B}>0$, or obtuse if $\mathbf{A} \cdot \mathbf{B}<0$).

```python
# The dot product of C and D
```

### Nested Data Structures: Lists of Lists

A *nested list* is a list within a list. (Recall *nested loops* from Seminar 1: Control Flow).

To access a __single element__ we need as many indices as there are levels of nested list.

This is more easily explained with an example:

```python
lab_group0 = ["Sara", "Mika", "Ryo", "Am"]
lab_group1 = ["Hemma", "Miri", "Qui", "Sajid"]
lab_group2 = ["Adam", "Yukari", "Farad", "Fumitoshi"]

lab_groups = [lab_group0, lab_group1]
```

`lab_group0`, `lab_group1` and `lab_group2` are nested within `lab_groups`.

There are __two__ levels of nested lists.

We need __two__ indices to select a single elememt from `lab_group0`, `lab_group1` or `lab_group2`.

The first index: a list (`lab_group0`, `lab_group1` or `lab_group2`).

The second index: an element in that list.

```python
group = lab_groups[0]
print(group)

name = lab_groups[1][2]
print(name)
```

## Tuples

Tuples are similar to lists.

However, after creatig a tuple:
 - you cannot add or remove elements from it without creating a new tuple.
 - you cannot change the value of a single tuple element e.g. by indexing.

Tuples are therefore used for values that should not change after being created.

 e.g. a vector of length three with fixed entries

It is 'safer' in this case since it cannot be modified accidentally in a program.

To create a tuple, use () parentheses.

__Example__
In Kyoto University, each professor is assigned an office.

Philamore-sensei is given room 32:

```python
room = ("Philamore", 32)

print("Room allocation:", room)

print("Length of entry:", len(room))

print(type(room))
```

### Iterating over Tuples

We can *iterate* over tuples in the same way as with lists,

```python
# Iterate over tuple values
for d in room:
    print(d)
```

###  Indexing

We can index into a tuple:

```python
# Index into tuple values
print(room[1])
print(room[0])
```

__Note__ Take care when creating a tuple of length 1:

```python
# Creating a list of length 1
a = [1]
print(a)
print(type(a))
print(len(a))
```

However, if we use the same process for a tuple:

```python
a = (1)
print(a)
print(type(a))
#print(len(a))
```

To create a tuple of length 1, we use a comma:

```python
a = (1,)
print(a)
print(type(a))
print(len(a))
```

```python
room = ("Endo",)
print("Room allocation:", room))
print("Length of entry:", len(room))
print(type(room))
```

### Nested Data Structures: Lists of Tuples
As part of a rooms database, we can create a list of tuples:

```python
room_allocation = [("Endo",),
                   ("Philamore", 32),
                   ("Matsuno", 31),
                   ("Sawaragi", 28),
                   ("Okino", 28),
                   ("Kumegawa", 19)]

print(room_allocation)
```

Index into the list room allocation

Refer to Link to the destination' for how to index into *nested* data structures.

In the cell below use indexing to print:
 - Matsuno-sensei's room number
 - Kumegawa-sensei's room number
 - The variable type of Kumegawa-sensei's room number

```python
# Matsuno-sensei's room number

# Kumegawa-sensei's room number

# The Python variable type of Kumegawa-sensei's room number

```

### Sorting Tuples
To make it easier to look up the office number each professor, we can __sort__ the list of tuples into an office directory.

The ordering rule is determined by the __first element__ of each tuple.

If the first element of each tuple is a numeric type (`int`, `float`...) the tulpes are sorted by ascending numerical order of the first element:

If the first element of each tuple is a `string` (as in this case), the tuples are sorted by alphabetical order of the first element.

A tuple is sorted using the same method to sort a list.

Refer to Sorting Lists remind yourself of this method.

In the cell provided below, sort the list, `room_allocation` by alphabetical order.

```python
# room_allocation sorted by alphabetical order
```

The office directory can be improved by excluding professors who do not have an office at Yoshida campus:

```python
for entry in room_allocation:

    # only professors with an office have an entry length > 1
    if len(entry) > 1:
        print("Name:", entry[0], ", Room:", entry[1])
```

In summary, use tuples over lists when the length will not change.

## Dictionaries

We used a list of tuples in the previous section to store room allocations.

What if we wanted to use a program to find which room a particular professor has been allocated?

we would need to either:
- iterate through the list and check each name.

> For a very large list, this might not be very efficient.

- use the index to select a specific entry of a list or tuple.

> This works if we know the index to the entry of interest. For a very large list, this is unlikely.

A human looking would identify individuals in an office directory by name (or "keyword") rather than a continuous set of integers.

Using a Python __dictionary__ we can build a 'map' from names (*keys*) to room numbers (*values*).

A Python dictionary (`dict`) is declared using curly braces:

```python
room_allocation = {"Endo": None,
                   "Philamore": 32,
                   "Matsuno": 31,
                   "Sawaragi": 28,
                   "Okino": 28,
                   "Kumegawa": 19}

print(room_allocation)

print(type(room_allocation))
```

Each entry is separated by a comma.

For each entry we have:
 - a 'key' (followed by a colon)
 - a 'value'.

__Note:__ For empty values (e.g. `Endo` in the example above) we use '`None`' for the value.

`None` is a Python keyword for 'nothing' or 'empty'.

Now if we want to know which office belongs to Philamore-sensei, we can query the dictionary by key:

```python
philamore_office = room_allocation["Philamore"]
print(philamore_office)
```

### Iterating over Dictionaries

We can __*iterate*__ over the keys in a dictionary as we iterated over the elements of a list or tuple:

__Try it yourself:__

Refer back to:
 - Iterating Over Lists
 - Iterating Over Tuples
to remind yourself how to *iterate* over a data structure.

Using __exactly the same method__, iterate over the entries in the dictionary `room allocation` using a `for` loop.

Each time the code loops, print the next dictionary entry.

```python
# iterate over the dictionary, room_allocation.
# print each entry

```

Notice that this only prints the keys.

We can access `keys` and `values` seperately by:
 - creating two variable names before `in`
 - putting `items()` after the dictionary name

```python
for name, room_number in room_allocation.items():
    print(name, room_number)
```

__Try it yourself__

Copy and paste the code from the previous cell.

Edit it so that it prints the room numbers only.

Remember you can __"comment out"__ the existing code (instead of deleting it) so that you can refer to it later.
e.g.
```python
#print(name, room_number)
```

```python
# iterate over the dictionary, room_allocation.
# print each name
```

Note that the order of the printed entries in the dictionary is different from the input order.

A dictionary stores data differently from a list or tuple.

### Look-up Keys

Lists and tuples store entries as continuous pieces of memory, which is why we can access entries by index.

Indexing cannot be used to access the entries of a dictionary. For example:
```python
print(room_allocation[0])
```
raises an error.

Dictionaries use a different type of storage which allows us to perform look-ups using a 'key'.

print(room_allocation["Philamore"])

### Adding Entries to a Dictionary

We use this same code to add new entries to an existing dictionary:

```python
print(room_allocation)

room_allocation["Fujiwara"]= 34

print("")

print(room_allocation)

```

### Removing Entries from a Dictionary

To remove an item from a disctionary we use the command `del`.

```python
print(room_allocation)

del room_allocation["Fujiwara"]

print("")

print(room_allocation)
```

__Try it yourself__

Okino-sensei is leaving Kyoto University.

Her office will be re-allocated to a new member of staff, Ito-sensei.

In the cell below, update the dictionary by deleting the entry for Okino-sensei and creating a new entry for Ito-sensei.

Print the new list.

```python
# Remove Okino-sensei (room 28) from the dictionary.
# Add a new entry for Ito-sensei (room 28)
```

So far we have used a string variable types for the dictionary keys.

However, we can use almost any variable type as a key and we can mix types.

### Re-structuring to make a new Dictionary
__Example__: We could 'invert' the room allocation dictionary to create a room-to-name map.

Let's build a new dictionary (`room_map`) by looping through the old dictionary (`room_allocation`) using a `for` loop:

```python
# Create empty dictionary
room_map = {}

# Build dictionary to map 'room number' -> name
for name, room_number in room_allocation.items():

    # Insert entry into new dictionary
    room_map[room_number] = name

print(room_map)
```

We can now consult the room-to-name map to find out if a particular room is occupied and by whom.

Let's assume some rooms are unoccupied and therefore do not exist in this dictionary.

If we try to use a key that does not exist in the dictionary, e.g.

   occupant17 = room_map[17]

Python will give an error (raise an exception).

If we're not sure that a __key__ is present (that a room is occupied or unocupied in this case), we can check using the funstion in '`in`'

(we used this function to check wether an entry exists in a __list__)

```python
print(19 in room_map)
print(17 in room_map)
```

So we know that:
 - room 17 is unoccupied
 - room 19 is occupied

When using `in`, take care to check for the __key__ (not the value)

```python
print('Kumegawa' in room_map)
```

#### Potential application: avoid generating errors if unoccupied room numbers are entered.

For example, in a program that checks the occupants of rooms by entering the room number:

```python
rooms_to_check = [17, 19]

for room in rooms_to_check:

    if room in room_map:
        print("Room", room, "is occupied by", room_map[room], "-sensei")

    else:
        print("Room", room, "is unoccupied.")
```

## Choosing a data structure

An important task when developing a computer program is selecting the *appropriate* data structure for a task.

Here are some examples of the suitablity of the data types we have studied for some common computing tasks.

- __Dynamically changing individual elements of a data structure.__

e.g. updating the occupant of a room number or adding a name to a list of group members.

__Lists and dictionaries__ allow us to do this.

__Tuples__ do not.

- __Storing items in a perticular sequence (so that they can be addressed by index or in a particular order)__.

e.g. representing the x, y, z coordinates of a 3D position vector, storing data collected from an experiment as a time series.

__Lists and tuples__ allow us to do this.

__Dictionaries__ do not.

- __Performing an operation on every item in a sequence.__

e.g. checking every item in a data set against a particular condition (e.g. prime number, multiple of 5....etc), performing an algebraic operation on every item in a data set.

__Lists and tuples__ make this simple as we can call each entry in turn using its index.

__Dictionaries__ this is less efficient as it requires more code.

- __Selecting a single item from a data structure without knowing its position in a sequence.__
e.g. looking up the profile of a person using their name, avoiding looping through a large data set in order to identify a single entry.

__Dictionaries__ allow us to select a single entry by an associated (unique) key variable.

__Lists and tuples__ make this difficult as to pick out a single value we must either i) know it's position in an ordered sequence, ii)loop through every item until we find it.

- __Protecting individual items of a data sequence from being added, removed or changed within the program.__

e.g. representing a vector of fixed length with fixed values, representing the coordintes of a fixed point.

__Tuples__ allow us to do this.

__Lists and dictionaries__ do not.

- __Speed__
For many numerical computations, efficiency is essential. More flexible data structures are generally less efficient computationally. They require more computer memory. We will study the difference in speed there can be between different data structures in a later seminar.

## Review Exercises
Here are a series of engineering problems for you to practise each of the new Python skills that you have learnt today.

### Review Exercise:  Data structures.

__(A)__ In the cell below, what type of data structure is C?

__(B)__ Write a line of code that checks whether 3 exists within the data structure.

__(C)__ Write a line of code that checks whether 3.0 exists within the data structure.

__(D)__ Write a line of code that checks whether "3" exists within the data structure.

```python
C = (2, 3, 5, 6, 1, "hello")
```

### Review Exercise:  Using Lists with  `for` Loops.

In the cell below:

- Create a list with the names of the months.

- Create a second list with the number of days in each month (for a regular year).

- Create a `for` loop that prints:

`The number of days in MONTH is XX days`

where, `MONTH` is the name of the month and `XX` is the correct number of days in that month.

Hint: Refer to Indexing Example: The dot product of two vectors for how to use two vectors in a loop.

```python
# A for loop to print the number of days in each month
```

### Review Exercise: Indexing.

__(A)__ In the cell below write a program that adds two vectors, $\mathbf{A}$ and $\mathbf{B}$, expressed as lists

$\mathbf{A} = [-2, 1, 3]$

$\mathbf{B} = [6, 2, 2]$

 $ \mathbf{C} = [C_1,
                 C_2, ...
                 C_n] = \mathbf{A} + \mathbf{B} = [(A_1 + B_1),
                                                           (A_2 + B_2), ...
                                                           (A_n + B_n)]$

__Hints:__
- Refer to Indexing Example: The dot product of two vectors for how to use two vectors in a loop.
- Start by creating an empty list, `C = []`.

Add an element to the list each time the code loops using the method `C.append()`

Jump to Adding an Item to a List

__(B)__ To add two vectors, the number of elements in each vectors must be equal.

Use the function `len()` to print the length of $\mathbf{A}$ and the length of $\mathbf{B}$ before adding the two vectors.

Jump to Finding the Length of a List

__(C)__ Use `if` and `else` statements (Seminar 2) to:
- add the two vectors __only__ if the length of $\mathbf{A}$ and the length of $\mathbf{B}$ are equal.
- otherwise print a message  (e.g. "`unequal vector length!`")

Hint: Use a logical operator (`==`, `<`, `>`....) to compare the lengths of $\mathbf{A}$ and $\mathbf{B}$.
Refer to __Logical Operators__ (Seminar 2).

__(D)__ Check your code works by using it to try and add two vectors with:

i) the same number of elements in each vector

ii) a different number of elements in each vector

```python
# Vector addition program with length check.
```

### Review Exercise: `if` and `else` statements.

Copy and paste the program you wrote earlier to find the dot product of two vectors  into the cell below.

Within the loop use `if`, `elif` and else `else` to make the program print:
 - "`The angle between vectors is acute`" if the dot product is positive.
 - "`The angle between vectors is obtuse`" if the dot product is negative.
 - "`The vectors are perpendicular`" if the dot product is 0.

```python
# Determinig angle types using the dot product.
```

### Review Exercise: Dictionaries.

__(A)__ Choose 5 elements from the periodic table.

In the cell below create a dictionary:
Jump to Dictionaries
 - __keys:__ chemical symbol names
 - __values:__ atomic numbers

 e.g.
 ```python
 dictionary = {"C":6, "N":7, "O":8....}

 ```

__(B)__ Remove one entry from the dictionary and print the updated version.

Jump to Removing Entries from a Dictionary

__(C)__ Add a new entry (chemical symbol and atomic number) to the dictionary and print the updated version.

Jump to Adding Entries to a Dictionary

__(D)__ Use a `for` loop to create a new dictionary:
 - __keys:__ atomic numbers
 - __values:__ chemical symbols
using your original dictionary.

 Hint: Refer to the earlier example of re-structuring to make a new dictioary.

__*Optional Extension*__

__(E)__ Print a __list__ of the chemical symbols in your dictionary, sorted into alphabetical order.
Hints:
 - Create an empty list
 - Use a for loop to add each chemical symbol to the list
 - Sort the list in alphabetical order

```python
# Dictionary of periodic table items.
```

### Review Exercise: `while` loops (bisection)

Bisection is an iterative method for approximating a root of a function $y = F(x)$

i.e. a value of $x$ for which the function $F(x)$ is equal to zero.

Therefore the roots are found where the line of the function F(x) __crosses__ the x axis (the red dot indicates the root of the function):

If we know such a __crossing point__ lies within the interval x = a and x = b we can repeatedly *bisect* this interval to narrow down the interval in which x = root must lie.

Each iteration, x$_{mid} = \frac{a + b}{2}$ is determined and used to determine whether the crossing point is between x$_{mid}$ and a or x$_{mid}$ and b.

This is used to define a new, narrower interval in which we know the crossing point lies.

    x_mid = (a + b) / 2

      # If F(x) changes sign between F(x_mid) and F(a),
      # the root must lie between F(x_mid) and F(a)

      if F(x_mid) * F(a) < 0:
          b = x_mid
          x_mid = (a + b)/2

      # If F(x) changes sign between F(x_mid) and F(b),
      # the root must lie between F(x_mid) and F(b)

      else:
          a = x_mid
          x_mid = (a + b)/2

In the example shown, the midpoint (x$_{mid}$) of a$_1$  and b$_1$ is b$_2$

F(a$_1$) $\times$ F(b$_2$) = negative

F(b$_1$) $\times$ F(b$_2$) = positive

So the new increment is between a$_1$ and b$_2$.

By repeating this process, the value of F(x$_{mid}$) should become closer to zero with each iteration.

The process is repeated until the *absolute* value |F(x$_{mid}$)| is sufficiently small (below a predetermined value (*tolerance*)).

We then determine x$_{mid}$ is the root of the function.

It is a very simple and robust method.

**Task:**

$$
F(x) = 4x^3 - 3x^2 - 25x - 6
$$

The function has one root between x = 0 and x = -0.6.

__(A)__ Use the bisection method to estimate the value of the root between x = 0 and x = -0.6.

Instructions:
- Use a while loop to repeat the code above __while__ absF(x$_{mid}$) > 1 $\times10^{-6}$.
- Each time the code loops:
 - __Compute__ F(a), F(b) and F(x_mid) [Hint: Use approprate variable names that don't contain () parentheses)
 - __Print__ F(x$_{mid}$) to check absF(x$_{mid}$)  $< 1 \times10^{-6}$.
Use  the function `abs()` to compute the absolute value of a number,
https://docs.python.org/2/library/functions.html#abs
 e.g. `y = abs(x)` assigns the absolute value of `x` to `y`.
 - __Bisect__ the increment using the code shown above
- __After__ the loop print the final value of x$_{mid}$ using `print("root = ", x_mid) `.
This value is the estimate of the root.

Jump to While Loops'

__(B)__ The bisection method is only effective where F(a) and F(b) are of opposite sign.

 i.e. where F(a) $\times$ F(b) $ < 0$

Add an if statement to your code so that the while loop is only run *if* the inputs a and b are of opposite sign.

```python
# Bisection while loop
```

 __(C)__ In the previous example you stopped the while loop when the value of the function was sufficiently small (abs(F(x$_{mid}$)) $< 1 \times10^{-6}$) that we can consider the corresponding value of x to be a root of the function.

This time we are going to edit your code so that the loop is stopped when it reaches a __maximum number of iterations__.
Copy and paste your code from the cell above in the cel below.

Replace your __while loop__ with a __for loop__ that runs the code in the loop 25 times then stops.

__(D)__ Within the for loop, add a `break` statement.

The `break` statement should exit the for loop __if__ abs(F$_mid$) $< 1 \times10^{-6}$.

i.e. __if__ abs(F$_mid$) $< 1 \times10^{-6}$ the loop will stop before the maximum number of iterations is reached.

Before the command `break`, print the value of x$_{mid}$ using `print("root = ", x_mid) `.
This value is the estimate of the root.

Jump to break'

```python
# Copy and paste your code from the cell above, here
```

```python
# Program to calculate the area of a polygon.
```

# Updating your git repository

You have made several changes to your interactive textbook.

The final thing we are going to do is add these changes to your online repository so that:
 - I can check your progress
 - You can access the changes from outside of the university server.

 > Save your work.
 >
 `git add -A`
 >
`git commit -m "A short message describing changes"`
 >
`git push origin master`

Refer to supplementary material: __S1_Introduction_to_Version_Control.ipynb__.

## Summary
 - A data structure is used to assign a collection of values to a single collection name.
 - A Python list can store multiple items of data in sequentially numbered elements (numbering starts at zero)
 - Data stored in a list element can be referenced using the list name can be referenced using the list name followed by an index number in [] square brackets.
 - The `len()` function returns the length of a specified list.
 - A Python tuple whose values can not be individually changed, removed or added to (except by adding another tuple).
 - Data stored in a tuple element can be referenced using the tuple name followed by an index number in [] square brackets.
 - A Python dictionary is a list of key: value pairs of data in which each key must be unique.
 - Data stored in a dictionary element can be referenced using the dictionary name followed by its key in [] square brackets.

# Homework

1. __PULL__ the changes you made in-class today to your personal computer.
1. __COMPLETE__ any unfinished Review Exercises.
1. __PUSH__ the changes you make at home to your online repository.

Refer to supplementary material: __S1_Introduction_to_Version_Control.ipynb__.

In particular, please complete: __Review Exercise: `while` loops (bisection)__.

You will need to refer to your answer in next week's Seminar.