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

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Promijeni vidljivost <a href="javascript:code_toggle()">ovdje</a>.''')
display(tag)

# Hide the code completely

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

```

Promijeni vidljivost ovdje.

## Beskonačne točke ravnoteže 3

Ovaj interaktivni primjer prikazuje $2\times2$ sustav koji ima beskonačno mnogo točaka ravnoteže koje leže na pravcu $x_1=-x_2$ (za podležeću teoriju pogledajte interaktivnu lekciju o točkama ravnoteže).

Da bi pravac $x_1=-x_2$ bio prostor ravnotežnih točaka, treba vrijediti:
$$
A\bar{x}=0 \quad \forall \, \bar{x}\in\begin{bmatrix} \alpha \\ -\alpha\end{bmatrix} \, \text{uz} \, \alpha\in\mathbb{R},
$$
stoga, $\begin{bmatrix} \alpha \\ -\alpha\end{bmatrix}$ mora pripadati "null-prostoru" (engl. nullspace) matrice $A$.

### Kako koristiti ovaj interaktivni primjer?
- Izravno izmijenite matricu $A$ da biste vidjeli kako se mijenjaju točke ravnoteže.
- Pokušajte modificirati $A$ na način da točke ravnoteže leže na zadanom skupu.

```python
#Preparatory Cell

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

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

#create a NxM matrix widget
def createMatrixWidget(n,m):
    M = widgets.GridBox(children=[widgets.FloatText(layout=widgets.Layout(width='100px', height='40px'),
    value=0.0, disabled=False, label=i) for i in range(n*m)],
    layout=widgets.Layout(
        #width='50%',
        grid_template_columns= ''.join(['100px ' for i in range(m)]),
        #grid_template_rows='80px 80px 80px',
        grid_row_gap='0px',
        track_size='0px')
    )
    return M

#extract matrix from widgets and convert to numpy matrix
def getNumpyMatFromWidget(M,n,m):
    #get W gridbox dims
    M_ = numpy.matrix(numpy.zeros((n,m)))
    for irow in range(0,n):
        for icol in range(0,m):
            M_[irow,icol] = M.children[irow*3+icol].value

#this is a simple derived class from FloatText used to experience with interact
class floatWidget(widgets.FloatText):
    def __init__(self,**kwargs):
        #self.n = n
        self.value = 30.0
        #self.M =
        widgets.FloatText.__init__(self, **kwargs)

#    def value(self):
#        return 0 #self.FloatText.value

from traitlets import Unicode
from ipywidgets import register

#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
#define the matrixes
A=matrixWidget(2,2)
A.setM(numpy.matrix('1. 0.; 0. 1.'))

def main_callback(matA,DW):

    As = sym.Matrix(matA)
    NAs = As.nullspace()

    t = numpy.linspace(-10,10,1000)
    if len(NAs) == 1:
        eq1 = [t[i]*numpy.matrix(NAs[0]) for i in range(0,len(t))]
        x1 = [eq1[i][0,0] for i in range(0,len(t))]
        x2 = [eq1[i][1,0] for i in range(0,len(t))]

    fig = plt.figure(figsize=(6,6))
    if len(NAs) == 0:
        plt.plot(0,0,'bo')
    if len(NAs) == 1:
        plt.plot(x1,x2)
    if len(NAs) == 2:
        plt.fill((-5,-5,5,5),(-5,5,5,-5),alpha=0.5)
        plt.xlim(left=-5,right=5)
        plt.ylim(top=5,bottom=-5)
    plt.grid()
    plt.xlabel('$x_1$')
    plt.ylabel('$x_2$')
    print('Osnova za A-nullspace (po retcima) je %s. \nSvojstvene vrijednosti su %s' %(str(numpy.array(NAs)),
                                                                          str(numpy.linalg.eig(matA)[0])))

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

out = widgets.interactive_output(main_callback,{'matA':A,'DW':DW})
out1 = widgets.HBox([out,
                     widgets.VBox([widgets.Label(''),widgets.Label(''),widgets.Label(''),widgets.Label('$\qquad \qquad A=$')]),
                     widgets.VBox([widgets.Label(''),widgets.Label(''),widgets.Label(''),A,START])])
out.layout.height = '450px'
display(out1)
```

    HBox(children=(Output(layout=Layout(height='450px')), VBox(children=(Label(value=''), Label(value=''), Label(v…

```python
#create dummy widget 2
DW2 = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))
DW2.value = -1

#create button widget
START2 = widgets.Button(
    description='Prikaži odgovore',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Klikni za prikaz odgovora',
    icon='check'
)

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

def main_callback2(DW2):
    if DW2 > 0:
        display(Markdown(r'''Odgovor:
Za konstrukciju matrice moguće je odabrati vektore redaka koji su ortogonalni u odnosu na nullspace. Moguća matrica je stoga:

$$
A=\begin{bmatrix} 1 & 1 \\ 2 & 2 \end{bmatrix}.
$$'''))
    else:
        display(Markdown(''))

#create a graphic structure to hold all widgets
alltogether2 =  widgets.VBox([START2])

out2 = widgets.interactive_output(main_callback2,{'DW2':DW2})
#out.layout.height = '300px'
display(out2,alltogether2)
```

    Output()

    VBox(children=(Button(description='Prikaži odgovore', icon='check', style=ButtonStyle(), tooltip='Klikni za pr…


# Half-Life and Radioactive Decay

The half-life of an unstable atom is the amount of time required on average for half of a population of that atom to decay to a different element. The half-life can range from yottoseconds ($10^{24}$ seconds!) to numbers so large that it's hard to consider the atom unstable (for example Tellerium-128 has a half-life of approximately $10^{24}$ (yotta) years). A short list of select atomic half-lives is available from Wikipedia [here](https://en.wikipedia.org/wiki/List_of_radioactive_isotopes_by_half-life).

As an example, suppose you have a collection of one thousand atoms which have a half-life of twenty minutes. After twenty minutes your collection of those atoms will have shrunk to approximately five hundred atoms. However, that does not mean that your atoms have disappeared. Those unstable atoms have **decayed**, meaning that the atom has lost energy by emitting radiation and changed either its number of neutrons, protons, or both! Most commonly atoms decay by emitting a electrons, neutrons, positrons, or alpha particles (which are helium nuclei, He$^{2+}$). What happens in each of these decay processes is outlined below.

## Modes of decay

### Alpha ($\alpha$) Decay

Alpha decay is a decay process in which an atom releases an $\alpha$ particle. An $\alpha$ particle is simply another name for an ionized (no electrons) helium atom $\left(^4_2He\right)$, so if an element undergoes alpha decay, the atom reduces its number of neutrons $N$, and the number of protons $Z$. In general, for an atom X with atomic number $A$, the sum of neutrons and protons, decaying into an atom $Y$ via $\alpha$ decay has the following form:

\begin{equation}
^A_ZX^N \rightarrow ^{A-4}_{Z-2}Y^{N-2} + ^4_2\alpha^2
\end{equation}

Here $A$ is the **atomic number** of an atom, or the sum of its number of neutrons $N$ and protons $Z$.

\begin{equation}
A = Z + N
\end{equation}

Alpha decay, written in terms of a concrete example, the $\alpha$ decay of Uranium-235 would appear as

\begin{equation}
_{92} ^{235} \text{U}^{143} \rightarrow _{90} ^{231}\text{Th}^{141} + _2 ^4\alpha^{2}
\end{equation}

Where we see that uranium-235 decays to Thorium-231 by emitting an $\alpha$ particle, it alpha decays. Notice however, that the total number of neutrons and protons are equal on both the right hand side and the left hand side of the reaction. Generally speaking however, the number of neutrons is not typically written explicitly, and calculated by subtracting $Z$ from $A$.

### Beta $\beta$ Decay

$\beta$ decay comes in two flavors: beta positive ($\beta^+$) decay and beta minus ($\beta^-$) decay. $\beta^-$ decay emits an electron, and $\beta^+$ decay emits the anti-particle of the electron: the positron (a particle with the same mass, but opposite charge of an electron). In the case of $\beta^+$ decay, the element emits a positron from a proton, therefore the atom 'loses' a proton and 'gains' a neutron.  In the case of $\beta^-$ decay, a neutron emits an electron meaning the atom 'loses' a neutron and 'gains' a proton.

They also emit mysterious and common particle known as a neutrino

## The Neutrino

### Beta Minus decay

Generally speaking $\beta^-$ decay has the following form

\begin{equation}
^A _Z\text{X}^N \rightarrow _{Z+1}^A\text{Y}^{N-1} + e^- + \bar{\nu}
\end{equation}

where $\bar{\nu}$ is an anti-neutrino. Notice that charge and mass is conserved. From the above formula, we see that a neutron is emitting an electron and anti-neutrino and then becomes a proton. Notice how both charge and mass is conserved, we gained a positive charge with the proton, but also gained a negative charge in the process with an electron.

### Beta plus decay

\begin{equation}
^A _Z\text{X}^{N} \rightarrow _{Z-1}^A\text{Y}^{N+1} + e^+ + \nu
\end{equation}

where $\nu$ is a neutrino. Notice that charge and mass is again conserved. From the above formula, we see that a proton is emitting a positron and neutrino, and the becomes a neutron.

### Electron Capture
This wasn't in the Alberta curriculum thing but would it important? There's also a whole bunch of others that we could toss in here that could be considered optional. I kind of want to put it in because then the neutron capture stuff will make more sense. Not that that's required either, but it would make for a cool animation.

## Some Mathematical Background
Let's build on our earlier discussion and start introducing some mathematical rigor to our discussion of half life. The half-life of an atom is denoted as $\tau_{1/2}$, measured in seconds,  where $\tau$ is the Greek letter tau. Using the half life of an atom, we can calculate the number of atoms as a function of time $N(t)$ with the following relationship

\begin{equation}
N(t) = N_0 \; 2.718^{-\lambda t}
\label{eq:decay}
\tag{1}
\end{equation}

Where $N(t)$ is the _number_ of atoms you have at time $t$, $N_0$ is the original number of atoms at $t=0$ and $\lambda$ is known as the "half life constant" which has units of inverse seconds. The half life constant is a bit of an abstraction from the more intuitive half life, however $\lambda$ is related to the half life by the following simple relationship

\begin{equation}
\lambda = \frac{0.693}{\tau_{1/2}}
\label{eq:lambda}
\tag{2}
\end{equation}

The above two equations make it is possible to calculate the amount of atoms remaining at any given time, provided   the half life and the original number of atoms is known. Another, more subtle, fact about equation \ref{eq:decay} that is worth pointing out is that the exponential part ($2.718^{\lambda t}$) of that equation is directly related to the _probability_ that an atom will decay at a given time. Intuitively this makes sense, which we illustrate with an example. Suppose we started with one thousand atoms, and after ten seconds we find that we only have 60% of our atoms remaining. Using equation \ref{eq:decay} we can write this scenario as follows with $N_0 = 1000$

\begin{equation}
N(t= 3 \text{ s}) = 1000 \times 2.718^{-\lambda \; 3 \text{s}} = 600
\tag{3}
\label{eq:probrelat}
\end{equation}

We can manipulate the left hand side of this equation to show

\begin{equation}
N(t = 3 \text{ s}) = 2.718^{-\lambda \; 3 \text{s}} = \frac{600}{1000} = 0.6 = 60 \%
\end{equation}

Where if we substitute our new expression for $N(t = 3 \text{ s})$ back to equation \ref{eq:probrelat}, we see that this function is analogous to a probability. In other words, the expression

\begin{equation}
2.718^{-\lambda t}
\end{equation}

is the probability that an atom **will not** decay at a given time $t$, or

\begin{equation}
P_{exists}(t) = 2.718^{-\lambda t}
\label{eq:exist}
\tag{4}
\end{equation}

Where $P_{exists}(t)$ is the probability that an atom will exist at time $t$.

Now that we have an expression for the probability that an atom will **not** decay, we can now calculate the probability that an atom **does** decay. The probability that an atom **does** decay is simply one minus the probability of it not decaying, or

\begin{equation}
P_{decayed}(t) = 1 - P_{exists}(t) = 1 - 2.718^{-\lambda t}
\label{eq:decayed}
\tag{5}
\end{equation}

Where $P_{decayed}(t)$ is the probability that an atom has decayed by time $t$.

This means that the expression in equation \ref{eq:decay} is a "smooth idealization". Radioactive decay is a statistical process, and actual data will only ever _approach_ the solution of equation \ref{eq:decay}. In reality, our observations of radioactive will randomly fluctuate around that solution. To visualize this, we can use Python to get an idea of what we may measure if we were to observe an actual population of atoms decay.

## Python Example

Using the relationships defined in equations \ref{eq:exist} and \ref{eq:decayed}, it is possible to construct a simple model to visualize how a population of atoms may be observed to decay. This is the case because we know the probability that an atom will decay or not at any given time step. If we know the probability of decay, we simply need to compare this probability to some metric in order to decide if our atom decays or not.

Sounds simple, but that's not an obvious metric to know. How do you decide which atoms decay? Suppose we have an atom that has a 40% probability of decay, how exactly do we decide if this atom decays at a given time or not? Do we simply consider a large group of these atoms and choose 40% of them and say they have decayed? This is certainly an option, however, if we do it this way we won't be observing the interesting random statistical fluctuations, as we would simply be using definition of equation \ref{eq:decay}. What methodology should we use in order to view actual fluctuations?

### Radioactive decay as an analogy to tossing a biased coin

Suppose you were sifting through a bag of atoms and deciding which ones decay by throwing them away according to their half life. Let's suppose that at your current instance in time, each atom has a 50% probability of decay. Doing this manually, conceivably you could pick an atom and flip a coin to decide its fate. If the coin showed heads you could keep the atom (the atom does not decay), and if the coin showed tails you could throw it away (the atom decays). Once you've gone through the entire set of atoms, you will have thrown away those atoms randomly according to their probability of decay. This is a reasonable methodology: you're still removing half of the atoms randomly, and the act of removing one atom is completely independent from the act of removing another. Is there some way to generalize this process and apply it to what we've learned about nuclear decay?

Luckily, Python allows us to draw random numbers which are uniformly distributed between 0 and 1 algorithmically. In other words, there is a Python function which allows us to randomly draw a percentage between 0 and 100%, where each percentage is equally probable to draw. Using this function, we can compare our new random percentage, against our probabilities of decay to decide if our atom decays or not! This is exactly the same as flipping a coin for each atom, except now we're using a "biased coin" which can favor heads over tails (or in our case decaying or not decaying). That's still pretty abstract in terms of what this actually means. For an introduction to simulating a biased coin toss please see [coin flipping](../Math/FlippingCoins.ipynb) for a brief introduction. Of course, if  a deeper understanding of how the simulation of nuclear decay is not of interest to you, the following section is not required and you can feel free to skip the next section.

### Simulating nuclear decay
The way in which we look through our "bag" of atoms can be depicted as a flow chart as seen below
>
> Here we see the flowchart for the process of simulating nuclear decay. Here we see that for every instance in time $t_i$ we have some number of atoms $N$. At that instance in time, we compare the probability of decay for each atom to a unique random number $r$. If the random number is less than our probability of decay, then the atom is said to have decayed. We then remove that atom from the bag. Once we have checked every atom at a particular instance in time, we now move to our next instance in time with our new number of atoms $N$ and repeat the process

The pseudo-code below shows how you would write this process with in code. Here we're looking through our "bag" of atoms, then the comparison between the random number and our calculated probability of decay acts as a (biased) coin toss to decide if the atom decays or not.

```python

for each instance_in_time in time:
    probability_of_decay = 1 - exp(-lambda * instance_in_time)
    for each atom in atoms:
        # This is a random number between 0 and 1
        random_percent = random_number()
        if prob_decay > r:
            atom.decays()
```

Where here `instance_in_time` is an equally spaced time step (steps of one second for example), and we're looping over many time steps. At each time step, we then calculate the probability that an atom decays at that instance in time by equation 5. We then loop over all atoms, generate a random number between 0 and 1 (random percentage between 0 and 100) and use that to decide if an atom decays on an atom to atom basis. Then, we do it again at the next time step. In terms of the coin toss analogy if `probability_of_decay` is greater than `random_percent`, our coin is heads and the atom does not decay. Should `probability_of_decay` be less than `random_percent` then our coin toss is tails and the atom decays.

To illustrate this with an example, if `probability_of_decay` is 76%, and the random number `random_percent` was 45%, our atom would not decay, however, if `random_percent` was greater than 76%, the atom would decay. As the random numbers are _uniformly_ distributed, that means that each percentage between 0 and 100 are all equally probable.

An important thing to notice that we calculate the probability of decay **before** we enter the loop where we see if an atom decays or not. This is because at a single instant of time, the probability of decay for each atom is **independent**: if one atom decays, this does not affect if any of the others also decay.

What to keep in mind here is that the basis behind this simulation is deceptively simple: We're simply deciding **if** each atom decays by flipping a biased coin at each step in time. If you were to do this "by hand" you would quite literally just be flipping coins and counting atoms. And that's precisely what our simulation is doing, just faster than we can.

Below, we see an example of this simulation behavior for carbon-15, which has a half-life of 2.249 seconds.

For each step in time, we need to check if each atom decays or not. We only calculate our probability once because at a single instant in time, the probability of decay is the same for each -- they are independent events.

```python
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import random
import pandas as pd

def HalfLifeEquation(N_0, lamb, t):
    return N_0 * 2.71828 ** (-lamb * t)

t = np.linspace(0,4*2.449,500) # Halflife of carbon-15 is 2.249 seconds
plt.figure(figsize=(3, 2), dpi= 175, facecolor='w', edgecolor='k')
plt.plot(t, HalfLifeEquation(1000, np.log(2)/2.249, t), label="Predicted")
plt.ylabel("Number of atoms")
plt.xlabel("t (seconds)")
plt.title("Carbon 15 Decay")

def Decay(N_0, lamb,max_time, steps=100):
    time_atom_pairs = []
    N = N_0
    time_step = max_time/steps
    t = 0
    for i in range(steps):
        # the equation from eariler, divide by N to reuse function
        p = 1 - HalfLifeEquation(N, lamb, time_step)/N
        for i in range(N):
            # a 'random' number between 0 and 1 to compare to our decay probability
            r = random.uniform(0,1)
            if r < p: # if our decay probability for the atom is greater, that atom decays.
                N = N -1
            if N == 0: # if we run out of atoms we should stop
                break
        # We have now moved forward in time once again!
        t = t + time_step
        # Store how many atoms at a given time t we have in an array.
        time_atom_pairs.append([N, t])
    return time_atom_pairs
decays = Decay(1000, np.log(2)/2.249, 10, steps=500)

y,x = np.array(decays).T
plt.plot(x, y, label = "Simulated")
plt.legend()
plt.show()
```

From the plot above we see that the simulated curve (orange) follows the predicted one (blue), but does not follow it exactly. This is a result of atomic decay being a statistical process.

# Simulations

Below you have a widget where you can compare how quickly different atoms decay, and view a simulation of how that process would look if you were to observe these counts yourself. The data is pulled automatically from the [National Institutes of Standards](https://www.nist.gov/pml/radionuclide-half-life-measurements/radionuclide-half-life-measurements-data).

You can choose which isotopes to watch their decay with the element drop-down menus, and you can run for more or less time with the `Time_Scale` slider. Move the slider around at the bottom to advance the decay simulation forward or backwards in time.

## Using the data

```python
url = 'https://www.nist.gov/pml/radionuclide-half-life-measurements/radionuclide-half-life-measurements-data'
df = pd.read_html(url)[0]

# Rename the columns
df.columns = ["Radionuclide",
              "NumberOfSources",
              "HalfLifes_Followed",
              "HalfLife",
              "StandardUncertainty",
              "OtherUncertainty",
              "ref"]
df = df.drop(['ref', 'OtherUncertainty'], axis=1)
df
```

Once the data is tabulated, we can use it in combination with equation 1 we can create the theoretical traces, and use it in combination with equation 5 to generate our "observed" decay path. These are shown graphically with the widget below

Feel free to simulate decay over a longer time frame by adjusting `Time_Scale` and changing the decay isotope using the drop down menus.

```python
from ipywidgets import interactive
import plotly as py
import plotly.graph_objs as go
py.offline.init_notebook_mode()

# Read and calculate the half-life of each element in the data frame.
def get_halflife(Element):
    multiplies = {'s':1., "min":60., "h":3600., "d":3600.*24.}
    index = index1 = np.where(df["Radionuclide"] == Element)[0][0]
    data = df["HalfLife"].iloc[index]
    half_life, pm, d_time, unit = data.split()
    scale = multiplies[unit]
    HalfLife = float(scale) * float(half_life)
    return HalfLife, unit

# calculate and animate the decay of two atoms on the same graph
def DecayRace2(Element1, Element2, Time_Scale=1):
    N_0 = 1000
    Decay_Points = 75
    # Grab the half-lives
    HalfLife_Element_1, unit1 = get_halflife(Element1)
    HalfLife_Element_2, unit2 = get_halflife(Element2)
    time_length = max(HalfLife_Element_1, HalfLife_Element_2)
    # Points for antimation of decay
    Points_Element_1 = Decay(N_0, np.log(2)/(float(HalfLife_Element_1)),
                      max_time= Time_Scale * float(time_length), steps=Decay_Points)
    Points_Element_2 = Decay(N_0, np.log(2)/(float(HalfLife_Element_2)),
                      max_time= Time_Scale * float(time_length), steps=Decay_Points)
    # Make Points
    y1,x1 = np.array(Points_Element_1).T
    y2,x2 = np.array(Points_Element_2).T
    # For the predicted traces
    t = np.linspace(0, Time_Scale * float(time_length), Decay_Points)
    y3 = HalfLifeEquation(N_0, np.log(2)/(float(HalfLife_Element_1)), t)
    y4 = HalfLifeEquation(N_0, np.log(2)/(float(HalfLife_Element_2)), t)
    Decay_Element_1 = go.Scatter(x = x1, y = y1, mode="markers+lines")
    Decay_Element_2 = go.Scatter(x = x2, y = y2, mode="markers+lines")
    Predicted_Element_1 = [dict(type = 'scatter',
            visible = False, # hide until we have selected with slider
            name = Element1, mode = 'lines',
            line = dict(color = 'rgb(116,130,143)'), x = t, y = y3) for step in range(len(y3))]
    # Make the first one visible by default
    Predicted_Element_1[0]['visible'] = True
    # Same as above with different data
    Predicted_Element_2 =  [dict(type='scatter', visible = False, mode = 'lines', name = Element2,
            line = dict(color = 'rgb(150,192,206)'), x = t, y = y4) for step in range(len(y4))]
    Predicted_Element_2[0]['visible'] = True
    data1 = [dict(type='scatter', visible = False, name = " ".join(["Simulated", Element1]),
            marker = dict(color = 'rgb(194,91,86)', line = dict(color = 'rgb(194,91,86)')),
            mode = 'markers+lines', x = x1[0:step], y = y1[0:step]) for step in range(len(x1))]
    data2 = [dict(type='scatter', visible = False, name = " ".join(["Simulated", Element2]),
            mode = 'markers+lines',
            marker = dict(color = 'rgb(190,185,191)', line = dict(color = 'rgb(190,185,191)')),
            x = x2[0:step], y = y2[0:step]) for step in range(len(x2))]
    steps = []
    for i in range(len(data1)):
        step = dict(method = 'restyle', args = ['visible', [False] * len(data1)],)
        step['args'][1][i] = True
        steps.append(step)
    # Add those arrays together for slider plots.
    # As long as they're the same size, this works perfectly
    data = data1 + data2 + Predicted_Element_1 + Predicted_Element_2
        # Create our slider for control of the graph
    sliders = [dict(active = 1, currentvalue = {"prefix": "Time Step: "},
                    pad = {"t":50}, steps = steps)]
        # Update title to display the half-lives of our elements
    hlt = "".join(['Half life of ',Element1,': ','{:0.3e}'.format(HalfLife_Element_1),' ',unit1,
                 '<br>',
                 'Half life of ',Element2,': ','{:0.3e}'.format(HalfLife_Element_2),' ',unit2])
        # Make a nice graph
    layout = dict(sliders=sliders, title = hlt,
                  xaxis = {"title":"Time (seconds)"},
                  yaxis = {"title":"Number of Atoms"})
    fig = py.graph_objs.Figure(data=data, layout=layout)
    py.offline.iplot(fig)
# Surpress warnings from IPython that we don't care about.
import warnings
import sys
if not sys.warnoptions:
    warnings.simplefilter("ignore")
# This gives us the drop down menu functionality
interactive_plot = interactive(DecayRace2, Element1=df["Radionuclide"], Element2 = df["Radionuclide"], continuous_update=True)
interactive_plot
```

## Questions
1. Drag the slider to animate the decay of the same element twice. Do the traces look the same? If so why? If not, why not?
2. Compare two isotopes with very different half-lives, what are the major differences you notice between their simulated and predicted traces?

# Fusion Processes and the Origin of the Heavy Elements

It is well accepted that every star in the observable universe is primarily powered by the fusion of hydrogen into helium. However, stars also gain energy from the fusion of heavier elements up until iron-56 $^{56}Fe$. This is because the amount of energy it takes to combine elements past this point actually [require more energy than they produce](https://en.wikipedia.org/wiki/Nuclear_binding_energy). Typically, when a star begins creating Iron, the star is at the end of its life. However, if stars stop producing the elements at iron 56, how do we get the rest of the periodic table? Well, stellar nucleosynthesis!

## Stellar Nucleosynthesis

[Stellar nucleosynthesis](https://en.wikipedia.org/wiki/Stellar_nucleosynthesis) is the creation of atomic nucleides during high-energy astrophysical events such as [Supernova](https://en.wikipedia.org/wiki/Supernova_nucleosynthesis), [neutron star mergers](https://physics.aps.org/articles/v10/114), or [quark novae](http://www.quarknova.ca/_include/files/feature_quark_stars.pdf). During these high energy events, it is theorized that there are an untold number of free neutrons which can be bound to various nuclei. Essentially an atom moving through a great number of neutrons would pick up many neutrons rapidly creating very heavy, very unstable isotopes.
If a nuclei picks up a free neutron, this process is known as neutron capture, which has the general formula for an atom $X$ and neutron $N$

\begin{equation}
^{A}_ZX^N + N \rightarrow ^{A+1}_Z X^{N+1}
\end{equation}

Where this has formed a different **isotope** of the atom $X$.
In a high energy astrophysical event, the process of nuclei rapidly gathering neutrons is known as the **$r$-process** or [**rapid neutron capture process**](https://en.wikipedia.org/wiki/R-process) is responsible for the creation of many elements heavier than iron. There is also another process known as the [**slow neutron capture process**](https://en.wikipedia.org/wiki/S-process), or the **$s$-process**. The difference between the $r$ and $s$ processes, for our purposes, is minor and the difference between the two is only in how quickly they happen.

A natural question to ask is "How does this create the rich variety of elements? This only tells me that there should be some incredibly neutron rich isotopes!". This is very true, however, the other half of the $r$ and $s$ processes is that they create incredibly _unstable_ neutron rich isotopes. And what do unstable isotopes do? Well, they $\beta$ decay of course! In this case, our neutrons will $\beta$ decay by spitting out an electron and turning into a proton -- thus creating a new element! Of course, if this happens too much, there is also a (much rarer) possibility that a proton will emit a positron thereby becoming a neutron. These processes are described for arbitrary atoms $X$, $Y$, and $W$ below, first $\beta^-$ decay

\begin{equation}
^{A}_ZX^N  \rightarrow ^{A}_{Z+1} Y^{N-1} + e^- + \bar{\nu}
\end{equation}

and then $\beta^+$ decay

\begin{equation}
^{A}_ZX^N  \rightarrow ^{A}_{Z-1} W^{N+1} + e^+ + \nu
\end{equation}

So essentially, the idea of stellar nucleosynthesis is that nuclei gather excess neutrons and then $\beta$ decay to stability. During an actual stellar nucleosynthesis event however, there are many other processes happening simultaneously that complicates the process, but these main events in the $r$ and $s$ processes are sufficient for the following demonstration.

## Question
1. What other potential processes have we ignored in this description?

## A (very) Simplified Model

To get a basic understanding of the life cycle of a nucleon trapped in the middle of the $r$ and $s processes, we're going to construct a toy model[1](#footnote1) of stellar nucleosynthesis that should produce some interesting visuals.

First, we'll start with a with a generic atom, any old atom will do, and we'll specify and arbitrary number of neutrons $N$ and an arbitrary number of protons $Z$. Once the atom is specified, we're going to assume it is trapped inside the $r$ or $s$ process and then we will toss a coin as to whether or not it captures a neutron, or beta decays. If it captures a neutron we'll increase the neutron count by one, if not we'll beta decay and lose a neutron and gain a proton, outlined in the flowchart below

>

> The image above depicts the calculation process required in order to create our toy model of nucleosynthesis. The green box outlines the calculation process when protons are not allowed to decay, and the gray box outlines the process when protons are allowed to decay. To begin we have an initial number of neutrons $N$ and an initial number if protons $Z$. We then draw a random number $r$ uniformly from the range $[0,1]$, and compare that to our neutron capture probability $p_{NC}$. If $r$ is greater than the probability of neutron capture, we gain a neutron, if not, we beta decay thereby losing a neutron and gaining a proton. Alternatively, if we're allowing proton decay, if our random number has dictated that we beta decay, we then draw a second random number $r_2$ uniformly from $[0,1]$ and compare it to our probability of proton decay $p_{PD}$. This comparison then decides which mode of decay that our atom will undergo. We however note that the exit condition in the widget you'll soon see is slightly different preventing cases where reaching $Z/N > 1$ may take a long time, or is changed to $N/Z > 1$ if there are more protons than neutrons at the beginning of the simulation.

-----

1: Here, toy model is understood to mean "wrong; but informative".

## Visualization

With the figure below you can visualize a nuclide's pathway to becoming a heavy element. We do note however that while this simulation will give you a basic idea of the process, the model described here is _far_ from realistic.

```python
# This is the exit condition of our loop, scaled to the original number of neutrons and protons
def Condition(Z,N, Z_0, N_0):
    if Z_0 >= N_0:
        return N/Z
    if N_0 > Z_0:
        return Z/N
# This is our toy model
def SimulateNewElement(N,Z, BetaNegative=False, ProbNegative = 0.1):
    # Initialize some lists and counters
    count = 0
    NArray = []
    ZArray = []
    min_iterations = 20
    N_0 = N
    Z_0 = Z
    # An infinite loop, relying on the break function to stop based on the conditions we've decided on
    while True:
        # Count how many times we've cycled and draw a random number
        count += 1
        r = random.uniform(0,1)
        # neutron capture
        if r >= 0.5:
            N = N + 1
            NArray.append(N)
            ZArray.append(Z)
        # beta decay
        elif r < 0.5:
            # draw a second random number to decide what kind of beta decay
            r2 = random.uniform(0,1)
            # Can only beta decay if we have a neutron and a proton
            if N > 0 and Z > 0:
                # If we're allowing proton decay
                if BetaNegative:
                    # Beta minus decay
                    if r2 < 1 - ProbNegative:
                        N = N - 1
                        Z = Z + 1
                    # beta plus decay
                    else:
                        N = N + 1
                        Z = Z - 1
                else:
                    # beta minus decay
                    N = N - 1
                    Z = Z + 1
            # save our counts at each iteration
            NArray.append(N)
            ZArray.append(Z)
        # Decide if we should leave
        if Condition(Z,N,Z_0,N_0) > 1.01 and count > min_iterations:
            if count > min_iterations:
                print("Reached Z = N line and passed minimum iterations, simulation finished")
            else:
                print("Ran minimum number of times and passed Z=N line.")
            break
        if count > 200:
            print("Taking too long, exiting early after 200 iterations")
            print("Try lowering the probability of negative beta decay")
            print("Or just simply running this simulation again")
            break
    return np.array(NArray), np.array(ZArray), np.linspace(0,count,count)
def TwoDAnimation(N,Z,ProtonDecayAllowed = True, ProbabilityOfProtonDecay = 0.0):
    N,Z,count = SimulateNewElement(N,
                                   Z,
                                   BetaNegative=ProtonDecayAllowed,
                                   ProbNegative=ProbabilityOfProtonDecay)
    steps = []
    data1 = [dict(type='scatter', visible = False, name = "Nuclide Path",
            marker = dict(color = 'rgb(194,91,86)', line = dict( color = 'rgb(194,91,86)')),
            mode = 'markers+lines', x = N[0:step], y = Z[0:step]) for step in range(len(N))]
    LinePoints = np.linspace(min(Z[0],N[0]), max(N[-1],Z[-1]), len(N))
    data2 = [dict(type='scatter', visible = False, name = "N = Z line",
            marker = dict(color = 'rgb(190,185,191)', line = dict( color = 'rgb(190,185,191)')),
            mode = 'lines', x = LinePoints, y = LinePoints) for step in range(len(N))]
    data2[1]['visible'] = True
    data = data1 + data2
    frames = data
    for i in range(len(data1)):
        step = dict(method = 'restyle', args = ['visible' ,[False] * len(data1)])
        step['args'][1][i] = True
        steps.append(step)
    sliders = [dict(active = 0, pad = {"t":50}, steps = steps,
                currentvalue = {'font': {'size': 20}, 'prefix': 'Nuclide:',
                                'visible': True, 'xanchor': 'right'},
                transition= {'duration': 300, 'easing': 'cubic-in-out'})]
    layout = dict(sliders=sliders, #updatemenus = updatemenus,
                  title = "Toy Model of Nucleosynthesis",
                  xaxis={"title":"Number of Neutrons"},
                  yaxis = {"title":"Number of Protons"})
    fig = py.graph_objs.Figure(data=data, layout=layout)
    py.offline.iplot(fig)
ProtonList = [i for i in range(5,100)]
NeutronList = [i for i in range(10, 105)]
interactive_plot2 = interactive(TwoDAnimation, Z=ProtonList, N=NeutronList, continuous_update=True)
interactive_plot2
```

In the figure above you can use the drop down menus to adjust the number of neutrons $N$ and the number of protons $Z$. By clicking `ProtonDecayAllowed` (enabled by default), you are allowing your protons to decay, with probability as selected by the `Probability...` slider. You can then view the nucleon's path through nucleosynthesis with the slider on the bottom. Also note by clicking and dragging you can zoom in on any point of the plot to your liking.

### Questions
1. Which way does the nucleon move on the path for neutron capture? $\beta^+$ decay? $\beta^-$ decay?
2. What does the path look like when you increase the probability of proton decay to fifty percent or more? Why? Be sure that `ProtonDecayAllowed` is clicked, and that you have at least 40 protons.

### Conclusion

In conclusion we covered the idea of radioactive decay as well as several modes of decay. We covered the general formulas for alpha and both flavors of beta decay. Using these ideas, we created a model which simulated the counts we might actually observe in nuclear decay and compared them to the theoretical curve for many unstable elements. This showed us that while the theory may predict smooth and equal counts, actual observation may be a little messier. This simulation can be used as preparation for actual experiments and to help understand what you would observe.

We also introduced the idea of neutron capture and the origin of the heavy elements. In order to help understand how the heavy elements are created, we created a toy model similar to the decay of an atom, except this time we simulated how an atom may capture neutrons and beta decay in order to create a heavier element.

[](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


# Lecture 1: What is Sound?
Audio Processing, MED4, Aalborg University, 2021

- Jesper Kjær Nielsen (jkn@create.aau.dk), Audio Analysis Lab, Aalborg University, and
- Cumhur Erkut (cer@create.aau.dk), Multisensory Experience Lab, Aalborg University

Table of Contents

1  Sound and simple vibrations

1.1  What is sound?

1.2  Human hearing

1.3  Sinusoids

1.4  Summary

2  Complex numbers

2.1  The need for complex numbers

2.2  The complex number

2.3  Summary

2.4  Additional information on complex numbers

2.5  Exit Question

3  Phasors

3.1  The polar (or exponential) form of a complex number

3.2  The phasor

3.3  Summary

## Sound and simple vibrations
In the next 20 minutes, you will learn
- What sound is and how it propagates
- How humans perceive sound
- What a sinusoid is
- that striking a bar creates a sinusoidal sound

### What is sound?

```
from IPython.display import display, YouTubeVideo, Image
YouTubeVideo('GkNJvZINSEY')
```

Sound is a **vibration** which travels through a medium as
- **longitudinal** waves: gasses (e.g., air), liquids (e.g., water), and solids (e.g., concrete)
- **transversal** sound waves: solids (e.g., concrete)

The **speed of sound** depends on the medium.

In air at room temperature, for example, it is approximately 343 m/s.

```
Image('apLecture1_files/wave.png')
```

Sound is normally divided into three types:
1. *Infrasound*: Sound with frequencies up to 20 Hz
2. *Audible sound*: Sound with frequencies in range 20 Hz - 20 kHz (*audio*)
3. *Ultrasound*: Sound with frequencies above 20 kHz

### Human hearing

```
YouTubeVideo('eQEaiZ2j9oc') #2:27
```

The human ear consists of the following parts:
- **Outer ear**: Everything on the outside of the ear drum, including the pinna
- **Middle ear**: The three bones (Malleus, Incus, Stapes). What do they do?
- **Inner ear**: The cochlea (Latin for what?) tube. What does the basilar membrane do inside? Hair cells?

The human ear
- does not hear all frequencies equally well
- is most sensitive to frequencies around 4 kHz
- is tuned to speech
- has a really large dynamic range of up to ~120 dB (i.e., we can hear sound intensities up to ~10^12 the quietest sounds)

### Sinusoids
A sinusoid (or a sine wave) is given by
$$
    x(t) = A \cos(\Omega t + \Phi)
$$
where
- $A\geq0$ is the **amplitude**
- $\Omega$ is the **frequency** measured in radians pr. second (SI symbol **rad/s**). Is related to the frequency $f$ measured in cycles pr. second (SI symbol **Hz**) via $\Omega = 2\pi f$.
- $t$ is the **time** measured in seconds (SI symbol **s**)
- $\Phi$ is the **initial phase** measured in radians (SI symbol **rad**)

The above form of the sinusoid is often referred to as the **polar form**. By using the angle addition formula for a cosine, i.e.,

$$
    \cos(\theta+\phi) = \cos(\theta)\cos(\phi)-\sin(\theta)\sin(\phi)\ ,
$$

a sinusoid can also be written in a **rectangular form** as

$$
    x(t) = a\cos(\Omega t) + b\sin(\Omega t)
$$

where a and b are scalars given by
\begin{align}
    a &= A\cos(\Phi)\\
    b &= -A\sin(\Phi)\ .
\end{align}

#### Numpy example: A sinusoid

```
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

samplingFreq = 44100 # Hz
nData = 2000
time = np.arange(0,nData).T/samplingFreq # s

# Generate a sinusoid
amp = 1;
freq = 100 # Hz
initPhase = np.pi/2 # rad
sinusoid = amp*np.cos(2*np.pi*freq*time+initPhase)

# Plot the sinusoids
plt.plot(time, sinusoid, linewidth=2)
plt.xlim((time[0],time[nData-1]))
plt.ylim((-1,1))
plt.xlabel('Time [s]')
plt.ylabel('Amplitude [.]')
plt.grid(True)
```

#### Example: Generation of a sinusoid from a vibrating bar

Assume that the act of striking a bar is modelled as **compressing a spring** in one dimension.

From Hooke's law, this compresssion can be written as
$$
    F(t) = -k x(t)
$$
where
- $F(t)$ is the **restoring force** measured in Newton (SI unit **N**)
- $x(t)$ is the **displacement** measured in meters (SI unit **m**) of the string from its resting position
- $k$ is the **spring constant** measured in N/m

From **Newton's second law**, the force can also be expressed as

$$
    F(t) = ma(t)
$$

where
- $m$ is the **mass** of the string measured in kilogram (SI unit **kg**)
- $a(t)$ is the **acceleration** measured in m/s^2.

The acceleration is related to the displacement $x(t)$ as
$$
    a(t) = \frac{dv(t)}{dt} = \frac{d^2 x(t)}{d t^2}
$$

where $v(t)$ is the **velocity** measured in m/s.

Combining these three equations gives
$$
    -k x(t) = F(t) = ma(t) = m \frac{d^2 x(t)}{dt^2}
$$
which can be rewritten as
$$
    \frac{d^2 x(t)}{dt^2} = -\frac{k}{m} x(t)\ .
$$
This is a constant-coefficient second-order differential equation.

Let us check if our sinusoid
$$
    x(t) = A\cos(\Omega t + \Phi)
$$
is a solution to the above differential equation. Since
\begin{align}
    \frac{dx(t)}{dt} &= -\Omega A\sin(\Omega t + \Phi)\\
    \frac{d^2 x(t)}{d t^2} &= -\Omega^2 A\cos(\Omega t + \Phi) = -\Omega^2 x(t)\ ,
\end{align}
we obtain
$$
    -\Omega^2x(t) = -\frac{k}{m} x(t)\ .
$$
Thus, striking a bar will make it vibrate sinusoidally with the frequency
$$
    \Omega = \sqrt{k/m}\ .
$$
This frequency can be changed by changing the spring constant and mass.

### Summary
- Sound is a vibration travelling through a medium.
- Sound waves are longitudal waves (and also transversal waves when travelling through a solid).
- The human ear converts pressure variations in the air to
 1. mechanical movement (interface is the eardrum)
 2. vibrations in a liquid (interface is the oval window)
 3. electrical signal to the brain (interface is the haircells attached to the basilar membrane)
- A sinusoid (or sine wave) is given by
$$
    x(t) = A\cos(\Omega t + \Phi)\ ,
$$
and it an extremely important building block (or atom) in analysing and manipulating sound.
- Assuming that striking a bar can be modelled as compressing a spring, the bar will vibrate sinusoidally.

## Complex numbers
In the next 20 minutes, you will learn
- that the equation
$$
    x^2+1=0
$$
has two solutions
- what a complex number is
- how you add and multiply complex numbers

### The need for complex numbers
While the **linear** equation
$$
    x + 1 = 0
$$
can easily be solved, the simple **quadratic** equation
$$
    x^2 + 1 = 0
$$
was in high school said to have **no** solution since its descriminant was negative.

```
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

nData = 100
x = np.linspace(-2,2,nData)
y = x**2+1
plt.plot(x,y,linewidth=2)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim((-1,5))
plt.grid(True);
```

However, the quadratic equation can in fact be solved by using **complex numbers**.

```
YouTubeVideo('T647CGsuOVU') #5:47
```

Rearranging our simple **quadratic** equation gives
$$
    x^2 = -1
$$
which allows us to write the solution as
$$
    x = \pm\sqrt{-1} = \pm j
$$
where
$$
    j = \sqrt{-1}
$$
is the **imaginary unit**. This unit also satisfies that
$$
    j^2 = \sqrt{-1}^2 = -1\ .
$$

Note that
- **engineers** normally use the symbol $j$ for the imaginary unit
- **mathematicians** normally use the symbol $i$ for the imaginary unit.

Let us now consider the quadratic equation
$$
    x^2 + 2x + 5 = 0\ .
$$

We know from high school that the solutions to the general quadratic
$$
    ax^2 + bx + c = 0\ ,\qquad\text{for }a\neq0
$$
have the form
$$
    x = \frac{-b\pm\sqrt{d}}{2a}
$$
where $d$ is the **discriminant** given by
$$
    d = b^2-4ac\ .
$$

We obtain
$$
    d = 4-20 = -16
$$
so that
$$
    x = \frac{-2\pm\sqrt{-16}}{2} = -1\pm\frac{1}{2}\sqrt{-1\cdot 4^2} = -1\pm 2\sqrt{-1} = -1\pm 2j\ .
$$
Thus, the **complex numbers** $-1+2j$ and $-1-2j$ are the solutions.

### The complex number
A **complex number** can be written as
$$
    z = a + jb
$$
where
- $a = \text{Re}\{z\}$ is the **real** part
- $b = \text{Im}\{z\}$ is the **imaginary** part.

A complex number can be depicted in the **complex plane** which is a 2D coordinate system.

#### The complex conjugate
The **complex conjugate** of a complex number $z$ is
$$
    z^* = a - jb\ .
$$
Thus, the conjugation operator ${}^*$ changes the sign of imaginary part, but not the real part.

#### Addition of complex numbers
Assume we have the two complex numbers
\begin{align}
    z_1 &= a_1+jb_1\\
    z_2 &= a_2+jb_2\ .
\end{align}

The **sum** of these two numbers is then
$$
    z = z_1 + z_2 = a_1+jb_1 + a_2+jb_2 = (a_1+a_2) + j(b_1+b_2).
$$

Thus, the real and imaginary part of of $z=a+jb$ are simply
\begin{align}
    a &= a_1 + a_2\\
    b &= b_1 + b_2\ .
\end{align}

Note that
\begin{align}
    z_1 + z_1^* &= 2a_1 + 0j = 2\text{Re}(z_1)\\
    z_1 - z_1^* &= 0 + 2jb_1 = 2\text{Im}(z_1)\ .
\end{align}

#### Multiplication of complex numbers
Assume we have the two complex numbers
\begin{align}
    z_1 &= a_1+jb_1\\
    z_2 &= a_2+jb_2\ .
\end{align}

The **product** of these two numbers is then
$$
    z = z_1z_2 = (a_1+jb_1)(a_2+jb_2) = (a_1a_2-b_1b_2) + j(a_1b_2+b_1a_2).
$$

Thus, the real and imaginary part of of $z=a+jb$ are
\begin{align}
    a &= (a_1a_2-b_1b_2)\\
    b &= (a_1b_2+b_1a_2)\ .
\end{align}

Note that
$$
    z_1z_1^* = (a_1a_1-b_1(-b_1)) +j(a_1b_1-b_1a_1) = a_1^2+b_1^2 = \text{Re}(z_1)^2+\text{Im}(z_1)^2
$$

### Summary
- Complex numbers were originally invented to solve algebraic equations (e.g., the cubic equation)
- The imaginary unit is $j=\sqrt{-1}$
- A **complex number** $z$ consists of a real part $a$ and imaginary part $b$, and is written as
$$
    z = a+jb\ .
$$
- The **complex conjugate** of $z$ is
$$
    z^* = a-jb\ .
$$
- It is much easier to add two complex numbers than it is to multiply them.

### Additional information on complex numbers
If you want to know more about complex numbers (e.g., its history), you can find some nice videos here:

https://www.youtube.com/playlist?list=PLiaHhY2iBX9g6KIvZ_703G3KJXapKkNaF

### Exit Question
Let
\begin{align}
    z_1 &= a_1+jb_1 =  2+3j\\
    z_2 &= a_2+jb_2 = -1-2j\ .
\end{align}

By hand, please calculate
\begin{align}
    z_1 + z_2 &= \\
    z_1 - z_2 &= \\
    z_1 + z_1^* &= \\
    z_2 - z_2^*+2z_1 &= \\
    z_1z_2^* &=\\
    z_1^2+z_2^*z_1 &=
\end{align}
Check the results with your neighbours.

---
**Tip:** Use the rules
\begin{align}
    z_1 + z_2 &= (a_1+a_2) + j(b_1+b_2)\\
     z_1z_2 &= (a_1a_2-b_1b_2) + j(a_1b_2+b_1a_2)\ .
\end{align}

## Phasors
In the next 20 minutes, you will learn
- how a complex number can be written in a **polar form**
- why the polar form makes multiplications much easier
- what a **phasor** is
- how a phasor is related to a **real sinusoid**

### The polar (or exponential) form of a complex number
As for 2D vectors, we can also write a complex number in terms of its **magnitude** $r$ and **angle** $\psi$. We have
\begin{align}
    a &= r\cos\psi\\
    b &= r\sin\psi\ .
\end{align}
Thus,
$$
    z = a + jb = r\left(\cos\psi + j\sin\psi\right) = r\mathrm{e}^{j\psi}
$$
where the last equality follows from **Euler's formula**.

#### Euler's formula
Given by
$$
    \mathrm{e}^{j\psi} = \cos\psi + j\sin\psi\ .
$$
- A very important formula used everywhere in science and engineering
- Simplifies notation and mathematical manipulations
- Its real and imaginary parts are a cosine and a sine, respectively, i.e.,
\begin{align}
    \text{Re}(\mathrm{e}^{j\psi}) &= \cos\psi\\
    \text{Im}(\mathrm{e}^{j\psi}) &= \sin\psi\ .
\end{align}

#### The complex conjugate
The **complex conjugate** of a complex number
$$
    z=r \mathrm{e}^{j\psi}
$$
is
$$
    z^* = r \mathrm{e}^{-j\psi}\ .
$$
Thus, the conjugation operator ${}^*$ changes the sign of the angle, but not the magnitude.

#### Multiplication of complex numbers
Multiplication of complex numbers is much easier when the polar form is used. Let
\begin{align}
    z_1 &= a_1+jb_1 = r_1 \mathrm{e}^{j\psi_1}\\
    z_2 &= a_2+jb_2 = r_2 \mathrm{e}^{j\psi_2}\ .
\end{align}

The **product** of these two numbers is then
$$
    z = z_1z_2 = r_1 \mathrm{e}^{j\psi_1}r_2 \mathrm{e}^{j\psi_2} = r_1 r_2 \mathrm{e}^{j\psi_1}r_2 \mathrm{e}^{j\psi_2} = r_1 r_2 \mathrm{e}^{j(\psi_1+\psi_2)}
$$
where we used $a^na^m = a^{n+m}$ to get the last equation.

Thus, to multiply two complex numbers we
- multiply their magnitudes
- add their angles

Note that **divisions** can be calculated as multiplications since
$$
    \frac{z_1}{z_2} = z_1\frac{1}{z_2} = z_1 z_2^{-1}
$$
and
$$
    z_2^{-1} = \frac{1}{r_2}\mathrm{e}^{-j\psi_2}\ .
$$

#### Converting between the rectangular and polar forms
We have seen that a complex number $z$ can be written as
$$
    z = a+jb = r\mathrm{e}^{j\psi}\ .
$$

We can convert from the polar coordinates $(r,\psi)$ to the rectangular coordinates $(a,b)$ via
\begin{align}
    a &= r\cos\psi\\
    b &= r\sin\psi\ .
\end{align}

We can convert from the rectangular coordinates $(a,b)$ to the polar coordinates $(r,\psi)$ via
\begin{align}
    r &= \sqrt{a^2+b^2}\\
    \psi &= \mathrm{arctan2}(b,a)\ .
\end{align}

### The phasor
We have previously looked at the sinusoid
$$
    x(t) = A\cos(\Omega t + \Phi)\ .
$$

Based on what we know about Euler's formula and complex numbers, we can now also write $x(t)$ as
$$
    x(t) = \text{Re}\left[A\exp(j(\Omega t +\Psi))\right]
$$
since (from Euler's formula)
$$
    A\exp(j(\Omega t +\Psi)) = A\cos(\Omega t +\Psi)+jA\sin(\Omega t +\Psi)\ .
$$
This time-varying complex number is called a **phasor** or a **complex sinusoid**.

Note that
- using the phasor instead of the real sinusoid makes life much easier (you will see this later in the course)
- even though we work with the phasor, we can always come back to the real sinusoid by taking the real part of the phasor

### Summary
- The **polar form** of a complex number $z=a+jb$ is
$$
    z = r\mathrm{e}^{j\psi}
$$
where the magnitude $r$ and angle $\psi$ are given by
\begin{align}
    r &= \sqrt{a^2+b^2}\\
    \psi &= \mathrm{arctan2}(b,a)\ .
\end{align}
- Multiplications (and divisions) are much easier when using the polar form.
- A **phasor** is a complex sinusoid given by
$$
    z(t) = A\exp(j(\Omega t +\Psi))\ ,
$$
and its real part is a real sinusoid, i.e.,
$$
    x(t) = \text{Re}(z(t)) = A\cos(\Omega t +\Psi)\ .
$$