## Usando sympy para manipular sistemas de referência

Documentação:

https://docs.sympy.org/latest/modules/physics/vector/api/classes.html

```python
import sympy
from sympy import sin, cos, pi, Symbol, symbols, Matrix, Transpose, init_session, Array, tensorproduct
from sympy.physics.vector import ReferenceFrame, outer, dynamicsymbols, Point
```

```python
# Símbolos que usarei pra angulos
alpha, beta, gamma = symbols('α β γ') # digite \alpha+tab pra usar o símbolo unicode
```

Criando um reference frame:

```python
#variável = ReferenceFrame("Representação da variável")
A = ReferenceFrame("A")
type(A)
```

    sympy.physics.vector.frame.ReferenceFrame

Toda referência já vem com vetores x, y, z, ortonormais:

```python
v1 = A.x # v1 = [1,0,0]
v1
```

$\displaystyle \mathbf{\hat{a}_x}$

### Operações com vetores:

```python
v2 = 2*A.x + A.y #v2 = [2, 1, 0]
v2
```

$\displaystyle 2\mathbf{\hat{a}_x} + \mathbf{\hat{a}_y}$

```python
# produto vetorial
v2.cross(v1)
```

$\displaystyle - \mathbf{\hat{a}_z}$

```python
# produto escalar
v2.dot(v1)
```

$\displaystyle 2$

### Criando outra base

```python
B = ReferenceFrame("B")
type(B)
```

    sympy.physics.vector.frame.ReferenceFrame

Criando relações entre A e B:

```python
B.orient(A, 'Space', (alpha, 0, 0), '123')
#   B a partir de  A
#               'space' = faz tres rotações a partir dos eixos estacionarios da origem
#                         (a, b, c) = angulo a rodar
#                         123 = ordem das rotações, "XYZ" é equivalente
# B.orient(A, 'Space', (alpha, 0, 0), "XYZ")
# B.orient(A, 'Space', (alpha, 0, 0), "XYZ")
```

```python
# Definindo vetores em B:
u1 = B.x - B.z
```

```python
# Como um vetor definido em A é escrito em B:
v2.express(B)
```

$\displaystyle 2\mathbf{\hat{b}_x} + \operatorname{cos}\left(α\right)\mathbf{\hat{b}_y} -  \operatorname{sin}\left(α\right)\mathbf{\hat{b}_z}$

```python
# Pode operar vetores de bases diferentes sem problema:
v2.cross(u1)
```

$\displaystyle -  \operatorname{cos}\left(α\right)\mathbf{\hat{b}_x} + (2 - \operatorname{sin}\left(α\right))\mathbf{\hat{b}_y} -  \operatorname{cos}\left(α\right)\mathbf{\hat{b}_z}$

### Matriz de rotação
 (Direct Cosine Matrix)

```python
A.dcm(B)
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(α \right)} & - \sin{\left(α \right)}\\0 & \sin{\left(α \right)} & \cos{\left(α \right)}\end{matrix}\right]$

```python
B.dcm(A)
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(α \right)} & \sin{\left(α \right)}\\0 & - \sin{\left(α \right)} & \cos{\left(α \right)}\end{matrix}\right]$

### ângulos de Euler

```python
C = ReferenceFrame("C")
D = ReferenceFrame("D")
E = ReferenceFrame("E")
```

```python
C.orient(A, "Body", (alpha, beta, gamma), "ZXZ")
# Body = corpo rígido = angulos de euler
# ZXZ = Roda alpha em torno de Z, roda beta em torno de X e gamma em torno de Z'
C.dcm(A)
```

$\displaystyle \left[\begin{matrix}- \sin{\left(α \right)} \sin{\left(γ \right)} \cos{\left(β \right)} + \cos{\left(α \right)} \cos{\left(γ \right)} & \sin{\left(α \right)} \cos{\left(γ \right)} + \sin{\left(γ \right)} \cos{\left(α \right)} \cos{\left(β \right)} & \sin{\left(β \right)} \sin{\left(γ \right)}\\- \sin{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} - \sin{\left(γ \right)} \cos{\left(α \right)} & - \sin{\left(α \right)} \sin{\left(γ \right)} + \cos{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} & \sin{\left(β \right)} \cos{\left(γ \right)}\\\sin{\left(α \right)} \sin{\left(β \right)} & - \sin{\left(β \right)} \cos{\left(α \right)} & \cos{\left(β \right)}\end{matrix}\right]$

```python
D.orient(A, "Body", (alpha, beta, gamma), "YZY") # outro exemplo
D.dcm(A)
```

$\displaystyle \left[\begin{matrix}- \sin{\left(α \right)} \sin{\left(γ \right)} + \cos{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} & \sin{\left(β \right)} \cos{\left(γ \right)} & - \sin{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} - \sin{\left(γ \right)} \cos{\left(α \right)}\\- \sin{\left(β \right)} \cos{\left(α \right)} & \cos{\left(β \right)} & \sin{\left(α \right)} \sin{\left(β \right)}\\\sin{\left(α \right)} \cos{\left(γ \right)} + \sin{\left(γ \right)} \cos{\left(α \right)} \cos{\left(β \right)} & \sin{\left(β \right)} \sin{\left(γ \right)} & - \sin{\left(α \right)} \sin{\left(γ \right)} \cos{\left(β \right)} + \cos{\left(α \right)} \cos{\left(γ \right)}\end{matrix}\right]$

```python
# como usar a matriz de rotação pra definir a base:

M = Matrix([[  1,  -.5, 0],
            [ .5,    1, 0],
            [  0,    0, 1]])
E.orient(A, "DCM", M)
E.dcm(A)

```

$\displaystyle \left[\begin{matrix}1 & 0.5 & 0\\-0.5 & 1 & 0\\0 & 0 & 1\end{matrix}\right]$

Como expressar um vetor em outra base:

```python
v2.express(E)
```

$\displaystyle 2.5\mathbf{\hat{e}_x}$

Dá pra converter em matriz

```python
w1 = v2.to_matrix(C) # saí um vetor coluna dos coeficientes de (C.x, C.y, C.z)
w1
```

$\displaystyle \left[\begin{matrix}- 2 \sin{\left(α \right)} \sin{\left(γ \right)} \cos{\left(β \right)} + \sin{\left(α \right)} \cos{\left(γ \right)} + \sin{\left(γ \right)} \cos{\left(α \right)} \cos{\left(β \right)} + 2 \cos{\left(α \right)} \cos{\left(γ \right)}\\- \sin{\left(α \right)} \sin{\left(γ \right)} - 2 \sin{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} - 2 \sin{\left(γ \right)} \cos{\left(α \right)} + \cos{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)}\\2 \sin{\left(α \right)} \sin{\left(β \right)} - \sin{\left(β \right)} \cos{\left(α \right)}\end{matrix}\right]$

```python
w1.T # transpõe
```

$\displaystyle \left[\begin{matrix}- 2 \sin{\left(α \right)} \sin{\left(γ \right)} \cos{\left(β \right)} + \sin{\left(α \right)} \cos{\left(γ \right)} + \sin{\left(γ \right)} \cos{\left(α \right)} \cos{\left(β \right)} + 2 \cos{\left(α \right)} \cos{\left(γ \right)} & - \sin{\left(α \right)} \sin{\left(γ \right)} - 2 \sin{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} - 2 \sin{\left(γ \right)} \cos{\left(α \right)} + \cos{\left(α \right)} \cos{\left(β \right)} \cos{\left(γ \right)} & 2 \sin{\left(α \right)} \sin{\left(β \right)} - \sin{\left(β \right)} \cos{\left(α \right)}\end{matrix}\right]$

```python

```


# Raumluftqualität 6.0

Für einen belüfteten Raum ist

$$
  k(t) = k_\infty + (k_0-k_\infty)\,\rm{e}^{-\beta\,t}
$$

Dieser exponentielle Verlauf nähert sich dem Wert $k_\infty$ immer stärker an, kann ihn theoretisch aber nicht erreichen. Das hat seinen Grund darin, dass die Exponentialfunktion (theoretisch) niemals den Wert 0 annehmen kann.

Für praktische Zwecke ist das Modell sehr gut geeignet, weil es die Annäherung von $k(t)$ an $k_\infty$ sehr gut zeigt.

```python
from sympy import *
init_printing()

from matplotlib import pyplot as plt, ticker as tk
%config InlineBackend.figure_format='retina' # nur für Macbook Pro nötig

import numpy as np
import pandas as pd

from fun_expr import Function_from_Expression as FE
```

```python
# Die Funktionsvariable definieren
t = Symbol('t')

# bekannte Konstanten
k_zul = 1000e-6
k_0 = k_au = 400e-6

beta = 0.5
t_0 = 0

# Die Funktion k(t) definieren
k = FE(t, k_zul + (k_0-k_zul)*exp(-beta*(t-t_0)),name='k')
k.equation()
```

$\displaystyle k\left(t\right) = 0.001 - 0.0006 e^{- 0.5 t}$

```python
fig,ax = plt.subplots()

ax.axhline(1e6*k_zul,c='r')

lx = np.linspace(0,20)
ax.plot(lx,1e6*k.lambdified(lx))

ax.set(
    ylim=(0,1200), ylabel='Schadstoffkonzentration $k$ in ppM',
    xlim=(0,20), xlabel='Zeit $t$ in Stunden'
)

Delta_k = (k_zul-k_0)*5e-2

#ax.fill_between(lx, 1e6*(k_zul+Delta_k), 1e6*(k_zul-Delta_k),color='r',alpha=0.25)

ax.grid(c='k',lw=0.25)
```

Wichtig ist die Frage, wann der Gleichgewichtszustand erreicht ist. Für diese Frage
ist die Exponentialfunktion nicht so gut geeignet, weil der Zustand $k_\infty$ theoretisch nie erreicht wird.

Deshalb kann nicht berechnet werden, wie lange es dauert, bis dieser Zustand erreicht ist.

Man stellt sich in diesem Fall nicht die Frage, wann der Grenzwert wirklich erreicht wird, sondern fragt stattdessen, wann ein bestimmtes "Toleranzband" erreicht wird.

Ein solches Toleranzband kann man sich wie einen Schlauch vorstellen, in dessen Mitte $k_\infty$ liegt.

```python
fig,ax = plt.subplots()

ax.axhline(1e6*k_zul,c='r')

lx = np.linspace(0,20)
ax.plot(lx,1e6*k.lambdified(lx))

ax.set(
    ylim=(0,1200), ylabel='Schadstoffkonzentration $k$ in ppM',
    xlim=(0,20), xlabel='Zeit $t$ in Stunden'
)

Delta_k = (k_zul-k_0)*5e-2

ax.fill_between(lx, 1e6*(k_zul+Delta_k), 1e6*(k_zul-Delta_k),color='r',alpha=0.25)

ax.scatter(6,970,c='k')

ax.grid(c='k',lw=0.25)
```

Hat das "Toleranzband" eine Breite von $\pm 5\%$, kann man sich die Rechnung sparen. Dann hat $t$ etwa den Wert von drei Zeitkonstanten, also sechs Stunden.

Wie berechnet man den Zeitpunkt, an dem $k(t)$ das Toleranzband erreicht?

$$
  k(t) = k_\infty + (k_0-k_\infty)\cdot\mathrm{e}^{-\beta\,t}
$$

Wir setzen

$$
  k(t) = k_\infty - (k_\infty - k_0)\cdot\epsilon
$$

Gleichsetzen liefert:

$$
  k_\infty + (k_0-k_\infty)\cdot\mathrm{e}^{-\beta\,t}
  =
  k_\infty - (k_\infty - k_0)\cdot\epsilon
$$

Gesucht ist die Zeit $t$ auf der linken Seite der Gleichung.

Um die zu berechnen, wird zunächst die Summe auf der linken Seite aufgelöst. Dazu wird $k_\infty$ auf beiden Seiten der Gleichung subtrahiert.

$$
  (k_0-k_\infty)\cdot\mathrm{e}^{-\beta\,t}
  = - (k_\infty - k_0)\cdot\epsilon
$$

Die Faktoren $k_0 - k_\infty$ und $-(k_\infty - k_0)$ sind gleich und können deshalb ebenfalls wegfallen:

$$
  \mathrm{e}^{-\beta\,t}  = \epsilon
$$

Die Umkehrung der Exponentialfunktion $\mathrm{e}^x$ ist die Logarithmusfunktion, in diesem Fall der natürliche Logarithmus $\ln x$. (Es ist übrigens üblich, keine Klammern um das Argument des Logarithmus zu schreiben).

Das bedeutet: Wird ab beide Seiten der Gleichung der natürliche Logarithmus angewendet, so steht link nur noch der Ausdruck $-\beta\,t$:

$$
  -\beta\,t = \ln\epsilon
$$

Nun muss nur noch durch den Zahlenwert $-\beta$ dividiert werden:

$$
  t = -\dfrac{\ln\epsilon}{\beta}
$$

```python
-1/beta*ln(0.05)
```


#### _Speech Processing Labs 2020: Signals: Module 1_

```python
## Run this first!
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

import cmath
from math import floor
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
plt.style.use('ggplot')

## some helper functions:
from dspMisc import *
```

# 4 The Discrete Fourier Transform

### Learning Outcomes
* Understand the DFT equation: what input does it take and what outputs does it produce.

### Need to know
* Topic Videos: Series Expansion, Fourier Analysis, Frequency Domain
* Vector operations: dot product
* [Digital Signals: Sampling sinusoids](./sp-m1-3-sampling-sinusoids.ipynb)

     Optional-ish:  This notebook goes through the DFT equation in quite a lot of detail - more than is strictly necessary for this course.  It's perfectly fine to just focus on the visualizations or to skip it for now.

That said, you might like to run the code anyway and look at the visualization of the different phasors (i.e. basis functions) that correspond to the different DFT outputs in the DFT work through section called [Generate Phasors](#genphasors), and the corresponding magnitude and phase response graphs for that example

Do you remember learning a bunch of math (trig, algebra, calculus) in high-school and thinking you'll never have to use this? While you might not use it directly, a lot of the technology you use everyday depends on it.  Pretty much all current speech technology depends on the Fourier Transform in some way! It has been (and continues to be) used to solve problems in many different fields from from image processing to quantum mechanics

In the previous notebook, you looked at how you can add 'pure' sinusoids together to form complicated waveforms. The Fourier Transform gives us a way of doing the opposite: taking sequence of measurements over time (like a speech wave) and decomposing it into sinusoids of different frequencies, magnitudes, and phases.

Since we are interested in digital signals (i.e. measuresment over discrete time steps), we'll need to use the **Discrete Fourier Transform**.  This makes it possible to actually do these calculations on a computer, but it does also lead to some unexpected side-effects.

Equation alert: If you're viewing this on github, please note that the equation rendering is not always perfect. You should view the notebooks through a jupyter notebook server for an accurate view.

## 4.1 The DFT equation

You can think of the DFT as a machine that takes $N$ amplitude samples over time and returns the frequency spectrum of the input signal (just like you when you view a spectral slice in praat).

You can treat this as a black box. But, now that we've gone through complex numbers, phasors, etc we can look at how the machine works and, more importantly, what it's limitations are.

* Input: $x[n]$, for $n=1..N$ (i.e. a time series of $N$ samples)
* Output: N complex numbers $\mapsto$ magnitude and phases of specific phasors:

$$
\begin{align}
DFT[k] &= \sum_{n=0}^{N-1} x[n] e^{-j \frac{2\pi n}{N} k} \\
DFT[k] &= \sum_{n=0}^{N-1} x[n]\big[\cos(\frac{2\pi n}{N} k) - j \sin(\frac{2\pi n}{N} k) \big]
\end{align}
$$

You'll usually see this written as $X[k]$ in signal processing textbooks, but we'll just say $DFT[k]$ to be a bit more transparent (and reduce the probability of typos!)

Notice the $-j$ in the phasor! This means as $n$ increases the 'clockhand' of the phasor is rotating clockwise!

## 4.2 The DFT Step-by-Step

But what does the DFT do for us? Very broadly, the DFT takes a series of $N$ values in time as input (e.g. waveform amplitudes over time for our sound waves) and outputs the **correlations** between the input and $N$ pure cosine waves with specific frequencies.  That is, it tells you how you would weight (and shift) these cosine waves so that you can add them all up and recover the original input.

_Let's break it down!_

### The DFT for $k=0$
Let's first look at the DFT for $k=0$:

$$
\begin{align}
DFT[0] = \sum_{n=0}^{N-1} x[n] e^{-j \frac{2\pi n}{N} 0}
\end{align}
$$

This is usually referred to as the **bias** term.  It tells us whether the input function is shifted up or down the amplitude axis (in a time v amplitude plot of the waveform).

### Exercise:

* Q: Why doesn't DFT[0] tell us anything about the frequency of the input?

No discrete observations have been made when $k=0$.

### The DFT for $k=1$:

Now we can start to look at the correlation between the input sequence and some phasors of different frequencies.  For $k=1$, we have:

$$ DFT[1] = \sum_{n=0}^{N-1} x[n] e^{-j \frac{2 \pi n}{N}}$$

Each of the $e^{j 2\pi n/N}$ terms in the sum is a step of $2\pi n/N$ radians around the circle.  That is, our $k=1$ phasor $e^{-j \frac{2 \pi}{N}n}$ makes completes 1 full circle every $N$ time steps.

Let's plot the the $k=1$ phasor for $N=16$:

```python
## plot complex numbers in the DFT[1] equation
N=16

## make an array of N steps: 0,...,N
nsteps = np.array(range(N))
tsteps = 2*np.pi*nsteps/N
Tmin = np.min(tsteps)
Tmax = np.max(tsteps)

## Corresponding complex numbers z[n] = e^{j 2 pi n/N}
zn_k1 = np.exp(-1j*2*np.pi*nsteps/N)

```

```python
## Plot the phasor corresponding to DFT[k=1]
X_k1, Y_k1, fig, phasor, iproj, rproj = plot_phasor_static(zn_k1, tsteps, plot_real=True, plot_imag=True)
```

```python
## Animate the phasor!
phasor1_vid = get_phasor_animation(X_k1, Y_k1, tsteps, phasor, iproj, rproj, fig)
phasor1_vid
```

What this animation shows is that for $k=1$, the $e^{-j\varphi}$ terms in the DFT represent $N$ evenly space samples around the unit circle aka one period of a sinusoid. So, The term $x[n] e^{-j2\pi n/N}$ represents multiplying the $n$th step around the unit circle with the $n$th input value.

### Exercise (optional)

We saw from Euler's formula previously that projecting the phasor (left fig) onto the imaginary (vertical) axis, would give us a sine wave, i.e. $\sin(t)$.  This isn't quite what we see above.  Instead the sinusoid on the right, looks like the mirror image of $\sin(t)$, i.e. $-\sin(t)$.  Why is this?

### Notes

- Because of the $-j$ term? The phasor is moving clockwise instead of counterclockwise.

So, we can interpret the DFT as taking the dot product between the input and a cosine (and sine) wave of specific frequency.  That is, it tells us if the input sequence repeats itself  with a specific frequency.

### The dot product with the input $x[n]$
It's useful at this point to break the DFT equation into two parts using Euler's rule. This esssentially gives us two dot products (i.e. elementwise multiplication and summation). This allows us to calculate the real and imaginary components of the DFT output separately as a dot product with a cosine (the real part) and a dot product with a sine (the imaginary part):

$$
\begin{align}
DFT[1] &= \sum_{n=0}^{N-1} x[n] \big(\cos(2\pi n/N) -j\sin(2\pi n/N)\big)\\
&= \sum_{n=0}^{N-1} x[n] \cos(2\pi n/N) - j \sum_{n=0}^{N-1} x[n] \sin(2\pi n/N)
\end{align}
$$

### Example:
Let's look at what's happening in these sum terms using a simple sampled square wave with the same period as the $k=1$ phasor for input.  The following visualizes the second sum (i.e. the dot product with $\sin(2\pi n/N)$).

```python

## Make the background and plot the k=1 phasor
X_k1, Y_k1, fig, phasor, iproj, rproj = plot_phasor_static(zn_k1, tsteps, plot_real=True, plot_imag=True)

## Now, let's say our input x looks like this square wave:
# xn = np.array([-1]*8 + [1]*8)

## Exercise: Try some other inputs later
# xn = np.array([-1, -1,-1,-1, 1, 1,1,1]*2)
# xn = np.array([-1]*4 + [1]*8 + [-1]*4)
xn = np.array([1] + [-1, -1,-1, 1, 1,1,1]*2 + [-1])
# xn = zn_imag_k1
#xn = np.concatenate([Ys[np.arange(0, 16, 2)], Ys[np.arange(0, 16, 2)]])
#xn = np.array([1] + [-1, -1,-1, 1, 1,1,1]*2 + [-1])

print("x[n]:", xn)

## Plot the input x[n]:

# with respect to the imaginary component (top right)
iproj.scatter(tsteps, xn, color='C1')

# and the real component (bottom left)
rproj.scatter(xn, tsteps, color='C1')

## Do the elementwise multiplication of input (xn) and the k=1 phasor (zn_k1)
xn_times_zn = xn * zn_k1

## Get the real and imaginary parts
real_xn_zn = np.real(xn_times_zn)
imag_xn_zn = np.imag(xn_times_zn)

## Plot the imaginary parts = x[n]*sin(theta) (top right) in yellow
iproj.plot(tsteps, imag_xn_zn, color='C4')
iproj.scatter(tsteps, imag_xn_zn, color='C4', s=100, alpha=0.5)

## Plot the real parts = x[n]*cos(theta) (bottom left) in yello
rproj.plot(real_xn_zn, tsteps,  color='C4')
rproj.scatter(real_xn_zn, tsteps, color='C4', s=100, alpha=0.5)
```

In the plot above, you can see that:
* The grey dots show our samples from the $k=1$ phasor (top left), and projected imaginary component, i.e. the sine in the DFT sum (top right), and the projected real component, i.e. the cosine (bottom left)
* The blue dots show our input sequence (top right and bottom left)
* the yellow dots show the element wise multiplication of the phasor values and the input, projected on the imaginary and real axes.

Let's just look at the imaginary (sine) part of the $x[n] \cdot z_1[n]$ multiplication (i.e., `xn_times_zn`):
When we multiply the values in both sequences together, we can see that (1)  the values in the input and phasor don't exactly match, but (2) they are always the same sign. That is, the input and the sine component of the $k=1$ phasor are correlated to some extent. In this case, this means that the multiplication terms (in yellow) in this case are all positive. So, adding them all up will give us a positive term for the imaginary component of the DFT.

We can also see that, even though we basically chose this example to match the sine component, we do also get a positive correlation with the real (cos) component.

The following cell shows that you get the same result from doing the dot products with the real (cos) and imaginary (sin) parts of the phasor separately, or doing the dot product with the phasor and then projecting the real and imaginary parts.

```python
## The dot product: sum all the elementwise multiplications
dft_1_real = np.sum(real_xn_zn)
dft_1_imag = np.sum(imag_xn_zn)

print("* projection and then two separate products: DFT[k] = %f + i%f" %(dft_1_real, dft_1_imag))

## check these are the same!
dft_1 = np.sum(xn*zn_k1)
print("* one dot product and then projection: DFT[k] = %f + i%f" %(dft_1.real, dft_1.imag))
```

    * projection and then two separate products: DFT[k] = -2.000000 + i10.054679
    * one dot product and then projection: DFT[k] = -2.000000 + i10.054679

### Exercise: Change the input
What happens when we change the input `xn` so that:

* it has a different period
* exactly matches the sine component of the k=1 phasor
* is out of phase with the sine component of the k=1 phasor
* has a different magnitude
* something else ...

There are some commented out options that you can try in the cell above

### Notes

```python

```

### Example: Phase shifted input

Let's see what happens when our input matches the imaginary component of $k=1$ phasor but has it's phase shifted a bit.  Remember, we can shift the start point of our phasor by multiply the whole phasor sequence a complex number.

$$ \sin(\theta + \phi) = Imag(e^{j\phi}e^{j\theta}) = Imag(e^{j\theta+\phi})$$

```python
## Let's generate our DFT[1] phasor for an input of N=16 points:
N=16

## make an array of N steps: 0,...,N
nsteps = np.array(range(N))
tsteps = 2*np.pi*nsteps/N

## The k=1 phasor:
zn_k1 = np.exp(-1j*tsteps)
```

Now let's create an input that's the same as the sine component of the DFT[1] phasor, but shifted by $\pi/3$ radians.

```python
## Plot the DFT[1] phasor
X_k1, Y_k1, fig, phasor, iproj, rproj = plot_phasor_static(zn_k1, tsteps, plot_real=False, plot_imag=True)

## Now, for the input let's use a phase shifted version of the sine component of our phasor, zn (defined above)
# Remember that multiplying a complex number by e^{j theta} rotates it by theta degrees
# (anticlockwise if theta is positive)
# So to shift the whole sequence we just multiply everything in the sequence by the same complex number

## For this example, let's shift our -sin(2 pi n/N) by pi/3
zn_shift = np.exp(1j*np.pi/3) * zn_k1

## And take as input just the imaginary component of this shifted sine wave
xn = np.imag(zn_shift)

## Plot the phase shifted sine wave it in blue!
iproj.scatter(2*np.pi*nsteps/N, xn, color='C1')
iproj.plot(2*np.pi*nsteps/N, xn, color='C1')

```

In the plot above, you should see the input (in blue) is the same as the sine component of the $k=1$ phasor but *phase shifted*. By multiplying everything by $e^{j\pi/3}$ _delays_ the our $-sin(\theta)$ function by $\pi/3$ seconds (in this example we're assuming that it takes $\theta$ seconds to travel $\theta$ degrees around the phasor).

Now let's see how this effects the DFT output:

```python
 ## Elementwise multiplication of the input and the k=1 phasor
xn_times_zn = xn * zn_k1

## Add it all up to get the dot product
dft_1 = np.sum(xn_times_zn)

print("DFT[1] = %f + i%f" %(dft_1.real, dft_1.imag))
print("in polar coordinates: magnitude=%f, phase=%f" %cmath.polar(dft_1))

## Plot the sequence of multiplications (yellow)
iproj.plot(2*np.pi*nsteps/N, np.imag(xn_times_zn), color='C4')
iproj.scatter(2*np.pi*nsteps/N, np.imag(xn_times_zn), color='C4')
fig
```

The result of the DFT is 6.93 + j4, which in polar coordinates has a magnitude=8 and phase angle=0.52

* Non-zero magnitude means that the input has a frequency component that matches the frequency of the $k=1$ phasor
* Non-zero phase means that the input is like the $k=1$ phasor in frequency, but starting from a different starting angle.

### How does the detected phase angle relate to the phase we used to shift the input?

The magnitude of the DFT output is relatively straightforward to interpret.  The bigger the magnitude, the bigger the amplitude of this frequency component in the input.

When we convert the $DFT[1]$ output to polar coordinates we can, as before, interpret $DFT[1]$ as a scaling (magnitude) and rotation (phase) factor.  We can think of the phase as signalling a bigger amplitude of this frequency in the the input.   We can think of the phase angle as rotating the starting position of the DFT[1] phasor 'clock hand' by that angle. When we convert this to time vs amplitude, this essential means starting from a future point in the sinusoid for positive phase (or a past point for negative phase) relative to the direction the phasor is rotating.

###  DFT phase is relative to cosine!

An important thing to note is that the phase output of the DFT is relative to the **cosine** function with the same frequency as the phasor. This is why the phase value we calculated isn't actually the same as the angle we used to shift the input sequence ($\pi/3=1.047$ radians), since that input sequence was actually based on a sine function.

### Exercise (optional)

Show that our input above $-\sin(t-\pi/3)$ is the same as a cosine wave with the phase shift derived above $\pi/6$.
You'll need to use these trig identities:
$$ \cos(\alpha - \pi/2) = \sin(\alpha) $$
$$\sin(t+\pi) = -\sin(t)$$

**Note** We definitely won't be asking you do these sorts of calculations for assessment, but going through it will help consolidate your intutions about sinusoids and the relationship between cosine and sine waves.

### Notes

```python

```

Even if you don't do the exercise above, we can see that shifting a cosine function by the DFT output phase gives us the same sinusoid as our input.

That is:
$$ \cos(t + \pi/6) = -\sin(t-\pi/3)$$

just by plotting them:

```python
## Plot the input from above and the equivalent cos and sin function based on DFT phase output.
_, _, fig, phasor, iproj, rproj = plot_phasor_static(zn_k1, tsteps, plot_real=False, plot_imag=True)

#fig, phasor, iproj, rproj = create_phasor_iproj_bkg(Tmin, Tmax, ymax=1.5)
#phasor.scatter(zn_real_k1, zn_imag_k1)

## Our input (C1=blue)
tsteps = 2*np.pi*nsteps/N
iproj.scatter(tsteps, xn, color='C1', s=300)
iproj.plot(tsteps, xn, color='C1')

## cos(t + pi/6) (C0=red)
iproj.scatter(tsteps, np.cos(tsteps+np.pi/6), color='C0', s=200)
iproj.plot(tsteps, np.cos(tsteps+np.pi/6), color='C0')

## -sin(t-pi/3) (C5=green)
iproj.scatter(tsteps, -np.sin(tsteps-np.pi/3), color='C5', s=80)
iproj.plot(tsteps, -np.sin(tsteps-np.pi/3), color='C5')
```

In the plot above, you should see:
* the DFT[1] phasor in grey, i.e. $-\sin(t)$
* Our phase shifted input in blue
* The phase shift determined by DFT[1] applied as a cosine wave: $\cos(t + \pi/6)$ in red
* Our phase shifted input generated directly using the `np.sin` function: $-\sin(t-\pi/3)$ in green

You should see is that that the last three functions are all the same! We can always write a sine wave as a cosine wave with a phase shift.

### The DFT for k = 2 and beyond

We can think of DFT[2] as representing the contribution of a phasor that spins around the unit circle
twice as fast as the $k=1$ DFT phasor:

* For $k=2$, Each $e^{i \frac{2\pi n}{N}k}$ is a step of $\frac{2\pi}{N}\times 2$ radians around the unit circle
    * i.e. we only sample every second point compared to the $k=1$ case.

* This means it only takes half the time to make a full cycle. So, this phasor represents a sinusoid with twice the frequency of the one for $k=1$

### Exercise:
Plot sinusoids for different values of `k` using the code below
* What happens when $k=N/2$?
* What happens when $k > N/2$?
* What if you increase $N$?
* How many actual frequencies can the DFT actually tell us about?
* How does this relate to the aliasing problem we saw in the previously?

```python
## Plot the phasor and sinusoid for different values of k

## Number of samples
N=16

## DFT output we're looking at: a sinusoid with k times the freq of the k=1 phasor
k=15

## indices of points in sample
nsteps = np.array(range(N))

## times of samples
tsteps = 2*np.pi*nsteps/N

## N sample values of the kth DFT phasor
zn_k = np.exp(k*-1j*tsteps)

X_k, Y_k, fig, phasor, iproj, rproj = plot_phasor_static(zn_k, tsteps, plot_real=False, plot_imag=True)

```

```python
kphasor_anim= get_phasor_animation(X_k, Y_k, tsteps, phasor, iproj, rproj, fig)
kphasor_anim
```

### Notes

- When $k = N/2$, there are only two samples per period which is not enough to capture the true waveform. (?)
- When $k > N$, a new waveform is created. (?)

## 4.3 The full DFT

Now that we've seen what happens when we calculate the individual DFT components, lets do the whole thing!

We need

1. **Input:** a sequence of $N$ samples
1. generate $N$ phasors, with $N$ sampled points
1. generate $N$ dot products between the input and the phasors
1. **Output:** $N$ complex numbers representing magnitude and phase

The magnitudes and phases give us the decomposition of the input into pure cosine waves

### Set the input $x[n]$

Let's use the same input as above $x[n] = -\sin(2\pi n/N -\pi/3)$

```python
## 1) Input: -sin(t-pi/3) as above
N=16
nsteps = np.array(range(N))
theta_step = 2*np.pi/N
theta_steps = theta_step * nsteps

## set the phase shift
phase_in = np.pi/3

## set the input as -sin with phase shift
x = -np.sin(theta_steps-phase_in)

## Plot the input x
fig, tdom = plt.subplots(figsize=(16, 4))
tdom.scatter(tsteps, x, color='magenta')
tdom.plot(tsteps, x, color='magenta')

```

You should see the input (16 points), `x`, plotted in magenta

### Generate phasors

Let's use some functions to generate all the DFT phasors in one go:

```python
## 2) Generate the phasors
# Given a sequence length N, return dictionary representing N DFT outputs
# the kth element of the return dictionary contains the complex numbers sampled around the unit circle after
# N steps, their real and imaginary parts, and the angles of those complex numbers (magnitude is always 1)
def get_dft_phasors(N, centered=False):

    ## N input steps, N phasors
    nsteps = np.array(range(N))

    ## DFT works for negatively indexed samples, i.e. x[-n], but we don't need this right now!
    if centered:
        nsteps = nsteps - floor(N/2)
        print(nsteps)

    # We know the smallest step around the unit circle we can take is 2pi/N
    theta_step = 2*np.pi/N
    # And we're going to take N steps
    theta_steps = theta_step * nsteps

    ## Generate N phasors
    phasors = {}
    for k in range(N):
        ## samples around the unit circle
        zs = np.exp(k*-1j*theta_steps)
        real = np.real(zs)
        imag = np.imag(zs)

        ## Since we're here, let's return all these things for convenience
        # zs: the phasor samples, real: the real component, imag: the imaginary component
        # theta_steps: the angles for each phasor sample, theta_step: the smallest angle step size

        phasors[k] = {'zs':zs, 'real':real, 'imag':imag, 'thetas':theta_steps, 'theta_step':theta_step}

    return phasors

```

```python
## get the list of phasors
phasors = get_dft_phasors(N)
```

```python
## plot the different phasors and sine (imaginary) components
## You should be able to see the frequency relationship between each DFT component
for k in range(N):
    X_k, Y_k, fig, phasor, iproj, rproj = plot_phasor_static(phasors[k]['zs'], tsteps, plot_real=False, plot_imag=True, color='C'+str(k))

```

You should see in the plots above that phasors $k > N/2$ repeat the frequencies we see for $k < N/2$. For example,
the $k=1$ phasor is equivalent in frequency to the $k=15$ phasor!

### Calculate dot products (i.e.  input and phasor correlations)

```python
## 3) Get the dot products for each for each phasor and the input

def get_dft_outputs(x, phasors):
    DFT = []

    ## Go through our list of N phasors
    for k, phasor in phasors.items():

        ## Do the dot product between the input and each phasor sequence
        DFT.append(np.sum(x * phasor['zs']))

    return DFT

```

```python
## do the DFT
dft = get_dft_outputs(x, phasors)
```

### Get the output  magnitudes and phases

Now we convert the results of the dot products we just calculated into polar form to get magnitudes and phases. We can then plot them!

```python
## convert to polar coordinates to get magnitude and phase
dft_polar = [cmath.polar(z) for z in dft]
mags = [m for m, _ in dft_polar]
phases = [ph if mag > 0.00001 else 0 for mag,ph   in dft_polar]

## we need to put a condition into the phase calculation because phase is calculated by
## a ratio of the imaginary component and the real component. Both of these values might be very, very small.
## This makes it susceptible to floating point errors.
## Then, the ratio of two very small things can actually end up to be quite (spuriously) big!

```

```python
## Plot the magnitudes
#print(mags)
fig, freqdom = plt.subplots(figsize=(16, 4))
freqdom.set(xlim=(-1, N), ylim=(-1, 10))
freqdom.scatter(range(N), mags)
```

```python
## plot the phase angle for each DFT component
#print(phases)
fig, fdom = plt.subplots(figsize=(16, 4))
fdom.set(xlim=(-1, N), ylim=(-2, 2))
fdom.scatter(range(N), phases)
```

### How do we interpret the magnitude and phase graphs?

The top plot shows the magnitude response of the DFT with respect to our input.  The bottom plot shows the phase respose.

The magnitude plot shows positive magnitudes for the 1st and the N-1th DFT components, and zero for all the rest.  This means that the input is a sinusoid with the same frequency as the 1st DFT component.

The positive phase for the first component indicates a phase shift.  We see the opposite phase shift for the 15th component because the phasor for that has the same frequency but rotates the opposite way.

So, this is what we expect since we generated the input as exactly the sine projection of the k=1 phasor, just shifted by $\pi/3$ radians!

### Plotting DFT outputs on the complex plane
So far, we've looked at the real and imaginary components of the DFT outputs separately.  But we can also think about their properties on the complex plane.

The following plots the DFT outputs for the example above on the complex plane.

```python
## Plot the actual DFT outputs as complex numbers on the complex plane
## For the example above, we can see that most of the outputs are at (0,0), i.e. not contribution
## but we get positive magnitudes for the 1st and 15th component, with the same
## magnitude by 'mirrored' phase angle:

ymax=(N/2) + 2
fig, ax = plt.subplots(figsize=(5, 5))
ax.set(xlim=(-10, 10), ylim=(-10, 10))
ax.plot([-ymax,ymax], [0,0], color='grey')
ax.plot([0,0],[-ymax,ymax], color='grey')
circle1 = plt.Circle((0, 0), 1, color='grey',fill=False)

ax.add_artist(circle1)

for k in range(N):
    zs = phasors[k]['zs']
    xz = x * zs
    dftk = sum(xz)
    #print(dftk, dftk.real, cmath.polar(dftk))
    ax.plot(dftk.real, dftk.imag, 'o')

    if dftk.real > 0.0001:
        print("k=%d, %f + j%f" % (k, dftk.real, dftk.imag))

```

You can see from this plot that all but two of the DFT outputs have zero magnitude.  The two that have non-zero magnitude are the 1st and 15th components.  You can see that they have the same magnitude, but mirror each other in phase (they are complex conjugates). This symmetry is something you see a lot of in digital signal processing.  We won't go into it in this class, but for one thing we can take advantage of to save some computations!

### Exercise (optional)

You can use the code below to visualize the dot products of the input with with cos (real) and sin (imaginary) parts of each DFT component by varying $k$:

* What's the relationship between the DFT phasors for $k=1$ and $k=N-1$ in terms of the dot product between the real (left plot) and imaginary (right plot) components.
* What's happen for components that don't have the same frequency as the input? e.g. $k=2$

```python
## 1) Input: -sin(t-pi/3) as above
nsteps = np.array(range(N))
theta_step = 2*np.pi/N
theta_steps = theta_step * nsteps

Tmin = np.min(theta_steps)
Tmax = np.max(theta_steps)

phase_in = np.pi/3
x = -np.sin(theta_steps-phase_in)

fig, tdom = plt.subplots(figsize=(16, 4))
tdom.scatter(tsteps, x, color='magenta')
tdom.plot(tsteps, x, color='magenta')

```

```python
## let's break it down into cos and sin components
k=15

## zcos is our sampling of the real part of the kth phasor
zcos = phasors[k]['real']
zsin = phasors[k]['imag']

## the elementwise multiplication between the input x and the real part of the phasor, zcos
xz_real = x * zcos
xz_imag = x * zsin

## This initializes our plots, makes them the appropriate size etc, but leave the plot empty
_, _, fig, phasor, iproj, rproj = plot_phasor_static(phasors[k]['zs'], tsteps, plot_phasor=False, plot_real=True, plot_imag=True, color='grey')

## plot the zero line: if the sequence we get after the elementwise multiplication is
## symmetric around zero, we know that the dot product will be zero since we're adding all the
## values together
#inusoid.plot([Tmin-1,Tmax+1], [0,0], color='grey')

## Plot the input in magenta
rproj.plot(tsteps, x, color='magenta')
rproj.scatter(tsteps, x, color='magenta')

## Plot the elementwise multiplication in orange
rproj.plot(tsteps, xz_real, color='orange')
rproj.scatter(tsteps, xz_real, color='orange')

## Plot the input in magenta
iproj.plot(tsteps, x, color='magenta')
iproj.scatter(tsteps, x, color='magenta')

## Plot the elementwise multiplication in orange
iproj.plot(tsteps, xz_imag, color='orange')
iproj.scatter(tsteps, xz_imag, color='orange')

## Add it all up to get the dot product
dftk_real, dftk_imag = sum(xz_real), sum(xz_imag)
print("dft[%d] = %f + j%f "  % (k, dftk_real, dftk_imag))
```

### Notes

```python

```

```python

```

```python

```

```python

```

```python

```


# Direct Optimal Control Plotting

```python
import sys; sys.path.append(2*'../') # go n dirs back
from src import *

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

```python
import time
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

from torchdyn.core import NeuralODE
from torchdyn.datasets import *
from torchdyn.numerics import odeint, Euler, HyperEuler
```

## Optimal Control

We want to control an inverted pendulum and stabilize it in the upright position. The equations in Hamiltonian form describing an inverted pendulum with a torsional spring are as following:

$$\begin{equation}
    \begin{bmatrix} \dot{q}\\ \dot{p}\\ \end{bmatrix} =
    \begin{bmatrix}
    0& 1/m \\
    -k& -\beta/m\\
    \end{bmatrix}
    \begin{bmatrix} q\\ p\\ \end{bmatrix} -
    \begin{bmatrix}
    0\\
    mgl \sin{q}\\
    \end{bmatrix}+
    \begin{bmatrix}
    0\\
    1\\
    \end{bmatrix} u
\end{equation}$$

```python
class ControlledPendulum(nn.Module):
    """
    Inverted pendulum with torsional spring
    """
    def __init__(self, u, m=1., k=.5, l=1., qr=0., β=.01, g=9.81):
        super().__init__()
        self.u = u # controller (nn.Module)
        self.nfe = 0 # number of function evaluations
        self.cur_f = None # current function evaluation
        self.cur_u = None # current controller evaluation
        self.m, self.k, self.l, self.qr, self.β, self.g = m, k, l, qr, β, g # physics

    def forward(self, t, x):
        self.nfe += 1
        q, p = x[..., :1], x[..., 1:]
        self.cur_u = self.u(t, x)
        dq = p/self.m
        dp = -self.k*(q - self.qr) - self.m*self.g*self.l*torch.sin(q) \
            -self.β*p/self.m + self.cur_u
        self.cur_f = torch.cat([dq, dp], -1)
        return self.cur_f

from math import pi as π
x_star = torch.Tensor([0., 0.]).to(device)
cost_func = IntegralCost(x_star, P=1, Q=1) # final position is more important

# Time span
dt = 0.2
t0, tf = 0, 3 # initial and final time for controlling the system
steps = int((tf - t0)/dt) + 1 # so we have a time step of 0.2s
t_span = torch.linspace(t0, tf, steps).to(device)

```

```python
u_euler = torch.load('saved_models/u_euler.pt')
u_hyper = torch.load('saved_models/u_hyper.pt')
u_mp = torch.load('saved_models/u_mp.pt')
u_rk4 = torch.load('saved_models/u_rk4.pt')
```

```python
# Testing the controller on the real system
frac = 0.95
init_dist = torch.distributions.Uniform(torch.Tensor([-frac*π, -frac*π]), torch.Tensor([frac*π, frac*π]))
x0 = init_dist.sample((50,)).to(device)
t0, tf, steps = 0, 5, 10*10 + 1 # nominal trajectory
t_span_fine = torch.linspace(t0, tf, steps).to(device)
sys = ControlledPendulum(None)

sys.u = u_euler; _, traj_eu = odeint(sys, x0, t_span_fine, solver='tsit5', atol=1e-5, rtol=1e-5)
sys.u = u_hyper; _, traj_hyper = odeint(sys, x0, t_span_fine, solver='tsit5', atol=1e-5, rtol=1e-5)
sys.u = u_mp; _, traj_mp = odeint(sys, x0, t_span_fine, solver='tsit5', atol=1e-5, rtol=1e-5)
sys.u = u_rk4; _, traj_rk4 = odeint(sys, x0, t_span_fine, solver='tsit5', atol=1e-5, rtol=1e-5)

print(cost_func(traj_eu), cost_func(traj_hyper), cost_func(traj_mp), cost_func(traj_rk4))

t_span_fine = t_span_fine.detach().cpu()
traj_eu, traj_hyper, traj_mp, traj_rk4 = traj_eu.detach().cpu(), traj_hyper.detach().cpu(), traj_mp.detach().cpu(), traj_rk4.detach().cpu()

# Plotting
fig, axs = plt.subplots(4, 2, figsize=(16,8))
for i in range(len(x0)):
    axs[0,0].plot(t_span_fine, traj_eu[:,i,0], 'r', alpha=.3, label='p' if i==0 else None)
    axs[0,1].plot(t_span_fine, traj_eu[:,i,1], 'r-.', alpha=.3, label='q' if i==0 else None)
    axs[1,0].plot(t_span_fine, traj_hyper[:,i,0], color='orange', alpha=.3, label='p' if i==0 else None)
    axs[1,1].plot(t_span_fine, traj_hyper[:,i,1], color='orange', linestyle='-.', alpha=.3, label='q' if i==0 else None)
    axs[2,0].plot(t_span_fine, traj_mp[:,i,0], color='green', alpha=.3, label='p' if i==0 else None)
    axs[2,1].plot(t_span_fine, traj_mp[:,i,1], color='green', linestyle='-.', alpha=.3, label='q' if i==0 else None)
    axs[3,0].plot(t_span_fine, traj_rk4[:,i,0], color='purple', alpha=.3, label='p' if i==0 else None)
    axs[3,1].plot(t_span_fine, traj_rk4[:,i,1], color='purple', linestyle='-.', alpha=.3, label='q' if i==0 else None)

# ax.legend()
# ax.set_title('Controlled trajectories')
# ax.set_xlabel(r"$t~[s]$")
```

Great 🎉

Training the controller with `HyperEuler` resulted in a working controller stabilizing the system in the given time; more importantly, while in the first part we used a high accuracy solver to obtain similar results training with the same number of epochs and all the same hyperparameters but using the hypersolver took less than time than higher-order solvers! Now, let's see the results

```python
# plt.rcParams.update({
#     "text.usetex": True,
#     "font.family": "serif",
#     "font.serif": ["Palatino"],
# })
```

```python
# Cool plots

from matplotlib import colors, cm
import matplotlib.gridspec as gridspec

norm = colors.Normalize(vmin=0, vmax=16)

controller = [u_hyper, u_euler, u_mp, u_rk4]
colors = ['tab:orange', 'tab:red', 'tab:green', 'tab:purple']
labels = ['HyperEuler', 'Euler', 'Midpoint', 'RK4']
cmap='bone'
titles = ['HyperEuler', 'Euler', 'Midpoint', 'RK4']
sys = []

fig = plt.figure(figsize=(11,5), constrained_layout=False)
spec = gridspec.GridSpec(ncols=4, nrows=8, figure=fig)

# Time span
t0, tf = 0, 5 # initial and final time for controlling the system
steps = int((tf - t0)/dt) + 1 # so we have a time step of 0.2s
t_span = torch.linspace(t0, tf, steps*20).to(device) # nominal

lim = π
graph_lim = lim*1.6
# x0 = torch.Tensor(50, 2).uniform_(-lim, lim).to(device)
x0_test = init_dist.sample((1000,)).to(device) # test on 1000 trajectories from random points
axs = []
axs_q, axs_p = [], []
for i in range(4):
    axs.append(fig.add_subplot(spec[:4, i]))
    axs_q.append(fig.add_subplot(spec[4:6, i]))
    axs_p.append(fig.add_subplot(spec[6:8, i])) #, sharex=axs_q[-1]

for u, ax, i in zip(controller, axs, range(len(controller))):
    sys.append(ControlledPendulum(u).to(device))
    n_grid = 50
    x = torch.linspace(-graph_lim, graph_lim, n_grid).to(device)
    Q, P = torch.meshgrid(x, x) ; z = torch.cat([Q.reshape(-1, 1), P.reshape(-1, 1)], 1)
    f = sys[-1](0, z).detach().cpu()
    Fq, Fp = f[:,0].reshape(n_grid, n_grid), f[:,1].reshape(n_grid, n_grid)
    val = sys[-1].u(0, z).detach().cpu()
    U = val.reshape(n_grid, n_grid)
    ax.streamplot(Q.T.detach().cpu().numpy(), P.T.detach().cpu().numpy(),
                  Fq.T.detach().cpu().numpy(), Fp.T.detach().cpu().numpy(), color='black', density=0.6, linewidth=0.5)

    ax.set_xlim([-graph_lim*0.7, graph_lim*0.7]) ; ax.set_ylim([-graph_lim, graph_lim])
    traj = odeint(sys[-1], x0, t_span,  solver='dopri5')[1].detach().cpu()
    for j in range(traj.shape[1]):
        ax.plot(traj[:,j,0], traj[:,j,1], c=colors[i], alpha=.4, label=labels[i] if j==0 else "")
        axs_q[i].plot(t_span, traj[:,j,0], c=colors[i], alpha=.4)
        axs_p[i].plot(t_span, traj[:,j,1], c=colors[i], alpha=.4)

    traj_test = odeint(sys[-1], x0_test, t_span,  solver='dopri5')[1].detach().cpu()
    print(titles[i], ' loss:\n', cost_func(traj.to(device)).item())
    distance_mean, distance_std = traj_test[-1,:,0].mean(), traj_test[-1,...,0].std()
    # Not actually "error", this the mean value of positions and std
    print(titles[i], ' error mean:', distance_mean, '  error_std: ', distance_std)

    ax.set_xlabel(r'$q$',  labelpad=-10)
    ax.set_ylabel(r'$p$',  labelpad=0)
    ax.set_xticks([-3,  3])
    ax.set_yticks([-5,  0, 5])
    ax.set_title(titles[i], family='cursive')

plt.suptitle('Pendulum Controlled Trajectories',fontsize=16, weight='bold', y=0.97)

axs_q[0].set_ylabel(r'$q(t)$', labelpad=0)
axs_p[0].set_ylabel(r'$p(t)$', labelpad=0)
for axq, axp in zip(axs_q, axs_p):
    axq.set_xticks([0, 1, 2, 3, 4, 5])
    axp.set_xticks([0, 1, 2, 3, 4, 5])
    axp.set_xlabel(r'$t$',  labelpad=0)
    axq.tick_params(labelbottom=False)
    axq.set_yticks([-4, 0, 4])
    axp.set_yticks([-5, 0, 5])
    axq.set_ylim([-4, 4])
    axp.set_ylim([-5, 5])

axs_q[0].set_yticks([-4, 0, 4])
axs_p[0].set_yticks([-5, 0, 5])

for i in range(1, 4, 1):
    axs_q[i].tick_params(labelleft=False)
    axs_p[i].tick_params(labelleft=False)

pad = 3
axs[0].annotate("Phase Space", xy=(0, 0.5), xytext=(-axs[0].yaxis.labelpad - pad, 0),
                xycoords=axs[0].yaxis.label, textcoords='offset points',
                size='large', ha='right', va='center', rotation='vertical', fontweight='bold')

axs_q[0].annotate("Position", xy=(0, 0.5), xytext=(-axs_q[0].yaxis.labelpad - pad, 0),
                xycoords=axs_q[0].yaxis.label, textcoords='offset points',
                size='large', ha='right', va='center', rotation='vertical', fontweight='bold')

axs_p[0].annotate("Momentum", xy=(0, 0.5), xytext=(-axs_p[0].yaxis.labelpad - pad, 0),
                xycoords=axs_p[0].yaxis.label, textcoords='offset points',
                size='large', ha='right', va='center', rotation='vertical', fontweight='bold')
fig.tight_layout(h_pad=0, w_pad=-0.1)

# Saving
# import tikzplotlib
fig.savefig('media/pendulum_control.pdf',  bbox_inches = 'tight')
# tikzplotlib.save("media/pendulum_control.tex")
```

## Calculating MACs and plotting losses
We can use the number of MACs instead of NFEs so we obtain a simpler

```python
from ptflops import get_model_complexity_info

bs = 1024 # batch size we used in training
def get_macs(net:nn.Module):
    params = []
    for p in net.parameters(): params.append(p.shape)
    with torch.cuda.device(0):
        macs, _ = get_model_complexity_info(net, (bs, params[0][1]), as_strings=False)
    return int(macs)
```

```python

controller = nn.Sequential(
                nn.Linear(2, 64),
                nn.Softplus(),
                nn.Linear(64, 64),
                nn.Softplus(),
                nn.Linear(64, 64),
                nn.Tanh(),
                nn.Linear(64, 1))

hypersolver = nn.Sequential(nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 1)).to(device)

hs_macs = get_macs(hypersolver)
u_macs = get_macs(controller)
print('Controller MACs per NFE:', u_macs, '\nHypersolver MACs per NFE:', hs_macs)
```

```python
unit_mac_hyper = u_macs + hs_macs
unit_mac_euler = u_macs
unit_mac_midpoint = u_macs*2 # midpoint has to evaluate the vector field twice per NFE
unit_mac_rk4 = u_macs*4 # rk4 has to do it 4 times
```

```python
multiplier = 2 # mac to flops
macs_hyper = unit_mac_hyper*multiplier
macs_euler = unit_mac_euler*multiplier
macs_midpoint = unit_mac_midpoint*multiplier
macs_rk4 = unit_mac_rk4*multiplier
```

## Getting FLOPs per each evaluation

```python
# # losses from above
# # Flops are 2*GMACs
# # https://github.com/sovrasov/flops-counter.pytorch/issues/16

print('HyperEuler FLOPs:', macs_hyper)
print('Euler FLOPs:', macs_euler)
print('Midpoing FLOPs:', macs_midpoint)
print('RK4 FLOPs:', macs_rk4)

```

    HyperEuler FLOPs: 17367492
    Euler FLOPs: 17170818
    Midpoing FLOPs: 34341636
    RK4 FLOPs: 68683272

```python
# Ratio
print('Hyper vs euler:', macs_hyper/macs_euler*100, '%')
print('Hyper vs midpoint:', 1-(macs_hyper/macs_midpoint)*100, '%')
```

    Hyper vs euler: 101.14539680054845 %
    Hyper vs midpoint: -49.57269840027423 %


# What is inversion?

For those with little or no background or experience, the concept of inversion can be intimidating. Many available resources assume a certain amount of background already, and for the uninitiated, this can make the topic difficult to grasp. Likewise, if material is too general and simplistic, it is of little use to someone who has some understanding but is looking for deeper insight. This suite of tutorials will begin in earnest from the first module, but if you have no background, then this is the place to start. I will assume that you have had some basic linear algebra and matrix theory, and little else. I will also assume, at least at first, that you may have forgotten a few of these things along the way. So let's begin from zero, with the absolute simplest terms:

In algebra, when we want to solve an equation for an unknown variable $x$, say in the equation:

\begin{equation}
ax=b
\end{equation}

where $a$ and $b$ are real numbers, we can do so, quite simply, by multiplying both sides of the equation by the *inverse* of $a$, that is $a^{-1}$. Our result $x = b/a$, is, in a very simple way, the solution to an inverse problem. And even though this example is trivial, inversion is simply an expansion of this process to larger systems of equations with more unknowns. Expanding ever so slightly, we do an analogous process by obtaining the inverse of a matrix. For example, say we have a system of equations that we represent in a square matrix $A$, a vector of unknowns $x$, and a vector of known values $b$. What we constuct is the matrix equation

\begin{equation}
Ax=b
\end{equation}

and provided an inverse for $A$ exists (that is, if $det(A) \neq 0$, or if you prefer $A^{-1}A = I$), the solution can be found by multiplying both sides by the inverse of $A$

\begin{equation}
A^{-1}Ax= Ix = x = A^{-1}b \\
x = A^{-1}b
\end{equation}

If you are the type of person that likes to see things through simple examples, consider the following. Say we have a set of two equations and two unknowns:

\begin{equation}
2x +0y = 4 \\
0x +3y = 6 \\
\end{equation}

Now, because this is a trivial example, we can see from inspection that $x=2$ and $y=2$. But for the purpose of seeing how things are put together, let's solve this using matrices anyway. We can assemble the coefficients for each equation into the matrix equation:

\begin{equation}
\begin{bmatrix}
    2 &0\\
    0 &3\\
\end{bmatrix}
\left[
\begin{array}{c}
x\\
y
\end{array}
\right]
=
\left[
\begin{array}{c}
4\\
6
\end{array}
\right]
\end{equation}

This is an equation of the form $Ax=b$, and we can solve this by multiplying both sides of the equation by $A^{-1}$:

\begin{equation}
\begin{bmatrix}
    \frac{1}{2} &0\\
    0 &\frac{1}{3}\\
\end{bmatrix}
\begin{bmatrix}
    2 &0\\
    0 &3\\
\end{bmatrix}
\left[
\begin{array}{c}
x\\
y
\end{array}
\right]
=
\begin{bmatrix}
    \frac{1}{2} &0\\
    0 &\frac{1}{3}\\
\end{bmatrix}
\left[
\begin{array}{c}
4\\
6
\end{array}
\right]
\end{equation}

Multiplying this out on both sides gives us (unsurprisingly):

\begin{equation}
\left[
\begin{array}{c}
x\\
y
\end{array}
\right]
=
\left[
\begin{array}{c}
2\\
2
\end{array}
\right]
\end{equation}

Again, this is a trivial example. Now we will build up the notation a little. Thus far I have used the standard matrix-vector equation ($Ax=b$) that you have likely seen in a basic linear algebra textbook. In this set of tutorials I will use slightly different notation, and our matrix equation will take the form $Gm=d$. Of course, the choice of symbols for matrices and vectors is arbitrary, but in the context that we are going to proceed, $G$, $m$, and $d$ will take on new meaning. We represent the right hand side of the equation by $d$, which is the *data* that we measure in the field, $m$ which represents our *model parameters* (the unknowns that we seek), and $G$, which is our *forward operator* that operates on the model to produce the data. Generally, the governing equations that go into the physical system are represented by our matrix $G$. And even more generally, our matrix $G$ is a special case of a more general idea of a forward operator, $F[m]$. For a geophysical survey the a broader representation of our problem, for the ith data point would be

\begin{equation}
F_i[m] = d_i + n_i
\end{equation}

where $F_i[m]$ is the forward operator, $d_i $ is the measured data and $n_i$ is additive noise. This distinction is important as it adds a degree of richness to our scenario. Here are a few points to consider:

First  $F[m]$ does not need to be linear. You may recall that a transformation is considered *linear* if the following property holds:

\begin{equation}
T(\alpha u+ \beta v) = \alpha T(u) + \beta T(v)
\end{equation}

In the most general case our forward operator $F[m]$ can be linear or nonlinear.

 In the case where  $F[m]$ is a matrix operation (our $G$, above), it might be a square matrix that is not invertible.

 $F[m]$  might represent an  overdetermined system (i.e. $G$ is not square matrix, but is a "tall" matrix).

 $F[m]$  might represent an underdetermined system (i.e. $G$ is not square matrix, but is a "wide" matrix).

 In geophysical applications, the number of values is large, and the doing these operations directly on large data sets is impractical.

 In geophysics we deal with physical systems, and physical systems will always have noise in the data, which is accounted for in the $n_i$ term above.

 We will need to estimate the uncertainties in our data, again, because we are dealing with physical systems.

 And it can never be emphasized enough, so I will introduce it now: geophysics problems produce nonunique solutions. Obtaining a solution will involve optimization theory and will require implementing what we call a "regularization." The solution that we get will depend heavily, very heavily,  on our choice of regularization. This choice is critical as it makes the difference between arriving at a solution that makes logical (and geological) sense, and complete gibberish.

The end goal of this set of tutorials is to understand and learn how to manage some of the complexities that go into the inversion process. I will, whenever possible, try to build intuition by first using simple systems and trivial solutions, and then develop these into more complex (and more useful) versions of the same.

### Forward modeling vs. inversion

Forward modeling and inversion are opposite processes. When the model of the Earth is known and we want to calculate the data, we call this a forward model. Forward modeling answers the question: given that I know the physics and have a model of the earth, what will the measured data look like? In the matrix-vector terms we used above, $Gm=d$, the forward model has two knowns, $G$ and $m$, and one unknown, $d$. Calculating the data is done by applying our forward operator $G$ to our model parameters, $m$. Inversion, as I have stated above already, takes measured data $d$ and a known forward operator $G$ and solves for the model parameters $m$. Put slightly differently, given that we have (1) field survey observations, (2) an estimate about the errors in these observations, (3) a physical representation of the earth, and (4) the ability to produce a forward model, the goal of the inversion is to produce a reasonable model that generated data.

### Types of Inversion Problems

Various methodologies for performing geophysical inversion have been developed. There are two broad classes of inversion: "Parametric" methods and "Generalized" inversion methods.

**Parametric methods**

These inversion methods involve finding a model of the earth which is described using only a few parameters. The solutions require that there be fewer parameters than there are data values so that the problem is formally "over-determined." A few examples of parametric models are:
 - **Buried object:** parameters could be depth to a sphere (or cylinder), a radius of a sphere or radius and length of a cylinder, and the physical property contrast between the object and host rocks.
 - **Layered earth:** parameters are layer thicknesses and physical property values.
 - **A buried sheet:** parameters might be depth to the top of sheet, it's dip, strike, thickness, and the physical property contrast between the sheet and host rocks.

**Generalized inversion methods**

This second class of inversion methods allows the earth's model to be more realistically complex, which means that more parameters than data points are permitted. Such problems are mathematically referred to as "under-determined". Most solutions to this more general form of the geophysical inversion problem involve three steps, which can be explained as follows:

1. Represent the earth with many cells so that complex distributions of physical properties can be simulated. In practice, the earth is divided into thousands or millions of cells of fixed geometry. Each cell has a constant, but unknown, value. The parameters we seek are the physical property values for these cells.

2. Design a model objective function. This is a mathematical quantity which measures the "size" of any solution. It is a single number. A priori information about the earth can be incorporated into the objective function. Usually the model objective has different components. One will make it "close" to a supplied reference model, others may control "smoothness" in various spatial directions. Mathematical optimization theory is used to find a solution that minimizes the objective function. The resultant solution will have minimum structure. This will be a good choice since it will tend to show large scale important features rather than a great deal of extraneous structure that can result from noisy observations.

3. The final solution must also acceptably reproduce the field observations. Our final optimization problem is to find that model which minimizes our model objective function subject to the constraint that the measured data are adequately reproduced.

In practice a number of inversions, with different reasonable objective functions, should be carried out so the interpreter has some insight about the range of earth models that can acceptably reproduce the field data. Error statistics about the data will determine how closely the reproduced data matches the real measured data. The fact that these error statistics are often poorly known is a second good reason for performing several inversions before settling upon a preferred model.

### Inversion as a workflow

Even though in the back of our minds we are really just solving a matrix equation, the complications that arise in practical situations will require inversion to take the form of a multi-step process, which is often represented as a workflow.

Each subsequent module will begin to discuss one of the boxes in the workflow, and we will build up the entire process, step by step, until completion. The first module of this tutorial, then, will begin with setting up the problem—taking a continous distribution of a physical property, discretizing it, and putting it on a mesh.

Final Drafts/.ipynb_checkpoints/Module 0, a quick overview-checkpoint.ipynb


# GEOL351 Lab 9
# Multiple Climate Equilibria: Icehouse vs Hothouse
## Student Name :  [WRITE YOUR NAME HERE]

In this lab, you will explore, tweak and tinker with the zero-dimensional climate model seen in class. This model, and much of the following lab, is strongly inspired by Ray Pierrehumbert's excellent book, [Principles of Planetary Climate](http://www.cambridge.org/raypierre).

We will be working in the iPython / Jupyter notebook system. We like these because they are a form of literate programming in which we can mix textbook instruction and explanations with code that can also be run and edited. You will be asked to run some cells, tweak parameters and write down results. **Please save the resulting notebook once you are done; this is what you will be handing out**.

Think you can't code? Be reassured, we will guide you by the hand. The beauty of iPython notebooks is that they integrate the code very naturally in a scholarly document, and the most you will have to do is sliughtly edit the code and watch how this affects the results. This should allow you to focus on the dynamics, not so much the informatics.

Let's start with a little preliminary learning.

## Part 0: iPython notebooks

### Python

Haven't you always wanted to code? Thanks to [Python](https://www.python.org/), anyone with a modicum of common sense and a keyboard can do so. Python is a high-level language whose syntax makes it very difficult to be a bad programmer. For a few of the many reasons why you should pick up Python before your education is over, read [this](http://www.nature.com/news/programming-pick-up-python-1.16833?WT.ec_id=NATURE-20150206).

### Markdown

You can document your iPython notebooks by making some cells into [Markdown Website](http://daringfireball.net/projects/markdown/) cells. Markdown is a way of formatting text that is supposed to be almost as readable un-rendered as when it is tidied up.  If you look at the **Markdown** cells as source code (by double-clicking on them) you will see how the raw text looks. If you can deal with the unwieldy and ugly Microsoft Word, you can certainly handle Markdown.

You will be expected to turn in your lab for grading as a notebook, so you should write all your notes, observations, and concluions in the notebook.

### Maths

In a browser, you can render beautiful equations using a javascript tool called [Mathjax Website](http://docs.mathjax.org) which is build into the iPython notebooks. It is based on a [LaTeX](https://www.latex-project.org/) engine,  You can build in symbols to your text such as $\pi$ and $\epsilon$ if you use the \$ signs to indicate where your equations begin and end, and you know enough $\LaTeX$ [try it here !](http://www.codecogs.com/latex/eqneditor.php) to get by.

Equations in 'display' mode are written like this (again look at the source for this cell to see what is used)

\\[ e^{i\pi} + 1 = 0 \\]

or even like this

\begin{equation}
%%
    \nabla^4 \psi = \frac{\partial T}{\partial x}
%%
\end{equation}

Go back to the rendered form of the cell by 'running' it.

## A zero-dimensional climate model

The dynamics are governed by conservation of energy, which takes the form of an ordinary linear equation:

$ C_s \frac{d T_e}{dt} = \left(1 -\alpha(T_e) \right) \frac{S_{\circ}}{4} -   \sigma {T_e}^4 \equiv G(T_e)$ [Eq (1)]

with $T_e$ the *emission temperature* and the albedo $\alpha$ is a nonlinear function of climate state (indexed by $T_e$, which is therefore the *state variable* for the system).  $\alpha$ is given by :

$\alpha(T) = \begin{cases}
\alpha_i & \text{for } T \leq T_i \\
\alpha_0 + (\alpha_i- \alpha_0) \frac{(T-T_0)^2}{(T_i-T_0)^2} & \text{for } T_i < T < T_0 \\
\alpha_0 & \text{for } T \geq T_0 \\
 \end{cases} $ [Eq (2)]

By definition, the emission of radiation to outer space takes place at some height $z_rad$ above the ground, corresponding to a pressure $p_\text{rad}$. The greenhouse effect can be understood as a tendency to raise the emission level, hence lower $p_\text{rad}$. Choosing $p_\text{rad} = 670 mb$ yields a climate not to dissimilar to our current one.

** Question 1: ** Is the planet's emission of electromagnetic radiation in the longwave or shortwave domain? What does this depend on?

** Answer 1 ** : PLEASE WRITE YOUR ANSWER HERE

The other relation we need is the relationship between the surface temperature $T_s$ and $T_e$:

 $T_s = T_e \left(\frac{p_s}{p_\text{rad}} \right)^{\kappa}$ where  $\kappa = 2/7$

 If we assume that $p_s$ and $p_\text{rad}$ are both constant, working with $T_e$ is equivalent to working with $T_s$, which we shall do from now on.

Now, in order to get going, we need to set up a few preliminaries. First, let's import other bits and pieces of Python code to save us some time.

```python
%matplotlib inline
# ensures that graphics display in the notebook

import matplotlib
import numpy as np
import matplotlib.pyplot as plt

import sys
sys.path.append("./CoursewareModules")

from ClimateUtilities import *  # import Ray Pierrehumbert's climate utilities
import phys
import seaborn as sns
```

## Outgoing Long-wave radiation
Let us define a function that will compute outgoing longwave emission from the planet for a given emission temperature $T$ (dropping the subscript for convenience).

```python
def OLR(T,param=None):
    pRad = param
    return phys.sigma * (T**4.)*(pRad/1000.)**(4.*2./7.)
```

The radiating pressure pRad, in mb, is passed as the
optional second parameter. It can be used to
pass parameters of other OLR expressions, e.g. CO2 concentrations. Let's see how this depends on T.

## Incoming solar radiation

Similarly, let's define a function for the albedo, $\alpha(T)$.

```python
def albedo(T):
    if T < T1:
        return alpha_ice
    elif (T >= T1)&(T<=T2):
        r = (T-T2)**2/(T2-T1)**2
        return alpha_0 + (alpha_ice - alpha_0)*r
    else:
        return alpha_0
```

Notice how the function uses "global" arguments  alpha_ice, alpha_ice, T1, T2. We need to define those for the function to work. Let's do this, along with defining pRad.

```python
L = 1.*1370.
alpha_ice = .6
alpha_0 = .2
T1 = 260. # in Kelvins
T2 = 290. # in Kelvins
pRad = 1000. # in mb
```

The next cell generates lists of values for OLR, the next flux, and the incoming radiation given a range of temperature values from 200 to 340 K.

```python
Tlist = [200.+ 2.*i for i in range(70)]
SabsList = []
OLRlist = []
NetFluxlist = []
Glist = []
aList = []

for T in Tlist:
    aList.append(albedo(T))  # albedo function
    SabsList.append((L/4.)*(1.-albedo(T))) # incident solar values
    OLRlist.append(OLR(T,pRad)) # outgoing longwave emissions
    NetFluxlist.append((L/4.)*(1.-albedo(T)) - OLR(T,pRad))  # net flux
    Glist.append(4.*OLR(T,pRad)/(1.-albedo(T)))  # balance function
```

Time to plot a few things! First, the albedo:

```python
c1 = Curve()
c1.addCurve(Tlist)
c1.addCurve(aList,'Albedo','Alb')
c1.Xlabel = 'Temperature (K)'
c1.Ylabel = 'Alpha'
c1.PlotTitle = 'Planetary Albedo'
w1 = plot(c1)
plt.ylim((0, 0.8))

```

## Energy Balance
Next, let's look at energy balance. What's coming in, what's going out?

```python
c1 = Curve()
c1.addCurve(Tlist)
c1.addCurve(SabsList,'Sabs','Absorbed Solar')
c1.addCurve(OLRlist,'OLR','Outgoing Longwave')
c1.Xlabel = 'Temperature (K)'
c1.Ylabel = 'Flux ($W/m^2$)'
c1.PlotTitle = 'Energy Balance Diagram'
w1 = plot(c1)
```

Another way to look at this is to graph the difference between these curves (which we called $G(T)$ above)

```python
c2 = Curve()
c2.addCurve(Tlist)
c2.addCurve(NetFluxlist,'Net','Net Flux')
c2.addCurve([0. for i in range(len(Glist))],'Zero','Equilibrium')
c2.Xlabel = 'Temperature (K)'
c2.Ylabel = 'Net Flux ($W/m^2$)'
c2.PlotTitle = 'Stability diagram'
w2 = plot(c2)

```

### Question 2:
Solving graphically, **for which values of the solar luminosity L do we get 1, 2 or 3 equilibria?**

### Answer 2:
Write your answer here

What you just did, informally, is to draw a **bifurcation diagram** for the system. Now we can be a lot smarter about this, and instead of typing like monkeys hoping to recreate Shakespeare (or Pierrehumbert as the case may be), we can try to solve for $G(T) = 0$.

### Question 3:
**Setting $G(T) = 0$, derive an analytical expression for $L_{eq}$, the value of $L$ for which incoming solar radiation balances exactly outgoing longwave radiation. **

### Answer 3:

Write your answer here

## Solar Bifurcation

Next we plot this value as a function of $T$. However, let's introduce a little twist: $T$ of course, is a consequence of the value of L and pRad, not a cause of it. So we should switch the X and Y axes so that we get $T = f(L)$, not $ L = f(T)$.

```python
c3 = Curve()
c3.addCurve(Tlist)
c3.addCurve(Glist,'G','Balance Function')
c3.addCurve([L for i in range(len(Glist))],'S','Incident Solar')
c3.switchXY = True #Switches axis so it looks like a hysteresis plot
c3.PlotTitle = 'Bifurcation diagram, pRad = %f mb'%pRad
c3.Xlabel = 'Surface Temperature (K)'
c3.Ylabel = 'Solar Constant ($W/m^2$)'
w3 = plot(c3)
```

Once again, wherever this balance function (blue curve) inersects the incident solar radiation (dashed green curve), we get one or more equilibrium temperature(s). From this it is easy to find the values of the solar constant for which we can have 1, 2 or 3 equilibria. Note that some of the branches in this diagram are unstable.  For instance, as you vary L from 1370 up to 3000, say, at some point you will leave the lower branch and land abruptly and without warning, on the upper branch.  This is one reason why bifurcations are relevant to climate risk assessment.

### Question 4
The current greenhouse effect yields a radiative pressure pRad in the neighborhood of 670mb. Repeat the calculations above for this value of $p_{Rad}$, (it will be cleaner if you copy and paste the relevant cells below).
** For which value of L, approximately, would we enter a stable snowball state? **
(to answer this, redraw the bifurcation diagram above with $p_{Rad}=670$mb. For $L = 1370 W.m^{-2}$ you should find 3 possible states. Assume that you are on the upper, toasty branch. For which value of $L$ would you fall onto the lower, icy branch?)

```python
# perform your new calculations here
```

### Answer 4
write your answer here

Finally, we could ask what kind of climate would be obtained by varying the atmosphere's opacity to outgoing longwave radiation (i.e. its concentration of greenhouse gases). We seek a stability diagram similar to the one above, but cast in terms of radiative pressure $p_{rad}$ instead of $L$.

### Question 5
** What is the effect of lowering $p_{rad}$ on the surface temperature $T_s$? **
You may answer using purely mathematical, physical, or heuristic arguments (one way to do this is to vary $p_{rad}$ and see how it affects the OLR).

### Answer 5
write your answer here

## Greenhouse Bifurcation
To draw the bifurcation diagram in terms of $p_{rad}$ we need a bit more machinery. Let's define a function that solves for radiative pressure given the solar flux and the temperature. (if you really want to know, it uses Newton-Raphson's method, but you needn't worry about that)

```python
def radPress(flux,T):
    def g(p,const):
        return OLR(const.T,p) - const.flux
    root = newtSolve(g)
    const= Dummy()
    const.T = T
    const.flux = flux
    root.setParams(const)
    return root(500.)

L = 1370.
TList = [220.+i for i in range(131)]
gList = [radPress((1.-albedo(T))*L/4.,T) for T in TList]
cG = Curve()
cG.addCurve(gList,'pRad')
cG.addCurve(TList,'T')
#Just for variety, here I've shown that instead of
# switching axes, you can just put the data into
# the Curve in a different order
cG.PlotTitle = 'Bifurcation diagram, L = %f W/m**2'%L
cG.Ylabel = 'Surface Temperature (K)'
cG.Xlabel = 'Radiating pressure (mb)'
cG.reverseX = True #Reverse pressure axis so warmer is to the right
plot(cG)
```

### Question 6 ###
Assume the current climate state is pRad = 670 mb. Where on the diagram do you think we are? (upper or lower branch?). The solar luminosity L was about 7% lower during the Neoproterozoic. ** Would this have been enough to trigger a glaciation? If not, what value of L would have? **

### Answer 6 ###


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

from sympy import *
from mpmath import degrees, radians

init_printing()
```

This derivation is based on the USGS Professional Paper 1395:
[Map Projections — A Working Manual](http://pubs.usgs.gov/pp/1395/report.pdf), ch 25, pp 195 – 199.

```
radius = Symbol('R')
semimajor, ellipticity = symbols('a e', positive=True)
central_lat, central_lon = symbols('phi_1 lambda_0')
point_lat, point_lon = symbols('phi lambda')

radius, semimajor, ellipticity, \
central_lat, central_lon, \
point_lat, point_lon
```

```
def subdict(eq, vals):
    for k, v in vals.items():
        eq = eq.subs(k, v)
    return eq
```

# Formulas for the Sphere

```
# Eq. 5-3
c = acos(sin(central_lat) * sin(point_lat) +
         cos(central_lat) * cos(point_lat) * cos(central_lon - point_lon))
c
```

```
# Eq. 25-2
kp = c / sin(c)
kp
```

```
xs = radius * kp * cos(point_lat) * sin(point_lon - central_lon)
xs
```

```
ys = radius * kp * (cos(central_lat) * sin(point_lat) -
                    sin(central_lat) * cos(point_lat) * cos(point_lon - central_lon))
ys
```

# Formulas for the Ellipsoid

Unfortunately, the paper does not state full _exact_ equations for the ellipsoid,
but only approximations. I don't quite know how to write them in SymPy yet.

## North Polar Aspect

The equation for $M$ is given as Eq. 3-21 on pg 197:

\begin{align}
 M = a [
   & (1 - e^2 / 4 - 3e^4 / 64 - 5e^6 / 256 - \dots) \phi \\
   - & (3e^2 / 8 + 3e^4 / 32 + 45e^6 / 1024 + \dots) \sin(2 \phi) \\
   + & (15e^4 / 256 + 45e^6 / 1024 + \dots) \sin(4 \phi) \\
   - & (35e^6 / 3072 + ...) \sin(6 \phi) \\
   + & \dots
 ]
\end{align}

The numerator in the first term can be written easily as:
$$
\sum_{n=1, 2, \dots} (2n-1) e^{2n}
$$
but the denominator is:
$$
4, 64, 256, \dots = 2^2, 2^6, 2^8, \dots
$$
which doesn't seem to follow a sane pattern, and the second, third, fourth, etc. terms are worse.
Possibly, some trig rules are reducing things, but the pattern is not particularly evident the way they've written it.
I also haven't yet read chapter 3, so perhaps this is all explained there.

Let's try out the reduced form anyway, assuming they kept only the relevant terms...

```
M = semimajor * (
    (1 - ellipticity**2 / 4 - 3 * ellipticity**4 / 64 - 5 * ellipticity**6 / 256) * point_lat -
    (3 * ellipticity**2 / 8 + 3 * ellipticity**4 / 32 + 45 * ellipticity**6 / 1024) * sin(2 * point_lat) +
    (15 * ellipticity**4 / 256 + 45 * ellipticity**6 / 1024) * sin(4 * point_lat) -
    (35 * ellipticity**6 / 3072) * sin(6 * point_lat)
)
M
```

```
Mp_np = M.subs(point_lat, radians(90))
Mp_np
```

```
rho_np = Mp_np - M
rho_np
```

```
xnp = rho_np * sin(point_lon - central_lon)
ynp = -rho_np * cos(point_lon - central_lon)
```

## South Polar Aspect

```
Mp_sp = M.subs(point_lat, radians(-90))
Mp_sp
```

```
rho_sp = Mp_sp + M
rho_sp
```

```
xsp = xnp
ysp = rho_sp * cos(point_lon - central_lon)
```

## Guam Projection

```
M_guam = M
M1_guam = M_guam.subs(point_lat, central_lat)
M1_guam
```

```
x = (semimajor * (point_lon - central_lon) * cos(point_lat) /
     (1 - ellipticity**2 * sin(point_lat)**2)**0.5)
y = (M_guam - M1_guam + x**2 * tan(point_lat) *
     (1 - ellipticity**2 * sin(point_lat)**2)**0.5 / (2 * semimajor))
```

# Cartopy Boundaries

```
# Default Globe in Cartopy
WGS84_semimajor = Rational(6378137)
WGS84_flattening = Rational(1, 298.257223563)
# Plotting fails if I leave this bit as exact, thinking there are complex values,
# even though the input to sqrt is positive, and it's supposed to return the positive root.
WGS84_ellipticity = sqrt(2 * WGS84_flattening - WGS84_flattening**2).evalf()

# Default parameters in Cartopy
lat0_default = radians(0)
lon0_default = radians(0)
```

## Sphere

```
# From numerical examples, pg 337
radius_example = 3.0
lat0_example = 40
lon0_example = -100

sphere_vars = {radius: radius_example,
               central_lat: radians(lat0_example),
               central_lon: radians(lon0_example)}
```

I chose the following boundary locations since they work for other projections,
but I'm not certain they'll work here.

```
lon_min_sphere = radians(lon0_example - 180)
lon_max_sphere = radians(lon0_example + 180)
lat_min_sphere = radians(lat0_example - 90)
lat_max_sphere = radians(lat0_example + 90)

example_xs = subdict(xs, sphere_vars)
eq1x = example_xs.subs(point_lat, lat_max_sphere)
eq2x = example_xs.subs(point_lon, lon_max_sphere)
eq3x = example_xs.subs(point_lat, lat_min_sphere)
eq4x = example_xs.subs(point_lon, lon_min_sphere)

example_ys = subdict(ys, sphere_vars)
eq1y = example_ys.subs(point_lat, lat_max_sphere)
eq2y = example_ys.subs(point_lon, lon_max_sphere)
eq3y = example_ys.subs(point_lat, lat_min_sphere)
eq4y = example_ys.subs(point_lon, lon_min_sphere)

plotting.plot_parametric((eq1x, eq1y, (point_lon, lon_min_sphere, lon_max_sphere)),
                         (eq2x, eq2y, (point_lat, lat_max_sphere, lat_min_sphere)),
                         (eq3x, eq3y, (point_lon, lon_max_sphere, lon_min_sphere)),
                         (eq4x, eq4y, (point_lat, lat_min_sphere, lat_max_sphere)),
                         title='Guessed Sphere Boundaries')
```

```
lat_magnitude = subdict((point_lat - central_lat) / radians(90), sphere_vars)
lon_magnitude = subdict((point_lon - central_lon) / radians(180), sphere_vars)

plotting.plot3d_parametric_surface(example_xs, example_ys, lat_magnitude,
                                   (point_lat, lat_min_sphere, lat_max_sphere),
                                   (point_lon, lon_min_sphere, lon_max_sphere),
                                   xlabel='X', ylabel='Y', title='Sphere - Latitude Dependence')

plotting.plot3d_parametric_surface(example_xs, example_ys, lon_magnitude,
                                   (point_lat, lat_min_sphere, lat_max_sphere),
                                   (point_lon, lon_min_sphere, lon_max_sphere),
                                   xlabel='X', ylabel='Y', title='Sphere - Longitude Dependence')
```

The longitude and latitude dependence is pretty complicated
(worse than the above seems to show).
Maybe we can find the boundaries using the derivative, but I'm not sure.

```
xs_deriv_lat = diff(xs, point_lat)
xs_deriv_lon = diff(xs, point_lon)
ys_deriv_lat = diff(ys, point_lat)
ys_deriv_lon = diff(ys, point_lon)
```

```
xsd_lat = subdict(xs_deriv_lat, sphere_vars)
xsd_lon = subdict(xs_deriv_lon, sphere_vars)
ysd_lat = subdict(ys_deriv_lat, sphere_vars)
ysd_lon = subdict(ys_deriv_lon, sphere_vars)
```

```
# Solving them together uses 14+ GiB and still doesn't complete!
# solve((xsd_lat, xsd_lon), (point_lat, point_lon))

# extreme_lat = solve(xsd_lat.subs(point_lon, 0), point_lat)
```

The above solve does not complete in either manner. So I'm going to bench this attempt for the time being.

## Ellipsoid

```
ellipsoid_vars = {semimajor: WGS84_semimajor,
                  ellipticity: WGS84_ellipticity,
                  central_lat: radians(lat0_default),
                  central_lon: radians(lon0_default)}
```

### North Polar Aspect

```
lat0_np = 90
lon0_np = 0

np_vars = {semimajor: WGS84_semimajor,
           ellipticity: WGS84_ellipticity,
           central_lat: radians(lat0_np),
           central_lon: radians(lon0_np)}

lon_min_np = radians(lon0_np - 180)
lon_max_np = radians(lon0_np + 180)
lat_min_np = radians(lat0_np - 180)
lat_max_np = radians(lat0_np)

default_xnp = subdict(xnp, np_vars)
default_ynp = subdict(ynp, np_vars)

lat_magnitude = subdict(point_lat / radians(90), np_vars)
lon_magnitude = subdict(point_lon / radians(180), np_vars)

plotting.plot3d_parametric_surface(default_xnp, default_ynp, lat_magnitude,
                                   (point_lat, lat_min_np, lat_max_np),
                                   (point_lon, lon_min_np, lon_max_np),
                                   xlabel='X', ylabel='Y',
                                   title='Ellipsoid - North Polar Aspect - Latitude Dependence')

plotting.plot3d_parametric_surface(default_xnp, default_ynp, lon_magnitude,
                                   (point_lat, lat_min_np, lat_max_np),
                                   (point_lon, lon_min_np, lon_max_np),
                                   xlabel='X', ylabel='Y',
                                   title='Ellipsoid - North Polar Aspect - Longitude Dependence')
```

From the above, it appears the boundary occurs on the minimum latitude,
at least for the North Polar Aspect.

### South Polar Aspect

```
lat0_sp = -90
lon0_sp = 0

sp_vars = {semimajor: WGS84_semimajor,
           ellipticity: WGS84_ellipticity,
           central_lat: radians(lat0_sp),
           central_lon: radians(lon0_sp)}

lon_min_sp = radians(lon0_sp - 180)
lon_max_sp = radians(lon0_sp + 180)
lat_min_sp = radians(lat0_sp)
lat_max_sp = radians(lat0_sp + 180)

default_xsp = subdict(xsp, sp_vars)
default_ysp = subdict(ysp, sp_vars)

lat_magnitude = subdict(point_lat / radians(90), sp_vars)
lon_magnitude = subdict(point_lon / radians(180), sp_vars)

plotting.plot3d_parametric_surface(default_xsp, default_ysp, lat_magnitude,
                                   (point_lat, lat_min_sp, lat_max_sp),
                                   (point_lon, lon_min_sp, lon_max_sp),
                                   xlabel='X', ylabel='Y',
                                   title='Ellipsoid - South Polar Aspect - Latitude Dependence')

plotting.plot3d_parametric_surface(default_xsp, default_ysp, lon_magnitude,
                                   (point_lat, lat_min_sp, lat_max_sp),
                                   (point_lon, lon_min_sp, lon_max_sp),
                                   xlabel='X', ylabel='Y',
                                   title='Ellipsoid - South Polar Aspect - Longitude Dependence')
```

Again, the boundary occurs at the minimum latitude.

### Oblique Aspect

After some thought, it seems that the boundary should be at the greatest
distance away from the central point. This makes sense given the properties
of this projection. The projection plots _azimuth_ after all. This even explains
the "strangeness" for the spherical case; because the central point is not at the
poles, the radial distance must be greater than zero. Just blindly taking negative
and positive latitudes and longitudes will not cover the whole azimuth, and
sometimes produces odd wraparound.

But this actually is good news! It means the boundary is simply half a circumference.
Obviously, the meaning is slightly different for an ellipsoid, but it's certainly a
lot easier to calculate.


Experimental notebook to explore use of SymPy for NMR simulations. Currently unfit for human consumption.

Note: [SymPsi](http://sympsi.github.io) looked promising, but hasn't been updated since 2015.

Concept: can Hamiltonians be calculated once, symbolically, and then used and reused with supplied frequency/coupling variables? Can their eigensolution?

If so, this could dramatically speed up calculations.

Note: apparently, it is not possible to get all eigenvalues for a scipy sparse matrix. A complete eigensolution is required by the simulation.

```python
import numpy as np
from sympy import *
```

```python
# For nice-looking output:
init_printing()
```

For reference, the original Hamiltonian code that we will try to improve upon:

```python
def hamiltonian(freqlist, couplings):
    """
    Computes the spin Hamiltonian for spin-1/2 nuclei.
    inputs for n nuclei:
        :param freqlist: a list of frequencies in Hz of length n
        :param couplings: an n x n array of coupling constants in Hz
    Returns: a Hamiltonian array
    """
    nspins = len(freqlist)

    # Define Pauli matrices
    sigma_x = np.matrix([[0, 1 / 2], [1 / 2, 0]])
    sigma_y = np.matrix([[0, -1j / 2], [1j / 2, 0]])
    sigma_z = np.matrix([[1 / 2, 0], [0, -1 / 2]])
    unit = np.matrix([[1, 0], [0, 1]])

    # The following empty arrays will be used to store the
    # Cartesian spin operators.
    Lx = np.empty((1, nspins), dtype='object')
    Ly = np.empty((1, nspins), dtype='object')
    Lz = np.empty((1, nspins), dtype='object')

    for n in range(nspins):
        Lx[0, n] = 1
        Ly[0, n] = 1
        Lz[0, n] = 1
        for k in range(nspins):
            if k == n:                                  # Diagonal element
                Lx[0, n] = np.kron(Lx[0, n], sigma_x)
                Ly[0, n] = np.kron(Ly[0, n], sigma_y)
                Lz[0, n] = np.kron(Lz[0, n], sigma_z)
            else:                                       # Off-diagonal element
                Lx[0, n] = np.kron(Lx[0, n], unit)
                Ly[0, n] = np.kron(Ly[0, n], unit)
                Lz[0, n] = np.kron(Lz[0, n], unit)

    Lcol = np.vstack((Lx, Ly, Lz)).real
    Lrow = Lcol.T  # As opposed to sparse version of code, this works!
    Lproduct = np.dot(Lrow, Lcol)

    # Hamiltonian operator
    H = np.zeros((2**nspins, 2**nspins))

    # Add Zeeman interactions:
    for n in range(nspins):
        H = H + freqlist[n] * Lz[0, n]

    # Scalar couplings

    # Testing with MATLAB discovered J must be /2.
    # Believe it is related to the fact that in the SpinDynamics.org simulation
    # freqs are *2pi, but Js by pi only.
    scalars = 0.5 * couplings
    scalars = np.multiply(scalars, Lproduct)
    for n in range(nspins):
        for k in range(nspins):
            H += scalars[n, k].real

    return H

```

Default frequencies and couplings from WINDNMR for 8-spin system:

```python
def spin8():
    v = np.array([85, 120, 160, 185, 205, 215, 235, 260])
    J = np.zeros((8, 8))
    J[0, 1] = -12
    J[0, 2] = 6
    J[0, 3] = 2
    # J[0, 4] = 0
    # J[0, 5] = 0
    # J[0, 6] = 0
    # J[0, 7] = 0
    # J[1, 2] = 0
    # J[1, 3] = 0
    J[1, 4] = 14
    # J[1, 5] = 0
    # J[1, 6] = 0
    J[1, 7] = 3
    # J[2, 3] = 0
    # J[2, 4] = 0
    J[2, 5] = 3
    # J[2, 6] = 0
    # J[2, 7] = 0
    # J[3, 4] = 0
    J[3, 5] = 5
    # J[3, 6] = 0
    # J[3, 7] = 0
    J[4, 5] = 2
    # J[4, 6] = 0
    # J[4, 7] = 0
    # J[5, 6] = 0
    # J[5, 7] = 0
    J[6, 7] = 12
    J = J + J.T
    return v, J
```

and for 2:

```python
def spin2():
    v = np.array([150-7.5, 150+7.5])
    J = np.zeros((2, 2))
    J[0, 1] = 12
    J = J + J.T
    return v, J
```

```python
# Test that old hamiltonian produces expected result:
v, J = spin2()
h = hamiltonian(v, J)
h
```

    matrix([[ 153. ,    0. ,    0. ,    0. ],
            [   0. ,  -10.5,    6. ,    0. ],
            [   0. ,    6. ,    4.5,    0. ],
            [   0. ,    0. ,    0. , -147. ]])

```python
E, V = np.linalg.eigh(h)
print(E)
V
```

    [-147.          -12.60468636    6.60468636  153.        ]

    matrix([[ 0.        ,  0.        , -0.        ,  1.        ],
            [ 0.        , -0.94362832, -0.33100694,  0.        ],
            [ 0.        ,  0.33100694, -0.94362832,  0.        ],
            [ 1.        ,  0.        , -0.        ,  0.        ]])

```python
# can sympy get all eigenvalues for a sparse matrix? scipy can't.
# the answer appears to be that it can!
h = SparseMatrix(4, 4, {
    (0, 0): 153,
    (3,3):-147,
    (1, 1): -10.5,
    (2,2):4.5,
    (1,2):6,(2,1):6})
h.eigenvects()
```

Now, will try to replicate hamiltonian using SymPy.

```python
sigma_x = MatrixSymbol('sigma_x', 2, 2)
sigma_y = MatrixSymbol('sigma_y', 2, 2)
sigma_z = MatrixSymbol('sigma_z', 2, 2)
unit = MatrixSymbol('I', 2, 2)  # careful--SymPy uses I for i
sigma_x, sigma_y, sigma_z, unit
```

```python
# Define Pauli matrices
sigma_x = Matrix([[0, 1 / 2], [1 / 2, 0]])
sigma_y = Matrix([[0, -1j / 2], [1j / 2, 0]])
sigma_z = Matrix([[1 / 2, 0], [0, -1 / 2]])
unit = Matrix([[1, 0], [0, 1]])
sigma_x, sigma_y, sigma_z, unit

```

```python
v1, v2 = symbols("v1, v2")
v1 + v2
```

```python
J = Symbol('J')
j_matrix = Matrix([[0, J], [J, 0]])
j_matrix
```

```python
from sympy.physics.quantum import TensorProduct
from sympy.tensor.array import MutableDenseNDimArray as MArray
```

Issues to resolve/work around:
Can an array of matrices be constructed for Lx, Ly, Lz as before?
TensorProduct, unlike np.kron, won't accept an integer, so tensor product between 1 and $\sigma_{x/y/z}$/I not possible.

Create a kron function to make this possible:

```python
def kron(m1, m2):
    """Compute a Kronecker product of m1 and m2 if m1 is a matrix,
    or scalar multiplication of m2 if m1 is a scalar.
    """
    try:
        prod = TensorProduct(m1, m2)
    except AttributeError:
        try:
            prod = m1 * m2
        except:
            prod = None
    return prod
```

```python
kron(2, sigma_y)
```

```python
# When sympy mutable arrays were tried, saving a Matrix as an MArray element
# gave a 'not implemented' error. The sympy code suggested MArray elements
# could not be certain iterable types.
Lx = MArray([1] * 4)
Ly = MArray([1] * 4)
Lz = MArray([1] * 4)

Lx, Ly, Lz
```

```python
TensorProduct(1, sigma_y)
```

```python
nspins = 2
for n in range(nspins):
        for k in range(nspins):
            if k == n:                                  # Diagonal element
                Lx[n] = kron(Lx[n], sigma_x)
                Ly[n] = TensorProduct(Ly[n], sigma_y)
                Lz[n] = TensorProduct(Lz[n], sigma_z)
            else:                                       # Off-diagonal element
                Lx[n] = TensorProduct(Lx[n], unit)
                Ly[n] = TensorProduct(Ly[n], unit)
                Lz[n] = TensorProduct(Lz[n], unit)
Lx, Ly, Lz
```

```python

```


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

- - - -

## Mechanical Design Notes and code

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

```python
pwd
```

    '/home/neal/Desktop/mechpy'

```python
import mechpy
```

```python
import os ; os.chdir('..')  # change to root from the examples folder
```

```python
from mechpy.design import fastened_joint
```

# 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}
$$

## Factors of safety
In aerospace, typically 1.2 for civilian aircraft and 1.15 for military

$$FS=\frac{\sigma_{yield}}{\sigma}-1$$

## Fastener Notes and Formulas

Finding the centroid of a bolt with coordinates, $\overline{x},\overline{y}$
$$ \overline{x}=\frac{\sum_{i=1}^{n_b}{A_i x_i} }{\sum_{i=1}^{n_b}{A_i} } \ \ \overline{y}=\frac{\sum_{i=1}^{n_b}{A_i y_i} }{\sum_{i=1}^{n_b}{A_i}}$$

Joint/Polar Moment of Inertia, $r=$ distance from centroid to fastener
$$J= \int{r^2dA}= \sum_{i=1}^{n_b}{A_k r_k^2}$$

Bearing Stress on a bolt
$$\sigma^i_{bearing}=\frac{V_{max}}{Dt}$$

Shear Stress on each bolt i due to shear force
$$\tau_f^i = \frac{P}{\sum_{i=1}^{n_b}{A_i} }$$
Where $A_i=$ the area of ith bolt, $n_b=$number of bolts, and $P=$ shear force

Shear Stress on each bolt i due to moment
$$\tau_t^i = \frac{T r_i}{J} $$

### Modes of failure of fastened Joints
1. Tensile Plate Failure across the net section between rivets/bolts
2. Failure of rivets through shear
3. Compression failure between rivet and plate
4. Edge shear-out at rivet hole
5. Edge tearing at rivet hole

#### 1.

$$\sigma_t =\frac{F_s}{(b-nd)t}$$

#### 2.

#### 3.

#### 4.

#### 5.

## Adhesive Joints

With members, or adherends, joined with adhesives, either the member will fail due to tensile loads or the adhesive will fail in shear.

The simple solution to finding the stress of bonded surfaces is taking the average stress
$$\tau_{avg}=\frac{P}{bL}$$, is not an accurate way to model maximum stress. A good rule of thumb based on the calculations below is
$$\tau_{max}=2.08\tau_{avg}$$

The maximum shearing stress of an adhesive layer, $\tau_{max}$, can be computed as
$$\tau_{max}=K_s\tau_{avg}=K_s\left(\frac{P}{bL_L}\right)$$
with $P$ as applied load, $b$ as the width ofthe adhesive layer, and $L_L$ as the length ofthe adhesive layer. The stress distribution factor, $K_s$, can be defined as $K_s=\frac{cL}{tanh(CL/2)}$ where $c=\sqrt{\frac{2G_a}{Et_mt_a}}$, where the shear modulus, $G_a=\frac{\tau}{\gamma}$, and $E$ as the modulus of elasticity.

The max shearing stress, $\tau_{max}$ in a scarf joint can be found with
$$\tau_{max}=K_s\tau_{avg}=K_s\left[ \frac{Pcos\theta}{\left(\frac{bt}{sin\theta} \right)  } \right] = K_s\left(  \frac{P}{bt} sin\theta cos\theta \right)$$
where $t$ is the thickness of the adherend members and $\theta=tan^{-1}\frac{t}{L_s}$ is the scarf angle

*Mechanical Design of Machine Elements and Machines by Collins, Jack A., Busby, Henry R., Staab, George H. (2009)*

```python
## Bolted Joint Example

# fastener Location
fx = [0,1,2,3,0,1,2,3]
fy = [0,0,0,0,1,1,1,1]
# Force magnitude(x,y)
P = [-300,-500]
# Force location
l = [2,1]
df = fastened_joint(fx, fy, P, l)

df.plot(kind='scatter', x='x', y='y');
#df.plot(style='o', x='x', y='y')
plt.plot(df.xbar[0],df.ybar[0],'*')
df
#ax = plt.gca()
#ax.arrow(l[0], l[1], Pnorm[0],Pnorm[1], head_width=0.05, head_length=0.1, fc='k', ec='k')
#x.arrow(xbar, ybar, Pnorm[0],0, head_width=0.05, head_length=0.1, fc='k', ec='k')
#ax.arrow(xbar, ybar, 0,Pnorm[1], head_width=0.05, head_length=0.1, fc='k', ec='k')
```

# Design

## Factors of Safety

DLL, Design Limit Load = max force or moment expected during a mission with a given statistical probability

Al, Allowable =  allowed minimum applied load or strength of a structure at a given statistical probablity

FS, factor of safety [1, $\infty$] = a factor applied to a DLL to decrease the chance of failure, typically around 1-3

KD, knockdown (0,1] = a percentage reduction of Allowable load to reduce the chance of failure

A KD=0.8 would be applied to the allowable to reduce it by 20%, $Al_{new}=Al_{old}*KD$

MS, margin of safety = a measure of reserve strength , how much applied loda can increase before the safety of the vehicle is comprimised. $ MS\geq0$ for a good design, $MS=\frac{Allowable}{DLL*FS}-1$

For example with a $FS=1.15$, $DLL=80$, $Al=100$, we have a margin of $MS=\frac{100}{80*1.15}-1=\frac{100}{92}-1=0.087$ which is passing our design checks based on the expected max load of 80

Lets Assume a knockdown of 27%, so $K=1-0.27=0.73$

$$
FS = \frac{1}{K}
$$

We can also say we have a $FS = \frac{1}{0.73}=1.3699$

$$
\sigma_{design}=\frac{\sigma_{ult}}{FS} = \sigma_{ult}*K
$$