```python
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, Aer
from qiskit import execute
from qiskit.tools.visualization import plot_histogram
import numpy as np
import matplotlib.pyplot as plt
from qiskit.ignis.mitigation.measurement import (complete_meas_cal,CompleteMeasFitter)
from qiskit.providers.aer.noise import NoiseModel
from qiskit.providers.aer.noise.errors import pauli_error, depolarizing_error
from IPython.display import Image
```

# Making a single qubit depolarizing error channel.
The aim is to use Qiskits built in depolarizing quantum error channel function
$\texttt{depolarizing\_error(param, num\_qubits, standard\_gates=True)}$ to create the desired depolarizing channel. It takes the depolarization parameter $\lambda$ and number of qubits $n$ to create the depolarizing channel
$$
\begin{equation}
    \label{eqn:depo-chan-qiskit}
    \varepsilon(\rho) = (1-\lambda)\rho + \lambda Tr\left[\rho\right]\frac{I}{2^{n}}
    \text{.}
\end{equation}$$

It can then be manipulated to create the single qubit depolarizing channel
$$
\begin{equation}
    \label{eqn:depo-chan-1qubit}
    \varepsilon(\rho_{single-qubit}) =
    (1-P)\rho + \frac{P}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right)
    \text{.}
\end{equation}$$
This is shown by the following derivation.

For a single qubit $n=1$
$$ \frac{I}{2^{1}} = \frac{1}{4} \left(I\rho I + X\rho X + Y\rho Y + Z\rho Z\right),$$
and $Tr[\rho] = 1.$

If we then chose
$$\lambda=\frac{4^{n}P}{4^{n}-1},$$
where $P$ is the probability. At $n=1$ the depolarization paramater
$$\lambda = \frac{4P}{3}$$ and by substituting these results into $\varepsilon(\rho)$ we get that

$$
\begin{align}
    \varepsilon(\rho) &= (1-\frac{4P}{3})\rho +
        \frac{4P}{3} \left(I\rho I + X\rho X + Y\rho Y + Z\rho Z\right)
    \frac{1}{4} \\
                      &=  \left( 1 - \frac{4P}{3} + \frac{P}{3} \right)\rho
                        + \frac{P}{3} \left(X\rho X + Y\rho Y + Z\rho Z\right) \\
                       &= (1-P)\rho + \frac{P}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right)
    \text{.}
\end{align}
$$

To prove that this works I wil be using a 2, 3 and 4 qubit system to show the affect of the depolarazation channel when $\lambda=\frac{4P}{3}$. The depolazation channel will act on every qubit indivually when a measuremnet is done. This means that every qubit wil experience a single qubit depolarazation channel of the form
$$\varepsilon(\rho_{single-qubit}) =
    (1-P)\rho + \frac{P}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right)$$

```python
def get_noise(p, qubits=1):

    # This creates the depolarizing error channel,
    # epsilon(P) = (1-P)rho + (P/3)(XrhoX + YrhoY + ZrhoZ).
    depo_err_chan = depolarizing_error((4*p)/3, qubits)

    # Creating the noise model to be used during execution.
    noise_model = NoiseModel()

    noise_model.add_all_qubit_quantum_error(depo_err_chan, "measure") # measurement error is applied to measurements

    return noise_model, depo_err_chan
```

```python
# The probability for depolarazation = 1,
# Number of qubits = 1.
noise_model, depo_err_chan = get_noise(1,1)
```

```python
# Properties of the depolarasation channel when P=1.
print(depo_err_chan)
```

    QuantumError on 1 qubits. Noise circuits:
      P(0) = 0.3333333333333333, QasmQobjInstructions = [[{'name': 'x', 'qubits': [0]}]
      P(1) = 0.3333333333333333, QasmQobjInstructions = [[{'name': 'y', 'qubits': [0]}]
      P(2) = 0.3333333333333333, QasmQobjInstructions = [[{'name': 'z', 'qubits': [0]}]

When $P=1$ the total probability for a qubit to be flipped is 2/3.
This is because of the $X$ and $Y$ operators both having a probability occuring of 1/3.
The total probability for a qubit to remain the same is 1/3.
This is because of $Z$ operator acting on the qubit.
As you can see this obeys the depolaration equation we want $\varepsilon(\rho_{single-qubit})$.
When $P=1$, $\varepsilon(\rho_{single-qubit})$ becomes

$$
\begin{align}
    \varepsilon(\rho_{single-qubit}) &=
    (1-1)\rho + \frac{1}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right) \\
                                    &=
    \frac{1}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right)
\end{align}
$$
This shows that the $X$, $Y$ and $Z$ operators occur with a probability 1/3, just like it's shown in the properties of the depolarasation channel above.

```python
# The structure of the noise model used during execution.
print(noise_model)
```

    NoiseModel:
      Basis gates: ['cx', 'id', 'u3']
      Instructions with noise: ['measure']
      All-qubits errors: ['measure']

We expect from the depolarazation channel acting on a 1 qubit system with initial qubit states $q_{0} = |0>$ to flip the qubit state $q_{0}$ with a total probability $2/3$ and remain the same with a probability of $1/3$. This will then give us the 2 possible states $|0>$ and $|1>$.

```python
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
qc = QuantumCircuit(qr, cr)
```

```python
qc.measure(0,0)
```

    <qiskit.circuit.instructionset.InstructionSet at 0x1df60b7ff48>

```python
print(qc)
```

          ┌─┐
    q0_0: ┤M├
          └╥┘
    c0_0: ═╩═

```python
counts = execute(qc, Aer.get_backend('qasm_simulator'),
                  noise_model=noise_model,
                  shots=1000000).result().get_counts()
```

```python
plot_histogram(counts)
```

```python
print(counts)
```

    {'1': 666688, '0': 333312}

|1-qubit state | Probability of the 1-qubit state  | Qiskit result |
|--------------|-----------------------------------|---------------|
|    $\vert 0>$ | $\frac{1}{3} = 0.333$ | 0.333|
|    $\vert 1>$ | $\frac{2}{3} = 0.666$ | 0.667|

From the results above you can see that the deporalizing error channel $\varepsilon(\rho_{single-qubit})$ works on the qubits like how it was expected with the small error due to statistical fluctuations involved in making measurements.

What we expect from the depolarazation channel acting on a 2 qubit system with initial qubit states $q_{0} = |0>$ and $q_{1} = |0>$ is that the $X$ and $Y$ will flip the individual qubit states $q_{0}$ and $q_{1}$ with a total probability $2/3$ and remain the same with a probability of $1/3$. This will then give us the 4 possible states $|00>$, $|01>$, $|10>$ and $|11>$.

```python
qr2 = QuantumRegister(2)
cr2 = ClassicalRegister(2)
qc2 = QuantumCircuit(qr2, cr2)
```

```python
qc2.measure(0,0)
qc2.measure(1,1)
```

    <qiskit.circuit.instructionset.InstructionSet at 0x1df60c83788>

```python
print(qc2)
```

          ┌─┐
    q1_0: ┤M├───
          └╥┘┌─┐
    q1_1: ─╫─┤M├
           ║ └╥┘
    c1_0: ═╩══╬═
              ║
    c1_1: ════╩═

```python
counts2 = execute(qc2, Aer.get_backend('qasm_simulator'),
                  noise_model=noise_model,
                  shots=1000000).result().get_counts()
```

```python
plot_histogram(counts2)
```

```python
print(counts2)
```

    {'01': 222551, '11': 443909, '00': 110806, '10': 222734}

|2-qubit state | Probability of the 2-qubit state  | Qiskit result |
|--------------|-----------------------------------|---------------|
|    $\vert 00>$ | $\frac{1}{3}\cdot \frac{1}{3} = \frac{1}{9}=0.111$ | 0.111|
|    $\vert 01>$ | $\frac{1}{3}\cdot \frac{2}{3} = \frac{2}{9}=0.222$ | 0.223|
|    $\vert 10>$ | $\frac{2}{3}\cdot \frac{1}{3} = \frac{2}{9}=0.222$ | 0.223|
|    $\vert 11>$ | $\frac{2}{3}\cdot \frac{2}{3} = \frac{4}{9}=0.444$ | 0.444|

From the results above you can see that the deporalizing error channel $\varepsilon(\rho_{single-qubit})$ works on the qubits like how it was expected with the small error due to statistical fluctuations involved in making measurements.

The same procedure applies to a 3 qubit system. With
8 possible states $|000>$, $|001>$, $|010>$, $|011>$, $|100>$, $|101>$, $|110>$ and $|111>$.

```python
qr3 = QuantumRegister(3)
cr3 = ClassicalRegister(3)
qc3 = QuantumCircuit(qr3, cr3)
```

```python
qc3.measure(0,0)
qc3.measure(1,1)
qc3.measure(2,2)
```

    <qiskit.circuit.instructionset.InstructionSet at 0x1df61ca0908>

```python
print(qc3)
```

          ┌─┐
    q2_0: ┤M├──────
          └╥┘┌─┐
    q2_1: ─╫─┤M├───
           ║ └╥┘┌─┐
    q2_2: ─╫──╫─┤M├
           ║  ║ └╥┘
    c2_0: ═╩══╬══╬═
              ║  ║
    c2_1: ════╩══╬═
                 ║
    c2_2: ═══════╩═

```python
counts3 = execute(qc3, Aer.get_backend('qasm_simulator'),
                  noise_model=noise_model,
                  shots=1000000).result().get_counts()
```

```python
plot_histogram(counts3)
```

```python
print(counts3)
```

    {'011': 148411, '000': 36983, '111': 295825, '100': 74554, '101': 148018, '110': 148366, '001': 73579, '010': 74264}

|3-qubit state | Probability of the 3-qubit state  | Qiskit result |
|--------------|-----------------------------------|---------------|
|    $\vert 000>$ | $\frac{1}{3}\cdot \frac{1}{3}\cdot \frac{1}{3} = \frac{1}{27}=0.037$ | 0.037|
|    $\vert 001>$ | $\frac{1}{3}\cdot\frac{1}{3} \cdot \frac{2}{3} = \frac{2}{27}=0.074$ | 0.074|
|    $\vert 010>$ |  $\frac{1}{3}\cdot\frac{2}{3} \cdot \frac{1}{3} = \frac{2}{27}=0.074$ | 0.074|
|    $\vert 011>$ | $\frac{1}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{4}{27}=0.148$ | 0.148|
|    $\vert 100>$ | $\frac{2}{3}\cdot\frac{1}{3} \cdot \frac{1}{3} = \frac{2}{27}=0.074$ | 0.075|
|    $\vert 101>$ | $\frac{2}{3}\cdot \frac{1}{3}\cdot \frac{2}{3} = \frac{4}{27}=0.148$ | 0.148|
|    $\vert 110>$ | $\frac{2}{3}\cdot \frac{2}{3}\cdot \frac{1}{3} = \frac{4}{27}=0.148$ | 0.148|
|    $\vert 111>$ | $\frac{2}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{4}{27}=0.296$ | 0.296|

From the results above you can see that the deporalizing error channel $\varepsilon(\rho_{single-qubit})$ works on the qubits like how it was expected with the small error due to statistical fluctuations involved in making measurements.

The procedure holds for a 4 qubit system. With
16 possible states $|0000>$, $|0001>$, $|0010>$, $|0011>$, $|0100>$, $|0101>$, $|0110>$, $|0111>$, $|1000>$, $|1001>$, $|1010>$, $|1011>$, $|1100>$, $|1101>$, $|1110>$ and $|1111>$

```python
qr4 = QuantumRegister(4)
cr4 = ClassicalRegister(4)
qc4 = QuantumCircuit(qr4, cr4)
```

```python
qc4.measure(0,0)
qc4.measure(1,1)
qc4.measure(2,2)
qc4.measure(3,3)
```

    <qiskit.circuit.instructionset.InstructionSet at 0x1df61d636c8>

```python
print(qc4)
```

          ┌─┐
    q3_0: ┤M├─────────
          └╥┘┌─┐
    q3_1: ─╫─┤M├──────
           ║ └╥┘┌─┐
    q3_2: ─╫──╫─┤M├───
           ║  ║ └╥┘┌─┐
    q3_3: ─╫──╫──╫─┤M├
           ║  ║  ║ └╥┘
    c3_0: ═╩══╬══╬══╬═
              ║  ║  ║
    c3_1: ════╩══╬══╬═
                 ║  ║
    c3_2: ═══════╩══╬═
                    ║
    c3_3: ══════════╩═

```python
counts4 = execute(qc4, Aer.get_backend('qasm_simulator'),
                  noise_model=noise_model,
                  shots=1000000).result().get_counts()
```

```python
plot_histogram(counts4)
```

```python
print(counts4)
```

    {'1110': 98904, '0011': 49256, '1101': 98781, '1100': 49420, '0100': 24619, '1000': 24672, '0101': 49765, '1011': 98487, '0110': 49569, '0000': 12271, '0001': 24395, '1111': 197279, '1001': 49583, '0111': 98822, '1010': 49413, '0010': 24764}

|4-qubit state | Probability of the 4-qubit state  | Qiskit result |
|--------------|-----------------------------------|---------------|
|    $\vert 0000>$ | $\frac{1}{3}\cdot \frac{1}{3}\cdot \frac{1}{3} \cdot \frac{1}{3} = \frac{1}{81}=0.012$ | 0.012|
|    $\vert 0001>$ | $\frac{1}{3}\cdot\frac{1}{3}\cdot \frac{1}{3} \cdot \frac{2}{3} = \frac{2}{81}=0.025$ | 0.024|
|    $\vert 0010>$ | $\frac{1}{3}\cdot\frac{1}{3}\cdot \frac{2}{3} \cdot \frac{1}{3} = \frac{2}{81}=0.025$ | 0.025|
|    $\vert 0011>$ | $\frac{1}{3}\cdot \frac{1}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{4}{81}=0.049$ | 0.049|
|    $\vert 0100>$ | $\frac{1}{3}\cdot\frac{2}{3} \cdot \frac{1}{3}\cdot \frac{1}{3} = \frac{2}{81}=0.025$ | 0.025|
|    $\vert 0101>$ |  $\frac{1}{3} \cdot \frac{2}{3}\cdot \frac{1}{3}\cdot \frac{2}{3} = \frac{4}{81}=0.049$ | 0.05|
|    $\vert 0110>$ | $ \frac{1}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} \cdot \frac{1}{3} = \frac{4}{81}=0.049$ | 0.05|
|    $\vert 0111>$ | $\frac{1}{3}\cdot \frac{2}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{8}{81}=0.099$ | 0.099|
|    $\vert 1000>$ | $\frac{2}{3}\cdot\frac{1}{3}\cdot \frac{1}{3} \cdot \frac{1}{3} = \frac{2}{81}=0.025$ | 0.025|
|    $\vert 1001>$ | $\frac{2}{3}\cdot \frac{1}{3}\cdot \frac{1}{3}\cdot \frac{2}{3} = \frac{4}{81}=0.049$ | 0.050|
|    $\vert 1010>$ | $\frac{2}{3}\cdot \frac{1}{3}\cdot \frac{2}{3}\cdot \frac{1}{3} = \frac{4}{81}=0.049$ | 0.049|
|    $\vert 1011>$ | $\frac{2}{3}\cdot \frac{1}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{8}{81}=0.099$ | 0.098|
|    $\vert 1100>$ | $ \frac{2}{3}\cdot \frac{2}{3}\cdot \frac{1}{3} \cdot \frac{1}{3} = \frac{4}{81}=0.049$ | 0.049|
|    $\vert 1101>$ | $\frac{2}{3}\cdot \frac{2}{3}\cdot \frac{1}{3}\cdot \frac{2}{3} = \frac{8}{81}=0.099$ | 0.099|
|    $\vert 1110>$ | $\frac{2}{3}\cdot \frac{2}{3}\cdot \frac{2}{3}\cdot \frac{1}{3} = \frac{8}{81}=0.099$ | 0.099|
|    $\vert 1111>$ | $\frac{2}{3}\cdot \frac{2}{3}\cdot \frac{2}{3}\cdot \frac{2}{3} = \frac{16}{81}=0.198$ | 0.197|

From the results above you can see that the deporalizing error channel $\varepsilon(\rho_{single-qubit})$ works on the qubits like how it was expected with the small error due to statistical fluctuations involved in making measurements.

To conclude the results show that the built in function $\texttt{depolarizing\_error}$ works the way we want it to as the majority of the results were acurate.
When
$$\lambda=\frac{4^{n}P}{4^{n}-1},$$
and $n=1$, $$\lambda = \frac{4P}{3}.$$
The depolarizing channel becomes
$$\varepsilon(\rho_{single-qubit}) =
    (1-P)\rho + \frac{P}{3}\left(X\rho X + Y\rho Y + Z\rho Z\right)$$


# A simple model for halogen bonds

What follows is a walkthrough of a simplified example of the analysis carried out to fit a statistical model to the interaction energies of halogen-bonded systems. It can be used both as an explanation of some of the analysis performed in the associated paper, and as a template for applying the same techniques to other types of interaction, such as chalcogen bonds.

These models take some functional form

\begin{equation}
E_{ij} = f(X_i, B_j)
\end{equation}

where $E_{ij}$ is the interaction energy between the halogen-containing donor $i$ and the Lewis base $j$, each with a single parameter $X_i$ and $B_j$, respectively. In this procedure, it is important to split the data into fitting and validation sets, so that the quality of the fit can be assessed later. We will start by looking at the fitting data.

## Examining the data

```python
import pandas as pd
```

```python
with open("./datasets/fitdata.csv") as f:
    fit_data = pd.read_csv(f)
```

Let's take a look at the form of this data:

```python
print(fit_data.head(n=5))
```

        AX     B      ECBS       R
    0  Br2   H2O -3.992106  2.7918
    1  Br2  CH2O -4.441659  2.7498
    2  Br2   HCN -3.656430  2.8888
    3  Br2  CH2S -6.419127  2.9106
    4  Br2   H3N -7.810330  2.5978

The first two columns, X and B, give the labels for the donors and Lewis bases, respectively. We then have the CCSD(T)-F12b [1] interaction energies (kcal/mol) extrapolated to the complete basis set limit in ECBS. This is followed finally by the intermolecular separation, $R_{ij}$, in angstroms from the optimized geometry at the CCSD(T)-F12b/VTZ-F12 [2] level; this is the distance between the halogen atom on the donor and the interacting atom on the base.

The CBS limit energies were determined using the approach of Hill *et al.* [3], taking the triple- and quadruple-zeta correlation energies and extrapolating as follows:

\begin{equation}
E_{\text{CBS}} \approx (E_{\text{QZ}} - E_{\text{TZ}})F + E_{\text{TZ}}
\end{equation}

where $F=1.363388$ for the CCSD part of the correlation energy, and $1.769474$ for the (T) part.

## Fitting a model

We now need to define a model to fit to; we start with a linear model:

\begin{equation}
f(X_i, B_j) = X_i + B_j + c
\end{equation}

where $c$ is a constant to be determined.

```python
import numpy as np
```

```python
# Make a matrix Eij = Xi + Bj + c (params[0])
# assumes X, B are numpy arrays
def f_linear(X, B, params):
    E = [[b for b in B] + x + params[0] for x in X]
    return np.matrix(E)
```

The above can be replaced by any functional form with the same argument list, returning a numpy matrix with shape $(nx, nb)$.

Then we choose a least squares loss function

\begin{equation}
\epsilon(f, X, B) = \sum_{i, j} (E_{ij}^{\text{CBS}} - f(X_i, B_j))^2
\end{equation}

For simplicity, we do not include regularisation here, although in practise it is often better to do so.

```python
# We concatenate X and B and into a single array params,
# followed by any extra parameters. nx and nb are the number
# of donors and Lewis bases, respectively.
def loss(params, f, E, nx, nb):
    X = np.array(params[:nx])
    B = np.array(params[nx:nx+nb])
    npoints = nx * nb

    # Difference between predicted and actual
    delta = (E - f(X, B, params[nx+nb:]))
    return np.sum(np.square(delta))/npoints
```

Before we can optimize the parameters by minimizing this loss, we need to reshape the interaction energies into a matrix, with the donors defining the rows, and the Lewis bases the columns.

```python
ECBS = fit_data.pivot(index="AX", columns="B", values="ECBS").as_matrix()
print(ECBS)
```

    [[ -4.441659  -6.419127  -3.992106  -4.03399   -7.81033   -3.65643 ]
     [ -6.181753  -8.712724  -5.666235  -5.675712 -11.052197  -5.350233]
     [ -5.349523  -7.760402  -4.88784   -4.915292  -9.796414  -4.560241]
     [ -7.186337 -10.144137  -6.643573  -6.656585 -12.942191  -6.409411]
     [ -8.672808 -14.343649  -8.054001  -9.031993 -16.674148  -7.795704]
     [ -5.850389  -9.781415  -5.364156  -5.368393 -11.651009  -4.818638]
     [-10.12494  -14.712791  -9.448778  -9.958498 -17.96776   -9.552144]
     [ -2.109841  -2.186941  -1.686314  -1.38504   -2.262964  -1.435816]
     [ -2.901197  -2.978537  -2.543681  -1.995972  -3.595643  -2.246903]
     [ -4.758603  -6.463099  -4.342814  -4.203034  -8.177679  -4.023849]]

Then we choose a starting guess for the parameters, and minimize the loss function.

```python
X = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
B = [-2, -2, -2, -2, -2, -2]
c = [0.0]
params = X + B + c
```

```python
from scipy.optimize import minimize
result = minimize(loss, params, args=(f_linear, ECBS, len(X), len(B)))
```

We can then inspect the resulting parameters, the value of the loss, and construct the predicted energy matrix:

The loss value here is just the averaged sum of square differences, whereas really we would like to know the root-mean-square deviation. The former is chosen as it is an easier loss function to use than the RMSD, and it can easily be shown that both must give the same results, and the RMSD can be calculated from it:

```python
linear_sse = result.fun * ECBS.size
linear_rmsd = np.sqrt(result.fun)
print("RMSD for model:", linear_rmsd, "kcal/mol")
```

    RMSD for model: 1.01412371373 kcal/mol

So we see that the linear model is not a particularly good one, essentially just revealing the relative strengths of the donors and acceptors, as shown from the parameters. This does give some interesting information, though, particularly the fact that the parameters for all the donors are positive, except for ClBr, ClI, and HI.

Note that as with most optimizations, the choice of starting guess can have a considerable effect on the results obtained, and so ideally multiple starting guesses should be tried. Additionally, the choice of algorithm (BFGS is the default here) can make a large difference. In the analysis in the paper, we have made use of the fact that the 1st and 2nd derivatives of the loss function are easily calculable, allowing us to use algorithms that make use of the Hessian, and to use a special type of line-search. All of these help to ensure that we are as close as possible to an optimal solution.

## Principal components analysis

For the product models, $Pn$, described in the paper, we can use an even simpler approach. These have the form

\begin{equation}
E_{ij}^{Pn} = k\frac{X_i B_j}{R_{ij}^n}
\end{equation}

where $k$ is a constant prefactor.

By rewriting this as

\begin{equation}
E_{ij}^{Pn} R_{ij}^n = k X_iB_j
\end{equation}

the parameters can be understood as arising from the principal component of a principal components analysis [4]. In this special case, this can be enacted by first forming the matrix of values $M_{ij}^n = E_{ij}^{Pn} R_{ij}^n$:

```python
n = 1
fit_data['Rnij'] = fit_data['R']**n
fit_data['Mnij'] = fit_data['ECBS'] * fit_data['Rnij']
Mn = fit_data.pivot(index="AX", columns="B", values="Mnij").as_matrix()
```

We then perform a singular value decomposition on the matrix $M^n$, to find left and right pseudo-eigenvectors, $u$ and $v$, along with a diagonal matrix $\Lambda$:

\begin{equation}
M^n_{ij} = \sum_{kl} U_{ik}\Lambda_{kl}V_{lj}
\end{equation}

but as $\Lambda$ is diagonal, this reduces to

\begin{equation}
M^n_{ij} = \sum_{k} U_{ik} \lambda_k V_{kj}
\end{equation}

The $\{\lambda_k\}$ are called the singular values; these are the square root of the eigenvalues (i.e. principal components) of the matrix. If only the first of these is significant, we can neglect all terms not involving $\lambda_1$, leaving

\begin{equation}
M^n_{ij} \approx U_{i1} \lambda_1 V_{1j}
\end{equation}

We then clearly have a correspondence to the $Pn$ model where $X_i = U_{i1}$, $B_j = V_{1j}$, and $k = \lambda_1$.

Let's look at the decomposition and singular values of $M^n$ more closely:

```python
u, lam, v = np.linalg.svd(Mn)
print(lam)
```

    [  1.57815317e+02   4.37224422e+00   2.30365897e+00   1.81594878e+00
       4.23030261e-01   1.32747165e-01]

The first component is orders of magnitude greater than any of the others. We can determine the percentage variance, $\sigma^2$, explained by each component $\lambda_i$ as follows:

\begin{equation}
\sigma^2(\lambda_i) = \frac{\lambda_i^2}{\sum_{j} \lambda_j^2}
\end{equation}

For the first two components above, this gives:

```python
s2_1 = lam[0]**2 / np.sum(lam**2)
s2_2 = lam[1]**2 / np.sum(lam**2)
print("% variance explained by 1st component:", s2_1 * 100)
print("% variance explained by 2nd component:", s2_2 * 100)
```

    % variance explained by 1st component: 99.8880321351
    % variance explained by 2nd component: 0.0766697376098

We can then extract the parameters and reconstruct the predicted, distance-weighted energies by a matrix-matrix multiplication.

```python
X = u[:, 0]
B = v[0, :]
print("k:", lam[0])
print("Donor parameters:", X)
print("Acceptor parameters:", B)

lam1 = np.zeros(shape=(len(X), len(B)))
lam1[0, 0] = lam[0]
Mnpred = np.dot(np.dot(u, lam1), v)
```

    k: 157.815317279
    Donor parameters: [-0.22875345 -0.32593938 -0.27338509 -0.37403785 -0.43610008 -0.29178583
     -0.50787669 -0.09511245 -0.14136147 -0.25397094]
    Acceptor parameters: [ 0.3295379   0.51412497  0.30764001  0.35252088  0.56392943  0.30025079]

The predicted energies are then recovered by dividing through by the distance powers from the original model.

```python
Rnij = fit_data.pivot(index="AX", columns="B", values="Rnij").as_matrix()
Enpred = Mnpred / Rnij
print(Enpred)
```

    [[ -4.32634425  -6.37680277  -3.97809659  -4.08692803  -7.8367475
       -3.75217852]
     [ -6.08211165  -8.87855931  -5.5976146   -5.73540254 -10.95947161
       -5.31483124]
     [ -5.30392455  -7.81234469  -4.87796022  -5.00371292  -9.67525798
       -4.61937974]
     [ -7.12954864 -10.39251856  -6.5579618   -6.70887588 -12.81101249
       -6.27512904]
     [ -9.17211017 -13.78301777  -8.4360476   -8.80384658 -16.66586595
       -8.15125319]
     [ -6.06767616  -9.30896793  -5.56938639  -5.76598503 -11.30074253
       -5.24488084]
     [-10.21530796 -14.92970802  -9.39765549  -9.66438717 -18.14506217
       -9.18418393]
     [ -1.59187413  -2.16669561  -1.48146895  -1.44982043  -2.66932441
       -1.388467  ]
     [ -2.33016251  -3.18981468  -2.16393698  -2.11961028  -3.94750688
       -2.0249392 ]
     [ -4.55041872  -6.62670066  -4.20429857  -4.27885728  -8.19616925
       -3.96409617]]

## Assessing the fit

A simple assessment of the fitting can be found by calculating the errors and root-mean-square deviation.

```python
errs = Enpred - ECBS
Pn_sse = np.sum(errs**2)
Pn_rmsd = np.sqrt(Pn_sse/errs.size)
print("RMSD for Pn model:", Pn_rmsd, "kcal/mol")
```

    RMSD for Pn model: 0.240946854194 kcal/mol

This seems like a considerable improvement on the linear model from earlier. Even more useful is to look at the distribution of the errors; here we choose a violin plot.

```python
errs_ravel = np.ravel(errs)
```

```python
import matplotlib.pyplot as plt
plt.violinplot(errs_ravel, [0], points=200, vert=False, widths=1.1,
                      showmeans=True, showextrema=True, showmedians=True,
                      bw_method=0.5)
plt.scatter(errs_ravel, np.zeros_like(errs_ravel))
plt.xlabel("Error (kcal/mol)")
plt.ylabel("Density")
plt.show()
```

For some information about the *efficiency* of the model, we can compute the Akaike Information Criterion [5], which is defined in terms of the log-likelihood of the loss function. For least squares, this means we can consider:

\begin{equation}
\text{AIC} = 2p + N\ln \epsilon(Pn, X, B)
\end{equation}

where $p$ is the number of parameters and $N$ is the number of samples. This is only meaningful when comparing models - a smaller AIC indicates that there is a better balance between the loss and the number of parameters, i.e. greater efficiency. The AIC for the two models used so far are:

```python
p = len(X) + len(B) + 1
N = ECBS.size

Pn_AIC = 2 * p + N * np.log(Pn_sse)
linear_AIC = 2 * p + N * np.log(linear_sse)

print("Pn AIC:", Pn_AIC)
print("Linear AIC:", linear_AIC)
```

    Pn AIC: 108.879206714
    Linear AIC: 281.343662138

The $Pn$ model thus appears to be more efficient than the linear model.

## Finding new parameters

Considerably more validation needs to be carried out than the quick approach described above. At the beginning, we separated out a validation set of molecules - bigger than the fitting set in this case -  such that we could try and predict interaction energies not present in the fitting set.

The principle of doing so is simple. We take a new molecule, and calculate its interaction energy with one of the existing molecules. The simple form of the $Pn$ models then allows us to determine the new parameter by dividing out the known quantities.

For example, suppose we wish to determine a parameter for some halogen donor $k$, and have calculated its interaction with water, giving us an interaction energy $E_{k1}$ and separation $R_{k1}$. Then, as we know the water parameter, we find that

\begin{equation}
X_k = \frac{E_{k1} R_{k1}}{k B_1}
\end{equation}

```python
with open("./datasets/bigdft.csv") as f:
    big_data = pd.read_csv(f)

print(big_data.head(5))
```

        AX    B  Eint      R
    0  Br2  Gly -5.08  2.752
    1   I2  Gly -5.78  2.869
    2  BrI  Gly -7.47  2.771
    3  Br2  Val -5.81  2.751
    4   I2  Val -6.62  2.879

The most interesting case is to take two new molecules, neither in the fitting or validation sets, calculated at a different level of theory, and see if the model still holds up. We do this here with some of the set of larger molecules used in the paper: glycine, valine, and leucine as the new Lewis bases; hexafluorobenzene with one fluorine substituted for Cl, Br, or I for the new donors. These were optimized at the M06-2X/aVTZ level of theory [6, 7], a considerably cheaper calculation than the benchmark CCSD(T)-F12b calculations used earlier.

We will choose for simplicity's sake to calculate the parameters relative to BrI for the bases and H2O for the donors; ideally, we would try and determine which system is the best reference. For valine and C6F5Br in the $Pn$ model, this gives:

```python
big_data['ERn'] = big_data['Eint'] * big_data['R'] ** n / lam[0]
Bvaline = big_data['ERn'][5] / X[2]
Xc6f5br = big_data['ERn'][12] / B[0]

print("Valine parameter:", Bvaline)
print("C6F5Br parameter:", Xc6f5br)
```

    Valine parameter: 0.536613425953
    C6F5Br parameter: -0.167461032687

The predicted interaction energy for the complex between valine and C6F5Br is thus

```python
E_valine_c6f5br = lam[0] * Bvaline * Xc6f5br / big_data['R'][22]**n
print("Predicted energy:", E_valine_c6f5br, "kcal/mol")
print("Actual energy:", big_data['Eint'][22], "kcal/mol")
```

    Predicted energy: -4.88851242624 kcal/mol
    Actual energy: -4.61 kcal/mol

This is within 0.3 kcal/mol despite this being an extrapolation to completely new molecules using a lower level of theory. Of course, unless $n = 0$, we need to know the separation, meaning that a calculation cannot be avoided. However, estimates of this separation can be readily obtained by much cheaper methods, and the resulting interaction is insensitive. For example, using the HF/VDZ [8] separation of $\sim 3.0$ angstrom, the predicted energy is

```python
lam[0] * Bvaline * Xc6f5br / 3.0**n
```

    -4.7271915161707323

which is within 0.2 kcal/mol of the original prediction. In fact, in this case it serendipitously improves the prediction, but more generally it demonstrates that the predictions are not especially sensitive to small errors in the separation.

### References

1. S. Ten-no, Chem. Phys. Lett., 2004, 398, 56–61; C. Hättig, D. P. Tew and A. Köhn, J. Chem. Phys., 2010, 132, 231102
2. K. A. Peterson, T. B. Adler and H.-J. Werner, J. Chem. Phys., 2008, 128, 084102; J. G. Hill and K. A. Peterson, J. Chem. Phys., 2014, 141, 094106
3. J. G. Hill, K. A. Peterson, G. Knizia and H.-J. Werner, J. Chem. Phys., 2009, 131, 194105
4. Karl Pearson F.R.S., Philosophical Magazine, Vol. 2, Iss. 11, 1901
5. H. Akaike, IEEE Transactions on Automatic Control, 19 (6), 1974
6. Y. Zhao and D. G. Truhlar, Theor. Chem. Acc., 2008, 120, 215–241
7. T. H. Dunning, J. Chem. Phys., 1989, 90, 1007–1023; R. A. Kendall, T. H. Dunning and R. J. Harrison, J. Chem. Phys., 1992, 96, 6796–6806; T. H. Dunning, K. A. Peterson and A. K. Wilson, J. Chem. Phys., 2001, 114, 9244–9253
8. T. H. Dunning, Jr., J. Chem. Phys. 1989, 90, 1007-1023;  A.K. Wilson, D.E. Woon, K.A. Peterson, and T.H. Dunning, Jr., J. Chem. Phys. 1999, 110, 7667-7676

```python

```


## Model Predictive Control (MPC) of a Quadcopter

The MPC formulation can be written as following
$$\begin{equation}
    \begin{aligned}
    \min_{u_k} \quad &  \sum_{k=0}^{T-1} J \left( x_k, u_k \right)\\
    \text{subject to} \quad & \dot{x}(t) = f(t, x(t), {u(t)})\\
    &x(0) = x_0\\
    & t \in \mathcal{T}
    \end{aligned}
\end{equation}$$
where $J$ is a cost function and $T \in \mathcal{T}$ is the _receding horizon_ over which predicted future trajectories are optimized

```python
%load_ext autoreload
%autoreload 2

import sys; sys.path.append(2*'../') # go n dirs back
import matplotlib.pyplot as plt

import torch
from torchcontrol.systems import Quadcopter
from torchcontrol.cost import IntegralCost
from torchcontrol.controllers import *
from torchcontrol.mpc import TorchMPC
from torchcontrol.plotting import *

```

The quadcopter model is a suitably modified version of the explicit dynamics update of [https://arxiv.org/abs/2103.02142](https://arxiv.org/abs/2103.02142) for batched training in PyTorch. The following accelerations update describes the dynamic model
$$\begin{equation}
\begin{aligned}
    \mathbf{\ddot{x}} &= \left( \mathbf{R} \cdot [0,0,k_F{\textstyle\sum}_{i=0}^3 \omega_{i}^2] - [0,0,m g] \right) m^{-1} \\
    \boldsymbol{\ddot{\psi}} &= \mathbf{J}^{-1} \left(
    \tau ( l, k_F, k_T, [\omega_{0}^2, \omega_{1}^2, \omega_{2}^2, \omega_{3}^2])
    - \boldsymbol{\dot{\psi}} \times \left( \mathbf{J} \boldsymbol{\dot{\psi}} \right) \right)
\end{aligned}
\end{equation}$$

where $\mathbf{\boldsymbol{x}} = [x, y, z]$ corresponds to the drone positions and $\boldsymbol{\psi} = [\phi,\theta, \psi]$ to its angular positions; $\boldsymbol{R}$ and $\boldsymbol{J}$ are its rotation and inertial matrices respectively, $\tau(\cdot)$ is a function calculating the torques induced by the motor speeds $\omega_i$, while arm length $l$, mass $m$, gravity acceleration constant $g$ along with $k_F$ and $k_T$ are scalar variables describing the quadcopter's physical properties.

$\Longrightarrow$ The model has 12 variables in total!

```python
?? Quadcopter
```

    Init signature:  Quadcopter(*args, **kwargs)
    Source:
    class Quadcopter(ControlledSystemTemplate):
        '''
        Quadcopter state space model compatible with batch inputs
        Appropriately modified version to run efficiently in Pytorch
        References and kudos to:
        Learning to Fly—a Gym Environment with PyBullet Physics for
    Reinforcement Learning of Multi-agent Quadcopter Control
        https://arxiv.org/pdf/2103.02142
        '''
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # Parameters
            self.G = 9.81
            self.RAD2DEG = 180/np.pi
            self.DEG2RAD = np.pi/180
            self.M = 0.027
            self.L = 0.0397
            self.THRUST2WEIGHT_RATIO = 2.25
            self.J = torch.diag(torch.Tensor([1.4e-5, 1.4e-5, 2.17e-5]))
            self.J_INV = torch.linalg.inv(self.J)
            self.KF = 3.16e-10
            self.KM = 7.94e-12
            self.GRAVITY = self.G*self.M
            self.HOVER_RPM = np.sqrt(self.GRAVITY / (4*self.KF))
            self.MAX_RPM = np.sqrt((self.THRUST2WEIGHT_RATIO*self.GRAVITY) / (4*self.KF))
            self.MAX_THRUST = (4*self.KF*self.MAX_RPM**2)
            # DroneModel.CF2X:
            self.MAX_XY_TORQUE = (2*self.L*self.KF*self.MAX_RPM**2)/sqrt2
            self.MAX_Z_TORQUE = (2*self.KM*self.MAX_RPM**2)

        def dynamics(self, t, x):
            self.nfe += 1 # increment number of function evaluations

            # Control input evaluation
            rpm = self._evaluate_controller(t, x)

            pos       = x[...,  0:3]
            rpy       = x[...,  3:6]
            vel       = x[...,  6:9]
            rpy_rates = x[..., 9:12]

            # Compute forces and torques
            forces = rpm**2 * self.KF
            thrust_z = torch.sum(forces, dim=-1)
            thrust = torch.zeros(pos.shape)
            thrust[..., 2] = thrust_z

            rotation = euler_matrix(rpy[...,0], rpy[...,1], rpy[...,2])
            thrust_world_frame =  einsum('...ij, ...j-> ...i', rotation, thrust)
            force_world_frame = thrust_world_frame - torch.Tensor([0, 0, self.GRAVITY])
            z_torques = rpm**2 *self.KM
            z_torque = (-z_torques[...,0] + z_torques[...,1] - z_torques[...,2] + z_torques[...,3])

            # DroneModel.CF2X:
            x_torque = (forces[...,0] + forces[...,1] - forces[...,2] - forces[...,3]) * (self.L/sqrt2)
            y_torque = (- forces[...,0] + forces[...,1] + forces[...,2] - forces[...,3]) * (self.L/sqrt2)

            torques = torch.cat([x_torque[...,None], y_torque[...,None], z_torque[...,None]], -1)
            torques = torques - cross(rpy_rates, einsum('ij,...i->...j',self.J, rpy_rates))
            rpy_rates_deriv = einsum('ij,...i->...j', self.J_INV, torques)
            no_pybullet_dyn_accs = force_world_frame / self.M
            self.cur_f = torch.cat([vel,
                                    rpy_rates,
                                    no_pybullet_dyn_accs,
                                    rpy_rates_deriv], -1)

            return self.cur_f
    File:           ~/Desktop/torchcontrol/torchcontrol/systems/quadcopter.py
    Type:           type
    Subclasses:

```python
# Change device according to your configuration
device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu') # feel free to change :)
device = torch.device('cpu') # override
```

## Cost function
We want the drone to reach position $[x, y, z]$ = $[3, 3, 3]~m$ while the other variables should be 0 (meaning: no rotation and velocities)

Note: finding good values for the cost function is hard - many times, it requires patience and lots of trials and error

```python
# Declaring the cost function
x_star = torch.Tensor([3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to(device)
u_star = 0.

P = torch.Tensor([0.1, 0.1, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to(device)
Q = torch.Tensor([1, 1, 1, 0.05, 0.05, 0.05, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]).to(device)
R = 0.
cost_function = IntegralCost(x_star=x_star, u_star=u_star, P=P, Q=Q, R=R)
```

## Initial Conditions
We start with all the variables at 0

```python
x0, y0, z0 = 0., 0., 0.
phi0, theta0, psi0 = 0., 0., 0.

init = torch.tensor([x0,
                    y0,
                    z0,
                    phi0,
                    theta0,
                    psi0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0], dtype=torch.float).to(device)
print(init)
```

    tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

## Controller
We use the neural box-constrained controller with output scaling

```python
# Instantiate drone
drone = Quadcopter(None, solver='euler') # dummy controller
scaling = torch.Tensor([0, drone.MAX_RPM])
controller = BoxConstrainedController(12, 4, output_scaling=scaling, constrained=True)
drone.u = controller # controller for the drone

```

## MPC initialization

```python
# MPC simulation variables
dt = 0.02 # time stef
tf = 2 # final time
t_span = torch.linspace(0, tf, int(tf/dt)+1)
lr = 1e-2 # learning rate
weight_decay = 1e-4 # weight decay for adam
steps_nom = 5 # nominal steps to do between each MPC step
max_iters = 20 # maximum SGD iteration / sampling step
eps_accept = 1e-3 # stop subproblem solution when this loss is reached
lookahead_steps = 25 # receding horizon
x0 = init # initial state

# Optimizer
opt = torch.optim.Adam(controller.parameters(), lr=lr, weight_decay=weight_decay)

# Instantiate MPC
mpc = TorchMPC(drone, cost_function, t_span, opt, eps_accept=eps_accept, max_g_iters=max_iters,
            lookahead_steps=lookahead_steps, lower_bounds=None,
            upper_bounds=None, penalties=None).to(device)
```

## MPC simulation
For simulating the MPC we need a system with an accurate solver, e.g. `dopri5` for carrying forward the solutions

```python
drone_nominal = Quadcopter(RandConstController((1,1),1,1), solver='dopri5', atol=1e-5, rtol=1e-5)
loss = mpc.forward_simulation(drone_nominal, x0, t_span, reset=True, reinit_zeros=False)
```

    Steps:   0%|          | 0/100 [00:00<?, ?it/s]

    Starting simulation...

    Steps: 100%|██████████| 100/100 [01:14<00:00,  1.34it/s, cost=0.145, timestamp=2, converged=0]

    The simulation has ended!

```python
# Get variables
traj = mpc.trajectory_nominal
controls = mpc.control_inputs
print('Nominal cost:', loss)
```

    Nominal cost: tensor(1.6980)

## Plotting

```python
plot_quadcopter_trajectories(traj)
```

```python
plot_quadcopter_controls(controls)
```

```python
# Here we reduce the resolution of the trajectory, so the plotter 'skips'
# some points and take less time

import numpy as np
# MPC trajectory: plot every 0.02s
skip = 20 # we skip 20 frams at a time
t = []
for i in range(int(traj.shape[0]/skip)):
    t.append(traj[i*skip,:][None])

trajs_reduced = np.concatenate(t)
t_span = torch.linspace(0, 2, trajs_reduced.shape[0]+1)
```

```python
plot_quadcopter_trajectories_3d(traj, x_star, i=len(traj))
```

```python
animate_quadcopter_3d(trajs_reduced, x_star, t_span)
```


- - - -
# Mechpy Tutorials
a mechanical engineering toolbox

source code - https://github.com/nagordon/mechpy
documentation - https://nagordon.github.io/mechpy/web/

- - - -

Neal Gordon
2017-02-20

- - - -

## Solid Mechanics Tutorial

## Python Initilaization with module imports

```python
# setup
import numpy as np
import sympy as sp
import scipy
from pprint import pprint
sp.init_printing(use_latex='mathjax')

import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (12, 8)  # (width, height)
plt.rcParams['font.size'] = 14
plt.rcParams['legend.fontsize'] = 16
from matplotlib import patches

get_ipython().magic('matplotlib')  # seperate window
get_ipython().magic('matplotlib inline') # inline plotting
```

    Using matplotlib backend: Qt4Agg

## Functions

```python
def T1(th):
    '''Stress Transform for Plane Stress
    th=ply angle in degrees
    voight notation for stress tranform. sigma1 = T1 @ sigmax
    recall T1(th)**-1 == T1(-th)'''
    n = np.sin(th*np.pi/180)
    m = np.cos(th*np.pi/180)
    T1 = np.array( [[m**2, n**2, 2*m*n],
                   [n**2, m**2,-2*m*n],
                   [-m*n, m*n,(m**2-n**2)]])
    return T1

def T2(th):
    '''Strain Transform for Plane Stress
    th=ply angle in degrees
    voight notation for strain transform. epsilon1 = T2 @ epsilonx'''
    n = np.sin(th*np.pi/180)
    m = np.cos(th*np.pi/180)
    T2 = np.array( [[m**2, n**2, m*n],
                 [n**2, m**2,-m*n],
                 [-2*m*n, 2*m*n,  (m**2-n**2)]])
    return T2
```

# Materials
[index](#Mechpy)

## Stress and Strain
Stress is a tensor that can be broken into

$$
\overline{\sigma}=\begin{bmatrix}
 \sigma_{xx} & \sigma_{xy} & \sigma_{xz}\\
 \sigma_{yx} & \sigma_{yy} & \sigma_{yz}\\
 \sigma_{zx} & \sigma_{zy} & \sigma_{zz}
 \end{bmatrix}
$$

# Stress Transformations
[index](#Mechpy)

$$
\overline{\sigma}=\begin{bmatrix}
 \sigma_{xx} & \sigma_{xy} & \sigma_{xz}\\
 \sigma_{yx} & \sigma_{yy} & \sigma_{yz}\\
 \sigma_{zx} & \sigma_{zy} & \sigma_{zz}
 \end{bmatrix}
$$

reduce to plane stress

$$
\overline{\sigma}=\begin{bmatrix}
 \sigma_{xx} & \sigma_{xy} & 0 \\
 \sigma_{yx} & \sigma_{yy} & 0\\
 0 &           0 &           \sigma_{zz}
 \end{bmatrix}
$$

or

$$
\overline{\sigma}=\begin{bmatrix}
 \sigma_{xx} & \tau_{xy} & 0 \\
 \tau_{yx} & \sigma_{yy} & 0\\
 0 &           0 &           \sigma_{zz}
 \end{bmatrix}
$$

$$
\overline{\sigma}=\begin{bmatrix}
 \sigma_{x} & \sigma_{xy} \\
 \sigma_{yx} & \sigma_{y} \\
 \end{bmatrix}
$$

Transformation

$$
A=\begin{bmatrix}
 cos(\theta) & sin(\theta) \\
 -sin(\theta) & cos(\theta) \\
 \end{bmatrix}
$$

$$
\sigma'=A \sigma A^T
$$

$$
\sigma_1 , \sigma_2 = \frac{\sigma_{x}}{2} + \frac{\sigma_{y}}{2} + \sqrt{\tau_{xy}^{2} + \left(\frac{\sigma_{x}}{2} - \frac{\sigma_{y}}{2}\right)^{2}}
$$

$$
T=\left[\begin{matrix}\sin^{2}{\left (\theta \right )} & \cos^{2}{\left (\theta \right )} & 2 \sin{\left (\theta \right )} \cos{\left (\theta \right )}\cos^{2}{\left (\theta \right )} & \\
\sin^{2}{\left (\theta \right )} & - 2 \sin{\left (\theta \right )} \cos{\left (\theta \right )}\- \sin{\left (\theta \right )} \cos{\left (\theta \right )} & \\
\sin{\left (\theta \right )} \cos{\left (\theta \right )} & \sin^{2}{\left (\theta \right )} - \cos^{2}{\left (\theta \right )}\end{matrix}\right]
$$

```python
import sympy as sp
from sympy.abc import tau, sigma
import numpy as np
sp.init_printing()
```

```python
sx,sy,txy,tp = sp.symbols('sigma_x,sigma_y,tau_xy,theta_p')
sp1 = (sx+sy)/2 + sp.sqrt( ((sx-sy)/2)**2 + txy**2 )
sp2 = (sx+sy)/2 - sp.sqrt( ((sx-sy)/2)**2 + txy**2 )
print(sp.latex(sp1))
sp1
```

```python
tp = sp.atan(2*txy/(sx-sy) )/2
tp
```

```python
tpp = tp.evalf(subs={sx:10,sy:15,txy:10})
tpp
```

```python
#s,s11,s22,s33,s12 = sp.var('s,s11,s22,s33,s12')
s,s11,s22,s33,s12,s13,t,t12 = sp.symbols('sigma, sigma11,sigma22,sigma33,sigma12,sigma13,tau,tau12')
s = sp.Matrix([[s11,t12,0],[t12,s22,0],[0,0,s33]])
s
```

```python
t = sp.symbols('theta')
m = sp.sin(t)
n = sp.cos(t)
T = sp.Matrix([[m**2,n**2, 2*m*n],[n**2,m**2,-2*m*n],[-m*n,m*n,m**2-n**2]])
T
```

```python
T1 = T.subs(t, sp.pi/4)
T1
```

```python
sprime = T1 * s * T1.inv()
sprime
```

```python
sprime.evalf(subs={s11:10, s22:00, s33:0, t12:0})
```

```python
s.eigenvals()
```

```python
s2 = s.evalf(subs={s11:2.2, s22:3, s33:sp.pi, s12:7.3})
s2
```

## Stress transformation with equations

```python
%matplotlib inline
```

```python
cd ..
```

    U:\Engineering Department\Gordon_Neal\Documents\eng_toolbox\mechpy

```python
from mechpy.math import T2rot
import matplotlib.pyplot as plt
import numpy as np
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (10,8)
```

```python
from IPython.html.widgets import *
from mechpy.math import T2rot
#x = [-1,1, 0,-1,]
#y = [-1,-1,1,-1]
#xy = np.array([x,y])
#plt.xlim([-11.1,11.1])
#plt.ylim([-11.1,11.1])
#xyR = np.dot(T2rot(30),xy)
#plt.plot(xyR[0,:],xyR[1,:])
fig1 = plt.figure(figsize=(10,8))
def rot2(th, xt,yt,zt):
    xyR = np.dot(T2rot(th),xy*zt)
    xyR[0,:]+=xt
    xyR[1,:]+=yt
    plt.plot(xyR[0,:],xyR[1,:])
    plt.axis('square')
    plt.xlim([-11.1,11.1])
    plt.ylim([-11.1,11.1])
    plt.show()

interact(rot2, th=(0,np.pi,np.pi/90), yt=(1,10,1), xt=(1,10,1), zt=(1,10,1));
```

```python
# stress angle transformation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (10,8)
mpl.rcParams['font.size'] = 16
mpl.rcParams['legend.fontsize'] = 14

plt.figure(figsize=(10,8))
def mohr(sigmax, sigmay, tauxy, angle):
    plt.figure(figsize=(10,8))
    # angle rotates clockwise
    theta = (angle-90) * np.pi/180
    # stress transformed to any angle
    sigmaxt = (sigmax + sigmay)/2 + (sigmax-sigmay)/2 * np.cos(2*theta) + tauxy*np.sin(2*theta)
    sigmayt = (sigmax + sigmay)/2 + (sigmax-sigmay)/2 * np.cos(2*(theta + np.pi/2)) + tauxy*np.sin(2*(theta+ np.pi/2))
    tauxyt = -(sigmax-sigmay)/2*np.sin(2*theta) + tauxy*np.cos(2*theta)
    print('transformed stress')
    print([sigmaxt, sigmayt, tauxyt])

    # principal stresses
    sigma1p = (sigmaxt + sigmayt)/2 + np.sqrt( ((sigmaxt-sigmayt)/2)**2 + tauxyt**2)
    sigma2p = (sigmaxt + sigmayt)/2 - np.sqrt( ((sigmaxt-sigmayt)/2)**2 + tauxyt**2)
    tauxyp = np.sqrt( ( (sigmaxt-sigmayt)/2 )**2 + tauxyt**2  )
    sigmap = [sigma1p, sigma2p]
    thetap = -np.arctan(tauxyt/ ((sigmaxt-sigmayt)/2)) / 2 * 180 / np.pi
    sigmaavg = (sigma1p+sigma2p)/2
    R = np.sqrt(((sigmaxt-sigmayt)/2)**2 + tauxyt**2)

    print('---principal stresses---')
    print('sigma1p = {:.2f}'.format(sigma1p) )
    print('sigma2p = {:.2f}'.format(sigma2p) )
    print('principal plane angle = {:.2f}'.format(thetap) )
    print('---principal shear---')
    print('tauxyp = {:.2f} with avg normal stress = {:.2f}'.format(tauxyp,sigmaavg))

    r = np.linspace(-2*np.pi,2*np.pi,100)

    ## keep this for sigma3
    # x = np.cos(r) * (sigma1p/2) + sigma1p/2
    # y = np.sin(r) * (sigma1p/2)
    # plt.plot(x,y,'bo', sigmaavg,0,'bo')
    # x = np.cos(r) * (sigma2p/2) + sigma2p/2
    # y = np.sin(r) * (sigma2p/2)
    # plt.plot(x,y,'bo', sigmaavg,0,'bo')

    x = np.cos(r) * R + sigmaavg
    y = np.sin(r) * R
    plt.plot(x,y,'b', sigmaavg,0,'bo')

    plt.plot([sigmaxt,sigmayt],[tauxyt, -tauxyt], 'g-o', label='applied stress');
    plt.plot([sigma1p,sigma2p],[0,0],'ro');
    plt.plot([sigmaavg,sigmaavg],[tauxyp,-tauxyp], 'ro', label='principal stress');
    plt.plot([sigmaxt,sigmaxt],[tauxyt, 0], '--g'); plt.plot([sigmaxt,sigmaxt],[tauxyt, 0], 'og');
    plt.plot([sigmayt,sigmayt],[-tauxyt, 0], '--g'); plt.plot([sigmayt,sigmayt],[-tauxyt, 0], 'og');
    plt.plot([sigmaavg, sigmaxt],[tauxyt, tauxyt], '--g'); plt.plot([sigmaavg, sigmaxt],[tauxyt, tauxyt], 'og');
    plt.plot([sigmaavg, sigmayt],[-tauxyt, -tauxyt], '--g'); plt.plot([sigmaavg, sigmayt],[-tauxyt, -tauxyt], 'og');
    plt.axis('equal') ; plt.grid();
    plt.xlabel('$\sigma_x,\sigma_y$');
    plt.ylabel('$\\tau_{xy}$');
    plt.title('Mohrs Circle 2D Plane Stress ');
    plt.legend();

interact(mohr, sigmax=(0,500,10),sigmay=(0,500,10),tauxy=(0,500,10),angle=(0,90,5));
```

```python
# stress angle transformation
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (10,8)
mpl.rcParams['font.size'] = 16
mpl.rcParams['legend.fontsize'] = 14

sigmax = -20
sigmay = 0
tauxy = 40

angle = 0

# angle rotates clockwise
theta = (angle-90) * np.pi/180
# stress transformed to any angle
sigmaxt = (sigmax + sigmay)/2 + (sigmax-sigmay)/2 * np.cos(2*theta) + tauxy*np.sin(2*theta)
sigmayt = (sigmax + sigmay)/2 + (sigmax-sigmay)/2 * np.cos(2*(theta + np.pi/2)) + tauxy*np.sin(2*(theta+ np.pi/2))
tauxyt = -(sigmax-sigmay)/2*np.sin(2*theta) + tauxy*np.cos(2*theta)
print('transformed stress')
print([sigmaxt, sigmayt, tauxyt])

# principal stresses
sigma1p = (sigmaxt + sigmayt)/2 + np.sqrt( ((sigmaxt-sigmayt)/2)**2 + tauxyt**2)
sigma2p = (sigmaxt + sigmayt)/2 - np.sqrt( ((sigmaxt-sigmayt)/2)**2 + tauxyt**2)
tauxyp = np.sqrt( ( (sigmaxt-sigmayt)/2 )**2 + tauxyt**2  )
sigmap = [sigma1p, sigma2p]
thetap = -np.arctan(tauxyt/ ((sigmaxt-sigmayt)/2)) / 2 * 180 / np.pi
sigmaavg = (sigma1p+sigma2p)/2
R = np.sqrt(((sigmaxt-sigmayt)/2)**2 + tauxyt**2)

print('---principal stresses---')
print('sigma1p = {:.2f}'.format(sigma1p) )
print('sigma2p = {:.2f}'.format(sigma2p) )
print('principal plane angle = {:.2f}'.format(thetap) )
print('---principal shear---')
print('tauxyp = {:.2f} with avg normal stress = {:.2f}'.format(tauxyp,sigmaavg))

r = np.linspace(-2*np.pi,2*np.pi,100)

## keep this for sigma3
# x = np.cos(r) * (sigma1p/2) + sigma1p/2
# y = np.sin(r) * (sigma1p/2)
# plt.plot(x,y,'bo', sigmaavg,0,'bo')
# x = np.cos(r) * (sigma2p/2) + sigma2p/2
# y = np.sin(r) * (sigma2p/2)
# plt.plot(x,y,'bo', sigmaavg,0,'bo')

x = np.cos(r) * R + sigmaavg
y = np.sin(r) * R
plt.plot(x,y,'b', sigmaavg,0,'bo')

plt.plot([sigmaxt,sigmayt],[tauxyt, -tauxyt], 'g-o', label='applied stress');
plt.plot([sigma1p,sigma2p],[0,0],'ro');
plt.plot([sigmaavg,sigmaavg],[tauxyp,-tauxyp], 'ro', label='principal stress');
plt.plot([sigmaxt,sigmaxt],[tauxyt, 0], '--g'); plt.plot([sigmaxt,sigmaxt],[tauxyt, 0], 'og');
plt.plot([sigmayt,sigmayt],[-tauxyt, 0], '--g'); plt.plot([sigmayt,sigmayt],[-tauxyt, 0], 'og');
plt.plot([sigmaavg, sigmaxt],[tauxyt, tauxyt], '--g'); plt.plot([sigmaavg, sigmaxt],[tauxyt, tauxyt], 'og');
plt.plot([sigmaavg, sigmayt],[-tauxyt, -tauxyt], '--g'); plt.plot([sigmaavg, sigmayt],[-tauxyt, -tauxyt], 'og');
plt.axis('equal') ; plt.grid();
plt.xlabel('$\sigma_x,\sigma_y$');
plt.ylabel('$\\tau_{xy}$');
plt.title('Mohrs Circle 2D Plane Stress ');
plt.legend();
```

## Stress Transformation with matrices

```python
# Principal PLane Stress

sigmax = -20
sigmay = 90
tauxy = 60

sigma = np.array([[sigmax, tauxy, 0],
                  [tauxy,  sigmay,0],
                  [0,      0,     0]])

sigmap = np.linalg.eig(sigma)[0]
print('\n principal stresses')
print(sigmap)

thetap = np.linalg.eig(sigma)[1]  # principal cosine angle

print('\n principal plane angle')
print(np.arccos(thetap)*180/np.pi-90)

```

     principal stresses
    [ -46.39410298  116.39410298    0.        ]

     principal plane angle
    [[ 66.25522354  23.74477646   0.        ]
     [-23.74477646  66.25522354   0.        ]
     [  0.           0.         -90.        ]]

```python
# specified angle stress transformation

sigmax = -20
sigmay = 90
tauxy = 60

sigma = np.array([[sigmax, tauxy, 0],
                  [tauxy,  sigmay,0],
                  [0,      0,     0]])

ang = 23
sigmat = T1(ang) @ sigma @ np.transpose(T1(ang))

print('\n transformed stresses')
print(sigmat)

```

     transformed stresses
    [[  3.26191148  53.53187815  26.02803718]
     [ 53.53187815  79.67433223  13.53565184]
     [ 26.02803718  13.53565184  -6.46812185]]

```python

```

```python
# maximum in-plane shear stress
eps = 1e-16   # machine epsilon to avoid divide-by-zero error
rad_to_deg = 180/np.pi
theta1 = 0.5 * np.arctan( 2*tauxy / ((sigmax-sigmay+eps))) * rad_to_deg
print(theta1)
```

    -23.744776461

```python
tauxy = 0    # lbs/in
sigmax = 100   # lbs/in
sigmay = np.linspace(0,1.100)   # lbs/in

eps = 1e-16   # machine epsilon to avoid divide-by-zero error
rad_to_deg = 180/np.pi
theta1 = 0.5 * np.arctan( 2*tauxy / ((sigmax-sigmay+eps))) * rad_to_deg
print(theta1)

# sigmax = 100
# sigmay = np.linspace(0,1.100)
# tauxy = 0
# tparray = sp.atan(2*tauxy/(sigmax-sigmay) )/2
# tparray
```

    [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
      0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
      0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

```python
sigma
```

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

```python
th = np.pi/4  # 45 deg
m = np.cos(th)
n = np.sin(th)
A = np.array([ [m,n],[-n,m]])

tauxy = 1    # lbs/in
sigmax = 0   # lbs/in
sigmay = 0   # lbs/in

sigma = np.array([[sigmax, tauxy],
                  [tauxy,   sigmay]])

sigmat = A @ sigma @ A.T  # transformed stress
sigmat
```

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

```python
sigmap = np.linalg.eig(sigmat)[0]  # principal stresses
print(sigmap)

thetap = np.linalg.eig(sigmat)[1]  # principal planes
print(thetap* 180/np.pi)
```

    [ 1. -1.]
    [[ 57.29577951   0.        ]
     [  0.          57.29577951]]

```python

```

```python
# Principal Stresses

sx  = 63.66
sy  = 0
sz  = 0
txy = 63.66
txz = 0
tyz = 0

S = np.matrix([[sx, txy, txz],
            [txy, sy, tyz],
            [txy, txz, sz]])

print(S)
```

    [[ 63.66  63.66   0.  ]
     [ 63.66   0.     0.  ]
     [ 63.66   0.     0.  ]]

```python
principal_stresses = np.linalg.eigvals(S)
print(principal_stresses)
```

    [   0.          -39.34404372  103.00404372]

```python
import sympy as sp
from sympy.abc import tau, sigma
#s,s11,s22,s33,s12 = sp.var('s,s11,s22,s33,s12')
s,s11,s22,s33,s12,s13 = sp.symbols('sigma, sigma11,sigma22,sigma33,sigma12,sigma13')
s = sp.Matrix([[s11,s12,0],[s12,s22,0],[0,0,s33]])
s
```

```python
s**2
```

```python
s.eigenvals()  # hmm looks familiar
```

```python
s1 = s.subs(s11,2.2).subs(s22,3).subs(s33,sp.pi).subs(s12,7.3)
s1
```

```python
# or
s2 = s.evalf(subs={s11:2.2, s22:3, s33:sp.pi, s12:7.3})
s2
```

```python
s1.eigenvals()
```

```python
s2.eigenvals()
```

```python
s2.inv()
```

```python
C = sp.symbols('C1:100')
C
```

```python

```


## BSA - $\epsilon(\lambda)$ according to Pahn. et.al 2013

They use Lorentz oscillators to get the dielectric constant of Bovine Serum Albumina as a function of the wavelngth.

\begin{equation}
    \epsilon_{protein}(\lambda) = 1 + \sum_j{\frac{1/{\Lambda_j}^2}
    {1/{\lambda_j}^2 -i/(\lambda\gamma_j) - 1/{\lambda}^2}}
\end{equation}

Where:

* $\Lambda_1 = 10853.54 $ nm
* $\Lambda_2 = 878.5 $ nm
* $\Lambda_3 = 92.6 $ nm
* $\Lambda_4 = 82.81 $ nm
-------------------------
* $\gamma_1 = \infty $ nm
* $\gamma_2 = 2484.52 $ nm
* $\gamma_3 = 155.28 $ nm
* $\gamma_4 = 65.38 $ nm
--------------------------
* $\lambda_1 = 6059.8 $ nm
* $\lambda_2 = 194.1 $ nm
* $\lambda_3 = 99.38 $ nm
* $\lambda_4 = 57.78 $ nm

Let's avoid the problems that $\gamma_1= \infty$ might cause when computing the function by separating that term and setting the $1/\gamma_1=0$.

Then our equation will look like:

\begin{equation}
    \epsilon_{protein}(\lambda) = 1 + \frac{1/{\Lambda_1}^2}
                                {1/{\lambda_1}^2 - 1/{\lambda}^2} +
                                \sum_{j=2}^{4}{\frac{1/{\Lambda_j}^2}
                                {1/{\lambda_j}^2 -i/(\lambda\gamma_j) - 1/{\lambda}^2}}
\end{equation}

```python
import numpy
```

```python
#define _1 variables
Lambda_1 = 10853.54
lambda_1 = 6059.8
```

```python
#define the rest (2-4) as an array

Lambda_array = numpy.array([878.5, 92.6, 82.81])
gamma_array = numpy.array([2484.52, 155.28, 65.38])
lambda_array = numpy.array([194.1, 99.38, 57.78])

#Let's use the version 1/ thing so it's less confusing

sigma_array = 1/Lambda_array
alpha_array = 1/gamma_array
f_array = 1/lambda_array
```

\begin{equation}
    \epsilon_{protein}(\lambda) = 1 + \frac{{\sigma_1}^2}
                                {{f_1}^2 - 1/{\lambda}^2} +
                                \sum_{j=2}^{4}{\frac{{\sigma_j}^2}
                                {{f_j}^2 -i\alpha_j/\lambda - 1/{\lambda}^2}}
\end{equation}

```python
def protein_dielectric(lamb, Lambda_1, lambda_1, Lambda_array, lambda_array, gamma_array):
    '''Computes the value of dielectric constant of a protein for a certain
       wavelength. It uses Lorentz oscillators, obtained from Pahn, etal. 2013

    Arguments:
    ----------
    lamb        : float, wavelength in [nm] where we want to
                         know the dielectric constant.
    Lambda_1    : float, Lorentz oscillator upper lambda 1.
    lambda_1    : float, Lorentz oscillator lower lambda 1.
    Lambda_array: array of floats, Lorentz oscillator upper lambda 2,3,4.
    lambda_array: array of floats, Lorentz oscillator lower lambda 2,3,4.
    gamma_array : array of floats, Lorentz oscillator gamma 2,3,4.

    Returns:
    --------
    epsilon: complex, dielectric constant.
    '''
    #Let's use the version 1/ thing so it's less confusing
    sigma_1 = 1/Lambda_1
    f_1 = 1/lambda_1

    sigma_array = 1/Lambda_array
    alpha_array = 1/gamma_array
    f_array = 1/lambda_array

    epsilon = 1 + sigma_1**2/(f_1**2 - (1/lamb**2)) + numpy.sum(
                            sigma_array**2/(f_array**2 -
                                            1j*alpha_array/lamb
                                          - (1/lamb**2)))

    return epsilon
```

```python
lamb = numpy.linspace(0.5,600,201)
```

```python
epsilon = numpy.zeros(len(lamb), dtype=numpy.complex128)
```

```python
for i in range(len(lamb)):
    epsilon[i] = protein_dielectric(lamb[i], Lambda_1, lambda_1, Lambda_array, lambda_array, gamma_array)

```

```python
from matplotlib import pyplot
%matplotlib inline
```

```python
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 14
rcParams['xtick.top'] = True
rcParams['ytick.right'] = True
rcParams['axes.linewidth'] = 2
```

```python
pyplot.figure(figsize=(12,4))

pyplot.subplot(121)

pyplot.plot(lamb,epsilon.real,ls='', marker='o', color='#2929a3', ms=5)

pyplot.xlabel('Wavelength [nm]')
pyplot.ylabel('Dielectric constant')
pyplot.xlim(min(lamb), max(lamb))
pyplot.ylim(0, 3.5)

pyplot.xticks(numpy.linspace(0, max(lamb), 13), rotation=35)
pyplot.tick_params(axis='both', length=8, width=1, direction='in')
pyplot.title('Real')
pyplot.grid(linestyle=':')

pyplot.subplot(122)

pyplot.plot(lamb,epsilon.imag, ls='', marker='o', color='#ff5733', ms=5)

pyplot.xlabel('Wavelength [nm]')
#pyplot.ylabel('Refractive index')
pyplot.xlim(min(lamb), max(lamb))
pyplot.ylim(0, 3.5)
pyplot.xticks(numpy.linspace(0, max(lamb), 13), rotation=35)
pyplot.tick_params(axis='both', length=8, width=1, direction='in')
pyplot.title('Imaginary')
pyplot.grid(linestyle=':')
```

To run the example lspr_7spheres we want to know the value of epsilon when the wavelength is 380 nm.

```python
eps_380 = protein_dielectric(380, Lambda_1, lambda_1, Lambda_array, lambda_array, gamma_array)
print('lambda = {}, epsilon_protein = {}'.format(380, eps_380))
```

    lambda = 380, epsilon_protein = (2.751434851700645+0.2860230708531996j)


```python
import ipyparallel as ipp
rc = ipp.Client()
rc.ids
```

```python
%matplotlib
import sympy
import numpy as np
from matplotlib import pyplot as plt

pi = sympy.pi.evalf(40)
pi
```

    Using matplotlib backend: Qt5Agg

$\displaystyle 3.141592653589793238462643383279502884197$

```python
def one_digit_freqs(digits, normalize=False):
    """
    Consume digits of pi and compute 1 digit freq. counts.
    """
    freqs = np.zeros(10, dtype='i4')
    for d in digits:
        freqs[int(d)] += 1
    if normalize:
        freqs = freqs/freqs.sum()
    return freqs

def plot_one_digit_freqs(f1):
    """
    Plot one digit frequency counts using matplotlib.
    """
    ax = plt.plot(f1,'bo-')
    plt.title('Single digit counts in pi')
    plt.xlabel('Digit')
    plt.ylabel('Count')
    return ax

```

```python
pi = sympy.pi.evalf(100000)
digits = (d for d in str(pi)[2:])  # create a sequence of digits
freqs = one_digit_freqs(digits)
plot_one_digit_freqs(freqs)
```

```python
run par_stdpi.py
```

```python

```


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Discussion: Week 5

## Exercise: More Business Cycle Data

The file `business_cycle_data_actual_trend.csv`, available at https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/business_cycle_data_actual_trend.csv, contains actual and trend data for real GDP per capita, real consumption per capita, real investment per capita, real physical capital per capita, TFP,  hours per capita, real M2 per capita, the rate of PCE (personal consumption expenditures) inflation, the three month T-bill rate, and the unemployment rate at quarterly frequency. The GDP, consumption, investment, capital, and M2 data are in terms of 2012 dollars. Hours is measured as an index with the value in October 2012 set to 100. PCE inflation is measured at the percent change from one year prior. PCE inflation, unemployment, and the T-bill rate are expressed in units of percent.

In this Notebook, you will replicate the work from class 9 but for the following quantities:
* Real GDP per capita
* 3-month T-Bill rate
* PCE inflation rate
* Unemployment rate

Refer to the Notebook from Class 9 for reference: https://github.com/letsgoexploring/econ126/blob/master/Lecture%20Notebooks/Econ126_Class_09.ipynb

## Part (a): Preliminaries

```python
# Read business_cycle_data_actual_trend.csv into a Pandas DataFrame with the first column set as the index and parse_dates=True

# Print the last five rows of the data

```

```python
# CELL PROVIDED
# Construct a 2x2 grid of plots of GDP, consumption, t_bill_3mo, unemployment, and pce_inflation. Multiply
# t_bill_3mo, unemployment, and pce_inflation by 100 inside the plot command to plot in units of percent.
base_year = '2012'
fig = plt.figure(figsize=(12,8))

ax1 = fig.add_subplot(2,2,1)
ax1.plot(df.gdp,'-',lw=3,alpha = 0.7)
ax1.grid()
ax1.set_title('GDP per capita')
ax1.set_ylabel('Thousands of '+base_year+' dollars')

ax2 = fig.add_subplot(2,2,2)
ax2.plot(df.t_bill_3mo*100,'-',lw=3,alpha = 0.7)
ax2.grid()
ax2.set_title('3-month T-Bill mate')
ax2.set_ylabel('Thousands of '+base_year+' dollars')

ax3 = fig.add_subplot(2,2,3)
ax3.plot(df.unemployment*100,'-',lw=3,alpha = 0.7)
ax3.grid()
ax3.set_title('Unemployment rate')
ax3.set_ylabel('Thousands of '+base_year+' dollars')

ax4 = fig.add_subplot(2,2,4)
ax4.plot(df.pce_inflation*100,'-',lw=3,alpha = 0.7)
ax4.grid()
ax4.set_title('PCE inflation')
ax4.set_ylabel('Percent')

fig.tight_layout()
```

## Part (b): Cycles and Trends

Recalle: A time series $X_t$ can be decomposed into a trend component $X_t^{trend}$ and a cyclical component $X_t^{cycle}$ such that:

\begin{align}
X_t & = X_t^{trend} + X_t^{cycle}. \tag{1}
\end{align}

In equation (1), $X_t^{trend}$ is the long-run value about which $X_t$ fluctuates. $X^{cycle}_t$ is the amount by which $X_t$ excedes its trend.

Often times, it's useful to express the cyclical component of a time series as the difference between the (natural) log of the series and the log of the trend:

\begin{align}
\hat{x}_t & = \log\left(X_t\right) - \log\left(X_t^{trend}\right) \approx\frac{X_t-X_t^{trend}}{X_t^{trend}}
\end{align}

The log-deviation from trend is approximately equal to the percent deviation of the series from trend (divided by 100).

### GDP

```python
# CELL PROVIDED
# Construct a plot of real GDP with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
#    3. A legend in an appropriate place
plt.plot(df.gdp,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.gdp_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.grid()
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('GDP per capita')
plt.legend(loc='lower right',ncol=2)
```

```python
# CELL PROVIDED
# Create a new column called gdp_cycle equal to the log difference between actual GDP and trend GDP:
df['gdp_cycle'] = np.log(df['gdp']) - np.log(df['gdp_trend'])

# Plot the log deviation of GDP from its trend (times 100)
plt.plot(df.gdp_cycle*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('GDP: Percent deviation from trend')
plt.grid()
```

### 3-month T-bill rate

```python
# Construct a plot of the 3-month T-bill rate (times 100) with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
#    3. A legend in an appropriate place

```

```python
# Create a new column called t_bill_3mo_cycle equal to the DIFFERENCE (not log difference!) between actual
# 3-month T-bill rate and it's trend:

# Plot the deviation of the 3-month T-bill rate from its trend (times 100)

```

### Unemployment rate

```python
# Construct a plot of the unemployment rate (times 100) with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
#    3. A legend in an appropriate place

```

```python
# Create a new column called unemployment_cycle equal to the DIFFERENCE (not log difference!) between actual
# unemployment and it's trend:

# Plot the deviation of the unemployment rate from its trend (times 100)

```

### PCE Inflation

```python
# Construct a plot of the pce_inflation rate (times 100) with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
#    3. A legend in an appropriate place

```

```python
# Create a new column called pce_inflation_cycle equal to the DIFFERENCE (not log difference!) between actual
# pce_inflation and it's trend:

# Plot the deviation of the PCE inflation rate from its trend (times 100)

```

## Part (c): Statistics

```python
# Create a new variable called df_cycle that is a DataFrame with columns columns gdp_cycle, t_bill_3mo_cycle,
# pce_inflation_cycle, unemployment_cycle from df.

# Print the first five rows of df_cycle

```

```python
# Use the DataFrame method .mean() to find the average values of the gdp_cycle, t_bill_3mo_cycle,
# pce_inflation_cycle, unemployment_cycle columns

```

```python
# Use the DataFrame method .std() to find the standard deviations of the gdp_cycle, t_bill_3mo_cycle,
# pce_inflation_cycle, unemployment_cycle columns

```

```python
# Use the DataFrame method .corr() to find the coeficients of correlation among the gdp_cycle, t_bill_3mo_cycle,
# pce_inflation_cycle, unemployment_cycle columns

```

**Questions**

1. Of the three quantities besides GDP in the columns of `df_cycle`, which varies the most over the business cycle?
2. Of the three quantities besides GDP in the columns of `df_cycle`, which varies the least over the business cycle?
3. Of the three quantities besides GDP in the columns of `df_cycle`, which is most correlated (in absolute value) with GDP over the business cycle?

**Answers**

1.

2.

3.

## Part (d): Bonus

Do the next cell for fun. You won't get extra credit, but do it if you want to and have time.

```python
# Plot the cyclical components of GDP, three month T-bill rate, PCE inflation rate, and the unemployment rate
# (all times 100) on the same set of axes

```


# Bayesian Hierarchical Linear Regression
Author: [Carlos Souza](mailto:souza@gatech.edu)

Probabilistic Machine Learning models can not only make predictions about future data, but also **model uncertainty**. In areas such as **personalized medicine**, there might be a large amount of data, but there is still a relatively **small amount of data for each patient**. To customize predictions for each person it becomes necessary to **build a model for each person** — with its inherent **uncertainties** — and to couple these models together in a **hierarchy** so that information can be borrowed from other **similar people** [1].

The purpose of this tutorial is to demonstrate how to **implement a Bayesian Hierarchical Linear Regression model using NumPyro**. To motivate the tutorial, I will use [OSIC Pulmonary Fibrosis Progression](https://www.kaggle.com/c/osic-pulmonary-fibrosis-progression) competition, hosted at Kaggle.

## 1. Understanding the task
Pulmonary fibrosis is a disorder with no known cause and no known cure, created by scarring of the lungs. In this competition, we were asked to predict a patient’s severity of decline in lung function. Lung function is assessed based on output from a spirometer, which measures the forced vital capacity (FVC), i.e. the volume of air exhaled.

In medical applications, it is useful to **evaluate a model's confidence in its decisions**. Accordingly, the metric used to rank the teams was designed to reflect **both the accuracy and certainty of each prediction**. It's a modified version of the Laplace Log Likelihood (more details on that later).

Let's explore the data and see what's that all about:

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

```python
train = pd.read_csv('https://gist.githubusercontent.com/ucals/'
                    '2cf9d101992cb1b78c2cdd6e3bac6a4b/raw/'
                    '43034c39052dcf97d4b894d2ec1bc3f90f3623d9/'
                    'osic_pulmonary_fibrosis.csv')
train.head()
```

      Patient
      Weeks
      FVC
      Percent
      Age
      Sex
      SmokingStatus

      0
      ID00007637202177411956430
      -4
      2315
      58.253649
      79
      Male
      Ex-smoker

      1
      ID00007637202177411956430
      5
      2214
      55.712129
      79
      Male
      Ex-smoker

      2
      ID00007637202177411956430
      7
      2061
      51.862104
      79
      Male
      Ex-smoker

      3
      ID00007637202177411956430
      9
      2144
      53.950679
      79
      Male
      Ex-smoker

      4
      ID00007637202177411956430
      11
      2069
      52.063412
      79
      Male
      Ex-smoker

In the dataset, we were provided with a baseline chest CT scan and associated clinical information for a set of patients. A patient has an image acquired at time Week = 0 and has numerous follow up visits over the course of approximately 1-2 years, at which time their FVC is measured. For this tutorial, I will use only the Patient ID, the weeks and the FVC measurements, discarding all the rest. Using only these columns enabled our team to achieve a competitive score, which shows the power of Bayesian hierarchical linear regression models especially when gauging uncertainty is an important part of the problem.

Since this is real medical data, the relative timing of FVC measurements varies widely, as shown in the 3 sample patients below:

```python
def chart(patient_id, ax):
    data = train[train['Patient'] == patient_id]
    x = data['Weeks']
    y = data['FVC']
    ax.set_title(patient_id)
    ax = sns.regplot(x, y, ax=ax, ci=None, line_kws={'color':'red'})

f, axes = plt.subplots(1, 3, figsize=(15, 5))
chart('ID00007637202177411956430', axes[0])
chart('ID00009637202177434476278', axes[1])
chart('ID00010637202177584971671', axes[2])
```

On average, each of the 176 provided patients made 9 visits, when FVC was measured. The visits happened in specific weeks in the [-12, 133] interval. The decline in lung capacity is very clear. We see, though, they are very different from patient to patient.

We were are asked to predict every patient's FVC measurement for every possible week in the [-12, 133] interval, and the confidence for each prediction. In other words: we were asked fill a matrix like the one below, and provide a confidence score for each prediction:

The task was perfect to apply Bayesian inference. However, the vast majority of solutions shared by Kaggle community used discriminative machine learning models, disconsidering the fact that most discriminative methods are very poor at providing realistic uncertainty estimates. Because they are typically trained in a manner that optimizes the parameters to minimize some loss criterion (e.g. the predictive error), they do not, in general, encode any uncertainty in either their parameters or the subsequent predictions. Though many methods can produce uncertainty estimates either as a by-product or from a post-processing step, these are typically heuristic based, rather than stemming naturally from a statistically principled estimate of the target uncertainty distribution [2].

## 2. Modelling: Bayesian Hierarchical Linear Regression with Partial Pooling
The simplest possible linear regression, not hierarchical, would assume all FVC decline curves have the same $\alpha$ and $\beta$. That's the **pooled model**. In the other extreme, we could assume a model where each patient has a personalized FVC decline curve, and **these curves are completely unrelated**. That's the **unpooled model**, where each patient has completely separate regressions.

Here, I'll use the middle ground: **Partial pooling**. Specifically, I'll assume that while $\alpha$'s and $\beta$'s are different for each patient as in the unpooled case, **the coefficients all share similarity**. We can model this by assuming that each individual coefficient comes from a common group distribution. The image below represents this model graphically:

Mathematically, the model is described by the following equations:
$$
\begin{align}
\mu_{\alpha} &\sim \mathcal{N}(0, 100) \\
\sigma_{\alpha} &\sim |\mathcal{N}(0, 100)| \\
\mu_{\beta} &\sim \mathcal{N}(0, 100) \\
\sigma_{\beta} &\sim |\mathcal{N}(0, 100)| \\
\alpha_i &\sim \mathcal{N}(\mu_{\alpha}, \sigma_{\alpha}) \\
\beta_i &\sim \mathcal{N}(\mu_{\beta}, \sigma_{\beta}) \\
\sigma &\sim \mathcal{N}(0, 100) \\
FVC_{ij} &\sim \mathcal{N}(\alpha_i + t \beta_i, \sigma)
\end{align}
$$

where *t* is the time in weeks. Those are very uninformative priors, but that's ok: our model will converge!

Implementing this model in NumPyro is pretty straightforward:

```python
import numpyro
from numpyro.infer import MCMC, NUTS, Predictive
import numpyro.distributions as dist
from jax import random
```

```python
def model(PatientID, Weeks, FVC_obs=None):
    μ_α = numpyro.sample("μ_α", dist.Normal(0., 100.))
    σ_α = numpyro.sample("σ_α", dist.HalfNormal(100.))
    μ_β = numpyro.sample("μ_β", dist.Normal(0., 100.))
    σ_β = numpyro.sample("σ_β", dist.HalfNormal(100.))

    unique_patient_IDs = np.unique(PatientID)
    n_patients = len(unique_patient_IDs)

    with numpyro.plate("plate_i", n_patients):
        α = numpyro.sample("α", dist.Normal(μ_α, σ_α))
        β = numpyro.sample("β", dist.Normal(μ_β, σ_β))

    σ = numpyro.sample("σ", dist.HalfNormal(100.))
    FVC_est = α[PatientID] + β[PatientID] * Weeks

    with numpyro.plate("data", len(PatientID)):
        numpyro.sample("obs", dist.Normal(FVC_est, σ), obs=FVC_obs)
```

That's all for modelling!

## 3. Fitting the model
A great achievement of Probabilistic Programming Languages such as NumPyro is to decouple model specification and inference. After specifying my generative model, with priors, condition statements and data likelihood, I can leave the hard work to NumPyro's inference engine.

Calling it requires just a few lines. Before we do it, let's add a numerical Patient ID for each patient code. That can be easily done with scikit-learn's LabelEncoder:

```python
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
train['PatientID'] = le.fit_transform(train['Patient'].values)

FVC_obs = train['FVC'].values
Weeks = train['Weeks'].values
PatientID = train['PatientID'].values
```

Now, calling NumPyro's inference engine:

```python
nuts_kernel = NUTS(model)

mcmc = MCMC(nuts_kernel, num_samples=2000, num_warmup=2000)
rng_key = random.PRNGKey(0)
mcmc.run(rng_key, PatientID, Weeks, FVC_obs=FVC_obs)

posterior_samples = mcmc.get_samples()
```

    sample: 100%|██████████| 4000/4000 [00:28<00:00, 142.85it/s, 63 steps of size 1.08e-01. acc. prob=0.88]

## 4. Checking the model
### 4.1. Inspecting the learned parameters
First, let's inspect the parameters learned. To do that, I will use [ArviZ](https://arviz-devs.github.io/arviz/), which perfectly integrates with NumPyro:

```python
import arviz as az

data = az.from_numpyro(mcmc)
az.plot_trace(data, compact=True);
```

Looks like our model learned personalized alphas and betas for each patient!

### 4.2. Visualizing FVC decline curves for some patients
Now, let's visually inspect FVC decline curves predicted by our model. We will completely fill in the FVC table, predicting all missing values. The first step is to create a table to fill:

```python
pred_template = []
for i in range(train['Patient'].nunique()):
    df = pd.DataFrame(columns=['PatientID', 'Weeks'])
    df['Weeks'] = np.arange(-12, 134)
    df['PatientID'] = i
    pred_template.append(df)
pred_template = pd.concat(pred_template, ignore_index=True)
```

Predicting the missing values in the FVC table and confidence (sigma) for each value becomes really easy:

```python
PatientID = pred_template['PatientID'].values
Weeks = pred_template['Weeks'].values
predictive = Predictive(model, posterior_samples,
                        return_sites=['σ', 'obs'])
samples_predictive = predictive(random.PRNGKey(0),
                                PatientID, Weeks, None)
```

Let's now put the predictions together with the true values, to visualize them:

```python
df = pd.DataFrame(columns=['Patient', 'Weeks', 'FVC_pred', 'sigma'])
df['Patient'] = le.inverse_transform(pred_template['PatientID'])
df['Weeks'] = pred_template['Weeks']
df['FVC_pred'] = samples_predictive['obs'].T.mean(axis=1)
df['sigma'] = samples_predictive['obs'].T.std(axis=1)
df['FVC_inf'] = df['FVC_pred'] - df['sigma']
df['FVC_sup'] = df['FVC_pred'] + df['sigma']
df = pd.merge(df, train[['Patient', 'Weeks', 'FVC']],
              how='left', on=['Patient', 'Weeks'])
df = df.rename(columns={'FVC': 'FVC_true'})
df.head()
```

      Patient
      Weeks
      FVC_pred
      sigma
      FVC_inf
      FVC_sup
      FVC_true

      0
      ID00007637202177411956430
      -12
      2215.250488
      156.240250
      2059.010254
      2371.490723
      NaN

      1
      ID00007637202177411956430
      -11
      2216.876465
      161.742844
      2055.133545
      2378.619385
      NaN

      2
      ID00007637202177411956430
      -10
      2217.119385
      152.493393
      2064.625977
      2369.612793
      NaN

      3
      ID00007637202177411956430
      -9
      2210.115479
      159.417191
      2050.698242
      2369.532715
      NaN

      4
      ID00007637202177411956430
      -8
      2198.447510
      153.805420
      2044.642090
      2352.252930
      NaN

Finally, let's see our predictions for 3 patients:

```python
def chart(patient_id, ax):
    data = df[df['Patient'] == patient_id]
    x = data['Weeks']
    ax.set_title(patient_id)
    ax.plot(x, data['FVC_true'], 'o')
    ax.plot(x, data['FVC_pred'])
    ax = sns.regplot(x, data['FVC_true'], ax=ax, ci=None,
                     line_kws={'color':'red'})
    ax.fill_between(x, data["FVC_inf"], data["FVC_sup"],
                    alpha=0.5, color='#ffcd3c')
    ax.set_ylabel('FVC')

f, axes = plt.subplots(1, 3, figsize=(15, 5))
chart('ID00007637202177411956430', axes[0])
chart('ID00009637202177434476278', axes[1])
chart('ID00011637202177653955184', axes[2])
```

The results are exactly what we expected to see! Highlight observations:
- The model adequately learned Bayesian Linear Regressions! The orange line (learned predicted FVC mean) is very inline with the red line (deterministic linear regression). But most important: it learned to predict uncertainty, showed in the light orange region (one sigma above and below the mean FVC line)
- The model predicts a higher uncertainty where the data points are more disperse (1st and 3rd patients). Conversely, where the points are closely grouped together (2nd patient), the model predicts a higher confidence (narrower light orange region)
- Finally, in all patients, we can see that the uncertainty grows as the look more into the future: the light orange region widens as the # of weeks grow!

## 4.3. Computing the modified Laplace Log Likelihood and RMSE

As mentioned earlier, the competition was evaluated on a modified version of the Laplace Log Likelihood. In medical applications, it is useful to evaluate a model's confidence in its decisions. Accordingly, the metric is designed to reflect both the accuracy and certainty of each prediction.

For each true FVC measurement, we predicted both an FVC and a confidence measure (standard deviation $\sigma$). The metric was computed as:

$$
\begin{align}
\sigma_{clipped} &= max(\sigma, 70) \\
\delta &= min(|FVC_{true} - FVC_{pred}|, 1000) \\
metric &= -\dfrac{\sqrt{2}\delta}{\sigma_{clipped}} - \ln(\sqrt{2} \sigma_{clipped})
\end{align}
$$

The error was thresholded at 1000 ml to avoid large errors adversely penalizing results, while the confidence values were clipped at 70 ml to reflect the approximate measurement uncertainty in FVC. The final score was calculated by averaging the metric across all (Patient, Week) pairs. Note that metric values will be negative and higher is better.

Next, we calculate the metric and RMSE:

```python
y = df.dropna()
rmse = ((y['FVC_pred'] - y['FVC_true']) ** 2).mean() ** (1/2)
print(f'RMSE: {rmse:.1f} ml')

sigma_c = y['sigma'].values
sigma_c[sigma_c < 70] = 70
delta = (y['FVC_pred'] - y['FVC_true']).abs()
delta[delta > 1000] = 1000
lll = - np.sqrt(2) * delta / sigma_c - np.log(np.sqrt(2) * sigma_c)
print(f'Laplace Log Likelihood: {lll.mean():.4f}')
```

    RMSE: 122.2 ml
    Laplace Log Likelihood: -6.1376

What do these numbers mean? It means if you adopted this approach, you would **outperform most of the public solutions** in the competition. Curiously, the vast majority of public solutions adopt a standard deterministic Neural Network, modelling uncertainty through a quantile loss. **Most of the people still adopt a frequentist approach**.

**Uncertainty** for single predictions becomes more and more important in machine learning and is often a requirement. **Especially when the consequenses of a wrong prediction are high**, we need to know what the probability distribution of an individual prediction is. For perspective, Kaggle just launched a new competition sponsored by Lyft, to build motion prediction models for self-driving vehicles. "We ask that you predict a few trajectories for every agent **and provide a confidence score for each of them**."

Finally, I hope the great work done by Pyro/NumPyro developers help democratize Bayesian methods, empowering an ever growing community of researchers and practitioners to create models that can not only generate predictions, but also assess uncertainty in their predictions.

# References
1. Ghahramani, Z. Probabilistic machine learning and artificial intelligence. Nature 521, 452–459 (2015). https://doi.org/10.1038/nature14541

2. Rainforth, Thomas William Gamlen. Automating Inference, Learning, and Design Using Probabilistic Programming. University of Oxford, 2017.

```python

```


# heaviside
> ## numpy.heaviside(t,0)
> ## numpy.heaviside(t-1,0)

```python
import numpy as np
import matplotlib as npl
import matplotlib.pyplot as plt
%matplotlib widget
```

```python
def x(t):
    # heaviside step function( if x1 = 0 then x2 )
    return np.heaviside(t,0) - np.heaviside(t-1,0)
```

```python
t = np.linspace(-5,5,1000)

fig = plt.figure(figsize=(6,6))

ax = fig.add_subplot(511)
ax.plot(t,np.heaviside(t,0),label='h(t,0)')
ax.plot(t,np.heaviside(t-1,0),label='h(t-1,0)')
plt.legend()

ax = fig.add_subplot(512)
ax.plot(t,np.heaviside(t,0)-np.heaviside(t-1,0),label='h(t,0)-h(t-1,0)')
plt.legend()

ax = fig.add_subplot(513)
ax.plot(t,t*np.heaviside(t,0),label='t*h(t,0)')
plt.legend()

ax = fig.add_subplot(514)
ax.plot(t,t*np.heaviside(t-1,0),label='t*h(t-1,0)')
plt.legend()

ax = fig.add_subplot(515)
ax.plot(t,t*np.heaviside(t,0)-t*np.heaviside(t-1,0),label=r'$t\cdot h(t,0) - t\cdot h(t-1,0)$')
plt.legend()
```

    <matplotlib.legend.Legend at 0x7f7324438eb0>

        Figure

```python
import sympy as sp
x,mu,sigma = sp.symbols('x mu sigma')
x**2 + mu + sigma
```

$\displaystyle \mu + \sigma + x^{2}$


# Learning in LTN

This tutorial explains how to learn some language symbols (predicates, functions, constants) using the satisfaction of a knowledgebase as an objective. It expects basic familiarity of the first two turoials on LTN (grounding symbols and connectives).

```python
import logictensornetworks as ltn
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
```

We use the following simple example to illustrate learning in LTN.

The domain is the square $[0,4] \times [0,4]$. We have one example of the class $A$ and one example of the class $B$. The rest of the individuals are not labelled, but there are two assumptions:
- $A$ and $B$ are mutually exclusive,
- any two close points should share the same label.

```python
points = np.array(
        [[0.4,0.3],[1.2,0.3],[2.2,1.3],[1.7,1.0],[0.5,0.5],[0.3, 1.5],[1.3, 1.1],[0.9, 1.7],
        [3.4,3.3],[3.2,3.3],[3.2,2.3],[2.7,2.0],[3.5,3.5],[3.3, 2.5],[3.3, 1.1],[1.9, 3.7],[1.3, 3.5],[3.3, 1.1],[3.9, 3.7]])
point_a = [3.3,2.5]
point_b = [1.3,1.1]
fig, ax = plt.subplots()
ax.set_xlim(0,4)
ax.set_ylim(0,4)
ax.scatter(points[:,0],points[:,1],color="black",label="unknown")
ax.scatter(point_a[0],point_a[1],color="blue",label="a")
ax.scatter(point_b[0],point_b[1],color="red",label="b")
ax.set_title("Dataset of individuals")
plt.legend();
```

We define the membership predicate $C(x,l)$, where $x$ is an individual and $l$ is a onehot label to denote the two classes. $C$ is approximated by a simple MLP. The last layer, that computes probabilities per class, uses a `softmax` activation, ensuring that the classes are mutually-exclusive.

We define the knowledgebase $\mathcal{K}$ composed of the following rules:
\begin{align}
& C(a,l_a)\\
& C(b,l_b)\\
\forall x_1,x_2,l\ \big(\mathrm{Sim}(x_1,x_2) & \rightarrow \big(C(x_1,l)\leftrightarrow C(x_2,l)\big)\big)
\end{align}
where $a$ and $b$ the two individuals already classified; $x_1$,$x_2$ are variables ranging over all individuals; $l_a$, $l_b$ are the one-hot labels for $A$ and $B$; $l$ is a variable ranging over the labels. $\mathrm{Sim}$ is a predicate measuring similarity between two points. $\mathcal{G}(\mathrm{Sim}):\vec{u},\vec{v}\mapsto \exp(-\|\vec{u}-\vec{v} \|^2)$.

The objective is to learn the predicate $C$ to maximize the satisfaction of $\mathcal{K}$. If $\theta$ denotes the set of trainable parameters, the task is :
$$
\theta^\ast = \mathrm{argmax}_{\theta\in\Theta}\ \mathrm{SatAgg}_{\phi\in\mathcal{K}}\mathcal{G}_{\theta}(\phi)
$$
where $\mathrm{SatAgg}$ is an operator that aggregates the truth values of the formulas in $\mathcal{K}$ (if there are more than one formula).

To evaluate the grounding of each formula, one has to define the grounding of the non-logical symbols and of the operators.

```python
class ModelC(tf.keras.Model):
    def __init__(self):
        super(ModelC, self).__init__()
        self.dense1 = tf.keras.layers.Dense(5, activation=tf.nn.elu)
        self.dense2 = tf.keras.layers.Dense(5, activation=tf.nn.elu)
        self.dense3 = tf.keras.layers.Dense(2, activation=tf.nn.softmax)

    def call(self, inputs):
        """inputs[0]: point, inputs[1]: onehot label"""
        x, label = inputs[0], inputs[1]
        x = self.dense1(x)
        x = self.dense2(x)
        prob = self.dense3(x)
        return tf.math.reduce_sum(prob*label,axis=1)

C = ltn.Predicate(ModelC())
```

```python
x1 = ltn.variable("x1",points)
x2 = ltn.variable("x2",points)
a = ltn.constant([3.3,2.5])
b = ltn.constant([1.3,1.1])
l_a = ltn.constant([1,0])
l_b = ltn.constant([0,1])
l = ltn.variable("l",[[1,0],[0,1]])

Sim = ltn.Predicate.Lambda(
    lambda args: tf.exp(-1.*tf.sqrt(tf.reduce_sum(tf.square(args[0]-args[1]),axis=1)))
)
```

```python
similarities_to_a = Sim([x1,a])
fig, ax = plt.subplots()
ax.set_xlim(0,4)
ax.set_ylim(0,4)
ax.scatter(points[:,0],points[:,1],color="black")
ax.scatter(a[0],a[1],color="blue")
ax.set_title("Illustrating the similarities of each point to a")
for i, sim_to_a in enumerate(similarities_to_a):
    plt.plot([points[i,0],a[0]],[points[i,1],a[1]], alpha=sim_to_a.numpy(),color="blue")
```

Notice the operator for equivalence $p \leftrightarrow q$; in LTN, it is simply implemented as $(p \rightarrow q)\land(p \leftarrow q)$ using one operator for conjunction and one operator for implication.

```python
Not = ltn.Wrapper_Connective(ltn.fuzzy_ops.Not_Std())
And = ltn.Wrapper_Connective(ltn.fuzzy_ops.And_Prod())
Or = ltn.Wrapper_Connective(ltn.fuzzy_ops.Or_ProbSum())
Implies = ltn.Wrapper_Connective(ltn.fuzzy_ops.Implies_Reichenbach())
Equiv = ltn.Wrapper_Connective(ltn.fuzzy_ops.Equiv(ltn.fuzzy_ops.And_Prod(),ltn.fuzzy_ops.Implies_Reichenbach()))
Forall = ltn.Wrapper_Quantifier(ltn.fuzzy_ops.Aggreg_pMeanError(p=2),semantics="forall")
Exists = ltn.Wrapper_Quantifier(ltn.fuzzy_ops.Aggreg_pMean(p=6),semantics="exists")
```

If there are several closed formulas in $\mathcal{K}$, their truth values need to be aggregated.
We recommend to use the generalized mean inspired operator `pMeanError`, already used to implement $\forall$.
The hyperparameter again allows flexibility in how strict the formula aggregation is ($p = 1$ corresponds to `mean`; $p \to +\inf$ corresponds to `min`).

The knowledgebase should be written inside of a function that is decorated with `tf.function`. This Tensorflow decorator compiles the function into a callable TensorFlow graph (static).

```python
formula_aggregator = ltn.fuzzy_ops.Aggreg_pMeanError(p=2)

@tf.function
def axioms():
    axioms = [
        C([a,l_a]),
        C([b,l_b]),
        Forall(
            [x1,x2,l],
            Implies( Sim([x1,x2]),
                    Equiv(C([x1,l]),C([x2,l]))
                   )
        )
    ]
    kb_sat = formula_aggregator(tf.stack(axioms))
    return kb_sat
```

The first call takes care of the graph compilation. It performs optimizations such that the subsequent calls are greatly accelerated.

```python
axioms()
```

    <tf.Tensor: shape=(), dtype=float32, numpy=0.46948296>

Eventually, one can write a custom training loop in Tensorflow and

```python
trainable_variables = C.trainable_variables
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)

for epoch in range(2000):
    with tf.GradientTape() as tape:
        loss = 1. - axioms()
    grads = tape.gradient(loss, trainable_variables)
    optimizer.apply_gradients(zip(grads, trainable_variables))
    if epoch%200 == 0:
        print("Epoch %d: Sat Level %.3f"%(epoch, axioms()))
print("Training finished at Epoch %d with Sat Level %.3f"%(epoch, axioms()))
```

    Epoch 0: Sat Level 0.471
    Epoch 200: Sat Level 0.832
    Epoch 400: Sat Level 0.942
    Epoch 600: Sat Level 0.952
    Epoch 800: Sat Level 0.954
    Epoch 1000: Sat Level 0.957
    Epoch 1200: Sat Level 0.959
    Epoch 1400: Sat Level 0.959
    Epoch 1600: Sat Level 0.959
    Epoch 1800: Sat Level 0.959
    Training finished at Epoch 1999 with Sat Level 0.959

After a few epochs, the system has learned to identify samples close to the point $a$ (resp. $b$) as belonging to class $A$ (resp. $B$) based on the rules of the knowledgebase.

```python
fig = plt.figure(figsize=(10,3))
fig.add_subplot(1,2,1)
plt.scatter(x1[:,0],x1[:,1],c=C([x1,l_a]).numpy(),vmin=0,vmax=1)
plt.title("C(x,l_a)")
plt.colorbar()
fig.add_subplot(1,2,2)
plt.scatter(x1[:,0],x1[:,1],c=C([x1,l_b]).numpy(),vmin=0,vmax=1)
plt.title("C(x,l_b)")
plt.colorbar()
plt.show();
```

## Special Cases

### Variables grounded by batch

When working with batches of data, grounding the variables with different values at each step:
1. Pass the values in arguments to the knowledgebase function,
2. Create the ltn variables within the function.

```python
@tf.function
def axioms(data_x, data_y):
    x = ltn.variable("x", data_x)
    y = ltn.variable("y", data_y)
    return Forall([x,y],P([x,y]))

...
for epoch in range(epochs):
    for batch_x, batch_y in dataset:
        with tf.GradientTape() as tape:
            loss_value = 1. - axioms(batch_x, batch_y)
        grads = tape.gradient(loss_value, trainable_variables)
        optimizer.apply_gradients(zip(grads, trainable_variables))
```

### Variables denoting a sequence of trainable constants

When a variable denotes a sequence of trainable constants (embeddings):
1. Do not create the variable outside the scope of `tf.GradientTape()`,
2. Create the variable within the training step function.

```python
c1 = ltn.constant([2.1,3], trainable=True)
c2 = ltn.constant([4.5,0.8], trainable=True)
# Do not assign the variable here. Tensorflow would not keep track of the
# gradients between c1/c2 and x during training.
# x = ltn.variable("x", tf.stack([c1,c2]))
...
@tf.function
def axioms():
    # The assignation must be done within the tf.GradientTape,
    # inside of the training step function.
    x = ltn.variable("x",tf.stack([c1,c2]))
    return Forall(x,P(x))
...
for epoch in range(epochs):
    with tf.GradientTape() as tape:
        loss_value = 1. - axioms()
    grads = tape.gradient(loss_value, trainable_variables)
    optimizer.apply_gradients(zip(grads, trainable_variables))
```