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


            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

```


```python
### Running in Google Colab? You'll want to uncomment and run these cell once each time you start this notebook.

"""
!wget https://raw.githubusercontent.com/psheehan/CIERA-HS-Program/master/Projects/SGRB-AfterglowModeling/t90.txt
!wget https://raw.githubusercontent.com/psheehan/CIERA-HS-Program/master/Projects/SGRB-AfterglowModeling/090510_OmuJy.txt
!wget https://raw.githubusercontent.com/psheehan/CIERA-HS-Program/master/Projects/SGRB-AfterglowModeling/130603b_OmuJy.txt
!wget https://github.com/psheehan/CIERA-HS-Program/blob/master/Projects/SGRB-AfterglowModeling/multiwave_OmuJy.txt
"""
```

# Short Gamma-Ray Burst Afterglow Fitting

```python
# Loading some packages to get us started

%load_ext autoreload
%autoreload 2
%matplotlib inline
%config InlineBackend.figure_format='retina'

from IPython.display import Image
import numpy as np
import matplotlib.pyplot as plt
import astropy.io.ascii as ascii
from astropy import cosmology
from astropy import units as u
from astropy.cosmology import FlatLambdaCDM

cosmo = FlatLambdaCDM(H0=70 * u.km / u.s / u.Mpc, Tcmb0=2.725 * u.K, Om0=0.3)
```

# Introduction

---

## Gamma-Rays

Consider the electromagnetic spectrum:

On the far right end of the spectrum, we see radio waves. Radio waves are at the lowest energy end of the spectrum. As we move across the spectrum to the left, we increase in energy and frequency. Visible light appears near the middle of the spectrum. Gamma-rays are the highest energy class of light and are only produced on Earth by high energy processes like fusion or gamma decay.

---

## Gamma-Ray Bursts

In the 1960s, astronomers started detecting bursts of gamma-ray emission. The high energy flashes did not repeat and were randomly scattered across the sky. After much work, astronomers realized that these bursts of gamma-rays were coming from other galaxies. To produce the high energy $\textbf{photons}$ (packets of light) that make up gamma-ray bursts (GRBs) a source with a lot of energy is needed! All of these clues led astronomers to believe that are the result of explosions in our universe.

GRBs are not $\textbf{isotropic}$ (spherically symmetric). In fact, much of the explosion's energy is focused along the two poles. The beam that the gamma-rays are accelerated along is referred to as the $\textbf{jet}$. The structure of the explosion means that in order to detect a GRB we must be directly in the path of the GRB.

# Exercise 1

## Short vs. Long Gamma-Ray Bursts (GRBs)

It became clear that there were two populations of GRBs - those with short durations and long durations. Below, we will plot the durations of GRBs discovered by the space telescope $\textit{Swift}$ from January 2004 to January 2020. Your job is to run the two cells below, examine the plot and choose a cutoff between short and long duration GRBs.

```python
# data file includes a list of GRBs and the duration of the burst, T90
swift_cat = ascii.read('t90.txt')

# T90 is the amount of time for 90% of the burst's light to be absorbed
t90 = swift_cat['T90']

# If you print the list of GRB names, you'll see that each GRB is named by the date
# it was detected on.
grb_name = swift_cat['GRBname']
```

```python
b=np.logspace(-1,3,40)
plt.hist(t90,b,color='xkcd:lilac')
plt.loglog()
plt.tick_params(direction = 'in', length = 10, labelsize = 16, right = True)
plt.tick_params(which = 'minor', direction = 'in', length = 6,  right = True)
plt.xlabel('Time (seconds)')
plt.ylabel('Count')
plt.title('Histogram of Burst Duration')
plt.show()
```

#### Question 1: Looking at the plot, are you convinced that there are two distinct populations of GRBs? Why?

Answer:

#### How many seconds would you choose as a cutoff between short and long GRBs? Keep in mind that the axes are in 'log' units.

Answer:

In reality, we use a few other parameters to classify GRBs as short or long. However, duration of the burst is still a useful tool to classify GRBs.

For this project, we will be focusing on short gamma-ray bursts.

## Gamma-Ray Burst Afterglows

Accompanying each GRB is a multiwavelength afterglow. Afterglows as produced by fast-moving ejecta from the explosion colliding with gas that exists in the space between stars. The relativistic (aka VERY fast) collisions between the ejecta and the gas produce what is called $\textbf{synchrotron emission}$. Though we won't go into the mechanics of synchrotron emission here, it's important to know that it creates a $\textbf{jetted multiwavelength afterglow}$. Below is a diagram of an top hat afterglow. In the far right part of the diagram, you see the explosion (jet) colliding with the gas (ambient medium) to produce an afterglow.

Studying observations of GRB afterglows can teach us about the properties of the burst, including how dense the environment around the explosion is. Below are answers to questions you may have as you simulate afterglow lightcurves.

#### 1. What does multiwavelength mean?
Multiwavelength means that light is emitted in more than one part of the electromagnetic spectrum (see the first figure!). An afterglow emits in the radio, the visible (hereafter called "optical") and the X-ray.

Identifying the multiwavelength components of the afterglow is important because our X-ray, optical and radio detectors are better at localizing sources than gamma-ray detectors. A better localization allows astronomers to study the host environment of the burst and build a better understanding of what precipitated the explosion.

#### 2. What is a lightcurve?
A lightcurve is a plot of time (x-axis) vs brightness (y-axis). Lightcurves are a ubiquitous tool for studying time-domain astronomy. A lightcurve shows us if a source dims, brightens, or stays the same over time. For a steady source like a galaxy, a lightcurve will be flat. A supernova's lightcurve will rise quickly and then fall over time. The lightcurve of a variable star (a star which brightens and dims periodically) will look like this:

Short GRB afterglow lightcurves are most like supernova lightcurves.

#### 3. What is a jet and why is it important?

A jet is a collimated source of relativistic emission. Among other things, a jet is characterized by it's opening angle and shape.

Both the shape and the opening angle of the jet determine if the afterglow will be detectable to an $\textbf{off-axis}$ (not directly in the path of the jet) observer.

#### 4. What does an afterglow jet look like?

Good question! The short answer is that astronomers don't know for sure yet but have many theories. The simulations we'll be using below allow the user to try out a few different jet shapes. These jet shapes include:

$\textbf{Top Hat Jet}$: flux concentrated equally across a narrow beam, outside of which no flux escapes.

$\textbf{Gaussian Jet}$: Similar to a top hat jet except a greater amount of flux is concentrated in the center of the beam.

$\textbf{Power Law Jet}$: Flux concentrated in the middle of the jet but decays as you move to larger viewing angles.

## Using $\texttt{afterglowpy}$

We'll be using the Python package $\texttt{afterglowpy}$ (Ryan et al. 2015, Ryan et al. 2019) to simulate multiwavelength afterglow (AG) lightcurves and match them to actual observations.

From https://github.com/geoffryan/afterglowpy/blob/master/README.md:

jetType can be:
1. -1 (Top Hat)
2. 0 (Gaussian)
3. 1 (Power Law w/ core)
4. 2 (Gaussian w/ core)
5. 3 (Cocoon)
6. 4 (Smooth Power Law).

# Exercise 2a)

The first three cells are mostly complete and will produce an optical Top Hat jet AG lightcurve plot. Using these cells as an example, produce a AG lightcurves for a Gaussian Jet and a Smooth Power Law Jet.

```python
# in order to do run the cells below, uncomment these two lines, run this cell, and then
# recomment since you only have to do it once!

!pip install afterglowpy

import afterglowpy as grb
```

```python
# Example based on Github code by Geoff Ryan (github.com/geoffryan/afterglowpy/)

# JET PARAMETERS:

thetaObs = 0.05  # Viewing angle in radians
E0 = 1.0e51     # Isotropic-equivalent energy in erg
thetaC = 0.2    # Half-opening angle in radians, range of 0.015 - 0.021 radians
thetaW = 0.1     # Truncation angle, unused for top-hat
b = 0.01           # power law index, unused for top-hat
n0 = 1.0         # circumburst density in cm^{-3}
p = 2.2          # electron energy distribution index
eps_e = 0.1      # epsilon_e
eps_B = 0.01     # epsilon_B
xi_N = 1.0       # Fraction of electrons accelerated
specType = 0 # global cooling time, no inverse compton
q = 0 # keep as 0
ts = 0 # keep as 0
z = 0.356         # redshift, keep this

dist = cosmo.luminosity_distance(z) # outputs distances in Mpc, keep this
dL = dist.value * 3.086e24 # Luminosity distance of burst in cm, keep this
```

```python
# creating a grid of times for the x-axis of our afterglow lightcurves

print("Choose time range")

ta = 2.0 * grb.day2sec
tb = 50 * grb.day2sec
tsec = np.geomspace(ta, tb, num=100)

# Calculate flux in a single optical band (all times have same frequency)
nu = np.empty(tsec.shape)
nu[:] = 4.8e14

# For convenience, place positional arguments in an array and keywords into a dict

Y = np.array([thetaObs, E0, thetaC, thetaW, b, specType, q, ts, n0, p, eps_e, eps_B, xi_N, dL])
Z = {'z': z}

# Calculate!

jetType_top = -1     # top hat jet

print("Calculate Top Hat Jet")
Fnu_top = grb.fluxDensity(tsec, nu, jetType_top, 0, *Y, **Z)

# Change time units back to days

t = tsec * grb.sec2day

# Plot!
print("Plot!")

plt.plot(t,Fnu_top)
plt.yscale('log')
plt.xlabel('Time since Burst (Days)')
plt.ylabel(r'$F_{nu}$ (mJy)')
plt.title('Example 1: Top Hat Optical Afterglow Lightcurve')
plt.show()
```

# Exercise 2b

Calculate Top Hat, Gaussian and Power Law jet afterglow lightcurves.

```python
# Set up array of relevant times
ta = 2.0 * grb.day2sec
tb = 50.0 * grb.day2sec
tsec = np.geomspace(ta, tb, num=100)

# Calculate flux in a single optical band (all times have same frequency)

nu = np.empty(tsec.shape)
nu[:] = 4.8e14 # approximate r-band frequency

# recall number that corresponds to Gaussian jet
jetType_gau =

# recall number that corresponds to Power Law jet
jetType_pl =

# Calculate the brightness of your jet!

print('Calculate Top Hat Jet')
Fnu_top = grb.fluxDensity() # fill in
print('Calculate Gaussian Jet')
Fnu_gau = grb.fluxDensity() # fill in
print('Calculate Power Law Jet')
Fnu_pl = grb.fluxDensity() # fill in

# change time back to days

t = tsec * grb.sec2day

print("Plot!")

plt.plot(t,Fnu_top, ls = ':', lw=4, label = 'Top Hat') # fill in label
plt.plot(t,Fnu_gau, label = 'Gaussian') # fill in label
plt.plot(t,Fnu_pl, ls = '-.', label = 'Power Law') # fill in label
plt.yscale('log')
plt.xlabel('Days since burst') # fill in
plt.ylabel(r'$F_{nu}$ (muJy)') # fill in
plt.title('Afterglow Models')
plt.legend(fontsize=14)
plt.show()
```

# Exercise 3a

Replot your four afterglows from Exercise 2 with the real optical afterglow dataset of the SGRB 130603B. Determine which afterglow jet model best fits the data. Explain why in the cell below.

```python
# Load in SGRB 130603B dataset

obs = np.loadtxt('130603b_OmuJy.txt')

time = obs[:,0]
filt = obs[:,2]
flux = obs[:,3]
ferr = obs[:,4]
det = obs[:,5]

# Add condition to only choose points at a certain wavelengths where something was detected

cond = (filt > 0.5) & (filt < 0.7) & (det == 1)

time=time[cond]
fil = filt[cond]
fl = flux[cond] * 1e-6 # convert micro-Jansky to Jansky to match model units
flerr = ferr[cond] * 1e-6 # convert micro-Jansky to Jansky

# Plotting the data with error bars

plt.errorbar(time,fl,yerr=flerr,capsize=3,fmt='o',color='xkcd:shocking pink')

# Re-plot your Top Hat, Gaussian, Power Law and Gaussian w/ core afterglow models

plt.plot(t, , ls = ':',lw=4, label = 'Top Hat') # fill in
plt.plot(t, , label = 'Gaussian') # fill in
plt.plot(t, , ls = '-.', label = 'Power Law') # fill in
plt.yscale('log')

plt.xlabel('Days since burst')
plt.ylabel(r'$F_{nu}$ (Jy)')
plt.legend()
plt.show()
```

#### Question: Which jet type(s) fit the optical afterglow of GRB 130603B the best?

Answer:

## Exercise 3a Challenge: Choose the model you think fits the data best and adjust the jet parameters (what goes into Y) to determine the best fit by eye.

Recommended: E0, thetaC, thetaW. Try it below and record your best fits to the data.

```python
thetaObs = 0.0  # Viewing angle in radians
E0 = 1.0e53      # Isotropic-equivalent energy in erg
thetaC = 0.1     # Half-opening angle in radians
thetaW = 0.1     # Truncation angle, unused for top-hat
b = .01            # power law index, unused for top-hat
n0 = 1.0         # circumburst density in cm^{-3}
p = 2.2          # electron energy distribution index
eps_e = 0.1      # epsilon_e
eps_B = 0.01     # epsilon_B
xi_N = 1.0       # Fraction of electrons accelerated
specType = 0 # global cooling time, no inverse compton

# repeat process to get new lightcurves

# plot

```

# Exercise 4

Now determine the best energy fit value for SGRB 090510.

```python
thetaObs = 0.05  # Viewing angle in radians
E0 = # choose     # Isotropic-equivalent energy in erg
thetaC = 0.1     # Half-opening angle in radians
thetaW = 0.1     # Truncation angle, unused for top-hat
b = .01            # power law index, unused for top-hat
n0 = 1.0         # circumburst density in cm^{-3}
p = 2.2          # electron energy distribution index
eps_e = 0.1      # epsilon_e
eps_B = 0.01     # epsilon_B
xi_N = 1.0       # Fraction of electrons accelerated
```

```python
# Load in SGRB 090510 dataset (see 130603B for example)

obs = np.loadtxt('090510_OmuJy.txt')

time = obs[:,0]
filt = obs[:,1]
flux = obs[:,2]
ferr = obs[:,3]
det = obs[:,4]

cond =

time =
fil =
fl =
flerr =
```

```python
# Choose time range

ta = grb.day2sec * # choose lower time
tb = grb.day2sec * # choose upper time
tsec = np.geomspace() # write

print('Time range chosen')

# Calculate flux in a single optical band (all times have same frequency)

nu = np.empty(tsec.shape)
nu[:] = 4.8e14 # approximate r-band frequency

Y = np.array([thetaObs, E0, thetaC, thetaW, b, specType, q, ts, n0, p, eps_e, eps_B, xi_N, dL])
Z = {'z': z}

# Calculate!

Fnu_gau =

print("Calculated Gaussian Jet flux")

# Convert time back to days and plot!

t =

print("Plot Observed Data")

plt.errorbar(time,fl,yerr=flerr,capsize=3,fmt='o',color='xkcd:shocking pink')

print("Plot Models")

plt.plot(t,Fnu_gau, label = 'Gaussian')
plt.yscale('log')
plt.xlabel('Time since burst (days)')
plt.ylabel(r'$F_{nu}$ (Jy)')
plt.title('090510 Afterglow Models')
plt.legend(fontsize=14)
plt.show()
```

# Exercise 4

## Creating multiwavelength afterglow lightcurves

SGRB 050724 was the first short gamma-ray burst whose afterglow was detected in the X-ray, optical, near-infrared and radio parts of the EM spectrum. As time went by, astronomers started sampling the afterglows more frequenctly across wavelengths. Here, we will compare the multiwavelength observations from the 130603B with our lightcurves for different jet types. Data from Table 1, Fong, Berger, Metzger et al. 2013 (https://iopscience.iop.org/article/10.1088/0004-637X/780/2/118/pdf).

## Unit conversions

In order to produce lightcurves in the correct units, we must provide the models with the correct units of frequency, $\nu$. We can calculate $\nu$ from wavelength $\lambda$ by:

\begin{equation}
c = \lambda * \nu
\end{equation}

Where $c$ is the speed of light ($c=3*10^{10}$ cm/s), $\nu$ is frequency, and $\lambda$ is wavelength.

Below is the electromagnetic spectrum with approximate values for $\lambda$. As $\texttt{afterglowpy}$ only accepts $\nu$ values, you'll need to convert to frequency to calculate your multiwavelength afterglow lightcurves. Below is an EM spectrum with frequency and wavelength so you can check you have approximately the correct values. However, I'll give you the exact wavelength values below to calculate.

# Part 4a)

Calculating frequency of each band

```python
# calculate X-ray, optical, near infrared and radio frequencies in Hz given

c = 3e10 # units = cm/s

lambda_xray = 1.24e-7 # cm units
nu_xray =

lambda_opt = 0.62e-4 # cm units
nu_opt =

lambda_nir = 1.2e-4 # cm units
nu_nir =

lambda_rad = 4.5e0 # cm units
nu_rad =
```

```python
# Preliminary Jet parameters - leave for now, can play with later!

thetaObs = 0.05  # Viewing angle in radians
E0 = 1.0e51    # Isotropic-equivalent energy in erg
thetaC = 0.1     # Half-opening angle in radians
thetaW = 0.1     # Truncation angle, unused for top-hat
b = .01            # power law index, unused for top-hat
n0 = 1.0         # circumburst density in cm^{-3}
p = 2.2          # electron energy distribution index
eps_e = 0.1      # epsilon_e
eps_B = 0.01     # epsilon_B
xi_N = 1.0       # Fraction of electrons accelerated
specType = 0 # global cooling time, no inverse compton
```

# Part 4b)

Load in observed multiwavelength data and sort it part filter (X-ray, Optical, Near-Infrared, and Radio) by detection (q = 1) or upper limit (q = 0). Upper limits can be useful because they can help us rule out models that are too bright.

```python
# Load in data

obs = np.loadtxt('multiwave_OmuJy.txt')

dt = obs[:,0] # time in days
filt = obs[:,1] # filter in microns (1 micron = 10000 centimeters)

print(filt) # print to see which part of the spectrum the data was taken at

flux = obs[:,2] # flux in microJansky
ferr = obs[:,3] # flux error in microJansky

det = obs[:,4] # detection = 1, upper limit = 0

# Separate data by filter and if detection or upper limit

```

# Part 4c)

Create multiwavelength models using frequencies calculated in Part 4a) and plot with observations.

```python
# Choose appropriate time range

ta = grb.day2sec * # choose lower time
tb = grb.day2sec * # choose upper time
tsec = np.geomspace() # write

# calculate multiwavelength lightcurves

print("Calc Radio")
FnuR = grb.fluxDensity()
print("Calc Optical")
FnuO = grb.fluxDensity()
print("Calc Near Infrared")
FnuN = grb.fluxDensity()
print("Calc X-ray")
FnuX = grb.fluxDensity()

# plot data by filter - make sure to use different markers for detections and upper limits

# plot models

print("Plot")

t =

plt.plot(t,FnuR, label = 'Radio')
plt.plot(t,FnuO, label = 'Optical')
plt.plot(t,FnuN, label = 'Near-Infrared')
plt.plot(t,FnuX, label = 'X-ray')

plt.yscale('log')
plt.xlabel('') # add in
plt.ylabel(r'') # add in
plt.title('') # add in
plt.legend(fontsize=14)
plt.show()
```

# Part 4d)

Play with your Jet parameters and jetType to get the best fit to all of the detections and upper limits.

```python
# Work space for 4d
```

#### Question: Which jet type fits the data best?

Answer:

# Nice job!

If you finished early, here's a challenge:

We have been determining the best models by eye for these exercises. However, in practice astronomers like to use statistics to make sure we are finding the best fit. To do this, we must:

1. Determine how to quantitatively measuring how good a fit is
2. Minimize or maximize this quantity to make sure we have the best fit possible

Feel free to ask more questions! I'm happy to help provide resources for smart ways to do this.

```python

```


```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)

# Hide the code completely

# from IPython.display import HTML
# tag = HTML('''<style>
# div.input {
#     display:none;
# }
# </style>''')
# display(tag)

```

Toggle cell visibility here.

## State feedback control - sledenje referenčni vrednosti

Za sistem definiran z enačbo:

$$
\dot{x}=\underbrace{\begin{bmatrix}-3&4\\0&2\end{bmatrix}}_{A}x+\underbrace{\begin{bmatrix}0\\1\end{bmatrix}}_{B}u.
$$

je dana zahteva, da prva spremenljivka stanj sledi sinusoidni referenčni funkciji s frekvenco 6 rad/s ($\approx 1$ Hz) brez odstopanja v amplitudi.

V prvem koraku dodamo integrator (z uvedbo fiktivne spremenljivke stanje; postopek je razložen v interaktivnem primeru [Krmiljenje povratne zveze stanj - zmogljivost krmiljenja](SS-31-Krmiljenje_povratne_zveze_stanj_zmogljivost)) tako, da preverimo da razširjen sistem ostane vodljiv, kar je pomembno, da se zaprtozančna prenosna funkcija od reference do $x_1$ začne pri vrednosti 0 dB. Končni razširjen sistem tako zapišemo kot:

$$
\dot{x}_a=\underbrace{\begin{bmatrix}-3&4&0\\0&2&0\\1&0&0\end{bmatrix}}_{A_a}x_a+\underbrace{\begin{bmatrix}0\\1\\0\end{bmatrix}}_{B_a}u+\underbrace{\begin{bmatrix}0\\0\\-1\end{bmatrix}}_{B_{\text{ref}}}x_{1r}\,.
$$

Z namenom zagotovitve dani zahtevi je ključno, da si predstavljamo obliko prenosne funkcije, ki zagotavlja zahtevan odziv, tj. 0 dB od $\omega=0$ do najmanj $\omega=6$ in fazo 0 deg v enakem intervalu frekvenc. Ob upoštevanju učinka polov v območje frekvenc izvedn danega intervala je rešitev ta, da prilagodimo pole tako, da le-ti ležijo v območju frekvenc višjih od 65 rad/s.

Izbrani poli so tako $\lambda_{1,2,3}= 65$ rad/s, matrika ojačanja pa $K_a = \begin{bmatrix}3024.75&194&68656.25\end{bmatrix}^T$.

Krmiljeni sistem zapišemo kot:

$$
\dot{x}_a=\underbrace{\begin{bmatrix}-3&4&0\\-3024.75&-192&-68656.25\\1&0&0\end{bmatrix}}_{A_a-B_aK_a}x_a+\underbrace{\begin{bmatrix}0\\1\\0\end{bmatrix}}_{B_a}v+\underbrace{\begin{bmatrix}0\\0\\-1\end{bmatrix}}_{B_{\text{ref}}}x_{1r}
$$

V tem primeru je prikazana simulacija skupaj z Bodejeveim diagramom prenosne funkcije od reference $x_{1r}$ do spremenljivke stanja $x_1$.

### Kako upravljati s tem interaktivnim primerom?
Zanimivo bi bilo doseči tudi odziv brez odstopka v faznem delu signala. Kako daleč je potrebno, za dosego tega scenarija, prestaviti pole?

```python
%matplotlib inline
import control as control
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt

#print a matrix latex-like
def bmatrix(a):
     """Returns a LaTeX bmatrix - by Damir Arbula (ICCT project)

     :a: numpy array
     :returns: LaTeX bmatrix as a string
     """
     if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
     lines = str(a).replace('[', '').replace(']', '').splitlines()
     rv = [r'\begin{bmatrix}']
     rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
     rv +=  [r'\end{bmatrix}']
     return '\n'.join(rv)

# Display formatted matrix:
def vmatrix(a):
    if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{vmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{vmatrix}']
    return '\n'.join(rv)

#matrixWidget is a matrix looking widget built with a VBox of HBox(es) that returns a numPy array as value !
class matrixWidget(widgets.VBox):
    def updateM(self,change):
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.M_[irow,icol] = self.children[irow].children[icol].value
                #print(self.M_[irow,icol])
        self.value = self.M_

    def dummychangecallback(self,change):
        pass

    def __init__(self,n,m):
        self.n = n
        self.m = m
        self.M_ = numpy.matrix(numpy.zeros((self.n,self.m)))
        self.value = self.M_
        widgets.VBox.__init__(self,
                             children = [
                                 widgets.HBox(children =
                                              [widgets.FloatText(value=0.0, layout=widgets.Layout(width='90px')) for i in range(m)]
                                             )
                                 for j in range(n)
                             ])

        #fill in widgets and tell interact to call updateM each time a children changes value
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
                self.children[irow].children[icol].observe(self.updateM, names='value')
        #value = Unicode('example@example.com', help="The email value.").tag(sync=True)
        self.observe(self.updateM, names='value', type= 'All')

    def setM(self, newM):
        #disable callbacks, change values, and reenable
        self.unobserve(self.updateM, names='value', type= 'All')
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].unobserve(self.updateM, names='value')
        self.M_ = newM
        self.value = self.M_
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].observe(self.updateM, names='value')
        self.observe(self.updateM, names='value', type= 'All')

                #self.children[irow].children[icol].observe(self.updateM, names='value')

#overlaod class for state space systems that DO NOT remove "useless" states (what "professor" of automatic control would do this?)
class sss(control.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        control.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

```python
# Preparatory cell

A = numpy.matrix('-3 4 0; 0 2 0; 1 0 0')
B = numpy.matrix('0; 1; 0')
Br = numpy.matrix('0; 0; -1')
C = numpy.matrix('1 0 0')
X0 = numpy.matrix('0; 0; 0')
K = numpy.matrix([842.25,104,10718.75])

Aw = matrixWidget(3,3)
Aw.setM(A)
Bw = matrixWidget(3,1)
Bw.setM(B)
Brw = matrixWidget(3,1)
Brw.setM(Br)
Cw = matrixWidget(1,3)
Cw.setM(C)
X0w = matrixWidget(3,1)
X0w.setM(X0)
Kw = matrixWidget(1,3)
Kw.setM(K)

eig1c = matrixWidget(1,1)
eig2c = matrixWidget(2,1)
eig3c = matrixWidget(1,1)
eig1c.setM(numpy.matrix([-65]))
eig2c.setM(numpy.matrix([[-65],[0]]))
eig3c.setM(numpy.matrix([-65]))
```

```python
# Misc

#create dummy widget
DW = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))

#create button widget
START = widgets.Button(
    description='Test',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Test',
    icon='check'
)

def on_start_button_clicked(b):
    #This is a workaround to have intreactive_output call the callback:
    #   force the value of the dummy widget to change
    if DW.value> 0 :
        DW.value = -1
    else:
        DW.value = 1
    pass
START.on_click(on_start_button_clicked)

# Define type of method
selm = widgets.Dropdown(
    options= ['Nastavi K', 'Nastavi lastne vrednosti'],
    value= 'Nastavi K',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues for the observer
selc = widgets.Dropdown(
    options= ['brez kompleksnih lastnih vrednosti', 'dve kompleksni lastni vrednosti'],
    value= 'brez kompleksnih lastnih vrednosti',
    description='Lastne vrednosti:',
    disabled=False
)

#define type of ipout
selu = widgets.Dropdown(
    options=['impulzna funkcija', 'koračna funkcija', 'sinusoidna funkcija', 'kvadratni val'],
    value='impulzna funkcija',
    description='Vhod:',
    disabled=False,
    style = {'description_width': 'initial'}
)
# Define the values of the input
u = widgets.FloatSlider(
    value=1,
    min=0,
    max=20.0,
    step=0.1,
    description='Referenca:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
period = widgets.FloatSlider(
    value=1,
    min=0.01,
    max=4,
    step=0.01,
    description='Perioda: ',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)
```

```python
# Support functions

def eigen_choice(selc):
    if selc == 'brez kompleksnih lastnih vrednosti':
        eig1c.children[0].children[0].disabled = False
        eig2c.children[1].children[0].disabled = True
        eigc = 0
    if selc == 'dve kompleksni lastni vrednosti':
        eig1c.children[0].children[0].disabled = True
        eig2c.children[1].children[0].disabled = False
        eigc = 2
    return eigc

def method_choice(selm):
    if selm == 'Nastavi K':
        method = 1
        selc.disabled = True
    if selm == 'Nastavi lastne vrednosti':
        method = 2
        selc.disabled = False
    return method
```

```python
def main_callback(Aw, Bw, Brw, X0w, K, eig1c, eig2c, eig3c, u, period, selm, selc, selu, DW):
    A, B, Br = Aw, Bw, Brw
    sols = numpy.linalg.eig(A)
    eigc = eigen_choice(selc)
    method = method_choice(selm)

    if method == 1:
        sol = numpy.linalg.eig(A-B*K)
    if method == 2:
        if eigc == 0:
            K = control.acker(A, B, [eig1c[0,0], eig2c[0,0], eig3c[0,0]])
            Kw.setM(K)
        if eigc == 2:
            K = control.acker(A, B, [eig1c[0,0],
                                      numpy.complex(eig2c[0,0],eig2c[1,0]),
                                      numpy.complex(eig2c[0,0],-eig2c[1,0])])
            Kw.setM(K)
        sol = numpy.linalg.eig(A-B*K)
    print('Lastne vrednosti sistema so:',round(sols[0][0],4),',',round(sols[0][1],4),'in',round(sols[0][2],4))
    print('Lastne vrednosti krmiljenega sistema so:',round(sol[0][0],4),',',round(sol[0][1],4),'in',round(sol[0][2],4))

    sys = sss(A-B*K,Br,C,0)
    T = numpy.linspace(0, 6, 1000)

    if selu == 'impulzna funkcija': #selu
        U = [0 for t in range(0,len(T))]
        U[0] = u
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'koračna funkcija':
        U = [u for t in range(0,len(T))]
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'sinusoidna funkcija':
        U = u*numpy.sin(2*numpy.pi/period*T)
        T, yout, xout = control.forced_response(sys,T,U,X0w)
    if selu == 'kvadratni val':
        U = u*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        T, yout, xout = control.forced_response(sys,T,U,X0w)

    fig = plt.figure(num='Bodejev diagram', figsize=(16,10))
    control.bode_plot(sys)
    fig.suptitle('Bodejev diagram', fontsize=16)

    plt.figure(num='Simulacija', figsize=(16,4))
    plt.title('Odziv prve spremenljivke stanj')
    plt.ylabel('$X_1$ vs ref')
    plt.plot(T,xout[0],T,U,'r--')
    plt.xlabel('$t$ [s]')
    plt.legend(['$x_1$','Referenca'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

alltogether = widgets.VBox([widgets.HBox([selm,
                                          selc,
                                          selu]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('K:',border=3), Kw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Lastne vrednosti:',border=3),
                                          eig1c,
                                          eig2c,
                                          eig3c,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('X0:',border=3), X0w]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([u,
                                          period,
                                          START]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('Dinamična matrika Aa:',border=3),
                                          Aw,
                                          widgets.Label('Vhodna matrika Ba:',border=3),
                                          Bw,
                                          widgets.Label('Referenčna matrika Br:',border=3),
                                          Brw])])
out = widgets.interactive_output(main_callback, {'Aw':Aw, 'Bw':Bw, 'Brw':Brw, 'X0w':X0w, 'K':Kw, 'eig1c':eig1c, 'eig2c':eig2c, 'eig3c':eig3c,
                                                 'u':u, 'period':period, 'selm':selm, 'selc':selc, 'selu':selu, 'DW':DW})
out.layout.height = '1050px'
display(out, alltogether)
```

    Output(layout=Layout(height='1050px'))

    VBox(children=(HBox(children=(Dropdown(options=('Nastavi K', 'Nastavi lastne vrednosti'), value='Nastavi K'), …