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 una [versión ya limpia del dataset](https://www.kaggle.com/gsutters/the-human-freedom-index/home) que pueden descargar desde Kaggle.

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

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy
import pandas
import seaborn
```

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

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

```python
dataset = pandas.read_csv('../datasets/hfi_cc_2018.csv')
dataset.shape
```

    (1458, 123)

En el práctico habíamos trabajado sobre las variables `ef_score` y `hf_score`, que hacen referencia a los índices de libertad humana 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 humana y una tendencia ascendiente de la libertad económica.

```python
plt.figure(figsize=(10,6))
seaborn.lineplot(data=dataset,
                 x='year', y='ef_score',
                 label='Economic Freedom', ci='sd')
seaborn.lineplot(data=dataset,
                 x='year', y='hf_score',
                 label='Human Freedom', ci='sd')
plt.legend()
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 humana está decreciendo, pero su libertad económica se mantiene constante.
  * Los países para los cuales sube la libertad económica decrecen en libertad humana.
  * **¿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
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='ef_score')
seaborn.regplot(data=dataset[(dataset.ISO_code == 'ARG')],
                    x='year', y='hf_score')
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", "hf_score", color=RED)
g.set(xlim=(2008, 2016))

#g.map(seaborn.regplot, "hf_score", "ef_score", color=BLUE)
print('Scores de libertad para Medio Oriente');
```

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. Básicamente, queremos escribir

$$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_h$$

$$\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_h) - x_e) \frac{\partial}{\partial a} (e(x_h) - x_e) = \frac{2}{|X|} \sum_{x \in X} (e(x_h) - x_e) e_h $$

$$\frac{\partial}{\partial b}mse = \frac{2}{|X|} \sum_{x \in X} \frac{\partial}{\partial b} e(x_h) - x_e = \frac{2}{|X|} \sum_{x \in X} e(x_h) - 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 hf_score tienen el año.
a, b = estimate_params(
    dataset[(dataset.ISO_code == 'ARG') & (dataset.hf_score.notnull())].year.dropna(),
    dataset[dataset.ISO_code == 'ARG'].hf_score)
a, b
```

    (-0.08980244453559766, 187.35957478506694)

```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."""
    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='hf_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='hf_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 `hf_score`.

```python
def build_regressions(data, x_var='year', y_var='hf_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
hf_regressions = build_regressions(dataset).set_index('ISO_code')
hf_regressions[:10]
```

      region
      country
      slope
      bias
      mse

      ISO_code

      ALB
      Eastern Europe
      Albania
      0.003464
      0.553010
      0.004710

      DZA
      Middle East & North Africa
      Algeria
      -0.005140
      15.481777
      0.001587

      AGO
      Sub-Saharan Africa
      Angola
      0.106828
      -209.439312
      0.046649

      ARG
      Latin America & the Caribbean
      Argentina
      -0.089802
      187.359575
      0.015508

      ARM
      Caucasus & Central Asia
      Armenia
      -0.035400
      78.639050
      0.005959

      AUS
      Oceania
      Australia
      -0.007955
      24.619030
      0.001704

      AUT
      Western Europe
      Austria
      0.019503
      -30.815546
      0.002145

      AZE
      Caucasus & Central Asia
      Azerbaijan
      -0.036932
      80.460165
      0.004310

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.060169
      128.618738
      0.003914

      BHR
      Middle East & North Africa
      Bahrain
      -0.042327
      91.963010
      0.019520

```python
plt.figure(figsize=(10,6))
seaborn.distplot(
    hf_regressions[hf_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
seaborn.distplot(hf_regressions.slope, color=RED, label='Global')
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(hf_regressions.loc[['ARG', 'BRA', 'VEN', 'CAN']])
plt.xlabel('year')
plt.ylabel('hf_score')
plt.ylim(4, 9)
```

### 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.037167
      -67.430444
      0.006890

      DZA
      Middle East & North Africa
      Algeria
      -0.031167
      67.739556
      0.003564

      AGO
      Sub-Saharan Africa
      Angola
      0.061500
      -118.595778
      0.050513

      ARG
      Latin America & the Caribbean
      Argentina
      -0.159667
      326.482667
      0.045977

      ARM
      Caucasus & Central Asia
      Armenia
      0.005000
      -2.425556
      0.005080

      AUS
      Oceania
      Australia
      -0.006667
      21.408889
      0.001528

      AUT
      Western Europe
      Austria
      -0.007333
      22.396889
      0.000770

      AZE
      Caucasus & Central Asia
      Azerbaijan
      0.053833
      -102.077111
      0.007016

      BHS
      Latin America & the Caribbean
      Bahamas
      -0.024833
      57.344667
      0.003578

      BHR
      Middle East & North Africa
      Bahrain
      0.007000
      -6.681778
      0.006713

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

```python
plt.figure(figsize=(10,6))
seaborn.distplot(
    ef_regressions[ef_regressions.region == 'Latin America & the Caribbean'].slope,
    color=BLUE, label='Latam y Caribe')
seaborn.distplot(ef_regressions.slope, color=RED, label='Global')
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 = hf_regressions.slope.quantile(0.1)
hf_regressions[hf_regressions.slope < quantil].country
```

    ISO_code
    ARG            Argentina
    BRA               Brazil
    BRN    Brunei Darussalam
    BDI              Burundi
    EGY                Egypt
    GRC               Greece
    LBR              Liberia
    LBY                Libya
    NER                Niger
    SYR                Syria
    TJK           Tajikistan
    THA             Thailand
    TLS          Timor-Leste
    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
    EGY              Egypt
    FJI               Fiji
    GHA              Ghana
    GRC             Greece
    IRN               Iran
    KWT             Kuwait
    LBR            Liberia
    MWI             Malawi
    NAM            Namibia
    PNG    Pap. New Guinea
    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 humana 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 hf_regressions.index and
        ef_regressions.loc[code].slope > 0.02 and
        hf_regressions.loc[code].slope < -0.02):
        codes.append(code)
ef_regressions.loc[codes].country
```

    ISO_code
    AZE        Azerbaijan
    BDI           Burundi
    COD    Congo, Dem. R.
    GMB       Gambia, The
    MKD         Macedonia
    NIC         Nicaragua
    NER             Niger
    RUS            Russia
    TJK        Tajikistan
    TLS       Timor-Leste
    YEM       Yemen, Rep.
    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
hf_regressions.mse.sort_values()[-10:]
```

    ISO_code
    VEN    0.041111
    BRN    0.042008
    CMR    0.042355
    TGO    0.043840
    COD    0.046000
    AGO    0.046649
    UKR    0.068001
    BDI    0.074905
    SYR    0.087395
    IRN    0.100911
    Name: mse, dtype: float64

```python
plt.figure(figsize=(10,6))
countries = ['SYR', 'VEN', 'IRN', 'UKR']
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


# Putting it all together: Simulating Rotational Spectra

In the previous notebooks you have written a series of pieces of Python code to:
- calculate the rotational energy levels for a rigid rotor,
- apply the specific selection rule to identify allowed and disallowed transitions,
- calculate the change in energy (and associated frequency) for allowed transitions,
- calculate the factors affecting the relative intensities of spectral lines.

In this, final, notebook, you will combine all of these pieces of code into a complete sequence of code that can simulate the rotational spectrum of a rigid rotor.

Assessment

When you have finished the exercise, save this completed notebook, using **File > Save and Checkpoint** in the Jupyter menu. Then upload your notebook for assessment using Moodle. Please make sure that you upload the `Putting it all together.ipynb` file, and that it is not an old version of the notebook (check the modification date and time before you upload).

Because your notebook will be marked by running your code, you should check that everything works as you expect when running from top to bottom. Because notebook cells can be run in any order, it is possible to have code that looks correct, but that gives errors when run by someone else.

Before uploading your notebook, you can test it by selecting **Kernel > Restart & Run All** from the Jupyter menu.

```python
# first, import the various pieces your code relies on.
import numpy as np
from scipy.constants import h, k
from math import sqrt, pi
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
```

Complete each of the functions below, by copying your working code from the respective previous notebooks.

```python
def reduced_mass( m1, m2 ):
    return (m1*m2)/(m1+m2)
```

```python
def moment_of_inertia( mu, r ):
    return mu*(r**2)
```

```python
rotational_prefactor = h**2/(8*(pi**2))

def rotational_constant( moment_of_inertia ):
    return rotational_prefactor/moment_of_inertia
```

```python
def rotational_energy( j, rotational_constant ):
    return rotational_constant*j*(j+1)
```

```python
def transition_energy( j1, j2, rotational_constant ):
    energy_1 = rotational_energy( j1, rotational_constant )
    energy_2 = rotational_energy( j2, rotational_constant )
    return energy_2 - energy_1
```

```python
def transition_probability( j_initial, j_final ):
    if j_final - j_initial == 1:
        return 1.0
    elif j_final - j_initial == -1:
        return 1.0
    else:
        return 0
```

```python
def degeneracy( j ):
    return 2*j + 1
```

```python
def boltzmann( j, rotational_constant, temperature ):
    return (2*j+1)*np.exp*(-(rotational_constant*j*(j+1))/k*temperature)
```

```python
def intensity( j, rotational_constant, temperature ):
    return ((h*rotational_constant)/(k*temperature))*((2*j)+1)*np.exp(-rotational_energy(j, rotational_constant)/(k*temperature))
```

The code below provides the skeleton for combining each of the functions you have written in the previous cells, to give your complete computational model. As a starting point, the molecular parameters for HCl are provided. The main part of the code will look familiar from the Calculating Rotational Transition Energies notebook, but with frequencies stored for each allowed transition, rather than energies. Remember that the frequency for a transition is related to the energy change via $E=h\nu$. In addition to storing the frequency for each allowed transition, you will also need to store the intensities, to model the complete spectrum shape. When plotting your spectrum, convert your frequencies into GHz, and label your axes appropriately.

```python
# atomic masses and interatomic distance for an HCl molecule
m_H = 1.66e-27 # kg
m_Cl = 5.81e-26 # kg
r_HCl = 1.29e-10 # m
```

```python
frequencies_in_GHz = []
intensities = []
max_j = 11
temperature = 298.0 # Kelvin

mu = reduced_mass( m_H, m_Cl )
I = moment_of_inertia( mu, r_HCl )
B = rotational_constant( I )

for j_initial in range(0,max_j):
    for j_final in range(j_initial+1,max_j): # only consider absorption J'' > J'
        if transition_probability( j_initial, j_final ) == 1: # check that the transition is allowed
            frequencies_in_GHz.append( transition_energy( j_initial, j_final, B)/ (h*1e9) )
            intensities.append( intensity( j_final, B, temperature) )

plt.stem( frequencies_in_GHz, intensities, basefmt='-' )
plt.show()
```

## 1. Modelling the effect of temperature

Use your code to simulate the spectrum for the first 20 lines in the rotational absorption spectrum of HCl at
a) 100 K,
b) 298 K, and
c) 1000 K.

For each simulated spectrum, plot the spectral shape using

```python
plt.stem( frequencies_in_GHz, intensities, basefmt='-' )
plt.plot( frequencies_in_GHz, intensities, '--', label=temperature )
```

```python
frequencies_in_GHz = []
intensities = []
max_j = 20
temperature = 100 # Kelvin

mu = reduced_mass( m_H, m_Cl )
I = moment_of_inertia( mu, r_HCl )
B = rotational_constant( I )

for j_initial in range(0,max_j):
    for j_final in range(j_initial+1,max_j): # only consider absorption J'' > J'
        if transition_probability( j_initial, j_final ) == 1: # check that the transition is allowed
            frequencies_in_GHz.append( transition_energy( j_initial, j_final, B)/ (h*1e9) )
            intensities.append( intensity( j_final, B, temperature) )

plt.stem( frequencies_in_GHz, intensities, basefmt='-' )
plt.plot( frequencies_in_GHz, intensities, '--', label=temperature )
plt.show()
```

```python
frequencies_in_GHz = []
intensities = []
max_j = 20
temperature = 298# Kelvin

mu = reduced_mass( m_H, m_Cl )
I = moment_of_inertia( mu, r_HCl )
B = rotational_constant( I )

for j_initial in range(0,max_j):
    for j_final in range(j_initial+1,max_j): # only consider absorption J'' > J'
        if transition_probability( j_initial, j_final ) == 1: # check that the transition is allowed
            frequencies_in_GHz.append( transition_energy( j_initial, j_final, B)/ (h*1e9) )
            intensities.append( intensity( j_final, B, temperature) )

plt.stem( frequencies_in_GHz, intensities, basefmt='-' )
plt.plot( frequencies_in_GHz, intensities, '--', label=temperature )
plt.show()
```

```python
frequencies_in_GHz = []
intensities = []
max_j = 20
temperature = 1000# Kelvin

mu = reduced_mass( m_H, m_Cl )
I = moment_of_inertia( mu, r_HCl )
B = rotational_constant( I )

for j_initial in range(0,max_j):
    for j_final in range(j_initial+1,max_j): # only consider absorption J'' > J'
        if transition_probability( j_initial, j_final ) == 1: # check that the transition is allowed
            frequencies_in_GHz.append( transition_energy( j_initial, j_final, B)/ (h*1e9) )
            intensities.append( intensity( j_final, B, temperature) )

plt.stem( frequencies_in_GHz, intensities, basefmt='-' )
plt.plot( frequencies_in_GHz, intensities, '--', label=temperature )
plt.show()
```

## 2. Rotational Raman Spectroscopy

Throughout this exercise you have considered rotational absorption spectroscopy, where a photon with energy $\Delta E$ promotes an electron from state $J^\prime$ to $J^{\prime\prime}$. For a rigid rotot, the specific selection rule is $\Delta J=\pm1$, and absorption spectra correspond to transitions of electrons being excited one rotational energy level. For a molecule to be active under rotational absorption (or emission) spectroscopy, it must have a dipole. Non-dipolar molecules, such as H2 (homodiatomic), CO2 (symmetic linear) are therefore observable by  rotational absorption spectroscopy. The origin of the specific selection rule, $\Delta J=\pm1$ is conservation of angular momentum. A photon has a angular momentum (spin) of $\pm$1. If a molecular absorbs (or emits) one photon the angular momentum of the molecule must change by 1, to account for the lost (or gained) spin.

Raman spectroscopy does not involve absorption of a photon. Instead, the incident photon is **scattered** by the molecule, and consequently Raman has different gross and specific selection rules. For rotational Raman spectroscopy, the gross selection rule is that there has to be anisotropy (directional dependance) in the polarizability of a molecule. This means that for homodiatomic molecules and symmetric linear molecules, like H2 and CO2 a rotational Raman spectrum is observed.

Because Raman is a scattering technique, Raman scattered photons are observed regardless of the wavelength of incident light, we can consider that the molecule is excited to a “virtual level” before relaxing to a “real” rotational energy level. Because we can consider this transition as being composed of “two photons”; one incident to the molecule and the other excident, this gives a specific selection rule in the case of rotational Raman of $\Delta J = 0, \pm2$.

In cases where the selection rule $\Delta J = 0$ is obeyed, the energy of the incident and excident photon is unchanged ($\Delta E = 0$), this is referred to as elastic or Rayleigh scattering.

If we look at how each of these transitions would appear on a spectrum then we observe the following.

Lines that occur when $\Delta J = +2$ are referred to as **Stokes** lines, and those that occur when $\Delta J = -2$ are referred to as **anti-Stokes** lines. Due to thermal effects the Stokes lines are usually of higher intensity than anti-Stokes lines.

Please note that this is a sketch.

In the case of rotational energy levels, the energy of each level is given by:

\begin{equation}
E = BJ(J+1)
\end{equation}

Therefore for the transitions in rotational Raman where $\Delta J = \pm2$ the energy of those transitions, $\Delta E$ is given by:

\begin{equation}
\Delta E = E_{J+2} - E_J
\end{equation}

Substituting in the energies of these levels as given above:
\begin{equation}
\Delta E = B(J+2)(J+3)-BJ(J+1)
\end{equation}

Rearranging gives:
\begin{equation}
\Delta E = B ((J^2 + 5J + 6) - (J^2+J))
\end{equation}

\begin{equation}
\Delta E = 2B (2J + 3).
\end{equation}

[Raman Reference](http://pubs.acs.org/doi/pdf/10.1021/ed052p568)

By adapting your code from above, model the Raman spectrum for C12O16 at 298 K, with your $x$ axis showing frequencies relative to the Rayleigh line. Remember to include both the Stokes branch ($\Delta J=+2$) and the anti-Stokes branch ($\Delta J=-1$). You are given the relevant atomic masses and bond length for CO.

```python
def transition_probability_raman( j_initial, j_final ):
    if j_final - j_initial == +2.0:
        return (2.0)
    elif j_final - j_initial == -2.0:
        return (-2.0)
    else:
        return( 0 )

```

```python
from scipy.constants import Avogadro
r_CO = 1.13e-10 # CO bond length in m
m_C = 12 / Avogadro * 1e-3 # mass of C^12 in kg
m_O = 16 / Avogadro * 1e-3 # mass of O^12 in kg

frequencies_in_GHz = []
intensities = []
max_j = 21
temperature = 298 # Kelvin

mu = reduced_mass(m_C, m_O)
I = moment_of_inertia(mu, r_CO)
B = rotational_constant( I )

for j_initial in range(0,max_j):
    for j_final in range(0, max_j):
        if transition_probability( j_initial, j_final ) == 1:
            frequencies_in_GHz.append( transition_energy( j_initial, j_final, B)/ (h*1e9) )
            intensities.append( intensity( j_final, B, temperature) )

plt.stem( frequencies_in_GHz, intensities, basefmt='-' )

plt.show()

```


```python
from __future__ import print_function
import sympy
import sympy.physics.mechanics as mech
sympy.init_printing()
mech.init_vprinting()
```

```python
t = sympy.symbols('t')
rot_N, rot_E, rot_D, vel_N, vel_E, vel_D, \
    gyro_bias_N, gyro_bias_E, gyro_bias_D, \
    accel_bias_N, accel_bias_E, accel_bias_D, \
    pos_N, pos_E, asl, terrain_asl, baro_bias, \
    wind_N, wind_E, wind_D, d, agl, phi, theta, psi  = mech.dynamicsymbols(
    'rot_N, rot_E, rot_D, vel_N, vel_E, vel_D, ' \
    'gyro_bias_N, gyro_bias_E, gyro_bias_D, ' \
    'accel_bias_N, accel_bias_E, accel_bias_D, ' \
    'pos_N, pos_E, asl, terrain_asl, baro_bias, ' \
    'wind_N, wind_E, wind_D, d, agl, phi, theta, psi')
frame_i = mech.ReferenceFrame('i')
frame_n = frame_i.orientnew('n', 'Quaternion', (1, rot_N, rot_E, rot_D))
#frame_b = frame_n.orientnew('b', 'Quaternion', (q_0, q_1, q_2, q_3))
# easier to see where we get divide by zeros if we express dcm in euler angles
frame_b = frame_n.orientnew('b', 'Body', (psi, theta, phi), '321')
C_nb = frame_n.dcm(frame_b)
assert C_nb[0, 1] == frame_n.x.dot(frame_b.y)
sub_C_nb = {}
for i in range(3):
    for j in range(3):
        sub_C_nb[C_nb[i, j]] = sympy.Symbol('C_nb({:d}, {:d})'.format(i, j))(t)
        sub_C_nb[-C_nb[i, j]] = -sympy.Symbol('C_nb({:d}, {:d})'.format(i, j))(t)
sub_C_nb_rev = { sub_C_nb[key]: key for key in sub_C_nb.keys() }
sub_lin = {
    rot_N: 0,
    rot_E: 0,
    rot_D: 0,
    gyro_bias_N: 0,
    gyro_bias_E: 0,
    gyro_bias_D: 0
}
sub_agl = {
    asl - terrain_asl: agl
}
omega_bx, omega_by, omega_bz = mech.dynamicsymbols('omega_bx, omega_by, omega_bz')
flowX, flowY = mech.dynamicsymbols('flowX, flowY')
omega_ib_b = omega_bx * frame_b.x \
    + omega_by * frame_b.y \
    + omega_bz * frame_b.z
gyro_bias_i = gyro_bias_N * frame_i.x \
    + gyro_bias_E * frame_i.y \
    + gyro_bias_D * frame_i.z
omega_nx, omega_ny, omega_nz = mech.dynamicsymbols('omega_nx, omega_ny, omega_nz')
omega_in_n = -gyro_bias_N * frame_n.x \
    - gyro_bias_E * frame_n.y \
    - gyro_bias_D * frame_n.z
a_N, a_E, a_D = mech.dynamicsymbols('a_N, a_E, a_D')
a_n = a_N*frame_n.x + a_E*frame_n.y + a_D*frame_n.z
a_bias_n = accel_bias_N*frame_n.x + accel_bias_E*frame_n.y + accel_bias_D*frame_n.z
a_n_correct = a_n - a_bias_n
v_i = vel_N*frame_i.x + vel_E*frame_i.y + vel_D*frame_i.z
p_i = pos_N*frame_i.x + pos_E*frame_i.y - asl*frame_i.z
I_wx, I_wy, I_wz = mech.dynamicsymbols('I_wx, I_wy, I_wz')
I_w_n = I_wx*frame_n.x + I_wy*frame_n.y  + I_wz*frame_n.z
```

```python
xe = sympy.Matrix([rot_N, rot_E, rot_D, vel_N, vel_E, vel_D, gyro_bias_N, gyro_bias_E, gyro_bias_D,
                   accel_bias_N, accel_bias_E, accel_bias_D,
                   pos_N, pos_E, asl, terrain_asl, baro_bias, wind_N, wind_E, wind_D])
xe.T
```

```python
def print_terms(terms):
    for t in terms:
        s = 'float {:s} = {:s};'.format(
                str(t[0]), str(t[1]))
        print(s.replace('(t)', ''))

def matrix_to_code(name, mat, i_name, i_syms, j_name, j_syms):
    print('Matrix<float, {:s}n, {:s}n> {:s};'.format(i_name, j_name, name))
    mat.simplify()
    terms, mat = sympy.cse(mat)
    print_terms(terms)
    mat = mat[0]
    for i in range(mat.shape[0]):
        for j in range(mat.shape[1]):
            if str(mat[i, j]) == "0":
                continue
            s = '{:s}({:s}{:s}, {:s}{:s}) = {:s};'.format(
                str(name), i_name, str(i_syms[i]),
                j_name, str(j_syms[j]), str(mat[i, j]))
            print(s.replace('(t)', ''))
```

## Dynamics

This is just to check the other derivaiton in IEKF Derivation notebook, doesn't match yet, needes further work.

```python
trans_kin_eqs = list((a_n_correct.express(frame_i) - v_i.diff(t, frame_i)).to_matrix(frame_i))
trans_kin_eqs
```

```python
nav_eqs = list((p_i.diff(t, frame_i) - v_i).to_matrix(frame_i))
nav_eqs
```

```python
sub_q = {
    (1 + rot_N**2 + rot_E**2 + rot_D**2): 1,
    2*(1 + rot_N**2 + rot_E**2 + rot_D**2): 2
}
```

```python
rot_kin_eqs = list((frame_n.ang_vel_in(frame_i) - omega_in_n).to_matrix(frame_n))
rot_kin_eqs
```

```python
static_eqs = [
    terrain_asl.diff(t),
    baro_bias.diff(t),
    wind_N.diff(t),
    wind_E.diff(t),
    wind_D.diff(t),
    accel_bias_N.diff(t),
    accel_bias_E.diff(t),
    accel_bias_D.diff(t),
]
static_eqs
```

```python
static_eqs
```

```python
gyro_eqs = list((omega_in_n.diff(t, frame_n) - frame_i.ang_vel_in(frame_n).cross(I_w_n)).to_matrix(frame_n))
gyro_eqs
```

```python
sol = sympy.solve(rot_kin_eqs + trans_kin_eqs + static_eqs + nav_eqs + gyro_eqs, xe.diff(t))
sol = { key:sol[key].subs(sub_q) for key in sol.keys() }
xe_dot = sympy.Matrix([ sol[var] for var in xe.diff(t) ]).applyfunc(lambda x: x.subs(sub_q))
#xe_dot
```

```python
A = xe_dot.jacobian(xe).subs(sub_lin)
#A
```

```python
matrix_to_code('A', A, 'Xe::', xe, 'Xe::', xe)
```

    Matrix<float, Xe::n, Xe::n> A;
    float x0 = 2*a_D;
    float x1 = 2*accel_bias_D;
    float x2 = 2*a_E;
    float x3 = 2*accel_bias_E;
    float x4 = 2*a_N;
    float x5 = 2*accel_bias_N;
    float x6 = I_wz;
    float x7 = I_wy;
    float x8 = I_wx;
    A(Xe::rot_N, Xe::gyro_bias_N) = -1/2;
    A(Xe::rot_E, Xe::gyro_bias_E) = -1/2;
    A(Xe::rot_D, Xe::gyro_bias_D) = -1/2;
    A(Xe::vel_N, Xe::rot_E) = x0 - x1;
    A(Xe::vel_N, Xe::rot_D) = -x2 + x3;
    A(Xe::vel_N, Xe::accel_bias_N) = -1;
    A(Xe::vel_E, Xe::rot_N) = -x0 + x1;
    A(Xe::vel_E, Xe::rot_D) = x4 - x5;
    A(Xe::vel_E, Xe::accel_bias_E) = -1;
    A(Xe::vel_D, Xe::rot_N) = x2 - x3;
    A(Xe::vel_D, Xe::rot_E) = -x4 + x5;
    A(Xe::vel_D, Xe::accel_bias_D) = -1;
    A(Xe::gyro_bias_N, Xe::gyro_bias_E) = -x6;
    A(Xe::gyro_bias_N, Xe::gyro_bias_D) = x7;
    A(Xe::gyro_bias_E, Xe::gyro_bias_N) = x6;
    A(Xe::gyro_bias_E, Xe::gyro_bias_D) = -x8;
    A(Xe::gyro_bias_D, Xe::gyro_bias_N) = -x7;
    A(Xe::gyro_bias_D, Xe::gyro_bias_E) = x8;
    A(Xe::pos_N, Xe::vel_N) = 1;
    A(Xe::pos_E, Xe::vel_E) = 1;
    A(Xe::asl, Xe::vel_D) = -1;

## Airspeed

```python
wind_i = wind_N*frame_i.x + wind_E*frame_i.y + wind_D*frame_i.z
vel_i = vel_N*frame_i.x + vel_E*frame_i.y + vel_D*frame_i.z
```

```python
rel_wind = wind_i - vel_i
y_airspeed = sympy.Matrix([rel_wind.dot(-frame_b.x)]).subs(sub_C_nb)
y_airspeed
```

```python
H_airspeed = y_airspeed.jacobian(xe).subs(sub_lin)
H_airspeed.T
```

```python
matrix_to_code('H', H_airspeed,
               'Y_airspeed::', [sympy.Symbol('airspeed')],
               'Xe::', xe)
```

    Matrix<float, Y_airspeed::n, Xe::n> H;
    float x0 = C_nb(1, 0);
    float x1 = 2*vel_D - 2*wind_D;
    float x2 = C_nb(2, 0);
    float x3 = 2*vel_E - 2*wind_E;
    float x4 = C_nb(0, 0);
    float x5 = 2*vel_N - 2*wind_N;
    H(Y_airspeed::airspeed, Xe::rot_N) = x0*x1 - x2*x3;
    H(Y_airspeed::airspeed, Xe::rot_E) = -x1*x4 + x2*x5;
    H(Y_airspeed::airspeed, Xe::rot_D) = -x0*x5 + x3*x4;
    H(Y_airspeed::airspeed, Xe::vel_N) = x4;
    H(Y_airspeed::airspeed, Xe::vel_E) = x0;
    H(Y_airspeed::airspeed, Xe::vel_D) = x2;
    H(Y_airspeed::airspeed, Xe::wind_N) = -x4;
    H(Y_airspeed::airspeed, Xe::wind_E) = -x0;
    H(Y_airspeed::airspeed, Xe::wind_D) = -x2;

## Distance

```python
d_eq = sympy.solve((d*frame_b.z).dot(frame_i.z).subs(sub_C_nb) - (asl - terrain_asl), d)[0]
d_eq.subs(sub_agl)
```

```python
y_dist = sympy.Matrix([d_eq]).subs(sub_C_nb)
y_dist[0].subs(sub_lin).subs(sub_agl)
```

```python
H_distance = y_dist.jacobian(xe).subs(sub_lin).subs(sub_agl)
H_distance.T
matrix_to_code('H', H_distance, 'Y_distance_down::',
               [sympy.symbols('d')], 'Xe::', xe)
```

    Matrix<float, Y_distance_down::n, Xe::n> H;
    float x0 = C_nb(2, 2);
    float x1 = 2*agl/x0**2;
    float x2 = 1/x0;
    H(Y_distance_down::d, Xe::rot_N) = -x1*C_nb(1, 2);
    H(Y_distance_down::d, Xe::rot_E) = x1*C_nb(0, 2);
    H(Y_distance_down::d, Xe::asl) = x2;
    H(Y_distance_down::d, Xe::terrain_asl) = -x2;

## Optical Flow

```python
#omega_nx, omega_ny, omega_nz = sympy.symbols('\omega_{nx}, \omega_{ny}, \omega_{nz}')
#omega_ib_n = omega_nx*frame_i.x + omega_ny*frame_i.y + omega_nz*frame_i.z
#omega_ib_n
```

```python
y_flow_sym = [flowX, flowY]
omega_n = (omega_ib_b - gyro_bias_i)
vel_f_b = -vel_i - omega_n.cross(d_eq*frame_b.z)
vel_f_b.subs(sub_lin).subs(sub_agl)
```

```python
y_flow = sympy.Matrix([
    -vel_f_b.dot(frame_b.x).subs(sub_C_nb),
    -vel_f_b.dot(frame_b.y).subs(sub_C_nb)
    ]).subs(sub_C_nb)
```

```python
def sym2latex(s):
    return sympy.latex(s).replace(r'{\left (t \right )}', '')
```

```python
y_flow_lin = y_flow.subs(sub_lin).subs(sub_agl).subs(sub_C_nb)
y_flow_lin.simplify()
matrix_to_code('y_flow_lin', y_flow_lin, 'Y_flow::', y_flow_sym, '', [0])
```

    Matrix<float, Y_flow::n, n> y_flow_lin;
    float x0 = vel_N;
    float x1 = vel_E;
    float x2 = vel_D;
    float x3 = agl/C_nb(2, 2);
    y_flow_lin(Y_flow::flowX, 0) = x0*C_nb(0, 0) + x1*C_nb(1, 0) + x2*C_nb(2, 0) + x3*omega_by;
    y_flow_lin(Y_flow::flowY, 0) = x0*C_nb(0, 1) + x1*C_nb(1, 1) + x2*C_nb(2, 1) - x3*omega_bx;

```python
H_flow = y_flow.jacobian(xe).subs(sub_lin).subs(sub_agl)
H_flow

for i in range(H_flow.shape[0]):
    for j in range(H_flow.shape[1]):
        if H_flow[i, j] != 0:
            s_mat = sym2latex(H_flow[i, j])
            print('H[{:s}, {:s}] =& {:s} \\\\'.format(
                sym2latex(y_flow_sym[i]),
                sym2latex(xe[j]),
                sym2latex(H_flow[i, j])))
```

    H[\operatorname{flowX}, \operatorname{rot_{N}}] =& 2 \operatorname{C_{nb(1, 0)}} \operatorname{vel_{D}} - \frac{2 \operatorname{C_{nb(1, 2)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}^{2}} \omega_{by} - 2 \operatorname{C_{nb(2, 0)}} \operatorname{vel_{E}} \\
    H[\operatorname{flowX}, \operatorname{rot_{E}}] =& - 2 \operatorname{C_{nb(0, 0)}} \operatorname{vel_{D}} + \frac{2 \operatorname{C_{nb(0, 2)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}^{2}} \omega_{by} + 2 \operatorname{C_{nb(2, 0)}} \operatorname{vel_{N}} \\
    H[\operatorname{flowX}, \operatorname{rot_{D}}] =& 2 \operatorname{C_{nb(0, 0)}} \operatorname{vel_{E}} - 2 \operatorname{C_{nb(1, 0)}} \operatorname{vel_{N}} \\
    H[\operatorname{flowX}, \operatorname{vel_{N}}] =& \operatorname{C_{nb(0, 0)}} \\
    H[\operatorname{flowX}, \operatorname{vel_{E}}] =& \operatorname{C_{nb(1, 0)}} \\
    H[\operatorname{flowX}, \operatorname{vel_{D}}] =& \operatorname{C_{nb(2, 0)}} \\
    H[\operatorname{flowX}, \operatorname{gyro_{bias N}}] =& - \frac{\operatorname{C_{nb(0, 1)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowX}, \operatorname{gyro_{bias E}}] =& - \frac{\operatorname{C_{nb(1, 1)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowX}, \operatorname{gyro_{bias D}}] =& - \frac{\operatorname{C_{nb(2, 1)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowX}, \operatorname{asl}] =& \frac{\omega_{by}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowX}, \operatorname{terrain_{asl}}] =& - \frac{\omega_{by}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowY}, \operatorname{rot_{N}}] =& 2 \operatorname{C_{nb(1, 1)}} \operatorname{vel_{D}} + \frac{2 \operatorname{C_{nb(1, 2)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}^{2}} \omega_{bx} - 2 \operatorname{C_{nb(2, 1)}} \operatorname{vel_{E}} \\
    H[\operatorname{flowY}, \operatorname{rot_{E}}] =& - 2 \operatorname{C_{nb(0, 1)}} \operatorname{vel_{D}} - \frac{2 \operatorname{C_{nb(0, 2)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}^{2}} \omega_{bx} + 2 \operatorname{C_{nb(2, 1)}} \operatorname{vel_{N}} \\
    H[\operatorname{flowY}, \operatorname{rot_{D}}] =& 2 \operatorname{C_{nb(0, 1)}} \operatorname{vel_{E}} - 2 \operatorname{C_{nb(1, 1)}} \operatorname{vel_{N}} \\
    H[\operatorname{flowY}, \operatorname{vel_{N}}] =& \operatorname{C_{nb(0, 1)}} \\
    H[\operatorname{flowY}, \operatorname{vel_{E}}] =& \operatorname{C_{nb(1, 1)}} \\
    H[\operatorname{flowY}, \operatorname{vel_{D}}] =& \operatorname{C_{nb(2, 1)}} \\
    H[\operatorname{flowY}, \operatorname{gyro_{bias N}}] =& \frac{\operatorname{C_{nb(0, 0)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowY}, \operatorname{gyro_{bias E}}] =& \frac{\operatorname{C_{nb(1, 0)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowY}, \operatorname{gyro_{bias D}}] =& \frac{\operatorname{C_{nb(2, 0)}} \operatorname{agl}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowY}, \operatorname{asl}] =& - \frac{\omega_{bx}}{\operatorname{C_{nb(2, 2)}}} \\
    H[\operatorname{flowY}, \operatorname{terrain_{asl}}] =& \frac{\omega_{bx}}{\operatorname{C_{nb(2, 2)}}} \\

```python
H_flow.shape[0]
```

```python
H_flow.subs(sub_C_nb_rev).subs({phi: 0, theta:0, psi:0})
```

```python
P = sympy.diag(*[sympy.Symbol('var_' + str(xi)) for xi in xe])
R = sympy.diag(*[sympy.Symbol('var_flowY'), sympy.Symbol('var_flowX')])
#P = sympy.MatrixSymbol('P', len(xe), len(xe))
#R = sympy.MatrixSymbol('R', 2, 2)
S = H_flow * P * H_flow.T + R
S.simplify()
```

```python
S[0, 0].subs(sub_agl)
```

```python
S[1, 1].subs(sub_agl)
```

```python
S[0, 0].subs(sub_agl).subs(sub_C_nb_rev).subs({phi: 0, theta: 0})
```

```python
S[1, 1].subs(sub_agl).subs(sub_C_nb_rev).subs({phi: 0, theta: 0, psi:0})
```

```python
S.subs(sub_agl).subs(sub_C_nb_rev).subs({phi: 0, theta: 0, psi:0, omega_bx:0, omega_by: 0})
```

```python
H_flow.subs(sub_agl).subs(sub_C_nb_rev).subs({phi: 0, theta: 0, psi:0, omega_bx:0, omega_by: 0})
```

```python
matrix_to_code('S', sympy.diag(S[0,0]), 'Y_flow::', y_flow_sym, 'Y_flow::', y_flow_sym,)
```

    Matrix<float, Y_flow::n, Y_flow::n> S;
    float x0 = C_nb(2, 2);
    float x1 = x0**4;
    float x2 = agl;
    float x3 = omega_by;
    float x4 = x2*x3;
    float x5 = x0**2;
    float x6 = C_nb(0, 0);
    float x7 = vel_D;
    float x8 = C_nb(2, 0);
    float x9 = vel_N;
    float x10 = C_nb(1, 0);
    float x11 = vel_E;
    float x12 = x3**2;
    float x13 = x2**2;
    S(Y_flow::flowX, Y_flow::flowX) = (4*var_rot_E*(-x4*C_nb(0, 2) + x5*(x6*x7 - x8*x9))**2 + 4*var_rot_N*(-x4*C_nb(1, 2) + x5*(x10*x7 - x11*x8))**2 + x1*(var_flowY + 4*var_rot_D*(-x10*x9 + x11*x6)**2 + var_vel_D*x8**2 + var_vel_E*x10**2 + var_vel_N*x6**2) + x5*(var_asl*x12 + var_gyro_bias_D*x13*C_nb(2, 1)**2 + var_gyro_bias_E*x13*C_nb(1, 1)**2 + var_gyro_bias_N*x13*C_nb(0, 1)**2 + var_terrain_asl*x12))/x1;

```python
matrix_to_code('H', H_flow, 'Y_flow::', y_flow_sym, 'Xe::', xe)
```

    Matrix<float, Y_flow::n, Xe::n> H;
    float x0 = vel_D;
    float x1 = C_nb(1, 0);
    float x2 = 2*x1;
    float x3 = vel_E;
    float x4 = C_nb(2, 0);
    float x5 = 2*x4;
    float x6 = omega_by;
    float x7 = agl;
    float x8 = C_nb(2, 2);
    float x9 = x8**(-2);
    float x10 = 2*x7*x9*C_nb(1, 2);
    float x11 = C_nb(0, 0);
    float x12 = 2*x11;
    float x13 = vel_N;
    float x14 = 2*x7*x9*C_nb(0, 2);
    float x15 = C_nb(0, 1);
    float x16 = 1/x8;
    float x17 = x16*x7;
    float x18 = C_nb(1, 1);
    float x19 = C_nb(2, 1);
    float x20 = x16*x6;
    float x21 = 2*x18;
    float x22 = 2*x19;
    float x23 = omega_bx;
    float x24 = 2*x15;
    float x25 = x16*x23;
    H(Y_flow::flowX, Xe::rot_N) = x0*x2 - x10*x6 - x3*x5;
    H(Y_flow::flowX, Xe::rot_E) = -x0*x12 + x13*x5 + x14*x6;
    H(Y_flow::flowX, Xe::rot_D) = x12*x3 - x13*x2;
    H(Y_flow::flowX, Xe::vel_N) = x11;
    H(Y_flow::flowX, Xe::vel_E) = x1;
    H(Y_flow::flowX, Xe::vel_D) = x4;
    H(Y_flow::flowX, Xe::gyro_bias_N) = -x15*x17;
    H(Y_flow::flowX, Xe::gyro_bias_E) = -x17*x18;
    H(Y_flow::flowX, Xe::gyro_bias_D) = -x17*x19;
    H(Y_flow::flowX, Xe::asl) = x20;
    H(Y_flow::flowX, Xe::terrain_asl) = -x20;
    H(Y_flow::flowY, Xe::rot_N) = x0*x21 + x10*x23 - x22*x3;
    H(Y_flow::flowY, Xe::rot_E) = -x0*x24 + x13*x22 - x14*x23;
    H(Y_flow::flowY, Xe::rot_D) = -x13*x21 + x24*x3;
    H(Y_flow::flowY, Xe::vel_N) = x15;
    H(Y_flow::flowY, Xe::vel_E) = x18;
    H(Y_flow::flowY, Xe::vel_D) = x19;
    H(Y_flow::flowY, Xe::gyro_bias_N) = x11*x17;
    H(Y_flow::flowY, Xe::gyro_bias_E) = x1*x17;
    H(Y_flow::flowY, Xe::gyro_bias_D) = x17*x4;
    H(Y_flow::flowY, Xe::asl) = -x25;
    H(Y_flow::flowY, Xe::terrain_asl) = x25;

## Attitude

```python
y_attitude = sympy.Matrix([
        rot_N, rot_E, rot_D
    ])
H_attitude = y_attitude.jacobian(xe).subs(sub_lin).subs(sub_agl)
H_attitude
```

## Accelerometer

```python
g = sympy.symbols('g')
g_i = -g*frame_i.z + accel_bias_N*frame_i.x + accel_bias_E*frame_i.y + accel_bias_D*frame_i.z
y_accel = sympy.Matrix(g_i.express(frame_b).subs(sub_C_nb).to_matrix(frame_b))
H_accel = y_accel.jacobian(xe).subs(sub_lin)
H_accel
```

```python
H_accel.subs(sub_C_nb_rev).subs({phi: 0, theta: 0, psi: 0})
```

## Magnetometer

```python
B_N, B_E, B_D = sympy.symbols('B_N, B_E, B_D')
b_i = B_N*frame_i.x + B_E*frame_i.y + B_D*frame_i.z
y_mag = sympy.Matrix(b_i.express(frame_b).subs(sub_C_nb).to_matrix(frame_b))
H_mag = y_mag.jacobian(xe).subs(sub_lin).subs(sub_agl)
H_mag.simplify()
H_mag
```

## Observability Analysis

```python
def find_observable_states(H, x, n_max=3):
    O = sympy.Matrix(H)
    for n in range(n_max):
        O = O.col_join(H*A**n)
    return [x[i] for i in O.rref()[1]]
```

```python
find_observable_states(H_mag, xe)
```

```python
find_observable_states(H_accel, xe)
```

```python
find_observable_states(H_mag.col_join(H_accel), xe)
```


```python
# Author-Vishal Burman
```

## Accessing and Reading Data Sets

```python
%matplotlib inline
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import data as gdata, loss as gloss, nn
import numpy as np
import pandas as pd
```

```python
train_data=pd.read_csv("train.csv")
test_data=pd.read_csv("test.csv")
```

```python
print(train_data.shape)
print(test_data.shape)
```

    (1460, 81)
    (1459, 80)

```python
# The first 4 and the last 2 features as well as label(Saleprice) from first 4 examples:
```

```python
train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]
```

      Id
      MSSubClass
      MSZoning
      LotFrontage
      SaleType
      SaleCondition
      SalePrice

      0
      1
      60
      RL
      65.0
      WD
      Normal
      208500

      1
      2
      20
      RL
      80.0
      WD
      Normal
      181500

      2
      3
      60
      RL
      68.0
      WD
      Normal
      223500

      3
      4
      70
      RL
      60.0
      WD
      Abnorml
      140000

```python
# Removing the id column from the dataset
```

```python
all_features=pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
```

## Data Preprocessing

```python
# We begin be replacing the missing values with mean
# Then we adjust the values to a common scale(with zero mean and unit variance)
```

\begin{equation}
x \leftarrow \frac{x - \mu}{\sigma}
\end{equation}

```python
numeric_features=all_features.dtypes[all_features.dtypes!='object'].index
all_features[numeric_features]=all_features[numeric_features].apply(lambda x: (x-x.mean())/(x.std()))
# After standardising the data all means vanish, hence we can set the missing values to 0
all_features[numeric_features]=all_features[numeric_features].fillna(0)
```

```python
# Next we deal with discrete values
# We replace them with one-hot encoding
```

```python
# dummy_na=True refers to a missing value being a legal eigen-value
# Creates an indicative feature for it
all_features=pd.get_dummies(all_features, dummy_na=True)
all_features.shape
```

    (2919, 331)

```python
# Via the values attribute we can extract the NumPy format from the Pandas dataframe
# We can then convert it to MxNet's native NDArray representation for training
```

```python
n_train=train_data.shape[0]
train_features=nd.array(all_features[:n_train].values)
test_features=nd.array(all_features[:n_train].values)
train_labels=nd.array(train_data.SalePrice.values).reshape((-1, 1))
```

## Training

```python
# We define a simple squared loss model
# It wont be the perfect criteria but provides for a baseline model
```

```python
loss=gloss.L2Loss()

def get_net():
    net=nn.Sequential()
    net.add(nn.Dense(1))
    net.initialize()
    return net
```

```python

```


```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
from scipy.integrate import quad
from scipy.special import eval_laguerre
import itertools
import copy
from image_matrix_helper import compute_master_list, imshow_list, rgb_map, color_to_rgb, list_to_matrix
import random
import time

nb_start = time.time()
```

## Exploring System Types

In this notebook, we again simulate the system presented in `general_grid_assembly.ipynb`, but do so for three different parameter regimes that have qualitatively distinct thermal behaviors.

We will use much of the code from that section so we copy it here without additional explanation

### Numerical representations of analytical work

#### Equations of Large $N$ approximation

In the large $N$ limit, the order parameters for the system can be approximated as

\begin{equation}
\langle k \rangle =   \sum_{j=1}^r \frac{n_j }{\bar{z} \gamma_j +1} \left(\bar{z} \gamma_j +  \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right) \qquad \langle m \rangle  =   \sum_{j=1}^r \frac{n_j \delta_j}{\delta_j-1} \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)},
\end{equation}
where $\bar{z}$ and $\bar{x}$ are defined as
\begin{equation}
\bar{z} = \sum_{j=1}^{R} \frac{n_j}{\bar{z} \gamma_j +1} \left(1- \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right), \qquad \bar{x} = \sum_{j=1}^{R} n_j\left(1- \frac{\displaystyle L_{n_j-1} \left( \bar{\phi}_{j}\right)}{\displaystyle  L_{n_j} \left( \bar{\phi}_{j} \right)}\right)
\end{equation}
with
\begin{equation}
\bar{\phi}_{j}\equiv \frac{\bar{x}}{1-\delta_j}\left(1+ \frac{1}{\bar{z} \gamma_j}\right).
\label{eq:phi_def}
\end{equation}
and $L_n(x)$ the $n$th Laguerre polynomial.

For these simulations we will take $$\gamma_j = (\beta E_V)^{3/2} e^{-\beta E_j}, \qquad \delta_j = e^{\beta \Delta_j}$$ where $E_V$ is a volumetric Boltzmann factor associated with free particles (e.g., for a point-particle $E_V \equiv h^2/2\pi mV^{2/3}$), and $E_j$ is the binding energy for particles to the grid. We also take where $\Delta_j$ is the binding energy advantage for particles binding to their correct locations in the grid.

#### Parameter function definitions

```python
# helper function definitions
gamma_func = lambda E0, Ev, T: 4*np.sqrt(2)*np.exp(E0/T)*(Ev/T)**(3/2)
delta_func = lambda Del, T: np.exp(Del/T)
phi_func = lambda x, z, gamma, delta: x*(1+ 1/(z*gamma))/(1-delta)
```

#### Equilibrium equations

```python
def constr_func(X, T, E0s, Dels, Evs, Ns):

    """
    Equations constraint equations that determine zbar and xbar
    """

    x = X[0]
    z = X[1]

    F = np.ones(2)

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    F[0] = z- np.sum([Ns[j]/(z*gammas_[j]+1)*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j])) for j in range(R)])
    F[1] = x- np.sum([Ns[j]*(1-eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j]) ) for j in range(R)])

    return F

def m_avg(T, E0s, Dels, Evs, Ns):

    """
    Function that computes m_avg
    """

    x, z =  fsolve(constr_func, x0 = (50,500), args = (T, E0s, Dels, Evs, Ns))

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    return np.sum([Ns[j]*deltas_[j]/(deltas_[j]-1)*eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j]) for j in range(R)] )

def k_avg( T, E0s, Dels, Evs, Ns):

    """
    Function that computes k_avg
    """

    x, z = fsolve(constr_func, x0 = (50,500), args = (T, E0s, Dels, Evs, Ns))

    R = len(Ns)
    gammas_ = gamma_func(E0s,Evs, T)
    deltas_ = delta_func(Dels, T)
    phis_ = phi_func(x, z, gammas_, deltas_)

    return np.sum([Ns[j]/(z*gammas_[j]+1)*(z*gammas_[j] + eval_laguerre(Ns[j]-1, phis_[j])/eval_laguerre(Ns[j], phis_[j])) for j in range(R)])
```

#### Important temperatures

For characterizing the differences between search and combinatorics-limited systems there are three relevant temperatures:

$$T_{\text{bind}}:\qquad 1=  \sum_{j=1}^R n_j \gamma_{j}(T)^{-1}\delta_j(T)^{-1}$$

$$ T_{\text{derang}}: \qquad 1=  \sum_{j=1}^R n_j \delta_j(T)^{-1}$$

$$ T_{\text{crit}}: \qquad 1=  \sum_{j=1}^R n_j \delta_j(T)^{-1}(1+ \gamma_{j}(T)^{-1})$$

Generally, we say that a system is combinatorics-limited when
\begin{equation}
\text{Combinatorics-limited System:} \qquad T_{\text{comb}} \simeq T_{\text{crit}}
\end{equation}
and a system is search-limited when
\begin{equation}
\text{Search-limited System:} \qquad T_{\text{search}} \simeq T_{\text{crit}}.
\end{equation}

```python
# Implicit equations defining temperature
def binding_constr(T, E0s, Dels, Evs, Ns):
    F =  1-np.sum(Ns*delta_func(Dels, T)**(-1)*gamma_func(E0s, Evs, T)**(-1))
    return F

def derang_constr(T,Dels, Ns):
    F =  1-np.sum(Ns*np.exp(-Dels/T))
    return F

def crit_constr(T, E0s, Dels, Evs, Ns):
    F =  1-np.sum(Ns*delta_func(Dels, T)**(-1)*(1+gamma_func(E0s, Evs, T)**(-1)))
    return F

# Computing temperatures from implicit equations
kBT_bind = lambda E0s, Dels, Evs, Ns: fsolve(binding_constr, x0 = 100.5, args = (E0s, Dels, Evs, Ns))[0]
kBT_derang = lambda Dels, Ns: fsolve(derang_constr, x0 = 100.5, args = (Dels, Ns))[0]
kBT_crit = lambda E0s, Dels, Evs, Ns: fsolve(crit_constr, x0 = 100.5, args = (E0s, Dels, Evs, Ns))[0]

```

### Metropolis Hastings simulation code

#### Microstate transitions

```python
## dissociation operator
def trans_dissoc(free_objs, bound_objs):

        # indices of non-empty
        indxs = [i for i, x in enumerate(bound_objs) if x != "-"]

        # random choice for bound object
        random_indx = random.choice(indxs)

        ## new state vector
        free_objs_new = copy.deepcopy(free_objs)
        bound_objs_new = copy.deepcopy(bound_objs)

        # putting empty slot
        bound_objs_new[random_indx] = '-'
        # appending previously bound object to free objects
        free_objs_new.append(bound_objs[random_indx])

        return free_objs_new, bound_objs_new

## association operator
def trans_assoc(free_objs, bound_objs):

        # random element to associate
        elem = random.choice(free_objs)

        # indices of empty spaces
        indxs = [i for i, x in enumerate(bound_objs) if x == "-"]

        # random choice for empty space
        random_indx = random.choice(indxs)

        ## new state vector
        free_objs_new = copy.deepcopy(free_objs)
        bound_objs_new = copy.deepcopy(bound_objs)

        ## state
        free_objs_new.remove(elem)
        bound_objs_new[random_indx] = elem

        return free_objs_new, bound_objs_new

## permutation operator
def trans_perm(free_objs, bound_objs):

    Ncomp = len(bound_objs)
    i1 = int(random.choice(range(Ncomp)))
    i2 = int(random.choice(range(Ncomp)))

    ## new omega vector
    bound_objs_new = copy.deepcopy(bound_objs)
    bound_objs_new[i2] = bound_objs[i1]
    bound_objs_new[i1] = bound_objs[i2]

    return free_objs, bound_objs_new
```

#### Logarithm of Botlzmann factor

The logarithm of the Botlzmann factor for a microstate (i.e., the temperature normalized negative energy of the microstate) is defined as

\begin{equation}
\beta E(\boldsymbol{k}, \boldsymbol{m}) = \sum_{i=1}^R(m_i \ln \delta_i + k_i \ln \gamma_i).
\label{eq:sim_en}
\end{equation}

```python
def log_boltz(free_objs, bound_objs, mstr_vec, deltas, gammas, name_key):

    elem_set = list(set(mstr_vec))
    count_dict = dict()
    for elem in elem_set:
        count_dict[elem] = bound_objs.count(elem)

    bind_log_factor = 0
    for elem in elem_set:
        key = name_key[elem]
        bind_log_factor += count_dict[elem]*np.log(gammas[key])

    corr_log_factor = 0
    for j in range(len(bound_objs)):
        if bound_objs[j] == mstr_vec[j]:
            elem = bound_objs[j]
            key = name_key[elem]
            corr_log_factor+=np.log(deltas[key])

    return bind_log_factor+corr_log_factor
```

#### Function to count the number of correctly bound particles

```python
def m_calc(bound_objs, mstr_vec):

    num = 0
    for k in range(len(mstr_vec)):
        if mstr_vec[k] == bound_objs[k]:
            num += 1
    return num
```

#### Metropolis Hastings algorithm

```python
### Metropolis Monte Carlo Algorithm

## loads uniform random sampling
runif = np.random.rand

def met_assembly_grid(Niter, free_objs, bound_objs, mstr_vec, deltas, gammas, name_key):
    '''
    #################################################################
    # function to sample using Metropolis
    #
    # n_iter:  number of iterations
    # initial_state: initial state for the start position for our chain
    # gamma: energy cost for incorrect component
    # temp: temperature
    ##################################################################
    '''

    # Initialize state values
    free_objs_vals = [0]*(Niter+1)
    bound_objs_vals = [0]*(Niter+1)

    # Set initial values
    free_objs_vals[0] = free_objs[:]
    bound_objs_vals[0] = bound_objs[:]

    # Initialize acceptance counts
    # We can use this to tune our number of steps
    accepted = 0

    # debugging code
    debug_assoc, debug_dissoc, debug_perm = 0, 0, 0

    for i in range(Niter):

        # get current monomer and dimer states
        current_free_objs = copy.deepcopy(free_objs_vals[i])
        current_bound_objs = copy.deepcopy(bound_objs_vals[i])

        N_free = len(current_free_objs)
        N_bound = len(current_bound_objs)-len(current_free_objs)

        u_trans = runif()

        if u_trans < 1/3: #first type of transition; monomer association

            if N_free < 1:

                log_alpha = np.log(1e-15)

            else:
                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_assoc(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # weight
                num = N_free*N_free
                den = N_bound+1

                # Log-acceptance rate
                log_alpha = log_final-log_init+np.log(num/den)

        elif 1/3 <= u_trans < 2/3: #second type of transition; bound monomer dissociation

            if N_bound <1:

                log_alpha = np.log(1e-15)

            else:

                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_dissoc(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # weight
                num = N_bound
                den = (N_free+1)*(N_free+1)

                # Log-acceptance rate
                log_alpha = log_final-log_init+np.log(num/den)

        elif 2/3 <= u_trans: #third type of transition; switching bounded elements

            if N_bound <2:

                log_alpha = np.log(1e-15)

            else:

                # proposed new monomer and dimer states
                new_free_objs, new_bound_objs = trans_perm(current_free_objs, current_bound_objs)

                # transition elements
                log_init = log_boltz(current_free_objs, current_bound_objs, mstr_vec, deltas, gammas, name_key)
                log_final = log_boltz(new_free_objs, new_bound_objs, mstr_vec, deltas, gammas, name_key)

                # Log-acceptance rate
                log_alpha = log_final-log_init

        # Sample a uniform random variate
        u = runif()

        # Test proposed value
        if np.log(u) < log_alpha:
            # Accept
            free_objs_vals[i+1] = new_free_objs
            bound_objs_vals[i+1] = new_bound_objs

            #log_current_prob = log_proposed_prob
            accepted += 1
        else:
            # Stay put
            free_objs_vals[i+1] = free_objs_vals[i]
            bound_objs_vals[i+1] = bound_objs_vals[i]

    # return our samples and the number of accepted steps
    return free_objs_vals, bound_objs_vals, accepted
```

#### Computing microstate averages from simiulations

```python
def avg_k(bound_objs_vals, Nmc):

    """
    Microstate average of number of bound objects
    We only consider microstates near the end of theh chain to ensure
    that the system has equilibrated
    """

    length = int(Nmc/50)

    ls = [0]*length
    ls = np.array(ls)
    for k in range(length):
        ls[k] = len(bound_objs_vals[Nmc-length+k]) - bound_objs_vals[Nmc-length+k].count('-')

    return(np.mean(ls))

# average number of correctly bound objects
def avg_m(bound_objs_vals, mstr_vec, Nmc):

    """
    Microstate average of number of correctly bound objects
    We only consider microstates near the end of theh chain to ensure
    that the system has equilibrated
    """

    length = int(Nmc/50)

    ls = [0]*length
    ls = np.array(ls)
    for k in range(length):
        ls[k] =  np.sum([1 for j in range(len(mstr_vec)) if bound_objs_vals[Nmc-length+k][j]==mstr_vec[j]])

    return(np.mean(ls))
```

#### Image grid for completely correct configuration

```python
# defining master_list
master_list =compute_master_list()
# testing plot
imshow_list(master_list, title = 'Completely Correct Configuration');
# defining Nelems
Nelems = np.zeros(8)
key_list = list(rgb_map.keys())[:-1]
name_key_ = dict()
for j in range(len(key_list)):
    name_key_[key_list[j]] = j
    Nelems[j] = master_list.count(key_list[j])
```

```python
# displaying copy-number counts of the various elements
Nelems
```

    array([ 9.,  9., 10.,  5.,  7.,  6.,  3., 51.])

### Search-limited, Indeterminate, and Combinatorics-limited system simulations

```python
# setting parameter dictionary
param_dict = {'Search Limited': {'Del_bar':7.7501 ,
                                 'sigma_D':2.0,
                                 'E0_bar':3.0,
                                 'sigma_E':1.0},
              'Combinatorics Limited':{'Del_bar': 4.75,
                                 'sigma_D': 2.0,
                                 'E0_bar': 16.0,
                                 'sigma_E':3.0},
              'Indeterminate': {'Del_bar': 6.75,
                                 'sigma_D': 2.0,
                                 'E0_bar': 10.75,
                                 'sigma_E': 3.0}, }
```

```python
## Generate lf for each temperature from .03 to 2.0 in npoints steps

t0_start = time.time()

# number of steps for MC algortihm
Nmc = 30000

random.seed(0)

# initial monomer and dimer states;
# system in microstate of all correct dimers
free_objs_0 = []
bound_objs_0 = random.sample(master_list, len(master_list))
mstr_vec = copy.deepcopy(master_list)

# temperature limits
Tmin = .05
Tmax = 3.0

npoints = 15 #number of temperature values
navg = 5 # number of times we run simulation at each temperature; 50 in paper
temp_vals = np.linspace(Tmin, Tmax, npoints).tolist()

sim_k_vals_dict = {'Search Limited': [0]*npoints,
              'Combinatorics Limited':[0]*npoints,
              'Indeterminate':[0]*npoints }

sim_m_vals_dict = {'Search Limited': [0]*npoints,
              'Combinatorics Limited':[0]*npoints,
              'Indeterminate':[0]*npoints }

# list of dimer values
sim_k_vals = [0]*npoints
# list of correct dimer values
sim_m_vals = [0]*npoints

for type_ in list(sim_k_vals_dict.keys()):

    t0 = time.time()

    # getting parameter values
    dict_vals = param_dict[type_]

    # drawing energy values
    np.random.seed(24)
    R=8
    Del_bar, sigma_D = dict_vals['Del_bar'], dict_vals['sigma_D']
    Dels = np.random.randn(R)*sigma_D+Del_bar
    E0_bar, sigma_E = dict_vals['E0_bar'], dict_vals['sigma_E']
    E0s = np.random.randn(R)*sigma_E+E0_bar
    Evs = np.ones(R)*0.001

    for k in range(npoints):

        fin_k_vals = [0]*navg
        fin_m_vals = [0]*navg
        fin_accepted = [0]*navg

        for j in range(navg):

            # make copy of initial monomer and dimer states
            free_objs_copy = copy.deepcopy(free_objs_0)
            bound_objs_copy = copy.deepcopy(bound_objs_0)

            # defining helper functions
            gammas_ = gamma_func(E0s, Evs, temp_vals[k])
            deltas_ = delta_func(Dels, temp_vals[k])

            # metroplois generator
            free_list, bound_list, accepted = met_assembly_grid(Nmc,
                                                                free_objs_copy,
                                                                bound_objs_copy,
                                                                mstr_vec,
                                                                deltas_,
                                                                gammas_,
                                                                name_key_)

            # averaging final states to compute observables
            fin_k_vals[j] = avg_k(bound_list, Nmc)
            fin_m_vals[j] = avg_m(bound_list, mstr_vec, Nmc)

        # averaging over computed equilibrium averages
        sim_k_vals_dict[type_][k] = np.mean(np.array(fin_k_vals))
        sim_m_vals_dict[type_][k] = np.mean(np.array(fin_m_vals))

        t_prelim = time.time()
        print("Temperature Run:",str(k+1),"; Current Time:", round(t_prelim-t0,2),"secs")

    t1 = time.time()
    print(f"\nTotal Simulation Run Time for {type_}: {round(t1-t0,2)} secs")
    print("----------\n")

t1 = time.time()
print("------------------------------\n------------------------------")
print(f"Total Simulation Run Time for all: {round(t1-t0_start,2)} secs")
```

    Temperature Run: 1 ; Current Time: 49.3 secs
    Temperature Run: 2 ; Current Time: 105.02 secs
    Temperature Run: 3 ; Current Time: 166.1 secs
    Temperature Run: 4 ; Current Time: 225.39 secs
    Temperature Run: 5 ; Current Time: 294.01 secs
    Temperature Run: 6 ; Current Time: 360.05 secs
    Temperature Run: 7 ; Current Time: 419.58 secs
    Temperature Run: 8 ; Current Time: 472.65 secs
    Temperature Run: 9 ; Current Time: 520.46 secs
    Temperature Run: 10 ; Current Time: 564.49 secs
    Temperature Run: 11 ; Current Time: 606.29 secs
    Temperature Run: 12 ; Current Time: 644.35 secs
    Temperature Run: 13 ; Current Time: 681.3 secs
    Temperature Run: 14 ; Current Time: 717.6 secs
    Temperature Run: 15 ; Current Time: 751.73 secs

    Total Simulation Run Time for Search Limited: 751.73 secs
    ----------

    Temperature Run: 1 ; Current Time: 44.95 secs
    Temperature Run: 2 ; Current Time: 91.21 secs
    Temperature Run: 3 ; Current Time: 137.59 secs
    Temperature Run: 4 ; Current Time: 179.81 secs
    Temperature Run: 5 ; Current Time: 224.67 secs
    Temperature Run: 6 ; Current Time: 273.25 secs
    Temperature Run: 7 ; Current Time: 327.02 secs
    Temperature Run: 8 ; Current Time: 385.52 secs
    Temperature Run: 9 ; Current Time: 442.11 secs
    Temperature Run: 10 ; Current Time: 494.63 secs
    Temperature Run: 11 ; Current Time: 546.67 secs
    Temperature Run: 12 ; Current Time: 596.38 secs
    Temperature Run: 13 ; Current Time: 643.46 secs
    Temperature Run: 14 ; Current Time: 688.62 secs
    Temperature Run: 15 ; Current Time: 731.06 secs

    Total Simulation Run Time for Combinatorics Limited: 731.07 secs
    ----------

    Temperature Run: 1 ; Current Time: 45.67 secs
    Temperature Run: 2 ; Current Time: 91.06 secs
    Temperature Run: 3 ; Current Time: 134.53 secs
    Temperature Run: 4 ; Current Time: 183.06 secs
    Temperature Run: 5 ; Current Time: 233.52 secs
    Temperature Run: 6 ; Current Time: 285.77 secs
    Temperature Run: 7 ; Current Time: 348.6 secs
    Temperature Run: 8 ; Current Time: 410.27 secs
    Temperature Run: 9 ; Current Time: 467.1 secs
    Temperature Run: 10 ; Current Time: 521.68 secs
    Temperature Run: 11 ; Current Time: 572.01 secs
    Temperature Run: 12 ; Current Time: 619.68 secs
    Temperature Run: 13 ; Current Time: 665.73 secs
    Temperature Run: 14 ; Current Time: 709.08 secs
    Temperature Run: 15 ; Current Time: 749.76 secs

    Total Simulation Run Time for Indeterminate: 749.76 secs
    ----------

    ------------------------------
    ------------------------------
    Total Simulation Run Time for all: 2232.56 secs

```python
# temperature limits
Tmin = .05
Tmax = 3.0

npoints = 15 #number of temperature values
navg = 1 # number of times we run simulation at each temperature; 50 in paper
temp_vals = np.linspace(Tmin, Tmax, npoints).tolist() # simulation temperature values
Tvals = np.linspace(0.1, 3.0, 50) # analytic temperature values

u = 0
for type_ in list(sim_k_vals_dict.keys()):

    plt.figure(figsize = (8,6))
    ax = plt.subplot(111)

    t0 = time.time()

    # getting parameter values
    dict_vals = param_dict[type_]

    # drawing energy values
    np.random.seed(24)
    R=8
    Del_bar, sigma_D = dict_vals['Del_bar'], dict_vals['sigma_D']
    Dels = np.random.randn(R)*sigma_D+Del_bar
    E0_bar, sigma_E = dict_vals['E0_bar'], dict_vals['sigma_E']
    E0s = np.random.randn(R)*sigma_E+E0_bar
    Evs = np.ones(R)*0.001

    # simulation results
    plt.plot(temp_vals,np.array(sim_k_vals_dict[type_])/np.sum(Nelems),
            label = r'Sim. $\langle k \rangle$/N',
            markersize = 7.5,
           marker = 'D',
           linestyle = '')
    plt.plot(temp_vals,np.array(sim_m_vals_dict[type_])/np.sum(Nelems),
            label = r'Sim. $\langle m \rangle$/N',
            markersize = 7.5,
           marker = 's',
           linestyle = '')

    # large N analytical results
    k_avg_approx_vals = [k_avg(T, E0s, Dels, Evs, Nelems)/np.sum(Nelems) for T in Tvals]
    m_avg_approx_vals = [m_avg(T, E0s, Dels, Evs, Nelems)/np.sum(Nelems) for T in Tvals]
    plt.plot(Tvals, k_avg_approx_vals, label = r'Large $N$ $\langle k \rangle$/N', linestyle= '--', linewidth = 3.0)
    plt.plot(Tvals, m_avg_approx_vals, label = r'Large $N$ $\langle m \rangle$/N', linewidth = 2.0 )

    ax.axvline(x = kBT_derang(Dels, Nelems), color = 'r', linestyle = '-.')
    ax.axvline(x = kBT_bind( E0s, Dels, Evs, Nelems), color = 'g', linestyle = ':', linewidth = 4)
    ax.axvline(x = kBT_crit( E0s, Dels, Evs, Nelems), color = 'k', linestyle = 'dashed', linewidth = 2)

    if u == 0:
        plt.legend(loc = 'best', fontsize = 13)

    # plot formatting
    ax.set_xlabel(r'$k_B T$', fontsize = 18)
    plt.xlim([-0.01,3.2])
    plt.ylim([0,1.1])
    plt.grid(alpha = 0.45)

    # Hide the right and top spines
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)

    # Only show ticks on the left and bottom spines
    ax.yaxis.set_ticks_position('left')
    ax.xaxis.set_ticks_position('bottom')

    # increase label size
    ax.tick_params(axis='both', which='major', labelsize=14)
    ax.tick_params(axis='both', which='minor', labelsize=14)

    if u<2:
        ax.text(kBT_derang(Dels, Nelems)-.2, 0.25, r'$k_BT_{comb}$', color='black', fontsize = 18,
                bbox=dict(facecolor='white', edgecolor='none', pad=5.0))
        ax.text(kBT_bind(E0s, Dels, Evs, Nelems)-.2, 0.25, r'$k_BT_{search}$', color='black', fontsize = 18,
                bbox=dict(facecolor='white', edgecolor='none', pad=5.0))
        ax.text(kBT_crit(E0s, Dels,Evs, Nelems)-.15, 0.65, r'$k_BT_{c}$', color='black', fontsize = 18,
                bbox=dict(facecolor='white', edgecolor='none', pad=5.0))

    else:
        ax.text(kBT_derang(Dels, Nelems)-.625, 0.25, r'$k_BT_{comb} \simeq k_BT_{search}$', color='black', fontsize = 18,
                bbox=dict(facecolor='white', edgecolor='none', pad=3.0))
        ax.text(kBT_crit(E0s, Dels,Evs, Nelems)-.15, 0.65, r'$k_BT_{c}$', color='black', fontsize = 18,
                bbox=dict(facecolor='white', edgecolor='none', pad=3.0))

    u+=1

#     type_name = type_.replace(' ', '_').lower()
#     plt.savefig(f'general_grid_assembly_sim_anl_{type_name}.png', bbox_inches='tight', format = 'png')

    print(type_)
    plt.show()
```

```python
print('Total Notebook Runtime: %.3f mins' % ((time.time()-nb_start)/60))
```

    Total Notebook Runtime: 37.247 mins


```python
from Fluid_Mechanics.FluidSystems import System
from sympy import Symbol
```

```python
system = System()
```

    Don't forget to set the u and v here manually only using passed variables!

```python
x = Symbol("x")
y = Symbol("y")
```

```python
system.u = -0.004 * (x /(x * x + y * y))
system.v = - 0.004 * (y /(x * x + y * y))
```

```python
summary = system.get_summary([[-2, 2],[-2, 2]], num=50)
summary
```

```python

```


# Dymos: Using an Optimizer to Solve a Simple Boundary Value Problem

In the previous notebook, we demonstrated
- how to install Dymos
- how to define a simple ODE system
- how to use that system to propagate a simple trajectory

Using the `scipy.itegrate.solve_ivp` functionality from within Dymos will provide a trajectory assuming we know the initial state, control profile, and time duration of that trajectory.

In our next use case we want to solve a simple boundary value problem:

_How far will the cannonball fly?_

In order to determine this, we need to allow the duration of the flight to vary such that the final height of the cannonball is zero.  There are two general approaches to this.

1. Use an optimizer and pose the problem as an optimal control problem.
2. Use a nonlinear solver and pose the problem with residuals to be driven to zero.

# Posing the cannonball boundary value problem as an optimal control problem.

The optimal control problem can be stated as:

\begin{align}
  \mathrm{Minimize}\;J &= t_f \\
  \mathrm{subject\;to\!:} \\
  x(t_0) &= 0 \;\mathrm{m} \\
  y(t_0) &= 0 \;\mathrm{m}\\
  vx(t_0) &= 100 \;\mathrm{m/s} \\
  vy(t_0) &= 100 \;\mathrm{m/s} \\
  y(t_f) &= 0 \;\mathrm{m}
\end{align}

Traditionally, the collocation techniques in Dymos have used an optimizer to satisfy the _defect constraints_ ($\Delta$).
These defects are the difference between the slope of each state polynomial representation in each segment and the rate of change as given by the ODE.
In Dymos, these defects are measured at a certain subset of our nodes called the _collocation nodes_.  For the Radau pseudospectral transcription, each 3rd-order segment has 4 state input nodes, and 3 collocation nodes.
Dymos handles this underlying transcription from an optimal control problem into a nonlinear programming (NLP) problem.
The equivalent NLP problem for our optimal control problem above is:

\begin{align}
  \mathrm{Minimize}\;J &= t[-1] \\
  \mathrm{subject\;to\!:} \\
  x_{lb} &\le x_i \le x_{ub} \\
  y_{lb} &\le y_i \le y_{ub} \\
  vx_{lb} &\le vx_i \le vx_{ub} \\
  vy_{lb} &\le vy_i \le vy_{ub} \\
  \Delta x_j &= 0 \\
  \Delta y_j &= 0 \\
  \Delta vx_j &= 0 \\
  \Delta vy_j &= 0 \\
  vy[-1] &= 0 \\
  i &= 1\;...n-1 \\
  j &= 0\;...n-2 \\
\end{align}

In the case of our single 3rd-order Radau segment, we have 4 nodes (n=4).
Since each initial state is fixed, we have 12 design variables and 12 constraints.  There is, at most, a single solution to this problem.  This makes sense.  Since there are no control variables or changes in the initial state allowed, there should be only one possible physical path for the cannonball.  Most optimizers will be perfectly capable of solving this problem, despite the fact that it only has a single feasible solution.

# Solving the optimal control problem

First, lets bring in our definition of the ODE and the other general setup that we used in the previous notebook.

```
!pip install dymos
```

    Requirement already satisfied: dymos in /usr/local/lib/python3.6/dist-packages (0.17.0)
    Requirement already satisfied: openmdao>=3.3.0 in /usr/local/lib/python3.6/dist-packages (from dymos) (3.5.0)
    Requirement already satisfied: numpy>=1.14.1 in /usr/local/lib/python3.6/dist-packages (from dymos) (1.18.5)
    Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.6/dist-packages (from dymos) (1.4.1)
    Requirement already satisfied: pyparsing in /usr/local/lib/python3.6/dist-packages (from openmdao>=3.3.0->dymos) (2.4.7)
    Requirement already satisfied: pyDOE2 in /usr/local/lib/python3.6/dist-packages (from openmdao>=3.3.0->dymos) (1.3.0)
    Requirement already satisfied: requests in /usr/local/lib/python3.6/dist-packages (from openmdao>=3.3.0->dymos) (2.23.0)
    Requirement already satisfied: networkx>=2.0 in /usr/local/lib/python3.6/dist-packages (from openmdao>=3.3.0->dymos) (2.5)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests->openmdao>=3.3.0->dymos) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests->openmdao>=3.3.0->dymos) (1.24.3)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests->openmdao>=3.3.0->dymos) (2.10)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests->openmdao>=3.3.0->dymos) (2020.12.5)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx>=2.0->openmdao>=3.3.0->dymos) (4.4.2)

```
import numpy as np
import openmdao.api as om
import dymos as dm
```

```
class ProjectileODE(om.ExplicitComponent):

  def initialize(self):
    self.options.declare('num_nodes', types=int,
                         desc='the number of points at which this ODE is simultaneously evaluated')

  def setup(self):
    nn = self.options['num_nodes']
    self.add_input('vx', shape=(nn,), units='m/s')
    self.add_input('vy', shape=(nn,), units='m/s')

    self.add_output('x_dot', shape=(nn,), units='m/s',
                    tags=['state_rate_source:x', 'state_units:m'])

    self.add_output('y_dot', shape=(nn,), units='m/s',
                    tags=['state_rate_source:y', 'state_units:m'])

    self.add_output('vx_dot', shape=(nn,), units='m/s**2',
                    tags=['state_rate_source:vx', 'state_units:m/s'])

    self.add_output('vy_dot', shape=(nn,), units='m/s**2',
                    tags=['state_rate_source:vy', 'state_units:m/s'])

    self.declare_partials(of='*', wrt='*', method='fd')

  def compute(self, inputs, outputs):
    outputs['x_dot'][:] = inputs['vx']
    outputs['y_dot'][:] = inputs['vy']
    outputs['vx_dot'][:] = 0.0
    outputs['vy_dot'][:] = -9.81

```

```
prob = om.Problem()

traj = prob.model.add_subsystem('traj', dm.Trajectory())

phase = traj.add_phase('phase', dm.Phase(ode_class=ProjectileODE, transcription=dm.Radau(num_segments=1, order=3)))

phase.set_time_options(fix_initial=True, duration_bounds=(1, 1000), units='s')

phase.set_state_options('x', fix_initial=True)
phase.set_state_options('y', fix_initial=True)
phase.set_state_options('vx', fix_initial=True)
phase.set_state_options('vy', fix_initial=True)
```

# Imposing state boundary constraints

There are two ways in which we can impose boundary constraints on the state.  The first is to use the arguments `fix_initial` and `fix_final`.

Using `fix_final` removes the final value of the state as a design variable.  That is, our final value of `y` will always be satisfied and the optimizer will work to find the rest of the state history.

The other option is to use a nonlinear boundary constraint.  This is a constraint applied after-the-fact.  The optimizer is free to suggest a final value of `y` that violates the constraint, but then will work to satisfy it as it iterates.  This approach gives the optimizer a bit more freedom to explore the design space and might therefore be more robust.  Using `fix_initial` and `fix_final` also impose limitations when using shooting methods in Dymos, which we'll discuss later.

```
phase.add_boundary_constraint('y', loc='final', equals=0)
```

# Adding an objective

Using an optimization driver **requires** that an objective be specified.
Again, in this case it's somewhat meaningless, there should only be a single valid solution that satisfies all of our constraints.
To satisfy the driver, we'll set the objective to be the final time.
Note that the Dymos `Phase` object has an `add_objective` method that's similar to the standard OpenMDAO `add_objective` method, but also allows us to specify the _locaiton_ in time where the objective should be evaluated (`'initial'` or `'final'`).

The variable `'time'` is recognized by Dymos as the one of the special variables in the phase (along with states, controls, and parameters).  If we wanted to, we could also make any output of the ODE an objective by specifying the ODE-relative path to that variable.

```
phase.add_objective('time', loc='final', ref=1.0)
```

# Specifying a driver

Since we're performing optimization now, we'll need a driver.
The optimizers in `scipy.optimize` will work in this case.  To use them, we use OpenMDAO's `ScipyOptimizeDriver`.

```
prob.driver = om.ScipyOptimizeDriver()
```

Next we call OpenMDAO's `Problem.setup` method.  This works out the various connections between the different systems that Dymos uses to solve this problem and allocates memory.  You can think of this step a bit like compiling our model before we run it.

```
prob.setup()
```

    <openmdao.core.problem.Problem at 0x7f3e99f6e320>

As before, we'll specify an initial guess for the initial time and duration of the phase, along with linear guesses to the state-time history.

```
prob.set_val('traj.phase.t_initial', 0.0, units='s')
prob.set_val('traj.phase.t_duration', 15.0, units='s')

prob.set_val('traj.phase.states:x', phase.interpolate(ys=[0, 100], nodes='state_input'), units='m')
prob.set_val('traj.phase.states:y', phase.interpolate(ys=[0, 0], nodes='state_input'), units='m')
prob.set_val('traj.phase.states:vx', phase.interpolate(ys=[100, 100], nodes='state_input'), units='m/s')
prob.set_val('traj.phase.states:vy', phase.interpolate(ys=[100, -100], nodes='state_input'), units='m/s')
```

# Solving the Problem

To solve this problem, we need to allow the driver to iterate.
The native OpenMDAO option to do so is `Problem.run_driver`.

Dymos provides its own function, `dymos.run_problem`, which is basically a wrapper around the OpenMDAO method with some additional conveniences built in.
Though not necessary yet, we'll use that method just to get in the habit of doing so.

```
dm.run_problem(prob, run_driver=True, simulate=True, make_plots=True)
```

    Model viewer data has already has already been recorded for Driver.
    Optimization terminated successfully.    (Exit mode 0)
                Current function value: 20.387359836901105
                Iterations: 2
                Function evaluations: 2
                Gradient evaluations: 2
    Optimization Complete
    -----------------------------------

    Simulating trajectory traj

    /usr/local/lib/python3.6/dist-packages/openmdao/recorders/recording_manager.py:266: UserWarning:The model is being run again, if the options or scaling of any components has changed then only their new values will be recorded.

    Done simulating trajectory traj

```
# TODO: Make these more automatic (and use an interactive plot library like bokeh)

from IPython.display import display
from ipywidgets import widgets, GridBox

items = [widgets.Image(value=open(f'plots/states_{state}.png', 'rb').read()) for state in ['x', 'y', 'vx', 'vy']] + \
[widgets.Image(value=open(f'plots/state_rates_{state}.png', 'rb').read()) for state in ['x', 'y', 'vx', 'vy']]
widgets.GridBox(items, layout=widgets.Layout(grid_template_columns="repeat(2, 500px)"))

```

    GridBox(children=(Image(value=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\xb0\x00\x00\x01 \x08\x06\x00\x…

# The Solution

The plots above show that the implicit solution and the simulated trajectory are now in agreement (the simulated trajectory is a reasonably accurate interpolation of the solution).

To extract the solution, we can pull values for the special variables in the phase from the _timeseries_ output.

```
t = prob.get_val('traj.phase.timeseries.time')
x = prob.get_val('traj.phase.timeseries.states:x')
y = prob.get_val('traj.phase.timeseries.states:y')

print(f'The time of flight is {t[-1, 0]} sec')
print(f'The range flown is {x[-1, 0]} m')
print(f'The final altitude of the cannonball is {y[-1, 0]} m')
```

    The time of flight is 20.387359836901105 sec
    The range flown is 2038.7359836901096 m
    The final altitude of the cannonball is -5.684341886080802e-14 m


*This notebook contains course material from [CBE30338](https://jckantor.github.io/CBE30338)
by Jeffrey Kantor (jeff at nd.edu); the content is available [on Github](https://github.com/jckantor/CBE30338.git).
The text is released under the [CC-BY-NC-ND-4.0 license](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode),
and code is released under the [MIT license](https://opensource.org/licenses/MIT).*

< [Implementing PID Controllers with Python Yield Statement](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.01-Implementing_PID_Control_with_Python_Yield_Statement.ipynb) | [Contents](toc.ipynb) | [PID Control with Bumpless Transfer](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.03-PID_Control_with_Bumpless_Transfer.ipynb) >

# PID Control with Setpoint Weighting

## Setpoint 'Kick'

Our standard form for PID control has been

\begin{align}
MV & = \overline{MV} + K_P (SP - PV) + K_I\int_0^t (SP - PV)\ dt' + K_D\frac{d(SP - PV)}{dt}
\end{align}

Our initial implementation of PID control uncovered an issue with this formula  when there are sudden changes to the setpoint $SP$. The issue is most obvious in the derivative term where

\begin{align}
K_D\frac{d(SP - PV)}{dt} & \longrightarrow \infty
\end{align}

for a step change in setpoint. The issue is also evident in the proportional term $K_P (SP - PV)$ which undergoes a step change in value. In process control applications, a step change in the manipulated variable may require sudden and abrupt changes in valve position, process flows, or pressures, all of which can cause significant strain on very large devices.

This phenomenon is called **setpoint kick** (or sometimes **derivative kick**) which is generally to be avoided.

## Setpoint Weighting

The most common remedy is to introduce setpoint weighting. To this end, the PID control formula is written

\begin{align}
MV & = \overline{MV} + K_P e_P + K_I\int_0^t e_I\ dt' + K_D\frac{de_D}{dt}
\end{align}

where the error terms are given by

\begin{align*}
e_P(t) & = \beta\ SP - PV \\
e_I(t) & = SP - PV \\
e_D(t) & = \gamma\ SP - PV
\end{align*}

The effect is to introduce a different error term for each term in the control equation. The integral term remains the same as before, which is necessary in order to force the process variable $PV$ to be equal to the setpoint $SP$ at steady-state. The error terms for the proportional and derivative terms, however, include constants $\beta$ and $\gamma$ which are used to modify the response to setpoint changes.

Common practice is to set $\gamma = 0$ which entirely eliminates derivative action based on change in the setpoint. When incorporated in PID control, this feature is sometimes called **derivative on output**. This almost always a good idea in process control because it eliminates the 'derivative kick' associated with a quick change in setpoint.

In practice, the term $\beta$ is generally tuned to meet the specific application requirements. In this case, where setpoint tracking is not a high priority, setting $\beta = 0$ is a reasonable starting point.

## Implementation of Setpoint Weighting

The implementation of setpoint weighting is straightforward. The object definition is modified to include additional parameters `beta` and `gamma` with typical default values. Error terms for the proportional and derivative terms are computed explicitly, and in the case of the derivative also saved for the next time step.

```python
def PID(Kp, Ki, Kd, MV_bar=0, beta=1, gamma=0):
    # initialize stored data
    eD_prev = 0
    t_prev = -100
    I = 0

    # initial control
    MV = MV_bar

    while True:
        # yield MV, wait for new t, SP, PV
        t, PV, SP = yield MV

        # PID calculations
        P = Kp*(beta*SP - PV)
        I = I + Ki*(SP - PV)*(t - t_prev)
        eD = gamma*SP - PV
        D = Kd*(eD - eD_prev)/(t - t_prev)
        MV = MV_bar + P + I + D

        # update stored data for next iteration
        eD_prev = eD
        t_prev = t
```

## Simulation

Let's see how well this PID implementation works. We'll perform the simulation with a setpoint that starts at room temperature, then rises to 50°C at $t = 50$ seconds. The data historian will record values of the setpoint, process variable, computed manipulated variable, and the actual value of the manipulated variable.

```python
%matplotlib inline
from tclab import clock, setup, Historian, Plotter

TCLab = setup(connected=False, speedup=10)

controller = PID(2, 0.1, 2, beta=0)   # create pid control
controller.send(None)                 # initialize

tfinal = 800

with TCLab() as lab:
    h = Historian([('SP', lambda: SP), ('T1', lambda: lab.T1), ('MV', lambda: MV), ('Q1', lab.Q1)])
    p = Plotter(h, tfinal)
    T1 = lab.T1
    for t in clock(tfinal, 2):
        SP = T1 if t < 50 else 50           # get setpoint
        PV = lab.T1                         # get measurement
        MV = controller.send([t, PV, SP])   # compute manipulated variable
        lab.U1 = MV                         # apply
        p.update(t)                         # update information display
```

```python

```

< [Implementing PID Controllers with Python Yield Statement](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.01-Implementing_PID_Control_with_Python_Yield_Statement.ipynb) | [Contents](toc.ipynb) | [PID Control with Bumpless Transfer](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.03-PID_Control_with_Bumpless_Transfer.ipynb) >