### Instructions

When running the notebook the first time, make sure to run all cells before making changes in the notebook. Hit Shift + Enter to run the selected cell or, in the top menu, click on: `Kernel` > `Restart Kernel and Run All Cells...` to rerun the whole notebook. If you make any changes in a cell, rerun that cell.

If you make any changes in a coding cell, rerun the notebook by `Run` > `Run Selected Cell and All Below`

```python
# Importing libraries and dependencies
import sys
sys.path.append('python/')
import NGC5533_functions as funcNGC5533        # Functions for the components of NGC 5533
import NGC5533_traced_curves as dataNGC5533    # Traced data of Galaxy NGC 5533
import NGC5533_fitting as fitNGC5533           # Fitting parameters for best fit values
import dataPython as dp
import numpy as np
import matplotlib.pyplot as plt                # Plotting
%matplotlib inline
from ipywidgets import interactive, fixed, FloatSlider, HBox, Layout, Button, Label, Output, VBox   # Widget
from IPython.display import display, clear_output
from IPython.display import Javascript
import scipy.stats as stats
import warnings
warnings.filterwarnings("ignore")              # Ignore warnings
```

# Interactive Widget with Variable Components - Galaxy: NGC 5533

In the following activity, you can adjust the ingredients of a galaxy and investigate how the total velocity of stars and gases change.

### Measured data points

Import the measured data points for the NGC 5533 galaxy. These are measured velocities of stars and gas in the galaxy as a function of radius. The data points were traced from Figure 4. in Noordermeer (2008).

```python
data = dp.getXYdata_wXYerr('data/NGC5533/noord-120kpc-datapoints.txt')
r_dat = np.asarray(data['xx'])
v_dat = np.asarray(data['yy'])
v_err0 = np.asarray(data['ex'])
v_err1 = np.asarray(data['ey'])
```

### Components

The stars and gas in a galaxy can be categorized into components depending on the shape of the distribution. These components are then added in quadrature to calculate the total velocity or the rotation curve of a galaxy. In the case of NGC 5533, we can define a central supermassive black hole, a central bulge, a flat disk, a mostly atomic gas and a dark matter halo component.

>__Total velocity__:

    \begin{equation}
    v_{total}(r) = \sqrt{v_{gas}^2 + \Upsilon _{bulge} v_{bulge}^2 + \Upsilon _{disk} v_{disk}^2 + v_{halo}^2}
    \end{equation}

Lets import the relevant functions or traced curves from the `NGC5533_functions.py` library below:

```python
def blackhole(r,Mbh):
    return funcNGC5533.bh_v(r,Mbh,load=False)

def bulge(r,bpref):
    return bpref*funcNGC5533.b_v(r,load=True)

def disk(r,dpref):
    return dpref*funcNGC5533.d_thief(r)

def gas(r,gpref):
    return gpref*funcNGC5533.g_thief(r)

def halo(r,rc,rho0):
    return funcNGC5533.h_v(r,rc,rho0,load=False)

# Total velocity containing all components
def total_all(r,Mbh,bpref,dpref,gpref,rc,rho0):
    total = np.sqrt(blackhole(r,Mbh)**2
                    + bulge(r,bpref)**2
                    + disk(r,dpref)**2
                    + gas(r,gpref)**2
                    + halo(r,rc,rho0)**2)
    return total

# Total velocity of baryonic or luminous matter (no dark matter component)
def total_bary(r,Mbh,bpref,dpref,gpref):
    total = np.sqrt(blackhole(r,Mbh)**2
                    + bulge(r,bpref)**2
                    + disk(r,dpref)**2
                    + gas(r,gpref)**2)
    return total
```

### Parameters

The scaling parameters for each components can be found by fitting the total velocity to the measured data points. Import these fitting parameters for our widget from the _NGC5533\_fitting.py_ library.

```python
best_Mbh = fitNGC5533.f_Mbh
best_bpref = fitNGC5533.f_bpref
best_dpref = fitNGC5533.f_dpref
best_gpref = fitNGC5533.f_gpref
best_rc = fitNGC5533.f_rc
best_rho0 = fitNGC5533.f_rho00
```

### Define plotting function and sliders for widget

The interactive widget is defined with six adjustable sliders that controls the parameters of each component of a galaxy.

```python
# Plotting function
def f(Mbh,bpref,dpref,gpref,rc,rho0):

    # Define radius
    r = np.linspace(np.min(r_dat),np.max(r_dat),1000)

    # Plot
    plt.figure(figsize=(11,7))
    plt.xlim(0,np.max(r_dat)+0.2)
    plt.ylim(0,np.max(v_dat)+100)

    plt.errorbar(r_dat,v_dat,yerr=v_err1,fmt='bo',label='Data')     # Measured data points
    plt.plot(r,blackhole(r,Mbh),label=("Central Supermassive Black Hole"),color='black') # Black hole component
    plt.plot(r,bulge(r,bpref),label=("Bulge"),color='orange')       # Bulge component
    plt.plot(r,disk(r,dpref),label=("Disk"),color='purple')         # Disk component
    plt.plot(r,gas(r,gpref),label=("Gas"),color='blue')             # Gas component
    plt.plot(r,halo(r,rc,rho0),label=("Halo"),color='green')        # Dark matter halo component
    plt.plot(r,total_all(r,Mbh,bpref,dpref,gpref,rc,rho0),label=("Total Curve"),color='red')    # Total velocity with dark matter
    plt.plot(r,total_bary(r,Mbh,bpref,dpref,gpref),label=("Luminous Matter"),linestyle='--')    # Total velocity without dark matter
    plt.fill_between(r,dataNGC5533.greyb_bottom(r),dataNGC5533.greyb_top(r),color='#dddddd',label="Confidence Band")    # Confidence band
    plt.title("Interactive Rotation Curve - Galaxy: NGC 5533",fontsize=16)
    plt.xlabel("Radius (kpc)")
    plt.ylabel("Velocity (km/s)")
    plt.legend(bbox_to_anchor=(1,1), loc="upper left")              # Put legend outside of the plot

    # Chi squared and reduced chi squared
    # Residuals
    residuals = v_dat - total_all(r_dat,Mbh,bpref,dpref,gpref,rc,rho0)
    # Error
    error = np.sqrt(v_err1**2 + dataNGC5533.band**2)
    # Chi squared
    chisquared = np.sum(residuals**2/error**2)
    # Degrees of freedom
    dof = len(r_dat) - 6                 # number of degrees of freedom = number of observed data - number of fitting parameters
    # Reduced chi squared
    reducedchisquared = chisquared / dof

    # Annotation
    props = dict(boxstyle='round', facecolor='white', alpha=0.5)
    plt.text(80,373,r"$\chi^2$: {:.5f}".format(chisquared)+'\n'+r"Reduced $\chi^2$: {:.5f}".format(reducedchisquared),bbox=props,size=10)
    props = dict(boxstyle='round', facecolor='white', alpha=0.5)
    plt.annotate('Data source: E. Noordermeer. The rotation curves of flattened Sérsic bulges. MNRAS,385(3):1359–1364, Apr 2008',
            xy=(0, 0), xytext=(0,5),
            xycoords=('axes fraction', 'figure fraction'),
            textcoords='offset points',
            size=10, ha='left', va='bottom')

    plt.show()
```

```python
# Widget appearance
style = {'description_width': 'initial'}
layout = {'width':'600px'}

# Define widget sliders
# Mass of central supermassive black hole
Mbh = FloatSlider(min=0, max=5e9, step=1e8,
                value=best_Mbh,
                description='Black Hole Mass [$M_{\odot}$]',
                readout_format='.2e',
                orientation='horizontal',
                style=style, layout=layout)

# Bulge prefactor (multiplier)
bpref = FloatSlider(min=0, max=5, step=0.1,
                    value=round(best_bpref,1),
                    description='Bulge Prefactor',
                    readout_format='.2f',
                    orientation='horizontal',
                    style=style, layout=layout)

# Disk prefactor (multiplier)
dpref = FloatSlider(min=0, max=5, step=0.1,
                    value=round(best_dpref,1),
                    description='Disk Prefactor',
                    readout_format='.2f',
                    orientation='horizontal',
                    style=style, layout=layout)

# Gas prefactor (multiplier)
gpref = FloatSlider(min=0, max=5, step=0.1,
                    value=round(best_gpref,1),
                    description='Gas Prefactor',
                    readout_format='.2f',
                    orientation='horizontal',
                    style=style, layout=layout)

# Core radius of dark matter halo
rc = FloatSlider(min=0, max=5, step=0.1,
                 value=round(best_rc,1),
                 description='Halo Core Radius [kpc]',
                 readout_format='.2f',
                 orientation='horizontal',
                 style=style, layout=layout)
#rc = fixed(best_rc)      # If we need the core radius to be fixed

# Central mass density of dark matter halo
rho0 = FloatSlider(min=0, max=1e9, step=1e7,
                    value=best_rho0,
                    description=r'Halo Central Mass Density [$M_{\odot} / kpc^3$]',
                    readout_format='.2e',
                    orientation='horizontal',
                    style=style, layout=layout)

# Interactive widget
def interactive_plot(f):
    interact = interactive(f, Mbh = Mbh,
                           bpref = bpref,
                           dpref = dpref,
                           gpref = gpref,
                           rc = rc,
                           rho0 = rho0,
                           continuous_update=False)
    return interact

# Button to revert back to Best Fit
button = Button(
    description="Best Fit",
    button_style='warning',
    icon='check')
out = Output()

# Define what happens when we click on the "Best Fit" button
def on_button_clicked(_):
    Mbh.value = best_Mbh
    bpref.value = round(best_bpref,1)
    dpref.value = round(best_dpref,1)
    gpref.value = round(best_gpref,1)
    rc.value = round(best_rc,1)
    rho0.value = best_rho0

button.on_click(on_button_clicked)
```

Using the sliders below, answer the following questions:

Activity 1)

>Can you get the Reduced $\chi^2$ close to zero?

Activity 2)

>Can you get the Reduced $\chi^2$ close to one?

Activity 3)

>Remove the Dark Matter by changing both Dark Matter parameters (halo core radius and halo central mass density) to zero using the sliders. Are you able to scale the other components (central black hole, bulge, disk, gas) to have the total curve aligned with the measured data points (Reduced $\chi^2$ close to one)?

In a strictly statistical sense, a Reduced $\chi^2$ of zero represents a perfect fit. However, a Reduced $\chi^2$ much less than 1 often indicates that more parameters were used than represented by the physical system. We would be simply connecting the measured data points.

In general, the gas component of a galaxy can be measured with great accuracy. It is made of mostly atomic gas. In most cases, the measured gas component is multiplied by 1.33 to account for helium. In spite of that, the gas component can also be adjusted in this widget. Here, we allow you to scale each component as you please. See how 'good' of a fit you can get!

```python
#NBVAL_IGNORE_OUTPUT
#Because the figure doesn't save to the repository correctly.
# Widget
VBox([button,out,interactive_plot(f)])
```

    VBox(children=(Button(button_style='warning', description='Best Fit', icon='check', style=ButtonStyle()), Outp…

### Slider key

The prefactor for the **black hole** is the square root of its mass.
Because it is more physically meaningful and not computationally intensive, we have you control the mass of the black hole rather than the prefactor itself.
The units of the black hole's mass are solar masses (multiply the value by the mass of our Sun. Note that 'e+9' equates to a billion--that's a billion solar masses!).

The prefactors for the **bulge** and the **disk** relate to the mass-to-light ratio.
A prefactor of one means that you are using the M/L we generally expect from stars. Increasing the prefactor means increasing the expected mass for a given amount of light.

The halo central mass density behaves as a prefactor for the **Dark Matter "halo"** in and around the galaxy.
Its units are solar masses per cubic kiloparsec and, in this fit, is on the scale of hundreds of millions.
This represents how much Dark Matter we think there is overall in the center of a galaxy.

The **gas** prefactor is similar to the bulge and disk prefactors.
Rather than being for a stellar M/L, it represents the amount of radiation emitted by interstellar gas.
A value of 1 means using the expected amount of radiation. Increasing the prefactor means increasing the expected mass for a given amount of radiation (note that this is why the gas component is kept static during fitting, as increasing or decreasing the gas prefactor would be digressing from the known physical laws of H1 emissions).

### References:

>Jimenez, Raul, Licia Verde, and S. Peng Oh. **Dark halo properties from rotation curves.** _Monthly Notices of the Royal Astronomical Society_ 339, no. 1 (2003): 243-259. https://doi.org/10.1046/j.1365-8711.2003.06165.x.

>Noordermeer, E., & Van Der Hulst, J. M. (2007). **The stellar mass distribution in early-type disc galaxies: Surface Photometry and bulge–disc decompositions.** Monthly Notices of the Royal Astronomical Society, 376(4), 1480–1512. https://doi.org/10.1111/j.1365-2966.2007.11532.x

>Noordermeer, E. (2008), **The rotation curves of flattened Sérsic bulges**. Monthly Notices of the Royal Astronomical Society, 385: 1359-1364. https://doi.org/10.1111/j.1365-2966.2008.12837.x

>Taylor, John Robert. __An Introduction to Error Analysis: The Study of Uncertainties in Physical Measurements.__ 2nd ed. United States of America: University Science Books, 1997.


# Exam 2019 - solutions

## Question 1

### (a) **definition of a Normal form game**:

An \\(N\\) player normal form game consists of:

- A finite set of $N$ players
- Strategy spaces for the players: $\{S_1,S_2,S_3,\dots,S_N\}$;
- Payoff functions for the players: $u_i:S_1\times S_2\dots\times S_N\to\mathbb{R}$

[2]

### (b) **Identifying best responses**

We have:

- $u_r((1, 0), (y, 1-y))= 30y + 6 (1 - y)=24y+6$ and $u_r((0, 1), (y, 1-y))= 12y + 30(1 - y)=30-18y$
- $u_c((x, 1 - x), (1, 0))=12x+42(1-x)=42-30x$ and $u_c((x, 1- x), (0, 1))=30x+12(1-x)=18x+12$

```python
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.figure()
ys = [0, 1]
plt.plot(ys, [30 * y + 6 * (1 - y) for y in ys], label="$u_r((1, 0), (y, 1-y))$")
plt.plot(ys, [12 * y + 30 * (1 - y) for y in ys], label="$u_r((0, 1), (y, 1-y))$")
plt.legend()
plt.xlabel("$y$")
plt.title("Utility to row player");
```

[1]

```python
plt.figure()
xs = [0, 1]
plt.plot(xs, [12 * x + 42 * (1 - x) for x in xs], label="$u_c((x, 1 - x), (1, 0))$")
plt.plot(xs, [30 * x + 12 * (1 - x) for x in xs], label="$u_c((x, 1 - x), (0, 1))$")
plt.legend()
plt.xlabel("$x$")
plt.title("Utility to column player");
```

The points of intersection:

For the row player:

```python
import sympy as sym
x, y = sym.symbols('x, y')

sym.solveset(sym.Eq(24 * y + 6, 30 - 18 * y), y)
```

    {4/7}

For the column player:

```python
sym.solveset(sym.Eq(42 - 30 * x, 18 * x + 12), x)
```

    {5/8}

[1]

The best responses are then given by:

$$
\sigma_r^* = \begin{cases}
(0,1)&\text{ if }y< 4/7\\
(1,0)&\text{ if }y> 4/7\\
\text{indifferent}&\text{ otherwise}
\end{cases}
$$

$$
\sigma_c^* = \begin{cases}
(1,0)&\text{ if }x< 5/8\\
(0,1)&\text{ if }x> 5/8\\
\text{indifferent}&\text{ otherwise}
\end{cases}
$$

[1]

### **(c) Considering a third strategy**

$u_c((x, 1 - x), (0, 0, 1))=22x+6(1-x)=6+16x$

```python
plt.figure()
xs = [0, 1]
plt.plot(xs, [12 * x + 42 * (1 - x) for x in xs], label="$u_c((x, 1 - x), (1, 0))$")
plt.plot(xs, [30 * x + 12 * (1 - x) for x in xs], label="$u_c((x, 1 - x), (0, 1))$")
plt.plot(xs, [16 * x + 6 for x in xs], label="$u_c((x, 1 - x), (0, 0, 1))$")
plt.legend()
plt.xlabel("$x$")
plt.title("Utility to column player");
```

[2]

We see that the third column is never a best response and so the best responses remain unchanged.

[4]

### (d) Definition of best response polytope

For a two player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the row/column player best response polytope $\mathcal{P}$/$\mathcal{Q}$ is defined by:

$$
\mathcal{P} = \left\{x\in\mathbb{R}^{m}\;|\;x\geq 0; xB\leq 1\right\}
$$

$$
\mathcal{Q} = \left\{y\in\mathbb{R}^{n}\;|\; Ay\leq 1; y\geq 0 \right\}
$$

[2]

### (e) The Lemke-Howson algorithm

For a nondegenerate 2 player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the following algorithm returns a nash equilibrium:

1. Start at the artificial equilibrium: $(0, 0)$
2. Choose a label to drop.
3. Remove this label from the corresponding vertex by traversing an edge of the corresponding polytope to another vertex.
4. The new vertex will now have a duplicate label in the other polytope. Remove this label from the vertex of the other polytope and traverse an edge of that polytope to another vertex.
5. Repeat step 4 until the pair of vertices is fully labelled.

[4]

### (f) Using the Lemke-Howson algorithm

The following is code used to carry out the various pivoting operations (students would do this by hand).

```python
import numpy as np
import nashpy as nash
row_tableau = np.array([[12, 42, 1, 0, 0, 1],
                        [30, 12,  0, 1, 0, 1],
                        [22, 6,  0, 0, 1, 1]])
col_tableau = np.array([[1, 0, 30, 6, 36, 1],
                        [0, 1, 12, 30, 18, 1]])

```

Let us drop label 0:

```python
dropped_label = nash.integer_pivoting.pivot_tableau(row_tableau, column_index=0)
row_tableau
```

    array([[   0, 1116,   30,  -12,    0,   18],
           [  30,   12,    0,    1,    0,    1],
           [   0,  -84,    0,  -22,   30,    8]])

We have picked up label 3, let us drop label 3 from the column tableau:

```python
dropped_label = nash.integer_pivoting.pivot_tableau(col_tableau, column_index=3)
col_tableau
```

    array([[ 30,  -6, 828,   0, 972,  24],
           [  0,   1,  12,  30,  18,   1]])

We have picked up label 1, let us drop label 1 from the row tableau:

```python
dropped_label = nash.integer_pivoting.pivot_tableau(row_tableau, column_index=1)
row_tableau
```

    array([[     0,   1116,     30,    -12,      0,     18],
           [ 33480,      0,   -360,   1260,      0,    900],
           [     0,      0,   2520, -25560,  33480,  10440]])

We have picked up label 2 so let us drop label 2 from the col tableau:

```python
dropped_label = nash.integer_pivoting.pivot_tableau(col_tableau, column_index=2)
col_tableau
```

    array([[   30,    -6,   828,     0,   972,    24],
           [ -360,   900,     0, 24840,  3240,   540]])

The row tableau has labels: $\{2, 3\}$ and the column tableau has labels: $\{0, 1, 4\}$ thus we have a fully labeled vertex pair.

The vertex pairs are given by:

\\[
    \left((900/33480, 18/1116), (24/828, 540/24840, 0)\right)
\\]

which gives Nash equilibria:

\\[
    \left((5/8, 3/8), (4/7, 3/7, 0)\right)
\\]

[7]

This confirms the previous result as this corresponds to a pair of best response.

[1]

Some code to confirm the numeric calculations:

```python
A = np.array([[30, 6, 36], [12, 30, 18]])
B = np.array([[12, 30, 22], [42, 12, 6]])
game = nash.Game(A, B)
game.lemke_howson(initial_dropped_label=0)
```

    (array([0.625, 0.375]), array([0.57142857, 0.42857143, 0.        ]))

##  Question 2

### (a) Definition of repeated game

Given a two player game $(A,B)\in{\mathbb{R}^{m\times n}}^2$, referred to as a **stage** game, a $T$-stage repeated game is a game in which players play that stage game for $T > 0$ periods. Players make decisions based on the full history of play over all the periods.

[2]

### (b) Definition of a strategy in a repeated game

Given a two player stage game $(A,B)\in{\mathbb{R}^{m\times n}}^2$, repeated to give a $T$-stage repeated game. A strategy is a mapping from the entire history of play to an action of the stage game:

$$
\bigcup_{t=0}^{T-1}H(t)\to a
$$

where:

- $H(t)$ is the history of player of **both** players up until stage $t$ ($H(0)=(\emptyset, \emptyset)$)
- $a$ is an action (for either player) of the **stage** game

[2]

### (c) Listing strategy spaces

$S_1\{r_1, r_2\}$, $S_2=\{c_1, c_2, c_3\}$ and $T=2$

$$\{(\emptyset, \emptyset), (r_1, c_1), (r_1, c_2), (r_1, c_3), (r_2, c_1), (r_2, c_2), (r_2, c_3)\}$$

[3]

### **(d) Pure Nash equilibria**

The pure Nash equilibria:

$$A = \begin{pmatrix}\underline{1} & \underline{4} & \underline{-1}\\-1 & 0 & -2\\\end{pmatrix}
\qquad
B = \begin{pmatrix}-2 & \underline{5} & \underline{5}\\\underline{18} & -1 & -2\\\end{pmatrix}$$

Thus, for our example we have the four Nash equilibria:

- $(r_1r_1, c_2c_2)$ with utility: (8, 10). [1]
- $(r_1r_1, c_2c_3)$ with utility: (3, 10). [1]
- $(r_1r_1, c_3c_2)$ with utility: (3, 10). [1]
- $(r_1r_1, c_3c_3)$ with utility: (-2, 10). [1]

### **(g) Not stage Nash, equilibria**

Consider the following two strategies:

1. For the row player:

   $$(\emptyset, \emptyset) \to r_2$$
   $$(r_2, c_1) \to r_1$$
   $$(r_2, c_2) \to r_1$$
   $$(r_2, c_3) \to r_1$$

   [2]

2. For the column player:

   $$(\emptyset, \emptyset) \to c_1$$
   $$(r_1, c_1) \to c_3$$
   $$(r_2, c_1) \to c_2$$

   [2]

This is a Nash equilibria because:

1. If the row player deviates, they would only be rational to do so in the first stage, if they did they would gain 2 in that stage but lose 5 in the second stage. Thus they have no incentive to deviate.
2. If the column player deviates, they would only do so in the first stage and gain no utility.

[1]

### (f) The Centipede game transformed to a normal form game

According to the definition of a repeated game, the strategy spaces for both players is:

\\[
    S_1 = S_2 = \{CC, CS, SC, SS\}
\\]

[3]

Using S for "stop" and "C" for continue, where the first letter corresponds to what the player will do at their first decision and the second letter corresponds to what the player will do at their second decision.

Using this, and reading from the diagram, the payoff matrix is obtained.

[3]

### (g) Pure Nash equibria:

\\[A =
          \begin{pmatrix}
              \underline{4} & 3 & 1 & 1\\
              \underline{4} & 4 & 1 & 1\\
              2 & 2 & \underline{2} & \underline{2}\\
              2 & 2 & \underline{2} & \underline{2}\\
          \end{pmatrix}
          \qquad
          B =
          \begin{pmatrix}
              4 & \underline{5} & 3 & 3\\
              2 & 2 & \underline{3} & \underline{3}\\
              \underline{0} & \underline{0} & \underline{0} & \underline{0}\\
              \underline{0} & \underline{0} & \underline{0} & \underline{0}\\
          \end{pmatrix}
\\]

There are 4 pure Nash equilibria:

\\[\{(SC, SC), (SC, SS), (SS, SC), (SS, SS)\}\\]

which all imply that both players stop the game at their first opportunity.

[3]

##  Question 3

### (a) Definition of a Prisoner's Dilemma (bookwork)

$$
A =
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}\qquad
B =
\begin{pmatrix}
    R & T\\
    S & P
\end{pmatrix}
$$

with the following constraints:

$$T > R > P > S$$
$$2R > T + S$$

[2]

### (b) Finding valid Prisoner's Dilemmas

(i)

For $A, B$ to be valid we need:

$$
\begin{pmatrix}
    3 & S\\
    6 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}
$$

which gives: $R=3, T=6$

furthermore:

$$
\begin{pmatrix}
    3 & T\\
    -1 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & T\\
    S & P
\end{pmatrix}
$$

[2]

which gives: $R=3, S=-1$

Thus we have (R, S, P, T) = (3, -1, 1, 6) which also follows the two required inequalities:

$$T > R > P > S \Leftrightarrow 6 > 3 > 1 > -1$$
$$2R > T + S \Leftrightarrow 6 > 5$$

[2]

(ii)

For $A, B$ to be valid we need:

$$
\begin{pmatrix}
    2 & S\\
    -2 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}
$$

which gives: $R=2, T=-2, P=1$

We immediately see that $R > T$ so this cannot be a Prisoner's Dilemma.

[4]

### (c) Different form of PD

For $A, B$ to be valid we need:

$$
\begin{pmatrix}
    b - c & c\\
    b     & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}
$$

thus \\(P=1\\), \\(S=c\\), \\(T=b\\), \\(R=b-c\\). The constraints imply: [1]

- \\(T > R\\): \\(c > 0\\) [1]
- \\(R > P\\): \\(b > c\\) [1]
- \\(P > S\\): \\(c < 1\\) [1]
- \\(2R > T + S\\): \\(b > 3c\\) [1]

### (d) Modified formula for reactive players

A theorem from the notes states the steady state probabilities of being in states \\((CC, CD, DC, DD)\\) is given by:

$$
\pi=(s_1s_2, s_1(1-s_2), (1-s_1)s_2, (1-s_1)(1-s_2))
$$

Using this with the new utilities gives the result immediately.

[3]

### (e) Computing specific case

For \((b, c)=(2, 1/2)\), the given formula is:

\\[
   s_1s_2\times 3/2 +  s1(1-s_2)/2 +  (1-s_1)s_2 \times 2 + (1-s_1)(1-s_2)
\\]

which gives:

\\[
    3s_1s_2/2 +  s1/2-s_1s_2/2 +  2s_2 - 2s_1s_2 + 1 -s_1 - s_2 + s_1s_2 = s_1s_2(3/2-1/2 - 2 + 1) + s_1(1/2-1) + s_2(2-1) + 1
\\]

which corresponds to:

\\[
u = s_2 - s_1 / 2 + 1
\\]

[3]

```python
s = sym.symbols("s:2")

expr = s[0] * s[1] * sym.S(3) / 2 + s[0] * (1 - s[1]) / 2 + (1 - s[0]) * s[1] * 2 + (1 - s[0]) * (1 - s[1])
expr.expand()
```

    -s0/2 + s1 + 1

If \\(q_1=q_2\\) then:

\\[
    r_2 = 0 \Rightarrow s_1=q_2r_1+p_1 \text{ and }s_2=q_2
\\]

[1]

thus the utility is given by:

\\[
    - q_2r_1/2 - p_2/2+ q_2 + 1
\\]

[1]

This is maximised when the first two terms are 0, the player in question controls \\(r_1=p_1 - p_2\\) and \\(p_2\\) thus \\(p_1 = p_2 = 0\\) is optimal behaviour.

This can also be seen intuitevly, if an opponent is playing independently of our actions then we should always defect.

[2]

## Question 4

### (a) Defining equations (bookwork)

The matrix $A$ correspods to the utility of a row player in a game where the row player is a given individual and the column player is the population.

This gives:

$$f_1=ax_1+bx_2\qquad f_2=cx_1+dx_2$$

[1]

or equivalently:

$$f=Ax\qquad \phi=fx$$

thus we have the same equation as before but in matrix notation:

$$\frac{dx}{dt}=x(f-\phi)$$

[1]

### (b) Defining mutated population (bookwork)

Given a strategy vector $x=(x_1, x_2)$, some $\epsilon>0$ and another strategy $y=(y_1, y_1)$, the post entry population $x_{\epsilon}$ is given by:

$$
x_{\epsilon} = (x_1 + \epsilon(y_1 - x_1), x_2 + \epsilon(y_2 - x_2))
$$

[2]

### (c) Defining an evolutionary stable strategy (bookwork)

Given a stable population distribution, $x$ it represents an **Evolutionary Stable Strategy** (ESS) if and only if there exists $\bar\epsilon>0$:

$$u(x, x_{\epsilon})>u(y, x_{\epsilon})\text{ for all }0<\epsilon<\bar\epsilon, y$$

[1]

where $u(x, y)$ corresponds to the fitness of strategy $x$ in population $y$ which is given by:

$$xAy^T$$

[1]

### (d) Proof of result for ESS (bookwork)

**Theorem:**

If $x$ is an ESS, then for all $y\ne x$, either:

1. $u(x,x)>u(y,x)$
2. $u(x,x)=u(y,x)$ and $u(x,y)>u(y,y)$
---

Conversely, if either (1) or (2) holds for all $y\ne x$ then $x$ is an ESS.

[2]

---

**Proof:**

---

If $x$ is an ESS, then by definition:

$$u(x,x_{\epsilon})>u(y,x_{\epsilon})$$

which corresponds to:

$$(1-\epsilon)u(x,x)+\epsilon u(x,y)>(1-\epsilon)u(y,x)+\epsilon u(y,y)$$

- If condition 1 of the theorem holds then the above inequality can be satisfied for \\(\epsilon\\) sufficiently small. If condition 2 holds then the inequality is satisfied.
- Conversely:

    - If $u(x,x) < u(y,x)$ then we can find $\epsilon$ sufficiently small such that the inequality is violated. [1]

    - If $u(x, x) = u(y,x)$ and $u(x,y) \leq u(y,y)$ then the inequality is violated. [1]

### (e) Obtain all ESS for a 2 by 2 game

First step is to identify the Nash equilibria. Identify best responses for the associated two player game $(A, A^T)$:

$$
A=\begin{pmatrix}
\underline{2}&\underline{3}\\
0&1
\end{pmatrix}\qquad
A^T=\begin{pmatrix}
\underline{2}&0\\
\underline{3}&1
\end{pmatrix}
$$

[1]

This immediately gives a single symmetric pure Nash equilibria (as the first strategy dominates the second this is the only Nash equilibria):

$$
(\sigma_r, \sigma_c) = ((1, 0), (1, 0))
$$

some code to verify this:

```python
A = np.array([[2, 3], [0, 1]])
game = nash.Game(A, A.transpose())
list(game.support_enumeration())
```

    [(array([1., 0.]), array([1., 0.]))]

as this strategy obeys the first condition of the theorem it is the evolutionary stable strategy.

[4]

### (f) Discussing the research paper

- (i) This paper looks at GT and cancer: it is a proof of concept sitting in literature that already exists on the subject. The main result is confirming intuitive understanding of proliferation/motility. Increased nutrients implies less motility. This is done in two ways: a theoretic game and a simulation. [3]
- (ii) The game matrix should in fact be: $\begin{pmatrix}b/2 & b - c\\ b & b - c / 2\end{pmatrix}$ [2]
- (iii) The theorem is used in the paper, although it is not done explicitly it is used to implictely describe the stability of the equilibria. [2]
- (iv) Another approach would be to use a Moran process. The main difference with the approach in the paper would be to use a Moran process. This would correspond to considering a finite population size as opposed to the infinite population model used. This would correspond more closely to the simulation model used in the paper. [3]


# Introduction to the Harmonic Oscillator

*Note:* Much of this is adapted/copied from https://flothesof.github.io/harmonic-oscillator-three-methods-solution.html

This week week we are going to begin studying molecular dynamics, which uses classical mechanics to study molecular systems. Our "hydrogen atom" in this section will be the 1D harmomic oscillator.

The harmonic oscillator is a system that, when displaced from its equilibrium position, experiences a restoring force F proportional to the displacement x:

$$F=-kx$$

The potential energy of this system is

$$V = {1 \over 2}k{x^2}$$

These are sometime rewritten as

$$ F=- \omega_0^2 m x, \text{ }  V(x) = {1 \over 2}  \omega_0^2 m {x^2}$$

Where $\omega_0  = \sqrt {{k \over m}} $

In classical mechanics, our goal is to determine the equations of motion, $x(t),y(t)$, that describe our system.

In this notebook we will use sympy to solve an second order, ordinary differential equation.

## 1. Solving differential equations with sympy

Soliving differential equations can be tough, and there is not always a set plan on how to proceed. Luckily for us, the harmonic osscillator is the classic second order diffferential eqations.

Consider the following second order differential equation

$$ay(t)''+by(t)'=c$$

where $y(t)'' = {{{d^2}y} \over {dt^2}}$, and $y(t)' = {{{d}y} \over {dt}}$

We can rewrite this as a homogeneous linear differential equations

$$ay(t)''+by(t)'-c=0$$

The goal here is to find $y(t)$, similar to our classical mechanics problems. Lets use sympy to solve this equation

### Second order ordinary differential equation

First we import the sympy library

```python
import sympy as sym
```

Next we initialize pretty printing

```python
sym.init_printing()
```

Next we will set our symbols

```python
t,a,b,c=sym.symbols("t,a,b,c")
```

Now for somehting new. We can define functions using `sym.Function("f")`

```python
y=sym.Function("y")
y(t)
```

Now, If I want to define a first or second derivative, I can use `sym.diff`

```python
sym.diff(y(t),(t,1)),sym.diff(y(t),(t,2))
```

My differential equation can be written as follows

```python
dfeq=a*sym.diff(y(t),(t,2))+b*sym.diff(y(t),(t,1))-c
dfeq
```

```python
sol = sym.dsolve(dfeq)
sol
```

The two constants $C_1$ and $C_2$ can be determined by setting boundry conditions.
First, we can set the condition $y(t=0)=y_0$

The next intial condition we will set is $y'(t=0)=v_0$

To setup the equality we want to solve, we are using `sym.Eq`. This function sets up an equaility between a lhs aand rhs of an equation

```python
# sym.Eq example
alpha,beta=sym.symbols("alpha,beta")
sym.Eq(alpha+2,beta)
```

Back to the actual problem

```python
y0,v0=sym.symbols("y_0,v_0")
ics=[sym.Eq(sol.args[1].subs(t, 0), y0),
     sym.Eq(sol.args[1].diff(t).subs(t, 0), v0)]
ics
```

We can use this result to first solve for $C_2$ and then solve for $C_1$.
Or we can use sympy to solve this for us.

```python
solved_ics=sym.solve(ics)
solved_ics
```

Substitute the result back into $y(t)$

```python
full_sol = sol.subs(solved_ics[0])
full_sol
```

We can plot this result too. Assume that $a,b,c=1$ and that the starting conditions are $y_0=0,v_0=0$

We will use two sample problems:

* case 1 : initial position is nonzero and initial velocity is zero
* case 2 : initial position is zero and initialvelocity is nonzero

```python
# Print plots
%matplotlib inline
```

#### Initial velocity set to zero

```python
case1 = sym.simplify(full_sol.subs({y0:0, v0:0, a:1, b:1, c:1}))
case1
```

```python
sym.plot(case1.rhs)
sym.plot(case1.rhs,(t,-2,2))
```

#### Initial velocity set to one

```python
case2 = sym.simplify(full_sol.subs({y0:0, v0:1, a:1, b:1, c:1}))
case2
```

```python
sym.plot(case2.lhs,(t,-2,2))
```

## Calculate the phase space

As we will see in lecture, the state of our classical systems are defined as points in phase space, a hyperspace defined by ${{\bf{r}}^N},{{\bf{p}}^N}$. We will convert our sympy expression into a numerical function so that we can plot the path of $y(t)$ in phase space $y,y'$.

```python
case1
```

```python
# Import numpy library
import numpy as np

# Make numerical functions out of symbolic expressions
yfunc=sym.lambdify(t,case1.rhs,'numpy')
vfunc=sym.lambdify(t,case1.rhs.diff(t),'numpy')

# Make list of numbers
tlst=np.linspace(-2,2,100)

# Import pyplot
import matplotlib
import matplotlib.pyplot as plt
# Make plot
plt.plot(yfunc(tlst),vfunc(tlst))
plt.xlabel('$y$')
plt.ylabel("$y'$")
plt.show()
```

### Exercise 1.1

Change the initial starting conditions and see how that changes the plots. Make three different plots with different starting conditions

```python
case1 = sym.simplify(full_sol.subs({y0:5, v0:4, a:1, b:1, c:1}))
case1
```

```python
sym.plot(case1.rhs)
sym.plot(case1.rhs,(t,-1,4))
```

```python
case2 = sym.simplify(full_sol.subs({y0:0, v0:0, a:6, b:7, c:3}))
case2
```

```python
sym.plot(case2.rhs)
sym.plot(case2.rhs,(t,-3,10))
```

```python
case3 = sym.simplify(full_sol.subs({y0:5, v0:1, a:2, b:3, c:6}))
case3
```

```python
sym.plot(case3.rhs)
sym.plot(case3.rhs,(t,-3,10))
```

## 2. Harmonic oscillator

Applying the harmonic oscillator force to Newton's second law leads to the following second order differential equation

$$ F = m a $$

$$ F= - \omega_0^2 m x $$

$$ a = - \omega_0^2 x $$

$$ x(t)'' = - \omega_0^2 x $$

The final expression can be rearranged into a second order homogenous differential equation, and can be solved using the methods we used above

Your goal is determine and plot the equations of motion of a 1D harmomnic oscillator

### Exercise 2.1

1. Use the methodology above to determine the equations of motion $x(t), v(t)$ for a harmonic ocillator
1. Solve for any constants by using the following initial conditions: $x(0)=x_0, v(0)=v_0$
1. Show expressions for and plot the equations of motion for the following cases:
    1. $x(0)=0, v(0)=0$
    1. $x(0)=0, v(0)>0$
    1. $x(0)>0, v(0)=0$
    1. $x(0)<0, v(0)=0$
1. Plot the phasespace diagram for the harmonic oscillator

```python
# Your code here
```

```python

```


# INF-285 / ILI-285
##  Desafío 4
### SCT 2020-1

# Instrucciones

- El desafío es individual, por lo cual se considera que todo el código entregado es de elaboración propia y no ha sido compartido de forma alguna.
- Las consultas sobre el desafío se deben realizar por medio de la plataforma Aula. **No está permitido publicar código en consultas de Aula**.
- El desafío debe ser realizado en Jupyter Notebook (Python3) utilizando este archivo como base.
- Debe utilizar arreglos de ```NumPy``` junto a las funciones y bibliotecas que se ponen a disposición en este archivo.
- Se debe respetar las firmas de las funciones, sus parámetros y retornos. Para eso se incluye un detalle de cada función tanto de las entregadas como las que deberá implementar. En caso de no seguir estas instrucciones, el desafío no podrá evaluarse.
- Se evaluará la correcta utilización de librerias ```NumPy```, ```SciPy```, entre otras, así como la correcta implementación de algoritmos de forma vectorizada.
- Evitar la impresión de mensajes salvo que se solicite en el enunciado.
- El archivo de entrega debe denominarse **ROL-desafio-numero.ipynb**. **De no respetarse este formato existirá un descuento de 50 puntos**
- La fecha de entrega es el **30 de julio a las 10:00 hrs**,  posteriormente existirá un descuento lineal hasta las 11:00 hrs del mismo día.

# Introducción

En este desafío vamos a comparar el rendimiento de la interpolación polinomial utilizando **Intepolación de Lagrange** e **Interpolación Baricéntrica**. Recordar que estamos buscando un polinomio interpolador de la forma:

\begin{equation}
    P(x) = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1}+a_{n}x^{n}= \sum_{i=0}^{n}a_ix^i,
\end{equation}

que se construye conociendo el conjunto de puntos $S=\{(x_0, y_0),(x_1, y_1), \dots, (x_n, y_n)\}$.

## Interpolación de Lagrange

El polinomio interpolador de Lagrange se define como:

\begin{equation*}
    P(x) = y_{0}L_{0}(x) + y_{1}L_{1}(x) + \dots + y_{n} L_{n}(x) = \displaystyle \sum_{k=0}^{n} y_{k} L_{k}(x),
\end{equation*}

con

\begin{equation*}
    L_k(x) = \frac{l_k(x)}{l_k(x_k)} \quad \text{y} \quad l_k(x) = \prod_{i=0, i\neq k}^{n}(x-x_i)= (x-x_1)(x-x_2)\cdots(x-x_{k-1})(x-x_{k+1})\cdots(x-x_n).
\end{equation*}

## Interpolación Baricéntrica

El polinomio interpolador utilizando **Interpolación Baricéntrica** se calcula de la siguiente manera:

\begin{equation*}
    P(x)=l(x)\,\sum_{k=0}^n y_k\,\dfrac{w_k}{(x-x_k)}
        =\dfrac{\displaystyle{\sum_{k=0}^n} y_k\dfrac{w_k}{(x-x_k)}}
        {\displaystyle{\sum_{k=0}^n} \dfrac{w_k}{(x-x_k)}},
\end{equation*}

donde $l(x)=\displaystyle{\prod_{i=0}^n(x-x_i)}$ y $w_k=\dfrac{1}{l_k(x_k)}$.

```python
import time
import numpy as np
import matplotlib.pyplot as plt
```

# Implementación

Implemente la función ```lagrange(x_i, y_i)``` que recibe como parámetros los puntos de interpolación $x_i, y_i$ y retorna el polinomio interpolador $L(x)$.
Asegúrese que su polinomio pueda evaluar arreglos de ```NumPy```.

```python
def lagrange(x_i, y_i):
    """
    Compute Interpolating Polynomial using Lagrange

    Parameters
    -----------
    x_i     : (n, ) array
             Data x_i to interpolate
    y_i     : (n, ) array
             Data y_i to interpolate

    Returns
    -------
    L     : Lambda function
            Lagrange interpolating polynomial
    """
    ...
```

Implemente la función ```barycentric(x_i, y_i)``` que recibe como parámetros los puntos de interpolación $x_i, y_i$ y retorna el polinomio interpolador $B(x)$.
Asegúrese que su polinomio pueda evaluar arreglos de ```NumPy```.

```python
def barycentric(x_i, y_i):
    """
    Compute Interpolating Polynomial using Barycentric interpolation

    Parameters
    -----------
    x_i     : (n, ) array
             Data x_i to interpolate
    y_i     : (n, ) array
             Data y_i to interpolate

    Returns
    -------
    B     : Lambda function
            Barycentric interpolation
    """
    ...
```

# Pruebas

Para que pueda probar sus funciones se ponen a disposición las siguientes funciones:

```python
f1 = lambda x: 2 * x + 3
f2 = lambda x: np.sin(x) ** 3
f3 = lambda x: np.exp(x)
f4 = lambda x: np.exp(-x ** 2)
f5 = lambda x: np.sinc(x)
f6 = lambda x: 1 + x * 0
f7 = lambda x: 1 / (1 + 25 * x ** 2)
```

## Datos a interpolar

Acá puede seleccionar la función de la celda anterior que estime conveniente para probar su interpolación.

```python
f = f5
```

Se generarán puntos equispaciados $x_i$ en un intervalo indicado y luego se evaluaran dichos puntos en una función $f$ para generar puntos $y_i$.

```python
N_i = 11
x_a, x_b = -2 * np.pi, 2 * np.pi
x_i = np.linspace(x_a, x_b, N_i)
y_i = f(x_i)
```

## Interpolación

Para probar su interpolación, utilice las siguientes celdas.

```python
Pl = lagrange(x_i, y_i)
```

```python
Pb = barycentric(x_i, y_i)
```

# Visualización

Utilice este código para visualizar su interpolación.

```python
N_e = 200
x_e = np.linspace(x_a, x_b, N_e)
y_e = f(x_e)
plt.figure(figsize=(12, 6))
plt.plot(x_e, y_e, 'b-', label=r"$f(x)$")
plt.plot(x_i, y_i, 'rd', label="Puntos para interpolar")
plt.plot(x_e, Pl(x_e), 'm--', label=r'$P_L(x)$')
plt.plot(x_e, Pb(x_e), 'k-.', label=r'$P_B(x)$')
plt.grid(True)
plt.legend()
plt.xlabel(r'$x$')
plt.ylabel(r'$y$')
plt.show()
```

## Comparar tiempo de evaluacion

Diseñe un experimento de manera que pueda mostrar la complejidad computacional en la **evaluación** de sus interpoladores. Recuerde que la complejidad de evaluar un polinomio utilizando Lagrange debería ser del orden $O(n^2)$ mientras que al usar Interpolación Baricéntrica $O(n)$.

```python
x_a, x_b = -2 * np.pi, 2 * np.pi # Interpolation domain
N_exp = 5
Nt = 2 ** np.arange(3, 9)
N = Nt.shape[-1]
times_el = np.zeros(N)
times_eb = np.zeros(N)
```

```python
# Evaluation time
```

# Visualización tiempos

Puede utilizar este código para visualizar los tiempos de evaluación.

```python
plt.figure(figsize=(12, 6))
plt.plot(Nt, times_el, 'rx', label="Lagrange")
plt.plot(Nt, times_eb, 'gd', label="Barycentric")
plt.plot(Nt, 1e-4 * Nt, label=r"$O(n)$") # Puede ajustar el coeficiente 1e-4
plt.plot(Nt, 1e-4 * Nt ** 2, label=r"$O(n^2)$") # Puede ajustar el coeficiente 1e-4
plt.xscale('log')
plt.yscale('log')
plt.ylabel("Time [s]")
plt.xlabel("Number of evaluation points")
plt.grid(True)
plt.legend()
plt.show()
```


# Complex vibration modes

Complex vibration modes arise in experimental research and numerical simulations with non proportional damping. In such cases the eigenproblem is [nonlinear](http://en.wikipedia.org/wiki/Nonlinear_eigenproblem), thus requiring additional work in order to obtain the vibration frequencies and mode shapes of the dynamic system.

The solution to the free vibration response of the dynamic equilibrium equation involves some form of linearization of the [quadratic eigenvalue problem](http://en.wikipedia.org/wiki/Quadratic_eigenvalue_problem) it yields. For more information on this subject see for example [this report](http://prod.sandia.gov/techlib/access-control.cgi/2007/072072.pdf).

## Table of contents

[Preamble](#Preamble)

[Dynamic equilibrium equation](#Dynamic-equilibrium-equation)

[State space formulation](#State-space-formulation)

[Dynamic system setup](#Dynamic-system-setup)

[Undamped system](#Undamped-system)

[Proportionally damped system](#Proportionally-damped-system)

[Non proportionally damped system](#Non-proportionally-damped-system)

[Conclusions](#Conclusions)

[Odds and ends](#Odds-and-ends)

## Preamble

We will start by setting up the computational environment for this notebook. Furthermore, we will need numpy and scipy for the numerical simulations and matplotlib for the plots:

```python
import sys
import numpy as np
import scipy as sp
import matplotlib as mpl

print('System: {}'.format(sys.version))
print('numpy version: {}'.format(np.__version__))
print('scipy version: {}'.format(sp.__version__))
print('matplotlib version: {}'.format(mpl.__version__))
```

    System: 3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]
    numpy version: 1.12.1
    scipy version: 0.19.0
    matplotlib version: 2.0.2

We will also need a couple of specific modules and a litle "IPython magic" to show the plots:

```python
from numpy import linalg as LA
import matplotlib.pyplot as plt

%matplotlib inline
```

[Back to top](#top)

## Dynamic equilibrium equation

In structural dynamics the second order differential dynamic equilibrium equation can be written in terms of generalized coordinates (d[isplacement]) and their first (v[elocity]) and second (a[cceleration]) time derivatives:

\begin{equation}
\mathbf{M} \times \mathbf{a(t)} + \mathbf{C} \times \mathbf{v(t)} + \mathbf{K} \times \mathbf{d(t)} = \mathbf{F(t)}
\end{equation}

where:

> $\mathbf{M}$ is the mass matrix
> $\mathbf{C}$ is the damping matrix
> $\mathbf{K}$ is the stiffness matrix
> $\mathbf{a(t)}$ is the acceleration vector
> $\mathbf{v(t)}$ is the velocity vector
> $\mathbf{d(t)}$ is the displacement vector
> $\mathbf{F(t)}$ is the force input vector

Considering a dynamic system with $NDOF$ is the number of generalized degrees of freedom, the vectors will have dimensions of $NDOF \times 1$ and the matrices $NDOF \times NDOF$.

When the system is undamped, the damping matrix will be null. In this case the eigenproblem is linear:

\begin{equation}
\left[ -\mathbf{M} \times \mathbf{\omega^2} + \mathbf{K} \right] \times \mathbf{v} = \mathbf{0}
\end{equation}

In a proportionally damped system, the damping matrix is proportional to the mass and stiffness matrices:

\begin{equation}
\mathbf{C} = \alpha \times \mathbf{M} + \beta \times \mathbf{K}
\end{equation}

where $\alpha$ and $\beta$ are mass and stiffness proportionality coefficients. These are typically very small positive numbers. The resulting eigenproblem is still linear because the damping matrix can be decomposed by the modal vectors.

In a system with non proportional damping, the damping matrix will not be proportional neither to the mass nor to the stiffness matrices.

[Back to top](#top)

## Dynamic system setup

In this example we will use the folowing mass and stiffness matrices:

```python
MM = np.matrix(np.diag([1., 2.]))
print(MM)
```

    [[ 1.  0.]
     [ 0.  2.]]

```python
KK = np.matrix([[20., -10.], [-10., 10.]])
print(KK)
```

    [[ 20. -10.]
     [-10.  10.]]

We will also setup two damping matrices, one proportional to the mass and stiffness matrices (C1) and the other non proportional (C2):

```python
C1 = 0.1*MM+0.04*KK
print(C1)
```

    [[ 0.9 -0.4]
     [-0.4  0.6]]

```python
C2 = np.matrix([[0.1, 0.2], [0.2, 0.2]])
print(C2)
```

    [[ 0.1  0.2]
     [ 0.2  0.2]]

[Back to top](#top)

## Undamped system

In the undamped system the damping matrix is all zeros and therefore the eigenproblem is a linear one as it involves only the mass and stiffness matrices:

```python
W2, F1 = LA.eig(LA.solve(MM,KK)) # eigenanalysis
ix = np.argsort(np.absolute(W2)) # sort eigenvalues in ascending order
W2 = W2[ix] # sorted eigenvalues
F1 = F1[:,ix] # sorted eigenvectors
print(np.round_(W2, 4))
print(np.round_(F1, 4))
```

    [  2.1922  22.8078]
    [[ 0.4896  0.9628]
     [ 0.8719 -0.2703]]

The angular frequencies are computed as the square root of the eigenvalues:

```python
print(np.sqrt(W2))
```

    [ 1.48062012  4.77574749]

The modal vectors, the columns of the modal matrix, have unit norm:

```python
print(LA.norm(F1, axis=0))
```

    [ 1.  1.]

Contrary to what is normaly done, we will visualize the modal vectors in a polar plot of the corresponding amplitudes and angles of equivalent complex values:

```python
fig, ax = plt.subplots(1, 2, subplot_kw=dict(polar=True))
for mode in range(2):
    ax[mode].set_title('Mode #{}'.format(mode+1))
    for dof in range(2):
        r = np.array([0, np.absolute(F1[dof,mode])])
        t = np.array([0, np.angle(F1[dof,mode])])
        ax[mode].plot(t, r, 'o-', label='DOF #{}'.format(dof+1))
plt.legend(loc='lower left', bbox_to_anchor=(1., 0.))
plt.show()
```

[Back to top](#top)

## Proportionally damped system

This damping matrix is orthogonal because the mass and stiffness matrices are also orthogonal:

```python
print(np.round_(F1.T*C1*F1, 4))
```

    [[ 0.3304  0.    ]
     [ 0.      1.0863]]

The system and input matrices are the following:

```python
A = np.bmat([[np.zeros_like(MM), MM], [MM, C1]])
print(A)
```

    [[ 0.   0.   1.   0. ]
     [ 0.   0.   0.   2. ]
     [ 1.   0.   0.9 -0.4]
     [ 0.   2.  -0.4  0.6]]

```python
B = np.bmat([[MM, np.zeros_like(MM)], [np.zeros_like(MM), -KK]])
print(B)
```

    [[  1.   0.   0.   0.]
     [  0.   2.   0.   0.]
     [  0.   0. -20.  10.]
     [  0.   0.  10. -10.]]

The eigenanalysis yields the eigenvalues and eigenvectors:

```python
w1, v1 = LA.eig(LA.solve(A,B))
ix = np.argsort(np.absolute(w1))
w1 = w1[ix]
v1 = v1[:,ix]
print(np.round_(w1, 4))
print(np.round_(v1, 4))
```

    [-0.0938+1.4776j -0.0938-1.4776j -0.5062+4.7488j -0.5062-4.7488j]
    [[ 0.4058-0.j      0.4058+0.j      0.9423+0.j      0.9423-0.j    ]
     [ 0.7226+0.j      0.7226-0.j     -0.2646-0.j     -0.2646+0.j    ]
     [-0.0174-0.2735j -0.0174+0.2735j -0.0209-0.1962j -0.0209+0.1962j]
     [-0.0309-0.487j  -0.0309+0.487j   0.0059+0.0551j  0.0059-0.0551j]]

As we can see, the eigenvalues come in complex conjugate pairs. Let us take only the ones in the [upper half-plane](http://en.wikipedia.org/wiki/Upper_half-plane):

```python
print(np.round_(w1[::2], 4))
```

    [-0.0938+1.4776j -0.5062+4.7488j]

These complex eigenvalues can be decomposed into angular frequency and damping coefficient:

```python
zw = -w1.real # damping coefficient time angular frequency
wD = w1.imag # damped angular frequency
zn = 1./np.sqrt(1.+(wD/-zw)**2) # the minus sign is formally correct!
wn = zw/zn # undamped angular frequency
print('Angular frequency: {}'.format(wn[[0,2]]))
print('Damping coefficient: {}'.format(zn[[0,2]]))
```

    Angular frequency: [ 1.48062012  4.77574749]
    Damping coefficient: [ 0.06338204  0.10598452]

The columns of the modal matrix, the modal vectors, also come in conjugate pairs, each vector having unit norm:

```python
print(LA.norm(v1[:,::2], axis=0))
```

    [ 1.  1.]

Moreover, the modal matrix is composed of four blocks, each with $NDOF \times NDOF$ dimension. Some column reordering is necessary in order to match both modal matrices:

```python
AA = v1[:2,[0,2]]
AB = AA.conjugate()
BA = np.multiply(AA,w1[[0,2]])
BB = BA.conjugate()
v1_new = np.bmat([[AA, AB], [BA, BB]])
print(np.round_(v1_new[:,[0,2,1,3]], 4))
```

    [[ 0.4058-0.j      0.4058+0.j      0.9423+0.j      0.9423-0.j    ]
     [ 0.7226+0.j      0.7226-0.j     -0.2646-0.j     -0.2646+0.j    ]
     [-0.0381+0.5996j -0.0381-0.5996j -0.4770+4.475j  -0.4770-4.475j ]
     [-0.0678+1.0677j -0.0678-1.0677j  0.1339-1.2565j  0.1339+1.2565j]]

We will visualize again the complex valued modal vectors with a polar plot of the corresponding amplitudes and angles:

```python
fig, ax = plt.subplots(1, 2, subplot_kw=dict(polar=True))
for mode in range(2):
    ax[mode].set_title('Mode #{}'.format(mode+1))
    for dof in range(2):
        r = np.array([0, np.absolute(v1[dof,2*mode])])
        t = np.array([0, np.angle(v1[dof,2*mode])])
        ax[mode].plot(t, r, 'o-', label='DOF #{}'.format(dof+1))
plt.legend(loc='lower left', bbox_to_anchor=(1., 0.))
plt.show()
```

[Back to top](#top)

## Non proportionally damped system

In non proportionally damped systems the damping matrix is not proportional neither to the mass matrix nor the stiffness matrix.

Non proportinal damping carries the fact that the damping matrix is not orthogonal anymore:

```python
print(np.round_(F1.T*C2*F1, 4))
```

    [[ 0.3468  0.1414]
     [ 0.1414  0.0032]]

The system and input matrices are the following:

```python
A = np.bmat([[np.zeros_like(MM), MM], [MM, C2]])
print(A)
```

    [[ 0.   0.   1.   0. ]
     [ 0.   0.   0.   2. ]
     [ 1.   0.   0.1  0.2]
     [ 0.   2.   0.2  0.2]]

```python
B = np.bmat([[MM, np.zeros_like(MM)], [np.zeros_like(MM), -KK]])
print(B)
```

    [[  1.   0.   0.   0.]
     [  0.   2.   0.   0.]
     [  0.   0. -20.  10.]
     [  0.   0.  10. -10.]]

The eigenanalysis yields the eigenvalues and eigenvectors of the system matrix:

```python
w2, v2 = LA.eig(LA.solve(A,B))
ix = np.argsort(np.absolute(w2))
w2 = w2[ix]
v2 = v2[:,ix]
print(np.round_(w2, 4))
print(np.round_(v2, 4))
```

    [-0.0986+1.4777j -0.0986-1.4777j -0.0014+4.7745j -0.0014-4.7745j]
    [[  4.06300000e-01-0.0087j   4.06300000e-01+0.0087j  -9.42000000e-01+0.j
       -9.42000000e-01-0.j    ]
     [  7.22300000e-01+0.j       7.22300000e-01-0.j       2.65100000e-01-0.0184j
        2.65100000e-01+0.0184j]
     [ -2.41000000e-02-0.2733j  -2.41000000e-02+0.2733j
        1.00000000e-04+0.1973j   1.00000000e-04-0.1973j]
     [ -3.25000000e-02-0.4866j  -3.25000000e-02+0.4866j
       -3.90000000e-03-0.0555j  -3.90000000e-03+0.0555j]]

As we can see, the eigenvalues come in complex conjugate pairs. Again, let us take only the ones in the [upper half-plane](http://en.wikipedia.org/wiki/Upper_half-plane):

```python
print(np.round_(w2[[0,2]], 4))
```

    [-0.0986+1.4777j -0.0014+4.7745j]

These complex eigenvalues can be decomposed into angular frequency and damping coefficient much like in the propotional damping case:

```python
zw = -w2.real # damping coefficient times angular frequency
wD = w2.imag # damped angular frequency
zn = 1./np.sqrt(1.+(wD/-zw)**2) # the minus sign is formally correct!
wn = zw/zn # undamped angular frequency
print('Angular frequency: {}'.format(wn[[0,2]]))
print('Damping coefficient: {}'.format(zn[[0,2]]))
```

    Angular frequency: [ 1.48099973  4.77452335]
    Damping coefficient: [ 0.06655163  0.00030098]

Again, the columns of the modal matrix, the modal vectors, come in conjugate pairs, and each vector has unit norm:

```python
print(LA.norm(v2[:,[0,2]], axis=0))
```

    [ 1.  1.]

Moreover, the modal matrix is composed of four blocks, each with $NDOF \times NDOF$ dimension. Some column reordering is necessary in order to match both modal matrices:

```python
AA = v2[:2,[0,2]]
AB = AA.conjugate()
BA = np.multiply(AA,w2[[0,2]])
BB = BA.conjugate()
v2_new = np.bmat([[AA, AB], [BA, BB]])
print(np.round_(v2_new[:,[0,2,1,3]], 4))
```

    [[ 0.4063-0.0087j  0.4063+0.0087j -0.9420+0.j     -0.9420-0.j    ]
     [ 0.7223+0.j      0.7223-0.j      0.2651-0.0184j  0.2651+0.0184j]
     [-0.0272+0.6012j -0.0272-0.6012j  0.0014-4.4975j  0.0014+4.4975j]
     [-0.0712+1.0673j -0.0712-1.0673j  0.0873+1.266j   0.0873-1.266j ]]

Once more we will visualize the complex valued modal vectors through a polar plot of the corresponding amplitudes and angles:

```python
fig, ax = plt.subplots(1, 2, subplot_kw=dict(polar=True))
for mode in range(2):
    ax[mode].set_title('Mode #{}'.format(mode+1))
    for dof in range(2):
        r = np.array([0, np.absolute(v2[dof,2*mode])])
        t = np.array([0, np.angle(v2[dof,2*mode])])
        ax[mode].plot(t, r, 'o-', label='DOF #{}'.format(dof+1))
plt.legend(loc='lower left', bbox_to_anchor=(1., 0.))
plt.show()
```

[Back to top](#top)

## Conclusions

Several conclusion can be drawn from this very simple example. First of all, damping changes the vibration frequencies and mode shapes. Furthermore, the polar plots show clearly that:

1. In the undamped system the vibration modes are fully "orthogonal";
2. In the proportionally damped system the vibration modes are no longer "orthogonal";
3. In the non proportionally damped system each DOF has a different phase angle.

[Back to top](#top)

## Odds and ends

This notebook was created by Paulo Xavier Candeias.

[Back to top](#top)


# Tutorial: Classification

Agenda:
1. Classification running example: Iris Flowers
2. Weight space & feature space intuition
3. Perceptron convergence proof
4. Gradient Descent for Multiclass Logisitc Regression

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

## Classification with Iris

We're going to use the Iris dataset.

We will only work with the first 2 flower classes
(Setosa and Versicolour), and with just the first
two features: length and width of the sepal

If you don't know what the sepal is, see this
diagram:
https://www.math.umd.edu/~petersd/666/html/iris_with_labels.jpg

```python
from sklearn.datasets import load_iris
iris = load_iris()
print iris['DESCR']
```

    Iris Plants Database

    Notes
    -----
    Data Set Characteristics:
        :Number of Instances: 150 (50 in each of three classes)
        :Number of Attributes: 4 numeric, predictive attributes and the class
        :Attribute Information:
            - sepal length in cm
            - sepal width in cm
            - petal length in cm
            - petal width in cm
            - class:
                    - Iris-Setosa
                    - Iris-Versicolour
                    - Iris-Virginica
        :Summary Statistics:
        ============== ==== ==== ======= ===== ====================
                        Min  Max   Mean    SD   Class Correlation
        ============== ==== ==== ======= ===== ====================
        sepal length:   4.3  7.9   5.84   0.83    0.7826
        sepal width:    2.0  4.4   3.05   0.43   -0.4194
        petal length:   1.0  6.9   3.76   1.76    0.9490  (high!)
        petal width:    0.1  2.5   1.20  0.76     0.9565  (high!)
        ============== ==== ==== ======= ===== ====================
        :Missing Attribute Values: None
        :Class Distribution: 33.3% for each of 3 classes.
        :Creator: R.A. Fisher
        :Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
        :Date: July, 1988

    This is a copy of UCI ML iris datasets.
    http://archive.ics.uci.edu/ml/datasets/Iris

    The famous Iris database, first used by Sir R.A Fisher

    This is perhaps the best known database to be found in the
    pattern recognition literature.  Fisher's paper is a classic in the field and
    is referenced frequently to this day.  (See Duda & Hart, for example.)  The
    data set contains 3 classes of 50 instances each, where each class refers to a
    type of iris plant.  One class is linearly separable from the other 2; the
    latter are NOT linearly separable from each other.

    References
    ----------
       - Fisher,R.A. "The use of multiple measurements in taxonomic problems"
         Annual Eugenics, 7, Part II, 179-188 (1936); also in "Contributions to
         Mathematical Statistics" (John Wiley, NY, 1950).
       - Duda,R.O., & Hart,P.E. (1973) Pattern Classification and Scene Analysis.
         (Q327.D83) John Wiley & Sons.  ISBN 0-471-22361-1.  See page 218.
       - Dasarathy, B.V. (1980) "Nosing Around the Neighborhood: A New System
         Structure and Classification Rule for Recognition in Partially Exposed
         Environments".  IEEE Transactions on Pattern Analysis and Machine
         Intelligence, Vol. PAMI-2, No. 1, 67-71.
       - Gates, G.W. (1972) "The Reduced Nearest Neighbor Rule".  IEEE Transactions
         on Information Theory, May 1972, 431-433.
       - See also: 1988 MLC Proceedings, 54-64.  Cheeseman et al"s AUTOCLASS II
         conceptual clustering system finds 3 classes in the data.
       - Many, many more ...

```python
# code from
# http://stackoverflow.com/questions/21131707/multiple-data-in-scatter-matrix
from pandas.tools.plotting import scatter_matrix
import pandas as pd

iris_data = pd.DataFrame(data=iris['data'],columns=iris['feature_names'])
iris_data["target"] = iris['target']
color_wheel = {1: "#0392cf",
               2: "#7bc043",
               3: "#ee4035"}
colors = iris_data["target"].map(lambda x: color_wheel.get(x + 1))
ax = scatter_matrix(iris_data, color=colors, alpha=0.6, figsize=(15, 15), diagonal='hist')
```

```python
# Select first 2 flower classes (~100 rows)
# And first 2 features

sepal_len = iris['data'][:100,0]
sepal_wid = iris['data'][:100,1]
labels = iris['target'][:100]

# We will also center the data
# This is done to make numbers nice, so that we have no
# need for biases in our classification. (You might not
# be able to remove biases this way in general.)

sepal_len -= np.mean(sepal_len)
sepal_wid -= np.mean(sepal_wid)
```

```python
# Plot Iris

plt.scatter(sepal_len,
            sepal_wid,
            c=labels,
            cmap=plt.cm.Paired)
plt.xlabel("sepal length")
plt.ylabel("sepal width")
```

### Plotting Decision Boundary

Plot decision boundary hypothese

$$w_1 x_1 + w_2 x_2 \ge 0$$

for classification as Setosa.

```python
def plot_sep(w1, w2, color='green'):
    '''
    Plot decision boundary hypothesis
      w1 * sepal_len + w2 * sepal_wid = 0
    in input space, highlighting the hyperplane
    '''
    plt.scatter(sepal_len,
                sepal_wid,
                c=labels,
                cmap=plt.cm.Paired)
    plt.title("Separation in Input Space")
    plt.ylim([-1.5,1.5])
    plt.xlim([-1.5,2])
    plt.xlabel("sepal length")
    plt.ylabel("sepal width")
    if w2 != 0:
        m = -w1/w2
        t = 1 if w2 > 0 else -1
        plt.plot(
            [-1.5,2.0],
            [-1.5*m, 2.0*m],
            '-y',
            color=color)
        plt.fill_between(
            [-1.5, 2.0],
            [m*-1.5, m*2.0],
            [t*1.5, t*1.5],
            alpha=0.2,
            color=color)
    if w2 == 0: # decision boundary is vertical
        t = 1 if w1 > 0 else -1
        plt.plot([0, 0],
                 [-1.5, 2.0],
                 '-y',
                color=color)
        plt.fill_between(
            [0, 2.0*t],
            [-1.5, -2.0],
            [1.5, 2],
            alpha=0.2,
            color=color)
```

```python
# Example hypothesis
#   sepal_wid >= 0

plot_sep(0, 1)
```

```python
# Another example hypothesis:
#   -0.5*sepal_len + 1*sepal_wid >= 0

plot_sep(-0.5, 1)
```

```python
# We're going to hand pick one point and
# analyze that point:

a1 = sepal_len[41]
a2 = sepal_wid[41]
print (a1, a2) # (-0.97, -0.79)

plot_sep(-0.5, 1)
plt.plot(a1, a2, 'ob') # highlight the point
```

### Plot Constraints in Weight Space

We'll plot the constraints for some of the points
that we chose earlier.

```python
def plot_weight_space(sepal_len, sepal_wid, lab=1,
                      color='steelblue',
                      maxlim=2.0):
    plt.title("Constraint(s) in Weight Space")
    plt.ylim([-maxlim,maxlim])
    plt.xlim([-maxlim,maxlim])
    plt.xlabel("w1")
    plt.ylabel("w2")

    if sepal_wid != 0:
        m = -sepal_len/sepal_wid
        t = 1*lab if sepal_wid > 0 else -1*lab
        plt.plot([-maxlim, maxlim],
                 [-maxlim*m, maxlim*m],
                 '-y',
                 color=color)
        plt.fill_between(
            [-maxlim, maxlim],     # x
            [m*-maxlim, m*maxlim], # y-min
            [t*maxlim, t*maxlim],      # y-max
            alpha=0.2,
            color=color)
    if sepal_wid == 0: # decision boundary is vertical
        t = 1*lab if sepal_len > 0 else -1*lab
        plt.plot([0, 0],
                 [-maxlim, maxlim],
                 '-y',
                 color=color)
        plt.fill_between(
            [0, 2.0*t],
            [-maxlim, -maxlim],
            [maxlim, maxlim],
            alpha=0.2,
            color=color)
```

```python
# Plot the constraint for the point identified earlier:

a1 = sepal_len[41]
a2 = sepal_wid[41]
print (a1, a2)

# Do this on the board first by hand

plot_weight_space(a1, a2, lab=1)

# Below is the hypothesis we plotted earlier
# Notice it falls outside the range.
plt.plot(-0.5, 1, 'og')
```

### Perceptron Learning Rule Example

We'll take one step using the perceptron learning rule

```python
# Using the perceptron learning rule
# TODO: Fill in

w1 = -0.5 # + ...
w2 =  1   # + ...
```

```python
# This should bring the point closer to the boundary
# In this case, the step brought the point into the
# condition boundary
plot_weight_space(a1, a2, lab=1)
plt.plot(-0.5+a1, 1+a2, 'og')
# old hypothesis
plt.plot(-0.5, 1, 'og')
plt.plot([-0.5, -0.5+a1], [1, 1+a2], '-g')

plt.axes().set_aspect('equal', 'box')
```

```python
# Which means that the point (a1, a2) in input
# space is correctly classified.

plot_sep(-0.5+a1, 1+a2)
```

### Visualizing Multiple Constraints

We'll visualize multiple constraints in weight space.

```python
# Pick a second point
b1 = sepal_len[84]
b2 = sepal_wid[84]

plot_sep(-0.5+a1, 1+a2)
plt.plot(b1, b2, 'or') # plot the circle in red
```

```python
# our weights fall outside constraint of second pt.

plot_weight_space(a1, a2, lab=1, color='blue')
plot_weight_space(b1, b2, lab=-1, color='red')
plt.plot(w1, w2, 'ob')
```

```python
# Example of a separating hyperplane
plot_weight_space(a1, a2, lab=1, color='blue')
plot_weight_space(b1, b2, lab=-1, color='red')
plt.plot(-1, 1, 'ok')
plt.show()
plot_sep(-1, 1)
plt.show()
```

## Perceptron Convergence Proof:

(From Geoffrey Hinton's slides 2d)

Hopeful claim: Every time the perceptron makes a mistake, the learning algo moves the current weight vector closer to all feasible weight vectors

BUT: weight vector may not get close to feasible vector in the boundary

```python
# The feasible region is inside the intersection of these two regions:
plot_weight_space(a1, a2, lab=1, color='blue')
#plot_weight_space(b1, b2, lab=-1, color='red')

# This is a vector in the feasible region.
plt.plot(-0.3, 0.3, 'ok')

# We started with this point
plt.plot(-0.5, 1, 'og')

# And ended up here
plt.plot(-0.5+a1, 1+a2, 'or')

# Notice that red point is further away to black than the green

plt.axes().set_aspect('equal', 'box')
```

* So consider â€œgenerously feasibleâ€ weight vectors that lie within the feasible region by a margin at least as great as the length of the input vector that defines each constraint plane.
* Every time the perceptron makes a mistake, the squared distance to all of these generously feasible weight vectors is always decreased by at least the squared length of the update vector.

```python
plot_weight_space(a1, a2, lab=1, color='blue' ,maxlim=15)
plot_weight_space(b1, b2, lab=-1, color='red', maxlim=15)

# We started with this point
plt.plot(-0.5, 1, 'og')
plt.plot(-0.5+a1, 1+a2, 'or')
plt.axes().set_aspect('equal', 'box')

# red is closer to "generously feasible" vectors on the top left
```

### Inform Sketch of Proof of Convergence

* Each time the perceptron makes a mistake, the current weight vector moves to decrease its squared distance from every weight vector in the â€œgenerously feasibleâ€ region.
* The squared distance decreases by at least the squared length of the input vector.
* So after a finite number of mistakes, the weight vector must lie in the feasible region if this region exists.

## Gradient Descent for Multiclass Logisitc Regression

Multiclass logistic regression:

\begin{align}
{\bf z} &= {\bf W}{\bf x} + {\bf b} \\
{\bf y} &= \text{softmax}({\bf z}) \\
{\mathcal L}_\text{CE} &= -{\bf t}^T(\log \bf{y})
\end{align}

Draw out the shapes on the board before continuing.

```python
# Aside: lots of functions work on vectors

print np.log([1.5,2,3])
print np.exp([1.5,2,3])
```

    [ 0.40546511  0.69314718  1.09861229]
    [  4.48168907   7.3890561   20.08553692]

Start by expanding the cross entropy loss so that we can work with it

$$ {\mathcal L}_\text{CE} = -\sum_l t_l \log(y_l)$$

### Main setup

We'll take the derivative with respect to the loss:

\begin{align}
\frac{\partial {\mathcal L}_\text{CE}}{\partial w_{kj}} &=
  \frac{\partial }{\partial w_{kj}} (-\sum_l t_l \log(y_l)) \\
&= -\sum_l \frac{t_l}{y_l} \frac{\partial y_l}{\partial w_{kj}}
\end{align}

Normally in calculus we have the rule:

\begin{align}
\frac{\partial y_l}{\partial w_{kj}} &=
  \sum_m \frac{\partial y_l}{\partial z_m} \frac{\partial z_m}{\partial w_{kj}}
\end{align}

But $w_{kj}$ is independent of $z_m$ for $m \ne k$, so

\begin{align}
\frac{\partial y_l}{\partial w_{kj}} &=
  \frac{\partial y_l}{\partial z_k} \frac{\partial z_k}{\partial w_{kj}}
\end{align}

AND

$$\frac{\partial z_k}{\partial w_{kj}} = x_j$$

Thus

\begin{align}
\frac{\partial {\mathcal L}_\text{CE}}{\partial w_{kj}} &=
-\sum_l \frac{t_l}{y_l} \frac{\partial y_l}{\partial z_k} \frac{\partial z_k}{\partial w_{kj}} \\
&=
-\sum_l \frac{t_l}{y_l} \frac{\partial y_l}{\partial z_k} x_j \\
&= x_j (-\sum_l \frac{t_l}{y_l} \frac{\partial y_l}{\partial z_k}) \\
&= x_j \frac{\partial {\mathcal L}_\text{CE}}{\partial z_k}
\end{align}

### Derivative with respect to $z_k$

But we can show (on board) that

$$
\frac{\partial y_l}{\partial z_k} = y_k (I_{k,l} - y_l)
$$

Where $I_{k,l} = 1$ if $k=l$ and $0$ otherwise.

Therefore

\begin{align}
\frac{\partial {\mathcal L}_\text{CE}}{\partial z_k} &=
-\sum_l \frac{t_l}{y_l} (y_k (I_{k,l} - y_l)) \\
&= -\frac{t_k}{y_k} y_k(1 - y_k) - \sum_{l \ne k} \frac{t_l}{y_l} (-y_k y_l) \\
&= - t_k(1 - y_k) + \sum_{l \ne k} t_l y_k \\
&= -t_k + t_k y_k + \sum_{l \ne k} t_l y_k \\
&= -t_k + \sum_{l} t_l y_k \\
&= -t_k + y_k \sum_{l} t_l  \\
&= -t_k + y_k \\
&= y_k - t_k
\end{align}

### Putting it all together

\begin{align}
\frac{\partial {\mathcal L}_\text{CE}}{\partial w_{kj}}
&= x_j (y_k - t_k)
\end{align}

### Vectorization

Outer product.

\begin{align}
\frac{\partial {\mathcal L}_\text{CE}}{\partial {\bf W}}
&= (\bf{y} - \bf{t}) \bf{x}^T \\
\frac{\partial {\mathcal L}_\text{CE}}{\partial {\bf b}}
&= (\bf{y} - \bf{t})
\end{align}

```python
def softmax(x):
    #return np.exp(x) / np.sum(np.exp(x))
    return np.exp(x - max(x)) / np.sum(np.exp(x - max(x)))
```

```python
x1 = np.array([1,3,3])
softmax(x1)
```

    array([ 0.06337894,  0.46831053,  0.46831053])

```python
x2 = np.array([1000,3000,3000])
softmax(x2)
```

    array([ 0. ,  0.5,  0.5])

```python
def gradient(W, b, x, t):
    '''
    Gradient update for a single data point.
        returns dW and db
    This is meant to show how to implement the
    obtained equation in code. (not tested)
    '''
    z = np.matmul(W, x) + b
    y = softmax(z)
    dW = np.matmul(x, (y-t).T)
    db = (y-t)
    return dW, db
```

```python

```


# Inferential Statistics Ib - Frequentism

## Learning objectives

Welcome to the second Frequentist inference mini-project! Over the course of working on this mini-project and the previous frequentist mini-project, you'll learn the fundamental concepts associated with frequentist inference. The following list includes the topics you will become familiar with as you work through these two mini-projects:
* the _z_-statistic
* the _t_-statistic
* the difference and relationship between the two
* the Central Limit Theorem, its assumptions and consequences
* how to estimate the population mean and standard deviation from a sample
* the concept of a sampling distribution of a test statistic, particularly for the mean
* how to combine these concepts to calculate confidence intervals and p-values
* how those confidence intervals and p-values allow you to perform hypothesis (or A/B) tests

## Prerequisites

* what a random variable is
* what a probability density function (pdf) is
* what the cumulative density function is
* a high-level sense of what the Normal distribution

If these concepts are new to you, please take a few moments to Google these topics in order to get a sense of what they are and how you might use them.

These two notebooks were designed to bridge the gap between having a basic understanding of probability and random variables and being able to apply these concepts in Python. This second frequentist inference mini-project focuses on a real-world application of this type of inference to give you further practice using these concepts.

In the previous notebook, we used only data from a known normal distribution. You'll now tackle real data, rather than simulated data, and answer some relevant real-world business problems using the data.

## Hospital medical charges

Imagine that a hospital has hired you as their data analyst. An administrator is working on the hospital's business operations plan and needs you to help them answer some business questions. This mini-project, as well as the bootstrap and Bayesian inference mini-projects also found in this unit are designed to illustrate how each of the inferential statistics methods have their uses for different use cases. In this assignment notebook, you're going to use frequentist statistical inference on a data sample to answer the questions:
* has the hospital's revenue stream fallen below a key threshold?
* are patients with insurance really charged different amounts than those without?
Answering that last question with a frequentist approach makes some assumptions, or requires some knowledge, about the two groups. In the next mini-project, you'll use bootstrapping to test that assumption. And in the final mini-project of the unit, you're going to create a model for simulating _individual_ charges (not a sampling distribution) that the hospital can use to model a range of scenarios.

We are going to use some data on medical charges obtained from [Kaggle](https://www.kaggle.com/easonlai/sample-insurance-claim-prediction-dataset). For the purposes of this exercise, assume the observations are the result of random sampling from our one hospital. Recall in the previous assignment, we introduced the Central Limit Theorem (CLT), and how it tells us that the distributions of sample statistics approach a normal distribution as $n$ increases. The amazing thing about this is that it applies to the sampling distributions of statistics that have been calculated from even highly non-normal distributions of data. Remember, also, that hypothesis testing is very much based on making inferences about such sample statistics. You're going to rely heavily on the CLT to apply frequentist (parametric) tests to answer the questions in this notebook.

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import t
from scipy.stats import norm
from numpy.random import seed
from scipy.stats import stats
medical = pd.read_csv('data/insurance2.csv')
```

```python
medical.shape
```

    (1338, 8)

```python
medical.head()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      0
      19
      0
      27.900
      0
      1
      3
      16884.92400
      1

      1
      18
      1
      33.770
      1
      0
      2
      1725.55230
      1

      2
      28
      1
      33.000
      3
      0
      2
      4449.46200
      0

      3
      33
      1
      22.705
      0
      0
      1
      21984.47061
      0

      4
      32
      1
      28.880
      0
      0
      1
      3866.85520
      1

__Q:__ Plot the histogram of charges and calculate the mean and standard deviation. Comment on the appropriateness of these statistics for the data.

__A:__

```python
#Histogram of Charges:
medical['charges'].plot(kind = 'hist', edgecolor = 'white', bins= 20, xlim = 0)
plt.xlabel('Medical Charges')
plt.title('Distribution of Medical Charges')
```

```python
mean_charges= np.mean(medical['charges'])
print('Mean Medical Charges:', mean_charges)

std_charges= np.std(medical['charges'], ddof=1)
print('Standard Deviation Medical Charges:', std_charges)

#Plot does notrepresent normal distribution.
```

    Mean Medical Charges: 13270.422265141257
    Standard Deviation Medical Charges: 12110.011236693994

```python
#Descriptive Statistics:
medical.describe()
```

      age
      sex
      bmi
      children
      smoker
      region
      charges
      insuranceclaim

      count
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000
      1338.000000

      mean
      39.207025
      0.505232
      30.663397
      1.094918
      0.204783
      1.515695
      13270.422265
      0.585202

      std
      14.049960
      0.500160
      6.098187
      1.205493
      0.403694
      1.104885
      12110.011237
      0.492871

      min
      18.000000
      0.000000
      15.960000
      0.000000
      0.000000
      0.000000
      1121.873900
      0.000000

      25%
      27.000000
      0.000000
      26.296250
      0.000000
      0.000000
      1.000000
      4740.287150
      0.000000

      50%
      39.000000
      1.000000
      30.400000
      1.000000
      0.000000
      2.000000
      9382.033000
      1.000000

      75%
      51.000000
      1.000000
      34.693750
      2.000000
      0.000000
      2.000000
      16639.912515
      1.000000

      max
      64.000000
      1.000000
      53.130000
      5.000000
      1.000000
      3.000000
      63770.428010
      1.000000

__Q:__ The administrator is concerned that the actual average charge has fallen below 12000, threatening the hospital's operational model. On the assumption that these data represent a random sample of charges, how would you justify that these data allow you to answer that question? And what would be the most appropriate frequentist test, of the ones discussed so far, to apply?

__A:__ Will justify as under:
1. Calculate Margin of Error using Z score.
2. Identify if the mean confidence interval is lower than 12000 or higher.
3. Will use one-sided test.

__Q:__ Given the nature of the administrator's concern, what is the appropriate confidence interval in this case? A one-sided or two-sided interval? Calculate the critical value and the relevant 95% confidence interval for the mean and comment on whether the administrator should be concerned?

__A:__ Appropriate Confidence Interval is one-sided interval ~ 12621 which shows with 95% confidence level that it is higher than the actual average charge of 12000.

```python
#Calculate Margin of error using Z:
#Margin of Error = z-critical value * standard deviation:
margin_error_z = norm.ppf(q= 0.975) * (std_charges / (np.sqrt(medical.shape[0]))) #here q = 1- (100%-95%)/2 = 0.975
margin_error_z
```

    648.880286912097

```python
print('Mean Confidence Interval Using Z:', [mean_charges-margin_error_z, mean_charges+margin_error_z])
```

    Mean Confidence Interval Using Z: [12621.54197822916, 13919.302552053354]

The administrator then wants to know whether people with insurance really are charged a different amount to those without.

__Q:__ State the null and alternative hypothesis here. Use the _t_-test for the difference between means where the pooled standard deviation of the two groups is given by
\begin{equation}
s_p = \sqrt{\frac{(n_0 - 1)s^2_0 + (n_1 - 1)s^2_1}{n_0 + n_1 - 2}}
\end{equation}

and the *t* test statistic is then given by

\begin{equation}
t = \frac{\bar{x}_0 - \bar{x}_1}{s_p \sqrt{1/n_0 + 1/n_1}}.
\end{equation}

What assumption about the variances of the two groups are we making here?

__A:__ Both groups have same variance.

__Q:__ Perform this hypothesis test both manually, using the above formulae, and then using the appropriate function from [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html#statistical-tests) (hint, you're looking for a function to perform a _t_-test on two independent samples). For the manual approach, calculate the value of the test statistic and then its probability (the p-value). Verify you get the same results from both.

__A:__

```python
#Insured Sample
insured=medical.query('insuranceclaim ==1')

#Uninsured Sample
uninsured = medical.query('insuranceclaim==0')
```

```python
#Mean Insured Charges:
mean_insured= np.mean(insured.charges)
print('Mean Insured Sample:', mean_insured)

#Standard Deviation of Insured Charges:
std_insured = np.std(insured.charges)
print('Standard Deviation Insured Sample:', std_insured)

#Length of Insured Samples:
n_insured = len(insured)
print(n_insured)
```

    Mean Insured Sample: 16423.928276537663
    Standard Deviation Insured Sample: 14036.956250260411
    783

```python
#Mean uninsured Charges:
mean_uninsured = np.mean(uninsured.charges)
print('Mean Uninsured Sample:', mean_uninsured)

#Standard Deviation of unInsured Charges:
std_uninsured = np.std(uninsured.charges)
print('Standard Deviation Uninsured Sample:', std_uninsured)

#Length of Uninsured Samples:
n_uninsured = len(uninsured)
print(n_uninsured)
```

    Mean Uninsured Sample: 8821.421892306294
    Standard Deviation Uninsured Sample: 6440.699841605233
    555

```python
#Compare numerical results using manual approach and t-test function:

#1) Manual Approach: Using formulae

std_pooled = np.sqrt((((n_insured-1)*std_insured**2) + ((n_uninsured-1)*std_uninsured**2))/(n_insured + n_uninsured-2))
print('Pooled Standard Deviation of two Samples:', std_pooled)

t = (mean_insured - mean_uninsured)/(std_pooled *(np.sqrt(1/n_insured + 1/n_uninsured)))
print('t-statistics:', t)

#P-Value:
p = norm.pdf(t)

print('p-value:', p)
```

    Pooled Standard Deviation of two Samples: 11512.282899205744
    t-statistics: 11.901306943555385
    p-value: 6.981270779090461e-32

```python
# Using Function: This is a two-sided test for the null hypothesis that two independent samples have identical average (expected) values.
#Assumption: Both independent sample population variance are equal
ttest_equal_variance=  stats.ttest_ind_from_stats(mean_insured, std_insured, n_insured, mean_uninsured, std_uninsured, n_uninsured, equal_var=True)
print('Result:',ttest_equal_variance)
```

    Result: Ttest_indResult(statistic=11.901306943555387, pvalue=4.090550214373679e-31)

Congratulations! Hopefully you got the exact same numerical results. This shows that you correctly calculated the numbers by hand. Secondly, you used the correct function and saw that it's much easier to use. All you need to do pass your data to it.

__Q:__ In the above calculations, we assumed the sample variances were equal. We may well suspect they are not (we'll explore this in another assignment). The calculation becomes a little more complicated to do by hand in this case, but we now know of a helpful function. Check the documentation for the function to tell it not to assume equal variances and perform the test again.

__A:__ Documentation: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ttest_ind_from_stats.html#r24a95fcea38c-2

Select option equal_var as False

```python
#Asuumption:  Both independent sample population variance are not equal:

ttest_unequal_variance=  stats.ttest_ind_from_stats(mean_insured, std_insured, n_insured, mean_uninsured, std_uninsured, n_uninsured, equal_var=False)
print('Result:',ttest_unequal_variance)
```

    Result: Ttest_indResult(statistic=13.307332624349796, pvalue=9.974516564429595e-38)

__Q:__ Conceptual question: look through the documentation for statistical test functions in scipy.stats. You'll see the above _t_-test for a sample, but can you see an equivalent one for performing a *z*-test from a sample? Comment on your answer.

__A:__ Documentation for z_test: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mstats.zscore.html

Use function scipy.stats.zscore to calculate z-score of each value in the sample, standardized by its mean and standard deviation.

## Learning outcomes

Having completed this project notebook, you now have good hands-on experience:
* using the central limit theorem to help you apply frequentist techniques to answer questions that pertain to very non-normally distributed data from the real world
* performing inference using such data to answer business questions
* forming a hypothesis and framing the null and alternative hypotheses
* testing this using a _t_-test


# Introduction to Deep Learning with PyTorch

In this notebook, you'll get introduced to [PyTorch](http://pytorch.org/), a framework for building and training neural networks. PyTorch in a lot of ways behaves like the arrays you love from Numpy. These Numpy arrays, after all, are just tensors. PyTorch takes these tensors and makes it simple to move them to GPUs for the faster processing needed when training neural networks. It also provides a module that automatically calculates gradients (for backpropagation!) and another module specifically for building neural networks. All together, PyTorch ends up being more coherent with Python and the Numpy/Scipy stack compared to TensorFlow and other frameworks.

## Neural Networks

Deep Learning is based on artificial neural networks which have been around in some form since the late 1950s. The networks are built from individual parts approximating neurons, typically called units or simply "neurons." Each unit has some number of weighted inputs. These weighted inputs are summed together (a linear combination) then passed through an activation function to get the unit's output.

Mathematically this looks like:

$$
\begin{align}
y &= f(w_1 x_1 + w_2 x_2 + b) \\
y &= f\left(\sum_i w_i x_i \right)
\end{align}
$$

With vectors this is the dot/inner product of two vectors:

$$
h = \begin{bmatrix}
x_1 \, x_2 \cdots  x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_1 \\
           w_2 \\
           \vdots \\
           w_n
\end{bmatrix}
$$

### Stack them up!

We can assemble these unit neurons into layers and stacks, into a network of neurons. The output of one layer of neurons becomes the input for the next layer. With multiple input units and output units, we now need to express the weights as a matrix.

We can express this mathematically with matrices again and use matrix multiplication to get linear combinations for each unit in one operation. For example, the hidden layer ($h_1$ and $h_2$ here) can be calculated

$$
\vec{h} = [h_1 \, h_2] =
\begin{bmatrix}
x_1 \, x_2 \cdots \, x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_{11} & w_{12} \\
           w_{21} &w_{22} \\
           \vdots &\vdots \\
           w_{n1} &w_{n2}
\end{bmatrix}
$$

The output for this small network is found by treating the hidden layer as inputs for the output unit. The network output is expressed simply

$$
y =  f_2 \! \left(\, f_1 \! \left(\vec{x} \, \mathbf{W_1}\right) \mathbf{W_2} \right)
$$

## Tensors

It turns out neural network computations are just a bunch of linear algebra operations on *tensors*, a generalization of matrices. A vector is a 1-dimensional tensor, a matrix is a 2-dimensional tensor, an array with three indices is a 3-dimensional tensor (RGB color images for example). The fundamental data structure for neural networks are tensors and PyTorch (as well as pretty much every other deep learning framework) is built around tensors.

With the basics covered, it's time to explore how we can use PyTorch to build a simple neural network.

```python
import sys
print("Python version")
print (sys.version)
print("Version info.")
print (sys.version_info)
```

    Python version
    3.6.5 | packaged by conda-forge | (default, Apr  6 2018, 13:44:09)
    [GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)]
    Version info.
    sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0)

```python
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

import numpy as np
import torch

import helper
```

First, let's see how we work with PyTorch tensors. These are the fundamental data structures of neural networks and PyTorch, so it's imporatant to understand how these work.

```python
x = torch.rand(3, 2)
x
```

    tensor([[ 0.5249,  0.2378],
            [ 0.1303,  0.2541],
            [ 0.9113,  0.7750]])

```python
y = torch.ones(x.size())
y
```

    tensor([[ 1.,  1.],
            [ 1.,  1.],
            [ 1.,  1.]])

```python
z = x + y
z
```

    tensor([[ 1.5249,  1.2378],
            [ 1.1303,  1.2541],
            [ 1.9113,  1.7750]])

In general PyTorch tensors behave similar to Numpy arrays. They are zero indexed and support slicing.

```python
z[0]
```

    tensor([ 1.5249,  1.2378])

```python
z[:, 1:]
```

    tensor([[ 1.2378],
            [ 1.2541],
            [ 1.7750]])

Tensors typically have two forms of methods,
- one method that returns another tensor and
- another method that performs the operation in place. That is, the values in memory for that tensor are changed without creating a new tensor. In-place functions are always followed by an underscore, for example `z.add()` and `z.add_()`.

```python
# Return a new tensor z + 1
z.add(1)
```

    tensor([[ 2.5249,  2.2378],
            [ 2.1303,  2.2541],
            [ 2.9113,  2.7750]])

```python
# z tensor is unchanged
z
```

    tensor([[ 1.5249,  1.2378],
            [ 1.1303,  1.2541],
            [ 1.9113,  1.7750]])

```python
# Add 1 and update z tensor in-place
z.add_(1)
```

    tensor([[ 2.5249,  2.2378],
            [ 2.1303,  2.2541],
            [ 2.9113,  2.7750]])

```python
# z has been updated
z
```

    tensor([[ 2.5249,  2.2378],
            [ 2.1303,  2.2541],
            [ 2.9113,  2.7750]])

### Reshaping

Reshaping tensors is a really common operation. First to get the size and shape of a tensor use `.size()`. Then, to reshape a tensor, use `.resize_()`. Notice the underscore, reshaping is an in-place operation.

```python
z.size()
```

    torch.Size([3, 2])

```python
z.resize_(2, 3)
```

    tensor([[ 2.5249,  2.2378,  2.1303],
            [ 2.2541,  2.9113,  2.7750]])

```python
z
```

    tensor([[ 2.5249,  2.2378,  2.1303],
            [ 2.2541,  2.9113,  2.7750]])

## Numpy to Torch and back

Converting between Numpy arrays and Torch tensors is super simple and useful. To create a tensor from a Numpy array, use `torch.from_numpy()`. To convert a tensor to a Numpy array, use the `.numpy()` method.

```python
a = np.random.rand(4,3)
a
```

    array([[0.31670837, 0.82964753, 0.57837596],
           [0.28175374, 0.62455649, 0.73768427],
           [0.04358543, 0.92271407, 0.81054312],
           [0.30038343, 0.42245814, 0.2019588 ]])

```python
b = torch.from_numpy(a)
b
```

    tensor([[ 0.3167,  0.8296,  0.5784],
            [ 0.2818,  0.6246,  0.7377],
            [ 0.0436,  0.9227,  0.8105],
            [ 0.3004,  0.4225,  0.2020]], dtype=torch.float64)

```python
b.numpy()
```

    array([[0.31670837, 0.82964753, 0.57837596],
           [0.28175374, 0.62455649, 0.73768427],
           [0.04358543, 0.92271407, 0.81054312],
           [0.30038343, 0.42245814, 0.2019588 ]])

The memory is shared between the Numpy array and Torch tensor, so if you change the values in-place of one object, the other will change as well.

```python
# Multiply PyTorch Tensor by 2, in place
b.mul_(2)
```

    tensor([[ 0.6334,  1.6593,  1.1568],
            [ 0.5635,  1.2491,  1.4754],
            [ 0.0872,  1.8454,  1.6211],
            [ 0.6008,  0.8449,  0.4039]], dtype=torch.float64)

```python
# Numpy array matches new values from Tensor
a
```

    array([[0.63341675, 1.65929505, 1.15675192],
           [0.56350749, 1.24911298, 1.47536853],
           [0.08717086, 1.84542815, 1.62108625],
           [0.60076685, 0.84491629, 0.40391761]])

```python

```