# ENV/ATM 415: Climate Laboratory

[Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany

# Lecture 7: Elementary greenhouse models

## Contents

1. [A single layer atmosphere](#section1)
2. [Introducing the two-layer grey gas model](#section2)
3. [Tuning the grey gas model to observations](#section3)
4. [Level of emission](#section4)
5. [Radiative forcing in the 2-layer grey gas model](#section5)
6. [Summary](#section6)

____________

## 1. A single layer atmosphere
____________

We will make our first attempt at quantifying the greenhouse effect in the simplest possible greenhouse model: a single layer of atmosphere that is able to absorb and emit longwave radiation.

### Assumptions

- Atmosphere is a single layer of air at temperature $T_a$
- Atmosphere is **completely transparent to shortwave** solar radiation.
- The **surface** absorbs shortwave radiation $(1-\alpha) Q$
- Atmosphere is **completely opaque to infrared** radiation
- Both surface and atmosphere emit radiation as **blackbodies** ($\sigma T_s^4, \sigma T_a^4$)
- Atmosphere radiates **equally up and down** ($\sigma T_a^4$)
- There are no other heat transfer mechanisms

We can now use the concept of energy balance to ask what the temperature need to be in order to balance the energy budgets at the surface and the atmosphere, i.e. the **radiative equilibrium temperatures**.

### Energy balance at the surface

\begin{align}
\text{energy in} &= \text{energy out} \\
(1-\alpha) Q + \sigma T_a^4 &= \sigma T_s^4 \\
\end{align}

The presence of the atmosphere above means there is an additional source term: downwelling infrared radiation from the atmosphere.

We call this the **back radiation**.

### Energy balance for the atmosphere

\begin{align}
\text{energy in} &= \text{energy out} \\
\sigma T_s^4 &= A\uparrow + A\downarrow = 2 \sigma T_a^4 \\
\end{align}

which means that
$$ T_s = 2^\frac{1}{4} T_a \approx 1.2 T_a $$

So we have just determined that, in order to have a purely **radiative equilibrium**, we must have $T_s > T_a$.

*The surface must be warmer than the atmosphere.*

### Solve for the radiative equilibrium surface temperature

Now plug this into the surface equation to find

$$ \frac{1}{2} \sigma T_s^4 = (1-\alpha) Q  $$

and use the definition of the emission temperature $T_e$ to write

$$ (1-\alpha) Q = \sigma T_e^4 $$

*In fact, in this model, $T_e$ is identical to the atmospheric temperature $T_a$, since all the OLR originates from this layer.*

Solve for the surface temperature:
$$ T_s = 2^\frac{1}{4} T_e $$

Putting in observed numbers, $T_e = 255$ K gives a surface temperature of
$$T_s = 303 ~\text{K}$$

This model is one small step closer to reality: surface is warmer than atmosphere, emissions to space generated in the atmosphere, atmosphere heated from below and helping to keep surface warm.

BUT our model now overpredicts the surface temperature by about 15ºC (or K).

Ideas about why?

Basically we just need to read our **list of assumptions** above and realize that none of them are very good approximations:

- Atmosphere absorbs some solar radiation.
- Atmosphere is NOT a perfect absorber of longwave radiation
- Absorption and emission varies strongly with wavelength *(atmosphere does not behave like a blackbody)*.
- Emissions are not determined by a single temperature $T_a$ but by the detailed *vertical profile* of air temperture.
- Energy is redistributed in the vertical by a variety of dynamical transport mechanisms (e.g. convection and boundary layer turbulence).

____________

## 2. Introducing the two-layer grey gas model
____________

Let's generalize the above model just a little bit to build a slighly more realistic model of longwave radiative transfer.

We will address two shortcomings of our single-layer model:
1. No vertical structure
2. 100% longwave opacity

Relaxing these two assumptions gives us what turns out to be a very useful prototype model for **understanding how the greenhouse effect works**.

### Assumptions

- The atmosphere is **transparent to shortwave radiation** (still)
- Divide the atmosphere up into **two layers of equal mass** (the dividing line is thus at 500 hPa pressure level)
- Each layer **absorbs only a fraction $\epsilon$ ** of whatever longwave radiation is incident upon it.
- We will call the fraction $\epsilon$ the **absorptivity** of the layer.
- Assume $\epsilon$ is the same in each layer

This is called the **grey gas** model, where grey here means the emission and absorption have no spectral dependence.

We can think of this model informally as a "leaky greenhouse".

Note that the assumption that $\epsilon$ is the same in each layer is appropriate if the absorption is actually carried out by a gas that is **well-mixed** in the atmosphere.

Out of our two most important absorbers:

- CO$_2$ is well mixed
- H$_2$O is not (mostly confined to lower troposphere due to strong temperature dependence of the saturation vapor pressure).

But we will ignore this aspect of reality for now.

In order to build our model, we need to introduce one additional piece of physics known as **Kirchoff's Law**:

$$ \text{absorptivity} = \text{emissivity} $$

So if a layer of atmosphere at temperature $T$ absorbs a fraction $\epsilon$ of incident longwave radiation, it must emit

$$ \epsilon ~\sigma ~T^4 $$

both up and down.

### A sketch of the radiative fluxes in the 2-layer atmosphere

- Surface temperature is $T_s$
- Atm. temperatures are $T_0, T_1$ where $T_0$ is closest to the surface.
- absorptivity of atm layers is $\epsilon$
- Surface emission is $\sigma T_s^4$
- Atm emission is $\epsilon \sigma T_0^4, \epsilon \sigma T_1^4$ (up and down)
- Absorptivity = emissivity for atmospheric layers
- a fraction $(1-\epsilon)$ of the longwave beam is **transmitted** through each layer

Let's think about the upwelling beam of longwave radiation, which we denote $U$.

We start at the surface. The upward flux from the surface to layer 0 is

$$U_0 = \sigma T_s^4$$

Following this beam upward, we can write the upward flux from layer 0 to layer 1 as the sum of the transmitted part of what is incident from below, and the new emissions from layer 0:

$$ U_1 = (1-\epsilon) \sigma T_s^4 + \epsilon \sigma T_0^4 $$

Continuing to follow the same beam, the upwelling flux above layer 1 is

$$ U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4 $$

Since there is no more atmosphere above layer 1, this upwelling beam is our OLR for this model:

$$ OLR = U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4 $$

which works out to

$$ OLR= (1-\epsilon)^2 \sigma T_s^4 + \epsilon(1-\epsilon)\sigma T_0^4 + \epsilon \sigma T_1^4 $$

Here the three terms represent **contributions to the total OLR that originate from each of the three levels**

*What happens to this expression if $\epsilon=1$? What does this represent physically?*

*What about $\epsilon=0$? *

By allowing the atmosphere to partially absorb emissions from other levels, we now see that the OLR includes emissions from every level - and therefore affected by temperature at every level!

____________

## 3. Tuning the grey gas model to observations
____________

In building our new model we have introduced exactly one parameter, the absorptivity $\epsilon$. We need to choose a value for $\epsilon$.

We will tune our model so that it **reproduces the observed global mean OLR** given **observed global mean temperatures**.

To get appropriate temperatures for $T_s, T_0, T_1$, let's revisit the [global, annual mean lapse rate plot from NCEP Reanalysis data](L06_Radiation.ipynb) from the previous lecture.

### Temperatures

First, we set
$$T_s = 288 \text{ K}  $$

From the lapse rate plot, an average temperature for the layer between 1000 and 500 hPa is

$$ T_0 = 275 \text{ K}$$

Defining an average temperature for the layer between 500 and 0 hPa is more ambiguous because of the lapse rate reversal at the tropopause. We will choose

$$ T_1 = 230 \text{ K}$$

From the graph, this is approximately the observed global mean temperature at 275 hPa or about 10 km.

### OLR

From the [observed global energy budget](Lecture01 -- Planetary energy budget.ipynb) we set

$$ OLR = 238.5 \text{ W m}^{-2} $$

### Solving for $\epsilon$

We wrote down the expression for OLR as a function of temperatures and absorptivity in our model above.

All we need to do is plug the observed values into the above expression for OLR, and solve for $\epsilon$.

It is a quadratic equation. We could work out the exact solution using the quadratic formula.

But let's do it graphically, using Python!

### Exercise

- Write a Python function that implements the OLR formula for the leaky greenhouse model
- The function should accept four input parameters:
    - The three temperatures $T_s, T_0, T_1$
    - The emissivity $\epsilon$
- Using this function, make a graph of OLR vs. $\epsilon$ *for the observed temperature values*
- For the graph, $\epsilon$ should range between 0 and 1.

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

sigma = 5.67E-8

def OLR(e, Ts=288, T0=275, T1=230):
    return (1-e)*((1-e)*sigma*Ts**4 + e*sigma*T0**4) + e*sigma*T1**4
```

```python
import numpy as np
e = np.linspace(0,1)
plt.plot(e, OLR(e))
plt.plot(e, 238.5 * np.ones_like(e))
plt.xlabel('epsilon (grey absorptivity)')
plt.ylabel('OLR (W/m2)')
plt.grid()
```

Note if you solve the quadratic equation algebraically you will get two solutions:

- $\epsilon = 0.58$
- $\epsilon = 3.93$

Why is the second solution not physically meaningful?

From the graphical solution, we end up choosing $\epsilon = 0.58$.

This is the absorptivity that guarantees that **our model reproduces the observed OLR given the observed temperatures**.

____________

## 4. Level of emission
____________

Now that we have tuned up our model, we can see exactly how strongly each level contributes to the OLR.

The three components of the OLR are

\begin{align*}
OLR_s &= (1-\epsilon)^2 \sigma T_s^4  \\
OLR_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
OLR_1 &= \epsilon \sigma T_1^4
\end{align*}

which of course add up to the total OLR we wrote down above.

**Write some simple Python code to calculate each term in the OLR using the observed temperatures and the tuned value $\epsilon = 0.58$. Fill out the list below using your calculated numbers.**

```python

```

**Contributions to the OLR originating from each level, in W/m2:**

- Surface: 68.8
- Level 0: 79.0
- Level 1: 92.0

```python
68.8 + 79. + 92.
```

    239.8

Notice that the largest single contribution is coming from the top layer.

*This is in spite of the fact that the emissions from this layer are weak, because it is so cold.*

###  Changing the level of emission by adding absorbers

Adding some extra greenhouse absorbers will mean that a greater fraction of incident longwave radiation is absorbed in each layer.

Thus **$\epsilon$ must increase** as we add greenhouse gases.

Suppose we have $\epsilon$ initially, and the absorptivity increases to $\epsilon_2 = \epsilon + \Delta \epsilon$.

Suppose further that this increase happens **abruptly** so that there is no time for the temperatures to respond to this change. **We hold the temperatures fixed** in the column and ask how the radiative fluxes change.

**Do you expect the OLR to increase or decrease?**

Let's use our two-layer leaky greenhouse model to investigate the answer.

The components of the OLR before the perturbation are

\begin{align*}
OLR_s &= (1-\epsilon)^2 \sigma T_s^4  \\
OLR_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
OLR_1 &= \epsilon \sigma T_1^4
\end{align*}

and after the perturbation we have

\begin{align*}
OLR_s &= (1-\epsilon - \Delta \epsilon)^2 \sigma T_s^4  \\
OLR_0 &= (\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon)\sigma T_0^4 \\
OLR_1 &= (\epsilon + \Delta \epsilon) \sigma T_1^4
\end{align*}

Let's subtract off the original components to get the contributions to the **change in OLR** from each layer:

\begin{align*}
\Delta OLR_s &= \left[(1-\epsilon - \Delta \epsilon)^2 - (1-\epsilon)^2\right]\sigma T_s^4  \\
\Delta OLR_0 &= \left[(\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon) - \epsilon(1-\epsilon)  \right] \sigma T_0^4 \\
\Delta OLR_1 &= \left[(\epsilon + \Delta \epsilon) - \epsilon \right] \sigma T_1^4
\end{align*}

Now expand this out, but to make things easier to deal with, neglect term in $\Delta \epsilon^2$ (very small - we will be considering changes of less than 10% in $\epsilon$):

\begin{align*}
\Delta OLR_s &\approx (\Delta \epsilon) \left[ -2(1-\epsilon) \right] \sigma T_s^4  \\
\Delta OLR_0 &\approx (\Delta \epsilon) (1 - 2 \epsilon) \sigma T_0^4 \\
\Delta OLR_1 &\approx (\Delta \epsilon) \sigma T_1^4
\end{align*}

Now look at the **sign** of each term. Recall that $0 < \epsilon < 1$. **Which terms in the OLR go up and which go down?**

**THIS IS VERY IMPORTANT, SO STOP AND THINK ABOUT IT.**

The contribution from the **surface** must **decrease**, while the contribution from the **top layer** must **increase**.

**When we add absorbers, the average level of emission goes up!**

____________

## 5. Radiative forcing in the 2-layer grey gas model
____________

We now define a very important quantity:

### "Radiative forcing" is the change in radiative flux at TOA after adding absorbers

In this model, only the longwave flux can change, so we define the radiative forcing as

$$ R = - \Delta OLR $$

(with the minus sign so that $R$ is positive when the climate system is gaining extra energy).

We just worked out that whenever we add some extra absorbers, the emissions to space (on average) will originate from higher levels in the atmosphere.

What does this mean for OLR? Will it increase or decrease?

To get the answer, we just have to sum up the three contributions we wrote above:

\begin{align*}
R &= -\Delta OLR_s - \Delta OLR_0 - \Delta OLR_1  \\
  &= -\Delta \epsilon \left[  -2(1-\epsilon) \sigma T_s^4 + (1 - 2 \epsilon) \sigma T_0^4  + \sigma T_1^4 \right]
\end{align*}

Is this a positive or negative number? The key point is this:

**It depends on the temperatures, i.e. on the lapse rate.**

### Greenhouse effect for an isothermal atmosphere

Stop and think about this question:

If the **surface and atmosphere are all at the same temperature**, does the OLR go up or down when $\epsilon$ increases (i.e. we add more absorbers)?

Understanding this question is key to understanding how the greenhouse effect works.

#### Let's solve the isothermal case

We will just set $T_s = T_0 = T_1$ in the above expression for the radiative forcing.

What do you get?

```python

```

#### The answer is $R=0$

For an isothermal atmosphere, there is **no change** in OLR when we add extra greenhouse absorbers. Hence, no radiative forcing and no greenhouse effect.

Why?

The level of emission still must go up. But since the temperature at the upper level is the **same** as everywhere else, the emissions are exactly the same.

### The radiative forcing (change in OLR) depends on the lapse rate!

For a more realistic example of radiative forcing due to an increase in greenhouse absorbers, we our observed temperatures and the tuned value for $\epsilon$.

We'll express the answer in W m$^{-2}$ for a 2% increase in $\epsilon$:

$$ \Delta \epsilon = 0.02 \times 0.58 $$

```python
epsilon = 0.58
delta_epsilon = 0.02 * epsilon
delta_epsilon
```

    0.0116

Calculate the three components of the radiative forcing:

```python
sigma = 5.67E-8
Ts = 288.
T0 = 275.
T1 = 230.
```

```python
#  Component originating from the surface
Rs = -delta_epsilon * (-2*(1-epsilon)*sigma * Ts**4)
Rs
```

    3.800933621091533

```python
#  Component originating from level 0
R0 = -delta_epsilon * (1-2*epsilon) * sigma * T0**4
R0
```

    0.6018549074999996

```python
#  Component originating from level 1
R1 = -delta_epsilon * sigma * T1**4
R1
```

    -1.8405702252

So just add them up to get the total radiative forcing:

```python
R = Rs + R0 + R1
R
```

    2.5622183033915324

So in our example, **the OLR decreases by 2.6 W m$^{-2}$**, or equivalently, the **radiative forcing is +2.6 W m$^{-2}$.**

What we have just calculated is this:

*Given the observed lapse rates, a small increase in absorbers will cause a small decrease in OLR.*

The greenhouse effect thus gets stronger, and energy will begin to accumulate in the system -- which will eventually cause temperatures to increase as the system adjusts to a new equilibrium.

____________

## 6. Summary
____________

## Key physical lessons

- Putting a **layer of longwave absorbers** above the surface keeps the **surface substantially warmer**, because of the **backradiation** from the atmosphere (greenhouse effect).
- The **grey gas** model assumes that each layer absorbs and emits a fraction $\epsilon$ of its blackbody value, independent of wavelength.

- With **incomplete absorption** ($\epsilon < 1$), there are contributions to the OLR from every level and the surface (there is no single **level of emission**)
- Adding more absorbers means that **contributions to the OLR** from **upper levels** go **up**, while contributions from the surface go **down**.
- This upward shift in the weighting of different levels is what we mean when we say the **level of emission goes up**.

- The **radiative forcing** caused by an increase in absorbers **depends on the lapse rate**.
- For an **isothermal atmosphere** the radiative forcing is zero and there is **no greenhouse effect**
- The radiative forcing is positive for our atmosphere **because tropospheric temperatures tends to decrease with height**.

```python

```


```python
import pandas                   as pd
import numpy                    as np
import matplotlib.pyplot        as plt
import seaborn                  as sns
import iirsBenchmark.regressors as regressors

import Auxiliary
import re

from sympy            import sympify
from IPython.display  import display
from CriticalDiagrams import draw_cd_diagram
from ExpressionParser import count_expression_nodes

# Setting everything up
Auxiliary.create_global_file_path_variables(results_path='../../results')
Auxiliary.set_mpl_sns_params(abnt=False)

%matplotlib inline

analyzed_path = f'../post hoc analysis files/' # Where to save the plots
```

## Post processing reported expressions

```python
def post_processing_regression_models(info_to_extract, index=False, digest=True):

    data = None

    # Only methods that returns an analytical expression
    for regressor in ['ITEA_regressor', 'Linear_regressor', 'Lasso_regressor', 'Operon_regressor']:
        df_regression = pd.read_csv(f'{Auxiliary.regression_path}{regressor}.csv')

        regressor_no_sufix = regressor.replace('_regressor', '')

        cols = [col_name for col_name in df_regression.columns
                if col_name.startswith(info_to_extract)]

        def simplify_and_count_nodes(s):
            try:
                return count_expression_nodes(
                    str(
                        sympify(
                            s.replace("^", "**").replace('id', '')
                        )
                    ).replace("**", "^")
                )
            except Exception as e:
                #print(e)
                return np.nan

        df_regression[f'expr_size_{regressor_no_sufix}'] = \
            df_regression['text_representation'].apply(simplify_and_count_nodes)

        df_regression[f'text_representation_{regressor_no_sufix}'] = \
            df_regression['text_representation']

        df_regression = df_regression.drop('text_representation', axis=1)

        if digest == False:
            data = df_regression if data is None else pd.merge(
                data, df_regression, on="dataset", how='outer')

            continue

        new_df = df_regression[['dataset']].reset_index(drop=True).join(
            pd.DataFrame(
                df_regression.loc[:, cols+[f'expr_size_{regressor_no_sufix}']].values,
                columns=cols+[f'expr_size_{regressor_no_sufix}']
            )
        ).groupby(['dataset']).mean().reset_index(level=[0])

        new_df.columns = [
            col_name + f'_{regressor_no_sufix}' if col_name.startswith(info_to_extract) else col_name
            for col_name in new_df.columns]

        data = new_df if data is None else pd.merge(
            data, new_df, on="dataset", how='outer')

    if index:
        data.set_index('dataset', inplace=True)

    return data
```

```python
df_expressions = results_regression_analysis_models('rmse_test', index=True, digest=False)

display(df_expressions)

top1 = pd.read_csv(
    '../../datasets/FeynmanEquations.csv')[['Filename', '# variables']].nlargest(1, '# variables')

for regressor in ['ITEA', 'Operon', 'Lasso', 'Linear']:
    display(df_expressions.nsmallest(1, f'expr_size_{regressor}')[[f'text_representation_{regressor}', f'expr_size_{regressor}']].values[0])
```

    ITEA_regressor
    Linear_regressor
    Lasso_regressor
    Operon_regressor

      rep_x
      rmse_train_x
      rmse_test_x
      r2_train_x
      r2_test_x
      tot_time_x
      expr_size_ITEA
      text_representation_ITEA
      rep_y
      rmse_train_y
      ...
      expr_size_Lasso
      text_representation_Lasso
      rep_y
      rmse_train_y
      rmse_test_y
      r2_train_y
      r2_test_y
      tot_time_y
      expr_size_Operon
      text_representation_Operon

      dataset

      I.12.1
      23
      2.842589e-07
      3.272662e-07
      1.0
      1.0
      404.790747
      67
      0.184*log(x_0^-3 * x_1^-2) + 0.0*cos(x_0^-1 * ...
      0
      1.327760
      ...
      9
      2.96*x_0 + 2.987*x_1 + -8.893
      19
      7.387918e-07
      8.117035e-07
      1.000000
      1.000000
      0.175114
      5.0
      (0.00000 + (0.23037 * ((1.44886 * x_1) * (2.99...

      I.12.1
      23
      2.842589e-07
      3.272662e-07
      1.0
      1.0
      404.790747
      67
      0.184*log(x_0^-3 * x_1^-2) + 0.0*cos(x_0^-1 * ...
      0
      1.327760
      ...
      9
      2.96*x_0 + 2.987*x_1 + -8.893
      25
      6.921345e-07
      8.643262e-07
      1.000000
      1.000000
      0.302377
      5.0
      (0.00000 + ((-0.45102) * ((0.92112 * x_2) * ((...

      I.12.1
      23
      2.842589e-07
      3.272662e-07
      1.0
      1.0
      404.790747
      67
      0.184*log(x_0^-3 * x_1^-2) + 0.0*cos(x_0^-1 * ...
      0
      1.327760
      ...
      9
      2.96*x_0 + 2.987*x_1 + -8.893
      14
      6.183685e-07
      6.952388e-07
      1.000000
      1.000000
      0.304705
      5.0
      (0.00000 + (1.76753 * ((1.60208 * x_2) * (0.35...

      I.12.1
      23
      2.842589e-07
      3.272662e-07
      1.0
      1.0
      404.790747
      67
      0.184*log(x_0^-3 * x_1^-2) + 0.0*cos(x_0^-1 * ...
      0
      1.327760
      ...
      9
      2.96*x_0 + 2.987*x_1 + -8.893
      0
      9.744334e-07
      1.156478e-06
      1.000000
      1.000000
      0.182745
      5.0
      (0.00000 + (0.70875 * ((3.03057 * x_1) * (0.46...

      I.12.1
      23
      2.842589e-07
      3.272662e-07
      1.0
      1.0
      404.790747
      67
      0.184*log(x_0^-3 * x_1^-2) + 0.0*cos(x_0^-1 * ...
      0
      1.327760
      ...
      9
      2.96*x_0 + 2.987*x_1 + -8.893
      18
      5.708457e-07
      8.220077e-07
      1.000000
      1.000000
      0.235873
      5.0
      (0.00000 + (0.27914 * (((2.68410 * x_1) * (0.7...

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      I.43.43
      14
      4.907660e-05
      5.860011e-05
      1.0
      1.0
      5062.816287
      160
      178.171*sqrt(x_0^-4 * x_1^2 * x_2^-2 * x_3^2) ...
      0
      0.852137
      ...
      17
      -0.738*x_0 + 0.54*x_1 + -0.649*x_2 + 0.531*x_3...
      10
      9.372554e-04
      9.790063e-04
      1.000000
      1.000000
      1.999059
      41.0
      (0.00017 + ((-27.56030) * (((((0.59361 * x_4) ...

      I.43.43
      14
      4.907660e-05
      5.860011e-05
      1.0
      1.0
      5062.816287
      160
      178.171*sqrt(x_0^-4 * x_1^2 * x_2^-2 * x_3^2) ...
      0
      0.852137
      ...
      17
      -0.738*x_0 + 0.54*x_1 + -0.649*x_2 + 0.531*x_3...
      13
      4.804711e-03
      5.772241e-03
      0.999991
      0.999988
      1.870718
      91.0
      (0.00201 + ((-0.07452) * ((((((-0.03067) * x_1...

      I.43.43
      14
      4.907660e-05
      5.860011e-05
      1.0
      1.0
      5062.816287
      160
      178.171*sqrt(x_0^-4 * x_1^2 * x_2^-2 * x_3^2) ...
      0
      0.852137
      ...
      17
      -0.738*x_0 + 0.54*x_1 + -0.649*x_2 + 0.531*x_3...
      6
      6.390276e-03
      7.025082e-03
      0.999984
      0.999982
      2.001663
      48.0
      (0.00080 + (4134.54883 * ((((((exp(1.97547) * ...

      I.43.43
      14
      4.907660e-05
      5.860011e-05
      1.0
      1.0
      5062.816287
      160
      178.171*sqrt(x_0^-4 * x_1^2 * x_2^-2 * x_3^2) ...
      0
      0.852137
      ...
      17
      -0.738*x_0 + 0.54*x_1 + -0.649*x_2 + 0.531*x_3...
      4
      4.640332e-03
      4.769003e-03
      0.999991
      0.999992
      1.717469
      58.0
      ((-0.02785) + (0.01793 * ((((((-2.89637) * x_2...

      I.43.43
      14
      4.907660e-05
      5.860011e-05
      1.0
      1.0
      5062.816287
      160
      178.171*sqrt(x_0^-4 * x_1^2 * x_2^-2 * x_3^2) ...
      0
      0.852137
      ...
      17
      -0.738*x_0 + 0.54*x_1 + -0.649*x_2 + 0.531*x_3...
      19
      9.878633e-03
      1.072610e-02
      0.999961
      0.999959
      1.005736
      49.0
      ((-0.00092) + ((-0.11420) * (((((1.23495 * (0....

89100 rows × 32 columns

    array(['-0.0*sqrt(x_0^2 * x_1^2) + 0.0*log(x_0^-2 * x_1^4) + -0.0*log(x_0 * x_1^3) + 1.0*id(x_0 * x_1) + 0.0*id(x_0 * x_1^-2) + -0.0*expn(x_0^4) + -0.0*id(x_1^-3) + 0.0',
           5], dtype=object)

    array(['((-2.97555) + ((-0.00000) * ((((exp((1.55768 * x_3)) ^ 2) / (((((-1.00114) * x_3) ^ 2) / ((-0.19546) * x_2) / ((0.04506 / (((-1.00114) * x_3) ^ 2)) ^ 2)) * exp(((-0.19546) * x_3)))) - (((-0.33103) * x_1) + ((-0.19546) * x_2))) / (((((0.04506 / (((-1.00114) * x_3) ^ 2)) ^ 2) * exp(((-0.19546) * x_3))) * exp(exp(((-0.19546) * x_2)))) * exp((1.55768 * x_3)) * 0.04506) / ((((0.04506 / (((-1.00114) * x_3) ^ 2)) ^ 2) * (2.48929 / ((-1.00114) * x_3))) * exp((1.55768 * x_3))))))',
           1.0], dtype=object)

    array(['2.138', 1], dtype=object)

    array(['2.961*x_0 + 2.988*x_1 + -8.898', 9], dtype=object)

```python
regression_analysis_results = results_regression_analysis_models('rmse_test', index=True)

fig, ax = plt.subplots(1, 1, figsize=(6, 2.0))

cols = [col_name for col_name in regression_analysis_results.columns if col_name.startswith('expr_size_')]

res_exprSize = regression_analysis_results.loc[:, cols]

meds = res_exprSize.median()
meds.sort_values(ascending=(True), inplace=True)
res_exprSize = res_exprSize[meds.index]

flierprops = dict(marker='o', markerfacecolor='black', markersize=2,  markeredgecolor='black')

sns.boxplot(data=res_exprSize, orient="h",medianprops={'color': 'k'},
    showfliers=True, ax=ax, flierprops=flierprops) #, x=x, y=y, order=order)

for box, color in zip(ax.artists, sns.color_palette("Blues", len(ax.artists))):
    box.set_color(color)

ax.set_yticklabels(
    [s.replace(f'expr_size_', '')
     for s in meds.index],
    rotation = 0, fontsize = 12, ha='right')

for spine in ['right', 'top', 'bottom']:
    ax.spines[spine].set_visible(False)

ax.grid()

plt.tight_layout()
plt.savefig(f'{analyzed_path}expression_sizes.pdf', bbox_inches='tight')
plt.show()
```

```python
fig, axs = plt.subplots(1, 1, figsize=(6, 1.5))

res_exprSize.columns = [col.replace('expr_size_', '') for col in res_exprSize.columns]

melted_res = pd.melt(res_exprSize.reset_index(), id_vars=['dataset'])
melted_res.columns = ['dataset_name', 'classifier_name', 'accuracy']

melted_res['accuracy'] = np.max(melted_res['accuracy']) - melted_res['accuracy']

draw_cd_diagram(
    df_perf=melted_res,
    labels=False, ax=axs, width=8, textspace=1.0, reverse=False)

plt.tight_layout()
plt.savefig(f'{analyzed_path}expression_sizes_criticaldiagram.pdf', bbox_inches='tight')
plt.show()
```

post hoc analysis/scripts and notebooks/Expression sizes.ipynb


Universidad Nacional de Córdoba - Facultad de Matemática, Astronomía, Física y Computación

Diplomatura en Ciencia de Datos, Aprendizaje Automático y sus Aplicaciones

 Regresión Linear - Ejemplo

 Análisis y Visualización de Datos - 2019

En este ejemplo veremos cómo implementar una regresión logística para predecir una variable numérica. Volveremos a utilizar el dataset [Human Freedom Index 2018](https://www.cato.org/human-freedom-index-new) de el instituto Cato.

Usaremos la misma versión del conjunto de datos que en el práctico.

En esta notebook vamos a tratar de estimar una función lineal que modele el cambio a través del tiempo de la libertad personal y la económica.

```python
%matplotlib inline

import numpy
import pandas
import matplotlib.pyplot as plt
import seaborn
```

```python
seaborn.set_context(context='talk', font_scale=1.2)
seaborn.set_style("darkgrid")
```

```python
BLUE = '#35A7FF'
RED = '#FF5964'
GREEN = '#6BF178'
YELLOW = '#FFE74C'
```

```python
dataset = pandas.read_csv(
    'https://object.cato.org/sites/cato.org/files/human-freedom-index-files/human-freedom-index-2019.csv')
dataset.shape
```

    (1620, 120)

```python
score_cols = [col for col in dataset.columns if 'pf_identity' in col] + [
    'pf_score', # Personal Freedom (score)
    'pf_rank', # Personal Freedom (rank)
    'ef_score', # Economic Freedom (score)
    'ef_rank', # Economic Freedom (rank)
    'hf_score', # Human Freedom (score)
    'hf_rank', # Human Freedom (rank)
]

important_cols = ['year', 'ISO_code', 'countries', 'region'] + score_cols
```

```python
dataset = dataset[important_cols].replace('-', numpy.nan)
for score_col in score_cols:
    dataset[score_col] = pandas.to_numeric(dataset[score_col])
dataset
```

      year
      ISO_code
      countries
      region
      pf_identity_legal
      pf_identity_sex_male
      pf_identity_sex_female
      pf_identity_sex
      pf_identity_divorce
      pf_identity
      pf_score
      pf_rank
      ef_score
      ef_rank
      hf_score
      hf_rank

      0
      2017
      ALB
      Albania
      Eastern Europe
      0.0
      10.0
      10.0
      10.0
      7.5
      5.8
      8.01
      46.0
      7.67
      30.0
      7.84
      38.0

      1
      2017
      DZA
      Algeria
      Middle East & North Africa
      NaN
      0.0
      0.0
      0.0
      0.0
      0.0
      5.20
      146.0
      4.77
      159.0
      4.99
      155.0

      2
      2017
      AGO
      Angola
      Sub-Saharan Africa
      10.0
      0.0
      0.0
      0.0
      5.0
      5.0
      5.98
      121.0
      4.83
      158.0
      5.40
      151.0

      3
      2017
      ARG
      Argentina
      Latin America & the Caribbean
      10.0
      10.0
      10.0
      10.0
      10.0
      10.0
      8.04
      41.0
      5.67
      147.0
      6.86
      77.0

      4
      2017
      ARM
      Armenia
      Caucasus & Central Asia
      7.0
      10.0
      10.0
      10.0
      7.5
      8.2
      7.15
      72.0
      7.70
      27.0
      7.42
      54.0

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      1615
      2008
      AUS
      Australia
      Oceania
      NaN
      10.0
      10.0
      10.0
      10.0
      10.0
      9.29
      7.0
      8.18
      6.0
      8.73
      4.0

      1616
      2008
      DNK
      Denmark
      Western Europe
      NaN
      10.0
      10.0
      10.0
      10.0
      10.0
      9.49
      3.0
      7.98
      9.0
      8.73
      4.0

      1617
      2008
      CHE
      Switzerland
      Western Europe
      NaN
      10.0
      10.0
      10.0
      10.0
      10.0
      9.31
      6.0
      8.35
      4.0
      8.83
      3.0

      1618
      2008
      NZL
      New Zealand
      Oceania
      NaN
      10.0
      10.0
      10.0
      10.0
      10.0
      9.42
      4.0
      8.46
      3.0
      8.94
      2.0

      1619
      2008
      HKG
      Hong Kong
      East Asia
      NaN
      10.0
      10.0
      10.0
      10.0
      10.0
      9.13
      12.0
      9.11
      1.0
      9.12
      1.0

1620 rows × 16 columns

En el práctico habíamos trabajado sobre las variables `ef_score` y `pf_score`, que hacen referencia a los índices de libertad personal y libertad económica de cada páis. Además, sabemos que el dataset incluye una medición del índice anual por país desde 2008 hasta 2016, aunque hay datos faltantes de algunos indicadores.

La motivación de este análisis comienza con este gráfico, que muestra una tendencia decreciente de la libertad personal y una tendencia ascendiente de la libertad económica. La libertad humana o `pf_score` es el promedio de ambos indicadores

```python
plt.figure(figsize=(10,6))
seaborn.lineplot(data=dataset,
                 x='year', y='ef_score',
                 label='Economic Freedom')
seaborn.lineplot(data=dataset,
                 x='year', y='pf_score',
                 label='Personal Freedom')

seaborn.lineplot(data=dataset,
                 x='year', y='hf_score',
                 label='Human Freedom')
plt.legend()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.ylabel('Index score')
seaborn.despine()
```

Este fenómeno podría estar dado por varios factores:
  * Hay pocos países en los que la libertad personal está decreciendo, pero su libertad económica se mantiene constante.
  * Los países para los cuales sube la libertad económica decrecen en libertad personal.
  * **¿Otras?**

Veamos qué sucede en Argentina. Si graficamos ambas variables, vemos que "van bajando". Formalmente, esto significa que hay la recta que las modela tiene una pendiente negativa.

**¿Y esto, es grave?**

```python
plt.figure(figsize=(15, 8))
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='ef_score', label='Economic Freedom')
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='pf_score', label='Personal Freedom')
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='hf_score', label='Human Freedom')
plt.legend()
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
seaborn.despine()
plt.xlim(2008, 2016)
```

Podemos graficar varios países, pero es difícil comparar visualmente entre tantas variables, qué países "decrecen" más rápido que otros.

```python
countries = ['Argentina', 'Brazil', 'Mexico', 'Bolivia',
             'Uruguay', 'Peru', 'Colombia', 'Venezuela']
g = seaborn.FacetGrid(dataset, col="countries",
                      margin_titles=True, aspect=2, col_wrap=2,
                      col_order=countries)
g.map(seaborn.lineplot, "year", "ef_score", color=BLUE)
g.map(seaborn.lineplot, "year", "pf_score", color=RED)
g.set(xlim=(2008, 2016))

print('Puntajes de libertad para América Latina');
```

Para poder comparar la situación de Argentina con otros países, podemos comparar la pendiente de la recta de la regresión lineal. A partir del gráfico anterior pudimos ver que la mayoría de los países tiene tendencias similares y que se pueden estimar con una recta sin perder generalidad. Esto es posible también, en cierta medida, porque tenemos pocos puntos para estimar.

## Regresión lineal

Queremos ver cuál es el coeficiente que relaciona ambas variables.

Primero: **¿Cuál es la variable dependiente? ¿Cuál la independiente?**

Una vez que hemos determinado eso, lo que queremos encontrar es la función de la siguiente forma:

$$ef = a * year + b$$

Reescribiremos esto como una función $e$ (por economic), cuyo parámetro es el valor $y$ (por year):

$$e(y) = a * y + b$$

Vamos a describir los ejemplos como pares $(x_y, x_e)$, donde $x_y$ denota el `year` y $x_e$ denota `ef_score`.

Para encontrar la recta $e$ que mejor describe los datos, queremos minimizar el error cuadrático medio, definido como:

$$mse = \frac{1}{|X|} \sum_{x \in X} (e(x_y) - x_e)^2 $$

Recordemos que para minimizar una función, una buena opción es comenzar por buscar los puntos estacionarios, donde la derivada se anula. Por suerte, la función $mse$ es convexa, y por lo tanto tiene todos sus puntos estacionarios son minimizadores. El minimizador es el valor de los parámetros $a$ y $b$ que minimizan la función. Ahora, en hemos cambiado nuestras "variables", lo que buscamos es encontrar la función adecuada, por lo tanto lo que cambia son los valores de los parámetros que definen la función.

Primero, notemos que:

$$\frac{\partial}{\partial a}e(y) = x_p$$

$$\frac{\partial}{\partial b}e(y) = 1$$

Con eso, calculamos las derivadas parciales para cada parámetro de la función $mse$.

$$\frac{\partial}{\partial a}mse = \frac{2}{|X|} \sum_{x \in X} (e(x_p) - x_e) \frac{\partial}{\partial a} (e(x_p) - x_e) = \frac{2}{|X|} \sum_{x \in X} (e(x_p) - x_e) e_p $$

$$\frac{\partial}{\partial b}mse = \frac{2}{|X|} \sum_{x \in X} \frac{\partial}{\partial b} e(x_p) - x_e = \frac{2}{|X|} \sum_{x \in X} e(x_p) - x_e $$

A pesar del formulerío, es bastante simple. Sólo reemplazamos $mse$ por su definición, y luego aplicamos un par de reglas como "la derivada de la suma es la suma de las derivadas", la regla de la cadena, o la definición de la derivada de la función cuadrática.

Una vez que tenemos esos valores, tenemos que igualarlos a cero para encontrar los puntos estacionarios.

\begin{align}
 \frac{\partial}{\partial a}mse &= \frac{2}{|X|} \sum_{x \in X} (e(x_y) - x_e) x_y = 0 \\
                                &\Rightarrow a = \frac{\bar{x_y} \bar{x_e} - \overline{x_yx_e}}{(\bar{x_y})^2 - \overline{x_y^2}}
\end{align}

\begin{align}
 \frac{\partial}{\partial b}mse &= \frac{2}{|X|} \sum_{x \in X} e(x_y) - x_e = 0 \\
                                &\Rightarrow b = \bar{x_e} - a \bar{x_y}
\end{align}

Donde $\bar{x}$ es la media del valor para todos los ejemplos. Vamos a confiar en estas fórmulas, pero una demostración de las mismas está en:

https://medium.freecodecamp.org/machine-learning-mean-squared-error-regression-line-c7dde9a26b93

```python
def estimate_params(X_y, X_e):
    """Caculates the value of a using all the examples."""
    num = numpy.mean(X_y)*numpy.mean(X_e) - numpy.mean(numpy.multiply(X_y, X_e))
    denom = numpy.mean(X_y)**2 - numpy.mean(numpy.multiply(X_y, X_y))
    a = num / denom
    b = numpy.mean(X_e) - a * numpy.mean(X_y)
    return a, b
```

```python
# Asumimos que todos los registros que tienen pf_score tienen el año.
a, b = estimate_params(
    dataset[(dataset.ISO_code == 'ARG') &
            (dataset.pf_score.notnull())].year.dropna(),
    dataset[dataset.ISO_code == 'ARG'].pf_score)
a, b
```

    (-0.02048484848483261, 49.32575757572563)

```python
def base_linear_regression(x_y, a):
    return a * x_y
```

```python
def regplot2(data, x, y, reg_func, **reg_func_args):
    """Plots the x, y columns from data and builds a
    line with the regression reg_func."""
    plt.figure(figsize=(8, 6))
    seaborn.scatterplot(data=data, x=x, y=y, color=BLUE)
    minimum = data[x].min()
    maximum = data[x].max()
    plt.plot([minimum, maximum],
             [reg_func(minimum, **reg_func_args),
              reg_func(maximum, **reg_func_args)],
             color=GREEN)
    seaborn.despine()
    plt.show()
```

```python
regplot2(dataset[dataset.ISO_code == 'ARG'],
         x='year', y='pf_score', reg_func=base_linear_regression,
         a=a)
```

Vemos que la recta va en el sentido correcto, pero está demasiado abajo. Esto ocurre porque no hemos usado el término de bias.

Redefinamos entonces la regresión logística

```python
def linear_regression(x_y, a, b):
    return a * x_y + b
```

```python
regplot2(dataset[dataset.ISO_code == 'ARG'],
         x='year', y='pf_score', reg_func=linear_regression,
         a=a, b=b)
```

## Continuamos el análisis

Perfecto! Ahora podemos calcular las pendientes y los biases para todos los años, para regresiones que estimen el `pf_score`.

```python
def build_regressions(data, x_var='year', y_var='pf_score'):
    records = []
    for code in data.ISO_code.unique():
        record = [code, data[data.ISO_code == code].region.values[0],
                  data[data.ISO_code == code].countries.values[0]]
        y_data = data[data.ISO_code == code][y_var].dropna()
        # Comprobamos que hay datos en el intervalo
        if len(y_data) <= 1:
            continue
        x_data = data[(data.ISO_code == code) &
                      (data[y_var].notnull())][x_var].dropna()
        # Estimamos los parámetros
        a, b = estimate_params(x_data, y_data)
        # Calculamos el error cuadrático medio de la regresión lineal estimada
        predictions = numpy.apply_along_axis(
            lambda x: linear_regression(x, a, b), 0, x_data)
        mse = numpy.mean(numpy.power(predictions - y_data, 2))
        record.extend([a, b, mse])
        # Agregamos el registro
        records.append(record)
    return pandas.DataFrame.from_records(
        records, columns=['ISO_code', 'region', 'country',
                          'slope', 'bias', 'mse']
    )
```

```python
pf_regressions = build_regressions(dataset).set_index('ISO_code')
pf_regressions[:10]
```

      region
      country
      slope
      bias
      mse

      ISO_code

      ALB
      Eastern Europe
      Albania
      -0.036909
      82.084545
      0.044086

      DZA
      Middle East & North Africa
      Algeria
      -0.006121
      17.587939
      0.009140

      AGO
      Sub-Saharan Africa
      Angola
      0.087636
      -170.678182
      0.039059

      ARG
      Latin America & the Caribbean
      Argentina
      -0.020485
      49.325758
      0.003178

      ARM
      Caucasus & Central Asia
      Armenia
      -0.016667
      40.756667
      0.008773

      AUS
      Oceania
      Australia
      -0.011576
      32.513212
      0.002516

      AUT
      Western Europe
      Austria
      0.037394
      -66.048303
      0.007005

      AZE
      Caucasus & Central Asia
      Azerbaijan
      -0.105758
      218.958121
      0.060136

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.032909
      74.170545
      0.009934

      BHR
      Middle East & North Africa
      Bahrain
      -0.089515
      186.388242
      0.048762

```python
plt.figure(figsize=(10,6))
seaborn.histplot(pf_regressions.slope, color=RED, label='Global')
seaborn.histplot(
    pf_regressions[pf_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
plt.xlabel('Slope of linear regression that fits the pf_score of each country')
plt.legend()
seaborn.despine()
```

```python
def plot_regressions(regressions):
    plt.figure(figsize=(10,6))
    colors = seaborn.color_palette("cubehelix", len(regressions))
    for color, (year, row) in zip(colors, regressions.iterrows()):
        minimum, maximum = 2008, 2016
        plt.plot([minimum, maximum],
                 [linear_regression(minimum, row['slope'], row['bias']),
                  linear_regression(maximum, row['slope'], row['bias'])],
                 color=color, label=str(year), linestyle='--')
    plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
    seaborn.despine()
```

```python
plot_regressions(pf_regressions.loc[['ARG', 'BRA', 'VEN', 'CAN']])
plt.xlabel('year')
plt.ylabel('pf_score')
plt.ylim(4, 10)
```

### Libertad Económica

```python
ef_regressions = build_regressions(dataset, y_var='ef_score').set_index('ISO_code')
ef_regressions[:10]
```

      region
      country
      slope
      bias
      mse

      ISO_code

      ALB
      Eastern Europe
      Albania
      0.047333
      -87.801333
      0.003397

      DZA
      Middle East & North Africa
      Algeria
      -0.042606
      90.805697
      0.009553

      AGO
      Sub-Saharan Africa
      Angola
      0.015394
      -25.786303
      0.084069

      ARG
      Latin America & the Caribbean
      Argentina
      -0.083273
      173.018364
      0.132188

      ARM
      Caucasus & Central Asia
      Armenia
      0.013333
      -19.165333
      0.003609

      AUS
      Oceania
      Australia
      -0.010909
      30.026545
      0.002294

      AUT
      Western Europe
      Austria
      -0.008485
      24.819758
      0.000690

      AZE
      Caucasus & Central Asia
      Azerbaijan
      0.042242
      -78.787879
      0.007204

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.026667
      61.038667
      0.003569

      BHR
      Middle East & North Africa
      Bahrain
      -0.010364
      28.097818
      0.009243

```python
plot_regressions(ef_regressions.loc[['ARG', 'BRA', 'VEN', 'CAN']])
plt.xlabel('year')
plt.ylabel('ef_score')
```

```python
plt.figure(figsize=(10,6))
seaborn.histplot(ef_regressions.slope, color=RED, label='Global')
seaborn.histplot(
    ef_regressions[ef_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
plt.xlabel('Slope of linear regression that fits the ef_score of each country')
plt.legend()
seaborn.despine()
```

## Análisis conjunto

**¿Cuáles es el 10% de países en los que la libertad humana disminuye más rápidamente?**

```python
quantil = pf_regressions.slope.quantile(0.1)
pf_regressions[pf_regressions.slope < quantil].country
```

    ISO_code
    BTN               Bhutan
    BRA               Brazil
    BRN    Brunei Darussalam
    BDI              Burundi
    EGY                Egypt
    GMB          Gambia, The
    MUS            Mauritius
    NPL                Nepal
    NER                Niger
    RWA               Rwanda
    SYR                Syria
    TJK           Tajikistan
    THA             Thailand
    TUR               Turkey
    UKR              Ukraine
    VEN            Venezuela
    YEM          Yemen, Rep.
    Name: country, dtype: object

**¿Cuáles es el 10% de países en los que la libertad económica disminuye más rápidamente?**

```python
quantil = ef_regressions.slope.quantile(0.1)
ef_regressions[ef_regressions.slope < quantil].country
```

    ISO_code
    ARG            Argentina
    BRA               Brazil
    BRN    Brunei Darussalam
    EGY                Egypt
    FJI                 Fiji
    GHA                Ghana
    IRQ                 Iraq
    KWT               Kuwait
    LBR              Liberia
    LBY                Libya
    PNG      Pap. New Guinea
    SLE         Sierra Leone
    SDN                Sudan
    SYR                Syria
    TUN              Tunisia
    VEN            Venezuela
    ZMB               Zambia
    Name: country, dtype: object

**¿Cuáles son los paises en los que la libertad económica aumenta pero la libertad personal disminuye (rápidamente)?**

```python
all_countries = dataset.ISO_code.unique()
codes = []
for code in all_countries:
    if (code in ef_regressions.index and code in pf_regressions.index and
        ef_regressions.loc[code].slope > 0.02 and
        pf_regressions.loc[code].slope < -0.02):
        codes.append(code)
ef_regressions.loc[codes].country
```

    ISO_code
    ALB        Albania
    AZE     Azerbaijan
    BGR       Bulgaria
    BDI        Burundi
    KHM       Cambodia
    CPV     Cape Verde
    CHN          China
    GMB    Gambia, The
    GTM      Guatemala
    GIN         Guinea
    ISL        Iceland
    IDN      Indonesia
    KAZ     Kazakhstan
    LAO           Laos
    MYS       Malaysia
    MLT          Malta
    MEX         Mexico
    MAR        Morocco
    MMR        Myanmar
    NPL          Nepal
    NER          Niger
    PRY       Paraguay
    PHL    Philippines
    RUS         Russia
    RWA         Rwanda
    ESP          Spain
    TZA       Tanzania
    VNM        Vietnam
    Name: country, dtype: object

# Errores

Calculamos el mse pero nunca lo usamos. Veamos cómo son los países para los que la regresión linear no produce una buena aproximación

```python
pf_regressions.mse.sort_values()[-10:]
```

    ISO_code
    TLS    0.106220
    SYC    0.107328
    TUR    0.126490
    CAF    0.142989
    VEN    0.145224
    COD    0.165123
    LBY    0.182368
    GNB    0.194737
    YEM    0.195064
    BDI    0.206774
    Name: mse, dtype: float64

```python
plt.figure(figsize=(10,6))
countries = ['BDI', 'YEM', 'GNB', 'LBY']
seaborn.lineplot(
    data=dataset[dataset.ISO_code.isin(countries)], x='year', y='hf_score',
    hue='countries')
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
seaborn.despine()
```

Claramente se ve que estas funciones no podían ser estimadas satisfactoriamente con una recta, pero aún así, la tendencia general (descendiente o ascendiente) habría sido aproximada


# +
# **First Notebook: Virtual machine test and assignment submission**
#### This notebook will test that the virtual machine (VM) is functioning properly and will show you how to submit an assignment to the autograder.  To move through the notebook just run each of the cells.  You will not need to solve any problems to complete this lab.  You can run a cell by pressing "shift-enter", which will compute the current cell and advance to the next cell, or by clicking in a cell and pressing "control-enter", which will compute the current cell and remain in that cell.  At the end of the notebook you will export / download the notebook and submit it to the autograder.
#### ** This notebook covers: **
#### *Part 1:* Test Spark functionality
#### *Part 2:* Check class testing library
#### *Part 3:* Check plotting
#### *Part 4:* Check MathJax formulas
#### *Part 5:* Export / download and submit

### ** Part 1: Test Spark functionality **

#### ** (1a) Parallelize, filter, and reduce **

```python
# Check that Spark is working
largeRange = sc.parallelize(xrange(100000))
reduceTest = largeRange.reduce(lambda a, b: a + b)
filterReduceTest = largeRange.filter(lambda x: x % 7 == 0).sum()

print reduceTest
print filterReduceTest

# If the Spark jobs don't work properly these will raise an AssertionError
assert reduceTest == 4999950000
assert filterReduceTest == 714264285
```

    4999950000
    714264285

#### ** (1b) Loading a text file **

```python
# Check loading data with sc.textFile
import os.path
baseDir = os.path.join('data')
inputPath = os.path.join('cs100', 'lab1', 'shakespeare.txt')
fileName = os.path.join(baseDir, inputPath)

rawData = sc.textFile(fileName)
shakespeareCount = rawData.count()

print shakespeareCount

# If the text file didn't load properly an AssertionError will be raised
assert shakespeareCount == 122395
```

    122395

### ** Part 2: Check class testing library **

#### ** (2a) Compare with hash **

```python
# TEST Compare with hash (2a)
# Check our testing library/package
# This should print '1 test passed.' on two lines
from test_helper import Test

twelve = 12
Test.assertEquals(twelve, 12, 'twelve should equal 12')
Test.assertEqualsHashed(twelve, '7b52009b64fd0a2a49e6d8a939753077792b0554',
                        'twelve, once hashed, should equal the hashed value of 12')
```

    1 test passed.
    1 test passed.

#### ** (2b) Compare lists **

```python
# TEST Compare lists (2b)
# This should print '1 test passed.'
unsortedList = [(5, 'b'), (5, 'a'), (4, 'c'), (3, 'a')]
Test.assertEquals(sorted(unsortedList), [(3, 'a'), (4, 'c'), (5, 'a'), (5, 'b')],
                  'unsortedList does not sort properly')
```

    1 test passed.

### ** Part 3: Check plotting **

#### ** (3a) Our first plot **
#### After executing the code cell below, you should see a plot with 50 blue circles.  The circles should start at the bottom left and end at the top right.

```python
# Check matplotlib plotting
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from math import log

# function for generating plot layout
def preparePlot(xticks, yticks, figsize=(10.5, 6), hideLabels=False, gridColor='#999999', gridWidth=1.0):
    plt.close()
    fig, ax = plt.subplots(figsize=figsize, facecolor='white', edgecolor='white')
    ax.axes.tick_params(labelcolor='#999999', labelsize='10')
    for axis, ticks in [(ax.get_xaxis(), xticks), (ax.get_yaxis(), yticks)]:
        axis.set_ticks_position('none')
        axis.set_ticks(ticks)
        axis.label.set_color('#999999')
        if hideLabels: axis.set_ticklabels([])
    plt.grid(color=gridColor, linewidth=gridWidth, linestyle='-')
    map(lambda position: ax.spines[position].set_visible(False), ['bottom', 'top', 'left', 'right'])
    return fig, ax

# generate layout and plot data
x = range(1, 50)
y = [log(x1 ** 2) for x1 in x]
fig, ax = preparePlot(range(5, 60, 10), range(0, 12, 1))
plt.scatter(x, y, s=14**2, c='#d6ebf2', edgecolors='#8cbfd0', alpha=0.75)
ax.set_xlabel(r'$range(1, 50)$'), ax.set_ylabel(r'$\log_e(x^2)$')
pass
```

### ** Part 4: Check MathJax Formulas **

#### ** (4a) Gradient descent formula **
#### You should see a formula on the line below this one: $$ \scriptsize \mathbf{w}_{i+1} = \mathbf{w}_i - \alpha_i \sum_j (\mathbf{w}_i^\top\mathbf{x}_j  - y_j) \mathbf{x}_j \,.$$

#### This formula is included inline with the text and is $ \scriptsize (\mathbf{w}^\top \mathbf{x} - y) \mathbf{x} $.

#### ** (4b) Log loss formula **
#### This formula shows log loss for single point. Log loss is defined as: $$  \begin{align} \scriptsize \ell_{log}(p, y) = \begin{cases} -\log (p) & \text{if } y = 1 \\\ -\log(1-p) & \text{if } y = 0 \end{cases} \end{align} $$

### ** Part 5: Export / download and submit **

#### ** (5a) Time to submit **

#### You have completed the lab.  To submit the lab for grading you will need to download it from your IPython Notebook environment.  You can do this by clicking on "File", then hovering your mouse over "Download as", and then clicking on "Python (.py)".  This will export your IPython Notebook as a .py file to your computer.
#### To upload this file to the course autograder, go to the edX website and find the page for submitting this assignment.  Click "Choose file", then navigate to and click on the downloaded .py file.  Now click the "Open" button and then the "Check" button.  Your submission will be graded shortly and will be available on the page where you submitted.  Note that when submission volumes are high, it may take as long as an hour to receive results.

Week 0 - Course Software Setup/lab0_student.ipynb

Week 1 - Data Science Background and Course Software Setup/lab0_student.ipynb


# Naive Bayes Spam Filter From Scratch

Although referred to as "Idiot Bayes" by some, Naive Bayes models perform surprisingly well despite strong independence assumptions in the model. In this project we will build a spam classifier from scratch for SMS messages using Multinomial Naive Bayes which is known to suit situations where data can be turned into counts, such as word counts in text.

You can find more information about the dataset by clicking [here](https://archive.ics.uci.edu/ml/datasets/sms+spam+collection).

## 1. Reading in the data

```python
import pandas as pd
import numpy as np
import re
```

```python
collection = pd.read_csv("SMSSpamCollection", sep="\t", header=None, names=["Label", "SMS"])
collection.head()
```

      Label
      SMS

      0
      ham
      Go until jurong point, crazy.. Available only ...

      1
      ham
      Ok lar... Joking wif u oni...

      2
      spam
      Free entry in 2 a wkly comp to win FA Cup fina...

      3
      ham
      U dun say so early hor... U c already then say...

      4
      ham
      Nah I don't think he goes to usf, he lives aro...

```python
collection["Label"].value_counts(dropna=False)
```

    ham     4825
    spam     747
    Name: Label, dtype: int64

As we can see, non-spam messages are labelled as "ham" and there's a lot more non-spam mails compared to spam mails.

## 2. Preprocessing

As a first step, we will randomize the entire dataset, then split it into the training and test sets. Training set will consist of 80% of the dataset, leaving 20% for testing purposes.

```python
#Specify random state to make sure the results are reproducable:
sampled_set = collection.sample(frac=1, random_state=1)

#Split into 80% - 20% sets:
train_set = sampled_set[:4459].copy().reset_index(drop=True)
test_set = sampled_set[4459:].copy().reset_index(drop=True)

train_set["Label"].value_counts(normalize=True)
```

    ham     0.865441
    spam    0.134559
    Name: Label, dtype: float64

```python
test_set["Label"].value_counts(normalize=True)
```

    ham     0.867925
    spam    0.132075
    Name: Label, dtype: float64

When we first read the dataset, we have checked value counts for the labels. We have found that 87% of the messages were labelled "ham" whereas 13% were labelled spam. We can see that our test and training sets have similar proportions for each value.

We will now work on the SMS column, starting with removing punctuation and making all letters lowercase.

```python
train_set["SMS"] = train_set["SMS"].str.replace(r"\W", " ").str.lower()
train_set.head()
```

      Label
      SMS

      0
      ham
      yep  by the pretty sculpture

      1
      ham
      yes  princess  are you going to make me moan

      2
      ham
      welp apparently he retired

      3
      ham
      havent

      4
      ham
      i forgot 2 ask ü all smth   there s a card on ...

We will now create a vocabulary for the training set. We will use sets for this to prevent duplicates.

```python
train_set["SMS"] = train_set["SMS"].str.split()

vocab = []
for message in train_set["SMS"]:
    for word in message:
        vocab.append(word)

vocab = set(vocab) #removing duplicates
vocab = list(vocab)
print(vocab[:5])
```

    ['ibm', 'monkey', 'sam', 'perform', 'still']

We will now create a dictionary containing the words as keys and word counts per SMS. Then, we will form a DataFrame from it.

```python
word_counts_per_sms = {word: [0] * len(train_set["SMS"]) for word in vocab}

for index, message in enumerate(train_set["SMS"]):
    for word in message:
        word_counts_per_sms[word][index] += 1

word_counts = pd.DataFrame(word_counts_per_sms)
training_set = pd.concat([train_set, word_counts], axis=1)
training_set.head()
```

      Label
      SMS
      ibm
      monkey
      sam
      perform
      still
      secretary
      in2
      figure
      ...
      rgent
      help
      alive
      fne
      ibh
      pub
      jog
      duo
      08000930705
      unconscious

      0
      ham
      [yep, by, the, pretty, sculpture]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      1
      ham
      [yes, princess, are, you, going, to, make, me,...
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      2
      ham
      [welp, apparently, he, retired]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3
      ham
      [havent]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      4
      ham
      [i, forgot, 2, ask, ü, all, smth, there, s, a,...
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

5 rows × 7787 columns

## 3. Creating our spam filter

We will be using the probabilities of the equations below for classification:

\begin{equation}
P(spam | w_1,w_2, ..., w_n) \propto P(spam) \cdot \prod_{i=1}^{n}P(w_i|spam) \\
P(ham | w_1,w_2, ..., w_n) \propto P(ham) \cdot \prod_{i=1}^{n}P(w_i|ham)
\end{equation}

For each P(w|Spam) and P(w|Ham) in the formulas above, we will be using these equations with Laplace smoothing:

\begin{equation}
P(w_i|spam) = \frac{N_{w_i|spam} + \alpha}{N_{spam} + \alpha \cdot N_{vocabulary}} \\
P(w_i|ham) = \frac{N_{w_i|ham} + \alpha}{N_{ham} + \alpha \cdot N_{vocabulary}}
\end{equation}

```python
n_spam = sum([len(row[1]) for row in training_set if row[0] == "spam"])
n_ham = sum([len(row[1]) for row in training_set if row[0] == "ham"])
n_vocab = len(vocab)

p_spam = training_set["Label"].value_counts()["spam"] / training_set["Label"].shape[0]
p_ham = training_set["Label"].value_counts()["ham"] / training_set["Label"].shape[0]

print(p_spam, p_ham)
```

    0.13455931823278763 0.8654406817672123

Now that we have calculated the constants, we will move on to calculating the parameters.

```python
spam_df = training_set[training_set["Label"] == "spam"].copy()
ham_df = training_set[training_set["Label"] == "ham"].copy()

alpha = 1 #Since we use Laplace smoothing

spam_params = {word: (sum(count) + alpha) / (n_spam + (alpha * n_vocab)) for word, count in spam_df.iloc[:, 2:].iteritems()}
ham_params = {word: (sum(count) + alpha) / (n_ham + (alpha * n_vocab)) for word, count in ham_df.iloc[:, 2:].iteritems()}
```

We have now calculated our parameters, too. We can now begin writing our function.

## 4. Classification process

```python
def classify(message, verbose=True):

    message = re.sub('\W', ' ', message).lower().split()

    try:
        p_spam_given_message = p_spam * np.prod([spam_params[word] for word in message])
        p_ham_given_message = p_ham * np.prod([ham_params[word] for word in message])
    except:
        p_spam_given_message = p_spam
        p_ham_given_message = p_ham

    if verbose == True:
        print("P(Spam|message):", p_spam_given_message)
        print("P(Ham|message):", p_ham_given_message)

        if p_ham_given_message > p_spam_given_message:
            print("Label: Non-spam")
        elif p_ham_given_message < p_spam_given_message:
            print("Label: Spam")
        else:
            print("This message carries equal probabilities of being non-spam vs. spam. Please consult a human for accurate classification.")
    else:
        if p_ham_given_message > p_spam_given_message:
            return "ham"
        elif p_ham_given_message < p_spam_given_message:
            return "spam"
        else:
            return "needs human classification"

classify("success")
```

    P(Spam|message): 1.728443394127009e-05
    P(Ham|message): 0.00022233543526453754
    Label: Non-spam

Our function seems to work well!

## 5. Measuring Accuracy

In this step, we will be measuring our function's accuracy on the test set using the formula below:

\begin{equation}
\text{Accuracy} = \frac{\text{number of correctly classified messages}}{\text{total number of classified messages}}
\end{equation}

```python
correctly_classified = 0
total_classified = test_set.shape[0]

test_set["prediction"] = test_set["SMS"].apply(classify, verbose=False)

for row in test_set.iterrows():
    row = row[1]
    if row["Label"] == row["prediction"]:
        correctly_classified += 1

test_set["prediction"].value_counts()
```

    ham     1062
    spam      51
    Name: prediction, dtype: int64

```python
accuracy = correctly_classified / total_classified
print("Predictions made by our function are {pred}% accurate.\nCorrect: {correct}\nIncorrect: {inc}\nTotal: {tot}".format(pred=round(accuracy * 100, 2), correct=correctly_classified, inc=total_classified - correctly_classified, tot=total_classified))
```

    Predictions made by our function are 91.37% accurate.
    Correct: 1017
    Incorrect: 96
    Total: 1113

## 6. Conclusion

In this project, we have built a spam classifier from scratch. As we can see in the final step's output, our spam filter has an accuracy of ~91.37% with 1017 correctly classified messages out of 1113.


# Μέτρηση της ροής της κατανομής νετρονίων σε υποκρίσιμο πυρηνικό αντιδραστήρα

## Θεωρητική εισαγωγή

### Αντιδραστήρας
Στο πείραμα αυτό μελετάται η κατανομή των νετρονίων σε υποκρίσιμο πυρηνικό αντιδραστήρα ($k_{eff} \approx 0.8$). Ο αντιδραστήρας αποτελείται από ράβδους φυσικού ουρανίου $U_3O_8(UO_2 . 2UO_3)$, επιβραδυντή (και ανακλαστή) ελαφρύ ύδωρ $H_2O$. Το ουράνιο βρίσκεται σε σωλήνες αλουμινίου για την αποφυγή διαρροής ραδιενεργού υλικού.

Στο κέντρο του αντιδραστήρα τοποθετείται πηγή νετρονίων 5Ci $\ ^{241}AmBe$.

Τα νετρόνια της πηγής θερμοποιούνται καθώς συγκρούνται με τους πυρήνες υδρογόνου στο νερό, και εν συνεχεία μέρος τους αντιδρά με το $\ ^{235}U$ προκαλώντας σχάση και απελευθέρωση επιπλέον νετρονίων. Το νερό δρα και ως ανακλαστής, ώστε μέρος των νετρονίων που κανονικά θα διέφευγαν του αντιδραστήρα να ανακλώνται πίσω.

### Διάχυση νετρονίων
Θεωρώντας ότι τα νετρόνια είναι μονοενεργειακά μπορούμε να χρησιμοποιήσουμε την μέθοδο διάχυσης μιας ομάδας. Σύμφωνα με τη μέθοδο αυτή, όταν ο αντιδραστήρας λειτουργεί σε σταθερές συνθήκες, για τα νετρόνια ισχύει:

$$ created - escaped - absorbed = 0 $$

ή

$$ D\nabla^2\Phi - \Sigma_a\Phi + S = 0$$

όπου Φ η ροή των θερμικών νετρονίων, D ο συντελεστής διάχυσης, $\Sigma_a$ η μακροσκοπική ενεργός διατομή απορρόφησης, και S ο αριθμός n ανά μονάδα όγκου ανά μονάδα χρόνου.

Επιλύοντας την παραπάνω εξίσωση καταλήγουμε στην:

$$ \Phi(r,z)=A\cdot J_0\cdot (2.405/R_{ex}r)\cdot e^{-\gamma z} $$

ή

$$ln\Phi(r,z) = ln[A\cdot J_0\cdot (2.405/R_{ex}r)] -\gamma z \qquad (1)$$

Για δεδομένο $r=r_0$ η συνάρτηση του $ln\Phi$ ως προς το ύψος $z$ είναι ευθεία με κλίση $\gamma$. Επομένως το ύψος από το σημείο $r_0$ στο οποίο η ροή μειώνεται στο $1/e$ της αρχικής, είναι $l=1/\gamma$ και ονομάζεται _μήκος χαλάρωσης_.

### Ιδιαιτερότητες πειράματος

Λόγω συντήρησης των ράβδων καυσίμου ο αντιδραστήρας περιείχε μόνο την πηγή $\ ^{241}AmBe$ και το νερό, χωρίς καμία ράβδο καυσίμου. Επίσης, θεωρήθηκε ότι η ενεργοποίηση του $In$ οφείλεται αποκλειστικά σε θερμικά νετρόνια.

---

## Πείραμα

Φύλλα $In$ τοποθετήθηκαν στον αντιδραστήρα σε συγκεκριμένη θέση, αλλά σε διαφορετικό ύψος ο καθένας. Στο φυσικό $In$ υπάρχει 95.7(1)% $\ ^{115}In$. Μετά από ακτινοβόληση 4-5 ωρών (περίπου 5 ημιζωές του $\ ^{116}In$ ώστε η ενεργότητα να φτάσει περίπου το 99(1)% της μέγιστης), τα φύλλα αφαιρέθηκαν.

Ο ρυθμός υποβάθρου χωρίς $In$ ήταν $8\ cpm$ και με **μη** ενεργοποιημένο $In$ ήταν $9\ cpm$, και αφαιρείται από τον αληθή ρυθμό.

Η ομάδα μας μέτρησε τους ψευδείς ρυθμούς R' από τις πηγές 3,6,9. Οι R' μετατράπηκαν σε αληθείς μέσω της σχέσης:

$$ R = \frac{R'}{1-\tau R'} $$

όπου $\tau=50\ \mu s$ o νεκρός χρόνος του ανιχνευτή Geiger.

Η απόδοση του Geiger στις συγκεκριμένες ενέργειες των $\beta^-$ είναι $\varepsilon=100\%$ και ο παράγοντας γεωμετρίας $g = 0.4$. H ενεργότητα $A'$ δίνεται από την σχέση:

$$ A' = \frac{R}{\varepsilon\cdot g} $$

Η ενεργότητα ανά gr $A$ δίνεται από την σχέση:

$$A = \frac{A'}{m}$$

όπου $m$ η μάζα του $In$ σε gr.

Θεωρούμε ότι οι αντίστοιχες ενεργότητες που υπολογίσαμε αντιστοιχούν στο μέσο της διάρκειας μέτρησης. Πχ. αν $t_{exit}=18\ min$ o χρόνος από την έξοδο από τον αντιδραστήρα και $t_0=60\ s$ η διάρκεια μέτρησης, τότε η ενεργότητα αντιστοιχεί στην χρονική στιγμή $t=18.5\ min$.

H ροή των θερμικών νετρονίων δίνεται από την σχέση:

$$ \Phi_\theta = \frac {A_\theta} {\sigma \cdot M (1-e^{-\lambda t_{irr} }) }$$

όπου:
 - $t_{irr}$ ο χρόνος ακτινοβόλησης στον αντιδραστήρα
 - $\sigma = 162\ barn$
 - $M = N_A\cdot m/A$, με $A=115$

```python
import numpy
import matplotlib as mpl
import matplotlib.pyplot as plt
import pandas as pd
import scipy.optimize
import scipy.constants
import sympy
sympy.init_printing()

# Used for latex-labels in plots .
# (if you don't have the dependencies from http://stackoverflow.com/a/37218925/4230591 installed,
# it will raise an error during plotting)
from matplotlib import rc
rc('text', usetex=True)
```

```python
# ====================================================================================================
# CONSTANTS AND MISC DATA
#
# One-letter variables should not be used to store data,
# since they'll be needed for `sympy.symbols()` to display functions.
# ====================================================================================================

t_count = 60    # secs
t_count_in_mins = t_count / 60.
t_exit_reactor = 18 * 60.
t_end = 66 * 60.
t_between_measurements = 2 * 60.

R_background = 8 /60.
R_background_In = 9 /60.
m_In = 1 /1000    # 1gr
indium_115_ratio = .957

M_constant = scipy.constants.N_A * m_In * indium_115_ratio / 115

irradiation_start = '3:00'
irradiation_t = 4.5 * 3600    # 4-5h
g_times_e = 0.4
τ_constant=50 * 10**-6

half_life = 54.29 * 60.  #sec
decay_constant = numpy.log(2) / half_life

σ_In = 162 * 10e-28

# =========================================================
# IRRELEVANT DATA (not used in calculations)
# =========================================================
#
# ~40μSv/h near reactor
# AmBe ~10e+6 n/s
# k_eff = ~.8
# v_geiger = 500V
```

```python
# =========================================================
# R
# =========================================================

R_αληθής, σ_R_αληθής, R_ψευδής, τ, σ_R_ψευδής, t = sympy.symbols("R, \sigma_R, R' tau, \sigma_R', t")
_R_αληθής_f = lambda R_ψευδής, τ: R_ψευδής/(1-R_ψευδής*τ)
R_αληθής_f = sympy.lambdify((R_ψευδής, τ), _R_αληθής_f(R_ψευδής, τ), (sympy, numpy))

print('\nΟ αληθής ρυθμός R δίνεται από την σχέση:')
sympy.Eq(R_αληθής, R_αληθής_f(R_ψευδής, τ))
```

```python
# =========================================================
# R' error
# =========================================================
_σ_R_ψευδής_f = lambda R_ψευδής, t: sympy.sqrt(R_ψευδής) / t
# (Currently `lambdify` can't handle both arrays *and* sympy functions,)
# (therefor one function is used for pretty printing and a different one for calculations.)
σ_R_ψευδής_f = sympy.lambdify((R_ψευδής, t), _σ_R_ψευδής_f(R_ψευδής, t), modules=['numpy'])

print('\nΤο σφάλμα στον ψευδή ρυθμό δίνεται από την σχέση:')
sympy.Eq(σ_R_ψευδής, _σ_R_ψευδής_f(R_ψευδής, t))
```

```python
# =========================================================
# R error
# =========================================================

_err_R_αληθής_sq = sympy.diff(R_αληθής_f(R_ψευδής, τ), R_ψευδής)**2 * σ_R_ψευδής**2
_err_R_αληθής = sympy.sqrt(_err_R_αληθής_sq)
err_R_αληθής_f = sympy.lambdify((R_ψευδής, σ_R_ψευδής, τ), _err_R_αληθής, modules=['numpy'])

print('\nΤο σφάλμα στον αληθή ρυθμό δίνεται από την σχέση:')
sympy.Eq(σ_R_αληθής, _err_R_αληθής)
```

```python
# ====================================================================================================
# DATA (sources 3, 6, 9)
# ====================================================================================================

df3 = pd.read_csv('source3', names=['t (min)', "R'(cpm)"])
df6 = pd.read_csv('source6', names=['t (min)', "R'(cpm)"])
df9 = pd.read_csv('source9', names=['t (min)', "R'(cpm)"])

dfs_3_6_9 = {3: df3, 6: df6, 9: df9}

for source_n, _df in dfs_3_6_9.items():
    # the measured rate is considered to have occured exactly in the middle of the measurement's duration
    _df['t (min)'] += t_count_in_mins / 2
    _df['t (s)'] = _df['t (min)'] * 60

    _df["R'(cps)"] = _df["R'(cpm)"] / 60
    _df["R(cps)"] = R_αληθής_f(_df["R'(cps)"], τ_constant) - R_background_In

    # errors
    _df["error σR'(cps)"] = σ_R_ψευδής_f(_df["R'(cpm)"], 60)
    _df["error σR(cps)"] = err_R_αληθής_f(_df["R(cps)"], _df["error σR'(cps)"], τ_constant)
    _df["relative σR %"] = _df["error σR(cps)"] / _df["R(cps)"] * 100

    print('\n' + '='*70)
    print('Πηγή {}:\n'.format(source_n))
    print(_df)

    source_label = r'source: {}'.format(source_n)
    plt.plot(_df['t (s)'], _df["R(cps)"], marker='.', label=source_label)
    plt.ylim(ymin=0, ymax=160)

plt.xlabel('t(s)')
plt.ylabel('R(cps)')
plt.legend(loc='upper right')
plt.title('FIGURE 1:\nRates vs time of sources 3,6 and 9. \n(Errorbars were omitted since they are too small.)')
plt.grid('on')
plt.show()
```

### Υπολογισμός ρυθμού R κατά την έξοδο του In από τον αντιδραστήρα

[Θεωρώντας ότι το σφάλμα στους ρυθμούς](http://www.mathworks.com/help/stats/examples/pitfalls-in-fitting-nonlinear-models-by-transforming-to-linearity.html) είναι πολλαπλασιαστικό και όχι προσθετικό, χρησιμοποιούμε μέθοδο ελαχίστων τετραγώνων, αφού μετατρέψουμε την εκθετική συνάρτηση σε γραμμική.

```python
# ====================================================================================================
# Linear fit
# ====================================================================================================

_f = lambda x, a, b: a+x*b
lin_popt, lin_pcov = scipy.optimize.curve_fit(_f, df3['t (s)'], scipy.log(df3['R(cps)']),
                                              sigma=_df["error σR(cps)"], absolute_sigma=True)
lin_perr = numpy.sqrt(numpy.diag(lin_pcov))

print('a = {:.6}, b = {:.6}'.format(*lin_popt))
print('Ro = {:.4}'.format(scipy.e**lin_popt[0]))
print('λ/b = {:.1%}'.format(-decay_constant/lin_popt[1]))
print('σφάλμα στο Ro: {:.4}'.format(scipy.e**lin_perr[0]))
# (using python variables in markdown is not supported yet, therefor it has to be done manually)
```

    a = 5.26188, b = -0.00021825
    Ro = 192.8
    λ/b = 97.5%
    σφάλμα στο Ro: 2.819

Ο ρυθμός κατά το τέλος της ακτινοβόλησης είναι $R_0 = 193(3) cps$.

```python
# ====================================================================================================
# Non linear fit (not used in further calculations; displayed just for comparison)
# ====================================================================================================

def exp_f(x, a,b):
    return a * scipy.e**(b*x)

exp_popt, _ = scipy.optimize.curve_fit(exp_f, df3['t (s)'], df3['R(cps)'],sigma=_df["error σR(cps)"], p0=(300, -decay_constant))
print('a = {:.6}, b = {:.6}'.format(*exp_popt))
print('λ/b = {:.6}'.format(-decay_constant/exp_popt[1]))
```

    a = 194.294, b = -0.000221309
    λ/b = 0.961513

## Κατανομή ροής νετρονίων συναρτήσει του ύψους z

Αρχικά υπολογίζουμε την ενεργότητα της εκάστοτε πηγής στο τέλος της ακτινοβόλησης, λαμβάνοντας υπόψιν τον παράγοντα γεωμετρίας $g=0.4$ και την απόδοση $\varepsilon=1$ του Geiger στις συγκεκριμένες ενέργειες των $\beta^-$.

```python
# ============================================================================================
# DATA (all sources' R at the end of irradiation)
# ============================================================================================

df_all = pd.read_csv('all_sources', names=['z (cm)', "R(cps)"])
df_all['A(Bq)'] = df_all["R(cps)"] / g_times_e
```

Στην παρακάτω γραφική παράσταση παρουσιάζεται η ροή των νετρονίων συναρτήσει του ύψους. Το $z=0$ αντιστοιχεί στο ύψος της πηγής $\ ^{241}AmBe$.

Παρατηρείται ότι η μέγιστη ροή βρίσκεται στο ύψος της πηγής $\ ^{241}AmBe$. Η ροή δεν είναι συμμετρική, λόγω της κατασκευής του αντιδραστήρα. Η επάνω επιφάνεια του αντιδραστήρα είναι ανοιχτή (αέρας), ενώ στην κάτω βρίσκεται ο μεταλλικός του πάτος καθώς και το τσιμέντο. Ως αποτέλεσμα αυτού, από την επάνω επιφάνεια τα νετρόνια διαφεύγουν ελεύθερα του αντιδραστήρα μόλις εξέλθουν του νερού, ενώ στην κάτω επιφάνεια μέρος των νετρονίων οπισθοσκεδάζεται από τα υλικά που προαναφέρθηκαν.

```python
# ============================================================================================
# Neutron flux formula
# ============================================================================================

A_θ, t , λ, σ, t_irr, M, Φ = sympy.symbols('A_th, t, \lambda, \sigma, t_irr, M, \Phi')
_flux_f = lambda A_θ, λ, t_irr, M, σ: A_θ / (σ * M * (1-sympy.E**(-λ * t_irr)))
flux_f = sympy.lambdify((A_θ, λ, t_irr, M, σ), _flux_f(A_θ, λ, t_irr, M, σ))

sympy.Eq(Φ, _flux_f(A_θ, λ, t_irr, M, σ))
```

```python
# ============================================================================================
# FLUX CALCULATION
# ============================================================================================

# flux unit: n per cm^3 per sec
df_all['Φ'] = flux_f(df_all["R(cps)"], decay_constant, irradiation_t, M_constant, σ_In)/ (100**3)
df_all['Φ'] = df_all['Φ'].astype(scipy.float64, copy=False)
print(df_all)

# --------------------------------------------------------------------
plt.plot(df_all['Φ'],df_all['z (cm)'])

plt.xlabel('flux$(n/cm^3/s)$')
plt.ylabel('height (cm)')
plt.title('FIGURE 2: \nThermal neutron flux')
plt.grid('on')

plt.show()
```

## Μήκος χαλάρωσης

```python
# ====================================================================================================
# Least squares
# ====================================================================================================

_f = lambda x, a, b: a+x*b
# Converting flux to n/m^3/s first, and z to meters.
# (No errors were given for Φ)
lin_popt, _ = scipy.optimize.curve_fit(_f, df_all['z (cm)']/100, scipy.log(df_all['Φ']*(100**3)))
print('a = {:.6}, b = {:.6}'.format(*lin_popt))
relaxation_length = -1/lin_popt[1]
print('μήκος χαλάρωσης: {:.5} cm'.format(relaxation_length * 100))
```

    a = 18.2358, b = -8.33343
    μήκος χαλάρωσης: 12.0 cm

Το μήκος χαλάρωσης προκύπτει 12.0cm.

(Λόγω του ότι δεν μας δόθηκαν τα σφάλματα από τις υπόλοιπες πηγές, δεν τα λαμβάνουμε υπόψιν στoυς υπολογισμούς και δεν μπορούμε να υπολογίσουμε το σφάλμα στο μήκος χαλάρωσης.)

## Πιθανές πηγές σφαλμάτων

### Οπισθοσκέδαση σωματιδίων $\beta^-$ από το υλικό κάτω από την πηγή.

Η οπισθοσκέδαση των $\beta^-$ από την βάση τοποθέτησης, τον πάγκο και τα υλικά γύρω από τον ανιχνευτή, συμπεριλαμβανομένου και της θέσης της ερευνήτριας κατά τις μετρήσεις, πιθανόν προκάλεσε υπερεκτίμηση των ρυθμών.

### Επιλογή χρόνου μέτρησης στο μέσο της διάρκειας μέτρησης

Ο εκάστοτε ρυθμός αποδόθηκε στο μέσο της διάρκειας μέτρησης $t_0$ (δηλ. $t_0 = t_1 + \frac{t_2-t_1}{2}$). Ωστόσο, λόγω της εκθετικής πτώσης της ενεργότητας, η υπολογιζόμενη ενεργότητα στην πραγματικότητα αντιστοιχεί σε χρόνο πριν του μέσου.

### Υπερεκτίμηση παράγοντα γεωμετρίας λόγω εγγύτητας πηγής-ανιχνευτή

Σε επαρκώς μεγάλες αποστάσεις ο παράγοντας γεωμετρίας μπορεί να αγνοεί τις διαστάσεις του ανιχνευτή, και να χρησιμοποιείται ως έχει για τον υπολογισμό των σωματιδίων που διέρχονται από μέσα του.

Για πολύ κοντινές αποστάσεις όμως, σε μεγάλες γωνίες πρόσπτωσης (δέσμη 2 στην παρακάτω εικόνα) τα σωματίδια διέρχονται από λιγότερο υλικό του ανιχνευτή, σε αντίθεση με σωματίδια που προσπίπτουν κάθετα στην επιφάνειά του (δέσμη 1).

Ως αποτέλεσμα, κάποια σωματίδια σε μεγάλες γωνίες δεν προκαλούν επαρκή ιονισμό ώστε να καταχωρηθούν από τον ανιχνευτή. Το φαινόμενο αυτό ίσως να μην είναι τόσο σημαντικό για άμεσα ιονίζουσες ακτινοβολίες, όπως τα $\beta^-$ του πειράματος, αλλά ωστόσο όχι αμελητέο.

### Ταυτόχρονη μετακίνηση/μέτρηση άλλων πηγών σε διπλανούς Geiger

Ταυτόχρονα με την μέτρησή μας διεξάγονταν και άλλες μετρήσεις σε απόσταση μικρότερη των 2 μέτρων, πιθανόν αυξάνοντας σημαντικά το υπόβαθρο. Ενδέχεται αυτό το σφάλμα να επηρέασε ιδιαίτερα τις μετρήσεις πηγών με μικρές ενεργότητες (πχ. πηγή 9).

```python
plt.plot(df9['t (s)'], df9["R(cps)"], marker='.', label='Source 9')
plt.errorbar(df9['t (s)'], df9["R(cps)"], df9["error σR(cps)"], label=None)
plt.ylim(ymin=0)
plt.xlabel('t(s)')
plt.ylabel('R(cps)')
plt.legend(loc='upper right')
plt.title('FIGURE 3: \nRate of source 9.')
plt.grid('on')

plt.show()
```

### Μη θερμικά νετρόνια

Λόγω της μη χρήσης $Cd$, ταυτόχρονα και μη θερμικά νετρόνια απορροφόνται από το $\ ^{115}In$. Ως αποτέλεσμα αυξάνει ο μετρούμενος ρυθμός, λόγω του ότι πολλά από αυτά τα νετρόνια αυτά θα διέφευγαν ή θα απορροφόνταν πριν θερμοποιηθούν,  επομένως δεν θα ήταν διαθέσιμα ως θερμικά.

## Αναφορές

Για την συγγραφή της εργασίας χρησιμοποιήθηκαν οι παρακάτω αναφορές:

1. Η ΠΥΡΗΝΙΚΗ ΦΥΣΙΚΗ ΣΤΟ ΕΡΓΑΣΤΗΡΙΟ - ΦΟΙΤΗΤΙΚΕΣ ΑΣΚΗΣΕΙΣ, X. ΕΛΕΥΘΕΡΙΑΔΗΣ, Μ. ΖΑΜΑΝΗ, Α. ΛΙΟΛΙΟΣ, Μ. ΜΑΝΩΛΟΠΟΥΛΟΥ, X. ΠΕΤΡΙΔΟΥ, Η. ΣΑΒΒΙΔΗΣ, Εκδόσεις: COPY CITY

2. https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, C. Cooper, G.F. Forsyth, A. Krishnan.

# Phugoid Motion

Welcome to [**"Practical Numerical Methods with Python!"**](http://openedx.seas.gwu.edu/courses/GW/MAE6286/2014_fall/about) This course is a collaborative, online, open education project, where we aim to give a foundation in scientific computing. The focus is on numerical solution of problems modeled by ordinary and partial differential equations.

This IPython Notebook introduces the problem we'll be studying in the **first module** of the course: the _phugoid model of glider flight_. We'll start with some background, explaining the physics, and working out the mathematical model.

First, we'll look at an idealized motion where there is no drag, resulting in a simple harmonic motion. We can plot some interesting trajectories that will pique your imagination. In the next notebook, you'll learn to numerically integrate the differential equation using Euler's method. But hang on ... first things first.

The term "phugoid" is used in aeronautics to refer to a motion pattern where an aircraft oscillates up and down —nose-up and climb, then nose-down and descend— around an equilibrium trajectory.  The aircraft oscillates in altitude, speed and pitch, with only small (neglected) variations in the angle of attack, as it repeatedly exchanges kinetic and potential energy.

A low-amplitude phugoid motion can be just a nuisance, as the aircraft does not exceed the stall angle of attack and nothing bad happens. But the mode can also be unstable leading to a stall or even a loop!

Look at this video showing a Cessna single-engine airplane in phugoid motion:

```python
from IPython.display import YouTubeVideo
YouTubeVideo('ysdU4mnRYdM')
```

That doesn't look too good! What's happening?

It can get a lot worse when an aircraft enters one of these modes that is unstable. For example, one of [NASA's Helios Solar Powered Aircraft](http://www.nasa.gov/centers/dryden/history/pastprojects/Helios/) prototype broke up in mid air due to extreme phugoid oscillations!

Helios was a proof-of-concept solar electric-powered flying wing that broke the world altitude record for a non-rocket-powered aircraft in August 2001. But in June 26, 2003, it broke something else. The aircraft entered phugoid motion after encountering turbulence near the Hawaiian Island of Kauai. The high speed in the oscillatory movement exceeded the design limits, and it ended up wrecked in the Pacific Ocean. Luckily, the Helios was remotely operated, and nobody got hurt.

## The physics of phugoids

The phugoid oscillation has the aircraft pitching up and down, as it decelerates and accelerates. The trajectory might look like a sinusoid, as in the figure below. The assumption is that the forward velocity of the aircraft, $v$, varies in such a way that the angle of attack remains (nearly) constant, which means that we can assume a constant lift coefficient.

#### Figure 1. Trajectory of an aircraft in phugoid motion.

In the descending portion of the trajectory, the aircraft's velocity increases as it proceeds from a peak to the minimum height—gaining kinetic energy at the expense of potential energy. The contrary happens in the upward segment, as its velocity decreases there.

We measure the pitch angle (between the aircraft's longitudinal axis and the horizontal) as positive when the aircraft's nose is pointing up. In the portion of the trajectory below the center-line, where it curves upwards, the pitch angle $\theta$ is increasing: $\dot{\theta}>0$. And where the trajectory curves down, the pitch angle is decreasing: $\dot{\theta}<0$, as shown in the figure.

Let's remind ourselves of the forces affecting an aircraft in a downward glide. Look at the figure below: we show the flight path, the forces on the glider (no thrust), and the _glide angle_ or flight path angle, $\gamma$, between the flight path and the horizontal.

#### Figure 2. Forces on a glider.

The force of lift, $L$ —created by the airflow around the wings— is perpendicular to the trajectory, and the force of drag, $D$, is parallel to the trajectory. Both forces are expressed in terms of coefficients of lift and drag, $C_L$ and $C_D$, respectively, that depend on the wing design and _angle of attack_—the angle between the wing chord and the flight path.

If you are not familiar with airplane aerodynamics, you might be getting confused with some terms here ... and all those angles! But be patient and look things up, if you need to. We're giving you a quick summary here.

Lift and drag are proportional to a surface area, $S$, and the dynamic pressure: $1/2 \rho v^2$, where $\rho$ is the density of air, and $v$ the forward velocity of the aircraft. The equations for lift and drag are:

$$\begin{eqnarray}
L &=& C_L S \times \frac{1}{2} \rho v^2 \\
D &=& C_D S \times \frac{1}{2} \rho v^2
\end{eqnarray}$$

If the glider were in equilibrium, the forces would balance each other. We can equate the forces in the directions perpendicular and parallel to the trajectory, as follows:

$$\begin{equation}
L = W \cos \gamma \quad \text{and} \quad D = W \sin \gamma
\end{equation}$$

where $W$ repesents the weight of the glider.

In the figure, we've drawn the angle $\gamma$ as the _glide angle_, formed between the direction of motion and the horizontal. We are not bothered with the _sign_ of the angle, because we draw a free-body diagram and take the direction of the forces into account in writing our balance equations. But later on, we will need to be careful with the sign of the angles. It can cause you a real headache to keep this straight, so be patient!

It looks like we've set this up to do a little bit of mathematics. Are you ready?

But before, a short glimpse of the history.

## Lanchester's Aerodonetics

"Phugoid theory" was first described by the British engineer Frederick W. Lanchester in _"Aerodonetics"_ (1909). This book is so old that it is now in the public domain, so you can actually download [from Google Books](http://books.google.com/books?id=6hxDAAAAIAAJ&dq=%22phugoid%20theory%20deals%20with%20the%20longitudinal%20stability%22&pg=PA37#v=onepage&q=%22phugoid%20theory%20deals%20with%20the%20longitudinal%20stability%22&f=false) a PDF file of a scan, or read it online.

Lanchester defines phugoid theory as the study of longitudinal stability of a flying machine (aerodone). He first considered the simplification where drag and moment of inertia are neglected. Then he included these effects, obtaining an equation of stability. In addition to describing many experiments by himself and others, Lanchester also reports on _"numerical work ... done by the aid of an ordinary 25-cm slide rule."_ Go figure!

### Ideal case of zero drag

In this section, we follow the derivation given by Milne-Thompson (1966), which we find a little bit easier than that of the original in "Aerodonetics"!

An aircraft flying in a steady, straight horizontal flight has a lift equal to its weight. The velocity in this condition is sometimes called _trim velocity_ ("trim" is what pilots do to set the controls to just stay in a steady flight). Let's use $v_t$ for the trim velocity, and from $L=W$ deduce that:

$$\begin{equation}
W = C_L S \times\frac{1}{2} \rho v_t^2
\end{equation}$$

The weight $W$ is constant for the aircraft, but the lift at any other flight condition depends on the flight speed, $v$. We can use the expression for the weight in terms of $v_t$ to obtain the ratio $L/W$ at any other flight velocity, as follows:

$$\begin{equation}
\frac{L}{W}= \frac{v^2}{v_t^2}
\end{equation}$$

Imagine that the aircraft experienced a little upset, a wind gust, and it finds itself off the "trim" level, in a curved path with an instantaneous angle $\theta$. In the sketch below, we exaggerate the curved trajectory of flight to help you visualize what we'll do next. The angle $\theta$ (using the same name as Milne-Thompson) is between the _trajectory_ and the horizontal, positive up.

#### Figure 3. Curved trajectory of the aircraft going up.

A balance of forces now has to take into account that our reference frame is moving with the aircraft, in a rotating frame: we have a centrifugal force. The balance in the direction of lift is thus:

$$\begin{equation}
L- W \cos \theta = \frac{W}{g} \frac{v^2}{R}
\end{equation}$$

where $R$ is the radius of curvature of the trajectory, and $g$ the acceleration of gravity. Recall that the centrifugal acceleration is $v^2/R$. Rearrange this by dividing the equation by the weight, and use the expression we found for $L/W$, above. The following equation results:

$$\begin{equation}
\frac{v^2}{v_t^2}-\cos \theta = \frac{v^2}{g R}
\end{equation}$$

Recall that we simplified the problem assuming that there is no friction, which means that the total energy is constant (the lift does no work). If $z$ represents the depth below a reference horizontal line, the energy per unit mass is (kinetic plus potential energy):

$$\begin{equation}
\frac{1}{2}v^2-g z = \text{constant}
\end{equation}$$

To get rid of that pesky constant, we can choose the reference horizontal line at the level that makes the constant energy equal to zero, so $v^2 = 2 g z$. That helps us re-write the phugoid equation in terms of $z$ as follows:

$$\begin{equation}
\frac{z}{z_t}-\cos \theta = \frac{2z}{R}
\end{equation}$$

Let $ds$ represent a small arc-length of the trajectory. We can write

$$\begin{equation}
\frac{1}{R} = \frac{d\theta}{ds} \quad \text{and}\quad  \sin\theta = -\frac{dz}{ds}
\end{equation}$$

Employing the chain rule of calculus,

$$\begin{equation}
\frac{1}{R} = \frac{d\theta}{ds} = \frac{dz}{ds}\frac{d\theta}{dz} = -\sin \theta\frac{d\theta}{dz}
\end{equation}$$

Multiply the phugoid equation by $\frac{1}{2\sqrt{z}}$ to get:

$$\begin{equation}
\frac{\sqrt{z}}{2z_t} - \frac{\cos\theta}{2\sqrt{z}} = \frac{\sqrt{z}}{R}
\end{equation}$$

Substituting for $1/R$ on the right hand side and bringing the cosine term over to the right, we get:

$$\begin{equation}
\frac{\sqrt{z}}{2z_t} = \frac{\cos \theta}{2 \sqrt{z}} - \sqrt{z} \sin \theta \frac{d\theta}{dz}
\end{equation}$$

The right-hand-side is an exact derivative! We can rewrite it as:

$$\begin{equation}
\frac{d}{dz} \left(\sqrt{z}\cos\theta \right) = \frac{\sqrt{z}}{2z_t}
\end{equation}$$

Integrating this equation, we add an arbitrary constant, chosen as $C\sqrt{z_t}$ which (after dividing through by $\sqrt{z}$) gives:

$$\begin{equation}
\cos \theta = \frac{1}{3}\frac{z}{z_t} + C\sqrt{\frac{z_t}{z}}
\end{equation}$$

Taking the derivative of both sides of equation (15) and applying the relations from equation (10) yields:

$$\begin{equation}
\frac{z_t}{R} = \frac{1}{3} - \frac{C}{2}\sqrt{\frac{z_t^3}{z^3}}
\end{equation}$$

Make sure you have followed the derivation, and perhaps write it out on paper!

## Phugoid Curves

Equation (15) is non-linear, which usually means we are hard-pressed to write a clean expression for the variable of interest, $z$.  In fact, Lanchester himself said that he was unable to _"reduce this expression to a form suitable for co-ordinate plotting."_ If the great polymath couldn't do it, we can't either!

But Lanchester _was_ able to plot a suitable approximation of the phugoid flight path using what he called the "trammel" method.  If you're interested in seeing how he did it, his explanation begins on page [48 of Aerodonetics](http://books.google.com/books?id=6hxDAAAAIAAJ&pg=PA49&lpg=PA48&dq=aerodonetics+the+use+of+the+trammel&source=bl&ots=lB6EVKYQuT&sig=aVE2kiDWZoWftaWczMIrcYftMOs&hl=en&sa=X&ei=gTD_U82fGYjzgwT3moGwCQ&ved=0CCAQ6AEwAA#v=onepage&q=aerodonetics%20the%20use%20of%20the%20trammel&f=false).  It's a trip.

Lanchester used Equations (15) and (16) to solve for the constant $C$ and the radius of curvature $R$ and then iteratively plotted small arcs of the phugoid path. By hand.

We wrote a neat little code that duplicates the manual trammel method, but it might be a bit much for you to absorb in the first lesson.  If you want to look it over, you are more than welcome to.  If you are just starting with Python, skip it for the moment and we'll return to it at the end of this module.

### Plotting the flight path

As we mentioned, we wrote a Python code to reproduce programmatically what Lanchester did graphically. Here's a neat feature of IPython Notebooks: you can run external programs with the magical keyword ... wait for it ... `run`. And the jargon of IPython _is_ to call this "magic." In fact, there are a bunch of [magic functions](http://ipython.org/ipython-doc/dev/interactive/tutorial.html) that you will learn about. They will make you a happy camper.

Let's do it:

```python
%run phugoid.py
%matplotlib inline
```

This code cell loaded our simulated-trammel code, `phugoid.py`. The code defined a function for you in the background, called `plot_flight_path`, taking three inputs: $z_t$, $z$ and $\theta$.

Look again at Equation (15), where we take the positive square root. There are several possibilities, depending on the value that the constant $C$ takes.

* There are no physical solutions for $C>2/3$, because it would result in $\cos\theta>1$.

* If $C=2/3$, then the solution is a horizontal straight line, because $\cos\theta=1$, $\theta=0$ and $R=\infty$.

* Any value of $C$ for which $0 < C < \frac{2}{3}$ will produce "trochoidal"-like paths.  What does this look like? Let's use our custom function `plot_flight_path` to find out!

```python
#zt = 64, z = 16, theta=0
plot_flight_path(64, 16, 0)
```

Cool!  Note that the plot title tells us what the calculated value of $C$ was for our input conditions.  We have a value of $C$ between $0$ and $\frac{2}{3}$ and our path is trochoidal, like we announced it would be.

* For negative values of $C$, the resultant flight path consists of a series of loops.  Let's try it out!

```python
plot_flight_path(64,16,numpy.pi)
```

You can play around with the input values and see what kind of behavior results.  Just note that any value of $C > \frac{2}{3}$ will result in $\cos \theta > 1$, which doesn't exist.  Python will probably throw a few errors if you hit that condition, but just try again!

* The last case is $C = 0$.  Take another look at Equation (16) and plug in $C = 0$, what should happen? It looks like it will just reduce to

$$R = 3z_t$$

It's a constant radius of curvature!  In fact, this solution is a series of semi-circles, with a cusp between them. One way to force $C = 0$ that we can figure out from Equation (15), is to make:

$$z = 3z_t\ \ \ ,\ \ \  \theta = 0$$

```python
plot_flight_path(16,48,0.)
```

That looks an awful lot like a quarter circle.  And what's the radius of the arc?  It's $$r = 48 = 3z_t.$$

We can also get a semi-circle out of our simulated trammel by changing to another configuration where $C$ is (near) zero. Here's one example:

```python
plot_flight_path(64,16,-numpy.pi/2)
```

That is so nice. We have reproduced the trajectories that Lanchester found more than a hundred years ago, painstakingly drawing them by hand with a contraption called a "trammel." It must have taken him days!

Here is how the different phugoid curves are drawn in von Kármán's book, _Aerodynamics_ (1957). He never says _how_ he drew them, but we're guessing by hand, also. We did pretty good!

#### Figure 4. Phugoid curves in von Kármán (1957).

In the next notebook of this series, we'll look at the differential equation that arises when you consider small perturbations on the horizontal phugoid, and we'll learn to numerically integrate that to get the flight paths.

## References

1. Lanchester, F. W. _Aerodonetics_, D. van Nostrand Company: New York, 1909. On the public domain. [Get it from Google Books](http://books.google.com/books?id=6hxDAAAAIAAJ&pg=PP1#v=onepage&q&f=false).

2. Milne-Thompson, L. M. _Theoretical Aerodynamics_, Dover 2012 reprint of the revised 1966 edition. [Read on Google Books](http://books.google.com/books?id=EMfCAgAAQBAJ&lpg=PP1&pg=PP1#v=onepage&q&f=false) (see section 18.5)

3. Sinha, N. K. and Ananthkrishnan, N. _Elementary Flight Dynamics with an introduction to Bifurcation and Continuation Methods_, CRC Press, 2013. [Read on Google Books](http://books.google.com/books?id=yXL6AQAAQBAJ&lpg=PP1&pg=PP1#v=onepage&q&f=false) (see chapter 5)

4. von Kármán, T. _Aerodynamics_, Dover 2004 reprint of the 1957 2nd edition. (see pages 149–151)

---

###### The cell below loads the style of this notebook.

```python
# Execute this cell to load the notebook's style sheet, then ignore it
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "PT Mono";
    font-size: 90%;
}

</style>